deb-sahara/sahara/service/shares.py
Trevor McKay 7e10f34cad Add manila nfs data sources
This change will allow data sources with urls of the form
"manila://share-id/path", similar to manila urls for job binaries.
The Sahara native url will be logged in the JobExecution, but
the true runtime url (file:///mnt/path) for manila shares will
be used in the cluster.

Partial-implements: blueprint manila-as-a-data-source
Change-Id: I0b43491decbe6cb0ec0b84314cf9b407b9e3fb4a
2015-08-18 17:12:05 -04:00

272 lines
10 KiB
Python

# 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()
_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
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")
@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)
_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