Coverage for manila/share/drivers/netapp/utils.py: 93%
227 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright (c) 2015 Bob Callaway. All rights reserved.
2# Copyright (c) 2015 Tom Barron. All rights reserved.
3# Copyright (c) 2015 Clinton Knight. All rights reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16"""Utilities for NetApp drivers."""
18from collections import abc
19import decimal
20import platform
21import re
23from oslo_concurrency import processutils as putils
24from oslo_log import log
25from oslo_utils import timeutils
26from oslo_utils import units
28from manila import exception
29from manila.i18n import _
30from manila import version
33LOG = log.getLogger(__name__)
35VALID_TRACE_FLAGS = ['method', 'api']
36TRACE_METHOD = False
37TRACE_API = False
38API_TRACE_PATTERN = '(.*)'
39SVM_MIGRATE_POLICY_TYPE_NAME = 'migrate'
40MIGRATION_OPERATION_ID_KEY = 'migration_operation_id'
41MIGRATION_STATE_READY_FOR_CUTOVER = 'ready_for_cutover'
42MIGRATION_STATE_READY_FOR_SOURCE_CLEANUP = 'ready_for_source_cleanup'
43MIGRATION_STATE_MIGRATE_COMPLETE = 'migrate_complete'
44MIGRATION_STATE_MIGRATE_PAUSED = 'migrate_paused'
46EXTENDED_DATA_PROTECTION_TYPE = 'extended_data_protection'
47MIRROR_ALL_SNAP_POLICY = 'MirrorAllSnapshots'
48DATA_PROTECTION_TYPE = 'data_protection'
50FLEXGROUP_STYLE_EXTENDED = 'flexgroup'
51FLEXVOL_STYLE_EXTENDED = 'flexvol'
53FLEXGROUP_DEFAULT_POOL_NAME = 'flexgroup_auto'
55ABSOLUTE_MAX_INODES = 2_040_109_45
57CLONE_SPLIT_STATUS_ONGOING = 'ongoing'
58CLONE_SPLIT_STATUS_UNKNOWN = 'unknown'
59CLONE_SPLIT_STATUS_FINISHED = 'finished'
62class NetAppDriverException(exception.ShareBackendException):
63 message = _("NetApp Manila Driver exception.")
66def validate_driver_instantiation(**kwargs):
67 """Checks if a driver is instantiated other than by the unified driver.
69 Helps check direct instantiation of netapp drivers.
70 Call this function in every netapp block driver constructor.
71 """
72 if kwargs and kwargs.get('netapp_mode') == 'proxy':
73 return
74 LOG.warning('Please use NetAppDriver in the configuration file '
75 'to load the driver instead of directly specifying '
76 'the driver module name.')
79def check_flags(required_flags, configuration):
80 """Ensure that the flags we care about are set."""
81 for flag in required_flags:
82 if getattr(configuration, flag, None) is None:
83 msg = _('Configuration value %s is not set.') % flag
84 raise exception.InvalidInput(reason=msg)
87def round_down(value, precision='0.00'):
88 """Round a number downward using a specified level of precision.
90 Example: round_down(float(total_space_in_bytes) / units.Gi, '0.01')
91 """
92 return float(decimal.Decimal(str(value)).quantize(
93 decimal.Decimal(precision), rounding=decimal.ROUND_DOWN))
96def setup_tracing(trace_flags_string, api_trace_pattern=API_TRACE_PATTERN):
97 global TRACE_METHOD
98 global TRACE_API
99 global API_TRACE_PATTERN
100 TRACE_METHOD = False
101 TRACE_API = False
102 API_TRACE_PATTERN = api_trace_pattern
103 if trace_flags_string:
104 flags = trace_flags_string.split(',')
105 flags = [flag.strip() for flag in flags]
106 for invalid_flag in list(set(flags) - set(VALID_TRACE_FLAGS)):
107 LOG.warning('Invalid trace flag: %s', invalid_flag)
108 try:
109 re.compile(api_trace_pattern)
110 except re.error:
111 msg = _('Cannot parse the API trace pattern. %s is not a '
112 'valid python regular expression.') % api_trace_pattern
113 raise exception.BadConfigurationException(reason=msg)
114 TRACE_METHOD = 'method' in flags
115 TRACE_API = 'api' in flags
118def trace(f):
119 def trace_wrapper(self, *args, **kwargs):
120 if TRACE_METHOD:
121 LOG.debug('Entering method %s', f.__name__)
122 result = f(self, *args, **kwargs)
123 if TRACE_METHOD:
124 LOG.debug('Leaving method %s', f.__name__)
125 return result
126 return trace_wrapper
129def convert_to_list(value):
131 if value is None:
132 return []
133 elif isinstance(value, str):
134 return [value]
135 elif isinstance(value, abc.Iterable):
136 return list(value)
137 else:
138 return [value]
141def convert_string_to_list(string, separator=','):
142 return [elem.strip() for elem in string.split(separator)]
145def get_relationship_type(is_flexgroup):
146 """Returns the snapmirror relationship type."""
147 return (EXTENDED_DATA_PROTECTION_TYPE if is_flexgroup
148 else DATA_PROTECTION_TYPE)
151def is_style_extended_flexgroup(style_extended):
152 """Returns whether the style is extended type or not."""
153 return style_extended == FLEXGROUP_STYLE_EXTENDED
156def parse_flexgroup_pool_config(config, cluster_aggr_set={}, check=False):
157 """Returns the dict with the FlexGroup pools and if it is auto provisioned.
159 :param config: the configuration flexgroup list of dict.
160 :param cluster_aggr_set: the set of aggregates in the cluster.
161 :param check: should check the config is correct.
162 """
164 flexgroup_pools_map = {}
165 aggr_list_used = []
166 for pool_dic in config:
167 for pool_name, aggr_str in pool_dic.items():
168 aggr_name_list = aggr_str.split()
170 if not check:
171 aggr_name_list.sort()
172 flexgroup_pools_map[pool_name] = aggr_name_list
173 continue
175 if pool_name in cluster_aggr_set:
176 msg = _('The %s FlexGroup pool name is not valid, because '
177 'it is a cluster aggregate name. Ensure that the '
178 'configuration option netapp_flexgroup_pools is '
179 'set correctly.')
180 raise exception.NetAppException(msg % pool_name)
182 aggr_name_set = set(aggr_name_list)
183 if len(aggr_name_set) != len(aggr_name_list):
184 msg = _('There is a repeated aggregate name in the '
185 'FlexGroup pool %s definition. Ensure that the '
186 'configuration option netapp_flexgroup_pools is '
187 'set correctly.')
188 raise exception.NetAppException(msg % pool_name)
190 not_found_aggr = aggr_name_set - cluster_aggr_set
191 if not_found_aggr:
192 not_found_list = [str(s) for s in not_found_aggr]
193 not_found_str = ", ".join(not_found_list)
194 msg = _('There is an aggregate name in the FlexGroup pool '
195 '%(pool)s that is not in the cluster: %(aggr)s. '
196 'Ensure that the configuration option '
197 'netapp_flexgroup_pools is set correctly.')
198 msg_args = {'pool': pool_name, 'aggr': not_found_str}
199 raise exception.NetAppException(msg % msg_args)
201 aggr_name_list.sort()
202 aggr_name_list_str = "".join(aggr_name_list)
203 if aggr_name_list_str in aggr_list_used:
204 msg = _('The FlexGroup pool %s is duplicated. Ensure that '
205 'the configuration option netapp_flexgroup_pools '
206 'is set correctly.')
207 raise exception.NetAppException(msg % pool_name)
209 aggr_list_used.append(aggr_name_list_str)
210 flexgroup_pools_map[pool_name] = aggr_name_list
212 return flexgroup_pools_map
215def calculate_max_files(size, max_files_multiplier, max_files=None):
216 """Returns the max_files as integer or None.
218 :param size: volume size in gb
219 :param max_files_multiplier: config out of string extra spec
220 :param max_files: pass max_files option
221 """
222 if size is None or max_files_multiplier is None:
223 return None
225 if max_files is not None:
226 msg = _('Something went wrong: '
227 'validate_provisioning_options_for_share should have made '
228 'sure that max_files and max_files_multiplier cannot be set '
229 'at same time.')
230 raise exception.NetAppException(msg)
232 # size_gb * units.Mi = size_kib
233 # calculation based upon TR-4617
234 max_files = int(size * units.Mi * float(max_files_multiplier) / 33.6925)
236 return min(max_files, ABSOLUTE_MAX_INODES)
239class OpenStackInfo(object):
240 """OS/distribution, release, and version.
242 NetApp uses these fields as content for EMS log entry.
243 """
245 PACKAGE_NAME = 'python3-manila'
247 def __init__(self):
248 self._version = 'unknown version'
249 self._release = 'unknown release'
250 self._vendor = 'unknown vendor'
251 self._platform = 'unknown platform'
253 def _update_version_from_version_string(self):
254 try:
255 self._version = version.version_info.version_string()
256 except Exception:
257 pass
259 def _update_release_from_release_string(self):
260 try:
261 self._release = version.version_info.release_string()
262 except Exception:
263 pass
265 def _update_platform(self):
266 try:
267 self._platform = platform.platform()
268 except Exception:
269 pass
271 @staticmethod
272 def _get_version_info_version():
273 return version.version_info.version
275 @staticmethod
276 def _get_version_info_release():
277 return version.version_info.release_string()
279 def _update_info_from_version_info(self):
280 try:
281 ver = self._get_version_info_version()
282 if ver:
283 self._version = ver
284 except Exception:
285 pass
286 try:
287 rel = self._get_version_info_release()
288 if rel:
289 self._release = rel
290 except Exception:
291 pass
293 # RDO, RHEL-OSP, Mirantis on Redhat, SUSE.
294 def _update_info_from_rpm(self):
295 LOG.debug('Trying rpm command.')
296 try:
297 out, err = putils.execute("rpm", "-q", "--queryformat",
298 "'%{version}\t%{release}\t%{vendor}'",
299 self.PACKAGE_NAME)
300 if not out:
301 LOG.info('No rpm info found for %(pkg)s package.', {
302 'pkg': self.PACKAGE_NAME})
303 return False
304 parts = out.split()
305 self._version = parts[0]
306 self._release = parts[1]
307 self._vendor = ' '.join(parts[2::])
308 return True
309 except Exception as e:
310 LOG.info('Could not run rpm command: %(msg)s.', {
311 'msg': e})
312 return False
314 # Ubuntu, Mirantis on Ubuntu.
315 def _update_info_from_dpkg(self):
316 LOG.debug('Trying dpkg-query command.')
317 try:
318 _vendor = None
319 out, err = putils.execute("dpkg-query", "-W", "-f='${Version}'",
320 self.PACKAGE_NAME)
321 if not out:
322 LOG.info(
323 'No dpkg-query info found for %(pkg)s package.', {
324 'pkg': self.PACKAGE_NAME})
325 return False
326 # Debian format: [epoch:]upstream_version[-debian_revision]
327 deb_version = out
328 # In case epoch or revision is missing, copy entire string.
329 _release = deb_version
330 if ':' in deb_version: 330 ↛ 333line 330 didn't jump to line 333 because the condition on line 330 was always true
331 deb_epoch, upstream_version = deb_version.split(':')
332 _release = upstream_version
333 if '-' in deb_version: 333 ↛ 336line 333 didn't jump to line 336 because the condition on line 333 was always true
334 deb_revision = deb_version.split('-')[1]
335 _vendor = deb_revision
336 self._release = _release
337 if _vendor: 337 ↛ 339line 337 didn't jump to line 339 because the condition on line 337 was always true
338 self._vendor = _vendor
339 return True
340 except Exception as e:
341 LOG.info('Could not run dpkg-query command: %(msg)s.', {
342 'msg': e})
343 return False
345 def _update_openstack_info(self):
346 self._update_version_from_version_string()
347 self._update_release_from_release_string()
348 self._update_platform()
349 # Some distributions override with more meaningful information.
350 self._update_info_from_version_info()
351 # See if we have still more targeted info from rpm or apt.
352 found_package = self._update_info_from_rpm()
353 if not found_package:
354 self._update_info_from_dpkg()
356 def info(self):
357 self._update_openstack_info()
358 return '%(version)s|%(release)s|%(vendor)s|%(platform)s' % {
359 'version': self._version, 'release': self._release,
360 'vendor': self._vendor, 'platform': self._platform}
363class DataCache(object):
364 """DataCache class for caching NetApp information.
366 The cache validity is measured by a stop watch that is
367 not thread-safe.
368 """
370 def __init__(self, duration):
371 self._stop_watch = timeutils.StopWatch(duration)
372 self._cached_data = None
374 def is_expired(self):
375 return not self._stop_watch.has_started() or self._stop_watch.expired()
377 def get_data(self):
378 return self._cached_data
380 def update_data(self, cached_data):
381 if not self._stop_watch.has_started():
382 self._stop_watch.start()
383 else:
384 self._stop_watch.restart()
386 self._cached_data = cached_data