Coverage for manila/share/drivers/veritas/veritas_isa.py: 97%

372 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright 2017 Veritas Technologies LLC. 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); you may 

4# not use this file except in compliance with the License. You may obtain 

5# a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

12# License for the specific language governing permissions and limitations 

13# under the License. 

14""" 

15Veritas Access Driver for manila shares. 

16 

17Limitation: 

18 

191) single tenant 

20""" 

21 

22import hashlib 

23from http import client as http_client 

24import json 

25 

26from oslo_config import cfg 

27from oslo_log import log as logging 

28from oslo_utils import units 

29from random import shuffle 

30import requests 

31import requests.auth 

32 

33from manila.common import constants as const 

34from manila import exception 

35from manila.share import driver 

36 

37LOG = logging.getLogger(__name__) 

38 

39 

40va_share_opts = [ 

41 cfg.StrOpt('va_server_ip', 

42 help='Console IP of Veritas Access server.'), 

43 cfg.IntOpt('va_port', 

44 default=14161, 

45 help='Veritas Access server REST port.'), 

46 cfg.StrOpt('va_user', 

47 help='Veritas Access server REST login name.'), 

48 cfg.StrOpt('va_pwd', 

49 secret=True, 

50 help='Veritas Access server REST password.'), 

51 cfg.StrOpt('va_pool', 

52 help='Veritas Access storage pool from which ' 

53 'shares are served.'), 

54 cfg.StrOpt('va_fstype', 

55 default='simple', 

56 help='Type of VA file system to be created.') 

57] 

58 

59 

60CONF = cfg.CONF 

61CONF.register_opts(va_share_opts) 

62 

63 

64class NoAuth(requests.auth.AuthBase): 

65 """This is a 'authentication' handler. 

66 

67 It exists for use with custom authentication systems, such as the 

68 one for the Access API, it simply passes the Authorization header as-is. 

69 

70 The default authentication handler for requests will clobber the 

71 Authorization header. 

72 """ 

73 

74 def __call__(self, r): 

75 return r 

76 

77 

78class ACCESSShareDriver(driver.ExecuteMixin, driver.ShareDriver): 

79 """ACCESS Share Driver. 

80 

81 Executes commands relating to Manila Shares. 

82 Supports creation of shares on ACCESS. 

83 

84 API version history: 

85 

86 1.0 - Initial version. 

87 """ 

88 

89 VA_SHARE_PATH_STR = '/vx/' 

90 

91 def __init__(self, *args, **kwargs): 

92 """Do initialization.""" 

93 

94 super(ACCESSShareDriver, self).__init__(False, *args, **kwargs) 

95 self.configuration.append_config_values(va_share_opts) 

96 self.backend_name = self.configuration.safe_get( 

97 'share_backend_name') or "VeritasACCESS" 

98 self._va_ip = None 

99 self._va_url = None 

100 self._pool = None 

101 self._fstype = None 

102 self._port = None 

103 self._user = None 

104 self._pwd = None 

105 self._cred = None 

106 self._connect_resp = None 

107 self._verify_ssl_cert = None 

108 self._fs_create_str = '/fs/create' 

109 self._fs_list_str = '/fs' 

110 self._fs_delete_str = '/fs/destroy' 

111 self._fs_extend_str = '/fs/grow' 

112 self._fs_shrink_str = '/fs/shrink' 

113 self._snap_create_str = '/snapshot/create' 

114 self._snap_delete_str = '/snapshot/delete' 

115 self._snap_list_str = '/snapshot/getSnapShotList' 

116 self._nfs_add_str = '/share/create' 

117 self._nfs_delete_str = '/share/delete' 

118 self._nfs_share_list_str = '/share/all_shares_details_by_path/?path=' 

119 self._ip_addr_show_str = '/common/get_all_ips' 

120 self._pool_free_str = '/storage/pool' 

121 self._update_object = '/objecttags' 

122 self.session = None 

123 self.host = None 

124 LOG.debug("ACCESSShareDriver called") 

125 

126 def do_setup(self, context): 

127 """Any initialization the share driver does while starting.""" 

128 super(ACCESSShareDriver, self).do_setup(context) 

129 

130 self._va_ip = self.configuration.va_server_ip 

131 self._pool = self.configuration.va_pool 

132 self._user = self.configuration.va_user 

133 self._pwd = self.configuration.va_pwd 

134 self._port = self.configuration.va_port 

135 self._fstype = self.configuration.va_fstype 

136 self.session = self._authenticate_access(self._va_ip, self._user, 

137 self._pwd) 

138 

139 def _get_va_share_name(self, name): 

140 length = len(name) 

141 index = int(length / 2) 

142 name1 = name[:index] 

143 name2 = name[index:] 

144 crc1 = hashlib.md5(name1.encode('utf-8'), 

145 usedforsecurity=False).hexdigest()[:8] 

146 crc2 = hashlib.md5(name2.encode('utf-8'), 

147 usedforsecurity=False).hexdigest()[:8] 

148 return crc1 + '-' + crc2 

149 

150 def _get_va_snap_name(self, name): 

151 return self._get_va_share_name(name) 

152 

153 def _get_va_share_path(self, name): 

154 return self.VA_SHARE_PATH_STR + name 

155 

156 def _does_item_exist_at_va_backend(self, item_name, path_given): 

157 """Check given share is exists on backend""" 

158 

159 path = path_given 

160 provider = '%s:%s' % (self.host, self._port) 

161 data = {} 

162 item_list = self._access_api(self.session, provider, path, 

163 json.dumps(data), 'GET') 

164 

165 for item in item_list: 

166 if item['name'] == item_name: 

167 return True 

168 

169 return False 

170 

171 def _return_access_lists_difference(self, list_a, list_b): 

172 """Returns a list of elements in list_a that are not in list_b""" 

173 

174 sub_list = [{"access_to": s.get('access_to'), 

175 "access_type": s.get('access_type'), 

176 "access_level": s.get('access_level')} 

177 for s in list_b] 

178 

179 return [r for r in list_a if ( 

180 {"access_to": r.get("access_to"), 

181 "access_type": r.get("access_type"), 

182 "access_level": r.get("access_level")} not in sub_list)] 

183 

184 def _fetch_existing_rule(self, share_name): 

185 """Return list of access rules on given share""" 

186 

187 share_path = self._get_va_share_path(share_name) 

188 path = self._nfs_share_list_str + share_path 

189 provider = '%s:%s' % (self.host, self._port) 

190 data = {} 

191 share_list = self._access_api(self.session, provider, path, 

192 json.dumps(data), 'GET') 

193 

194 va_access_list = [] 

195 for share in share_list: 

196 if share['shareType'] == 'NFS': 

197 for share_info in share['shares']: 

198 if share_info['name'] == share_path: 

199 access_to = share_info['host_name'] 

200 a_level = const.ACCESS_LEVEL_RO 

201 if const.ACCESS_LEVEL_RW in share_info['privilege']: 

202 a_level = const.ACCESS_LEVEL_RW 

203 va_access_list.append({ 

204 'access_to': access_to, 

205 'access_level': a_level, 

206 'access_type': 'ip' 

207 }) 

208 

209 return va_access_list 

210 

211 def create_share(self, ctx, share, share_server=None): 

212 """Create an ACCESS file system that will be represented as share.""" 

213 

214 sharename = share['name'] 

215 sizestr = '%sg' % share['size'] 

216 LOG.debug("ACCESSShareDriver create_share sharename %s sizestr %r", 

217 sharename, sizestr) 

218 va_sharename = self._get_va_share_name(sharename) 

219 va_sharepath = self._get_va_share_path(va_sharename) 

220 va_fs_type = self._fstype 

221 path = self._fs_create_str 

222 provider = '%s:%s' % (self.host, self._port) 

223 data1 = { 

224 "largefs": "no", 

225 "blkSize": "blksize=8192", 

226 "pdirEnable": "pdir_enable=yes" 

227 } 

228 data1["layout"] = va_fs_type 

229 data1["fs_name"] = va_sharename 

230 data1["fs_size"] = sizestr 

231 data1["pool_disks"] = self._pool 

232 result = self._access_api(self.session, provider, path, 

233 json.dumps(data1), 'POST') 

234 if not result: 

235 message = (('ACCESSShareDriver create share failed %s'), sharename) 

236 LOG.error(message) 

237 raise exception.ShareBackendException(msg=message) 

238 

239 data2 = {"type": "FS", "key": "manila"} 

240 data2["id"] = va_sharename 

241 data2["value"] = 'manila_fs' 

242 path = self._update_object 

243 result = self._access_api(self.session, provider, path, 

244 json.dumps(data2), 'POST') 

245 

246 vip = self._get_vip() 

247 location = vip + ':' + va_sharepath 

248 LOG.debug("ACCESSShareDriver create_share location %s", location) 

249 return location 

250 

251 def _get_vip(self): 

252 """Get a virtual IP from ACCESS.""" 

253 ip_list = self._get_access_ips(self.session, self.host) 

254 vip = [] 

255 for ips in ip_list: 

256 if ips['isconsoleip'] == 1: 

257 continue 

258 if ips['type'] == 'Virtual' and ips['status'] == 'ONLINE': 

259 vip.append(ips['ip']) 

260 shuffle(vip) 

261 return str(vip[0]) 

262 

263 def delete_share(self, context, share, share_server=None): 

264 """Delete a share from ACCESS.""" 

265 

266 sharename = share['name'] 

267 va_sharename = self._get_va_share_name(sharename) 

268 LOG.debug("ACCESSShareDriver delete_share %s called", 

269 sharename) 

270 if share['snapshot_id']: 

271 message = (('ACCESSShareDriver delete share %s' 

272 ' early return'), sharename) 

273 LOG.debug(message) 

274 return 

275 

276 ret_val = self._does_item_exist_at_va_backend(va_sharename, 

277 self._fs_list_str) 

278 if not ret_val: 

279 return 

280 

281 path = self._fs_delete_str 

282 provider = '%s:%s' % (self.host, self._port) 

283 data = {} 

284 data["fs_name"] = va_sharename 

285 result = self._access_api(self.session, provider, path, 

286 json.dumps(data), 'POST') 

287 if not result: 

288 message = (('ACCESSShareDriver delete share failed %s'), sharename) 

289 LOG.error(message) 

290 raise exception.ShareBackendException(msg=message) 

291 

292 data2 = {"type": "FS", "key": "manila"} 

293 data2["id"] = va_sharename 

294 path = self._update_object 

295 result = self._access_api(self.session, provider, path, 

296 json.dumps(data2), 'DELETE') 

297 

298 def extend_share(self, share, new_size, share_server=None): 

299 """Extend existing share to new size.""" 

300 sharename = share['name'] 

301 size = '%s%s' % (str(new_size), 'g') 

302 va_sharename = self._get_va_share_name(sharename) 

303 path = self._fs_extend_str 

304 provider = '%s:%s' % (self.host, self._port) 

305 data1 = {"operationOption": "growto", "tier": "primary"} 

306 data1["fs_name"] = va_sharename 

307 data1["fs_size"] = size 

308 result = self._access_api(self.session, provider, path, 

309 json.dumps(data1), 'POST') 

310 if not result: 

311 message = (('ACCESSShareDriver extend share failed %s'), sharename) 

312 LOG.error(message) 

313 raise exception.ShareBackendException(msg=message) 

314 

315 LOG.debug('ACCESSShareDriver extended share' 

316 ' successfully %s', sharename) 

317 

318 def shrink_share(self, share, new_size, share_server=None): 

319 """Shrink existing share to new size.""" 

320 sharename = share['name'] 

321 va_sharename = self._get_va_share_name(sharename) 

322 size = '%s%s' % (str(new_size), 'g') 

323 path = self._fs_extend_str 

324 provider = '%s:%s' % (self.host, self._port) 

325 data1 = {"operationOption": "shrinkto", "tier": "primary"} 

326 data1["fs_name"] = va_sharename 

327 data1["fs_size"] = size 

328 result = self._access_api(self.session, provider, path, 

329 json.dumps(data1), 'POST') 

330 if not result: 

331 message = (('ACCESSShareDriver shrink share failed %s'), sharename) 

332 LOG.error(message) 

333 raise exception.ShareBackendException(msg=message) 

334 

335 LOG.debug('ACCESSShareDriver shrunk share successfully %s', sharename) 

336 

337 def _allow_access(self, context, share, access, share_server=None): 

338 """Give access of a share to an IP.""" 

339 

340 access_type = access['access_type'] 

341 server = access['access_to'] 

342 if access_type != 'ip': 

343 raise exception.InvalidShareAccess('Only ip access type ' 

344 'supported.') 

345 access_level = access['access_level'] 

346 

347 if access_level not in (const.ACCESS_LEVEL_RW, const.ACCESS_LEVEL_RO): 

348 raise exception.InvalidShareAccessLevel(level=access_level) 

349 export_path = share['export_locations'][0]['path'].split(':', 1) 

350 va_sharepath = str(export_path[1]) 

351 access_level = '%s,%s' % (str(access_level), 

352 'sync,no_root_squash') 

353 

354 path = self._nfs_add_str 

355 provider = '%s:%s' % (self.host, self._port) 

356 data = {} 

357 va_share_info = ("{\"share\":[{\"fileSystemPath\":\"" + va_sharepath + 

358 "\",\"shareType\":\"NFS\",\"shareDetails\":" + 

359 "[{\"client\":\"" + server + 

360 "\",\"exportOptions\":\"" + 

361 access_level + "\"}]}]}") 

362 

363 data["shareDetails"] = va_share_info 

364 

365 result = self._access_api(self.session, provider, path, 

366 json.dumps(data), 'POST') 

367 

368 if not result: 

369 message = (('ACCESSShareDriver access failed sharepath %s ' 

370 'server %s'), 

371 va_sharepath, 

372 server) 

373 LOG.error(message) 

374 raise exception.ShareBackendException(msg=message) 

375 

376 LOG.debug("ACCESSShareDriver allow_access sharepath %s server %s", 

377 va_sharepath, server) 

378 

379 data2 = {"type": "SHARE", "key": "manila"} 

380 data2["id"] = va_sharepath 

381 data2["value"] = 'manila_share' 

382 path = self._update_object 

383 result = self._access_api(self.session, provider, path, 

384 json.dumps(data2), 'POST') 

385 

386 def _deny_access(self, context, share, access, share_server=None): 

387 """Deny access to the share.""" 

388 

389 server = access['access_to'] 

390 access_type = access['access_type'] 

391 if access_type != 'ip': 

392 return 

393 export_path = share['export_locations'][0]['path'].split(':', 1) 

394 va_sharepath = str(export_path[1]) 

395 LOG.debug("ACCESSShareDriver deny_access sharepath %s server %s", 

396 va_sharepath, server) 

397 

398 path = self._nfs_delete_str 

399 provider = '%s:%s' % (self.host, self._port) 

400 data = {} 

401 va_share_info = ("{\"share\":[{\"fileSystemPath\":\"" + va_sharepath + 

402 "\",\"shareType\":\"NFS\",\"shareDetails\":" + 

403 "[{\"client\":\"" + server + "\"}]}]}") 

404 

405 data["shareDetails"] = va_share_info 

406 result = self._access_api(self.session, provider, path, 

407 json.dumps(data), 'DELETE') 

408 if not result: 

409 message = (('ACCESSShareDriver deny failed' 

410 ' sharepath %s server %s'), 

411 va_sharepath, 

412 server) 

413 LOG.error(message) 

414 raise exception.ShareBackendException(msg=message) 

415 

416 LOG.debug("ACCESSShareDriver deny_access sharepath %s server %s", 

417 va_sharepath, server) 

418 

419 data2 = {"type": "SHARE", "key": "manila"} 

420 data2["id"] = va_sharepath 

421 path = self._update_object 

422 result = self._access_api(self.session, provider, path, 

423 json.dumps(data2), 'DELETE') 

424 

425 def update_access(self, context, share, access_rules, add_rules, 

426 delete_rules, update_rules, share_server=None): 

427 """Update access to the share.""" 

428 

429 if (add_rules or delete_rules): 

430 # deleting rules 

431 for rule in delete_rules: 

432 self._deny_access(context, share, rule, share_server) 

433 

434 # adding rules 

435 for rule in add_rules: 

436 self._allow_access(context, share, rule, share_server) 

437 else: 

438 if not access_rules: 

439 LOG.warning("No access rules provided in update_access.") 

440 else: 

441 sharename = self._get_va_share_name(share['name']) 

442 existing_a_rules = self._fetch_existing_rule(sharename) 

443 

444 d_rule = self._return_access_lists_difference(existing_a_rules, 

445 access_rules) 

446 for rule in d_rule: 

447 LOG.debug("Removing rule %s in recovery.", 

448 str(rule)) 

449 self._deny_access(context, share, rule, share_server) 

450 

451 a_rule = self._return_access_lists_difference(access_rules, 

452 existing_a_rules) 

453 for rule in a_rule: 

454 LOG.debug("Adding rule %s in recovery.", 

455 str(rule)) 

456 self._allow_access(context, share, rule, share_server) 

457 

458 def create_snapshot(self, context, snapshot, share_server=None): 

459 """create snapshot of a share.""" 

460 LOG.debug('ACCESSShareDriver create_snapshot called ' 

461 'for snapshot ID %s.', 

462 snapshot['snapshot_id']) 

463 

464 sharename = snapshot['share_name'] 

465 va_sharename = self._get_va_share_name(sharename) 

466 snapname = snapshot['name'] 

467 va_snapname = self._get_va_snap_name(snapname) 

468 

469 path = self._snap_create_str 

470 provider = '%s:%s' % (self.host, self._port) 

471 data = {} 

472 data["snapShotname"] = va_snapname 

473 data["fileSystem"] = va_sharename 

474 data["removable"] = 'yes' 

475 result = self._access_api(self.session, provider, path, 

476 json.dumps(data), 'PUT') 

477 if not result: 

478 message = (('ACCESSShareDriver create snapshot failed snapname %s' 

479 ' sharename %s'), 

480 snapname, 

481 va_sharename) 

482 LOG.error(message) 

483 raise exception.ShareBackendException(msg=message) 

484 

485 data2 = {"type": "SNAPSHOT", "key": "manila"} 

486 data2["id"] = va_snapname 

487 data2["value"] = 'manila_snapshot' 

488 path = self._update_object 

489 result = self._access_api(self.session, provider, path, 

490 json.dumps(data2), 'POST') 

491 

492 def delete_snapshot(self, context, snapshot, share_server=None): 

493 """Deletes a snapshot.""" 

494 sharename = snapshot['share_name'] 

495 va_sharename = self._get_va_share_name(sharename) 

496 snapname = snapshot['name'] 

497 va_snapname = self._get_va_snap_name(snapname) 

498 

499 ret_val = self._does_item_exist_at_va_backend(va_snapname, 

500 self._snap_list_str) 

501 if not ret_val: 

502 return 

503 

504 path = self._snap_delete_str 

505 provider = '%s:%s' % (self.host, self._port) 

506 

507 data = {} 

508 data["name"] = va_snapname 

509 data["fsName"] = va_sharename 

510 data_to_send = {"snapShotDetails": {"snapshot": [data]}} 

511 result = self._access_api(self.session, provider, path, 

512 json.dumps(data_to_send), 'DELETE') 

513 if not result: 

514 message = (('ACCESSShareDriver delete snapshot failed snapname %s' 

515 ' sharename %s'), 

516 snapname, 

517 va_sharename) 

518 LOG.error(message) 

519 raise exception.ShareBackendException(msg=message) 

520 

521 data2 = {"type": "SNAPSHOT", "key": "manila"} 

522 data2["id"] = va_snapname 

523 path = self._update_object 

524 result = self._access_api(self.session, provider, path, 

525 json.dumps(data2), 'DELETE') 

526 

527 def create_share_from_snapshot(self, ctx, share, snapshot, 

528 share_server=None, parent_share=None): 

529 """create share from a snapshot.""" 

530 sharename = snapshot['share_name'] 

531 va_sharename = self._get_va_share_name(sharename) 

532 snapname = snapshot['name'] 

533 va_snapname = self._get_va_snap_name(snapname) 

534 va_sharepath = self._get_va_share_path(va_sharename) 

535 LOG.debug(('ACCESSShareDriver create_share_from_snapshot snapname %s' 

536 ' sharename %s'), 

537 va_snapname, 

538 va_sharename) 

539 vip = self._get_vip() 

540 location = vip + ':' + va_sharepath + ':' + va_snapname 

541 LOG.debug("ACCESSShareDriver create_share location %s", location) 

542 return location 

543 

544 def _get_api(self, provider, tail): 

545 api_root = 'https://%s/api' % (provider) 

546 return api_root + tail 

547 

548 def _access_api(self, session, provider, path, input_data, method): 

549 """Returns False if failure occurs.""" 

550 kwargs = {'data': input_data} 

551 if not isinstance(input_data, dict): 551 ↛ 553line 551 didn't jump to line 553 because the condition on line 551 was always true

552 kwargs['headers'] = {'Content-Type': 'application/json'} 

553 full_url = self._get_api(provider, path) 

554 response = session.request(method, full_url, **kwargs) 

555 if response.status_code != http_client.OK: 

556 LOG.debug('Access API operation Failed.') 

557 return False 

558 if path == self._update_object: 

559 return True 

560 result = response.json() 

561 return result 

562 

563 def _get_access_ips(self, session, host): 

564 

565 path = self._ip_addr_show_str 

566 provider = '%s:%s' % (host, self._port) 

567 data = {} 

568 ip_list = self._access_api(session, provider, path, 

569 json.dumps(data), 'GET') 

570 return ip_list 

571 

572 def _authenticate_access(self, address, username, password): 

573 session = requests.session() 

574 session.verify = False 

575 session.auth = NoAuth() 

576 

577 response = session.post('https://%s:%s/api/rest/authenticate' 

578 % (address, self._port), 

579 data={'username': username, 

580 'password': password}) 

581 if response.status_code != http_client.OK: 

582 LOG.debug(('failed to authenticate to remote cluster at %s as %s'), 

583 address, username) 

584 raise exception.NotAuthorized('Authentication failure.') 

585 result = response.json() 

586 session.headers.update({'Authorization': 'Bearer {}' 

587 .format(result['token'])}) 

588 session.headers.update({'Content-Type': 'application/json'}) 

589 

590 return session 

591 

592 def _get_access_pool_details(self): 

593 """Get access pool details.""" 

594 path = self._pool_free_str 

595 provider = '%s:%s' % (self.host, self._port) 

596 data = {} 

597 pool_details = self._access_api(self.session, provider, path, 

598 json.dumps(data), 'GET') 

599 

600 for pool in pool_details: 

601 if pool['device_group_name'] == str(self._pool): 

602 total_capacity = (int(pool['capacity']) / units.Gi) 

603 used_size = (int(pool['used_size']) / units.Gi) 

604 return (total_capacity, (total_capacity - used_size)) 

605 

606 message = 'Fetching pool details operation failed.' 

607 LOG.error(message) 

608 raise exception.ShareBackendException(msg=message) 

609 

610 def _update_share_stats(self): 

611 """Retrieve status info from share volume group.""" 

612 

613 LOG.debug("VRTSISA Updating share status.") 

614 self.host = str(self._va_ip) 

615 self.session = self._authenticate_access(self._va_ip, 

616 self._user, self._pwd) 

617 total_capacity, free_capacity = self._get_access_pool_details() 

618 data = { 

619 'share_backend_name': self.backend_name, 

620 'vendor_name': 'Veritas', 

621 'driver_version': '1.0', 

622 'storage_protocol': 'NFS', 

623 'total_capacity_gb': total_capacity, 

624 'free_capacity_gb': free_capacity, 

625 'reserved_percentage': 0, 

626 'reserved_snapshot_percentage': 0, 

627 'reserved_share_extend_percentage': 0, 

628 'QoS_support': False, 

629 'snapshot_support': True, 

630 'create_share_from_snapshot_support': True 

631 } 

632 super(ACCESSShareDriver, self)._update_share_stats(data)