diff --git a/manila/exception.py b/manila/exception.py index 08eaf43b58..355c6d22cb 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -415,10 +415,6 @@ class FailedCmdWithDump(ManilaException): message = _("Operation failed with status=%(status)s. Full dump: %(data)s") -class InstanceNotFound(NotFound): - message = _("Instance %(instance_id)s could not be found.") - - class ShareBackendAPIException(ManilaException): message = _("Bad or unexpected response from the storage share " "backend API: %(data)s") diff --git a/manila/network/neutron/api.py b/manila/network/neutron/api.py index 5fa824f21e..bd2c89cd46 100644 --- a/manila/network/neutron/api.py +++ b/manila/network/neutron/api.py @@ -90,7 +90,11 @@ class API(base.Base): @property def admin_tenant_id(self): if self.client.httpclient.auth_token is None: - self.client.httpclient.authenticate() + try: + self.client.httpclient.authenticate() + except neutron_client_exc.NeutronClientException as e: + raise exception.NetworkException(code=e.status_code, + message=e.message) return self.client.httpclient.auth_tenant_id def get_all_tenant_networks(self, tenant_id): diff --git a/manila/share/drivers/generic.py b/manila/share/drivers/generic.py new file mode 100644 index 0000000000..6c6705dd25 --- /dev/null +++ b/manila/share/drivers/generic.py @@ -0,0 +1,1133 @@ +# Copyright 2014 Mirantis Inc. +# All Rights Reserved. +# +# 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. +""" +Generic Driver for shares. + +""" + +import ConfigParser +import netaddr +import os +import re +import shutil +import socket +import threading +import time + +from manila import compute +from manila import context +from manila import exception +from manila.network.linux import ip_lib +from manila.network.neutron import api as neutron +from manila.openstack.common import importutils +from manila.openstack.common import log as logging +from manila.share import driver +from manila import utils +from manila import volume + +from oslo.config import cfg + +LOG = logging.getLogger(__name__) + +share_opts = [ + cfg.StrOpt('service_image_name', + default='manila-service-image', + help="Name of image in glance, that will be used to create " + "service instance"), + cfg.StrOpt('smb_template_config_path', + default='$state_path/smb.conf', + help="Path to smb config"), + cfg.StrOpt('service_instance_name_template', + default='manila_service_instance-%s', + help="Name of service instance"), + cfg.StrOpt('service_instance_user', + help="User in service instance"), + cfg.StrOpt('service_instance_password', + default=None, + help="Password to service instance user"), + cfg.StrOpt('volume_name_template', + default='manila-share-%s', + help="Volume name template"), + cfg.StrOpt('manila_service_keypair_name', + default='manila-service', + help="Name of keypair that will be created and used " + "for service instance"), + cfg.StrOpt('path_to_public_key', + default='/home/stack/.ssh/id_rsa.pub', + help="Path to hosts public key"), + cfg.StrOpt('path_to_private_key', + default='/home/stack/.ssh/id_rsa', + help="Path to hosts private key"), + cfg.StrOpt('volume_snapshot_name_template', + default='manila-snapshot-%s', + help="Volume snapshot name template"), + cfg.IntOpt('max_time_to_build_instance', + default=300, + help="Maximum time to wait for creating service instance"), + cfg.StrOpt('share_mount_path', + default='/shares', + help="Parent path in service instance where shares " + "will be mounted"), + cfg.IntOpt('max_time_to_create_volume', + default=180, + help="Maximum time to wait for creating cinder volume"), + cfg.IntOpt('max_time_to_attach', + default=120, + help="Maximum time to wait for attaching cinder volume"), + cfg.IntOpt('service_instance_flavor_id', + default=100, + help="ID of flavor, that will be used for service instance " + "creation"), + cfg.StrOpt('service_instance_smb_config_path', + default='$share_mount_path/smb.conf', + help="Path to smb config in service instance"), + cfg.StrOpt('service_network_name', + default='manila_service_network', + help="Name of manila service network"), + cfg.StrOpt('service_network_cidr', + default='10.254.0.0/16', + help="CIDR of manila service network"), + cfg.StrOpt('interface_driver', + default='manila.network.linux.interface.OVSInterfaceDriver', + help="Vif driver"), + cfg.ListOpt('share_helpers', + default=[ + 'CIFS=manila.share.drivers.generic.CIFSHelper', + 'NFS=manila.share.drivers.generic.NFSHelper', + ], + help='Specify list of share export helpers.'), +] + +CONF = cfg.CONF +CONF.register_opts(share_opts) + + +def synchronized(f): + """Decorates function with unique locks for each share network.""" + def wrapped_func(self, *args, **kwargs): + for arg in args: + share_network_id = getattr(arg, 'share_network_id', None) + if isinstance(arg, dict): + share_network_id = arg.get('share_network_id', None) + if share_network_id: + break + else: + raise exception.\ + ManilaException(_('Could not get share network id')) + with self.share_networks_locks.setdefault(share_network_id, + threading.RLock()): + return f(self, *args, **kwargs) + return wrapped_func + + +def _ssh_exec(server, command): + """Executes ssh commands and checks/restores ssh connection.""" + if not server['ssh'].get_transport().is_active(): + server['ssh_pool'].remove(server['ssh']) + server['ssh'] = server['ssh_pool'].create() + return utils.ssh_execute(server['ssh'], ' '.join(command)) + + +class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver): + """Executes commands relating to Shares.""" + + def __init__(self, db, *args, **kwargs): + """Do initialization.""" + super(GenericShareDriver, self).__init__(*args, **kwargs) + self.admin_context = context.get_admin_context() + self.db = db + self.configuration.append_config_values(share_opts) + self._helpers = None + + def check_for_setup_error(self): + """Returns an error if prerequisites aren't met.""" + if not self.configuration.service_instance_user: + raise exception.ManilaException(_('Service instance user is not ' + 'specified')) + + def do_setup(self, context): + """Any initialization the generic driver does while starting.""" + super(GenericShareDriver, self).do_setup(context) + self.compute_api = compute.API() + self.volume_api = volume.API() + self.neutron_api = neutron.API() + self.share_networks_locks = {} + self.share_networks_servers = {} + attempts = 5 + while attempts: + try: + self.service_tenant_id = self.neutron_api.admin_tenant_id + break + except exception.NetworkException: + LOG.debug(_('Connection to neutron failed')) + attempts -= 1 + time.sleep(3) + else: + raise exception.\ + ManilaException(_('Can not receive service tenant id')) + self.service_network_id = self._get_service_network() + self.vif_driver = importutils.\ + import_class(self.configuration.interface_driver)() + self._setup_connectivity_with_service_instances() + self._setup_helpers() + + def _get_service_network(self): + """Finds existing or creates new service network.""" + service_network_name = self.configuration.service_network_name + networks = [network for network in self.neutron_api. + get_all_tenant_networks(self.service_tenant_id) + if network['name'] == service_network_name] + if len(networks) > 1: + raise exception.ManilaException(_('Ambiguous service networks')) + elif not networks: + return self.neutron_api.network_create(self.service_tenant_id, + service_network_name)['id'] + else: + return networks[0]['id'] + + def _setup_helpers(self): + """Initializes protocol-specific NAS drivers.""" + self._helpers = {} + for helper_str in self.configuration.share_helpers: + share_proto, _, import_str = helper_str.partition('=') + helper = importutils.import_class(import_str) + self._helpers[share_proto.upper()] = helper(self._execute, + self.configuration, + self.share_networks_locks) + + def create_share(self, context, share): + """Creates share.""" + if share['share_network_id'] is None: + raise exception.\ + ManilaException(_('Share Network is not specified')) + server = self._get_service_instance(self.admin_context, share) + volume = self._allocate_container(context, share) + volume = self._attach_volume(context, share, server, volume) + self._format_device(server, volume) + self._mount_device(context, share, server, volume) + location = self._get_helper(share).create_export(server, share['name']) + return location + + def _format_device(self, server, volume): + """Formats device attached to the service vm.""" + command = ['sudo', 'mkfs.ext4', volume['mountpoint']] + _ssh_exec(server, command) + + def _mount_device(self, context, share, server, volume): + """Mounts attached and formatted block device to the directory.""" + mount_path = self._get_mount_path(share) + command = ['sudo', 'mkdir', '-p', mount_path, ';'] + command.extend(['sudo', 'mount', volume['mountpoint'], mount_path]) + try: + _ssh_exec(server, command) + except exception.ProcessExecutionError as e: + if 'already mounted' not in e.stderr: + raise + LOG.debug(_('Share %s is already mounted') % share['name']) + command = ['sudo', 'chmod', '777', mount_path] + _ssh_exec(server, command) + + def _unmount_device(self, context, share, server): + """Unmounts device from directory on service vm.""" + mount_path = self._get_mount_path(share) + command = ['sudo', 'umount', mount_path, ';'] + command.extend(['sudo', 'rmdir', mount_path]) + try: + _ssh_exec(server, command) + except exception.ProcessExecutionError as e: + if 'not found' in e.stderr: + LOG.debug(_('%s is not mounted') % share['name']) + + def _get_mount_path(self, share): + """ + Returns the path, that will be used for mount device in service vm. + """ + return os.path.join(self.configuration.share_mount_path, share['name']) + + @synchronized + def _attach_volume(self, context, share, server, volume): + """Attaches cinder volume to service vm.""" + if volume['status'] == 'in-use': + attached_volumes = [vol.id for vol in + self.compute_api.instance_volumes_list(self.admin_context, + server['id'])] + if volume['id'] in attached_volumes: + return volume + else: + raise exception.ManilaException(_('Volume %s is already ' + 'attached to another instance') % volume['id']) + device_path = self._get_device_path(self.admin_context, server) + self.compute_api.instance_volume_attach(self.admin_context, + server['id'], + volume['id'], + device_path) + + t = time.time() + while time.time() - t < self.configuration.max_time_to_attach: + volume = self.volume_api.get(context, volume['id']) + if volume['status'] == 'in-use': + break + elif volume['status'] != 'attaching': + raise exception.ManilaException(_('Failed to attach volume %s') + % volume['id']) + time.sleep(1) + else: + raise exception.ManilaException(_('Volume have not been attached ' + 'in %ss. Giving up') % + self.configuration.max_time_to_attach) + + return volume + + def _get_volume(self, context, share_id): + """Finds volume, associated to the specific share.""" + volume_name = self.configuration.volume_name_template % share_id + search_opts = {'display_name': volume_name} + if context.is_admin: + search_opts['all_tenants'] = True + volumes_list = self.volume_api.get_all(context, search_opts) + volume = None + if len(volumes_list) == 1: + volume = volumes_list[0] + elif len(volumes_list) > 1: + raise exception.ManilaException(_('Error. Ambiguous volumes')) + return volume + + def _get_volume_snapshot(self, context, snapshot_id): + """Finds volume snaphots, associated to the specific share snaphots.""" + volume_snapshot_name = self.configuration.\ + volume_snapshot_name_template % snapshot_id + volume_snapshot_list = self.volume_api.get_all_snapshots(context, + {'display_name': volume_snapshot_name}) + volume_snapshot = None + if len(volume_snapshot_list) == 1: + volume_snapshot = volume_snapshot_list[0] + elif len(volume_snapshot_list) > 1: + raise exception.\ + ManilaException(_('Error. Ambiguous volume snaphots')) + return volume_snapshot + + @synchronized + def _detach_volume(self, context, share, server): + """Detaches cinder volume from service vm.""" + attached_volumes = [vol.id for vol in + self.compute_api.instance_volumes_list(self.admin_context, + server['id'])] + volume = self._get_volume(context, share['id']) + if volume and volume['id'] in attached_volumes: + self.compute_api.instance_volume_detach(self.admin_context, + server['id'], + volume['id']) + t = time.time() + while time.time() - t < self.configuration.max_time_to_attach: + volume = self.volume_api.get(context, volume['id']) + if volume['status'] in ('available', 'error'): + break + time.sleep(1) + else: + raise exception.ManilaException(_('Volume have not been ' + 'detached in %ss. Giving up') + % self.configuration.max_time_to_attach) + + def _get_device_path(self, context, server): + """Returns device path, that will be used for cinder volume attaching. + """ + volumes = self.compute_api.instance_volumes_list(context, server['id']) + used_literals = set(volume.device[-1] for volume in volumes + if '/dev/vd' in volume.device) + lit = 'b' + while lit in used_literals: + lit = chr(ord(lit) + 1) + device_name = '/dev/vd' + lit + return device_name + + def _get_service_instance_name(self, share): + """Returns service vms name.""" + return self.configuration.service_instance_name_template % \ + share['share_network_id'] + + def _get_server_ip(self, server): + """Returns service vms ip address.""" + net = server['networks'] + try: + net_ips = net[self.configuration.service_network_name] + return net_ips[0] + except KeyError: + msg = _('Service vm is not attached to %s network') + except IndexError: + msg = _('Service vm has no ips on %s network') + msg = msg % self.configuration.service_network_name + LOG.error(msg) + raise exception.ManilaException(msg) + + def _ensure_or_delete_server(self, context, server, update=False): + """Ensures that server exists and active, otherwise deletes it.""" + if update: + try: + server.update(self.compute_api.server_get(context, + server['id'])) + except exception.InstanceNotFound as e: + LOG.debug(e) + return False + if server['status'] == 'ACTIVE': + if self._check_server_availability(server): + return True + + self._delete_server(context, server) + return False + + def _delete_server(self, context, server): + """Deletes the server.""" + self.compute_api.server_delete(context, server['id']) + t = time.time() + while time.time() - t < self.configuration.\ + max_time_to_build_instance: + try: + server = self.compute_api.server_get(context, + server['id']) + except exception.InstanceNotFound: + LOG.debug(_('Service instance was deleted succesfully')) + break + time.sleep(1) + else: + raise exception.ManilaException(_('Instance have not been deleted ' + 'in %ss. Giving up') % + self.configuration.max_time_to_build_instance) + + @synchronized + def _get_service_instance(self, context, share, create=True): + """Finds or creates and setups service vm.""" + server = self.share_networks_servers.get(share['share_network_id'], {}) + old_server_ip = server.get('ip', None) + if server and self._ensure_or_delete_server(context, + server, + update=True): + return server + else: + server = {} + service_instance_name = self._get_service_instance_name(share) + search_opts = {'name': service_instance_name} + servers = self.compute_api.server_list(context, search_opts, True) + if len(servers) == 1: + server = servers[0] + server['ip'] = self._get_server_ip(server) + old_server_ip = server['ip'] + if not self._ensure_or_delete_server(context, server): + server.clear() + elif len(servers) > 1: + raise exception.\ + ManilaException(_('Ambiguous service instances')) + if not server and create: + server = self._create_service_instance(context, + service_instance_name, + share, old_server_ip) + if server: + server['share_network_id'] = share['share_network_id'] + server['ip'] = self._get_server_ip(server) + server['ssh_pool'] = self._get_ssh_pool(server) + server['ssh'] = server['ssh_pool'].create() + for helper in self._helpers.values(): + helper.init_helper(server) + + self.share_networks_servers[share['share_network_id']] = server + return server + + def _get_ssh_pool(self, server): + """Returns ssh connection pool for service vm.""" + ssh_pool = utils.SSHPool(server['ip'], 22, None, + self.configuration.service_instance_user, + password=self.configuration.service_instance_password, + privatekey=self.configuration.path_to_private_key, + max_size=1) + return ssh_pool + + def _get_key(self, context): + """Returns name of key, that will be injected to service vm.""" + if not self.configuration.path_to_public_key or \ + not self.configuration.path_to_private_key: + return + if not os.path.exists(self.configuration.path_to_public_key) or \ + not os.path.exists(self.configuration.path_to_private_key): + return + keypair_name = self.configuration.manila_service_keypair_name + keypairs = [k for k in self.compute_api.keypair_list(context) + if k.name == keypair_name] + if len(keypairs) > 1: + raise exception.ManilaException(_('Ambiguous keypairs')) + + public_key, _ = self._execute('cat', + self.configuration.path_to_public_key, + run_as_root=True) + if not keypairs: + keypair = self.compute_api.keypair_import(context, keypair_name, + public_key) + else: + keypair = keypairs[0] + if keypair.public_key != public_key: + LOG.debug('Public key differs from existing keypair. ' + 'Creating new keypair') + self.compute_api.keypair_delete(context, keypair.id) + keypair = self.compute_api.keypair_import(context, + keypair_name, + public_key) + return keypair.name + + def _get_service_image(self, context): + """Returns ID of service image, that will be used for service vm + creating. + """ + images = [image.id for image in self.compute_api.image_list(context) + if image.name == self.configuration.service_image_name] + if len(images) == 1: + return images[0] + elif not images: + raise exception.\ + ManilaException(_('No appropriate image was found')) + else: + raise exception.ManilaException(_('Ambiguous image name')) + + def _create_service_instance(self, context, instance_name, share, + old_server_ip): + """Creates service vm and sets up networking for it.""" + service_image_id = self._get_service_image(context) + key_name = self._get_key(context) + if not self.configuration.service_instance_password and not key_name: + raise exception.ManilaException(_('Neither service instance' + 'password nor key are available')) + + port = self._setup_network_for_instance(context, share, old_server_ip) + try: + self._setup_connectivity_with_service_instances() + except Exception as e: + LOG.debug(e) + self.neutron_api.delete_port(port['id']) + raise + service_instance = self.compute_api.server_create(context, + instance_name, service_image_id, + self.configuration.service_instance_flavor_id, + key_name, None, None, + nics=[{'port-id': port['id']}]) + + t = time.time() + while time.time() - t < self.configuration.max_time_to_build_instance: + if service_instance['status'] == 'ACTIVE': + break + if service_instance['status'] == 'ERROR': + raise exception.ManilaException(_('Failed to build service ' + 'instance')) + time.sleep(1) + try: + service_instance = self.compute_api.server_get(context, + service_instance['id']) + except exception.InstanceNotFound as e: + LOG.debug(e) + else: + raise exception.ManilaException(_('Instance have not been spawned ' + 'in %ss. Giving up') % + self.configuration.max_time_to_build_instance) + + service_instance['ip'] = self._get_server_ip(service_instance) + if not self._check_server_availability(service_instance): + raise exception.ManilaException(_('SSH connection have not been ' + 'established in %ss. Giving up') + % self.configuration.max_time_to_build_instance) + return service_instance + + def _check_server_availability(self, server): + t = time.time() + while time.time() - t < self.configuration.max_time_to_build_instance: + LOG.debug('Checking service vm availablity') + try: + socket.socket().connect((server['ip'], 22)) + LOG.debug(_('Service vm is available via ssh.')) + return True + except socket.error as e: + LOG.debug(e) + LOG.debug(_('Server is not available through ssh. Waiting...')) + time.sleep(5) + return False + + def _setup_network_for_instance(self, context, share, old_server_ip): + """Setups network for service vm.""" + service_network = self.neutron_api.get_network(self.service_network_id) + all_service_subnets = [self.neutron_api.get_subnet(subnet_id) + for subnet_id in service_network['subnets']] + service_subnets = [subnet for subnet in all_service_subnets + if subnet['name'] == share['share_network_id']] + if len(service_subnets) > 1: + raise exception.ManilaException(_('Ambiguous subnets')) + elif not service_subnets: + service_subnet = \ + self.neutron_api.subnet_create(self.service_tenant_id, + self.service_network_id, + share['share_network_id'], + self._get_cidr_for_subnet(all_service_subnets)) + else: + service_subnet = service_subnets[0] + + share_network = self.db.share_network_get(context, + share['share_network_id']) + private_router = self._get_private_router(share_network) + try: + self.neutron_api.router_add_interface(private_router['id'], + service_subnet['id']) + except exception.NetworkException as e: + if 'already has' not in e.msg: + raise + LOG.debug(_('Subnet %(subnet_id)s is already attached to the ' + 'router %(router_id)s') % + {'subnet_id': service_subnet['id'], + 'router_id': private_router['id']}) + + return self.neutron_api.create_port(self.service_tenant_id, + self.service_network_id, + subnet_id=service_subnet['id'], + fixed_ip=old_server_ip, + device_owner='manila') + + def _get_private_router(self, share_network): + """Returns router attached to private subnet gateway.""" + private_subnet = self.neutron_api.\ + get_subnet(share_network['neutron_subnet_id']) + if not private_subnet['gateway_ip']: + raise exception.ManilaException(_('Subnet must have gateway')) + private_network_ports = [p for p in self.neutron_api.list_ports( + network_id=share_network['neutron_net_id'])] + for p in private_network_ports: + fixed_ip = p['fixed_ips'][0] + if fixed_ip['subnet_id'] == private_subnet['id'] and \ + fixed_ip['ip_address'] == private_subnet['gateway_ip']: + private_subnet_gateway_port = p + break + else: + raise exception.ManilaException(_('Subnet gateway is not attached ' + 'the router')) + private_subnet_router = self.neutron_api.show_router( + private_subnet_gateway_port['device_id']) + return private_subnet_router + + def _setup_connectivity_with_service_instances(self): + """Setups connectivity with service instances by creating port + in service network, creating and setting up required network devices. + """ + port = self._setup_service_port() + interface_name = self.vif_driver.get_device_name(port) + self.vif_driver.plug(port['id'], interface_name, port['mac_address']) + ip_cidrs = [] + for fixed_ip in port['fixed_ips']: + subnet = self.neutron_api.get_subnet(fixed_ip['subnet_id']) + net = netaddr.IPNetwork(subnet['cidr']) + ip_cidr = '%s/%s' % (fixed_ip['ip_address'], net.prefixlen) + ip_cidrs.append(ip_cidr) + + self.vif_driver.init_l3(interface_name, ip_cidrs) + + # ensure that interface is first in the list + device = ip_lib.IPDevice(interface_name) + device.route.pullup_route(interface_name) + + # here we are checking for garbage devices from removed service port + self._clean_garbage(device) + + def _clean_garbage(self, device): + """Finds and removes network device, that was associated with deleted + service port. + """ + list_dev = [] + for dev in ip_lib.IPWrapper().get_devices(): + if dev.name != device.name and dev.name[:3] == device.name[:3]: + cidr_set = set() + for a in dev.addr.list(): + if a['ip_version'] == 4: + cidr_set.add(str(netaddr.IPNetwork(a['cidr']).cidr)) + list_dev.append((dev.name, cidr_set)) + device_cidr_set = set(str(netaddr.IPNetwork(a['cidr']).cidr) + for a in device.addr.list() + if a['ip_version'] == 4) + + for dev_name, cidr_set in list_dev: + if device_cidr_set & cidr_set: + self.vif_driver.unplug(dev_name) + + def _setup_service_port(self): + """Find or creates neutron port, that will be used for connectivity + with service instances. + """ + ports = [port for port in self.neutron_api. + list_ports(device_id='manila-share')] + if len(ports) > 1: + raise exception.\ + ManilaException(_('Error. Ambiguous service ports')) + elif not ports: + services = self.db.service_get_all_by_topic(self.admin_context, + 'manila-share') + host = services[0]['host'] if services else None + if host is None: + raise exception.ManilaException('Unable to get host') + port = self.neutron_api.create_port(self.service_tenant_id, + self.service_network_id, + device_id='manila-share', + device_owner='manila:generic_driver', + host_id=host) + else: + port = ports[0] + + network = self.neutron_api.get_network(self.service_network_id) + subnets = set(network['subnets']) + port_fixed_ips = [] + for fixed_ip in port['fixed_ips']: + port_fixed_ips.append({'subnet_id': fixed_ip['subnet_id'], + 'ip_address': fixed_ip['ip_address']}) + if fixed_ip['subnet_id'] in subnets: + subnets.remove(fixed_ip['subnet_id']) + + # If there are subnets here that means that + # we need to add those to the port and call update. + if subnets: + port_fixed_ips.extend([dict(subnet_id=s) for s in subnets]) + port = self.neutron_api.update_port_fixed_ips( + port['id'], {'fixed_ips': port_fixed_ips}) + + return port + + def _get_cidr_for_subnet(self, subnets): + """Returns not used cidr for service subnet creating.""" + used_cidrs = set(subnet['cidr'] for subnet in subnets) + serv_cidr = netaddr.IPNetwork(self.configuration.service_network_cidr) + for subnet in serv_cidr.subnet(29): + cidr = str(subnet.cidr) + if cidr not in used_cidrs: + return cidr + else: + raise exception.ManilaException(_('No available cidrs')) + + def _allocate_container(self, context, share, snapshot=None): + """Creates cinder volume, associated to share by name.""" + volume_snapshot = None + if snapshot: + volume_snapshot = self._get_volume_snapshot(context, + snapshot['id']) + volume = self.volume_api.create(context, share['size'], + self.configuration.volume_name_template % share['id'], '', + snapshot=volume_snapshot) + + t = time.time() + while time.time() - t < self.configuration.max_time_to_create_volume: + if volume['status'] == 'available': + break + if volume['status'] == 'error': + raise exception.ManilaException(_('Failed to create volume')) + time.sleep(1) + volume = self.volume_api.get(context, volume['id']) + else: + raise exception.ManilaException(_('Volume have not been created ' + 'in %ss. Giving up') % + self.configuration.max_time_to_create_volume) + + return volume + + def _deallocate_container(self, context, share): + """Deletes cinder volume.""" + volume = self._get_volume(context, share['id']) + if volume: + self.volume_api.delete(context, volume['id']) + t = time.time() + while time.time() - t < self.configuration.\ + max_time_to_create_volume: + try: + volume = self.volume_api.get(context, volume['id']) + except exception.VolumeNotFound: + LOG.debug(_('Volume was deleted succesfully')) + break + time.sleep(1) + else: + raise exception.ManilaException(_('Volume have not been ' + 'deleted in %ss. Giving up') + % self.configuration.max_time_to_create_volume) + + def get_share_stats(self, refresh=False): + """Get share status. + If 'refresh' is True, run update the stats first.""" + if refresh: + self._update_share_status() + + return self._stats + + def _update_share_status(self): + """Retrieve status info from share volume group.""" + + LOG.debug(_("Updating share status")) + data = {} + + # Note(zhiteng): These information are driver/backend specific, + # each driver may define these values in its own config options + # or fetch from driver specific configuration file. + data["share_backend_name"] = 'Cinder Volumes' + data["vendor_name"] = 'Open Source' + data["driver_version"] = '1.0' + data["storage_protocol"] = 'NFS_CIFS' + + data['total_capacity_gb'] = 'infinite' + data['free_capacity_gb'] = 'infinite' + data['reserved_percentage'] = \ + self.configuration.reserved_share_percentage + data['QoS_support'] = False + + self._stats = data + + def create_share_from_snapshot(self, context, share, snapshot): + """Is called to create share from snapshot.""" + server = self._get_service_instance(self.admin_context, share) + volume = self._allocate_container(context, share, snapshot) + volume = self._attach_volume(context, share, server, volume) + self._mount_device(context, share, server, volume) + location = self._get_helper(share).create_export(server, + share['name']) + return location + + def delete_share(self, context, share): + """Deletes share.""" + if not share['share_network_id']: + return + server = self._get_service_instance(self.admin_context, + share, create=False) + if server: + self._get_helper(share).remove_export(server, share['name']) + self._unmount_device(context, share, server) + self._detach_volume(context, share, server) + self._deallocate_container(context, share) + + def create_snapshot(self, context, snapshot): + """Creates a snapshot.""" + volume = self._get_volume(context, snapshot['share_id']) + volume_snapshot_name = self.configuration.\ + volume_snapshot_name_template % snapshot['id'] + volume_snapshot = self.volume_api.create_snapshot_force(context, + volume['id'], + volume_snapshot_name, + '') + t = time.time() + while time.time() - t < self.configuration.max_time_to_create_volume: + if volume_snapshot['status'] == 'available': + break + if volume_snapshot['status'] == 'error': + raise exception.ManilaException(_('Failed to create volume ' + 'snapshot')) + time.sleep(1) + volume_snapshot = self.volume_api.get_snapshot(context, + volume_snapshot['id']) + else: + raise exception.ManilaException(_('Volume snapshot have not been ' + 'created in %ss. Giving up') % + self.configuration.max_time_to_create_volume) + + def delete_snapshot(self, context, snapshot): + """Deletes a snapshot.""" + volume_snapshot = self._get_volume_snapshot(context, snapshot['id']) + if volume_snapshot is None: + return + self.volume_api.delete_snapshot(context, volume_snapshot['id']) + t = time.time() + while time.time() - t < self.configuration.max_time_to_create_volume: + try: + snapshot = self.volume_api.get_snapshot(context, + volume_snapshot['id']) + except exception.VolumeSnapshotNotFound: + LOG.debug(_('Volume snapshot was deleted succesfully')) + break + time.sleep(1) + else: + raise exception.ManilaException(_('Volume snapshot have not been ' + 'deleted in %ss. Giving up') % + self.configuration.max_time_to_create_volume) + + def ensure_share(self, context, share): + """Ensure that storage are mounted and exported.""" + server = self._get_service_instance(context, share) + volume = self._get_volume(context, share['id']) + volume = self._attach_volume(context, share, server, volume) + self._mount_device(context, share, server, volume) + self._get_helper(share).create_export(server, share['name']) + + def allow_access(self, context, share, access): + """Allow access to the share.""" + server = self._get_service_instance(self.admin_context, + share, + create=False) + if not server: + raise exception.ManilaException('Server not found. Try to ' + 'restart manila share service') + self._get_helper(share).allow_access(server, share['name'], + access['access_type'], + access['access_to']) + + def deny_access(self, context, share, access): + """Deny access to the share.""" + if not share['share_network_id']: + return + server = self._get_service_instance(self.admin_context, + share, + create=False) + if server: + self._get_helper(share).deny_access(server, share['name'], + access['access_type'], + access['access_to']) + + def _get_helper(self, share): + if share['share_proto'].startswith('NFS'): + return self._helpers['NFS'] + elif share['share_proto'].startswith('CIFS'): + return self._helpers['CIFS'] + else: + raise exception.InvalidShare(reason='Wrong share type') + + def get_network_allocations_number(self): + return 0 + + def setup_network(self, network_info): + pass + + +class NASHelperBase(object): + """Interface to work with share.""" + + def __init__(self, execute, config_object, locks): + self.configuration = config_object + self._execute = execute + self.share_networks_locks = locks + + def init_helper(self, server): + pass + + def create_export(self, server, share_name, recreate=False): + """Create new export, delete old one if exists.""" + raise NotImplementedError() + + def remove_export(self, server, share_name): + """Remove export.""" + raise NotImplementedError() + + def allow_access(self, server, share_name, access_type, access): + """Allow access to the host.""" + raise NotImplementedError() + + def deny_access(self, local_path, share_name, access_type, access, + force=False): + """Deny access to the host.""" + raise NotImplementedError() + + +class NFSHelper(NASHelperBase): + """Interface to work with share.""" + + def create_export(self, server, share_name, recreate=False): + """Create new export, delete old one if exists.""" + return ':'.join([server['ip'], + os.path.join(self.configuration.share_mount_path, share_name)]) + + def init_helper(self, server): + try: + _ssh_exec(server, ['sudo', 'exportfs']) + except exception.ProcessExecutionError as e: + if 'command not found' in e.stderr: + raise exception.ManilaException( + _('NFS server is not installed on %s') % server['id']) + LOG.error(e.stderr) + + def remove_export(self, server, share_name): + """Remove export.""" + pass + + def allow_access(self, server, share_name, access_type, access): + """Allow access to the host""" + local_path = os.path.join(self.configuration.share_mount_path, + share_name) + if access_type != 'ip': + reason = 'only ip access type allowed' + raise exception.InvalidShareAccess(reason) + #check if presents in export + out, _ = _ssh_exec(server, ['sudo', 'exportfs']) + out = re.search(re.escape(local_path) + '[\s\n]*' + re.escape(access), + out) + if out is not None: + raise exception.ShareAccessExists(access_type=access_type, + access=access) + _ssh_exec(server, ['sudo', 'exportfs', '-o', 'rw,no_subtree_check', + ':'.join([access, local_path])]) + + def deny_access(self, server, share_name, access_type, access, + force=False): + """Deny access to the host.""" + local_path = os.path.join(self.configuration.share_mount_path, + share_name) + _ssh_exec(server, ['sudo', 'exportfs', '-u', + ':'.join([access, local_path])]) + + +class CIFSHelper(NASHelperBase): + """Class provides functionality to operate with cifs shares""" + + def __init__(self, *args): + """Store executor and configuration path.""" + super(CIFSHelper, self).__init__(*args) + self.config_path = self.configuration.service_instance_smb_config_path + self.smb_template_config = self.configuration.smb_template_config_path + self.test_config = "%s_" % (self.smb_template_config,) + self.local_configs = {} + + def _create_local_config(self, share_network_id): + path, ext = os.path.splitext(self.smb_template_config) + local_config = '%s-%s%s' % (path, share_network_id, ext) + self.local_configs[share_network_id] = local_config + shutil.copy(self.smb_template_config, local_config) + return local_config + + def _get_local_config(self, share_network_id): + local_config = self.local_configs.get(share_network_id, None) + if local_config is None: + local_config = self._create_local_config(share_network_id) + return local_config + + def init_helper(self, server): + self._recreate_template_config() + local_config = self._create_local_config(server['share_network_id']) + config_dir = os.path.dirname(self.config_path) + try: + _ssh_exec(server, ['sudo', 'mkdir', + config_dir]) + except exception.ProcessExecutionError as e: + if 'File exists' not in e.stderr: + raise + LOG.debug(_('Directory %s already exists') % config_dir) + _ssh_exec(server, ['sudo', 'chown', + self.configuration.service_instance_user, + config_dir]) + _ssh_exec(server, ['touch', self.config_path]) + try: + _ssh_exec(server, ['sudo', 'stop', 'smbd']) + except exception.ProcessExecutionError as e: + if 'Unknown instance' not in e.stderr: + raise + LOG.debug(_('Samba service is not running')) + self._write_remote_config(local_config, server) + _ssh_exec(server, ['sudo', 'smbd', '-s', self.config_path]) + self._restart_service(server) + + def create_export(self, server, share_name, recreate=False): + """Create new export, delete old one if exists.""" + local_path = os.path.join(self.configuration.share_mount_path, + share_name) + config = self._get_local_config(server['share_network_id']) + parser = ConfigParser.ConfigParser() + parser.read(config) + #delete old one + if parser.has_section(share_name): + if recreate: + parser.remove_section(share_name) + else: + raise exception.Error('Section exists') + #Create new one + parser.add_section(share_name) + parser.set(share_name, 'path', local_path) + parser.set(share_name, 'browseable', 'yes') + parser.set(share_name, 'guest ok', 'yes') + parser.set(share_name, 'read only', 'no') + parser.set(share_name, 'writable', 'yes') + parser.set(share_name, 'create mask', '0755') + parser.set(share_name, 'hosts deny', '0.0.0.0/0') # denying all ips + parser.set(share_name, 'hosts allow', '127.0.0.1') + self._update_config(parser, config) + self._write_remote_config(config, server) + self._restart_service(server) + return '//%s/%s' % (server['ip'], share_name) + + def remove_export(self, server, share_name): + """Remove export.""" + config = self._get_local_config(server['share_network_id']) + parser = ConfigParser.ConfigParser() + parser.read(config) + #delete old one + if parser.has_section(share_name): + parser.remove_section(share_name) + self._update_config(parser, config) + self._write_remote_config(config, server) + _ssh_exec(server, ['sudo', 'smbcontrol', 'all', 'close-share', + share_name]) + + @synchronized + def _write_remote_config(self, config, server): + with open(config, 'r') as f: + cfg = "'" + f.read() + "'" + _ssh_exec(server, ['echo %s > %s' % (cfg, self.config_path)]) + + def allow_access(self, server, share_name, access_type, access): + """Allow access to the host.""" + if access_type != 'ip': + reason = 'only ip access type allowed' + raise exception.InvalidShareAccess(reason) + config = self._get_local_config(server['share_network_id']) + parser = ConfigParser.ConfigParser() + parser.read(config) + + hosts = parser.get(share_name, 'hosts allow') + + if access in hosts.split(): + raise exception.ShareAccessExists(access_type=access_type, + access=access) + hosts += ' %s' % (access,) + parser.set(share_name, 'hosts allow', hosts) + self._update_config(parser, config) + self._write_remote_config(config, server) + self._restart_service(server) + + def deny_access(self, server, share_name, access_type, access, + force=False): + """Deny access to the host.""" + config = self._get_local_config(server['share_network_id']) + parser = ConfigParser.ConfigParser() + try: + parser.read(config) + hosts = parser.get(share_name, 'hosts allow') + hosts = hosts.replace(' %s' % (access,), '', 1) + parser.set(share_name, 'hosts allow', hosts) + self._update_config(parser, config) + except ConfigParser.NoSectionError: + if not force: + raise + self._write_remote_config(config, server) + self._restart_service(server) + + def _recreate_template_config(self): + """Create new SAMBA configuration file.""" + if os.path.exists(self.smb_template_config): + os.unlink(self.smb_template_config) + parser = ConfigParser.ConfigParser() + parser.add_section('global') + parser.set('global', 'security', 'user') + parser.set('global', 'server string', '%h server (Samba, Openstack)') + self._update_config(parser, self.smb_template_config) + + def _restart_service(self, server): + _ssh_exec(server, 'sudo pkill -HUP smbd'.split()) + + def _update_config(self, parser, config): + """Check if new configuration is correct and save it.""" + #Check that configuration is correct + with open(self.test_config, 'w') as fp: + parser.write(fp) + self._execute('testparm', '-s', self.test_config, + check_exit_code=True) + #save it + with open(config, 'w') as fp: + parser.write(fp) diff --git a/manila/tests/fake_compute.py b/manila/tests/fake_compute.py new file mode 100644 index 0000000000..ff6f7fca60 --- /dev/null +++ b/manila/tests/fake_compute.py @@ -0,0 +1,96 @@ +# Copyright 2013 OpenStack Foundation +# All Rights Reserved +# +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + + +from oslo.config import cfg + +from manila.openstack.common import log as logging + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class FakeServer(object): + def __init__(self, **kwargs): + self.id = kwargs.pop('id', 'fake_id') + self.status = kwargs.pop('status', 'ACTIVE') + self.networks = kwargs.pop('networks', {}) + for key, value in kwargs.items(): + setattr(self, key, value) + + def __getitem__(self, attr): + return getattr(self, attr) + + def __setitem__(self, attr, value): + setattr(self, attr, value) + + def get(self, attr, default): + return getattr(self, attr, default) + + def update(self, *args, **kwargs): + pass + + +class FakeKeypair(object): + def __init__(self, **kwargs): + self.id = kwargs.pop('id', 'fake_keypair_id') + for key, value in kwargs.items(): + setattr(self, key, value) + + +class FakeImage(object): + def __init__(self, **kwargs): + self.id = kwargs.pop('id', 'fake_image_id') + for key, value in kwargs.items(): + setattr(self, key, value) + + +class API(object): + """Fake Compute API""" + def instance_volume_attach(self, ctx, server_id, volume_id, mount_path): + pass + + def instance_volume_detach(self, ctx, server_id, volume_id): + pass + + def instance_volumes_list(self, ctx, server_id): + pass + + def server_list(self, ctx, search_opts, all_tenants): + pass + + def server_create(self, *args, **kwargs): + pass + + def server_delete(self, *args, **kwargs): + pass + + def server_get(self, *args, **kwargs): + pass + + def keypair_list(self, *args, **kwargs): + pass + + def keypair_import(self, *args, **kwargs): + pass + + def keypair_delete(self, *args, **kwargs): + pass + + def image_list(self, *args, **kwargs): + pass diff --git a/manila/tests/fake_network.py b/manila/tests/fake_network.py index cd58127751..4567204ae8 100644 --- a/manila/tests/fake_network.py +++ b/manila/tests/fake_network.py @@ -26,8 +26,60 @@ CONF = cfg.CONF LOG = logging.getLogger(__name__) +class FakeNetwork(object): + def __init__(self, **kwargs): + self.id = kwargs.pop('id', 'fake_net_id') + self.name = kwargs.pop('name', 'net_name') + self.subnets = kwargs.pop('subnets', []) + for key, value in kwargs.items(): + setattr(self, key, value) + + def __getitem__(self, attr): + return getattr(self, attr) + + +class FakeSubnet(object): + def __init__(self, **kwargs): + self.id = kwargs.pop('id', 'fake_subnet_id') + self.network_id = kwargs.pop('network_id', 'fake_net_id') + self.cidr = kwargs.pop('cidr', 'fake_cidr') + for key, value in kwargs.items(): + setattr(self, key, value) + + def __getitem__(self, attr): + return getattr(self, attr) + + +class FakePort(object): + def __init__(self, **kwargs): + self.id = kwargs.pop('id', 'fake_subnet_id') + self.network_id = kwargs.pop('network_id', 'fake_net_id') + self.fixed_ips = kwargs.pop('fixed_ips', []) + for key, value in kwargs.items(): + setattr(self, key, value) + + def __getitem__(self, attr): + return getattr(self, attr) + + +class FakeRouter(object): + def __init__(self, **kwargs): + self.id = kwargs.pop('id', 'fake_router_id') + self.name = kwargs.pop('name', 'fake_router_name') + for key, value in kwargs.items(): + setattr(self, key, value) + + def __getitem__(self, attr): + return getattr(self, attr) + + def __setitem__(self, attr, value): + setattr(self, attr, value) + + class API(object): """Fake Network API""" + admin_tenant_id = 'fake admin tenant id' + network = { "status": "ACTIVE", "subnets": ["fake_subnet_id"], @@ -103,6 +155,24 @@ class API(object): port['id'] = port_id return port + def delete_port(self, port_id): + pass + + def get_subnet(self, subnet_id): + pass + + def subnet_create(self, *args, **kwargs): + pass + + def router_add_interface(self, *args, **kwargs): + pass + + def show_router(self, *args, **kwargs): + pass + + def update_port_fixed_ips(self, *args, **kwargs): + pass + def get_all_networks(self): """Get all networks for client.""" net1 = self.network.copy() @@ -116,3 +186,9 @@ class API(object): network = self.network.copy() network['id'] = network_uuid return network + + def network_create(self, tenant_id, name): + network = self.network.copy() + network['tenant_id'] = tenant_id + network['name'] = name + return network diff --git a/manila/tests/fake_volume.py b/manila/tests/fake_volume.py new file mode 100644 index 0000000000..08b13d90af --- /dev/null +++ b/manila/tests/fake_volume.py @@ -0,0 +1,77 @@ +# Copyright 2013 OpenStack Foundation +# All Rights Reserved +# +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + + +from oslo.config import cfg + +from manila.openstack.common import log as logging + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class FakeVolume(object): + def __init__(self, **kwargs): + self.id = kwargs.pop('id', 'fake_vol_id') + self.status = kwargs.pop('status', 'available') + self.device = kwargs.pop('device', '') + self.display_name = kwargs.pop('display_name', 'fake_vol_name') + for key, value in kwargs.items(): + setattr(self, key, value) + + def __getitem__(self, attr): + return getattr(self, attr) + + +class FakeVolumeSnapshot(object): + def __init__(self, **kwargs): + self.id = kwargs.pop('id', 'fake_volsnap_id') + self.status = kwargs.pop('status', 'available') + self.display_name = kwargs.pop('display_name', 'fake_volsnap_name') + for key, value in kwargs.items(): + setattr(self, key, value) + + def __getitem__(self, attr): + return getattr(self, attr) + + +class API(object): + """Fake Volume API""" + def get(self, volume_id): + pass + + def create_snapshot_force(self, *args, **kwargs): + pass + + def get_snapshot(self, *args, **kwargs): + pass + + def delete_snapshot(self, *args, **kwargs): + pass + + def create(self, *args, **kwargs): + pass + + def get_all(self, search_opts): + pass + + def delete(self, volume_id): + pass + + def get_all_snapshots(self, search_opts): + pass diff --git a/manila/tests/test_share_generic.py b/manila/tests/test_share_generic.py new file mode 100644 index 0000000000..72200b8917 --- /dev/null +++ b/manila/tests/test_share_generic.py @@ -0,0 +1,1264 @@ +# Copyright 2014 Mirantis Inc. +# All Rights Reserved. +# +# 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. +"""Unit tests for the Generic driver module.""" + +import copy +import mock +import os + +from manila import context + +from manila import compute +from manila import exception +from manila.network.neutron import api as neutron +from manila import volume + +from manila.share.configuration import Configuration +from manila.share.drivers import generic +from manila import test +from manila.tests.db import fakes as db_fakes +from manila.tests import fake_compute +from manila.tests import fake_network +from manila.tests import fake_utils +from manila.tests import fake_volume + +from oslo.config import cfg + +CONF = cfg.CONF + + +def fake_share(**kwargs): + share = { + 'id': 'fakeid', + 'name': 'fakename', + 'size': 1, + 'share_proto': 'NFS', + 'share_network_id': 'fake share network id', + 'export_location': '127.0.0.1:/mnt/nfs/volume-00002', + } + share.update(kwargs) + return db_fakes.FakeModel(share) + + +def fake_snapshot(**kwargs): + snapshot = { + 'id': 'fakesnapshotid', + 'share_name': 'fakename', + 'share_id': 'fakeid', + 'name': 'fakesnapshotname', + 'share_size': 1, + 'share_proto': 'NFS', + 'export_location': '127.0.0.1:/mnt/nfs/volume-00002', + } + snapshot.update(kwargs) + return db_fakes.FakeModel(snapshot) + + +def fake_access(**kwargs): + access = { + 'id': 'fakeaccid', + 'access_type': 'ip', + 'access_to': '10.0.0.2', + 'state': 'active', + } + access.update(kwargs) + return db_fakes.FakeModel(access) + + +class GenericShareDriverTestCase(test.TestCase): + """Tests GenericShareDriver.""" + + def setUp(self): + super(GenericShareDriverTestCase, self).setUp() + self._context = context.get_admin_context() + self._execute = mock.Mock(return_value=('', '')) + + self._helper_cifs = mock.Mock() + self._helper_nfs = mock.Mock() + self.fake_conf = Configuration(None) + self._db = mock.Mock() + self._driver = generic.GenericShareDriver(self._db, + execute=self._execute, + configuration=self.fake_conf) + self._driver.service_tenant_id = 'service tenant id' + self._driver.service_network_id = 'service network id' + self._driver.neutron_api = fake_network.API() + self._driver.compute_api = fake_compute.API() + self._driver.volume_api = fake_volume.API() + self._driver.share_networks_locks = {} + self._driver.share_networks_servers = {} + self._driver.admin_context = self._context + self._driver.vif_driver = mock.Mock() + self.stubs.Set(generic, '_ssh_exec', mock.Mock()) + self.stubs.Set(generic, 'synchronized', mock.Mock(side_effect= + lambda f: f)) + self.stubs.Set(generic.os.path, 'exists', mock.Mock(return_value=True)) + self._driver._helpers = { + 'CIFS': self._helper_cifs, + 'NFS': self._helper_nfs, + } + self.share = fake_share() + self.access = fake_access() + self.snapshot = fake_snapshot() + + def test_do_setup(self): + self.stubs.Set(neutron, 'API', mock.Mock()) + self.stubs.Set(volume, 'API', mock.Mock()) + self.stubs.Set(compute, 'API', mock.Mock()) + self.stubs.Set(self._driver, + '_setup_connectivity_with_service_instances', + mock.Mock()) + self.stubs.Set(self._driver, + '_get_service_network', + mock.Mock(return_value='fake network id')) + self.stubs.Set(self._driver, '_setup_helpers', mock.Mock()) + self._driver.do_setup(self._context) + neutron.API.assert_called_once() + volume.API.assert_called_once() + compute.API.assert_called_once() + self._driver._setup_helpers.assert_called_once() + self._driver._setup_connectivity_with_service_instances.\ + assert_called_once() + self.assertEqual(self._driver.service_network_id, 'fake network id') + + def test_do_setup_exception(self): + self.stubs.Set(neutron, 'API', mock.Mock()) + neutron.API.return_value = fake_network.API() + self.stubs.Set(volume, 'API', mock.Mock()) + self.stubs.Set(compute, 'API', mock.Mock()) + self.stubs.Set(neutron.API, 'admin_tenant_id', mock.Mock()) + neutron.API.admin_tenant_id.side_effect = Exception + self.assertRaises(exception.ManilaException, + self._driver.do_setup, self._context) + + def test_get_service_network_net_exists(self): + net1 = copy.copy(fake_network.API.network) + net2 = copy.copy(fake_network.API.network) + net1['name'] = CONF.service_network_name + net1['id'] = 'fake service network id' + self.stubs.Set(self._driver.neutron_api, 'get_all_tenant_networks', + mock.Mock(return_value=[net1, net2])) + result = self._driver._get_service_network() + self.assertEqual(result, net1['id']) + + def test_get_service_network_net_does_not_exists(self): + net = fake_network.FakeNetwork() + self.stubs.Set(self._driver.neutron_api, 'get_all_tenant_networks', + mock.Mock(return_value=[])) + self.stubs.Set(self._driver.neutron_api, 'network_create', + mock.Mock(return_value=net)) + result = self._driver._get_service_network() + self.assertEqual(result, net['id']) + + def test_get_service_network_ambiguos(self): + net = fake_network.FakeNetwork(name=CONF.service_network_name) + self.stubs.Set(self._driver.neutron_api, 'get_all_tenant_networks', + mock.Mock(return_value=[net, net])) + self.assertRaises(exception.ManilaException, + self._driver._get_service_network) + + def test_setup_helpers(self): + CONF.set_default('share_helpers', ['NFS=fakenfs']) + self.stubs.Set(generic.importutils, 'import_class', + mock.Mock(return_value=self._helper_nfs)) + self._driver._setup_helpers() + generic.importutils.import_class.assert_has_calls([ + mock.call('fakenfs') + ]) + self._helper_nfs.assert_called_once_with(self._execute, + self.fake_conf, + self._driver.share_networks_locks) + self.assertEqual(len(self._driver._helpers), 1) + + def test_create_share(self): + self._helper_nfs.create_export.return_value = 'fakelocation' + methods = ('_get_service_instance', '_allocate_container', + '_attach_volume', '_format_device', '_mount_device') + for method in methods: + self.stubs.Set(self._driver, method, mock.Mock()) + result = self._driver.create_share(self._context, self.share) + for method in methods: + getattr(self._driver, method).assert_called_once() + self.assertEqual(result, 'fakelocation') + + def test_create_share_exception(self): + share = fake_share(share_network_id=None) + self.assertRaises(exception.ManilaException, self._driver.create_share, + self._context, share) + + def test_format_device(self): + volume = {'mountpoint': 'fake_mount_point'} + self._driver._format_device('fake_server', volume) + generic._ssh_exec.assert_called_once_with('fake_server', + ['sudo', 'mkfs.ext4', volume['mountpoint']]) + + def _test_mount_device(self): + volume = {'mountpoint': 'fake_mount_point'} + self.stubs.Set(self._driver, '_get_mount_path', + mock.Mock(return_value='fake_mount_path')) + + self._driver._mount_device(self._context, self.share, 'fake_server', + volume) + + generic._ssh_exec.assert_has_calls([ + mock.call('fake_server', ['sudo', 'mkdir', '-p', + 'fake_mount_path', + ';', 'sudo', 'mount', + volume['mountpoint'], + 'fake_mount_path']), + mock.call('fake_server', ['sudo', 'chmod', '777', + 'fake_mount_path']) + ]) + + def test_mount_device_exception_01(self): + volume = {'mountpoint': 'fake_mount_point'} + generic._ssh_exec.side_effect = [ + exception.ProcessExecutionError(stderr='already mounted'), None] + self.stubs.Set(self._driver, '_get_mount_path', + mock.Mock(return_value='fake_mount_path')) + + self._driver._mount_device(self._context, self.share, 'fake_server', + volume) + + generic._ssh_exec.assert_has_calls([ + mock.call('fake_server', ['sudo', 'mkdir', '-p', + 'fake_mount_path', + ';', 'sudo', 'mount', + volume['mountpoint'], + 'fake_mount_path']), + mock.call('fake_server', ['sudo', 'chmod', '777', + 'fake_mount_path']) + ]) + + def test_mount_device_exception_02(self): + volume = {'mountpoint': 'fake_mount_point'} + generic._ssh_exec.side_effect = exception.ManilaException + self.stubs.Set(self._driver, '_get_mount_path', + mock.Mock(return_value='fake_mount_path')) + self.assertRaises(exception.ManilaException, + self._driver._mount_device, + self._context, self.share, 'fake_server', volume) + + def test_umount_device(self): + self.stubs.Set(self._driver, '_get_mount_path', + mock.Mock(return_value='fake_mount_path')) + self._driver._unmount_device(self._context, self.share, 'fake_server') + generic._ssh_exec.assert_called_once_with('fake_server', + ['sudo', 'umount', 'fake_mount_path', ';', 'sudo', 'rmdir', + 'fake_mount_path']) + + def test_get_mount_path(self): + result = self._driver._get_mount_path(self.share) + self.assertEqual(result, os.path.join(CONF.share_mount_path, + self.share['name'])) + + def test_attach_volume_not_attached(self): + fake_server = fake_compute.FakeServer() + availiable_volume = fake_volume.FakeVolume() + attached_volume = fake_volume.FakeVolume(status='in-use') + self.stubs.Set(self._driver, '_get_device_path', + mock.Mock(return_value='fake_device_path')) + self.stubs.Set(self._driver.compute_api, 'instance_volume_attach', + mock.Mock()) + self.stubs.Set(self._driver.volume_api, 'get', + mock.Mock(return_value=attached_volume)) + + result = self._driver._attach_volume(self._context, self.share, + fake_server, availiable_volume) + + self._driver._get_device_path.assert_called_once_with(self._context, + fake_server) + self._driver.compute_api.instance_volume_attach.\ + assert_called_once_with(self._context, fake_server['id'], + availiable_volume['id'], 'fake_device_path') + self._driver.volume_api.get.\ + assert_called_once_with(self._context, attached_volume['id']) + self.assertEqual(result, attached_volume) + + def test_attach_volume_attached_correct(self): + fake_server = fake_compute.FakeServer() + attached_volume = fake_volume.FakeVolume(status='in-use') + self.stubs.Set(self._driver.compute_api, 'instance_volumes_list', + mock.Mock(return_value=[attached_volume])) + + result = self._driver._attach_volume(self._context, self.share, + fake_server, attached_volume) + + self.assertEqual(result, attached_volume) + + def test_attach_volume_attached_incorrect(self): + fake_server = fake_compute.FakeServer() + attached_volume = fake_volume.FakeVolume(status='in-use') + anoter_volume = fake_volume.FakeVolume(id='fake_id2', status='in-use') + self.stubs.Set(self._driver.compute_api, 'instance_volumes_list', + mock.Mock(return_value=[anoter_volume])) + self.assertRaises(exception.ManilaException, + self._driver._attach_volume, self._context, + self.share, fake_server, attached_volume) + + def test_attach_volume_failed_attach(self): + fake_server = fake_compute.FakeServer() + availiable_volume = fake_volume.FakeVolume() + self.stubs.Set(self._driver, '_get_device_path', + mock.Mock(return_value='fake_device_path')) + self.stubs.Set(self._driver.compute_api, 'instance_volume_attach', + mock.Mock(side_effect=exception.ManilaException)) + self.assertRaises(exception.ManilaException, + self._driver._attach_volume, + self._context, self.share, fake_server, + availiable_volume) + + def test_attach_volume_error(self): + fake_server = fake_compute.FakeServer() + availiable_volume = fake_volume.FakeVolume() + error_volume = fake_volume.FakeVolume(status='error') + self.stubs.Set(self._driver, '_get_device_path', + mock.Mock(return_value='fake_device_path')) + self.stubs.Set(self._driver.compute_api, 'instance_volume_attach', + mock.Mock()) + self.stubs.Set(self._driver.volume_api, 'get', + mock.Mock(return_value=error_volume)) + self.assertRaises(exception.ManilaException, + self._driver._attach_volume, + self._context, self.share, + fake_server, availiable_volume) + + def test_get_volume(self): + volume = fake_volume.FakeVolume( + display_name=CONF.volume_name_template % self.share['id']) + self.stubs.Set(self._driver.volume_api, 'get_all', + mock.Mock(return_value=[volume])) + result = self._driver._get_volume(self._context, self.share['id']) + self.assertEqual(result, volume) + + def test_get_volume_none(self): + self.stubs.Set(self._driver.volume_api, 'get_all', + mock.Mock(return_value=[])) + result = self._driver._get_volume(self._context, self.share['id']) + self.assertEqual(result, None) + + def test_get_volume_error(self): + volume = fake_volume.FakeVolume( + display_name=CONF.volume_name_template % self.share['id']) + self.stubs.Set(self._driver.volume_api, 'get_all', + mock.Mock(return_value=[volume, volume])) + self.assertRaises(exception.ManilaException, + self._driver._get_volume, self._context, self.share['id']) + + def test_get_volume_snapshot(self): + volume_snapshot = fake_volume.FakeVolumeSnapshot(display_name= + CONF.volume_snapshot_name_template % self.snapshot['id']) + self.stubs.Set(self._driver.volume_api, 'get_all_snapshots', + mock.Mock(return_value=[volume_snapshot])) + result = self._driver._get_volume_snapshot(self._context, + self.snapshot['id']) + self.assertEqual(result, volume_snapshot) + + def test_get_volume_snapshot_none(self): + self.stubs.Set(self._driver.volume_api, 'get_all_snapshots', + mock.Mock(return_value=[])) + result = self._driver._get_volume_snapshot(self._context, + self.share['id']) + self.assertEqual(result, None) + + def test_get_volume_snapshot_error(self): + volume_snapshot = fake_volume.FakeVolumeSnapshot(display_name= + CONF.volume_snapshot_name_template % self.snapshot['id']) + self.stubs.Set(self._driver.volume_api, 'get_all_snapshots', + mock.Mock(return_value=[volume_snapshot, volume_snapshot])) + self.assertRaises(exception.ManilaException, + self._driver._get_volume_snapshot, self._context, self.share['id']) + + def test_detach_volume(self): + fake_server = fake_compute.FakeServer() + availiable_volume = fake_volume.FakeVolume() + attached_volume = fake_volume.FakeVolume(status='in-use') + self.stubs.Set(self._driver, '_get_volume', + mock.Mock(return_value=attached_volume)) + self.stubs.Set(self._driver.compute_api, 'instance_volumes_list', + mock.Mock(return_value=[attached_volume])) + self.stubs.Set(self._driver.compute_api, 'instance_volume_detach', + mock.Mock()) + self.stubs.Set(self._driver.volume_api, 'get', + mock.Mock(return_value=availiable_volume)) + + self._driver._detach_volume(self._context, self.share, fake_server) + + self._driver.compute_api.instance_volume_detach.\ + assert_called_once_with(self._context, fake_server['id'], + availiable_volume['id']) + self._driver.volume_api.get.\ + assert_called_once_with(self._context, availiable_volume['id']) + + def test_detach_volume_detached(self): + fake_server = fake_compute.FakeServer() + availiable_volume = fake_volume.FakeVolume() + attached_volume = fake_volume.FakeVolume(status='in-use') + self.stubs.Set(self._driver, '_get_volume', + mock.Mock(return_value=attached_volume)) + self.stubs.Set(self._driver.compute_api, 'instance_volumes_list', + mock.Mock(return_value=[])) + self.stubs.Set(self._driver.volume_api, 'get', + mock.Mock(return_value=availiable_volume)) + self.stubs.Set(self._driver.compute_api, 'instance_volume_detach', + mock.Mock()) + + self._driver._detach_volume(self._context, self.share, fake_server) + + self.assertFalse(self._driver.volume_api.get.called) + self.assertFalse(self._driver.compute_api. + instance_volume_detach.called) + + def test_get_device_path_01(self): + fake_server = fake_compute.FakeServer() + vol_list = [[], [fake_volume.FakeVolume(device='/dev/vdc')], + [fake_volume.FakeVolume(device='/dev/vdd')]] + self.stubs.Set(self._driver.compute_api, 'instance_volumes_list', + mock.Mock(side_effect=lambda x, y: vol_list.pop())) + + result = self._driver._get_device_path(self._context, fake_server) + + self.assertEqual(result, '/dev/vdb') + + def test_get_device_path_02(self): + fake_server = fake_compute.FakeServer() + vol_list = [[fake_volume.FakeVolume(device='/dev/vdb')], + [fake_volume.FakeVolume(device='/dev/vdb'), + fake_volume.FakeVolume(device='/dev/vdd')]] + self.stubs.Set(self._driver.compute_api, 'instance_volumes_list', + mock.Mock(side_effect=lambda x, y: vol_list.pop())) + + result = self._driver._get_device_path(self._context, fake_server) + + self.assertEqual(result, '/dev/vdc') + + def test_get_service_instance_name(self): + result = self._driver._get_service_instance_name(self.share) + self.assertEqual(result, CONF.service_instance_name_template % + self.share['share_network_id']) + + def test_get_server_ip(self): + fake_server = fake_compute.FakeServer(networks= + {CONF.service_network_name: '10.254.0.1'}) + + result = self._driver._get_server_ip(fake_server) + + self.assertEqual(result, + fake_server['networks'][CONF.service_network_name][0]) + + def test_get_server_ip_exception(self): + fake_server = fake_compute.FakeServer(networks={}) + self.assertRaises(exception.ManilaException, + self._driver._get_server_ip, fake_server) + + def test_get_service_instance(self): + fake_server = fake_compute.FakeServer() + self.stubs.Set(self._driver, '_ensure_or_delete_server', + mock.Mock(return_value=True)) + self.stubs.Set(self._driver, '_get_server_ip', + mock.Mock(return_value='fake_ip')) + self.stubs.Set(self._driver.compute_api, 'server_list', + mock.Mock(return_value=[])) + self.stubs.Set(self._driver, '_create_service_instance', + mock.Mock(return_value=fake_server)) + self.stubs.Set(self._driver, '_get_ssh_pool', + mock.Mock(return_value=mock.Mock())) + + result = self._driver._get_service_instance(self._context, self.share) + + self.assertFalse(self._driver._ensure_or_delete_server.called) + self._driver._get_ssh_pool.assert_called_once_with(fake_server) + self._driver.compute_api.server_list.assert_called_once() + self._driver._get_server_ip.assert_called_once() + self._driver._create_service_instance.assert_called_once() + self.assertEqual(result, fake_server) + + def test_get_service_instance_existed_in_memory(self): + fake_server = fake_compute.FakeServer() + self._driver.share_networks_servers = {self.share['share_network_id']: + fake_server} + self.stubs.Set(self._driver, '_ensure_or_delete_server', + mock.Mock(return_value=True)) + self.stubs.Set(self._driver.compute_api, 'server_list', + mock.Mock(return_value=[fake_server])) + self.stubs.Set(self._driver, '_get_ssh_pool', + mock.Mock(return_value=mock.Mock())) + self.stubs.Set(self._driver, '_create_service_instance', + mock.Mock(return_value=fake_server)) + + result = self._driver._get_service_instance(self._context, self.share) + + self._driver._ensure_or_delete_server.assert_called_once() + self.assertFalse(self._driver._get_ssh_pool.called) + self.assertFalse(self._driver.compute_api.server_list.called) + self.assertFalse(self._driver._create_service_instance.called) + + self.assertEqual(result, fake_server) + + def test_get_service_instance_existed_in_memory_non_active(self): + old_fake_server = fake_compute.FakeServer(status='ERROR') + new_fake_server = fake_compute.FakeServer() + self._driver.share_networks_servers = {self.share['share_network_id']: + old_fake_server} + self.stubs.Set(self._driver, '_ensure_or_delete_server', + mock.Mock(return_value=False)) + self.stubs.Set(self._driver, '_get_server_ip', + mock.Mock(return_value='fake_ip')) + self.stubs.Set(self._driver.compute_api, 'server_list', + mock.Mock(return_value=[])) + self.stubs.Set(self._driver, '_create_service_instance', + mock.Mock(return_value=new_fake_server)) + self.stubs.Set(self._driver, '_get_ssh_pool', + mock.Mock(return_value=mock.Mock())) + + result = self._driver._get_service_instance(self._context, self.share) + + self._driver._ensure_or_delete_server.assert_has_calls( + [mock.call(self._context, old_fake_server, update=True)]) + self._driver._get_ssh_pool.assert_called_once_with(new_fake_server) + self._driver.compute_api.server_list.assert_called_once() + self._driver._get_server_ip.assert_called_once() + self._driver._create_service_instance.assert_called_once() + + self.assertEqual(result, new_fake_server) + + def test_get_service_instance_existed(self): + fake_server = fake_compute.FakeServer() + self.stubs.Set(self._driver, '_ensure_or_delete_server', + mock.Mock(return_value=True)) + self.stubs.Set(self._driver, '_get_server_ip', + mock.Mock(return_value='fake_ip')) + self.stubs.Set(self._driver.compute_api, 'server_list', + mock.Mock(return_value=[fake_server])) + self.stubs.Set(self._driver, '_create_service_instance', + mock.Mock()) + self.stubs.Set(self._driver, '_get_ssh_pool', + mock.Mock(return_value=mock.Mock())) + + result = self._driver._get_service_instance(self._context, self.share) + + self._driver._ensure_or_delete_server.assert_called_once() + self._driver._get_ssh_pool.assert_called_once_with(fake_server) + self._driver.compute_api.server_list.assert_called_once() + self._driver._get_server_ip.assert_called_once() + self.assertFalse(self._driver._create_service_instance.called) + self.assertEqual(result, fake_server) + + def test_ensure_or_delete_server(self): + fake_server = fake_compute.FakeServer() + self.stubs.Set(self._driver, '_check_server_availability', + mock.Mock(return_value=True)) + self.stubs.Set(self._driver.compute_api, 'server_get', + mock.Mock(return_value=fake_server)) + result = self._driver._ensure_or_delete_server(self._context, + fake_server, + update=True) + self._driver.compute_api.server_get.\ + assert_called_once_with(self._context, fake_server['id']) + self._driver._check_server_availability.\ + assert_called_once_with(fake_server) + self.assertTrue(result) + + def test_ensure_or_delete_server_not_exists(self): + fake_server = fake_compute.FakeServer() + self.stubs.Set(self._driver, '_check_server_availability', + mock.Mock(return_value=True)) + self.stubs.Set(self._driver.compute_api, 'server_get', + mock.Mock(side_effect=exception.InstanceNotFound( + instance_id=fake_server['id']))) + result = self._driver._ensure_or_delete_server(self._context, + fake_server, + update=True) + self._driver.compute_api.server_get.\ + assert_called_once_with(self._context, fake_server['id']) + self.assertFalse(self._driver._check_server_availability.called) + self.assertFalse(result) + + def test_ensure_or_delete_server_exception(self): + fake_server = fake_compute.FakeServer() + self.stubs.Set(self._driver, '_check_server_availability', + mock.Mock(return_value=True)) + self.stubs.Set(self._driver.compute_api, 'server_get', + mock.Mock(side_effect=exception.ManilaException)) + self.assertRaises(exception.ManilaException, + self._driver._ensure_or_delete_server, + self._context, + fake_server, + update=True) + self._driver.compute_api.server_get.\ + assert_called_once_with(self._context, fake_server['id']) + self.assertFalse(self._driver._check_server_availability.called) + + def test_ensure_or_delete_server_non_active(self): + fake_server = fake_compute.FakeServer(status='ERROR') + self.stubs.Set(self._driver, '_delete_server', mock.Mock()) + self.stubs.Set(self._driver, '_check_server_availability', + mock.Mock(return_value=True)) + result = self._driver._ensure_or_delete_server(self._context, + fake_server) + self.assertFalse(self._driver._check_server_availability.called) + self._driver._delete_server.assert_called_once_with(self._context, + fake_server) + self.assertFalse(result) + + def test_get_key_create_new(self): + fake_keypair = fake_compute.FakeKeypair(name= + CONF.manila_service_keypair_name) + self.stubs.Set(self._driver.compute_api, 'keypair_list', + mock.Mock(return_value=[])) + self.stubs.Set(self._driver.compute_api, 'keypair_import', + mock.Mock(return_value=fake_keypair)) + + result = self._driver._get_key(self._context) + + self.assertEqual(result, fake_keypair.name) + self._driver.compute_api.keypair_list.assert_called_once() + self._driver.compute_api.keypair_import.assert_called_once() + + def test_get_key_exists(self): + fake_keypair = fake_compute.FakeKeypair( + name=CONF.manila_service_keypair_name, + public_key='fake_public_key') + self.stubs.Set(self._driver.compute_api, 'keypair_list', + mock.Mock(return_value=[fake_keypair])) + self.stubs.Set(self._driver.compute_api, 'keypair_import', + mock.Mock(return_value=fake_keypair)) + self.stubs.Set(self._driver, '_execute', + mock.Mock(return_value=('fake_public_key', ''))) + + result = self._driver._get_key(self._context) + + self._driver.compute_api.keypair_list.assert_called_once() + self.assertFalse(self._driver.compute_api.keypair_import.called) + self.assertEqual(result, fake_keypair.name) + + def test_get_key_exists_recreate(self): + fake_keypair = fake_compute.FakeKeypair( + name=CONF.manila_service_keypair_name, + public_key='fake_public_key1') + self.stubs.Set(self._driver.compute_api, 'keypair_list', + mock.Mock(return_value=[fake_keypair])) + self.stubs.Set(self._driver.compute_api, 'keypair_import', + mock.Mock(return_value=fake_keypair)) + self.stubs.Set(self._driver.compute_api, 'keypair_delete', mock.Mock()) + self.stubs.Set(self._driver, '_execute', + mock.Mock(return_value=('fake_public_key2', ''))) + + result = self._driver._get_key(self._context) + + self._driver.compute_api.keypair_list.assert_called_once() + self._driver.compute_api.keypair_delete.assert_called_once() + self._driver.compute_api.keypair_import.\ + assert_called_once_with(self._context, fake_keypair.name, + 'fake_public_key2') + self.assertEqual(result, fake_keypair.name) + + def test_get_service_image(self): + fake_image1 = fake_compute.FakeImage(name=CONF.service_image_name) + fake_image2 = fake_compute.FakeImage(name='another-image') + self.stubs.Set(self._driver.compute_api, 'image_list', + mock.Mock(return_value=[fake_image1, fake_image2])) + + result = self._driver._get_service_image(self._context) + + self.assertEqual(result, fake_image1.id) + + def test_get_service_image_not_found(self): + self.stubs.Set(self._driver.compute_api, 'image_list', + mock.Mock(return_value=[])) + + self.assertRaises(exception.ManilaException, + self._driver._get_service_image, + self._context) + + def test_get_service_image_ambiguous(self): + fake_image = fake_compute.FakeImage(name=CONF.service_image_name) + self.stubs.Set(self._driver.compute_api, 'image_list', + mock.Mock(return_value=[fake_image, fake_image])) + + self.assertRaises(exception.ManilaException, + self._driver._get_service_image, + self._context) + + def test_create_service_instance(self): + fake_server = fake_compute.FakeServer() + fake_port = fake_network.FakePort() + self.stubs.Set(self._driver, '_get_service_image', + mock.Mock(return_value='fake_image_id')) + self.stubs.Set(self._driver, '_get_key', + mock.Mock(return_value='fake_key_name')) + self.stubs.Set(self._driver, '_setup_network_for_instance', + mock.Mock(return_value=fake_port)) + self.stubs.Set(self._driver, + '_setup_connectivity_with_service_instances', + mock.Mock()) + self.stubs.Set(self._driver.compute_api, 'server_create', + mock.Mock(return_value=fake_server)) + self.stubs.Set(self._driver, '_get_server_ip', + mock.Mock(return_value='fake_ip')) + self.stubs.Set(generic.socket, 'socket', mock.Mock()) + + result = self._driver._create_service_instance(self._context, + 'instance_name', self.share, None) + + self._driver._get_service_image.assert_called_once() + self._driver._get_key.assert_called_once() + self._driver._setup_network_for_instance.assert_called_once() + self._driver._setup_connectivity_with_service_instances.\ + assert_called_once() + self._driver.compute_api.server_create.assert_called_once_with( + self._context, 'instance_name', 'fake_image_id', + CONF.service_instance_flavor_id, 'fake_key_name', None, None, + nics=[{'port-id': fake_port['id']}]) + generic.socket.socket.assert_called_once() + self.assertEqual(result, fake_server) + + def test_create_service_instance_error(self): + fake_server = fake_compute.FakeServer(status='ERROR') + fake_port = fake_network.FakePort() + self.stubs.Set(self._driver, '_get_service_image', + mock.Mock(return_value='fake_image_id')) + self.stubs.Set(self._driver, '_get_key', + mock.Mock(return_value='fake_key_name')) + self.stubs.Set(self._driver, '_setup_network_for_instance', + mock.Mock(return_value=fake_port)) + self.stubs.Set(self._driver, + '_setup_connectivity_with_service_instances', + mock.Mock()) + self.stubs.Set(self._driver.compute_api, 'server_create', + mock.Mock(return_value=fake_server)) + self.stubs.Set(self._driver.compute_api, 'server_get', + mock.Mock(return_value=fake_server)) + self.stubs.Set(generic.socket, 'socket', mock.Mock()) + + self.assertRaises(exception.ManilaException, + self._driver._create_service_instance, self._context, + 'instance_name', self.share, None) + + self._driver.compute_api.server_create.assert_called_once() + self.assertFalse(self._driver.compute_api.server_get.called) + self.assertFalse(generic.socket.socket.called) + + def test_create_service_instance_failed_setup_connectivity(self): + fake_server = fake_compute.FakeServer(status='ERROR') + fake_port = fake_network.FakePort() + self.stubs.Set(self._driver, '_get_service_image', + mock.Mock(return_value='fake_image_id')) + self.stubs.Set(self._driver, '_get_key', + mock.Mock(return_value='fake_key_name')) + self.stubs.Set(self._driver, '_setup_network_for_instance', + mock.Mock(return_value=fake_port)) + self.stubs.Set(self._driver, + '_setup_connectivity_with_service_instances', + mock.Mock(side_effect=exception.ManilaException)) + self.stubs.Set(self._driver.neutron_api, 'delete_port', mock.Mock()) + self.stubs.Set(self._driver.compute_api, 'server_create', + mock.Mock(return_value=fake_server)) + self.stubs.Set(self._driver.compute_api, 'server_get', + mock.Mock(return_value=fake_server)) + self.stubs.Set(generic.socket, 'socket', mock.Mock()) + + self.assertRaises(exception.ManilaException, + self._driver._create_service_instance, + self._context, 'instance_name', self.share, None) + + self._driver.neutron_api.delete_port.\ + assert_called_once_with(fake_port['id']) + self.assertFalse(self._driver.compute_api.server_create.called) + self.assertFalse(self._driver.compute_api.server_get.called) + self.assertFalse(generic.socket.socket.called) + + def test_create_service_instance_no_key_and_password(self): + self.stubs.Set(self._driver, '_get_service_image', + mock.Mock(return_value='fake_image_id')) + self.stubs.Set(self._driver, '_get_key', + mock.Mock(return_value=None)) + self.assertRaises(exception.ManilaException, + self._driver._create_service_instance, self._context, + 'instance_name', self.share, None) + + def test_setup_network_for_instance(self): + fake_service_net = fake_network.FakeNetwork(subnets=[]) + fake_service_subnet = fake_network.\ + FakeSubnet(name=self.share['share_network_id']) + fake_router = fake_network.FakeRouter() + fake_port = fake_network.FakePort() + self.stubs.Set(self._driver.neutron_api, 'get_network', + mock.Mock(return_value=fake_service_net)) + self.stubs.Set(self._driver.neutron_api, 'subnet_create', + mock.Mock(return_value=fake_service_subnet)) + self.stubs.Set(self._driver.db, 'share_network_get', + mock.Mock(return_value='fake_share_network')) + self.stubs.Set(self._driver, '_get_private_router', + mock.Mock(return_value=fake_router)) + self.stubs.Set(self._driver.neutron_api, 'router_add_interface', + mock.Mock()) + self.stubs.Set(self._driver.neutron_api, 'create_port', + mock.Mock(return_value=fake_port)) + self.stubs.Set(self._driver, '_get_cidr_for_subnet', + mock.Mock(return_value='fake_cidr')) + + result = self._driver._setup_network_for_instance(self._context, + self.share, None) + + self._driver.neutron_api.get_network.\ + assert_called_once_with(self._driver.service_network_id) + self._driver._get_private_router.\ + assert_called_once_with('fake_share_network') + self._driver.neutron_api.router_add_interface.\ + assert_called_once_with('fake_router_id', 'fake_subnet_id') + self._driver.neutron_api.subnet_create.assert_called_once_with( + self._driver.service_tenant_id, + self._driver.service_network_id, + self.share['share_network_id'], + 'fake_cidr') + self._driver.neutron_api.create_port.assert_called_once_with( + self._driver.service_tenant_id, + self._driver.service_network_id, + subnet_id='fake_subnet_id', + fixed_ip=None, + device_owner='manila') + self._driver._get_cidr_for_subnet.assert_called_once_with([]) + self.assertEqual(result, fake_port) + + def test_get_private_router(self): + fake_net = fake_network.FakeNetwork() + fake_subnet = fake_network.FakeSubnet(gateway_ip='fake_ip') + fake_port = fake_network.FakePort(fixed_ips=[ + {'subnet_id': fake_subnet['id'], + 'ip_address': fake_subnet['gateway_ip']}], + device_id='fake_router_id') + fake_router = fake_network.FakeRouter(id='fake_router_id') + self.stubs.Set(self._driver.neutron_api, 'get_subnet', + mock.Mock(return_value=fake_subnet)) + self.stubs.Set(self._driver.neutron_api, 'list_ports', + mock.Mock(return_value=[fake_port])) + self.stubs.Set(self._driver.neutron_api, 'show_router', + mock.Mock(return_value=fake_router)) + + result = self._driver._get_private_router( + {'neutron_subnet_id': fake_subnet['id'], + 'neutron_net_id': fake_net['id']}) + + self._driver.neutron_api.get_subnet.\ + assert_called_once_with(fake_subnet['id']) + self._driver.neutron_api.list_ports.\ + assert_called_once_with(network_id=fake_net['id']) + self._driver.neutron_api.show_router.\ + assert_called_once_with(fake_router['id']) + self.assertEqual(result, fake_router) + + def test_get_private_router_exception(self): + fake_net = fake_network.FakeNetwork() + fake_subnet = fake_network.FakeSubnet(gateway_ip='fake_ip') + self.stubs.Set(self._driver.neutron_api, 'get_subnet', + mock.Mock(return_value=fake_subnet)) + self.stubs.Set(self._driver.neutron_api, 'list_ports', + mock.Mock(return_value=[])) + + self.assertRaises(exception.ManilaException, + self._driver._get_private_router, + {'neutron_subnet_id': fake_subnet['id'], + 'neutron_net_id': fake_net['id']}) + + def test_setup_connectivity_with_service_instances(self): + fake_subnet = fake_network.FakeSubnet(cidr='10.254.0.1/29') + fake_port = fake_network.FakePort(fixed_ips=[ + {'subnet_id': fake_subnet['id'], 'ip_address': '10.254.0.2'}], + mac_address='fake_mac_address') + + self.stubs.Set(self._driver, '_setup_service_port', + mock.Mock(return_value=fake_port)) + self.stubs.Set(self._driver.vif_driver, 'get_device_name', + mock.Mock(return_value='fake_interface_name')) + self.stubs.Set(self._driver.neutron_api, 'get_subnet', + mock.Mock(return_value=fake_subnet)) + self.stubs.Set(self._driver, '_clean_garbage', mock.Mock()) + self.stubs.Set(self._driver.vif_driver, 'plug', mock.Mock()) + device_mock = mock.Mock() + self.stubs.Set(generic.ip_lib, 'IPDevice', + mock.Mock(return_value=device_mock)) + + self._driver._setup_connectivity_with_service_instances() + + self._driver._setup_service_port.assert_called_once() + self._driver.vif_driver.get_device_name.\ + assert_called_once_with(fake_port) + self._driver.vif_driver.plug.assert_called_once_with(fake_port['id'], + 'fake_interface_name', fake_port['mac_address']) + self._driver.neutron_api.get_subnet.\ + assert_called_once_with(fake_subnet['id']) + self._driver.vif_driver.init_l3.assert_called_once() + generic.ip_lib.IPDevice.assert_called_once() + device_mock.route.pullup_route.assert_called_once() + self._driver._clean_garbage.assert_called_once_with(device_mock) + + def test_setup_service_port(self): + fake_service_port = fake_network.FakePort(device_id='manila-share') + fake_service_net = fake_network.FakeNetwork(subnets=[]) + self.stubs.Set(self._driver.neutron_api, 'list_ports', + mock.Mock(return_value=[])) + self.stubs.Set(self._driver.db, 'service_get_all_by_topic', + mock.Mock(return_value=[{'host': 'fake_host'}])) + self.stubs.Set(self._driver.neutron_api, 'create_port', + mock.Mock(return_value=fake_service_port)) + self.stubs.Set(self._driver.neutron_api, 'get_network', + mock.Mock(return_value=fake_service_net)) + self.stubs.Set(self._driver.neutron_api, 'update_port_fixed_ips', + mock.Mock(return_value=fake_service_port)) + + result = self._driver._setup_service_port() + + self._driver.neutron_api.list_ports.\ + assert_called_once_with(device_id='manila-share') + self._driver.db.service_get_all_by_topic.assert_called_once() + self._driver.neutron_api.create_port.assert_called_once_with( + self._driver.service_tenant_id, + self._driver.service_network_id, + device_id='manila-share', + device_owner='manila:generic_driver', + host_id='fake_host' + ) + self._driver.neutron_api.get_network.assert_called_once() + self.assertFalse(self._driver.neutron_api.update_port_fixed_ips.called) + self.assertEqual(result, fake_service_port) + + def test_setup_service_port_ambigious_ports(self): + fake_service_port = fake_network.FakePort(device_id='manila-share') + self.stubs.Set(self._driver.neutron_api, 'list_ports', + mock.Mock(return_value=[fake_service_port, fake_service_port])) + self.assertRaises(exception.ManilaException, + self._driver._setup_service_port) + + def test_setup_service_port_exists(self): + fake_service_port = fake_network.FakePort(device_id='manila-share') + fake_service_net = fake_network.FakeNetwork(subnets=[]) + self.stubs.Set(self._driver.neutron_api, 'list_ports', + mock.Mock(return_value=[fake_service_port])) + self.stubs.Set(self._driver.db, 'service_get_all_by_topic', + mock.Mock(return_value=[{'host': 'fake_host'}])) + self.stubs.Set(self._driver.neutron_api, 'create_port', + mock.Mock(return_value=fake_service_port)) + self.stubs.Set(self._driver.neutron_api, 'get_network', + mock.Mock(return_value=fake_service_net)) + self.stubs.Set(self._driver.neutron_api, 'update_port_fixed_ips', + mock.Mock(return_value=fake_service_port)) + + result = self._driver._setup_service_port() + + self._driver.neutron_api.list_ports.\ + assert_called_once_with(device_id='manila-share') + self.assertFalse(self._driver.db.service_get_all_by_topic.called) + self.assertFalse(self._driver.neutron_api.create_port.called) + self._driver.neutron_api.get_network.assert_called_once() + self.assertFalse(self._driver.neutron_api.update_port_fixed_ips.called) + self.assertEqual(result, fake_service_port) + + def test_get_cidr_for_subnet(self): + serv_cidr = generic.netaddr.IPNetwork(CONF.service_network_cidr) + cidrs = serv_cidr.subnet(29) + cidr1 = str(cidrs.next()) + cidr2 = str(cidrs.next()) + + result = self._driver._get_cidr_for_subnet([]) + self.assertEqual(result, cidr1) + + fake_subnet = fake_network.FakeSubnet(cidr=cidr1) + result = self._driver._get_cidr_for_subnet([fake_subnet]) + self.assertEqual(result, cidr2) + + def test_allocate_container(self): + fake_vol = fake_volume.FakeVolume() + self.stubs.Set(self._driver.volume_api, 'create', + mock.Mock(return_value=fake_vol)) + + result = self._driver._allocate_container(self._context, self.share) + self.assertEqual(result, fake_vol) + self._driver.volume_api.create.assert_called_once_with(self._context, + self.share['size'], + CONF.volume_name_template % self.share['id'], + '', + snapshot=None) + + def test_allocate_container_with_snaphot(self): + fake_vol = fake_volume.FakeVolume() + fake_vol_snap = fake_volume.FakeVolumeSnapshot() + self.stubs.Set(self._driver, '_get_volume_snapshot', + mock.Mock(return_value=fake_vol_snap)) + self.stubs.Set(self._driver.volume_api, 'create', + mock.Mock(return_value=fake_vol)) + + result = self._driver._allocate_container(self._context, + self.share, + self.snapshot) + self.assertEqual(result, fake_vol) + self._driver.volume_api.create.assert_called_once_with(self._context, + self.share['size'], + CONF.volume_name_template % self.share['id'], + '', + snapshot=fake_vol_snap) + + def test_allocate_container_error(self): + fake_vol = fake_volume.FakeVolume(status='error') + self.stubs.Set(self._driver.volume_api, 'create', + mock.Mock(return_value=fake_vol)) + + self.assertRaises(exception.ManilaException, + self._driver._allocate_container, + self._context, + self.share) + + def test_deallocate_container(self): + fake_vol = fake_volume.FakeVolume() + self.stubs.Set(self._driver, '_get_volume', + mock.Mock(return_value=fake_vol)) + self.stubs.Set(self._driver.volume_api, 'delete', mock.Mock()) + self.stubs.Set(self._driver.volume_api, 'get', mock.Mock( + side_effect=exception.VolumeNotFound(volume_id=fake_vol['id']))) + + self._driver._deallocate_container(self._context, self.share) + + self._driver._get_volume.assert_called_once() + self._driver.volume_api.delete.assert_called_once() + self._driver.volume_api.get.assert_called_once() + + def test_create_share_from_snapshot(self): + self._helper_nfs.create_export.return_value = 'fakelocation' + methods = ('_get_service_instance', '_allocate_container', + '_attach_volume', '_mount_device') + for method in methods: + self.stubs.Set(self._driver, method, mock.Mock()) + result = self._driver.create_share_from_snapshot(self._context, + self.share, + self.snapshot) + for method in methods: + getattr(self._driver, method).assert_called_once() + self.assertEqual(result, 'fakelocation') + + def test_delete_share(self): + fake_server = fake_compute.FakeServer() + self.stubs.Set(self._driver, '_get_service_instance', + mock.Mock(return_value=fake_server)) + self.stubs.Set(self._driver, '_unmount_device', mock.Mock()) + self.stubs.Set(self._driver, '_detach_volume', mock.Mock()) + self.stubs.Set(self._driver, '_deallocate_container', mock.Mock()) + + self._driver.delete_share(self._context, self.share) + + self._driver._get_service_instance.assert_called_once() + self._driver._unmount_device.assert_called_once() + self._driver._detach_volume.assert_called_once() + self._driver._deallocate_container.assert_called_once() + + def test_create_snapshot(self): + fake_vol = fake_volume.FakeVolume() + fake_vol_snap = fake_volume.FakeVolumeSnapshot() + self.stubs.Set(self._driver, '_get_volume', + mock.Mock(return_value=fake_vol)) + self.stubs.Set(self._driver.volume_api, 'create_snapshot_force', + mock.Mock(return_value=fake_vol_snap)) + + self._driver.create_snapshot(self._context, self.snapshot) + + self._driver._get_volume.assert_called_once() + self._driver.volume_api.create_snapshot_force.assert_called_once_with( + self._context, + fake_vol['id'], + CONF.volume_snapshot_name_template % self.snapshot['id'], + '' + ) + + def test_delete_snapshot(self): + fake_vol_snap = fake_volume.FakeVolumeSnapshot() + self.stubs.Set(self._driver, '_get_volume_snapshot', + mock.Mock(return_value=fake_vol_snap)) + self.stubs.Set(self._driver.volume_api, 'delete_snapshot', mock.Mock()) + self.stubs.Set(self._driver.volume_api, 'get_snapshot', + mock.Mock(side_effect=exception.VolumeSnapshotNotFound( + snapshot_id=fake_vol_snap['id']))) + + self._driver.delete_snapshot(self._context, fake_vol_snap) + + self._driver._get_volume_snapshot.assert_called_once() + self._driver.volume_api.delete_snapshot.assert_called_once() + self._driver.volume_api.get_snapshot.assert_called_once() + + def test_ensure_share(self): + self._helper_nfs.create_export.return_value = 'fakelocation' + methods = ('_get_service_instance', '_get_volume', + '_attach_volume', '_mount_device') + for method in methods: + self.stubs.Set(self._driver, method, mock.Mock()) + self._driver.ensure_share(self._context, self.share) + for method in methods: + getattr(self._driver, method).assert_called_once() + + def test_allow_access(self): + fake_server = fake_compute.FakeServer() + access = {'access_type': 'ip', 'access_to': 'fake_dest'} + self.stubs.Set(self._driver, '_get_service_instance', + mock.Mock(return_value=fake_server)) + self._driver.allow_access(self._context, self.share, access) + + self._driver._get_service_instance.assert_called_once() + self._driver._helpers[self.share['share_proto']].\ + allow_access.assert_called_once_with(fake_server, + self.share['name'], + access['access_type'], + access['access_to']) + + def test_deny_access(self): + fake_server = fake_compute.FakeServer() + access = {'access_type': 'ip', 'access_to': 'fake_dest'} + self.stubs.Set(self._driver, '_get_service_instance', + mock.Mock(return_value=fake_server)) + self._driver.deny_access(self._context, self.share, access) + + self._driver._get_service_instance.assert_called_once() + self._driver._helpers[self.share['share_proto']].\ + deny_access.assert_called_once_with(fake_server, + self.share['name'], + access['access_type'], + access['access_to']) + + +class NFSHelperTestCase(test.TestCase): + """Test case for NFS helper of generic driver.""" + + def setUp(self): + super(NFSHelperTestCase, self).setUp() + fake_utils.stub_out_utils_execute(self.stubs) + self.fake_conf = Configuration(None) + self.stubs.Set(generic, '_ssh_exec', mock.Mock(return_value=('', ''))) + self._execute = mock.Mock(return_value=('', '')) + self._helper = generic.NFSHelper(self._execute, self.fake_conf, {}) + + def test_create_export(self): + fake_server = fake_compute.FakeServer(ip='10.254.0.3') + ret = self._helper.create_export(fake_server, 'volume-00001') + expected_location = ':'.join([fake_server['ip'], + os.path.join(CONF.share_mount_path, 'volume-00001')]) + self.assertEqual(ret, expected_location) + + def test_allow_access(self): + fake_server = fake_compute.FakeServer(ip='10.254.0.3') + self._helper.allow_access(fake_server, 'volume-00001', + 'ip', '10.0.0.2') + local_path = os.path.join(CONF.share_mount_path, 'volume-00001') + generic._ssh_exec.assert_has_calls([ + mock.call(fake_server, ['sudo', 'exportfs']), + mock.call(fake_server, ['sudo', 'exportfs', '-o', + 'rw,no_subtree_check', + ':'.join(['10.0.0.2', local_path])]) + ]) + + def test_allow_access_no_ip(self): + self.assertRaises(exception.InvalidShareAccess, + self._helper.allow_access, 'fake_server', 'share0', + 'fake', 'fakerule') + + def test_deny_access(self): + fake_server = fake_compute.FakeServer(ip='10.254.0.3') + local_path = os.path.join(CONF.share_mount_path, 'volume-00001') + self._helper.deny_access(fake_server, 'volume-00001', 'ip', '10.0.0.2') + export_string = ':'.join(['10.0.0.2', local_path]) + expected_exec = ['sudo', 'exportfs', '-u', export_string] + generic._ssh_exec.assert_called_once_with(fake_server, expected_exec) + + +class CIFSHelperTestCase(test.TestCase): + """Test case for CIFS helper of generic driver.""" + + def setUp(self): + super(CIFSHelperTestCase, self).setUp() + self.fake_conf = Configuration(None) + self.stubs.Set(generic, '_ssh_exec', mock.Mock(return_value=('', ''))) + self._execute = mock.Mock(return_value=('', '')) + self._helper = generic.CIFSHelper(self._execute, self.fake_conf, {}) + + def test_create_export(self): + fake_server = fake_compute.FakeServer(ip='10.254.0.3', + share_network_id='fake_share_network_id') + self.stubs.Set(self._helper, '_update_config', mock.Mock()) + self.stubs.Set(self._helper, '_write_remote_config', mock.Mock()) + self.stubs.Set(self._helper, '_restart_service', mock.Mock()) + self.stubs.Set(self._helper, '_get_local_config', mock.Mock()) + self.stubs.Set(generic.ConfigParser, 'ConfigParser', mock.Mock()) + + ret = self._helper.create_export(fake_server, 'volume-00001', + recreate=True) + self._helper._get_local_config.\ + assert_called_once_with(fake_server['share_network_id']) + self._helper._update_config.assert_called_once() + self._helper._write_remote_config.assert_called_once() + self._helper._restart_service.assert_called_once() + expected_location = '//%s/%s' % (fake_server['ip'], 'volume-00001') + self.assertEqual(ret, expected_location) + + def test_remove_export(self): + fake_server = fake_compute.FakeServer(ip='10.254.0.3', + share_network_id='fake_share_network_id') + self.stubs.Set(generic.ConfigParser, 'ConfigParser', mock.Mock()) + self.stubs.Set(self._helper, '_get_local_config', mock.Mock()) + self.stubs.Set(self._helper, '_update_config', mock.Mock()) + self.stubs.Set(self._helper, '_write_remote_config', mock.Mock()) + self._helper.remove_export(fake_server, 'volume-00001') + self._helper._get_local_config.assert_called_once() + self._helper._update_config.assert_called_once() + self._helper._write_remote_config.assert_called_once() + generic._ssh_exec.assert_called_once_with(fake_server, + ['sudo', 'smbcontrol', 'all', 'close-share', 'volume-00001']) + + def test_allow_access(self): + class FakeParser(object): + def read(self, *args, **kwargs): + pass + + def get(self, *args, **kwargs): + return '' + + def set(self, *args, **kwargs): + pass + + fake_server = fake_compute.FakeServer(ip='10.254.0.3', + share_network_id='fake_share_network_id') + self.stubs.Set(generic.ConfigParser, 'ConfigParser', FakeParser) + self.stubs.Set(self._helper, '_get_local_config', mock.Mock()) + self.stubs.Set(self._helper, '_update_config', mock.Mock()) + self.stubs.Set(self._helper, '_write_remote_config', mock.Mock()) + self.stubs.Set(self._helper, '_restart_service', mock.Mock()) + + self._helper.allow_access(fake_server, 'volume-00001', + 'ip', '10.0.0.2') + self._helper._get_local_config.assert_called_once() + self._helper._update_config.assert_called_once() + self._helper._write_remote_config.assert_called_once() + self._helper._restart_service.assert_called_once() + + def test_deny_access(self): + fake_server = fake_compute.FakeServer(ip='10.254.0.3', + share_network_id='fake_share_network_id') + self.stubs.Set(generic.ConfigParser, 'ConfigParser', mock.Mock()) + self.stubs.Set(self._helper, '_get_local_config', mock.Mock()) + self.stubs.Set(self._helper, '_update_config', mock.Mock()) + self.stubs.Set(self._helper, '_write_remote_config', mock.Mock()) + self.stubs.Set(self._helper, '_restart_service', mock.Mock()) + + self._helper.deny_access(fake_server, 'volume-00001', + 'ip', '10.0.0.2') + self._helper._get_local_config.assert_called_once() + self._helper._update_config.assert_called_once() + self._helper._write_remote_config.assert_called_once() + self._helper._restart_service.assert_called_once()