group-based-policy/gbpservice/neutron/services/servicechain/plugins/msc/drivers/simplechain_driver.py

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)