diff --git a/neutron/plugins/cisco/cfg_agent/__init__.py b/neutron/plugins/cisco/cfg_agent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/plugins/cisco/cfg_agent/cfg_agent.py b/neutron/plugins/cisco/cfg_agent/cfg_agent.py new file mode 100644 index 00000000000..a1ae4d8db53 --- /dev/null +++ b/neutron/plugins/cisco/cfg_agent/cfg_agent.py @@ -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 index 00000000000..06ab4d4d6cf --- /dev/null +++ b/neutron/plugins/cisco/cfg_agent/cfg_exceptions.py @@ -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 index 00000000000..e69de29bb2d 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 index 00000000000..e69de29bb2d 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 index 00000000000..dc20c7434cf --- /dev/null +++ b/neutron/plugins/cisco/cfg_agent/device_drivers/csr1kv/cisco_csr1kv_snippets.py @@ -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 = """ +# +# +# <__XML__MODE__exec_configure>%s +# +# +# +# """ + + +#=================================================# +# Set ip address on an interface +# $(config)interface GigabitEthernet 1 +# $(config)ip address 10.0.100.1 255.255.255.0 +#=================================================# +SET_INTC = """ + + + interface %s + ip address %s %s + + +""" + +#=================================================# +# Enable an interface +# $(config)interface GigabitEthernet 1 +# $(config)no shutdown +#=================================================# +ENABLE_INTF = """ + + + interface %s + no shutdown + + +""" + +#=================================================# +# Create VRF +# $(config)ip routing +# $(config)ip vrf nrouter-e7d4y5 +#=================================================# +CREATE_VRF = """ + + + ip routing + ip vrf %s + + +""" + +#=================================================# +# Remove VRF +# $(config)ip routing +# $(config)no ip vrf nrouter-e7d4y5 +#=================================================# +REMOVE_VRF = """ + + + ip routing + no ip vrf %s + + +""" + +#=================================================# +# 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 = """ + + + interface %s + encapsulation dot1Q %s + ip vrf forwarding %s + ip address %s %s + + + +""" + +#=================================================# +# Remove Subinterface +# $(config)no interface GigabitEthernet 2.500 +#=================================================# +REMOVE_SUBINTERFACE = """ + + + no interface %s + + +""" + +#=================================================# +# Enable HSRP on a Subinterface +# $(config)interface GigabitEthernet 2.500 +# $(config)vrf forwarding nrouter-e7d4y5 +# $(config)standby version 2 +# $(config)standby priority +# $(config)standby ip +#=================================================# +SET_INTC_HSRP = """ + + + interface %s + ip vrf forwarding %s + standby version 2 + standby %s priority %s + standby %s ip %s + + + +""" + +#=================================================# +# Remove HSRP on a Subinterface +# $(config)interface GigabitEthernet 2.500 +# $(config)no standby version 2 +# $(config)no standby +#=================================================# +REMOVE_INTC_HSRP = """ + + + interface %s + no standby %s + no standby version 2 + + + +""" + + +#=================================================# +# Create Access Control List +# $(config)ip access-list standard acl_500 +# $(config)permit 192.168.0.1 255.255.255.0 +#=================================================# +CREATE_ACL = """ + + + ip access-list standard %s + permit %s %s + + +""" + +#=================================================# +# Remove Access Control List +# $(config)no ip access-list standard acl_500 +#=================================================# +REMOVE_ACL = """ + + + no ip access-list standard %s + + +""" + +#=========================================================================# +# Set Dynamic source translation on an interface +# Syntax: ip nat inside source list interface +# .......vrf 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 = """ + + + ip nat inside source list %s interface %s vrf %s + overload + + + +""" + +#=========================================================================# +# Remove Dynamic source translation on an interface +# Syntax: no ip nat inside source list interface +# .......vrf overload +# eg: $(config)no ip nat inside source list acl_500 +# ..........interface GigabitEthernet3.100 vrf nrouter-e7d4y5 overload +#========================================================================# +REMOVE_DYN_SRC_TRL_INTFC = """ + + + no ip nat inside source list %s interface %s vrf %s + overload + + + +""" + +#=================================================# +# Set NAT +# Syntax : interface +# ip nat +#=================================================# +SET_NAT = """ + + + interface %s + ip nat %s + + +""" + +#=================================================# +# Remove NAT +# Syntax : interface +# no ip nat +#=================================================# +REMOVE_NAT = """ + + + interface %s + no ip nat %s + + +""" + +#=========================================================================# +# Set Static source translation on an interface +# Syntax: ip nat inside source static +# .......vrf 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 = """ + + + ip nat inside source static %s %s vrf %s match-in-vrf + + + +""" + +#=========================================================================# +# Remove Static source translation on an interface +# Syntax: no ip nat inside source static +# .......vrf 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 = """ + + + no ip nat inside source static %s %s vrf %s match-in-vrf + + + +""" + +#=============================================================================# +# Set ip route +# Syntax: ip route vrf [] +# eg: $(config)ip route vrf nrouter-e7d4y5 8.8.0.0 255.255.0.0 10.0.100.255 +#=============================================================================# +SET_IP_ROUTE = """ + + + ip route vrf %s %s %s %s + + +""" + +#=============================================================================# +# Remove ip route +# Syntax: no ip route vrf +# [] +# eg: $(config)no ip route vrf nrouter-e7d4y5 8.8.0.0 255.255.0.0 10.0.100.255 +#=============================================================================# +REMOVE_IP_ROUTE = """ + + + no ip route vrf %s %s %s %s + + +""" +#=============================================================================# +# Set default ip route +# Syntax: ip route vrf 0.0.0.0 0.0.0.0 [] +# 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 = """ + + + ip route vrf %s 0.0.0.0 0.0.0.0 %s + + +""" + +#=============================================================================# +# Remove default ip route +# Syntax: ip route vrf 0.0.0.0 0.0.0.0 [] +# eg: $(config)ip route vrf nrouter-e7d4y5 0.0.0.0 0.0.0.0 10.0.100.255 +#=============================================================================# +REMOVE_DEFAULT_ROUTE = """ + + + no ip route vrf %s 0.0.0.0 0.0.0.0 %s + + +""" + +#=============================================================================# +# 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 = """ +# +# clear ip nat translation forced +# +# """ +CLEAR_DYN_NAT_TRANS = """ + + + do clear ip nat translation forced + + +""" 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 index 00000000000..68f8fac77d0 --- /dev/null +++ b/neutron/plugins/cisco/cfg_agent/device_drivers/csr1kv/csr1kv_routing_driver.py @@ -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: : + 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 = 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. + + + + + In case of error, CSR1kv sends a response as follows. + We take the error type and tag. + + + + protocol + operation-failed + error + + + :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 "" 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 index 00000000000..48c60166d30 --- /dev/null +++ b/neutron/plugins/cisco/cfg_agent/device_drivers/devicedriver_api.py @@ -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 index 00000000000..c04c96b391f --- /dev/null +++ b/neutron/plugins/cisco/cfg_agent/device_drivers/driver_mgr.py @@ -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': + 'hosting_device': { 'id': , } + 'router_type': {'cfg_agent_driver': , } + } + :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 index 00000000000..926cc47e546 --- /dev/null +++ b/neutron/plugins/cisco/cfg_agent/device_drivers/dummy_driver.py @@ -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 index 00000000000..b5c50074740 --- /dev/null +++ b/neutron/plugins/cisco/cfg_agent/device_status.py @@ -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': [,..], 'dead': [,..]} + """ + 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 index 00000000000..e69de29bb2d 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 index 00000000000..474efc0cb33 --- /dev/null +++ b/neutron/plugins/cisco/cfg_agent/service_helpers/routing_svc_helper.py @@ -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) diff --git a/neutron/plugins/cisco/common/cisco_constants.py b/neutron/plugins/cisco/common/cisco_constants.py index b4a9f1408f9..b90123c6143 100644 --- a/neutron/plugins/cisco/common/cisco_constants.py +++ b/neutron/plugins/cisco/common/cisco_constants.py @@ -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 index 00000000000..e69de29bb2d 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 index 00000000000..740a7308148 --- /dev/null +++ b/neutron/tests/unit/cisco/cfg_agent/test_cfg_agent.py @@ -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 index 00000000000..c93b2d45b01 --- /dev/null +++ b/neutron/tests/unit/cisco/cfg_agent/test_csr1kv_routing_driver.py @@ -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 index 00000000000..d296ddf6731 --- /dev/null +++ b/neutron/tests/unit/cisco/cfg_agent/test_device_status.py @@ -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 index 00000000000..38a6840aa79 --- /dev/null +++ b/neutron/tests/unit/cisco/cfg_agent/test_routing_svc_helper.py @@ -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") diff --git a/setup.cfg b/setup.cfg index 5770756ac38..756560d8da6 100644 --- 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