Coverage for manila/share/drivers/ibm/gpfs.py: 94%
719 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 2014 IBM Corp.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14"""
15GPFS Driver for shares.
17Config Requirements:
18 GPFS file system must have quotas enabled (`mmchfs -Q yes`).
19Notes:
20 GPFS independent fileset is used for each share.
22TODO(nileshb): add support for share server creation/deletion/handling.
24Limitation:
25 While using remote GPFS node, with Ganesha NFS, 'gpfs_ssh_private_key'
26 for remote login to the GPFS node must be specified and there must be
27 a passwordless authentication already setup between the Manila share
28 service and the remote GPFS node.
30"""
31import abc
32import math
33import os
34import re
35import shlex
36import socket
38from oslo_config import cfg
39from oslo_log import log
40from oslo_utils import excutils
41from oslo_utils import importutils
42from oslo_utils import strutils
43from oslo_utils import units
45from manila.common import constants
46from manila import exception
47from manila.i18n import _
48from manila.share import driver
49from manila.share.drivers.helpers import NFSHelper
50from manila.share import share_types
51from manila import ssh_utils
52from manila import utils
54LOG = log.getLogger(__name__)
56# matches multiple comma separated avpairs on a line. values with an embedded
57# comma must be wrapped in quotation marks
58AVPATTERN = re.compile(r'\s*(?P<attr>\w+)\s*=\s*(?P<val>'
59 r'(["][a-zA-Z0-9_, ]+["])|(\w+))\s*[,]?')
61ERR_FILE_NOT_FOUND = 2
63gpfs_share_opts = [
64 cfg.HostAddressOpt('gpfs_share_export_ip',
65 help='IP to be added to GPFS export string.'),
66 cfg.StrOpt('gpfs_mount_point_base',
67 default='$state_path/mnt',
68 help='Base folder where exported shares are located.'),
69 cfg.StrOpt('gpfs_nfs_server_type',
70 default='CES',
71 help=('NFS Server type. Valid choices are "CES" (Ganesha NFS) '
72 'or "KNFS" (Kernel NFS).')),
73 cfg.ListOpt('gpfs_nfs_server_list',
74 help=('A list of the fully qualified NFS server names that '
75 'make up the OpenStack Manila configuration.')),
76 cfg.BoolOpt('is_gpfs_node',
77 default=False,
78 help=('True:when Manila services are running on one of the '
79 'Spectrum Scale node. '
80 'False:when Manila services are not running on any of '
81 'the Spectrum Scale node.')),
82 cfg.PortOpt('gpfs_ssh_port',
83 default=22,
84 help='GPFS server SSH port.'),
85 cfg.StrOpt('gpfs_ssh_login',
86 help='GPFS server SSH login name.'),
87 cfg.StrOpt('gpfs_ssh_password',
88 secret=True,
89 help='GPFS server SSH login password. '
90 'The password is not needed, if \'gpfs_ssh_private_key\' '
91 'is configured.'),
92 cfg.StrOpt('gpfs_ssh_private_key',
93 help='Path to GPFS server SSH private key for login.'),
94 cfg.ListOpt('gpfs_share_helpers',
95 default=[
96 'KNFS=manila.share.drivers.ibm.gpfs.KNFSHelper',
97 'CES=manila.share.drivers.ibm.gpfs.CESHelper',
98 ],
99 help='Specify list of share export helpers.'),
100]
103CONF = cfg.CONF
104CONF.register_opts(gpfs_share_opts)
107class GPFSShareDriver(driver.ExecuteMixin, driver.GaneshaMixin,
108 driver.ShareDriver):
110 """GPFS Share Driver.
112 Executes commands relating to Shares.
113 Supports creation of shares on a GPFS cluster.
115 API version history:
117 1.0 - Initial version.
118 1.1 - Added extend_share functionality
119 2.0 - Added CES support for NFS Ganesha
120 """
122 def __init__(self, *args, **kwargs):
123 """Do initialization."""
124 super(GPFSShareDriver, self).__init__(False, *args, **kwargs)
125 self._helpers = {}
126 self.configuration.append_config_values(gpfs_share_opts)
127 self.backend_name = self.configuration.safe_get(
128 'share_backend_name') or "IBM Storage System"
129 self.sshpool = None
130 self.ssh_connections = {}
131 self._gpfs_execute = None
132 if self.configuration.is_gpfs_node: 132 ↛ 133line 132 didn't jump to line 133 because the condition on line 132 was never true
133 self.GPFS_PATH = ''
134 else:
135 self.GPFS_PATH = '/usr/lpp/mmfs/bin/'
137 def do_setup(self, context):
138 """Any initialization the share driver does while starting."""
139 super(GPFSShareDriver, self).do_setup(context)
140 if self.configuration.is_gpfs_node:
141 self._gpfs_execute = self._gpfs_local_execute
142 else:
143 self._gpfs_execute = self._gpfs_remote_execute
144 self._setup_helpers()
146 def _gpfs_local_execute(self, *cmd, **kwargs):
147 if 'run_as_root' not in kwargs: 147 ↛ 149line 147 didn't jump to line 149 because the condition on line 147 was always true
148 kwargs.update({'run_as_root': True})
149 if 'ignore_exit_code' in kwargs: 149 ↛ 154line 149 didn't jump to line 154 because the condition on line 149 was always true
150 check_exit_code = kwargs.pop('ignore_exit_code')
151 check_exit_code.append(0)
152 kwargs.update({'check_exit_code': check_exit_code})
154 return utils.execute(*cmd, **kwargs)
156 def _gpfs_remote_execute(self, *cmd, **kwargs):
157 host = self.configuration.gpfs_share_export_ip
158 check_exit_code = kwargs.pop('check_exit_code', True)
159 ignore_exit_code = kwargs.pop('ignore_exit_code', None)
161 return self._run_ssh(host, cmd, ignore_exit_code, check_exit_code)
163 def _sanitize_command(self, cmd_list):
164 # pylint: disable=too-many-function-args
165 return ' '.join(shlex.quote(cmd_arg) for cmd_arg in cmd_list)
167 def _run_ssh(self, host, cmd_list, ignore_exit_code=None,
168 check_exit_code=True):
169 command = self._sanitize_command(cmd_list)
170 if not self.sshpool: 170 ↛ 187line 170 didn't jump to line 187 because the condition on line 170 was always true
171 gpfs_ssh_login = self.configuration.gpfs_ssh_login
172 password = self.configuration.gpfs_ssh_password
173 privatekey = self.configuration.gpfs_ssh_private_key
174 gpfs_ssh_port = self.configuration.gpfs_ssh_port
175 ssh_conn_timeout = self.configuration.ssh_conn_timeout
176 min_size = self.configuration.ssh_min_pool_conn
177 max_size = self.configuration.ssh_max_pool_conn
179 self.sshpool = ssh_utils.SSHPool(host,
180 gpfs_ssh_port,
181 ssh_conn_timeout,
182 gpfs_ssh_login,
183 password=password,
184 privatekey=privatekey,
185 min_size=min_size,
186 max_size=max_size)
187 try:
188 with self.sshpool.item() as ssh:
189 return self._gpfs_ssh_execute(
190 ssh,
191 command,
192 ignore_exit_code=ignore_exit_code,
193 check_exit_code=check_exit_code)
195 except Exception as e:
196 with excutils.save_and_reraise_exception():
197 msg = (_('Error running SSH command: %(cmd)s. '
198 'Error: %(excmsg)s.') %
199 {'cmd': command, 'excmsg': e})
200 LOG.error(msg)
201 raise exception.GPFSException(msg)
203 def _gpfs_ssh_execute(self, ssh, cmd, ignore_exit_code=None,
204 check_exit_code=True):
205 sanitized_cmd = strutils.mask_password(cmd)
206 LOG.debug('Running cmd (SSH): %s', sanitized_cmd)
208 stdin_stream, stdout_stream, stderr_stream = ssh.exec_command(cmd)
209 channel = stdout_stream.channel
211 stdout = stdout_stream.read()
212 sanitized_stdout = strutils.mask_password(stdout)
213 stderr = stderr_stream.read()
214 sanitized_stderr = strutils.mask_password(stderr)
216 stdin_stream.close()
218 exit_status = channel.recv_exit_status()
220 # exit_status == -1 if no exit code was returned
221 if exit_status != -1:
222 LOG.debug('Result was %s', exit_status)
223 if ((check_exit_code and exit_status != 0) 223 ↛ 232line 223 didn't jump to line 232 because the condition on line 223 was always true
224 and
225 (ignore_exit_code is None or
226 exit_status not in ignore_exit_code)):
227 raise exception.ProcessExecutionError(exit_code=exit_status,
228 stdout=sanitized_stdout,
229 stderr=sanitized_stderr,
230 cmd=sanitized_cmd)
232 return (sanitized_stdout, sanitized_stderr)
234 def _check_gpfs_state(self):
235 try:
236 out, __ = self._gpfs_execute(self.GPFS_PATH + 'mmgetstate', '-Y')
237 except exception.ProcessExecutionError as e:
238 msg = (_('Failed to check GPFS state. Error: %(excmsg)s.') %
239 {'excmsg': e})
240 LOG.error(msg)
241 raise exception.GPFSException(msg)
242 lines = out.splitlines()
243 try:
244 state_token = lines[0].split(':').index('state')
245 gpfs_state = lines[1].split(':')[state_token]
246 except (IndexError, ValueError) as e:
247 msg = (_('Failed to check GPFS state. Error: %(excmsg)s.') %
248 {'excmsg': e})
249 LOG.error(msg)
250 raise exception.GPFSException(msg)
251 if gpfs_state != 'active':
252 return False
253 return True
255 def _is_dir(self, path):
256 try:
257 output, __ = self._gpfs_execute('stat', '--format=%F', path,
258 run_as_root=False)
259 except exception.ProcessExecutionError as e:
260 msg = (_('%(path)s is not a directory. Error: %(excmsg)s') %
261 {'path': path, 'excmsg': e})
262 LOG.error(msg)
263 raise exception.GPFSException(msg)
265 return output.strip() == 'directory'
267 def _is_gpfs_path(self, directory):
268 try:
269 self._gpfs_execute(self.GPFS_PATH + 'mmlsattr', directory)
270 except exception.ProcessExecutionError as e:
271 msg = (_('%(dir)s is not on GPFS filesystem. Error: %(excmsg)s.') %
272 {'dir': directory, 'excmsg': e})
273 LOG.error(msg)
274 raise exception.GPFSException(msg)
276 return True
278 def _setup_helpers(self):
279 """Initializes protocol-specific NAS drivers."""
280 self._helpers = {}
281 for helper_str in self.configuration.gpfs_share_helpers:
282 share_proto, _, import_str = helper_str.partition('=')
283 helper = importutils.import_class(import_str)
284 self._helpers[share_proto.upper()] = helper(self._gpfs_execute,
285 self.configuration)
287 def _local_path(self, sharename):
288 """Get local path for a share or share snapshot by name."""
289 return os.path.join(self.configuration.gpfs_mount_point_base,
290 sharename)
292 def _get_gpfs_device(self):
293 fspath = self.configuration.gpfs_mount_point_base
294 try:
295 (out, __) = self._gpfs_execute('df', fspath)
296 except exception.ProcessExecutionError as e:
297 msg = (_('Failed to get GPFS device for %(fspath)s.'
298 'Error: %(excmsg)s') %
299 {'fspath': fspath, 'excmsg': e})
300 LOG.error(msg)
301 raise exception.GPFSException(msg)
303 lines = out.splitlines()
304 fs = lines[1].split()[0]
305 return fs
307 def _create_share(self, shareobj):
308 """Create a linked fileset file in GPFS.
310 Note: GPFS file system must have quotas enabled
311 (mmchfs -Q yes).
312 """
313 sharename = shareobj['name']
314 sizestr = '%sG' % shareobj['size']
315 sharepath = self._local_path(sharename)
316 fsdev = self._get_gpfs_device()
318 # create fileset for the share, link it to root path and set max size
319 try:
320 self._gpfs_execute(self.GPFS_PATH + 'mmcrfileset', fsdev,
321 sharename, '--inode-space', 'new')
322 except exception.ProcessExecutionError as e:
323 msg = (_('Failed to create fileset on %(fsdev)s for '
324 'the share %(sharename)s. Error: %(excmsg)s.') %
325 {'fsdev': fsdev, 'sharename': sharename,
326 'excmsg': e})
327 LOG.error(msg)
328 raise exception.GPFSException(msg)
330 try:
331 self._gpfs_execute(self.GPFS_PATH + 'mmlinkfileset', fsdev,
332 sharename, '-J', sharepath)
333 except exception.ProcessExecutionError as e:
334 msg = (_('Failed to link fileset for the share %(sharename)s. '
335 'Error: %(excmsg)s.') %
336 {'sharename': sharename, 'excmsg': e})
337 LOG.error(msg)
338 raise exception.GPFSException(msg)
340 try:
341 self._gpfs_execute(self.GPFS_PATH + 'mmsetquota', fsdev + ':' +
342 sharename, '--block', '0:' + sizestr)
343 except exception.ProcessExecutionError as e:
344 msg = (_('Failed to set quota for the share %(sharename)s. '
345 'Error: %(excmsg)s.') %
346 {'sharename': sharename, 'excmsg': e})
347 LOG.error(msg)
348 raise exception.GPFSException(msg)
350 try:
351 self._gpfs_execute('chmod', '777', sharepath)
352 except exception.ProcessExecutionError as e:
353 msg = (_('Failed to set permissions for share %(sharename)s. '
354 'Error: %(excmsg)s.') %
355 {'sharename': sharename, 'excmsg': e})
356 LOG.error(msg)
357 raise exception.GPFSException(msg)
359 def _delete_share(self, shareobj):
360 """Remove container by removing GPFS fileset."""
361 sharename = shareobj['name']
362 fsdev = self._get_gpfs_device()
363 # ignore error, when the fileset does not exist
364 # it may happen, when the share creation failed, the share is in
365 # 'error' state, and the fileset was never created
366 # we want to ignore that error condition while deleting the fileset,
367 # i.e. 'Fileset name share-xyz not found', with error code '2'
368 # and mark the deletion successful
369 ignore_exit_code = [ERR_FILE_NOT_FOUND]
371 # unlink and delete the share's fileset
372 try:
373 self._gpfs_execute(self.GPFS_PATH + 'mmunlinkfileset', fsdev,
374 sharename, '-f',
375 ignore_exit_code=ignore_exit_code)
376 except exception.ProcessExecutionError as e:
377 msg = (_('Failed unlink fileset for share %(sharename)s. '
378 'Error: %(excmsg)s.') %
379 {'sharename': sharename, 'excmsg': e})
380 LOG.error(msg)
381 raise exception.GPFSException(msg)
383 try:
384 self._gpfs_execute(self.GPFS_PATH + 'mmdelfileset', fsdev,
385 sharename, '-f',
386 ignore_exit_code=ignore_exit_code)
387 except exception.ProcessExecutionError as e:
388 msg = (_('Failed delete fileset for share %(sharename)s. '
389 'Error: %(excmsg)s.') %
390 {'sharename': sharename, 'excmsg': e})
391 LOG.error(msg)
392 raise exception.GPFSException(msg)
394 def _get_available_capacity(self, path):
395 """Calculate available space on path."""
396 try:
397 out, __ = self._gpfs_execute('df', '-P', '-B', '1', path)
398 except exception.ProcessExecutionError as e:
399 msg = (_('Failed to check available capacity for %(path)s.'
400 'Error: %(excmsg)s.') %
401 {'path': path, 'excmsg': e})
402 LOG.error(msg)
403 raise exception.GPFSException(msg)
405 out = out.splitlines()[1]
406 size = int(out.split()[1])
407 available = int(out.split()[3])
408 return available, size
410 def _create_share_snapshot(self, snapshot):
411 """Create a snapshot of the share."""
412 sharename = snapshot['share_name']
413 snapshotname = snapshot['name']
414 fsdev = self._get_gpfs_device()
415 LOG.debug(
416 'Attempting to create a snapshot %(snap)s from share %(share)s '
417 'on device %(dev)s.',
418 {'share': sharename, 'snap': snapshotname, 'dev': fsdev}
419 )
421 try:
422 self._gpfs_execute(self.GPFS_PATH + 'mmcrsnapshot', fsdev,
423 snapshot['name'], '-j', sharename)
424 except exception.ProcessExecutionError as e:
425 msg = (_('Failed to create snapshot %(snapshot)s. '
426 'Error: %(excmsg)s.') %
427 {'snapshot': snapshot['name'], 'excmsg': e})
428 LOG.error(msg)
429 raise exception.GPFSException(msg)
431 def _delete_share_snapshot(self, snapshot):
432 """Delete a snapshot of the share."""
433 sharename = snapshot['share_name']
434 fsdev = self._get_gpfs_device()
436 try:
437 self._gpfs_execute(self.GPFS_PATH + 'mmdelsnapshot', fsdev,
438 snapshot['name'], '-j', sharename)
439 except exception.ProcessExecutionError as e:
440 msg = (_('Failed to delete snapshot %(snapshot)s. '
441 'Error: %(excmsg)s.') %
442 {'snapshot': snapshot['name'], 'excmsg': e})
443 LOG.error(msg)
444 raise exception.GPFSException(msg)
446 def _create_share_from_snapshot(self, share, snapshot, share_path):
447 """Create share from a share snapshot."""
448 self._create_share(share)
449 snapshot_path = self._get_snapshot_path(snapshot)
450 snapshot_path = snapshot_path + "/"
451 try:
452 self._gpfs_execute('rsync', '-rp', snapshot_path, share_path)
453 except exception.ProcessExecutionError as e:
454 msg = (_('Failed to create share %(share)s from '
455 'snapshot %(snapshot)s. Error: %(excmsg)s.') %
456 {'share': share['name'], 'snapshot': snapshot['name'],
457 'excmsg': e})
458 LOG.error(msg)
459 raise exception.GPFSException(msg)
461 def _extend_share(self, shareobj, new_size):
462 sharename = shareobj['name']
463 sizestr = '%sG' % new_size
464 fsdev = self._get_gpfs_device()
465 try:
466 self._gpfs_execute(self.GPFS_PATH + 'mmsetquota', fsdev + ':' +
467 sharename, '--block', '0:' + sizestr)
468 except exception.ProcessExecutionError as e:
469 msg = (_('Failed to set quota for the share %(sharename)s. '
470 'Error: %(excmsg)s.') %
471 {'sharename': sharename, 'excmsg': e})
472 LOG.error(msg)
473 raise exception.GPFSException(msg)
475 def get_network_allocations_number(self):
476 return 0
478 def create_share(self, ctx, share, share_server=None):
479 """Create GPFS directory that will be represented as share."""
480 self._create_share(share)
481 share_path = self._get_share_path(share)
482 location = self._get_helper(share).create_export(share_path)
483 return location
485 def create_share_from_snapshot(self, ctx, share, snapshot,
486 share_server=None, parent_share=None):
487 """Is called to create share from a snapshot."""
488 share_path = self._get_share_path(share)
489 self._create_share_from_snapshot(share, snapshot, share_path)
490 location = self._get_helper(share).create_export(share_path)
491 return location
493 def create_snapshot(self, context, snapshot, share_server=None):
494 """Creates a snapshot."""
495 self._create_share_snapshot(snapshot)
497 def delete_share(self, ctx, share, share_server=None):
498 """Remove and cleanup share storage."""
499 location = self._get_share_path(share)
500 self._get_helper(share).remove_export(location, share)
501 self._delete_share(share)
503 def delete_snapshot(self, context, snapshot, share_server=None):
504 """Deletes a snapshot."""
505 self._delete_share_snapshot(snapshot)
507 def extend_share(self, share, new_size, share_server=None):
508 """Extends the quota on the share fileset."""
509 self._extend_share(share, new_size)
511 def ensure_share(self, ctx, share, share_server=None):
512 """Ensure that storage are mounted and exported."""
514 def update_access(self, context, share, access_rules, add_rules,
515 delete_rules, update_rules, share_server=None):
516 """Update access rules for given share."""
517 helper = self._get_helper(share)
518 location = self._get_share_path(share)
520 for access in delete_rules:
521 helper.deny_access(location, share, access)
523 for access in add_rules:
524 helper.allow_access(location, share, access)
526 if not (add_rules or delete_rules):
527 helper.resync_access(location, share, access_rules)
529 def check_for_setup_error(self):
530 """Returns an error if prerequisites aren't met."""
531 if not self._check_gpfs_state():
532 msg = (_('GPFS is not active.'))
533 LOG.error(msg)
534 raise exception.GPFSException(msg)
536 if not self.configuration.gpfs_share_export_ip:
537 msg = (_('gpfs_share_export_ip must be specified.'))
538 LOG.error(msg)
539 raise exception.InvalidParameterValue(err=msg)
541 gpfs_base_dir = self.configuration.gpfs_mount_point_base
542 if not gpfs_base_dir.startswith('/'):
543 msg = (_('%s must be an absolute path.') % gpfs_base_dir)
544 LOG.error(msg)
545 raise exception.GPFSException(msg)
547 if not self._is_dir(gpfs_base_dir):
548 msg = (_('%s is not a directory.') % gpfs_base_dir)
549 LOG.error(msg)
550 raise exception.GPFSException(msg)
552 if not self._is_gpfs_path(gpfs_base_dir):
553 msg = (_('%s is not on GPFS. Perhaps GPFS not mounted.')
554 % gpfs_base_dir)
555 LOG.error(msg)
556 raise exception.GPFSException(msg)
558 if self.configuration.gpfs_nfs_server_type not in ("KNFS", "CES"):
559 msg = (_('Invalid gpfs_nfs_server_type value: %s. '
560 'Valid values are: "KNFS", "CES".')
561 % self.configuration.gpfs_nfs_server_type)
562 LOG.error(msg)
563 raise exception.InvalidParameterValue(err=msg)
565 if ((not self.configuration.gpfs_nfs_server_list) and 565 ↛ exitline 565 didn't return from function 'check_for_setup_error' because the condition on line 565 was always true
566 (self.configuration.gpfs_nfs_server_type != 'CES')):
567 msg = (_('Missing value for gpfs_nfs_server_list.'))
568 LOG.error(msg)
569 raise exception.InvalidParameterValue(err=msg)
571 def _is_share_valid(self, fsdev, location):
572 try:
573 out, __ = self._gpfs_execute(self.GPFS_PATH + 'mmlsfileset', fsdev,
574 '-J', location, '-L', '-Y')
575 except exception.ProcessExecutionError:
576 msg = (_('Given share path %(share_path)s does not exist at '
577 'mount point %(mount_point)s.')
578 % {'share_path': location, 'mount_point': fsdev})
579 LOG.exception(msg)
580 raise exception.ManageInvalidShare(reason=msg)
582 lines = out.splitlines()
583 try:
584 validation_token = lines[0].split(':').index('allocInodes')
585 alloc_inodes = lines[1].split(':')[validation_token]
586 except (IndexError, ValueError):
587 msg = (_('Failed to check share at %s.') % location)
588 LOG.exception(msg)
589 raise exception.GPFSException(msg)
591 return alloc_inodes != '0'
593 def _get_share_name(self, fsdev, location):
594 try:
595 out, __ = self._gpfs_execute(self.GPFS_PATH + 'mmlsfileset', fsdev,
596 '-J', location, '-L', '-Y')
597 except exception.ProcessExecutionError:
598 msg = (_('Given share path %(share_path)s does not exist at '
599 'mount point %(mount_point)s.')
600 % {'share_path': location, 'mount_point': fsdev})
601 LOG.exception(msg)
602 raise exception.ManageInvalidShare(reason=msg)
604 lines = out.splitlines()
605 try:
606 validation_token = lines[0].split(':').index('filesetName')
607 share_name = lines[1].split(':')[validation_token]
608 except (IndexError, ValueError):
609 msg = (_('Failed to check share at %s.') % location)
610 LOG.exception(msg)
611 raise exception.GPFSException(msg)
613 return share_name
615 def _manage_existing(self, fsdev, share, old_share_name):
616 new_share_name = share['name']
617 new_export_location = self._local_path(new_share_name)
618 try:
619 self._gpfs_execute(self.GPFS_PATH + 'mmunlinkfileset', fsdev,
620 old_share_name, '-f')
621 except exception.ProcessExecutionError:
622 msg = _('Failed to unlink fileset for share %s.') % new_share_name
623 LOG.exception(msg)
624 raise exception.GPFSException(msg)
625 LOG.debug('Unlinked the fileset of share %s.', old_share_name)
627 try:
628 self._gpfs_execute(self.GPFS_PATH + 'mmchfileset', fsdev,
629 old_share_name, '-j', new_share_name)
630 except exception.ProcessExecutionError:
631 msg = _('Failed to rename fileset for share %s.') % new_share_name
632 LOG.exception(msg)
633 raise exception.GPFSException(msg)
634 LOG.debug('Renamed the fileset from %(old_share)s to %(new_share)s.',
635 {'old_share': old_share_name, 'new_share': new_share_name})
637 try:
638 self._gpfs_execute(self.GPFS_PATH + 'mmlinkfileset', fsdev,
639 new_share_name, '-J', new_export_location)
640 except exception.ProcessExecutionError:
641 msg = _('Failed to link fileset for the share %s.'
642 ) % new_share_name
643 LOG.exception(msg)
644 raise exception.GPFSException(msg)
645 LOG.debug('Linked the fileset of share %(share_name)s at location '
646 '%(export_location)s.',
647 {'share_name': new_share_name,
648 'export_location': new_export_location})
650 try:
651 self._gpfs_execute('chmod', '777', new_export_location)
652 except exception.ProcessExecutionError:
653 msg = _('Failed to set permissions for share %s.') % new_share_name
654 LOG.exception(msg)
655 raise exception.GPFSException(msg)
656 LOG.debug('Changed the permission of share %s.', new_share_name)
658 try:
659 out, __ = self._gpfs_execute(self.GPFS_PATH + 'mmlsquota', '-j',
660 new_share_name, '-Y', fsdev)
661 except exception.ProcessExecutionError:
662 msg = _('Failed to check size for share %s.') % new_share_name
663 LOG.exception(msg)
664 raise exception.GPFSException(msg)
666 lines = out.splitlines()
667 try:
668 quota_limit = lines[0].split(':').index('blockLimit')
669 quota_status = lines[1].split(':')[quota_limit]
670 except (IndexError, ValueError):
671 msg = _('Failed to check quota for share %s.') % new_share_name
672 LOG.exception(msg)
673 raise exception.GPFSException(msg)
675 share_size = int(quota_status)
676 # Note: since share_size returns integer value in KB,
677 # we are checking whether share is less than 1GiB.
678 # (units.Mi * KB = 1GB)
679 if share_size < units.Mi:
680 try:
681 self._gpfs_execute(self.GPFS_PATH + 'mmsetquota', fsdev + ':' +
682 new_share_name, '--block', '0:1G')
683 except exception.ProcessExecutionError:
684 msg = _('Failed to set quota for share %s.') % new_share_name
685 LOG.exception(msg)
686 raise exception.GPFSException(msg)
687 LOG.info('Existing share %(shr)s has size %(size)s KB '
688 'which is below 1GiB, so extended it to 1GiB.',
689 {'shr': new_share_name, 'size': share_size})
690 share_size = 1
691 else:
692 orig_share_size = share_size
693 share_size = int(math.ceil(float(share_size) / units.Mi))
694 if orig_share_size != share_size * units.Mi:
695 try:
696 self._gpfs_execute(self.GPFS_PATH + 'mmsetquota', fsdev +
697 ':' + new_share_name, '--block', '0:' +
698 str(share_size) + 'G')
699 except exception.ProcessExecutionError:
700 msg = _('Failed to set quota for share %s.'
701 ) % new_share_name
702 LOG.exception(msg)
703 raise exception.GPFSException(msg)
705 new_export_location = self._get_helper(share).create_export(
706 new_export_location)
707 return share_size, new_export_location
709 def manage_existing(self, share, driver_options):
711 old_export = share['export_location'].split(':')
712 try:
713 ces_ip = old_export[0]
714 old_export_location = old_export[1]
715 except IndexError:
716 msg = _('Incorrect export path. Expected format: '
717 'IP:/gpfs_mount_point_base/share_id.')
718 LOG.exception(msg)
719 raise exception.ShareBackendException(msg=msg)
721 if ces_ip not in self.configuration.gpfs_nfs_server_list:
722 msg = _('The CES IP %s is not present in the '
723 'configuration option "gpfs_nfs_server_list".') % ces_ip
724 raise exception.ShareBackendException(msg=msg)
726 fsdev = self._get_gpfs_device()
727 if not self._is_share_valid(fsdev, old_export_location):
728 err_msg = _('Given share path %s does not have a valid '
729 'share.') % old_export_location
730 raise exception.ManageInvalidShare(reason=err_msg)
732 share_name = self._get_share_name(fsdev, old_export_location)
734 out = self._get_helper(share)._has_client_access(old_export_location)
735 if out:
736 err_msg = _('Clients have access to %s share currently. Evict any '
737 'clients before trying again.') % share_name
738 raise exception.ManageInvalidShare(reason=err_msg)
740 share_size, new_export_location = self._manage_existing(
741 fsdev, share, share_name)
742 return {"size": share_size, "export_locations": new_export_location}
744 def _update_share_stats(self):
745 """Retrieve stats info from share volume group."""
747 data = dict(
748 share_backend_name=self.backend_name,
749 vendor_name='IBM',
750 storage_protocol='NFS',
751 reserved_percentage=self.configuration.reserved_share_percentage,
752 reserved_snapshot_percentage=(
753 self.configuration.reserved_share_from_snapshot_percentage
754 or self.configuration.reserved_share_percentage),
755 reserved_share_extend_percentage=(
756 self.configuration.reserved_share_extend_percentage
757 or self.configuration.reserved_share_percentage))
759 free, capacity = self._get_available_capacity(
760 self.configuration.gpfs_mount_point_base)
762 data['total_capacity_gb'] = math.ceil(capacity / units.Gi)
763 data['free_capacity_gb'] = math.ceil(free / units.Gi)
765 super(GPFSShareDriver, self)._update_share_stats(data)
767 def _get_helper(self, share):
768 if share['share_proto'] == 'NFS':
769 return self._helpers[self.configuration.gpfs_nfs_server_type]
770 else:
771 msg = (_('Share protocol %s not supported by GPFS driver.')
772 % share['share_proto'])
773 LOG.error(msg)
774 raise exception.InvalidShare(reason=msg)
776 def _get_share_path(self, share):
777 """Returns share path on storage provider."""
778 return os.path.join(self.configuration.gpfs_mount_point_base,
779 share['name'])
781 def _get_snapshot_path(self, snapshot):
782 """Returns share path on storage provider."""
783 snapshot_dir = ".snapshots"
784 return os.path.join(self.configuration.gpfs_mount_point_base,
785 snapshot["share_name"], snapshot_dir,
786 snapshot["name"])
789class NASHelperBase(metaclass=abc.ABCMeta):
790 """Interface to work with share."""
792 def __init__(self, execute, config_object):
793 self.configuration = config_object
794 self._execute = execute
796 def create_export(self, local_path):
797 """Construct location of new export."""
798 return ':'.join([self.configuration.gpfs_share_export_ip, local_path])
800 def get_export_options(self, share, access, helper):
801 """Get the export options."""
802 extra_specs = share_types.get_extra_specs_from_share(share)
803 if helper == 'KNFS':
804 export_options = extra_specs.get('knfs:export_options')
805 elif helper == 'CES': 805 ↛ 808line 805 didn't jump to line 808 because the condition on line 805 was always true
806 export_options = extra_specs.get('ces:export_options')
807 else:
808 export_options = None
810 options = self._get_validated_opt_list(export_options)
811 options.append(self.get_access_option(access))
812 return ','.join(options)
814 def _validate_export_options(self, options):
815 """Validate the export options."""
816 options_not_allowed = self._get_options_not_allowed()
817 invalid_options = [
818 option for option in options if option in options_not_allowed
819 ]
821 if invalid_options:
822 raise exception.InvalidInput(reason='Invalid export_option %s as '
823 'it is set by access_type.'
824 % invalid_options)
826 def _get_validated_opt_list(self, export_options):
827 """Validate the export options and return an option list."""
828 if export_options:
829 options = export_options.lower().split(',')
830 self._validate_export_options(options)
831 else:
832 options = []
833 return options
835 @abc.abstractmethod
836 def get_access_option(self, access):
837 """Get access option string based on access level."""
839 @abc.abstractmethod
840 def _get_options_not_allowed(self):
841 """Get access options that are not allowed in extra-specs."""
843 @abc.abstractmethod
844 def remove_export(self, local_path, share):
845 """Remove export."""
847 @abc.abstractmethod
848 def allow_access(self, local_path, share, access):
849 """Allow access to the host."""
851 @abc.abstractmethod
852 def deny_access(self, local_path, share, access):
853 """Deny access to the host."""
855 @abc.abstractmethod
856 def resync_access(self, local_path, share, access_rules):
857 """Re-sync all access rules for given share."""
860class KNFSHelper(NASHelperBase):
861 """Wrapper for Kernel NFS Commands."""
863 def __init__(self, execute, config_object):
864 super(KNFSHelper, self).__init__(execute, config_object)
865 self._execute = execute
866 try:
867 self._execute('exportfs', check_exit_code=True, run_as_root=True)
868 except exception.ProcessExecutionError as e:
869 msg = (_('NFS server not found. Error: %s.') % e)
870 LOG.error(msg)
871 raise exception.GPFSException(msg)
873 def _has_client_access(self, local_path, access_to=None):
874 try:
875 out, __ = self._execute('exportfs', run_as_root=True)
876 except exception.ProcessExecutionError:
877 msg = _('Failed to check exports on the systems.')
878 LOG.exception(msg)
879 raise exception.GPFSException(msg)
881 if access_to:
882 if (re.search(re.escape(local_path) + r'[\s\n]*'
883 + re.escape(access_to), out)):
884 return True
885 else:
886 if re.findall(local_path + '\\b', ''.join(out)):
887 return True
888 return False
890 def _publish_access(self, *cmd, **kwargs):
891 check_exit_code = kwargs.get('check_exit_code', True)
893 outs = []
894 localserver_iplist = socket.gethostbyname_ex(socket.gethostname())[2]
895 for server in self.configuration.gpfs_nfs_server_list:
896 if server in localserver_iplist:
897 run_command = cmd
898 run_local = True
899 else:
900 sshlogin = self.configuration.gpfs_ssh_login
901 remote_login = sshlogin + '@' + server
902 run_command = ['ssh', remote_login] + list(cmd)
903 run_local = False
904 try:
905 out = utils.execute(*run_command,
906 run_as_root=run_local,
907 check_exit_code=check_exit_code)
908 except exception.ProcessExecutionError:
909 raise
910 outs.append(out)
911 return outs
913 def _verify_denied_access(self, local_path, share, ip):
914 try:
915 cmd = ['exportfs']
916 outs = self._publish_access(*cmd)
917 except exception.ProcessExecutionError:
918 msg = _('Failed to verify denied access for '
919 'share %s.') % share['name']
920 LOG.exception(msg)
921 raise exception.GPFSException(msg)
923 for stdout, stderr in outs:
924 if stderr and stderr.strip():
925 msg = ('Log/ignore stderr during _validate_denied_access for '
926 'share %(sharename)s. Return code OK. '
927 'Stderr: %(stderr)s' % {'sharename': share['name'],
928 'stderr': stderr})
929 LOG.debug(msg)
931 gpfs_ips = NFSHelper.get_host_list(stdout, local_path)
932 if ip in gpfs_ips:
933 msg = (_('Failed to deny access for share %(sharename)s. '
934 'IP %(ip)s still has access.') %
935 {'sharename': share['name'],
936 'ip': ip})
937 LOG.error(msg)
938 raise exception.GPFSException(msg)
940 def remove_export(self, local_path, share):
941 """Remove export."""
943 def get_access_option(self, access):
944 """Get access option string based on access level."""
945 return access['access_level']
947 def _get_options_not_allowed(self):
948 """Get access options that are not allowed in extra-specs."""
949 return list(constants.ACCESS_LEVELS)
951 def _get_exports(self):
952 """Get exportfs output."""
953 try:
954 out, __ = self._execute('exportfs', run_as_root=True)
955 except exception.ProcessExecutionError as e:
956 msg = (_('Failed to check exports on the systems. '
957 ' Error: %s.') % e)
958 LOG.error(msg)
959 raise exception.GPFSException(msg)
960 return out
962 def allow_access(self, local_path, share, access, error_on_exists=True):
963 """Allow access to one or more vm instances."""
965 if access['access_type'] != 'ip':
966 raise exception.InvalidShareAccess(reason='Only ip access type '
967 'supported.')
969 if error_on_exists:
970 # check if present in export
971 out = re.search(
972 re.escape(local_path) + r'[\s\n]*'
973 + re.escape(access['access_to']), self._get_exports())
975 if out is not None:
976 access_type = access['access_type']
977 access_to = access['access_to']
978 raise exception.ShareAccessExists(access_type=access_type,
979 access=access_to)
981 export_opts = self.get_export_options(share, access, 'KNFS')
982 cmd = ['exportfs', '-o', export_opts,
983 ':'.join([access['access_to'], local_path])]
984 try:
985 self._publish_access(*cmd)
986 except exception.ProcessExecutionError:
987 msg = _('Failed to allow access for share %s.') % share['name']
988 LOG.exception(msg)
989 raise exception.GPFSException(msg)
991 def _deny_ip(self, local_path, share, ip):
992 """Remove access for one or more vm instances."""
993 cmd = ['exportfs', '-u', ':'.join([ip, local_path])]
994 try:
995 # Can get exit code 0 for success or 1 for already gone (also
996 # potentially get 1 due to exportfs bug). So allow
997 # _publish_access to continue with [0, 1] and then verify after
998 # it is done.
999 self._publish_access(*cmd, check_exit_code=[0, 1])
1000 except exception.ProcessExecutionError:
1001 msg = _('Failed to deny access for share %s.') % share['name']
1002 LOG.exception(msg)
1003 raise exception.GPFSException(msg)
1005 # Error code (0 or 1) makes deny IP success indeterminate.
1006 # So, verify that the IP access was completely removed.
1007 self._verify_denied_access(local_path, share, ip)
1009 def deny_access(self, local_path, share, access):
1010 """Remove access for one or more vm instances."""
1011 self._deny_ip(local_path, share, access['access_to'])
1013 def _remove_other_access(self, local_path, share, access_rules):
1014 """Remove any client access that is not in access_rules."""
1015 exports = self._get_exports()
1016 gpfs_ips = set(NFSHelper.get_host_list(exports, local_path))
1017 manila_ips = set([x['access_to'] for x in access_rules])
1018 remove_ips = gpfs_ips - manila_ips
1019 for ip in remove_ips:
1020 self._deny_ip(local_path, share, ip)
1022 def resync_access(self, local_path, share, access_rules):
1023 """Re-sync all access rules for given share."""
1024 for access in access_rules:
1025 self.allow_access(local_path, share, access, error_on_exists=False)
1026 self._remove_other_access(local_path, share, access_rules)
1029class CESHelper(NASHelperBase):
1030 """Wrapper for NFS by Spectrum Scale CES"""
1032 def __init__(self, execute, config_object):
1033 super(CESHelper, self).__init__(execute, config_object)
1034 self._execute = execute
1035 if self.configuration.is_gpfs_node: 1035 ↛ 1036line 1035 didn't jump to line 1036 because the condition on line 1035 was never true
1036 self.GPFS_PATH = ''
1037 else:
1038 self.GPFS_PATH = '/usr/lpp/mmfs/bin/'
1040 def _execute_mmnfs_command(self, cmd, err_msg):
1041 try:
1042 out, __ = self._execute(self.GPFS_PATH + 'mmnfs', 'export', *cmd)
1043 except exception.ProcessExecutionError as e:
1044 msg = (_('%(err_msg)s Error: %(e)s.')
1045 % {'err_msg': err_msg, 'e': e})
1046 LOG.error(msg)
1047 raise exception.GPFSException(msg)
1048 return out
1050 @staticmethod
1051 def _fix_export_data(data, headers):
1052 """Export data split by ':' may need fixing if client had colons."""
1054 # If an IPv6 client shows up then ':' delimiters don't work.
1055 # So use header positions to get data before/after Clients.
1056 # Then what is left in between can be joined back into a client IP.
1057 client_index = headers.index('Clients')
1058 # reverse_client_index is distance from end.
1059 reverse_client_index = len(headers) - (client_index + 1)
1060 after_client_index = len(data) - reverse_client_index
1062 before_client = data[:client_index]
1063 client = data[client_index: after_client_index]
1064 after_client = data[after_client_index:]
1066 result_data = before_client
1067 result_data.append(':'.join(client)) # Fixes colons in client IP
1068 result_data.extend(after_client)
1069 return result_data
1071 def _get_nfs_client_exports(self, local_path):
1072 """Get the current NFS client export details from GPFS."""
1074 out = self._execute_mmnfs_command(
1075 ('list', '-n', local_path, '-Y'),
1076 'Failed to get exports from the system.')
1078 # Remove the header line and use the headers to describe the data
1079 lines = out.splitlines()
1080 for line in lines:
1081 data = line.split(':')
1082 if "HEADER" in data:
1083 headers = data
1084 lines.remove(line)
1085 break
1086 else:
1087 msg = _('Failed to parse exports for path %s. '
1088 'No HEADER found.') % local_path
1089 LOG.error(msg)
1090 raise exception.GPFSException(msg)
1092 exports = []
1093 for line in lines:
1094 data = line.split(':')
1095 if len(data) < 3:
1096 continue # Skip empty lines (and anything less than minimal).
1098 result_data = self._fix_export_data(data, headers)
1099 exports.append(dict(zip(headers, result_data)))
1101 return exports
1103 def _has_client_access(self, local_path, access_to=None):
1104 """Check path for any export or for one with a specific IP address."""
1105 gpfs_clients = self._get_nfs_client_exports(local_path)
1106 return gpfs_clients and (access_to is None or access_to in [
1107 x['Clients'] for x in gpfs_clients])
1109 def remove_export(self, local_path, share):
1110 """Remove export."""
1111 if self._has_client_access(local_path):
1112 err_msg = ('Failed to remove export for share %s.'
1113 % share['name'])
1114 self._execute_mmnfs_command(('remove', local_path), err_msg)
1116 def _get_options_not_allowed(self):
1117 """Get access options that are not allowed in extra-specs."""
1118 return ['access_type=ro', 'access_type=rw']
1120 def get_access_option(self, access):
1121 """Get access option string based on access level."""
1122 if access['access_level'] == constants.ACCESS_LEVEL_RO:
1123 return 'access_type=ro'
1124 else:
1125 return 'access_type=rw'
1127 def allow_access(self, local_path, share, access):
1128 """Allow access to the host."""
1130 if access['access_type'] != 'ip':
1131 raise exception.InvalidShareAccess(reason='Only ip access type '
1132 'supported.')
1133 has_exports = self._has_client_access(local_path)
1135 export_opts = self.get_export_options(share, access, 'CES')
1137 if not has_exports:
1138 cmd = ['add', local_path, '-c',
1139 access['access_to'] +
1140 '(' + export_opts + ')']
1141 else:
1142 cmd = ['change', local_path, '--nfsadd',
1143 access['access_to'] +
1144 '(' + export_opts + ')']
1146 err_msg = ('Failed to allow access for share %s.'
1147 % share['name'])
1148 self._execute_mmnfs_command(cmd, err_msg)
1150 def deny_access(self, local_path, share, access, force=False):
1151 """Deny access to the host."""
1152 has_export = self._has_client_access(local_path, access['access_to'])
1154 if has_export: 1154 ↛ exitline 1154 didn't return from function 'deny_access' because the condition on line 1154 was always true
1155 err_msg = ('Failed to remove access for share %s.'
1156 % share['name'])
1157 self._execute_mmnfs_command(('change', local_path,
1158 '--nfsremove', access['access_to']),
1159 err_msg)
1161 def _get_client_opts(self, access, opts_list):
1162 """Get client options string for access rule and NFS options."""
1163 nfs_opts = ','.join([self.get_access_option(access)] + opts_list)
1165 return '%(ip)s(%(nfs_opts)s)' % {'ip': access['access_to'],
1166 'nfs_opts': nfs_opts}
1168 def _get_share_opts(self, share):
1169 """Get a list of NFS options from the share's share type."""
1170 extra_specs = share_types.get_extra_specs_from_share(share)
1171 opts_list = self._get_validated_opt_list(
1172 extra_specs.get('ces:export_options'))
1173 return opts_list
1175 def _nfs_change(self, local_path, share, access_rules, gpfs_clients):
1176 """Bulk add/update/remove of access rules for share."""
1177 opts_list = self._get_share_opts(share)
1179 # Create a map of existing client access rules from GPFS.
1180 # Key from 'Clients' is an IP address or
1181 # Value from 'Access_Type' is RW|RO (case varies)
1182 gpfs_map = {
1183 x['Clients']: x['Access_Type'].lower() for x in gpfs_clients}
1184 gpfs_ips = set(gpfs_map.keys())
1186 manila_ips = set([x['access_to'] for x in access_rules])
1187 add_ips = manila_ips - gpfs_ips
1188 update_ips = gpfs_ips.intersection(manila_ips)
1189 remove_ips = gpfs_ips - manila_ips
1191 adds = []
1192 updates = []
1193 if add_ips or update_ips: 1193 ↛ 1202line 1193 didn't jump to line 1202 because the condition on line 1193 was always true
1194 for access in access_rules:
1195 ip = access['access_to']
1196 if ip in add_ips:
1197 adds.append(self._get_client_opts(access, opts_list))
1198 elif (ip in update_ips 1198 ↛ 1194line 1198 didn't jump to line 1194 because the condition on line 1198 was always true
1199 and access['access_level'] != gpfs_map[ip]):
1200 updates.append(self._get_client_opts(access, opts_list))
1202 if remove_ips or adds or updates: 1202 ↛ exitline 1202 didn't return from function '_nfs_change' because the condition on line 1202 was always true
1203 cmd = ['change', local_path]
1204 if remove_ips: 1204 ↛ 1207line 1204 didn't jump to line 1207 because the condition on line 1204 was always true
1205 cmd.append('--nfsremove')
1206 cmd.append(','.join(remove_ips))
1207 if adds: 1207 ↛ 1210line 1207 didn't jump to line 1210 because the condition on line 1207 was always true
1208 cmd.append('--nfsadd')
1209 cmd.append(';'.join(adds))
1210 if updates: 1210 ↛ 1213line 1210 didn't jump to line 1213 because the condition on line 1210 was always true
1211 cmd.append('--nfschange')
1212 cmd.append(';'.join(updates))
1213 err_msg = ('Failed to resync access for share %s.' % share['name'])
1214 self._execute_mmnfs_command(cmd, err_msg)
1216 def _nfs_add(self, access_rules, local_path, share):
1217 """Bulk add of access rules to share."""
1218 if not access_rules:
1219 return
1221 opts_list = self._get_share_opts(share)
1222 client_options = []
1223 for access in access_rules:
1224 client_options.append(self._get_client_opts(access, opts_list))
1226 cmd = ['add', local_path, '-c', ';'.join(client_options)]
1227 err_msg = ('Failed to resync access for share %s.' % share['name'])
1228 self._execute_mmnfs_command(cmd, err_msg)
1230 def resync_access(self, local_path, share, access_rules):
1231 """Re-sync all access rules for given share."""
1232 gpfs_clients = self._get_nfs_client_exports(local_path)
1233 if not gpfs_clients:
1234 self._nfs_add(access_rules, local_path, share)
1235 else:
1236 self._nfs_change(local_path, share, access_rules, gpfs_clients)