Merge "Configuration agent for Cisco devices"

This commit is contained in:
Jenkins 2014-08-01 17:51:01 +00:00 committed by Gerrit Code Review
commit e5949d151c
20 changed files with 3882 additions and 0 deletions

View File

@ -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()

View File

@ -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.")

View File

@ -0,0 +1,351 @@
# Copyright 2014 Cisco Systems, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: Hareesh Puthalath, Cisco Systems, Inc.
"""
CSR (IOS-XE) XML-based configuration snippets
"""
# The standard Template used to interact with IOS-XE(CSR).
# This template is added by the netconf client
# EXEC_CONF_SNIPPET = """
# <config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
# <configure>
# <__XML__MODE__exec_configure>%s
# </__XML__MODE__exec_configure>
# </configure>
# </config>
# """
#=================================================#
# Set ip address on an interface
# $(config)interface GigabitEthernet 1
# $(config)ip address 10.0.100.1 255.255.255.0
#=================================================#
SET_INTC = """
<config>
<cli-config-data>
<cmd>interface %s</cmd>
<cmd>ip address %s %s</cmd>
</cli-config-data>
</config>
"""
#=================================================#
# Enable an interface
# $(config)interface GigabitEthernet 1
# $(config)no shutdown
#=================================================#
ENABLE_INTF = """
<config>
<cli-config-data>
<cmd>interface %s</cmd>
<cmd>no shutdown</cmd>
</cli-config-data>
</config>
"""
#=================================================#
# Create VRF
# $(config)ip routing
# $(config)ip vrf nrouter-e7d4y5
#=================================================#
CREATE_VRF = """
<config>
<cli-config-data>
<cmd>ip routing</cmd>
<cmd>ip vrf %s</cmd>
</cli-config-data>
</config>
"""
#=================================================#
# Remove VRF
# $(config)ip routing
# $(config)no ip vrf nrouter-e7d4y5
#=================================================#
REMOVE_VRF = """
<config>
<cli-config-data>
<cmd>ip routing</cmd>
<cmd>no ip vrf %s</cmd>
</cli-config-data>
</config>
"""
#=================================================#
# Create Subinterface
# $(config)interface GigabitEthernet 2.500
# $(config)encapsulation dot1Q 500
# $(config)vrf forwarding nrouter-e7d4y5
# $(config)ip address 192.168.0.1 255.255.255.0
#=================================================#
CREATE_SUBINTERFACE = """
<config>
<cli-config-data>
<cmd>interface %s</cmd>
<cmd>encapsulation dot1Q %s</cmd>
<cmd>ip vrf forwarding %s</cmd>
<cmd>ip address %s %s</cmd>
</cli-config-data>
</config>
"""
#=================================================#
# Remove Subinterface
# $(config)no interface GigabitEthernet 2.500
#=================================================#
REMOVE_SUBINTERFACE = """
<config>
<cli-config-data>
<cmd>no interface %s</cmd>
</cli-config-data>
</config>
"""
#=================================================#
# Enable HSRP on a Subinterface
# $(config)interface GigabitEthernet 2.500
# $(config)vrf forwarding nrouter-e7d4y5
# $(config)standby version 2
# $(config)standby <group> priority <priority>
# $(config)standby <group> ip <ip>
#=================================================#
SET_INTC_HSRP = """
<config>
<cli-config-data>
<cmd>interface %s</cmd>
<cmd>ip vrf forwarding %s</cmd>
<cmd>standby version 2</cmd>
<cmd>standby %s priority %s</cmd>
<cmd>standby %s ip %s</cmd>
</cli-config-data>
</config>
"""
#=================================================#
# Remove HSRP on a Subinterface
# $(config)interface GigabitEthernet 2.500
# $(config)no standby version 2
# $(config)no standby <group>
#=================================================#
REMOVE_INTC_HSRP = """
<config>
<cli-config-data>
<cmd>interface %s</cmd>
<cmd>no standby %s</cmd>
<cmd>no standby version 2</cmd>
</cli-config-data>
</config>
"""
#=================================================#
# Create Access Control List
# $(config)ip access-list standard acl_500
# $(config)permit 192.168.0.1 255.255.255.0
#=================================================#
CREATE_ACL = """
<config>
<cli-config-data>
<cmd>ip access-list standard %s</cmd>
<cmd>permit %s %s</cmd>
</cli-config-data>
</config>
"""
#=================================================#
# Remove Access Control List
# $(config)no ip access-list standard acl_500
#=================================================#
REMOVE_ACL = """
<config>
<cli-config-data>
<cmd>no ip access-list standard %s</cmd>
</cli-config-data>
</config>
"""
#=========================================================================#
# Set Dynamic source translation on an interface
# Syntax: ip nat inside source list <acl_no> interface <interface>
# .......vrf <vrf_name> overload
# eg: $(config)ip nat inside source list acl_500
# ..........interface GigabitEthernet3.100 vrf nrouter-e7d4y5 overload
#========================================================================#
SNAT_CFG = "ip nat inside source list %s interface %s vrf %s overload"
SET_DYN_SRC_TRL_INTFC = """
<config>
<cli-config-data>
<cmd>ip nat inside source list %s interface %s vrf %s
overload</cmd>
</cli-config-data>
</config>
"""
#=========================================================================#
# Remove Dynamic source translation on an interface
# Syntax: no ip nat inside source list <acl_no> interface <interface>
# .......vrf <vrf_name> overload
# eg: $(config)no ip nat inside source list acl_500
# ..........interface GigabitEthernet3.100 vrf nrouter-e7d4y5 overload
#========================================================================#
REMOVE_DYN_SRC_TRL_INTFC = """
<config>
<cli-config-data>
<cmd>no ip nat inside source list %s interface %s vrf %s
overload</cmd>
</cli-config-data>
</config>
"""
#=================================================#
# Set NAT
# Syntax : interface <interface>
# ip nat <inside|outside>
#=================================================#
SET_NAT = """
<config>
<cli-config-data>
<cmd>interface %s</cmd>
<cmd>ip nat %s</cmd>
</cli-config-data>
</config>
"""
#=================================================#
# Remove NAT
# Syntax : interface <interface>
# no ip nat <inside|outside>
#=================================================#
REMOVE_NAT = """
<config>
<cli-config-data>
<cmd>interface %s</cmd>
<cmd>no ip nat %s</cmd>
</cli-config-data>
</config>
"""
#=========================================================================#
# Set Static source translation on an interface
# Syntax: ip nat inside source static <fixed_ip> <floating_ip>
# .......vrf <vrf_name> match-in-vrf
# eg: $(config)ip nat inside source static 192.168.0.1 121.158.0.5
# ..........vrf nrouter-e7d4y5 match-in-vrf
#========================================================================#
SET_STATIC_SRC_TRL = """
<config>
<cli-config-data>
<cmd>ip nat inside source static %s %s vrf %s match-in-vrf</cmd>
</cli-config-data>
</config>
"""
#=========================================================================#
# Remove Static source translation on an interface
# Syntax: no ip nat inside source static <fixed_ip> <floating_ip>
# .......vrf <vrf_name> match-in-vrf
# eg: $(config)no ip nat inside source static 192.168.0.1 121.158.0.5
# ..........vrf nrouter-e7d4y5 match-in-vrf
#========================================================================#
REMOVE_STATIC_SRC_TRL = """
<config>
<cli-config-data>
<cmd>no ip nat inside source static %s %s vrf %s match-in-vrf</cmd>
</cli-config-data>
</config>
"""
#=============================================================================#
# Set ip route
# Syntax: ip route vrf <vrf-name> <destination> <mask> [<interface>] <next hop>
# eg: $(config)ip route vrf nrouter-e7d4y5 8.8.0.0 255.255.0.0 10.0.100.255
#=============================================================================#
SET_IP_ROUTE = """
<config>
<cli-config-data>
<cmd>ip route vrf %s %s %s %s</cmd>
</cli-config-data>
</config>
"""
#=============================================================================#
# Remove ip route
# Syntax: no ip route vrf <vrf-name> <destination> <mask>
# [<interface>] <next hop>
# eg: $(config)no ip route vrf nrouter-e7d4y5 8.8.0.0 255.255.0.0 10.0.100.255
#=============================================================================#
REMOVE_IP_ROUTE = """
<config>
<cli-config-data>
<cmd>no ip route vrf %s %s %s %s</cmd>
</cli-config-data>
</config>
"""
#=============================================================================#
# Set default ip route
# Syntax: ip route vrf <vrf-name> 0.0.0.0 0.0.0.0 [<interface>] <next hop>
# eg: $(config)ip route vrf nrouter-e7d4y5 0.0.0.0 0.0.0.0 10.0.100.255
#=============================================================================#
DEFAULT_ROUTE_CFG = 'ip route vrf %s 0.0.0.0 0.0.0.0 %s'
SET_DEFAULT_ROUTE = """
<config>
<cli-config-data>
<cmd>ip route vrf %s 0.0.0.0 0.0.0.0 %s</cmd>
</cli-config-data>
</config>
"""
#=============================================================================#
# Remove default ip route
# Syntax: ip route vrf <vrf-name> 0.0.0.0 0.0.0.0 [<interface>] <next hop>
# eg: $(config)ip route vrf nrouter-e7d4y5 0.0.0.0 0.0.0.0 10.0.100.255
#=============================================================================#
REMOVE_DEFAULT_ROUTE = """
<config>
<cli-config-data>
<cmd>no ip route vrf %s 0.0.0.0 0.0.0.0 %s</cmd>
</cli-config-data>
</config>
"""
#=============================================================================#
# Clear dynamic nat translations. This is used to clear any nat bindings before
# we can turn off NAT on an interface
# Syntax: clear ip nat translation [forced]
#=============================================================================#
# CLEAR_DYN_NAT_TRANS = """
# <oper-data-format-text-block>
# <exec>clear ip nat translation forced</exec>
# </oper-data-format-text-block>
# """
CLEAR_DYN_NAT_TRANS = """
<config>
<cli-config-data>
<cmd>do clear ip nat translation forced</cmd>
</cli-config-data>
</config>
"""

View File

@ -0,0 +1,687 @@
# Copyright 2014 Cisco Systems, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: Hareesh Puthalath, Cisco Systems, Inc.
import logging
import netaddr
import re
import time
import xml.etree.ElementTree as ET
import ciscoconfparse
from ncclient import manager
from oslo.config import cfg
from neutron.plugins.cisco.cfg_agent import cfg_exceptions as cfg_exc
from neutron.plugins.cisco.cfg_agent.device_drivers.csr1kv import (
cisco_csr1kv_snippets as snippets)
from neutron.plugins.cisco.cfg_agent.device_drivers import devicedriver_api
LOG = logging.getLogger(__name__)
# N1kv constants
T1_PORT_NAME_PREFIX = 't1_p:' # T1 port/network is for VXLAN
T2_PORT_NAME_PREFIX = 't2_p:' # T2 port/network is for VLAN
class CSR1kvRoutingDriver(devicedriver_api.RoutingDriverBase):
"""CSR1kv Routing Driver.
This driver encapsulates the configuration logic via NETCONF protocol to
configure a CSR1kv Virtual Router (IOS-XE based) for implementing
Neutron L3 services. These services include routing, NAT and floating
IPs (as per Neutron terminology).
"""
DEV_NAME_LEN = 14
def __init__(self, **device_params):
try:
self._csr_host = device_params['management_ip_address']
self._csr_ssh_port = device_params['protocol_port']
credentials = device_params['credentials']
if credentials:
self._csr_user = credentials['username']
self._csr_password = credentials['password']
self._timeout = cfg.CONF.device_connection_timeout
self._csr_conn = None
self._intfs_enabled = False
except KeyError as e:
LOG.error(_("Missing device parameter:%s. Aborting "
"CSR1kvRoutingDriver initialization"), e)
raise cfg_exc.CSR1kvInitializationException
###### Public Functions ########
def router_added(self, ri):
self._csr_create_vrf(ri)
def router_removed(self, ri):
self._csr_remove_vrf(ri)
def internal_network_added(self, ri, port):
self._csr_create_subinterface(ri, port)
if port.get('ha_info') is not None and ri.ha_info['ha:enabled']:
self._csr_add_ha(ri, port)
def internal_network_removed(self, ri, port):
self._csr_remove_subinterface(port)
def external_gateway_added(self, ri, ex_gw_port):
self._csr_create_subinterface(ri, ex_gw_port)
ex_gw_ip = ex_gw_port['subnet']['gateway_ip']
if ex_gw_ip:
#Set default route via this network's gateway ip
self._csr_add_default_route(ri, ex_gw_ip)
def external_gateway_removed(self, ri, ex_gw_port):
ex_gw_ip = ex_gw_port['subnet']['gateway_ip']
if ex_gw_ip:
#Remove default route via this network's gateway ip
self._csr_remove_default_route(ri, ex_gw_ip)
#Finally, remove external network subinterface
self._csr_remove_subinterface(ex_gw_port)
def enable_internal_network_NAT(self, ri, port, ex_gw_port):
self._csr_add_internalnw_nat_rules(ri, port, ex_gw_port)
def disable_internal_network_NAT(self, ri, port, ex_gw_port):
self._csr_remove_internalnw_nat_rules(ri, [port], ex_gw_port)
def floating_ip_added(self, ri, ex_gw_port, floating_ip, fixed_ip):
self._csr_add_floating_ip(ri, floating_ip, fixed_ip)
def floating_ip_removed(self, ri, ex_gw_port, floating_ip, fixed_ip):
self._csr_remove_floating_ip(ri, ex_gw_port, floating_ip, fixed_ip)
def routes_updated(self, ri, action, route):
self._csr_update_routing_table(ri, action, route)
def clear_connection(self):
self._csr_conn = None
##### Internal Functions ####
def _csr_create_subinterface(self, ri, port):
vrf_name = self._csr_get_vrf_name(ri)
ip_cidr = port['ip_cidr']
netmask = netaddr.IPNetwork(ip_cidr).netmask
gateway_ip = ip_cidr.split('/')[0]
subinterface = self._get_interface_name_from_hosting_port(port)
vlan = self._get_interface_vlan_from_hosting_port(port)
self._create_subinterface(subinterface, vlan, vrf_name,
gateway_ip, netmask)
def _csr_remove_subinterface(self, port):
subinterface = self._get_interface_name_from_hosting_port(port)
self._remove_subinterface(subinterface)
def _csr_add_ha(self, ri, port):
func_dict = {
'HSRP': CSR1kvRoutingDriver._csr_add_ha_HSRP,
'VRRP': CSR1kvRoutingDriver._csr_add_ha_VRRP,
'GBLP': CSR1kvRoutingDriver._csr_add_ha_GBLP
}
#Invoke the right function for the ha type
func_dict[ri.ha_info['ha:type']](self, ri, port)
def _csr_add_ha_HSRP(self, ri, port):
priority = ri.ha_info['priority']
port_ha_info = port['ha_info']
group = port_ha_info['group']
ip = port_ha_info['virtual_port']['fixed_ips'][0]['ip_address']
if ip and group and priority:
vrf_name = self._csr_get_vrf_name(ri)
subinterface = self._get_interface_name_from_hosting_port(port)
self._set_ha_HSRP(subinterface, vrf_name, priority, group, ip)
def _csr_add_ha_VRRP(self, ri, port):
raise NotImplementedError
def _csr_add_ha_GBLP(self, ri, port):
raise NotImplementedError
def _csr_remove_ha(self, ri, port):
pass
def _csr_add_internalnw_nat_rules(self, ri, port, ex_port):
vrf_name = self._csr_get_vrf_name(ri)
in_vlan = self._get_interface_vlan_from_hosting_port(port)
acl_no = 'acl_' + str(in_vlan)
internal_cidr = port['ip_cidr']
internal_net = netaddr.IPNetwork(internal_cidr).network
netmask = netaddr.IPNetwork(internal_cidr).hostmask
inner_intfc = self._get_interface_name_from_hosting_port(port)
outer_intfc = self._get_interface_name_from_hosting_port(ex_port)
self._nat_rules_for_internet_access(acl_no, internal_net,
netmask, inner_intfc,
outer_intfc, vrf_name)
def _csr_remove_internalnw_nat_rules(self, ri, ports, ex_port):
acls = []
#First disable nat in all inner ports
for port in ports:
in_intfc_name = self._get_interface_name_from_hosting_port(port)
inner_vlan = self._get_interface_vlan_from_hosting_port(port)
acls.append("acl_" + str(inner_vlan))
self._remove_interface_nat(in_intfc_name, 'inside')
#Wait for two second
LOG.debug("Sleep for 2 seconds before clearing NAT rules")
time.sleep(2)
#Clear the NAT translation table
self._remove_dyn_nat_translations()
# Remove dynamic NAT rules and ACLs
vrf_name = self._csr_get_vrf_name(ri)
ext_intfc_name = self._get_interface_name_from_hosting_port(ex_port)
for acl in acls:
self._remove_dyn_nat_rule(acl, ext_intfc_name, vrf_name)
def _csr_add_default_route(self, ri, gw_ip):
vrf_name = self._csr_get_vrf_name(ri)
self._add_default_static_route(gw_ip, vrf_name)
def _csr_remove_default_route(self, ri, gw_ip):
vrf_name = self._csr_get_vrf_name(ri)
self._remove_default_static_route(gw_ip, vrf_name)
def _csr_add_floating_ip(self, ri, floating_ip, fixed_ip):
vrf_name = self._csr_get_vrf_name(ri)
self._add_floating_ip(floating_ip, fixed_ip, vrf_name)
def _csr_remove_floating_ip(self, ri, ex_gw_port, floating_ip, fixed_ip):
vrf_name = self._csr_get_vrf_name(ri)
out_intfc_name = self._get_interface_name_from_hosting_port(ex_gw_port)
# First remove NAT from outer interface
self._remove_interface_nat(out_intfc_name, 'outside')
#Clear the NAT translation table
self._remove_dyn_nat_translations()
#Remove the floating ip
self._remove_floating_ip(floating_ip, fixed_ip, vrf_name)
#Enable NAT on outer interface
self._add_interface_nat(out_intfc_name, 'outside')
def _csr_update_routing_table(self, ri, action, route):
vrf_name = self._csr_get_vrf_name(ri)
destination_net = netaddr.IPNetwork(route['destination'])
dest = destination_net.network
dest_mask = destination_net.netmask
next_hop = route['nexthop']
if action is 'replace':
self._add_static_route(dest, dest_mask, next_hop, vrf_name)
elif action is 'delete':
self._remove_static_route(dest, dest_mask, next_hop, vrf_name)
else:
LOG.error(_('Unknown route command %s'), action)
def _csr_create_vrf(self, ri):
vrf_name = self._csr_get_vrf_name(ri)
self._create_vrf(vrf_name)
def _csr_remove_vrf(self, ri):
vrf_name = self._csr_get_vrf_name(ri)
self._remove_vrf(vrf_name)
def _csr_get_vrf_name(self, ri):
return ri.router_name()[:self.DEV_NAME_LEN]
def _get_connection(self):
"""Make SSH connection to the CSR.
The external ncclient library is used for creating this connection.
This method keeps state of any existing connections and reuses them if
already connected. Also CSR1kv's interfaces (except management) are
disabled by default when it is booted. So if connecting for the first
time, driver will enable all other interfaces and keep that status in
the `_intfs_enabled` flag.
"""
try:
if self._csr_conn and self._csr_conn.connected:
return self._csr_conn
else:
self._csr_conn = manager.connect(host=self._csr_host,
port=self._csr_ssh_port,
username=self._csr_user,
password=self._csr_password,
device_params={'name': "csr"},
timeout=self._timeout)
if not self._intfs_enabled:
self._intfs_enabled = self._enable_intfs(self._csr_conn)
return self._csr_conn
except Exception as e:
conn_params = {'host': self._csr_host, 'port': self._csr_ssh_port,
'user': self._csr_user,
'timeout': self._timeout, 'reason': e.message}
raise cfg_exc.CSR1kvConnectionException(**conn_params)
def _get_interface_name_from_hosting_port(self, port):
vlan = self._get_interface_vlan_from_hosting_port(port)
int_no = self._get_interface_no_from_hosting_port(port)
intfc_name = 'GigabitEthernet%s.%s' % (int_no, vlan)
return intfc_name
@staticmethod
def _get_interface_vlan_from_hosting_port(port):
return port['hosting_info']['segmentation_id']
@staticmethod
def _get_interface_no_from_hosting_port(port):
"""Calculate interface number from the hosting port's name.
Interfaces in the CSR1kv are created in pairs (T1 and T2) where
T1 interface is used for VLAN and T2 interface for VXLAN traffic
respectively. On the neutron side these are named T1 and T2 ports and
follows the naming convention: <Tx_PORT_NAME_PREFIX>:<PAIR_INDEX>
where the `PORT_NAME_PREFIX` indicates either VLAN or VXLAN and
`PAIR_INDEX` is the pair number. `PAIR_INDEX` starts at 1.
In CSR1kv, GigabitEthernet 0 is not present and GigabitEthernet 1
is used as a management interface (Note: this might change in
future). So the first (T1,T2) pair corresponds to
(GigabitEthernet 2, GigabitEthernet 3) and so forth. This function
extracts the `PAIR_INDEX` and calculates the corresponding interface
number.
:param port: neutron port corresponding to the interface.
:return: number of the interface (eg: 1 in case of GigabitEthernet1)
"""
_name = port['hosting_info']['hosting_port_name']
if_type = _name.split(':')[0] + ':'
if if_type == T1_PORT_NAME_PREFIX:
return str(int(_name.split(':')[1]) * 2)
elif if_type == T2_PORT_NAME_PREFIX:
return str(int(_name.split(':')[1]) * 2 + 1)
else:
params = {'attribute': 'hosting_port_name', 'value': _name}
raise cfg_exc.CSR1kvUnknownValueException(**params)
def _get_interfaces(self):
"""Get a list of interfaces on this hosting device.
:return: List of the interfaces
"""
ioscfg = self._get_running_config()
parse = ciscoconfparse.CiscoConfParse(ioscfg)
intfs_raw = parse.find_lines("^interface GigabitEthernet")
intfs = [raw_if.strip().split(' ')[1] for raw_if in intfs_raw]
LOG.info(_("Interfaces:%s"), intfs)
return intfs
def _get_interface_ip(self, interface_name):
"""Get the ip address for an interface.
:param interface_name: interface_name as a string
:return: ip address of interface as a string
"""
ioscfg = self._get_running_config()
parse = ciscoconfparse.CiscoConfParse(ioscfg)
children = parse.find_children("^interface %s" % interface_name)
for line in children:
if 'ip address' in line:
ip_address = line.strip().split(' ')[2]
LOG.info(_("IP Address:%s"), ip_address)
return ip_address
LOG.warn(_("Cannot find interface: %s"), interface_name)
return None
def _interface_exists(self, interface):
"""Check whether interface exists."""
ioscfg = self._get_running_config()
parse = ciscoconfparse.CiscoConfParse(ioscfg)
intfs_raw = parse.find_lines("^interface " + interface)
return len(intfs_raw) > 0
def _enable_intfs(self, conn):
"""Enable the interfaces of a CSR1kv Virtual Router.
When the virtual router first boots up, all interfaces except
management are down. This method will enable all data interfaces.
Note: In CSR1kv, GigabitEthernet 0 is not present. GigabitEthernet 1
is used as management and GigabitEthernet 2 and up are used for data.
This might change in future releases.
Currently only the second and third Gig interfaces corresponding to a
single (T1,T2) pair and configured as trunk for VLAN and VXLAN
is enabled.
:param conn: Connection object
:return: True or False
"""
#ToDo(Hareesh): Interfaces are hard coded for now. Make it dynamic.
interfaces = ['GigabitEthernet 2', 'GigabitEthernet 3']
try:
for i in interfaces:
confstr = snippets.ENABLE_INTF % i
rpc_obj = conn.edit_config(target='running', config=confstr)
if self._check_response(rpc_obj, 'ENABLE_INTF'):
LOG.info(_("Enabled interface %s "), i)
time.sleep(1)
except Exception:
return False
return True
def _get_vrfs(self):
"""Get the current VRFs configured in the device.
:return: A list of vrf names as string
"""
vrfs = []
ioscfg = self._get_running_config()
parse = ciscoconfparse.CiscoConfParse(ioscfg)
vrfs_raw = parse.find_lines("^ip vrf")
for line in vrfs_raw:
# raw format ['ip vrf <vrf-name>',....]
vrf_name = line.strip().split(' ')[2]
vrfs.append(vrf_name)
LOG.info(_("VRFs:%s"), vrfs)
return vrfs
def _get_capabilities(self):
"""Get the servers NETCONF capabilities.
:return: List of server capabilities.
"""
conn = self._get_connection()
capabilities = []
for c in conn.server_capabilities:
capabilities.append(c)
LOG.debug("Server capabilities: %s", capabilities)
return capabilities
def _get_running_config(self):
"""Get the CSR's current running config.
:return: Current IOS running config as multiline string
"""
conn = self._get_connection()
config = conn.get_config(source="running")
if config:
root = ET.fromstring(config._raw)
running_config = root[0][0]
rgx = re.compile("\r*\n+")
ioscfg = rgx.split(running_config.text)
return ioscfg
def _check_acl(self, acl_no, network, netmask):
"""Check a ACL config exists in the running config.
:param acl_no: access control list (ACL) number
:param network: network which this ACL permits
:param netmask: netmask of the network
:return:
"""
exp_cfg_lines = ['ip access-list standard ' + str(acl_no),
' permit ' + str(network) + ' ' + str(netmask)]
ioscfg = self._get_running_config()
parse = ciscoconfparse.CiscoConfParse(ioscfg)
acls_raw = parse.find_children(exp_cfg_lines[0])
if acls_raw:
if exp_cfg_lines[1] in acls_raw:
return True
LOG.error(_("Mismatch in ACL configuration for %s"), acl_no)
return False
LOG.debug("%s is not present in config", acl_no)
return False
def _cfg_exists(self, cfg_str):
"""Check a partial config string exists in the running config.
:param cfg_str: config string to check
:return : True or False
"""
ioscfg = self._get_running_config()
parse = ciscoconfparse.CiscoConfParse(ioscfg)
cfg_raw = parse.find_lines("^" + cfg_str)
LOG.debug("_cfg_exists(): Found lines %s", cfg_raw)
return len(cfg_raw) > 0
def _set_interface(self, name, ip_address, mask):
conn = self._get_connection()
confstr = snippets.SET_INTC % (name, ip_address, mask)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'SET_INTC')
def _create_vrf(self, vrf_name):
try:
conn = self._get_connection()
confstr = snippets.CREATE_VRF % vrf_name
rpc_obj = conn.edit_config(target='running', config=confstr)
if self._check_response(rpc_obj, 'CREATE_VRF'):
LOG.info(_("VRF %s successfully created"), vrf_name)
except Exception:
LOG.exception(_("Failed creating VRF %s"), vrf_name)
def _remove_vrf(self, vrf_name):
if vrf_name in self._get_vrfs():
conn = self._get_connection()
confstr = snippets.REMOVE_VRF % vrf_name
rpc_obj = conn.edit_config(target='running', config=confstr)
if self._check_response(rpc_obj, 'REMOVE_VRF'):
LOG.info(_("VRF %s removed"), vrf_name)
else:
LOG.warning(_("VRF %s not present"), vrf_name)
def _create_subinterface(self, subinterface, vlan_id, vrf_name, ip, mask):
if vrf_name not in self._get_vrfs():
LOG.error(_("VRF %s not present"), vrf_name)
confstr = snippets.CREATE_SUBINTERFACE % (subinterface, vlan_id,
vrf_name, ip, mask)
self._edit_running_config(confstr, 'CREATE_SUBINTERFACE')
def _remove_subinterface(self, subinterface):
#Optional : verify this is the correct subinterface
if self._interface_exists(subinterface):
confstr = snippets.REMOVE_SUBINTERFACE % subinterface
self._edit_running_config(confstr, 'REMOVE_SUBINTERFACE')
def _set_ha_HSRP(self, subinterface, vrf_name, priority, group, ip):
if vrf_name not in self._get_vrfs():
LOG.error(_("VRF %s not present"), vrf_name)
confstr = snippets.SET_INTC_HSRP % (subinterface, vrf_name, group,
priority, group, ip)
action = "SET_INTC_HSRP (Group: %s, Priority: % s)" % (group, priority)
self._edit_running_config(confstr, action)
def _remove_ha_HSRP(self, subinterface, group):
confstr = snippets.REMOVE_INTC_HSRP % (subinterface, group)
action = ("REMOVE_INTC_HSRP (subinterface:%s, Group:%s)"
% (subinterface, group))
self._edit_running_config(confstr, action)
def _get_interface_cfg(self, interface):
ioscfg = self._get_running_config()
parse = ciscoconfparse.CiscoConfParse(ioscfg)
return parse.find_children('interface ' + interface)
def _nat_rules_for_internet_access(self, acl_no, network,
netmask,
inner_intfc,
outer_intfc,
vrf_name):
"""Configure the NAT rules for an internal network.
Configuring NAT rules in the CSR1kv is a three step process. First
create an ACL for the IP range of the internal network. Then enable
dynamic source NATing on the external interface of the CSR for this
ACL and VRF of the neutron router. Finally enable NAT on the
interfaces of the CSR where the internal and external networks are
connected.
:param acl_no: ACL number of the internal network.
:param network: internal network
:param netmask: netmask of the internal network.
:param inner_intfc: (name of) interface connected to the internal
network
:param outer_intfc: (name of) interface connected to the external
network
:param vrf_name: VRF corresponding to this virtual router
:return: True if configuration succeeded
:raises: neutron.plugins.cisco.cfg_agent.cfg_exceptions.
CSR1kvConfigException
"""
conn = self._get_connection()
# Duplicate ACL creation throws error, so checking
# it first. Remove it in future as this is not common in production
acl_present = self._check_acl(acl_no, network, netmask)
if not acl_present:
confstr = snippets.CREATE_ACL % (acl_no, network, netmask)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'CREATE_ACL')
confstr = snippets.SET_DYN_SRC_TRL_INTFC % (acl_no, outer_intfc,
vrf_name)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'CREATE_SNAT')
confstr = snippets.SET_NAT % (inner_intfc, 'inside')
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'SET_NAT')
confstr = snippets.SET_NAT % (outer_intfc, 'outside')
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'SET_NAT')
def _add_interface_nat(self, intfc_name, intfc_type):
conn = self._get_connection()
confstr = snippets.SET_NAT % (intfc_name, intfc_type)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'SET_NAT ' + intfc_type)
def _remove_interface_nat(self, intfc_name, intfc_type):
conn = self._get_connection()
confstr = snippets.REMOVE_NAT % (intfc_name, intfc_type)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'REMOVE_NAT ' + intfc_type)
def _remove_dyn_nat_rule(self, acl_no, outer_intfc_name, vrf_name):
conn = self._get_connection()
confstr = snippets.SNAT_CFG % (acl_no, outer_intfc_name, vrf_name)
if self._cfg_exists(confstr):
confstr = snippets.REMOVE_DYN_SRC_TRL_INTFC % (acl_no,
outer_intfc_name,
vrf_name)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'REMOVE_DYN_SRC_TRL_INTFC')
confstr = snippets.REMOVE_ACL % acl_no
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'REMOVE_ACL')
def _remove_dyn_nat_translations(self):
conn = self._get_connection()
confstr = snippets.CLEAR_DYN_NAT_TRANS
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'CLEAR_DYN_NAT_TRANS')
def _add_floating_ip(self, floating_ip, fixed_ip, vrf):
conn = self._get_connection()
confstr = snippets.SET_STATIC_SRC_TRL % (fixed_ip, floating_ip, vrf)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'SET_STATIC_SRC_TRL')
def _remove_floating_ip(self, floating_ip, fixed_ip, vrf):
conn = self._get_connection()
confstr = snippets.REMOVE_STATIC_SRC_TRL % (fixed_ip, floating_ip, vrf)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'REMOVE_STATIC_SRC_TRL')
def _get_floating_ip_cfg(self):
ioscfg = self._get_running_config()
parse = ciscoconfparse.CiscoConfParse(ioscfg)
res = parse.find_lines('ip nat inside source static')
return res
def _add_static_route(self, dest, dest_mask, next_hop, vrf):
conn = self._get_connection()
confstr = snippets.SET_IP_ROUTE % (vrf, dest, dest_mask, next_hop)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'SET_IP_ROUTE')
def _remove_static_route(self, dest, dest_mask, next_hop, vrf):
conn = self._get_connection()
confstr = snippets.REMOVE_IP_ROUTE % (vrf, dest, dest_mask, next_hop)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'REMOVE_IP_ROUTE')
def _get_static_route_cfg(self):
ioscfg = self._get_running_config()
parse = ciscoconfparse.CiscoConfParse(ioscfg)
return parse.find_lines('ip route')
def _add_default_static_route(self, gw_ip, vrf):
conn = self._get_connection()
confstr = snippets.DEFAULT_ROUTE_CFG % (vrf, gw_ip)
if not self._cfg_exists(confstr):
confstr = snippets.SET_DEFAULT_ROUTE % (vrf, gw_ip)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'SET_DEFAULT_ROUTE')
def _remove_default_static_route(self, gw_ip, vrf):
conn = self._get_connection()
confstr = snippets.DEFAULT_ROUTE_CFG % (vrf, gw_ip)
if self._cfg_exists(confstr):
confstr = snippets.REMOVE_DEFAULT_ROUTE % (vrf, gw_ip)
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, 'REMOVE_DEFAULT_ROUTE')
def _edit_running_config(self, confstr, snippet):
conn = self._get_connection()
rpc_obj = conn.edit_config(target='running', config=confstr)
self._check_response(rpc_obj, snippet)
@staticmethod
def _check_response(rpc_obj, snippet_name):
"""This function checks the rpc response object for status.
This function takes as input the response rpc_obj and the snippet name
that was executed. It parses it to see, if the last edit operation was
a success or not.
<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply message-id="urn:uuid:81bf8082-....-b69a-000c29e1b85c"
xmlns="urn:ietf:params:netconf:base:1.0">
<ok />
</rpc-reply>
In case of error, CSR1kv sends a response as follows.
We take the error type and tag.
<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply message-id="urn:uuid:81bf8082-....-b69a-000c29e1b85c"
xmlns="urn:ietf:params:netconf:base:1.0">
<rpc-error>
<error-type>protocol</error-type>
<error-tag>operation-failed</error-tag>
<error-severity>error</error-severity>
</rpc-error>
</rpc-reply>
:return: True if the config operation completed successfully
:raises: neutron.plugins.cisco.cfg_agent.cfg_exceptions.
CSR1kvConfigException
"""
LOG.debug("RPCReply for %(snippet_name)s is %(rpc_obj)s",
{'snippet_name': snippet_name, 'rpc_obj': rpc_obj.xml})
xml_str = rpc_obj.xml
if "<ok />" in xml_str:
LOG.debug("RPCReply for %s is OK", snippet_name)
LOG.info(_("%s successfully executed"), snippet_name)
return True
# Not Ok, we throw a ConfigurationException
e_type = rpc_obj._root[0][0].text
e_tag = rpc_obj._root[0][1].text
params = {'snippet': snippet_name, 'type': e_type, 'tag': e_tag}
raise cfg_exc.CSR1kvConfigException(**params)

View File

@ -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

View File

@ -0,0 +1,98 @@
# Copyright 2014 Cisco Systems, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: Hareesh Puthalath, Cisco Systems, Inc.
from neutron.openstack.common import excutils
from neutron.openstack.common import importutils
from neutron.openstack.common import log as logging
from neutron.plugins.cisco.cfg_agent import cfg_exceptions
LOG = logging.getLogger(__name__)
class DeviceDriverManager(object):
"""This class acts as a manager for device drivers.
The device driver manager maintains the relationship between the
different neutron logical resource (eg: routers, firewalls, vpns etc.) and
where they are hosted. For configuring a logical resource (router) in a
hosting device, a corresponding device driver object is used.
Device drivers encapsulate the necessary configuration information to
configure a logical resource (eg: routers, firewalls, vpns etc.) on a
hosting device (eg: CSR1kv).
The device driver class loads one driver object per hosting device.
The loaded drivers are cached in memory, so when a request is made to
get driver object for the same hosting device and resource (like router),
the existing driver object is reused.
This class is used by the service helper classes.
"""
def __init__(self):
self._drivers = {}
self._hosting_device_routing_drivers_binding = {}
def get_driver(self, resource_id):
try:
return self._drivers[resource_id]
except KeyError:
with excutils.save_and_reraise_exception(reraise=False):
raise cfg_exceptions.DriverNotFound(id=resource_id)
def set_driver(self, resource):
"""Set the driver for a neutron resource.
:param resource: Neutron resource in dict format. Expected keys:
{ 'id': <value>
'hosting_device': { 'id': <value>, }
'router_type': {'cfg_agent_driver': <value>, }
}
:return driver : driver object
"""
try:
resource_id = resource['id']
hosting_device = resource['hosting_device']
hd_id = hosting_device['id']
if hd_id in self._hosting_device_routing_drivers_binding:
driver = self._hosting_device_routing_drivers_binding[hd_id]
self._drivers[resource_id] = driver
else:
driver_class = resource['router_type']['cfg_agent_driver']
driver = importutils.import_object(driver_class,
**hosting_device)
self._hosting_device_routing_drivers_binding[hd_id] = driver
self._drivers[resource_id] = driver
return driver
except ImportError:
LOG.exception(_("Error loading cfg agent driver %(driver)s for "
"hosting device template %(t_name)s(%(t_id)s)"),
{'driver': driver_class, 't_id': hd_id,
't_name': hosting_device['name']})
with excutils.save_and_reraise_exception(reraise=False):
raise cfg_exceptions.DriverNotExist(driver=driver_class)
except KeyError as e:
with excutils.save_and_reraise_exception(reraise=False):
raise cfg_exceptions.DriverNotSetForMissingParameter(e)
def remove_driver(self, resource_id):
"""Remove driver associated to a particular resource."""
if resource_id in self._drivers:
del self._drivers[resource_id]
def remove_driver_for_hosting_device(self, hd_id):
"""Remove driver associated to a particular hosting device."""
if hd_id in self._hosting_device_routing_drivers_binding:
del self._hosting_device_routing_drivers_binding[hd_id]

View File

@ -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.")

View File

@ -0,0 +1,174 @@
# Copyright 2014 Cisco Systems, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: Hareesh Puthalath, Cisco Systems, Inc.
import datetime
from oslo.config import cfg
from neutron.agent.linux import utils as linux_utils
from neutron.openstack.common import log as logging
from neutron.openstack.common import timeutils
LOG = logging.getLogger(__name__)
STATUS_OPTS = [
cfg.IntOpt('device_connection_timeout', default=30,
help=_("Time in seconds for connecting to a hosting device")),
cfg.IntOpt('hosting_device_dead_timeout', default=300,
help=_("The time in seconds until a backlogged hosting device "
"is presumed dead. This value should be set up high "
"enough to recover from a period of connectivity loss "
"or high load when the device may not be responding.")),
]
cfg.CONF.register_opts(STATUS_OPTS)
def _is_pingable(ip):
"""Checks whether an IP address is reachable by pinging.
Use linux utils to execute the ping (ICMP ECHO) command.
Sends 5 packets with an interval of 0.2 seconds and timeout of 1
seconds. Runtime error implies unreachability else IP is pingable.
:param ip: IP to check
:return: bool - True or False depending on pingability.
"""
ping_cmd = ['ping',
'-c', '5',
'-W', '1',
'-i', '0.2',
ip]
try:
linux_utils.execute(ping_cmd, check_exit_code=True)
return True
except RuntimeError:
LOG.warn(_("Cannot ping ip address: %s"), ip)
return False
class DeviceStatus(object):
"""Device status and backlog processing."""
_instance = None
def __new__(cls):
if not cls._instance:
cls._instance = super(DeviceStatus, cls).__new__(cls)
return cls._instance
def __init__(self):
self.backlog_hosting_devices = {}
def get_backlogged_hosting_devices(self):
return self.backlog_hosting_devices.keys()
def get_backlogged_hosting_devices_info(self):
wait_time = datetime.timedelta(
seconds=cfg.CONF.hosting_device_dead_timeout)
resp = []
for hd_id in self.backlog_hosting_devices:
hd = self.backlog_hosting_devices[hd_id]['hd']
created_time = hd['created_at']
boottime = datetime.timedelta(seconds=hd['booting_time'])
backlogged_at = hd['backlog_insertion_ts']
booted_at = created_time + boottime
dead_at = backlogged_at + wait_time
resp.append({'host id': hd['id'],
'created at': str(created_time),
'backlogged at': str(backlogged_at),
'estimate booted at': str(booted_at),
'considered dead at': str(dead_at)})
return resp
def is_hosting_device_reachable(self, hosting_device):
"""Check the hosting device which hosts this resource is reachable.
If the resource is not reachable, it is added to the backlog.
:param hosting_device : dict of the hosting device
:return True if device is reachable, else None
"""
hd = hosting_device
hd_id = hosting_device['id']
hd_mgmt_ip = hosting_device['management_ip_address']
# Modifying the 'created_at' to a date time object
hosting_device['created_at'] = datetime.datetime.strptime(
hosting_device['created_at'], '%Y-%m-%d %H:%M:%S')
if hd_id not in self.backlog_hosting_devices:
if _is_pingable(hd_mgmt_ip):
LOG.debug("Hosting device: %(hd_id)s@%(ip)s is reachable.",
{'hd_id': hd_id, 'ip': hd_mgmt_ip})
return True
LOG.debug("Hosting device: %(hd_id)s@%(ip)s is NOT reachable.",
{'hd_id': hd_id, 'ip': hd_mgmt_ip})
hd['backlog_insertion_ts'] = max(
timeutils.utcnow(),
hd['created_at'] +
datetime.timedelta(seconds=hd['booting_time']))
self.backlog_hosting_devices[hd_id] = {'hd': hd}
LOG.debug("Hosting device: %(hd_id)s @ %(ip)s is now added "
"to backlog", {'hd_id': hd_id, 'ip': hd_mgmt_ip})
def check_backlogged_hosting_devices(self):
""""Checks the status of backlogged hosting devices.
Skips newly spun up instances during their booting time as specified
in the boot time parameter.
:return A dict of the format:
{'reachable': [<hd_id>,..], 'dead': [<hd_id>,..]}
"""
response_dict = {'reachable': [], 'dead': []}
LOG.debug("Current Backlogged hosting devices: %s",
self.backlog_hosting_devices.keys())
for hd_id in self.backlog_hosting_devices.keys():
hd = self.backlog_hosting_devices[hd_id]['hd']
if not timeutils.is_older_than(hd['created_at'],
hd['booting_time']):
LOG.info(_("Hosting device: %(hd_id)s @ %(ip)s hasn't passed "
"minimum boot time. Skipping it. "),
{'hd_id': hd_id, 'ip': hd['management_ip_address']})
continue
LOG.info(_("Checking hosting device: %(hd_id)s @ %(ip)s for "
"reachability."), {'hd_id': hd_id,
'ip': hd['management_ip_address']})
if _is_pingable(hd['management_ip_address']):
hd.pop('backlog_insertion_ts', None)
del self.backlog_hosting_devices[hd_id]
response_dict['reachable'].append(hd_id)
LOG.info(_("Hosting device: %(hd_id)s @ %(ip)s is now "
"reachable. Adding it to response"),
{'hd_id': hd_id, 'ip': hd['management_ip_address']})
else:
LOG.info(_("Hosting device: %(hd_id)s @ %(ip)s still not "
"reachable "), {'hd_id': hd_id,
'ip': hd['management_ip_address']})
if timeutils.is_older_than(
hd['backlog_insertion_ts'],
cfg.CONF.hosting_device_dead_timeout):
LOG.debug("Hosting device: %(hd_id)s @ %(ip)s hasn't "
"been reachable for the last %(time)d seconds. "
"Marking it dead.",
{'hd_id': hd_id,
'ip': hd['management_ip_address'],
'time': cfg.CONF.hosting_device_dead_timeout})
response_dict['dead'].append(hd_id)
hd.pop('backlog_insertion_ts', None)
del self.backlog_hosting_devices[hd_id]
LOG.debug("Response: %s", response_dict)
return response_dict

View File

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

View File

@ -106,3 +106,12 @@ NEXUS_VLAN_RESERVED_MIN = 3968
NEXUS_VLAN_RESERVED_MAX = 4047
NEXUS_VXLAN_MIN = 4096
NEXUS_VXLAN_MAX = 16000000
# Type and topic for Cisco cfg agent
# ==================================
AGENT_TYPE_CFG = 'Cisco cfg agent'
# Topic for Cisco configuration agent
CFG_AGENT = 'cisco_cfg_agent'
# Topic for routing service helper in Cisco configuration agent
CFG_AGENT_L3_ROUTING = 'cisco_cfg_agent_l3_routing'

View File

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

View File

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

View File

@ -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())

View File

@ -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")

View File

@ -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