Coverage for manila/share/drivers/inspur/instorage/cli_helper.py: 89%
250 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 2019 Inspur Corp.
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.
16"""
17CLI helpers for Inspur InStorage
18"""
20import paramiko
21import re
22import time
24from oslo_concurrency import processutils
25from oslo_log import log
26from oslo_utils import excutils
28from manila import exception
29from manila.i18n import _
30from manila import ssh_utils
31from manila import utils as manila_utils
33LOG = log.getLogger(__name__)
36class SSHRunner(object):
37 """SSH runner is used to run ssh command on inspur instorage system."""
39 def __init__(self, host, port, login, password, privatekey=None):
40 self.host = host
41 self.port = port
42 self.login = login
43 self.password = password
44 self.privatekey = privatekey
46 self.ssh_conn_timeout = 60
47 self.ssh_min_pool_size = 1
48 self.ssh_max_pool_size = 10
50 self.sshpool = None
52 def __call__(self, cmd_list, check_exit_code=True, attempts=1):
53 """SSH tool"""
54 manila_utils.check_ssh_injection(cmd_list)
55 command = ' '.join(cmd_list)
56 if not self.sshpool: 56 ↛ 71line 56 didn't jump to line 71 because the condition on line 56 was always true
57 try:
58 self.sshpool = ssh_utils.SSHPool(
59 self.host,
60 self.port,
61 self.ssh_conn_timeout,
62 self.login,
63 password=self.password,
64 privatekey=self.privatekey,
65 min_size=self.ssh_min_pool_size,
66 max_size=self.ssh_max_pool_size
67 )
68 except paramiko.SSHException:
69 LOG.error("Unable to create SSHPool")
70 raise
71 try:
72 return self._ssh_execute(self.sshpool, command,
73 check_exit_code, attempts)
74 except Exception:
75 LOG.error("Error running SSH command: %s", command)
76 raise
78 def _ssh_execute(self, sshpool, command,
79 check_exit_code=True, attempts=1):
80 try:
81 with sshpool.item() as ssh:
82 last_exception = None
83 while attempts > 0:
84 attempts -= 1
85 try:
86 return processutils.ssh_execute(
87 ssh,
88 command,
89 check_exit_code=check_exit_code)
90 except Exception as e:
91 LOG.exception('Error has occurred')
92 last_exception = e
93 time.sleep(1)
95 try:
96 raise processutils.ProcessExecutionError(
97 exit_code=last_exception.exit_code,
98 stdout=last_exception.stdout,
99 stderr=last_exception.stderr,
100 cmd=last_exception.cmd)
101 except AttributeError:
102 raise processutils.ProcessExecutionError(
103 exit_code=-1,
104 stdout="",
105 stderr="Error running SSH command",
106 cmd=command)
108 except Exception:
109 with excutils.save_and_reraise_exception():
110 LOG.error("Error running SSH command: %s", command)
113class CLIParser(object):
114 """Parse MCS CLI output and generate iterable."""
116 def __init__(self, raw, ssh_cmd=None, delim='!', with_header=True):
117 super(CLIParser, self).__init__()
118 if ssh_cmd: 118 ↛ 121line 118 didn't jump to line 121 because the condition on line 118 was always true
119 self.ssh_cmd = ' '.join(ssh_cmd)
120 else:
121 self.ssh_cmd = 'None'
122 self.raw = raw
123 self.delim = delim
124 self.with_header = with_header
125 self.result = self._parse()
127 def __getitem__(self, key):
128 try:
129 return self.result[key]
130 except KeyError:
131 msg = (_('Did not find the expected key %(key)s in %(fun)s: '
132 '%(raw)s.') % {'key': key, 'fun': self.ssh_cmd,
133 'raw': self.raw})
134 raise exception.ShareBackendException(msg=msg)
136 def __iter__(self):
137 for a in self.result:
138 yield a
140 def __len__(self):
141 return len(self.result)
143 def _parse(self):
144 def get_reader(content, delim):
145 for line in content.lstrip().splitlines():
146 line = line.strip()
147 if line:
148 yield line.split(delim)
149 else:
150 yield []
152 if isinstance(self.raw, str):
153 stdout, stderr = self.raw, ''
154 else:
155 stdout, stderr = self.raw
156 reader = get_reader(stdout, self.delim)
157 result = []
159 if self.with_header:
160 hds = tuple()
161 for row in reader:
162 hds = row
163 break
164 for row in reader:
165 cur = dict()
166 if len(hds) != len(row): 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 msg = (_('Unexpected CLI response: header/row mismatch. '
168 'header: %(header)s, row: %(row)s.')
169 % {'header': hds,
170 'row': row})
171 raise exception.ShareBackendException(msg=msg)
172 for k, v in zip(hds, row):
173 CLIParser.append_dict(cur, k, v)
174 result.append(cur)
175 else:
176 cur = dict()
177 for row in reader:
178 if row:
179 CLIParser.append_dict(cur, row[0], ' '.join(row[1:]))
180 elif cur: # start new section 180 ↛ 177line 180 didn't jump to line 177 because the condition on line 180 was always true
181 result.append(cur)
182 cur = dict()
183 if cur: 183 ↛ 185line 183 didn't jump to line 185 because the condition on line 183 was always true
184 result.append(cur)
185 return result
187 @staticmethod
188 def append_dict(dict_, key, value):
189 key, value = key.strip(), value.strip()
190 obj = dict_.get(key, None)
191 if obj is None: 191 ↛ 193line 191 didn't jump to line 193 because the condition on line 191 was always true
192 dict_[key] = value
193 elif isinstance(obj, list):
194 obj.append(value)
195 dict_[key] = obj
196 else:
197 dict_[key] = [obj, value]
198 return dict_
201class InStorageSSH(object):
202 """SSH interface to Inspur InStorage systems."""
204 def __init__(self, ssh_runner):
205 self._ssh = ssh_runner
207 def _run_ssh(self, ssh_cmd):
208 try:
209 return self._ssh(ssh_cmd)
210 except processutils.ProcessExecutionError as e:
211 msg = (_('CLI Exception output:\n command: %(cmd)s\n '
212 'stdout: %(out)s\n stderr: %(err)s.') %
213 {'cmd': ssh_cmd,
214 'out': e.stdout,
215 'err': e.stderr})
216 LOG.error(msg)
217 raise exception.ShareBackendException(msg=msg)
219 def run_ssh_inq(self, ssh_cmd, delim='!', with_header=False):
220 """Run an SSH command and return parsed output."""
221 raw = self._run_ssh(ssh_cmd)
222 LOG.debug('Response for cmd %s is %s', ssh_cmd, raw)
223 return CLIParser(raw, ssh_cmd=ssh_cmd, delim=delim,
224 with_header=with_header)
226 def run_ssh_assert_no_output(self, ssh_cmd):
227 """Run an SSH command and assert no output returned."""
228 out, err = self._run_ssh(ssh_cmd)
229 if len(out.strip()) != 0: 229 ↛ 230line 229 didn't jump to line 230 because the condition on line 229 was never true
230 msg = (_('Expected no output from CLI command %(cmd)s, '
231 'got %(out)s.') % {'cmd': ' '.join(ssh_cmd), 'out': out})
232 LOG.error(msg)
233 raise exception.ShareBackendException(msg=msg)
235 def run_ssh_check_created(self, ssh_cmd):
236 """Run an SSH command and return the ID of the created object."""
237 out, err = self._run_ssh(ssh_cmd)
238 try:
239 match_obj = re.search(r'\[([0-9]+)\],? successfully created', out)
240 return match_obj.group(1)
241 except (AttributeError, IndexError):
242 msg = (_('Failed to parse CLI output:\n command: %(cmd)s\n '
243 'stdout: %(out)s\n stderr: %(err)s.') %
244 {'cmd': ssh_cmd,
245 'out': out,
246 'err': err})
247 LOG.error(msg)
248 raise exception.ShareBackendException(msg=msg)
250 def lsnode(self, node_id=None):
251 with_header = True
252 ssh_cmd = ['mcsinq', 'lsnode', '-delim', '!']
253 if node_id:
254 with_header = False
255 ssh_cmd.append(node_id)
256 return self.run_ssh_inq(ssh_cmd, with_header=with_header)
258 def lsnaspool(self, pool_id=None):
259 ssh_cmd = ['mcsinq', 'lsnaspool', '-delim', '!']
260 if pool_id:
261 ssh_cmd.append(pool_id)
262 return self.run_ssh_inq(ssh_cmd, with_header=True)
264 def lsfs(self, node_name=None, fsname=None):
265 if fsname and not node_name:
266 msg = _('Node name should be set when file system name is set.')
267 LOG.error(msg)
268 raise exception.InvalidParameterValue(msg)
270 ssh_cmd = ['mcsinq', 'lsfs', '-delim', '!']
271 to_append = []
273 if node_name:
274 to_append += ['-node', '"%s"' % node_name]
276 if fsname:
277 to_append += ['-name', '"%s"' % fsname]
279 if not to_append:
280 to_append += ['-all']
282 ssh_cmd += to_append
283 return self.run_ssh_inq(ssh_cmd, with_header=True)
285 def addfs(self, fsname, pool_name, size, node_name):
286 """Create a file system on the storage.
288 :param fsname: file system name
289 :param pool_name: pool in which to create the file system
290 :param size: file system size in GB
291 :param node_name: the primary node name
292 :return:
293 """
295 ssh_cmd = ['mcsop', 'addfs', '-name', '"%s"' % fsname, '-pool',
296 '"%s"' % pool_name, '-size', '%dg' % size,
297 '-node', '"%s"' % node_name]
298 self.run_ssh_assert_no_output(ssh_cmd)
300 def rmfs(self, fsname):
301 """Remove the specific file system.
303 :param fsname: file system name to be removed
304 :return:
305 """
307 ssh_cmd = ['mcsop', 'rmfs', '-name', '"%s"' % fsname]
308 self.run_ssh_assert_no_output(ssh_cmd)
310 def expandfs(self, fsname, size):
311 """Expand the space of the specific file system.
313 :param fsname: file system name
314 :param size: the size(GB) to be expanded, origin + size = result
315 :return:
316 """
318 ssh_cmd = ['mcsop', 'expandfs', '-name', '"%s"' % fsname,
319 '-size', '%dg' % size]
320 self.run_ssh_assert_no_output(ssh_cmd)
322 # NAS directory operation
323 def lsnasdir(self, dirpath):
324 """List the child directory under dirpath.
326 :param dirpath: the parent directory to list with
327 :return:
328 """
330 ssh_cmd = ['mcsinq', 'lsnasdir', '-delim', '!', '"%s"' % dirpath]
331 return self.run_ssh_inq(ssh_cmd, with_header=True)
333 def addnasdir(self, dirpath):
334 """Create a new NAS directory indicated by dirpath."""
336 ssh_cmd = ['mcsop', 'addnasdir', '"%s"' % dirpath]
337 self.run_ssh_assert_no_output(ssh_cmd)
339 def chnasdir(self, old_path, new_path):
340 """Rename the NAS directory name."""
342 ssh_cmd = ['mcsop', 'chnasdir', '-oldpath', '"%s"' % old_path,
343 '-newpath', '"%s"' % new_path]
344 self.run_ssh_assert_no_output(ssh_cmd)
346 def rmnasdir(self, dirpath):
347 """Remove the specific dirpath."""
349 ssh_cmd = ['mcsop', 'rmnasdir', '"%s"' % dirpath]
350 self.run_ssh_assert_no_output(ssh_cmd)
352 # NFS operation
353 def rmnfs(self, share_path):
354 """Remove the NFS indicated by path."""
356 ssh_cmd = ['mcsop', 'rmnfs', '"%s"' % share_path]
357 self.run_ssh_assert_no_output(ssh_cmd)
359 def lsnfslist(self, prefix=None):
360 """List NFS shares on a system."""
362 ssh_cmd = ['mcsinq', 'lsnfslist', '-delim', '!']
363 if prefix:
364 ssh_cmd.append('"%s"' % prefix)
366 return self.run_ssh_inq(ssh_cmd, with_header=True)
368 def lsnfsinfo(self, share_path):
369 """List a specific NFS share's information."""
371 ssh_cmd = ['mcsinq', 'lsnfsinfo', '-delim', '!', '"%s"' % share_path]
372 return self.run_ssh_inq(ssh_cmd, with_header=True)
374 def addnfsclient(self, share_path, client_spec):
375 """Add a client access rule to NFS share.
377 :param share_path: the NFS share path.
378 :param client_spec: IP/MASK:RIGHTS:ALL_SQUASH:ROOT_SQUASH.
379 :return:
380 """
382 ssh_cmd = ['mcsop', 'addnfsclient', '-path', '"%s"' % share_path,
383 '-client', client_spec]
384 self.run_ssh_assert_no_output(ssh_cmd)
386 def chnfsclient(self, share_path, client_spec):
387 """Change a NFS share's client info."""
389 ssh_cmd = ['mcsop', 'chnfsclient', '-path', '"%s"' % share_path,
390 '-client', client_spec]
391 self.run_ssh_assert_no_output(ssh_cmd)
393 def rmnfsclient(self, share_path, client_spec):
394 """Remove a client info from the NFS share."""
396 # client_spec parameter for rmnfsclient is IP/MASK,
397 # so we need remove the right part
398 client_spec = client_spec.split(':')[0]
400 ssh_cmd = ['mcsop', 'rmnfsclient', '-path', '"%s"' % share_path,
401 '-client', client_spec]
402 self.run_ssh_assert_no_output(ssh_cmd)
404 # CIFS operation
405 def lscifslist(self, filter=None):
406 """List CIFS shares on the system."""
408 ssh_cmd = ['mcsinq', 'lscifslist', '-delim', '!']
409 if filter:
410 ssh_cmd.append('"%s"' % filter)
412 return self.run_ssh_inq(ssh_cmd, with_header=True)
414 def lscifsinfo(self, share_name):
415 """List a specific CIFS share's information."""
417 ssh_cmd = ['mcsinq', 'lscifsinfo', '-delim', '!', '"%s"' % share_name]
418 return self.run_ssh_inq(ssh_cmd, with_header=True)
420 def addcifs(self, share_name, dirpath, oplocks='off'):
421 """Create a CIFS share with given path."""
422 ssh_cmd = ['mcsop', 'addcifs', '-name', share_name, '-path', dirpath,
423 '-oplocks', oplocks]
424 self.run_ssh_assert_no_output(ssh_cmd)
426 def rmcifs(self, share_name):
427 """Remove a CIFS share."""
429 ssh_cmd = ['mcsop', 'rmcifs', share_name]
430 self.run_ssh_assert_no_output(ssh_cmd)
432 def chcifs(self, share_name, oplocks='off'):
433 """Change a CIFS share's attribute.
435 :param share_name: share's name
436 :param oplocks: 'off' or 'on'
437 :return:
438 """
439 ssh_cmd = ['mcsop', 'chcifs', '-name', share_name, '-oplocks', oplocks]
440 self.run_ssh_assert_no_output(ssh_cmd)
442 def addcifsuser(self, share_name, rights):
443 """Add a user access rule to CIFS share.
445 :param share_name: share's name
446 :param rights: [LU|LG]:xxx:[rw|ro]
447 :return:
448 """
449 ssh_cmd = ['mcsop', 'addcifsuser', '-name', share_name,
450 '-rights', rights]
451 self.run_ssh_assert_no_output(ssh_cmd)
453 def chcifsuser(self, share_name, rights):
454 """Change a user access rule."""
456 ssh_cmd = ['mcsop', 'chcifsuser', '-name', share_name,
457 '-rights', rights]
458 self.run_ssh_assert_no_output(ssh_cmd)
460 def rmcifsuser(self, share_name, rights):
461 """Remove CIFS user from a CIFS share."""
463 # the rights parameter for rmcifsuser is LU:NAME
464 rights = ':'.join(rights.split(':')[0:-1])
466 ssh_cmd = ['mcsop', 'rmcifsuser', '-name', share_name,
467 '-rights', rights]
468 self.run_ssh_assert_no_output(ssh_cmd)
470 # NAS port ip
471 def lsnasportip(self):
472 """List NAS service port ip address."""
474 ssh_cmd = ['mcsinq', 'lsnasportip', '-delim', '!']
475 return self.run_ssh_inq(ssh_cmd, with_header=True)