Coverage for manila/share/drivers/vastdata/driver.py: 99%

176 statements  

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

1# Copyright 2024 VAST Data 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""" 

16VAST's Share Driver 

17 

18 

19Configuration: 

20 

21 

22[DEFAULT] 

23enabled_share_backends = vast 

24 

25[vast] 

26share_driver = manila.share.drivers.vastdata.driver.VASTShareDriver 

27share_backend_name = vast 

28snapshot_support = true 

29driver_handles_share_servers = false 

30vast_mgmt_host = v11 

31vast_vippool_name = vippool-1 

32vast_root_export = manila 

33vast_mgmt_user = admin 

34vast_mgmt_password = 123456 

35""" 

36 

37import collections 

38 

39import netaddr 

40from oslo_config import cfg 

41from oslo_log import log as logging 

42from oslo_utils import units 

43 

44from manila.common import constants 

45from manila import exception 

46from manila.i18n import _ 

47from manila.share import driver 

48from manila.share.drivers.vastdata import driver_util 

49import manila.share.drivers.vastdata.rest as vast_rest 

50 

51 

52LOG = logging.getLogger(__name__) 

53 

54OPTS = [ 

55 cfg.HostAddressOpt( 

56 "vast_mgmt_host", 

57 help="Hostname or IP address VAST storage system management VIP.", 

58 ), 

59 cfg.PortOpt( 

60 "vast_mgmt_port", 

61 help="Port for VAST management", 

62 default=443 

63 ), 

64 cfg.StrOpt( 

65 "vast_vippool_name", 

66 help="Name of Virtual IP pool" 

67 ), 

68 cfg.StrOpt( 

69 "vast_root_export", 

70 default="manila", 

71 help="Base path for shares" 

72 ), 

73 cfg.StrOpt( 

74 "vast_mgmt_user", 

75 help="Username for VAST management" 

76 ), 

77 cfg.StrOpt( 

78 "vast_mgmt_password", 

79 help="Password for VAST management", 

80 secret=True 

81 ), 

82 cfg.StrOpt( 

83 "vast_api_token", 

84 default="", 

85 secret=True, 

86 help=( 

87 "API token for accessing VAST mgmt. " 

88 "If provided, it will be used instead " 

89 "of 'san_login' and 'san_password'." 

90 ) 

91 ), 

92] 

93 

94CONF = cfg.CONF 

95CONF.register_opts(OPTS) 

96 

97MANILA_TO_VAST_ACCESS_LEVEL = { 

98 constants.ACCESS_LEVEL_RW: "nfs_read_write", 

99 constants.ACCESS_LEVEL_RO: "nfs_read_only", 

100} 

101 

102 

103@driver_util.decorate_methods_with( 

104 driver_util.verbose_driver_trace 

105) 

106class VASTShareDriver(driver.ShareDriver): 

107 """Driver for the VastData Filesystem.""" 

108 

109 VERSION = "1.0" # driver version 

110 

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

112 super().__init__(False, *args, config_opts=[OPTS], **kwargs) 

113 

114 def do_setup(self, context): 

115 """Driver initialization""" 

116 backend_name = self.configuration.safe_get("share_backend_name") 

117 root_export = self.configuration.vast_root_export 

118 vip_pool_name = self.configuration.safe_get("vast_vippool_name") 

119 if not vip_pool_name: 

120 raise exception.VastDriverException( 

121 reason="vast_vippool_name must be set" 

122 ) 

123 self._backend_name = backend_name or self.__class__.__name__ 

124 self._vippool_name = vip_pool_name 

125 self._root_export = "/" + root_export.strip("/") 

126 

127 username = self.configuration.safe_get("vast_mgmt_user") 

128 password = self.configuration.safe_get("vast_mgmt_password") 

129 api_token = self.configuration.safe_get("vast_api_token") 

130 host = self.configuration.safe_get("vast_mgmt_host") 

131 port = self.configuration.safe_get("vast_mgmt_port") 

132 if not host: 

133 raise exception.VastDriverException( 

134 reason="`vast_mgmt_host` must be set in manila.conf." 

135 ) 

136 # Require either (username & password) OR (API token) 

137 if not ((username and password) or api_token): 

138 raise exception.VastDriverException( 

139 reason="Authentication failed: You must specify either " 

140 "`vast_mgmt_user` and `vast_mgmt_password`, " 

141 "or provide `vast_api_token` in manila.conf." 

142 ) 

143 if port: 143 ↛ 145line 143 didn't jump to line 145 because the condition on line 143 was always true

144 host = f"{host}:{port}" 

145 self.rest = vast_rest.RestApi( 

146 host=host, 

147 username=username, 

148 password=password, 

149 api_token=api_token, 

150 ssl_verify=False, 

151 plugin_version=self.VERSION, 

152 ) 

153 LOG.debug("VAST Data driver setup is complete.") 

154 

155 def _update_share_stats(self, data=None): 

156 """Retrieve stats info from share group.""" 

157 metrics_list = [ 

158 "Capacity,drr", 

159 "Capacity,logical_space", 

160 "Capacity,logical_space_in_use", 

161 "Capacity,physical_space", 

162 "Capacity,physical_space_in_use", 

163 ] 

164 metrics = self.rest.capacity_metrics.get(metrics_list) 

165 data = dict( 

166 share_backend_name=self._backend_name, 

167 vendor_name="VAST STORAGE", 

168 driver_version=self.VERSION, 

169 storage_protocol="NFS", 

170 data_reduction=metrics.drr, 

171 total_capacity_gb=float(metrics.logical_space) / units.Gi, 

172 free_capacity_gb=float( 

173 metrics.logical_space - metrics.logical_space_in_use 

174 ) 

175 / units.Gi, 

176 provisioned_capacity_gb=float( 

177 metrics.logical_space_in_use) / units.Gi, 

178 snapshot_support=True, 

179 create_share_from_snapshot_support=False, 

180 mount_snapshot_support=False, 

181 revert_to_snapshot_support=False, 

182 ) 

183 

184 super()._update_share_stats(data) 

185 

186 def _to_volume_path(self, share_id, root=None): 

187 if not root: 187 ↛ 189line 187 didn't jump to line 189 because the condition on line 187 was always true

188 root = self._root_export 

189 return f"{root}/manila-{share_id}" 

190 

191 def create_share(self, context, share, share_server=None): 

192 return self._ensure_share(share) 

193 

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

195 """Called to delete a share""" 

196 share_id = share["id"] 

197 src = self._to_volume_path(share_id) 

198 LOG.debug(f"Deleting '{src}'.") 

199 self.rest.folders.delete(path=src) 

200 self.rest.views.delete(name=share_id) 

201 self.rest.quotas.delete(name=share_id) 

202 self.rest.view_policies.delete(name=share_id) 

203 

204 def update_access( 

205 self, context, share, access_rules, 

206 add_rules, delete_rules, update_rules, share_server=None 

207 ): 

208 """Update access rules for share.""" 

209 rule_state_map = {} 

210 

211 if not (add_rules or delete_rules): 

212 add_rules = access_rules 

213 

214 if share["share_proto"] != "NFS": 

215 LOG.error("The share protocol flavor is invalid. Please use NFS.") 

216 return 

217 

218 valid_add_rules = [] 

219 for rule in (add_rules or []): 

220 try: 

221 validate_access_rule(rule) 

222 except ( 

223 exception.InvalidShareAccess, 

224 exception.InvalidShareAccessLevel, 

225 ) as exc: 

226 rule_id = rule["access_id"] 

227 access_level = rule["access_level"] 

228 access_to = rule["access_to"] 

229 LOG.exception( 

230 f"Failed to provide {access_level} access to " 

231 f"{access_to} (Rule ID: {rule_id}, Reason: {exc}). " 

232 "Setting rule to 'error' state." 

233 ) 

234 rule_state_map[rule['id']] = {'state': 'error'} 

235 else: 

236 valid_add_rules.append(rule) 

237 

238 share_id = share["id"] 

239 export = self._to_volume_path(share_id) 

240 

241 LOG.debug(f"Changing access on {share_id}.") 

242 data = { 

243 "name": share_id, 

244 "nfs_no_squash": ["*"], 

245 "nfs_root_squash": ["*"] 

246 } 

247 policy = self.rest.view_policies.one(name=share_id) 

248 if not policy: 

249 raise exception.VastDriverException( 

250 reason=f"Policy not found for share {share_id}." 

251 ) 

252 if valid_add_rules: 

253 policy_rules = policy_payload_from_rules( 

254 rules=valid_add_rules, policy=policy, action="update" 

255 ) 

256 data.update(policy_rules) 

257 LOG.debug(f"Changing access on {export}. Rules: {policy_rules}.") 

258 self.rest.view_policies.update(policy.id, **data) 

259 

260 if delete_rules: 

261 policy_rules = policy_payload_from_rules( 

262 rules=delete_rules, policy=policy, action="deny" 

263 ) 

264 LOG.debug(f"Changing access on {export}. Rules: {policy_rules}.") 

265 data.update(policy_rules) 

266 self.rest.view_policies.update(policy.id, **data) 

267 

268 return rule_state_map 

269 

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

271 """uses resize_share to extend a share""" 

272 self._resize_share(share, new_size) 

273 

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

275 """uses resize_share to shrink a share""" 

276 self._resize_share(share, new_size) 

277 

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

279 """Is called to create snapshot.""" 

280 path = self._to_volume_path(snapshot["share_instance_id"]) 

281 self.rest.snapshots.create(path=path, name=snapshot["name"]) 

282 

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

284 """Is called to remove share.""" 

285 self.rest.snapshots.delete(name=snapshot["name"]) 

286 

287 def get_network_allocations_number(self): 

288 return 0 

289 

290 def ensure_shares(self, context, shares): 

291 updates = {} 

292 for share in shares: 

293 export_locations = self._ensure_share(share) 

294 updates[share["id"]] = { 

295 'export_locations': export_locations 

296 } 

297 return updates 

298 

299 def get_backend_info(self, context): 

300 backend_info = { 

301 "vast_vippool_name": self.configuration.vast_vippool_name, 

302 "vast_mgmt_host": self.configuration.vast_mgmt_host, 

303 } 

304 return backend_info 

305 

306 def _resize_share(self, share, new_size): 

307 share_id = share["id"] 

308 quota = self.rest.quotas.one(name=share_id) 

309 if not quota: 

310 raise exception.ShareNotFound( 

311 reason="Share not found", share_id=share_id 

312 ) 

313 requested_capacity = new_size * units.Gi 

314 if requested_capacity < quota.used_effective_capacity: 

315 raise exception.ShareShrinkingPossibleDataLoss( 

316 share_id=share['id']) 

317 self.rest.quotas.update(quota.id, hard_limit=requested_capacity) 

318 

319 def _ensure_share(self, share): 

320 share_proto = share["share_proto"] 

321 if share_proto != "NFS": 

322 raise exception.InvalidShare( 

323 reason=_( 

324 "Invalid NAS protocol supplied: {}.".format(share_proto) 

325 ) 

326 ) 

327 

328 vips = self.rest.vip_pools.vips(pool_name=self._vippool_name) 

329 

330 share_id = share["id"] 

331 requested_capacity = share["size"] * units.Gi 

332 path = self._to_volume_path(share_id) 

333 policy = self.rest.view_policies.ensure(name=share_id) 

334 quota = self.rest.quotas.ensure( 

335 name=share_id, path=path, 

336 create_dir=True, hard_limit=requested_capacity 

337 ) 

338 if quota.hard_limit != requested_capacity: 

339 raise exception.VastDriverException( 

340 reason=f"Share already exists with different capacity" 

341 f" (requested={requested_capacity}, exists={quota.hard_limit})" 

342 ) 

343 view = self.rest.views.ensure( 

344 name=share_id, path=path, policy_id=policy.id 

345 ) 

346 if view.policy != share_id: 

347 self.rest.views.update(view.id, policy_id=policy.id) 

348 return [ 

349 dict(path=f"{vip}:{path}", is_admin_only=False) for vip in vips 

350 ] 

351 

352 

353def policy_payload_from_rules(rules, policy, action): 

354 """Convert list of manila rules 

355 

356 into vast compatible payload for updating/creating policy. 

357 """ 

358 hosts = collections.defaultdict(set) 

359 for rule in rules: 

360 addr_list = map( 

361 str, netaddr.IPNetwork(rule["access_to"]).iter_hosts() 

362 ) 

363 hosts[ 

364 MANILA_TO_VAST_ACCESS_LEVEL[rule["access_level"]] 

365 ].update(addr_list) 

366 

367 _default_rules = set() 

368 

369 # Delete default_vast_policy on each update. 

370 # There is no sense to keep * in list of allowed/denied hosts 

371 # as user want to set particular ip/ips only. 

372 _default_vast_policy = {"*"} 

373 if action == "update": 

374 rw = set(policy.nfs_read_write).union( 

375 hosts.get("nfs_read_write", _default_rules) 

376 ) 

377 ro = set(policy.nfs_read_only).union( 

378 hosts.get("nfs_read_only", _default_rules) 

379 ) 

380 elif action == "deny": 

381 rw = set(policy.nfs_read_write).difference( 

382 hosts.get("nfs_read_write", _default_rules) 

383 ) 

384 ro = set(policy.nfs_read_only).difference( 

385 hosts.get("nfs_read_only", _default_rules) 

386 ) 

387 else: 

388 raise ValueError("Invalid action") 

389 

390 # When policy created default access is 

391 # "*" for read-write and read-only operations. 

392 # After updating any of rules (rw or ro) 

393 # we need to delete "*" to prevent ambiguous state when 

394 # resource available for certain ip and for all range of ip addresses. 

395 if len(rw) > 1: 

396 rw -= _default_vast_policy 

397 

398 if len(ro) > 1: 

399 ro -= _default_vast_policy 

400 

401 return {"nfs_read_write": list(rw), "nfs_read_only": list(ro)} 

402 

403 

404def validate_access_rule(access_rule): 

405 allowed_types = {"ip"} 

406 allowed_levels = MANILA_TO_VAST_ACCESS_LEVEL.keys() 

407 

408 access_type = access_rule["access_type"] 

409 access_level = access_rule["access_level"] 

410 if access_type not in allowed_types: 

411 reason = _("Only {} access type allowed.").format( 

412 ", ".join(tuple([f"'{x}'" for x in allowed_types])) 

413 ) 

414 raise exception.InvalidShareAccess(reason=reason) 

415 if access_level not in allowed_levels: 

416 raise exception.InvalidShareAccessLevel(level=access_level) 

417 try: 

418 netaddr.IPNetwork(access_rule["access_to"]) 

419 except (netaddr.core.AddrFormatError, OSError) as exc: 

420 raise exception.InvalidShareAccess(reason=str(exc))