Coverage for manila/share/drivers/infortrend/infortrend_nas.py: 60%
423 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) 2019 Infortrend Technology, 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.
16import json
17import re
19from oslo_concurrency import processutils
20from oslo_log import log
21from oslo_utils import units
23from manila.common import constants
24from manila import exception
25from manila.i18n import _
26from manila.share import utils as share_utils
27from manila import ssh_utils
28from manila import utils as manila_utils
31LOG = log.getLogger(__name__)
34def _bi_to_gi(bi_size):
35 return bi_size / units.Gi
38class InfortrendNAS(object):
40 _SSH_PORT = 22
42 def __init__(self, nas_ip, username, password, ssh_key,
43 timeout, pool_dict, channel_dict):
44 self.nas_ip = nas_ip
45 self.port = self._SSH_PORT
46 self.username = username
47 self.password = password
48 self.ssh_key = ssh_key
49 self.ssh_timeout = timeout
50 self.pool_dict = pool_dict
51 self.channel_dict = channel_dict
52 self.command = ""
53 self.ssh = None
54 self.sshpool = None
55 self.location = 'a@0'
57 def _execute(self, command_line):
58 command_line.extend(['-z', self.location])
59 commands = ' '.join(command_line)
60 manila_utils.check_ssh_injection(commands)
61 LOG.debug('Executing: %(command)s', {'command': commands})
63 cli_out = self._ssh_execute(commands)
65 return self._parser(cli_out)
67 def _ssh_execute(self, commands):
68 try:
69 out, err = processutils.ssh_execute(
70 self.ssh, commands,
71 timeout=self.ssh_timeout, check_exit_code=True)
72 except processutils.ProcessExecutionError as pe:
73 rc = pe.exit_code
74 out = pe.stdout
75 out = out.replace('\n', '\\n')
76 msg = _('Error on execute ssh command. '
77 'Exit code: %(rc)d, msg: %(out)s') % {
78 'rc': rc, 'out': out}
79 raise exception.InfortrendNASException(err=msg)
81 return out
83 def _parser(self, content=None):
84 LOG.debug('parsing data:\n%s', content)
85 content = content.replace("\r", "")
86 content = content.strip()
87 json_string = content.replace("'", "\"")
88 cli_data = json_string.splitlines()[2]
89 if cli_data: 89 ↛ 105line 89 didn't jump to line 105 because the condition on line 89 was always true
90 try:
91 data_dict = json.loads(cli_data)
92 except Exception:
93 msg = _('Failed to parse data: '
94 '%(cli_data)s to dictionary.') % {
95 'cli_data': cli_data}
96 LOG.error(msg)
97 raise exception.InfortrendNASException(err=msg)
99 rc = int(data_dict['cliCode'][0]['Return'], 16)
100 if rc == 0: 100 ↛ 103line 100 didn't jump to line 103 because the condition on line 100 was always true
101 result = data_dict['data']
102 else:
103 result = data_dict['cliCode'][0]['CLI']
104 else:
105 msg = _('No data is returned from NAS.')
106 LOG.error(msg)
107 raise exception.InfortrendNASException(err=msg)
109 if rc != 0: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 msg = _('NASCLI error, returned: %(result)s.') % {
111 'result': result}
112 LOG.error(msg)
113 raise exception.InfortrendCLIException(
114 err=msg, rc=rc, out=result)
116 return rc, result
118 def do_setup(self):
119 self._init_connect()
120 self._ensure_service_on('nfs')
121 self._ensure_service_on('cifs')
123 def _init_connect(self):
124 if not (self.sshpool and self.ssh):
125 self.sshpool = ssh_utils.SSHPool(ip=self.nas_ip,
126 port=self.port,
127 conn_timeout=None,
128 login=self.username,
129 password=self.password,
130 privatekey=self.ssh_key)
131 self.ssh = self.sshpool.create()
133 if not self.ssh.get_transport().is_active():
134 self.sshpool = ssh_utils.SSHPool(ip=self.nas_ip,
135 port=self.port,
136 conn_timeout=None,
137 login=self.username,
138 password=self.password,
139 privatekey=self.ssh_key)
140 self.ssh = self.sshpool.create()
142 LOG.debug('NAScmd [%s@%s] start!', self.username, self.nas_ip)
144 def check_for_setup_error(self):
145 self._check_pools_setup()
146 self._check_channels_status()
148 def _ensure_service_on(self, proto, slot='A'):
149 command_line = ['service', 'status', proto]
150 rc, service_status = self._execute(command_line)
151 if not service_status[0][slot][proto.upper()]['enabled']: 151 ↛ exitline 151 didn't return from function '_ensure_service_on' because the condition on line 151 was always true
152 command_line = ['service', 'restart', proto]
153 self._execute(command_line)
155 def _check_channels_status(self):
156 channel_list = list(self.channel_dict.keys())
157 command_line = ['ifconfig', 'inet', 'show']
158 rc, channels_status = self._execute(command_line)
159 for channel in channels_status:
160 if 'CH' in channel['datalink']:
161 ch = channel['datalink'].strip('CH')
162 if ch in self.channel_dict.keys():
163 self.channel_dict[ch] = channel['IP']
164 channel_list.remove(ch)
165 if channel['status'] == 'DOWN':
166 LOG.warning('Channel [%(ch)s] status '
167 'is down, please check.', {
168 'ch': ch})
169 if len(channel_list) != 0:
170 msg = _('Channel setting %(channel_list)s is invalid!') % {
171 'channel_list': channel_list}
172 LOG.error(msg)
173 raise exception.InfortrendNASException(message=msg)
175 def _check_pools_setup(self):
176 pool_list = list(self.pool_dict.keys())
177 command_line = ['folder', 'status']
178 rc, pool_data = self._execute(command_line)
179 for pool in pool_data:
180 pool_name = self._extract_pool_name(pool)
181 if pool_name in self.pool_dict.keys():
182 pool_list.remove(pool_name)
183 self.pool_dict[pool_name]['id'] = pool['volumeId']
184 self.pool_dict[pool_name]['path'] = pool['directory'] + '/'
185 if len(pool_list) == 0:
186 break
188 if len(pool_list) != 0:
189 msg = _('Please create %(pool_list)s pool/s in advance!') % {
190 'pool_list': pool_list}
191 LOG.error(msg)
192 raise exception.InfortrendNASException(message=msg)
194 def _extract_pool_name(self, pool_info):
195 return pool_info['directory'].split('/')[1]
197 def _extract_lv_name(self, pool_info):
198 return pool_info['path'].split('/')[2]
200 def update_pools_stats(self):
201 pools = []
202 command_line = ['folder', 'status']
203 rc, pools_data = self._execute(command_line)
205 for pool_info in pools_data:
206 pool_name = self._extract_pool_name(pool_info)
208 if pool_name in self.pool_dict.keys():
209 total_space = float(pool_info['size'])
210 pool_quota_used = self._get_pool_quota_used(pool_name)
211 available_space = total_space - pool_quota_used
213 total_capacity_gb = round(_bi_to_gi(total_space), 2)
214 free_capacity_gb = round(_bi_to_gi(available_space), 2)
216 pool = {
217 'pool_name': pool_name,
218 'total_capacity_gb': total_capacity_gb,
219 'free_capacity_gb': free_capacity_gb,
220 'reserved_percentage': 0,
221 'qos': False,
222 'dedupe': False,
223 'compression': False,
224 'snapshot_support': False,
225 'thin_provisioning': False,
226 'thick_provisioning': True,
227 'replication_type': None,
228 }
229 pools.append(pool)
231 return pools
233 def _get_pool_quota_used(self, pool_name):
234 pool_quota_used = 0.0
235 pool_data = self._get_share_pool_data(pool_name)
236 folder_name = self._extract_lv_name(pool_data)
238 command_line = ['fquota', 'status', pool_data['id'],
239 folder_name, '-t', 'folder']
240 rc, quota_status = self._execute(command_line)
242 for share_quota in quota_status:
243 pool_quota_used += int(share_quota['quota'])
245 return pool_quota_used
247 def _get_share_pool_data(self, pool_name):
248 if not pool_name: 248 ↛ 249line 248 didn't jump to line 249 because the condition on line 248 was never true
249 msg = _("Pool is not available in the share host.")
250 raise exception.InvalidHost(reason=msg)
252 if pool_name in self.pool_dict.keys(): 252 ↛ 255line 252 didn't jump to line 255 because the condition on line 252 was always true
253 return self.pool_dict[pool_name]
254 else:
255 msg = _('Pool [%(pool_name)s] not set in conf.') % {
256 'pool_name': pool_name}
257 LOG.error(msg)
258 raise exception.InfortrendNASException(err=msg)
260 def create_share(self, share, share_server=None):
261 pool_name = share_utils.extract_host(share['host'], level='pool')
262 pool_data = self._get_share_pool_data(pool_name)
263 folder_name = self._extract_lv_name(pool_data)
264 share_proto = share['share_proto'].lower()
265 share_name = share['id'].replace('-', '')
266 share_path = pool_data['path'] + share_name
268 command_line = ['folder', 'options', pool_data['id'],
269 folder_name, '-c', share_name]
270 self._execute(command_line)
272 self._set_share_size(
273 pool_data['id'], pool_name, share_name, share['size'])
274 self._ensure_protocol_on(share_path, share_proto, share_name)
276 LOG.info('Create Share [%(share)s] completed.', {
277 'share': share['id']})
279 return self._export_location(
280 share_name, share_proto, pool_data['path'])
282 def _export_location(self, share_name, share_proto, pool_path=None):
283 location = []
284 location_data = {
285 'pool_path': pool_path,
286 'share_name': share_name,
287 }
288 self._check_channels_status()
289 for ch in sorted(self.channel_dict.keys()):
290 ip = self.channel_dict[ch]
291 if share_proto == 'nfs':
292 location.append(
293 ip + ':%(pool_path)s%(share_name)s' % location_data)
294 elif share_proto == 'cifs': 294 ↛ 298line 294 didn't jump to line 298 because the condition on line 294 was always true
295 location.append(
296 '\\\\' + ip + '\\%(share_name)s' % location_data)
297 else:
298 msg = _('Unsupported protocol: [%s].') % share_proto
299 raise exception.InvalidInput(msg)
301 return location
303 def _set_share_size(self, pool_id, pool_name, share_name, share_size):
304 pool_data = self._get_share_pool_data(pool_name)
305 folder_name = self._extract_lv_name(pool_data)
306 command_line = ['fquota', 'create', pool_id, folder_name,
307 share_name, str(share_size) + 'G', '-t', 'folder']
308 self._execute(command_line)
310 LOG.debug('Set Share [%(share_name)s] '
311 'Size [%(share_size)s G] completed.', {
312 'share_name': share_name,
313 'share_size': share_size})
314 return
316 def _get_share_size(self, pool_id, pool_name, share_name):
317 share_size = None
318 command_line = ['fquota', 'status', pool_id,
319 share_name, '-t', 'folder']
320 rc, quota_status = self._execute(command_line)
322 for share_quota in quota_status:
323 if share_quota['name'] == share_name:
324 share_size = round(_bi_to_gi(float(share_quota['quota'])), 2)
325 break
327 return share_size
329 def delete_share(self, share, share_server=None):
330 pool_name = share_utils.extract_host(share['host'], level='pool')
331 pool_data = self._get_share_pool_data(pool_name)
332 folder_name = self._extract_lv_name(pool_data)
333 share_name = share['id'].replace('-', '')
335 if self._check_share_exist(pool_name, share_name):
336 command_line = ['folder', 'options', pool_data['id'],
337 folder_name, '-d', share_name]
338 self._execute(command_line)
339 else:
340 LOG.warning('Share [%(share_name)s] is already deleted.', {
341 'share_name': share_name})
343 LOG.info('Delete Share [%(share)s] completed.', {
344 'share': share['id']})
346 def _check_share_exist(self, pool_name, share_name):
347 path = self.pool_dict[pool_name]['path']
348 command_line = ['pagelist', 'folder', path]
349 rc, subfolders = self._execute(command_line)
350 return any(subfolder['name'] == share_name for subfolder in subfolders)
352 def update_access(self, share, access_rules, add_rules,
353 delete_rules, share_server=None):
354 self._evict_unauthorized_clients(share, access_rules, share_server)
355 access_dict = {}
356 for access in access_rules:
357 try:
358 self._allow_access(share, access, share_server)
359 except (exception.InfortrendNASException) as e:
360 msg = _('Failed to allow access to client %(access)s, '
361 'reason %(e)s.') % {
362 'access': access['access_to'], 'e': e}
363 LOG.error(msg)
364 access_dict[access['id']] = 'error'
366 return access_dict
368 def _evict_unauthorized_clients(self, share, access_rules,
369 share_server=None):
370 pool_name = share_utils.extract_host(share['host'], level='pool')
371 pool_data = self._get_share_pool_data(pool_name)
372 share_proto = share['share_proto'].lower()
373 share_name = share['id'].replace('-', '')
374 share_path = pool_data['path'] + share_name
376 access_list = []
377 for access in access_rules:
378 access_list.append(access['access_to'])
380 if share_proto == 'nfs':
381 host_ip_list = []
382 command_line = ['share', 'status', '-f', share_path]
383 rc, nfs_status = self._execute(command_line)
384 host_list = nfs_status[0]['nfs_detail']['hostList']
385 for host in host_list:
386 if host['host'] != '*':
387 host_ip_list.append(host['host'])
388 for ip in host_ip_list:
389 if ip not in access_list:
390 command_line = ['share', 'options', share_path,
391 'nfs', '-c', ip]
392 try:
393 self._execute(command_line)
394 except exception.InfortrendNASException:
395 msg = _("Failed to remove share access rule %s") % (ip)
396 LOG.exception(msg)
397 pass
399 elif share_proto == 'cifs':
400 host_user_list = []
401 command_line = ['acl', 'get', share_path]
402 rc, cifs_status = self._execute(command_line)
403 for cifs_rule in cifs_status:
404 if cifs_rule['name']:
405 host_user_list.append(cifs_rule['name'])
406 for user in host_user_list:
407 if user not in access_list:
408 command_line = ['acl', 'delete', share_path, '-u', user]
409 try:
410 self._execute(command_line)
411 except exception.InfortrendNASException:
412 msg = _("Failed to remove share access rule %s") % (
413 user)
414 LOG.exception(msg)
415 pass
417 def _allow_access(self, share, access, share_server=None):
418 pool_name = share_utils.extract_host(share['host'], level='pool')
419 pool_data = self._get_share_pool_data(pool_name)
420 share_name = share['id'].replace('-', '')
421 share_path = pool_data['path'] + share_name
422 share_proto = share['share_proto'].lower()
423 access_type = access['access_type']
424 access_level = access['access_level'] or constants.ACCESS_LEVEL_RW
425 access_to = access['access_to']
426 ACCESS_LEVEL_MAP = {access_level: access_level}
427 msg = self._check_access_legal(share_proto, access_type)
428 if msg:
429 raise exception.InvalidShareAccess(reason=msg)
431 if share_proto == 'nfs':
432 command_line = ['share', 'options', share_path, 'nfs',
433 '-h', access_to, '-p', access_level]
434 self._execute(command_line)
436 elif share_proto == 'cifs':
437 if not self._check_user_exist(access_to):
438 msg = _('Please create user [%(user)s] in advance.') % {
439 'user': access_to}
440 LOG.error(msg)
441 raise exception.InfortrendNASException(err=msg)
443 if access_level == constants.ACCESS_LEVEL_RW:
444 cifs_access = 'f'
445 elif access_level == constants.ACCESS_LEVEL_RO:
446 cifs_access = 'r'
447 try:
448 access_level = ACCESS_LEVEL_MAP[access_level]
449 except KeyError:
450 msg = _('Unsupported access_level: [%s].') % access_level
451 raise exception.InvalidInput(msg)
453 command_line = ['acl', 'set', share_path,
454 '-u', access_to, '-a', cifs_access]
455 self._execute(command_line)
457 LOG.info('Share [%(share)s] access to [%(access_to)s] '
458 'level [%(level)s] protocol [%(share_proto)s] completed.', {
459 'share': share['id'],
460 'access_to': access_to,
461 'level': access_level,
462 'share_proto': share_proto})
464 def _ensure_protocol_on(self, share_path, share_proto, cifs_name):
465 if not self._check_proto_enabled(share_path, share_proto): 465 ↛ exitline 465 didn't return from function '_ensure_protocol_on' because the condition on line 465 was always true
466 command_line = ['share', share_path, share_proto, 'on']
467 if share_proto == 'cifs':
468 command_line.extend(['-n', cifs_name])
469 self._execute(command_line)
471 def _check_proto_enabled(self, share_path, share_proto):
472 command_line = ['share', 'status', '-f', share_path]
473 rc, share_status = self._execute(command_line)
474 if share_status: 474 ↛ 478line 474 didn't jump to line 478 because the condition on line 474 was always true
475 check_enabled = share_status[0][share_proto]
476 if check_enabled: 476 ↛ 477line 476 didn't jump to line 477 because the condition on line 476 was never true
477 return True
478 return False
480 def _check_user_exist(self, user_name):
481 command_line = ['useradmin', 'user', 'list']
482 rc, user_list = self._execute(command_line)
483 for user in user_list:
484 if user['Name'] == user_name:
485 return True
486 return False
488 def _check_access_legal(self, share_proto, access_type):
489 msg = None
490 if share_proto == 'cifs' and access_type != 'user':
491 msg = _('Infortrend CIFS share only supports USER access type.')
492 elif share_proto == 'nfs' and access_type != 'ip':
493 msg = _('Infortrend NFS share only supports IP access type.')
494 elif share_proto not in ('nfs', 'cifs'):
495 msg = _('Unsupported share protocol [%s].') % share_proto
496 return msg
498 def get_pool(self, share):
499 pool_name = share_utils.extract_host(share['host'], level='pool')
500 if not pool_name:
501 share_name = share['id'].replace('-', '')
502 for pool in self.pool_dict.keys(): 502 ↛ 506line 502 didn't jump to line 506 because the loop on line 502 didn't complete
503 if self._check_share_exist(pool, share_name): 503 ↛ 502line 503 didn't jump to line 502 because the condition on line 503 was always true
504 pool_name = pool
505 break
506 return pool_name
508 def ensure_share(self, share, share_server=None):
509 share_proto = share['share_proto'].lower()
510 pool_name = share_utils.extract_host(share['host'], level='pool')
511 pool_data = self._get_share_pool_data(pool_name)
512 share_name = share['id'].replace('-', '')
513 return self._export_location(
514 share_name, share_proto, pool_data['path'])
516 def extend_share(self, share, new_size, share_server=None):
517 pool_name = share_utils.extract_host(share['host'], level='pool')
518 pool_data = self._get_share_pool_data(pool_name)
519 share_name = share['id'].replace('-', '')
520 self._set_share_size(pool_data['id'], pool_name, share_name, new_size)
522 LOG.info('Successfully Extend Share [%(share)s] '
523 'to size [%(new_size)s G].', {
524 'share': share['id'],
525 'new_size': new_size})
527 def shrink_share(self, share, new_size, share_server=None):
528 pool_name = share_utils.extract_host(share['host'], level='pool')
529 pool_data = self._get_share_pool_data(pool_name)
530 share_name = share['id'].replace('-', '')
531 folder_name = self._extract_lv_name(pool_data)
533 command_line = ['fquota', 'status', pool_data['id'],
534 folder_name, '-t', 'folder']
535 rc, quota_status = self._execute(command_line)
537 for share_quota in quota_status:
538 if share_quota['name'] == share_name:
539 used_space = round(_bi_to_gi(float(share_quota['used'])), 2)
541 if new_size < used_space:
542 raise exception.ShareShrinkingPossibleDataLoss(
543 share_id=share['id'])
545 self._set_share_size(pool_data['id'], pool_name, share_name, new_size)
547 LOG.info('Successfully Shrink Share [%(share)s] '
548 'to size [%(new_size)s G].', {
549 'share': share['id'],
550 'new_size': new_size})
552 def manage_existing(self, share, driver_options):
553 share_proto = share['share_proto'].lower()
554 pool_name = share_utils.extract_host(share['host'], level='pool')
555 pool_data = self._get_share_pool_data(pool_name)
556 volume_name = self._extract_lv_name(pool_data)
557 input_location = share['export_locations'][0]['path']
558 share_name = share['id'].replace('-', '')
560 ch_ip, folder_name = self._parse_location(input_location, share_proto)
562 if not self._check_channel_ip(ch_ip):
563 msg = _('Export location ip: [%(ch_ip)s] '
564 'is incorrect, please use data port ip.') % {
565 'ch_ip': ch_ip}
566 LOG.error(msg)
567 raise exception.InfortrendNASException(err=msg)
569 if not self._check_share_exist(pool_name, folder_name):
570 msg = _('Can not find folder [%(folder_name)s] '
571 'in pool [%(pool_name)s].') % {
572 'folder_name': folder_name,
573 'pool_name': pool_name}
574 LOG.error(msg)
575 raise exception.InfortrendNASException(err=msg)
577 share_path = pool_data['path'] + folder_name
578 self._ensure_protocol_on(share_path, share_proto, share_name)
579 share_size = self._get_share_size(
580 pool_data['id'], pool_name, folder_name)
582 if not share_size:
583 msg = _('Folder [%(folder_name)s] has no size limitation, '
584 'please set it first for Openstack management.') % {
585 'folder_name': folder_name}
586 LOG.error(msg)
587 raise exception.InfortrendNASException(err=msg)
589 # rename folder name
590 command_line = ['folder', 'options', pool_data['id'], volume_name,
591 '-k', folder_name, share_name]
592 self._execute(command_line)
594 location = self._export_location(
595 share_name, share_proto, pool_data['path'])
597 LOG.info('Successfully Manage Infortrend Share [%(folder_name)s], '
598 'Size: [%(size)s G], Protocol: [%(share_proto)s], '
599 'new name: [%(share_name)s].', {
600 'folder_name': folder_name,
601 'size': share_size,
602 'share_proto': share_proto,
603 'share_name': share_name})
605 return {'size': share_size, 'export_locations': location}
607 def _parse_location(self, input_location, share_proto):
608 ip = None
609 folder_name = None
610 pattern_ip = r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'
611 if share_proto == 'nfs':
612 pattern_folder = r'[^\/]+$'
613 ip = "".join(re.findall(pattern_ip, input_location))
614 folder_name = "".join(re.findall(pattern_folder, input_location))
616 elif share_proto == 'cifs': 616 ↛ 621line 616 didn't jump to line 621 because the condition on line 616 was always true
617 pattern_folder = r'[^\\]+$'
618 ip = "".join(re.findall(pattern_ip, input_location))
619 folder_name = "".join(re.findall(pattern_folder, input_location))
621 if not (ip and folder_name):
622 msg = _('Export location error, please check '
623 'ip: [%(ip)s], folder_name: [%(folder_name)s].') % {
624 'ip': ip,
625 'folder_name': folder_name}
626 LOG.error(msg)
627 raise exception.InfortrendNASException(err=msg)
629 return ip, folder_name
631 def _check_channel_ip(self, channel_ip):
632 return any(ip == channel_ip for ip in self.channel_dict.values())
634 def unmanage(self, share):
635 pool_name = share_utils.extract_host(share['host'], level='pool')
636 share_name = share['id'].replace('-', '')
638 if not self._check_share_exist(pool_name, share_name):
639 LOG.warning('Share [%(share_name)s] does not exist.', {
640 'share_name': share_name})
641 return
643 LOG.info('Successfully Unmanaged Share [%(share)s].', {
644 'share': share['id']})