Skip to content

Commit

Permalink
Introducing API and compliance tests for VARP (#234)
Browse files Browse the repository at this point in the history
Also including all needed classes to build and run the compliance test
  • Loading branch information
stephanerobert authored and joseph2rs committed Nov 8, 2018
1 parent 3db61b9 commit 247fc0e
Show file tree
Hide file tree
Showing 15 changed files with 264 additions and 21 deletions.
4 changes: 2 additions & 2 deletions constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ idna==2.7 # via requests
itsdangerous==1.1.0 # via flask
jinja2==2.10 # via flask
lxml==4.2.5 # via ncclient
markupsafe==1.0 # via jinja2
markupsafe==1.1.0 # via jinja2
ncclient==0.6.3
netaddr==0.7.19
paramiko==1.17.6
pbr==5.1.0
pbr==5.1.1
pycrypto==2.6.1 # via paramiko
pyeapi==0.8.2
requests==2.20.0
Expand Down
11 changes: 11 additions & 0 deletions netman/adapters/switches/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,17 @@ def add_vrrp_group(self, vlan_number, group_id, ips=None, priority=None, hello_i
def remove_vrrp_group(self, vlan_number, group_id):
self.delete("/vlans/{}/vrrp-groups/{}".format(vlan_number, group_id))

def add_vlan_varp_ip(self, vlan_number, ip_network):
self.post('/vlans/{vlan_number}/varp-ips'.format(
vlan_number=vlan_number,
), raw_data=str(ip_network))

def remove_vlan_varp_ip(self, vlan_number, ip_network):
self.delete('/vlans/{vlan_number}/varp-ips/{ip_network}'.format(
vlan_number=vlan_number,
ip_network=ip_network
))

def add_dhcp_relay_server(self, vlan_number, ip_address):
self.post("/vlans/{}/dhcp-relay-server".format(
vlan_number), raw_data=str(ip_address))
Expand Down
11 changes: 9 additions & 2 deletions netman/api/doc_config/api_samples/get_switch_hostname_vlans.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
"arp_routing": null,
"icmp_redirects": false,
"unicast_rpf_mode": null,
"ntp": false
"ntp": false,
"varp_ips": [
{"address": "3.3.3.3", "mask": 24}
]
},
{
"number": 2,
Expand Down Expand Up @@ -63,6 +66,10 @@
"arp_routing": true,
"icmp_redirects": true,
"unicast_rpf_mode": "STRICT",
"ntp": null
"ntp": null,
"varp_ips": [
{"address": "4.4.4.4", "mask": 24},
{"address": "5.5.5.5", "mask": 24}
]
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@
"arp_routing": true,
"icmp_redirects": false,
"unicast_rpf_mode": null,
"ntp": null
"ntp": null,
"varp_ips": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"address": "1.2.3.4",
"mask": 25
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.2.3.4/25
17 changes: 14 additions & 3 deletions netman/api/objects/vlan.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def to_api(vlan):
return dict(
number=vlan.number,
name=vlan.name,
ips=sorted([{'address': ipn.ip.format(), 'mask': ipn.prefixlen} for ipn in vlan.ips], key=lambda i: i['address']),
ips=serialize_ip_network(vlan.ips),
vrrp_groups=sorted([vrrp_group.to_api(group) for group in vlan.vrrp_groups], key=lambda i: i['id']),
vrf_forwarding=vlan.vrf_forwarding,
access_groups={
Expand All @@ -34,7 +34,8 @@ def to_api(vlan):
arp_routing=vlan.arp_routing,
icmp_redirects=vlan.icmp_redirects,
unicast_rpf_mode=vlan.unicast_rpf_mode,
ntp=vlan.ntp
ntp=vlan.ntp,
varp_ips=serialize_ip_network(vlan.varp_ips)
)


Expand All @@ -43,11 +44,21 @@ def to_core(serialized):
ips = serialized.pop('ips')
vrrp_groups = serialized.pop('vrrp_groups')
dhcp_relay_servers = serialized.pop('dhcp_relay_servers')
varp_ips = serialized.pop('varp_ips')
return Vlan(
access_group_in=access_groups['in'],
access_group_out=access_groups['out'],
ips=[IPNetwork('{address}/{mask}'.format(**ip)) for ip in ips],
ips=deserialize_ip_network(ips),
vrrp_groups=[vrrp_group.to_core(group) for group in vrrp_groups],
dhcp_relay_servers=[IPAddress(i) for i in dhcp_relay_servers],
varp_ips=deserialize_ip_network(varp_ips),
**serialized
)


def deserialize_ip_network(ips):
return [IPNetwork('{address}/{mask}'.format(**ip)) for ip in ips]


def serialize_ip_network(ips):
return sorted([{'address': ipn.ip.format(), 'mask': ipn.prefixlen} for ipn in ips], key=lambda i: i['address'])
40 changes: 40 additions & 0 deletions netman/api/switch_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def hook_to(self, server):
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/ips/<path:ip_network>', view_func=self.remove_ip, methods=['DELETE'])
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/vrrp-groups', view_func=self.add_vrrp_group, methods=['POST'])
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/vrrp-groups/<vrrp_group_id>', view_func=self.remove_vrrp_group, methods=['DELETE'])
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/varp-ips', view_func=self.add_varp_ip, methods=['POST'])
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/varp-ips/<path:ip_network>', view_func=self.remove_varp_ip, methods=['DELETE'])
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/access-groups/<direction>', view_func=self.set_vlan_access_group, methods=['PUT'])
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/access-groups/<direction>', view_func=self.unset_vlan_access_group, methods=['DELETE'])
server.add_url_rule('/switches/<hostname>/vlans/<vlan_number>/vrf-forwarding', view_func=self.set_vlan_vrf, methods=['PUT'])
Expand Down Expand Up @@ -279,6 +281,44 @@ def remove_vrrp_group(self, switch, vlan_number, vrrp_group_id):
switch.remove_vrrp_group(vlan_number, vrrp_group_id)
return 204, None

@to_response
@content(is_ip_network)
@resource(Switch, Vlan)
def add_varp_ip(self, switch, vlan_number, validated_ip_network):
"""
Adds a VARP IP Network to a VLAN
:arg str hostname: Hostname or IP of the switch
:arg int vlan_number: VLAN number, between 1 and 4096
:body:
Highlighted fields are mandatory
.. literalinclude:: ../doc_config/api_samples/post_switch_hostname_vlans_vlanid_varp_ips.json
:language: json
:emphasize-lines: 2-3
or
.. literalinclude:: ../doc_config/api_samples/post_switch_hostname_vlans_vlanid_varp_ips.txt
"""

switch.add_vlan_varp_ip(vlan_number=vlan_number, ip_network=validated_ip_network)
return 201, None

@to_response
@resource(Switch, Vlan, IPNetworkResource)
def remove_varp_ip(self, switch, vlan_number, ip_network):
"""
Removes a VARP ip from a VLAN
:arg str hostname: Hostname or IP of the switch
:arg int vlan_number: VLAN number, between 1 and 4096
:arg str ip_network: IP/Subnet in the "x.x.x.x/xx" format
"""

switch.remove_vlan_varp_ip(vlan_number=vlan_number, ip_network=ip_network)
return 204, None

@to_response
@content(is_access_group_name)
@resource(Switch, Vlan, Direction)
Expand Down
6 changes: 3 additions & 3 deletions test-constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ cryptography==2.3.1 # via twisted
docutils==0.14 # via sphinx
ecdsa==0.13
enum34==1.1.6 # via cryptography, flake8
fake-switches==1.3.5
fake-switches==1.3.6
flake8==3.4.1
flask==1.0.2
flexmock==0.10.2
Expand All @@ -38,15 +38,15 @@ itsdangerous==1.1.0
jabstract==0.1.2
jinja2==2.10
lxml==4.2.5
markupsafe==1.0
markupsafe==1.1.0
mccabe==0.6.1 # via flake8
mock==2.0.0
mockssh==1.4.3
ncclient==0.6.3
netaddr==0.7.19
nose==1.3.7
paramiko==1.17.6
pbr==5.1.0
pbr==5.1.1
pyasn1==0.4.4 # via mockssh, twisted
pycodestyle==2.3.1 # via flake8
pycparser==2.19 # via cffi
Expand Down
2 changes: 1 addition & 1 deletion test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ nose>=1.2.1
mock>=1.0.1
pyhamcrest>=1.6
flexmock>=0.10.2
fake-switches>=1.3.5
fake-switches>=1.3.6
MockSSH>=1.4.2,!=1.4.5 # 1.4.5 has fixed dependency and freezes paramiko
gunicorn>=19.4.5
flake8==3.4.1
Expand Down
41 changes: 41 additions & 0 deletions tests/adapters/compliance_tests/add_varp_ip_to_vlan_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2019 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 netaddr import IPNetwork

from netman.core.objects.exceptions import VarpAlreadyExistsForVlan
from tests.adapters.compliance_test_case import ComplianceTestCase


class AddVarpIpToVlanTest(ComplianceTestCase):
_dev_sample = "arista_http"

def setUp(self):
super(AddVarpIpToVlanTest, self).setUp()
self.client.add_vlan(1000, name="vlan_name")

def tearDown(self):
self.janitor.remove_vlan(1000)
super(AddVarpIpToVlanTest, self).tearDown()

def test_assigns_the_varp_ip(self):
self.client.add_vlan_varp_ip(1000, IPNetwork("10.10.40.2/29"))
assert_that(self.client.get_vlan(1000).varp_ips, is_([IPNetwork("10.10.40.2/29")]))

def test_raises_if_ip_is_already_assigned_to_current_vlan(self):
self.client.add_vlan_varp_ip(1000, IPNetwork("10.10.10.222/29"))

with self.assertRaises(VarpAlreadyExistsForVlan):
self.client.add_vlan_varp_ip(1000, IPNetwork("10.10.10.222/29"))
43 changes: 43 additions & 0 deletions tests/adapters/compliance_tests/remove_varp_ip_from_vlan_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2019 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 netaddr import IPNetwork

from netman.core.objects.exceptions import VarpDoesNotExistForVlan
from tests.adapters.compliance_test_case import ComplianceTestCase


class RemoveVarpIpFromVlanTest(ComplianceTestCase):
_dev_sample = "arista_http"

def setUp(self):
super(RemoveVarpIpFromVlanTest, self).setUp()
self.client.add_vlan(1000, name="vlan_name")
self.client.add_vlan_varp_ip(1000, IPNetwork("10.10.40.2/29"))
self.client.add_vlan_varp_ip(1000, IPNetwork("10.10.30.2/29"))
self.client.get_vlans()

def tearDown(self):
self.janitor.remove_vlan(1000)
super(RemoveVarpIpFromVlanTest, self).tearDown()

def test_removes_a_varp_ip(self):
self.client.remove_vlan_varp_ip(1000, IPNetwork("10.10.40.2/29"))

assert_that(self.client.get_vlan(1000).varp_ips, is_([IPNetwork("10.10.30.2/29")]))

def test_raises_if_ip_is_not_on_current_vlan(self):
with self.assertRaises(VarpDoesNotExistForVlan):
self.client.remove_vlan_varp_ip(1000, IPNetwork("1.2.3.4/29"))
12 changes: 6 additions & 6 deletions tests/adapters/switches/cached_switch_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,23 +825,23 @@ def test_add_vlan_varp_ip(self):
self.switch.get_vlans()

self.real_switch_mock.should_receive("add_vlan_varp_ip").once() \
.with_args(1, IPAddress("1.1.1.1"))
.with_args(1, IPNetwork("1.1.1.1/29"))

self.switch.add_vlan_varp_ip(1, IPAddress("1.1.1.1"))
self.switch.add_vlan_varp_ip(1, IPNetwork("1.1.1.1/29"))

assert_that(
self.switch.get_vlans(),
is_([Vlan(1, varp_ips=[IPAddress("1.1.1.1")])]))
is_([Vlan(1, varp_ips=[IPNetwork("1.1.1.1/29")])]))

def test_remove_vlan_varp_ip(self):
self.real_switch_mock.should_receive("get_vlans").once() \
.and_return([Vlan(1, varp_ips=[IPAddress("1.1.1.1")])])
.and_return([Vlan(1, varp_ips=[IPNetwork("1.1.1.1/29")])])
self.switch.get_vlans()

self.real_switch_mock.should_receive("remove_vlan_varp_ip").once() \
.with_args(1, IPAddress("1.1.1.1"))
.with_args(1, IPNetwork("1.1.1.1/29"))

self.switch.remove_vlan_varp_ip(1, IPAddress("1.1.1.1"))
self.switch.remove_vlan_varp_ip(1, IPNetwork("1.1.1.1/29"))

assert_that(
self.switch.get_vlans(),
Expand Down
28 changes: 27 additions & 1 deletion tests/adapters/switches/remote_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from hamcrest import assert_that, equal_to, is_, instance_of
import mock
from ncclient.operations import RPCError
from netaddr import IPAddress
from netaddr import IPAddress, IPNetwork
from flexmock import flexmock, flexmock_teardown

from netman.core.objects.interface_states import OFF, ON
Expand Down Expand Up @@ -501,6 +501,7 @@ def test_get_vlan(self):
assert_that(vrrp_group.dead_interval, is_(15))
assert_that(vrrp_group.track_id, is_("101"))
assert_that(vrrp_group.track_decrement, is_(50))
assert_that(vlan1.varp_ips, is_([]))

def test_get_vlans(self):
self.requests_mock.should_receive("get").once().with_args(
Expand Down Expand Up @@ -528,6 +529,7 @@ def test_get_vlans(self):
assert_that(vrrp_group.dead_interval, is_(15))
assert_that(vrrp_group.track_id, is_("101"))
assert_that(vrrp_group.track_decrement, is_(50))
assert_that(vlan1.varp_ips, is_([IPNetwork('3.3.3.3/24')]))

assert_that(vlan2.number, is_(2))
assert_that(vlan2.name, is_(''))
Expand All @@ -547,6 +549,7 @@ def test_get_vlans(self):
assert_that(vrrp_group2.id, is_(2))
assert_that(vrrp_group2.ips, is_([IPAddress("3.3.3.1")]))
assert_that(vrrp_group2.priority, is_(100))
assert_that(vlan2.varp_ips, is_([IPNetwork('4.4.4.4/24'), IPNetwork('5.5.5.5/24')]))

def test_get_vlan_interfaces(self):
self.requests_mock.should_receive("get").once().with_args(
Expand Down Expand Up @@ -1489,6 +1492,29 @@ def test_remove_dhcp_relay_server(self):

self.switch.remove_dhcp_relay_server(2000, '1.2.3.4')

def test_add_varp_ip(self):
self.requests_mock.should_receive("post").once().with_args(
url=self.netman_url+'/switches/toto/vlans/2000/varp-ips',
headers=self.headers,
data='1.2.3.4/29'
).and_return(
Reply(
content='',
status_code=201))

self.switch.add_vlan_varp_ip(2000, IPNetwork('1.2.3.4/29'))

def test_remove_varp_ip(self):
self.requests_mock.should_receive("delete").once().with_args(
url=self.netman_url+'/switches/toto/vlans/2000/varp-ips/1.2.3.4/29',
headers=self.headers
).and_return(
Reply(
content='',
status_code=204))

self.switch.remove_vlan_varp_ip(2000, IPNetwork('1.2.3.4/29'))

def test_set_interface_lldp_state(self):
self.requests_mock.should_receive("put").once().with_args(
url=self.netman_url+'/switches/toto/interfaces/ge-0/0/6/lldp',
Expand Down

0 comments on commit 247fc0e

Please sign in to comment.