fe6bd97afe
Manila share manager has possibility to save share servers details even if it failed to be created on some step. To use it, share driver should attach 'detail_data' attr to exception with dict of share server details. But, if such data not provided or has unexpected type, then manager writes "warnings" to LOG. So, make it have "debug" level of logging because here is nothing to do for admin/operator. Also, fix Generic driver's case when it does not provide information when has it - on negative result of share server network availability check. Change-Id: If8926ad383669fd5ccbe69cf13b574bc044f19be Closes-Bug: #1547076
1137 lines
48 KiB
Python
1137 lines
48 KiB
Python
# Copyright (c) 2014 NetApp, Inc.
|
|
# Copyright (c) 2015 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.
|
|
|
|
"""Module for managing nova instances for share drivers."""
|
|
|
|
import abc
|
|
import os
|
|
import socket
|
|
import time
|
|
|
|
import netaddr
|
|
from oslo_config import cfg
|
|
from oslo_log import log
|
|
from oslo_utils import importutils
|
|
import six
|
|
|
|
from manila.common import constants as const
|
|
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 import utils
|
|
|
|
LOG = log.getLogger(__name__)
|
|
NEUTRON_NAME = "neutron"
|
|
NOVA_NAME = "nova"
|
|
|
|
share_servers_handling_mode_opts = [
|
|
cfg.StrOpt(
|
|
"service_image_name",
|
|
default="manila-service-image",
|
|
help="Name of image in Glance, that will be used for service instance "
|
|
"creation. Only used if driver_handles_share_servers=True."),
|
|
cfg.StrOpt(
|
|
"service_instance_name_template",
|
|
default="manila_service_instance_%s",
|
|
help="Name of service instance. "
|
|
"Only used if driver_handles_share_servers=True."),
|
|
cfg.StrOpt(
|
|
"manila_service_keypair_name",
|
|
default="manila-service",
|
|
help="Keypair name that will be created and used for service "
|
|
"instances. Only used if driver_handles_share_servers=True."),
|
|
cfg.StrOpt(
|
|
"path_to_public_key",
|
|
default="~/.ssh/id_rsa.pub",
|
|
help="Path to hosts public key. "
|
|
"Only used if driver_handles_share_servers=True."),
|
|
cfg.StrOpt(
|
|
"service_instance_security_group",
|
|
default="manila-service",
|
|
help="Security group name, that will be used for "
|
|
"service instance creation. "
|
|
"Only used if driver_handles_share_servers=True."),
|
|
cfg.IntOpt(
|
|
"service_instance_flavor_id",
|
|
default=100,
|
|
help="ID of flavor, that will be used for service instance "
|
|
"creation. Only used if driver_handles_share_servers=True."),
|
|
cfg.StrOpt(
|
|
"service_network_name",
|
|
default="manila_service_network",
|
|
help="Name of manila service network. Used only with Neutron. "
|
|
"Only used if driver_handles_share_servers=True."),
|
|
cfg.StrOpt(
|
|
"service_network_cidr",
|
|
default="10.254.0.0/16",
|
|
help="CIDR of manila service network. Used only with Neutron and "
|
|
"if driver_handles_share_servers=True."),
|
|
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. Used only with Neutron "
|
|
"and if driver_handles_share_servers=True."),
|
|
cfg.StrOpt(
|
|
"interface_driver",
|
|
default="manila.network.linux.interface.OVSInterfaceDriver",
|
|
help="Vif driver. Used only with Neutron and "
|
|
"if driver_handles_share_servers=True."),
|
|
cfg.BoolOpt(
|
|
"connect_share_server_to_tenant_network",
|
|
default=False,
|
|
help="Attach share server directly to share network. "
|
|
"Used only with Neutron and "
|
|
"if driver_handles_share_servers=True."),
|
|
cfg.StrOpt(
|
|
"service_instance_network_helper_type",
|
|
default=NEUTRON_NAME,
|
|
help="Allowed values are %s. " % [NOVA_NAME, NEUTRON_NAME] +
|
|
"Only used if driver_handles_share_servers=True."),
|
|
cfg.StrOpt(
|
|
"admin_network_id",
|
|
help="ID of neutron network used to communicate with admin network,"
|
|
" to create additional admin export locations on."),
|
|
cfg.StrOpt(
|
|
"admin_subnet_id",
|
|
help="ID of neutron subnet used to communicate with admin network,"
|
|
" to create additional admin export locations on. "
|
|
"Related to 'admin_network_id'."),
|
|
]
|
|
|
|
no_share_servers_handling_mode_opts = [
|
|
cfg.StrOpt(
|
|
"service_instance_name_or_id",
|
|
help="Name or ID of service instance in Nova to use for share "
|
|
"exports. Used only when share servers handling is disabled."),
|
|
cfg.StrOpt(
|
|
"service_net_name_or_ip",
|
|
help="Can be either name of network that is used by service "
|
|
"instance within Nova to get IP address or IP address itself "
|
|
"for managing shares there. "
|
|
"Used only when share servers handling is disabled."),
|
|
cfg.StrOpt(
|
|
"tenant_net_name_or_ip",
|
|
help="Can be either name of network that is used by service "
|
|
"instance within Nova to get IP address or IP address itself "
|
|
"for exporting shares. "
|
|
"Used only when share servers handling is disabled."),
|
|
]
|
|
|
|
common_opts = [
|
|
cfg.StrOpt(
|
|
"service_instance_user",
|
|
help="User in service instance that will be used for authentication."),
|
|
cfg.StrOpt(
|
|
"service_instance_password",
|
|
secret=True,
|
|
help="Password for service instance user."),
|
|
cfg.StrOpt(
|
|
"path_to_private_key",
|
|
help="Path to host's private key."),
|
|
cfg.IntOpt(
|
|
"max_time_to_build_instance",
|
|
default=300,
|
|
help="Maximum time in seconds to wait for creating service instance."),
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
|
|
|
|
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.
|
|
"""
|
|
_INSTANCE_CONNECTION_PROTO = "SSH"
|
|
|
|
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.
|
|
"""
|
|
if self.driver_config:
|
|
return self.driver_config.safe_get(key)
|
|
return CONF.get(key)
|
|
|
|
def _get_network_helper(self):
|
|
network_helper_type = (
|
|
self.get_config_option(
|
|
"service_instance_network_helper_type").lower())
|
|
if network_helper_type == NEUTRON_NAME:
|
|
return NeutronNetworkHelper(self)
|
|
elif network_helper_type == NOVA_NAME:
|
|
return NovaNetworkHelper(self)
|
|
else:
|
|
raise exception.ManilaException(
|
|
_("Wrong value '%(provided)s' for config opt "
|
|
"'service_instance_network_helper_type'. "
|
|
"Allowed values are %(allowed)s.") % dict(
|
|
provided=network_helper_type,
|
|
allowed=[NOVA_NAME, NEUTRON_NAME]))
|
|
|
|
def __init__(self, driver_config=None):
|
|
super(ServiceInstanceManager, self).__init__()
|
|
self.driver_config = driver_config
|
|
|
|
if self.driver_config:
|
|
self.driver_config.append_config_values(common_opts)
|
|
if self.get_config_option("driver_handles_share_servers"):
|
|
self.driver_config.append_config_values(
|
|
share_servers_handling_mode_opts)
|
|
else:
|
|
self.driver_config.append_config_values(
|
|
no_share_servers_handling_mode_opts)
|
|
else:
|
|
CONF.register_opts(common_opts)
|
|
if self.get_config_option("driver_handles_share_servers"):
|
|
CONF.register_opts(share_servers_handling_mode_opts)
|
|
else:
|
|
CONF.register_opts(no_share_servers_handling_mode_opts)
|
|
|
|
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.path_to_private_key = self.get_config_option(
|
|
"path_to_private_key")
|
|
self.max_time_to_build_instance = self.get_config_option(
|
|
"max_time_to_build_instance")
|
|
|
|
if self.get_config_option("driver_handles_share_servers"):
|
|
self.path_to_public_key = self.get_config_option(
|
|
"path_to_public_key")
|
|
self._network_helper = None
|
|
|
|
@property
|
|
@utils.synchronized("instantiate_network_helper")
|
|
def network_helper(self):
|
|
if not self._network_helper:
|
|
self._network_helper = self._get_network_helper()
|
|
self._network_helper.setup_connectivity_with_service_instances()
|
|
return self._network_helper
|
|
|
|
def get_common_server(self):
|
|
data = {
|
|
'public_address': None,
|
|
'private_address': None,
|
|
'service_net_name_or_ip': self.get_config_option(
|
|
'service_net_name_or_ip'),
|
|
'tenant_net_name_or_ip': self.get_config_option(
|
|
'tenant_net_name_or_ip'),
|
|
}
|
|
|
|
data['instance'] = self.compute_api.server_get_by_name_or_id(
|
|
self.admin_context,
|
|
self.get_config_option('service_instance_name_or_id'))
|
|
|
|
if netaddr.valid_ipv4(data['service_net_name_or_ip']):
|
|
data['private_address'] = [data['service_net_name_or_ip']]
|
|
else:
|
|
data['private_address'] = self._get_addresses_by_network_name(
|
|
data['service_net_name_or_ip'], data['instance'])
|
|
|
|
if netaddr.valid_ipv4(data['tenant_net_name_or_ip']):
|
|
data['public_address'] = [data['tenant_net_name_or_ip']]
|
|
else:
|
|
data['public_address'] = self._get_addresses_by_network_name(
|
|
data['tenant_net_name_or_ip'], data['instance'])
|
|
|
|
if not (data['public_address'] and data['private_address']):
|
|
raise exception.ManilaException(
|
|
"Can not find one of net addresses for service instance. "
|
|
"Instance: %(instance)s, "
|
|
"private_address: %(private_address)s, "
|
|
"public_address: %(public_address)s." % data)
|
|
|
|
share_server = {
|
|
'username': self.get_config_option('service_instance_user'),
|
|
'password': self.get_config_option('service_instance_password'),
|
|
'pk_path': self.path_to_private_key,
|
|
'instance_id': data['instance']['id'],
|
|
}
|
|
for key in ('private_address', 'public_address'):
|
|
data[key + '_v4'] = None
|
|
for address in data[key]:
|
|
if netaddr.valid_ipv4(address):
|
|
data[key + '_v4'] = address
|
|
break
|
|
share_server['ip'] = data['private_address_v4']
|
|
share_server['public_address'] = data['public_address_v4']
|
|
return {'backend_details': share_server}
|
|
|
|
def _get_addresses_by_network_name(self, net_name, server):
|
|
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]]
|
|
return net_ips
|
|
|
|
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, net_name):
|
|
"""Returns service IP address of service instance."""
|
|
net_ips = self._get_addresses_by_network_name(net_name, server)
|
|
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 const.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."""
|
|
if 'instance_id' not in server:
|
|
LOG.warning(_LW("Unable to check server existence since "
|
|
"'instance_id' key is not set in share server "
|
|
"backend details."))
|
|
return False
|
|
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 "
|
|
"successfully.", 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, network_info):
|
|
"""Finds or creates and sets up service vm.
|
|
|
|
:param context: defines context, that should be used
|
|
:param network_info: network info for getting allocations
|
|
:returns: dict with service instance details
|
|
:raises: exception.ServiceInstanceException
|
|
"""
|
|
instance_name = network_info['server_id']
|
|
server = self._create_service_instance(
|
|
context, instance_name, network_info)
|
|
instance_details = self._get_new_instance_details(server)
|
|
|
|
if not self._check_server_availability(instance_details):
|
|
e = exception.ServiceInstanceException(
|
|
_('%(conn_proto)s connection has not been '
|
|
'established to %(server)s in %(time)ss. Giving up.') % {
|
|
'conn_proto': self._INSTANCE_CONNECTION_PROTO,
|
|
'server': server['ip'],
|
|
'time': self.max_time_to_build_instance})
|
|
e.detail_data = {'server_details': instance_details}
|
|
raise e
|
|
|
|
return instance_details
|
|
|
|
def _get_new_instance_details(self, server):
|
|
instance_details = {
|
|
'instance_id': server['id'],
|
|
'ip': server['ip'],
|
|
'pk_path': server.get('pk_path'),
|
|
'subnet_id': server.get('subnet_id'),
|
|
'password': self.get_config_option('service_instance_password'),
|
|
'username': self.get_config_option('service_instance_user'),
|
|
'public_address': server['public_address'],
|
|
}
|
|
if server.get('admin_ip'):
|
|
instance_details['admin_ip'] = server['admin_ip']
|
|
if server.get('router_id'):
|
|
instance_details['router_id'] = server['router_id']
|
|
if server.get('service_port_id'):
|
|
instance_details['service_port_id'] = server['service_port_id']
|
|
if server.get('public_port_id'):
|
|
instance_details['public_port_id'] = server['public_port_id']
|
|
if server.get('admin_port_id'):
|
|
instance_details['admin_port_id'] = server['admin_port_id']
|
|
|
|
for key in ('password', 'pk_path', 'subnet_id'):
|
|
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(
|
|
_("Image with name '%s' not found.") % service_image_name)
|
|
else:
|
|
raise exception.ServiceInstanceException(
|
|
_("Found more than one image by name '%s'.") %
|
|
service_image_name)
|
|
|
|
def _create_service_instance(self, context, instance_name, network_info):
|
|
"""Creates service vm and sets up networking for it."""
|
|
service_image_id = self._get_service_image(context)
|
|
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:
|
|
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.'), dict(
|
|
private_path=self.path_to_private_key,
|
|
public_path=self.path_to_public_key))
|
|
network_data = self.network_helper.setup_network(network_info)
|
|
fail_safe_data = dict(
|
|
router_id=network_data.get('router_id'),
|
|
subnet_id=network_data.get('subnet_id'))
|
|
if network_data.get('service_port'):
|
|
fail_safe_data['service_port_id'] = (
|
|
network_data['service_port']['id'])
|
|
if network_data.get('public_port'):
|
|
fail_safe_data['public_port_id'] = (
|
|
network_data['public_port']['id'])
|
|
if network_data.get('admin_port'):
|
|
fail_safe_data['admin_port_id'] = (
|
|
network_data['admin_port']['id'])
|
|
try:
|
|
create_kwargs = self._get_service_instance_create_kwargs()
|
|
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=network_data['nics'],
|
|
availability_zone=CONF.storage_availability_zone,
|
|
**create_kwargs)
|
|
|
|
fail_safe_data['instance_id'] = service_instance['id']
|
|
|
|
service_instance = self.wait_for_instance_to_be_active(
|
|
service_instance['id'],
|
|
self.max_time_to_build_instance)
|
|
|
|
security_group = self._get_or_create_security_group(context)
|
|
if security_group:
|
|
if self.network_helper.NAME == NOVA_NAME:
|
|
# NOTE(vponomaryov): Nova-network allows to assign
|
|
# secgroups only by names.
|
|
sg_id = security_group.name
|
|
else:
|
|
sg_id = security_group.id
|
|
LOG.debug(
|
|
"Adding security group '%(sg)s' to server '%(si)s'.",
|
|
dict(sg=sg_id, si=service_instance["id"]))
|
|
self.compute_api.add_security_group_to_server(
|
|
context, service_instance["id"], sg_id)
|
|
|
|
if self.network_helper.NAME == NEUTRON_NAME:
|
|
ip = (network_data.get('service_port',
|
|
network_data.get(
|
|
'admin_port'))['fixed_ips'])
|
|
service_instance['ip'] = ip[0]['ip_address']
|
|
public_ip = (network_data.get(
|
|
'public_port', network_data.get(
|
|
'service_port'))['fixed_ips'])
|
|
service_instance['public_address'] = public_ip[0]['ip_address']
|
|
else:
|
|
net_name = self.network_helper.get_network_name(network_info)
|
|
service_instance['ip'] = self._get_server_ip(
|
|
service_instance, net_name)
|
|
service_instance['public_address'] = service_instance['ip']
|
|
|
|
except Exception as e:
|
|
e.detail_data = {'server_details': fail_safe_data}
|
|
raise
|
|
|
|
service_instance.update(fail_safe_data)
|
|
service_instance['pk_path'] = key_path
|
|
for pair in [('router', 'router_id'), ('service_subnet', 'subnet_id')]:
|
|
if pair[0] in network_data and 'id' in network_data[pair[0]]:
|
|
service_instance[pair[1]] = network_data[pair[0]]['id']
|
|
|
|
admin_port = network_data.get('admin_port')
|
|
if admin_port:
|
|
try:
|
|
service_instance['admin_ip'] = (
|
|
admin_port['fixed_ips'][0]['ip_address'])
|
|
except Exception:
|
|
msg = _("Admin port is being used but Admin IP was not found.")
|
|
LOG.exception(msg)
|
|
raise exception.AdminIPNotFound(reason=msg)
|
|
|
|
return service_instance
|
|
|
|
def _get_service_instance_create_kwargs(self):
|
|
"""Specify extra arguments used when creating the service instance.
|
|
|
|
Classes inheriting the service instance manager can use this to easily
|
|
pass extra arguments such as user data or metadata.
|
|
"""
|
|
return {}
|
|
|
|
def _check_server_availability(self, instance_details):
|
|
t = time.time()
|
|
while time.time() - t < self.max_time_to_build_instance:
|
|
LOG.debug('Checking server availability.')
|
|
if not self._test_server_connection(instance_details):
|
|
time.sleep(5)
|
|
else:
|
|
return True
|
|
return False
|
|
|
|
def _test_server_connection(self, server):
|
|
try:
|
|
socket.socket().connect((server['ip'], 22))
|
|
LOG.debug('Server %s is available via SSH.',
|
|
server['ip'])
|
|
return True
|
|
except socket.error as e:
|
|
LOG.debug(e)
|
|
LOG.debug("Server %s is not available via SSH. Waiting...",
|
|
server['ip'])
|
|
return False
|
|
|
|
def delete_service_instance(self, context, server_details):
|
|
"""Removes share infrastructure.
|
|
|
|
Deletes service vm and subnet, associated to share network.
|
|
"""
|
|
instance_id = server_details.get("instance_id")
|
|
self._delete_server(context, instance_id)
|
|
self.network_helper.teardown_network(server_details)
|
|
|
|
def wait_for_instance_to_be_active(self, instance_id, timeout):
|
|
t = time.time()
|
|
while time.time() - t < timeout:
|
|
try:
|
|
service_instance = self.compute_api.server_get(
|
|
self.admin_context,
|
|
instance_id)
|
|
except exception.InstanceNotFound as e:
|
|
LOG.debug(e)
|
|
time.sleep(1)
|
|
continue
|
|
|
|
instance_status = service_instance['status']
|
|
# NOTE(vponomaryov): emptiness of 'networks' field checked as
|
|
# workaround for nova/neutron bug #1210483.
|
|
if (instance_status == 'ACTIVE' and
|
|
service_instance.get('networks', {})):
|
|
return service_instance
|
|
elif service_instance['status'] == 'ERROR':
|
|
break
|
|
|
|
LOG.debug("Waiting for instance %(instance_id)s to be active. "
|
|
"Current status: %(instance_status)s." %
|
|
dict(instance_id=instance_id,
|
|
instance_status=instance_status))
|
|
time.sleep(1)
|
|
raise exception.ServiceInstanceException(
|
|
_("Instance %(instance_id)s failed to reach active state "
|
|
"in %(timeout)s seconds. "
|
|
"Current status: %(instance_status)s.") %
|
|
dict(instance_id=instance_id,
|
|
timeout=timeout,
|
|
instance_status=instance_status))
|
|
|
|
def reboot_server(self, server, soft_reboot=False):
|
|
self.compute_api.server_reboot(self.admin_context,
|
|
server['instance_id'],
|
|
soft_reboot)
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class BaseNetworkhelper(object):
|
|
|
|
@abc.abstractproperty
|
|
def NAME(self):
|
|
"""Returns code name of network helper."""
|
|
|
|
@abc.abstractmethod
|
|
def __init__(self, service_instance_manager):
|
|
"""Instantiates class and its attrs."""
|
|
|
|
@abc.abstractmethod
|
|
def get_network_name(self, network_info):
|
|
"""Returns name of network for service instance."""
|
|
|
|
@abc.abstractmethod
|
|
def setup_connectivity_with_service_instances(self):
|
|
"""Sets up connectivity between Manila host and service instances."""
|
|
|
|
@abc.abstractmethod
|
|
def setup_network(self, network_info):
|
|
"""Sets up network for service instance."""
|
|
|
|
@abc.abstractmethod
|
|
def teardown_network(self, server_details):
|
|
"""Teardowns network resources provided for service instance."""
|
|
|
|
|
|
class NeutronNetworkHelper(BaseNetworkhelper):
|
|
|
|
def __init__(self, service_instance_manager):
|
|
self.get_config_option = service_instance_manager.get_config_option
|
|
self.vif_driver = importutils.import_class(
|
|
self.get_config_option("interface_driver"))()
|
|
|
|
if service_instance_manager.driver_config:
|
|
self._network_config_group = (
|
|
service_instance_manager.driver_config.network_config_group or
|
|
service_instance_manager.driver_config.config_group)
|
|
else:
|
|
self._network_config_group = None
|
|
|
|
self.use_admin_port = False
|
|
self.use_service_network = True
|
|
self._neutron_api = None
|
|
self._service_network_id = None
|
|
self.connect_share_server_to_tenant_network = (
|
|
self.get_config_option('connect_share_server_to_tenant_network'))
|
|
|
|
self.admin_network_id = self.get_config_option('admin_network_id')
|
|
self.admin_subnet_id = self.get_config_option('admin_subnet_id')
|
|
|
|
if self.admin_network_id and self.admin_subnet_id:
|
|
self.use_admin_port = True
|
|
if self.use_admin_port and self.connect_share_server_to_tenant_network:
|
|
self.use_service_network = False
|
|
|
|
@property
|
|
def NAME(self):
|
|
return NEUTRON_NAME
|
|
|
|
@property
|
|
def admin_project_id(self):
|
|
return self.neutron_api.admin_project_id
|
|
|
|
@property
|
|
@utils.synchronized("instantiate_neutron_api_neutron_net_helper")
|
|
def neutron_api(self):
|
|
if not self._neutron_api:
|
|
self._neutron_api = neutron.API(
|
|
config_group_name=self._network_config_group)
|
|
return self._neutron_api
|
|
|
|
@property
|
|
@utils.synchronized("service_network_id_neutron_net_helper")
|
|
def service_network_id(self):
|
|
if not self._service_network_id:
|
|
self._service_network_id = self._get_service_network_id()
|
|
return self._service_network_id
|
|
|
|
def get_network_name(self, network_info):
|
|
"""Returns name of network for service instance."""
|
|
net = self.neutron_api.get_network(network_info['neutron_net_id'])
|
|
return net['name']
|
|
|
|
@utils.synchronized("service_instance_get_service_network", external=True)
|
|
def _get_service_network_id(self):
|
|
"""Finds existing or creates new service network."""
|
|
service_network_name = self.get_config_option("service_network_name")
|
|
networks = []
|
|
for network in self.neutron_api.get_all_admin_project_networks():
|
|
if network['name'] == service_network_name:
|
|
networks.append(network)
|
|
if len(networks) > 1:
|
|
raise exception.ServiceInstanceException(
|
|
_('Ambiguous service networks.'))
|
|
elif not networks:
|
|
return self.neutron_api.network_create(
|
|
self.admin_project_id, service_network_name)['id']
|
|
else:
|
|
return networks[0]['id']
|
|
|
|
@utils.synchronized(
|
|
"service_instance_setup_and_teardown_network_for_instance",
|
|
external=True)
|
|
def teardown_network(self, server_details):
|
|
subnet_id = server_details.get("subnet_id")
|
|
router_id = server_details.get("router_id")
|
|
|
|
service_port_id = server_details.get("service_port_id")
|
|
public_port_id = server_details.get("public_port_id")
|
|
admin_port_id = server_details.get("admin_port_id")
|
|
for port_id in (service_port_id, public_port_id, admin_port_id):
|
|
if port_id:
|
|
try:
|
|
self.neutron_api.delete_port(port_id)
|
|
except exception.NetworkException as e:
|
|
if e.kwargs.get('code') != 404:
|
|
raise
|
|
LOG.debug("Failed to delete port %(port_id)s with error: "
|
|
"\n %(exc)s", {"port_id": port_id, "exc": e})
|
|
|
|
if router_id and subnet_id:
|
|
ports = self.neutron_api.list_ports(
|
|
fields=['fixed_ips', 'device_id', 'device_owner'])
|
|
# NOTE(vponomaryov): iterate ports to get to know whether current
|
|
# subnet is used or not. We will not remove it from router if it
|
|
# is used.
|
|
for port in ports:
|
|
# NOTE(vponomaryov): if device_id is present, then we know that
|
|
# this port is used. Also, if device owner is 'compute:*', then
|
|
# we know that it is VM. We continue only if both are 'True'.
|
|
if (port['device_id'] and
|
|
port['device_owner'].startswith('compute:')):
|
|
for fixed_ip in port['fixed_ips']:
|
|
if fixed_ip['subnet_id'] == subnet_id:
|
|
# NOTE(vponomaryov): There are other share servers
|
|
# exist that use this subnet. So, do not remove it
|
|
# from router.
|
|
return
|
|
try:
|
|
# NOTE(vponomaryov): there is no other share servers or
|
|
# some VMs that use this subnet. So, remove it from router.
|
|
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_setup_and_teardown_network_for_instance",
|
|
external=True)
|
|
def setup_network(self, network_info):
|
|
neutron_net_id = network_info['neutron_net_id']
|
|
neutron_subnet_id = network_info['neutron_subnet_id']
|
|
network_data = dict()
|
|
subnet_name = ('service_subnet_for_handling_of_share_server_for_'
|
|
'tenant_subnet_%s' % neutron_subnet_id)
|
|
|
|
if self.use_service_network:
|
|
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.admin_project_id, self.service_network_id,
|
|
subnet_name, self._get_cidr_for_subnet()))
|
|
|
|
network_data['ports'] = []
|
|
|
|
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']})
|
|
else:
|
|
network_data['public_port'] = self.neutron_api.create_port(
|
|
self.admin_project_id, neutron_net_id,
|
|
subnet_id=neutron_subnet_id, device_owner='manila')
|
|
network_data['ports'].append(network_data['public_port'])
|
|
|
|
if self.use_service_network:
|
|
network_data['service_port'] = self.neutron_api.create_port(
|
|
self.admin_project_id, self.service_network_id,
|
|
subnet_id=network_data['service_subnet']['id'],
|
|
device_owner='manila')
|
|
network_data['ports'].append(network_data['service_port'])
|
|
|
|
if self.use_admin_port:
|
|
network_data['admin_port'] = self.neutron_api.create_port(
|
|
self.admin_project_id, self.admin_network_id,
|
|
subnet_id=self.admin_subnet_id, device_owner='manila')
|
|
network_data['ports'].append(network_data['admin_port'])
|
|
|
|
try:
|
|
self.setup_connectivity_with_service_instances()
|
|
except Exception:
|
|
for port in network_data['ports']:
|
|
self.neutron_api.delete_port(port['id'])
|
|
raise
|
|
|
|
network_data['nics'] = [
|
|
{'port-id': port['id']} for port in network_data['ports']]
|
|
public_ip = network_data.get(
|
|
'public_port', network_data.get('service_port'))
|
|
network_data['ip_address'] = public_ip['fixed_ips'][0]['ip_address']
|
|
|
|
return network_data
|
|
|
|
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 = six.text_type(subnet.cidr)
|
|
if cidr not in used_cidrs:
|
|
return cidr
|
|
else:
|
|
raise exception.ServiceInstanceException(_('No available cidrs.'))
|
|
|
|
def setup_connectivity_with_service_instances(self):
|
|
"""Sets up connectivity with service instances.
|
|
|
|
Creates host port in service network and/or admin network, creating
|
|
and setting up required network devices.
|
|
"""
|
|
if self.use_service_network:
|
|
port = self._get_service_port(
|
|
self.service_network_id, None, 'manila-share')
|
|
port = self._add_fixed_ips_to_service_port(port)
|
|
interface_name = self.vif_driver.get_device_name(port)
|
|
device = ip_lib.IPDevice(interface_name)
|
|
self._plug_interface_in_host(interface_name, device, port)
|
|
|
|
if self.use_admin_port:
|
|
port = self._get_service_port(
|
|
self.admin_network_id, self.admin_subnet_id,
|
|
'manila-admin-share')
|
|
interface_name = self.vif_driver.get_device_name(port)
|
|
device = ip_lib.IPDevice(interface_name)
|
|
for fixed_ip in port['fixed_ips']:
|
|
subnet = self.neutron_api.get_subnet(fixed_ip['subnet_id'])
|
|
device.route.clear_outdated_routes(subnet['cidr'])
|
|
self._plug_interface_in_host(interface_name, device, port)
|
|
|
|
def _plug_interface_in_host(self, interface_name, device, 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.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."""
|
|
device_cidr_set = self._get_set_of_device_cidrs(device)
|
|
for dev in ip_lib.IPWrapper().get_devices():
|
|
if dev.name != device.name and dev.name[:3] == device.name[:3]:
|
|
cidr_set = self._get_set_of_device_cidrs(dev)
|
|
if device_cidr_set & cidr_set:
|
|
self.vif_driver.unplug(dev.name)
|
|
|
|
def _get_set_of_device_cidrs(self, device):
|
|
cidrs = set()
|
|
addr_list = []
|
|
try:
|
|
# NOTE(ganso): I could call ip_lib.device_exists here, but since
|
|
# this is a concurrency problem, it would not fix the problem.
|
|
addr_list = device.addr.list()
|
|
except Exception as e:
|
|
if 'does not exist' in six.text_type(e):
|
|
LOG.warning(_LW(
|
|
"Device %s does not exist anymore.") % device.name)
|
|
else:
|
|
raise
|
|
for addr in addr_list:
|
|
if addr['ip_version'] == 4:
|
|
cidrs.add(six.text_type(netaddr.IPNetwork(addr['cidr']).cidr))
|
|
return cidrs
|
|
|
|
@utils.synchronized("service_instance_get_service_port", external=True)
|
|
def _get_service_port(self, network_id, subnet_id, device_id):
|
|
"""Find or creates service neutron port.
|
|
|
|
This port will be used for connectivity with service instances.
|
|
"""
|
|
host = socket.gethostname()
|
|
search_opts = {'device_id': device_id,
|
|
'binding:host_id': host}
|
|
ports = [port for port in self.neutron_api.
|
|
list_ports(**search_opts)]
|
|
if len(ports) > 1:
|
|
raise exception.ServiceInstanceException(
|
|
_('Error. Ambiguous service ports.'))
|
|
elif not ports:
|
|
port = self.neutron_api.create_port(
|
|
self.admin_project_id, network_id, subnet_id=subnet_id,
|
|
device_id=device_id, 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
|
|
|
|
@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 to the router.'))
|
|
private_subnet_router = self.neutron_api.show_router(
|
|
private_subnet_gateway_port['device_id'])
|
|
return private_subnet_router
|
|
|
|
@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.'))
|
|
|
|
@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)
|
|
subnets = []
|
|
for subnet_id in service_network['subnets']:
|
|
subnets.append(self.neutron_api.get_subnet(subnet_id))
|
|
return subnets
|
|
|
|
|
|
class NovaNetworkHelper(BaseNetworkhelper):
|
|
"""Nova network helper for Manila service instances.
|
|
|
|
All security-group rules are applied to all interfaces of Nova VM
|
|
using Nova-network. In that case there is no need to create additional
|
|
service network. Only one thing should be satisfied - Manila host
|
|
should have access to all tenant networks.
|
|
This network helper does not create resources.
|
|
"""
|
|
|
|
def __init__(self, service_instance_manager):
|
|
self.compute_api = service_instance_manager.compute_api
|
|
self.admin_context = service_instance_manager.admin_context
|
|
|
|
@property
|
|
def NAME(self):
|
|
return NOVA_NAME
|
|
|
|
def setup_network(self, network_info):
|
|
net = self._get_nova_network(network_info['nova_net_id'])
|
|
network_info['nics'] = [{'net-id': net['id']}]
|
|
return network_info
|
|
|
|
def get_network_name(self, network_info):
|
|
"""Returns name of network for service instance."""
|
|
return self._get_nova_network(network_info['nova_net_id'])['label']
|
|
|
|
def teardown_network(self, server_details):
|
|
"""Nothing to do. Placeholder."""
|
|
|
|
def setup_connectivity_with_service_instances(self):
|
|
"""Nothing to do. Placeholder."""
|
|
|
|
def _get_nova_network(self, nova_network_id):
|
|
"""Returns network to be used for service instance.
|
|
|
|
:param nova_network_id: string with id of network.
|
|
:returns: dict -- network data as dict
|
|
:raises: exception.ManilaException
|
|
"""
|
|
if not nova_network_id:
|
|
raise exception.ManilaException(
|
|
_('Nova network for service instance is not provided.'))
|
|
net = self.compute_api.network_get(self.admin_context, nova_network_id)
|
|
return net
|