snmpit_cisco_stack.pm 12.4 KB
Newer Older
1
#!/usr/bin/perl -w
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
#
# 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.
#
28
# usage: new(string name, string stack_id, int debuglevel, list of devicenames)
29 30
# returns a new object blessed into the snmpit_cisco_stack class
#
31
sub new($$#@) {
32 33 34 35 36 37

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

    my $stack_id = shift;
38
    my $debuglevel = shift;
39 40 41 42 43 44 45 46 47 48
    my @devicenames = @_;

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

    #
    # Set up some defaults
    #
49 50 51 52 53
    if (defined $debuglevel) {
	$self->{DEBUG} = $debuglevel;
    } else {
	$self->{DEBUG} = 0;
    }
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109

    #
    # 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;

    #
    # 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) {
	    /cisco6509/ && do {
		use snmpit_cisco;
		$device = new snmpit_cisco($devicename,$self->{DEBUG});
		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
110 111 112
	use snmpit_cisco;
	$self->{LEADER} = new snmpit_cisco($self->{STACKID});
    }
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154

    bless($self,$class);

    return $self;
}

#
# List all VLANs on all switches in the stack
#
# usage: vlistVlans(self)
#
# 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;
	    if ($memberRef) {
		${$vlans{$vlan_id}}[0] = $vlan_number;
		push @{${$vlans{$vlan_id}}[1]}, @$memberRef;
	    }
	}
    }

    #
    # 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
155
    foreach my $vlan (sort {tbsort($a,$b)} keys %vlans) {
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 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 227 228 229 230 231 232
	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;

    #
    # 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
	#
	$errors += $device->setPortVlan($vlan_id,@{$map{$devicename}});
    }

    return $errors;
}

#
233 234 235
# 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.
236
#
237
# usage: createVlan(self, vlan identfier, port list)
238 239 240 241
#
# returns: 1 on success
# returns: 0 on failure
#
242
sub createVlan($$;@) {
243 244
    my $self = shift;
    my $vlan_id = shift;
245
    my @ports = @_;
246 247 248 249

    #
    # We just need to create the VLAN on the stack leader
    #
250
    my $okay = $self->{LEADER}->createVlan($vlan_id);
251

252 253 254 255 256 257 258 259 260
    #
    # 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;
	}
    }
261 262

    return $okay;
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
}

#
# 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
    #
307
    if ($self->{LEADER}->findVlan($vlan_id,1)) {
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 370 371 372 373 374 375 376
	return 1;
    } else {
	return 0;
    }

}

#
# 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.
#
# usage: removeVlan(self, vlan identifier)
#
# returns: 1 on success
# returns: 0 on failure
#
sub removeVlan($$) {
    my $self = shift;
    my $vlan_id = shift;
    my $errors = 0;

    #
    # First, make sure that the VLAN really does exist
    #
    my $vlan_number = $self->{LEADER}->findVlan($vlan_id);
    if (!$vlan_number) {
	warn "ERROR: VLAN $vlan_id not found on switch!";
	return 0;
    }

    #
    # Now, we go through each device and remove all ports from the VLAN
    # on that device
    #
    foreach my $devicename (sort {tbsort($a,$b)} keys %{$self->{DEVICES}}) {
	my $device = $self->{DEVICES}{$devicename};

	#
	# Only remove ports from the VLAN if it exists on this
	# device
	#
	if (defined(my $number = $device->findVlan($vlan_id))) {
		$errors += $device->removePortsFromVlan($vlan_id);
	}
    }

    my $ok = $self->{LEADER}->removeVlan($vlan_id);

    return ($ok && ($errors == 0));
}

#
# 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;
}

#
377
# Get port statistics for all devices in the stack
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
#
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;
}

400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
#
# 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
# switches in a stack
#
# 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;
		if (($vlan == $vlan_number) && $memberRef && @$memberRef){
		    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];
	    $errors += !($self->{DEVICES}{$src}->setVlanOnTrunk($vlan_number,
		$modport,$value));
	}
	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];
	    $errors += !($self->{DEVICES}{$dst}->setVlanOnTrunk($vlan_number,
		$modport,$value));
	}
    }

    return $errors;
}

493 494
# End with true
1;