1# Copyright (c) 2018 Dell Inc. or its subsidiaries.
2# All Rights Reserved.
3#
4#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5#    not use this file except in compliance with the License. You may obtain
6#    a copy of the License at
7#
8#         http://www.apache.org/licenses/LICENSE-2.0
9#
10#    Unless required by applicable law or agreed to in writing, software
11#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13#    License for the specific language governing permissions and limitations
14#    under the License.
15
16import json
17
18from oslo_log import log as logging
19from oslo_service import loopingcall
20from oslo_utils import units
21import requests
22import requests.auth
23import requests.packages.urllib3.exceptions as urllib_exp
24import six
25
26from cinder import coordination
27from cinder import exception
28from cinder.i18n import _
29from cinder.utils import retry
30from cinder.volume.drivers.dell_emc.vmax import utils
31
32requests.packages.urllib3.disable_warnings(urllib_exp.InsecureRequestWarning)
33
34LOG = logging.getLogger(__name__)
35SLOPROVISIONING = 'sloprovisioning'
36REPLICATION = 'replication'
37SYSTEM = 'system'
38U4V_VERSION = '84'
39UCODE_5978 = '5978'
40retry_exc_tuple = (exception.VolumeBackendAPIException,)
41# HTTP constants
42GET = 'GET'
43POST = 'POST'
44PUT = 'PUT'
45DELETE = 'DELETE'
46STATUS_200 = 200
47STATUS_201 = 201
48STATUS_202 = 202
49STATUS_204 = 204
50# Job constants
51INCOMPLETE_LIST = ['created', 'unscheduled', 'scheduled', 'running',
52                   'validating', 'validated']
53CREATED = 'created'
54SUCCEEDED = 'succeeded'
55CREATE_VOL_STRING = "Creating new Volumes"
56
57
58class VMAXRest(object):
59    """Rest class based on Unisphere for VMAX Rest API."""
60
61    def __init__(self):
62        self.utils = utils.VMAXUtils()
63        self.session = None
64        self.base_uri = None
65        self.user = None
66        self.passwd = None
67        self.verify = None
68        self.cert = None
69
70    def set_rest_credentials(self, array_info):
71        """Given the array record set the rest server credentials.
72
73        :param array_info: record
74        """
75        ip = array_info['RestServerIp']
76        port = array_info['RestServerPort']
77        self.user = array_info['RestUserName']
78        self.passwd = array_info['RestPassword']
79        self.verify = array_info['SSLVerify']
80        ip_port = "%(ip)s:%(port)s" % {'ip': ip, 'port': port}
81        self.base_uri = ("https://%(ip_port)s/univmax/restapi" % {
82            'ip_port': ip_port})
83        self.session = self._establish_rest_session()
84
85    def _establish_rest_session(self):
86        """Establish the rest session.
87
88        :returns: requests.session() -- session, the rest session
89        """
90        session = requests.session()
91        session.headers = {'content-type': 'application/json',
92                           'accept': 'application/json',
93                           'Application-Type': 'openstack'}
94        session.auth = requests.auth.HTTPBasicAuth(self.user, self.passwd)
95        if self.verify is not None:
96            session.verify = self.verify
97
98        return session
99
100    def request(self, target_uri, method, params=None, request_object=None):
101        """Sends a request (GET, POST, PUT, DELETE) to the target api.
102
103        :param target_uri: target uri (string)
104        :param method: The method (GET, POST, PUT, or DELETE)
105        :param params: Additional URL parameters
106        :param request_object: request payload (dict)
107        :returns: server response object (dict)
108        :raises: VolumeBackendAPIException
109        """
110        message, status_code = None, None
111        if not self.session:
112            self.session = self._establish_rest_session()
113        url = ("%(self.base_uri)s%(target_uri)s" %
114               {'self.base_uri': self.base_uri,
115                'target_uri': target_uri})
116        try:
117            if request_object:
118                response = self.session.request(
119                    method=method, url=url,
120                    data=json.dumps(request_object, sort_keys=True,
121                                    indent=4))
122            elif params:
123                response = self.session.request(method=method, url=url,
124                                                params=params)
125            else:
126                response = self.session.request(method=method, url=url)
127            status_code = response.status_code
128            try:
129                message = response.json()
130            except ValueError:
131                LOG.debug("No response received from API. Status code "
132                          "received is: %(status_code)s",
133                          {'status_code': status_code})
134                message = None
135            LOG.debug("%(method)s request to %(url)s has returned with "
136                      "a status code of: %(status_code)s.",
137                      {'method': method, 'url': url,
138                       'status_code': status_code})
139
140        except requests.Timeout:
141            LOG.error("The %(method)s request to URL %(url)s timed-out, "
142                      "but may have been successful. Please check the array.",
143                      {'method': method, 'url': url})
144        except Exception as e:
145            exception_message = (_("The %(method)s request to URL %(url)s "
146                                   "failed with exception %(e)s")
147                                 % {'method': method, 'url': url,
148                                    'e': six.text_type(e)})
149            LOG.exception(exception_message)
150            raise exception.VolumeBackendAPIException(data=exception_message)
151
152        return status_code, message
153
154    def wait_for_job_complete(self, job, extra_specs):
155        """Given the job wait for it to complete.
156
157        :param job: the job dict
158        :param extra_specs: the extra_specs dict.
159        :returns: rc -- int, result -- string, status -- string,
160                  task -- list of dicts detailing tasks in the job
161        :raises: VolumeBackendAPIException
162        """
163        res, tasks = None, None
164        if job['status'].lower == CREATED:
165            try:
166                res, tasks = job['result'], job['task']
167            except KeyError:
168                pass
169            return 0, res, job['status'], tasks
170
171        def _wait_for_job_complete():
172            result = None
173            # Called at an interval until the job is finished.
174            retries = kwargs['retries']
175            try:
176                kwargs['retries'] = retries + 1
177                if not kwargs['wait_for_job_called']:
178                    is_complete, result, rc, status, task = (
179                        self._is_job_finished(job_id))
180                    if is_complete is True:
181                        kwargs['wait_for_job_called'] = True
182                        kwargs['rc'], kwargs['status'] = rc, status
183                        kwargs['result'], kwargs['task'] = result, task
184            except Exception:
185                exception_message = (_("Issue encountered waiting for job."))
186                LOG.exception(exception_message)
187                raise exception.VolumeBackendAPIException(
188                    data=exception_message)
189
190            if retries > int(extra_specs[utils.RETRIES]):
191                LOG.error("_wait_for_job_complete failed after "
192                          "%(retries)d tries.", {'retries': retries})
193                kwargs['rc'], kwargs['result'] = -1, result
194
195                raise loopingcall.LoopingCallDone()
196            if kwargs['wait_for_job_called']:
197                raise loopingcall.LoopingCallDone()
198
199        job_id = job['jobId']
200        kwargs = {'retries': 0, 'wait_for_job_called': False,
201                  'rc': 0, 'result': None}
202
203        timer = loopingcall.FixedIntervalLoopingCall(_wait_for_job_complete)
204        timer.start(interval=int(extra_specs[utils.INTERVAL])).wait()
205        LOG.debug("Return code is: %(rc)lu. Result is %(res)s.",
206                  {'rc': kwargs['rc'], 'res': kwargs['result']})
207        return (kwargs['rc'], kwargs['result'],
208                kwargs['status'], kwargs['task'])
209
210    def _is_job_finished(self, job_id):
211        """Check if the job is finished.
212
213        :param job_id: the id of the job
214        :returns: complete -- bool, result -- string,
215                  rc -- int, status -- string, task -- list of dicts
216        """
217        complete, rc, status, result, task = False, 0, None, None, None
218        job_url = "/%s/system/job/%s" % (U4V_VERSION, job_id)
219        job = self._get_request(job_url, 'job')
220        if job:
221            status = job['status']
222            try:
223                result, task = job['result'], job['task']
224            except KeyError:
225                pass
226            if status.lower() == SUCCEEDED:
227                complete = True
228            elif status.lower() in INCOMPLETE_LIST:
229                complete = False
230            else:
231                rc, complete = -1, True
232        return complete, result, rc, status, task
233
234    @staticmethod
235    def check_status_code_success(operation, status_code, message):
236        """Check if a status code indicates success.
237
238        :param operation: the operation
239        :param status_code: the status code
240        :param message: the server response
241        :raises: VolumeBackendAPIException
242        """
243        if status_code not in [STATUS_200, STATUS_201,
244                               STATUS_202, STATUS_204]:
245            exception_message = (
246                _("Error %(operation)s. The status code received is %(sc)s "
247                  "and the message is %(message)s.") % {
248                    'operation': operation, 'sc': status_code,
249                    'message': message})
250            raise exception.VolumeBackendAPIException(
251                data=exception_message)
252
253    def wait_for_job(self, operation, status_code, job, extra_specs):
254        """Check if call is async, wait for it to complete.
255
256        :param operation: the operation being performed
257        :param status_code: the status code
258        :param job: the job
259        :param extra_specs: the extra specifications
260        :returns: task -- list of dicts detailing tasks in the job
261        :raises: VolumeBackendAPIException
262        """
263        task = None
264        if status_code == STATUS_202:
265            rc, result, status, task = self.wait_for_job_complete(
266                job, extra_specs)
267            if rc != 0:
268                exception_message = (
269                    _("Error %(operation)s. Status code: %(sc)lu. Error: "
270                      "%(error)s. Status: %(status)s.") % {
271                        'operation': operation, 'sc': rc,
272                        'error': six.text_type(result), 'status': status})
273                LOG.error(exception_message)
274                raise exception.VolumeBackendAPIException(
275                    data=exception_message)
276        return task
277
278    @staticmethod
279    def _build_uri(array, category, resource_type,
280                   resource_name=None, private='', version=U4V_VERSION):
281        """Build the target url.
282
283        :param array: the array serial number
284        :param category: the resource category e.g. sloprovisioning
285        :param resource_type: the resource type e.g. maskingview
286        :param resource_name: the name of a specific resource
287        :param private: empty string or '/private' if private url
288        :returns: target url, string
289        """
290        target_uri = ('%(private)s/%(version)s/%(category)s/symmetrix/'
291                      '%(array)s/%(resource_type)s'
292                      % {'private': private, 'version': version,
293                         'category': category, 'array': array,
294                         'resource_type': resource_type})
295        if resource_name:
296            target_uri += '/%(resource_name)s' % {
297                'resource_name': resource_name}
298        return target_uri
299
300    def _get_request(self, target_uri, resource_type, params=None):
301        """Send a GET request to the array.
302
303        :param target_uri: the target uri
304        :param resource_type: the resource type, e.g. maskingview
305        :param params: optional dict of filter params
306        :returns: resource_object -- dict or None
307        """
308        resource_object = None
309        sc, message = self.request(target_uri, GET, params=params)
310        operation = 'get %(res)s' % {'res': resource_type}
311        try:
312            self.check_status_code_success(operation, sc, message)
313        except Exception as e:
314            LOG.debug("Get resource failed with %(e)s",
315                      {'e': e})
316        if sc == STATUS_200:
317            resource_object = message
318        return resource_object
319
320    def get_resource(self, array, category, resource_type,
321                     resource_name=None, params=None, private='',
322                     version=U4V_VERSION):
323        """Get resource details from array.
324
325        :param array: the array serial number
326        :param category: the resource category e.g. sloprovisioning
327        :param resource_type: the resource type e.g. maskingview
328        :param resource_name: the name of a specific resource
329        :param params: query parameters
330        :param private: empty string or '/private' if private url
331        :param version: None or specific version number if required
332        :returns: resource object -- dict or None
333        """
334        target_uri = self._build_uri(array, category, resource_type,
335                                     resource_name, private, version=version)
336        return self._get_request(target_uri, resource_type, params)
337
338    def create_resource(self, array, category, resource_type, payload,
339                        private=''):
340        """Create a provisioning resource.
341
342        :param array: the array serial number
343        :param category: the category
344        :param resource_type: the resource type
345        :param payload: the payload
346        :param private: empty string or '/private' if private url
347        :returns: status_code -- int, message -- string, server response
348        """
349        target_uri = self._build_uri(array, category, resource_type,
350                                     None, private)
351        status_code, message = self.request(target_uri, POST,
352                                            request_object=payload)
353        operation = 'Create %(res)s resource' % {'res': resource_type}
354        self.check_status_code_success(
355            operation, status_code, message)
356        return status_code, message
357
358    def modify_resource(self, array, category, resource_type, payload,
359                        version=U4V_VERSION, resource_name=None, private=''):
360        """Modify a resource.
361
362        :param version: the uv4 version
363        :param array: the array serial number
364        :param category: the category
365        :param resource_type: the resource type
366        :param payload: the payload
367        :param resource_name: the resource name
368        :param private: empty string or '/private' if private url
369        :returns: status_code -- int, message -- string (server response)
370        """
371        target_uri = self._build_uri(array, category, resource_type,
372                                     resource_name, private, version)
373        status_code, message = self.request(target_uri, PUT,
374                                            request_object=payload)
375        operation = 'modify %(res)s resource' % {'res': resource_type}
376        self.check_status_code_success(operation, status_code, message)
377        return status_code, message
378
379    def delete_resource(
380            self, array, category, resource_type, resource_name,
381            payload=None, private='', params=None):
382        """Delete a provisioning resource.
383
384        :param array: the array serial number
385        :param category: the resource category e.g. sloprovisioning
386        :param resource_type: the type of resource to be deleted
387        :param resource_name: the name of the resource to be deleted
388        :param payload: the payload, optional
389        :param private: empty string or '/private' if private url
390        :param params: dict of optional query params
391        """
392        target_uri = self._build_uri(array, category, resource_type,
393                                     resource_name, private)
394        status_code, message = self.request(target_uri, DELETE,
395                                            request_object=payload,
396                                            params=params)
397        operation = 'delete %(res)s resource' % {'res': resource_type}
398        self.check_status_code_success(operation, status_code, message)
399
400    def get_array_serial(self, array):
401        """Get an array from its serial number.
402
403        :param array: the array serial number
404        :returns: array_details -- dict or None
405        """
406        target_uri = '/%s/system/symmetrix/%s' % (U4V_VERSION, array)
407        array_details = self._get_request(target_uri, 'system')
408        if not array_details:
409            LOG.error("Cannot connect to array %(array)s.",
410                      {'array': array})
411        return array_details
412
413    def is_next_gen_array(self, array):
414        """Check to see if array is a next gen array(ucode 5978 or greater).
415
416        :param array: the array serial number
417        :returns: bool
418        """
419        is_next_gen = False
420        array_details = self.get_array_serial(array)
421        if array_details:
422            ucode_version = array_details['ucode'].split('.')[0]
423            if ucode_version >= UCODE_5978:
424                is_next_gen = True
425        return is_next_gen
426
427    def get_uni_version(self):
428        """Get the unisphere version from the server.
429
430        :return: version and major_version(e.g. ("V8.4.0.16", "84"))
431        """
432        version, major_version = None, None
433        target_uri = "/%s/system/version" % U4V_VERSION
434        response = self._get_request(target_uri, 'version')
435        if response and response.get('version'):
436            version = response['version']
437            version_list = version.split('.')
438            major_version = version_list[0][1] + version_list[1]
439        return version, major_version
440
441    def get_srp_by_name(self, array, srp=None):
442        """Returns the details of a storage pool.
443
444        :param array: the array serial number
445        :param srp: the storage resource pool name
446        :returns: SRP_details -- dict or None
447        """
448        LOG.debug("storagePoolName: %(srp)s, array: %(array)s.",
449                  {'srp': srp, 'array': array})
450        srp_details = self.get_resource(array, SLOPROVISIONING, 'srp',
451                                        resource_name=srp, params=None)
452        return srp_details
453
454    def get_slo_list(self, array):
455        """Retrieve the list of slo's from the array
456
457        :param array: the array serial number
458        :returns: slo_list -- list of service level names
459        """
460        slo_list = []
461        slo_dict = self.get_resource(array, SLOPROVISIONING, 'slo')
462        if slo_dict and slo_dict.get('sloId'):
463            if not self.is_next_gen_array(array) and (
464                    any(self.get_vmax_model(array) in x for x in
465                        utils.VMAX_AFA_MODELS)):
466                if 'Optimized' in slo_dict.get('sloId'):
467                    slo_dict['sloId'].remove('Optimized')
468            for slo in slo_dict['sloId']:
469                if slo and slo not in slo_list:
470                    slo_list.append(slo)
471        return slo_list
472
473    def get_workload_settings(self, array):
474        """Get valid workload options from array.
475
476        Workloads are no longer supported from HyperMaxOS 5978 onwards.
477        :param array: the array serial number
478        :returns: workload_setting -- list of workload names
479        """
480        workload_setting = []
481        if self.is_next_gen_array(array):
482            workload_setting.append('None')
483        else:
484            wl_details = self.get_resource(
485                array, SLOPROVISIONING, 'workloadtype')
486            if wl_details:
487                workload_setting = wl_details['workloadId']
488        return workload_setting
489
490    def get_vmax_model(self, array):
491        """Get the VMAX model.
492
493        :param array: the array serial number
494        :return: the VMAX model
495        """
496        vmax_version = ''
497        system_uri = ("/%(version)s/system/symmetrix/%(array)s" % {
498            'version': U4V_VERSION, 'array': array})
499        system_info = self._get_request(system_uri, SYSTEM)
500        if system_info and system_info.get('model'):
501            vmax_version = system_info.get('model')
502        return vmax_version
503
504    def is_compression_capable(self, array):
505        """Check if array is compression capable.
506
507        :param array: array serial number
508        :returns: bool
509        """
510        is_compression_capable = False
511        target_uri = "/84/sloprovisioning/symmetrix?compressionCapable=true"
512        status_code, message = self.request(target_uri, GET)
513        self.check_status_code_success(
514            "Check if compression enabled", status_code, message)
515        if message.get('symmetrixId'):
516            if array in message['symmetrixId']:
517                is_compression_capable = True
518        return is_compression_capable
519
520    def get_storage_group(self, array, storage_group_name):
521        """Given a name, return storage group details.
522
523        :param array: the array serial number
524        :param storage_group_name: the name of the storage group
525        :returns: storage group dict or None
526        """
527        return self.get_resource(
528            array, SLOPROVISIONING, 'storagegroup',
529            resource_name=storage_group_name)
530
531    def get_num_vols_in_sg(self, array, storage_group_name):
532        """Get the number of volumes in a storage group.
533
534        :param array: the array serial number
535        :param storage_group_name: the storage group name
536        :returns: num_vols -- int
537        """
538        num_vols = 0
539        storagegroup = self.get_storage_group(array, storage_group_name)
540        try:
541            num_vols = int(storagegroup['num_of_vols'])
542        except (KeyError, TypeError):
543            pass
544        return num_vols
545
546    def is_child_sg_in_parent_sg(self, array, child_name, parent_name):
547        """Check if a child storage group is a member of a parent group.
548
549        :param array: the array serial number
550        :param child_name: the child sg name
551        :param parent_name: the parent sg name
552        :returns: bool
553        """
554        parent_sg = self.get_storage_group(array, parent_name)
555        if parent_sg and parent_sg.get('child_storage_group'):
556            child_sg_list = parent_sg['child_storage_group']
557            if child_name in child_sg_list:
558                return True
559        return False
560
561    def add_child_sg_to_parent_sg(
562            self, array, child_sg, parent_sg, extra_specs):
563        """Add a storage group to a parent storage group.
564
565        This method adds an existing storage group to another storage
566        group, i.e. cascaded storage groups.
567        :param array: the array serial number
568        :param child_sg: the name of the child sg
569        :param parent_sg: the name of the parent sg
570        :param extra_specs: the extra specifications
571        """
572        payload = {"editStorageGroupActionParam": {
573            "expandStorageGroupParam": {
574                "addExistingStorageGroupParam": {
575                    "storageGroupId": [child_sg]}}}}
576        sc, job = self.modify_storage_group(array, parent_sg, payload)
577        self.wait_for_job('Add child sg to parent sg', sc, job, extra_specs)
578
579    def add_empty_child_sg_to_parent_sg(
580            self, array, child_sg, parent_sg, extra_specs):
581        """Add an empty storage group to a parent storage group.
582
583        This method adds an existing storage group to another storage
584        group, i.e. cascaded storage groups.
585        :param array: the array serial number
586        :param child_sg: the name of the child sg
587        :param parent_sg: the name of the parent sg
588        :param extra_specs: the extra specifications
589        """
590        payload = {"editStorageGroupActionParam": {
591            "addExistingStorageGroupParam": {
592                "storageGroupId": [child_sg]}}}
593        sc, job = self.modify_storage_group(array, parent_sg, payload,
594                                            version="83")
595        self.wait_for_job('Add child sg to parent sg', sc, job, extra_specs)
596
597    def remove_child_sg_from_parent_sg(
598            self, array, child_sg, parent_sg, extra_specs):
599        """Remove a storage group from its parent storage group.
600
601        This method removes a child storage group from its parent group.
602        :param array: the array serial number
603        :param child_sg: the name of the child sg
604        :param parent_sg: the name of the parent sg
605        :param extra_specs: the extra specifications
606        """
607        payload = {"editStorageGroupActionParam": {
608            "removeStorageGroupParam": {
609                "storageGroupId": [child_sg], "force": 'true'}}}
610        status_code, job = self.modify_storage_group(
611            array, parent_sg, payload)
612        self.wait_for_job(
613            'Remove child sg from parent sg', status_code, job, extra_specs)
614
615    def _create_storagegroup(self, array, payload):
616        """Create a storage group.
617
618        :param array: the array serial number
619        :param payload: the payload -- dict
620        :returns: status_code -- int, message -- string, server response
621        """
622        return self.create_resource(
623            array, SLOPROVISIONING, 'storagegroup', payload)
624
625    def create_storage_group(self, array, storagegroup_name,
626                             srp, slo, workload, extra_specs,
627                             do_disable_compression=False):
628        """Create the volume in the specified storage group.
629
630        :param array: the array serial number
631        :param storagegroup_name: the group name (String)
632        :param srp: the SRP (String)
633        :param slo: the SLO (String)
634        :param workload: the workload (String)
635        :param do_disable_compression: flag for disabling compression
636        :param extra_specs: additional info
637        :returns: storagegroup_name - string
638        """
639        srp_id = srp if slo else "None"
640        payload = ({"srpId": srp_id,
641                    "storageGroupId": storagegroup_name,
642                    "emulation": "FBA"})
643
644        if slo:
645            if self.is_next_gen_array(array):
646                workload = 'NONE'
647            slo_param = {"num_of_vols": 0,
648                         "sloId": slo,
649                         "workloadSelection": workload,
650                         "volumeAttribute": {
651                             "volume_size": "0",
652                             "capacityUnit": "GB"}}
653            if do_disable_compression:
654                slo_param.update({"noCompression": "true"})
655            elif self.is_compression_capable(array):
656                slo_param.update({"noCompression": "false"})
657
658            payload.update({"sloBasedStorageGroupParam": [slo_param]})
659
660        status_code, job = self._create_storagegroup(array, payload)
661        self.wait_for_job('Create storage group', status_code,
662                          job, extra_specs)
663        return storagegroup_name
664
665    def modify_storage_group(self, array, storagegroup, payload,
666                             version=U4V_VERSION):
667        """Modify a storage group (PUT operation).
668
669        :param version: the uv4 version
670        :param array: the array serial number
671        :param storagegroup: storage group name
672        :param payload: the request payload
673        :returns: status_code -- int, message -- string, server response
674        """
675        return self.modify_resource(
676            array, SLOPROVISIONING, 'storagegroup', payload, version,
677            resource_name=storagegroup)
678
679    def create_volume_from_sg(self, array, volume_name, storagegroup_name,
680                              volume_size, extra_specs):
681        """Create a new volume in the given storage group.
682
683        :param array: the array serial number
684        :param volume_name: the volume name (String)
685        :param storagegroup_name: the storage group name
686        :param volume_size: volume size (String)
687        :param extra_specs: the extra specifications
688        :returns: dict -- volume_dict - the volume dict
689        :raises: VolumeBackendAPIException
690        """
691        payload = (
692            {"executionOption": "ASYNCHRONOUS",
693             "editStorageGroupActionParam": {
694                 "expandStorageGroupParam": {
695                     "addVolumeParam": {
696                         "num_of_vols": 1,
697                         "emulation": "FBA",
698                         "volumeIdentifier": {
699                             "identifier_name": volume_name,
700                             "volumeIdentifierChoice": "identifier_name"},
701                         "volumeAttribute": {
702                             "volume_size": volume_size,
703                             "capacityUnit": "GB"}}}}})
704        status_code, job = self.modify_storage_group(
705            array, storagegroup_name, payload)
706
707        LOG.debug("Create Volume: %(volumename)s. Status code: %(sc)lu.",
708                  {'volumename': volume_name,
709                   'sc': status_code})
710
711        task = self.wait_for_job('Create volume', status_code,
712                                 job, extra_specs)
713
714        # Find the newly created volume.
715        device_id = None
716        if task:
717            for t in task:
718                try:
719                    desc = t["description"]
720                    if CREATE_VOL_STRING in desc:
721                        t_list = desc.split()
722                        device_id = t_list[(len(t_list) - 1)]
723                        device_id = device_id[1:-1]
724                        break
725                    if device_id:
726                        self.get_volume(array, device_id)
727                except Exception as e:
728                    LOG.info("Could not retrieve device id from job. "
729                             "Exception received was %(e)s. Attempting "
730                             "retrieval by volume_identifier.",
731                             {'e': e})
732
733        if not device_id:
734            device_id = self.find_volume_device_id(array, volume_name)
735
736        volume_dict = {'array': array, 'device_id': device_id}
737        return volume_dict
738
739    def check_volume_device_id(self, array, device_id, volume_id,
740                               name_id=None):
741        """Check if the identifiers match for a given volume.
742
743        :param array: the array serial number
744        :param device_id: the device id
745        :param volume_id: cinder volume id
746        :param name_id: name id - used in host_assisted migration, optional
747        :returns: found_device_id
748        """
749        element_name = self.utils.get_volume_element_name(volume_id)
750        found_device_id = None
751        vol_details = self.get_volume(array, device_id)
752        if vol_details:
753            vol_identifier = vol_details.get('volume_identifier', None)
754            LOG.debug('Element name = %(en)s, Vol identifier = %(vi)s, '
755                      'Device id = %(di)s, vol details = %(vd)s',
756                      {'en': element_name, 'vi': vol_identifier,
757                       'di': device_id, 'vd': vol_details})
758            if vol_identifier == element_name:
759                found_device_id = device_id
760            elif name_id:
761                # This may be host-assisted migration case
762                element_name = self.utils.get_volume_element_name(name_id)
763                if vol_identifier == element_name:
764                    found_device_id = device_id
765        return found_device_id
766
767    def add_vol_to_sg(self, array, storagegroup_name, device_id, extra_specs):
768        """Add a volume to a storage group.
769
770        :param array: the array serial number
771        :param storagegroup_name: storage group name
772        :param device_id: the device id
773        :param extra_specs: extra specifications
774        """
775        if not isinstance(device_id, list):
776            device_id = [device_id]
777        payload = ({"executionOption": "ASYNCHRONOUS",
778                    "editStorageGroupActionParam": {
779                        "expandStorageGroupParam": {
780                            "addSpecificVolumeParam": {
781                                "volumeId": device_id}}}})
782        status_code, job = self.modify_storage_group(
783            array, storagegroup_name, payload)
784
785        self.wait_for_job('Add volume to sg', status_code, job, extra_specs)
786
787    @retry(retry_exc_tuple, interval=2, retries=3)
788    def remove_vol_from_sg(self, array, storagegroup_name,
789                           device_id, extra_specs):
790        """Remove a volume from a storage group.
791
792        :param array: the array serial number
793        :param storagegroup_name: storage group name
794        :param device_id: the device id
795        :param extra_specs: the extra specifications
796        """
797        if not isinstance(device_id, list):
798            device_id = [device_id]
799        payload = ({"executionOption": "ASYNCHRONOUS",
800                    "editStorageGroupActionParam": {
801                        "removeVolumeParam": {
802                            "volumeId": device_id}}})
803        status_code, job = self.modify_storage_group(
804            array, storagegroup_name, payload)
805
806        self.wait_for_job('Remove vol from sg', status_code, job, extra_specs)
807
808    def update_storagegroup_qos(self, array, storage_group_name, extra_specs):
809        """Update the storagegroupinstance with qos details.
810
811        If maxIOPS or maxMBPS is in extra_specs, then DistributionType can be
812        modified in addition to maxIOPS or/and maxMBPS
813        If maxIOPS or maxMBPS is NOT in extra_specs, we check to see if
814        either is set in StorageGroup. If so, then DistributionType can be
815        modified
816        :param array: the array serial number
817        :param storage_group_name: the storagegroup instance name
818        :param extra_specs: extra specifications
819        :returns: bool, True if updated, else False
820        """
821        return_value = False
822        sg_details = self.get_storage_group(array, storage_group_name)
823        sg_qos_details = None
824        sg_maxiops = None
825        sg_maxmbps = None
826        sg_distribution_type = None
827        property_dict = {}
828        try:
829            sg_qos_details = sg_details['hostIOLimit']
830            sg_maxiops = sg_qos_details['host_io_limit_io_sec']
831            sg_maxmbps = sg_qos_details['host_io_limit_mb_sec']
832            sg_distribution_type = sg_qos_details['dynamicDistribution']
833        except KeyError:
834            LOG.debug("Unable to get storage group QoS details.")
835        if 'total_iops_sec' in extra_specs.get('qos'):
836            property_dict = self.validate_qos_input(
837                'total_iops_sec', sg_maxiops, extra_specs.get('qos'),
838                property_dict)
839        if 'total_bytes_sec' in extra_specs.get('qos'):
840            property_dict = self.validate_qos_input(
841                'total_bytes_sec', sg_maxmbps, extra_specs.get('qos'),
842                property_dict)
843        if 'DistributionType' in extra_specs.get('qos') and property_dict:
844            property_dict = self.validate_qos_distribution_type(
845                sg_distribution_type, extra_specs.get('qos'), property_dict)
846
847        if property_dict:
848            payload = {"editStorageGroupActionParam": {
849                "setHostIOLimitsParam": property_dict}}
850            status_code, message = (
851                self.modify_storage_group(array, storage_group_name, payload))
852            try:
853                self.check_status_code_success('Add qos specs', status_code,
854                                               message)
855                return_value = True
856            except Exception as e:
857                LOG.error("Error setting qos. Exception received was: "
858                          "%(e)s", {'e': e})
859                return_value = False
860        return return_value
861
862    @staticmethod
863    def validate_qos_input(input_key, sg_value, qos_extra_spec, property_dict):
864        max_value = 100000
865        qos_unit = "IO/Sec"
866        if input_key == 'total_iops_sec':
867            min_value = 100
868            input_value = int(qos_extra_spec['total_iops_sec'])
869            sg_key = 'host_io_limit_io_sec'
870        else:
871            qos_unit = "MB/sec"
872            min_value = 1
873            input_value = int(qos_extra_spec['total_bytes_sec']) / units.Mi
874            sg_key = 'host_io_limit_mb_sec'
875        if min_value <= input_value <= max_value:
876            if sg_value is None or input_value != int(sg_value):
877                property_dict[sg_key] = input_value
878        else:
879            exception_message = (
880                _("Invalid %(ds)s with value %(dt)s entered. Valid values "
881                  "range from %(du)s %(dv)s to 100,000 %(dv)s") % {
882                    'ds': input_key, 'dt': input_value, 'du': min_value,
883                    'dv': qos_unit})
884            LOG.error(exception_message)
885            raise exception.VolumeBackendAPIException(
886                data=exception_message)
887        return property_dict
888
889    @staticmethod
890    def validate_qos_distribution_type(
891            sg_value, qos_extra_spec, property_dict):
892        dynamic_list = ['never', 'onfailure', 'always']
893        if qos_extra_spec.get('DistributionType').lower() in dynamic_list:
894            distribution_type = qos_extra_spec['DistributionType']
895            if distribution_type != sg_value:
896                property_dict["dynamicDistribution"] = distribution_type
897        else:
898            exception_message = (
899                _("Wrong Distribution type value %(dt)s entered. Please enter "
900                  "one of: %(dl)s") % {
901                    'dt': qos_extra_spec.get('DistributionType'),
902                    'dl': dynamic_list})
903            LOG.error(exception_message)
904            raise exception.VolumeBackendAPIException(
905                data=exception_message)
906        return property_dict
907
908    def set_storagegroup_srp(
909            self, array, storagegroup_name, srp_name, extra_specs):
910        """Modify a storage group's srp value.
911
912        :param array: the array serial number
913        :param storagegroup_name: the storage group name
914        :param srp_name: the srp pool name
915        :param extra_specs: the extra specifications
916        """
917        payload = {"editStorageGroupActionParam": {
918            "editStorageGroupSRPParam": {"srpId": srp_name}}}
919        status_code, job = self.modify_storage_group(
920            array, storagegroup_name, payload)
921        self.wait_for_job("Set storage group srp", status_code,
922                          job, extra_specs)
923
924    def get_vmax_default_storage_group(
925            self, array, srp, slo, workload,
926            do_disable_compression=False, is_re=False, rep_mode=None):
927        """Get the default storage group.
928
929        :param array: the array serial number
930        :param srp: the pool name
931        :param slo: the SLO
932        :param workload: the workload
933        :param do_disable_compression: flag for disabling compression
934        :param is_re: flag for replication
935        :param rep_mode: flag to indicate replication mode
936        :returns: the storage group dict (or None), the storage group name
937        """
938        if self.is_next_gen_array(array):
939            workload = 'NONE'
940        storagegroup_name = self.utils.get_default_storage_group_name(
941            srp, slo, workload, do_disable_compression, is_re, rep_mode)
942        storagegroup = self.get_storage_group(array, storagegroup_name)
943        return storagegroup, storagegroup_name
944
945    def delete_storage_group(self, array, storagegroup_name):
946        """Delete a storage group.
947
948        :param array: the array serial number
949        :param storagegroup_name: storage group name
950        """
951        self.delete_resource(
952            array, SLOPROVISIONING, 'storagegroup', storagegroup_name)
953        LOG.debug("Storage Group successfully deleted.")
954
955    def move_volume_between_storage_groups(
956            self, array, device_id, source_storagegroup_name,
957            target_storagegroup_name, extra_specs, force=False):
958        """Move a volume to a different storage group.
959
960        :param array: the array serial number
961        :param source_storagegroup_name: the originating storage group name
962        :param target_storagegroup_name: the destination storage group name
963        :param device_id: the device id
964        :param extra_specs: extra specifications
965        :param force: force flag (necessary on a detach)
966        """
967        force_flag = "true" if force else "false"
968        payload = ({"executionOption": "ASYNCHRONOUS",
969                    "editStorageGroupActionParam": {
970                        "moveVolumeToStorageGroupParam": {
971                            "volumeId": [device_id],
972                            "storageGroupId": target_storagegroup_name,
973                            "force": force_flag}}})
974        status_code, job = self.modify_storage_group(
975            array, source_storagegroup_name, payload)
976        self.wait_for_job('move volume between storage groups', status_code,
977                          job, extra_specs)
978
979    def get_volume(self, array, device_id):
980        """Get a VMAX volume from array.
981
982        :param array: the array serial number
983        :param device_id: the volume device id
984        :returns: volume dict
985        :raises: VolumeBackendAPIException
986        """
987        version = self.get_uni_version()[1]
988        volume_dict = self.get_resource(
989            array, SLOPROVISIONING, 'volume', resource_name=device_id,
990            version=version)
991        if not volume_dict:
992            exception_message = (_("Volume %(deviceID)s not found.")
993                                 % {'deviceID': device_id})
994            LOG.error(exception_message)
995            raise exception.VolumeBackendAPIException(data=exception_message)
996        return volume_dict
997
998    def _get_private_volume(self, array, device_id):
999        """Get a more detailed list of attributes of a volume.
1000
1001        :param array: the array serial number
1002        :param device_id: the volume device id
1003        :returns: volume dict
1004        :raises: VolumeBackendAPIException
1005        """
1006        try:
1007            wwn = (self.get_volume(array, device_id))['wwn']
1008            params = {'wwn': wwn}
1009            volume_info = self.get_resource(
1010                array, SLOPROVISIONING, 'volume', params=params,
1011                private='/private')
1012            volume_dict = volume_info['resultList']['result'][0]
1013        except (KeyError, TypeError):
1014            exception_message = (_("Volume %(deviceID)s not found.")
1015                                 % {'deviceID': device_id})
1016            LOG.error(exception_message)
1017            raise exception.VolumeBackendAPIException(data=exception_message)
1018        return volume_dict
1019
1020    def get_volume_list(self, array, params):
1021        """Get a filtered list of VMAX volumes from array.
1022
1023        Filter parameters are required as the unfiltered volume list could be
1024        very large and could affect performance if called often.
1025        :param array: the array serial number
1026        :param params: filter parameters
1027        :returns: device_ids -- list
1028        """
1029        device_ids = []
1030        volumes = self.get_resource(
1031            array, SLOPROVISIONING, 'volume', params=params)
1032        try:
1033            volume_dict_list = volumes['resultList']['result']
1034            for vol_dict in volume_dict_list:
1035                device_id = vol_dict['volumeId']
1036                device_ids.append(device_id)
1037        except (KeyError, TypeError):
1038            pass
1039        return device_ids
1040
1041    def _modify_volume(self, array, device_id, payload):
1042        """Modify a volume (PUT operation).
1043
1044        :param array: the array serial number
1045        :param device_id: volume device id
1046        :param payload: the request payload
1047        """
1048        return self.modify_resource(array, SLOPROVISIONING, 'volume',
1049                                    payload, resource_name=device_id)
1050
1051    def extend_volume(self, array, device_id, new_size, extra_specs):
1052        """Extend a VMAX volume.
1053
1054        :param array: the array serial number
1055        :param device_id: volume device id
1056        :param new_size: the new required size for the device
1057        :param extra_specs: the extra specifications
1058        """
1059        extend_vol_payload = {"executionOption": "ASYNCHRONOUS",
1060                              "editVolumeActionParam": {
1061                                  "expandVolumeParam": {
1062                                      "volumeAttribute": {
1063                                          "volume_size": new_size,
1064                                          "capacityUnit": "GB"}}}}
1065
1066        status_code, job = self._modify_volume(
1067            array, device_id, extend_vol_payload)
1068        LOG.debug("Extend Device: %(device_id)s. Status code: %(sc)lu.",
1069                  {'device_id': device_id, 'sc': status_code})
1070        self.wait_for_job('Extending volume', status_code, job, extra_specs)
1071
1072    def rename_volume(self, array, device_id, new_name):
1073        """Rename a volume.
1074
1075        :param array: the array serial number
1076        :param device_id: the volume device id
1077        :param new_name: the new name for the volume, can be None
1078        """
1079        if new_name is not None:
1080            vol_identifier_dict = {
1081                "identifier_name": new_name,
1082                "volumeIdentifierChoice": "identifier_name"}
1083        else:
1084            vol_identifier_dict = {"volumeIdentifierChoice": "none"}
1085        rename_vol_payload = {"editVolumeActionParam": {
1086            "modifyVolumeIdentifierParam": {
1087                "volumeIdentifier": vol_identifier_dict}}}
1088        self._modify_volume(array, device_id, rename_vol_payload)
1089
1090    def delete_volume(self, array, device_id):
1091        """Deallocate or delete a volume.
1092
1093        :param array: the array serial number
1094        :param device_id: volume device id
1095        """
1096        # Deallocate volume. Can fail if there are no tracks allocated.
1097        payload = {"editVolumeActionParam": {
1098            "freeVolumeParam": {"free_volume": 'true'}}}
1099        try:
1100            self._modify_volume(array, device_id, payload)
1101            # Rename volume, removing the OS-<cinderUUID>
1102            self.rename_volume(array, device_id, None)
1103        except Exception as e:
1104            LOG.warning('Deallocate volume failed with %(e)s.'
1105                        'Attempting delete.', {'e': e})
1106            # Try to delete the volume if deallocate failed.
1107            self.delete_resource(array, SLOPROVISIONING, "volume", device_id)
1108
1109    def find_mv_connections_for_vol(self, array, maskingview, device_id):
1110        """Find the host_lun_id for a volume in a masking view.
1111
1112        :param array: the array serial number
1113        :param maskingview: the masking view name
1114        :param device_id: the device ID
1115        :returns: host_lun_id -- int
1116        """
1117        host_lun_id = None
1118        resource_name = ('%(maskingview)s/connections'
1119                         % {'maskingview': maskingview})
1120        params = {'volume_id': device_id}
1121        connection_info = self.get_resource(
1122            array, SLOPROVISIONING, 'maskingview',
1123            resource_name=resource_name, params=params)
1124        if not connection_info:
1125            LOG.error('Cannot retrive masking view connection information '
1126                      'for %(mv)s.', {'mv': maskingview})
1127        else:
1128            try:
1129                host_lun_id = (
1130                    connection_info[
1131                        'maskingViewConnection'][0]['host_lun_address'])
1132                host_lun_id = int(host_lun_id, 16)
1133            except Exception as e:
1134                LOG.error("Unable to retrieve connection information "
1135                          "for volume %(vol)s in masking view %(mv)s"
1136                          "Exception received: %(e)s.",
1137                          {'vol': device_id, 'mv': maskingview,
1138                           'e': e})
1139        return host_lun_id
1140
1141    def get_storage_groups_from_volume(self, array, device_id):
1142        """Returns all the storage groups for a particular volume.
1143
1144        :param array: the array serial number
1145        :param device_id: the volume device id
1146        :returns: storagegroup_list
1147        """
1148        sg_list = []
1149        vol = self.get_volume(array, device_id)
1150        if vol and vol.get('storageGroupId'):
1151            sg_list = vol['storageGroupId']
1152        num_storage_groups = len(sg_list)
1153        LOG.debug("There are %(num)d storage groups associated "
1154                  "with volume %(deviceId)s.",
1155                  {'num': num_storage_groups, 'deviceId': device_id})
1156        return sg_list
1157
1158    def is_volume_in_storagegroup(self, array, device_id, storagegroup):
1159        """See if a volume is a member of the given storage group.
1160
1161        :param array: the array serial number
1162        :param device_id: the device id
1163        :param storagegroup: the storage group name
1164        :returns: bool
1165        """
1166        is_vol_in_sg = False
1167        sg_list = self.get_storage_groups_from_volume(array, device_id)
1168        if storagegroup in sg_list:
1169            is_vol_in_sg = True
1170        return is_vol_in_sg
1171
1172    def find_volume_device_id(self, array, volume_name):
1173        """Given a volume identifier, find the corresponding device_id.
1174
1175        :param array: the array serial number
1176        :param volume_name: the volume name (OS-<UUID>)
1177        :returns: device_id
1178        """
1179        device_id = None
1180        params = {"volume_identifier": volume_name}
1181
1182        volume_list = self.get_volume_list(array, params)
1183        if not volume_list:
1184            LOG.debug("Cannot find record for volume %(volumeId)s.",
1185                      {'volumeId': volume_name})
1186        else:
1187            device_id = volume_list[0]
1188        return device_id
1189
1190    def find_volume_identifier(self, array, device_id):
1191        """Get the volume identifier of a VMAX volume.
1192
1193        :param array: array serial number
1194        :param device_id: the device id
1195        :returns: the volume identifier -- string
1196        """
1197        vol = self.get_volume(array, device_id)
1198        return vol['volume_identifier']
1199
1200    def get_size_of_device_on_array(self, array, device_id):
1201        """Get the size of the volume from the array.
1202
1203        :param array: the array serial number
1204        :param device_id: the volume device id
1205        :returns: size --  or None
1206        """
1207        cap = None
1208        try:
1209            vol = self.get_volume(array, device_id)
1210            cap = vol['cap_gb']
1211        except Exception as e:
1212            LOG.error("Error retrieving size of volume %(vol)s. "
1213                      "Exception received was %(e)s.",
1214                      {'vol': device_id, 'e': e})
1215        return cap
1216
1217    def get_portgroup(self, array, portgroup):
1218        """Get a portgroup from the array.
1219
1220        :param array: array serial number
1221        :param portgroup: the portgroup name
1222        :returns: portgroup dict or None
1223        """
1224        return self.get_resource(
1225            array, SLOPROVISIONING, 'portgroup', resource_name=portgroup)
1226
1227    def get_port_ids(self, array, portgroup):
1228        """Get a list of port identifiers from a port group.
1229
1230        :param array: the array serial number
1231        :param portgroup: the name of the portgroup
1232        :returns: list of port ids, e.g. ['FA-3D:35', 'FA-4D:32']
1233        """
1234        portlist = []
1235        portgroup_info = self.get_portgroup(array, portgroup)
1236        if portgroup_info:
1237            port_key = portgroup_info["symmetrixPortKey"]
1238            for key in port_key:
1239                port = key['portId']
1240                portlist.append(port)
1241        return portlist
1242
1243    def get_port(self, array, port_id):
1244        """Get director port details.
1245
1246        :param array: the array serial number
1247        :param port_id: the port id
1248        :returns: port dict, or None
1249        """
1250        dir_id = port_id.split(':')[0]
1251        port_no = port_id.split(':')[1]
1252
1253        resource_name = ('%(directorId)s/port/%(port_number)s'
1254                         % {'directorId': dir_id, 'port_number': port_no})
1255        return self.get_resource(array, SLOPROVISIONING, 'director',
1256                                 resource_name=resource_name)
1257
1258    def get_iscsi_ip_address_and_iqn(self, array, port_id):
1259        """Get the IPv4Address from the director port.
1260
1261        :param array: the array serial number
1262        :param port_id: the director port identifier
1263        :returns: (list of ip_addresses, iqn)
1264        """
1265        ip_addresses, iqn = None, None
1266        port_details = self.get_port(array, port_id)
1267        if port_details:
1268            ip_addresses = port_details['symmetrixPort']['ip_addresses']
1269            iqn = port_details['symmetrixPort']['identifier']
1270        return ip_addresses, iqn
1271
1272    def get_target_wwns(self, array, portgroup):
1273        """Get the director ports' wwns.
1274
1275        :param array: the array serial number
1276        :param portgroup: portgroup
1277        :returns: target_wwns -- the list of target wwns for the masking view
1278        """
1279        target_wwns = []
1280        port_ids = self.get_port_ids(array, portgroup)
1281        for port in port_ids:
1282            port_info = self.get_port(array, port)
1283            if port_info:
1284                wwn = port_info['symmetrixPort']['identifier']
1285                target_wwns.append(wwn)
1286            else:
1287                LOG.error("Error retrieving port %(port)s "
1288                          "from portgroup %(portgroup)s.",
1289                          {'port': port, 'portgroup': portgroup})
1290        return target_wwns
1291
1292    def get_initiator_group(self, array, initiator_group=None, params=None):
1293        """Retrieve initiator group details from the array.
1294
1295        :param array: the array serial number
1296        :param initiator_group: the initaitor group name
1297        :param params: optional filter parameters
1298        :returns: initiator group dict, or None
1299        """
1300        return self.get_resource(
1301            array, SLOPROVISIONING, 'host',
1302            resource_name=initiator_group, params=params)
1303
1304    def get_initiator(self, array, initiator_id):
1305        """Retrieve initiator details from the array.
1306
1307        :param array: the array serial number
1308        :param initiator_id: the initiator id
1309        :returns: initiator dict, or None
1310        """
1311        return self.get_resource(
1312            array, SLOPROVISIONING, 'initiator',
1313            resource_name=initiator_id)
1314
1315    def get_initiator_list(self, array, params=None):
1316        """Retrieve initiator list from the array.
1317
1318        :param array: the array serial number
1319        :param params: dict of optional params
1320        :returns: list of initiators
1321        """
1322        version = '90' if self.is_next_gen_array(array) else U4V_VERSION
1323        init_dict = self.get_resource(array, SLOPROVISIONING, 'initiator',
1324                                      params=params, version=version)
1325        try:
1326            init_list = init_dict['initiatorId']
1327        except KeyError:
1328            init_list = []
1329        return init_list
1330
1331    def get_initiator_group_from_initiator(self, array, initiator):
1332        """Given an initiator, get its corresponding initiator group, if any.
1333
1334        :param array: the array serial number
1335        :param initiator: the initiator id
1336        :returns: found_init_group_name -- string
1337        """
1338        found_init_group_name = None
1339        init_details = self.get_initiator(array, initiator)
1340        if init_details:
1341            found_init_group_name = init_details.get('host')
1342        else:
1343            LOG.error("Unable to retrieve initiator details for "
1344                      "%(init)s.", {'init': initiator})
1345        return found_init_group_name
1346
1347    def create_initiator_group(self, array, init_group_name,
1348                               init_list, extra_specs):
1349        """Create a new initiator group containing the given initiators.
1350
1351        :param array: the array serial number
1352        :param init_group_name: the initiator group name
1353        :param init_list: the list of initiators
1354        :param extra_specs: extra specifications
1355        """
1356        new_ig_data = ({"executionOption": "ASYNCHRONOUS",
1357                        "hostId": init_group_name, "initiatorId": init_list})
1358        sc, job = self.create_resource(array, SLOPROVISIONING,
1359                                       'host', new_ig_data)
1360        self.wait_for_job('create initiator group', sc, job, extra_specs)
1361
1362    def delete_initiator_group(self, array, initiatorgroup_name):
1363        """Delete an initiator group.
1364
1365        :param array: the array serial number
1366        :param initiatorgroup_name: initiator group name
1367        """
1368        self.delete_resource(
1369            array, SLOPROVISIONING, 'host', initiatorgroup_name)
1370        LOG.debug("Initiator Group successfully deleted.")
1371
1372    def get_masking_view(self, array, masking_view_name):
1373        """Get details of a masking view.
1374
1375        :param array: array serial number
1376        :param masking_view_name: the masking view name
1377        :returns: masking view dict
1378        """
1379        return self.get_resource(
1380            array, SLOPROVISIONING, 'maskingview', masking_view_name)
1381
1382    def get_masking_view_list(self, array, params):
1383        """Get a list of masking views from the array.
1384
1385        :param array: array serial number
1386        :param params: optional GET parameters
1387        :returns: masking view list
1388        """
1389        masking_view_list = []
1390        masking_view_details = self.get_resource(
1391            array, SLOPROVISIONING, 'maskingview', params=params)
1392        try:
1393            masking_view_list = masking_view_details['maskingViewId']
1394        except (KeyError, TypeError):
1395            pass
1396        return masking_view_list
1397
1398    def get_masking_views_from_storage_group(self, array, storagegroup):
1399        """Return any masking views associated with a storage group.
1400
1401        :param array: the array serial number
1402        :param storagegroup: the storage group name
1403        :returns: masking view list
1404        """
1405        maskingviewlist = []
1406        storagegroup = self.get_storage_group(array, storagegroup)
1407        if storagegroup and storagegroup.get('maskingview'):
1408            maskingviewlist = storagegroup['maskingview']
1409        return maskingviewlist
1410
1411    def get_masking_views_by_initiator_group(
1412            self, array, initiatorgroup_name):
1413        """Given initiator group, retrieve the masking view instance name.
1414
1415        Retrieve the list of masking view instances associated with the
1416        given initiator group.
1417        :param array: the array serial number
1418        :param initiatorgroup_name: the name of the initiator group
1419        :returns: list of masking view names
1420        """
1421        masking_view_list = []
1422        ig_details = self.get_initiator_group(
1423            array, initiatorgroup_name)
1424        if ig_details:
1425            if ig_details.get('maskingview'):
1426                masking_view_list = ig_details['maskingview']
1427        else:
1428            LOG.error("Error retrieving initiator group %(ig_name)s",
1429                      {'ig_name': initiatorgroup_name})
1430        return masking_view_list
1431
1432    def get_element_from_masking_view(
1433            self, array, maskingview_name, portgroup=False, host=False,
1434            storagegroup=False):
1435        """Return the name of the specified element from a masking view.
1436
1437        :param array: the array serial number
1438        :param maskingview_name: the masking view name
1439        :param portgroup: the port group name - optional
1440        :param host: the host name - optional
1441        :param storagegroup: the storage group name - optional
1442        :returns: name of the specified element -- string
1443        :raises: VolumeBackendAPIException
1444        """
1445        element = None
1446        masking_view_details = self.get_masking_view(array, maskingview_name)
1447        if masking_view_details:
1448            if portgroup:
1449                element = masking_view_details['portGroupId']
1450            elif host:
1451                element = masking_view_details['hostId']
1452            elif storagegroup:
1453                element = masking_view_details['storageGroupId']
1454        else:
1455            exception_message = (_("Error retrieving masking group."))
1456            LOG.error(exception_message)
1457            raise exception.VolumeBackendAPIException(data=exception_message)
1458        return element
1459
1460    def get_common_masking_views(self, array, portgroup_name, ig_name):
1461        """Get common masking views for a given portgroup and initiator group.
1462
1463        :param array: the array serial number
1464        :param portgroup_name: the port group name
1465        :param ig_name: the initiator group name
1466        :returns: masking view list
1467        """
1468        params = {'port_group_name': portgroup_name,
1469                  'host_or_host_group_name': ig_name}
1470        masking_view_list = self.get_masking_view_list(array, params)
1471        if not masking_view_list:
1472            LOG.info("No common masking views found for %(pg_name)s "
1473                     "and %(ig_name)s.",
1474                     {'pg_name': portgroup_name, 'ig_name': ig_name})
1475        return masking_view_list
1476
1477    def create_masking_view(self, array, maskingview_name, storagegroup_name,
1478                            port_group_name, init_group_name, extra_specs):
1479        """Create a new masking view.
1480
1481        :param array: the array serial number
1482        :param maskingview_name: the masking view name
1483        :param storagegroup_name: the storage group name
1484        :param port_group_name: the port group
1485        :param init_group_name: the initiator group
1486        :param extra_specs: extra specifications
1487        """
1488        payload = ({"executionOption": "ASYNCHRONOUS",
1489                    "portGroupSelection": {
1490                        "useExistingPortGroupParam": {
1491                            "portGroupId": port_group_name}},
1492                    "maskingViewId": maskingview_name,
1493                    "hostOrHostGroupSelection": {
1494                        "useExistingHostParam": {
1495                            "hostId": init_group_name}},
1496                    "storageGroupSelection": {
1497                        "useExistingStorageGroupParam": {
1498                            "storageGroupId": storagegroup_name}}})
1499
1500        status_code, job = self.create_resource(
1501            array, SLOPROVISIONING, 'maskingview', payload)
1502
1503        self.wait_for_job('Create masking view', status_code, job, extra_specs)
1504
1505    def delete_masking_view(self, array, maskingview_name):
1506        """Delete a masking view.
1507
1508        :param array: the array serial number
1509        :param maskingview_name: the masking view name
1510        """
1511        return self.delete_resource(
1512            array, SLOPROVISIONING, 'maskingview', maskingview_name)
1513
1514    def get_replication_capabilities(self, array):
1515        """Check what replication features are licensed and enabled.
1516
1517        Example return value for this method:
1518
1519        .. code:: python
1520
1521          {"symmetrixId": "000197800128",
1522           "snapVxCapable": true,
1523           "rdfCapable": true}
1524
1525        :param: array
1526        :returns: capabilities dict for the given array
1527        """
1528        array_capabilities = None
1529        target_uri = ("/%s/replication/capabilities/symmetrix"
1530                      % U4V_VERSION)
1531        capabilities = self._get_request(
1532            target_uri, 'replication capabilities')
1533        if capabilities:
1534            symm_list = capabilities['symmetrixCapability']
1535            for symm in symm_list:
1536                if symm['symmetrixId'] == array:
1537                    array_capabilities = symm
1538                    break
1539        return array_capabilities
1540
1541    def is_snapvx_licensed(self, array):
1542        """Check if the snapVx feature is licensed and enabled.
1543
1544        :param array: the array serial number
1545        :returns: True if licensed and enabled; False otherwise.
1546        """
1547        snap_capability = False
1548        capabilities = self.get_replication_capabilities(array)
1549        if capabilities:
1550            snap_capability = capabilities['snapVxCapable']
1551        else:
1552            LOG.error("Cannot access replication capabilities "
1553                      "for array %(array)s", {'array': array})
1554        return snap_capability
1555
1556    def create_volume_snap(self, array, snap_name, device_id, extra_specs):
1557        """Create a snapVx snapshot of a volume.
1558
1559        :param array: the array serial number
1560        :param snap_name: the name of the snapshot
1561        :param device_id: the source device id
1562        :param extra_specs: the extra specifications
1563        """
1564        payload = {"deviceNameListSource": [{"name": device_id}],
1565                   "bothSides": 'false', "star": 'false',
1566                   "force": 'false'}
1567        resource_type = 'snapshot/%(snap)s' % {'snap': snap_name}
1568        status_code, job = self.create_resource(
1569            array, REPLICATION, resource_type,
1570            payload, private='/private')
1571        self.wait_for_job('Create volume snapVx', status_code,
1572                          job, extra_specs)
1573
1574    def modify_volume_snap(self, array, source_id, target_id, snap_name,
1575                           extra_specs, link=False, unlink=False,
1576                           rename=False, new_snap_name=None, restore=False,
1577                           list_volume_pairs=None):
1578        """Modify a snapvx snapshot
1579
1580        :param array: the array serial number
1581        :param source_id: the source device id
1582        :param target_id: the target device id
1583        :param snap_name: the snapshot name
1584        :param extra_specs: extra specifications
1585        :param link: Flag to indicate action = Link
1586        :param unlink: Flag to indicate action = Unlink
1587        :param rename: Flag to indicate action = Rename
1588        :param new_snap_name: Optional new snapshot name
1589        :param restore: Flag to indicate action = Restore
1590        :param list_volume_pairs: list of volume pairs to link, optional
1591        """
1592        action, operation, payload = '', '', {}
1593        if link:
1594            action = "Link"
1595        elif unlink:
1596            action = "Unlink"
1597        elif rename:
1598            action = "Rename"
1599        elif restore:
1600            action = "Restore"
1601
1602        payload = {}
1603        if action == "Restore":
1604            operation = 'Restore snapVx snapshot'
1605            payload = {"deviceNameListSource": [{"name": source_id}],
1606                       "deviceNameListTarget": [{"name": source_id}],
1607                       "action": action,
1608                       "star": 'false', "force": 'false'}
1609        elif action in ('Link', 'Unlink'):
1610            operation = 'Modify snapVx relationship to target'
1611            src_list, tgt_list = [], []
1612            if list_volume_pairs:
1613                for a, b in list_volume_pairs:
1614                    src_list.append({'name': a})
1615                    tgt_list.append({'name': b})
1616            else:
1617                src_list.append({'name': source_id})
1618                tgt_list.append({'name': target_id})
1619            payload = {"deviceNameListSource": src_list,
1620                       "deviceNameListTarget": tgt_list,
1621                       "copy": 'true', "action": action,
1622                       "star": 'false', "force": 'false',
1623                       "exact": 'false', "remote": 'false',
1624                       "symforce": 'false', "nocopy": 'false'}
1625
1626        elif action == "Rename":
1627            operation = 'Rename snapVx snapshot'
1628            payload = {"deviceNameListSource": [{"name": source_id}],
1629                       "deviceNameListTarget": [{"name": source_id}],
1630                       "action": action, "newsnapshotname": new_snap_name}
1631
1632        if action:
1633            status_code, job = self.modify_resource(
1634                array, REPLICATION, 'snapshot', payload,
1635                resource_name=snap_name, private='/private')
1636            self.wait_for_job(operation, status_code, job, extra_specs)
1637
1638    def delete_volume_snap(self, array, snap_name,
1639                           source_device_ids, restored=False):
1640        """Delete the snapshot of a volume or volumes.
1641
1642        :param array: the array serial number
1643        :param snap_name: the name of the snapshot
1644        :param source_device_ids: the source device ids
1645        :param restored: Flag to indicate terminate restore session
1646        """
1647        device_list = []
1648        if not isinstance(source_device_ids, list):
1649            source_device_ids = [source_device_ids]
1650        for dev in source_device_ids:
1651            device_list.append({"name": dev})
1652        payload = {"deviceNameListSource": device_list}
1653        if restored:
1654            payload.update({"restore": True})
1655        return self.delete_resource(
1656            array, REPLICATION, 'snapshot', snap_name, payload=payload,
1657            private='/private')
1658
1659    def get_volume_snap_info(self, array, source_device_id):
1660        """Get snapVx information associated with a volume.
1661
1662        :param array: the array serial number
1663        :param source_device_id: the source volume device ID
1664        :returns: message -- dict, or None
1665        """
1666        resource_name = ("%(device_id)s/snapshot"
1667                         % {'device_id': source_device_id})
1668        return self.get_resource(array, REPLICATION, 'volume',
1669                                 resource_name, private='/private')
1670
1671    def get_volume_snap(self, array, device_id, snap_name):
1672        """Given a volume snap info, retrieve the snapVx object.
1673
1674        :param array: the array serial number
1675        :param device_id: the source volume device id
1676        :param snap_name: the name of the snapshot
1677        :returns: snapshot dict, or None
1678        """
1679        snapshot = None
1680        snap_info = self.get_volume_snap_info(array, device_id)
1681        if snap_info:
1682            if (snap_info.get('snapshotSrcs') and
1683                    bool(snap_info['snapshotSrcs'])):
1684                        for snap in snap_info['snapshotSrcs']:
1685                            if snap['snapshotName'] == snap_name:
1686                                snapshot = snap
1687        return snapshot
1688
1689    def get_volume_snapshot_list(self, array, source_device_id):
1690        """Get a list of snapshot details for a particular volume.
1691
1692        :param array: the array serial number
1693        :param source_device_id: the osurce device id
1694        :returns: snapshot list or None
1695        """
1696        snapshot_list = []
1697        snap_info = self.get_volume_snap_info(array, source_device_id)
1698        if snap_info:
1699            if bool(snap_info['snapshotSrcs']):
1700                snapshot_list = snap_info['snapshotSrcs']
1701        return snapshot_list
1702
1703    def is_vol_in_rep_session(self, array, device_id):
1704        """Check if a volume is in a replication session.
1705
1706        :param array: the array serial number
1707        :param device_id: the device id
1708        :returns: snapvx_tgt -- bool, snapvx_src -- bool,
1709                 rdf_grp -- list or None
1710        """
1711        snapvx_src = False
1712        snapvx_tgt = False
1713        rdf_grp = None
1714        volume_details = self.get_volume(array, device_id)
1715        if volume_details:
1716            LOG.debug("Vol details: %(vol)s", {'vol': volume_details})
1717            if volume_details.get('snapvx_target'):
1718                snapvx_tgt = volume_details['snapvx_target']
1719            if volume_details.get('snapvx_source'):
1720                snapvx_src = volume_details['snapvx_source']
1721            if volume_details.get('rdfGroupId'):
1722                rdf_grp = volume_details['rdfGroupId']
1723        return snapvx_tgt, snapvx_src, rdf_grp
1724
1725    def is_sync_complete(self, array, source_device_id,
1726                         target_device_id, snap_name, extra_specs):
1727        """Check if a sync session is complete.
1728
1729        :param array: the array serial number
1730        :param source_device_id: source device id
1731        :param target_device_id: target device id
1732        :param snap_name: snapshot name
1733        :param extra_specs: extra specifications
1734        :returns: bool
1735        """
1736
1737        def _wait_for_sync():
1738            """Called at an interval until the synchronization is finished.
1739
1740            :raises: loopingcall.LoopingCallDone
1741            :raises: VolumeBackendAPIException
1742            """
1743            retries = kwargs['retries']
1744            try:
1745                kwargs['retries'] = retries + 1
1746                if not kwargs['wait_for_sync_called']:
1747                    if self._is_sync_complete(
1748                            array, source_device_id, snap_name,
1749                            target_device_id):
1750                        kwargs['wait_for_sync_called'] = True
1751            except Exception:
1752                exception_message = (_("Issue encountered waiting for "
1753                                       "synchronization."))
1754                LOG.exception(exception_message)
1755                raise exception.VolumeBackendAPIException(
1756                    data=exception_message)
1757
1758            if kwargs['retries'] > int(extra_specs[utils.RETRIES]):
1759                LOG.error("_wait_for_sync failed after %(retries)d "
1760                          "tries.", {'retries': retries})
1761                raise loopingcall.LoopingCallDone(
1762                    retvalue=int(extra_specs[utils.RETRIES]))
1763            if kwargs['wait_for_sync_called']:
1764                raise loopingcall.LoopingCallDone()
1765
1766        kwargs = {'retries': 0,
1767                  'wait_for_sync_called': False}
1768        timer = loopingcall.FixedIntervalLoopingCall(_wait_for_sync)
1769        rc = timer.start(interval=int(extra_specs[utils.INTERVAL])).wait()
1770        return rc
1771
1772    def _is_sync_complete(self, array, source_device_id, snap_name,
1773                          target_device_id):
1774        """Helper function to check if snapVx sync session is complete.
1775
1776        :param array: the array serial number
1777        :param source_device_id: source device id
1778        :param snap_name: the snapshot name
1779        :param target_device_id: the target device id
1780        :returns: defined -- bool
1781        """
1782        defined = True
1783        session = self.get_sync_session(
1784            array, source_device_id, snap_name, target_device_id)
1785        if session:
1786            defined = session['defined']
1787        return defined
1788
1789    def get_sync_session(self, array, source_device_id, snap_name,
1790                         target_device_id):
1791        """Get a particular sync session.
1792
1793        :param array: the array serial number
1794        :param source_device_id: source device id
1795        :param snap_name: the snapshot name
1796        :param target_device_id: the target device id
1797        :returns: sync session -- dict, or None
1798        """
1799        session = None
1800        linked_device_list = self.get_snap_linked_device_list(
1801            array, source_device_id, snap_name)
1802        for target in linked_device_list:
1803            if target_device_id == target['targetDevice']:
1804                session = target
1805        return session
1806
1807    def _find_snap_vx_source_sessions(self, array, source_device_id):
1808        """Find all snap sessions for a given source volume.
1809
1810        :param array: the array serial number
1811        :param source_device_id: the source device id
1812        :returns: list of snapshot dicts
1813        """
1814        snap_dict_list = []
1815        snapshots = self.get_volume_snapshot_list(array, source_device_id)
1816        for snapshot in snapshots:
1817            if bool(snapshot['linkedDevices']):
1818                link_info = {'linked_vols': snapshot['linkedDevices'],
1819                             'snap_name': snapshot['snapshotName']}
1820                snap_dict_list.append(link_info)
1821        return snap_dict_list
1822
1823    def get_snap_linked_device_list(self, array, source_device_id, snap_name):
1824        """Get the list of linked devices for a particular snapVx snapshot.
1825
1826        :param array: the array serial number
1827        :param source_device_id: source device id
1828        :param snap_name: the snapshot name
1829        :returns: linked_device_list
1830        """
1831        linked_device_list = []
1832        snap_list = self._find_snap_vx_source_sessions(array, source_device_id)
1833        for snap in snap_list:
1834            if snap['snap_name'] == snap_name:
1835                linked_device_list = snap['linked_vols']
1836        return linked_device_list
1837
1838    def find_snap_vx_sessions(self, array, device_id, tgt_only=False):
1839        """Find all snapVX sessions for a device (source and target).
1840
1841        :param array: the array serial number
1842        :param device_id: the device id
1843        :param tgt_only: Flag - return only sessions where device is target
1844        :returns: list of snapshot dicts
1845        """
1846        snap_dict_list, sessions = [], []
1847        vol_details = self._get_private_volume(array, device_id)
1848        snap_vx_info = vol_details['timeFinderInfo']
1849        is_snap_src = snap_vx_info['snapVXSrc']
1850        is_snap_tgt = snap_vx_info['snapVXTgt']
1851        if snap_vx_info.get('snapVXSession'):
1852            sessions = snap_vx_info['snapVXSession']
1853        if is_snap_src and not tgt_only:
1854            for session in sessions:
1855                if session.get('srcSnapshotGenInfo'):
1856                    src_list = session['srcSnapshotGenInfo']
1857                    for src in src_list:
1858                        snap_name = src['snapshotHeader']['snapshotName']
1859                        target_list, target_dict = [], {}
1860                        if src.get('lnkSnapshotGenInfo'):
1861                            target_dict = src['lnkSnapshotGenInfo']
1862                        for tgt in target_dict:
1863                            target_list.append(tgt['targetDevice'])
1864                        link_info = {'target_vol_list': target_list,
1865                                     'snap_name': snap_name,
1866                                     'source_vol': device_id}
1867                        snap_dict_list.append(link_info)
1868        if is_snap_tgt:
1869            for session in sessions:
1870                if session.get('tgtSrcSnapshotGenInfo'):
1871                    tgt = session['tgtSrcSnapshotGenInfo']
1872                    snap_name = tgt['snapshotName']
1873                    target_list = [tgt['targetDevice']]
1874                    source_vol = tgt['sourceDevice']
1875                    link_info = {'target_vol_list': target_list,
1876                                 'snap_name': snap_name,
1877                                 'source_vol': source_vol}
1878                    snap_dict_list.append(link_info)
1879        return snap_dict_list
1880
1881    def get_rdf_group(self, array, rdf_number):
1882        """Get specific rdf group details.
1883
1884        :param array: the array serial number
1885        :param rdf_number: the rdf number
1886        """
1887        return self.get_resource(array, REPLICATION, 'rdf_group',
1888                                 rdf_number)
1889
1890    def get_rdf_group_list(self, array):
1891        """Get rdf group list from array.
1892
1893        :param array: the array serial number
1894        """
1895        return self.get_resource(array, REPLICATION, 'rdf_group')
1896
1897    def get_rdf_group_volume(self, array, src_device_id):
1898        """Get the RDF details for a volume.
1899
1900        :param array: the array serial number
1901        :param src_device_id: the source device id
1902        :returns: rdf_session
1903        """
1904        rdf_session = None
1905        volume = self._get_private_volume(array, src_device_id)
1906        try:
1907            rdf_session = volume['rdfInfo']['RDFSession'][0]
1908        except (KeyError, TypeError, IndexError):
1909            LOG.warning("Cannot locate source RDF volume %s", src_device_id)
1910        return rdf_session
1911
1912    def are_vols_rdf_paired(self, array, remote_array,
1913                            device_id, target_device):
1914        """Check if a pair of volumes are RDF paired.
1915
1916        :param array: the array serial number
1917        :param remote_array: the remote array serial number
1918        :param device_id: the device id
1919        :param target_device: the target device id
1920        :returns: paired -- bool, local_vol_state, rdf_pair_state
1921        """
1922        paired, local_vol_state, rdf_pair_state = False, '', ''
1923        rdf_session = self.get_rdf_group_volume(array, device_id)
1924        if rdf_session:
1925            remote_volume = rdf_session['remoteDeviceID']
1926            remote_symm = rdf_session['remoteSymmetrixID']
1927            if (remote_volume == target_device
1928                    and remote_array == remote_symm):
1929                paired = True
1930                local_vol_state = rdf_session['SRDFStatus']
1931                rdf_pair_state = rdf_session['pairState']
1932        else:
1933            LOG.warning("Cannot locate RDF session for volume %s", device_id)
1934        return paired, local_vol_state, rdf_pair_state
1935
1936    def wait_for_rdf_consistent_state(
1937            self, array, remote_array, device_id, target_device, extra_specs):
1938        """Wait for async pair to be in a consistent state before suspending.
1939
1940        :param array: the array serial number
1941        :param remote_array: the remote array serial number
1942        :param device_id: the device id
1943        :param target_device: the target device id
1944        :param extra_specs: the extra specifications
1945        """
1946
1947        def _wait_for_consistent_state():
1948            # Called at an interval until the state of the
1949            # rdf pair is 'consistent'.
1950            retries = kwargs['retries']
1951            try:
1952                kwargs['retries'] = retries + 1
1953                if not kwargs['consistent_state']:
1954                    __, __, state = (
1955                        self.are_vols_rdf_paired(
1956                            array, remote_array, device_id, target_device))
1957                    kwargs['state'] = state
1958                    if state.lower() == utils.RDF_CONSISTENT_STATE:
1959                        kwargs['consistent_state'] = True
1960                        kwargs['rc'] = 0
1961            except Exception:
1962                exception_message = _("Issue encountered waiting for job.")
1963                LOG.exception(exception_message)
1964                raise exception.VolumeBackendAPIException(
1965                    data=exception_message)
1966
1967            if retries > int(extra_specs[utils.RETRIES]):
1968                LOG.error("_wait_for_consistent_state failed after "
1969                          "%(retries)d tries.", {'retries': retries})
1970                kwargs['rc'] = -1
1971
1972                raise loopingcall.LoopingCallDone()
1973            if kwargs['consistent_state']:
1974                raise loopingcall.LoopingCallDone()
1975
1976        kwargs = {'retries': 0, 'consistent_state': False,
1977                  'rc': 0, 'state': 'syncinprog'}
1978
1979        timer = loopingcall.FixedIntervalLoopingCall(
1980            _wait_for_consistent_state)
1981        timer.start(interval=int(extra_specs[utils.INTERVAL])).wait()
1982        LOG.debug("Return code is: %(rc)lu. State is %(state)s",
1983                  {'rc': kwargs['rc'], 'state': kwargs['state']})
1984
1985    def get_rdf_group_number(self, array, rdf_group_label):
1986        """Given an rdf_group_label, return the associated group number.
1987
1988        :param array: the array serial number
1989        :param rdf_group_label: the group label
1990        :returns: rdf_group_number
1991        """
1992        number = None
1993        rdf_list = self.get_rdf_group_list(array)
1994        if rdf_list and rdf_list.get('rdfGroupID'):
1995            number_list = [rdf['rdfgNumber'] for rdf in rdf_list['rdfGroupID']
1996                           if rdf['label'] == rdf_group_label]
1997            number = number_list[0] if len(number_list) > 0 else None
1998        if number:
1999            rdf_group = self.get_rdf_group(array, number)
2000            if not rdf_group:
2001                number = None
2002        return number
2003
2004    @coordination.synchronized('emc-rg-{rdf_group_no}')
2005    def create_rdf_device_pair(self, array, device_id, rdf_group_no,
2006                               target_device, remote_array, extra_specs):
2007        """Create an RDF pairing.
2008
2009        Create a remote replication relationship between source and target
2010        devices.
2011        :param array: the array serial number
2012        :param device_id: the device id
2013        :param rdf_group_no: the rdf group number
2014        :param target_device: the target device id
2015        :param remote_array: the remote array serial
2016        :param extra_specs: the extra specs
2017        :returns: rdf_dict
2018        """
2019        rep_mode = extra_specs[utils.REP_MODE]
2020        if rep_mode == utils.REP_METRO:
2021            rep_mode = 'Active'
2022        payload = ({"deviceNameListSource": [{"name": device_id}],
2023                    "deviceNameListTarget": [{"name": target_device}],
2024                    "replicationMode": rep_mode,
2025                    "establish": 'true',
2026                    "rdfType": 'RDF1'})
2027        if rep_mode == utils.REP_ASYNC:
2028            payload_update = self._get_async_payload_info(array, rdf_group_no)
2029            payload.update(payload_update)
2030        elif rep_mode == 'Active':
2031            payload = self.get_metro_payload_info(
2032                array, payload, rdf_group_no, extra_specs)
2033        resource_type = ("rdf_group/%(rdf_num)s/volume"
2034                         % {'rdf_num': rdf_group_no})
2035        status_code, job = self.create_resource(array, REPLICATION,
2036                                                resource_type, payload,
2037                                                private="/private")
2038        self.wait_for_job('Create rdf pair', status_code,
2039                          job, extra_specs)
2040        rdf_dict = {'array': remote_array, 'device_id': target_device}
2041        return rdf_dict
2042
2043    def _get_async_payload_info(self, array, rdf_group_no):
2044        """Get the payload details for an async create pair.
2045
2046        :param array: the array serial number
2047        :param rdf_group_no: the rdf group number
2048        :return: payload_update
2049        """
2050        num_vols, payload_update = 0, {}
2051        rdfg_details = self.get_rdf_group(array, rdf_group_no)
2052        if rdfg_details is not None and rdfg_details.get('numDevices'):
2053            num_vols = int(rdfg_details['numDevices'])
2054        if num_vols > 0:
2055            payload_update = {'consExempt': 'true'}
2056        return payload_update
2057
2058    def get_metro_payload_info(self, array, payload,
2059                               rdf_group_no, extra_specs):
2060        """Get the payload details for a metro active create pair.
2061
2062        :param array: the array serial number
2063        :param payload: the payload
2064        :param rdf_group_no: the rdf group number
2065        :param extra_specs: the replication configuration
2066        :return: updated payload
2067        """
2068        num_vols = 0
2069        rdfg_details = self.get_rdf_group(array, rdf_group_no)
2070        if rdfg_details is not None and rdfg_details.get('numDevices'):
2071            num_vols = int(rdfg_details['numDevices'])
2072        if num_vols == 0:
2073            # First volume - set bias if required
2074            if (extra_specs.get(utils.METROBIAS)
2075                    and extra_specs[utils.METROBIAS] is True):
2076                payload.update({'metroBias': 'true'})
2077        else:
2078            # Need to format subsequent volumes
2079            payload['format'] = 'true'
2080            payload.pop('establish')
2081            payload['rdfType'] = 'NA'
2082        return payload
2083
2084    def modify_rdf_device_pair(
2085            self, array, device_id, rdf_group, extra_specs, suspend=False):
2086        """Modify an rdf device pair.
2087
2088        :param array: the array serial number
2089        :param device_id: the device id
2090        :param rdf_group: the rdf group
2091        :param extra_specs: the extra specs
2092        :param suspend: flag to indicate "suspend" action
2093        """
2094        common_opts = {"force": 'false',
2095                       "symForce": 'false',
2096                       "star": 'false',
2097                       "hop2": 'false',
2098                       "bypass": 'false'}
2099        if suspend:
2100            if (extra_specs.get(utils.REP_MODE)
2101                    and extra_specs[utils.REP_MODE] == utils.REP_ASYNC):
2102                common_opts.update({"immediate": 'false',
2103                                    "consExempt": 'true'})
2104            payload = {"action": "Suspend",
2105                       "executionOption": "ASYNCHRONOUS",
2106                       "suspend": common_opts}
2107
2108        else:
2109            common_opts.update({"establish": 'true',
2110                                "restore": 'false',
2111                                "remote": 'false',
2112                                "immediate": 'false'})
2113            payload = {"action": "Failover",
2114                       "executionOption": "ASYNCHRONOUS",
2115                       "failover": common_opts}
2116        resource_name = ("%(rdf_num)s/volume/%(device_id)s"
2117                         % {'rdf_num': rdf_group, 'device_id': device_id})
2118        sc, job = self.modify_resource(
2119            array, REPLICATION, 'rdf_group',
2120            payload, resource_name=resource_name, private="/private")
2121        self.wait_for_job('Modify device pair', sc,
2122                          job, extra_specs)
2123
2124    def delete_rdf_pair(self, array, device_id, rdf_group):
2125        """Delete an rdf pair.
2126
2127        :param array: the array serial number
2128        :param device_id: the device id
2129        :param rdf_group: the rdf group
2130        """
2131        params = {'half': 'false', 'force': 'true', 'symforce': 'false',
2132                  'star': 'false', 'bypass': 'false'}
2133        resource_name = ("%(rdf_num)s/volume/%(device_id)s"
2134                         % {'rdf_num': rdf_group, 'device_id': device_id})
2135        self.delete_resource(array, REPLICATION, 'rdf_group', resource_name,
2136                             private="/private", params=params)
2137
2138    def get_storage_group_rep(self, array, storage_group_name):
2139        """Given a name, return storage group details wrt replication.
2140
2141        :param array: the array serial number
2142        :param storage_group_name: the name of the storage group
2143        :returns: storage group dict or None
2144        """
2145        return self.get_resource(
2146            array, REPLICATION, 'storagegroup',
2147            resource_name=storage_group_name)
2148
2149    def get_volumes_in_storage_group(self, array, storagegroup_name):
2150        """Given a volume identifier, find the corresponding device_id.
2151
2152        :param array: the array serial number
2153        :param storagegroup_name: the storage group name
2154        :returns: volume_list
2155        """
2156        params = {"storageGroupId": storagegroup_name}
2157
2158        volume_list = self.get_volume_list(array, params)
2159        if not volume_list:
2160            LOG.debug("Cannot find record for storage group %(storageGrpId)s",
2161                      {'storageGrpId': storagegroup_name})
2162        return volume_list
2163
2164    def create_storagegroup_snap(self, array, source_group,
2165                                 snap_name, extra_specs):
2166        """Create a snapVx snapshot of a storage group.
2167
2168        :param array: the array serial number
2169        :param source_group: the source group name
2170        :param snap_name: the name of the snapshot
2171        :param extra_specs: the extra specifications
2172        """
2173        payload = {"snapshotName": snap_name}
2174        resource_type = ('storagegroup/%(sg_name)s/snapshot'
2175                         % {'sg_name': source_group})
2176        status_code, job = self.create_resource(
2177            array, REPLICATION, resource_type, payload)
2178        self.wait_for_job('Create storage group snapVx', status_code,
2179                          job, extra_specs)
2180
2181    def get_storagegroup_rdf_details(self, array, storagegroup_name,
2182                                     rdf_group_num):
2183        """Get the remote replication details of a storage group.
2184
2185        :param array: the array serial number
2186        :param storagegroup_name: the storage group name
2187        :param rdf_group_num: the rdf group number
2188        """
2189        resource_name = ("%(sg_name)s/rdf_group/%(rdf_num)s"
2190                         % {'sg_name': storagegroup_name,
2191                            'rdf_num': rdf_group_num})
2192        return self.get_resource(array, REPLICATION, 'storagegroup',
2193                                 resource_name=resource_name)
2194
2195    def replicate_group(self, array, storagegroup_name,
2196                        rdf_group_num, remote_array, extra_specs):
2197        """Create a target group on the remote array and enable replication.
2198
2199        :param array: the array serial number
2200        :param storagegroup_name: the name of the group
2201        :param rdf_group_num: the rdf group number
2202        :param remote_array: the remote array serial number
2203        :param extra_specs: the extra specifications
2204        """
2205        resource_name = ("storagegroup/%(sg_name)s/rdf_group"
2206                         % {'sg_name': storagegroup_name})
2207        payload = {"executionOption": "ASYNCHRONOUS",
2208                   "replicationMode": utils.REP_SYNC,
2209                   "remoteSymmId": remote_array,
2210                   "remoteStorageGroupName": storagegroup_name,
2211                   "rdfgNumber": rdf_group_num, "establish": 'true'}
2212        status_code, job = self.create_resource(
2213            array, REPLICATION, resource_name, payload)
2214        self.wait_for_job('Create storage group rdf', status_code,
2215                          job, extra_specs)
2216
2217    def _verify_rdf_state(self, array, storagegroup_name,
2218                          rdf_group_num, action):
2219        """Verify if a storage group requires the requested state change.
2220
2221        :param array: the array serial number
2222        :param storagegroup_name: the storage group name
2223        :param rdf_group_num: the rdf group number
2224        :param action: the requested action
2225        :returns: bool
2226        """
2227        mod_rqd = False
2228        sg_rdf_details = self.get_storagegroup_rdf_details(
2229            array, storagegroup_name, rdf_group_num)
2230        if sg_rdf_details:
2231            state_list = sg_rdf_details['states']
2232            LOG.debug("RDF state: %(sl)s; Action required: %(action)s",
2233                      {'sl': state_list, 'action': action})
2234            for state in state_list:
2235                if (action.lower() in ["establish", "failback", "resume"] and
2236                        state.lower() in [utils.RDF_SUSPENDED_STATE,
2237                                          utils.RDF_FAILEDOVER_STATE]):
2238                    mod_rqd = True
2239                    break
2240                elif (action.lower() in ["split", "failover", "suspend"] and
2241                      state.lower() in [utils.RDF_SYNC_STATE,
2242                                        utils.RDF_SYNCINPROG_STATE,
2243                                        utils.RDF_CONSISTENT_STATE,
2244                                        utils.RDF_ACTIVE,
2245                                        utils.RDF_ACTIVEACTIVE,
2246                                        utils.RDF_ACTIVEBIAS]):
2247                    mod_rqd = True
2248                    break
2249        return mod_rqd
2250
2251    def modify_storagegroup_rdf(self, array, storagegroup_name,
2252                                rdf_group_num, action, extra_specs):
2253        """Modify the rdf state of a storage group.
2254
2255        :param array: the array serial number
2256        :param storagegroup_name: the name of the storage group
2257        :param rdf_group_num: the number of the rdf group
2258        :param action: the required action
2259        :param extra_specs: the extra specifications
2260        """
2261        # Check if group is in valid state for desired action
2262        mod_reqd = self._verify_rdf_state(array, storagegroup_name,
2263                                          rdf_group_num, action)
2264        if mod_reqd:
2265            payload = {"executionOption": "ASYNCHRONOUS", "action": action}
2266            if action.lower() == 'suspend':
2267                payload['suspend'] = {"force": "true"}
2268            elif action.lower() == 'establish':
2269                metro_bias = (
2270                    True if extra_specs.get(utils.METROBIAS) and extra_specs[
2271                        utils.METROBIAS] is True else False)
2272                payload['establish'] = {"metroBias": metro_bias,
2273                                        "full": 'false'}
2274            resource_name = ('%(sg_name)s/rdf_group/%(rdf_num)s'
2275                             % {'sg_name': storagegroup_name,
2276                                'rdf_num': rdf_group_num})
2277
2278            status_code, job = self.modify_resource(
2279                array, REPLICATION, 'storagegroup', payload,
2280                resource_name=resource_name)
2281
2282            self.wait_for_job('Modify storagegroup rdf',
2283                              status_code, job, extra_specs)
2284
2285    def delete_storagegroup_rdf(self, array, storagegroup_name,
2286                                rdf_group_num):
2287        """Delete the rdf pairs for a storage group.
2288
2289        :param array: the array serial number
2290        :param storagegroup_name: the name of the storage group
2291        :param rdf_group_num: the number of the rdf group
2292        """
2293        resource_name = ('%(sg_name)s/rdf_group/%(rdf_num)s'
2294                         % {'sg_name': storagegroup_name,
2295                            'rdf_num': rdf_group_num})
2296        self.delete_resource(
2297            array, REPLICATION, 'storagegroup', resource_name=resource_name)
2298