Coverage for manila/share/drivers/tegile/tegile.py: 98%
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) 2016 by Tegile Systems, Inc.
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"""
16Share driver for Tegile storage.
17"""
19import json
20import requests
22from oslo_config import cfg
23from oslo_log import log
25from manila import exception
26from manila.i18n import _
27from manila.share import driver
28from manila.share import utils as share_utils
29from manila import utils
31tegile_opts = [
32 cfg.HostAddressOpt('tegile_nas_server',
33 help='Tegile NAS server hostname or IP address.'),
34 cfg.StrOpt('tegile_nas_login',
35 help='User name for the Tegile NAS server.'),
36 cfg.StrOpt('tegile_nas_password',
37 secret=True,
38 help='Password for the Tegile NAS server.'),
39 cfg.StrOpt('tegile_default_project',
40 help='Create shares in this project')]
43CONF = cfg.CONF
44CONF.register_opts(tegile_opts)
46LOG = log.getLogger(__name__)
47DEFAULT_API_SERVICE = 'openstack'
48TEGILE_API_PATH = 'zebi/api'
49TEGILE_LOCAL_CONTAINER_NAME = 'Local'
50TEGILE_SNAPSHOT_PREFIX = 'Manual-S-'
51VENDOR = 'Tegile Systems Inc.'
52DEFAULT_BACKEND_NAME = 'Tegile'
53VERSION = '1.0.0'
54DEBUG_LOGGING = False # For debugging purposes
57def debugger(func):
58 """Returns a wrapper that wraps func.
60 The wrapper will log the entry and exit points of the function.
61 """
63 def wrapper(*args, **kwds):
64 if DEBUG_LOGGING: 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true
65 LOG.debug('Entering %(classname)s.%(funcname)s',
66 {
67 'classname': args[0].__class__.__name__,
68 'funcname': func.__name__,
69 })
70 LOG.debug('Arguments: %(args)s, %(kwds)s',
71 {
72 'args': args[1:],
73 'kwds': kwds,
74 })
75 f_result = func(*args, **kwds)
76 if DEBUG_LOGGING: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 LOG.debug('Exiting %(classname)s.%(funcname)s',
78 {
79 'classname': args[0].__class__.__name__,
80 'funcname': func.__name__,
81 })
82 LOG.debug('Results: %(result)s',
83 {'result': f_result})
84 return f_result
86 return wrapper
89class TegileAPIExecutor(object):
90 def __init__(self, classname, hostname, username, password):
91 self._classname = classname
92 self._hostname = hostname
93 self._username = username
94 self._password = password
96 def __call__(self, *args, **kwargs):
97 return self._send_api_request(*args, **kwargs)
99 @debugger
100 @utils.retry(retry_param=(requests.ConnectionError, requests.Timeout),
101 interval=30,
102 retries=3,
103 backoff_rate=1)
104 def _send_api_request(self, method, params=None,
105 request_type='post',
106 api_service=DEFAULT_API_SERVICE,
107 fine_logging=DEBUG_LOGGING):
108 if params is not None:
109 params = json.dumps(params)
111 url = 'https://%s/%s/%s/%s' % (self._hostname,
112 TEGILE_API_PATH,
113 api_service,
114 method)
115 if fine_logging:
116 LOG.debug('TegileAPIExecutor(%(classname)s) method: %(method)s, '
117 'url: %(url)s', {
118 'classname': self._classname,
119 'method': method,
120 'url': url,
121 })
122 if request_type == 'post':
123 if fine_logging:
124 LOG.debug('TegileAPIExecutor(%(classname)s) '
125 'method: %(method)s, payload: %(payload)s',
126 {
127 'classname': self._classname,
128 'method': method,
129 'payload': params,
130 })
131 req = requests.post(url,
132 data=params,
133 auth=(self._username, self._password),
134 verify=False)
135 else:
136 req = requests.get(url,
137 auth=(self._username, self._password),
138 verify=False)
140 if fine_logging:
141 LOG.debug('TegileAPIExecutor(%(classname)s) method: %(method)s, '
142 'return code: %(retcode)s',
143 {
144 'classname': self._classname,
145 'method': method,
146 'retcode': req,
147 })
148 try:
149 response = req.json()
150 if fine_logging:
151 LOG.debug('TegileAPIExecutor(%(classname)s) '
152 'method: %(method)s, response: %(response)s',
153 {
154 'classname': self._classname,
155 'method': method,
156 'response': response,
157 })
158 except ValueError:
159 # Some APIs don't return output and that's fine
160 response = ''
161 req.close()
163 if req.status_code != 200:
164 raise exception.TegileAPIException(response=req.text)
166 return response
169class TegileShareDriver(driver.ShareDriver):
170 """Tegile NAS driver. Allows for NFS and CIFS NAS storage usage."""
171 def __init__(self, *args, **kwargs):
172 super(TegileShareDriver, self).__init__(False, *args, **kwargs)
174 LOG.warning('Tegile share driver has been deprecated and will be '
175 'removed in a future release.')
177 self.configuration.append_config_values(tegile_opts)
178 self._default_project = (self.configuration.safe_get(
179 "tegile_default_project") or 'openstack')
180 self._backend_name = (self.configuration.safe_get('share_backend_name')
181 or CONF.share_backend_name
182 or DEFAULT_BACKEND_NAME)
183 self._hostname = self.configuration.safe_get('tegile_nas_server')
184 username = self.configuration.safe_get('tegile_nas_login')
185 password = self.configuration.safe_get('tegile_nas_password')
186 self._api = TegileAPIExecutor(self.__class__.__name__,
187 self._hostname,
188 username,
189 password)
191 @debugger
192 def create_share(self, context, share, share_server=None):
193 """Is called to create share."""
194 share_name = share['name']
195 share_proto = share['share_proto']
197 pool_name = share_utils.extract_host(share['host'], level='pool')
199 params = (pool_name, self._default_project, share_name, share_proto)
201 # Share name coming from the backend is the most reliable. Sometimes
202 # a few options in Tegile array could cause sharename to be different
203 # from the one passed to it. Eg. 'projectname-sharename' instead
204 # of 'sharename' if inherited share properties are selected.
205 ip, real_share_name = self._api('createShare', params).split()
207 LOG.info("Created share %(sharename)s, share id %(shid)s.",
208 {'sharename': share_name, 'shid': share['id']})
210 return self._get_location_path(real_share_name, share_proto, ip)
212 @debugger
213 def extend_share(self, share, new_size, share_server=None):
214 """Is called to extend share.
216 There is no resize for Tegile shares.
217 We just adjust the quotas. The API is still called 'resizeShare'.
218 """
220 self._adjust_size(share, new_size, share_server)
222 @debugger
223 def shrink_share(self, shrink_share, shrink_size, share_server=None):
224 """Uses resize_share to shrink a share.
226 There is no shrink for Tegile shares.
227 We just adjust the quotas. The API is still called 'resizeShare'.
228 """
229 self._adjust_size(shrink_share, shrink_size, share_server)
231 @debugger
232 def _adjust_size(self, share, new_size, share_server=None):
233 pool, project, share_name = self._get_pool_project_share_name(share)
234 params = ('%s/%s/%s/%s' % (pool,
235 TEGILE_LOCAL_CONTAINER_NAME,
236 project,
237 share_name),
238 str(new_size),
239 'GB')
240 self._api('resizeShare', params)
242 @debugger
243 def delete_share(self, context, share, share_server=None):
244 """Is called to remove share."""
245 pool, project, share_name = self._get_pool_project_share_name(share)
246 params = ('%s/%s/%s/%s' % (pool,
247 TEGILE_LOCAL_CONTAINER_NAME,
248 project,
249 share_name),
250 True,
251 False)
253 self._api('deleteShare', params)
255 @debugger
256 def create_snapshot(self, context, snapshot, share_server=None):
257 """Is called to create snapshot."""
258 snap_name = snapshot['name']
260 pool, project, share_name = self._get_pool_project_share_name(
261 snapshot['share'])
263 share = {
264 'poolName': '%s' % pool,
265 'projectName': '%s' % project,
266 'name': share_name,
267 'availableSize': 0,
268 'totalSize': 0,
269 'datasetPath': '%s/%s/%s' %
270 (pool,
271 TEGILE_LOCAL_CONTAINER_NAME,
272 project),
273 'mountpoint': share_name,
274 'local': 'true',
275 }
277 params = (share, snap_name, False)
279 LOG.info('Creating snapshot for share_name=%(shr)s'
280 ' snap_name=%(name)s',
281 {'shr': share_name, 'name': snap_name})
283 self._api('createShareSnapshot', params)
285 @debugger
286 def create_share_from_snapshot(self, context, share, snapshot,
287 share_server=None, parent_share=None):
288 """Create a share from a snapshot - clone a snapshot."""
289 pool, project, share_name = self._get_pool_project_share_name(share)
291 params = ('%s/%s/%s/%s@%s%s' % (pool,
292 TEGILE_LOCAL_CONTAINER_NAME,
293 project,
294 snapshot['share_name'],
295 TEGILE_SNAPSHOT_PREFIX,
296 snapshot['name'],
297 ),
298 share_name,
299 True,
300 )
302 ip, real_share_name = self._api('cloneShareSnapshot',
303 params).split()
305 share_proto = share['share_proto']
306 return self._get_location_path(real_share_name, share_proto, ip)
308 @debugger
309 def delete_snapshot(self, context, snapshot, share_server=None):
310 """Is called to remove snapshot."""
311 pool, project, share_name = self._get_pool_project_share_name(
312 snapshot['share'])
313 params = ('%s/%s/%s/%s@%s%s' % (pool,
314 TEGILE_LOCAL_CONTAINER_NAME,
315 project,
316 share_name,
317 TEGILE_SNAPSHOT_PREFIX,
318 snapshot['name']),
319 False)
321 self._api('deleteShareSnapshot', params)
323 @debugger
324 def ensure_share(self, context, share, share_server=None):
325 """Invoked to sure that share is exported."""
327 # Fetching share name from server, because some configuration
328 # options can cause sharename different from the OpenStack share name
329 pool, project, share_name = self._get_pool_project_share_name(share)
330 params = [
331 '%s/%s/%s/%s' % (pool,
332 TEGILE_LOCAL_CONTAINER_NAME,
333 project,
334 share_name),
335 ]
336 ip, real_share_name = self._api('getShareIPAndMountPoint',
337 params).split()
339 share_proto = share['share_proto']
340 location = self._get_location_path(real_share_name, share_proto, ip)
341 return [location]
343 @debugger
344 def _allow_access(self, context, share, access, share_server=None):
345 """Allow access to the share."""
346 share_proto = share['share_proto']
347 access_type = access['access_type']
348 access_level = access['access_level']
349 access_to = access['access_to']
351 self._check_share_access(share_proto, access_type)
353 pool, project, share_name = self._get_pool_project_share_name(share)
354 params = ('%s/%s/%s/%s' % (pool,
355 TEGILE_LOCAL_CONTAINER_NAME,
356 project,
357 share_name),
358 share_proto,
359 access_type,
360 access_to,
361 access_level)
363 self._api('shareAllowAccess', params)
365 @debugger
366 def _deny_access(self, context, share, access, share_server=None):
367 """Deny access to the share."""
368 share_proto = share['share_proto']
369 access_type = access['access_type']
370 access_level = access['access_level']
371 access_to = access['access_to']
373 self._check_share_access(share_proto, access_type)
375 pool, project, share_name = self._get_pool_project_share_name(share)
376 params = ('%s/%s/%s/%s' % (pool,
377 TEGILE_LOCAL_CONTAINER_NAME,
378 project,
379 share_name),
380 share_proto,
381 access_type,
382 access_to,
383 access_level)
385 self._api('shareDenyAccess', params)
387 def _check_share_access(self, share_proto, access_type):
388 if share_proto == 'CIFS' and access_type != 'user':
389 reason = ('Only USER access type is allowed for '
390 'CIFS shares.')
391 LOG.warning(reason)
392 raise exception.InvalidShareAccess(reason=reason)
393 elif share_proto == 'NFS' and access_type not in ('ip', 'user'):
394 reason = ('Only IP or USER access types are allowed for '
395 'NFS shares.')
396 LOG.warning(reason)
397 raise exception.InvalidShareAccess(reason=reason)
398 elif share_proto not in ('NFS', 'CIFS'):
399 reason = ('Unsupported protocol \"%s\" specified for '
400 'access rule.') % share_proto
401 raise exception.InvalidShareAccess(reason=reason)
403 @debugger
404 def update_access(self, context, share, access_rules, add_rules,
405 delete_rules, update_rules, share_server=None):
406 if not (add_rules or delete_rules):
407 # Recovery mode
408 pool, project, share_name = (
409 self._get_pool_project_share_name(share))
410 share_proto = share['share_proto']
411 params = ('%s/%s/%s/%s' % (pool,
412 TEGILE_LOCAL_CONTAINER_NAME,
413 project,
414 share_name),
415 share_proto)
417 # Clears all current ACLs
418 # Remove ip and user ACLs if share_proto is NFS
419 # Remove user ACLs if share_proto is CIFS
420 self._api('clearAccessRules', params)
422 # Looping through all rules.
423 # Will have one API call per rule.
424 for access in access_rules:
425 self._allow_access(context, share, access, share_server)
426 else:
427 # Adding/Deleting specific rules
428 for access in delete_rules:
429 self._deny_access(context, share, access, share_server)
430 for access in add_rules:
431 self._allow_access(context, share, access, share_server)
433 @debugger
434 def _update_share_stats(self, **kwargs):
435 """Retrieve stats info."""
437 try:
438 data = self._api(method='getArrayStats',
439 request_type='get',
440 fine_logging=False)
441 # fixing values coming back here as String to float
442 for pool in data.get('pools', []):
443 pool['total_capacity_gb'] = float(
444 pool.get('total_capacity_gb', 0))
445 pool['free_capacity_gb'] = float(
446 pool.get('free_capacity_gb', 0))
447 pool['allocated_capacity_gb'] = float(
448 pool.get('allocated_capacity_gb', 0))
450 pool['qos'] = pool.pop('QoS_support', False)
451 pool['reserved_percentage'] = (
452 self.configuration.reserved_share_percentage)
453 pool['reserved_snapshot_percentage'] = (
454 self.configuration.reserved_share_from_snapshot_percentage
455 or self.configuration.reserved_share_percentage)
456 pool['reserved_share_extend_percentage'] = (
457 self.configuration.reserved_share_extend_percentage
458 or self.configuration.reserved_share_percentage)
459 pool['dedupe'] = True
460 pool['compression'] = True
461 pool['thin_provisioning'] = True
462 pool['max_over_subscription_ratio'] = (
463 self.configuration.max_over_subscription_ratio)
465 data['share_backend_name'] = self._backend_name
466 data['vendor_name'] = VENDOR
467 data['driver_version'] = VERSION
468 data['storage_protocol'] = 'NFS_CIFS'
469 data['snapshot_support'] = True
470 data['create_share_from_snapshot_support'] = True
471 data['qos'] = False
473 super(TegileShareDriver, self)._update_share_stats(data)
474 except Exception:
475 msg = _('Unexpected error while trying to get the '
476 'usage stats from array.')
477 LOG.exception(msg)
478 raise
480 @debugger
481 def get_pool(self, share):
482 """Returns pool name where share resides.
484 :param share: The share hosted by the driver.
485 :return: Name of the pool where given share is hosted.
486 """
487 pool = share_utils.extract_host(share['host'], level='pool')
488 return pool
490 @debugger
491 def get_network_allocations_number(self):
492 """Get number of network interfaces to be created."""
493 return 0
495 @debugger
496 def _get_location_path(self, share_name, share_proto, ip=None):
497 if ip is None:
498 ip = self._hostname
499 if share_proto == 'NFS':
500 location = '%s:%s' % (ip, share_name)
501 elif share_proto == 'CIFS':
502 location = r'\\%s\%s' % (ip, share_name)
503 else:
504 message = _('Invalid NAS protocol supplied: %s.') % share_proto
505 raise exception.InvalidInput(message)
507 export_location = {
508 'path': location,
509 'is_admin_only': False,
510 'metadata': {
511 'preferred': True,
512 },
513 }
514 return export_location
516 @debugger
517 def _get_pool_project_share_name(self, share):
518 pool = share_utils.extract_host(share['host'], level='pool')
519 project = self._default_project
521 share_name = share['name']
523 return pool, project, share_name