manila/manila/share/drivers/service_instance.py

710 lines
31 KiB
Python

# Copyright (c) 2014 NetApp, 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.
"""Module for managing nova instances for share drivers."""
import os
import socket
import threading
import time
import netaddr
from oslo_config import cfg
from oslo_utils import importutils
import six
from manila.common import constants
from manila import compute
from manila import context
from manila import exception
from manila.i18n import _
from manila.i18n import _LW
from manila.network.linux import ip_lib
from manila.network.neutron import api as neutron
from manila.openstack.common import log as logging
from manila import utils
LOG = logging.getLogger(__name__)
server_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('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('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='~/.ssh/id_rsa.pub',
help="Path to hosts public key."),
cfg.StrOpt('path_to_private_key',
default='~/.ssh/id_rsa',
help="Path to hosts private key."),
cfg.IntOpt('max_time_to_build_instance',
default=300,
help="Maximum time to wait for creating service instance."),
cfg.StrOpt('service_instance_security_group',
default="manila-service",
help="Name of security group, that will be used for "
"service instance creation."),
cfg.IntOpt('service_instance_flavor_id',
default=100,
help="ID of flavor, that will be used for service instance "
"creation."),
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.IntOpt('service_network_division_mask',
default=28,
help="This mask is used for dividing service network into "
"subnets, IP capacity of subnet with this mask directly "
"defines possible amount of created service VMs "
"per tenant's subnet."),
cfg.StrOpt('interface_driver',
default='manila.network.linux.interface.OVSInterfaceDriver',
help="Vif driver."),
cfg.BoolOpt('connect_share_server_to_tenant_network',
default=False,
help='Attach share server directly to share network.'),
]
CONF = cfg.CONF
CONF.register_opts(server_opts)
lock = threading.Lock()
class ServiceInstanceManager(object):
"""Manages nova instances for various share drivers.
This class provides following external methods:
1. set_up_service_instance: creates instance and sets up share
infrastructure.
2. ensure_service_instance: ensure service instance is available.
3. delete_service_instance: removes service instance and network
infrastructure.
"""
def get_config_option(self, key):
"""Returns value of config option.
:param key: key of config' option.
:returns: str -- value of config's option.
first priority is driver's config,
second priority is global config.
"""
value = None
if self.driver_config:
value = self.driver_config.safe_get(key)
else:
value = CONF.get(key)
return value
def __init__(self, db, *args, **kwargs):
"""Do initialization."""
super(ServiceInstanceManager, self).__init__()
self.driver_config = None
if "driver_config" in kwargs:
self.driver_config = kwargs["driver_config"]
if not self.get_config_option("service_instance_user"):
raise exception.ServiceInstanceException(_('Service instance user '
'is not specified'))
self.admin_context = context.get_admin_context()
self._execute = utils.execute
self.compute_api = compute.API()
self.neutron_api = neutron.API()
self.db = db
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.ServiceInstanceException(_('Can not receive '
'service tenant id.'))
self.service_network_id = self._get_service_network()
self.vif_driver = importutils.import_class(
self.get_config_option("interface_driver"))()
self._setup_connectivity_with_service_instances()
self.max_time_to_build_instance = self.get_config_option(
"max_time_to_build_instance")
self.path_to_private_key = self.get_config_option(
"path_to_private_key")
self.path_to_public_key = self.get_config_option("path_to_public_key")
self.connect_share_server_to_tenant_network = self.get_config_option(
'connect_share_server_to_tenant_network')
@utils.synchronized("service_instance_get_service_network", external=True)
def _get_service_network(self):
"""Finds existing or creates new service network."""
service_network_name = self.get_config_option("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.ServiceInstanceException(_('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 _get_service_instance_name(self, share_server_id):
"""Returns service vms name."""
if self.driver_config:
# Make service instance name unique for multibackend installation
name = "%s_%s" % (self.driver_config.config_group,
share_server_id)
else:
name = share_server_id
return self.get_config_option("service_instance_name_template") % name
def _get_server_ip(self, server):
"""Returns service vms ip address."""
net_name = self.get_config_option("service_network_name")
net_ips = []
if 'networks' in server and net_name in server['networks']:
net_ips = server['networks'][net_name]
elif 'addresses' in server and net_name in server['addresses']:
net_ips = [addr['addr'] for addr in server['addresses'][net_name]]
if not net_ips:
msg = _("Failed to get service instance ip address. "
"Service network name is '%(net_name)s' "
"and provided data are '%(data)s'.")
msg = msg % {'net_name': net_name, 'data': six.text_type(server)}
raise exception.ServiceInstanceException(msg)
return net_ips[0]
@utils.synchronized(
"service_instance_get_or_create_security_group", external=True)
def _get_or_create_security_group(self, context, name=None,
description=None):
"""Get or create security group for service_instance.
:param context: context, that should be used
:param name: this is used for selection/creation of sec.group
:param description: this is used on sec.group creation step only
:returns: SecurityGroup -- security group instance from Nova
:raises: exception.ServiceInstanceException.
"""
name = name or self.get_config_option(
"service_instance_security_group")
if not name:
LOG.warning(_LW("Name for service instance security group is not "
"provided. Skipping security group step."))
return None
s_groups = [s for s in self.compute_api.security_group_list(context)
if s.name == name]
if not s_groups:
# Creating security group
if not description:
description = "This security group is intended "\
"to be used by share service."
LOG.debug("Creating security group with name '%s'.", name)
sg = self.compute_api.security_group_create(
context, name, description)
for protocol, ports in constants.SERVICE_INSTANCE_SECGROUP_DATA:
self.compute_api.security_group_rule_create(
context,
parent_group_id=sg.id,
ip_protocol=protocol,
from_port=ports[0],
to_port=ports[1],
cidr="0.0.0.0/0",
)
elif len(s_groups) > 1:
msg = _("Ambiguous security_groups.")
raise exception.ServiceInstanceException(msg)
else:
sg = s_groups[0]
return sg
def ensure_service_instance(self, context, server):
"""Ensures that server exists and active."""
try:
inst = self.compute_api.server_get(self.admin_context,
server['instance_id'])
except exception.InstanceNotFound:
LOG.warning(_LW("Service instance %s does not exist."),
server['instance_id'])
return False
if inst['status'] == 'ACTIVE':
return self._check_server_availability(server)
return False
def _delete_server(self, context, server_id):
"""Deletes the server."""
try:
self.compute_api.server_get(context, server_id)
except exception.InstanceNotFound:
LOG.debug("Service instance '%s' was not found. "
"Nothing to delete, skipping.", server_id)
return
self.compute_api.server_delete(context, server_id)
t = time.time()
while time.time() - t < self.max_time_to_build_instance:
try:
self.compute_api.server_get(context, server_id)
except exception.InstanceNotFound:
LOG.debug("Service instance '%s' was deleted "
"succesfully.", server_id)
break
time.sleep(2)
else:
raise exception.ServiceInstanceException(
_("Instance '%(id)s' has not been deleted in %(s)ss. "
"Giving up.") % {
'id': server_id, 's': self.max_time_to_build_instance})
def set_up_service_instance(self, context, instance_name, neutron_net_id,
neutron_subnet_id):
"""Finds or creates and sets up service vm.
:param context: defines context, that should be used
:param instance_name: provides name for service VM
:param neutron_net_id: provides network id for service VM
:param neutron_subnet_id: provides subnet id for service VM
:returns: dict with service instance details
:raises: exception.ServiceInstanceException
"""
server = self._create_service_instance(
context, instance_name, neutron_net_id, neutron_subnet_id)
instance_details = {
'instance_id': server['id'],
'ip': server['ip'],
'pk_path': server['pk_path'],
'subnet_id': server['subnet_id'],
'password': self.get_config_option('service_instance_password'),
'username': self.get_config_option('service_instance_user'),
'public_address': server['public_address'],
}
if 'router_id' in server:
instance_details['router_id'] = server['router_id']
for key in ('password', 'pk_path'):
if not instance_details[key]:
instance_details.pop(key)
return instance_details
@utils.synchronized("service_instance_get_key", external=True)
def _get_key(self, context):
"""Get ssh key.
:param context: defines context, that should be used
:returns: tuple with keypair name and path to private key.
"""
if not (self.path_to_public_key and self.path_to_private_key):
return (None, None)
path_to_public_key = os.path.expanduser(self.path_to_public_key)
path_to_private_key = os.path.expanduser(self.path_to_private_key)
if (not os.path.exists(path_to_public_key) or
not os.path.exists(path_to_private_key)):
return (None, None)
keypair_name = self.get_config_option("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.ServiceInstanceException(_('Ambiguous keypairs.'))
public_key, __ = self._execute('cat', path_to_public_key)
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, path_to_private_key
def _get_service_image(self, context):
"""Returns ID of service image for service vm creating."""
service_image_name = self.get_config_option("service_image_name")
images = [image.id for image in self.compute_api.image_list(context)
if image.name == service_image_name]
if len(images) == 1:
return images[0]
elif not images:
raise exception.ServiceInstanceException(_('No appropriate '
'image was found.'))
else:
raise exception.ServiceInstanceException(
_('Ambiguous image name.'))
def _create_service_instance(self, context, instance_name, neutron_net_id,
neutron_subnet_id):
"""Creates service vm and sets up networking for it."""
service_image_id = self._get_service_image(context)
with lock:
key_name, key_path = self._get_key(context)
if not (self.get_config_option("service_instance_password") or
key_name):
raise exception.ServiceInstanceException(
_('Neither service '
'instance password nor key are available.'))
if not key_path:
str_params = {
'private_path': self.path_to_private_key,
'public_path': self.path_to_public_key,
}
LOG.warning(_LW('No key path is available. May be '
'non-existent key path is provided. Check '
'path_to_private_key (current value '
'%(private_path)s) and '
'path_to_public_key (current value '
'%(public_path)s) in manila '
'configuration file.'), str_params)
security_group = self._get_or_create_security_group(context)
network_data = self._setup_network_for_instance(neutron_net_id,
neutron_subnet_id)
try:
self._setup_connectivity_with_service_instances()
except Exception as e:
LOG.debug(e)
for port in network_data['ports']:
self.neutron_api.delete_port(port['id'])
raise
service_instance = self.compute_api.server_create(
context,
name=instance_name,
image=service_image_id,
flavor=self.get_config_option("service_instance_flavor_id"),
key_name=key_name,
nics=[{'port-id': port['id']} for port in network_data['ports']])
t = time.time()
while time.time() - t < self.max_time_to_build_instance:
# NOTE(vponomaryov): emptiness of 'networks' field is checked as
# workaround for nova/neutron bug #1210483.
if (service_instance['status'] == 'ACTIVE' and
service_instance.get('networks', {})):
break
if service_instance['status'] == 'ERROR':
raise exception.ServiceInstanceException(
_('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.ServiceInstanceException(
_('Instance have not been spawned in %ss. Giving up.') %
self.max_time_to_build_instance)
if security_group:
LOG.debug("Adding security group "
"'%s' to server '%s'." % (security_group.id,
service_instance["id"]))
self.compute_api.add_security_group_to_server(
context,
service_instance["id"], security_group.id)
service_instance['ip'] = self._get_server_ip(service_instance)
service_instance['pk_path'] = key_path
if 'router' in network_data and 'id' in network_data['router']:
service_instance['router_id'] = network_data['router']['id']
service_instance['subnet_id'] = network_data['service_subnet']['id']
service_instance['port_id'] = network_data['service_port']['id']
try:
public_ip = network_data['public_port']
except KeyError:
public_ip = network_data['service_port']
public_ip = public_ip['fixed_ips']
public_ip = public_ip[0]
public_ip = public_ip['ip_address']
service_instance['public_address'] = public_ip
if not self._check_server_availability(service_instance):
raise exception.ServiceInstanceException(
_('SSH connection have not been '
'established in %ss. Giving up.') %
self.max_time_to_build_instance)
return service_instance
def _check_server_availability(self, server):
t = time.time()
while time.time() - t < self.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
@utils.synchronized(
"service_instance_setup_network_for_instance", external=True)
def _setup_network_for_instance(self, neutron_net_id, neutron_subnet_id):
"""Sets up network for service vm."""
network_data = dict()
subnet_name = ('service_subnet_for_handling_of_share_server_for_'
'tenant_subnet_%s' % neutron_subnet_id)
network_data['service_subnet'] = self._get_service_subnet(subnet_name)
if not network_data['service_subnet']:
network_data['service_subnet'] = self.neutron_api.subnet_create(
self.service_tenant_id,
self.service_network_id,
subnet_name,
self._get_cidr_for_subnet())
if not self.connect_share_server_to_tenant_network:
network_data['router'] = self._get_private_router(
neutron_net_id, neutron_subnet_id)
try:
self.neutron_api.router_add_interface(
network_data['router']['id'],
network_data['service_subnet']['id'])
except exception.NetworkException as e:
if e.kwargs['code'] != 400:
raise
LOG.debug('Subnet %(subnet_id)s is already attached to the '
'router %(router_id)s.',
{'subnet_id': network_data['service_subnet']['id'],
'router_id': network_data['router']['id']})
network_data['service_port'] = self.neutron_api.create_port(
self.service_tenant_id,
self.service_network_id,
subnet_id=network_data['service_subnet']['id'],
device_owner='manila')
network_data['ports'] = [network_data['service_port']]
if self.connect_share_server_to_tenant_network:
network_data['public_port'] = self.neutron_api.create_port(
self.service_tenant_id, neutron_net_id,
subnet_id=neutron_subnet_id, device_owner='manila')
network_data['ports'].append(network_data['public_port'])
return network_data
@utils.synchronized("service_instance_get_private_router", external=True)
def _get_private_router(self, neutron_net_id, neutron_subnet_id):
"""Returns router attached to private subnet gateway."""
private_subnet = self.neutron_api.get_subnet(neutron_subnet_id)
if not private_subnet['gateway_ip']:
raise exception.ServiceInstanceException(
_('Subnet must have gateway.'))
private_network_ports = [p for p in self.neutron_api.list_ports(
network_id=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.ServiceInstanceException(
_('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):
"""Sets up connectivity with service instances.
Creates creating port in service network, creating and setting up
required network devices.
"""
port = self._get_service_port()
port = self._add_fixed_ips_to_service_port(port)
interface_name = self.vif_driver.get_device_name(port)
self.vif_driver.plug(interface_name, port['id'], 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._remove_outdated_interfaces(device)
@utils.synchronized(
"service_instance_remove_outdated_interfaces", external=True)
def _remove_outdated_interfaces(self, device):
"""Finds and removes unused network device."""
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)
@utils.synchronized("service_instance_get_service_port", external=True)
def _get_service_port(self):
"""Find or creates service neutron port.
This port 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.ServiceInstanceException(
_('Error. Ambiguous service ports.'))
elif not ports:
try:
stdout, stderr = self._execute('hostname')
host = stdout.strip()
except exception.ProcessExecutionError as e:
msg = _('Unable to get host. %s') % e.stderr
raise exception.ManilaException(msg)
port = self.neutron_api.create_port(
self.service_tenant_id,
self.service_network_id,
device_id='manila-share',
device_owner='manila:share',
host_id=host)
else:
port = ports[0]
return port
@utils.synchronized(
"service_instance_add_fixed_ips_to_service_port", external=True)
def _add_fixed_ips_to_service_port(self, port):
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):
"""Returns not used cidr for service subnet creating."""
subnets = self._get_all_service_subnets()
used_cidrs = set(subnet['cidr'] for subnet in subnets)
serv_cidr = netaddr.IPNetwork(
self.get_config_option("service_network_cidr"))
division_mask = self.get_config_option("service_network_division_mask")
for subnet in serv_cidr.subnet(division_mask):
cidr = str(subnet.cidr)
if cidr not in used_cidrs:
return cidr
else:
raise exception.ServiceInstanceException(_('No available cidrs.'))
def delete_service_instance(self, context, instance_id, subnet_id,
router_id=None):
"""Removes share infrastructure.
Deletes service vm and subnet, associated to share network.
"""
self._delete_server(context, instance_id)
if router_id and subnet_id:
try:
self.neutron_api.router_remove_interface(router_id,
subnet_id)
except exception.NetworkException as e:
if e.kwargs['code'] != 404:
raise
LOG.debug('Subnet %(subnet_id)s is not attached to the '
'router %(router_id)s.',
{'subnet_id': subnet_id,
'router_id': router_id})
self.neutron_api.update_subnet(subnet_id, '')
@utils.synchronized(
"service_instance_get_all_service_subnets", external=True)
def _get_all_service_subnets(self):
service_network = self.neutron_api.get_network(self.service_network_id)
return [self.neutron_api.get_subnet(subnet_id)
for subnet_id in service_network['subnets']]
@utils.synchronized("service_instance_get_service_subnet", external=True)
def _get_service_subnet(self, subnet_name):
all_service_subnets = self._get_all_service_subnets()
service_subnets = [subnet for subnet in all_service_subnets
if subnet['name'] == subnet_name]
if len(service_subnets) == 1:
return service_subnets[0]
elif not service_subnets:
unused_service_subnets = [subnet for subnet in all_service_subnets
if subnet['name'] == '']
if unused_service_subnets:
service_subnet = unused_service_subnets[0]
self.neutron_api.update_subnet(service_subnet['id'],
subnet_name)
return service_subnet
return None
else:
raise exception.ServiceInstanceException(_('Ambiguous service '
'subnets.'))