]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
ML2 Mechanism Driver for Tail-f Network Control System (NCS)
authorLuke Gorrie <luke@snabb.co>
Thu, 18 Jul 2013 09:25:48 +0000 (09:25 +0000)
committerLuke Gorrie <luke@snabb.co>
Tue, 3 Sep 2013 18:14:32 +0000 (18:14 +0000)
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 [new file with mode: 0644]
neutron/plugins/ml2/drivers/mechanism_ncs.py [new file with mode: 0644]
neutron/tests/unit/ml2/test_mechanism_ncs.py [new file with mode: 0644]
requirements.txt
setup.cfg

diff --git a/etc/neutron/plugins/ml2/ml2_conf_ncs.ini b/etc/neutron/plugins/ml2/ml2_conf_ncs.ini
new file mode 100644 (file)
index 0000000..dbbfcbd
--- /dev/null
@@ -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 (file)
index 0000000..fc65447
--- /dev/null
@@ -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 (file)
index 0000000..9c2eb5f
--- /dev/null
@@ -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
index 828d4d5b8de88ae7f7b7fb6e0c73d0f69d85e801..ef4de91a80b5eef62b0a4ad45e30068630c8c0d2 100644 (file)
@@ -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
index 227d17e965883abbccad5c9124853c97f90c4948..e453e2b89d338da83b758f3db5dfdef948e5c3c6 100644 (file)
--- 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