OpenStack Block Storage (Cinder)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

989 lines
36KB

  1. # Copyright (c) 2017-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. from copy import deepcopy
  16. import datetime
  17. import hashlib
  18. import re
  19. from cinder.objects.group import Group
  20. from oslo_log import log as logging
  21. from oslo_utils import strutils
  22. from oslo_utils import units
  23. import six
  24. from cinder import exception
  25. from cinder.i18n import _
  26. from cinder.objects import fields
  27. from cinder.volume import volume_types
  28. from cinder.volume import volume_utils
  29. LOG = logging.getLogger(__name__)
  30. # SHARED CONSTANTS
  31. ISCSI = 'iscsi'
  32. FC = 'fc'
  33. INTERVAL = 'interval'
  34. RETRIES = 'retries'
  35. VOLUME_ELEMENT_NAME_PREFIX = 'OS-'
  36. VMAX_AFA_MODELS = ['VMAX250F', 'VMAX450F', 'VMAX850F', 'VMAX950F']
  37. MAX_SRP_LENGTH = 16
  38. TRUNCATE_5 = 5
  39. TRUNCATE_27 = 27
  40. UCODE_5978_ELMSR = 221
  41. UCODE_5978 = 5978
  42. ARRAY = 'array'
  43. SLO = 'slo'
  44. WORKLOAD = 'workload'
  45. SRP = 'srp'
  46. PORTGROUPNAME = 'storagetype:portgroupname'
  47. DEVICE_ID = 'device_id'
  48. INITIATOR_CHECK = 'initiator_check'
  49. SG_NAME = 'storagegroup_name'
  50. MV_NAME = 'maskingview_name'
  51. IG_NAME = 'init_group_name'
  52. PARENT_SG_NAME = 'parent_sg_name'
  53. CONNECTOR = 'connector'
  54. VOL_NAME = 'volume_name'
  55. EXTRA_SPECS = 'extra_specs'
  56. HOST_NAME = 'short_host_name'
  57. IS_RE = 'replication_enabled'
  58. DISABLECOMPRESSION = 'storagetype:disablecompression'
  59. REP_SYNC = 'Synchronous'
  60. REP_ASYNC = 'Asynchronous'
  61. REP_METRO = 'Metro'
  62. REP_MODE = 'rep_mode'
  63. RDF_SYNC_STATE = 'synchronized'
  64. RDF_SYNCINPROG_STATE = 'syncinprog'
  65. RDF_CONSISTENT_STATE = 'consistent'
  66. RDF_SUSPENDED_STATE = 'suspended'
  67. RDF_FAILEDOVER_STATE = 'failed over'
  68. RDF_ACTIVE = 'active'
  69. RDF_ACTIVEACTIVE = 'activeactive'
  70. RDF_ACTIVEBIAS = 'activebias'
  71. RDF_CONS_EXEMPT = 'consExempt'
  72. METROBIAS = 'metro_bias'
  73. DEFAULT_PORT = 8443
  74. CLONE_SNAPSHOT_NAME = "snapshot_for_clone"
  75. # Multiattach constants
  76. IS_MULTIATTACH = 'multiattach'
  77. OTHER_PARENT_SG = 'other_parent_sg_name'
  78. FAST_SG = 'fast_managed_sg'
  79. NO_SLO_SG = 'no_slo_sg'
  80. # SG for unmanaged volumes
  81. UNMANAGED_SG = 'OS-Unmanaged'
  82. # Cinder.conf vmax configuration
  83. VMAX_SERVER_IP = 'san_ip'
  84. VMAX_USER_NAME = 'san_login'
  85. VMAX_PASSWORD = 'san_password'
  86. U4P_SERVER_PORT = 'san_api_port'
  87. VMAX_ARRAY = 'vmax_array'
  88. VMAX_WORKLOAD = 'vmax_workload'
  89. VMAX_SRP = 'vmax_srp'
  90. VMAX_SERVICE_LEVEL = 'vmax_service_level'
  91. VMAX_PORT_GROUPS = 'vmax_port_groups'
  92. VMAX_SNAPVX_UNLINK_LIMIT = 'vmax_snapvx_unlink_limit'
  93. U4P_FAILOVER_TIMEOUT = 'u4p_failover_timeout'
  94. U4P_FAILOVER_RETRIES = 'u4p_failover_retries'
  95. U4P_FAILOVER_BACKOFF_FACTOR = 'u4p_failover_backoff_factor'
  96. U4P_FAILOVER_AUTOFAILBACK = 'u4p_failover_autofailback'
  97. U4P_FAILOVER_TARGETS = 'u4p_failover_target'
  98. POWERMAX_ARRAY = 'powermax_array'
  99. POWERMAX_SRP = 'powermax_srp'
  100. POWERMAX_SERVICE_LEVEL = 'powermax_service_level'
  101. POWERMAX_PORT_GROUPS = 'powermax_port_groups'
  102. POWERMAX_SNAPVX_UNLINK_LIMIT = 'powermax_snapvx_unlink_limit'
  103. class PowerMaxUtils(object):
  104. """Utility class for Rest based PowerMax volume drivers.
  105. This Utility class is for PowerMax volume drivers based on Unisphere
  106. Rest API.
  107. """
  108. def __init__(self):
  109. """Utility class for Rest based PowerMax volume drivers."""
  110. def get_host_short_name(self, host_name):
  111. """Returns the short name for a given qualified host name.
  112. Checks the host name to see if it is the fully qualified host name
  113. and returns part before the dot. If there is no dot in the host name
  114. the full host name is returned.
  115. :param host_name: the fully qualified host name
  116. :returns: string -- the short host_name
  117. """
  118. host_array = host_name.split('.')
  119. if len(host_array) > 1:
  120. short_host_name = host_array[0]
  121. else:
  122. short_host_name = host_name
  123. return self.generate_unique_trunc_host(short_host_name)
  124. @staticmethod
  125. def get_volumetype_extra_specs(volume, volume_type_id=None):
  126. """Gets the extra specs associated with a volume type.
  127. :param volume: the volume dictionary
  128. :param volume_type_id: Optional override for volume.volume_type_id
  129. :returns: dict -- extra_specs - the extra specs
  130. :raises: VolumeBackendAPIException
  131. """
  132. extra_specs = {}
  133. try:
  134. if volume_type_id:
  135. type_id = volume_type_id
  136. else:
  137. type_id = volume.volume_type_id
  138. if type_id is not None:
  139. extra_specs = volume_types.get_volume_type_extra_specs(type_id)
  140. except Exception as e:
  141. LOG.debug('Exception getting volume type extra specs: %(e)s',
  142. {'e': six.text_type(e)})
  143. return extra_specs
  144. @staticmethod
  145. def get_short_protocol_type(protocol):
  146. """Given the protocol type, return I for iscsi and F for fc.
  147. :param protocol: iscsi or fc
  148. :returns: string -- 'I' for iscsi or 'F' for fc
  149. """
  150. if protocol.lower() == ISCSI.lower():
  151. return 'I'
  152. elif protocol.lower() == FC.lower():
  153. return 'F'
  154. else:
  155. return protocol
  156. @staticmethod
  157. def truncate_string(str_to_truncate, max_num):
  158. """Truncate a string by taking first and last characters.
  159. :param str_to_truncate: the string to be truncated
  160. :param max_num: the maximum number of characters
  161. :returns: string -- truncated string or original string
  162. """
  163. if len(str_to_truncate) > max_num:
  164. new_num = len(str_to_truncate) - max_num // 2
  165. first_chars = str_to_truncate[:max_num // 2]
  166. last_chars = str_to_truncate[new_num:]
  167. str_to_truncate = first_chars + last_chars
  168. return str_to_truncate
  169. @staticmethod
  170. def get_time_delta(start_time, end_time):
  171. """Get the delta between start and end time.
  172. :param start_time: the start time
  173. :param end_time: the end time
  174. :returns: string -- delta in string H:MM:SS
  175. """
  176. delta = end_time - start_time
  177. return six.text_type(datetime.timedelta(seconds=int(delta)))
  178. def get_default_storage_group_name(
  179. self, srp_name, slo, workload, is_compression_disabled=False,
  180. is_re=False, rep_mode=None):
  181. """Determine default storage group from extra_specs.
  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. if is_compression_disabled:
  195. prefix += "-CD"
  196. else:
  197. prefix = "OS-no_SLO"
  198. if is_re:
  199. prefix += self.get_replication_prefix(rep_mode)
  200. storage_group_name = ("%(prefix)s-SG" % {'prefix': prefix})
  201. return storage_group_name
  202. @staticmethod
  203. def get_volume_element_name(volume_id):
  204. """Get volume element name follows naming convention, i.e. 'OS-UUID'.
  205. :param volume_id: Openstack volume ID containing uuid
  206. :returns: volume element name in format of OS-UUID
  207. """
  208. element_name = volume_id
  209. uuid_regex = (re.compile(
  210. r'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}',
  211. re.I))
  212. match = uuid_regex.search(volume_id)
  213. if match:
  214. volume_uuid = match.group()
  215. element_name = ("%(prefix)s%(volumeUUID)s"
  216. % {'prefix': VOLUME_ELEMENT_NAME_PREFIX,
  217. 'volumeUUID': volume_uuid})
  218. LOG.debug(
  219. "get_volume_element_name elementName: %(elementName)s.",
  220. {'elementName': element_name})
  221. return element_name
  222. @staticmethod
  223. def modify_snapshot_prefix(snapshot_name, manage=False, unmanage=False):
  224. """Modify a Snapshot prefix on PowerMax/VMAX backend.
  225. Prepare a snapshot name for manage/unmanage snapshot process either
  226. by adding or removing 'OS-' prefix.
  227. :param snapshot_name: the old snapshot backend display name
  228. :param manage: (bool) if the operation is managing a snapshot
  229. :param unmanage: (bool) if the operation is unmanaging a snapshot
  230. :return: snapshot name ready for backend PowerMax/VMAX assignment
  231. """
  232. new_snap_name = None
  233. if manage:
  234. new_snap_name = ("%(prefix)s%(snapshot_name)s"
  235. % {'prefix': 'OS-',
  236. 'snapshot_name': snapshot_name})
  237. if unmanage:
  238. snap_split = snapshot_name.split("-", 1)
  239. if snap_split[0] == 'OS':
  240. new_snap_name = snap_split[1]
  241. return new_snap_name
  242. def generate_unique_trunc_host(self, host_name):
  243. """Create a unique short host name under 16 characters.
  244. :param host_name: long host name
  245. :returns: truncated host name
  246. """
  247. if host_name and len(host_name) > 16:
  248. host_name = host_name.lower()
  249. m = hashlib.md5()
  250. m.update(host_name.encode('utf-8'))
  251. uuid = m.hexdigest()
  252. new_name = ("%(host)s%(uuid)s"
  253. % {'host': host_name[-6:],
  254. 'uuid': uuid})
  255. host_name = self.truncate_string(new_name, 16)
  256. return host_name
  257. def get_pg_short_name(self, portgroup_name):
  258. """Create a unique port group name under 12 characters.
  259. :param portgroup_name: long portgroup_name
  260. :returns: truncated portgroup_name
  261. """
  262. if portgroup_name and len(portgroup_name) > 12:
  263. portgroup_name = portgroup_name.lower()
  264. m = hashlib.md5()
  265. m.update(portgroup_name.encode('utf-8'))
  266. uuid = m.hexdigest()
  267. new_name = ("%(pg)s%(uuid)s"
  268. % {'pg': portgroup_name[-6:],
  269. 'uuid': uuid})
  270. portgroup_name = self.truncate_string(new_name, 12)
  271. return portgroup_name
  272. @staticmethod
  273. def get_default_oversubscription_ratio(max_over_sub_ratio):
  274. """Override ratio if necessary.
  275. The over subscription ratio will be overridden if the user supplied
  276. max oversubscription ratio is less than 1.
  277. :param max_over_sub_ratio: user supplied over subscription ratio
  278. :returns: max_over_sub_ratio
  279. """
  280. if max_over_sub_ratio < 1.0:
  281. LOG.info("The user supplied value for max_over_subscription "
  282. "ratio is less than 1.0. Using the default value of "
  283. "20.0 instead...")
  284. max_over_sub_ratio = 20.0
  285. return max_over_sub_ratio
  286. def get_temp_snap_name(self, source_device_id):
  287. """Construct a temporary snapshot name for clone operation
  288. :param source_device_id: the source device id
  289. :return: snap_name
  290. """
  291. snap_name = ("temp-%(device)s-%(snap_name)s"
  292. % {'device': source_device_id,
  293. 'snap_name': CLONE_SNAPSHOT_NAME})
  294. return snap_name
  295. @staticmethod
  296. def get_array_and_device_id(volume, external_ref):
  297. """Helper function for manage volume to get array name and device ID.
  298. :param volume: volume object from API
  299. :param external_ref: the existing volume object to be manged
  300. :returns: string value of the array name and device ID
  301. """
  302. device_id = external_ref.get(u'source-name', None)
  303. LOG.debug("External_ref: %(er)s", {'er': external_ref})
  304. if not device_id:
  305. device_id = external_ref.get(u'source-id', None)
  306. host = volume.host
  307. host_list = host.split('+')
  308. array = host_list[(len(host_list) - 1)]
  309. if device_id:
  310. if len(device_id) != 5:
  311. error_message = (_("Device ID: %(device_id)s is invalid. "
  312. "Device ID should be exactly 5 digits.") %
  313. {'device_id': device_id})
  314. LOG.error(error_message)
  315. raise exception.VolumeBackendAPIException(
  316. message=error_message)
  317. LOG.debug("Get device ID of existing volume - device ID: "
  318. "%(device_id)s, Array: %(array)s.",
  319. {'device_id': device_id,
  320. 'array': array})
  321. else:
  322. exception_message = (_("Source volume device ID is required."))
  323. raise exception.VolumeBackendAPIException(
  324. message=exception_message)
  325. return array, device_id.upper()
  326. @staticmethod
  327. def is_compression_disabled(extra_specs):
  328. """Check is compression is to be disabled.
  329. :param extra_specs: extra specifications
  330. :returns: boolean
  331. """
  332. do_disable_compression = False
  333. if (DISABLECOMPRESSION in extra_specs and strutils.bool_from_string(
  334. extra_specs[DISABLECOMPRESSION])) or not extra_specs.get(SLO):
  335. do_disable_compression = True
  336. return do_disable_compression
  337. def change_compression_type(self, is_source_compr_disabled, new_type):
  338. """Check if volume type have different compression types
  339. :param is_source_compr_disabled: from source
  340. :param new_type: from target
  341. :returns: boolean
  342. """
  343. extra_specs = new_type['extra_specs']
  344. is_target_compr_disabled = self.is_compression_disabled(extra_specs)
  345. if is_target_compr_disabled == is_source_compr_disabled:
  346. return False
  347. else:
  348. return True
  349. def change_replication(self, vol_is_replicated, new_type):
  350. """Check if volume types have different replication status.
  351. :param vol_is_replicated: from source
  352. :param new_type: from target
  353. :return: bool
  354. """
  355. is_tgt_rep = self.is_replication_enabled(new_type['extra_specs'])
  356. return vol_is_replicated != is_tgt_rep
  357. @staticmethod
  358. def is_replication_enabled(extra_specs):
  359. """Check if replication is to be enabled.
  360. :param extra_specs: extra specifications
  361. :returns: bool - true if enabled, else false
  362. """
  363. replication_enabled = False
  364. if IS_RE in extra_specs:
  365. replication_enabled = True
  366. return replication_enabled
  367. @staticmethod
  368. def get_replication_config(rep_device_list):
  369. """Gather necessary replication configuration info.
  370. :param rep_device_list: the replication device list from cinder.conf
  371. :returns: rep_config, replication configuration dict
  372. """
  373. rep_config = {}
  374. if not rep_device_list:
  375. return None
  376. else:
  377. target = rep_device_list[0]
  378. try:
  379. rep_config['array'] = target['target_device_id']
  380. rep_config['srp'] = target['remote_pool']
  381. rep_config['rdf_group_label'] = target['rdf_group_label']
  382. rep_config['portgroup'] = target['remote_port_group']
  383. except KeyError as ke:
  384. error_message = (_("Failed to retrieve all necessary SRDF "
  385. "information. Error received: %(ke)s.") %
  386. {'ke': six.text_type(ke)})
  387. LOG.exception(error_message)
  388. raise exception.VolumeBackendAPIException(
  389. message=error_message)
  390. allow_extend = target.get('allow_extend', 'false')
  391. if strutils.bool_from_string(allow_extend):
  392. rep_config['allow_extend'] = True
  393. else:
  394. rep_config['allow_extend'] = False
  395. rep_mode = target.get('mode', '')
  396. if rep_mode.lower() in ['async', 'asynchronous']:
  397. rep_config['mode'] = REP_ASYNC
  398. elif rep_mode.lower() == 'metro':
  399. rep_config['mode'] = REP_METRO
  400. metro_bias = target.get('metro_use_bias', 'false')
  401. if strutils.bool_from_string(metro_bias):
  402. rep_config[METROBIAS] = True
  403. else:
  404. rep_config[METROBIAS] = False
  405. allow_delete_metro = target.get('allow_delete_metro', 'false')
  406. if strutils.bool_from_string(allow_delete_metro):
  407. rep_config['allow_delete_metro'] = True
  408. else:
  409. rep_config['allow_delete_metro'] = False
  410. else:
  411. rep_config['mode'] = REP_SYNC
  412. return rep_config
  413. @staticmethod
  414. def is_volume_failed_over(volume):
  415. """Check if a volume has been failed over.
  416. :param volume: the volume object
  417. :returns: bool
  418. """
  419. if volume is not None:
  420. if volume.get('replication_status') and (
  421. volume.replication_status ==
  422. fields.ReplicationStatus.FAILED_OVER):
  423. return True
  424. return False
  425. @staticmethod
  426. def update_volume_model_updates(volume_model_updates,
  427. volumes, group_id, status='available'):
  428. """Update the volume model's status and return it.
  429. :param volume_model_updates: list of volume model update dicts
  430. :param volumes: volumes object api
  431. :param group_id: consistency group id
  432. :param status: string value reflects the status of the member volume
  433. :returns: volume_model_updates - updated volumes
  434. """
  435. LOG.info("Updating status for group: %(id)s.", {'id': group_id})
  436. if volumes:
  437. for volume in volumes:
  438. volume_model_updates.append({'id': volume.id,
  439. 'status': status})
  440. else:
  441. LOG.info("No volume found for group: %(cg)s.", {'cg': group_id})
  442. return volume_model_updates
  443. @staticmethod
  444. def get_grp_volume_model_update(volume, volume_dict, group_id, meta=None):
  445. """Create and return the volume model update on creation.
  446. :param volume: volume object
  447. :param volume_dict: the volume dict
  448. :param group_id: consistency group id
  449. :param meta: the volume metadata
  450. :returns: model_update
  451. """
  452. LOG.info("Updating status for group: %(id)s.", {'id': group_id})
  453. model_update = ({'id': volume.id, 'status': 'available',
  454. 'provider_location': six.text_type(volume_dict)})
  455. if meta:
  456. model_update['metadata'] = meta
  457. return model_update
  458. @staticmethod
  459. def update_extra_specs(extraspecs):
  460. """Update extra specs.
  461. :param extraspecs: the additional info
  462. :returns: extraspecs
  463. """
  464. try:
  465. pool_details = extraspecs['pool_name'].split('+')
  466. extraspecs[SLO] = pool_details[0]
  467. if len(pool_details) == 4:
  468. extraspecs[WORKLOAD] = pool_details[1]
  469. extraspecs[SRP] = pool_details[2]
  470. extraspecs[ARRAY] = pool_details[3]
  471. else:
  472. # Assume no workload given in pool name
  473. extraspecs[SRP] = pool_details[1]
  474. extraspecs[ARRAY] = pool_details[2]
  475. extraspecs[WORKLOAD] = 'NONE'
  476. except KeyError:
  477. LOG.error("Error parsing SLO, workload from"
  478. " the provided extra_specs.")
  479. return extraspecs
  480. def get_volume_group_utils(self, group, interval, retries):
  481. """Standard utility for generic volume groups.
  482. :param group: the generic volume group object to be created
  483. :param interval: Interval in seconds between retries
  484. :param retries: Retry count
  485. :returns: array, intervals_retries_dict
  486. :raises: VolumeBackendAPIException
  487. """
  488. arrays = set()
  489. # Check if it is a generic volume group instance
  490. if isinstance(group, Group):
  491. for volume_type in group.volume_types:
  492. extra_specs = self.update_extra_specs(volume_type.extra_specs)
  493. arrays.add(extra_specs[ARRAY])
  494. else:
  495. msg = (_("Unable to get volume type ids."))
  496. LOG.error(msg)
  497. raise exception.VolumeBackendAPIException(message=msg)
  498. if len(arrays) != 1:
  499. if not arrays:
  500. msg = (_("Failed to get an array associated with "
  501. "volume group: %(groupid)s.")
  502. % {'groupid': group.id})
  503. else:
  504. msg = (_("There are multiple arrays "
  505. "associated with volume group: %(groupid)s.")
  506. % {'groupid': group.id})
  507. LOG.error(msg)
  508. raise exception.VolumeBackendAPIException(message=msg)
  509. array = arrays.pop()
  510. intervals_retries_dict = {INTERVAL: interval, RETRIES: retries}
  511. return array, intervals_retries_dict
  512. def update_volume_group_name(self, group):
  513. """Format id and name consistency group.
  514. :param group: the generic volume group object
  515. :returns: group_name -- formatted name + id
  516. """
  517. group_name = ""
  518. if group.name is not None and group.name != group.id:
  519. group_name = (
  520. self.truncate_string(
  521. group.name, TRUNCATE_27) + "_")
  522. group_name += group.id
  523. return group_name
  524. @staticmethod
  525. def add_legacy_pools(pools):
  526. """Add legacy pools to allow extending a volume after upgrade.
  527. :param pools: the pool list
  528. :return: pools - the updated pool list
  529. """
  530. extra_pools = []
  531. for pool in pools:
  532. if 'none' in pool['pool_name'].lower():
  533. extra_pools.append(pool)
  534. for pool in extra_pools:
  535. try:
  536. slo = pool['pool_name'].split('+')[0]
  537. srp = pool['pool_name'].split('+')[2]
  538. array = pool['pool_name'].split('+')[3]
  539. except IndexError:
  540. slo = pool['pool_name'].split('+')[0]
  541. srp = pool['pool_name'].split('+')[1]
  542. array = pool['pool_name'].split('+')[2]
  543. new_pool_name = ('%(slo)s+%(srp)s+%(array)s'
  544. % {'slo': slo, 'srp': srp, 'array': array})
  545. new_pool = deepcopy(pool)
  546. new_pool['pool_name'] = new_pool_name
  547. pools.append(new_pool)
  548. return pools
  549. def check_replication_matched(self, volume, extra_specs):
  550. """Check volume type and group type.
  551. This will make sure they do not conflict with each other.
  552. :param volume: volume to be checked
  553. :param extra_specs: the extra specifications
  554. :raises: InvalidInput
  555. """
  556. # If volume is not a member of group, skip this check anyway.
  557. if not volume.group:
  558. return
  559. vol_is_re = self.is_replication_enabled(extra_specs)
  560. group_is_re = volume.group.is_replicated
  561. if not (vol_is_re == group_is_re):
  562. msg = _('Replication should be enabled or disabled for both '
  563. 'volume or group. Volume replication status: '
  564. '%(vol_status)s, group replication status: '
  565. '%(group_status)s') % {
  566. 'vol_status': vol_is_re, 'group_status': group_is_re}
  567. raise exception.InvalidInput(reason=msg)
  568. @staticmethod
  569. def check_rep_status_enabled(group):
  570. """Check replication status for group.
  571. Group status must be enabled before proceeding with certain
  572. operations.
  573. :param group: the group object
  574. :raises: InvalidInput
  575. """
  576. if group.is_replicated:
  577. if group.replication_status != fields.ReplicationStatus.ENABLED:
  578. msg = (_('Replication status should be %s for '
  579. 'replication-enabled group.')
  580. % fields.ReplicationStatus.ENABLED)
  581. LOG.error(msg)
  582. raise exception.InvalidInput(reason=msg)
  583. else:
  584. LOG.debug('Replication is not enabled on group %s, '
  585. 'skip status check.', group.id)
  586. @staticmethod
  587. def get_replication_prefix(rep_mode):
  588. """Get the replication prefix.
  589. Replication prefix for storage group naming is based on whether it is
  590. synchronous, asynchronous, or metro replication mode.
  591. :param rep_mode: flag to indicate if replication is async
  592. :return: prefix
  593. """
  594. if rep_mode == REP_ASYNC:
  595. prefix = "-RA"
  596. elif rep_mode == REP_METRO:
  597. prefix = "-RM"
  598. else:
  599. prefix = "-RE"
  600. return prefix
  601. @staticmethod
  602. def get_async_rdf_managed_grp_name(rep_config):
  603. """Get the name of the group used for async replication management.
  604. :param rep_config: the replication configuration
  605. :return: group name
  606. """
  607. async_grp_name = ("OS-%(rdf)s-%(mode)s-rdf-sg"
  608. % {'rdf': rep_config['rdf_group_label'],
  609. 'mode': rep_config['mode']})
  610. LOG.debug("The async/ metro rdf managed group name is %(name)s",
  611. {'name': async_grp_name})
  612. return async_grp_name
  613. def is_metro_device(self, rep_config, extra_specs):
  614. """Determine if a volume is a Metro enabled device.
  615. :param rep_config: the replication configuration
  616. :param extra_specs: the extra specifications
  617. :return: bool
  618. """
  619. is_metro = (True if self.is_replication_enabled(extra_specs)
  620. and rep_config is not None
  621. and rep_config['mode'] == REP_METRO else False)
  622. return is_metro
  623. def does_vol_need_rdf_management_group(self, extra_specs):
  624. """Determine if a volume is a Metro or Async.
  625. :param extra_specs: the extra specifications
  626. :return: bool
  627. """
  628. if (self.is_replication_enabled(extra_specs) and
  629. extra_specs.get(REP_MODE, None) in
  630. [REP_ASYNC, REP_METRO]):
  631. return True
  632. return False
  633. def derive_default_sg_from_extra_specs(self, extra_specs, rep_mode=None):
  634. """Get the name of the default sg from the extra specs.
  635. :param extra_specs: extra specs
  636. :returns: default sg - string
  637. """
  638. do_disable_compression = self.is_compression_disabled(
  639. extra_specs)
  640. rep_enabled = self.is_replication_enabled(extra_specs)
  641. return self.get_default_storage_group_name(
  642. extra_specs[SRP], extra_specs[SLO],
  643. extra_specs[WORKLOAD],
  644. is_compression_disabled=do_disable_compression,
  645. is_re=rep_enabled, rep_mode=rep_mode)
  646. @staticmethod
  647. def merge_dicts(d1, *args):
  648. """Merge dictionaries
  649. :param d1: dict 1
  650. :param *args: one or more dicts
  651. :returns: merged dict
  652. """
  653. d2 = {}
  654. for d in args:
  655. d2 = d.copy()
  656. d2.update(d1)
  657. d1 = d2
  658. return d2
  659. @staticmethod
  660. def get_temp_failover_grp_name(rep_config):
  661. """Get the temporary group name used for failover.
  662. :param rep_config: the replication config
  663. :return: temp_grp_name
  664. """
  665. temp_grp_name = ("OS-%(rdf)s-temp-rdf-sg"
  666. % {'rdf': rep_config['rdf_group_label']})
  667. LOG.debug("The temp rdf managed group name is %(name)s",
  668. {'name': temp_grp_name})
  669. return temp_grp_name
  670. def get_child_sg_name(self, host_name, extra_specs):
  671. """Get the child storage group name for a masking view.
  672. :param host_name: the short host name
  673. :param extra_specs: the extra specifications
  674. :return: child sg name, compression flag, rep flag, short pg name
  675. """
  676. do_disable_compression = False
  677. pg_name = self.get_pg_short_name(extra_specs[PORTGROUPNAME])
  678. rep_enabled = self.is_replication_enabled(extra_specs)
  679. if extra_specs[SLO]:
  680. slo_wl_combo = self.truncate_string(
  681. extra_specs[SLO] + extra_specs[WORKLOAD], 10)
  682. unique_name = self.truncate_string(extra_specs[SRP], 12)
  683. child_sg_name = (
  684. "OS-%(shortHostName)s-%(srpName)s-%(combo)s-%(pg)s"
  685. % {'shortHostName': host_name,
  686. 'srpName': unique_name,
  687. 'combo': slo_wl_combo,
  688. 'pg': pg_name})
  689. do_disable_compression = self.is_compression_disabled(
  690. extra_specs)
  691. if do_disable_compression:
  692. child_sg_name = ("%(child_sg_name)s-CD"
  693. % {'child_sg_name': child_sg_name})
  694. else:
  695. child_sg_name = (
  696. "OS-%(shortHostName)s-No_SLO-%(pg)s"
  697. % {'shortHostName': host_name, 'pg': pg_name})
  698. if rep_enabled:
  699. rep_mode = extra_specs.get(REP_MODE, None)
  700. child_sg_name += self.get_replication_prefix(rep_mode)
  701. return child_sg_name, do_disable_compression, rep_enabled, pg_name
  702. @staticmethod
  703. def change_multiattach(extra_specs, new_type_extra_specs):
  704. """Check if a change in multiattach is required for retype.
  705. :param extra_specs: the source type extra specs
  706. :param new_type_extra_specs: the target type extra specs
  707. :return: bool
  708. """
  709. is_src_multiattach = volume_utils.is_boolean_str(
  710. extra_specs.get('multiattach'))
  711. is_tgt_multiattach = volume_utils.is_boolean_str(
  712. new_type_extra_specs.get('multiattach'))
  713. return is_src_multiattach != is_tgt_multiattach
  714. @staticmethod
  715. def is_volume_manageable(source_vol):
  716. """Check if a volume with verbose description is valid for management.
  717. :param source_vol: the verbose volume dict
  718. :return: bool True/False
  719. """
  720. vol_head = source_vol['volumeHeader']
  721. # PowerMax/VMAX disk geometry uses cylinders, so volume sizes are
  722. # matched to the nearest full cylinder size: 1GB = 547cyl = 1026MB
  723. if vol_head['capMB'] < 1026 or not vol_head['capGB'].is_integer():
  724. return False
  725. if (vol_head['numSymDevMaskingViews'] > 0 or
  726. vol_head['mapped'] is True or
  727. source_vol['maskingInfo']['masked'] is True):
  728. return False
  729. if (vol_head['status'] != 'Ready' or
  730. vol_head['serviceState'] != 'Normal' or
  731. vol_head['emulationType'] != 'FBA' or
  732. vol_head['configuration'] != 'TDEV' or
  733. vol_head['system_resource'] is True or
  734. vol_head['private'] is True or
  735. vol_head['encapsulated'] is True or
  736. vol_head['reservationInfo']['reserved'] is True):
  737. return False
  738. for key, value in source_vol['rdfInfo'].items():
  739. if value is True:
  740. return False
  741. if source_vol['timeFinderInfo']['snapVXTgt'] is True:
  742. return False
  743. if vol_head['nameModifier'][0:3] == 'OS-':
  744. return False
  745. return True
  746. @staticmethod
  747. def is_snapshot_manageable(source_vol):
  748. """Check if a volume with snapshot description is valid for management.
  749. :param source_vol: the verbose volume dict
  750. :return: bool True/False
  751. """
  752. vol_head = source_vol['volumeHeader']
  753. if not source_vol['timeFinderInfo']['snapVXSrc']:
  754. return False
  755. # PowerMax/VMAX disk geometry uses cylinders, so volume sizes are
  756. # matched to the nearest full cylinder size: 1GB = 547cyl = 1026MB
  757. if (vol_head['capMB'] < 1026 or
  758. not vol_head['capGB'].is_integer()):
  759. return False
  760. if (vol_head['emulationType'] != 'FBA' or
  761. vol_head['configuration'] != 'TDEV' or
  762. vol_head['private'] is True or
  763. vol_head['system_resource'] is True):
  764. return False
  765. snap_gen_info = (source_vol['timeFinderInfo']['snapVXSession'][0][
  766. 'srcSnapshotGenInfo'][0]['snapshotHeader'])
  767. if (snap_gen_info['snapshotName'][0:3] == 'OS-' or
  768. snap_gen_info['snapshotName'][0:5] == 'temp-'):
  769. return False
  770. if (snap_gen_info['expired'] is True
  771. or snap_gen_info['generation'] > 0):
  772. return False
  773. return True
  774. @staticmethod
  775. def get_volume_attached_hostname(device_info):
  776. """Parse a hostname from a storage group ID.
  777. :param device_info: the device info dict
  778. :return: str -- the attached hostname
  779. """
  780. try:
  781. sg_id = device_info.get("storageGroupId")[0]
  782. return sg_id.split('-')[1]
  783. except IndexError:
  784. return None
  785. @staticmethod
  786. def validate_qos_input(input_key, sg_value, qos_extra_spec, property_dict):
  787. max_value = 100000
  788. qos_unit = "IO/Sec"
  789. if input_key == 'total_iops_sec':
  790. min_value = 100
  791. input_value = int(qos_extra_spec['total_iops_sec'])
  792. sg_key = 'host_io_limit_io_sec'
  793. else:
  794. qos_unit = "MB/sec"
  795. min_value = 1
  796. input_value = int(
  797. int(qos_extra_spec['total_bytes_sec']) / units.Mi)
  798. sg_key = 'host_io_limit_mb_sec'
  799. if min_value <= input_value <= max_value:
  800. if sg_value is None or input_value != int(sg_value):
  801. property_dict[sg_key] = input_value
  802. else:
  803. exception_message = (
  804. _("Invalid %(ds)s with value %(dt)s entered. Valid values "
  805. "range from %(du)s %(dv)s to 100,000 %(dv)s") % {
  806. 'ds': input_key, 'dt': input_value, 'du': min_value,
  807. 'dv': qos_unit})
  808. LOG.error(exception_message)
  809. raise exception.VolumeBackendAPIException(
  810. message=exception_message)
  811. return property_dict
  812. @staticmethod
  813. def validate_qos_distribution_type(
  814. sg_value, qos_extra_spec, property_dict):
  815. dynamic_list = ['never', 'onfailure', 'always']
  816. if qos_extra_spec.get('DistributionType').lower() in dynamic_list:
  817. distribution_type = qos_extra_spec['DistributionType']
  818. if distribution_type != sg_value:
  819. property_dict["dynamicDistribution"] = distribution_type
  820. else:
  821. exception_message = (
  822. _("Wrong Distribution type value %(dt)s entered. Please "
  823. "enter one of: %(dl)s") % {
  824. 'dt': qos_extra_spec.get('DistributionType'),
  825. 'dl': dynamic_list})
  826. LOG.error(exception_message)
  827. raise exception.VolumeBackendAPIException(
  828. message=exception_message)
  829. return property_dict
  830. @staticmethod
  831. def compare_cylinders(cylinders_source, cylinder_target):
  832. """Compare number of cylinders of source and target.
  833. :param cylinders_source: number of cylinders on source
  834. :param cylinders_target: number of cylinders on target
  835. """
  836. if float(cylinders_source) > float(cylinder_target):
  837. exception_message = (
  838. _("The number of source cylinders %(cylinders_source)s "
  839. "cannot be greater than the number of target cylinders "
  840. "%(cylinder_target)s. Please extend your source volume by "
  841. "at least 1GiB.") % {
  842. 'cylinders_source': cylinders_source,
  843. 'cylinder_target': cylinder_target})
  844. raise exception.VolumeBackendAPIException(
  845. message=exception_message)
  846. @staticmethod
  847. def get_service_level_workload(extra_specs):
  848. """Get the service level and workload combination from extra specs.
  849. :param extra_specs: extra specifications
  850. :return: string, string
  851. """
  852. service_level, workload = 'None', 'None'
  853. if extra_specs.get(SLO):
  854. service_level = extra_specs.get(SLO)
  855. if (extra_specs.get(WORKLOAD)
  856. and 'NONE' not in extra_specs.get(WORKLOAD)):
  857. workload = extra_specs.get(WORKLOAD)
  858. return service_level, workload