blob: 3230b3f07e84adb34cb299170d4388f9586a7f4e [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")
101 self.connection.expect("Y/N")
102 self._cli("y")
103 self.connection.expect("Copy succeeded")
104
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
184 ################################
185
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)
259 self._cli("switchport general pvid %d" % tag)
260 self._cli("switchport general allowed vlan add %d untagged" % tag)
261 self._end_configure()
262
263 # Validate it happened
264 read_vlan = self.PortGetGeneralVlan(port)
265 if read_vlan != tag:
266 raise IOError("Failed to add general port %d to VLAN %d - got VLAN %d"
267 % (port, tag, read_vlan))
268
269 # Remove a general port from a specified VLAN (tag)
270 def PortRemoveGeneralFromVlan(self, port, tag):
271 logging.debug("Removing general 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) == "general"):
275 raise IndexError("Port %s not in general mode" % port)
276
277 # Special handling for VLAN 1 (default)
278 if tag == 1:
279 return self.PortBlockDefaultVlan(port)
280 else:
281 self._configure()
282 self._cli("interface %s" % port)
283 self._cli("no switchport general pvid")
284 self._cli("switchport general allowed vlan remove %d" % tag)
285 self._end_configure()
286
287 # Validate it happened
288 read_vlan = self.PortGetGeneralVlan(port)
289 if read_vlan == tag:
290 raise IOError("Failed to remove general port %d from VLAN %d"
291 % (port, tag))
292
293 # Add a trunk port to a specified VLAN (tag)
294 def PortAddTrunkToVlan(self, port, tag):
295 logging.debug("Adding trunk port %s to VLAN %d" % (port, tag))
296 if not self._is_port_name_valid(port):
297 raise IndexError("Port name %s not recognised" % port)
298 if not (self.PortGetMode(port) == "trunk"):
299 raise IndexError("Port %s not in trunk mode" % port)
300 self._configure()
301 self._cli("interface %s" % port)
302 self._cli("switchport trunk allowed vlan add %d" % tag)
303 self._end_configure()
304
305 # Validate it happened
306 read_vlans = self.PortGetTrunkVlanList(port)
307 for vlan in read_vlans:
308 if vlan == tag:
309 return
310 raise IOError("Failed to add trunk port %d to VLAN %d" % (port, tag))
311
312 # Remove a trunk port from a specified VLAN (tag)
313 def PortRemoveTrunkFromVlan(self, port, tag):
314 logging.debug("Removing trunk port %s from VLAN %d" % (port, tag))
315 if not self._is_port_name_valid(port):
316 raise IndexError("Port name %s not recognised" % port)
317 if not (self.PortGetMode(port) == "trunk"):
318 raise IndexError("Port %s not in trunk mode" % port)
319 self._configure()
320 self._cli("interface %s" % port)
321 self._cli("switchport trunk allowed vlan remove %d" % tag)
322 self._end_configure()
323
324 # Validate it happened
325 read_vlans = self.PortGetTrunkVlanList(port)
326 for vlan in read_vlans:
327 if vlan == tag:
328 raise IOError("Failed to remove trunk port %d from VLAN %d" % (port, tag))
329
330 # Get the configured VLAN tag for an general port (tag)
331 def PortGetGeneralVlan(self, port):
332 logging.debug("Getting VLAN for general port %s" % port)
333 vlan = 1
334 if not self._is_port_name_valid(port):
335 raise IndexError("Port name %s not recognised" % port)
336 if not (self.PortGetMode(port) == "general"):
337 raise IndexError("Port %s not in general mode" % port)
338 regex = re.compile('(\d+)\s+\S+\s+Untagged\s+Static')
339 self._cli("show interfaces switchport %s" % port)
340 for line in self._read_paged_output():
341 match = regex.match(line)
342 if match:
343 vlan = match.group(1)
344 return int(vlan)
345
346 # Get the list of configured VLAN tags for a trunk port
347 def PortGetTrunkVlanList(self, port):
348 logging.debug("Getting VLANs for trunk port %s" % port)
349 vlans = [ ]
350 if not self._is_port_name_valid(port):
351 raise IndexError("Port name %s not recognised" % port)
352 if not (self.PortGetMode(port) == "trunk"):
353 raise IndexError("Port %s not in general mode" % port)
354 regex = re.compile('(\d+)\s+\S+\s+(Tagged|Untagged)\s+Static')
355 self._cli("show interfaces switchport %s" % port)
356 for line in self._read_paged_output():
357 match = regex.match(line)
358 if match:
359 vlans.append (int(match.group(1)))
360 return vlans
361
362 ################################
363 ### Internal functions
364 ################################
365
366 def _login(self, username, password, enablepassword):
367 logging.debug("attempting login with username %s, password %s" % (username, password))
368 self.connection.expect('User Access Verification')
369 if username is not None:
370 self.connection.expect("User Name:")
371 self._cli("%s" % username)
372 if password is not None:
373 self.connection.expect("Password:")
374 self._cli("%s" % password, False)
375 while True:
376 index = self.connection.expect(['User Name:', 'Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)'])
377 if index != 4: # Any other means: failed to log in!
378 logging.error("Login failure: index %d\n" % index)
379 logging.error("Login failure: %s\n" % self.connection.match.before)
380 raise IOError
381
382 # else
383 self.prompt_name = self.connection.match.group(1).strip()
384 if self.connection.match.group(2) == ">":
385 # Need to enter "enable" mode too
386 self._cli("enable")
387 if enablepassword is not None:
388 self.connection.expect("Password:")
389 self._cli("%s" % enablepassword, False)
390 index = self.connection.expect(['Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)'])
391 if index != 3: # Any other means: failed to log in!
392 logging.error("Enable password failure: %s\n" % self.connection.match)
393 raise IOError
394 return 0
395
396 def _logout(self):
397 logging.debug("Logging out")
398 self._cli("exit", False)
399
400 def _configure(self):
401 self._cli("configure terminal")
402
403 def _end_configure(self):
404 self._cli("end")
405
406 def _read_paged_output(self):
407 buf = []
408 prompt = self.prompt_name + '#'
409 while True:
410 index = self.connection.expect([' -*More-*', prompt])
411 if index == 0: # More: <space>
412 for line in self.connection.before.split('\r\n'):
413 line1 = re.sub('(\x08|\x0D)*', '', line.strip())
414 buf.append(line1)
415 self._cli(' ', False)
416 elif index == 1: # Back to a prompt, says output is finished
417 break
418
419 for line in self.connection.before.split('\r\n'):
420 line1 = re.sub('(\x08|\x0D)*', '', line.strip())
421 buf.append(line1)
422
423 return buf
424
425 def _get_port_names(self):
426 logging.debug("Grabbing list of ports")
427 interfaces = []
428
429 # Use "Up" or "Down" to only identify lines in the output that
430 # match interfaces that exist
431 regex = re.compile('^\s*([a-zA-Z0-9_/]*).*(connect)(.*)')
432 regex1 = re.compile('.*Not Present.*')
433
434 self._cli("show interfaces status")
435 for line in self._read_paged_output():
436 match = regex.match(line)
437 if match:
438 interface = match.group(1)
439 junk = match.group(3)
440 match1 = regex1.match(junk) # Deliberately drop things
441 # marked as "Not Present"
442 if not match1:
443 interfaces.append(interface)
444 return interfaces
445
446 def _is_port_name_valid(self, name):
447 logging.debug("Checking if supplied port name \"%s\" is valid" % name)
448 for port in self.ports:
449 if name == port:
450 return True
451 return False
452
453 def _is_port_mode_valid(self, mode):
454 logging.debug("Checking if supplied port mode \"%s\" is valid" % mode)
455 for allowed in self.allowed_port_modes:
456 if allowed == mode:
457 return True
458 return False
459
460 def _show_config(self):
461 logging.debug("Grabbing config")
462 self._cli("show running-config")
463 return self._read_paged_output()
464
465 def _show_clock(self):
466 logging.debug("Grabbing time")
467 self._cli("show clock")
468 return self._read_paged_output()
469
470 def _show_clock(self):
471 logging.debug("Grabbing ")
472 self._cli("show clock")
473 return self._read_paged_output()
474
475 def _get_systemdata(self):
476 data = []
477
478 logging.debug("Grabbing system sw and hw versions")
479 self._cli("show version")
480 for line in self._read_paged_output():
481 data.append(line)
482
483 return data
484
485 # Wrapper around connection.send - by default, expect() the same
486 # text we've sent, to remove it from the output from the
487 # switch. For the few cases where we don't need that, override
488 # this using echo=False.
489 # Horrible, but seems to work.
490 def _cli(self, text, echo=True):
491 self.connection.send(text + '\r')
492 if echo:
493 self.connection.expect(text)
494
495if __name__ == "__main__":
496 p = CiscoCatalyst('lngswitch02', 23)
497 p.SwitchConnect(None, 'lngvirtual', 'lngenable')
498
499 print "VLANs are:"
500 buf = p.VlanGetList()
501 p._dump_list(buf)
502
503 buf = p.VlanGetName(2)
504 print "VLAN 2 is named \"%s\"" % buf
505
506 print "Create VLAN 3"
507 p.VlanCreate(3)
508
509 buf = p.VlanGetName(3)
510 print "VLAN 3 is named \"%s\"" % buf
511
512 print "Set name of VLAN 3 to test333"
513 p.VlanSetName(3, "test333")
514
515 buf = p.VlanGetName(3)
516 print "VLAN 3 is named \"%s\"" % buf
517
518 print "VLANs are:"
519 buf = p.VlanGetList()
520 p._dump_list(buf)
521
522 print "Destroy VLAN 3"
523 p.VlanDestroy(3)
524
525 print "VLANs are:"
526 buf = p.VlanGetList()
527 p._dump_list(buf)
528
529 #print "List ports"
530 #buf = p.SwitchGetPortNames()
531 #p._dump_list(buf)
532
533 #print "System data:"
534 #p._dump_list(p.systemdata)
535
536# print "Creating VLANs for testing:"
537# for i in [ 2, 3, 4, 5, 20 ]:
538# p.VlanCreate(i)
539# p.VlanSetName(i, "test%d" % i)
540# print " %d (test%d)" % (i, i)
541
542 #print "And dump config\n"
543 #buf = p._show_config()
544 #print "%s" % buf
545
546 #print "Destroying VLAN 2\n"
547 #p.VlanDestroy(2)
548
549 #print "And dump config\n"
550 #buf = p._show_config()
551 #print "%s" % buf
552
553 #print "Port names are:"
554 #buf = p.SwitchGetPortNames()
555 #p._dump_list(buf)
556
557 #buf = p.VlanGetName(25)
558 #print "VLAN with tag 25 is called \"%s\"" % buf
559
560 #p.VlanSetName(35, "foo")
561 #print "VLAN with tag 35 is called \"foo\""
562
Steve McIntyreb7adc782014-08-13 00:22:21 +0100563 buf = p.PortGetMode("Gi1/0/10")
564 print "Port Gi1/0/10 is in %s mode" % buf
565
566 buf = p.PortGetMode("Gi1/0/11")
567 print "Port Gi1/0/11 is in %s mode" % buf
Steve McIntyred6759dd2014-08-12 18:10:00 +0100568
569 # Test general stuff
570# print "Set fa6 to general mode"
571# p.PortSetMode("fa6", "general")
572 #print "Remove fa6 from VLAN 1 (default)"
573 #p.PortRemoveGeneralFromVlan("fa6", 1)
574 #print "Move fa6 to VLAN 2"
575 #p.PortAddGeneraltoVlan("fa6", 2)
576# buf = p.PortGetGeneralVlan("fa6")
577# print "Read from switch: fa6 is on VLAN %s" % buf
578# print "Remove fa6 from VLAN 2"
579# p.PortRemoveGeneralFromVlan("fa6", 2)
580# print "And re-enable the default VLAN for fa6"
581# p.PortAddGeneraltoVlan("fa6", 1)
582 #print "And move fa6 back to a trunk port"
583 #p.PortSetMode("fa6", "trunk")
584 #buf = p.PortGetMode("fa6")
585 #print "Port fa6 is in %s mode" % buf
586
587 # Test trunk stuff
588# print "Set gi2 to trunk mode"
589# p.PortSetMode("gi2", "trunk")
590# print "Add gi2 to VLAN 2"
591# p.PortAddTrunkToVlan("gi2", 2)
592# print "Add gi2 to VLAN 3"
593# p.PortAddTrunkToVlan("gi2", 3)
594# print "Add gi2 to VLAN 4"
595# p.PortAddTrunkToVlan("gi2", 4)
596# print "Read from switch: which VLANs is gi2 on?"
597# buf = p.PortGetTrunkVlanList("gi2")
598# p._dump_list(buf)
599
600# p.PortRemoveTrunkFromVlan("gi2", 3)
601# p.PortRemoveTrunkFromVlan("gi2", 3)
602# p.PortRemoveTrunkFromVlan("gi2", 4)
603# print "Read from switch: which VLANs is gi2 on?"
604# buf = p.PortGetTrunkVlanList("gi2")
605# p._dump_list(buf)
606
607 # print "Adding lots of ports to VLANs"
608 # p.PortAddTrunkToVlan("fa1", 2)
609 # p.PortAddTrunkToVlan("fa3", 2)
610 # p.PortAddTrunkToVlan("fa5", 2)
611 # p.PortAddTrunkToVlan("fa7", 2)
612 # p.PortAddTrunkToVlan("fa9", 2)
613 # p.PortAddTrunkToVlan("fa11", 2)
614 # p.PortAddTrunkToVlan("fa13", 2)
615 # p.PortAddTrunkToVlan("fa15", 2)
616 # p.PortAddTrunkToVlan("fa17", 2)
617 # p.PortAddTrunkToVlan("fa19", 2)
618 # p.PortAddTrunkToVlan("fa21", 2)
619 # p.PortAddTrunkToVlan("fa23", 2)
620 # p.PortAddTrunkToVlan("gi4", 2)
621
622# p.SwitchSaveRunningConfig()
623
624# p.SwitchDisconnect()
625# p._show_config()
626