blob: e9b6dae05b082ff098d1ad28dec6a0a83f8479be [file] [log] [blame]
Steve McIntyred6759dd2014-08-12 18:10:00 +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 CiscoCatalyst(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('WS-C\S+-\d+P')
37
38 allowed_port_modes = [ "trunk", "general" ]
39
40 logfile = sys.stderr
41 logfile = None
42
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, enablepassword):
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, enablepassword)
55
56 # Try to avoid paged output
57 self.connection.setwinsize(132,1000)
58
59 # And grab details about the switch. in case we need it
60 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('^cisco\s+(\S+)')
66 sn_regex = re.compile('System serial number\s+:\s+(\S+)')
67 descr = ""
68
69 print "Grabbed %d lines of systemdata" % len(self.systemdata)
70
71 for line in self.systemdata:
72 match = descr_regex.match(line)
73 if match:
74 descr = match.group(1)
75 match = sn_regex.match(line)
76 if match:
77 self.serial_number = match.group(1)
78
79 print "serial number is %s" % self.serial_number
80 print "system description is %s" % descr
81
82 if not self.expected_descr_re.match(descr):
83 raise IOError("Switch %s not recognised by this driver: abort" % descr)
84
85 # Now build a list of our ports, for later sanity checking
86 self.ports = self._get_port_names()
87 if len(self.ports) < 4:
88 raise IOError("Not enough ports detected - problem!")
89
90 # Log out of the switch and drop the connection and all state
91 def SwitchDisconnect(self):
92 self._logout()
93 logging.debug("Closing connection: %s" % self.connection)
94 self.connection.close(True)
95 del(self)
96
97 # Save the current running config into flash - we want config to
98 # remain across reboots
99 def SwitchSaveRunningConfig(self):
100 self._cli("copy running-config startup-config")
Steve McIntyree1bf11a2014-08-14 17:56:25 +0100101 self.connection.expect("startup-config")
102 self._cli("startup-config")
103 self.connection.expect("OK")
Steve McIntyred6759dd2014-08-12 18:10:00 +0100104
105 # List the names of all the ports on the switch
106 def SwitchGetPortNames(self):
107 return self.ports
108
109 ################################
110 ### VLAN API functions
111 ################################
112
113 # Create a VLAN with the specified tag
114 def VlanCreate(self, tag):
115 logging.debug("Creating VLAN %d" % tag)
116 self._configure()
117 self._cli("vlan %d" % tag)
118 self._end_configure()
119
120 # Validate it happened
121 vlans = self.VlanGetList()
122 for vlan in vlans:
123 if vlan == tag:
124 return
125 raise IOError("Failed to create VLAN %d" % tag)
126
127 # Destroy a VLAN with the specified tag
128 def VlanDestroy(self, tag):
129 logging.debug("Destroying VLAN %d" % tag)
130 self._configure()
131 self._cli("no vlan %d" % tag)
132 self._end_configure()
133
134 # Validate it happened
135 vlans = self.VlanGetList()
136 for vlan in vlans:
137 if vlan == tag:
138 raise IOError("Failed to destroy VLAN %d" % tag)
139
140 # Set the name of a VLAN
141 def VlanSetName(self, tag, name):
142 logging.debug("Setting name of VLAN %d to %s" % (tag, name))
143 self._configure()
144 self._cli("vlan %d" % tag)
145 self._cli("name %s" % name)
146 self._end_configure()
147
148 # Validate it happened
149 read_name = self.VlanGetName(tag)
150 if read_name != name:
151 raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")"
152 % (tag, read_name, name))
153
154 # Get a list of the VLAN tags currently registered on the switch
155 def VlanGetList(self):
156 logging.debug("Grabbing list of VLANs")
157 vlans = []
158
159 regex = re.compile('^ *(\d+).*(active)')
160
161 self._cli("show vlan brief")
162 for line in self._read_paged_output():
163 match = regex.match(line)
164 if match:
165 vlans.append(int(match.group(1)))
166 return vlans
167
168 # For a given VLAN tag, ask the switch what the associated name is
169 def VlanGetName(self, tag):
170 logging.debug("Grabbing the name of VLAN %d" % tag)
171 name = None
172 regex = re.compile('^ *\d+\s+(\S+).*(active)')
173 self._cli("show vlan id %d" % tag)
174 for line in self._read_paged_output():
175 match = regex.match(line)
176 if match:
177 name = match.group(1)
178 name.strip()
179 return name
180
181
182 ################################
183 ### Port API functions
Steve McIntyree1bf11a2014-08-14 17:56:25 +0100184 ################################
Steve McIntyred6759dd2014-08-12 18:10:00 +0100185
186 # Set the mode of a port: general or trunk
187 def PortSetMode(self, port, mode):
188 logging.debug("Setting port %s to %s" % (port, mode))
189 if not self._is_port_mode_valid(mode):
190 raise IndexError("Port mode %s is not allowed" % mode)
191 if not self._is_port_name_valid(port):
192 raise IndexError("Port name %s not recognised" % port)
Steve McIntyreb7adc782014-08-13 00:22:21 +0100193 # Catalyst uses a different name here :-(
194 if mode == "general":
195 mode = "access"
Steve McIntyred6759dd2014-08-12 18:10:00 +0100196 self._configure()
197 self._cli("interface %s" % port)
198 self._cli("switchport mode %s" % mode)
199 self._end_configure()
200
201 # Validate it happened
202 read_mode = self.PortGetMode(port)
203 if read_mode != mode:
204 raise IOError("Failed to set mode for port %s" % port)
205
Steve McIntyred6759dd2014-08-12 18:10:00 +0100206 # Get the mode of a port: general or trunk
207 def PortGetMode(self, port):
208 logging.debug("Getting mode of port %s" % port)
209 mode = ''
210 if not self._is_port_name_valid(port):
211 raise IndexError("Port name %s not recognised" % port)
Steve McIntyreb7adc782014-08-13 00:22:21 +0100212 regex = re.compile('Administrative Mode: \S+ (\S+)')
213 self._cli("show interfaces %s switchport" % port)
Steve McIntyred6759dd2014-08-12 18:10:00 +0100214 for line in self._read_paged_output():
215 match = regex.match(line)
216 if match:
217 mode = match.group(1)
Steve McIntyreb7adc782014-08-13 00:22:21 +0100218 # Munge the names to match what we expect
219 if mode.lower() == "access":
220 mode = "general"
221 elif mode.lower() == "auto":
222 mode = "trunk"
Steve McIntyred6759dd2014-08-12 18:10:00 +0100223 return mode.lower()
224
225 # Allow the default VLAN on a port
226 def PortAllowDefaultVlan(self, port):
227 if not self._is_port_name_valid(port):
228 raise IndexError("Port name %s not recognised" % port)
229 self._configure()
230 self._cli("interface %s" % port)
231 self._cli("no switchport forbidden default-vlan")
232 self._end_configure()
233 # Difficult to validate
234
235 # Block the default VLAN on a port
236 def PortBlockDefaultVlan(self, port):
237 if not self._is_port_name_valid(port):
238 raise IndexError("Port name %s not recognised" % port)
239 self._configure()
240 self._cli("interface %s" % port)
241 self._cli("switchport forbidden default-vlan")
242 self._end_configure()
243 # Difficult to validate
244
245 # Add a general port to a specified VLAN (tag)
246 def PortAddGeneraltoVlan(self, port, tag):
247 logging.debug("Adding general port %s to VLAN %d" % (port, tag))
248 if not self._is_port_name_valid(port):
249 raise IndexError("Port name %s not recognised" % port)
250 if not (self.PortGetMode(port) == "general"):
251 raise IndexError("Port %s not in general mode" % port)
252
253 # Special handling for VLAN 1 (default)
254 if tag == 1:
255 return self.PortAllowDefaultVlan(port)
256 else:
257 self._configure()
258 self._cli("interface %s" % port)
Steve McIntyree1bf11a2014-08-14 17:56:25 +0100259 self._cli("switchport access vlan %d" % tag)
Steve McIntyred6759dd2014-08-12 18:10:00 +0100260 self._end_configure()
261
262 # Validate it happened
263 read_vlan = self.PortGetGeneralVlan(port)
264 if read_vlan != tag:
265 raise IOError("Failed to add general port %d to VLAN %d - got VLAN %d"
266 % (port, tag, read_vlan))
267
268 # Remove a general port from a specified VLAN (tag)
269 def PortRemoveGeneralFromVlan(self, port, tag):
270 logging.debug("Removing general port %s from VLAN %d" % (port, tag))
271 if not self._is_port_name_valid(port):
272 raise IndexError("Port name %s not recognised" % port)
273 if not (self.PortGetMode(port) == "general"):
274 raise IndexError("Port %s not in general mode" % port)
275
276 # Special handling for VLAN 1 (default)
277 if tag == 1:
278 return self.PortBlockDefaultVlan(port)
279 else:
280 self._configure()
281 self._cli("interface %s" % port)
282 self._cli("no switchport general pvid")
283 self._cli("switchport general allowed vlan remove %d" % tag)
284 self._end_configure()
285
286 # Validate it happened
287 read_vlan = self.PortGetGeneralVlan(port)
288 if read_vlan == tag:
289 raise IOError("Failed to remove general port %d from VLAN %d"
290 % (port, tag))
291
292 # Add a trunk port to a specified VLAN (tag)
293 def PortAddTrunkToVlan(self, port, tag):
294 logging.debug("Adding trunk port %s to VLAN %d" % (port, tag))
295 if not self._is_port_name_valid(port):
296 raise IndexError("Port name %s not recognised" % port)
297 if not (self.PortGetMode(port) == "trunk"):
298 raise IndexError("Port %s not in trunk mode" % port)
299 self._configure()
300 self._cli("interface %s" % port)
301 self._cli("switchport trunk allowed vlan add %d" % tag)
302 self._end_configure()
303
304 # Validate it happened
305 read_vlans = self.PortGetTrunkVlanList(port)
306 for vlan in read_vlans:
307 if vlan == tag:
308 return
309 raise IOError("Failed to add trunk port %d to VLAN %d" % (port, tag))
310
311 # Remove a trunk port from a specified VLAN (tag)
312 def PortRemoveTrunkFromVlan(self, port, tag):
313 logging.debug("Removing trunk port %s from VLAN %d" % (port, tag))
314 if not self._is_port_name_valid(port):
315 raise IndexError("Port name %s not recognised" % port)
316 if not (self.PortGetMode(port) == "trunk"):
317 raise IndexError("Port %s not in trunk mode" % port)
318 self._configure()
319 self._cli("interface %s" % port)
320 self._cli("switchport trunk allowed vlan remove %d" % tag)
321 self._end_configure()
322
323 # Validate it happened
324 read_vlans = self.PortGetTrunkVlanList(port)
325 for vlan in read_vlans:
326 if vlan == tag:
327 raise IOError("Failed to remove trunk port %d from VLAN %d" % (port, tag))
328
329 # Get the configured VLAN tag for an general port (tag)
330 def PortGetGeneralVlan(self, port):
331 logging.debug("Getting VLAN for general port %s" % port)
332 vlan = 1
333 if not self._is_port_name_valid(port):
334 raise IndexError("Port name %s not recognised" % port)
335 if not (self.PortGetMode(port) == "general"):
336 raise IndexError("Port %s not in general mode" % port)
337 regex = re.compile('(\d+)\s+\S+\s+Untagged\s+Static')
338 self._cli("show interfaces switchport %s" % port)
339 for line in self._read_paged_output():
340 match = regex.match(line)
341 if match:
342 vlan = match.group(1)
343 return int(vlan)
344
345 # Get the list of configured VLAN tags for a trunk port
346 def PortGetTrunkVlanList(self, port):
347 logging.debug("Getting VLANs for trunk port %s" % port)
348 vlans = [ ]
349 if not self._is_port_name_valid(port):
350 raise IndexError("Port name %s not recognised" % port)
351 if not (self.PortGetMode(port) == "trunk"):
352 raise IndexError("Port %s not in general mode" % port)
353 regex = re.compile('(\d+)\s+\S+\s+(Tagged|Untagged)\s+Static')
354 self._cli("show interfaces switchport %s" % port)
355 for line in self._read_paged_output():
356 match = regex.match(line)
357 if match:
358 vlans.append (int(match.group(1)))
359 return vlans
360
361 ################################
362 ### Internal functions
363 ################################
364
365 def _login(self, username, password, enablepassword):
366 logging.debug("attempting login with username %s, password %s" % (username, password))
367 self.connection.expect('User Access Verification')
368 if username is not None:
369 self.connection.expect("User Name:")
370 self._cli("%s" % username)
371 if password is not None:
372 self.connection.expect("Password:")
373 self._cli("%s" % password, False)
374 while True:
375 index = self.connection.expect(['User Name:', 'Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)'])
376 if index != 4: # Any other means: failed to log in!
377 logging.error("Login failure: index %d\n" % index)
378 logging.error("Login failure: %s\n" % self.connection.match.before)
379 raise IOError
380
381 # else
382 self.prompt_name = self.connection.match.group(1).strip()
383 if self.connection.match.group(2) == ">":
384 # Need to enter "enable" mode too
385 self._cli("enable")
386 if enablepassword is not None:
387 self.connection.expect("Password:")
388 self._cli("%s" % enablepassword, False)
389 index = self.connection.expect(['Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)'])
390 if index != 3: # Any other means: failed to log in!
391 logging.error("Enable password failure: %s\n" % self.connection.match)
392 raise IOError
393 return 0
394
395 def _logout(self):
396 logging.debug("Logging out")
397 self._cli("exit", False)
398
399 def _configure(self):
400 self._cli("configure terminal")
401
402 def _end_configure(self):
403 self._cli("end")
404
405 def _read_paged_output(self):
406 buf = []
407 prompt = self.prompt_name + '#'
408 while True:
409 index = self.connection.expect([' -*More-*', prompt])
410 if index == 0: # More: <space>
411 for line in self.connection.before.split('\r\n'):
412 line1 = re.sub('(\x08|\x0D)*', '', line.strip())
413 buf.append(line1)
414 self._cli(' ', False)
415 elif index == 1: # Back to a prompt, says output is finished
416 break
417
418 for line in self.connection.before.split('\r\n'):
419 line1 = re.sub('(\x08|\x0D)*', '', line.strip())
420 buf.append(line1)
421
422 return buf
423
424 def _get_port_names(self):
425 logging.debug("Grabbing list of ports")
426 interfaces = []
427
428 # Use "Up" or "Down" to only identify lines in the output that
429 # match interfaces that exist
430 regex = re.compile('^\s*([a-zA-Z0-9_/]*).*(connect)(.*)')
431 regex1 = re.compile('.*Not Present.*')
432
433 self._cli("show interfaces status")
434 for line in self._read_paged_output():
435 match = regex.match(line)
436 if match:
437 interface = match.group(1)
438 junk = match.group(3)
439 match1 = regex1.match(junk) # Deliberately drop things
440 # marked as "Not Present"
441 if not match1:
442 interfaces.append(interface)
443 return interfaces
444
445 def _is_port_name_valid(self, name):
446 logging.debug("Checking if supplied port name \"%s\" is valid" % name)
447 for port in self.ports:
448 if name == port:
449 return True
450 return False
451
452 def _is_port_mode_valid(self, mode):
453 logging.debug("Checking if supplied port mode \"%s\" is valid" % mode)
454 for allowed in self.allowed_port_modes:
455 if allowed == mode:
456 return True
457 return False
458
459 def _show_config(self):
460 logging.debug("Grabbing config")
461 self._cli("show running-config")
462 return self._read_paged_output()
463
464 def _show_clock(self):
465 logging.debug("Grabbing time")
466 self._cli("show clock")
467 return self._read_paged_output()
468
469 def _show_clock(self):
470 logging.debug("Grabbing ")
471 self._cli("show clock")
472 return self._read_paged_output()
473
474 def _get_systemdata(self):
475 data = []
476
477 logging.debug("Grabbing system sw and hw versions")
478 self._cli("show version")
479 for line in self._read_paged_output():
480 data.append(line)
481
482 return data
483
484 # Wrapper around connection.send - by default, expect() the same
485 # text we've sent, to remove it from the output from the
486 # switch. For the few cases where we don't need that, override
487 # this using echo=False.
488 # Horrible, but seems to work.
489 def _cli(self, text, echo=True):
490 self.connection.send(text + '\r')
491 if echo:
492 self.connection.expect(text)
493
494if __name__ == "__main__":
495 p = CiscoCatalyst('lngswitch02', 23)
496 p.SwitchConnect(None, 'lngvirtual', 'lngenable')
497
498 print "VLANs are:"
499 buf = p.VlanGetList()
500 p._dump_list(buf)
501
502 buf = p.VlanGetName(2)
503 print "VLAN 2 is named \"%s\"" % buf
504
505 print "Create VLAN 3"
506 p.VlanCreate(3)
507
508 buf = p.VlanGetName(3)
509 print "VLAN 3 is named \"%s\"" % buf
510
511 print "Set name of VLAN 3 to test333"
512 p.VlanSetName(3, "test333")
513
514 buf = p.VlanGetName(3)
515 print "VLAN 3 is named \"%s\"" % buf
516
517 print "VLANs are:"
518 buf = p.VlanGetList()
519 p._dump_list(buf)
520
521 print "Destroy VLAN 3"
522 p.VlanDestroy(3)
523
524 print "VLANs are:"
525 buf = p.VlanGetList()
526 p._dump_list(buf)
527
528 #print "List ports"
529 #buf = p.SwitchGetPortNames()
530 #p._dump_list(buf)
531
532 #print "System data:"
533 #p._dump_list(p.systemdata)
534
535# print "Creating VLANs for testing:"
536# for i in [ 2, 3, 4, 5, 20 ]:
537# p.VlanCreate(i)
538# p.VlanSetName(i, "test%d" % i)
539# print " %d (test%d)" % (i, i)
540
541 #print "And dump config\n"
542 #buf = p._show_config()
543 #print "%s" % buf
544
545 #print "Destroying VLAN 2\n"
546 #p.VlanDestroy(2)
547
548 #print "And dump config\n"
549 #buf = p._show_config()
550 #print "%s" % buf
551
552 #print "Port names are:"
553 #buf = p.SwitchGetPortNames()
554 #p._dump_list(buf)
555
556 #buf = p.VlanGetName(25)
557 #print "VLAN with tag 25 is called \"%s\"" % buf
558
559 #p.VlanSetName(35, "foo")
560 #print "VLAN with tag 35 is called \"foo\""
561
Steve McIntyreb7adc782014-08-13 00:22:21 +0100562 buf = p.PortGetMode("Gi1/0/10")
563 print "Port Gi1/0/10 is in %s mode" % buf
564
565 buf = p.PortGetMode("Gi1/0/11")
566 print "Port Gi1/0/11 is in %s mode" % buf
Steve McIntyred6759dd2014-08-12 18:10:00 +0100567
568 # Test general stuff
569# print "Set fa6 to general mode"
570# p.PortSetMode("fa6", "general")
571 #print "Remove fa6 from VLAN 1 (default)"
572 #p.PortRemoveGeneralFromVlan("fa6", 1)
573 #print "Move fa6 to VLAN 2"
574 #p.PortAddGeneraltoVlan("fa6", 2)
575# buf = p.PortGetGeneralVlan("fa6")
576# print "Read from switch: fa6 is on VLAN %s" % buf
577# print "Remove fa6 from VLAN 2"
578# p.PortRemoveGeneralFromVlan("fa6", 2)
579# print "And re-enable the default VLAN for fa6"
580# p.PortAddGeneraltoVlan("fa6", 1)
581 #print "And move fa6 back to a trunk port"
582 #p.PortSetMode("fa6", "trunk")
583 #buf = p.PortGetMode("fa6")
584 #print "Port fa6 is in %s mode" % buf
585
586 # Test trunk stuff
587# print "Set gi2 to trunk mode"
588# p.PortSetMode("gi2", "trunk")
589# print "Add gi2 to VLAN 2"
590# p.PortAddTrunkToVlan("gi2", 2)
591# print "Add gi2 to VLAN 3"
592# p.PortAddTrunkToVlan("gi2", 3)
593# print "Add gi2 to VLAN 4"
594# p.PortAddTrunkToVlan("gi2", 4)
595# print "Read from switch: which VLANs is gi2 on?"
596# buf = p.PortGetTrunkVlanList("gi2")
597# p._dump_list(buf)
598
599# p.PortRemoveTrunkFromVlan("gi2", 3)
600# p.PortRemoveTrunkFromVlan("gi2", 3)
601# p.PortRemoveTrunkFromVlan("gi2", 4)
602# print "Read from switch: which VLANs is gi2 on?"
603# buf = p.PortGetTrunkVlanList("gi2")
604# p._dump_list(buf)
605
606 # print "Adding lots of ports to VLANs"
607 # p.PortAddTrunkToVlan("fa1", 2)
608 # p.PortAddTrunkToVlan("fa3", 2)
609 # p.PortAddTrunkToVlan("fa5", 2)
610 # p.PortAddTrunkToVlan("fa7", 2)
611 # p.PortAddTrunkToVlan("fa9", 2)
612 # p.PortAddTrunkToVlan("fa11", 2)
613 # p.PortAddTrunkToVlan("fa13", 2)
614 # p.PortAddTrunkToVlan("fa15", 2)
615 # p.PortAddTrunkToVlan("fa17", 2)
616 # p.PortAddTrunkToVlan("fa19", 2)
617 # p.PortAddTrunkToVlan("fa21", 2)
618 # p.PortAddTrunkToVlan("fa23", 2)
619 # p.PortAddTrunkToVlan("gi4", 2)
620
621# p.SwitchSaveRunningConfig()
622
623# p.SwitchDisconnect()
624# p._show_config()
625