blob: 1d62eae14972292c92ee15e6faad5c6f491773cf [file] [log] [blame]
Steve McIntyrecc297112014-08-11 18:46:58 +01001#! /usr/bin/python
2
3# Copyright 2014 Linaro Limited
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18# MA 02110-1301, USA.
19
20import logging
21import pexpect
22import sys
23import time
24import re
25from common import SwitchDriver
26
27class CiscoSX300(SwitchDriver):
28
29 connection = None
30 prompt_name = ''
31 ports = []
32 systemdata = []
33 serial_number = ''
34 # Regexp of expected hardware information - fail if we don't see
35 # this
36 expected_descr_re = re.compile('S.300-\d+P')
37
38 allowed_port_modes = [ "trunk", "general" ]
39
40 logfile = sys.stderr
Steve McIntyre16f02162014-08-14 17:47:36 +010041 logfile = None
Steve McIntyrecc297112014-08-11 18:46:58 +010042
43 def __init__(self, switch_hostname, switch_telnetport=23):
44 self.exec_string = "/usr/bin/telnet %s %d" % (switch_hostname, switch_telnetport)
45
46 ################################
47 ### Switch-level API functions
48 ################################
49
50 # Connect to the switch and log in
51 def SwitchConnect(self, username, password):
52 logging.debug("Connecting to Switch with: %s" % self.exec_string)
53 self.connection = pexpect.spawn(self.exec_string, logfile = self.logfile)
54 self._login(username, password)
55
Steve McIntyredf9c2c92014-08-12 18:14:49 +010056 # Try to avoid paged output
Steve McIntyre7ced0732014-08-12 18:07:56 +010057 self.connection.setwinsize(132,1000)
58
Steve McIntyrecc297112014-08-11 18:46:58 +010059 # And grab details about the switch. in case we need it
Steve McIntyrecc297112014-08-11 18:46:58 +010060 for line in self._get_systemdata():
61 self.systemdata.append (line)
62
63 # And also validate them - make sure we're driving a switch of
64 # the correct model! Also store the serial number
65 descr_regex = re.compile('System Description:.\s+(\S.*)')
66 sn_regex = re.compile('SN:\s+(\S_)')
67 descr = ""
68
69 for line in self.systemdata:
70 match = descr_regex.match(line)
71 if match:
72 descr = match.group(1)
73 match = sn_regex.match(line)
74 if match:
75 self.serial_number = match.group(1)
76
77 if not self.expected_descr_re.match(descr):
78 raise IOError("Switch %s not recognised by this driver: abort" % descr)
79
Steve McIntyre28d34ca2014-08-12 15:40:24 +010080 # Now build a list of our ports, for later sanity checking
81 self.ports = self._get_port_names()
82 if len(self.ports) < 4:
83 raise IOError("Not enough ports detected - problem!")
84
Steve McIntyrecc297112014-08-11 18:46:58 +010085 # Log out of the switch and drop the connection and all state
86 def SwitchDisconnect(self):
87 self._logout()
88 logging.debug("Closing connection: %s" % self.connection)
89 self.connection.close(True)
90 del(self)
91
92 # Save the current running config into flash - we want config to
93 # remain across reboots
94 def SwitchSaveRunningConfig(self):
95 self._cli("copy running-config startup-config")
96 self.connection.expect("Y/N")
97 self._cli("y")
98 self.connection.expect("Copy succeeded")
99
100 # List the names of all the ports on the switch
101 def SwitchGetPortNames(self):
102 return self.ports
103
104 ################################
105 ### VLAN API functions
106 ################################
107
108 # Create a VLAN with the specified tag
109 def VlanCreate(self, tag):
110 logging.debug("Creating VLAN %d" % tag)
111 self._configure()
112 self._cli("vlan database")
113 self._cli("vlan %d" % tag)
114 self._end_configure()
115
116 # Validate it happened
117 vlans = self.VlanGetList()
118 for vlan in vlans:
119 if vlan == tag:
120 return
121 raise IOError("Failed to create VLAN %d" % tag)
122
123 # Destroy a VLAN with the specified tag
124 def VlanDestroy(self, tag):
125 logging.debug("Destroying VLAN %d" % tag)
126 self._configure()
127 self._cli("no vlan %d" % tag)
128 self._end_configure()
129
130 # Validate it happened
131 vlans = self.VlanGetList()
132 for vlan in vlans:
133 if vlan == tag:
134 raise IOError("Failed to destroy VLAN %d" % tag)
135
136 # Set the name of a VLAN
137 def VlanSetName(self, tag, name):
138 logging.debug("Setting name of VLAN %d to %s" % (tag, name))
139 self._configure()
140 self._cli("vlan %d" % tag)
141 self._cli("interface vlan %d" % tag)
142 self._cli("name %s" % name)
143 self._end_configure()
144
145 # Validate it happened
146 read_name = self.VlanGetName(tag)
147 if read_name != name:
148 raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")"
149 % (tag, read_name, name))
150
151 # Get a list of the VLAN tags currently registered on the switch
152 def VlanGetList(self):
153 logging.debug("Grabbing list of VLANs")
154 vlans = []
155
156 regex = re.compile('^ *(\d+).*(D|S|G|R)')
157
158 self._cli("show vlan")
159 for line in self._read_paged_output():
160 match = regex.match(line)
161 if match:
162 vlans.append(int(match.group(1)))
163 return vlans
164
165 # For a given VLAN tag, ask the switch what the associated name is
166 def VlanGetName(self, tag):
167 logging.debug("Grabbing the name of VLAN %d" % tag)
168 name = None
169 regex = re.compile('^ *\d+\s+(\S+).*(D|S|G|R)')
170 self._cli("show vlan tag %d" % tag)
171 for line in self._read_paged_output():
172 match = regex.match(line)
173 if match:
174 name = match.group(1)
175 name.strip()
176 return name
177
178
179 ################################
180 ### Port API functions
181 ################################
182
183 # Set the mode of a port: general or trunk
184 def PortSetMode(self, port, mode):
185 logging.debug("Setting port %s to %s" % (port, mode))
186 if not self._is_port_mode_valid(mode):
187 raise IndexError("Port mode %s is not allowed" % mode)
188 if not self._is_port_name_valid(port):
189 raise IndexError("Port name %s not recognised" % port)
190 self._configure()
191 self._cli("interface %s" % port)
192 self._cli("switchport mode %s" % mode)
193 self._end_configure()
194
195 # Validate it happened
196 read_mode = self.PortGetMode(port)
197 if read_mode != mode:
198 raise IOError("Failed to set mode for port %s" % port)
199
200
201 # Get the mode of a port: general or trunk
202 def PortGetMode(self, port):
203 logging.debug("Getting mode of port %s" % port)
204 mode = ''
205 if not self._is_port_name_valid(port):
206 raise IndexError("Port name %s not recognised" % port)
207 regex = re.compile('Port Mode: (\S+)')
208 self._cli("show interfaces switchport %s" % port)
209 for line in self._read_paged_output():
210 match = regex.match(line)
211 if match:
212 mode = match.group(1)
213 return mode.lower()
214
Steve McIntyre16f02162014-08-14 17:47:36 +0100215 # Set a general port to be in a specified VLAN (tag)
216 def PortSetGeneralVlan(self, port, tag):
217 logging.debug("Setting general port %s to VLAN %d" % (port, tag))
Steve McIntyrecc297112014-08-11 18:46:58 +0100218 if not self._is_port_name_valid(port):
219 raise IndexError("Port name %s not recognised" % port)
220 if not (self.PortGetMode(port) == "general"):
221 raise IndexError("Port %s not in general mode" % port)
222
Steve McIntyre16f02162014-08-14 17:47:36 +0100223 # More complicated than just a "set" on the hardware, so let's
224 # split it up into separate helper calls.
Steve McIntyrecc297112014-08-11 18:46:58 +0100225
Steve McIntyre16f02162014-08-14 17:47:36 +0100226 # First, get the current VLAN
227 current_vlan = self.PortGetGeneralVlan(port)
228
229 # Next, drop off that current VLAN
230 # VLAN 1 is handled specially :-(
231 if current_vlan == 1:
232 self._port_general_block_default_vlan(port)
233 else:
234 self._port_remove_general_from_vlan(port, current_vlan)
235
236 # Next, add the desired VLAN
237 # VLAN 1 is handled specially again :-(
238 if tag == 1:
239 self._port_general_allow_default_vlan(port)
240 else:
241 self._port_add_general_to_vlan(port, tag)
242
243 # Finally, validate things worked
244 read_vlan = int(self.PortGetGeneralVlan(port))
245 print "read_vlan is %s, tag is %d" % (read_vlan, tag)
Steve McIntyrecc297112014-08-11 18:46:58 +0100246 if read_vlan != tag:
Steve McIntyre5b3ce212014-08-15 13:10:41 +0100247 raise IOError("Failed to move general port %s to VLAN %d - got VLAN %d instead"
Steve McIntyrecc297112014-08-11 18:46:58 +0100248 % (port, tag, read_vlan))
249
Steve McIntyrecc297112014-08-11 18:46:58 +0100250 # Add a trunk port to a specified VLAN (tag)
251 def PortAddTrunkToVlan(self, port, tag):
252 logging.debug("Adding trunk port %s to VLAN %d" % (port, tag))
253 if not self._is_port_name_valid(port):
254 raise IndexError("Port name %s not recognised" % port)
255 if not (self.PortGetMode(port) == "trunk"):
256 raise IndexError("Port %s not in trunk mode" % port)
257 self._configure()
258 self._cli("interface %s" % port)
259 self._cli("switchport trunk allowed vlan add %d" % tag)
260 self._end_configure()
261
262 # Validate it happened
263 read_vlans = self.PortGetTrunkVlanList(port)
264 for vlan in read_vlans:
265 if vlan == tag:
266 return
Steve McIntyre5b3ce212014-08-15 13:10:41 +0100267 raise IOError("Failed to add trunk port %s to VLAN %d" % (port, tag))
Steve McIntyrecc297112014-08-11 18:46:58 +0100268
269 # Remove a trunk port from a specified VLAN (tag)
270 def PortRemoveTrunkFromVlan(self, port, tag):
271 logging.debug("Removing trunk port %s from VLAN %d" % (port, tag))
272 if not self._is_port_name_valid(port):
273 raise IndexError("Port name %s not recognised" % port)
274 if not (self.PortGetMode(port) == "trunk"):
275 raise IndexError("Port %s not in trunk mode" % port)
276 self._configure()
277 self._cli("interface %s" % port)
278 self._cli("switchport trunk allowed vlan remove %d" % tag)
279 self._end_configure()
280
281 # Validate it happened
282 read_vlans = self.PortGetTrunkVlanList(port)
283 for vlan in read_vlans:
284 if vlan == tag:
Steve McIntyre5b3ce212014-08-15 13:10:41 +0100285 raise IOError("Failed to remove trunk port %s from VLAN %d" % (port, tag))
Steve McIntyrecc297112014-08-11 18:46:58 +0100286
287 # Get the configured VLAN tag for an general port (tag)
288 def PortGetGeneralVlan(self, port):
289 logging.debug("Getting VLAN for general port %s" % port)
290 vlan = 1
291 if not self._is_port_name_valid(port):
292 raise IndexError("Port name %s not recognised" % port)
293 if not (self.PortGetMode(port) == "general"):
294 raise IndexError("Port %s not in general mode" % port)
295 regex = re.compile('(\d+)\s+\S+\s+Untagged\s+Static')
296 self._cli("show interfaces switchport %s" % port)
297 for line in self._read_paged_output():
298 match = regex.match(line)
299 if match:
300 vlan = match.group(1)
301 return int(vlan)
302
303 # Get the list of configured VLAN tags for a trunk port
304 def PortGetTrunkVlanList(self, port):
305 logging.debug("Getting VLANs for trunk port %s" % port)
306 vlans = [ ]
307 if not self._is_port_name_valid(port):
308 raise IndexError("Port name %s not recognised" % port)
309 if not (self.PortGetMode(port) == "trunk"):
310 raise IndexError("Port %s not in general mode" % port)
311 regex = re.compile('(\d+)\s+\S+\s+(Tagged|Untagged)\s+Static')
312 self._cli("show interfaces switchport %s" % port)
313 for line in self._read_paged_output():
314 match = regex.match(line)
315 if match:
316 vlans.append (int(match.group(1)))
317 return vlans
318
319 ################################
320 ### Internal functions
321 ################################
322
323 def _login(self, username, password):
324 logging.debug("attempting login with username %s, password %s" % (username, password))
325 self._cli("")
326 self.connection.expect("User Name:")
327 self._cli("%s" % username)
328 self.connection.expect("Password:")
329 self._cli("%s" % password, False)
330 while True:
331 index = self.connection.expect(['User Name:', 'authentication failed', r'(.*)#', '.*'])
332 if index == 0 or index == 1: # Failed to log in!
333 logging.error("Login failure: %s\n" % self.connection.match)
334 raise IOError
335 elif index == 2:
336 self.prompt_name = self.connection.match.group(1).strip()
337 return 0
338
339 def _logout(self):
340 logging.debug("Logging out")
341 self._cli("exit", False)
342
Steve McIntyrecc297112014-08-11 18:46:58 +0100343 def _configure(self):
344 self._cli("configure terminal")
345
346 def _end_configure(self):
347 self._cli("end")
348
349 def _read_paged_output(self):
350 buf = []
351 prompt = self.prompt_name + '#'
352 while True:
353 index = self.connection.expect(['\x1b\[0mMore:.*<return>.*$', prompt])
354 if index == 0: # More: <space>
355 for line in self.connection.before.split('\r\n'):
356 buf.append(line.strip())
357 self._cli(' ', False)
358 elif index == 1: # Back to a prompt, says output is finished
359 break
360
361 for line in self.connection.before.split('\r\n'):
362 buf.append(line.strip())
363
364 return buf
365
366 def _get_port_names(self):
367 logging.debug("Grabbing list of ports")
368 interfaces = []
369
370 # Use "Up" or "Down" to only identify lines in the output that
371 # match interfaces that exist
372 regex = re.compile('^(\w+).*(Up|Down)')
373
374 self._cli("show interfaces status detailed")
375 for line in self._read_paged_output():
376 match = regex.match(line)
377 if match:
378 interfaces.append(match.group(1))
379 return interfaces
380
381 def _is_port_name_valid(self, name):
382 logging.debug("Checking if supplied port name \"%s\" is valid" % name)
383 for port in self.ports:
384 if name == port:
385 return True
386 return False
387
388 def _is_port_mode_valid(self, mode):
389 logging.debug("Checking if supplied port mode \"%s\" is valid" % mode)
390 for allowed in self.allowed_port_modes:
391 if allowed == mode:
392 return True
393 return False
394
395 def _show_config(self):
396 logging.debug("Grabbing config")
397 self._cli("show running-config")
398 return self._read_paged_output()
399
400 def _show_clock(self):
401 logging.debug("Grabbing time")
402 self._cli("show clock")
403 return self._read_paged_output()
404
405 def _show_clock(self):
406 logging.debug("Grabbing ")
407 self._cli("show clock")
408 return self._read_paged_output()
409
Steve McIntyrecc297112014-08-11 18:46:58 +0100410 def _get_systemdata(self):
Steve McIntyrecc297112014-08-11 18:46:58 +0100411 data = []
412
Steve McIntyre28d34ca2014-08-12 15:40:24 +0100413 logging.debug("Grabbing system data")
Steve McIntyrecc297112014-08-11 18:46:58 +0100414 self._cli("show system")
415 for line in self._read_paged_output():
416 data.append(line)
Steve McIntyre28d34ca2014-08-12 15:40:24 +0100417
418 logging.debug("Grabbing system sw and hw versions")
419 self._cli("show version")
420 for line in self._read_paged_output():
421 data.append(line)
422
Steve McIntyrecc297112014-08-11 18:46:58 +0100423 return data
424
Steve McIntyre16f02162014-08-14 17:47:36 +0100425 ######################################
426 # Internal port access helper methods
427 ######################################
428 # N.B. No parameter checking here, for speed reasons - if you're
429 # calling this internal API then you should already have validated
430 # things yourself! Equally, no post-set checks in here - do that
431 # at the higher level.
432 ######################################
433
434 # Allow the default VLAN on a port
435 def _port_general_allow_default_vlan(self, port):
436 logging.debug("Allowing default VLAN (1) for general port %s" % port)
437 self._configure()
438 self._cli("interface %s" % port)
439 self._cli("no switchport forbidden default-vlan")
440 self._end_configure()
441 # Difficult to validate
442
443 # Block the default VLAN on a port
444 def _port_general_block_default_vlan(self, port):
445 logging.debug("Blocking default VLAN (1) for general port %s" % port)
446 self._configure()
447 self._cli("interface %s" % port)
448 self._cli("switchport forbidden default-vlan")
449 self._end_configure()
450 # Difficult to validate
451
452 # Add a general port to a specified VLAN (tag)
453 def _port_add_general_to_vlan(self, port, tag):
454 logging.debug("Adding general port %s to VLAN %d" % (port, tag))
455 self._configure()
456 self._cli("interface %s" % port)
457 self._cli("switchport general pvid %d" % tag)
458 self._cli("switchport general allowed vlan add %d untagged" % tag)
459 self._end_configure()
460
461 # Remove a general port from a specified VLAN (tag)
462 def _port_remove_general_from_vlan(self, port, tag):
463 logging.debug("Removing general port %s from VLAN %d" % (port, tag))
464 self._configure()
465 self._cli("interface %s" % port)
466 self._cli("no switchport general pvid")
467 self._cli("switchport general allowed vlan remove %d" % tag)
468 self._end_configure()
469
Steve McIntyrecc297112014-08-11 18:46:58 +0100470 # Wrapper around connection.send - by default, expect() the same
471 # text we've sent, to remove it from the output from the
472 # switch. For the few cases where we don't need that, override
473 # this using echo=False.
474 # Horrible, but seems to work.
475 def _cli(self, text, echo=True):
476 self.connection.send(text + '\r')
477 if echo:
478 self.connection.expect(text)
479
480if __name__ == "__main__":
481 p = CiscoSX300('10.172.2.52', 23)
482 p.SwitchConnect('cisco', 'cisco')
483 #buf = p._show_clock()
484 #print "%s" % buf
485 #buf = p._show_config()
486 #p._dump_list(buf)
487
488 #print "System data:"
489 #p._dump_list(p.systemdata)
490
491 print "Creating VLANs for testing:"
492 for i in [ 2, 3, 4, 5, 20 ]:
493 p.VlanCreate(i)
494 p.VlanSetName(i, "test%d" % i)
495 print " %d (test%d)" % (i, i)
496
497 #print "And dump config\n"
498 #buf = p._show_config()
499 #print "%s" % buf
500
501 #print "Destroying VLAN 2\n"
502 #p.VlanDestroy(2)
503
504 #print "And dump config\n"
505 #buf = p._show_config()
506 #print "%s" % buf
507
508 #print "Port names are:"
509 #buf = p.SwitchGetPortNames()
510 #p._dump_list(buf)
511
512 #buf = p.VlanGetName(25)
513 #print "VLAN with tag 25 is called \"%s\"" % buf
514
515 #p.VlanSetName(35, "foo")
516 #print "VLAN with tag 35 is called \"foo\""
517
518 #buf = p.PortGetMode("fa12")
519 #print "Port fa12 is in %s mode" % buf
520
521 # Test general stuff
522 print "Set fa6 to general mode"
523 p.PortSetMode("fa6", "general")
Steve McIntyre16f02162014-08-14 17:47:36 +0100524 print "Move fa6 to VLAN 2"
525 p.PortSetGeneralVlan("fa6", 2)
Steve McIntyrecc297112014-08-11 18:46:58 +0100526 buf = p.PortGetGeneralVlan("fa6")
527 print "Read from switch: fa6 is on VLAN %s" % buf
Steve McIntyre16f02162014-08-14 17:47:36 +0100528 print "Move fa6 back to default VLAN 1"
529 p.PortSetGeneralVlan("fa6", 1)
Steve McIntyrecc297112014-08-11 18:46:58 +0100530 #print "And move fa6 back to a trunk port"
531 #p.PortSetMode("fa6", "trunk")
532 #buf = p.PortGetMode("fa6")
533 #print "Port fa6 is in %s mode" % buf
534
535 # Test trunk stuff
536 print "Set gi2 to trunk mode"
537 p.PortSetMode("gi2", "trunk")
538 print "Add gi2 to VLAN 2"
539 p.PortAddTrunkToVlan("gi2", 2)
540 print "Add gi2 to VLAN 3"
541 p.PortAddTrunkToVlan("gi2", 3)
542 print "Add gi2 to VLAN 4"
543 p.PortAddTrunkToVlan("gi2", 4)
544 print "Read from switch: which VLANs is gi2 on?"
545 buf = p.PortGetTrunkVlanList("gi2")
546 p._dump_list(buf)
547
548 p.PortRemoveTrunkFromVlan("gi2", 3)
549 p.PortRemoveTrunkFromVlan("gi2", 3)
550 p.PortRemoveTrunkFromVlan("gi2", 4)
551 print "Read from switch: which VLANs is gi2 on?"
552 buf = p.PortGetTrunkVlanList("gi2")
553 p._dump_list(buf)
554
555 # print "Adding lots of ports to VLANs"
556 # p.PortAddTrunkToVlan("fa1", 2)
557 # p.PortAddTrunkToVlan("fa3", 2)
558 # p.PortAddTrunkToVlan("fa5", 2)
559 # p.PortAddTrunkToVlan("fa7", 2)
560 # p.PortAddTrunkToVlan("fa9", 2)
561 # p.PortAddTrunkToVlan("fa11", 2)
562 # p.PortAddTrunkToVlan("fa13", 2)
563 # p.PortAddTrunkToVlan("fa15", 2)
564 # p.PortAddTrunkToVlan("fa17", 2)
565 # p.PortAddTrunkToVlan("fa19", 2)
566 # p.PortAddTrunkToVlan("fa21", 2)
567 # p.PortAddTrunkToVlan("fa23", 2)
568 # p.PortAddTrunkToVlan("gi4", 2)
569
570 print "VLANs are:"
571 buf = p.VlanGetList()
572 p._dump_list(buf)
573
574# p.SwitchSaveRunningConfig()
575
576 p.SwitchDisconnect()
577# p._show_config()
578