Coverage for manila/share/drivers/qnap/qnap.py: 94%
440 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 QNAP 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 QNAP Storage.
17This driver supports QNAP Storage for NFS.
18"""
19import datetime
20import math
21import re
22import time
24from oslo_config import cfg
25from oslo_log import log as logging
26from oslo_utils import timeutils
27from oslo_utils import units
29from manila.common import constants
30from manila import exception
31from manila.i18n import _
32from manila import share
33from manila.share import driver
34from manila.share.drivers.qnap import api
35from manila.share import share_types
36from manila import utils
38LOG = logging.getLogger(__name__)
40qnap_manila_opts = [
41 cfg.StrOpt('qnap_management_url',
42 required=True,
43 help='The URL to manage QNAP Storage.'),
44 cfg.HostAddressOpt('qnap_share_ip',
45 required=True,
46 help='NAS share IP for mounting shares.'),
47 cfg.StrOpt('qnap_nas_login',
48 required=True,
49 help='Username for QNAP storage.'),
50 cfg.StrOpt('qnap_nas_password',
51 required=True,
52 secret=True,
53 help='Password for QNAP storage.'),
54 cfg.StrOpt('qnap_poolname',
55 required=True,
56 help='Pool within which QNAP shares must be created.'),
57]
59CONF = cfg.CONF
60CONF.register_opts(qnap_manila_opts)
63class QnapShareDriver(driver.ShareDriver):
64 """OpenStack driver to enable QNAP Storage.
66 Version history:
67 1.0.0 - Initial driver (Only NFS)
68 1.0.1 - Add support for QES fw 1.1.4.
69 1.0.2 - Fix bug #1736370, QNAP Manila driver: Access rule setting is
70 override by the another access rule.
71 1.0.3 - Add supports for Thin Provisioning, SSD Cache, Deduplication
72 and Compression.
73 1.0.4 - Add support for QES fw 2.0.0.
74 1.0.5 - Fix bug #1773761, when user tries to manage share, the size
75 of managed share should not be changed.
76 1.0.6 - Add support for QES fw 2.1.0.
77 1.0.7 - Add support for QES fw on TDS series NAS model.
78 1.0.8 - Fix bug, driver should not manage snapshot which does not
79 exist in NAS.
80 Fix bug, driver should create share from snapshot with
81 specified size.
82 """
84 DRIVER_VERSION = '1.0.8'
86 def __init__(self, *args, **kwargs):
87 """Initialize QnapShareDriver."""
88 super(QnapShareDriver, self).__init__(False, *args, **kwargs)
89 self.private_storage = kwargs.get('private_storage')
90 self.api_executor = None
91 self.group_stats = {}
92 self.configuration.append_config_values(qnap_manila_opts)
93 self.share_api = share.API()
95 def do_setup(self, context):
96 """Setup the QNAP Manila share driver."""
97 self.ctxt = context
98 LOG.debug('context: %s', context)
100 # Setup API Executor
101 try:
102 self.api_executor = self._create_api_executor()
103 except Exception:
104 LOG.exception('Failed to create HTTP client. Check IP '
105 'address, port, username, password and make '
106 'sure the array version is compatible.')
107 raise
109 def check_for_setup_error(self):
110 """Check the status of setup."""
111 if self.api_executor is None: 111 ↛ exitline 111 didn't return from function 'check_for_setup_error' because the condition on line 111 was always true
112 msg = _("Failed to instantiate API client to communicate with "
113 "QNAP storage systems.")
114 raise exception.ShareBackendException(msg=msg)
116 def _create_api_executor(self):
117 """Create API executor by NAS model."""
118 """LOG.debug('CONF.qnap_nas_login=%(conf)s',
119 {'conf': CONF.qnap_nas_login})
120 LOG.debug('self.configuration.qnap_nas_login=%(conf)s',
121 {'conf': self.configuration.qnap_nas_login})"""
122 self.api_executor = api.QnapAPIExecutor(
123 username=self.configuration.qnap_nas_login,
124 password=self.configuration.qnap_nas_password,
125 management_url=self.configuration.qnap_management_url)
127 display_model_name, internal_model_name, fw_version = (
128 self.api_executor.get_basic_info(
129 self.configuration.qnap_management_url))
131 pattern = re.compile(r"^([A-Z]+)-?[A-Z]{0,2}(\d+)\d{2}(U|[a-z]*)")
132 matches = pattern.match(display_model_name)
134 if not matches: 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true
135 return None
136 model_type = matches.group(1)
138 ts_model_types = (
139 "TS", "SS", "IS", "TVS", "TBS"
140 )
141 tes_model_types = (
142 "TES", "TDS"
143 )
144 es_model_types = (
145 "ES",
146 )
148 if model_type in ts_model_types:
149 if (fw_version.startswith("4.2") or fw_version.startswith("4.3")):
150 LOG.debug('Create TS API Executor')
151 # modify the pool name to pool index
152 self.configuration.qnap_poolname = (
153 self._get_ts_model_pool_id(
154 self.configuration.qnap_poolname))
156 return api.QnapAPIExecutorTS(
157 username=self.configuration.qnap_nas_login,
158 password=self.configuration.qnap_nas_password,
159 management_url=self.configuration.qnap_management_url)
160 elif model_type in tes_model_types:
161 if 'TS' in internal_model_name:
162 if (fw_version.startswith("4.2") or
163 fw_version.startswith("4.3")):
164 LOG.debug('Create TS API Executor')
165 # modify the pool name to pool index
166 self.configuration.qnap_poolname = (
167 self._get_ts_model_pool_id(
168 self.configuration.qnap_poolname))
169 return api.QnapAPIExecutorTS(
170 username=self.configuration.qnap_nas_login,
171 password=self.configuration.qnap_nas_password,
172 management_url=self.configuration.qnap_management_url)
173 elif "1.1.2" <= fw_version <= "2.1.9999":
174 LOG.debug('Create ES API Executor')
175 return api.QnapAPIExecutor(
176 username=self.configuration.qnap_nas_login,
177 password=self.configuration.qnap_nas_password,
178 management_url=self.configuration.qnap_management_url)
179 elif model_type in es_model_types:
180 if "1.1.2" <= fw_version <= "2.1.9999":
181 LOG.debug('Create ES API Executor')
182 return api.QnapAPIExecutor(
183 username=self.configuration.qnap_nas_login,
184 password=self.configuration.qnap_nas_password,
185 management_url=self.configuration.qnap_management_url)
187 msg = _('QNAP Storage model is not supported by this driver.')
188 raise exception.ShareBackendException(msg=msg)
190 def _get_ts_model_pool_id(self, pool_name):
191 """Modify the pool name to pool index."""
192 pattern = re.compile(r"^(\d+)+|^Storage Pool (\d+)+")
193 matches = pattern.match(pool_name)
194 if matches.group(1):
195 return matches.group(1)
196 else:
197 return matches.group(2)
199 @utils.synchronized('qnap-gen_name')
200 def _gen_random_name(self, type):
201 if type == 'share':
202 infix = "shr-"
203 elif type == 'snapshot':
204 infix = "snp-"
205 elif type == 'host':
206 infix = "hst-"
207 else:
208 infix = ""
209 return ("manila-%(ifx)s%(time)s" %
210 {'ifx': infix,
211 'time': timeutils.utcnow().strftime('%Y%m%d%H%M%S%f')})
213 def _gen_host_name(self, vol_name_timestamp, access_level):
214 # host_name will be manila-{vol_name_timestamp}-ro or
215 # manila-{vol_name_timestamp}-rw
216 return 'manila-{}-{}'.format(vol_name_timestamp, access_level)
218 def _get_timestamp_from_vol_name(self, vol_name):
219 vol_name_split = vol_name.split('-')
220 dt = datetime.datetime.strptime(vol_name_split[2], '%Y%m%d%H%M%S%f')
221 return int(time.mktime(dt.timetuple()))
223 def _get_location_path(self, share_name, share_proto, ip, vol_id):
224 if share_proto == 'NFS':
225 vol = self.api_executor.get_specific_volinfo(vol_id)
226 vol_mount_path = vol.find('vol_mount_path').text
228 location = '%s:%s' % (ip, vol_mount_path)
229 else:
230 msg = _('Invalid NAS protocol: %s') % share_proto
231 raise exception.InvalidInput(reason=msg)
233 export_location = {
234 'path': location,
235 'is_admin_only': False,
236 }
237 return export_location
239 def _update_share_stats(self):
240 """Get latest share stats."""
241 backend_name = (self.configuration.safe_get(
242 'share_backend_name') or
243 self.__class__.__name__)
244 LOG.debug('backend_name=%(backend_name)s',
245 {'backend_name': backend_name})
247 selected_pool = self.api_executor.get_specific_poolinfo(
248 self.configuration.qnap_poolname)
249 total_capacity_gb = (int(selected_pool.find('capacity_bytes').text) /
250 units.Gi)
251 LOG.debug('total_capacity_gb: %s GB', total_capacity_gb)
252 free_capacity_gb = (int(selected_pool.find('freesize_bytes').text) /
253 units.Gi)
254 LOG.debug('free_capacity_gb: %s GB', free_capacity_gb)
255 alloc_capacity_gb = (int(selected_pool.find('allocated_bytes').text) /
256 units.Gi)
257 LOG.debug('allocated_capacity_gb: %s GB', alloc_capacity_gb)
259 reserved_percentage = self.configuration.safe_get(
260 'reserved_share_percentage')
262 reserved_snapshot_percentage = self.configuration.safe_get(
263 'reserved_share_from_snapshot_percentage') or reserved_percentage
265 reserved_shr_extend_percentage = self.configuration.safe_get(
266 'reserved_share_extend_percentage') or reserved_percentage
268 # single pool now, need support multiple pools in the future
269 single_pool = {
270 "pool_name": self.configuration.qnap_poolname,
271 "total_capacity_gb": total_capacity_gb,
272 "free_capacity_gb": free_capacity_gb,
273 "allocated_capacity_gb": alloc_capacity_gb,
274 "reserved_percentage": reserved_percentage,
275 "reserved_snapshot_percentage": reserved_snapshot_percentage,
276 "reserved_share_extend_percentage": reserved_shr_extend_percentage,
277 "qos": False,
278 "dedupe": [True, False],
279 "compression": [True, False],
280 "thin_provisioning": [True, False],
281 "qnap_ssd_cache": [True, False]
282 }
284 data = {
285 "share_backend_name": backend_name,
286 "vendor_name": "QNAP",
287 "driver_version": self.DRIVER_VERSION,
288 "storage_protocol": "NFS",
289 "snapshot_support": True,
290 "create_share_from_snapshot_support": True,
291 "driver_handles_share_servers": self.configuration.safe_get(
292 'driver_handles_share_servers'),
293 'pools': [single_pool],
294 }
295 super(QnapShareDriver, self)._update_share_stats(data)
297 @utils.retry(retry_param=exception.ShareBackendException,
298 interval=3,
299 retries=5)
300 @utils.synchronized('qnap-create_share')
301 def create_share(self, context, share, share_server=None):
302 """Create a new share."""
303 LOG.debug('share: %s', share.__dict__)
304 extra_specs = share_types.get_extra_specs_from_share(share)
305 LOG.debug('extra_specs: %s', extra_specs)
306 qnap_thin_provision = share_types.parse_boolean_extra_spec(
307 'thin_provisioning', extra_specs.get("thin_provisioning") or
308 extra_specs.get('capabilities:thin_provisioning') or 'true')
309 qnap_compression = share_types.parse_boolean_extra_spec(
310 'compression', extra_specs.get("compression") or
311 extra_specs.get('capabilities:compression') or 'true')
312 qnap_deduplication = share_types.parse_boolean_extra_spec(
313 'dedupe', extra_specs.get("dedupe") or
314 extra_specs.get('capabilities:dedupe') or 'false')
315 qnap_ssd_cache = share_types.parse_boolean_extra_spec(
316 'qnap_ssd_cache', extra_specs.get("qnap_ssd_cache") or
317 extra_specs.get("capabilities:qnap_ssd_cache") or 'false')
318 LOG.debug('qnap_thin_provision: %(qnap_thin_provision)s '
319 'qnap_compression: %(qnap_compression)s '
320 'qnap_deduplication: %(qnap_deduplication)s '
321 'qnap_ssd_cache: %(qnap_ssd_cache)s',
322 {'qnap_thin_provision': qnap_thin_provision,
323 'qnap_compression': qnap_compression,
324 'qnap_deduplication': qnap_deduplication,
325 'qnap_ssd_cache': qnap_ssd_cache})
327 share_proto = share['share_proto']
329 # User could create two shares with the same name on horizon.
330 # Therefore, we should not use displayname to create shares on NAS.
331 create_share_name = self._gen_random_name("share")
332 # If share name exists, need to change to another name.
333 created_share = self.api_executor.get_share_info(
334 self.configuration.qnap_poolname,
335 vol_label=create_share_name)
336 LOG.debug('created_share: %s', created_share)
337 if created_share is not None:
338 msg = (_("The share name %s is used by other share on NAS.") %
339 create_share_name)
340 LOG.error(msg)
341 raise exception.ShareBackendException(msg=msg)
343 if (qnap_deduplication and not qnap_thin_provision):
344 msg = _("Dedupe cannot be enabled without thin_provisioning.")
345 LOG.debug('Dedupe cannot be enabled without thin_provisioning.')
346 raise exception.InvalidExtraSpec(reason=msg)
347 self.api_executor.create_share(
348 share,
349 self.configuration.qnap_poolname,
350 create_share_name,
351 share_proto,
352 qnap_thin_provision=qnap_thin_provision,
353 qnap_compression=qnap_compression,
354 qnap_deduplication=qnap_deduplication,
355 qnap_ssd_cache=qnap_ssd_cache)
356 created_share = self._get_share_info(create_share_name)
357 volID = created_share.find('vol_no').text
358 # Use private_storage to record volume ID and Name created in the NAS.
359 LOG.debug('volID: %(volID)s '
360 'volName: %(create_share_name)s',
361 {'volID': volID,
362 'create_share_name': create_share_name})
363 _metadata = {'volID': volID,
364 'volName': create_share_name,
365 'thin_provision': qnap_thin_provision,
366 'compression': qnap_compression,
367 'deduplication': qnap_deduplication,
368 'ssd_cache': qnap_ssd_cache}
369 self.private_storage.update(share['id'], _metadata)
371 return self._get_location_path(create_share_name,
372 share['share_proto'],
373 self.configuration.qnap_share_ip,
374 volID)
376 @utils.retry(retry_param=exception.ShareBackendException,
377 interval=5, retries=5, backoff_rate=1)
378 def _get_share_info(self, share_name):
379 share = self.api_executor.get_share_info(
380 self.configuration.qnap_poolname,
381 vol_label=share_name)
382 if share is None:
383 msg = _("Fail to get share info of %s on NAS.") % share_name
384 LOG.error(msg)
385 raise exception.ShareBackendException(msg=msg)
386 else:
387 return share
389 @utils.synchronized('qnap-delete_share')
390 def delete_share(self, context, share, share_server=None):
391 """Delete the specified share."""
392 # Use private_storage to retrieve volume ID created in the NAS.
393 volID = self.private_storage.get(share['id'], 'volID')
394 if not volID:
395 LOG.warning('volID for Share %s does not exist', share['id'])
396 return
397 LOG.debug('volID: %s', volID)
399 del_share = self.api_executor.get_share_info(
400 self.configuration.qnap_poolname,
401 vol_no=volID)
402 if del_share is None:
403 LOG.warning('Share %s does not exist', share['id'])
404 return
406 vol_no = del_share.find('vol_no').text
408 self.api_executor.delete_share(vol_no)
409 self.private_storage.delete(share['id'])
411 @utils.synchronized('qnap-extend_share')
412 def extend_share(self, share, new_size, share_server=None):
413 """Extend an existing share."""
414 LOG.debug('Entering extend_share share_name=%(share_name)s '
415 'share_id=%(share_id)s '
416 'new_size=%(size)s',
417 {'share_name': share['display_name'],
418 'share_id': share['id'],
419 'size': new_size})
421 # Use private_storage to retrieve volume Name created in the NAS.
422 volName = self.private_storage.get(share['id'], 'volName')
423 if not volName:
424 LOG.debug('Share %s does not exist', share['id'])
425 raise exception.ShareResourceNotFound(share_id=share['id'])
426 LOG.debug('volName: %s', volName)
427 thin_provision = self.private_storage.get(
428 share['id'], 'thin_provision')
429 compression = self.private_storage.get(share['id'], 'compression')
430 deduplication = self.private_storage.get(share['id'], 'deduplication')
431 ssd_cache = self.private_storage.get(share['id'], 'ssd_cache')
432 LOG.debug('thin_provision: %(thin_provision)s '
433 'compression: %(compression)s '
434 'deduplication: %(deduplication)s '
435 'ssd_cache: %(ssd_cache)s',
436 {'thin_provision': thin_provision,
437 'compression': compression,
438 'deduplication': deduplication,
439 'ssd_cache': ssd_cache})
440 share_dict = {
441 'sharename': volName,
442 'old_sharename': volName,
443 'new_size': new_size,
444 'thin_provision': thin_provision == 'True',
445 'compression': compression == 'True',
446 'deduplication': deduplication == 'True',
447 'ssd_cache': ssd_cache == 'True',
448 'share_proto': share['share_proto']
449 }
450 self.api_executor.edit_share(share_dict)
452 @utils.retry(retry_param=exception.ShareBackendException,
453 interval=3,
454 retries=5)
455 @utils.synchronized('qnap-create_snapshot')
456 def create_snapshot(self, context, snapshot, share_server=None):
457 """Create a snapshot."""
458 LOG.debug('snapshot[share][share_id]: %s',
459 snapshot['share']['share_id'])
460 LOG.debug('snapshot id: %s', snapshot['id'])
462 # Use private_storage to retrieve volume ID created in the NAS.
463 volID = self.private_storage.get(snapshot['share']['id'], 'volID')
464 if not volID:
465 LOG.warning(
466 'volID for Share %s does not exist',
467 snapshot['share']['id'])
468 raise exception.ShareResourceNotFound(
469 share_id=snapshot['share']['id'])
470 LOG.debug('volID: %s', volID)
472 # User could create two snapshot with the same name on horizon.
473 # Therefore, we should not use displayname to create snapshot on NAS.
475 # if snapshot exist, need to change another
476 create_snapshot_name = self._gen_random_name("snapshot")
477 LOG.debug('create_snapshot_name: %s', create_snapshot_name)
478 check_snapshot = self.api_executor.get_snapshot_info(
479 volID=volID, snapshot_name=create_snapshot_name)
480 if check_snapshot is not None: 480 ↛ 481line 480 didn't jump to line 481 because the condition on line 480 was never true
481 msg = _("Failed to create an unused snapshot name.")
482 raise exception.ShareBackendException(msg=msg)
484 LOG.debug('create_snapshot_name: %s', create_snapshot_name)
485 self.api_executor.create_snapshot_api(volID, create_snapshot_name)
487 snapshot_id = ""
488 created_snapshot = self.api_executor.get_snapshot_info(
489 volID=volID, snapshot_name=create_snapshot_name)
490 if created_snapshot is not None: 490 ↛ 493line 490 didn't jump to line 493 because the condition on line 490 was always true
491 snapshot_id = created_snapshot.find('snapshot_id').text
492 else:
493 msg = _("Failed to get snapshot information.")
494 raise exception.ShareBackendException(msg=msg)
496 LOG.debug('created_snapshot: %s', created_snapshot)
497 LOG.debug('snapshot_id: %s', snapshot_id)
499 # Use private_storage to record data instead of metadata.
500 _metadata = {'snapshot_id': snapshot_id}
501 self.private_storage.update(snapshot['id'], _metadata)
503 # Test to get value from private_storage.
504 snapshot_id = self.private_storage.get(snapshot['id'], 'snapshot_id')
505 LOG.debug('snapshot_id: %s', snapshot_id)
507 return {'provider_location': snapshot_id}
509 @utils.synchronized('qnap-delete_snapshot')
510 def delete_snapshot(self, context, snapshot, share_server=None):
511 """Delete a snapshot."""
512 LOG.debug('Entering delete_snapshot. The deleted snapshot=%(snap)s',
513 {'snap': snapshot['id']})
515 snapshot_id = (snapshot.get('provider_location') or
516 self.private_storage.get(snapshot['id'], 'snapshot_id'))
517 if not snapshot_id:
518 LOG.warning('Snapshot %s does not exist', snapshot['id'])
519 return
520 LOG.debug('snapshot_id: %s', snapshot_id)
522 self.api_executor.delete_snapshot_api(snapshot_id)
523 self.private_storage.delete(snapshot['id'])
525 @utils.retry(retry_param=exception.ShareBackendException,
526 interval=3,
527 retries=5)
528 @utils.synchronized('qnap-create_share_from_snapshot')
529 def create_share_from_snapshot(self, context, share, snapshot,
530 share_server=None, parent_share=None):
531 """Create a share from a snapshot."""
532 LOG.debug('Entering create_share_from_snapshot. The source '
533 'snapshot=%(snap)s. The created share=%(share)s',
534 {'snap': snapshot['id'], 'share': share['id']})
536 snapshot_id = (snapshot.get('provider_location') or
537 self.private_storage.get(snapshot['id'], 'snapshot_id'))
538 if not snapshot_id:
539 LOG.warning('Snapshot %s does not exist', snapshot['id'])
540 raise exception.SnapshotResourceNotFound(name=snapshot['id'])
541 LOG.debug('snapshot_id: %s', snapshot_id)
543 create_share_name = self._gen_random_name("share")
544 # if sharename exist, need to change another
545 created_share = self.api_executor.get_share_info(
546 self.configuration.qnap_poolname,
547 vol_label=create_share_name)
549 if created_share is not None:
550 msg = _("Failed to create an unused share name.")
551 raise exception.ShareBackendException(msg=msg)
553 self.api_executor.clone_snapshot(snapshot_id,
554 create_share_name, share['size'])
556 create_volID = ""
557 created_share = self.api_executor.get_share_info(
558 self.configuration.qnap_poolname,
559 vol_label=create_share_name)
560 if created_share is not None:
561 create_volID = created_share.find('vol_no').text
562 LOG.debug('create_volID: %s', create_volID)
563 else:
564 msg = _("Failed to clone a snapshot in time.")
565 raise exception.ShareBackendException(msg=msg)
567 thin_provision = self.private_storage.get(
568 snapshot['share_instance_id'], 'thin_provision')
569 compression = self.private_storage.get(
570 snapshot['share_instance_id'], 'compression')
571 deduplication = self.private_storage.get(
572 snapshot['share_instance_id'], 'deduplication')
573 ssd_cache = self.private_storage.get(
574 snapshot['share_instance_id'], 'ssd_cache')
575 LOG.debug('thin_provision: %(thin_provision)s '
576 'compression: %(compression)s '
577 'deduplication: %(deduplication)s '
578 'ssd_cache: %(ssd_cache)s',
579 {'thin_provision': thin_provision,
580 'compression': compression,
581 'deduplication': deduplication,
582 'ssd_cache': ssd_cache})
584 # Use private_storage to record volume ID and Name created in the NAS.
585 _metadata = {
586 'volID': create_volID,
587 'volName': create_share_name,
588 'thin_provision': thin_provision,
589 'compression': compression,
590 'deduplication': deduplication,
591 'ssd_cache': ssd_cache
592 }
593 self.private_storage.update(share['id'], _metadata)
595 # Test to get value from private_storage.
596 volName = self.private_storage.get(share['id'], 'volName')
597 LOG.debug('volName: %s', volName)
599 return self._get_location_path(create_share_name,
600 share['share_proto'],
601 self.configuration.qnap_share_ip,
602 create_volID)
604 def _get_vol_host(self, host_list, vol_name_timestamp):
605 vol_host_list = []
606 if host_list is None:
607 return vol_host_list
608 for host in host_list:
609 # Check host alias name with prefix "manila-{vol_name_timestamp}"
610 # to find the host of this manila share.
611 LOG.debug('_get_vol_host name:%s', host.find('name').text)
612 # Because driver supports only IPv4 now, check "netaddrs"
613 # have "ipv4" tag to get address.
614 if re.match("^manila-{}".format(vol_name_timestamp),
615 host.find('name').text):
616 host_dict = {
617 'index': host.find('index').text,
618 'hostid': host.find('hostid').text,
619 'name': host.find('name').text,
620 'ipv4': [],
621 }
622 for ipv4 in host.findall('netaddrs/ipv4'):
623 host_dict['ipv4'].append(ipv4.text)
624 vol_host_list.append(host_dict)
625 LOG.debug('_get_vol_host vol_host_list:%s', vol_host_list)
626 return vol_host_list
628 @utils.synchronized('qnap-update_access')
629 def update_access(self, context, share, access_rules, add_rules,
630 delete_rules, update_rules, share_server=None):
631 if not (add_rules or delete_rules):
632 volName = self.private_storage.get(share['id'], 'volName')
633 LOG.debug('volName: %s', volName)
635 if volName is None:
636 LOG.debug('Share %s does not exist', share['id'])
637 raise exception.ShareResourceNotFound(share_id=share['id'])
639 # Clear all current ACLs
640 self.api_executor.set_nfs_access(volName, 2, "all")
642 vol_name_timestamp = self._get_timestamp_from_vol_name(volName)
643 host_list = self.api_executor.get_host_list()
644 LOG.debug('host_list:%s', host_list)
645 vol_host_list = self._get_vol_host(host_list, vol_name_timestamp)
646 # If host already exist, delete the host
647 if len(vol_host_list) > 0:
648 for vol_host in vol_host_list:
649 self.api_executor.delete_host(vol_host['name'])
651 # Add each one through all rules.
652 for access in access_rules:
653 self._allow_access(context, share, access, share_server)
654 else:
655 # Adding/Deleting specific rules
656 for access in delete_rules:
657 self._deny_access(context, share, access, share_server)
658 for access in add_rules:
659 self._allow_access(context, share, access, share_server)
661 def _allow_access(self, context, share, access, share_server=None):
662 """Allow access to the share."""
663 share_proto = share['share_proto']
664 access_type = access['access_type']
665 access_level = access['access_level']
666 access_to = access['access_to']
667 LOG.debug('share_proto: %(share_proto)s '
668 'access_type: %(access_type)s '
669 'access_level: %(access_level)s '
670 'access_to: %(access_to)s',
671 {'share_proto': share_proto,
672 'access_type': access_type,
673 'access_level': access_level,
674 'access_to': access_to})
676 self._check_share_access(share_proto, access_type)
678 vol_name = self.private_storage.get(share['id'], 'volName')
679 vol_name_timestamp = self._get_timestamp_from_vol_name(vol_name)
680 host_name = self._gen_host_name(vol_name_timestamp, access_level)
682 host_list = self.api_executor.get_host_list()
683 LOG.debug('vol_name: %(vol_name)s '
684 'access_level: %(access_level)s '
685 'host_name: %(host_name)s '
686 'host_list: %(host_list)s ',
687 {'vol_name': vol_name,
688 'access_level': access_level,
689 'host_name': host_name,
690 'host_list': host_list})
691 filter_host_list = self._get_vol_host(host_list, vol_name_timestamp)
692 if len(filter_host_list) == 0:
693 # if host does not exist, create a host for the share
694 self.api_executor.add_host(host_name, access_to)
695 elif (len(filter_host_list) == 1 and
696 filter_host_list[0]['name'] == host_name):
697 # if the host exist, and this host is for the same access right,
698 # add ip to the host.
699 ipv4_list = filter_host_list[0]['ipv4']
700 if access_to not in ipv4_list: 700 ↛ 702line 700 didn't jump to line 702 because the condition on line 700 was always true
701 ipv4_list.append(access_to)
702 LOG.debug('vol_host["ipv4"]: %s', filter_host_list[0]['ipv4'])
703 LOG.debug('ipv4_list: %s', ipv4_list)
704 self.api_executor.edit_host(host_name, ipv4_list)
705 else:
706 # Until now, share of QNAP NAS can only apply one access level for
707 # all ips. "rw" for some ips and "ro" for else is not allowed.
708 support_level = (constants.ACCESS_LEVEL_RW if
709 access_level == constants.ACCESS_LEVEL_RO
710 else constants.ACCESS_LEVEL_RO)
711 reason = _('Share only supports one access '
712 'level: %s') % support_level
713 LOG.error(reason)
714 raise exception.InvalidShareAccess(reason=reason)
715 access = 1 if access_level == constants.ACCESS_LEVEL_RO else 0
716 self.api_executor.set_nfs_access(vol_name, access, host_name)
718 def _deny_access(self, context, share, access, share_server=None):
719 """Deny access to the share."""
720 share_proto = share['share_proto']
721 access_type = access['access_type']
722 access_level = access['access_level']
723 access_to = access['access_to']
724 LOG.debug('share_proto: %(share_proto)s '
725 'access_type: %(access_type)s '
726 'access_level: %(access_level)s '
727 'access_to: %(access_to)s',
728 {'share_proto': share_proto,
729 'access_type': access_type,
730 'access_level': access_level,
731 'access_to': access_to})
733 try:
734 self._check_share_access(share_proto, access_type)
735 except exception.InvalidShareAccess:
736 LOG.warning('The denied rule is invalid and does not exist.')
737 return
739 vol_name = self.private_storage.get(share['id'], 'volName')
740 vol_name_timestamp = self._get_timestamp_from_vol_name(vol_name)
741 host_name = self._gen_host_name(vol_name_timestamp, access_level)
742 host_list = self.api_executor.get_host_list()
743 LOG.debug('vol_name: %(vol_name)s '
744 'access_level: %(access_level)s '
745 'host_name: %(host_name)s '
746 'host_list: %(host_list)s ',
747 {'vol_name': vol_name,
748 'access_level': access_level,
749 'host_name': host_name,
750 'host_list': host_list})
751 filter_host_list = self._get_vol_host(host_list, vol_name_timestamp)
752 # if share already have host, remove ip from host
753 for vol_host in filter_host_list:
754 if vol_host['name'] == host_name: 754 ↛ 753line 754 didn't jump to line 753 because the condition on line 754 was always true
755 ipv4_list = vol_host['ipv4']
756 if access_to in ipv4_list: 756 ↛ 758line 756 didn't jump to line 758 because the condition on line 756 was always true
757 ipv4_list.remove(access_to)
758 LOG.debug('vol_host["ipv4"]: %s', vol_host['ipv4'])
759 LOG.debug('ipv4_list: %s', ipv4_list)
760 if len(ipv4_list) == 0: # if list empty, remove the host 760 ↛ 765line 760 didn't jump to line 765 because the condition on line 760 was always true
761 self.api_executor.set_nfs_access(
762 vol_name, 2, host_name)
763 self.api_executor.delete_host(host_name)
764 else:
765 self.api_executor.edit_host(host_name, ipv4_list)
766 break
768 def _check_share_access(self, share_proto, access_type):
769 if share_proto == 'NFS' and access_type != 'ip':
770 reason = _('Only "ip" access type is allowed for '
771 'NFS shares.')
772 LOG.warning(reason)
773 raise exception.InvalidShareAccess(reason=reason)
774 elif share_proto != 'NFS': 774 ↛ exitline 774 didn't return from function '_check_share_access' because the condition on line 774 was always true
775 reason = _('Invalid NAS protocol: %s') % share_proto
776 raise exception.InvalidShareAccess(reason=reason)
778 def manage_existing(self, share, driver_options):
779 """Manages a share that exists on backend."""
780 if share['share_proto'].lower() == 'nfs':
781 # 10.0.0.1:/share/example
782 LOG.info("Share %(shr_path)s will be managed with ID "
783 "%(shr_id)s.",
784 {'shr_path': share['export_locations'][0]['path'],
785 'shr_id': share['id']})
787 old_path_info = share['export_locations'][0]['path'].split(
788 ':/share/')
790 if len(old_path_info) == 2:
791 ip = old_path_info[0]
792 share_name = old_path_info[1]
793 else:
794 msg = _("Incorrect path. It should have the following format: "
795 "IP:/share/share_name.")
796 raise exception.ShareBackendException(msg=msg)
797 else:
798 msg = _('Invalid NAS protocol: %s') % share['share_proto']
799 raise exception.InvalidInput(reason=msg)
801 if ip != self.configuration.qnap_share_ip:
802 msg = _("The NAS IP %(ip)s is not configured.") % {'ip': ip}
803 raise exception.ShareBackendException(msg=msg)
805 existing_share = self.api_executor.get_share_info(
806 self.configuration.qnap_poolname,
807 vol_label=share_name)
808 if existing_share is None:
809 msg = _("The share %s trying to be managed was not found on "
810 "backend.") % share['id']
811 raise exception.ManageInvalidShare(reason=msg)
813 extra_specs = share_types.get_extra_specs_from_share(share)
814 qnap_thin_provision = share_types.parse_boolean_extra_spec(
815 'thin_provisioning', extra_specs.get("thin_provisioning") or
816 extra_specs.get('capabilities:thin_provisioning') or 'true')
817 qnap_compression = share_types.parse_boolean_extra_spec(
818 'compression', extra_specs.get("compression") or
819 extra_specs.get('capabilities:compression') or 'true')
820 qnap_deduplication = share_types.parse_boolean_extra_spec(
821 'dedupe', extra_specs.get("dedupe") or
822 extra_specs.get('capabilities:dedupe') or 'false')
823 qnap_ssd_cache = share_types.parse_boolean_extra_spec(
824 'qnap_ssd_cache', extra_specs.get("qnap_ssd_cache") or
825 extra_specs.get("capabilities:qnap_ssd_cache") or 'false')
826 LOG.debug('qnap_thin_provision: %(qnap_thin_provision)s '
827 'qnap_compression: %(qnap_compression)s '
828 'qnap_deduplication: %(qnap_deduplication)s '
829 'qnap_ssd_cache: %(qnap_ssd_cache)s',
830 {'qnap_thin_provision': qnap_thin_provision,
831 'qnap_compression': qnap_compression,
832 'qnap_deduplication': qnap_deduplication,
833 'qnap_ssd_cache': qnap_ssd_cache})
834 if (qnap_deduplication and not qnap_thin_provision):
835 msg = _("Dedupe cannot be enabled without thin_provisioning.")
836 LOG.debug('Dedupe cannot be enabled without thin_provisioning.')
837 raise exception.InvalidExtraSpec(reason=msg)
839 vol_no = existing_share.find('vol_no').text
840 vol = self.api_executor.get_specific_volinfo(vol_no)
841 vol_size_gb = math.ceil(float(vol.find('size').text) / units.Gi)
843 share_dict = {
844 'sharename': share_name,
845 'old_sharename': share_name,
846 'thin_provision': qnap_thin_provision,
847 'compression': qnap_compression,
848 'deduplication': qnap_deduplication,
849 'ssd_cache': qnap_ssd_cache,
850 'share_proto': share['share_proto']
851 }
852 self.api_executor.edit_share(share_dict)
854 _metadata = {}
855 _metadata['volID'] = vol_no
856 _metadata['volName'] = share_name
857 _metadata['thin_provision'] = qnap_thin_provision
858 _metadata['compression'] = qnap_compression
859 _metadata['deduplication'] = qnap_deduplication
860 _metadata['ssd_cache'] = qnap_ssd_cache
861 self.private_storage.update(share['id'], _metadata)
863 LOG.info("Share %(shr_path)s was successfully managed with ID "
864 "%(shr_id)s.",
865 {'shr_path': share['export_locations'][0]['path'],
866 'shr_id': share['id']})
868 export_locations = self._get_location_path(
869 share_name,
870 share['share_proto'],
871 self.configuration.qnap_share_ip,
872 vol_no)
874 return {'size': vol_size_gb, 'export_locations': export_locations}
876 def unmanage(self, share):
877 """Remove the specified share from Manila management."""
878 self.private_storage.delete(share['id'])
880 def manage_existing_snapshot(self, snapshot, driver_options):
881 """Manage existing share snapshot with manila."""
882 volID = self.private_storage.get(snapshot['share']['id'], 'volID')
883 LOG.debug('volID: %s', volID)
885 existing_share = self.api_executor.get_share_info(
886 self.configuration.qnap_poolname,
887 vol_no=volID)
889 if existing_share is None: 889 ↛ 890line 889 didn't jump to line 890 because the condition on line 889 was never true
890 msg = _("The share id %s was not found on backend.") % volID
891 LOG.error(msg)
892 raise exception.ShareNotFound(msg)
894 snapshot_id = snapshot.get('provider_location')
895 snapshot_id_info = snapshot_id.split('@')
897 if len(snapshot_id_info) == 2: 897 ↛ 901line 897 didn't jump to line 901 because the condition on line 897 was always true
898 share_name = snapshot_id_info[0]
899 snapshot_name = snapshot_id_info[1]
900 else:
901 msg = _("Incorrect provider_location format. It should have the "
902 "following format: share_name@snapshot_name.")
903 LOG.error(msg)
904 raise exception.InvalidParameterValue(msg)
906 if share_name != existing_share.find('vol_label').text: 906 ↛ 907line 906 didn't jump to line 907 because the condition on line 906 was never true
907 msg = (_("The assigned share %(share_name)s was not matched "
908 "%(vol_label)s on backend.") %
909 {'share_name': share_name,
910 'vol_label': existing_share.find('vol_label').text})
911 LOG.error(msg)
912 raise exception.ShareNotFound(msg)
914 check_snapshot = self.api_executor.get_snapshot_info(
915 volID=volID, snapshot_name=snapshot_name)
916 if check_snapshot is None: 916 ↛ 917line 916 didn't jump to line 917 because the condition on line 916 was never true
917 msg = (_("The snapshot %(snapshot_name)s was not "
918 "found on backend.") %
919 {'snapshot_name': snapshot_name})
920 LOG.error(msg)
921 raise exception.InvalidParameterValue(err=msg)
923 _metadata = {
924 'snapshot_id': snapshot_id,
925 }
926 self.private_storage.update(snapshot['id'], _metadata)
927 parent_size = check_snapshot.find('parent_size')
928 snap_size_gb = None
929 if parent_size is not None: 929 ↛ 931line 929 didn't jump to line 931 because the condition on line 929 was always true
930 snap_size_gb = math.ceil(float(parent_size.text) / units.Gi)
931 return {'size': snap_size_gb}
933 def unmanage_snapshot(self, snapshot):
934 """Remove the specified snapshot from Manila management."""
935 self.private_storage.delete(snapshot['id'])