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

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 

23 

24from oslo_config import cfg 

25from oslo_log import log as logging 

26from oslo_utils import timeutils 

27from oslo_utils import units 

28 

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 

37 

38LOG = logging.getLogger(__name__) 

39 

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] 

58 

59CONF = cfg.CONF 

60CONF.register_opts(qnap_manila_opts) 

61 

62 

63class QnapShareDriver(driver.ShareDriver): 

64 """OpenStack driver to enable QNAP Storage. 

65 

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 """ 

83 

84 DRIVER_VERSION = '1.0.8' 

85 

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() 

94 

95 def do_setup(self, context): 

96 """Setup the QNAP Manila share driver.""" 

97 self.ctxt = context 

98 LOG.debug('context: %s', context) 

99 

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 

108 

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) 

115 

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) 

126 

127 display_model_name, internal_model_name, fw_version = ( 

128 self.api_executor.get_basic_info( 

129 self.configuration.qnap_management_url)) 

130 

131 pattern = re.compile(r"^([A-Z]+)-?[A-Z]{0,2}(\d+)\d{2}(U|[a-z]*)") 

132 matches = pattern.match(display_model_name) 

133 

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) 

137 

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 ) 

147 

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)) 

155 

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) 

186 

187 msg = _('QNAP Storage model is not supported by this driver.') 

188 raise exception.ShareBackendException(msg=msg) 

189 

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) 

198 

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')}) 

212 

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) 

217 

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())) 

222 

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 

227 

228 location = '%s:%s' % (ip, vol_mount_path) 

229 else: 

230 msg = _('Invalid NAS protocol: %s') % share_proto 

231 raise exception.InvalidInput(reason=msg) 

232 

233 export_location = { 

234 'path': location, 

235 'is_admin_only': False, 

236 } 

237 return export_location 

238 

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}) 

246 

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) 

258 

259 reserved_percentage = self.configuration.safe_get( 

260 'reserved_share_percentage') 

261 

262 reserved_snapshot_percentage = self.configuration.safe_get( 

263 'reserved_share_from_snapshot_percentage') or reserved_percentage 

264 

265 reserved_shr_extend_percentage = self.configuration.safe_get( 

266 'reserved_share_extend_percentage') or reserved_percentage 

267 

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 } 

283 

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) 

296 

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}) 

326 

327 share_proto = share['share_proto'] 

328 

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) 

342 

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) 

370 

371 return self._get_location_path(create_share_name, 

372 share['share_proto'], 

373 self.configuration.qnap_share_ip, 

374 volID) 

375 

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 

388 

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) 

398 

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 

405 

406 vol_no = del_share.find('vol_no').text 

407 

408 self.api_executor.delete_share(vol_no) 

409 self.private_storage.delete(share['id']) 

410 

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}) 

420 

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) 

451 

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']) 

461 

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) 

471 

472 # User could create two snapshot with the same name on horizon. 

473 # Therefore, we should not use displayname to create snapshot on NAS. 

474 

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) 

483 

484 LOG.debug('create_snapshot_name: %s', create_snapshot_name) 

485 self.api_executor.create_snapshot_api(volID, create_snapshot_name) 

486 

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) 

495 

496 LOG.debug('created_snapshot: %s', created_snapshot) 

497 LOG.debug('snapshot_id: %s', snapshot_id) 

498 

499 # Use private_storage to record data instead of metadata. 

500 _metadata = {'snapshot_id': snapshot_id} 

501 self.private_storage.update(snapshot['id'], _metadata) 

502 

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) 

506 

507 return {'provider_location': snapshot_id} 

508 

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']}) 

514 

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) 

521 

522 self.api_executor.delete_snapshot_api(snapshot_id) 

523 self.private_storage.delete(snapshot['id']) 

524 

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']}) 

535 

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) 

542 

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) 

548 

549 if created_share is not None: 

550 msg = _("Failed to create an unused share name.") 

551 raise exception.ShareBackendException(msg=msg) 

552 

553 self.api_executor.clone_snapshot(snapshot_id, 

554 create_share_name, share['size']) 

555 

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) 

566 

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}) 

583 

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) 

594 

595 # Test to get value from private_storage. 

596 volName = self.private_storage.get(share['id'], 'volName') 

597 LOG.debug('volName: %s', volName) 

598 

599 return self._get_location_path(create_share_name, 

600 share['share_proto'], 

601 self.configuration.qnap_share_ip, 

602 create_volID) 

603 

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 

627 

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) 

634 

635 if volName is None: 

636 LOG.debug('Share %s does not exist', share['id']) 

637 raise exception.ShareResourceNotFound(share_id=share['id']) 

638 

639 # Clear all current ACLs 

640 self.api_executor.set_nfs_access(volName, 2, "all") 

641 

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']) 

650 

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) 

660 

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}) 

675 

676 self._check_share_access(share_proto, access_type) 

677 

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) 

681 

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) 

717 

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}) 

732 

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 

738 

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 

767 

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) 

777 

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']}) 

786 

787 old_path_info = share['export_locations'][0]['path'].split( 

788 ':/share/') 

789 

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) 

800 

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) 

804 

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) 

812 

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) 

838 

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) 

842 

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) 

853 

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) 

862 

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']}) 

867 

868 export_locations = self._get_location_path( 

869 share_name, 

870 share['share_proto'], 

871 self.configuration.qnap_share_ip, 

872 vol_no) 

873 

874 return {'size': vol_size_gb, 'export_locations': export_locations} 

875 

876 def unmanage(self, share): 

877 """Remove the specified share from Manila management.""" 

878 self.private_storage.delete(share['id']) 

879 

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) 

884 

885 existing_share = self.api_executor.get_share_info( 

886 self.configuration.qnap_poolname, 

887 vol_no=volID) 

888 

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) 

893 

894 snapshot_id = snapshot.get('provider_location') 

895 snapshot_id_info = snapshot_id.split('@') 

896 

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) 

905 

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) 

913 

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) 

922 

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} 

932 

933 def unmanage_snapshot(self, snapshot): 

934 """Remove the specified snapshot from Manila management.""" 

935 self.private_storage.delete(snapshot['id'])