snmpit_cisco_stack.pm 16.3 KB
Newer Older
1
#!/usr/bin/perl -w
Leigh B. Stoller's avatar
Leigh B. Stoller committed
2
3
4

#
# EMULAB-COPYRIGHT
5
# Copyright (c) 2000-2003 University of Utah and the Flux Group.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
6
7
8
# All rights reserved.
#

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#
# snmpit module for a stack of Cisco Catalyst 6509 switches. The main purpose
# of this module is to contain knowledge of how to manage stack-wide operations
# (such as VLAN creation), and to coallate the results of listing operations
# on multiple switches
#

package snmpit_cisco_stack;
use strict;

$| = 1; # Turn off line buffering on output

use English;
use SNMP;
use snmpit_lib;

use libdb;

#
# Creates a new object. A list of devices that will be operated on is given
# so that the object knows which to connect to. A future version may not 
# require the device list, and dynamically connect to devices as appropriate
#
# For a Cisco stack, the stack_id happens to also be the name of the stack
# leader.
#
35
# usage: new(string name, string stack_id, int debuglevel, list of devicenames)
36
37
# returns a new object blessed into the snmpit_cisco_stack class
#
38
sub new($$$#@) {
39
40
41
42
43
44

    # The next two lines are some voodoo taken from perltoot(1)
    my $proto = shift;
    my $class = ref($proto) || $proto;

    my $stack_id = shift;
45
    my $debuglevel = shift;
46
    my $community = shift;
Robert Ricci's avatar
Robert Ricci committed
47
    my $supports_private = shift;
48
    my $uses_vtp = shift;
49
50
51
52
53
54
55
56
57
58
    my @devicenames = @_;

    #
    # Create the actual object
    #
    my $self = {};

    #
    # Set up some defaults
    #
59
60
61
62
63
    if (defined $debuglevel) {
	$self->{DEBUG} = $debuglevel;
    } else {
	$self->{DEBUG} = 0;
    }
64
65
66
67
68
69
70
71
72
73
74

    #
    # The stackid just happens to also be leader of the stack
    # 
    $self->{STACKID} = $stack_id;

    #
    # Store the list of devices we're supposed to operate on
    #
    @{$self->{DEVICENAMES}} = @devicenames;

75
76
77
78
79
    #
    # Whether or not this stack uses VTP to keep the VLANs synchronized
    #
    $self->{VTP} = $uses_vtp;

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
    #
    # Make a device-dependant object for each switch
    #
    foreach my $devicename (@devicenames) {
	print("Making device object for $devicename\n") if $self->{DEBUG};
	my $type = getDeviceType($devicename);
	my $device;

	#
	# Check to see if this is a duplicate
	#
	if (defined($self->{DEVICES}{$devicename})) {
	    warn "WARNING: Device $device was specified twice, skipping\n";
	    next;
	}

	#
	# We check the type for two reasons: We may have multiple types of
	# ciscos in the future, and for some sanity checking to make sure
	# we weren't given devicenames for devices that aren't ciscos
	#
	SWITCH: for ($type) {
102
103
	    (/cisco6509/ || /cisco4006/ || /cisco2980/ || /catalyst2950/)
		    && do {
104
		use snmpit_cisco;
105
		$device = new snmpit_cisco($devicename,$self->{DEBUG},$type,
Robert Ricci's avatar
Robert Ricci committed
106
			$community,$supports_private);
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
		if (!$device) {
		    die "Failed to create a device object for $devicename\n";
		} else {
		    $self->{DEVICES}{$devicename} = $device;
		    if ($devicename eq $self->{STACKID}) {
			$self->{LEADER} = $device;
		    }
		    last;
		}
	    };
	    die "Device $devicename is not of a known type, skipping\n";
	}

    }

    #
    # Check for the stack leader, and create it if it hasn't been so far
    #
    if (!$self->{LEADER}) {
	# XXX: For simplicity, we assume for now that the leader is a Cisco
Robert Ricci's avatar
Robert Ricci committed
127
	use snmpit_cisco;
128
129
130
	my $type = getDeviceType($self->{STACKID});
	$self->{LEADER} = new snmpit_cisco($self->{STACKID}, $self->{DEBUG},
	    $type, $community, $supports_private)
Robert Ricci's avatar
Robert Ricci committed
131
    }
132
133
134
135
136
137
138
139
140

    bless($self,$class);

    return $self;
}

#
# List all VLANs on all switches in the stack
#
141
# usage: listVlans(self)
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#
# returns: A list of VLAN information. Each entry is an array reference. The
#	array is in the form [id, ddep, members] where:
#		id is the VLAN identifier, as stored in the database
#		ddep is an opaque string that is device-dependant (mostly for
#			debugging purposes)
#		members is a reference to an array of VLAN members
#
sub listVlans($) {
    my $self = shift;

    #
    # We need to 'coallate' the results from each switch by putting together
    # the results from each switch, based on the VLAN identifier
    #
    my %vlans = ();
    foreach my $devicename (sort {tbsort($a,$b)} keys %{$self->{DEVICES}}) {
	my $device = $self->{DEVICES}{$devicename};
	foreach my $line ($device->listVlans()) {
	    my ($vlan_id, $vlan_number, $memberRef) = @$line;
162
163
	    ${$vlans{$vlan_id}}[0] = $vlan_number;
	    push @{${$vlans{$vlan_id}}[1]}, @$memberRef;
164
165
166
167
168
169
170
171
	}
    }

    #
    # Now, we put the information we've found in the format described by
    # the header comment for this function
    #
    my @vlanList;
Robert Ricci's avatar
Robert Ricci committed
172
    foreach my $vlan (sort {tbsort($a,$b)} keys %vlans) {
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
	push @vlanList, [$vlan, @{$vlans{$vlan}}];
    } 
    return @vlanList;
}

#
# List all ports on all switches in the stack
#
# usage: listPorts(self)
#
# returns: A list of port information. Each entry is an array reference. The
#	array is in the form [id, enabled, link, speed, duplex] where:
#		id is the port identifier (in node:port form)
#		enabled is "yes" if the port is enabled, "no" otherwise
#		link is "up" if the port has carrier, "down" otherwise
#		speed is in the form "XMbps"
#		duplex is "full" or "half"
#
sub listPorts($) {
    my $self = shift;

    #
    # All we really need to do here is coallate the results of listing the
    # ports on all devices
    #
    my %portinfo = (); 
    foreach my $devicename (sort {tbsort($a,$b)} keys %{$self->{DEVICES}}) {
	my $device = $self->{DEVICES}{$devicename};
	foreach my $line ($device->listPorts()) {
	    my $port = $$line[0];
	    if (defined $portinfo{$port}) {
		warn "WARNING: Two ports found for $port\n";
	    }
	    $portinfo{$port} = $line;
	}
    }

    return map $portinfo{$_}, sort {tbsort($a,$b)} keys %portinfo;
}

#
# Puts ports in the VLAN with the given identifier. Contacts the device
# appropriate for each port.
#
# usage: setPortVlan(self, vlan_id, list of ports)
# returns: the number of errors encountered in processing the request
#
sub setPortVlan($$@) {
    my $self = shift;
    my $vlan_id = shift;
    my @ports = @_;

    my $errors = 0;

227
228
229
230
231
232
233
234
235
    #
    # Grab the VLAN number
    #
    my $vlan_number = $self->{LEADER}->findVlan($vlan_id);
    if (!$vlan_number) {
	print STDERR "ERROR: VLAN with identifier $vlan_id does not exist!\n";
	return 1;
    }

236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
    #
    # Split up the ports among the devices involved
    #
    my %map = mapPortsToDevices(@ports);
    foreach my $devicename (keys %map) {
	my $device = $self->{DEVICES}{$devicename};
    	if (!defined($device)) {
    	    warn "Unable to find device entry for $devicename - some ports " .
	    	"will not be set up properly\n";
    	    $errors++;
    	    next;
    	}

	#
	# Simply make the appropriate call on the device
	#
252
	$errors += $device->setPortVlan($vlan_number,@{$map{$devicename}});
253
254
    }

255
    if ($vlan_id ne 'default') {
256
	$errors += (!$self->setVlanOnTrunks($vlan_number,1,@ports));
257
258
    }

259
260
261
262
    return $errors;
}

#
263
264
265
# Creates a VLAN with the given VLAN identifier on the stack. If ports are
# given, puts them into the newly created VLAN. It is an error to create a
# VLAN that already exists.
266
#
267
# usage: createVlan(self, vlan identfier, port list)
268
269
270
271
#
# returns: 1 on success
# returns: 0 on failure
#
Robert Ricci's avatar
Robert Ricci committed
272
sub createVlan($$$;$$$) {
273
274
    my $self = shift;
    my $vlan_id = shift;
Robert Ricci's avatar
Robert Ricci committed
275
276
    my @ports = @{shift()};
    my @otherargs = @_;
277
278

    #
279
280
    # What we do here depends on whether this stack uses VTP to synchronize
    # VLANs or not
281
    #
Robert Ricci's avatar
Robert Ricci committed
282
    my $okay = 1;
283
284
285
286
287
288
289
290
291
292
293
294
295
296
    if ($self->{VTP}) {
	#
	# We just need to create the VLAN on the stack leader
	#
	#
	my $vlan_number = $self->{LEADER}->createVlan($vlan_id,undef,@otherargs);
	$okay = ($vlan_number != 0);
    } else {
	#
	# We need to create the VLAN on all devices
	# XXX - should we do the leader first?
	#
	my $vlan_number = undef;
	foreach my $devicename (sort {tbsort($a,$b)} keys %{$self->{DEVICES}}) {
Robert Ricci's avatar
Robert Ricci committed
297
	    print "Creating VLAN on switch $devicename ... \n" if $self->{DEBUG};
298
	    my $device = $self->{DEVICES}{$devicename};
Robert Ricci's avatar
Robert Ricci committed
299
	    my $res = $device->createVlan($vlan_id,$vlan_number,@otherargs);
300
301
302
303
304
305
306
307
308
309
310
311
312
313
	    if (!$res) {
		#
		# Ooops, failed. Don't try any more
		#
		$okay = 0;
		last;
	    } else {
		#
		# Use the VLAN number we just got back for the other switches
		#
		$vlan_number = $res;
	    }
	}
    }
314

315
316
317
318
319
320
321
322
323
    #
    # We need to add the ports to VLANs at the stack level, since they are
    # not necessarily on the leader
    #
    if ($okay && @ports) {
	if ($self->setPortVlan($vlan_id,@ports)) {
	    $okay = 0;
	}
    }
324
325

    return $okay;
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
}

#
# Adds devices to an existing VLAN, in preparation for adding ports on these
# devices. It is an error to call this function on a VLAN that does not
# exist. It is _not_ an error to add a VLAN to a device on which it already
# exists.
#
# usage: addDevicesToVlan(self, vlan identfier, device list)
#
# returns: the number of errors encountered in processing the request
#
sub addDevicesToVlan($$@) {
    my $self = shift;
    my $vlan_id = shift;
    my @devicenames = @_; # Note: This is not used for Cisco switches

    #
    # This function is not needed on Cisco stacks, since all switches
    # share the same set of VLANs. We will, however, check to make sure
    # the VLAN really does exist
    #
    if (!$self->vlanExists($vlan_id)) {
	return 1;
    }

    return 0;
}

#
# Check to see if the given VLAN exists in the stack
#
# usage: vlanExists(self, vlan identifier)
#
# returns 1 if the VLAN exists
#         0 otherwise
#
sub vlanExists($$) {
    my $self = shift;
    my $vlan_id = shift;

    #
    # The leader holds the list of which VLANs exist
    #
370
    if ($self->{LEADER}->findVlan($vlan_id,1)) {
371
372
373
374
375
376
377
	return 1;
    } else {
	return 0;
    }

}

378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
#
# Return a list of which VLANs from the input set exist on this stack
#
# usage: existantVlans(self, vlan identifiers)
#
# returns: a list containing the VLANs from the input set that exist on this
# 	stack
#
sub existantVlans($@) {
    my $self = shift;
    my @vlan_ids = @_;

    #
    # The leader holds the list of which VLANs exist
    #
    my %mapping = $self->{LEADER}->findVlans(@vlan_ids);

    my @existant = ();
    foreach my $vlan_id (@vlan_ids) {
	if (defined $mapping{$vlan_id}) {
	    push @existant, $vlan_id;
	}
    }

    return @existant;

}

406
407
408
409
#
# Removes a VLAN from the stack. This implicitly removes all ports from the
# VLAN. It is an error to remove a VLAN that does not exist.
#
410
# usage: removeVlan(self, vlan identifiers)
411
412
413
414
#
# returns: 1 on success
# returns: 0 on failure
#
415
sub removeVlan($@) {
416
    my $self = shift;
417
    my @vlan_ids = @_;
418
419
    my $errors = 0;

420
421
422
423
424
425
426
    #
    # Exit early if no VLANs given
    #
    if (!@vlan_ids) {
	return 1;
    }

427
    my %vlan_numbers = $self->{LEADER}->findVlans(@vlan_ids);
428
429
430
431
    foreach my $vlan_id (@vlan_ids) {
	#
	# First, make sure that the VLAN really does exist
	#
432
	my $vlan_number = $vlan_numbers{$vlan_id};
433
434
435
436
	if (!$vlan_number) {
	    warn "ERROR: VLAN $vlan_id not found on switch!";
	    return 0;
	}
437

438
	#
439
	# Prevent the VLAN from being sent across trunks.
440
	#
441
442
443
444
445
446
447
	if (!$self->setVlanOnTrunks($vlan_number,0)) {
	    warn "ERROR: Unable to set up VLANs on trunks!\n";
	    #
	    # We can keep going, 'cause we can still remove the VLAN
	    #
	}
	
448
449
    }

450
451
    #
    # Now, we go through each device and remove all ports from the VLAN
452
453
454
455
456
    # on that device. Note the reverse sort order! This way, we do not
    # interfere with another snmpit processes, since createVlan tries
    # in 'forward' order (we will remove the VLAN from the 'last' switch
    # first, so the other snmpit will not see it free until it's been
    # removed from all switches)
457
    #
458
    foreach my $devicename (sort {tbsort($b,$a)} keys %{$self->{DEVICES}}) {
459
	my $device = $self->{DEVICES}{$devicename};
460
	my @existant_vlans = ();
461
	my %vlan_numbers = $device->findVlans(@vlan_ids);
462
463
464
465
466
467
	foreach my $vlan_id (@vlan_ids) {

	    #
	    # Only remove ports from the VLAN if it exists on this
	    # device. Do it in one pass for efficiency
	    #
468
	    if (defined $vlan_numbers{$vlan_id}) {
469
		push @existant_vlans, $vlan_numbers{$vlan_id};
470
	    }
471
	}
472

473
474
475
	print "Removing ports on $devicename from VLANS " . 
	    join(",",@existant_vlans)."\n" if $self->{DEBUG};

476
	$errors += $device->removePortsFromVlan(@existant_vlans);
477
478
479
480
481
482
483
484
485
486

	#
	# If this stack doesn't use VTP, delete the VLAN, too, while
	# we're at it. If it does, we remove all VLANs down below (we can't
	# do it until the ports have been cleared from all switches.)
	#
	if (!$self->{VTP}) {
	    my $ok = $device->removeVlan(@existant_vlans);
	    if (!$ok) { $errors++; }
	}
487
488
    }

489
    if ($self->{VTP}) {
490
	#
491
492
	# For efficiency, we remove all VLANs from the leader in one function
	# call. This can save a _lot_ of locking and unlocking.
493
	#
494
495
496
497
498
499
500
501
502
503
	if (!$errors) {
	    #
	    # Make a list of all the VLANs that really did exist
	    #
	    my @vlan_numbers;
	    my ($key, $value);
	    while (($key, $value) = each %vlan_numbers) {
		if ($value) {
		    push @vlan_numbers, $value;
		}
504
505
	    }

506
507
508
509
	    my $ok = $self->{LEADER}->removeVlan(@vlan_numbers);
	    if (!$ok) {
		$errors++;
	    }
510
511
	}
    }
512

513
    return ($errors == 0);
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
}

#
# Set a variable associated with a port. 
# TODO: Need a list of variables here
#
sub portControl ($$@) { 
    my $self = shift;
    my $cmd = shift;
    my @ports = @_;
    my %portDeviceMap = mapPortsToDevices(@ports);
    my $errors = 0;
    # XXX: each
    while (my ($devicename,$ports) = each %portDeviceMap) {
	$errors += $self->{DEVICES}{$devicename}->portControl($cmd,@$ports);
    }
    return $errors;
}

#
534
# Get port statistics for all devices in the stack
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
#
sub getStats($) {
    my $self = shift;

    #
    # All we really need to do here is coallate the results of listing the
    # ports on all devices
    #
    my %stats = (); 
    foreach my $devicename (sort {tbsort($a,$b)} keys %{$self->{DEVICES}}) {
	my $device = $self->{DEVICES}{$devicename};
	foreach my $line ($device->getStats()) {
	    my $port = $$line[0];
	    if (defined $stats{$port}) {
		warn "WARNING: Two ports found for $port\n";
	    }
	    $stats{$port} = $line;
	}
    }
    return map $stats{$_}, sort {tbsort($a,$b)} keys %stats;
}

557
558
559
560
561
#
# Not a 'public' function - only needs to get called by other functions in
# this file, not external functions.
#
# Enables or disables (depending on $value) a VLAN on all appropriate
562
# switches in a stack. Returns 1 on sucess, 0 on failure.
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
#
# ONLY pass in @ports if you're SURE that they are the only ports in the
# VLAN - basically, only if you just created it. This is a shortcut, so
# that we don't have to ask all switches if they have any ports in the VLAN.
#
sub setVlanOnTrunks($$$;@) {
    my $self = shift;
    my $vlan_number = shift;
    my $value = shift;
    my @ports = @_;

    #
    # First, get a list of all trunks
    #
    my %trunks = getTrunks();

    #
    # Next, figure out which switches this VLAN exists on
    #
    my @switches;
    if (@ports) {
	#
	# I'd rather not have to go out to the switches to ask which ones
	# have ports in the VLAN. So, if they gave me ports, I'll just 
	# trust that those are the only ones in the VLAN
	#
	@switches = getDeviceNames(@ports);
    } else {
	#
	# Since this may be a hand-created (not from the database) VLAN, the
	# only way we can figure out which swtiches this VLAN spans is to
	# ask them all.
	#
	foreach my $devicename (keys %{$self->{DEVICES}}) {
	    my $device = $self->{DEVICES}{$devicename};
	    foreach my $line ($device->listVlans()) {
		my ($vlan_id, $vlan, $memberRef) = @$line;
600
		if (($vlan == $vlan_number)){
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
		    push @switches, $devicename;
		}
	    }
	}
    }

    #
    # Next, get a list of the trunks that are used to move between these
    # switches
    #
    my @trunks = getTrunksFromSwitches(\%trunks,@switches);

    #
    # Now, we go through the list of trunks that need to be modifed, and
    # do it! We have to modify both ends of the trunk, or we'll end up wasting
    # the trunk bandwidth.
    #
    my $errors = 0;
    foreach my $trunk (@trunks) {
	my ($src,$dst) = @$trunk;
	if (!$self->{DEVICES}{$src}) {
	    warn "ERROR - Bad device $src found in setVlanOnTrunks!\n";
	    $errors++;
	} else {
	    #
	    # On ciscos, we can use any port in the trunk, so we'll use the
	    # first
	    #
	    my $modport = $trunks{$src}{$dst}[0];
630
631
	    $errors += !($self->{DEVICES}{$src}->setVlansOnTrunk($modport,
		    $value,$vlan_number));
632
633
634
635
636
637
638
639
640
641
	}
	if (!$self->{DEVICES}{$dst}) {
	    warn "ERROR - Bad device $dst found in setVlanOnTrunks!\n";
	    $errors++;
	} else {
	    #
	    # On ciscos, we can use any port in the trunk, so we'll use the
	    # first
	    #
	    my $modport = $trunks{$dst}{$src}[0];
642
	    $errors += !($self->{DEVICES}{$dst}->setVlansOnTrunk($modport,
643
		    $value,$vlan_number));
644
645
646
	}
    }

647
    return (!$errors);
648
649
}

650
651
# End with true
1;