Coverage for manila/share/drivers/hitachi/hnas/driver.py: 99%
456 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 Hitachi Data 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.
16import os
18from oslo_config import cfg
19from oslo_log import log
20from oslo_utils import excutils
21from oslo_utils import importutils
23from manila.common import constants
24from manila import exception
25from manila.i18n import _
26from manila.share import driver
27from manila.share import utils
29LOG = log.getLogger(__name__)
31hitachi_hnas_opts = [
32 cfg.HostAddressOpt('hitachi_hnas_ip',
33 help="HNAS management interface IP for communication "
34 "between Manila controller and HNAS."),
35 cfg.StrOpt('hitachi_hnas_user',
36 help="HNAS username Base64 String in order to perform tasks "
37 "such as create file-systems and network interfaces."),
38 cfg.StrOpt('hitachi_hnas_password',
39 secret=True,
40 help="HNAS user password. Required only if private key is not "
41 "provided."),
42 cfg.IntOpt('hitachi_hnas_evs_id',
43 help="Specify which EVS this backend is assigned to."),
44 cfg.HostAddressOpt('hitachi_hnas_evs_ip',
45 help="Specify IP for mounting shares."),
46 cfg.HostAddressOpt('hitachi_hnas_admin_network_ip',
47 help="Specify IP for mounting shares in the Admin "
48 "network."),
49 cfg.StrOpt('hitachi_hnas_file_system_name',
50 help="Specify file-system name for creating shares."),
51 cfg.StrOpt('hitachi_hnas_ssh_private_key',
52 secret=True,
53 help="RSA/DSA private key value used to connect into HNAS. "
54 "Required only if password is not provided."),
55 cfg.HostAddressOpt('hitachi_hnas_cluster_admin_ip0',
56 help="The IP of the clusters admin node. Only set in "
57 "HNAS multinode clusters."),
58 cfg.IntOpt('hitachi_hnas_stalled_job_timeout',
59 default=30,
60 help="The time (in seconds) to wait for stalled HNAS jobs "
61 "before aborting."),
62 cfg.StrOpt('hitachi_hnas_driver_helper',
63 default='manila.share.drivers.hitachi.hnas.ssh.HNASSSHBackend',
64 help="Python class to be used for driver helper."),
65 cfg.BoolOpt('hitachi_hnas_allow_cifs_snapshot_while_mounted',
66 default=False,
67 help="By default, CIFS snapshots are not allowed to be taken "
68 "when the share has clients connected because consistent "
69 "point-in-time replica cannot be guaranteed for all "
70 "files. Enabling this might cause inconsistent snapshots "
71 "on CIFS shares."),
72]
74CONF = cfg.CONF
75CONF.register_opts(hitachi_hnas_opts)
78class HitachiHNASDriver(driver.ShareDriver):
79 """Manila HNAS Driver implementation.
81 Driver versions::
83 1.0.0 - Initial Version.
84 2.0.0 - Refactoring, bugfixes, implemented Share Shrink and
85 Update Access.
86 3.0.0 - New driver location, implemented support for CIFS protocol.
87 3.1.0 - Added admin network export location support.
88 4.0.0 - Added mountable snapshots, revert-to-snapshot and
89 manage snapshots features support.
90 """
92 def __init__(self, *args, **kwargs):
93 """Do initialization."""
95 LOG.debug("Invoking base constructor for Manila Hitachi HNAS Driver.")
96 super(HitachiHNASDriver, self).__init__(False, *args, **kwargs)
98 LOG.debug("Setting up attributes for Manila Hitachi HNAS Driver.")
99 self.configuration.append_config_values(hitachi_hnas_opts)
101 LOG.debug("Reading config parameters for Manila Hitachi HNAS Driver.")
102 self.backend_name = self.configuration.safe_get('share_backend_name')
103 hnas_helper = self.configuration.safe_get('hitachi_hnas_driver_helper')
104 hnas_ip = self.configuration.safe_get('hitachi_hnas_ip')
105 hnas_username = self.configuration.safe_get('hitachi_hnas_user')
106 hnas_password = self.configuration.safe_get('hitachi_hnas_password')
107 hnas_evs_id = self.configuration.safe_get('hitachi_hnas_evs_id')
108 self.hnas_evs_ip = self.configuration.safe_get('hitachi_hnas_evs_ip')
109 self.hnas_admin_network_ip = self.configuration.safe_get(
110 'hitachi_hnas_admin_network_ip')
111 self.fs_name = self.configuration.safe_get(
112 'hitachi_hnas_file_system_name')
113 self.cifs_snapshot = self.configuration.safe_get(
114 'hitachi_hnas_allow_cifs_snapshot_while_mounted')
115 ssh_private_key = self.configuration.safe_get(
116 'hitachi_hnas_ssh_private_key')
117 cluster_admin_ip0 = self.configuration.safe_get(
118 'hitachi_hnas_cluster_admin_ip0')
119 self.private_storage = kwargs.get('private_storage')
120 job_timeout = self.configuration.safe_get(
121 'hitachi_hnas_stalled_job_timeout')
123 if hnas_helper is None:
124 msg = _("The config parameter hitachi_hnas_driver_helper is not "
125 "set.")
126 raise exception.InvalidParameterValue(err=msg)
128 if hnas_evs_id is None:
129 msg = _("The config parameter hitachi_hnas_evs_id is not set.")
130 raise exception.InvalidParameterValue(err=msg)
132 if self.hnas_evs_ip is None:
133 msg = _("The config parameter hitachi_hnas_evs_ip is not set.")
134 raise exception.InvalidParameterValue(err=msg)
136 if hnas_ip is None:
137 msg = _("The config parameter hitachi_hnas_ip is not set.")
138 raise exception.InvalidParameterValue(err=msg)
140 if hnas_username is None:
141 msg = _("The config parameter hitachi_hnas_user is not set.")
142 raise exception.InvalidParameterValue(err=msg)
144 if hnas_password is None and ssh_private_key is None:
145 msg = _("Credentials configuration parameters missing: "
146 "you need to set hitachi_hnas_password or "
147 "hitachi_hnas_ssh_private_key.")
148 raise exception.InvalidParameterValue(err=msg)
150 LOG.debug("Initializing HNAS Layer.")
152 helper = importutils.import_class(hnas_helper)
154 self.hnas = helper(hnas_ip, hnas_username, hnas_password,
155 ssh_private_key, cluster_admin_ip0,
156 hnas_evs_id, self.hnas_evs_ip, self.fs_name,
157 job_timeout)
159 def update_access(self, context, share, access_rules, add_rules,
160 delete_rules, update_rules, share_server=None):
161 """Update access rules for given share.
163 :param context: The `context.RequestContext` object for the request
164 :param share: Share that will have its access rules updated.
165 :param access_rules: All access rules for given share.
166 :param add_rules: Empty List or List of access rules which should be
167 added. access_rules already contains these rules.
168 :param delete_rules: Empty List or List of access rules which should be
169 removed. access_rules doesn't contain these rules.
170 :param update_rules: Empty List or List of access rules which should be
171 updated. access_rules already contains these rules.
172 :param share_server: Data structure with share server information.
173 Not used by this driver.
174 """
176 hnas_share_id = self._get_hnas_share_id(share['id'])
178 try:
179 self._ensure_share(share, hnas_share_id)
180 except exception.HNASItemNotFoundException:
181 raise exception.ShareResourceNotFound(share_id=share['id'])
183 self._check_protocol(share['id'], share['share_proto'])
185 if share['share_proto'].lower() == 'nfs':
186 self._nfs_update_access(share, hnas_share_id, access_rules)
187 else:
188 if not (add_rules or delete_rules):
189 # recovery mode
190 self._clean_cifs_access_list(hnas_share_id)
191 self._cifs_allow_access(share, hnas_share_id, access_rules)
192 else:
193 self._cifs_deny_access(share, hnas_share_id, delete_rules)
194 self._cifs_allow_access(share, hnas_share_id, add_rules)
196 def _nfs_update_access(self, share, hnas_share_id, access_rules):
197 host_list = []
199 for rule in access_rules:
200 if rule['access_type'].lower() != 'ip':
201 msg = _("Only IP access type currently supported for NFS. "
202 "Share provided %(share)s with rule type "
203 "%(type)s.") % {'share': share['id'],
204 'type': rule['access_type']}
205 raise exception.InvalidShareAccess(reason=msg)
207 if rule['access_level'] == constants.ACCESS_LEVEL_RW:
208 host_list.append(rule['access_to'] + '(' +
209 rule['access_level'] +
210 ',norootsquash)')
211 else:
212 host_list.append(rule['access_to'] + '(' +
213 rule['access_level'] + ')')
215 self.hnas.update_nfs_access_rule(host_list, share_id=hnas_share_id)
217 if host_list:
218 LOG.debug("Share %(share)s has the rules: %(rules)s",
219 {'share': share['id'], 'rules': ', '.join(host_list)})
220 else:
221 LOG.debug("Share %(share)s has no rules.", {'share': share['id']})
223 def _cifs_allow_access(self, share_or_snapshot, hnas_id, add_rules,
224 is_snapshot=False):
226 entity_type = "share"
227 if is_snapshot:
228 entity_type = "snapshot"
230 for rule in add_rules:
231 if rule['access_type'].lower() != 'user':
232 msg = _("Only USER access type currently supported for CIFS. "
233 "%(entity_type)s provided %(share)s with "
234 "rule %(r_id)s type %(type)s allowing permission "
235 "to %(to)s.") % {
236 'entity_type': entity_type.capitalize(),
237 'share': share_or_snapshot['id'],
238 'type': rule['access_type'],
239 'r_id': rule['id'],
240 'to': rule['access_to'],
241 }
242 raise exception.InvalidShareAccess(reason=msg)
244 if rule['access_level'] == constants.ACCESS_LEVEL_RW:
245 # Adding permission acr = Allow Change&Read
246 permission = 'acr'
247 else:
248 # Adding permission ar = Allow Read
249 permission = 'ar'
251 formatted_user = rule['access_to'].replace('\\', '\\\\')
253 self.hnas.cifs_allow_access(hnas_id, formatted_user,
254 permission, is_snapshot=is_snapshot)
256 LOG.debug("Added %(rule)s rule for user/group %(user)s "
257 "to %(entity_type)s %(share)s.",
258 {'rule': rule['access_level'],
259 'user': rule['access_to'],
260 'entity_type': entity_type,
261 'share': share_or_snapshot['id']})
263 def _cifs_deny_access(self, share_or_snapshot, hnas_id, delete_rules,
264 is_snapshot=False):
265 if is_snapshot:
266 entity_type = "snapshot"
267 share_proto = share_or_snapshot['share']['share_proto']
268 else:
269 entity_type = "share"
270 share_proto = share_or_snapshot['share_proto']
272 for rule in delete_rules:
273 if rule['access_type'].lower() != 'user':
274 LOG.warning('Only USER access type is allowed for '
275 'CIFS. %(entity_type)s '
276 'provided %(share)s with '
277 'protocol %(proto)s.',
278 {'entity_type': entity_type.capitalize(),
279 'share': share_or_snapshot['id'],
280 'proto': share_proto})
281 continue
283 formatted_user = rule['access_to'].replace('\\', '\\\\')
285 self.hnas.cifs_deny_access(hnas_id, formatted_user,
286 is_snapshot=is_snapshot)
288 LOG.debug("Access denied for user/group %(user)s "
289 "to %(entity_type)s %(share)s.",
290 {'user': rule['access_to'],
291 'entity_type': entity_type,
292 'share': share_or_snapshot['id']})
294 def _clean_cifs_access_list(self, hnas_id, is_snapshot=False):
295 permission_list = self.hnas.list_cifs_permissions(hnas_id)
297 for permission in permission_list:
298 formatted_user = r'"\{1}{0}\{1}"'.format(permission[0], '"')
299 self.hnas.cifs_deny_access(hnas_id, formatted_user,
300 is_snapshot=is_snapshot)
302 def create_share(self, context, share, share_server=None):
303 r"""Creates share.
305 :param context: The `context.RequestContext` object for the request
306 :param share: Share that will be created.
307 :param share_server: Data structure with share server information.
308 Not used by this driver.
309 :returns: Returns a list of dicts containing the EVS IP concatenated
310 with the path of share in the filesystem.
312 Example for NFS::
314 [
316 {
318 'path': '172.24.44.10:/shares/id',
319 'metadata': {},
320 'is_admin_only': False
322 },
324 {
326 'path': '192.168.0.10:/shares/id',
327 'metadata': {},
328 'is_admin_only': True
330 }
332 ]
334 Example for CIFS::
336 [
338 {
340 'path': '\\172.24.44.10\id',
341 'metadata': {},
342 'is_admin_only': False
344 },
346 {
348 'path': '\\192.168.0.10\id',
349 'metadata': {},
350 'is_admin_only': True
352 }
354 ]
356 """
357 LOG.debug("Creating share in HNAS: %(shr)s.", {'shr': share['id']})
359 self._check_protocol(share['id'], share['share_proto'])
361 export_list = self._create_share(share['id'], share['size'],
362 share['share_proto'])
364 LOG.debug("Share %(share)s created successfully on path(s): "
365 "%(paths)s.",
366 {'paths': ', '.join([x['path'] for x in export_list]),
367 'share': share['id']})
369 return export_list
371 def delete_share(self, context, share, share_server=None):
372 """Deletes share.
374 :param context: The `context.RequestContext` object for the request
375 :param share: Share that will be deleted.
376 :param share_server: Data structure with share server information.
377 Not used by this driver.
378 """
379 hnas_share_id = self._get_hnas_share_id(share['id'])
381 LOG.debug("Deleting share in HNAS: %(shr)s.",
382 {'shr': share['id']})
384 self._delete_share(hnas_share_id, share['share_proto'])
386 LOG.debug("Export and share successfully deleted: %(shr)s.",
387 {'shr': share['id']})
389 def create_snapshot(self, context, snapshot, share_server=None):
390 """Creates snapshot.
392 :param context: The `context.RequestContext` object for the request
393 :param snapshot: Snapshot that will be created.
394 :param share_server: Data structure with share server information.
395 Not used by this driver.
396 """
397 hnas_share_id = self._get_hnas_share_id(snapshot['share_id'])
399 LOG.debug("The snapshot of share %(snap_share_id)s will be created "
400 "with id %(snap_id)s.",
401 {'snap_share_id': snapshot['share_id'],
402 'snap_id': snapshot['id']})
404 export_locations = self._create_snapshot(hnas_share_id, snapshot)
405 LOG.info("Snapshot %(id)s successfully created.",
406 {'id': snapshot['id']})
408 output = {
409 'provider_location': os.path.join(
410 '/snapshots', hnas_share_id, snapshot['id'])
411 }
413 if export_locations:
414 output['export_locations'] = export_locations
416 return output
418 def delete_snapshot(self, context, snapshot, share_server=None):
419 """Deletes snapshot.
421 :param context: The `context.RequestContext` object for the request
422 :param snapshot: Snapshot that will be deleted.
423 :param share_server: Data structure with share server information.
424 Not used by this driver.
425 """
426 hnas_share_id = self._get_hnas_share_id(snapshot['share_id'])
427 hnas_snapshot_id = self._get_hnas_snapshot_id(snapshot)
429 LOG.debug("The snapshot %(snap_id)s will be deleted. The related "
430 "share ID is %(snap_share_id)s.",
431 {'snap_id': snapshot['id'],
432 'snap_share_id': snapshot['share_id']})
434 self._delete_snapshot(snapshot['share'],
435 hnas_share_id, hnas_snapshot_id)
437 LOG.info("Snapshot %(id)s successfully deleted.",
438 {'id': snapshot['id']})
440 def create_share_from_snapshot(self, context, share, snapshot,
441 share_server=None, parent_share=None):
442 r"""Creates a new share from snapshot.
444 :param context: The `context.RequestContext` object for the request
445 :param share: Information about the new share.
446 :param snapshot: Information about the snapshot that will be copied
447 to new share.
448 :param share_server: Data structure with share server information.
449 Not used by this driver.
450 :returns: Returns a list of dicts containing the EVS IP concatenated
451 with the path of share in the filesystem.
453 Example for NFS::
455 [
457 {
459 'path': '172.24.44.10:/shares/id',
460 'metadata': {},
461 'is_admin_only': False
463 },
465 {
467 'path': '192.168.0.10:/shares/id',
468 'metadata': {},
469 'is_admin_only': True
471 }
473 ]
475 Example for CIFS::
477 [
479 {
481 'path': '\\172.24.44.10\id',
482 'metadata': {},
483 'is_admin_only': False
485 },
487 {
489 'path': '\\192.168.0.10\id',
490 'metadata': {},
491 'is_admin_only': True
493 }
495 ]
497 """
498 LOG.debug("Creating a new share from snapshot: %(ss_id)s.",
499 {'ss_id': snapshot['id']})
501 hnas_src_share_id = self._get_hnas_share_id(snapshot['share_id'])
502 hnas_src_snap_id = self._get_hnas_snapshot_id(snapshot)
504 export_list = self._create_share_from_snapshot(
505 share, hnas_src_share_id, hnas_src_snap_id)
507 LOG.debug("Share %(share)s created successfully on path(s): "
508 "%(paths)s.",
509 {'paths': ', '.join([x['path'] for x in export_list]),
510 'share': share['id']})
511 return export_list
513 def ensure_share(self, context, share, share_server=None):
514 r"""Ensure that share is exported.
516 :param context: The `context.RequestContext` object for the request
517 :param share: Share that will be checked.
518 :param share_server: Data structure with share server information.
519 Not used by this driver.
520 :returns: Returns a list of dicts containing the EVS IP concatenated
521 with the path of share in the filesystem.
523 Example for NFS::
525 [
527 {
529 'path': '172.24.44.10:/shares/id',
530 'metadata': {},
531 'is_admin_only': False
533 },
535 {
537 'path': '192.168.0.10:/shares/id',
538 'metadata': {},
539 'is_admin_only': True
541 }
543 ]
545 Example for CIFS::
547 [
549 {
551 'path': '\\172.24.44.10\id',
552 'metadata': {},
553 'is_admin_only': False
555 },
557 {
559 'path': '\\192.168.0.10\id',
560 'metadata': {},
561 'is_admin_only': True
563 }
565 ]
567 """
568 LOG.debug("Ensuring share in HNAS: %(shr)s.", {'shr': share['id']})
570 hnas_share_id = self._get_hnas_share_id(share['id'])
572 export_list = self._ensure_share(share, hnas_share_id)
574 LOG.debug("Share ensured in HNAS: %(shr)s, protocol %(proto)s.",
575 {'shr': share['id'], 'proto': share['share_proto']})
576 return export_list
578 def extend_share(self, share, new_size, share_server=None):
579 """Extends a share to new size.
581 :param share: Share that will be extended.
582 :param new_size: New size of share.
583 :param share_server: Data structure with share server information.
584 Not used by this driver.
585 """
586 hnas_share_id = self._get_hnas_share_id(share['id'])
588 LOG.debug("Expanding share in HNAS: %(shr_id)s.",
589 {'shr_id': share['id']})
591 self._extend_share(hnas_share_id, share, new_size)
592 LOG.info("Share %(shr_id)s successfully extended to "
593 "%(shr_size)s.",
594 {'shr_id': share['id'],
595 'shr_size': str(new_size)})
597 # TODO(alyson): Implement in DHSS = true mode
598 def get_network_allocations_number(self):
599 """Track allocations_number in DHSS = true.
601 When using the setting driver_handles_share_server = false
602 does not require to track allocations_number because we do not handle
603 network stuff.
604 """
605 return 0
607 def _update_share_stats(self, data=None):
608 """Updates the Capability of Backend."""
609 LOG.debug("Updating Backend Capability Information - Hitachi HNAS.")
611 self._check_fs_mounted()
613 total_space, free_space, dedupe = self.hnas.get_stats()
615 reserved = self.configuration.safe_get('reserved_share_percentage')
616 reserved_snapshot = self.configuration.safe_get(
617 'reserved_share_from_snapshot_percentage') or reserved
618 reserved_share_extend = self.configuration.safe_get(
619 'reserved_share_extend_percentage') or reserved
621 data = {
622 'share_backend_name': self.backend_name,
623 'driver_handles_share_servers': self.driver_handles_share_servers,
624 'vendor_name': 'Hitachi',
625 'driver_version': '4.0.0',
626 'storage_protocol': 'NFS_CIFS',
627 'total_capacity_gb': total_space,
628 'free_capacity_gb': free_space,
629 'reserved_percentage': reserved,
630 'reserved_snapshot_percentage': reserved_snapshot,
631 'reserved_share_extend_percentage': reserved_share_extend,
632 'qos': False,
633 'thin_provisioning': True,
634 'dedupe': dedupe,
635 'revert_to_snapshot_support': True,
636 'mount_snapshot_support': True,
637 }
639 LOG.info("HNAS Capabilities: %(data)s.",
640 {'data': str(data)})
642 super(HitachiHNASDriver, self)._update_share_stats(data)
644 def manage_existing(self, share, driver_options):
645 r"""Manages a share that exists on backend.
647 :param share: Share that will be managed.
648 :param driver_options: Empty dict or dict with 'volume_id' option.
649 :returns: Returns a dict with size of the share managed and a list of
650 dicts containing its export locations.
652 Example for NFS::
654 {
656 'size': 10,
657 'export_locations': [
659 {
661 'path': '172.24.44.10:/shares/id',
662 'metadata': {},
663 'is_admin_only': False
665 },
667 {
669 'path': '192.168.0.10:/shares/id',
670 'metadata': {},
671 'is_admin_only': True
673 }
675 ]
677 }
679 Example for CIFS::
681 {
683 'size': 10,
684 'export_locations': [
686 {
688 'path': '\\172.24.44.10\id',
689 'metadata': {},
690 'is_admin_only': False
692 },
694 {
696 'path': '\\192.168.0.10\id',
697 'metadata': {},
698 'is_admin_only': True
700 }
702 ]
704 }
706 """
707 hnas_share_id = self._get_hnas_share_id(share['id'])
709 # Make sure returned value is the same as provided,
710 # confirming it does not exist.
711 if hnas_share_id != share['id']:
712 msg = _("Share ID %s already exists, cannot manage.") % share['id']
713 raise exception.HNASBackendException(msg=msg)
715 self._check_protocol(share['id'], share['share_proto'])
717 if share['share_proto'].lower() == 'nfs':
718 # 10.0.0.1:/shares/example
719 LOG.info("Share %(shr_path)s will be managed with ID "
720 "%(shr_id)s.",
721 {'shr_path': share['export_locations'][0]['path'],
722 'shr_id': share['id']})
724 old_path_info = share['export_locations'][0]['path'].split(
725 ':/shares/')
727 if len(old_path_info) == 2:
728 evs_ip = old_path_info[0]
729 hnas_share_id = old_path_info[1]
730 else:
731 msg = _("Incorrect path. It should have the following format: "
732 "IP:/shares/share_id.")
733 raise exception.ShareBackendException(msg=msg)
734 else: # then its CIFS
735 # \\10.0.0.1\example
736 old_path = share['export_locations'][0]['path'].split('\\')
738 if len(old_path) == 4:
739 evs_ip = old_path[2]
740 hnas_share_id = old_path[3]
741 else:
742 msg = _("Incorrect path. It should have the following format: "
743 "\\\\IP\\share_id.")
744 raise exception.ShareBackendException(msg=msg)
746 if evs_ip != self.hnas_evs_ip:
747 msg = _("The EVS IP %(evs)s is not "
748 "configured.") % {'evs': evs_ip}
749 raise exception.ShareBackendException(msg=msg)
751 if self.backend_name not in share['host']:
752 msg = _("The backend passed in the host parameter (%(shr)s) is "
753 "not configured.") % {'shr': share['host']}
754 raise exception.ShareBackendException(msg=msg)
756 output = self._manage_existing(share, hnas_share_id)
757 self.private_storage.update(
758 share['id'], {'hnas_id': hnas_share_id})
760 LOG.debug("HNAS ID %(hnas_id)s has been saved to private storage for "
761 "Share ID %(share_id)s", {'hnas_id': hnas_share_id,
762 'share_id': share['id']})
764 LOG.info("Share %(shr_path)s was successfully managed with ID "
765 "%(shr_id)s.",
766 {'shr_path': share['export_locations'][0]['path'],
767 'shr_id': share['id']})
769 return output
771 def unmanage(self, share):
772 """Unmanages a share.
774 :param share: Share that will be unmanaged.
775 """
776 self.private_storage.delete(share['id'])
778 if len(share['export_locations']) == 0:
779 LOG.info("The share with ID %(shr_id)s is no longer being "
780 "managed.", {'shr_id': share['id']})
781 else:
782 LOG.info("The share with current path %(shr_path)s and ID "
783 "%(shr_id)s is no longer being managed.",
784 {'shr_path': share['export_locations'][0]['path'],
785 'shr_id': share['id']})
787 def shrink_share(self, share, new_size, share_server=None):
788 """Shrinks a share to new size.
790 :param share: Share that will be shrunk.
791 :param new_size: New size of share.
792 :param share_server: Data structure with share server information.
793 Not used by this driver.
794 """
795 hnas_share_id = self._get_hnas_share_id(share['id'])
797 LOG.debug("Shrinking share in HNAS: %(shr_id)s.",
798 {'shr_id': share['id']})
800 self._shrink_share(hnas_share_id, share, new_size)
801 LOG.info("Share %(shr_id)s successfully shrunk to "
802 "%(shr_size)sG.",
803 {'shr_id': share['id'],
804 'shr_size': str(new_size)})
806 def revert_to_snapshot(self, context, snapshot, share_access_rules,
807 snapshot_access_rules, share_server=None):
808 """Reverts a share to a given snapshot.
810 :param context: The `context.RequestContext` object for the request
811 :param snapshot: The snapshot to which the share is to be reverted to.
812 :param share_access_rules: List of all access rules for the affected
813 share. Not used by this driver.
814 :param snapshot_access_rules: List of all access rules for the affected
815 snapshot. Not used by this driver.
816 :param share_server: Data structure with share server information.
817 Not used by this driver.
818 """
820 hnas_share_id = self._get_hnas_share_id(snapshot['share_id'])
822 hnas_snapshot_id = self._get_hnas_snapshot_id(snapshot)
824 self._ensure_snapshot(snapshot, hnas_snapshot_id)
826 dest_path = os.path.join('/shares', hnas_share_id)
827 src_path = os.path.join('/snapshots', hnas_share_id, hnas_snapshot_id)
829 self.hnas.tree_delete(dest_path)
831 self.hnas.vvol_create(hnas_share_id)
833 self.hnas.quota_add(hnas_share_id, snapshot['size'])
835 try:
836 self.hnas.tree_clone(src_path, dest_path)
837 except exception.HNASNothingToCloneException:
838 LOG.warning("Source directory is empty, creating an empty "
839 "directory.")
841 LOG.info("Share %(share)s successfully reverted to snapshot "
842 "%(snapshot)s.", {'share': snapshot['share_id'],
843 'snapshot': snapshot['id']})
845 def _get_hnas_share_id(self, share_id):
846 hnas_id = self.private_storage.get(share_id, 'hnas_id')
848 if hnas_id is None:
849 hnas_id = share_id
851 LOG.debug("Share ID is %(shr_id)s and respective HNAS ID "
852 "is %(hnas_id)s.", {'shr_id': share_id,
853 'hnas_id': hnas_id})
855 return hnas_id
857 def _get_hnas_snapshot_id(self, snapshot):
858 hnas_snapshot_id = snapshot['id']
860 if snapshot['provider_location']: 860 ↛ 867line 860 didn't jump to line 867 because the condition on line 860 was always true
861 LOG.debug("Snapshot %(snap_id)s with provider_location: "
862 "%(p_loc)s.",
863 {'snap_id': hnas_snapshot_id,
864 'p_loc': snapshot['provider_location']})
865 hnas_snapshot_id = snapshot['provider_location'].split('/')[-1]
867 return hnas_snapshot_id
869 def _create_share(self, share_id, share_size, share_proto):
870 """Creates share.
872 Creates a virtual-volume, adds a quota limit and exports it.
873 :param share_id: manila's database ID of share that will be created.
874 :param share_size: Size limit of share.
875 :param share_proto: Protocol of share that will be created
876 (NFS or CIFS)
877 :returns: Returns a list of dicts containing the new share's export
878 locations.
880 """
881 self._check_fs_mounted()
883 self.hnas.vvol_create(share_id)
885 self.hnas.quota_add(share_id, share_size)
887 LOG.debug("Share created with id %(shr)s, size %(size)sG.",
888 {'shr': share_id, 'size': share_size})
890 self._create_export(share_id, share_proto)
892 export_list = self._get_export_locations(share_proto, share_id)
893 return export_list
895 def _create_export(self, share_id, share_proto, snapshot_id=None):
896 try:
897 if share_proto.lower() == 'nfs':
898 # Create NFS export
899 self.hnas.nfs_export_add(share_id, snapshot_id=snapshot_id)
900 LOG.debug("NFS Export created to %(shr)s.",
901 {'shr': share_id})
902 else:
903 # Create CIFS share with vvol path
904 self.hnas.cifs_share_add(share_id, snapshot_id=snapshot_id)
905 LOG.debug("CIFS share created to %(shr)s.",
906 {'shr': share_id})
907 except exception.HNASBackendException:
908 with excutils.save_and_reraise_exception():
909 if snapshot_id is None: 909 ↛ exitline 909 didn't jump to the function exit
910 self.hnas.vvol_delete(share_id)
912 def _check_fs_mounted(self):
913 mounted = self.hnas.check_fs_mounted()
914 if not mounted:
915 msg = _("Filesystem %s is not mounted.") % self.fs_name
916 raise exception.HNASBackendException(msg=msg)
918 def _ensure_share(self, share, hnas_share_id):
919 """Ensure that share is exported.
921 :param share: Share that will be checked.
922 :param hnas_share_id: HNAS ID of share that will be checked.
923 :returns: Returns a list of dicts containing the share's export
924 locations.
926 """
927 self._check_protocol(share['id'], share['share_proto'])
928 self._check_fs_mounted()
930 self.hnas.check_vvol(hnas_share_id)
931 self.hnas.check_quota(hnas_share_id)
933 if share['share_proto'].lower() == 'nfs':
934 self.hnas.check_export(hnas_share_id)
935 else:
936 self.hnas.check_cifs(hnas_share_id)
938 export_list = self._get_export_locations(
939 share['share_proto'], hnas_share_id)
941 return export_list
943 def _shrink_share(self, hnas_share_id, share, new_size):
944 """Shrinks a share to new size.
946 :param hnas_share_id: HNAS ID of share that will be shrunk.
947 :param share: model of share that will be shrunk.
948 :param new_size: New size of share after shrink operation.
949 """
950 self._ensure_share(share, hnas_share_id)
952 usage = self.hnas.get_share_usage(hnas_share_id)
954 LOG.debug("Usage space in share %(share)s: %(usage)sG",
955 {'share': share['id'], 'usage': usage})
957 if new_size > usage:
958 self.hnas.modify_quota(hnas_share_id, new_size)
959 else:
960 raise exception.ShareShrinkingPossibleDataLoss(
961 share_id=share['id'])
963 def _extend_share(self, hnas_share_id, share, new_size):
964 """Extends a share to new size.
966 :param hnas_share_id: HNAS ID of share that will be extended.
967 :param share: model of share that will be extended.
968 :param new_size: New size of share after extend operation.
969 """
970 self._ensure_share(share, hnas_share_id)
972 old_size = share['size']
973 available_space = self.hnas.get_stats()[1]
975 LOG.debug("Available space in filesystem: %(space)sG.",
976 {'space': available_space})
978 if (new_size - old_size) < available_space:
979 self.hnas.modify_quota(hnas_share_id, new_size)
980 else:
981 msg = (_("Share %s cannot be extended due to insufficient space.")
982 % share['id'])
983 raise exception.HNASBackendException(msg=msg)
985 def _delete_share(self, hnas_share_id, share_proto):
986 """Deletes share.
988 It uses tree-delete-job-submit to format and delete virtual-volumes.
989 Quota is deleted with virtual-volume.
990 :param hnas_share_id: HNAS ID of share that will be deleted.
991 :param share_proto: Protocol of share that will be deleted.
992 """
993 self._check_fs_mounted()
995 if share_proto.lower() == 'nfs':
996 self.hnas.nfs_export_del(hnas_share_id)
997 elif share_proto.lower() == 'cifs': 997 ↛ 999line 997 didn't jump to line 999 because the condition on line 997 was always true
998 self.hnas.cifs_share_del(hnas_share_id)
999 self.hnas.vvol_delete(hnas_share_id)
1001 def _manage_existing(self, share, hnas_share_id):
1002 """Manages a share that exists on backend.
1004 :param share: share that will be managed.
1005 :param hnas_share_id: HNAS ID of share that will be managed.
1006 :returns: Returns a dict with size of the share managed and a list of
1007 dicts containing its export locations.
1008 """
1009 self._ensure_share(share, hnas_share_id)
1011 share_size = self.hnas.get_share_quota(hnas_share_id)
1012 if share_size is None:
1013 msg = (_("The share %s trying to be managed does not have a "
1014 "quota limit, please set it before manage.")
1015 % share['id'])
1016 raise exception.ManageInvalidShare(reason=msg)
1018 export_list = self._get_export_locations(
1019 share['share_proto'], hnas_share_id)
1021 return {'size': share_size, 'export_locations': export_list}
1023 def _create_snapshot(self, hnas_share_id, snapshot):
1024 """Creates a snapshot of share.
1026 It copies the directory and all files to a new directory inside
1027 /snapshots/share_id/.
1028 :param hnas_share_id: HNAS ID of share for snapshot.
1029 :param snapshot: Snapshot that will be created.
1030 """
1031 self._ensure_share(snapshot['share'], hnas_share_id)
1032 saved_list = []
1034 share_proto = snapshot['share']['share_proto']
1035 self._check_protocol(snapshot['share_id'], share_proto)
1037 if share_proto.lower() == 'nfs':
1038 saved_list = self.hnas.get_nfs_host_list(hnas_share_id)
1039 new_list = []
1040 for access in saved_list:
1041 for rw in ('read_write', 'readwrite', 'rw'):
1042 access = access.replace(rw, 'ro')
1043 new_list.append(access)
1044 self.hnas.update_nfs_access_rule(new_list, share_id=hnas_share_id)
1045 else: # CIFS
1046 if (self.hnas.is_cifs_in_use(hnas_share_id) and
1047 not self.cifs_snapshot):
1048 msg = _("CIFS snapshot when share is mounted is disabled. "
1049 "Set hitachi_hnas_allow_cifs_snapshot_while_mounted to"
1050 " True or unmount the share to take a snapshot.")
1051 raise exception.ShareBackendException(msg=msg)
1053 src_path = os.path.join('/shares', hnas_share_id)
1054 dest_path = os.path.join('/snapshots', hnas_share_id, snapshot['id'])
1055 try:
1056 self.hnas.tree_clone(src_path, dest_path)
1057 except exception.HNASNothingToCloneException:
1058 LOG.warning("Source directory is empty, creating an empty "
1059 "directory.")
1060 self.hnas.create_directory(dest_path)
1061 finally:
1062 if share_proto.lower() == 'nfs':
1063 self.hnas.update_nfs_access_rule(saved_list,
1064 share_id=hnas_share_id)
1066 export_locations = []
1068 if snapshot['share'].get('mount_snapshot_support'):
1069 self._create_export(hnas_share_id, share_proto,
1070 snapshot_id=snapshot['id'])
1071 export_locations = self._get_export_locations(
1072 share_proto, snapshot['id'], is_snapshot=True)
1074 return export_locations
1076 def _delete_snapshot(self, share, hnas_share_id, snapshot_id):
1077 """Deletes snapshot.
1079 It receives the hnas_share_id only to join the path for snapshot.
1080 :param hnas_share_id: HNAS ID of share from which snapshot was taken.
1081 :param snapshot_id: ID of snapshot.
1082 """
1083 self._check_fs_mounted()
1084 share_proto = share['share_proto']
1086 if share.get('mount_snapshot_support'):
1087 if share_proto.lower() == 'nfs':
1088 self.hnas.nfs_export_del(snapshot_id=snapshot_id)
1089 elif share_proto.lower() == 'cifs': 1089 ↛ 1092line 1089 didn't jump to line 1092 because the condition on line 1089 was always true
1090 self.hnas.cifs_share_del(snapshot_id)
1092 path = os.path.join('/snapshots', hnas_share_id, snapshot_id)
1093 self.hnas.tree_delete(path)
1094 path = os.path.join('/snapshots', hnas_share_id)
1095 self.hnas.delete_directory(path)
1097 def _create_share_from_snapshot(self, share, src_hnas_share_id,
1098 hnas_snapshot_id):
1099 """Creates a new share from snapshot.
1101 It copies everything from snapshot directory to a new vvol,
1102 set a quota limit for it and export.
1103 :param share: a dict from new share.
1104 :param src_hnas_share_id: HNAS ID of share from which snapshot was
1105 taken.
1106 :param hnas_snapshot_id: HNAS ID from snapshot that will be copied to
1107 new share.
1108 :returns: Returns a list of dicts containing the new share's export
1109 locations.
1110 """
1111 dest_path = os.path.join('/shares', share['id'])
1112 src_path = os.path.join('/snapshots', src_hnas_share_id,
1113 hnas_snapshot_id)
1115 # Before copying everything to new vvol, we need to create it,
1116 # because we only can transform an empty directory into a vvol.
1118 self._check_fs_mounted()
1120 self.hnas.vvol_create(share['id'])
1122 self.hnas.quota_add(share['id'], share['size'])
1124 try:
1125 self.hnas.tree_clone(src_path, dest_path)
1126 except exception.HNASNothingToCloneException:
1127 LOG.warning("Source directory is empty, exporting "
1128 "directory.")
1130 self._check_protocol(share['id'], share['share_proto'])
1132 try:
1133 if share['share_proto'].lower() == 'nfs':
1134 self.hnas.nfs_export_add(share['id'])
1136 else:
1137 self.hnas.cifs_share_add(share['id'])
1138 except exception.HNASBackendException:
1139 with excutils.save_and_reraise_exception():
1140 self.hnas.vvol_delete(share['id'])
1142 return self._get_export_locations(
1143 share['share_proto'], share['id'])
1145 def _check_protocol(self, share_id, protocol):
1146 if protocol.lower() not in ('nfs', 'cifs'):
1147 msg = _("Only NFS or CIFS protocol are currently supported. "
1148 "Share provided %(share)s with protocol "
1149 "%(proto)s.") % {'share': share_id,
1150 'proto': protocol}
1151 raise exception.ShareBackendException(msg=msg)
1153 def _get_export_locations(self, share_proto, hnas_id, is_snapshot=False):
1154 export_list = []
1155 for ip in (self.hnas_evs_ip, self.hnas_admin_network_ip):
1156 if ip: 1156 ↛ 1155line 1156 didn't jump to line 1155 because the condition on line 1156 was always true
1157 path = self._get_export_path(ip, share_proto, hnas_id,
1158 is_snapshot)
1159 export_list.append({
1160 "path": path,
1161 "is_admin_only": ip == self.hnas_admin_network_ip,
1162 "metadata": {},
1163 })
1164 return export_list
1166 def _get_export_path(self, ip, share_proto, hnas_id, is_snapshot):
1167 r"""Gets and returns export path.
1169 :param ip: IP from HNAS EVS configured.
1170 :param share_proto: Share or snapshot protocol (NFS or CIFS).
1171 :param hnas_id: Entity ID in HNAS, it can be the ID from a share or
1172 a snapshot.
1173 :param is_snapshot: Boolean to determine if export is related to a
1174 share or a snapshot.
1175 :return: Complete export path, for example:
1176 - In NFS:
1177 SHARE: 172.24.44.10:/shares/id
1178 SNAPSHOT: 172.24.44.10:/snapshots/id
1179 - In CIFS:
1180 SHARE and SNAPSHOT: \\172.24.44.10\id
1181 """
1182 if share_proto.lower() == 'nfs':
1183 if is_snapshot:
1184 path = os.path.join('/snapshots', hnas_id)
1185 else:
1186 path = os.path.join('/shares', hnas_id)
1187 export = ':'.join((ip, path))
1188 else:
1189 export = r'\\%s\%s' % (ip, hnas_id)
1190 return export
1192 def _ensure_snapshot(self, snapshot, hnas_snapshot_id):
1193 """Ensure that snapshot is exported.
1195 :param snapshot: Snapshot that will be checked.
1196 :param hnas_snapshot_id: HNAS ID of snapshot that will be checked.
1198 :returns: Returns a list of dicts containing the snapshot's export
1199 locations or None if mount_snapshot_support is False.
1200 """
1201 self._check_protocol(snapshot['share_id'],
1202 snapshot['share']['share_proto'])
1203 self._check_fs_mounted()
1205 self.hnas.check_directory(snapshot['provider_location'])
1207 export_list = None
1208 if snapshot['share'].get('mount_snapshot_support'):
1209 if snapshot['share']['share_proto'].lower() == 'nfs':
1210 self.hnas.check_export(hnas_snapshot_id, is_snapshot=True)
1211 else:
1212 self.hnas.check_cifs(hnas_snapshot_id)
1214 export_list = self._get_export_locations(
1215 snapshot['share']['share_proto'],
1216 hnas_snapshot_id,
1217 is_snapshot=True)
1219 return export_list
1221 def ensure_snapshot(self, context, snapshot, share_server=None):
1222 r"""Ensure that snapshot is exported.
1224 :param context: The `context.RequestContext` object for the request.
1225 :param snapshot: Snapshot that will be checked.
1226 :param share_server: Data structure with share server information.
1227 Not used by this driver.
1229 :returns: Returns a list of dicts containing the EVS IP concatenated
1230 with the path of snapshot in the filesystem or None if
1231 mount_snapshot_support is False.
1233 Example for NFS::
1235 [
1237 {
1239 'path': '172.24.44.10:/snapshots/id',
1240 'metadata': {},
1241 'is_admin_only': False
1243 },
1245 {
1247 'path': '192.168.0.10:/snapshots/id',
1248 'metadata': {},
1249 'is_admin_only': True
1251 }
1253 ]
1255 Example for CIFS::
1257 [
1259 {
1261 'path': '\\172.24.44.10\id',
1262 'metadata': {},
1263 'is_admin_only': False
1265 },
1267 {
1269 'path': '\\192.168.0.10\id',
1270 'metadata': {},
1271 'is_admin_only': True
1273 }
1275 ]
1277 """
1278 LOG.debug("Ensuring snapshot in HNAS: %(snap)s.",
1279 {'snap': snapshot['id']})
1281 hnas_snapshot_id = self._get_hnas_snapshot_id(snapshot)
1283 export_list = self._ensure_snapshot(snapshot, hnas_snapshot_id)
1285 LOG.debug("Snapshot ensured in HNAS: %(snap)s, protocol %(proto)s.",
1286 {'snap': snapshot['id'],
1287 'proto': snapshot['share']['share_proto']})
1288 return export_list
1290 def manage_existing_snapshot(self, snapshot, driver_options):
1291 """Manages a snapshot that exists only in HNAS.
1293 The snapshot to be managed should be in the path
1294 /snapshots/SHARE_ID/SNAPSHOT_ID. Also, the size of snapshot should be
1295 provided as --driver_options size=<size>.
1296 :param snapshot: snapshot that will be managed.
1297 :param driver_options: expects only one key 'size'. It must be
1298 provided in order to manage a snapshot.
1300 :returns: Returns a dict with size of snapshot managed
1301 """
1302 try:
1303 snapshot_size = int(driver_options.get("size", 0))
1304 except (ValueError, TypeError):
1305 msg = _("The size in driver options to manage snapshot "
1306 "%(snap_id)s should be an integer, in format "
1307 "driver-options size=<SIZE>. Value passed: "
1308 "%(size)s.") % {'snap_id': snapshot['id'],
1309 'size': driver_options.get("size")}
1310 raise exception.ManageInvalidShareSnapshot(reason=msg)
1312 if snapshot_size == 0:
1313 msg = _("Snapshot %(snap_id)s has no size specified for manage. "
1314 "Please, provide the size with parameter driver-options "
1315 "size=<SIZE>.") % {'snap_id': snapshot['id']}
1316 raise exception.ManageInvalidShareSnapshot(reason=msg)
1318 hnas_share_id = self._get_hnas_share_id(snapshot['share_id'])
1320 LOG.debug("Path provided to manage snapshot: %(path)s.",
1321 {'path': snapshot['provider_location']})
1323 path_info = snapshot['provider_location'].split('/')
1325 if len(path_info) == 4 and path_info[1] == 'snapshots':
1326 path_share_id = path_info[2]
1327 hnas_snapshot_id = path_info[3]
1328 else:
1329 msg = (_("Incorrect path %(path)s for manage snapshot "
1330 "%(snap_id)s. It should have the following format: "
1331 "/snapshots/SHARE_ID/SNAPSHOT_ID.") %
1332 {'path': snapshot['provider_location'],
1333 'snap_id': snapshot['id']})
1334 raise exception.ManageInvalidShareSnapshot(reason=msg)
1336 if hnas_share_id != path_share_id:
1337 msg = _("The snapshot %(snap_id)s does not belong to share "
1338 "%(share_id)s.") % {'snap_id': snapshot['id'],
1339 'share_id': snapshot['share_id']}
1340 raise exception.ManageInvalidShareSnapshot(reason=msg)
1342 if not self.hnas.check_directory(snapshot['provider_location']):
1343 msg = _("Snapshot %(snap_id)s does not exist in "
1344 "HNAS.") % {'snap_id': hnas_snapshot_id}
1345 raise exception.ManageInvalidShareSnapshot(reason=msg)
1347 try:
1348 self._ensure_snapshot(snapshot, hnas_snapshot_id)
1349 except exception.HNASItemNotFoundException:
1350 LOG.warning("Export does not exist for snapshot %s, "
1351 "creating a new one.", snapshot['id'])
1352 self._create_export(hnas_share_id,
1353 snapshot['share']['share_proto'],
1354 snapshot_id=hnas_snapshot_id)
1356 output = {'size': snapshot_size}
1357 if snapshot['share'].get('mount_snapshot_support'):
1358 export_locations = self._get_export_locations(
1359 snapshot['share']['share_proto'],
1360 hnas_snapshot_id,
1361 is_snapshot=True)
1362 output['export_locations'] = export_locations
1364 LOG.info("Snapshot %(snap_path)s for share %(shr_id)s was "
1365 "successfully managed with ID %(snap_id)s.",
1366 {'snap_path': snapshot['provider_location'],
1367 'shr_id': snapshot['share_id'],
1368 'snap_id': snapshot['id']})
1370 return output
1372 def unmanage_snapshot(self, snapshot):
1373 """Unmanage a share snapshot
1375 :param snapshot: Snapshot that will be unmanaged.
1376 """
1377 LOG.info("The snapshot with ID %(snap_id)s from share "
1378 "%(share_id)s is no longer being managed by Manila. "
1379 "However, it is not deleted and can be found in HNAS.",
1380 {'snap_id': snapshot['id'],
1381 'share_id': snapshot['share_id']})
1383 def snapshot_update_access(self, context, snapshot, access_rules,
1384 add_rules, delete_rules, share_server=None):
1385 """Update access rules for given snapshot.
1387 Drivers should support 2 different cases in this method:
1388 1. Recovery after error - 'access_rules' contains all access rules,
1389 'add_rules' and 'delete_rules' shall be empty. Driver should clear any
1390 existent access rules and apply all access rules for given snapshot.
1391 This recovery is made at driver start up.
1393 2. Adding/Deleting of several access rules - 'access_rules' contains
1394 all access rules, 'add_rules' and 'delete_rules' contain rules which
1395 should be added/deleted. Driver can ignore rules in 'access_rules' and
1396 apply only rules from 'add_rules' and 'delete_rules'. All snapshots
1397 rules should be read only.
1399 :param context: Current context
1400 :param snapshot: Snapshot model with snapshot data.
1401 :param access_rules: All access rules for given snapshot
1402 :param add_rules: Empty List or List of access rules which should be
1403 added. access_rules already contains these rules.
1404 :param delete_rules: Empty List or List of access rules which should be
1405 removed. access_rules doesn't contain these rules.
1406 :param share_server: None or Share server model
1407 """
1408 hnas_snapshot_id = self._get_hnas_snapshot_id(snapshot)
1410 self._ensure_snapshot(snapshot, hnas_snapshot_id)
1412 access_rules, add_rules, delete_rules = utils.change_rules_to_readonly(
1413 access_rules, add_rules, delete_rules)
1415 if snapshot['share']['share_proto'].lower() == 'nfs':
1416 host_list = []
1418 for rule in access_rules:
1419 if rule['access_type'].lower() != 'ip':
1420 msg = _("Only IP access type currently supported for NFS. "
1421 "Snapshot provided %(snapshot)s with rule type "
1422 "%(type)s.") % {'snapshot': snapshot['id'],
1423 'type': rule['access_type']}
1424 raise exception.InvalidSnapshotAccess(reason=msg)
1426 host_list.append(rule['access_to'] + '(ro)')
1428 self.hnas.update_nfs_access_rule(host_list,
1429 snapshot_id=hnas_snapshot_id)
1431 if host_list:
1432 LOG.debug("Snapshot %(snapshot)s has the rules: %(rules)s",
1433 {'snapshot': snapshot['id'],
1434 'rules': ', '.join(host_list)})
1435 else:
1436 LOG.debug("Snapshot %(snapshot)s has no rules.",
1437 {'snapshot': snapshot['id']})
1438 else:
1439 if not (add_rules or delete_rules):
1440 # cifs recovery mode
1441 self._clean_cifs_access_list(hnas_snapshot_id,
1442 is_snapshot=True)
1443 self._cifs_allow_access(snapshot, hnas_snapshot_id,
1444 access_rules, is_snapshot=True)
1445 else:
1446 self._cifs_deny_access(snapshot, hnas_snapshot_id,
1447 delete_rules, is_snapshot=True)
1448 self._cifs_allow_access(snapshot, hnas_snapshot_id,
1449 add_rules, is_snapshot=True)