From a775ab3b61122993fbbecabc57d09f38abb03570 Mon Sep 17 00:00:00 2001 From: Luke Gorrie Date: Thu, 18 Jul 2013 09:25:48 +0000 Subject: [PATCH] ML2 Mechanism Driver for Tail-f Network Control System (NCS) Define a new ML2 Mechanism Driver that replicates Neutron network/port configuration changes to NCS: http://www.tail-f.com/network-control-system/ Configuration is sent using a HTTP/JSON interface. Implements blueprint tailf-ncs Change-Id: I1f73fa3f2e4eec8e5a0f2865aec2d934e25c76d1 --- etc/neutron/plugins/ml2/ml2_conf_ncs.ini | 28 +++ neutron/plugins/ml2/drivers/mechanism_ncs.py | 182 +++++++++++++++++++ neutron/tests/unit/ml2/test_mechanism_ncs.py | 45 +++++ requirements.txt | 1 + setup.cfg | 1 + 5 files changed, 257 insertions(+) create mode 100644 etc/neutron/plugins/ml2/ml2_conf_ncs.ini create mode 100644 neutron/plugins/ml2/drivers/mechanism_ncs.py create mode 100644 neutron/tests/unit/ml2/test_mechanism_ncs.py diff --git a/etc/neutron/plugins/ml2/ml2_conf_ncs.ini b/etc/neutron/plugins/ml2/ml2_conf_ncs.ini new file mode 100644 index 000000000..dbbfcbd28 --- /dev/null +++ b/etc/neutron/plugins/ml2/ml2_conf_ncs.ini @@ -0,0 +1,28 @@ +# Defines configuration options specific to the Tail-f NCS Mechanism Driver + +[ml2_ncs] +# (StrOpt) Tail-f NCS HTTP endpoint for REST access to the OpenStack +# subtree. +# If this is not set then no HTTP requests will be made. +# +# url = +# Example: url = http://ncs/api/running/services/openstack + +# (StrOpt) Username for HTTP basic authentication to NCS. +# This is an optional parameter. If unspecified then no authentication is used. +# +# username = +# Example: username = admin + +# (StrOpt) Password for HTTP basic authentication to NCS. +# This is an optional parameter. If unspecified then no authentication is used. +# +# password = +# Example: password = admin + +# (IntOpt) Timeout in seconds to wait for NCS HTTP request completion. +# This is an optional parameter, default value is 10 seconds. +# +# timeout = +# Example: timeout = 15 + diff --git a/neutron/plugins/ml2/drivers/mechanism_ncs.py b/neutron/plugins/ml2/drivers/mechanism_ncs.py new file mode 100644 index 000000000..fc65447fa --- /dev/null +++ b/neutron/plugins/ml2/drivers/mechanism_ncs.py @@ -0,0 +1,182 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +import re + +from oslo.config import cfg +import requests + +from neutron.openstack.common import jsonutils +from neutron.openstack.common import log +from neutron.plugins.ml2 import driver_api as api + +LOG = log.getLogger(__name__) + +ncs_opts = [ + cfg.StrOpt('url', + help=_("HTTP URL of Tail-f NCS REST interface.")), + cfg.StrOpt('username', + help=_("HTTP username for authentication")), + cfg.StrOpt('password', secret=True, + help=_("HTTP password for authentication")), + cfg.IntOpt('timeout', default=10, + help=_("HTTP timeout in seconds.")) +] + +cfg.CONF.register_opts(ncs_opts, "ml2_ncs") + + +class NCSMechanismDriver(api.MechanismDriver): + + """Mechanism Driver for Tail-f Network Control System (NCS). + + This driver makes portions of the Neutron database available for + service provisioning in NCS. For example, NCS can use this + information to provision physical switches and routers in response + to OpenStack configuration changes. + + The database is replicated from Neutron to NCS using HTTP and JSON. + + The driver has two states: out-of-sync (initially) and in-sync. + + In the out-of-sync state each driver event triggers an attempt + to synchronize the complete database. On success the driver + transitions to the in-sync state. + + In the in-sync state each driver event triggers synchronization + of one network or port. On success the driver stays in-sync and + on failure it transitions to the out-of-sync state. + """ + out_of_sync = True + + def initialize(self): + self.url = cfg.CONF.ml2_ncs.url + self.timeout = cfg.CONF.ml2_ncs.timeout + self.username = cfg.CONF.ml2_ncs.username + self.password = cfg.CONF.ml2_ncs.password + + # Postcommit hooks are used to trigger synchronization. + + def create_network_postcommit(self, context): + self.synchronize('create', 'network', context) + + def update_network_postcommit(self, context): + self.synchronize('update', 'network', context) + + def delete_network_postcommit(self, context): + self.synchronize('delete', 'network', context) + + def create_subnet_postcommit(self, context): + self.synchronize('create', 'subnet', context) + + def update_subnet_postcommit(self, context): + self.synchronize('update', 'subnet', context) + + def delete_subnet_postcommit(self, context): + self.synchronize('delete', 'subnet', context) + + def create_port_postcommit(self, context): + self.synchronize('create', 'port', context) + + def update_port_postcommit(self, context): + self.synchronize('update', 'port', context) + + def delete_port_postcommit(self, context): + self.synchronize('delete', 'port', context) + + def synchronize(self, operation, object_type, context): + """Synchronize NCS with Neutron following a configuration change.""" + if self.out_of_sync: + self.sync_full(context) + else: + self.sync_object(operation, object_type, context) + + def sync_full(self, context): + """Resync the entire database to NCS. + Transition to the in-sync state on success. + """ + dbcontext = context._plugin_context + networks = context._plugin.get_networks(dbcontext) + subnets = context._plugin.get_subnets(dbcontext) + ports = context._plugin.get_ports(dbcontext) + for port in ports: + self.add_security_groups(context, dbcontext, port) + json = {'openstack': {'network': networks, + 'subnet': subnets, + 'port': ports}} + self.sendjson('put', self.url, json) + self.out_of_sync = False + + def sync_object(self, operation, object_type, context): + """Synchronize the single modified record to NCS. + Transition to the out-of-sync state on failure. + """ + self.out_of_sync = True + dbcontext = context._plugin_context + id = context.current['id'] + urlpath = object_type + '/' + id + if operation == 'delete': + self.sendjson('delete', urlpath, None) + else: + assert operation == 'create' or operation == 'update' + if object_type == 'network': + network = context._plugin.get_network(dbcontext, id) + self.sendjson('put', urlpath, {'network': network}) + elif object_type == 'subnet': + subnet = context._plugin.get_subnet(dbcontext, id) + self.sendjson('put', urlpath, {'subnet': subnet}) + else: + assert object_type == 'port' + port = context._plugin.get_port(dbcontext, id) + self.add_security_groups(context, dbcontext, port) + self.sendjson('put', urlpath, {'port': port}) + self.out_of_sync = False + + def add_security_groups(self, context, dbcontext, port): + """Populate the 'security_groups' field with entire records.""" + groups = [context._plugin.get_security_group(dbcontext, sg) + for sg in port['security_groups']] + port['security_groups'] = groups + + def sendjson(self, method, urlpath, obj): + obj = self.escape_keys(obj) + headers = {'Content-Type': 'application/vnd.yang.data+json'} + if obj is None: + data = None + else: + data = jsonutils.dumps(obj, indent=2) + auth = None + if self.username and self.password: + auth = (self.username, self.password) + if self.url: + url = '/'.join([self.url, urlpath]) + r = requests.request(method, url=url, + headers=headers, data=data, + auth=auth, timeout=self.timeout) + r.raise_for_status() + + def escape_keys(self, obj): + """Escape JSON keys to be NCS compatible. + NCS does not allow period (.) or colon (:) characters. + """ + if isinstance(obj, dict): + obj = dict((self.escape(k), self.escape_keys(v)) + for k, v in obj.iteritems()) + if isinstance(obj, list): + obj = [self.escape_keys(x) for x in obj] + return obj + + def escape(self, string): + return re.sub('[:._]', '-', string) diff --git a/neutron/tests/unit/ml2/test_mechanism_ncs.py b/neutron/tests/unit/ml2/test_mechanism_ncs.py new file mode 100644 index 000000000..9c2eb5fe9 --- /dev/null +++ b/neutron/tests/unit/ml2/test_mechanism_ncs.py @@ -0,0 +1,45 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +from neutron.plugins.ml2 import config as config +from neutron.tests.unit import test_db_plugin as test_plugin + +PLUGIN_NAME = 'neutron.plugins.ml2.plugin.Ml2Plugin' + + +class NCSTestCase(test_plugin.NeutronDbPluginV2TestCase): + + def setUp(self): + # Enable the test mechanism driver to ensure that + # we can successfully call through to all mechanism + # driver apis. + config.cfg.CONF.set_override('mechanism_drivers', + ['logger', 'ncs'], + 'ml2') + self.addCleanup(config.cfg.CONF.reset) + super(NCSTestCase, self).setUp(PLUGIN_NAME) + self.port_create_status = 'DOWN' + + +class NCSMechanismTestBasicGet(test_plugin.TestBasicGet, NCSTestCase): + pass + + +class NCSMechanismTestNetworksV2(test_plugin.TestNetworksV2, NCSTestCase): + pass + + +class NCSMechanismTestPortsV2(test_plugin.TestPortsV2, NCSTestCase): + pass diff --git a/requirements.txt b/requirements.txt index 828d4d5b8..ef4de91a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ Babel>=0.9.6 eventlet>=0.13.0 greenlet>=0.3.2 httplib2 +requests>=1.1 iso8601>=0.1.4 kombu>=2.4.8 netaddr diff --git a/setup.cfg b/setup.cfg index 227d17e96..e453e2b89 100644 --- a/setup.cfg +++ b/setup.cfg @@ -116,6 +116,7 @@ neutron.ml2.type_drivers = neutron.ml2.mechanism_drivers = logger = neutron.tests.unit.ml2.drivers.mechanism_logger:LoggerMechanismDriver test = neutron.tests.unit.ml2.drivers.mechanism_test:TestMechanismDriver + ncs = neutron.plugins.ml2.drivers.mechanism_ncs:NCSMechanismDriver [build_sphinx] all_files = 1