1# Copyright (c) 2017 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 16from copy import deepcopy 17import datetime 18import hashlib 19import random 20import re 21from xml.dom import minidom 22 23from cinder.objects.group import Group 24from oslo_log import log as logging 25from oslo_utils import strutils 26import six 27 28from cinder import exception 29from cinder.i18n import _ 30from cinder.objects import fields 31from cinder.volume import volume_types 32 33 34LOG = logging.getLogger(__name__) 35# SHARED CONSTANTS 36ISCSI = 'iscsi' 37FC = 'fc' 38INTERVAL = 'interval' 39RETRIES = 'retries' 40VOLUME_ELEMENT_NAME_PREFIX = 'OS-' 41VMAX_AFA_MODELS = ['VMAX250F', 'VMAX450F', 'VMAX850F', 'VMAX950F'] 42MAX_SRP_LENGTH = 16 43TRUNCATE_5 = 5 44TRUNCATE_27 = 27 45 46ARRAY = 'array' 47SLO = 'slo' 48WORKLOAD = 'workload' 49SRP = 'srp' 50PORTGROUPNAME = 'storagetype:portgroupname' 51DEVICE_ID = 'device_id' 52INITIATOR_CHECK = 'initiator_check' 53SG_NAME = 'storagegroup_name' 54MV_NAME = 'maskingview_name' 55IG_NAME = 'init_group_name' 56PARENT_SG_NAME = 'parent_sg_name' 57CONNECTOR = 'connector' 58VOL_NAME = 'volume_name' 59EXTRA_SPECS = 'extra_specs' 60IS_RE = 'replication_enabled' 61DISABLECOMPRESSION = 'storagetype:disablecompression' 62REP_SYNC = 'Synchronous' 63REP_ASYNC = 'Asynchronous' 64REP_METRO = 'Metro' 65REP_MODE = 'rep_mode' 66RDF_SYNC_STATE = 'synchronized' 67RDF_SYNCINPROG_STATE = 'syncinprog' 68RDF_CONSISTENT_STATE = 'consistent' 69RDF_SUSPENDED_STATE = 'suspended' 70RDF_FAILEDOVER_STATE = 'failed over' 71RDF_ACTIVE = 'active' 72RDF_ACTIVEACTIVE = 'activeactive' 73RDF_ACTIVEBIAS = 'activebias' 74METROBIAS = 'metro_bias' 75 76# Cinder.conf vmax configuration 77VMAX_SERVER_IP = 'san_ip' 78VMAX_USER_NAME = 'san_login' 79VMAX_PASSWORD = 'san_password' 80VMAX_SERVER_PORT = 'san_rest_port' 81VMAX_ARRAY = 'vmax_array' 82VMAX_WORKLOAD = 'vmax_workload' 83VMAX_SRP = 'vmax_srp' 84VMAX_SERVICE_LEVEL = 'vmax_service_level' 85VMAX_PORT_GROUPS = 'vmax_port_groups' 86 87 88class VMAXUtils(object): 89 """Utility class for Rest based VMAX volume drivers. 90 91 This Utility class is for VMAX volume drivers based on Unisphere Rest API. 92 """ 93 94 def __init__(self): 95 """Utility class for Rest based VMAX volume drivers.""" 96 97 def get_host_short_name(self, host_name): 98 """Returns the short name for a given qualified host name. 99 100 Checks the host name to see if it is the fully qualified host name 101 and returns part before the dot. If there is no dot in the host name 102 the full host name is returned. 103 :param host_name: the fully qualified host name 104 :returns: string -- the short host_name 105 """ 106 host_array = host_name.split('.') 107 if len(host_array) > 1: 108 short_host_name = host_array[0] 109 else: 110 short_host_name = host_name 111 112 return self.generate_unique_trunc_host(short_host_name) 113 114 @staticmethod 115 def get_volumetype_extra_specs(volume, volume_type_id=None): 116 """Gets the extra specs associated with a volume type. 117 118 :param volume: the volume dictionary 119 :param volume_type_id: Optional override for volume.volume_type_id 120 :returns: dict -- extra_specs - the extra specs 121 :raises: VolumeBackendAPIException 122 """ 123 extra_specs = {} 124 125 try: 126 if volume_type_id: 127 type_id = volume_type_id 128 else: 129 type_id = volume.volume_type_id 130 if type_id is not None: 131 extra_specs = volume_types.get_volume_type_extra_specs(type_id) 132 except Exception as e: 133 LOG.debug('Exception getting volume type extra specs: %(e)s', 134 {'e': six.text_type(e)}) 135 return extra_specs 136 137 @staticmethod 138 def get_short_protocol_type(protocol): 139 """Given the protocol type, return I for iscsi and F for fc. 140 141 :param protocol: iscsi or fc 142 :returns: string -- 'I' for iscsi or 'F' for fc 143 """ 144 if protocol.lower() == ISCSI.lower(): 145 return 'I' 146 elif protocol.lower() == FC.lower(): 147 return 'F' 148 else: 149 return protocol 150 151 @staticmethod 152 def truncate_string(str_to_truncate, max_num): 153 """Truncate a string by taking first and last characters. 154 155 :param str_to_truncate: the string to be truncated 156 :param max_num: the maximum number of characters 157 :returns: string -- truncated string or original string 158 """ 159 if len(str_to_truncate) > max_num: 160 new_num = len(str_to_truncate) - max_num // 2 161 first_chars = str_to_truncate[:max_num // 2] 162 last_chars = str_to_truncate[new_num:] 163 str_to_truncate = first_chars + last_chars 164 return str_to_truncate 165 166 @staticmethod 167 def get_time_delta(start_time, end_time): 168 """Get the delta between start and end time. 169 170 :param start_time: the start time 171 :param end_time: the end time 172 :returns: string -- delta in string H:MM:SS 173 """ 174 delta = end_time - start_time 175 return six.text_type(datetime.timedelta(seconds=int(delta))) 176 177 def get_default_storage_group_name( 178 self, srp_name, slo, workload, is_compression_disabled=False, 179 is_re=False, rep_mode=None): 180 """Determine default storage group from extra_specs. 181 182 :param srp_name: the name of the srp on the array 183 :param slo: the service level string e.g Bronze 184 :param workload: the workload string e.g DSS 185 :param is_compression_disabled: flag for disabling compression 186 :param is_re: flag for replication 187 :param rep_mode: flag to indicate replication mode 188 :returns: storage_group_name 189 """ 190 if slo and workload: 191 prefix = ("OS-%(srpName)s-%(slo)s-%(workload)s" 192 % {'srpName': srp_name, 'slo': slo, 193 'workload': workload}) 194 195 if is_compression_disabled: 196 prefix += "-CD" 197 198 else: 199 prefix = "OS-no_SLO" 200 if is_re: 201 prefix += self.get_replication_prefix(rep_mode) 202 203 storage_group_name = ("%(prefix)s-SG" % {'prefix': prefix}) 204 return storage_group_name 205 206 @staticmethod 207 def get_volume_element_name(volume_id): 208 """Get volume element name follows naming convention, i.e. 'OS-UUID'. 209 210 :param volume_id: Openstack volume ID containing uuid 211 :returns: volume element name in format of OS-UUID 212 """ 213 element_name = volume_id 214 uuid_regex = (re.compile( 215 '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}', 216 re.I)) 217 match = uuid_regex.search(volume_id) 218 if match: 219 volume_uuid = match.group() 220 element_name = ("%(prefix)s%(volumeUUID)s" 221 % {'prefix': VOLUME_ELEMENT_NAME_PREFIX, 222 'volumeUUID': volume_uuid}) 223 LOG.debug( 224 "get_volume_element_name elementName: %(elementName)s.", 225 {'elementName': element_name}) 226 return element_name 227 228 @staticmethod 229 def modify_snapshot_prefix(snapshot_name, manage=False, unmanage=False): 230 """Modify a Snapshot prefix on VMAX backend. 231 232 Prepare a snapshot name for manage/unmanage snapshot process either 233 by adding or removing 'OS-' prefix. 234 235 :param snapshot_name: the old snapshot backend display name 236 :param manage: (bool) if the operation is managing a snapshot 237 :param unmanage: (bool) if the operation is unmanaging a snapshot 238 :return: snapshot name ready for backend VMAX assignment 239 """ 240 new_snap_name = None 241 if manage: 242 new_snap_name = ("%(prefix)s%(snapshot_name)s" 243 % {'prefix': 'OS-', 244 'snapshot_name': snapshot_name}) 245 246 if unmanage: 247 snap_split = snapshot_name.split("-", 1) 248 if snap_split[0] == 'OS': 249 new_snap_name = snap_split[1] 250 251 return new_snap_name 252 253 def generate_unique_trunc_host(self, host_name): 254 """Create a unique short host name under 16 characters. 255 256 :param host_name: long host name 257 :returns: truncated host name 258 """ 259 if host_name and len(host_name) > 16: 260 host_name = host_name.lower() 261 m = hashlib.md5() 262 m.update(host_name.encode('utf-8')) 263 uuid = m.hexdigest() 264 new_name = ("%(host)s%(uuid)s" 265 % {'host': host_name[-6:], 266 'uuid': uuid}) 267 host_name = self.truncate_string(new_name, 16) 268 return host_name 269 270 def get_pg_short_name(self, portgroup_name): 271 """Create a unique port group name under 12 characters. 272 273 :param portgroup_name: long portgroup_name 274 :returns: truncated portgroup_name 275 """ 276 if portgroup_name and len(portgroup_name) > 12: 277 portgroup_name = portgroup_name.lower() 278 m = hashlib.md5() 279 m.update(portgroup_name.encode('utf-8')) 280 uuid = m.hexdigest() 281 new_name = ("%(pg)s%(uuid)s" 282 % {'pg': portgroup_name[-6:], 283 'uuid': uuid}) 284 portgroup_name = self.truncate_string(new_name, 12) 285 return portgroup_name 286 287 @staticmethod 288 def get_default_oversubscription_ratio(max_over_sub_ratio): 289 """Override ratio if necessary. 290 291 The over subscription ratio will be overridden if the user supplied 292 max oversubscription ratio is less than 1. 293 :param max_over_sub_ratio: user supplied over subscription ratio 294 :returns: max_over_sub_ratio 295 """ 296 if max_over_sub_ratio < 1.0: 297 LOG.info("The user supplied value for max_over_subscription " 298 "ratio is less than 1.0. Using the default value of " 299 "20.0 instead...") 300 max_over_sub_ratio = 20.0 301 return max_over_sub_ratio 302 303 @staticmethod 304 def _process_tag(element, tag_name): 305 """Process the tag to get the value. 306 307 :param element: the parent element 308 :param tag_name: the tag name 309 :returns: nodeValue(can be None) 310 """ 311 node_value = None 312 try: 313 processed_element = element.getElementsByTagName(tag_name)[0] 314 node_value = processed_element.childNodes[0].nodeValue 315 if node_value: 316 node_value = node_value.strip() 317 except IndexError: 318 pass 319 return node_value 320 321 def _get_connection_info(self, rest_element): 322 """Given the filename get the rest server connection details. 323 324 :param rest_element: the rest element 325 :returns: dict -- connargs - the connection info dictionary 326 :raises: VolumeBackendAPIException 327 """ 328 connargs = { 329 'RestServerIp': ( 330 self._process_tag(rest_element, 'RestServerIp')), 331 'RestServerPort': ( 332 self._process_tag(rest_element, 'RestServerPort')), 333 'RestUserName': ( 334 self._process_tag(rest_element, 'RestUserName')), 335 'RestPassword': ( 336 self._process_tag(rest_element, 'RestPassword'))} 337 338 for k, __ in connargs.items(): 339 if connargs[k] is None: 340 exception_message = (_( 341 "RestServerIp, RestServerPort, RestUserName, " 342 "RestPassword must have valid values.")) 343 LOG.error(exception_message) 344 raise exception.VolumeBackendAPIException( 345 data=exception_message) 346 347 # These can be None 348 connargs['SSLCert'] = self._process_tag(rest_element, 'SSLCert') 349 connargs['SSLVerify'] = ( 350 self._process_tag(rest_element, 'SSLVerify')) 351 352 return connargs 353 354 def parse_file_to_get_array_map(self, file_name): 355 """Parses a file and gets array map. 356 357 Given a file, parse it to get array and pool(srp). 358 359 .. code:: ini 360 361 <EMC> 362 <RestServerIp>10.108.246.202</RestServerIp> 363 <RestServerPort>8443</RestServerPort> 364 <RestUserName>smc</RestUserName> 365 <RestPassword>smc</RestPassword> 366 <SSLCert>/path/client.cert</SSLCert> 367 <SSLVerify>/path/to/certfile.pem</SSLVerify> 368 <PortGroups> 369 <PortGroup>OS-PORTGROUP1-PG</PortGroup> 370 </PortGroups> 371 <Array>000198700439</Array> 372 <SRP>SRP_1</SRP> 373 </EMC> 374 375 :param file_name: the configuration file 376 :returns: list 377 """ 378 LOG.warning("Use of xml file in backend configuration is deprecated " 379 "in Queens and will not be supported in future releases.") 380 kwargs = {} 381 my_file = open(file_name, 'r') 382 data = my_file.read() 383 my_file.close() 384 dom = minidom.parseString(data) 385 try: 386 connargs = self._get_connection_info(dom) 387 portgroup = self._get_random_portgroup(dom) 388 serialnumber = self._process_tag(dom, 'Array') 389 if serialnumber is None: 390 LOG.error("Array Serial Number must be in the file %(file)s.", 391 {'file': file_name}) 392 srp_name = self._process_tag(dom, 'SRP') 393 if srp_name is None: 394 LOG.error("SRP Name must be in the file %(file)s.", 395 {'file': file_name}) 396 slo = self._process_tag(dom, 'ServiceLevel') 397 workload = self._process_tag(dom, 'Workload') 398 kwargs = ( 399 {'RestServerIp': connargs['RestServerIp'], 400 'RestServerPort': connargs['RestServerPort'], 401 'RestUserName': connargs['RestUserName'], 402 'RestPassword': connargs['RestPassword'], 403 'SSLCert': connargs['SSLCert'], 404 'SSLVerify': connargs['SSLVerify'], 405 'SerialNumber': serialnumber, 406 'srpName': srp_name, 407 'PortGroup': portgroup}) 408 if slo is not None: 409 kwargs.update({'ServiceLevel': slo, 'Workload': workload}) 410 411 except IndexError: 412 pass 413 return kwargs 414 415 @staticmethod 416 def _get_random_portgroup(element): 417 """Randomly choose a portgroup from list of portgroups. 418 419 :param element: the parent element 420 :returns: the randomly chosen port group 421 """ 422 portgroupelements = element.getElementsByTagName('PortGroup') 423 if portgroupelements and len(portgroupelements) > 0: 424 portgroupnames = [portgroupelement.childNodes[0].nodeValue.strip() 425 for portgroupelement in portgroupelements 426 if portgroupelement.childNodes] 427 portgroupnames = list(set(filter(None, portgroupnames))) 428 pg_len = len(portgroupnames) 429 if pg_len > 0: 430 return portgroupnames[random.randint(0, pg_len - 1)] 431 return None 432 433 def get_temp_snap_name(self, clone_name, source_device_id): 434 """Construct a temporary snapshot name for clone operation. 435 436 :param clone_name: the name of the clone 437 :param source_device_id: the source device id 438 :returns: snap_name 439 """ 440 trunc_clone = self.truncate_string(clone_name, 10) 441 snap_name = ("temp-%(device)s-%(clone)s" 442 % {'device': source_device_id, 'clone': trunc_clone}) 443 return snap_name 444 445 @staticmethod 446 def get_array_and_device_id(volume, external_ref): 447 """Helper function for manage volume to get array name and device ID. 448 449 :param volume: volume object from API 450 :param external_ref: the existing volume object to be manged 451 :returns: string value of the array name and device ID 452 """ 453 device_id = external_ref.get(u'source-name', None) 454 LOG.debug("External_ref: %(er)s", {'er': external_ref}) 455 if not device_id: 456 device_id = external_ref.get(u'source-id', None) 457 host = volume.host 458 host_list = host.split('+') 459 array = host_list[(len(host_list) - 1)] 460 461 if device_id: 462 LOG.debug("Get device ID of existing volume - device ID: " 463 "%(device_id)s, Array: %(array)s.", 464 {'device_id': device_id, 465 'array': array}) 466 else: 467 exception_message = (_("Source volume device ID is required.")) 468 raise exception.VolumeBackendAPIException( 469 data=exception_message) 470 return array, device_id 471 472 @staticmethod 473 def is_compression_disabled(extra_specs): 474 """Check is compression is to be disabled. 475 476 :param extra_specs: extra specifications 477 :returns: boolean 478 """ 479 do_disable_compression = False 480 if DISABLECOMPRESSION in extra_specs: 481 if strutils.bool_from_string(extra_specs[DISABLECOMPRESSION]): 482 do_disable_compression = True 483 return do_disable_compression 484 485 def change_compression_type(self, is_source_compr_disabled, new_type): 486 """Check if volume type have different compression types 487 488 :param is_source_compr_disabled: from source 489 :param new_type: from target 490 :returns: boolean 491 """ 492 extra_specs = new_type['extra_specs'] 493 is_target_compr_disabled = self.is_compression_disabled(extra_specs) 494 if is_target_compr_disabled == is_source_compr_disabled: 495 return False 496 else: 497 return True 498 499 @staticmethod 500 def is_replication_enabled(extra_specs): 501 """Check if replication is to be enabled. 502 503 :param extra_specs: extra specifications 504 :returns: bool - true if enabled, else false 505 """ 506 replication_enabled = False 507 if IS_RE in extra_specs: 508 replication_enabled = True 509 return replication_enabled 510 511 @staticmethod 512 def get_replication_config(rep_device_list): 513 """Gather necessary replication configuration info. 514 515 :param rep_device_list: the replication device list from cinder.conf 516 :returns: rep_config, replication configuration dict 517 """ 518 rep_config = {} 519 if not rep_device_list: 520 return None 521 else: 522 target = rep_device_list[0] 523 try: 524 rep_config['array'] = target['target_device_id'] 525 rep_config['srp'] = target['remote_pool'] 526 rep_config['rdf_group_label'] = target['rdf_group_label'] 527 rep_config['portgroup'] = target['remote_port_group'] 528 529 except KeyError as ke: 530 error_message = (_("Failed to retrieve all necessary SRDF " 531 "information. Error received: %(ke)s.") % 532 {'ke': six.text_type(ke)}) 533 LOG.exception(error_message) 534 raise exception.VolumeBackendAPIException(data=error_message) 535 536 allow_extend = target.get('allow_extend', 'false') 537 if strutils.bool_from_string(allow_extend): 538 rep_config['allow_extend'] = True 539 else: 540 rep_config['allow_extend'] = False 541 542 rep_mode = target.get('mode', '') 543 if rep_mode.lower() in ['async', 'asynchronous']: 544 rep_config['mode'] = REP_ASYNC 545 elif rep_mode.lower() == 'metro': 546 rep_config['mode'] = REP_METRO 547 metro_bias = target.get('metro_use_bias', 'false') 548 if strutils.bool_from_string(metro_bias): 549 rep_config[METROBIAS] = True 550 else: 551 rep_config[METROBIAS] = False 552 allow_delete_metro = target.get('allow_delete_metro', 'false') 553 if strutils.bool_from_string(allow_delete_metro): 554 rep_config['allow_delete_metro'] = True 555 else: 556 rep_config['allow_delete_metro'] = False 557 else: 558 rep_config['mode'] = REP_SYNC 559 560 return rep_config 561 562 @staticmethod 563 def is_volume_failed_over(volume): 564 """Check if a volume has been failed over. 565 566 :param volume: the volume object 567 :returns: bool 568 """ 569 if volume is not None: 570 if volume.get('replication_status') and ( 571 volume.replication_status == 572 fields.ReplicationStatus.FAILED_OVER): 573 return True 574 return False 575 576 @staticmethod 577 def update_volume_model_updates(volume_model_updates, 578 volumes, group_id, status='available'): 579 """Update the volume model's status and return it. 580 581 :param volume_model_updates: list of volume model update dicts 582 :param volumes: volumes object api 583 :param group_id: consistency group id 584 :param status: string value reflects the status of the member volume 585 :returns: volume_model_updates - updated volumes 586 """ 587 LOG.info("Updating status for group: %(id)s.", {'id': group_id}) 588 if volumes: 589 for volume in volumes: 590 volume_model_updates.append({'id': volume.id, 591 'status': status}) 592 else: 593 LOG.info("No volume found for group: %(cg)s.", {'cg': group_id}) 594 return volume_model_updates 595 596 @staticmethod 597 def get_grp_volume_model_update(volume, volume_dict, group_id): 598 """Create and return the volume model update on creation. 599 600 :param volume: volume object 601 :param volume_dict: the volume dict 602 :param group_id: consistency group id 603 :returns: model_update 604 """ 605 LOG.info("Updating status for group: %(id)s.", {'id': group_id}) 606 model_update = ({'id': volume.id, 'status': 'available', 607 'provider_location': six.text_type(volume_dict)}) 608 return model_update 609 610 @staticmethod 611 def update_extra_specs(extraspecs): 612 """Update extra specs. 613 614 :param extraspecs: the additional info 615 :returns: extraspecs 616 """ 617 try: 618 pool_details = extraspecs['pool_name'].split('+') 619 extraspecs[SLO] = pool_details[0] 620 if len(pool_details) == 4: 621 extraspecs[WORKLOAD] = pool_details[1] 622 extraspecs[SRP] = pool_details[2] 623 extraspecs[ARRAY] = pool_details[3] 624 else: 625 # Assume no workload given in pool name 626 extraspecs[SRP] = pool_details[1] 627 extraspecs[ARRAY] = pool_details[2] 628 extraspecs[WORKLOAD] = 'NONE' 629 except KeyError: 630 LOG.error("Error parsing SLO, workload from" 631 " the provided extra_specs.") 632 return extraspecs 633 634 def get_volume_group_utils(self, group, interval, retries): 635 """Standard utility for generic volume groups. 636 637 :param group: the generic volume group object to be created 638 :param interval: Interval in seconds between retries 639 :param retries: Retry count 640 :returns: array, intervals_retries_dict 641 :raises: VolumeBackendAPIException 642 """ 643 arrays = set() 644 # Check if it is a generic volume group instance 645 if isinstance(group, Group): 646 for volume_type in group.volume_types: 647 extra_specs = self.update_extra_specs(volume_type.extra_specs) 648 arrays.add(extra_specs[ARRAY]) 649 else: 650 msg = (_("Unable to get volume type ids.")) 651 LOG.error(msg) 652 raise exception.VolumeBackendAPIException(data=msg) 653 654 if len(arrays) != 1: 655 if not arrays: 656 msg = (_("Failed to get an array associated with " 657 "volume group: %(groupid)s.") 658 % {'groupid': group.id}) 659 else: 660 msg = (_("There are multiple arrays " 661 "associated with volume group: %(groupid)s.") 662 % {'groupid': group.id}) 663 LOG.error(msg) 664 raise exception.VolumeBackendAPIException(data=msg) 665 array = arrays.pop() 666 intervals_retries_dict = {INTERVAL: interval, RETRIES: retries} 667 return array, intervals_retries_dict 668 669 def update_volume_group_name(self, group): 670 """Format id and name consistency group. 671 672 :param group: the generic volume group object 673 :returns: group_name -- formatted name + id 674 """ 675 group_name = "" 676 if group.name is not None and group.name != group.id: 677 group_name = ( 678 self.truncate_string( 679 group.name, TRUNCATE_27) + "_") 680 681 group_name += group.id 682 return group_name 683 684 @staticmethod 685 def add_legacy_pools(pools): 686 """Add legacy pools to allow extending a volume after upgrade. 687 688 :param pools: the pool list 689 :return: pools - the updated pool list 690 """ 691 extra_pools = [] 692 for pool in pools: 693 if 'none' in pool['pool_name'].lower(): 694 extra_pools.append(pool) 695 for pool in extra_pools: 696 try: 697 slo = pool['pool_name'].split('+')[0] 698 srp = pool['pool_name'].split('+')[2] 699 array = pool['pool_name'].split('+')[3] 700 except IndexError: 701 slo = pool['pool_name'].split('+')[0] 702 srp = pool['pool_name'].split('+')[1] 703 array = pool['pool_name'].split('+')[2] 704 new_pool_name = ('%(slo)s+%(srp)s+%(array)s' 705 % {'slo': slo, 'srp': srp, 'array': array}) 706 new_pool = deepcopy(pool) 707 new_pool['pool_name'] = new_pool_name 708 pools.append(new_pool) 709 return pools 710 711 def check_replication_matched(self, volume, extra_specs): 712 """Check volume type and group type. 713 714 This will make sure they do not conflict with each other. 715 716 :param volume: volume to be checked 717 :param extra_specs: the extra specifications 718 :raises: InvalidInput 719 """ 720 # If volume is not a member of group, skip this check anyway. 721 if not volume.group: 722 return 723 vol_is_re = self.is_replication_enabled(extra_specs) 724 group_is_re = volume.group.is_replicated 725 726 if not (vol_is_re == group_is_re): 727 msg = _('Replication should be enabled or disabled for both ' 728 'volume or group. Volume replication status: ' 729 '%(vol_status)s, group replication status: ' 730 '%(group_status)s') % { 731 'vol_status': vol_is_re, 'group_status': group_is_re} 732 raise exception.InvalidInput(reason=msg) 733 734 @staticmethod 735 def check_rep_status_enabled(group): 736 """Check replication status for group. 737 738 Group status must be enabled before proceeding with certain 739 operations. 740 741 :param group: the group object 742 :raises: InvalidInput 743 """ 744 if group.is_replicated: 745 if group.replication_status != fields.ReplicationStatus.ENABLED: 746 msg = (_('Replication status should be %s for ' 747 'replication-enabled group.') 748 % fields.ReplicationStatus.ENABLED) 749 LOG.error(msg) 750 raise exception.InvalidInput(reason=msg) 751 else: 752 LOG.debug('Replication is not enabled on group %s, ' 753 'skip status check.', group.id) 754 755 @staticmethod 756 def get_replication_prefix(rep_mode): 757 """Get the replication prefix. 758 759 Replication prefix for storage group naming is based on whether it is 760 synchronous, asynchronous, or metro replication mode. 761 762 :param rep_mode: flag to indicate if replication is async 763 :return: prefix 764 """ 765 if rep_mode == REP_ASYNC: 766 prefix = "-RA" 767 elif rep_mode == REP_METRO: 768 prefix = "-RM" 769 else: 770 prefix = "-RE" 771 return prefix 772 773 @staticmethod 774 def get_async_rdf_managed_grp_name(rep_config): 775 """Get the name of the group used for async replication management. 776 777 :param rep_config: the replication configuration 778 :return: group name 779 """ 780 async_grp_name = ("OS-%(rdf)s-%(mode)s-rdf-sg" 781 % {'rdf': rep_config['rdf_group_label'], 782 'mode': rep_config['mode']}) 783 LOG.debug("The async/ metro rdf managed group name is %(name)s", 784 {'name': async_grp_name}) 785 return async_grp_name 786 787 def is_metro_device(self, rep_config, extra_specs): 788 """Determine if a volume is a Metro enabled device. 789 790 :param rep_config: the replication configuration 791 :param extra_specs: the extra specifications 792 :return: bool 793 """ 794 is_metro = (True if self.is_replication_enabled(extra_specs) 795 and rep_config is not None 796 and rep_config['mode'] == REP_METRO else False) 797 return is_metro 798 799 def does_vol_need_rdf_management_group(self, extra_specs): 800 """Determine if a volume is a Metro or Async. 801 802 :param extra_specs: the extra specifications 803 :return: bool 804 """ 805 if (self.is_replication_enabled(extra_specs) and 806 extra_specs.get(REP_MODE, None) in 807 [REP_ASYNC, REP_METRO]): 808 return True 809 return False 810