Skip to content

Commit

Permalink
Support unicast RPF mode for the cisco adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
Martin Roy authored and lindycoder committed Mar 20, 2017
1 parent c498ef3 commit 9ff4f0f
Show file tree
Hide file tree
Showing 21 changed files with 599 additions and 19 deletions.
8 changes: 8 additions & 0 deletions netman/adapters/switches/cached.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,14 @@ def set_vlan_icmp_redirects_state(self, vlan_number, state):
self.real_switch.set_vlan_icmp_redirects_state(vlan_number, state)
self.vlans_cache[vlan_number].icmp_redirects = state

def set_vlan_unicast_rpf_mode(self, vlan_number, mode):
self.real_switch.set_vlan_unicast_rpf_mode(vlan_number, mode)
self.vlans_cache[vlan_number].unicast_rpf_mode = mode

def unset_vlan_unicast_rpf_mode(self, vlan_number):
self.real_switch.unset_vlan_unicast_rpf_mode(vlan_number)
self.vlans_cache[vlan_number].unicast_rpf_mode = None

def get_versions(self):
if self.versions_cache.refresh_items:
self.versions_cache = Cache([(0, self.real_switch.get_versions())])
Expand Down
28 changes: 27 additions & 1 deletion netman/adapters/switches/cisco.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@
from netman.core.objects.exceptions import IPNotAvailable, UnknownVlan, UnknownIP, UnknownAccessGroup, BadVlanNumber, \
BadVlanName, UnknownInterface, UnknownVrf, VlanVrfNotSet, IPAlreadySet, VrrpAlreadyExistsForVlan, BadVrrpGroupNumber, \
BadVrrpPriorityNumber, VrrpDoesNotExistForVlan, BadVrrpTimers, BadVrrpTracking, UnknownDhcpRelayServer, DhcpRelayServerAlreadyExists, \
VlanAlreadyExist, UnknownBond, InvalidAccessGroupName
VlanAlreadyExist, UnknownBond, InvalidAccessGroupName, InvalidUnicastRPFMode, UnsupportedOperation
from netman.core.objects.interface import Interface
from netman.core.objects.interface_states import OFF, ON
from netman.core.objects.port_modes import DYNAMIC, ACCESS, TRUNK
from netman.core.objects.switch_base import SwitchBase
from netman.core.objects.switch_transactional import FlowControlSwitch
from netman.core.objects.unicast_rpf_modes import STRICT
from netman.core.objects.vlan import Vlan
from netman.core.objects.vrrp_group import VrrpGroup

Expand Down Expand Up @@ -455,6 +456,28 @@ def get_versions(self):

return versions

def set_vlan_unicast_rpf_mode(self, vlan_number, mode):
operations = {STRICT: self._set_unicast_rpf_strict}

if mode not in operations:
raise InvalidUnicastRPFMode(mode)

self.get_vlan_interface_data(vlan_number)

with self.config(), self.interface_vlan(vlan_number):
operations[mode]()

def unset_vlan_unicast_rpf_mode(self, vlan_number):
self.get_vlan_interface_data(vlan_number)

with self.config(), self.interface_vlan(vlan_number):
self.ssh.do('no ip verify unicast')

def _set_unicast_rpf_strict(self):
result = self.ssh.do('ip verify unicast source reachable-via rx')
if len(result) > 0:
raise UnsupportedOperation("Unicast RPF Mode Strict", "\n".join(result))


def parse_interface(data):
if data and (regex.match("interface (\w*Ethernet[^\s]*)", data[0])
Expand Down Expand Up @@ -531,6 +554,9 @@ def apply_interface_running_config_data(vlan, data):
elif regex.match("^ no ip redirects", line):
vlan.icmp_redirects = False

elif regex.match("^ ip verify unicast source reachable-via rx", line):
vlan.unicast_rpf_mode = STRICT


def apply_vlan_running_config_data(vlan, data):
for line in data:
Expand Down
7 changes: 7 additions & 0 deletions netman/adapters/switches/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,13 @@ def set_vlan_icmp_redirects_state(self, vlan_number, state):
self.put('/vlans/{}/icmp-redirects'.format(vlan_number),
raw_data=_get_json_boolean(state))

def set_vlan_unicast_rpf_mode(self, vlan_number, mode):
self.put('/vlans/{}/unicast-rpf-mode'.format(vlan_number),
raw_data=str(mode))

def unset_vlan_unicast_rpf_mode(self, vlan_number):
self.delete('/vlans/{}/unicast-rpf-mode'.format(vlan_number))

def get_versions(self):
return self.get("/versions").json()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
],
"dhcp_relay_servers": [],
"arp_routing": null,
"icmp_redirects": false
"icmp_redirects": false,
"unicast_rpf_mode": null
},
{
"number": 2,
Expand Down Expand Up @@ -59,6 +60,7 @@
],
"dhcp_relay_servers": ["10.10.10.1"],
"arp_routing": true,
"icmp_redirects": true
"icmp_redirects": true,
"unicast_rpf_mode": "STRICT"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
],
"dhcp_relay_servers": [],
"arp_routing": true,
"icmp_redirects": false
"icmp_redirects": false,
"unicast_rpf_mode": null
}
1 change: 1 addition & 0 deletions netman/api/objects/vlan.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def to_api(vlan):
dhcp_relay_servers=[str(server) for server in vlan.dhcp_relay_servers],
arp_routing=vlan.arp_routing,
icmp_redirects=vlan.icmp_redirects,
unicast_rpf_mode=vlan.unicast_rpf_mode
)


Expand Down
34 changes: 33 additions & 1 deletion netman/api/switch_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from netman.api.validators import Switch, is_boolean, is_vlan_number, Interface, Vlan, resource, content, is_ip_network, \
IPNetworkResource, is_access_group_name, Direction, is_vlan, is_bond, Bond, \
is_bond_link_speed, is_bond_number, is_description, is_vrf_name, \
is_vrrp_group, VrrpGroup, is_dict_with, optional, is_type, is_int
is_vrrp_group, VrrpGroup, is_dict_with, optional, is_type, is_int, is_unincast_rpf_mode
from netman.core.objects.interface_states import OFF, ON


Expand All @@ -45,6 +45,8 @@ def hook_to(self, server):
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/dhcp-relay-server/<ip_network>', view_func=self.remove_dhcp_relay_server, methods=['DELETE'])
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/arp-routing', view_func=self.set_vlan_arp_routing_state, methods=['PUT'])
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/icmp-redirects', view_func=self.set_vlan_icmp_redirects_state, methods=['PUT'])
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/unicast-rpf-mode', view_func=self.set_vlan_unicast_rpf_mode, methods=['PUT'])
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/unicast-rpf-mode', view_func=self.unset_vlan_unicast_rpf_mode, methods=['DELETE'])
server.add_url_rule('/switches/<hostname>/interfaces', view_func=self.get_interfaces, methods=['GET'])
server.add_url_rule('/switches/<hostname>/interfaces/<path:interface_id>', view_func=self.reset_interface, methods=['PUT'])
server.add_url_rule('/switches/<hostname>/interfaces/<path:interface_id>', view_func=self.get_interface, methods=['GET'])
Expand Down Expand Up @@ -1031,3 +1033,33 @@ def set_vlan_icmp_redirects_state(self, switch, vlan_number, state):
switch.set_vlan_icmp_redirects_state(vlan_number, state)

return 204, None

@to_response
@content(is_unincast_rpf_mode)
@resource(Switch, Vlan)
def set_vlan_unicast_rpf_mode(self, switch, vlan_number, mode):
"""
Sets the Unicast RPF state of an interface *only strict is supported*
:arg str hostname: Hostname or IP of the switch
:arg int vlan_number: Vlan number, between 1 and 4096
:body:
``STRICT``
"""

switch.set_vlan_unicast_rpf_mode(vlan_number, mode)

return 204, None

@to_response
@resource(Switch, Vlan)
def unset_vlan_unicast_rpf_mode(self, switch, vlan_number):
"""
Remove Unicast RPF configuration of an interface
:arg str hostname: Hostname or IP of the switch
:arg int vlan_number: Vlan number, between 1 and 4096
"""

switch.unset_vlan_unicast_rpf_mode(vlan_number)
return 204, None
9 changes: 8 additions & 1 deletion netman/api/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from netman.core.objects.exceptions import UnknownResource, BadVlanNumber,\
BadVlanName, BadBondNumber, BadBondLinkSpeed, MalformedSwitchSessionRequest, \
BadVrrpGroupNumber
from netman.core.objects.unicast_rpf_modes import STRICT


def resource(*validators):
Expand Down Expand Up @@ -310,7 +311,6 @@ def is_access_group_name(data, **_):

return {'access_group_name': data}


def is_vrf_name(data, **_):
if data == "" or " " in data:
raise BadRequest('Malformed VRF name')
Expand Down Expand Up @@ -342,6 +342,13 @@ def is_bond(data, **_):
}


def is_unincast_rpf_mode(data, **_):
if data not in [STRICT]:
raise BadRequest('Invalid unicast rpf mode')

return {'mode': data}


def is_bond_link_speed(data, **_):
if re.match(r'^\d+[mg]$', data):
return {'bond_link_speed': data}
Expand Down
10 changes: 10 additions & 0 deletions netman/core/objects/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,13 @@ def __init__(self, name=None):
class InvalidMtuSize(InvalidValue):
def __init__(self, err_msg=None):
super(InvalidMtuSize, self).__init__("MTU value is invalid : {}".format(err_msg))


class InvalidUnicastRPFMode(InvalidValue):
def __init__(self, mode=None):
super(InvalidUnicastRPFMode, self).__init__("Unknown Unicast RPF mode: \"{}\"".format(mode))


class UnsupportedOperation(NotImplementedError):
def __init__(self, operation=None, message=None):
super(UnsupportedOperation, self).__init__("Operation \"{}\" is not supported on this equipment: {}".format(operation, message))
6 changes: 6 additions & 0 deletions netman/core/objects/switch_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ def set_vlan_arp_routing_state(self, vlan_number, state):
def set_vlan_icmp_redirects_state(self, vlan_number, state):
raise NotImplementedError()

def set_vlan_unicast_rpf_mode(self, vlan_number, mode):
raise NotImplementedError()

def unset_vlan_unicast_rpf_mode(self, vlan_number):
raise NotImplementedError()

def get_versions(self):
raise NotImplementedError()

Expand Down
16 changes: 16 additions & 0 deletions netman/core/objects/unicast_rpf_modes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2015 Internap.
#
# 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.


STRICT = "STRICT"
4 changes: 3 additions & 1 deletion netman/core/objects/vlan.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@

class Vlan(Model):
def __init__(self, number=None, name=None, ips=None, vrrp_groups=None, vrf_forwarding=None, access_group_in=None,
access_group_out=None, dhcp_relay_servers=None, arp_routing=None, icmp_redirects=None):
access_group_out=None, dhcp_relay_servers=None, arp_routing=None, icmp_redirects=None,
unicast_rpf_mode=None):
self.number = number
self.name = name
self.access_groups = {IN: access_group_in, OUT: access_group_out}
Expand All @@ -28,3 +29,4 @@ def __init__(self, number=None, name=None, ips=None, vrrp_groups=None, vrf_forwa
self.dhcp_relay_servers = dhcp_relay_servers or []
self.arp_routing = arp_routing
self.icmp_redirects = icmp_redirects
self.unicast_rpf_mode = unicast_rpf_mode
46 changes: 46 additions & 0 deletions tests/adapters/compliance_tests/set_vlan_unicast_rpf_mode_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2016 Internap.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from hamcrest import assert_that, is_

from netman.core.objects.exceptions import UnknownVlan
from netman.core.objects.unicast_rpf_modes import STRICT
from tests import has_message
from tests.adapters.compliance_test_case import ComplianceTestCase


class SetVlanUnicastRPFModeTest(ComplianceTestCase):
_dev_sample = "cisco"

def setUp(self):
super(SetVlanUnicastRPFModeTest, self).setUp()
self.client.add_vlan(1000)

def test_can_activate_the_strict_unicast_anti_spoofing(self):
self.client.set_vlan_unicast_rpf_mode(1000, STRICT)

vlan = self.get_vlan_from_list(1000)

assert_that(vlan.unicast_rpf_mode, is_(STRICT))

def test_raises_UnknownVlan_when_operating_on_a_vlan_that_does_not_exist(self):
with self.assertRaises(UnknownVlan) as expect:
self.client.set_vlan_unicast_rpf_mode(2000, STRICT)

assert_that(expect.exception, has_message("Vlan 2000 not found"))

def tearDown(self):
self.janitor.remove_vlan(1000)
super(SetVlanUnicastRPFModeTest, self).tearDown()
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright 2016 Internap.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from hamcrest import assert_that, is_

from netman.core.objects.exceptions import UnknownVlan
from netman.core.objects.unicast_rpf_modes import STRICT
from tests import has_message
from tests.adapters.compliance_test_case import ComplianceTestCase


class UnsetVlanUnicastRPFModeTest(ComplianceTestCase):
_dev_sample = "cisco"

def setUp(self):
super(UnsetVlanUnicastRPFModeTest, self).setUp()
self.client.add_vlan(1000)
self.try_to.set_vlan_unicast_rpf_mode(1000, STRICT)

def test_can_reset_the_unicast_anti_spoofing_mode_to_default(self):
self.client.unset_vlan_unicast_rpf_mode(1000)

vlan = self.get_vlan_from_list(1000)

assert_that(vlan.unicast_rpf_mode, is_(None))

def test_raises_UnknownVlan_when_operating_on_a_vlan_that_does_not_exist(self):
with self.assertRaises(UnknownVlan) as expect:
self.client.unset_vlan_unicast_rpf_mode(2000)

assert_that(expect.exception, has_message("Vlan 2000 not found"))

def tearDown(self):
self.janitor.remove_vlan(1000)
super(UnsetVlanUnicastRPFModeTest, self).tearDown()
20 changes: 20 additions & 0 deletions tests/adapters/model_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from fake_switches.brocade.brocade_core import BrocadeSwitchCore
from fake_switches.cisco.cisco_core import CiscoSwitchCore
from fake_switches.cisco6500.cisco_core import Cisco6500SwitchCore
from fake_switches.dell.dell_core import DellSwitchCore
from fake_switches.dell10g.dell_core import Dell10GSwitchCore
from fake_switches.juniper.juniper_core import JuniperSwitchCore
Expand Down Expand Up @@ -43,6 +44,25 @@
Port("FastEthernet0/4"),
]
},
{
"switch_descriptor": SwitchDescriptor(
model="cisco",
hostname="127.0.0.1",
port=11014,
username="root",
password="root",
),
"test_port_name": "FastEthernet0/4",
"test_vrrp_track_id": "102",
"core_class": Cisco6500SwitchCore,
"service_class": SwitchSshService,
"ports": [
Port("FastEthernet0/1"),
Port("FastEthernet0/2"),
Port("FastEthernet0/3"),
Port("FastEthernet0/4"),
]
},
{
"switch_descriptor": SwitchDescriptor(
model="brocade",
Expand Down
Loading

0 comments on commit 9ff4f0f

Please sign in to comment.