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