Coverage for manila/share/drivers/macrosan/macrosan_helper.py: 95%
347 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 (c) 2022 MacroSAN Technologies Co., Ltd.
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.
16import re
18from oslo_config import cfg
19from oslo_log import log
20from oslo_utils import units
22from manila import exception
23from manila.i18n import _
24from manila.share.drivers.macrosan import macrosan_constants as constants
25from manila.share.drivers.macrosan import rest_helper
26from manila.share import utils as share_utils
28CONF = cfg.CONF
30LOG = log.getLogger(__name__)
33class MacrosanHelper(object):
34 def __init__(self, configuration):
35 self.configuration = configuration
36 self.rest = rest_helper.RestHelper(self.configuration)
37 self.snapshot_support = False
38 self.replication_support = False
39 self.pools = self.configuration.macrosan_share_pools
41 def check_share_service(self):
42 nfs_service = self.rest._get_nfs_service_status()
43 if nfs_service['serviceStatus'] not in [constants.NFS_NON_CONFIG,
44 constants.NFS_ENABLED,
45 constants.NFS_DISABLED]:
46 raise exception.MacrosanBackendExeption(
47 reason=_("nfs service exception. Please check backend"))
48 elif nfs_service['serviceStatus'] == constants.NFS_NON_CONFIG:
49 self.rest._config_nfs_service()
50 self.rest._start_nfs_service()
51 elif nfs_service['serviceStatus'] == constants.NFS_DISABLED:
52 if (nfs_service['nfs3Status'] == constants.NFS_NON_SUPPORTED and 52 ↛ 55line 52 didn't jump to line 55 because the condition on line 52 was always true
53 nfs_service['nfs4Status'] == constants.NFS_NON_SUPPORTED):
54 self.rest._config_nfs_service()
55 self.rest._start_nfs_service()
56 else:
57 if (nfs_service['nfs3Status'] == constants.NFS_NON_SUPPORTED and
58 nfs_service['nfs4Status'] == constants.NFS_NON_SUPPORTED):
59 self.rest._config_nfs_service()
61 cifs_status = self.rest._get_cifs_service_status()
62 if cifs_status == constants.CIFS_EXCEPTION:
63 raise exception.MacrosanBackendExeption(
64 reason=_("cifs service exception. Please check backend"))
65 elif cifs_status == constants.CIFS_NON_CONFIG:
66 """need config first, then start service"""
67 self.rest._config_cifs_service()
68 self.rest._start_cifs_service()
69 elif cifs_status == constants.CIFS_DISABLED:
70 self.rest._start_cifs_service()
71 status = self.rest._get_cifs_service_status()
72 if status == constants.CIFS_SHARE_MODE: 72 ↛ exitline 72 didn't return from function 'check_share_service' because the condition on line 72 was always true
73 self.rest._config_cifs_service()
74 elif cifs_status == constants.CIFS_SHARE_MODE:
75 self.rest._config_cifs_service()
77 def do_setup(self):
78 """get token"""
79 self.rest.login()
81 def create_share(self, share, share_server=None):
82 """Create a share"""
83 pool_name, share_name, proto = self._get_share_instance_pnp(share)
84 share_size = ''.join((str(share['size']), 'GB'))
86 # first create filesystem
87 self.rest._create_filesystem(fs_name=share_name,
88 pool_name=pool_name,
89 filesystem_quota=share_size)
91 share_path = self._generate_share_path(share_name)
92 # second create filesystem dir
93 self.rest._create_filesystem_dir(share_path)
94 # third create nfs or cifs share
95 if proto == 'NFS':
96 self.rest._create_nfs_share(share_path=share_path)
97 else:
98 user_name = 'manilanobody'
99 user_passwd = 'manilanobody'
100 group_name = 'manilanobody'
101 ret = self._ensure_user(user_name, user_passwd, group_name)
102 if not ret:
103 self.rest._delete_filesystem(share_name)
104 raise exception.MacrosanBackendExeption(
105 reason=(_(
106 'Failed to create share %(share)s. Reason: '
107 'username %(user_name)s error.')
108 % {'share': share_name, 'user_name': user_name}))
110 rw_list = [user_name]
111 rw_list_type = ['0']
112 self.rest._create_cifs_share(share_name=share_name,
113 share_path=share_path,
114 rw_list=rw_list,
115 rw_list_type=rw_list_type)
117 location = self._get_location_path(share_path, share_name, proto)
118 return location
120 def delete_share(self, share, share_server=None):
121 """Delete a share."""
122 pool, share_name, proto = self._get_share_instance_pnp(share)
123 share_path = self._generate_share_path(share_name)
125 backend_share = self._get_share(share_path, proto)
126 if not backend_share:
127 LOG.error(f'Share {share_name} not found.')
128 filesystem = self.rest._get_filesystem(share_name)
129 if filesystem: 129 ↛ exitline 129 didn't return from function 'delete_share' because the condition on line 129 was always true
130 self.rest._delete_filesystem(share_name)
131 else:
132 if proto == 'NFS':
133 self.rest._delete_nfs_share(share_path)
134 else:
135 self.rest._delete_cifs_share(share_name, share_path)
136 self.rest._delete_filesystem(share_name)
138 def extend_share(self, share, new_size, share_server=None):
139 """Extend share"""
140 pool, share_name, proto = self._get_share_instance_pnp(share)
141 share_path = self._generate_share_path(share_name)
142 backend_share = self._get_share(share_path, proto)
143 if not backend_share:
144 msg = f"Can't find the share by share name: {share_name}."
145 msg = _(msg)
146 LOG.error(msg)
147 raise exception.ShareResourceNotFound(share_id=share['id'])
149 # storage size logic already in manila/share/api.py extend func
150 # size param need unit
151 new_size = ''.join((str(new_size), 'GB'))
152 self.rest._update_share_size(share_name, new_size)
154 def shrink_share(self, share, new_size, share_server=None):
155 """Shrink share"""
156 pool, share_name, proto = self._get_share_instance_pnp(share)
157 share_path = self._generate_share_path(share_name)
158 backend_share = self._get_share(share_path, proto)
159 if not backend_share:
160 msg = f"Can't find the share by share name: {share_name}."
161 msg = _(msg)
162 LOG.error(msg)
163 raise exception.ShareResourceNotFound(share_id=share['id'])
165 filesystem_info = self.rest._get_filesystem(share_name)
166 used_size = self._unit_convert_toGB(filesystem_info['usedCapacity'])
167 if new_size <= used_size:
168 raise exception.ShareShrinkingPossibleDataLoss(
169 share_id=share['id'])
170 # storage size logic already in manila/share/api.py shrink func
171 new_size = ''.join((str(new_size), 'GB'))
172 self.rest._update_share_size(share_name, new_size)
174 def ensure_share(self, share, share_server=None):
175 """Enusre that share is exported"""
176 pool, share_name, proto = self._get_share_instance_pnp(share)
177 share_path = self._generate_share_path(share_name)
178 backend_share = self._get_share(share_path, proto)
179 if not backend_share:
180 raise exception.ShareResourceNotFound(share_id=share['id'])
182 location = self._get_location_path(share_path, share_name, proto)
183 return [location]
185 def _allow_access(self, share, access, share_server=None):
186 """Allow access to the share."""
187 pool, share_name, proto = self._get_share_instance_pnp(share)
188 share_path = self._generate_share_path(share_name)
189 access_level = access['access_level']
190 share_id = share['id']
191 if access_level not in ('rw', 'ro'):
192 raise exception.InvalidShareAccess(
193 reason=(_('Unsupported access level: %s.') % access_level))
194 if proto == 'NFS':
195 self._allow_nfs_access(share_path, share_name, access, share_id)
196 elif proto == 'CIFS': 196 ↛ exitline 196 didn't return from function '_allow_access' because the condition on line 196 was always true
197 self._allow_cifs_access(share_path, share_name, access, share_id)
199 def _allow_nfs_access(self, share_path, share_name, access, share_id):
200 """Allow nfs access."""
201 access_type = access['access_type']
202 access_to = access['access_to']
203 access_level = access['access_level']
204 # Only use 'ip',
205 # input "*" replace all, or ip 172.0.1.11 ,
206 # or ip network segment 172.0.1.11/255.255.0.0 172.0.1.11/16
207 if access_type != 'ip':
208 message = (_('NFS shares only allow IP access types. '
209 'access_type: %(access_type)s') %
210 {'access_type': access_type})
211 raise exception.InvalidShareAccess(reason=message)
212 backend_share = self.rest._get_nfs_share(share_path)
213 if not backend_share:
214 msg = (_("Can't find the share by share name: %s.")
215 % share_name)
216 LOG.error(msg)
217 raise exception.ShareResourceNotFound(share_id=share_id)
219 if access_to == '0.0.0.0/0':
220 access_to = '*'
221 share_client = self.rest._get_access_from_nfs_share(share_path,
222 access_to)
224 if share_client:
225 if access_level != share_client['accessRight']: 225 ↛ exitline 225 didn't return from function '_allow_nfs_access' because the condition on line 225 was always true
226 self.rest._change_nfs_access_rest(share_path,
227 access_to,
228 access_level)
229 else:
230 self.rest._allow_nfs_access_rest(share_path,
231 access_to,
232 access_level)
234 def _allow_cifs_access(self, share_path, share_name, access, share_id):
235 """Allow cifs access."""
236 access_type = access['access_type']
237 access_to = access['access_to']
238 access_level = access['access_level']
239 if access_type != 'user':
240 message = _('Only user access type is '
241 'allowed for CIFS shares.')
242 raise exception.InvalidShareAccess(reason=message)
244 backend_share = self.rest._get_cifs_share(share_path)
245 if not backend_share:
246 msg = (_("Can't find the share by share name: %s.")
247 % share_name)
248 LOG.error(msg)
249 raise exception.ShareResourceNotFound(share_id=share_id)
251 share_client = self.rest._get_access_from_cifs_share(share_path,
252 access_to)
253 if share_client:
254 if access_level != share_client['accessRight']: 254 ↛ exitline 254 didn't return from function '_allow_cifs_access' because the condition on line 254 was always true
255 self.rest._change_cifs_access_rest(share_path,
256 access_to,
257 access_level,
258 share_client['ugType'])
259 else:
260 self.rest._allow_cifs_access_rest(share_path,
261 access_to,
262 access_level)
264 def _deny_access(self, share, access, share_server=None):
265 """Deny access to the share."""
266 pool, share_name, proto = self._get_share_instance_pnp(share)
267 share_path = self._generate_share_path(share_name)
269 if proto == 'NFS':
270 self._deny_nfs_access(share_path, share_name, access)
271 else:
272 self._deny_cifs_access(share_path, share_name, access)
274 def _deny_nfs_access(self, share_path, share_name, access):
275 """Deny nfs access."""
276 access_type = access['access_type']
277 access_to = access['access_to']
278 if access_type != 'ip':
279 LOG.error('Only IP access types are allowed '
280 'for NFS shares.')
281 return
282 if access_to == '0.0.0.0/0':
283 access_to = '*'
284 share_client = self.rest._get_access_from_nfs_share(share_path,
285 access_to)
286 if not share_client:
287 LOG.error(f'Could not list the share access for share '
288 f'{share_name}')
289 return
290 self.rest._delete_nfs_access_rest(share_path, access_to)
292 def _deny_cifs_access(self, share_path, share_name, access):
293 """Deny cifs access."""
294 access_type = access['access_type']
295 access_to = access['access_to']
296 if access_type != 'user':
297 LOG.error('Only USER access types are allowed '
298 'for CIFS shares.')
299 return
300 share_client = self.rest._get_access_from_cifs_share(share_path,
301 access_to)
302 if not share_client:
303 LOG.error(f'Could not list the share access for share '
304 f'{share_name}')
305 return
306 self.rest._delete_cifs_access_rest(share_path,
307 share_client['ugName'],
308 share_client['ugType'])
310 def _clear_access(self, share, share_server=None):
311 """Remove all access rules of the share"""
312 pool, share_name, proto = self._get_share_instance_pnp(share)
313 share_path = self._generate_share_path(share_name)
314 access_list = self._get_all_access_from_share(share_path, proto)
315 if not access_list:
316 LOG.error(f'Could not list the share access for share '
317 f'{share_name}')
318 return
320 if proto == 'NFS':
321 for share_access in access_list:
322 # IPv4 Address Blocks Reserved for Documentation
323 if share_access['access_to'] == '192.0.2.0': 323 ↛ 324line 323 didn't jump to line 324 because the condition on line 323 was never true
324 continue
325 self.rest._delete_nfs_access_rest(share_path,
326 share_access['access_to'])
327 elif proto == 'CIFS': 327 ↛ exitline 327 didn't return from function '_clear_access' because the condition on line 327 was always true
328 for share_access in access_list:
329 if (share_access['access_to'] == 'manilanobody'
330 and share_access['ugType'] == '0'):
331 continue
332 self.rest._delete_cifs_access_rest(share_path,
333 share_access['access_to'],
334 share_access['ugType'])
336 def update_access(self, share, access_rules, add_rules,
337 delete_rules, share_server=None):
338 """Update access rules list."""
339 access_updates = {}
340 if not (add_rules or delete_rules):
341 self._clear_access(share, share_server)
342 for access_rule in access_rules:
343 try:
344 self._allow_access(share, access_rule, share_server)
345 except exception.InvalidShareAccess as e:
346 msg = f'Failed to allow {access_rule["access_level"]} ' \
347 f'access to {access_rule["access_to"]}, reason {e}'
348 msg = _(msg)
349 LOG.error(msg)
350 access_updates.update(
351 {access_rule['access_id']: {'state': 'error'}})
352 else:
353 for access_rule in delete_rules:
354 self._deny_access(share, access_rule, share_server)
355 for access_rule in add_rules:
356 try:
357 self._allow_access(share, access_rule, share_server)
358 except exception.InvalidShareAccess as e:
359 msg = f'Failed to allow {access_rule["access_level"]} ' \
360 f'access to {access_rule["access_to"]}, reason {e}'
361 msg = _(msg)
362 LOG.error(msg)
363 access_updates.update(
364 {access_rule['access_id']: {'state': 'error'}})
365 return access_updates
367 def _get_all_access_from_share(self, share_path, share_proto):
368 access_list = []
369 if share_proto == 'NFS':
370 access_list = self.rest._get_all_nfs_access_rest(share_path)
371 elif share_proto == 'CIFS': 371 ↛ 373line 371 didn't jump to line 373 because the condition on line 371 was always true
372 access_list = self.rest._get_all_cifs_access_rest(share_path)
373 return access_list
375 def _ensure_user(self, user_name, user_passwd, group_name):
376 ret_user = self.rest._query_user(user_name)
377 if ret_user == constants.USER_NOT_EXIST:
378 ret_group = self.rest._query_group(group_name)
379 if ret_group not in [constants.GROUP_NOT_EXIST,
380 constants.GROUP_EXIST]:
381 msg = f'Failed to use group {group_name}'
382 msg = _(msg)
383 raise exception.InvalidInput(reason=msg)
384 elif ret_group == constants.GROUP_NOT_EXIST: 384 ↛ 386line 384 didn't jump to line 386 because the condition on line 384 was always true
385 self.rest._add_localgroup(group_name)
386 self.rest._add_localuser(user_name, user_passwd, group_name)
387 return True
388 elif ret_user == constants.USER_EXIST:
389 return True
390 else:
391 return False
393 def update_share_stats(self, dict_data):
394 """Update pools info"""
395 result = self.rest._get_all_pool()
396 dict_data["pools"] = []
397 for pool_name in self.pools:
398 pool_capacity = self._get_pool_capacity(pool_name, result)
399 if pool_capacity:
400 pool = {
401 'pool_name': pool_name,
402 'total_capacity_gb': pool_capacity['totalcapacity'],
403 'free_capacity_gb': pool_capacity['freecapacity'],
404 'allocated_capacity_gb':
405 pool_capacity['allocatedcapacity'],
406 'reserved_percentage':
407 self.configuration.reserved_share_percentage,
408 'reserved_snapshot_percentage':
409 self.configuration
410 .reserved_share_from_snapshot_percentage
411 or self.configuration.reserved_share_percentage,
412 'reserved_share_extend_percentage':
413 self.configuration.reserved_share_extend_percentage
414 or self.configuration.reserved_share_percentage,
415 'dedupe': False,
416 'compression': False,
417 'qos': False,
418 'thin_provisioning': False,
419 'snapshot_support': self.snapshot_support,
420 'create_share_from_snapshot_support':
421 self.snapshot_support,
422 }
424 dict_data["pools"].append(pool)
426 if not dict_data['pools']:
427 msg = _("StoragePool is None")
428 LOG.error(msg)
429 raise exception.InvalidInput(reason=msg)
431 def _get_pool_capacity(self, pool_name, result):
432 """Get total,allocated,free capacity of the pools"""
433 pool_info = self._find_pool_info(pool_name, result)
435 if pool_info: 435 ↛ 447line 435 didn't jump to line 447 because the condition on line 435 was always true
436 total_capacity = int(self._unit_convert_toGB(
437 pool_info['totalcapacity']))
438 free_capacity = int(self._unit_convert_toGB(
439 pool_info['freecapacity']))
440 allocated_capacity = int(self._unit_convert_toGB(
441 pool_info['allocatedcapacity']))
443 pool_info['totalcapacity'] = total_capacity
444 pool_info['freecapacity'] = free_capacity
445 pool_info['allocatedcapacity'] = allocated_capacity
447 return pool_info
449 def _unit_convert_toGB(self, capacity):
450 """Convert unit to GB"""
451 capacity = capacity.upper()
453 try:
454 # get unit char array and use join connect to string
455 unit = re.findall(r'[A-Z]', capacity)
456 unit = ''.join(unit)
457 except BaseException:
458 unit = ''
459 # get capacity size,unit is GB
460 capacity = capacity.replace(unit, '')
461 capacity = float(capacity)
462 if unit in ['B', '']:
463 capacity = capacity / units.Gi
464 elif unit in ['K', 'KB']:
465 capacity = capacity / units.Mi
466 elif unit in ['M', 'MB']:
467 capacity = capacity / units.Ki
468 elif unit in ['G', 'GB']:
469 capacity = capacity
470 elif unit in ['T', 'TB']: 470 ↛ 472line 470 didn't jump to line 472 because the condition on line 470 was always true
471 capacity = capacity * units.Ki
472 elif unit in ['E', 'EB']:
473 capacity = capacity * units.Mi
475 capacity = '%.0f' % capacity
477 return float(capacity)
479 def _generate_share_name(self, share):
480 share_name = 'manila_%s' % share['id']
481 return self._format_name(share_name)
483 def _format_name(self, name):
484 """format name to meet the backend requirements"""
485 name = name[0: 31]
486 name = name.replace('-', '_')
487 return name
489 def _generate_share_path(self, share_name):
490 """add '/' as path"""
491 share_path = r'/%(path)s/%(dirName)s' % {
492 'path': share_name.replace("-", "_"),
493 'dirName': share_name.replace("-", "_")
494 }
495 return share_path
497 def _get_location_path(self, share_path, share_name, share_proto, ip=None):
498 location = None
499 if ip is None: 499 ↛ 501line 499 didn't jump to line 501 because the condition on line 499 was always true
500 ip = self.configuration.macrosan_nas_ip
501 share_proto = share_proto.upper()
502 if share_proto == 'NFS':
503 location = f'{ip}:{share_path}'
504 elif share_proto == 'CIFS': 504 ↛ 506line 504 didn't jump to line 506 because the condition on line 504 was always true
505 location = f'\\\\{ip}\\{share_name}'
506 return location
508 def _get_share_instance_pnp(self, share_instance):
510 proto = share_instance['share_proto'].upper()
511 share_name = self._generate_share_name(share_instance)
512 pool = share_utils.extract_host(share_instance['host'], level='pool')
513 if not pool:
514 msg = _("Pool doesn't exist in host field.")
515 raise exception.InvalidHost(reason=msg)
517 if proto != 'NFS' and proto != 'CIFS':
518 msg = f'Share protocol {proto} is not supported.'
519 msg = _(msg)
520 raise exception.MacrosanBackendExeption(reason=msg)
522 return pool, share_name, proto
524 def _get_share(self, share_path, proto):
525 return (self.rest._get_nfs_share(share_path)
526 if proto == 'NFS'
527 else self.rest._get_cifs_share(share_path))
529 def _find_pool_info(self, pool_name, result):
530 if pool_name is None: 530 ↛ 531line 530 didn't jump to line 531 because the condition on line 530 was never true
531 return
532 pool_info = {}
533 pool_name = pool_name.strip()
535 for item in result.get('data', []):
536 if pool_name == item['name']:
537 pool_info['name'] = item['name']
538 pool_info['totalcapacity'] = item['size']
539 pool_info['allocatedcapacity'] = item['allocated']
540 pool_info['freecapacity'] = item['free']
541 pool_info['health'] = item['health']
542 pool_info['rw'] = item['rwStatus']
543 break
545 return pool_info