Coverage for manila/share/drivers/veritas/veritas_isa.py: 97%
372 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 2017 Veritas Technologies LLC.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14"""
15Veritas Access Driver for manila shares.
17Limitation:
191) single tenant
20"""
22import hashlib
23from http import client as http_client
24import json
26from oslo_config import cfg
27from oslo_log import log as logging
28from oslo_utils import units
29from random import shuffle
30import requests
31import requests.auth
33from manila.common import constants as const
34from manila import exception
35from manila.share import driver
37LOG = logging.getLogger(__name__)
40va_share_opts = [
41 cfg.StrOpt('va_server_ip',
42 help='Console IP of Veritas Access server.'),
43 cfg.IntOpt('va_port',
44 default=14161,
45 help='Veritas Access server REST port.'),
46 cfg.StrOpt('va_user',
47 help='Veritas Access server REST login name.'),
48 cfg.StrOpt('va_pwd',
49 secret=True,
50 help='Veritas Access server REST password.'),
51 cfg.StrOpt('va_pool',
52 help='Veritas Access storage pool from which '
53 'shares are served.'),
54 cfg.StrOpt('va_fstype',
55 default='simple',
56 help='Type of VA file system to be created.')
57]
60CONF = cfg.CONF
61CONF.register_opts(va_share_opts)
64class NoAuth(requests.auth.AuthBase):
65 """This is a 'authentication' handler.
67 It exists for use with custom authentication systems, such as the
68 one for the Access API, it simply passes the Authorization header as-is.
70 The default authentication handler for requests will clobber the
71 Authorization header.
72 """
74 def __call__(self, r):
75 return r
78class ACCESSShareDriver(driver.ExecuteMixin, driver.ShareDriver):
79 """ACCESS Share Driver.
81 Executes commands relating to Manila Shares.
82 Supports creation of shares on ACCESS.
84 API version history:
86 1.0 - Initial version.
87 """
89 VA_SHARE_PATH_STR = '/vx/'
91 def __init__(self, *args, **kwargs):
92 """Do initialization."""
94 super(ACCESSShareDriver, self).__init__(False, *args, **kwargs)
95 self.configuration.append_config_values(va_share_opts)
96 self.backend_name = self.configuration.safe_get(
97 'share_backend_name') or "VeritasACCESS"
98 self._va_ip = None
99 self._va_url = None
100 self._pool = None
101 self._fstype = None
102 self._port = None
103 self._user = None
104 self._pwd = None
105 self._cred = None
106 self._connect_resp = None
107 self._verify_ssl_cert = None
108 self._fs_create_str = '/fs/create'
109 self._fs_list_str = '/fs'
110 self._fs_delete_str = '/fs/destroy'
111 self._fs_extend_str = '/fs/grow'
112 self._fs_shrink_str = '/fs/shrink'
113 self._snap_create_str = '/snapshot/create'
114 self._snap_delete_str = '/snapshot/delete'
115 self._snap_list_str = '/snapshot/getSnapShotList'
116 self._nfs_add_str = '/share/create'
117 self._nfs_delete_str = '/share/delete'
118 self._nfs_share_list_str = '/share/all_shares_details_by_path/?path='
119 self._ip_addr_show_str = '/common/get_all_ips'
120 self._pool_free_str = '/storage/pool'
121 self._update_object = '/objecttags'
122 self.session = None
123 self.host = None
124 LOG.debug("ACCESSShareDriver called")
126 def do_setup(self, context):
127 """Any initialization the share driver does while starting."""
128 super(ACCESSShareDriver, self).do_setup(context)
130 self._va_ip = self.configuration.va_server_ip
131 self._pool = self.configuration.va_pool
132 self._user = self.configuration.va_user
133 self._pwd = self.configuration.va_pwd
134 self._port = self.configuration.va_port
135 self._fstype = self.configuration.va_fstype
136 self.session = self._authenticate_access(self._va_ip, self._user,
137 self._pwd)
139 def _get_va_share_name(self, name):
140 length = len(name)
141 index = int(length / 2)
142 name1 = name[:index]
143 name2 = name[index:]
144 crc1 = hashlib.md5(name1.encode('utf-8'),
145 usedforsecurity=False).hexdigest()[:8]
146 crc2 = hashlib.md5(name2.encode('utf-8'),
147 usedforsecurity=False).hexdigest()[:8]
148 return crc1 + '-' + crc2
150 def _get_va_snap_name(self, name):
151 return self._get_va_share_name(name)
153 def _get_va_share_path(self, name):
154 return self.VA_SHARE_PATH_STR + name
156 def _does_item_exist_at_va_backend(self, item_name, path_given):
157 """Check given share is exists on backend"""
159 path = path_given
160 provider = '%s:%s' % (self.host, self._port)
161 data = {}
162 item_list = self._access_api(self.session, provider, path,
163 json.dumps(data), 'GET')
165 for item in item_list:
166 if item['name'] == item_name:
167 return True
169 return False
171 def _return_access_lists_difference(self, list_a, list_b):
172 """Returns a list of elements in list_a that are not in list_b"""
174 sub_list = [{"access_to": s.get('access_to'),
175 "access_type": s.get('access_type'),
176 "access_level": s.get('access_level')}
177 for s in list_b]
179 return [r for r in list_a if (
180 {"access_to": r.get("access_to"),
181 "access_type": r.get("access_type"),
182 "access_level": r.get("access_level")} not in sub_list)]
184 def _fetch_existing_rule(self, share_name):
185 """Return list of access rules on given share"""
187 share_path = self._get_va_share_path(share_name)
188 path = self._nfs_share_list_str + share_path
189 provider = '%s:%s' % (self.host, self._port)
190 data = {}
191 share_list = self._access_api(self.session, provider, path,
192 json.dumps(data), 'GET')
194 va_access_list = []
195 for share in share_list:
196 if share['shareType'] == 'NFS':
197 for share_info in share['shares']:
198 if share_info['name'] == share_path:
199 access_to = share_info['host_name']
200 a_level = const.ACCESS_LEVEL_RO
201 if const.ACCESS_LEVEL_RW in share_info['privilege']:
202 a_level = const.ACCESS_LEVEL_RW
203 va_access_list.append({
204 'access_to': access_to,
205 'access_level': a_level,
206 'access_type': 'ip'
207 })
209 return va_access_list
211 def create_share(self, ctx, share, share_server=None):
212 """Create an ACCESS file system that will be represented as share."""
214 sharename = share['name']
215 sizestr = '%sg' % share['size']
216 LOG.debug("ACCESSShareDriver create_share sharename %s sizestr %r",
217 sharename, sizestr)
218 va_sharename = self._get_va_share_name(sharename)
219 va_sharepath = self._get_va_share_path(va_sharename)
220 va_fs_type = self._fstype
221 path = self._fs_create_str
222 provider = '%s:%s' % (self.host, self._port)
223 data1 = {
224 "largefs": "no",
225 "blkSize": "blksize=8192",
226 "pdirEnable": "pdir_enable=yes"
227 }
228 data1["layout"] = va_fs_type
229 data1["fs_name"] = va_sharename
230 data1["fs_size"] = sizestr
231 data1["pool_disks"] = self._pool
232 result = self._access_api(self.session, provider, path,
233 json.dumps(data1), 'POST')
234 if not result:
235 message = (('ACCESSShareDriver create share failed %s'), sharename)
236 LOG.error(message)
237 raise exception.ShareBackendException(msg=message)
239 data2 = {"type": "FS", "key": "manila"}
240 data2["id"] = va_sharename
241 data2["value"] = 'manila_fs'
242 path = self._update_object
243 result = self._access_api(self.session, provider, path,
244 json.dumps(data2), 'POST')
246 vip = self._get_vip()
247 location = vip + ':' + va_sharepath
248 LOG.debug("ACCESSShareDriver create_share location %s", location)
249 return location
251 def _get_vip(self):
252 """Get a virtual IP from ACCESS."""
253 ip_list = self._get_access_ips(self.session, self.host)
254 vip = []
255 for ips in ip_list:
256 if ips['isconsoleip'] == 1:
257 continue
258 if ips['type'] == 'Virtual' and ips['status'] == 'ONLINE':
259 vip.append(ips['ip'])
260 shuffle(vip)
261 return str(vip[0])
263 def delete_share(self, context, share, share_server=None):
264 """Delete a share from ACCESS."""
266 sharename = share['name']
267 va_sharename = self._get_va_share_name(sharename)
268 LOG.debug("ACCESSShareDriver delete_share %s called",
269 sharename)
270 if share['snapshot_id']:
271 message = (('ACCESSShareDriver delete share %s'
272 ' early return'), sharename)
273 LOG.debug(message)
274 return
276 ret_val = self._does_item_exist_at_va_backend(va_sharename,
277 self._fs_list_str)
278 if not ret_val:
279 return
281 path = self._fs_delete_str
282 provider = '%s:%s' % (self.host, self._port)
283 data = {}
284 data["fs_name"] = va_sharename
285 result = self._access_api(self.session, provider, path,
286 json.dumps(data), 'POST')
287 if not result:
288 message = (('ACCESSShareDriver delete share failed %s'), sharename)
289 LOG.error(message)
290 raise exception.ShareBackendException(msg=message)
292 data2 = {"type": "FS", "key": "manila"}
293 data2["id"] = va_sharename
294 path = self._update_object
295 result = self._access_api(self.session, provider, path,
296 json.dumps(data2), 'DELETE')
298 def extend_share(self, share, new_size, share_server=None):
299 """Extend existing share to new size."""
300 sharename = share['name']
301 size = '%s%s' % (str(new_size), 'g')
302 va_sharename = self._get_va_share_name(sharename)
303 path = self._fs_extend_str
304 provider = '%s:%s' % (self.host, self._port)
305 data1 = {"operationOption": "growto", "tier": "primary"}
306 data1["fs_name"] = va_sharename
307 data1["fs_size"] = size
308 result = self._access_api(self.session, provider, path,
309 json.dumps(data1), 'POST')
310 if not result:
311 message = (('ACCESSShareDriver extend share failed %s'), sharename)
312 LOG.error(message)
313 raise exception.ShareBackendException(msg=message)
315 LOG.debug('ACCESSShareDriver extended share'
316 ' successfully %s', sharename)
318 def shrink_share(self, share, new_size, share_server=None):
319 """Shrink existing share to new size."""
320 sharename = share['name']
321 va_sharename = self._get_va_share_name(sharename)
322 size = '%s%s' % (str(new_size), 'g')
323 path = self._fs_extend_str
324 provider = '%s:%s' % (self.host, self._port)
325 data1 = {"operationOption": "shrinkto", "tier": "primary"}
326 data1["fs_name"] = va_sharename
327 data1["fs_size"] = size
328 result = self._access_api(self.session, provider, path,
329 json.dumps(data1), 'POST')
330 if not result:
331 message = (('ACCESSShareDriver shrink share failed %s'), sharename)
332 LOG.error(message)
333 raise exception.ShareBackendException(msg=message)
335 LOG.debug('ACCESSShareDriver shrunk share successfully %s', sharename)
337 def _allow_access(self, context, share, access, share_server=None):
338 """Give access of a share to an IP."""
340 access_type = access['access_type']
341 server = access['access_to']
342 if access_type != 'ip':
343 raise exception.InvalidShareAccess('Only ip access type '
344 'supported.')
345 access_level = access['access_level']
347 if access_level not in (const.ACCESS_LEVEL_RW, const.ACCESS_LEVEL_RO):
348 raise exception.InvalidShareAccessLevel(level=access_level)
349 export_path = share['export_locations'][0]['path'].split(':', 1)
350 va_sharepath = str(export_path[1])
351 access_level = '%s,%s' % (str(access_level),
352 'sync,no_root_squash')
354 path = self._nfs_add_str
355 provider = '%s:%s' % (self.host, self._port)
356 data = {}
357 va_share_info = ("{\"share\":[{\"fileSystemPath\":\"" + va_sharepath +
358 "\",\"shareType\":\"NFS\",\"shareDetails\":" +
359 "[{\"client\":\"" + server +
360 "\",\"exportOptions\":\"" +
361 access_level + "\"}]}]}")
363 data["shareDetails"] = va_share_info
365 result = self._access_api(self.session, provider, path,
366 json.dumps(data), 'POST')
368 if not result:
369 message = (('ACCESSShareDriver access failed sharepath %s '
370 'server %s'),
371 va_sharepath,
372 server)
373 LOG.error(message)
374 raise exception.ShareBackendException(msg=message)
376 LOG.debug("ACCESSShareDriver allow_access sharepath %s server %s",
377 va_sharepath, server)
379 data2 = {"type": "SHARE", "key": "manila"}
380 data2["id"] = va_sharepath
381 data2["value"] = 'manila_share'
382 path = self._update_object
383 result = self._access_api(self.session, provider, path,
384 json.dumps(data2), 'POST')
386 def _deny_access(self, context, share, access, share_server=None):
387 """Deny access to the share."""
389 server = access['access_to']
390 access_type = access['access_type']
391 if access_type != 'ip':
392 return
393 export_path = share['export_locations'][0]['path'].split(':', 1)
394 va_sharepath = str(export_path[1])
395 LOG.debug("ACCESSShareDriver deny_access sharepath %s server %s",
396 va_sharepath, server)
398 path = self._nfs_delete_str
399 provider = '%s:%s' % (self.host, self._port)
400 data = {}
401 va_share_info = ("{\"share\":[{\"fileSystemPath\":\"" + va_sharepath +
402 "\",\"shareType\":\"NFS\",\"shareDetails\":" +
403 "[{\"client\":\"" + server + "\"}]}]}")
405 data["shareDetails"] = va_share_info
406 result = self._access_api(self.session, provider, path,
407 json.dumps(data), 'DELETE')
408 if not result:
409 message = (('ACCESSShareDriver deny failed'
410 ' sharepath %s server %s'),
411 va_sharepath,
412 server)
413 LOG.error(message)
414 raise exception.ShareBackendException(msg=message)
416 LOG.debug("ACCESSShareDriver deny_access sharepath %s server %s",
417 va_sharepath, server)
419 data2 = {"type": "SHARE", "key": "manila"}
420 data2["id"] = va_sharepath
421 path = self._update_object
422 result = self._access_api(self.session, provider, path,
423 json.dumps(data2), 'DELETE')
425 def update_access(self, context, share, access_rules, add_rules,
426 delete_rules, update_rules, share_server=None):
427 """Update access to the share."""
429 if (add_rules or delete_rules):
430 # deleting rules
431 for rule in delete_rules:
432 self._deny_access(context, share, rule, share_server)
434 # adding rules
435 for rule in add_rules:
436 self._allow_access(context, share, rule, share_server)
437 else:
438 if not access_rules:
439 LOG.warning("No access rules provided in update_access.")
440 else:
441 sharename = self._get_va_share_name(share['name'])
442 existing_a_rules = self._fetch_existing_rule(sharename)
444 d_rule = self._return_access_lists_difference(existing_a_rules,
445 access_rules)
446 for rule in d_rule:
447 LOG.debug("Removing rule %s in recovery.",
448 str(rule))
449 self._deny_access(context, share, rule, share_server)
451 a_rule = self._return_access_lists_difference(access_rules,
452 existing_a_rules)
453 for rule in a_rule:
454 LOG.debug("Adding rule %s in recovery.",
455 str(rule))
456 self._allow_access(context, share, rule, share_server)
458 def create_snapshot(self, context, snapshot, share_server=None):
459 """create snapshot of a share."""
460 LOG.debug('ACCESSShareDriver create_snapshot called '
461 'for snapshot ID %s.',
462 snapshot['snapshot_id'])
464 sharename = snapshot['share_name']
465 va_sharename = self._get_va_share_name(sharename)
466 snapname = snapshot['name']
467 va_snapname = self._get_va_snap_name(snapname)
469 path = self._snap_create_str
470 provider = '%s:%s' % (self.host, self._port)
471 data = {}
472 data["snapShotname"] = va_snapname
473 data["fileSystem"] = va_sharename
474 data["removable"] = 'yes'
475 result = self._access_api(self.session, provider, path,
476 json.dumps(data), 'PUT')
477 if not result:
478 message = (('ACCESSShareDriver create snapshot failed snapname %s'
479 ' sharename %s'),
480 snapname,
481 va_sharename)
482 LOG.error(message)
483 raise exception.ShareBackendException(msg=message)
485 data2 = {"type": "SNAPSHOT", "key": "manila"}
486 data2["id"] = va_snapname
487 data2["value"] = 'manila_snapshot'
488 path = self._update_object
489 result = self._access_api(self.session, provider, path,
490 json.dumps(data2), 'POST')
492 def delete_snapshot(self, context, snapshot, share_server=None):
493 """Deletes a snapshot."""
494 sharename = snapshot['share_name']
495 va_sharename = self._get_va_share_name(sharename)
496 snapname = snapshot['name']
497 va_snapname = self._get_va_snap_name(snapname)
499 ret_val = self._does_item_exist_at_va_backend(va_snapname,
500 self._snap_list_str)
501 if not ret_val:
502 return
504 path = self._snap_delete_str
505 provider = '%s:%s' % (self.host, self._port)
507 data = {}
508 data["name"] = va_snapname
509 data["fsName"] = va_sharename
510 data_to_send = {"snapShotDetails": {"snapshot": [data]}}
511 result = self._access_api(self.session, provider, path,
512 json.dumps(data_to_send), 'DELETE')
513 if not result:
514 message = (('ACCESSShareDriver delete snapshot failed snapname %s'
515 ' sharename %s'),
516 snapname,
517 va_sharename)
518 LOG.error(message)
519 raise exception.ShareBackendException(msg=message)
521 data2 = {"type": "SNAPSHOT", "key": "manila"}
522 data2["id"] = va_snapname
523 path = self._update_object
524 result = self._access_api(self.session, provider, path,
525 json.dumps(data2), 'DELETE')
527 def create_share_from_snapshot(self, ctx, share, snapshot,
528 share_server=None, parent_share=None):
529 """create share from a snapshot."""
530 sharename = snapshot['share_name']
531 va_sharename = self._get_va_share_name(sharename)
532 snapname = snapshot['name']
533 va_snapname = self._get_va_snap_name(snapname)
534 va_sharepath = self._get_va_share_path(va_sharename)
535 LOG.debug(('ACCESSShareDriver create_share_from_snapshot snapname %s'
536 ' sharename %s'),
537 va_snapname,
538 va_sharename)
539 vip = self._get_vip()
540 location = vip + ':' + va_sharepath + ':' + va_snapname
541 LOG.debug("ACCESSShareDriver create_share location %s", location)
542 return location
544 def _get_api(self, provider, tail):
545 api_root = 'https://%s/api' % (provider)
546 return api_root + tail
548 def _access_api(self, session, provider, path, input_data, method):
549 """Returns False if failure occurs."""
550 kwargs = {'data': input_data}
551 if not isinstance(input_data, dict): 551 ↛ 553line 551 didn't jump to line 553 because the condition on line 551 was always true
552 kwargs['headers'] = {'Content-Type': 'application/json'}
553 full_url = self._get_api(provider, path)
554 response = session.request(method, full_url, **kwargs)
555 if response.status_code != http_client.OK:
556 LOG.debug('Access API operation Failed.')
557 return False
558 if path == self._update_object:
559 return True
560 result = response.json()
561 return result
563 def _get_access_ips(self, session, host):
565 path = self._ip_addr_show_str
566 provider = '%s:%s' % (host, self._port)
567 data = {}
568 ip_list = self._access_api(session, provider, path,
569 json.dumps(data), 'GET')
570 return ip_list
572 def _authenticate_access(self, address, username, password):
573 session = requests.session()
574 session.verify = False
575 session.auth = NoAuth()
577 response = session.post('https://%s:%s/api/rest/authenticate'
578 % (address, self._port),
579 data={'username': username,
580 'password': password})
581 if response.status_code != http_client.OK:
582 LOG.debug(('failed to authenticate to remote cluster at %s as %s'),
583 address, username)
584 raise exception.NotAuthorized('Authentication failure.')
585 result = response.json()
586 session.headers.update({'Authorization': 'Bearer {}'
587 .format(result['token'])})
588 session.headers.update({'Content-Type': 'application/json'})
590 return session
592 def _get_access_pool_details(self):
593 """Get access pool details."""
594 path = self._pool_free_str
595 provider = '%s:%s' % (self.host, self._port)
596 data = {}
597 pool_details = self._access_api(self.session, provider, path,
598 json.dumps(data), 'GET')
600 for pool in pool_details:
601 if pool['device_group_name'] == str(self._pool):
602 total_capacity = (int(pool['capacity']) / units.Gi)
603 used_size = (int(pool['used_size']) / units.Gi)
604 return (total_capacity, (total_capacity - used_size))
606 message = 'Fetching pool details operation failed.'
607 LOG.error(message)
608 raise exception.ShareBackendException(msg=message)
610 def _update_share_stats(self):
611 """Retrieve status info from share volume group."""
613 LOG.debug("VRTSISA Updating share status.")
614 self.host = str(self._va_ip)
615 self.session = self._authenticate_access(self._va_ip,
616 self._user, self._pwd)
617 total_capacity, free_capacity = self._get_access_pool_details()
618 data = {
619 'share_backend_name': self.backend_name,
620 'vendor_name': 'Veritas',
621 'driver_version': '1.0',
622 'storage_protocol': 'NFS',
623 'total_capacity_gb': total_capacity,
624 'free_capacity_gb': free_capacity,
625 'reserved_percentage': 0,
626 'reserved_snapshot_percentage': 0,
627 'reserved_share_extend_percentage': 0,
628 'QoS_support': False,
629 'snapshot_support': True,
630 'create_share_from_snapshot_support': True
631 }
632 super(ACCESSShareDriver, self)._update_share_stats(data)