Coverage for nova/share/manila.py: 90%
140 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
13"""
14Handles all requests relating to shares + manila.
15"""
17from dataclasses import dataclass
18import functools
19from typing import Optional
21from openstack import exceptions as sdk_exc
22from oslo_log import log as logging
24import nova.conf
25from nova import exception
26from nova import utils
28CONF = nova.conf.CONF
29LOG = logging.getLogger(__name__)
30MIN_SHARE_FILE_SYSTEM_MICROVERSION = "2.82"
33def _manilaclient(context, admin=False):
34 """Constructs a manila client object for making API requests.
36 :return: An openstack.proxy.Proxy object for the specified service_type.
37 :raise: ConfGroupForServiceTypeNotFound If no conf group name could be
38 found for the specified service_type.
39 :raise: ServiceUnavailable if the service is down
40 """
42 return utils.get_sdk_adapter(
43 "shared-file-system",
44 admin=admin,
45 check_service=True,
46 context=context,
47 shared_file_system_api_version=MIN_SHARE_FILE_SYSTEM_MICROVERSION,
48 global_request_id=context.global_id
49 )
52@dataclass(frozen=True)
53class Share():
54 id: str
55 size: int
56 availability_zone: Optional[str]
57 created_at: str
58 status: str
59 name: Optional[str]
60 description: Optional[str]
61 project_id: str
62 snapshot_id: Optional[str]
63 share_network_id: Optional[str]
64 share_proto: str
65 export_location: str
66 metadata: dict
67 share_type: Optional[str]
68 is_public: bool
70 @classmethod
71 def from_manila_share(cls, manila_share, export_location):
72 return cls(
73 id=manila_share.id,
74 size=manila_share.size,
75 availability_zone=manila_share.availability_zone,
76 created_at=manila_share.created_at,
77 status=manila_share.status,
78 name=manila_share.name,
79 description=manila_share.description,
80 project_id=manila_share.project_id,
81 snapshot_id=manila_share.snapshot_id,
82 share_network_id=manila_share.share_network_id,
83 share_proto=manila_share.share_protocol,
84 export_location=export_location,
85 metadata=manila_share.metadata,
86 share_type=manila_share.share_type,
87 is_public=manila_share.is_public,
88 )
91@dataclass(frozen=True)
92class Access():
93 id: str
94 access_level: str
95 state: str
96 access_type: str
97 access_to: str
98 access_key: Optional[str]
100 @classmethod
101 def from_manila_access(cls, manila_access):
102 return cls(
103 id=manila_access.id,
104 access_level=manila_access.access_level,
105 state=manila_access.state,
106 access_type=manila_access.access_type,
107 access_to=manila_access.access_to,
108 access_key= getattr(manila_access, 'access_key', None)
109 )
111 @classmethod
112 def from_dict(cls, manila_access):
113 return cls(
114 id=manila_access['id'],
115 access_level=manila_access['access_level'],
116 state=manila_access['state'],
117 access_type=manila_access['access_type'],
118 access_to=manila_access['access_to'],
119 access_key=manila_access['access_key'],
120 )
123def translate_sdk_exception(method):
124 """Transforms a manila exception but keeps its traceback intact."""
125 @functools.wraps(method)
126 def wrapper(self, *args, **kwargs):
127 try:
128 res = method(self, *args, **kwargs)
129 except (exception.ServiceUnavailable,
130 exception.ConfGroupForServiceTypeNotFound) as exc:
131 raise exception.ManilaConnectionFailed(reason=str(exc)) from exc
132 except (sdk_exc.BadRequestException) as exc:
133 raise exception.InvalidInput(reason=str(exc)) from exc
134 except (sdk_exc.ForbiddenException) as exc:
135 raise exception.Forbidden(str(exc)) from exc
136 return res
137 return wrapper
140def translate_share_exception(method):
141 """Transforms the exception for the share but keeps its traceback intact.
142 """
144 def wrapper(self, *args, **kwargs):
145 try:
146 res = method(self, *args, **kwargs)
147 except (sdk_exc.ResourceNotFound) as exc:
148 raise exception.ShareNotFound(
149 share_id=args[1], reason=exc) from exc
150 except (sdk_exc.BadRequestException) as exc:
151 raise exception.ShareNotFound(
152 share_id=args[1], reason=exc) from exc
153 return res
154 return translate_sdk_exception(wrapper)
157def translate_allow_exception(method):
158 """Transforms the exception for allow but keeps its traceback intact.
159 """
161 def wrapper(self, *args, **kwargs):
162 try:
163 res = method(self, *args, **kwargs)
164 except (sdk_exc.BadRequestException) as exc:
165 raise exception.ShareAccessGrantError(
166 share_id=args[1], reason=exc) from exc
167 except (sdk_exc.ResourceNotFound) as exc:
168 raise exception.ShareNotFound(
169 share_id=args[1], reason=exc) from exc
170 return res
171 return translate_sdk_exception(wrapper)
174def translate_deny_exception(method):
175 """Transforms the exception for deny but keeps its traceback intact.
176 """
178 def wrapper(self, *args, **kwargs):
179 try:
180 res = method(self, *args, **kwargs)
181 except (sdk_exc.BadRequestException) as exc:
182 raise exception.ShareAccessRemovalError(
183 share_id=args[1], reason=exc) from exc
184 except (sdk_exc.ResourceNotFound) as exc:
185 raise exception.ShareNotFound(
186 share_id=args[1], reason=exc) from exc
187 return res
188 return translate_sdk_exception(wrapper)
191class API(object):
192 """API for interacting with the share manager."""
194 @translate_share_exception
195 def get(self, context, share_id):
196 """Get the details about a share given its ID.
198 :param share_id: the id of the share to get
199 :raises: ShareNotFound if the share_id specified is not available.
200 :returns: Share object.
201 """
203 def filter_export_locations(export_locations):
204 # Return the preferred path otherwise choose the first one
205 paths = []
206 try:
207 for export_location in export_locations: 207 ↛ 212line 207 didn't jump to line 212 because the loop on line 207 didn't complete
208 if export_location.is_preferred: 208 ↛ 211line 208 didn't jump to line 211 because the condition on line 208 was always true
209 return export_location.path
210 else:
211 paths.append(export_location.path)
212 return paths[0]
213 except (IndexError, NameError):
214 return None
216 client = _manilaclient(context, admin=False)
217 LOG.debug("Get share id:'%s' data from manila", share_id)
218 share = client.get_share(share_id)
219 export_locations = client.export_locations(share.id)
220 export_location = filter_export_locations(export_locations)
222 return Share.from_manila_share(share, export_location)
224 @translate_share_exception
225 def get_access(
226 self,
227 context,
228 share_id,
229 access_type,
230 access_to,
231 ):
232 """Get share access
234 :param share_id: the id of the share to get
235 :param access_type: the type of access ("ip", "cert", "user")
236 :param access_to: ip:cidr or cert:cn or user:group or user name
237 :raises: ShareNotFound if the share_id specified is not available.
238 :returns: Access object or None if there is no access granted to this
239 share.
240 """
242 LOG.debug("Get share access id for share id:'%s'",
243 share_id)
244 access_list = _manilaclient(
245 context, admin=True).access_rules(share_id)
247 for access in access_list:
248 if (
249 access.access_type == access_type and
250 access.access_to == access_to
251 ):
252 return Access.from_manila_access(access)
253 return None
255 @translate_allow_exception
256 def allow(
257 self,
258 context,
259 share_id,
260 access_type,
261 access_to,
262 access_level,
263 ):
264 """Allow share access
266 :param share_id: the id of the share
267 :param access_type: the type of access ("ip", "cert", "user")
268 :param access_to: ip:cidr or cert:cn or user:group or user name
269 :param access_level: "ro" for read only or "rw" for read/write
270 :raises: ShareNotFound if the share_id specified is not available.
271 :raises: BadRequest if the share already exists.
272 :raises: ShareAccessGrantError if the answer from manila allow API is
273 not the one expected.
274 """
276 def check_manila_access_response(access):
277 if not ( 277 ↛ 283line 277 didn't jump to line 283 because the condition on line 277 was never true
278 isinstance(access, Access) and
279 access.access_type == access_type and
280 access.access_to == access_to and
281 access.access_level == access_level
282 ):
283 raise exception.ShareAccessGrantError(share_id=share_id)
285 LOG.debug("Allow host access to share id:'%s'",
286 share_id)
288 access = _manilaclient(context, admin=True).create_access_rule(
289 share_id,
290 access_type=access_type,
291 access_to=access_to,
292 access_level=access_level,
293 lock_visibility=True,
294 lock_deletion=True,
295 lock_reason="Lock by nova",
296 )
298 access = Access.from_manila_access(access)
299 check_manila_access_response(access)
300 return access
302 @translate_deny_exception
303 def deny(
304 self,
305 context,
306 share_id,
307 access_type,
308 access_to,
309 ):
310 """Deny share access
311 :param share_id: the id of the share
312 :param access_type: the type of access ("ip", "cert", "user")
313 :param access_to: ip:cidr or cert:cn or user:group or user name
314 :raises: ShareAccessNotFound if the access_id specified is not
315 available.
316 :raises: ShareAccessRemovalError if the manila deny API does not
317 respond with a status code 202.
318 """
320 access = self.get_access(
321 context,
322 share_id,
323 access_type,
324 access_to,
325 )
327 if access:
328 client = _manilaclient(context, admin=True)
329 LOG.debug("Deny host access to share id:'%s'", share_id)
330 resp = client.delete_access_rule(
331 access.id, share_id, unrestrict=True
332 )
333 if resp.status_code != 202:
334 raise exception.ShareAccessRemovalError(
335 share_id=share_id, reason=resp.reason
336 )
337 else:
338 raise exception.ShareAccessNotFound(share_id=share_id)
340 def has_access(self, context, share_id, access_type, access_to):
341 """Helper method to check if a policy is applied to a share
342 :param context: The request context.
343 :param share_id: the id of the share
344 :param access_type: the type of access ("ip", "cert", "user")
345 :param access_to: ip:cidr or cert:cn or user:group or user name
346 :returns: boolean, true means the policy is applied.
347 """
348 access = self.get_access(
349 context,
350 share_id,
351 access_type,
352 access_to
353 )
354 return access is not None and access.state == 'active'