483 lines
19 KiB
Python
483 lines
19 KiB
Python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import ast
|
|
import time
|
|
|
|
from heatclient import client as heat_client
|
|
from heatclient import exc as heat_exc
|
|
from keystoneclient.v2_0 import client as keyclient
|
|
from neutron._i18n import _LE
|
|
from neutron._i18n import _LW
|
|
from neutron.db import model_base
|
|
from neutron import manager
|
|
from neutron.plugins.common import constants as pconst
|
|
from oslo_config import cfg
|
|
from oslo_log import helpers as log
|
|
from oslo_log import log as logging
|
|
from oslo_serialization import jsonutils
|
|
import sqlalchemy as sa
|
|
|
|
from gbpservice.neutron.services.servicechain.common import exceptions as exc
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
cfg.CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token')
|
|
|
|
service_chain_opts = [
|
|
cfg.IntOpt('stack_delete_retries',
|
|
default=5,
|
|
help=_("Number of attempts to retry for stack deletion")),
|
|
cfg.IntOpt('stack_delete_retry_wait',
|
|
default=3,
|
|
help=_("Wait time between two successive stack delete "
|
|
"retries")),
|
|
cfg.StrOpt('heat_uri',
|
|
default='http://localhost:8004/v1',
|
|
help=_("Heat server address to create services "
|
|
"specified in the service chain.")),
|
|
cfg.StrOpt('heat_ca_certificates_file', default=None,
|
|
help=_('CA file for heatclient to verify server certificates')),
|
|
cfg.BoolOpt('heat_api_insecure', default=False,
|
|
help=_("If True, ignore any SSL validation issues")),
|
|
]
|
|
|
|
|
|
cfg.CONF.register_opts(service_chain_opts, "simplechain")
|
|
|
|
# Service chain API supported Values
|
|
sc_supported_type = [pconst.LOADBALANCER, pconst.FIREWALL]
|
|
STACK_DELETE_RETRIES = cfg.CONF.simplechain.stack_delete_retries
|
|
STACK_DELETE_RETRY_WAIT = cfg.CONF.simplechain.stack_delete_retry_wait
|
|
|
|
|
|
class ServiceChainInstanceStack(model_base.BASEV2):
|
|
"""ServiceChainInstance stacks owned by the servicechain driver."""
|
|
|
|
__tablename__ = 'sc_instance_stacks'
|
|
instance_id = sa.Column(sa.String(36),
|
|
nullable=False, primary_key=True)
|
|
stack_id = sa.Column(sa.String(36),
|
|
nullable=False, primary_key=True)
|
|
|
|
|
|
class SimpleChainDriver(object):
|
|
|
|
@log.log_method_call
|
|
def initialize(self):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def create_servicechain_node_precommit(self, context):
|
|
if context.current['service_profile_id'] is None:
|
|
if context.current['service_type'] not in sc_supported_type:
|
|
raise exc.InvalidServiceTypeForReferenceDriver()
|
|
elif context.current['service_type']:
|
|
LOG.warning(_LW('Both service_profile_id and service_type are'
|
|
'specified, service_type will be ignored.'))
|
|
|
|
@log.log_method_call
|
|
def create_servicechain_node_postcommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def update_servicechain_node_precommit(self, context):
|
|
if (context.original['config'] != context.current['config']):
|
|
filters = {'servicechain_spec': context.original[
|
|
'servicechain_specs']}
|
|
sc_instances = context._plugin.get_servicechain_instances(
|
|
context._plugin_context, filters)
|
|
if sc_instances:
|
|
raise exc.NodeUpdateNotSupported()
|
|
|
|
@log.log_method_call
|
|
def update_servicechain_node_postcommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def delete_servicechain_node_precommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def delete_servicechain_node_postcommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def create_servicechain_spec_precommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def create_servicechain_spec_postcommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def update_servicechain_spec_precommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def update_servicechain_spec_postcommit(self, context):
|
|
if context.original['nodes'] != context.current['nodes']:
|
|
filters = {'servicechain_spec': [context.original['id']]}
|
|
sc_instances = context._plugin.get_servicechain_instances(
|
|
context._plugin_context, filters)
|
|
for sc_instance in sc_instances:
|
|
self._update_servicechain_instance(context,
|
|
sc_instance,
|
|
context._sc_spec)
|
|
|
|
@log.log_method_call
|
|
def delete_servicechain_spec_precommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def delete_servicechain_spec_postcommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def create_servicechain_instance_precommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def create_servicechain_instance_postcommit(self, context):
|
|
sc_instance = context.current
|
|
sc_spec_ids = sc_instance.get('servicechain_specs')
|
|
for sc_spec_id in sc_spec_ids:
|
|
sc_spec = context._plugin.get_servicechain_spec(
|
|
context._plugin_context, sc_spec_id)
|
|
sc_node_ids = sc_spec.get('nodes')
|
|
self._create_servicechain_instance_stacks(context, sc_node_ids,
|
|
sc_instance, sc_spec)
|
|
|
|
@log.log_method_call
|
|
def update_servicechain_instance_precommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def update_servicechain_instance_postcommit(self, context):
|
|
original_spec_ids = context.original.get('servicechain_specs')
|
|
new_spec_ids = context.current.get('servicechain_specs')
|
|
if set(original_spec_ids) != set(new_spec_ids):
|
|
for new_spec_id in new_spec_ids:
|
|
newspec = context._plugin.get_servicechain_spec(
|
|
context._plugin_context, new_spec_id)
|
|
self._update_servicechain_instance(context, context.current,
|
|
newspec)
|
|
|
|
@log.log_method_call
|
|
def delete_servicechain_instance_precommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def delete_servicechain_instance_postcommit(self, context):
|
|
self._delete_servicechain_instance_stacks(context._plugin_context,
|
|
context.current['id'])
|
|
|
|
@log.log_method_call
|
|
def create_service_profile_precommit(self, context):
|
|
if context.current['service_type'] not in sc_supported_type:
|
|
raise exc.InvalidServiceTypeForReferenceDriver()
|
|
|
|
@log.log_method_call
|
|
def create_service_profile_postcommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def update_service_profile_precommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def update_service_profile_postcommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def delete_service_profile_precommit(self, context):
|
|
pass
|
|
|
|
@log.log_method_call
|
|
def delete_service_profile_postcommit(self, context):
|
|
pass
|
|
|
|
def _get_ptg(self, context, ptg_id):
|
|
return self._get_resource(self._grouppolicy_plugin,
|
|
context._plugin_context,
|
|
'policy_target_group',
|
|
ptg_id)
|
|
|
|
def _get_pt(self, context, pt_id):
|
|
return self._get_resource(self._grouppolicy_plugin,
|
|
context._plugin_context,
|
|
'policy_target',
|
|
pt_id)
|
|
|
|
def _get_port(self, context, port_id):
|
|
return self._get_resource(self._core_plugin,
|
|
context._plugin_context,
|
|
'port',
|
|
port_id)
|
|
|
|
def _get_ptg_subnet(self, context, ptg_id):
|
|
ptg = self._get_ptg(context, ptg_id)
|
|
return ptg.get("subnets")[0]
|
|
|
|
def _get_member_ips(self, context, ptg_id):
|
|
ptg = self._get_ptg(context, ptg_id)
|
|
pt_ids = ptg.get("policy_targets")
|
|
member_addresses = []
|
|
for pt_id in pt_ids:
|
|
pt = self._get_pt(context, pt_id)
|
|
port_id = pt.get("port_id")
|
|
port = self._get_port(context, port_id)
|
|
ipAddress = port.get('fixed_ips')[0].get("ip_address")
|
|
member_addresses.append(ipAddress)
|
|
return member_addresses
|
|
|
|
def _fetch_template_and_params(self, context, sc_instance,
|
|
sc_spec, sc_node):
|
|
stack_template = sc_node.get('config')
|
|
# TODO(magesh):Raise an exception ??
|
|
if not stack_template:
|
|
LOG.error(_LE("Service Config is not defined for the service"
|
|
" chain Node"))
|
|
return
|
|
stack_template = jsonutils.loads(stack_template)
|
|
config_param_values = sc_instance.get('config_param_values', {})
|
|
stack_params = {}
|
|
# config_param_values has the parameters for all Nodes. Only apply
|
|
# the ones relevant for this Node
|
|
if config_param_values:
|
|
config_param_values = jsonutils.loads(config_param_values)
|
|
config_param_names = sc_spec.get('config_param_names', [])
|
|
if config_param_names:
|
|
config_param_names = ast.literal_eval(config_param_names)
|
|
|
|
# This service chain driver knows how to fill in two parameter values
|
|
# for the template at present.
|
|
# 1)Subnet -> Provider PTG subnet is used
|
|
# 2)PoolMemberIPs -> List of IP Addresses of all PTs in Provider PTG
|
|
|
|
# TODO(magesh):Process on the basis of ResourceType rather than Name
|
|
# eg: Type: OS::Neutron::PoolMember
|
|
# Variable number of pool members is not handled yet. We may have to
|
|
# dynamically modify the template json to achieve that
|
|
member_ips = []
|
|
provider_ptg_id = sc_instance.get("provider_ptg_id")
|
|
# If we have the key "PoolMemberIP*" in template input parameters,
|
|
# fetch the list of IPs of all PTs in the PTG
|
|
for key in config_param_names or []:
|
|
if "PoolMemberIP" in key:
|
|
member_ips = self._get_member_ips(context, provider_ptg_id)
|
|
break
|
|
|
|
member_count = 0
|
|
for key in config_param_names or []:
|
|
if "PoolMemberIP" in key:
|
|
value = (member_ips[member_count]
|
|
if len(member_ips) > member_count else '0')
|
|
member_count = member_count + 1
|
|
config_param_values[key] = value
|
|
elif key == "Subnet":
|
|
value = self._get_ptg_subnet(context, provider_ptg_id)
|
|
config_param_values[key] = value
|
|
node_params = (stack_template.get('Parameters')
|
|
or stack_template.get('parameters'))
|
|
if node_params:
|
|
for parameter in config_param_values.keys():
|
|
if parameter in node_params.keys():
|
|
stack_params[parameter] = config_param_values[parameter]
|
|
return (stack_template, stack_params)
|
|
|
|
def _create_servicechain_instance_stacks(self, context, sc_node_ids,
|
|
sc_instance, sc_spec):
|
|
heatclient = HeatClient(context._plugin_context)
|
|
for sc_node_id in sc_node_ids:
|
|
sc_node = context._plugin.get_servicechain_node(
|
|
context._plugin_context, sc_node_id)
|
|
|
|
stack_template, stack_params = self._fetch_template_and_params(
|
|
context, sc_instance, sc_spec, sc_node)
|
|
|
|
stack_name = ("stack_" + sc_instance['name'] + sc_node['name'] +
|
|
sc_instance['id'][:8])
|
|
stack = heatclient.create(
|
|
stack_name.replace(" ", ""), stack_template, stack_params)
|
|
|
|
self._insert_chain_stack_db(
|
|
context._plugin_context.session, sc_instance['id'],
|
|
stack['stack']['id'])
|
|
|
|
def _delete_servicechain_instance_stacks(self, context, instance_id):
|
|
stack_ids = self._get_chain_stacks(context.session, instance_id)
|
|
heatclient = HeatClient(context)
|
|
for stack in stack_ids:
|
|
heatclient.delete(stack.stack_id)
|
|
for stack in stack_ids:
|
|
self._wait_for_stack_delete(heatclient, stack.stack_id)
|
|
self._delete_chain_stacks_db(context.session, instance_id)
|
|
|
|
# Wait for the heat stack to be deleted for a maximum of 15 seconds
|
|
# we check the status every 3 seconds and call sleep again
|
|
# This is required because cleanup of subnet fails when the stack created
|
|
# some ports on the subnet and the resource delete is not completed by
|
|
# the time subnet delete is triggered by Resource Mapping driver
|
|
def _wait_for_stack_delete(self, heatclient, stack_id):
|
|
stack_delete_retries = STACK_DELETE_RETRIES
|
|
while True:
|
|
try:
|
|
stack = heatclient.get(stack_id)
|
|
if stack.stack_status == 'DELETE_COMPLETE':
|
|
return
|
|
elif stack.stack_status == 'DELETE_FAILED':
|
|
heatclient.delete(stack_id)
|
|
except Exception:
|
|
LOG.exception(_LE(
|
|
"Service Chain Instance cleanup may not have "
|
|
"happened because Heat API request failed "
|
|
"while waiting for the stack %(stack)s to be "
|
|
"deleted"), {'stack': stack_id})
|
|
return
|
|
else:
|
|
time.sleep(STACK_DELETE_RETRY_WAIT)
|
|
stack_delete_retries = stack_delete_retries - 1
|
|
if stack_delete_retries == 0:
|
|
LOG.warning(_LW(
|
|
"Resource cleanup for service chain instance"
|
|
" is not completed within %(wait)s seconds"
|
|
" as deletion of Stack %(stack)s is not"
|
|
" completed"),
|
|
{'wait': (STACK_DELETE_RETRIES *
|
|
STACK_DELETE_RETRY_WAIT),
|
|
'stack': stack_id})
|
|
return
|
|
else:
|
|
continue
|
|
|
|
def _get_instance_by_spec_id(self, context, spec_id):
|
|
filters = {'servicechain_spec': [spec_id]}
|
|
return context._plugin.get_servicechain_instances(
|
|
context._plugin_context, filters)
|
|
|
|
def _update_servicechain_instance(self, context, sc_instance, newspec):
|
|
self._delete_servicechain_instance_stacks(context._plugin_context,
|
|
sc_instance['id'])
|
|
sc_node_ids = newspec.get('nodes')
|
|
self._create_servicechain_instance_stacks(context,
|
|
sc_node_ids,
|
|
sc_instance,
|
|
newspec)
|
|
|
|
def _delete_chain_stacks_db(self, session, sc_instance_id):
|
|
with session.begin(subtransactions=True):
|
|
stacks = session.query(ServiceChainInstanceStack
|
|
).filter_by(instance_id=sc_instance_id
|
|
).all()
|
|
for stack in stacks:
|
|
session.delete(stack)
|
|
|
|
def _insert_chain_stack_db(self, session, sc_instance_id, stack_id):
|
|
with session.begin(subtransactions=True):
|
|
chainstack = ServiceChainInstanceStack(
|
|
instance_id=sc_instance_id,
|
|
stack_id=stack_id)
|
|
session.add(chainstack)
|
|
|
|
def _get_chain_stacks(self, session, sc_instance_id):
|
|
with session.begin(subtransactions=True):
|
|
stack_ids = session.query(ServiceChainInstanceStack.stack_id
|
|
).filter_by(instance_id=sc_instance_id
|
|
).all()
|
|
return stack_ids
|
|
|
|
def _get_resource(self, plugin, context, resource, resource_id):
|
|
obj_getter = getattr(plugin, 'get_' + resource)
|
|
obj = obj_getter(context, resource_id)
|
|
return obj
|
|
|
|
@property
|
|
def _core_plugin(self):
|
|
# REVISIT(Magesh): Need initialization method after all
|
|
# plugins are loaded to grab and store plugin.
|
|
return manager.NeutronManager.get_plugin()
|
|
|
|
@property
|
|
def _grouppolicy_plugin(self):
|
|
# REVISIT(Magesh): Need initialization method after all
|
|
# plugins are loaded to grab and store plugin.
|
|
plugins = manager.NeutronManager.get_service_plugins()
|
|
grouppolicy_plugin = plugins.get(pconst.GROUP_POLICY)
|
|
if not grouppolicy_plugin:
|
|
LOG.error(_LE("No Grouppolicy service plugin found."))
|
|
raise exc.ServiceChainDeploymentError()
|
|
return grouppolicy_plugin
|
|
|
|
|
|
class HeatClient(object):
|
|
|
|
def __init__(self, context, password=None):
|
|
api_version = "1"
|
|
self.tenant = context.tenant
|
|
|
|
self._keystone = None
|
|
endpoint = "%s/%s" % (cfg.CONF.simplechain.heat_uri, self.tenant)
|
|
kwargs = {
|
|
'token': self._get_auth_token(self.tenant),
|
|
'username': context.user_name,
|
|
'password': password,
|
|
'cacert': cfg.CONF.simplechain.heat_ca_certificates_file,
|
|
'insecure': cfg.CONF.simplechain.heat_api_insecure
|
|
}
|
|
self.client = heat_client.Client(api_version, endpoint, **kwargs)
|
|
self.stacks = self.client.stacks
|
|
|
|
def create(self, name, data, parameters=None):
|
|
fields = {
|
|
'stack_name': name,
|
|
'timeout_mins': 10,
|
|
'disable_rollback': True,
|
|
'password': data.get('password')
|
|
}
|
|
fields['template'] = data
|
|
fields['parameters'] = parameters
|
|
return self.stacks.create(**fields)
|
|
|
|
def delete(self, stack_id):
|
|
try:
|
|
self.stacks.delete(stack_id)
|
|
except heat_exc.HTTPNotFound:
|
|
LOG.warning(_LW(
|
|
"Stack %(stack)s created by service chain driver is "
|
|
"not found at cleanup"), {'stack': stack_id})
|
|
|
|
def get(self, stack_id):
|
|
return self.stacks.get(stack_id)
|
|
|
|
@property
|
|
def keystone(self):
|
|
if not self._keystone:
|
|
keystone_conf = cfg.CONF.keystone_authtoken
|
|
if keystone_conf.get('auth_uri'):
|
|
auth_url = keystone_conf.auth_uri
|
|
else:
|
|
auth_url = ('%s://%s:%s/v2.0/' % (
|
|
keystone_conf.auth_protocol,
|
|
keystone_conf.auth_host,
|
|
keystone_conf.auth_port))
|
|
user = (keystone_conf.get('admin_user') or keystone_conf.username)
|
|
pw = (keystone_conf.get('admin_password') or
|
|
keystone_conf.password)
|
|
self._keystone = keyclient.Client(
|
|
username=user, password=pw, auth_url=auth_url,
|
|
tenant_id=self.tenant)
|
|
return self._keystone
|
|
|
|
def _get_auth_token(self, tenant):
|
|
return self.keystone.get_token(tenant)
|