Skip to content

Commit d30be21

Browse files
Stephane Robertlindycoder
authored andcommitted
Introducing arista Vlan management
In addition, netman's communications to Arista switches uses the pyeapi tool
1 parent 03e6115 commit d30be21

File tree

6 files changed

+222
-1
lines changed

6 files changed

+222
-1
lines changed

netman/adapters/switches/arista.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import pyeapi
2+
from pyeapi.api.vlans import Vlans, isvlan
3+
from pyeapi.client import Node
4+
from pyeapi.eapilib import CommandError
5+
6+
from netman.core.objects.exceptions import VlanAlreadyExist, UnknownVlan, BadVlanNumber, BadVlanName, \
7+
OperationNotCompleted
8+
from netman.core.objects.switch_base import SwitchBase
9+
from netman.core.objects.vlan import Vlan
10+
11+
12+
class Arista(SwitchBase):
13+
def __init__(self, switch_descriptor):
14+
super(Arista, self).__init__(switch_descriptor)
15+
self.switch_descriptor = switch_descriptor
16+
17+
def _connect(self):
18+
self.conn = pyeapi.connect(host=self.switch_descriptor.hostname,
19+
username=self.switch_descriptor.username,
20+
password=self.switch_descriptor.password,
21+
port=self.switch_descriptor.port,
22+
transport='http')
23+
24+
self.node = Node(self.conn, transport='http', host=self.switch_descriptor.hostname,
25+
username=self.switch_descriptor.username, password=self.switch_descriptor.password,
26+
port=self.switch_descriptor.port)
27+
28+
def _disconnect(self):
29+
self.conn = None
30+
31+
def _end_transaction(self):
32+
pass
33+
34+
def _start_transaction(self):
35+
pass
36+
37+
def get_vlan(self, number):
38+
try:
39+
vlans_info = self.conn.execute("show vlan {}".format(number))
40+
except CommandError:
41+
raise UnknownVlan(number)
42+
43+
return self._extract_vlan_list(vlans_info)[0]
44+
45+
def get_vlans(self):
46+
vlans_info = self.conn.execute("show vlan")
47+
48+
return self._extract_vlan_list(vlans_info)
49+
50+
def add_vlan(self, number, name=None):
51+
if not isvlan(number):
52+
raise BadVlanNumber()
53+
try:
54+
self.conn.execute("show vlan {}".format(number))
55+
raise VlanAlreadyExist(number)
56+
except CommandError:
57+
pass
58+
59+
commands = ["name {}".format(name)] if name else []
60+
61+
vlan = Vlans(self.node)
62+
if not vlan.configure_vlan(number, commands):
63+
raise BadVlanName()
64+
65+
def remove_vlan(self, number):
66+
try:
67+
self.conn.execute("show vlan {}".format(number))
68+
except CommandError:
69+
raise UnknownVlan(number)
70+
71+
vlan = Vlans(self.node)
72+
if not vlan.delete(number):
73+
raise OperationNotCompleted("Unable to remove vlan {}".format(number))
74+
75+
def _extract_vlan_list(self, vlans_info):
76+
vlan_list = []
77+
for id, vlan in vlans_info['result'][0]['vlans'].items():
78+
if vlan['name'] == ("VLAN{:04d}".format(int(id))):
79+
vlan['name'] = None
80+
81+
vlan_list.append(Vlan(number=int(id), name=vlan['name'], icmp_redirects=True, arp_routing=True, ntp=True))
82+
return vlan_list

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ paramiko>=1.15.1,<1.18
55
netaddr>=0.7.13
66
ncclient>=0.5.0
77
requests>=2.6.2
8+
pyeapi

test-requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ nose>=1.2.1
22
mock>=1.0.1
33
pyhamcrest>=1.6
44
flexmock>=0.10.2
5-
fake-switches>=1.2.4
5+
fake-switches>=1.3.0
66
MockSSH>=1.4.2,!=1.4.5 # 1.4.5 has fixed dependency and freezes paramiko
77
gunicorn>=19.4.5
88
flake8==3.4.1
99
futures; # Runtime dependency of gunicorn
10+
jabstract
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import unittest
2+
3+
from flexmock import flexmock, flexmock_teardown
4+
from hamcrest import assert_that, has_length, equal_to, is_
5+
from pyeapi.api.vlans import Vlans
6+
from pyeapi.eapilib import CommandError
7+
8+
from netman.adapters.switches.arista import Arista
9+
from netman.core.objects.exceptions import BadVlanNumber, VlanAlreadyExist, BadVlanName, UnknownVlan, \
10+
OperationNotCompleted
11+
from netman.core.objects.switch_descriptor import SwitchDescriptor
12+
from tests.fixtures.arista import vlans_payload, vlan_data
13+
14+
15+
class AristaTest(unittest.TestCase):
16+
17+
def setUp(self):
18+
self.switch = Arista(SwitchDescriptor(model='arista', hostname="my.hostname"))
19+
self.switch.conn = flexmock()
20+
self.switch.node = flexmock()
21+
22+
def tearDown(self):
23+
flexmock_teardown()
24+
25+
def test_get_vlans(self):
26+
four_vlans_payload = vlans_payload(result=[{'vlans': {'1': vlan_data(name='default'),
27+
'123': vlan_data(name='VLAN0123'),
28+
'456': vlan_data(name='Patate'),
29+
'4444': vlan_data(name='VLAN4444')}}])
30+
31+
self.switch.conn.should_receive("execute").with_args("show vlan").once().and_return(four_vlans_payload)
32+
33+
vlan_list = self.switch.get_vlans()
34+
vlan_list = sorted(vlan_list, key=lambda x: x.number)
35+
36+
assert_that(vlan_list, has_length(4))
37+
assert_that(vlan_list[0].number, equal_to(1))
38+
assert_that(vlan_list[0].name, equal_to("default"))
39+
40+
def test_get_vlans_with_default_name_returns_no_name(self):
41+
my_vlan = {u'status': u'active', u'interfaces': {}, u'dynamic': False, u'name': u'VLAN0123'}
42+
43+
self.switch.conn.should_receive("execute").with_args("show vlan").once() \
44+
.and_return(vlans_payload(result=[{'vlans': {'123': my_vlan}}]))
45+
46+
vlan_list = self.switch.get_vlans()
47+
vlan_list = sorted(vlan_list, key=lambda x: x.number)
48+
49+
assert_that(vlan_list[0].number, equal_to(123))
50+
assert_that(vlan_list[0].name, equal_to(None))
51+
52+
def test_get_vlan_doesnt_exist(self):
53+
self.switch.conn.should_receive("execute").with_args("show vlan 111").and_raise(CommandError(1000, 'msg'))
54+
55+
with self.assertRaises(UnknownVlan):
56+
self.switch.get_vlan(111)
57+
58+
def test_get_vlan(self):
59+
self.switch.conn.should_receive("execute").with_args("show vlan 123").once() \
60+
.and_return(vlans_payload(result=[{'vlans': {'123': vlan_data(name='My-Vlan-Name')}}]))
61+
62+
vlan = self.switch.get_vlan(123)
63+
64+
assert_that(vlan.number, is_(123))
65+
assert_that(vlan.name, is_('My-Vlan-Name'))
66+
67+
def test_add_vlan(self):
68+
vlan = flexmock(spec=Vlans)
69+
70+
self.switch.conn.should_receive("execute").with_args("show vlan 123").once().and_raise(
71+
CommandError(1000, 'msg'))
72+
vlan.should_receive("configure_vlan").with_args(123, []).once().and_return(True)
73+
74+
self.switch.add_vlan(123)
75+
76+
def test_add_vlan_with_name(self):
77+
vlan = flexmock(spec=Vlans)
78+
79+
self.switch.conn.should_receive("execute").with_args("show vlan 123").once().and_raise(
80+
CommandError(1000, 'msg'))
81+
vlan.should_receive("configure_vlan").with_args(123, ["name gertrude"]).once().and_return(True)
82+
83+
self.switch.add_vlan(123, "gertrude")
84+
85+
def test_add_vlan_bad_vlan_number(self):
86+
with self.assertRaises(BadVlanNumber):
87+
self.switch.add_vlan(12334)
88+
89+
def test_add_vlan_already_exits(self):
90+
self.switch.conn.should_receive("execute").with_args("show vlan 123").once()\
91+
.and_return(vlans_payload(result=[{'vlans': {'123': vlan_data(name='VLAN0123')}}]))
92+
93+
with self.assertRaises(VlanAlreadyExist):
94+
self.switch.add_vlan(123)
95+
96+
def test_add_vlan_bad_vlan_name(self):
97+
vlan = flexmock(spec=Vlans)
98+
99+
self.switch.conn.should_receive("execute").with_args("show vlan 123").once().and_raise(
100+
CommandError(1000, 'msg'))
101+
vlan.should_receive("configure_vlan").with_args(123, ["name gertrude_invalid_name"]).once().and_return(False)
102+
103+
with self.assertRaises(BadVlanName):
104+
self.switch.add_vlan(123, "gertrude_invalid_name")
105+
106+
def test_remove_vlan(self):
107+
vlan = flexmock(spec=Vlans)
108+
109+
self.switch.conn.should_receive("execute").with_args("show vlan 123").once()
110+
vlan.should_receive("delete").with_args(123).once().and_return(True)
111+
112+
self.switch.remove_vlan(123)
113+
114+
def test_remove_vlan_unknown_vlan(self):
115+
self.switch.conn.should_receive("execute").with_args("show vlan 123").once().and_raise(
116+
CommandError(1000, 'msg'))
117+
118+
with self.assertRaises(UnknownVlan):
119+
self.switch.remove_vlan(123)
120+
121+
def test_remove_vlan_unable_to_remove(self):
122+
vlan = flexmock(spec=Vlans)
123+
124+
self.switch.conn.should_receive("execute").with_args("show vlan 123").once()
125+
vlan.should_receive("delete").with_args(123).once().and_return(False)
126+
127+
with self.assertRaises(OperationNotCompleted):
128+
self.switch.remove_vlan(123)

tests/fixtures/__init__.py

Whitespace-only changes.

tests/fixtures/arista.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from jabstract import jabstract
2+
3+
vlans_payload = jabstract(
4+
{'jsonrpc': '2.0', 'id': '4418851472',
5+
'result': [{'sourceDetail': '', 'vlans': {}}]
6+
}
7+
)
8+
9+
vlan_data = jabstract({'status': 'active', 'interfaces': {}, 'dynamic': False, 'name': 'Name'})

0 commit comments

Comments
 (0)