# Copyright (c) 2015 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import collections import itertools from oslo_log import log import six from sahara import context from sahara.i18n import _LW from sahara.utils.openstack import manila LOG = log.getLogger(__name__) def mount_shares(cluster): """Mounts all shares specified for the cluster and any of its node groups. - In the event that a specific share is configured for both the cluster and a specific node group, configuration at the node group level will be ignored. - In the event that utilities required to mount the share are not already installed on the node, this method may fail if the node cannot access the internet. - This method will not remove already-mounted shares. - This method will not remove or remount (or currently, reconfigure) shares already mounted to the desired local mount point. :param cluster: The cluster model. """ node_groups = (ng for ng in cluster.node_groups if ng.shares) ng_mounts = [_mount(ng, share_config) for ng in node_groups for share_config in ng.shares] c_mounts = [_mount(ng, share_config) for ng in cluster.node_groups for share_config in cluster.shares or []] if not (ng_mounts or c_mounts): return ng_mounts_by_share_id = _group_mounts_by_share_id(ng_mounts) c_mounts_by_share_id = _group_mounts_by_share_id(c_mounts) all_share_ids = (set(ng_mounts_by_share_id.keys()) | set(c_mounts_by_share_id.keys())) mounts_by_share_id = { share_id: c_mounts_by_share_id.get(share_id) or ng_mounts_by_share_id[share_id] for share_id in all_share_ids} all_mounts = itertools.chain(*mounts_by_share_id.values()) mounts_by_ng_id = _group_mounts_by_ng_id(all_mounts) client = manila.client() handlers_by_share_id = {id: _ShareHandler.create_from_id(id, client) for id in all_share_ids} for mounts in mounts_by_ng_id.values(): node_group_shares = _NodeGroupShares(mounts[0].node_group) for mount in mounts: share_id = mount.share_config['id'] node_group_shares.add_share(mount.share_config, handlers_by_share_id[share_id]) node_group_shares.mount_shares_to_node_group() def unmount_shares(cluster, unmount_share_list): """Unmounts all shares in unmount_share_list on the given cluster :param cluster: The cluster model. :param unmount_share_list: list of shares to unmount """ client = manila.client() unmount_share_ids = (set(s['id'] for s in unmount_share_list)) handlers_by_share_id = {id: _ShareHandler.create_from_id(id, client) for id in unmount_share_ids} for share in unmount_share_list: for ng in cluster.node_groups: for instance in ng.instances: handlers_by_share_id[share['id']].unmount_from_instance( instance.remote(), share) _mount = collections.namedtuple('Mount', ['node_group', 'share_config']) def _group_mounts(mounts, grouper): result = collections.defaultdict(list) for mount in mounts: result[grouper(mount)].append(mount) return result def _group_mounts_by_share_id(mounts): return _group_mounts(mounts, lambda mount: mount.share_config['id']) def _group_mounts_by_ng_id(mounts): return _group_mounts(mounts, lambda mount: mount.node_group['id']) class _NodeGroupShares(object): """Organizes share mounting for a single node group.""" _share = collections.namedtuple('Share', ['share_config', 'handler']) def __init__(self, node_group): self.node_group = node_group self.shares = [] def add_share(self, share_config, handler): """Adds a share to mount; add all shares before mounting.""" self.shares.append(self._share(share_config, handler)) def mount_shares_to_node_group(self): """Mounts all configured shares to the node group.""" for instance in self.node_group.instances: with context.set_current_instance_id(instance.instance_id): self._mount_shares_to_instance(instance) def _mount_shares_to_instance(self, instance): # Note: Additional iteration here is critical: based on # experimentation, failure to execute allow_access before spawning # the remote results in permission failure. for share in self.shares: share.handler.allow_access_to_instance(instance, share.share_config) with instance.remote() as remote: share_types = set(type(share.handler) for share in self.shares) for share_type in share_types: share_type.setup_instance(remote) for share in self.shares: share.handler.mount_to_instance(remote, share.share_config) @six.add_metaclass(abc.ABCMeta) class _ShareHandler(object): """Handles mounting of a single share to any number of instances.""" @classmethod def setup_instance(cls, remote): """Prepares an instance to mount this type of share.""" pass @classmethod def create_from_id(cls, share_id, client): """Factory method for creation from a share_id of unknown type.""" share = manila.get_share(client, share_id, raise_on_error=True) mounter_class = _share_types[share.share_proto] return mounter_class(share, client) def __init__(self, share, client): self.share = share self.client = client def allow_access_to_instance(self, instance, share_config): """Mounts a specific share to a specific instance.""" access_level = self._get_access_level(share_config) ip_filter = lambda x: (x.access_type == 'ip' and x.access_to == instance.internal_ip) accesses = list(filter(ip_filter, self.share.access_list())) if accesses: access = accesses[0] if access.access_level not in ('ro', 'rw'): LOG.warning( _LW("Unknown permission level %(access_level)s on share " "id %(share_id)s for ip %(ip)s. Leaving pre-existing " "permissions.").format( access_level=access.access_level, share_id=self.share.id, ip=instance.internal_ip)) elif access.access_level == 'ro' and access_level == 'rw': self.share.deny(access.id) self.share.allow('ip', instance.internal_ip, access_level) else: self.share.allow('ip', instance.internal_ip, access_level) @abc.abstractmethod def mount_to_instance(self, remote, share_info): """Mounts the share to the instance as configured.""" pass @abc.abstractmethod def unmount_from_instance(self, remote, share_info): """Unmounts the share from the instance.""" pass def _get_access_level(self, share_config): return share_config.get('access_level', 'rw') def _default_mount(self): return '/mnt/{0}'.format(self.share.id) def _get_path(self, share_info): return share_info.get('path', self._default_mount()) class _NFSMounter(_ShareHandler): """Handles mounting of a single NFS share to any number of instances.""" _DEBIAN_INSTALL = "dpkg -s nfs-common || apt-get -y install nfs-common" _REDHAT_INSTALL = "rpm -q nfs-utils || yum install -y nfs-utils" _NFS_CHECKS = { "centos": _REDHAT_INSTALL, "fedora": _REDHAT_INSTALL, "redhatenterpriseserver": _REDHAT_INSTALL, "ubuntu": _DEBIAN_INSTALL } _MKDIR_COMMAND = 'mkdir -p %s' _MOUNT_COMMAND = ("mount | grep '%(remote)s' | grep '%(local)s' | " "grep nfs || mount -t nfs %(access_arg)s %(remote)s " "%(local)s") _UNMOUNT_COMMAND = ("umount -f %s ") _RMDIR_COMMAND = 'rmdir %s' @classmethod def setup_instance(cls, remote): """Prepares an instance to mount this type of share.""" response = remote.execute_command('lsb_release -is') distro = response[1].strip().lower() if distro in cls._NFS_CHECKS: command = cls._NFS_CHECKS[distro] remote.execute_command(command, run_as_root=True) else: LOG.warning( _LW("Cannot verify installation of NFS mount tools for " "unknown distro {distro}.").format(distro=distro)) def mount_to_instance(self, remote, share_info): """Mounts the share to the instance as configured.""" local_path = self._get_path(share_info) access_level = self._get_access_level(share_info) access_arg = '-w' if access_level == 'rw' else '-r' remote.execute_command(self._MKDIR_COMMAND % local_path, run_as_root=True) mount_command = self._MOUNT_COMMAND % { "remote": self.share.export_location, "local": local_path, "access_arg": access_arg} remote.execute_command(mount_command, run_as_root=True) def unmount_from_instance(self, remote, share_info): """Unounts the share from the instance.""" local_path = self._get_path(share_info) unmount_command = self._UNMOUNT_COMMAND % local_path rmdir_command = self._RMDIR_COMMAND % local_path remote.execute_command(unmount_command, run_as_root=True) remote.execute_command(rmdir_command, run_as_root=True) _share_types = {"NFS": _NFSMounter} SUPPORTED_SHARE_TYPES = _share_types.keys() def _make_share_path(mount_point, path): return "{0}{1}".format(mount_point, path) def default_mount(share_id): client = manila.client() return _ShareHandler.create_from_id(share_id, client)._default_mount() def get_share_path(url, shares): # url example: 'manila://ManilaShare-uuid/path_to_file' url = six.moves.urllib.parse.urlparse(url) # using list() as a python2/3 workaround share_list = list(filter(lambda s: s['id'] == url.netloc, shares)) if not share_list: # Share id is not in the share list, let the caller # determine a default path if possible path = None else: # We will always select the first one. Let the # caller determine whether duplicates are okay mount_point = share_list[0].get('path', None) # Do this in two steps instead of passing the default # expression to get(), because it's a big side effect if mount_point is None: # The situation here is that the user specified a # share without a path, so the default mnt was used # during cluster provisioning. mount_point = default_mount(share_list[0]['id']) path = _make_share_path(mount_point, url.path) return path