]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Configuration agent for Cisco devices
authorHareesh Puthalath <hareesh.puthalath@gmail.com>
Thu, 26 Jun 2014 15:39:56 +0000 (17:39 +0200)
committerHareesh Puthalath <hareesh.puthalath@gmail.com>
Fri, 25 Jul 2014 17:34:27 +0000 (19:34 +0200)
A generic config agent for configuring L3+ services in Cisco devices.

This patch targets specifically configuration of L3 fuctionality,
namely routing, NAT and floatingIPs in Cisco CSR1kv virtual appliance.

Implements blueprint: cisco-config-agent
https://blueprints.launchpad.net/neutron/+spec/cisco-config-agent

Change-Id: Ic887a93480eca0b56049c67e32c98658e3a4427f

20 files changed:
neutron/plugins/cisco/cfg_agent/__init__.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/cfg_agent.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/cfg_exceptions.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/device_drivers/__init__.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/device_drivers/csr1kv/__init__.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/device_drivers/csr1kv/cisco_csr1kv_snippets.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/device_drivers/csr1kv/csr1kv_routing_driver.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/device_drivers/devicedriver_api.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/device_drivers/driver_mgr.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/device_drivers/dummy_driver.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/device_status.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/service_helpers/__init__.py [new file with mode: 0644]
neutron/plugins/cisco/cfg_agent/service_helpers/routing_svc_helper.py [new file with mode: 0644]
neutron/plugins/cisco/common/cisco_constants.py
neutron/tests/unit/cisco/cfg_agent/__init__.py [new file with mode: 0644]
neutron/tests/unit/cisco/cfg_agent/test_cfg_agent.py [new file with mode: 0644]
neutron/tests/unit/cisco/cfg_agent/test_csr1kv_routing_driver.py [new file with mode: 0644]
neutron/tests/unit/cisco/cfg_agent/test_device_status.py [new file with mode: 0644]
neutron/tests/unit/cisco/cfg_agent/test_routing_svc_helper.py [new file with mode: 0644]
setup.cfg

diff --git a/neutron/plugins/cisco/cfg_agent/__init__.py b/neutron/plugins/cisco/cfg_agent/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/neutron/plugins/cisco/cfg_agent/cfg_agent.py b/neutron/plugins/cisco/cfg_agent/cfg_agent.py
new file mode 100644 (file)
index 0000000..a1ae4d8
--- /dev/null
@@ -0,0 +1,352 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+import eventlet
+eventlet.monkey_patch()
+import pprint
+import sys
+import time
+
+from oslo.config import cfg
+
+from neutron.agent.common import config
+from neutron.agent.linux import external_process
+from neutron.agent.linux import interface
+from neutron.agent import rpc as agent_rpc
+from neutron.common import config as common_config
+from neutron.common import rpc as n_rpc
+from neutron.common import topics
+from neutron import context as n_context
+from neutron import manager
+from neutron.openstack.common import importutils
+from neutron.openstack.common import lockutils
+from neutron.openstack.common import log as logging
+from neutron.openstack.common import loopingcall
+from neutron.openstack.common import periodic_task
+from neutron.openstack.common import service
+from neutron.openstack.common import timeutils
+from neutron.plugins.cisco.cfg_agent import device_status
+from neutron.plugins.cisco.common import cisco_constants as c_constants
+from neutron import service as neutron_service
+
+LOG = logging.getLogger(__name__)
+
+# Constants for agent registration.
+REGISTRATION_RETRY_DELAY = 2
+MAX_REGISTRATION_ATTEMPTS = 30
+
+
+class CiscoDeviceManagementApi(n_rpc.RpcProxy):
+    """Agent side of the device manager RPC API."""
+
+    BASE_RPC_API_VERSION = '1.0'
+
+    def __init__(self, topic, host):
+        super(CiscoDeviceManagementApi, self).__init__(
+            topic=topic, default_version=self.BASE_RPC_API_VERSION)
+        self.host = host
+
+    def report_dead_hosting_devices(self, context, hd_ids=None):
+        """Report that a hosting device cannot be contacted (presumed dead).
+
+        :param: context: session context
+        :param: hosting_device_ids: list of non-responding hosting devices
+        :return: None
+        """
+        # Cast since we don't expect a return value.
+        self.cast(context,
+                  self.make_msg('report_non_responding_hosting_devices',
+                                host=self.host,
+                                hosting_device_ids=hd_ids),
+                  topic=self.topic)
+
+    def register_for_duty(self, context):
+        """Report that a config agent is ready for duty."""
+        return self.call(context,
+                         self.make_msg('register_for_duty',
+                                       host=self.host),
+                         topic=self.topic)
+
+
+class CiscoCfgAgent(manager.Manager):
+    """Cisco Cfg Agent.
+
+    This class defines a generic configuration agent for cisco devices which
+    implement network services in the cloud backend. It is based on the
+    (reference) l3-agent, but has been enhanced to support multiple services
+     in addition to routing.
+
+    The agent acts like as a container for services and does not do any
+    service specific processing or configuration itself.
+    All service specific processing is delegated to service helpers which
+    the agent loads. Thus routing specific updates are processed by the
+    routing service helper, firewall by firewall helper etc.
+    A further layer of abstraction is implemented by using device drivers for
+    encapsulating all configuration operations of a service on a device.
+    Device drivers are specific to a particular device/service VM eg: CSR1kv.
+
+    The main entry points in this class are the `process_services()` and
+    `_backlog_task()` .
+    """
+    RPC_API_VERSION = '1.1'
+
+    OPTS = [
+        cfg.IntOpt('rpc_loop_interval', default=10,
+                   help=_("Interval when the process_services() loop "
+                          "executes in seconds. This is when the config agent "
+                          "lets each service helper to process its neutron "
+                          "resources.")),
+        cfg.StrOpt('routing_svc_helper_class',
+                   default='neutron.plugins.cisco.cfg_agent.service_helpers'
+                           '.routing_svc_helper.RoutingServiceHelper',
+                   help=_("Path of the routing service helper class.")),
+    ]
+
+    def __init__(self, host, conf=None):
+        self.conf = conf or cfg.CONF
+        self._dev_status = device_status.DeviceStatus()
+        self.context = n_context.get_admin_context_without_session()
+
+        self._initialize_rpc(host)
+        self._initialize_service_helpers(host)
+        self._start_periodic_tasks()
+        super(CiscoCfgAgent, self).__init__(host=self.conf.host)
+
+    def _initialize_rpc(self, host):
+        self.devmgr_rpc = CiscoDeviceManagementApi(topics.L3PLUGIN, host)
+
+    def _initialize_service_helpers(self, host):
+        svc_helper_class = self.conf.routing_svc_helper_class
+        try:
+            self.routing_service_helper = importutils.import_object(
+                svc_helper_class, host, self.conf, self)
+        except ImportError as e:
+            LOG.warn(_("Error in loading routing service helper. Class "
+                       "specified is %(class)s. Reason:%(reason)s"),
+                     {'class': self.conf.routing_svc_helper_class,
+                      'reason': e})
+            self.routing_service_helper = None
+
+    def _start_periodic_tasks(self):
+        self.loop = loopingcall.FixedIntervalLoopingCall(self.process_services)
+        self.loop.start(interval=self.conf.rpc_loop_interval)
+
+    def after_start(self):
+        LOG.info(_("Cisco cfg agent started"))
+
+    def get_routing_service_helper(self):
+        return self.routing_service_helper
+
+    ## Periodic tasks ##
+    @periodic_task.periodic_task
+    def _backlog_task(self, context):
+        """Process backlogged devices."""
+        LOG.debug("Processing backlog.")
+        self._process_backlogged_hosting_devices(context)
+
+    ## Main orchestrator ##
+    @lockutils.synchronized('cisco-cfg-agent', 'neutron-')
+    def process_services(self, device_ids=None, removed_devices_info=None):
+        """Process services managed by this config agent.
+
+        This method is invoked by any of three scenarios.
+
+        1. Invoked by a periodic task running every `RPC_LOOP_INTERVAL`
+        seconds. This is the most common scenario.
+        In this mode, the method is called without any arguments.
+
+        2. Called by the `_process_backlogged_hosting_devices()` as part of
+        the backlog processing task. In this mode, a list of device_ids
+        are passed as arguments. These are the list of backlogged
+        hosting devices that are now reachable and we want to sync services
+        on them.
+
+        3. Called by the `hosting_devices_removed()` method. This is when
+        the config agent has received a notification from the plugin that
+        some hosting devices are going to be removed. The payload contains
+        the details of the hosting devices and the associated neutron
+        resources on them which should be processed and removed.
+
+        To avoid race conditions with these scenarios, this function is
+        protected by a lock.
+
+        This method goes on to invoke `process_service()` on the
+        different service helpers.
+
+        :param device_ids : List of devices that are now available and needs
+         to be processed
+        :param removed_devices_info: Info about the hosting devices which
+        are going to be removed and details of the resources hosted on them.
+        Expected Format:
+                {
+                 'hosting_data': {'hd_id1': {'routers': [id1, id2, ...]},
+                                  'hd_id2': {'routers': [id3, id4, ...]}, ...},
+                 'deconfigure': True/False
+                }
+        :return: None
+        """
+        LOG.debug("Processing services started")
+        # Now we process only routing service, additional services will be
+        # added in future
+        if self.routing_service_helper:
+            self.routing_service_helper.process_service(device_ids,
+                                                        removed_devices_info)
+        else:
+            LOG.warn(_("No routing service helper loaded"))
+        LOG.debug("Processing services completed")
+
+    def _process_backlogged_hosting_devices(self, context):
+        """Process currently backlogged devices.
+
+        Go through the currently backlogged devices and process them.
+        For devices which are now reachable (compared to last time), we call
+        `process_services()` passing the now reachable device's id.
+        For devices which have passed the `hosting_device_dead_timeout` and
+        hence presumed dead, execute a RPC to the plugin informing that.
+        :param context: RPC context
+        :return: None
+        """
+        res = self._dev_status.check_backlogged_hosting_devices()
+        if res['reachable']:
+            self.process_services(device_ids=res['reachable'])
+        if res['dead']:
+            LOG.debug("Reporting dead hosting devices: %s", res['dead'])
+            self.devmgr_rpc.report_dead_hosting_devices(context,
+                                                        hd_ids=res['dead'])
+
+    def hosting_devices_removed(self, context, payload):
+        """Deal with hosting device removed RPC message."""
+        try:
+            if payload['hosting_data']:
+                if payload['hosting_data'].keys():
+                    self.process_services(removed_devices_info=payload)
+        except KeyError as e:
+            LOG.error(_("Invalid payload format for received RPC message "
+                        "`hosting_devices_removed`. Error is %{error}s. "
+                        "Payload is %(payload)s"),
+                      {'error': e, 'payload': payload})
+
+
+class CiscoCfgAgentWithStateReport(CiscoCfgAgent):
+
+    def __init__(self, host, conf=None):
+        self.state_rpc = agent_rpc.PluginReportStateAPI(topics.PLUGIN)
+        self.agent_state = {
+            'binary': 'neutron-cisco-cfg-agent',
+            'host': host,
+            'topic': c_constants.CFG_AGENT,
+            'configurations': {},
+            'start_flag': True,
+            'agent_type': c_constants.AGENT_TYPE_CFG}
+        report_interval = cfg.CONF.AGENT.report_interval
+        self.use_call = True
+        self._initialize_rpc(host)
+        self._agent_registration()
+        super(CiscoCfgAgentWithStateReport, self).__init__(host=host,
+                                                           conf=conf)
+        if report_interval:
+            self.heartbeat = loopingcall.FixedIntervalLoopingCall(
+                self._report_state)
+            self.heartbeat.start(interval=report_interval)
+
+    def _agent_registration(self):
+        """Register this agent with the server.
+
+        This method registers the cfg agent with the neutron server so hosting
+        devices can be assigned to it. In case the server is not ready to
+        accept registration (it sends a False) then we retry registration
+        for `MAX_REGISTRATION_ATTEMPTS` with a delay of
+        `REGISTRATION_RETRY_DELAY`. If there is no server response or a
+        failure to register after the required number of attempts,
+        the agent stops itself.
+        """
+        for attempts in xrange(MAX_REGISTRATION_ATTEMPTS):
+            context = n_context.get_admin_context_without_session()
+            self.send_agent_report(self.agent_state, context)
+            res = self.devmgr_rpc.register_for_duty(context)
+            if res is True:
+                LOG.info(_("[Agent registration] Agent successfully "
+                           "registered"))
+                return
+            elif res is False:
+                LOG.warn(_("[Agent registration] Neutron server said that "
+                           "device manager was not ready. Retrying in %0.2f "
+                           "seconds "), REGISTRATION_RETRY_DELAY)
+                time.sleep(REGISTRATION_RETRY_DELAY)
+            elif res is None:
+                LOG.error(_("[Agent registration] Neutron server said that no "
+                            "device manager was found. Cannot "
+                            "continue. Exiting!"))
+                raise SystemExit("Cfg Agent exiting")
+        LOG.error(_("[Agent registration] %d unsuccessful registration "
+                    "attempts. Exiting!"), MAX_REGISTRATION_ATTEMPTS)
+        raise SystemExit("Cfg Agent exiting")
+
+    def _report_state(self):
+        """Report state to the plugin.
+
+        This task run every `report_interval` period.
+        Collects, creates and sends a summary of the services currently
+        managed by this agent. Data is collected from the service helper(s).
+        Refer the `configurations` dict for the parameters reported.
+        :return: None
+        """
+        LOG.debug("Report state task started")
+        configurations = {}
+        if self.routing_service_helper:
+            configurations = self.routing_service_helper.collect_state(
+                self.agent_state['configurations'])
+        non_responding = self._dev_status.get_backlogged_hosting_devices_info()
+        configurations['non_responding_hosting_devices'] = non_responding
+        self.agent_state['configurations'] = configurations
+        self.agent_state['local_time'] = str(timeutils.utcnow())
+        LOG.debug("State report data: %s", pprint.pformat(self.agent_state))
+        self.send_agent_report(self.agent_state, self.context)
+
+    def send_agent_report(self, report, context):
+        """Send the agent report via RPC."""
+        try:
+            self.state_rpc.report_state(context, report, self.use_call)
+            report.pop('start_flag', None)
+            self.use_call = False
+            LOG.debug("Send agent report successfully completed")
+        except AttributeError:
+            # This means the server does not support report_state
+            LOG.warn(_("Neutron server does not support state report. "
+                       "State report for this agent will be disabled."))
+            self.heartbeat.stop()
+            return
+        except Exception:
+            LOG.exception(_("Failed sending agent report!"))
+
+
+def main(manager='neutron.plugins.cisco.cfg_agent.'
+                 'cfg_agent.CiscoCfgAgentWithStateReport'):
+    conf = cfg.CONF
+    conf.register_opts(CiscoCfgAgent.OPTS)
+    config.register_agent_state_opts_helper(conf)
+    config.register_root_helper(conf)
+    conf.register_opts(interface.OPTS)
+    conf.register_opts(external_process.OPTS)
+    common_config.init(sys.argv[1:])
+    conf(project='neutron')
+    config.setup_logging(conf)
+    server = neutron_service.Service.create(
+        binary='neutron-cisco-cfg-agent',
+        topic=c_constants.CFG_AGENT,
+        report_interval=cfg.CONF.AGENT.report_interval,
+        manager=manager)
+    service.launch(server).wait()
diff --git a/neutron/plugins/cisco/cfg_agent/cfg_exceptions.py b/neutron/plugins/cisco/cfg_agent/cfg_exceptions.py
new file mode 100644 (file)
index 0000000..06ab4d4
--- /dev/null
@@ -0,0 +1,60 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+"""Exceptions by Cisco Configuration Agent."""
+
+from neutron.common import exceptions
+
+
+class DriverException(exceptions.NeutronException):
+    """Exception created by the Driver class."""
+
+
+class CSR1kvInitializationException(DriverException):
+    """Exception when initialization of CSR1kv Routing Driver object."""
+    message = (_("Critical device parameter missing. Failed initializing "
+                 "CSR1kv routing driver."))
+
+
+class CSR1kvConnectionException(DriverException):
+    """Connection exception when connecting to CSR1kv hosting device."""
+    message = (_("Failed connecting to CSR1kv. Reason: %(reason)s. "
+                 "Connection params are User:%(user)s, Host:%(host)s, "
+                 "Port:%(port)s, Device timeout:%(timeout)s."))
+
+
+class CSR1kvConfigException(DriverException):
+    """Configuration exception thrown when modifying the running config."""
+    message = (_("Error executing snippet:%(snippet)s. "
+                 "ErrorType:%(type)s ErrorTag:%(tag)s."))
+
+
+class CSR1kvUnknownValueException(DriverException):
+    """CSR1kv Exception thrown when an unknown value is received."""
+    message = (_("Data in attribute: %(attribute)s does not correspond to "
+                 "expected value. Value received is %(value)s. "))
+
+
+class DriverNotExist(DriverException):
+    message = _("Driver %(driver)s does not exist.")
+
+
+class DriverNotFound(DriverException):
+    message = _("Driver not found for resource id:%(id)s.")
+
+
+class DriverNotSetForMissingParameter(DriverException):
+    message = _("Driver cannot be set for missing parameter:%(p)s.")
diff --git a/neutron/plugins/cisco/cfg_agent/device_drivers/__init__.py b/neutron/plugins/cisco/cfg_agent/device_drivers/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/neutron/plugins/cisco/cfg_agent/device_drivers/csr1kv/__init__.py b/neutron/plugins/cisco/cfg_agent/device_drivers/csr1kv/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/neutron/plugins/cisco/cfg_agent/device_drivers/csr1kv/cisco_csr1kv_snippets.py b/neutron/plugins/cisco/cfg_agent/device_drivers/csr1kv/cisco_csr1kv_snippets.py
new file mode 100644 (file)
index 0000000..dc20c74
--- /dev/null
@@ -0,0 +1,351 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+"""
+CSR (IOS-XE) XML-based configuration snippets
+"""
+
+# The standard Template used to interact with IOS-XE(CSR).
+# This template is added by the netconf client
+# EXEC_CONF_SNIPPET = """
+#       <config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
+#         <configure>
+#           <__XML__MODE__exec_configure>%s
+#           </__XML__MODE__exec_configure>
+#         </configure>
+#       </config>
+# """
+
+
+#=================================================#
+# Set ip address on an interface
+# $(config)interface GigabitEthernet 1
+# $(config)ip address 10.0.100.1 255.255.255.0
+#=================================================#
+SET_INTC = """
+<config>
+        <cli-config-data>
+            <cmd>interface %s</cmd>
+            <cmd>ip address %s %s</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=================================================#
+# Enable an interface
+# $(config)interface GigabitEthernet 1
+# $(config)no shutdown
+#=================================================#
+ENABLE_INTF = """
+<config>
+        <cli-config-data>
+            <cmd>interface %s</cmd>
+            <cmd>no shutdown</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=================================================#
+# Create VRF
+# $(config)ip routing
+# $(config)ip vrf nrouter-e7d4y5
+#=================================================#
+CREATE_VRF = """
+<config>
+        <cli-config-data>
+            <cmd>ip routing</cmd>
+            <cmd>ip vrf %s</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=================================================#
+# Remove VRF
+# $(config)ip routing
+# $(config)no ip vrf nrouter-e7d4y5
+#=================================================#
+REMOVE_VRF = """
+<config>
+        <cli-config-data>
+            <cmd>ip routing</cmd>
+            <cmd>no ip vrf %s</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=================================================#
+# Create Subinterface
+# $(config)interface GigabitEthernet 2.500
+# $(config)encapsulation dot1Q 500
+# $(config)vrf forwarding nrouter-e7d4y5
+# $(config)ip address 192.168.0.1 255.255.255.0
+#=================================================#
+CREATE_SUBINTERFACE = """
+<config>
+        <cli-config-data>
+            <cmd>interface %s</cmd>
+            <cmd>encapsulation dot1Q %s</cmd>
+            <cmd>ip vrf forwarding %s</cmd>
+            <cmd>ip address %s %s</cmd>
+        </cli-config-data>
+</config>
+
+"""
+
+#=================================================#
+# Remove Subinterface
+# $(config)no interface GigabitEthernet 2.500
+#=================================================#
+REMOVE_SUBINTERFACE = """
+<config>
+        <cli-config-data>
+            <cmd>no interface %s</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=================================================#
+# Enable HSRP on a Subinterface
+# $(config)interface GigabitEthernet 2.500
+# $(config)vrf forwarding nrouter-e7d4y5
+# $(config)standby version 2
+# $(config)standby <group> priority <priority>
+# $(config)standby <group> ip <ip>
+#=================================================#
+SET_INTC_HSRP = """
+<config>
+        <cli-config-data>
+            <cmd>interface %s</cmd>
+            <cmd>ip vrf forwarding %s</cmd>
+            <cmd>standby version 2</cmd>
+            <cmd>standby %s priority %s</cmd>
+            <cmd>standby %s ip %s</cmd>
+        </cli-config-data>
+</config>
+
+"""
+
+#=================================================#
+# Remove HSRP on a Subinterface
+# $(config)interface GigabitEthernet 2.500
+# $(config)no standby version 2
+# $(config)no standby <group>
+#=================================================#
+REMOVE_INTC_HSRP = """
+<config>
+        <cli-config-data>
+            <cmd>interface %s</cmd>
+            <cmd>no standby %s</cmd>
+            <cmd>no standby version 2</cmd>
+        </cli-config-data>
+</config>
+
+"""
+
+
+#=================================================#
+# Create Access Control List
+# $(config)ip access-list standard acl_500
+# $(config)permit 192.168.0.1 255.255.255.0
+#=================================================#
+CREATE_ACL = """
+<config>
+        <cli-config-data>
+            <cmd>ip access-list standard %s</cmd>
+            <cmd>permit %s %s</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=================================================#
+# Remove Access Control List
+# $(config)no ip access-list standard acl_500
+#=================================================#
+REMOVE_ACL = """
+<config>
+        <cli-config-data>
+            <cmd>no ip access-list standard %s</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=========================================================================#
+# Set Dynamic source translation on an interface
+# Syntax: ip nat inside source list <acl_no> interface <interface>
+# .......vrf <vrf_name> overload
+# eg: $(config)ip nat inside source list acl_500
+#    ..........interface GigabitEthernet3.100 vrf nrouter-e7d4y5 overload
+#========================================================================#
+SNAT_CFG = "ip nat inside source list %s interface %s vrf %s overload"
+
+SET_DYN_SRC_TRL_INTFC = """
+<config>
+        <cli-config-data>
+            <cmd>ip nat inside source list %s interface %s vrf %s
+            overload</cmd>
+        </cli-config-data>
+</config>
+
+"""
+
+#=========================================================================#
+# Remove Dynamic source translation on an interface
+# Syntax: no ip nat inside source list <acl_no> interface <interface>
+# .......vrf <vrf_name> overload
+# eg: $(config)no ip nat inside source list acl_500
+#    ..........interface GigabitEthernet3.100 vrf nrouter-e7d4y5 overload
+#========================================================================#
+REMOVE_DYN_SRC_TRL_INTFC = """
+<config>
+        <cli-config-data>
+            <cmd>no ip nat inside source list %s interface %s vrf %s
+            overload</cmd>
+        </cli-config-data>
+</config>
+
+"""
+
+#=================================================#
+# Set NAT
+# Syntax : interface <interface>
+#          ip nat <inside|outside>
+#=================================================#
+SET_NAT = """
+<config>
+        <cli-config-data>
+            <cmd>interface %s</cmd>
+            <cmd>ip nat %s</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=================================================#
+# Remove NAT
+# Syntax : interface <interface>
+#          no ip nat <inside|outside>
+#=================================================#
+REMOVE_NAT = """
+<config>
+        <cli-config-data>
+            <cmd>interface %s</cmd>
+            <cmd>no ip nat %s</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=========================================================================#
+# Set Static source translation on an interface
+# Syntax: ip nat inside source static <fixed_ip> <floating_ip>
+# .......vrf <vrf_name> match-in-vrf
+# eg: $(config)ip nat inside source static 192.168.0.1 121.158.0.5
+#    ..........vrf nrouter-e7d4y5 match-in-vrf
+#========================================================================#
+SET_STATIC_SRC_TRL = """
+<config>
+        <cli-config-data>
+            <cmd>ip nat inside source static %s %s vrf %s match-in-vrf</cmd>
+        </cli-config-data>
+</config>
+
+"""
+
+#=========================================================================#
+# Remove Static source translation on an interface
+# Syntax: no ip nat inside source static <fixed_ip> <floating_ip>
+# .......vrf <vrf_name> match-in-vrf
+# eg: $(config)no ip nat inside source static 192.168.0.1 121.158.0.5
+#    ..........vrf nrouter-e7d4y5 match-in-vrf
+#========================================================================#
+REMOVE_STATIC_SRC_TRL = """
+<config>
+        <cli-config-data>
+            <cmd>no ip nat inside source static %s %s vrf %s match-in-vrf</cmd>
+        </cli-config-data>
+</config>
+
+"""
+
+#=============================================================================#
+# Set ip route
+# Syntax: ip route vrf <vrf-name> <destination> <mask> [<interface>] <next hop>
+# eg: $(config)ip route vrf nrouter-e7d4y5 8.8.0.0  255.255.0.0 10.0.100.255
+#=============================================================================#
+SET_IP_ROUTE = """
+<config>
+        <cli-config-data>
+            <cmd>ip route vrf %s %s %s %s</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=============================================================================#
+# Remove ip route
+# Syntax: no ip route vrf <vrf-name> <destination> <mask>
+#        [<interface>] <next hop>
+# eg: $(config)no ip route vrf nrouter-e7d4y5 8.8.0.0  255.255.0.0 10.0.100.255
+#=============================================================================#
+REMOVE_IP_ROUTE = """
+<config>
+        <cli-config-data>
+            <cmd>no ip route vrf %s %s %s %s</cmd>
+        </cli-config-data>
+</config>
+"""
+#=============================================================================#
+# Set default ip route
+# Syntax: ip route vrf <vrf-name> 0.0.0.0 0.0.0.0 [<interface>] <next hop>
+# eg: $(config)ip route vrf nrouter-e7d4y5 0.0.0.0  0.0.0.0 10.0.100.255
+#=============================================================================#
+DEFAULT_ROUTE_CFG = 'ip route vrf %s 0.0.0.0 0.0.0.0 %s'
+
+SET_DEFAULT_ROUTE = """
+<config>
+        <cli-config-data>
+            <cmd>ip route vrf %s 0.0.0.0 0.0.0.0 %s</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=============================================================================#
+# Remove default ip route
+# Syntax: ip route vrf <vrf-name> 0.0.0.0 0.0.0.0 [<interface>] <next hop>
+# eg: $(config)ip route vrf nrouter-e7d4y5 0.0.0.0  0.0.0.0 10.0.100.255
+#=============================================================================#
+REMOVE_DEFAULT_ROUTE = """
+<config>
+        <cli-config-data>
+            <cmd>no ip route vrf %s 0.0.0.0 0.0.0.0 %s</cmd>
+        </cli-config-data>
+</config>
+"""
+
+#=============================================================================#
+# Clear dynamic nat translations. This is used to clear any nat bindings before
+# we can turn off NAT on an interface
+# Syntax: clear ip nat translation [forced]
+#=============================================================================#
+# CLEAR_DYN_NAT_TRANS = """
+# <oper-data-format-text-block>
+#     <exec>clear ip nat translation forced</exec>
+# </oper-data-format-text-block>
+# """
+CLEAR_DYN_NAT_TRANS = """
+<config>
+        <cli-config-data>
+            <cmd>do clear ip nat translation forced</cmd>
+        </cli-config-data>
+</config>
+"""
diff --git a/neutron/plugins/cisco/cfg_agent/device_drivers/csr1kv/csr1kv_routing_driver.py b/neutron/plugins/cisco/cfg_agent/device_drivers/csr1kv/csr1kv_routing_driver.py
new file mode 100644 (file)
index 0000000..68f8fac
--- /dev/null
@@ -0,0 +1,687 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+import logging
+import netaddr
+import re
+import time
+import xml.etree.ElementTree as ET
+
+import ciscoconfparse
+from ncclient import manager
+
+from oslo.config import cfg
+
+from neutron.plugins.cisco.cfg_agent import cfg_exceptions as cfg_exc
+from neutron.plugins.cisco.cfg_agent.device_drivers.csr1kv import (
+    cisco_csr1kv_snippets as snippets)
+from neutron.plugins.cisco.cfg_agent.device_drivers import devicedriver_api
+
+LOG = logging.getLogger(__name__)
+
+
+# N1kv constants
+T1_PORT_NAME_PREFIX = 't1_p:'  # T1 port/network is for VXLAN
+T2_PORT_NAME_PREFIX = 't2_p:'  # T2 port/network is for VLAN
+
+
+class CSR1kvRoutingDriver(devicedriver_api.RoutingDriverBase):
+    """CSR1kv Routing Driver.
+
+    This driver encapsulates the configuration logic via NETCONF protocol to
+    configure a CSR1kv Virtual Router (IOS-XE based) for implementing
+    Neutron L3 services. These services include routing, NAT and floating
+    IPs (as per Neutron terminology).
+    """
+
+    DEV_NAME_LEN = 14
+
+    def __init__(self, **device_params):
+        try:
+            self._csr_host = device_params['management_ip_address']
+            self._csr_ssh_port = device_params['protocol_port']
+            credentials = device_params['credentials']
+            if credentials:
+                self._csr_user = credentials['username']
+                self._csr_password = credentials['password']
+            self._timeout = cfg.CONF.device_connection_timeout
+            self._csr_conn = None
+            self._intfs_enabled = False
+        except KeyError as e:
+            LOG.error(_("Missing device parameter:%s. Aborting "
+                        "CSR1kvRoutingDriver initialization"), e)
+            raise cfg_exc.CSR1kvInitializationException
+
+    ###### Public Functions ########
+    def router_added(self, ri):
+        self._csr_create_vrf(ri)
+
+    def router_removed(self, ri):
+        self._csr_remove_vrf(ri)
+
+    def internal_network_added(self, ri, port):
+        self._csr_create_subinterface(ri, port)
+        if port.get('ha_info') is not None and ri.ha_info['ha:enabled']:
+            self._csr_add_ha(ri, port)
+
+    def internal_network_removed(self, ri, port):
+        self._csr_remove_subinterface(port)
+
+    def external_gateway_added(self, ri, ex_gw_port):
+        self._csr_create_subinterface(ri, ex_gw_port)
+        ex_gw_ip = ex_gw_port['subnet']['gateway_ip']
+        if ex_gw_ip:
+            #Set default route via this network's gateway ip
+            self._csr_add_default_route(ri, ex_gw_ip)
+
+    def external_gateway_removed(self, ri, ex_gw_port):
+        ex_gw_ip = ex_gw_port['subnet']['gateway_ip']
+        if ex_gw_ip:
+            #Remove default route via this network's gateway ip
+            self._csr_remove_default_route(ri, ex_gw_ip)
+        #Finally, remove external network subinterface
+        self._csr_remove_subinterface(ex_gw_port)
+
+    def enable_internal_network_NAT(self, ri, port, ex_gw_port):
+        self._csr_add_internalnw_nat_rules(ri, port, ex_gw_port)
+
+    def disable_internal_network_NAT(self, ri, port, ex_gw_port):
+        self._csr_remove_internalnw_nat_rules(ri, [port], ex_gw_port)
+
+    def floating_ip_added(self, ri, ex_gw_port, floating_ip, fixed_ip):
+        self._csr_add_floating_ip(ri, floating_ip, fixed_ip)
+
+    def floating_ip_removed(self, ri, ex_gw_port, floating_ip, fixed_ip):
+        self._csr_remove_floating_ip(ri, ex_gw_port, floating_ip, fixed_ip)
+
+    def routes_updated(self, ri, action, route):
+        self._csr_update_routing_table(ri, action, route)
+
+    def clear_connection(self):
+        self._csr_conn = None
+
+    ##### Internal Functions  ####
+
+    def _csr_create_subinterface(self, ri, port):
+        vrf_name = self._csr_get_vrf_name(ri)
+        ip_cidr = port['ip_cidr']
+        netmask = netaddr.IPNetwork(ip_cidr).netmask
+        gateway_ip = ip_cidr.split('/')[0]
+        subinterface = self._get_interface_name_from_hosting_port(port)
+        vlan = self._get_interface_vlan_from_hosting_port(port)
+        self._create_subinterface(subinterface, vlan, vrf_name,
+                                  gateway_ip, netmask)
+
+    def _csr_remove_subinterface(self, port):
+        subinterface = self._get_interface_name_from_hosting_port(port)
+        self._remove_subinterface(subinterface)
+
+    def _csr_add_ha(self, ri, port):
+        func_dict = {
+            'HSRP': CSR1kvRoutingDriver._csr_add_ha_HSRP,
+            'VRRP': CSR1kvRoutingDriver._csr_add_ha_VRRP,
+            'GBLP': CSR1kvRoutingDriver._csr_add_ha_GBLP
+        }
+        #Invoke the right function for the ha type
+        func_dict[ri.ha_info['ha:type']](self, ri, port)
+
+    def _csr_add_ha_HSRP(self, ri, port):
+        priority = ri.ha_info['priority']
+        port_ha_info = port['ha_info']
+        group = port_ha_info['group']
+        ip = port_ha_info['virtual_port']['fixed_ips'][0]['ip_address']
+        if ip and group and priority:
+            vrf_name = self._csr_get_vrf_name(ri)
+            subinterface = self._get_interface_name_from_hosting_port(port)
+            self._set_ha_HSRP(subinterface, vrf_name, priority, group, ip)
+
+    def _csr_add_ha_VRRP(self, ri, port):
+        raise NotImplementedError
+
+    def _csr_add_ha_GBLP(self, ri, port):
+        raise NotImplementedError
+
+    def _csr_remove_ha(self, ri, port):
+        pass
+
+    def _csr_add_internalnw_nat_rules(self, ri, port, ex_port):
+        vrf_name = self._csr_get_vrf_name(ri)
+        in_vlan = self._get_interface_vlan_from_hosting_port(port)
+        acl_no = 'acl_' + str(in_vlan)
+        internal_cidr = port['ip_cidr']
+        internal_net = netaddr.IPNetwork(internal_cidr).network
+        netmask = netaddr.IPNetwork(internal_cidr).hostmask
+        inner_intfc = self._get_interface_name_from_hosting_port(port)
+        outer_intfc = self._get_interface_name_from_hosting_port(ex_port)
+        self._nat_rules_for_internet_access(acl_no, internal_net,
+                                            netmask, inner_intfc,
+                                            outer_intfc, vrf_name)
+
+    def _csr_remove_internalnw_nat_rules(self, ri, ports, ex_port):
+        acls = []
+        #First disable nat in all inner ports
+        for port in ports:
+            in_intfc_name = self._get_interface_name_from_hosting_port(port)
+            inner_vlan = self._get_interface_vlan_from_hosting_port(port)
+            acls.append("acl_" + str(inner_vlan))
+            self._remove_interface_nat(in_intfc_name, 'inside')
+
+        #Wait for two second
+        LOG.debug("Sleep for 2 seconds before clearing NAT rules")
+        time.sleep(2)
+
+        #Clear the NAT translation table
+        self._remove_dyn_nat_translations()
+
+        # Remove dynamic NAT rules and ACLs
+        vrf_name = self._csr_get_vrf_name(ri)
+        ext_intfc_name = self._get_interface_name_from_hosting_port(ex_port)
+        for acl in acls:
+            self._remove_dyn_nat_rule(acl, ext_intfc_name, vrf_name)
+
+    def _csr_add_default_route(self, ri, gw_ip):
+        vrf_name = self._csr_get_vrf_name(ri)
+        self._add_default_static_route(gw_ip, vrf_name)
+
+    def _csr_remove_default_route(self, ri, gw_ip):
+        vrf_name = self._csr_get_vrf_name(ri)
+        self._remove_default_static_route(gw_ip, vrf_name)
+
+    def _csr_add_floating_ip(self, ri, floating_ip, fixed_ip):
+        vrf_name = self._csr_get_vrf_name(ri)
+        self._add_floating_ip(floating_ip, fixed_ip, vrf_name)
+
+    def _csr_remove_floating_ip(self, ri, ex_gw_port, floating_ip, fixed_ip):
+        vrf_name = self._csr_get_vrf_name(ri)
+        out_intfc_name = self._get_interface_name_from_hosting_port(ex_gw_port)
+        # First remove NAT from outer interface
+        self._remove_interface_nat(out_intfc_name, 'outside')
+        #Clear the NAT translation table
+        self._remove_dyn_nat_translations()
+        #Remove the floating ip
+        self._remove_floating_ip(floating_ip, fixed_ip, vrf_name)
+        #Enable NAT on outer interface
+        self._add_interface_nat(out_intfc_name, 'outside')
+
+    def _csr_update_routing_table(self, ri, action, route):
+        vrf_name = self._csr_get_vrf_name(ri)
+        destination_net = netaddr.IPNetwork(route['destination'])
+        dest = destination_net.network
+        dest_mask = destination_net.netmask
+        next_hop = route['nexthop']
+        if action is 'replace':
+            self._add_static_route(dest, dest_mask, next_hop, vrf_name)
+        elif action is 'delete':
+            self._remove_static_route(dest, dest_mask, next_hop, vrf_name)
+        else:
+            LOG.error(_('Unknown route command %s'), action)
+
+    def _csr_create_vrf(self, ri):
+        vrf_name = self._csr_get_vrf_name(ri)
+        self._create_vrf(vrf_name)
+
+    def _csr_remove_vrf(self, ri):
+        vrf_name = self._csr_get_vrf_name(ri)
+        self._remove_vrf(vrf_name)
+
+    def _csr_get_vrf_name(self, ri):
+        return ri.router_name()[:self.DEV_NAME_LEN]
+
+    def _get_connection(self):
+        """Make SSH connection to the CSR.
+
+        The external ncclient library is used for creating this connection.
+        This method keeps state of any existing connections and reuses them if
+        already connected. Also CSR1kv's interfaces (except management) are
+        disabled by default when it is booted. So if connecting for the first
+        time, driver will enable all other interfaces and keep that status in
+        the `_intfs_enabled` flag.
+        """
+        try:
+            if self._csr_conn and self._csr_conn.connected:
+                return self._csr_conn
+            else:
+                self._csr_conn = manager.connect(host=self._csr_host,
+                                                 port=self._csr_ssh_port,
+                                                 username=self._csr_user,
+                                                 password=self._csr_password,
+                                                 device_params={'name': "csr"},
+                                                 timeout=self._timeout)
+                if not self._intfs_enabled:
+                    self._intfs_enabled = self._enable_intfs(self._csr_conn)
+            return self._csr_conn
+        except Exception as e:
+            conn_params = {'host': self._csr_host, 'port': self._csr_ssh_port,
+                           'user': self._csr_user,
+                           'timeout': self._timeout, 'reason': e.message}
+            raise cfg_exc.CSR1kvConnectionException(**conn_params)
+
+    def _get_interface_name_from_hosting_port(self, port):
+        vlan = self._get_interface_vlan_from_hosting_port(port)
+        int_no = self._get_interface_no_from_hosting_port(port)
+        intfc_name = 'GigabitEthernet%s.%s' % (int_no, vlan)
+        return intfc_name
+
+    @staticmethod
+    def _get_interface_vlan_from_hosting_port(port):
+        return port['hosting_info']['segmentation_id']
+
+    @staticmethod
+    def _get_interface_no_from_hosting_port(port):
+        """Calculate interface number from the hosting port's name.
+
+         Interfaces in the CSR1kv are created in pairs (T1 and T2) where
+         T1 interface is used for VLAN and T2 interface for VXLAN traffic
+         respectively. On the neutron side these are named T1 and T2 ports and
+         follows the naming convention: <Tx_PORT_NAME_PREFIX>:<PAIR_INDEX>
+         where the `PORT_NAME_PREFIX` indicates either VLAN or VXLAN and
+         `PAIR_INDEX` is the pair number. `PAIR_INDEX` starts at 1.
+
+         In CSR1kv, GigabitEthernet 0 is not present and GigabitEthernet 1
+         is used as a management interface (Note: this might change in
+         future). So the first (T1,T2) pair corresponds to
+         (GigabitEthernet 2, GigabitEthernet 3) and so forth. This function
+         extracts the `PAIR_INDEX` and calculates the corresponding interface
+         number.
+
+        :param port: neutron port corresponding to the interface.
+        :return: number of the interface (eg: 1 in case of GigabitEthernet1)
+        """
+        _name = port['hosting_info']['hosting_port_name']
+        if_type = _name.split(':')[0] + ':'
+        if if_type == T1_PORT_NAME_PREFIX:
+            return str(int(_name.split(':')[1]) * 2)
+        elif if_type == T2_PORT_NAME_PREFIX:
+            return str(int(_name.split(':')[1]) * 2 + 1)
+        else:
+            params = {'attribute': 'hosting_port_name', 'value': _name}
+            raise cfg_exc.CSR1kvUnknownValueException(**params)
+
+    def _get_interfaces(self):
+        """Get a list of interfaces on this hosting device.
+
+        :return: List of the interfaces
+        """
+        ioscfg = self._get_running_config()
+        parse = ciscoconfparse.CiscoConfParse(ioscfg)
+        intfs_raw = parse.find_lines("^interface GigabitEthernet")
+        intfs = [raw_if.strip().split(' ')[1] for raw_if in intfs_raw]
+        LOG.info(_("Interfaces:%s"), intfs)
+        return intfs
+
+    def _get_interface_ip(self, interface_name):
+        """Get the ip address for an interface.
+
+        :param interface_name: interface_name as a string
+        :return: ip address of interface as a string
+        """
+        ioscfg = self._get_running_config()
+        parse = ciscoconfparse.CiscoConfParse(ioscfg)
+        children = parse.find_children("^interface %s" % interface_name)
+        for line in children:
+            if 'ip address' in line:
+                ip_address = line.strip().split(' ')[2]
+                LOG.info(_("IP Address:%s"), ip_address)
+                return ip_address
+        LOG.warn(_("Cannot find interface: %s"), interface_name)
+        return None
+
+    def _interface_exists(self, interface):
+        """Check whether interface exists."""
+        ioscfg = self._get_running_config()
+        parse = ciscoconfparse.CiscoConfParse(ioscfg)
+        intfs_raw = parse.find_lines("^interface " + interface)
+        return len(intfs_raw) > 0
+
+    def _enable_intfs(self, conn):
+        """Enable the interfaces of a CSR1kv Virtual Router.
+
+        When the virtual router first boots up, all interfaces except
+        management are down. This method will enable all data interfaces.
+
+        Note: In CSR1kv, GigabitEthernet 0 is not present. GigabitEthernet 1
+        is used as management and GigabitEthernet 2 and up are used for data.
+        This might change in future releases.
+
+        Currently only the second and third Gig interfaces corresponding to a
+        single (T1,T2) pair and configured as trunk for VLAN and VXLAN
+        is enabled.
+
+        :param conn: Connection object
+        :return: True or False
+        """
+
+        #ToDo(Hareesh): Interfaces are hard coded for now. Make it dynamic.
+        interfaces = ['GigabitEthernet 2', 'GigabitEthernet 3']
+        try:
+            for i in interfaces:
+                confstr = snippets.ENABLE_INTF % i
+                rpc_obj = conn.edit_config(target='running', config=confstr)
+                if self._check_response(rpc_obj, 'ENABLE_INTF'):
+                    LOG.info(_("Enabled interface %s "), i)
+                    time.sleep(1)
+        except Exception:
+            return False
+        return True
+
+    def _get_vrfs(self):
+        """Get the current VRFs configured in the device.
+
+        :return: A list of vrf names as string
+        """
+        vrfs = []
+        ioscfg = self._get_running_config()
+        parse = ciscoconfparse.CiscoConfParse(ioscfg)
+        vrfs_raw = parse.find_lines("^ip vrf")
+        for line in vrfs_raw:
+            #  raw format ['ip vrf <vrf-name>',....]
+            vrf_name = line.strip().split(' ')[2]
+            vrfs.append(vrf_name)
+        LOG.info(_("VRFs:%s"), vrfs)
+        return vrfs
+
+    def _get_capabilities(self):
+        """Get the servers NETCONF capabilities.
+
+        :return: List of server capabilities.
+        """
+        conn = self._get_connection()
+        capabilities = []
+        for c in conn.server_capabilities:
+            capabilities.append(c)
+        LOG.debug("Server capabilities: %s", capabilities)
+        return capabilities
+
+    def _get_running_config(self):
+        """Get the CSR's current running config.
+
+        :return: Current IOS running config as multiline string
+        """
+        conn = self._get_connection()
+        config = conn.get_config(source="running")
+        if config:
+            root = ET.fromstring(config._raw)
+            running_config = root[0][0]
+            rgx = re.compile("\r*\n+")
+            ioscfg = rgx.split(running_config.text)
+            return ioscfg
+
+    def _check_acl(self, acl_no, network, netmask):
+        """Check a ACL config exists in the running config.
+
+        :param acl_no: access control list (ACL) number
+        :param network: network which this ACL permits
+        :param netmask: netmask of the network
+        :return:
+        """
+        exp_cfg_lines = ['ip access-list standard ' + str(acl_no),
+                         ' permit ' + str(network) + ' ' + str(netmask)]
+        ioscfg = self._get_running_config()
+        parse = ciscoconfparse.CiscoConfParse(ioscfg)
+        acls_raw = parse.find_children(exp_cfg_lines[0])
+        if acls_raw:
+            if exp_cfg_lines[1] in acls_raw:
+                return True
+            LOG.error(_("Mismatch in ACL configuration for %s"), acl_no)
+            return False
+        LOG.debug("%s is not present in config", acl_no)
+        return False
+
+    def _cfg_exists(self, cfg_str):
+        """Check a partial config string exists in the running config.
+
+        :param cfg_str: config string to check
+        :return : True or False
+        """
+        ioscfg = self._get_running_config()
+        parse = ciscoconfparse.CiscoConfParse(ioscfg)
+        cfg_raw = parse.find_lines("^" + cfg_str)
+        LOG.debug("_cfg_exists(): Found lines %s", cfg_raw)
+        return len(cfg_raw) > 0
+
+    def _set_interface(self, name, ip_address, mask):
+        conn = self._get_connection()
+        confstr = snippets.SET_INTC % (name, ip_address, mask)
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'SET_INTC')
+
+    def _create_vrf(self, vrf_name):
+        try:
+            conn = self._get_connection()
+            confstr = snippets.CREATE_VRF % vrf_name
+            rpc_obj = conn.edit_config(target='running', config=confstr)
+            if self._check_response(rpc_obj, 'CREATE_VRF'):
+                LOG.info(_("VRF %s successfully created"), vrf_name)
+        except Exception:
+            LOG.exception(_("Failed creating VRF %s"), vrf_name)
+
+    def _remove_vrf(self, vrf_name):
+        if vrf_name in self._get_vrfs():
+            conn = self._get_connection()
+            confstr = snippets.REMOVE_VRF % vrf_name
+            rpc_obj = conn.edit_config(target='running', config=confstr)
+            if self._check_response(rpc_obj, 'REMOVE_VRF'):
+                LOG.info(_("VRF %s removed"), vrf_name)
+        else:
+            LOG.warning(_("VRF %s not present"), vrf_name)
+
+    def _create_subinterface(self, subinterface, vlan_id, vrf_name, ip, mask):
+        if vrf_name not in self._get_vrfs():
+            LOG.error(_("VRF %s not present"), vrf_name)
+        confstr = snippets.CREATE_SUBINTERFACE % (subinterface, vlan_id,
+                                                  vrf_name, ip, mask)
+        self._edit_running_config(confstr, 'CREATE_SUBINTERFACE')
+
+    def _remove_subinterface(self, subinterface):
+        #Optional : verify this is the correct subinterface
+        if self._interface_exists(subinterface):
+            confstr = snippets.REMOVE_SUBINTERFACE % subinterface
+            self._edit_running_config(confstr, 'REMOVE_SUBINTERFACE')
+
+    def _set_ha_HSRP(self, subinterface, vrf_name, priority, group, ip):
+        if vrf_name not in self._get_vrfs():
+            LOG.error(_("VRF %s not present"), vrf_name)
+        confstr = snippets.SET_INTC_HSRP % (subinterface, vrf_name, group,
+                                            priority, group, ip)
+        action = "SET_INTC_HSRP (Group: %s, Priority: % s)" % (group, priority)
+        self._edit_running_config(confstr, action)
+
+    def _remove_ha_HSRP(self, subinterface, group):
+        confstr = snippets.REMOVE_INTC_HSRP % (subinterface, group)
+        action = ("REMOVE_INTC_HSRP (subinterface:%s, Group:%s)"
+                  % (subinterface, group))
+        self._edit_running_config(confstr, action)
+
+    def _get_interface_cfg(self, interface):
+        ioscfg = self._get_running_config()
+        parse = ciscoconfparse.CiscoConfParse(ioscfg)
+        return parse.find_children('interface ' + interface)
+
+    def _nat_rules_for_internet_access(self, acl_no, network,
+                                       netmask,
+                                       inner_intfc,
+                                       outer_intfc,
+                                       vrf_name):
+        """Configure the NAT rules for an internal network.
+
+        Configuring NAT rules in the CSR1kv is a three step process. First
+        create an ACL for the IP range of the internal network. Then enable
+        dynamic source NATing on the external interface of the CSR for this
+        ACL and VRF of the neutron router. Finally enable NAT on the
+        interfaces of the CSR where the internal and external networks are
+        connected.
+
+        :param acl_no: ACL number of the internal network.
+        :param network: internal network
+        :param netmask: netmask of the internal network.
+        :param inner_intfc: (name of) interface connected to the internal
+        network
+        :param outer_intfc: (name of) interface connected to the external
+        network
+        :param vrf_name: VRF corresponding to this virtual router
+        :return: True if configuration succeeded
+        :raises: neutron.plugins.cisco.cfg_agent.cfg_exceptions.
+        CSR1kvConfigException
+        """
+        conn = self._get_connection()
+        # Duplicate ACL creation throws error, so checking
+        # it first. Remove it in future as this is not common in production
+        acl_present = self._check_acl(acl_no, network, netmask)
+        if not acl_present:
+            confstr = snippets.CREATE_ACL % (acl_no, network, netmask)
+            rpc_obj = conn.edit_config(target='running', config=confstr)
+            self._check_response(rpc_obj, 'CREATE_ACL')
+
+        confstr = snippets.SET_DYN_SRC_TRL_INTFC % (acl_no, outer_intfc,
+                                                    vrf_name)
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'CREATE_SNAT')
+
+        confstr = snippets.SET_NAT % (inner_intfc, 'inside')
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'SET_NAT')
+
+        confstr = snippets.SET_NAT % (outer_intfc, 'outside')
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'SET_NAT')
+
+    def _add_interface_nat(self, intfc_name, intfc_type):
+        conn = self._get_connection()
+        confstr = snippets.SET_NAT % (intfc_name, intfc_type)
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'SET_NAT ' + intfc_type)
+
+    def _remove_interface_nat(self, intfc_name, intfc_type):
+        conn = self._get_connection()
+        confstr = snippets.REMOVE_NAT % (intfc_name, intfc_type)
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'REMOVE_NAT ' + intfc_type)
+
+    def _remove_dyn_nat_rule(self, acl_no, outer_intfc_name, vrf_name):
+        conn = self._get_connection()
+        confstr = snippets.SNAT_CFG % (acl_no, outer_intfc_name, vrf_name)
+        if self._cfg_exists(confstr):
+            confstr = snippets.REMOVE_DYN_SRC_TRL_INTFC % (acl_no,
+                                                           outer_intfc_name,
+                                                           vrf_name)
+            rpc_obj = conn.edit_config(target='running', config=confstr)
+            self._check_response(rpc_obj, 'REMOVE_DYN_SRC_TRL_INTFC')
+
+        confstr = snippets.REMOVE_ACL % acl_no
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'REMOVE_ACL')
+
+    def _remove_dyn_nat_translations(self):
+        conn = self._get_connection()
+        confstr = snippets.CLEAR_DYN_NAT_TRANS
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'CLEAR_DYN_NAT_TRANS')
+
+    def _add_floating_ip(self, floating_ip, fixed_ip, vrf):
+        conn = self._get_connection()
+        confstr = snippets.SET_STATIC_SRC_TRL % (fixed_ip, floating_ip, vrf)
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'SET_STATIC_SRC_TRL')
+
+    def _remove_floating_ip(self, floating_ip, fixed_ip, vrf):
+        conn = self._get_connection()
+        confstr = snippets.REMOVE_STATIC_SRC_TRL % (fixed_ip, floating_ip, vrf)
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'REMOVE_STATIC_SRC_TRL')
+
+    def _get_floating_ip_cfg(self):
+        ioscfg = self._get_running_config()
+        parse = ciscoconfparse.CiscoConfParse(ioscfg)
+        res = parse.find_lines('ip nat inside source static')
+        return res
+
+    def _add_static_route(self, dest, dest_mask, next_hop, vrf):
+        conn = self._get_connection()
+        confstr = snippets.SET_IP_ROUTE % (vrf, dest, dest_mask, next_hop)
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'SET_IP_ROUTE')
+
+    def _remove_static_route(self, dest, dest_mask, next_hop, vrf):
+        conn = self._get_connection()
+        confstr = snippets.REMOVE_IP_ROUTE % (vrf, dest, dest_mask, next_hop)
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, 'REMOVE_IP_ROUTE')
+
+    def _get_static_route_cfg(self):
+        ioscfg = self._get_running_config()
+        parse = ciscoconfparse.CiscoConfParse(ioscfg)
+        return parse.find_lines('ip route')
+
+    def _add_default_static_route(self, gw_ip, vrf):
+        conn = self._get_connection()
+        confstr = snippets.DEFAULT_ROUTE_CFG % (vrf, gw_ip)
+        if not self._cfg_exists(confstr):
+            confstr = snippets.SET_DEFAULT_ROUTE % (vrf, gw_ip)
+            rpc_obj = conn.edit_config(target='running', config=confstr)
+            self._check_response(rpc_obj, 'SET_DEFAULT_ROUTE')
+
+    def _remove_default_static_route(self, gw_ip, vrf):
+        conn = self._get_connection()
+        confstr = snippets.DEFAULT_ROUTE_CFG % (vrf, gw_ip)
+        if self._cfg_exists(confstr):
+            confstr = snippets.REMOVE_DEFAULT_ROUTE % (vrf, gw_ip)
+            rpc_obj = conn.edit_config(target='running', config=confstr)
+            self._check_response(rpc_obj, 'REMOVE_DEFAULT_ROUTE')
+
+    def _edit_running_config(self, confstr, snippet):
+        conn = self._get_connection()
+        rpc_obj = conn.edit_config(target='running', config=confstr)
+        self._check_response(rpc_obj, snippet)
+
+    @staticmethod
+    def _check_response(rpc_obj, snippet_name):
+        """This function checks the rpc response object for status.
+
+        This function takes as input the response rpc_obj and the snippet name
+        that was executed. It parses it to see, if the last edit operation was
+        a success or not.
+            <?xml version="1.0" encoding="UTF-8"?>
+            <rpc-reply message-id="urn:uuid:81bf8082-....-b69a-000c29e1b85c"
+                       xmlns="urn:ietf:params:netconf:base:1.0">
+                <ok />
+            </rpc-reply>
+        In case of error, CSR1kv sends a response as follows.
+        We take the error type and tag.
+            <?xml version="1.0" encoding="UTF-8"?>
+            <rpc-reply message-id="urn:uuid:81bf8082-....-b69a-000c29e1b85c"
+            xmlns="urn:ietf:params:netconf:base:1.0">
+                <rpc-error>
+                    <error-type>protocol</error-type>
+                    <error-tag>operation-failed</error-tag>
+                    <error-severity>error</error-severity>
+                </rpc-error>
+            </rpc-reply>
+        :return: True if the config operation completed successfully
+        :raises: neutron.plugins.cisco.cfg_agent.cfg_exceptions.
+        CSR1kvConfigException
+        """
+        LOG.debug("RPCReply for %(snippet_name)s is %(rpc_obj)s",
+                  {'snippet_name': snippet_name, 'rpc_obj': rpc_obj.xml})
+        xml_str = rpc_obj.xml
+        if "<ok />" in xml_str:
+            LOG.debug("RPCReply for %s is OK", snippet_name)
+            LOG.info(_("%s successfully executed"), snippet_name)
+            return True
+        # Not Ok, we throw a ConfigurationException
+        e_type = rpc_obj._root[0][0].text
+        e_tag = rpc_obj._root[0][1].text
+        params = {'snippet': snippet_name, 'type': e_type, 'tag': e_tag}
+        raise cfg_exc.CSR1kvConfigException(**params)
diff --git a/neutron/plugins/cisco/cfg_agent/device_drivers/devicedriver_api.py b/neutron/plugins/cisco/cfg_agent/device_drivers/devicedriver_api.py
new file mode 100644 (file)
index 0000000..48c6016
--- /dev/null
@@ -0,0 +1,160 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+import abc
+import six
+
+
+@six.add_metaclass(abc.ABCMeta)
+class RoutingDriverBase(object):
+    """Base class that defines an abstract interface for the Routing Driver.
+
+    This class defines the abstract interface/API for the Routing and
+    NAT related operations. Driver class corresponding to a hosting device
+    should inherit this base driver and implement its methods.
+    RouterInfo object (neutron.plugins.cisco.cfg_agent.router_info.RouterInfo)
+    is a wrapper around the router dictionary, with attributes for easy access
+    to parameters.
+    """
+
+    @abc.abstractmethod
+    def router_added(self, router_info):
+        """A logical router was assigned to the hosting device.
+
+        :param router_info: RouterInfo object for this router
+        :return None
+        """
+        pass
+
+    @abc.abstractmethod
+    def router_removed(self, router_info):
+        """A logical router was de-assigned from the hosting device.
+
+        :param router_info: RouterInfo object for this router
+        :return None
+        """
+
+        pass
+
+    @abc.abstractmethod
+    def internal_network_added(self, router_info, port):
+        """An internal network was connected to a router.
+
+        :param router_info: RouterInfo object for this router
+        :param port : port dictionary for the port where the internal
+                      network is connected
+        :return None
+        """
+        pass
+
+    @abc.abstractmethod
+    def internal_network_removed(self, router_info, port):
+        """An internal network was removed from a router.
+
+        :param router_info: RouterInfo object for this router
+        :param port : port dictionary for the port where the internal
+                     network was connected
+        :return None
+        """
+        pass
+
+    @abc.abstractmethod
+    def external_gateway_added(self, router_info, ex_gw_port):
+        """An external network was added to a router.
+
+        :param router_info: RouterInfo object of the router
+        :param ex_gw_port : port dictionary for the port where the external
+                           gateway network is connected
+        :return None
+        """
+        pass
+
+    @abc.abstractmethod
+    def external_gateway_removed(self, router_info, ex_gw_port):
+        """An external network was removed from the router.
+
+        :param router_info: RouterInfo object of the router
+        :param ex_gw_port : port dictionary for the port where the external
+                           gateway network was connected
+        :return None
+        """
+        pass
+
+    @abc.abstractmethod
+    def enable_internal_network_NAT(self, router_info, port, ex_gw_port):
+        """Enable NAT on an internal network.
+
+        :param router_info: RouterInfo object for this router
+        :param port       : port dictionary for the port where the internal
+                           network is connected
+        :param ex_gw_port : port dictionary for the port where the external
+                           gateway network is connected
+        :return None
+        """
+        pass
+
+    @abc.abstractmethod
+    def disable_internal_network_NAT(self, router_info, port, ex_gw_port):
+        """Disable NAT on an internal network.
+
+        :param router_info: RouterInfo object for this router
+        :param port       : port dictionary for the port where the internal
+                           network is connected
+        :param ex_gw_port : port dictionary for the port where the external
+                           gateway network is connected
+        :return None
+        """
+        pass
+
+    @abc.abstractmethod
+    def floating_ip_added(self, router_info, ex_gw_port,
+                          floating_ip, fixed_ip):
+        """A floating IP was added.
+
+        :param router_info: RouterInfo object for this router
+        :param ex_gw_port : port dictionary for the port where the external
+                           gateway network is connected
+        :param floating_ip: Floating IP as a string
+        :param fixed_ip   : Fixed IP of internal internal interface as
+                           a string
+        :return None
+        """
+        pass
+
+    @abc.abstractmethod
+    def floating_ip_removed(self, router_info, ex_gw_port,
+                            floating_ip, fixed_ip):
+        """A floating IP was removed.
+
+        :param router_info: RouterInfo object for this router
+        :param ex_gw_port : port dictionary for the port where the external
+                            gateway network is connected
+        :param floating_ip: Floating IP as a string
+        :param fixed_ip: Fixed IP of internal internal interface as a string
+        :return None
+        """
+        pass
+
+    @abc.abstractmethod
+    def routes_updated(self, router_info, action, route):
+        """Routes were updated for router.
+
+        :param router_info: RouterInfo object for this router
+        :param action : Action on the route , either 'replace' or 'delete'
+        :param route: route dictionary with keys 'destination' & 'next_hop'
+        :return None
+        """
+        pass
diff --git a/neutron/plugins/cisco/cfg_agent/device_drivers/driver_mgr.py b/neutron/plugins/cisco/cfg_agent/device_drivers/driver_mgr.py
new file mode 100644 (file)
index 0000000..c04c96b
--- /dev/null
@@ -0,0 +1,98 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+from neutron.openstack.common import excutils
+from neutron.openstack.common import importutils
+from neutron.openstack.common import log as logging
+from neutron.plugins.cisco.cfg_agent import cfg_exceptions
+
+LOG = logging.getLogger(__name__)
+
+
+class DeviceDriverManager(object):
+    """This class acts as a manager for device drivers.
+
+    The device driver manager  maintains the relationship between the
+    different neutron logical resource (eg: routers, firewalls, vpns etc.) and
+    where they are hosted. For configuring a logical resource (router) in a
+    hosting device, a corresponding device driver object is used.
+    Device drivers encapsulate the necessary configuration information to
+    configure a logical resource (eg: routers, firewalls, vpns etc.) on a
+    hosting device (eg: CSR1kv).
+
+    The device driver class loads one driver object per hosting device.
+    The loaded drivers are cached in memory, so when a request is made to
+    get driver object for the same hosting device and resource (like router),
+    the existing driver object is reused.
+
+    This class is used by the service helper classes.
+    """
+
+    def __init__(self):
+        self._drivers = {}
+        self._hosting_device_routing_drivers_binding = {}
+
+    def get_driver(self, resource_id):
+        try:
+            return self._drivers[resource_id]
+        except KeyError:
+            with excutils.save_and_reraise_exception(reraise=False):
+                raise cfg_exceptions.DriverNotFound(id=resource_id)
+
+    def set_driver(self, resource):
+        """Set the driver for a neutron resource.
+
+        :param resource: Neutron resource in dict format. Expected keys:
+                        { 'id': <value>
+                          'hosting_device': { 'id': <value>, }
+                          'router_type': {'cfg_agent_driver': <value>,  }
+                        }
+        :return driver : driver object
+        """
+        try:
+            resource_id = resource['id']
+            hosting_device = resource['hosting_device']
+            hd_id = hosting_device['id']
+            if hd_id in self._hosting_device_routing_drivers_binding:
+                driver = self._hosting_device_routing_drivers_binding[hd_id]
+                self._drivers[resource_id] = driver
+            else:
+                driver_class = resource['router_type']['cfg_agent_driver']
+                driver = importutils.import_object(driver_class,
+                                                   **hosting_device)
+                self._hosting_device_routing_drivers_binding[hd_id] = driver
+                self._drivers[resource_id] = driver
+            return driver
+        except ImportError:
+            LOG.exception(_("Error loading cfg agent driver %(driver)s for "
+                            "hosting device template %(t_name)s(%(t_id)s)"),
+                          {'driver': driver_class, 't_id': hd_id,
+                           't_name': hosting_device['name']})
+            with excutils.save_and_reraise_exception(reraise=False):
+                raise cfg_exceptions.DriverNotExist(driver=driver_class)
+        except KeyError as e:
+            with excutils.save_and_reraise_exception(reraise=False):
+                raise cfg_exceptions.DriverNotSetForMissingParameter(e)
+
+    def remove_driver(self, resource_id):
+        """Remove driver associated to a particular resource."""
+        if resource_id in self._drivers:
+            del self._drivers[resource_id]
+
+    def remove_driver_for_hosting_device(self, hd_id):
+        """Remove driver associated to a particular hosting device."""
+        if hd_id in self._hosting_device_routing_drivers_binding:
+            del self._hosting_device_routing_drivers_binding[hd_id]
diff --git a/neutron/plugins/cisco/cfg_agent/device_drivers/dummy_driver.py b/neutron/plugins/cisco/cfg_agent/device_drivers/dummy_driver.py
new file mode 100644 (file)
index 0000000..926cc47
--- /dev/null
@@ -0,0 +1,77 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+import json
+import logging
+
+from neutron.plugins.cisco.cfg_agent.device_drivers import devicedriver_api
+
+LOG = logging.getLogger(__name__)
+
+
+class DummyRoutingDriver(devicedriver_api.RoutingDriverBase):
+    """Dummy Routing Driver.
+
+    This class emulates a routing driver without a real backing device.
+    """
+
+    def __init__(self, **device_params):
+        my_device_params = device_params
+        # Datetime values causes json decoding errors. So removing it locally
+        if my_device_params.get('created_at'):
+            del my_device_params['created_at']
+        LOG.debug(json.dumps(my_device_params, sort_keys=True, indent=4))
+
+    ###### Public Functions ########
+    def router_added(self, ri):
+        LOG.debug("DummyDriver router_added() called.")
+
+    def router_removed(self, ri):
+        LOG.debug("DummyDriver router_removed() called.")
+
+    def internal_network_added(self, ri, port):
+        LOG.debug("DummyDriver internal_network_added() called.")
+        LOG.debug("Int port data: " + json.dumps(port, sort_keys=True,
+                  indent=4))
+
+    def internal_network_removed(self, ri, port):
+        LOG.debug("DummyDriver internal_network_removed() called.")
+
+    def external_gateway_added(self, ri, ex_gw_port):
+        LOG.debug("DummyDriver external_gateway_added() called.")
+        LOG.debug("Ext port data: " + json.dumps(ex_gw_port, sort_keys=True,
+                  indent=4))
+
+    def external_gateway_removed(self, ri, ex_gw_port):
+        LOG.debug("DummyDriver external_gateway_removed() called.")
+
+    def enable_internal_network_NAT(self, ri, port, ex_gw_port):
+        LOG.debug("DummyDriver external_gateway_added() called.")
+
+    def disable_internal_network_NAT(self, ri, port, ex_gw_port):
+        LOG.debug("DummyDriver disable_internal_network_NAT() called.")
+
+    def floating_ip_added(self, ri, ex_gw_port, floating_ip, fixed_ip):
+        LOG.debug("DummyDriver floating_ip_added() called.")
+
+    def floating_ip_removed(self, ri, ex_gw_port, floating_ip, fixed_ip):
+        LOG.debug("DummyDriver floating_ip_removed() called.")
+
+    def routes_updated(self, ri, action, route):
+        LOG.debug("DummyDriver routes_updated() called.")
+
+    def clear_connection(self):
+        LOG.debug("DummyDriver clear_connection() called.")
diff --git a/neutron/plugins/cisco/cfg_agent/device_status.py b/neutron/plugins/cisco/cfg_agent/device_status.py
new file mode 100644 (file)
index 0000000..b5c5007
--- /dev/null
@@ -0,0 +1,174 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+import datetime
+
+from oslo.config import cfg
+
+from neutron.agent.linux import utils as linux_utils
+from neutron.openstack.common import log as logging
+from neutron.openstack.common import timeutils
+
+LOG = logging.getLogger(__name__)
+
+
+STATUS_OPTS = [
+    cfg.IntOpt('device_connection_timeout', default=30,
+               help=_("Time in seconds for connecting to a hosting device")),
+    cfg.IntOpt('hosting_device_dead_timeout', default=300,
+               help=_("The time in seconds until a backlogged hosting device "
+                      "is presumed dead. This value should be set up high "
+                      "enough to recover from a period of connectivity loss "
+                      "or high load when the device may not be responding.")),
+]
+
+cfg.CONF.register_opts(STATUS_OPTS)
+
+
+def _is_pingable(ip):
+    """Checks whether an IP address is reachable by pinging.
+
+    Use linux utils to execute the ping (ICMP ECHO) command.
+    Sends 5 packets with an interval of 0.2 seconds and timeout of 1
+    seconds. Runtime error implies unreachability else IP is pingable.
+    :param ip: IP to check
+    :return: bool - True or False depending on pingability.
+    """
+    ping_cmd = ['ping',
+                '-c', '5',
+                '-W', '1',
+                '-i', '0.2',
+                ip]
+    try:
+        linux_utils.execute(ping_cmd, check_exit_code=True)
+        return True
+    except RuntimeError:
+        LOG.warn(_("Cannot ping ip address: %s"), ip)
+        return False
+
+
+class DeviceStatus(object):
+    """Device status and backlog processing."""
+
+    _instance = None
+
+    def __new__(cls):
+        if not cls._instance:
+            cls._instance = super(DeviceStatus, cls).__new__(cls)
+        return cls._instance
+
+    def __init__(self):
+        self.backlog_hosting_devices = {}
+
+    def get_backlogged_hosting_devices(self):
+        return self.backlog_hosting_devices.keys()
+
+    def get_backlogged_hosting_devices_info(self):
+        wait_time = datetime.timedelta(
+            seconds=cfg.CONF.hosting_device_dead_timeout)
+        resp = []
+        for hd_id in self.backlog_hosting_devices:
+            hd = self.backlog_hosting_devices[hd_id]['hd']
+            created_time = hd['created_at']
+            boottime = datetime.timedelta(seconds=hd['booting_time'])
+            backlogged_at = hd['backlog_insertion_ts']
+            booted_at = created_time + boottime
+            dead_at = backlogged_at + wait_time
+            resp.append({'host id': hd['id'],
+                         'created at': str(created_time),
+                         'backlogged at': str(backlogged_at),
+                         'estimate booted at': str(booted_at),
+                         'considered dead at': str(dead_at)})
+        return resp
+
+    def is_hosting_device_reachable(self, hosting_device):
+        """Check the hosting device which hosts this resource is reachable.
+
+        If the resource is not reachable, it is added to the backlog.
+
+        :param hosting_device : dict of the hosting device
+        :return True if device is reachable, else None
+        """
+        hd = hosting_device
+        hd_id = hosting_device['id']
+        hd_mgmt_ip = hosting_device['management_ip_address']
+        # Modifying the 'created_at' to a date time object
+        hosting_device['created_at'] = datetime.datetime.strptime(
+            hosting_device['created_at'], '%Y-%m-%d %H:%M:%S')
+
+        if hd_id not in self.backlog_hosting_devices:
+            if _is_pingable(hd_mgmt_ip):
+                LOG.debug("Hosting device: %(hd_id)s@%(ip)s is reachable.",
+                          {'hd_id': hd_id, 'ip': hd_mgmt_ip})
+                return True
+            LOG.debug("Hosting device: %(hd_id)s@%(ip)s is NOT reachable.",
+                      {'hd_id': hd_id, 'ip': hd_mgmt_ip})
+            hd['backlog_insertion_ts'] = max(
+                timeutils.utcnow(),
+                hd['created_at'] +
+                datetime.timedelta(seconds=hd['booting_time']))
+            self.backlog_hosting_devices[hd_id] = {'hd': hd}
+            LOG.debug("Hosting device: %(hd_id)s @ %(ip)s is now added "
+                      "to backlog", {'hd_id': hd_id, 'ip': hd_mgmt_ip})
+
+    def check_backlogged_hosting_devices(self):
+        """"Checks the status of backlogged hosting devices.
+
+        Skips newly spun up instances during their booting time as specified
+        in the boot time parameter.
+
+        :return A dict of the format:
+        {'reachable': [<hd_id>,..], 'dead': [<hd_id>,..]}
+        """
+        response_dict = {'reachable': [], 'dead': []}
+        LOG.debug("Current Backlogged hosting devices: %s",
+                  self.backlog_hosting_devices.keys())
+        for hd_id in self.backlog_hosting_devices.keys():
+            hd = self.backlog_hosting_devices[hd_id]['hd']
+            if not timeutils.is_older_than(hd['created_at'],
+                                           hd['booting_time']):
+                LOG.info(_("Hosting device: %(hd_id)s @ %(ip)s hasn't passed "
+                           "minimum boot time. Skipping it. "),
+                         {'hd_id': hd_id, 'ip': hd['management_ip_address']})
+                continue
+            LOG.info(_("Checking hosting device: %(hd_id)s @ %(ip)s for "
+                       "reachability."), {'hd_id': hd_id,
+                                          'ip': hd['management_ip_address']})
+            if _is_pingable(hd['management_ip_address']):
+                hd.pop('backlog_insertion_ts', None)
+                del self.backlog_hosting_devices[hd_id]
+                response_dict['reachable'].append(hd_id)
+                LOG.info(_("Hosting device: %(hd_id)s @ %(ip)s is now "
+                           "reachable. Adding it to response"),
+                         {'hd_id': hd_id, 'ip': hd['management_ip_address']})
+            else:
+                LOG.info(_("Hosting device: %(hd_id)s @ %(ip)s still not "
+                           "reachable "), {'hd_id': hd_id,
+                                           'ip': hd['management_ip_address']})
+                if timeutils.is_older_than(
+                        hd['backlog_insertion_ts'],
+                        cfg.CONF.hosting_device_dead_timeout):
+                    LOG.debug("Hosting device: %(hd_id)s @ %(ip)s hasn't "
+                              "been reachable for the last %(time)d seconds. "
+                              "Marking it dead.",
+                              {'hd_id': hd_id,
+                               'ip': hd['management_ip_address'],
+                               'time': cfg.CONF.hosting_device_dead_timeout})
+                    response_dict['dead'].append(hd_id)
+                    hd.pop('backlog_insertion_ts', None)
+                    del self.backlog_hosting_devices[hd_id]
+        LOG.debug("Response: %s", response_dict)
+        return response_dict
diff --git a/neutron/plugins/cisco/cfg_agent/service_helpers/__init__.py b/neutron/plugins/cisco/cfg_agent/service_helpers/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/neutron/plugins/cisco/cfg_agent/service_helpers/routing_svc_helper.py b/neutron/plugins/cisco/cfg_agent/service_helpers/routing_svc_helper.py
new file mode 100644 (file)
index 0000000..474efc0
--- /dev/null
@@ -0,0 +1,639 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+import collections
+import eventlet
+import netaddr
+
+from neutron.common import constants as l3_constants
+from neutron.common import rpc as n_rpc
+from neutron.common import topics
+from neutron.common import utils as common_utils
+from neutron import context as n_context
+from neutron.openstack.common import excutils
+from neutron.openstack.common import log as logging
+
+from neutron.plugins.cisco.cfg_agent import cfg_exceptions
+from neutron.plugins.cisco.cfg_agent.device_drivers import driver_mgr
+from neutron.plugins.cisco.cfg_agent import device_status
+from neutron.plugins.cisco.common import cisco_constants as c_constants
+
+LOG = logging.getLogger(__name__)
+
+N_ROUTER_PREFIX = 'nrouter-'
+
+
+class RouterInfo(object):
+    """Wrapper class around the (neutron) router dictionary.
+
+    Information about the neutron router is exchanged as a python dictionary
+    between plugin and config agent. RouterInfo is a wrapper around that dict,
+    with attributes for common parameters. These attributes keep the state
+    of the current router configuration, and are used for detecting router
+    state changes when an updated router dict is received.
+
+    This is a modified version of the RouterInfo class defined in the
+    (reference) l3-agent implementation, for use with cisco config agent.
+    """
+
+    def __init__(self, router_id, router):
+        self.router_id = router_id
+        self.ex_gw_port = None
+        self._snat_enabled = None
+        self._snat_action = None
+        self.internal_ports = []
+        self.floating_ips = []
+        self._router = None
+        self.router = router
+        self.routes = []
+        self.ha_info = router.get('ha_info')
+
+    @property
+    def router(self):
+        return self._router
+
+    @property
+    def id(self):
+        return self.router_id
+
+    @property
+    def snat_enabled(self):
+        return self._snat_enabled
+
+    @router.setter
+    def router(self, value):
+        self._router = value
+        if not self._router:
+            return
+        # enable_snat by default if it wasn't specified by plugin
+        self._snat_enabled = self._router.get('enable_snat', True)
+
+    def router_name(self):
+        return N_ROUTER_PREFIX + self.router_id
+
+
+class CiscoRoutingPluginApi(n_rpc.RpcProxy):
+    """RoutingServiceHelper(Agent) side of the  routing RPC API."""
+
+    BASE_RPC_API_VERSION = '1.1'
+
+    def __init__(self, topic, host):
+        super(CiscoRoutingPluginApi, self).__init__(
+            topic=topic, default_version=self.BASE_RPC_API_VERSION)
+        self.host = host
+
+    def get_routers(self, context, router_ids=None, hd_ids=None):
+        """Make a remote process call to retrieve the sync data for routers.
+
+        :param context: session context
+        :param router_ids: list of  routers to fetch
+        :param hd_ids : hosting device ids, only routers assigned to these
+                        hosting devices will be returned.
+        """
+        return self.call(context,
+                         self.make_msg('cfg_sync_routers',
+                                       host=self.host,
+                                       router_ids=router_ids,
+                                       hosting_device_ids=hd_ids),
+                         topic=self.topic)
+
+
+class RoutingServiceHelper():
+
+    def __init__(self, host, conf, cfg_agent):
+        self.conf = conf
+        self.cfg_agent = cfg_agent
+        self.context = n_context.get_admin_context_without_session()
+        self.plugin_rpc = CiscoRoutingPluginApi(topics.L3PLUGIN, host)
+        self._dev_status = device_status.DeviceStatus()
+        self._drivermgr = driver_mgr.DeviceDriverManager()
+
+        self.router_info = {}
+        self.updated_routers = set()
+        self.removed_routers = set()
+        self.sync_devices = set()
+        self.fullsync = True
+        self.topic = '%s.%s' % (c_constants.CFG_AGENT_L3_ROUTING, host)
+
+        self._setup_rpc()
+
+    def _setup_rpc(self):
+        self.conn = n_rpc.create_connection(new=True)
+        self.endpoints = [self]
+        self.conn.create_consumer(self.topic, self.endpoints, fanout=False)
+        self.conn.consume_in_threads()
+
+    ### Notifications from Plugin ####
+
+    def router_deleted(self, context, routers):
+        """Deal with router deletion RPC message."""
+        LOG.debug('Got router deleted notification for %s', routers)
+        self.removed_routers.update(routers)
+
+    def routers_updated(self, context, routers):
+        """Deal with routers modification and creation RPC message."""
+        LOG.debug('Got routers updated notification :%s', routers)
+        if routers:
+            # This is needed for backward compatibility
+            if isinstance(routers[0], dict):
+                routers = [router['id'] for router in routers]
+            self.updated_routers.update(routers)
+
+    def router_removed_from_agent(self, context, payload):
+        LOG.debug('Got router removed from agent :%r', payload)
+        self.removed_routers.add(payload['router_id'])
+
+    def router_added_to_agent(self, context, payload):
+        LOG.debug('Got router added to agent :%r', payload)
+        self.routers_updated(context, payload)
+
+    # Routing service helper public methods
+
+    def process_service(self, device_ids=None, removed_devices_info=None):
+        try:
+            LOG.debug("Routing service processing started")
+            resources = {}
+            routers = []
+            removed_routers = []
+            all_routers_flag = False
+            if self.fullsync:
+                LOG.debug("FullSync flag is on. Starting fullsync")
+                # Setting all_routers_flag and clear the global full_sync flag
+                all_routers_flag = True
+                self.fullsync = False
+                self.updated_routers.clear()
+                self.removed_routers.clear()
+                self.sync_devices.clear()
+                routers = self._fetch_router_info(all_routers=True)
+            else:
+                if self.updated_routers:
+                    router_ids = list(self.updated_routers)
+                    LOG.debug("Updated routers:%s", router_ids)
+                    self.updated_routers.clear()
+                    routers = self._fetch_router_info(router_ids=router_ids)
+                if device_ids:
+                    LOG.debug("Adding new devices:%s", device_ids)
+                    self.sync_devices = set(device_ids) | self.sync_devices
+                if self.sync_devices:
+                    sync_devices_list = list(self.sync_devices)
+                    LOG.debug("Fetching routers on:%s", sync_devices_list)
+                    routers.extend(self._fetch_router_info(
+                        device_ids=sync_devices_list))
+                    self.sync_devices.clear()
+                if removed_devices_info:
+                    if removed_devices_info.get('deconfigure'):
+                        ids = self._get_router_ids_from_removed_devices_info(
+                            removed_devices_info)
+                        self.removed_routers = self.removed_routers | set(ids)
+                if self.removed_routers:
+                    removed_routers_ids = list(self.removed_routers)
+                    LOG.debug("Removed routers:%s", removed_routers_ids)
+                    for r in removed_routers_ids:
+                        if r in self.router_info:
+                            removed_routers.append(self.router_info[r].router)
+
+            # Sort on hosting device
+            if routers:
+                resources['routers'] = routers
+            if removed_routers:
+                resources['removed_routers'] = removed_routers
+            hosting_devices = self._sort_resources_per_hosting_device(
+                resources)
+
+            # Dispatch process_services() for each hosting device
+            pool = eventlet.GreenPool()
+            for device_id, resources in hosting_devices.items():
+                routers = resources.get('routers')
+                removed_routers = resources.get('removed_routers')
+                pool.spawn_n(self._process_routers, routers, removed_routers,
+                             device_id, all_routers=all_routers_flag)
+            pool.waitall()
+            if removed_devices_info:
+                for hd_id in removed_devices_info['hosting_data']:
+                    self._drivermgr.remove_driver_for_hosting_device(hd_id)
+            LOG.debug("Routing service processing successfully completed")
+        except Exception:
+            LOG.exception(_("Failed processing routers"))
+            self.fullsync = True
+
+    def collect_state(self, configurations):
+        """Collect state from this helper.
+
+        A set of attributes which summarizes the state of the routers and
+        configurations managed by this config agent.
+        :param configurations: dict of configuration values
+        :return dict of updated configuration values
+        """
+        num_ex_gw_ports = 0
+        num_interfaces = 0
+        num_floating_ips = 0
+        router_infos = self.router_info.values()
+        num_routers = len(router_infos)
+        num_hd_routers = collections.defaultdict(int)
+        for ri in router_infos:
+            ex_gw_port = ri.router.get('gw_port')
+            if ex_gw_port:
+                num_ex_gw_ports += 1
+            num_interfaces += len(ri.router.get(
+                l3_constants.INTERFACE_KEY, []))
+            num_floating_ips += len(ri.router.get(
+                l3_constants.FLOATINGIP_KEY, []))
+            hd = ri.router['hosting_device']
+            if hd:
+                num_hd_routers[hd['id']] += 1
+        routers_per_hd = dict((hd_id, {'routers': num})
+                              for hd_id, num in num_hd_routers.items())
+        non_responding = self._dev_status.get_backlogged_hosting_devices()
+        configurations['total routers'] = num_routers
+        configurations['total ex_gw_ports'] = num_ex_gw_ports
+        configurations['total interfaces'] = num_interfaces
+        configurations['total floating_ips'] = num_floating_ips
+        configurations['hosting_devices'] = routers_per_hd
+        configurations['non_responding_hosting_devices'] = non_responding
+        return configurations
+
+    # Routing service helper internal methods
+
+    def _fetch_router_info(self, router_ids=None, device_ids=None,
+                           all_routers=False):
+        """Fetch router dict from the routing plugin.
+
+        :param router_ids: List of router_ids of routers to fetch
+        :param device_ids: List of device_ids whose routers to fetch
+        :param all_routers:  If True fetch all the routers for this agent.
+        :return: List of router dicts of format:
+                 [ {router_dict1}, {router_dict2},.....]
+        """
+        try:
+            if all_routers:
+                return self.plugin_rpc.get_routers(self.context)
+            if router_ids:
+                return self.plugin_rpc.get_routers(self.context,
+                                                   router_ids=router_ids)
+            if device_ids:
+                return self.plugin_rpc.get_routers(self.context,
+                                                   hd_ids=device_ids)
+        except n_rpc.RPCException:
+            LOG.exception(_("RPC Error in fetching routers from plugin"))
+            self.fullsync = True
+
+    @staticmethod
+    def _get_router_ids_from_removed_devices_info(removed_devices_info):
+        """Extract router_ids from the removed devices info dict.
+
+        :param removed_devices_info: Dict of removed devices and their
+               associated resources.
+        Format:
+                {
+                  'hosting_data': {'hd_id1': {'routers': [id1, id2, ...]},
+                                   'hd_id2': {'routers': [id3, id4, ...]},
+                                   ...
+                                  },
+                  'deconfigure': True/False
+                }
+        :return removed_router_ids: List of removed router ids
+        """
+        removed_router_ids = []
+        for hd_id, resources in removed_devices_info['hosting_data'].items():
+            removed_router_ids += resources.get('routers', [])
+        return removed_router_ids
+
+    @staticmethod
+    def _sort_resources_per_hosting_device(resources):
+        """This function will sort the resources on hosting device.
+
+        The sorting on hosting device is done by looking up the
+        `hosting_device` attribute of the resource, and its `id`.
+
+        :param resources: a dict with key of resource name
+        :return dict sorted on the hosting device of input resource. Format:
+        hosting_devices = {
+                            'hd_id1' : {'routers':[routers],
+                                        'removed_routers':[routers], .... }
+                            'hd_id2' : {'routers':[routers], .. }
+                            .......
+                            }
+        """
+        hosting_devices = {}
+        for key in resources.keys():
+            for r in resources.get(key) or []:
+                hd_id = r['hosting_device']['id']
+                hosting_devices.setdefault(hd_id, {})
+                hosting_devices[hd_id].setdefault(key, []).append(r)
+        return hosting_devices
+
+    def _process_routers(self, routers, removed_routers,
+                         device_id=None, all_routers=False):
+        """Process the set of routers.
+
+        Iterating on the set of routers received and comparing it with the
+        set of routers already in the routing service helper, new routers
+        which are added are identified. Before processing check the
+        reachability (via ping) of hosting device where the router is hosted.
+        If device is not reachable it is backlogged.
+
+        For routers which are only updated, call `_process_router()` on them.
+
+        When all_routers is set to True (because of a full sync),
+        this will result in the detection and deletion of routers which
+        have been removed.
+
+        Whether the router can only be assigned to a particular hosting device
+        is decided and enforced by the plugin. No checks are done here.
+
+        :param routers: The set of routers to be processed
+        :param removed_routers: the set of routers which where removed
+        :param device_id: Id of the hosting device
+        :param all_routers: Flag for specifying a partial list of routers
+        :return: None
+        """
+        try:
+            if all_routers:
+                prev_router_ids = set(self.router_info)
+            else:
+                prev_router_ids = set(self.router_info) & set(
+                    [router['id'] for router in routers])
+            cur_router_ids = set()
+            for r in routers:
+                try:
+                    if not r['admin_state_up']:
+                        continue
+                    cur_router_ids.add(r['id'])
+                    hd = r['hosting_device']
+                    if not self._dev_status.is_hosting_device_reachable(hd):
+                        LOG.info(_("Router: %(id)s is on an unreachable "
+                                   "hosting device. "), {'id': r['id']})
+                        continue
+                    if r['id'] not in self.router_info:
+                        self._router_added(r['id'], r)
+                    ri = self.router_info[r['id']]
+                    ri.router = r
+                    self._process_router(ri)
+                except KeyError as e:
+                    LOG.exception(_("Key Error, missing key: %s"), e)
+                    self.updated_routers.add(r['id'])
+                    continue
+                except cfg_exceptions.DriverException as e:
+                    LOG.exception(_("Driver Exception on router:%(id)s. "
+                                    "Error is %(e)s"), {'id': r['id'], 'e': e})
+                    self.updated_routers.update(r['id'])
+                    continue
+            # identify and remove routers that no longer exist
+            for router_id in prev_router_ids - cur_router_ids:
+                self._router_removed(router_id)
+            if removed_routers:
+                for router in removed_routers:
+                    self._router_removed(router['id'])
+        except Exception:
+            LOG.exception(_("Exception in processing routers on device:%s"),
+                          device_id)
+            self.sync_devices.add(device_id)
+
+    def _process_router(self, ri):
+        """Process a router, apply latest configuration and update router_info.
+
+        Get the router dict from  RouterInfo and proceed to detect changes
+        from the last known state. When new ports or deleted ports are
+        detected, `internal_network_added()` or `internal_networks_removed()`
+        are called accordingly. Similarly changes in ex_gw_port causes
+         `external_gateway_added()` or `external_gateway_removed()` calls.
+        Next, floating_ips and routes are processed. Also, latest state is
+        stored in ri.internal_ports and ri.ex_gw_port for future comparisons.
+
+        :param ri : RouterInfo object of the router being processed.
+        :return:None
+        :raises: neutron.plugins.cisco.cfg_agent.cfg_exceptions.DriverException
+        if the configuration operation fails.
+        """
+        try:
+            ex_gw_port = ri.router.get('gw_port')
+            ri.ha_info = ri.router.get('ha_info', None)
+            internal_ports = ri.router.get(l3_constants.INTERFACE_KEY, [])
+            existing_port_ids = set([p['id'] for p in ri.internal_ports])
+            current_port_ids = set([p['id'] for p in internal_ports
+                                    if p['admin_state_up']])
+            new_ports = [p for p in internal_ports
+                         if
+                         p['id'] in (current_port_ids - existing_port_ids)]
+            old_ports = [p for p in ri.internal_ports
+                         if p['id'] not in current_port_ids]
+
+            for p in new_ports:
+                self._set_subnet_info(p)
+                self._internal_network_added(ri, p, ex_gw_port)
+                ri.internal_ports.append(p)
+
+            for p in old_ports:
+                self._internal_network_removed(ri, p, ri.ex_gw_port)
+                ri.internal_ports.remove(p)
+
+            if ex_gw_port and not ri.ex_gw_port:
+                self._set_subnet_info(ex_gw_port)
+                self._external_gateway_added(ri, ex_gw_port)
+            elif not ex_gw_port and ri.ex_gw_port:
+                self._external_gateway_removed(ri, ri.ex_gw_port)
+
+            if ex_gw_port:
+                self._process_router_floating_ips(ri, ex_gw_port)
+
+            ri.ex_gw_port = ex_gw_port
+            self._routes_updated(ri)
+        except cfg_exceptions.DriverException as e:
+            with excutils.save_and_reraise_exception():
+                self.updated_routers.update(ri.router_id)
+                LOG.error(e)
+
+    def _process_router_floating_ips(self, ri, ex_gw_port):
+        """Process a router's floating ips.
+
+        Compare current floatingips (in ri.floating_ips) with the router's
+        updated floating ips (in ri.router.floating_ips) and detect
+        flaoting_ips which were added or removed. Notify driver of
+        the change via `floating_ip_added()` or `floating_ip_removed()`.
+
+        :param ri:  RouterInfo object of the router being processed.
+        :param ex_gw_port: Port dict of the external gateway port.
+        :return: None
+        :raises: neutron.plugins.cisco.cfg_agent.cfg_exceptions.DriverException
+        if the configuration operation fails.
+        """
+
+        floating_ips = ri.router.get(l3_constants.FLOATINGIP_KEY, [])
+        existing_floating_ip_ids = set(
+            [fip['id'] for fip in ri.floating_ips])
+        cur_floating_ip_ids = set([fip['id'] for fip in floating_ips])
+
+        id_to_fip_map = {}
+
+        for fip in floating_ips:
+            if fip['port_id']:
+                # store to see if floatingip was remapped
+                id_to_fip_map[fip['id']] = fip
+                if fip['id'] not in existing_floating_ip_ids:
+                    ri.floating_ips.append(fip)
+                    self._floating_ip_added(ri, ex_gw_port,
+                                            fip['floating_ip_address'],
+                                            fip['fixed_ip_address'])
+
+        floating_ip_ids_to_remove = (existing_floating_ip_ids -
+                                     cur_floating_ip_ids)
+        for fip in ri.floating_ips:
+            if fip['id'] in floating_ip_ids_to_remove:
+                ri.floating_ips.remove(fip)
+                self._floating_ip_removed(ri, ri.ex_gw_port,
+                                          fip['floating_ip_address'],
+                                          fip['fixed_ip_address'])
+            else:
+                # handle remapping of a floating IP
+                new_fip = id_to_fip_map[fip['id']]
+                new_fixed_ip = new_fip['fixed_ip_address']
+                existing_fixed_ip = fip['fixed_ip_address']
+                if (new_fixed_ip and existing_fixed_ip and
+                        new_fixed_ip != existing_fixed_ip):
+                    floating_ip = fip['floating_ip_address']
+                    self._floating_ip_removed(ri, ri.ex_gw_port,
+                                              floating_ip,
+                                              existing_fixed_ip)
+                    self._floating_ip_added(ri, ri.ex_gw_port,
+                                            floating_ip, new_fixed_ip)
+                    ri.floating_ips.remove(fip)
+                    ri.floating_ips.append(new_fip)
+
+    def _router_added(self, router_id, router):
+        """Operations when a router is added.
+
+        Create a new RouterInfo object for this router and add it to the
+        service helpers router_info dictionary.  Then `router_added()` is
+        called on the device driver.
+
+        :param router_id: id of the router
+        :param router: router dict
+        :return: None
+        """
+        ri = RouterInfo(router_id, router)
+        driver = self._drivermgr.set_driver(router)
+        driver.router_added(ri)
+        self.router_info[router_id] = ri
+
+    def _router_removed(self, router_id, deconfigure=True):
+        """Operations when a router is removed.
+
+        Get the RouterInfo object corresponding to the router in the service
+        helpers's router_info dict. If deconfigure is set to True,
+        remove this router's configuration from the hosting device.
+        :param router_id: id of the router
+        :param deconfigure: if True, the router's configuration is deleted from
+        the hosting device.
+        :return: None
+        """
+        ri = self.router_info.get(router_id)
+        if ri is None:
+            LOG.warn(_("Info for router %s was not found. "
+                       "Skipping router removal"), router_id)
+            return
+        ri.router['gw_port'] = None
+        ri.router[l3_constants.INTERFACE_KEY] = []
+        ri.router[l3_constants.FLOATINGIP_KEY] = []
+        try:
+            if deconfigure:
+                self._process_router(ri)
+                driver = self._drivermgr.get_driver(router_id)
+                driver.router_removed(ri, deconfigure)
+                self._drivermgr.remove_driver(router_id)
+            del self.router_info[router_id]
+            self.removed_routers.discard(router_id)
+        except cfg_exceptions.DriverException:
+            LOG.warn(_("Router remove for router_id: %s was incomplete. "
+                       "Adding the router to removed_routers list"), router_id)
+            self.removed_routers.add(router_id)
+            # remove this router from updated_routers if it is there. It might
+            # end up there too if exception was thrown earlier inside
+            # `_process_router()`
+            self.updated_routers.discard(router_id)
+
+    def _internal_network_added(self, ri, port, ex_gw_port):
+        driver = self._drivermgr.get_driver(ri.id)
+        driver.internal_network_added(ri, port)
+        if ri.snat_enabled and ex_gw_port:
+            driver.enable_internal_network_NAT(ri, port, ex_gw_port)
+
+    def _internal_network_removed(self, ri, port, ex_gw_port):
+        driver = self._drivermgr.get_driver(ri.id)
+        driver.internal_network_removed(ri, port)
+        if ri.snat_enabled and ex_gw_port:
+            driver.disable_internal_network_NAT(ri, port, ex_gw_port)
+
+    def _external_gateway_added(self, ri, ex_gw_port):
+        driver = self._drivermgr.get_driver(ri.id)
+        driver.external_gateway_added(ri, ex_gw_port)
+        if ri.snat_enabled and ri.internal_ports:
+            for port in ri.internal_ports:
+                driver.enable_internal_network_NAT(ri, port, ex_gw_port)
+
+    def _external_gateway_removed(self, ri, ex_gw_port):
+        driver = self._drivermgr.get_driver(ri.id)
+        if ri.snat_enabled and ri.internal_ports:
+            for port in ri.internal_ports:
+                driver.disable_internal_network_NAT(ri, port, ex_gw_port)
+        driver.external_gateway_removed(ri, ex_gw_port)
+
+    def _floating_ip_added(self, ri, ex_gw_port, floating_ip, fixed_ip):
+        driver = self._drivermgr.get_driver(ri.id)
+        driver.floating_ip_added(ri, ex_gw_port, floating_ip, fixed_ip)
+
+    def _floating_ip_removed(self, ri, ex_gw_port, floating_ip, fixed_ip):
+        driver = self._drivermgr.get_driver(ri.id)
+        driver.floating_ip_removed(ri, ex_gw_port, floating_ip, fixed_ip)
+
+    def _routes_updated(self, ri):
+        """Update the state of routes in the router.
+
+        Compares the current routes with the (configured) existing routes
+        and detect what was removed or added. Then configure the
+        logical router in the hosting device accordingly.
+        :param ri: RouterInfo corresponding to the router.
+        :return: None
+        :raises: neutron.plugins.cisco.cfg_agent.cfg_exceptions.DriverException
+        if the configuration operation fails.
+        """
+        new_routes = ri.router['routes']
+        old_routes = ri.routes
+        adds, removes = common_utils.diff_list_of_dict(old_routes,
+                                                       new_routes)
+        for route in adds:
+            LOG.debug("Added route entry is '%s'", route)
+            # remove replaced route from deleted route
+            for del_route in removes:
+                if route['destination'] == del_route['destination']:
+                    removes.remove(del_route)
+            driver = self._drivermgr.get_driver(ri.id)
+            driver.routes_updated(ri, 'replace', route)
+
+        for route in removes:
+            LOG.debug("Removed route entry is '%s'", route)
+            driver = self._drivermgr.get_driver(ri.id)
+            driver.routes_updated(ri, 'delete', route)
+        ri.routes = new_routes
+
+    @staticmethod
+    def _set_subnet_info(port):
+        ips = port['fixed_ips']
+        if not ips:
+            raise Exception(_("Router port %s has no IP address") % port['id'])
+        if len(ips) > 1:
+            LOG.error(_("Ignoring multiple IPs on router port %s"), port['id'])
+        prefixlen = netaddr.IPNetwork(port['subnet']['cidr']).prefixlen
+        port['ip_cidr'] = "%s/%s" % (ips[0]['ip_address'], prefixlen)
index b4a9f1408f9e495fa8e6da9c58a6d8ef5c54d757..b90123c6143c4c92f28a7579bea1786dbcaaf756 100644 (file)
@@ -106,3 +106,12 @@ NEXUS_VLAN_RESERVED_MIN = 3968
 NEXUS_VLAN_RESERVED_MAX = 4047
 NEXUS_VXLAN_MIN = 4096
 NEXUS_VXLAN_MAX = 16000000
+
+# Type and topic for Cisco cfg agent
+# ==================================
+AGENT_TYPE_CFG = 'Cisco cfg agent'
+
+# Topic for Cisco configuration agent
+CFG_AGENT = 'cisco_cfg_agent'
+# Topic for routing service helper in Cisco configuration agent
+CFG_AGENT_L3_ROUTING = 'cisco_cfg_agent_l3_routing'
diff --git a/neutron/tests/unit/cisco/cfg_agent/__init__.py b/neutron/tests/unit/cisco/cfg_agent/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/neutron/tests/unit/cisco/cfg_agent/test_cfg_agent.py b/neutron/tests/unit/cisco/cfg_agent/test_cfg_agent.py
new file mode 100644 (file)
index 0000000..740a730
--- /dev/null
@@ -0,0 +1,141 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+import mock
+from oslo.config import cfg
+import testtools
+
+from neutron.agent.common import config
+from neutron.common import config as base_config
+from neutron.common import constants as l3_constants
+from neutron.openstack.common import log as logging
+from neutron.openstack.common import uuidutils
+from neutron.plugins.cisco.cfg_agent import cfg_agent
+from neutron.tests import base
+
+_uuid = uuidutils.generate_uuid
+HOSTNAME = 'myhost'
+FAKE_ID = _uuid()
+
+LOG = logging.getLogger(__name__)
+
+
+def prepare_router_data(enable_snat=None, num_internal_ports=1):
+    router_id = _uuid()
+    ex_gw_port = {'id': _uuid(),
+                  'network_id': _uuid(),
+                  'fixed_ips': [{'ip_address': '19.4.4.4',
+                                 'subnet_id': _uuid()}],
+                  'subnet': {'cidr': '19.4.4.0/24',
+                             'gateway_ip': '19.4.4.1'}}
+    int_ports = []
+    for i in range(num_internal_ports):
+        int_ports.append({'id': _uuid(),
+                          'network_id': _uuid(),
+                          'admin_state_up': True,
+                          'fixed_ips': [{'ip_address': '35.4.%s.4' % i,
+                                         'subnet_id': _uuid()}],
+                          'mac_address': 'ca:fe:de:ad:be:ef',
+                          'subnet': {'cidr': '35.4.%s.0/24' % i,
+                                     'gateway_ip': '35.4.%s.1' % i}})
+    hosting_device = {'id': _uuid(),
+                      'host_type': 'CSR1kv',
+                      'ip_address': '20.0.0.5',
+                      'port': '23'}
+
+    router = {
+        'id': router_id,
+        l3_constants.INTERFACE_KEY: int_ports,
+        'routes': [],
+        'gw_port': ex_gw_port,
+        'hosting_device': hosting_device}
+    if enable_snat is not None:
+        router['enable_snat'] = enable_snat
+    return router, int_ports
+
+
+class TestCiscoCfgAgentWIthStateReporting(base.BaseTestCase):
+
+    def setUp(self):
+        self.conf = cfg.ConfigOpts()
+        config.register_agent_state_opts_helper(cfg.CONF)
+        self.conf.register_opts(base_config.core_opts)
+        self.conf.register_opts(cfg_agent.CiscoCfgAgent.OPTS)
+        cfg.CONF.set_override('report_interval', 0, 'AGENT')
+        super(TestCiscoCfgAgentWIthStateReporting, self).setUp()
+        self.devmgr_plugin_api_cls_p = mock.patch(
+            'neutron.plugins.cisco.cfg_agent.cfg_agent.'
+            'CiscoDeviceManagementApi')
+        devmgr_plugin_api_cls = self.devmgr_plugin_api_cls_p.start()
+        self.devmgr_plugin_api = mock.Mock()
+        devmgr_plugin_api_cls.return_value = self.devmgr_plugin_api
+        self.devmgr_plugin_api.register_for_duty.return_value = True
+
+        self.plugin_reportstate_api_cls_p = mock.patch(
+            'neutron.agent.rpc.PluginReportStateAPI')
+        plugin_reportstate_api_cls = self.plugin_reportstate_api_cls_p.start()
+        self.plugin_reportstate_api = mock.Mock()
+        plugin_reportstate_api_cls.return_value = self.plugin_reportstate_api
+
+        self.looping_call_p = mock.patch(
+            'neutron.openstack.common.loopingcall.FixedIntervalLoopingCall')
+        self.looping_call_p.start()
+
+        mock.patch('neutron.common.rpc.create_connection').start()
+
+    def test_agent_registration_success(self):
+        agent = cfg_agent.CiscoCfgAgentWithStateReport(HOSTNAME, self.conf)
+        self.assertTrue(agent.devmgr_rpc.register_for_duty(agent.context))
+
+    def test_agent_registration_success_after_2_tries(self):
+        self.devmgr_plugin_api.register_for_duty = mock.Mock(
+            side_effect=[False, False, True])
+        cfg_agent.REGISTRATION_RETRY_DELAY = 0.01
+        agent = cfg_agent.CiscoCfgAgentWithStateReport(HOSTNAME, self.conf)
+        self.assertEqual(agent.devmgr_rpc.register_for_duty.call_count, 3)
+
+    def test_agent_registration_fail_always(self):
+        self.devmgr_plugin_api.register_for_duty = mock.Mock(
+            return_value=False)
+        cfg_agent.REGISTRATION_RETRY_DELAY = 0.01
+        cfg_agent.MAX_REGISTRATION_ATTEMPTS = 3
+        with testtools.ExpectedException(SystemExit):
+            cfg_agent.CiscoCfgAgentWithStateReport(HOSTNAME, self.conf)
+
+    def test_agent_registration_no_device_mgr(self):
+        self.devmgr_plugin_api.register_for_duty = mock.Mock(
+            return_value=None)
+        cfg_agent.REGISTRATION_RETRY_DELAY = 0.01
+        cfg_agent.MAX_REGISTRATION_ATTEMPTS = 3
+        with testtools.ExpectedException(SystemExit):
+            cfg_agent.CiscoCfgAgentWithStateReport(HOSTNAME, self.conf)
+
+    def test_report_state(self):
+        agent = cfg_agent.CiscoCfgAgentWithStateReport(HOSTNAME, self.conf)
+        agent._report_state()
+        self.assertIn('total routers', agent.agent_state['configurations'])
+        self.assertEqual(0, agent.agent_state[
+            'configurations']['total routers'])
+
+    @mock.patch('neutron.plugins.cisco.cfg_agent.'
+                'cfg_agent.CiscoCfgAgentWithStateReport._agent_registration')
+    def test_report_state_attribute_error(self, agent_registration):
+        cfg.CONF.set_override('report_interval', 1, 'AGENT')
+        self.plugin_reportstate_api.report_state.side_effect = AttributeError
+        agent = cfg_agent.CiscoCfgAgentWithStateReport(HOSTNAME, self.conf)
+        agent.heartbeat = mock.Mock()
+        agent.send_agent_report(None, None)
+        self.assertTrue(agent.heartbeat.stop.called)
\ No newline at end of file
diff --git a/neutron/tests/unit/cisco/cfg_agent/test_csr1kv_routing_driver.py b/neutron/tests/unit/cisco/cfg_agent/test_csr1kv_routing_driver.py
new file mode 100644 (file)
index 0000000..c93b2d4
--- /dev/null
@@ -0,0 +1,284 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+import sys
+
+import mock
+import netaddr
+
+from neutron.common import constants as l3_constants
+from neutron.openstack.common import uuidutils
+from neutron.tests import base
+
+from neutron.plugins.cisco.cfg_agent.device_drivers.csr1kv import (
+    cisco_csr1kv_snippets as snippets)
+sys.modules['ncclient'] = mock.MagicMock()
+sys.modules['ciscoconfparse'] = mock.MagicMock()
+from neutron.plugins.cisco.cfg_agent.device_drivers.csr1kv import (
+    csr1kv_routing_driver as csr_driver)
+from neutron.plugins.cisco.cfg_agent.service_helpers import routing_svc_helper
+
+_uuid = uuidutils.generate_uuid
+FAKE_ID = _uuid()
+PORT_ID = _uuid()
+
+
+class TestCSR1kvRouting(base.BaseTestCase):
+
+    def setUp(self):
+        super(TestCSR1kvRouting, self).setUp()
+
+        device_params = {'management_ip_address': 'fake_ip',
+                         'protocol_port': 22,
+                         'credentials': {"username": "stack",
+                                         "password": "cisco"},
+                         }
+        self.driver = csr_driver.CSR1kvRoutingDriver(
+            **device_params)
+        self.mock_conn = mock.MagicMock()
+        self.driver._csr_conn = self.mock_conn
+        self.driver._check_response = mock.MagicMock(return_value=True)
+
+        self.vrf = ('nrouter-' + FAKE_ID)[:csr_driver.CSR1kvRoutingDriver.
+                                          DEV_NAME_LEN]
+        self.driver._get_vrfs = mock.Mock(return_value=[self.vrf])
+        self.ex_gw_ip = '20.0.0.30'
+        self.ex_gw_cidr = '20.0.0.30/24'
+        self.ex_gw_vlan = 1000
+        self.ex_gw_gateway_ip = '20.0.0.1'
+        self.ex_gw_port = {'id': _uuid(),
+                           'network_id': _uuid(),
+                           'fixed_ips': [{'ip_address': self.ex_gw_ip,
+                                          'subnet_id': _uuid()}],
+                           'subnet': {'cidr': self.ex_gw_cidr,
+                                      'gateway_ip': self.ex_gw_gateway_ip},
+                           'ip_cidr': self.ex_gw_cidr,
+                           'mac_address': 'ca:fe:de:ad:be:ef',
+                           'hosting_info': {'segmentation_id': self.ex_gw_vlan,
+                                          'hosting_port_name': 't2_p:0'}}
+        self.vlan_no = 500
+        self.gw_ip_cidr = '10.0.0.1/16'
+        self.gw_ip = '10.0.0.1'
+        self.hosting_port = 't1_p:0'
+        self.port = {'id': PORT_ID,
+                     'ip_cidr': self.gw_ip_cidr,
+                     'fixed_ips': [{'ip_address': self.gw_ip}],
+                     'hosting_info': {'segmentation_id': self.vlan_no,
+                                      'hosting_port_name': self.hosting_port}}
+        int_ports = [self.port]
+
+        self.router = {
+            'id': FAKE_ID,
+            l3_constants.INTERFACE_KEY: int_ports,
+            'enable_snat': True,
+            'routes': [],
+            'gw_port': self.ex_gw_port}
+
+        self.ri = routing_svc_helper.RouterInfo(FAKE_ID, self.router)
+        self.ri.internal_ports = int_ports
+
+    def test_csr_get_vrf_name(self):
+        self.assertEqual(self.driver._csr_get_vrf_name(self.ri), self.vrf)
+
+    def test_create_vrf(self):
+        confstr = snippets.CREATE_VRF % self.vrf
+
+        self.driver._create_vrf(self.vrf)
+
+        self.assertTrue(self.driver._csr_conn.edit_config.called)
+        self.driver._csr_conn.edit_config.assert_called_with(target='running',
+                                                             config=confstr)
+
+    def test_remove_vrf(self):
+        confstr = snippets.REMOVE_VRF % self.vrf
+
+        self.driver._remove_vrf(self.vrf)
+
+        self.assertTrue(self.driver._csr_conn.edit_config.called)
+        self.driver._csr_conn.edit_config.assert_called_with(target='running',
+                                                             config=confstr)
+
+    def test_router_added(self):
+        confstr = snippets.CREATE_VRF % self.vrf
+
+        self.driver.router_added(self.ri)
+
+        self.assertTrue(self.driver._csr_conn.edit_config.called)
+        self.driver._csr_conn.edit_config.assert_called_with(target='running',
+                                                             config=confstr)
+
+    def test_router_removed(self):
+        confstr = snippets.REMOVE_VRF % self.vrf
+
+        self.driver._remove_vrf(self.vrf)
+
+        self.assertTrue(self.driver._csr_conn.edit_config.called)
+        self.driver._csr_conn.edit_config.assert_called_once_with(
+            target='running', config=confstr)
+
+    def test_internal_network_added(self):
+        self.driver._create_subinterface = mock.MagicMock()
+        interface = 'GigabitEthernet0' + '.' + str(self.vlan_no)
+
+        self.driver.internal_network_added(self.ri, self.port)
+
+        args = (interface, self.vlan_no, self.vrf, self.gw_ip,
+                netaddr.IPAddress('255.255.0.0'))
+        self.driver._create_subinterface.assert_called_once_with(*args)
+
+    def test_internal_network_removed(self):
+        self.driver._remove_subinterface = mock.MagicMock()
+        interface = 'GigabitEthernet0' + '.' + str(self.vlan_no)
+
+        self.driver.internal_network_removed(self.ri, self.port)
+
+        self.driver._remove_subinterface.assert_called_once_with(interface)
+
+    def test_routes_updated(self):
+        dest_net = '20.0.0.0/16'
+        next_hop = '10.0.0.255'
+        route = {'destination': dest_net,
+                 'nexthop': next_hop}
+
+        dest = netaddr.IPAddress('20.0.0.0')
+        destmask = netaddr.IPNetwork(dest_net).netmask
+        self.driver._add_static_route = mock.MagicMock()
+        self.driver._remove_static_route = mock.MagicMock()
+
+        self.driver.routes_updated(self.ri, 'replace', route)
+        self.driver._add_static_route.assert_called_once_with(
+            dest, destmask, next_hop, self.vrf)
+
+        self.driver.routes_updated(self.ri, 'delete', route)
+        self.driver._remove_static_route.assert_called_once_with(
+            dest, destmask, next_hop, self.vrf)
+
+    def test_floatingip(self):
+        floating_ip = '15.1.2.3'
+        fixed_ip = '10.0.0.3'
+
+        self.driver._add_floating_ip = mock.MagicMock()
+        self.driver._remove_floating_ip = mock.MagicMock()
+        self.driver._add_interface_nat = mock.MagicMock()
+        self.driver._remove_dyn_nat_translations = mock.MagicMock()
+        self.driver._remove_interface_nat = mock.MagicMock()
+
+        self.driver.floating_ip_added(self.ri, self.ex_gw_port,
+                                      floating_ip, fixed_ip)
+        self.driver._add_floating_ip.assert_called_once_with(
+            floating_ip, fixed_ip, self.vrf)
+
+        self.driver.floating_ip_removed(self.ri, self.ex_gw_port,
+                                        floating_ip, fixed_ip)
+
+        self.driver._remove_interface_nat.assert_called_once_with(
+            'GigabitEthernet1.1000', 'outside')
+        self.driver._remove_dyn_nat_translations.assert_called_once_with()
+        self.driver._remove_floating_ip.assert_called_once_with(
+            floating_ip, fixed_ip, self.vrf)
+        self.driver._add_interface_nat.assert_called_once_with(
+            'GigabitEthernet1.1000', 'outside')
+
+    def test_external_gateway_added(self):
+        self.driver._create_subinterface = mock.MagicMock()
+        self.driver._add_default_static_route = mock.MagicMock()
+
+        ext_interface = 'GigabitEthernet1' + '.' + str(1000)
+        args = (ext_interface, self.ex_gw_vlan, self.vrf, self.ex_gw_ip,
+                netaddr.IPAddress('255.255.255.0'))
+
+        self.driver.external_gateway_added(self.ri, self.ex_gw_port)
+
+        self.driver._create_subinterface.assert_called_once_with(*args)
+        self.driver._add_default_static_route.assert_called_once_with(
+            self.ex_gw_gateway_ip, self.vrf)
+
+    def test_enable_internal_network_NAT(self):
+        self.driver._nat_rules_for_internet_access = mock.MagicMock()
+        int_interface = ('GigabitEthernet0' + '.' + str(self.vlan_no))
+        ext_interface = 'GigabitEthernet1' + '.' + str(1000)
+        args = (('acl_' + str(self.vlan_no)),
+                netaddr.IPNetwork(self.gw_ip_cidr).network,
+                netaddr.IPNetwork(self.gw_ip_cidr).hostmask,
+                int_interface,
+                ext_interface,
+                self.vrf)
+
+        self.driver.enable_internal_network_NAT(self.ri, self.port,
+                                                self.ex_gw_port)
+
+        self.driver._nat_rules_for_internet_access.assert_called_once_with(
+            *args)
+
+    def test_enable_internal_network_NAT_with_confstring(self):
+        self.driver._csr_conn.reset_mock()
+        self.driver._check_acl = mock.Mock(return_value=False)
+        int_interface = ('GigabitEthernet0' + '.' + str(self.vlan_no))
+        ext_interface = 'GigabitEthernet1' + '.' + str(1000)
+        acl_no = ('acl_' + str(self.vlan_no))
+        int_network = netaddr.IPNetwork(self.gw_ip_cidr).network
+        int_net_mask = netaddr.IPNetwork(self.gw_ip_cidr).hostmask
+
+        self.driver.enable_internal_network_NAT(self.ri, self.port,
+                                                self.ex_gw_port)
+
+        self.assert_edit_running_config(
+            snippets.CREATE_ACL, (acl_no, int_network, int_net_mask))
+        self.assert_edit_running_config(
+            snippets.SET_DYN_SRC_TRL_INTFC, (acl_no, ext_interface, self.vrf))
+        self.assert_edit_running_config(
+            snippets.SET_NAT, (int_interface, 'inside'))
+        self.assert_edit_running_config(
+            snippets.SET_NAT, (ext_interface, 'outside'))
+
+    def test_disable_internal_network_NAT(self):
+        self.driver._remove_interface_nat = mock.MagicMock()
+        self.driver._remove_dyn_nat_translations = mock.MagicMock()
+        self.driver._remove_dyn_nat_rule = mock.MagicMock()
+        int_interface = ('GigabitEthernet0' + '.' + str(self.vlan_no))
+        ext_interface = 'GigabitEthernet1' + '.' + str(1000)
+        self.driver.disable_internal_network_NAT(self.ri, self.port,
+                                                 self.ex_gw_port)
+        args = (('acl_' + str(self.vlan_no)), ext_interface, self.vrf)
+
+        self.driver._remove_interface_nat.assert_called_once_with(
+            int_interface, 'inside')
+        self.driver._remove_dyn_nat_translations.assert_called_once_with()
+        self.driver._remove_dyn_nat_rule.assert_called_once_with(*args)
+
+    def assert_edit_running_config(self, snippet_name, args):
+        if args:
+            confstr = snippet_name % args
+        else:
+            confstr = snippet_name
+        self.driver._csr_conn.edit_config.assert_any_call(
+            target='running', config=confstr)
+
+    def test_disable_internal_network_NAT_with_confstring(self):
+        self.driver._cfg_exists = mock.Mock(return_value=True)
+        int_interface = ('GigabitEthernet0' + '.' + str(self.vlan_no))
+        ext_interface = 'GigabitEthernet1' + '.' + str(1000)
+        acl_no = 'acl_' + str(self.vlan_no)
+        self.driver.disable_internal_network_NAT(self.ri, self.port,
+                                                 self.ex_gw_port)
+
+        self.assert_edit_running_config(
+            snippets.REMOVE_NAT, (int_interface, 'inside'))
+        self.assert_edit_running_config(snippets.CLEAR_DYN_NAT_TRANS, None)
+        self.assert_edit_running_config(
+            snippets.REMOVE_DYN_SRC_TRL_INTFC, (acl_no, ext_interface,
+                                                self.vrf))
+        self.assert_edit_running_config(snippets.REMOVE_ACL, acl_no)
diff --git a/neutron/tests/unit/cisco/cfg_agent/test_device_status.py b/neutron/tests/unit/cisco/cfg_agent/test_device_status.py
new file mode 100644 (file)
index 0000000..d296ddf
--- /dev/null
@@ -0,0 +1,193 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+import sys
+
+import datetime
+import mock
+
+from neutron.openstack.common import log as logging
+from neutron.openstack.common import uuidutils
+
+sys.modules['ncclient'] = mock.MagicMock()
+sys.modules['ciscoconfparse'] = mock.MagicMock()
+from neutron.plugins.cisco.cfg_agent import device_status
+from neutron.tests import base
+
+_uuid = uuidutils.generate_uuid
+LOG = logging.getLogger(__name__)
+
+TYPE_STRING = 'string'
+TYPE_DATETIME = 'datetime'
+NOW = 0
+BOOT_TIME = 420
+DEAD_TIME = 300
+BELOW_BOOT_TIME = 100
+
+
+def create_timestamp(seconds_from_now, type=TYPE_STRING):
+    timedelta = datetime.timedelta(seconds=seconds_from_now)
+    past_time = datetime.datetime.utcnow() - timedelta
+    if type is TYPE_STRING:
+        return past_time.strftime("%Y-%m-%dT%H:%M:%S.%f")
+    if type is TYPE_DATETIME:
+        return past_time
+
+
+class TestHostingDevice(base.BaseTestCase):
+
+    def setUp(self):
+        super(TestHostingDevice, self).setUp()
+        self.status = device_status.DeviceStatus()
+        device_status._is_pingable = mock.MagicMock(return_value=True)
+
+        self.hosting_device = {'id': 123,
+                               'host_type': 'CSR1kv',
+                               'management_ip_address': '10.0.0.1',
+                               'port': '22',
+                               'booting_time': 420}
+        self.created_at_str = datetime.datetime.utcnow().strftime(
+            "%Y-%m-%d %H:%M:%S")
+        self.hosting_device['created_at'] = self.created_at_str
+        self.router_id = _uuid()
+        self.router = {id: self.router_id,
+                       'hosting_device': self.hosting_device}
+
+    def test_hosting_devices_object(self):
+        self.assertEqual({}, self.status.backlog_hosting_devices)
+
+    def test_is_hosting_device_reachable_positive(self):
+        self.assertTrue(self.status.is_hosting_device_reachable(
+            self.hosting_device))
+
+    def test_is_hosting_device_reachable_negative(self):
+        self.assertEqual(0, len(self.status.backlog_hosting_devices))
+        self.hosting_device['created_at'] = self.created_at_str  # Back to str
+        device_status._is_pingable.return_value = False
+
+        self.assertFalse(device_status._is_pingable('1.2.3.4'))
+        self.assertIsNone(self.status.is_hosting_device_reachable(
+            self.hosting_device))
+        self.assertEqual(1, len(self.status.get_backlogged_hosting_devices()))
+        self.assertTrue(123 in self.status.get_backlogged_hosting_devices())
+        self.assertEqual(self.status.backlog_hosting_devices[123]['hd'],
+                         self.hosting_device)
+
+    def test_test_is_hosting_device_reachable_negative_exisiting_hd(self):
+        self.status.backlog_hosting_devices.clear()
+        self.status.backlog_hosting_devices[123] = {'hd': self.hosting_device}
+
+        self.assertEqual(1, len(self.status.backlog_hosting_devices))
+        self.assertIsNone(self.status.is_hosting_device_reachable(
+            self.hosting_device))
+        self.assertEqual(1, len(self.status.get_backlogged_hosting_devices()))
+        self.assertTrue(123 in self.status.backlog_hosting_devices.keys())
+        self.assertEqual(self.status.backlog_hosting_devices[123]['hd'],
+                         self.hosting_device)
+
+    def test_check_backlog_empty(self):
+
+        expected = {'reachable': [],
+                    'dead': []}
+
+        self.assertEqual(expected,
+                         self.status.check_backlogged_hosting_devices())
+
+    def test_check_backlog_below_booting_time(self):
+        expected = {'reachable': [],
+                    'dead': []}
+
+        self.hosting_device['created_at'] = create_timestamp(NOW)
+        hd = self.hosting_device
+        hd_id = hd['id']
+        self.status.backlog_hosting_devices[hd_id] = {'hd': hd,
+                                                      'routers': [
+                                                          self.router_id]
+                                                      }
+
+        self.assertEqual(expected,
+                         self.status.check_backlogged_hosting_devices())
+
+        #Simulate 20 seconds before boot time finishes
+        self.hosting_device['created_at'] = create_timestamp(BOOT_TIME - 20)
+        self.assertEqual(self.status.check_backlogged_hosting_devices(),
+                         expected)
+
+        #Simulate 1 second before boot time
+        self.hosting_device['created_at'] = create_timestamp(BOOT_TIME - 1)
+        self.assertEqual(self.status.check_backlogged_hosting_devices(),
+                         expected)
+
+    def test_check_backlog_above_booting_time_pingable(self):
+        """Test for backlog processing after booting.
+
+        Simulates a hosting device which has passed the created time.
+        The device should now be pingable.
+        """
+        self.hosting_device['created_at'] = create_timestamp(BOOT_TIME + 10)
+        hd = self.hosting_device
+        hd_id = hd['id']
+        device_status._is_pingable.return_value = True
+        self.status.backlog_hosting_devices[hd_id] = {'hd': hd,
+                                                      'routers': [
+                                                          self.router_id]}
+        expected = {'reachable': [hd_id],
+                    'dead': []}
+        self.assertEqual(expected,
+                         self.status.check_backlogged_hosting_devices())
+
+    def test_check_backlog_above_BT_not_pingable_below_deadtime(self):
+        """Test for backlog processing in dead time interval.
+
+        This test simulates a hosting device which has passed the created
+        time but less than the 'declared dead' time.
+        Hosting device is still not pingable.
+        """
+        hd = self.hosting_device
+        hd['created_at'] = create_timestamp(BOOT_TIME + 10)
+        #Inserted in backlog now
+        hd['backlog_insertion_ts'] = create_timestamp(NOW, type=TYPE_DATETIME)
+        hd_id = hd['id']
+        device_status._is_pingable.return_value = False
+        self.status.backlog_hosting_devices[hd_id] = {'hd': hd,
+                                                      'routers': [
+                                                          self.router_id]}
+        expected = {'reachable': [],
+                    'dead': []}
+        self.assertEqual(expected,
+                         self.status.check_backlogged_hosting_devices())
+
+    def test_check_backlog_above_BT_not_pingable_aboveDeadTime(self):
+        """Test for backlog processing after dead time interval.
+
+        This test simulates a hosting device which has passed the
+        created time but greater than the 'declared dead' time.
+        Hosting device is still not pingable.
+        """
+        hd = self.hosting_device
+        hd['created_at'] = create_timestamp(BOOT_TIME + DEAD_TIME + 10)
+        #Inserted in backlog 5 seconds after booting time
+        hd['backlog_insertion_ts'] = create_timestamp(BOOT_TIME + 5,
+                                                      type=TYPE_DATETIME)
+
+        hd_id = hd['id']
+        device_status._is_pingable.return_value = False
+        self.status.backlog_hosting_devices[hd_id] = {'hd': hd,
+                                                      'routers': [
+                                                          self.router_id]}
+        expected = {'reachable': [],
+                    'dead': [hd_id]}
+        self.assertEqual(expected,
+                         self.status.check_backlogged_hosting_devices())
\ No newline at end of file
diff --git a/neutron/tests/unit/cisco/cfg_agent/test_routing_svc_helper.py b/neutron/tests/unit/cisco/cfg_agent/test_routing_svc_helper.py
new file mode 100644 (file)
index 0000000..38a6840
--- /dev/null
@@ -0,0 +1,655 @@
+# Copyright 2014 Cisco Systems, Inc.  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.
+#
+# @author: Hareesh Puthalath, Cisco Systems, Inc.
+
+import copy
+import mock
+from oslo.config import cfg
+
+from neutron.common import config as base_config
+from neutron.common import constants as l3_constants
+from neutron.common import rpc as n_rpc
+from neutron.openstack.common import log as logging
+from neutron.openstack.common import uuidutils
+from neutron.plugins.cisco.cfg_agent import cfg_agent
+from neutron.plugins.cisco.cfg_agent import cfg_exceptions
+from neutron.plugins.cisco.cfg_agent.service_helpers.routing_svc_helper import(
+    RouterInfo)
+from neutron.plugins.cisco.cfg_agent.service_helpers.routing_svc_helper import(
+    RoutingServiceHelper)
+
+
+from neutron.tests import base
+
+
+_uuid = uuidutils.generate_uuid
+HOST = 'myhost'
+FAKE_ID = _uuid()
+
+LOG = logging.getLogger(__name__)
+
+
+def prepare_router_data(enable_snat=None, num_internal_ports=1):
+    router_id = _uuid()
+    ex_gw_port = {'id': _uuid(),
+                  'network_id': _uuid(),
+                  'fixed_ips': [{'ip_address': '19.4.4.4',
+                                 'subnet_id': _uuid()}],
+                  'subnet': {'cidr': '19.4.4.0/24',
+                             'gateway_ip': '19.4.4.1'}}
+    int_ports = []
+    for i in range(num_internal_ports):
+        int_ports.append({'id': _uuid(),
+                          'network_id': _uuid(),
+                          'admin_state_up': True,
+                          'fixed_ips': [{'ip_address': '35.4.%s.4' % i,
+                                         'subnet_id': _uuid()}],
+                          'mac_address': 'ca:fe:de:ad:be:ef',
+                          'subnet': {'cidr': '35.4.%s.0/24' % i,
+                                     'gateway_ip': '35.4.%s.1' % i}})
+    hosting_device = {'id': _uuid(),
+                      "name": "CSR1kv_template",
+                      "booting_time": 300,
+                      "host_category": "VM",
+                      'management_ip_address': '20.0.0.5',
+                      'protocol_port': 22,
+                      "credentials": {
+                          "username": "user",
+                          "password": "4getme"},
+                      }
+    router = {
+        'id': router_id,
+        'admin_state_up': True,
+        l3_constants.INTERFACE_KEY: int_ports,
+        'routes': [],
+        'gw_port': ex_gw_port,
+        'hosting_device': hosting_device}
+    if enable_snat is not None:
+        router['enable_snat'] = enable_snat
+    return router, int_ports
+
+
+class TestRouterInfo(base.BaseTestCase):
+
+    def setUp(self):
+        super(TestRouterInfo, self).setUp()
+        self.ex_gw_port = {'id': _uuid(),
+                           'network_id': _uuid(),
+                           'fixed_ips': [{'ip_address': '19.4.4.4',
+                                          'subnet_id': _uuid()}],
+                           'subnet': {'cidr': '19.4.4.0/24',
+                                      'gateway_ip': '19.4.4.1'}}
+        self.router = {'id': _uuid(),
+                       'enable_snat': True,
+                       'routes': [],
+                       'gw_port': self.ex_gw_port}
+
+    def test_router_info_create(self):
+        router_id = _uuid()
+        fake_router = {}
+        ri = RouterInfo(router_id, fake_router)
+
+        self.assertTrue(ri.router_name().endswith(router_id))
+
+    def test_router_info_create_with_router(self):
+        router_id = _uuid()
+        ri = RouterInfo(router_id, self.router)
+        self.assertTrue(ri.router_name().endswith(router_id))
+        self.assertEqual(ri.router, self.router)
+        self.assertEqual(ri._router, self.router)
+        self.assertTrue(ri.snat_enabled)
+        self.assertIsNone(ri.ex_gw_port)
+
+    def test_router_info_create_snat_disabled(self):
+        router_id = _uuid()
+        self.router['enable_snat'] = False
+        ri = RouterInfo(router_id, self.router)
+        self.assertFalse(ri.snat_enabled)
+
+
+class TestBasicRoutingOperations(base.BaseTestCase):
+
+    def setUp(self):
+        super(TestBasicRoutingOperations, self).setUp()
+        self.conf = cfg.ConfigOpts()
+        self.conf.register_opts(base_config.core_opts)
+        self.conf.register_opts(cfg_agent.CiscoCfgAgent.OPTS)
+        self.ex_gw_port = {'id': _uuid(),
+                           'network_id': _uuid(),
+                           'fixed_ips': [{'ip_address': '19.4.4.4',
+                                         'subnet_id': _uuid()}],
+                           'subnet': {'cidr': '19.4.4.0/24',
+                                      'gateway_ip': '19.4.4.1'}}
+        self.hosting_device = {'id': "100",
+                               'name': "CSR1kv_template",
+                               'booting_time': 300,
+                               'host_category': "VM",
+                               'management_ip_address': '20.0.0.5',
+                               'protocol_port': 22,
+                               'credentials': {'username': 'user',
+                                               "password": '4getme'},
+                               }
+        self.router = {
+            'id': _uuid(),
+            'enable_snat': True,
+            'routes': [],
+            'gw_port': self.ex_gw_port,
+            'hosting_device': self.hosting_device}
+
+        self.agent = mock.Mock()
+
+        #Patches & Mocks
+
+        self.l3pluginApi_cls_p = mock.patch(
+            'neutron.plugins.cisco.cfg_agent.service_helpers.'
+            'routing_svc_helper.CiscoRoutingPluginApi')
+        l3plugin_api_cls = self.l3pluginApi_cls_p.start()
+        self.plugin_api = mock.Mock()
+        l3plugin_api_cls.return_value = self.plugin_api
+        self.plugin_api.get_routers = mock.MagicMock()
+        self.looping_call_p = mock.patch(
+            'neutron.openstack.common.loopingcall.FixedIntervalLoopingCall')
+        self.looping_call_p.start()
+        mock.patch('neutron.common.rpc.create_connection').start()
+
+        self.routing_helper = RoutingServiceHelper(
+            HOST, self.conf, self.agent)
+        self.routing_helper._internal_network_added = mock.Mock()
+        self.routing_helper._external_gateway_added = mock.Mock()
+        self.routing_helper._internal_network_removed = mock.Mock()
+        self.routing_helper._external_gateway_removed = mock.Mock()
+        self.driver = self._mock_driver_and_hosting_device(
+            self.routing_helper)
+
+    def _mock_driver_and_hosting_device(self, svc_helper):
+        svc_helper._dev_status.is_hosting_device_reachable = mock.MagicMock(
+            return_value=True)
+        driver = mock.MagicMock()
+        svc_helper._drivermgr.get_driver = mock.Mock(return_value=driver)
+        svc_helper._drivermgr.set_driver = mock.Mock(return_value=driver)
+        return driver
+
+    def _reset_mocks(self):
+        self.routing_helper._process_router_floating_ips.reset_mock()
+        self.routing_helper._internal_network_added.reset_mock()
+        self.routing_helper._external_gateway_added.reset_mock()
+        self.routing_helper._internal_network_removed.reset_mock()
+        self.routing_helper._external_gateway_removed.reset_mock()
+
+    def test_process_router_throw_config_error(self):
+        snip_name = 'CREATE_SUBINTERFACE'
+        e_type = 'Fake error'
+        e_tag = 'Fake error tag'
+        params = {'snippet': snip_name, 'type': e_type, 'tag': e_tag}
+        self.routing_helper._internal_network_added.side_effect = (
+            cfg_exceptions.CSR1kvConfigException(**params))
+        router, ports = prepare_router_data()
+        ri = RouterInfo(router['id'], router)
+        self.assertRaises(cfg_exceptions.CSR1kvConfigException,
+                          self.routing_helper._process_router, ri)
+
+    def test_process_router(self):
+        router, ports = prepare_router_data()
+        #Setup mock for call to proceess floating ips
+        self.routing_helper._process_router_floating_ips = mock.Mock()
+        fake_floatingips1 = {'floatingips': [
+            {'id': _uuid(),
+             'floating_ip_address': '8.8.8.8',
+             'fixed_ip_address': '7.7.7.7',
+             'port_id': _uuid()}]}
+        ri = RouterInfo(router['id'], router=router)
+        # Process with initial values
+        self.routing_helper._process_router(ri)
+        ex_gw_port = ri.router.get('gw_port')
+        # Assert that process_floating_ips, internal_network & external network
+        # added were all called with the right params
+        self.routing_helper._process_router_floating_ips.assert_called_with(
+            ri, ex_gw_port)
+        self.routing_helper._internal_network_added.assert_called_with(
+            ri, ports[0], ex_gw_port)
+        self.routing_helper._external_gateway_added.assert_called_with(
+            ri, ex_gw_port)
+        self._reset_mocks()
+        # remap floating IP to a new fixed ip
+        fake_floatingips2 = copy.deepcopy(fake_floatingips1)
+        fake_floatingips2['floatingips'][0]['fixed_ip_address'] = '7.7.7.8'
+        router[l3_constants.FLOATINGIP_KEY] = fake_floatingips2['floatingips']
+
+        # Process again and check that this time only the process_floating_ips
+        # was only called.
+        self.routing_helper._process_router(ri)
+        ex_gw_port = ri.router.get('gw_port')
+        self.routing_helper._process_router_floating_ips.assert_called_with(
+            ri, ex_gw_port)
+        self.assertFalse(self.routing_helper._internal_network_added.called)
+        self.assertFalse(self.routing_helper._external_gateway_added.called)
+        self._reset_mocks()
+        # remove just the floating ips
+        del router[l3_constants.FLOATINGIP_KEY]
+        # Process again and check that this time also only the
+        # process_floating_ips and external_network remove was called
+        self.routing_helper._process_router(ri)
+        ex_gw_port = ri.router.get('gw_port')
+        self.routing_helper._process_router_floating_ips.assert_called_with(
+            ri, ex_gw_port)
+        self.assertFalse(self.routing_helper._internal_network_added.called)
+        self.assertFalse(self.routing_helper._external_gateway_added.called)
+        self._reset_mocks()
+        # now no ports so state is torn down
+        del router[l3_constants.INTERFACE_KEY]
+        del router['gw_port']
+        # Update router_info object
+        ri.router = router
+        # Keep a copy of the ex_gw_port before its gone after processing.
+        ex_gw_port = ri.ex_gw_port
+        # Process router and verify that internal and external network removed
+        # were called and floating_ips_process was called
+        self.routing_helper._process_router(ri)
+        self.assertFalse(self.routing_helper.
+                         _process_router_floating_ips.called)
+        self.assertFalse(self.routing_helper._external_gateway_added.called)
+        self.assertTrue(self.routing_helper._internal_network_removed.called)
+        self.assertTrue(self.routing_helper._external_gateway_removed.called)
+        self.routing_helper._internal_network_removed.assert_called_with(
+            ri, ports[0], ex_gw_port)
+        self.routing_helper._external_gateway_removed.assert_called_with(
+            ri, ex_gw_port)
+
+    def test_routing_table_update(self):
+        router = self.router
+        fake_route1 = {'destination': '135.207.0.0/16',
+                       'nexthop': '1.2.3.4'}
+        fake_route2 = {'destination': '135.207.111.111/32',
+                       'nexthop': '1.2.3.4'}
+
+        # First we set the routes to fake_route1 and see if the
+        # driver.routes_updated was called with 'replace'(==add or replace)
+        # and fake_route1
+        router['routes'] = [fake_route1]
+        ri = RouterInfo(router['id'], router)
+        self.routing_helper._process_router(ri)
+
+        self.driver.routes_updated.assert_called_with(ri, 'replace',
+                                                      fake_route1)
+
+        # Now we replace fake_route1 with fake_route2. This should cause driver
+        # to be invoked to delete fake_route1 and 'replace'(==add or replace)
+        self.driver.reset_mock()
+        router['routes'] = [fake_route2]
+        ri.router = router
+        self.routing_helper._process_router(ri)
+
+        self.driver.routes_updated.assert_called_with(ri, 'delete',
+                                                      fake_route1)
+        self.driver.routes_updated.assert_any_call(ri, 'replace', fake_route2)
+
+        # Now we add back fake_route1 as a new route, this should cause driver
+        # to be invoked to 'replace'(==add or replace) fake_route1
+        self.driver.reset_mock()
+        router['routes'] = [fake_route2, fake_route1]
+        ri.router = router
+        self.routing_helper._process_router(ri)
+
+        self.driver.routes_updated.assert_any_call(ri, 'replace', fake_route1)
+
+        # Now we delete all routes. This should cause driver
+        # to be invoked to delete fake_route1 and fake-route2
+        self.driver.reset_mock()
+        router['routes'] = []
+        ri.router = router
+        self.routing_helper._process_router(ri)
+
+        self.driver.routes_updated.assert_any_call(ri, 'delete', fake_route2)
+        self.driver.routes_updated.assert_any_call(ri, 'delete', fake_route1)
+
+    def test_process_router_internal_network_added_unexpected_error(self):
+        router, ports = prepare_router_data()
+        ri = RouterInfo(router['id'], router=router)
+        # raise RuntimeError to simulate that an unexpected exception occurrs
+        self.routing_helper._internal_network_added.side_effect = RuntimeError
+        self.assertRaises(RuntimeError,
+                          self.routing_helper._process_router,
+                          ri)
+        self.assertNotIn(
+            router[l3_constants.INTERFACE_KEY][0], ri.internal_ports)
+
+        # The unexpected exception has been fixed manually
+        self.routing_helper._internal_network_added.side_effect = None
+
+        # Failure will cause a retry next time, then were able to add the
+        # port to ri.internal_ports
+        self.routing_helper._process_router(ri)
+        self.assertIn(
+            router[l3_constants.INTERFACE_KEY][0], ri.internal_ports)
+
+    def test_process_router_internal_network_removed_unexpected_error(self):
+        router, ports = prepare_router_data()
+        ri = RouterInfo(router['id'], router=router)
+        # add an internal port
+        self.routing_helper._process_router(ri)
+
+        # raise RuntimeError to simulate that an unexpected exception occurrs
+
+        self.routing_helper._internal_network_removed.side_effect = mock.Mock(
+            side_effect=RuntimeError)
+        ri.internal_ports[0]['admin_state_up'] = False
+        # The above port is set to down state, remove it.
+        self.assertRaises(RuntimeError,
+                          self.routing_helper._process_router,
+                          ri)
+        self.assertIn(
+            router[l3_constants.INTERFACE_KEY][0], ri.internal_ports)
+
+        # The unexpected exception has been fixed manually
+        self.routing_helper._internal_network_removed.side_effect = None
+
+        # Failure will cause a retry next time,
+        # We were able to add the port to ri.internal_ports
+        self.routing_helper._process_router(ri)
+        # We were able to remove the port from ri.internal_ports
+        self.assertNotIn(
+            router[l3_constants.INTERFACE_KEY][0], ri.internal_ports)
+
+    def test_routers_with_admin_state_down(self):
+        self.plugin_api.get_external_network_id.return_value = None
+
+        routers = [
+            {'id': _uuid(),
+             'admin_state_up': False,
+             'external_gateway_info': {}}]
+        self.routing_helper._process_routers(routers, None)
+        self.assertNotIn(routers[0]['id'], self.routing_helper.router_info)
+
+    def test_router_deleted(self):
+        self.routing_helper.router_deleted(None, [FAKE_ID])
+        self.assertIn(FAKE_ID, self.routing_helper.removed_routers)
+
+    def test_routers_updated(self):
+        self.routing_helper.routers_updated(None, [FAKE_ID])
+        self.assertIn(FAKE_ID, self.routing_helper.updated_routers)
+
+    def test_removed_from_agent(self):
+        self.routing_helper.router_removed_from_agent(None,
+                                                      {'router_id': FAKE_ID})
+        self.assertIn(FAKE_ID, self.routing_helper.removed_routers)
+
+    def test_added_to_agent(self):
+        self.routing_helper.router_added_to_agent(None, [FAKE_ID])
+        self.assertIn(FAKE_ID, self.routing_helper.updated_routers)
+
+    def test_process_router_delete(self):
+        router = self.router
+        router['gw_port'] = self.ex_gw_port
+        self.routing_helper._router_added(router['id'], router)
+        self.assertIn(router['id'], self.routing_helper.router_info)
+        # Now we remove the router
+        self.routing_helper._router_removed(router['id'], deconfigure=True)
+        self.assertNotIn(router['id'], self.routing_helper.router_info)
+
+    def test_collect_state(self):
+        router, ports = prepare_router_data(enable_snat=True,
+                                            num_internal_ports=2)
+        self.routing_helper._router_added(router['id'], router)
+
+        configurations = {}
+        configurations = self.routing_helper.collect_state(configurations)
+        hd_exp_result = {
+            router['hosting_device']['id']: {'routers': 1}}
+        self.assertEqual(1, configurations['total routers'])
+        self.assertEqual(1, configurations['total ex_gw_ports'])
+        self.assertEqual(2, configurations['total interfaces'])
+        self.assertEqual(0, configurations['total floating_ips'])
+        self.assertEqual(hd_exp_result, configurations['hosting_devices'])
+        self.assertEqual([], configurations['non_responding_hosting_devices'])
+
+    def test_sort_resources_per_hosting_device(self):
+        router1, port = prepare_router_data()
+        router2, port = prepare_router_data()
+        router3, port = prepare_router_data()
+        router4, port = prepare_router_data()
+
+        hd1_id = router1['hosting_device']['id']
+        hd2_id = router4['hosting_device']['id']
+        #Setting router2 and router3 device id same as router1's device id
+        router2['hosting_device']['id'] = hd1_id
+        router3['hosting_device']['id'] = hd1_id
+
+        resources = {'routers': [router1, router2, router4],
+                     'removed_routers': [router3]}
+        devices = self.routing_helper._sort_resources_per_hosting_device(
+            resources)
+
+        self.assertEqual(2, len(devices.keys()))  # Two devices
+        hd1_routers = [router1, router2]
+        self.assertEqual(hd1_routers, devices[hd1_id]['routers'])
+        self.assertEqual([router3], devices[hd1_id]['removed_routers'])
+        self.assertEqual([router4], devices[hd2_id]['routers'])
+
+    def test_get_router_ids_from_removed_devices_info(self):
+        removed_devices_info = {
+            'hosting_data': {'device_1': {'routers': ['id1', 'id2']},
+                             'device_2': {'routers': ['id3', 'id4'],
+                                          'other_key': ['value1', 'value2']}}
+        }
+        resp = self.routing_helper._get_router_ids_from_removed_devices_info(
+            removed_devices_info)
+        self.assertEqual(sorted(resp), sorted(['id1', 'id2', 'id3', 'id4']))
+
+    @mock.patch("eventlet.GreenPool.spawn_n")
+    def test_process_services_full_sync_different_devices(self, mock_spawn):
+        router1, port = prepare_router_data()
+        router2, port = prepare_router_data()
+        self.plugin_api.get_routers = mock.Mock(
+            return_value=[router1, router2])
+        self.routing_helper.process_service()
+        self.assertEqual(2, mock_spawn.call_count)
+        call1 = mock.call(self.routing_helper._process_routers, [router1],
+                          None, router1['hosting_device']['id'],
+                          all_routers=True)
+        call2 = mock.call(self.routing_helper._process_routers, [router2],
+                          None, router2['hosting_device']['id'],
+                          all_routers=True)
+        mock_spawn.assert_has_calls([call1, call2], any_order=True)
+
+    @mock.patch("eventlet.GreenPool.spawn_n")
+    def test_process_services_full_sync_same_device(self, mock_spawn):
+        router1, port = prepare_router_data()
+        router2, port = prepare_router_data()
+        router2['hosting_device']['id'] = router1['hosting_device']['id']
+        self.plugin_api.get_routers = mock.Mock(return_value=[router1,
+                                                              router2])
+        self.routing_helper.process_service()
+        self.assertEqual(1, mock_spawn.call_count)
+        mock_spawn.assert_called_with(self.routing_helper._process_routers,
+                                      [router1, router2],
+                                      None,
+                                      router1['hosting_device']['id'],
+                                      all_routers=True)
+
+    @mock.patch("eventlet.GreenPool.spawn_n")
+    def test_process_services_with_updated_routers(self, mock_spawn):
+
+        router1, port = prepare_router_data()
+
+        def routers_data(context, router_ids=None, hd_ids=None):
+            if router_ids:
+                return [router1]
+        self.plugin_api.get_routers.side_effect = routers_data
+
+        self.routing_helper.fullsync = False
+        self.routing_helper.updated_routers.add(router1['id'])
+        self.routing_helper.process_service()
+        self.assertEqual(1, self.plugin_api.get_routers.call_count)
+        self.plugin_api.get_routers.assert_called_with(
+            self.routing_helper.context,
+            router_ids=[router1['id']])
+        self.assertEqual(1, mock_spawn.call_count)
+        mock_spawn.assert_called_with(self.routing_helper._process_routers,
+                                      [router1],
+                                      None,
+                                      router1['hosting_device']['id'],
+                                      all_routers=False)
+
+    @mock.patch("eventlet.GreenPool.spawn_n")
+    def test_process_services_with_deviceid(self, mock_spawn):
+
+        router, port = prepare_router_data()
+        device_id = router['hosting_device']['id']
+
+        def routers_data(context, router_ids=None, hd_ids=None):
+            if hd_ids:
+                self.assertEqual([device_id], hd_ids)
+                return [router]
+
+        self.plugin_api.get_routers.side_effect = routers_data
+        self.routing_helper.fullsync = False
+        self.routing_helper.process_service(device_ids=[device_id])
+        self.assertEqual(1, self.plugin_api.get_routers.call_count)
+        self.plugin_api.get_routers.assert_called_with(
+            self.routing_helper.context,
+            hd_ids=[device_id])
+        self.assertEqual(1, mock_spawn.call_count)
+        mock_spawn.assert_called_with(self.routing_helper._process_routers,
+                                      [router],
+                                      None,
+                                      device_id,
+                                      all_routers=False)
+
+    @mock.patch("eventlet.GreenPool.spawn_n")
+    def test_process_services_with_removed_routers(self, mock_spawn):
+        router, port = prepare_router_data()
+        device_id = router['hosting_device']['id']
+
+        self._mock_driver_and_hosting_device(self.routing_helper)
+        self.routing_helper.fullsync = False
+        # Emulate router added for setting up internal structures
+        self.routing_helper._router_added(router['id'], router)
+        # Add router to removed routers list and process it
+        self.routing_helper.removed_routers.add(router['id'])
+        self.routing_helper.process_service()
+
+        self.assertEqual(1, mock_spawn.call_count)
+        mock_spawn.assert_called_with(self.routing_helper._process_routers,
+                                      None,
+                                      [router],
+                                      device_id,
+                                      all_routers=False)
+
+    @mock.patch("eventlet.GreenPool.spawn_n")
+    def test_process_services_with_removed_routers_info(self, mock_spawn):
+        router1, port = prepare_router_data()
+        device_id = router1['hosting_device']['id']
+        router2, port = prepare_router_data()
+        router2['hosting_device']['id'] = _uuid()
+
+        removed_devices_info = {
+            'hosting_data': {device_id: {'routers': [router1['id']]}},
+            'deconfigure': True
+        }
+
+        self._mock_driver_and_hosting_device(self.routing_helper)
+        self.routing_helper.fullsync = False
+        # Emulate router added for setting up internal structures
+        self.routing_helper._router_added(router1['id'], router1)
+        self.routing_helper._router_added(router2['id'], router2)
+        # Add router to removed routers list and process it
+        self.routing_helper.removed_routers.add(router2['id'])
+        self.routing_helper.process_service(
+            removed_devices_info=removed_devices_info)
+
+        self.assertEqual(2, mock_spawn.call_count)
+        call1 = mock.call(self.routing_helper._process_routers,
+                          None,
+                          [router1],
+                          router1['hosting_device']['id'],
+                          all_routers=False)
+        call2 = mock.call(self.routing_helper._process_routers,
+                          None,
+                          [router2],
+                          router2['hosting_device']['id'],
+                          all_routers=False)
+        mock_spawn.assert_has_calls([call1, call2], any_order=True)
+
+    @mock.patch("eventlet.GreenPool.spawn_n")
+    def test_process_services_with_rpc_error(self, mock_spawn):
+        router, port = prepare_router_data()
+        self.plugin_api.get_routers.side_effect = n_rpc.RPCException
+        self.routing_helper.fullsync = False
+        self.routing_helper.updated_routers.add(router['id'])
+        self.routing_helper.process_service()
+        self.assertEqual(1, self.plugin_api.get_routers.call_count)
+        self.plugin_api.get_routers.assert_called_with(
+            self.routing_helper.context,
+            router_ids=[router['id']])
+        self.assertFalse(mock_spawn.called)
+        self.assertTrue(self.routing_helper.fullsync)
+
+    def test_process_routers(self):
+        router, port = prepare_router_data()
+        driver = self._mock_driver_and_hosting_device(self.routing_helper)
+        self.routing_helper._process_router = mock.Mock()
+        self.routing_helper._process_routers([router], None)
+        ri = self.routing_helper.router_info[router['id']]
+        driver.router_added.assert_called_with(ri)
+        self.routing_helper._process_router.assert_called_with(ri)
+
+    def _process_routers_floatingips(self, action='add'):
+        router, port = prepare_router_data()
+        driver = self._mock_driver_and_hosting_device(self.routing_helper)
+        ex_gw_port = router['gw_port']
+        floating_ip_address = '19.4.4.10'
+        fixed_ip_address = '35.4.1.10'
+        fixed_ip_address_2 = '35.4.1.15'
+        port_id = 'fake_port_id'
+        floating_ip = {'fixed_ip_address': fixed_ip_address,
+                       'floating_ip_address': floating_ip_address,
+                       'id': 'floating_ip_id',
+                       'port_id': port_id,
+                       'status': 'ACTIVE', }
+        router[l3_constants.FLOATINGIP_KEY] = [floating_ip]
+        ri = RouterInfo(router['id'], router=router)
+
+        # Default add action
+        self.routing_helper._process_router_floating_ips(ri, ex_gw_port)
+        driver.floating_ip_added.assert_called_with(
+            ri, ex_gw_port, floating_ip_address, fixed_ip_address)
+
+        if action == 'remove':
+            router[l3_constants.FLOATINGIP_KEY] = []
+            self.routing_helper._process_router_floating_ips(ri, ex_gw_port)
+            driver.floating_ip_removed.assert_called_with(
+                ri, ri.ex_gw_port, floating_ip_address, fixed_ip_address)
+
+        if action == 'remap':
+            driver.reset_mock()
+            floating_ip_2 = copy.deepcopy(floating_ip)
+            floating_ip_2['fixed_ip_address'] = fixed_ip_address_2
+            ri.router[l3_constants.FLOATINGIP_KEY] = [floating_ip_2]
+
+            self.routing_helper._process_router_floating_ips(ri, ex_gw_port)
+            driver.floating_ip_added.assert_called_with(
+                ri, ri.ex_gw_port, floating_ip_address, fixed_ip_address_2)
+
+            driver.floating_ip_removed.assert_called_with(
+                ri, ri.ex_gw_port, floating_ip_address, fixed_ip_address)
+
+    def test_process_routers_floatingips_add(self):
+        self._process_routers_floatingips(action="add")
+
+    def test_process_routers_floatingips_remove(self):
+        self._process_routers_floatingips(action="remove")
+
+    def test_process_routers_floatingips_remap(self):
+        self._process_routers_floatingips(action="remap")
index 5770756ac38b48ad739a45be1f4bf0a0b0a5ca81..756560d8da67872723c32492e90e393e1e9a323b 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -53,6 +53,7 @@ data_files =
     etc/neutron/plugins/brocade = etc/neutron/plugins/brocade/brocade.ini
     etc/neutron/plugins/cisco =
         etc/neutron/plugins/cisco/cisco_plugins.ini
+        etc/neutron/plugins/cisco/cisco_cfg_agent.ini
         etc/neutron/plugins/cisco/cisco_vpn_agent.ini
     etc/neutron/plugins/embrane = etc/neutron/plugins/embrane/heleos_conf.ini
     etc/neutron/plugins/hyperv = etc/neutron/plugins/hyperv/hyperv_neutron_plugin.ini
@@ -91,6 +92,7 @@ setup-hooks =
 
 [entry_points]
 console_scripts =
+    neutron-cisco-cfg-agent = neutron.plugins.cisco.cfg_agent.cfg_agent:main
     neutron-check-nsx-config = neutron.plugins.vmware.check_nsx_config:main
     neutron-db-manage = neutron.db.migration.cli:main
     neutron-debug = neutron.debug.shell:main