# 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)