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
« 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
19Configuration:
22[DEFAULT]
23enabled_share_backends = vast
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"""
37import collections
39import netaddr
40from oslo_config import cfg
41from oslo_log import log as logging
42from oslo_utils import units
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
52LOG = logging.getLogger(__name__)
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]
94CONF = cfg.CONF
95CONF.register_opts(OPTS)
97MANILA_TO_VAST_ACCESS_LEVEL = {
98 constants.ACCESS_LEVEL_RW: "nfs_read_write",
99 constants.ACCESS_LEVEL_RO: "nfs_read_only",
100}
103@driver_util.decorate_methods_with(
104 driver_util.verbose_driver_trace
105)
106class VASTShareDriver(driver.ShareDriver):
107 """Driver for the VastData Filesystem."""
109 VERSION = "1.0" # driver version
111 def __init__(self, *args, **kwargs):
112 super().__init__(False, *args, config_opts=[OPTS], **kwargs)
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("/")
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.")
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 )
184 super()._update_share_stats(data)
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}"
191 def create_share(self, context, share, share_server=None):
192 return self._ensure_share(share)
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)
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 = {}
211 if not (add_rules or delete_rules):
212 add_rules = access_rules
214 if share["share_proto"] != "NFS":
215 LOG.error("The share protocol flavor is invalid. Please use NFS.")
216 return
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)
238 share_id = share["id"]
239 export = self._to_volume_path(share_id)
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)
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)
268 return rule_state_map
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)
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)
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"])
283 def delete_snapshot(self, context, snapshot, share_server=None):
284 """Is called to remove share."""
285 self.rest.snapshots.delete(name=snapshot["name"])
287 def get_network_allocations_number(self):
288 return 0
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
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
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)
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 )
328 vips = self.rest.vip_pools.vips(pool_name=self._vippool_name)
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 ]
353def policy_payload_from_rules(rules, policy, action):
354 """Convert list of manila rules
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)
367 _default_rules = set()
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")
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
398 if len(ro) > 1:
399 ro -= _default_vast_policy
401 return {"nfs_read_write": list(rw), "nfs_read_only": list(ro)}
404def validate_access_rule(access_rule):
405 allowed_types = {"ip"}
406 allowed_levels = MANILA_TO_VAST_ACCESS_LEVEL.keys()
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))