portstats.in 12 KB
Newer Older
1
#!/usr/bin/perl -T
Leigh B. Stoller's avatar
Leigh B. Stoller committed
2
3
4

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

9
#
10
# portstats - Get port statistics for nodes in an experiment
11
#
12
13
14
15
16
#
# NOTE: no -w, because $::line below is used in the eval, which perl
# can't pick up on, so it warns about this variable being only used once
#

Leigh B Stoller's avatar
Leigh B Stoller committed
17
18
19
20
21
22
# un-taint path
$ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

# Turn off line buffering on output
$| = 1;
23
24
25
26

#
# Configure variables
#
27
28
my $ELABINELAB = @ELABINELAB@;

29
30
31
32
use lib '@prefix@/lib';

use libdb;
use snmpit_lib;
Kevin Atkinson's avatar
Kevin Atkinson committed
33
use libtblog;
34
35
36
37
38
39

use English;
use Getopt::Long;
use strict;

sub usage {
40
    print << "END";
41
Usage: $0 [-h] <-p | <pid> <eid> > [vname ...] [vname:port ...]
42
43
44
-h    This message
-e    Show only error counters
-a    Show all stats
45
46
47
-z    Zero out counts for selected counters, after printing
-q    Quiet: don't actually print counts - useful with -z
-c    Print absolute, rather than relative, counts
48
49
-p    The machines given are physical, not virtual, node IDs. No pid and
      eid should be given when using this option.
50
-s    Ports are specified in switch.port syntax
51
-C    List control net, rather than experimental net, ports
52
53
54
55
56
57
58
59
60

If only pid and eid are given, prints out information about all ports in the
experiment. Otherwise, output is limited to the nodes and/or ports given.

NOTE: Statistics are reported from the switch's perspective. This means that
'In' packets are those sent FROM the node, and 'Out' packets are those
sent TO the node.

In the output, packets described as 'NUnicast' or 'NUcast' are non-unicast
61
(broadcast or multicast) packets.'
62
63
END

64
65
66
    return 1;
}

67
68
69
70
#
# Process command-line arguments
#
my %opt = ();
71
72
Getopt::Long::Configure("no_ignore_case");
GetOptions(\%opt,'h','a','e','p','b','z','q','c','s','C');
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

if ($opt{h}) {
    exit &usage;
}

my @oids = (); # The set of OIDs we care about

if ($opt{a}) {
    @oids = ('ifInOctets', 'ifInUcastPkts', 'ifInNUcastPkts', 'ifInDiscards',
	     'ifInErrors', 'ifInUnknownProtos', 'ifOutOctets', 'ifOutUcastPkts',
	     'ifOutNUcastPkts', 'ifOutDiscards', 'ifOutErrors', 'ifOutQLen');
} elsif ($opt{e}) {
    @oids = ('ifInDiscards', 'ifInErrors', 'ifInUnknownProtos', 'ifOutDiscards',
	     'ifOutErrors');
} else {
    @oids = ('ifInOctets', 'ifInUcastPkts', 'ifInNUcastPkts',
	     'ifOutOctets', 'ifOutUcastPkts', 'ifOutNUcastPkts');
}

Mike Hibler's avatar
Mike Hibler committed
92
93
94
95
96
97
98
99
#
# Warn about OIDs that do not return numeric values.
# Most likely these are ones that are unimplemented.
# It will only warn once per device per OID, but even that might get tedious
# so warnings can be turned off entirely here.
#
my $badoidwarnings = 1;

100
my ($pid, $eid);
101
if (!$opt{p} && !$opt{s}) {
102
103
104
105
    if (@ARGV < 2) {
	exit &usage;
    }
    ($pid,$eid) = (shift,shift);
106
107
108
109
110

    #
    # Untaint args.
    #
    if ($pid =~ /^([-\w]+)$/) {
111
	$pid = $1;
112
113
    }
    else {
Kevin Atkinson's avatar
   
Kevin Atkinson committed
114
        tbdie("Bad data in pid: $pid.");
115
116
117
118
119
    }
    if ($eid =~ /^([-\w]+)$/) {
        $eid = $1;
    }
    else {
Kevin Atkinson's avatar
   
Kevin Atkinson committed
120
        tbdie("Bad data in eid: $eid.");
121
    }
122
123
}

124
125
126
127
128
129
#
# Scan the rest of the arguments, doing a generic taint check. More
# specific patterns are below.
# 
my @passedPorts = ();
foreach my $arg (@ARGV) {
130
131
132
133
134
135
136
137
138
139
140
141
142
143
    if ($opt{s}) {
        if ($arg =~ /^([-\w\.\/]+)$/) {
            $arg = $1;
        }
        else {
            tbdie("Bad data in arg: $arg.");
        }
    } else {
        if ($arg =~ /^([-\w\.:]+)$/) {
            $arg = $1;
        }
        else {
            tbdie("Bad data in arg: $arg.");
        }
144
145
146
    }
    push(@passedPorts, $arg);
}
147

148
149
150
151
152
153
154
155
156
157
158
#
# This hash is used to create colmn headers and the format string
#
my %oids = (
    'ifInOctets'        => [['        In','    Octets'], '@>>>>>>>>>'],
    'ifInUcastPkts'     => [[' InUnicast','   Packets'], '@>>>>>>>>>'],
    'ifInNUcastPkts'    => [['InNUnicast','   Packets'], '@>>>>>>>>>'],
    'ifInDiscards'      => [['        In','  Discards'], '@>>>>>>>>>'],
    'ifInErrors'        => [['        In','    Errors'], '@>>>>>>>>>'],
    'ifInUnknownProtos' => [['   Unknown','  Protocol'], '@>>>>>>>>>'],
    'ifOutOctets'       => [['       Out','    Octets'], '@>>>>>>>>>'],
159
    'ifOutUcastPkts'    => [['OutUnicast','   Packets'], '@>>>>>>>>>'],
160
161
162
163
164
    'ifOutNUcastPkts'   => [[' OutNUcast','   Packets'], '@>>>>>>>>>'],
    'ifOutDiscards'     => [['       Out','  Discards'], '@>>>>>>>>>'],
    'ifOutErrors'       => [['       Out','    Errors'], '@>>>>>>>>>'],
    'ifOutQLen'         => [['  OutQueue','    Length'], '@>>>>>>>>>']
);
165
166
167
168

#
# First, make sure the experiment exists
#
169
if (!$opt{p} && !$opt{s}) {
170
171
172
    if (!ExpState($pid,$eid)) {
	die "There is no experiment $eid in project $pid\n";
    }
173
174
175
176
177
}

#
# Make sure they have access to it
#
178
179
180
181
182
if ($opt{s}) {
    if ($UID && !TBAdmin($UID)) {
        die("Only root or TB admins can use -s.");
    }
} elsif ($opt{p}) {
183
    my @nodes = map { /^([^:]+)(:(\d+))?$/; $1; } @passedPorts;
184
    if (!TBNodeAccessCheck($UID,TB_NODEACCESS_READINFO,@nodes)) {
185
	die "You do not have permission to view one or more of @nodes\n";
186
187
188
189
190
    }
} else {
    if (!TBExptAccessCheck($UID,$pid,$eid,TB_EXPT_READINFO)) {
	die "You do not have permission to view experiment $pid/$eid\n";
    }
191
192
}

193
194
195
196
197
198
199
200
#
# Eventually, we can pass this out via XMLRPC. For now just exit.
# 
if ($ELABINELAB) {
    print "Not bothering with portstats inside an elabinelab.\n";
    exit(0);
}

201
202
snmpit_lib::init(0);

203
204
205
206
207
208
209
210
211
212
213
214
215
216
my @ports;
#
# If using "switch" syntax, make sure all names are in the correct format
#
if ($opt{s}) {
    foreach my $port (@passedPorts) {
        if ($port =~ /^[^.]+\.\d+\/\d+$/) {
	    push @ports, $port;
	} else {
	    print "'$port' not in correct switch.port syntax, ignoring\n";
	}
    }

}
217
#
218
219
# If using physical port IDs, just use the list passed on the command line -
# For an experiment, figure out which one(s) they wanted.
220
#
221
elsif ($opt{p}) {
222
    #
223
224
    # If they gave a node:port form, use just that port. Otherwise, try to find
    # all the node's ports
225
226
227
228
    #
    foreach my $port (@passedPorts) {
	$port =~ /^([^:]+)(:(\d+))?$/;
	my ($hostname,$portnumber) = ($1,$3);
229
230
	if (defined $portnumber) {
	    push @ports, $port;
231
	} else  {
232
233
234
235
236
	    my $interfaces = DBQueryFatal("select card from interfaces " .
		"where node_id = '$hostname'");
	    while (my ($card) = $interfaces->fetchrow()) {
		push @ports, "$port:$card";
	    }
237
238
239
	}
    }
} else {
240
241
242
243
244
245
246
247
248
249
    my @experimentPorts;
    #
    # Get control net or experimental net ports, depending on what they
    # asked for
    #
    if ($opt{C}) {
        @experimentPorts = getExperimentControlPorts($pid,$eid);
    } else {
        @experimentPorts = getExperimentPorts($pid,$eid);
    }
Robert Ricci's avatar
Robert Ricci committed
250
    #print "ep: " . join(";",@experimentPorts) . "\n";
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
    if (@passedPorts) {
	#
	# Handle a set of passed-in ports
	#
	foreach my $port (@passedPorts) {
	    $port =~ /^([^:]+)(:(\d+))?$/;
	    my ($hostname,$portnumber) = ($1,$3);
	    my $nodeid;
	    if (!VnameToNodeid($pid,$eid,$hostname,\$nodeid)) {
		die "There is no node $hostname in $pid/$eid\n";
	    }

	    if (defined($portnumber)) {
		# They gave us a specific interface
		push @ports, "$nodeid:$portnumber";
	    } else {
		# We need to find all experimental ports for this node
		push @ports, grep(/^$nodeid:(\d+)$/,@experimentPorts);
	    }

	}
    } else {
	#
	# They didn't ask for specific ports, so we'll use 'em all
	#
	@ports = @experimentPorts;
    }
278
279
280
281
282
283
284
285
286
287
288
289
}

#
# List of OIDs we want to look at for each port
#

#
# Find out which devices these ports are on
#
my %portMap = mapPortsToDevices(@ports);

my @stats;
290
DEVICE: foreach my $name (keys %portMap) {
291
    my @ports = @{$portMap{$name}};
Mike Hibler's avatar
Mike Hibler committed
292
    my %oidwarned = ();
293
294
295
296

    #
    # Connect to the switch and get the data we want off of it
    #
Robert Ricci's avatar
Robert Ricci committed
297
298
299
300
301
    my $type = getDeviceType($name);
    my $device;
    SWITCH: for ($type) {
	/cisco/ && do {
	    require snmpit_cisco;
302
	    $device = new snmpit_cisco($name,0);
Robert Ricci's avatar
Robert Ricci committed
303
304
305
306
307
308
309
	    last;
	};
	/intel/ && do {
	    require snmpit_intel;
	    $device = new snmpit_intel($name);
	    last;
	};
310
311
312
313
314
	/foundry/ && do {
	    require snmpit_foundry;
	    $device = new snmpit_foundry($name);
	    last;
	};
315
316
317
318
319
	/nortel/ && do {
	    require snmpit_nortel;
	    $device = new snmpit_nortel($name);
	    last;
	};
320
321
322
323
324
	/hp/ && do {
	    require snmpit_hp;
	    $device = new snmpit_hp($name);
	    last;
	};
Robert Ricci's avatar
Robert Ricci committed
325
326

	# 'default' case
327
328
	warn "WARNING: Unknown switch type ($type) for $name, skipping some ports\n";
        last DEVICE;
Robert Ricci's avatar
Robert Ricci committed
329
    }
330
331
332
333
334
    my @results = $device->getFields(\@ports,\@oids);

    foreach my $result (@results) {
	my $port = shift @ports;

335
336
337
	#
	# Figure out which port on which switch this corresponds to
	#
338
339
340
341
342
343
344
345
	my $switchport;
	if ($opt{s}) {
	    if ($port =~ /^([^.]+)\.(\d+)\/(\d+)$/) {
		$switchport = "$1:$2.$3";
	    }
	} else {
	    $switchport = portnum($port);
	}
346
347
348
349
350
351
352
353
	if (!($switchport && ($switchport =~ /(.+):(\d+)\.(\d+)/))) {
	    warn "WARNING: No switch port found for $port\n";
	} else {
	    my ($switch_id,$switch_card,$switch_port) = ($1, $2, $3);
	    my $dbresult = DBQueryFatal("select * from port_counters where ".
	    		"node_id='$switch_id' and card=$switch_card and ".
			"port=$switch_port");

Mike Hibler's avatar
Mike Hibler committed
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
	    #
	    # Make sure returned values are integers.  If not, warn (just
	    # once per device) and set to zero.
	    #
	    for (my $i = 0; $i < @oids; $i++) {
		if ($result->[$i] !~ /^(\d+)$/) {
		    if ($badoidwarnings && !$oidwarned{$oids[$i]}) {
			warn("WARNING: invalid value '" , $result->[$i],
			     "' for OID '", $oids[$i], "'\n");
			$oidwarned{$oids[$i]} = 1;
		    }
		    $result->[$i] = 0;
		}
	    }

369
370
371
372
373
374
375
376
377
378
379
	    my $oldresult = [@$result];

	    #
	    # Unless they want absolute counters, go through and subtract
	    # out the values stored in the database
	    #
	    if (!$opt{c}) {
		if ($dbresult && $dbresult->num_rows()) {
		    my %oldvals = $dbresult->fetchhash();
		    for (my $i = 0; $i < @oids; $i++) {
			if ($oldvals{$oids[$i]}) {
380
381
382
383
384
385
386
387
388
389
390
			    #
			    # Check for wraparound - of course, we can't tell
			    # how many times it wrapped, but we can at least
			    # not print a negative number.
			    # XXX - we harcode the size of the counter here
			    #
			    if ($result->[$i] >= $oldvals{$oids[$i]}) {
				$result->[$i] -= $oldvals{$oids[$i]};
			    } else {
				$result->[$i] += (2**32 - $oldvals{$oids[$i]});
			    }
391
392
393
394
395
396
397
398
399
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
			}
		    }
		}
	    }

	    #
	    # If requested, update the counters in the database
	    #
	    if ($opt{z}) {
		#
		# What we do to the DB depends on whether or not there is
		# already an entry for this port
		#
		my $query;
		if ($dbresult->num_rows()) {
		    $query = "update port_counters set ";
		} else {
		    $query = "insert into port_counters set ";
		}

		my @query_terms = ();
		for (my $i = 0; $i < @oids; $i++) {
		    push @query_terms, " $oids[$i]=$oldresult->[$i] ";
		}
		$query .= join(",",@query_terms);

		if ($dbresult->num_rows()) {
		    $query .= " where node_id='$switch_id' and " .
			    "card=$switch_card and port=$switch_port";
		} else {
		    $query .= ", node_id='$switch_id', card=$switch_card, " .
			    "port=$switch_port ";
		}

		DBQueryFatal($query);
	    }
	}

429
	if (!$opt{p} && !$opt{s}) {
430
431
432
433
434
435
436
437
438
439
	    #
	    # Try to translate the port name to the node's vname
	    #
	    $port =~ /^(.+):(\d+)/;
	    if ($1) {
		my $portnum = $2;
		my ($junk, $vname);
		NodeidToExp($1,\$junk,\$junk,\$vname);
		$port = "$vname:$portnum";
	    }
440
441
442
443
444
445
446
447
448
	}

	#
	# Throw this onto a list, so that we can sort it
	#
	push @stats, [$port,@$result];
    }
}

449
450
451
452
453
454
455
#
# Exit now if they wanted quiet operation
#
if ($opt{q}) {
    exit(0);
}

456
#
457
# Build up the heading a format strings
458
#
459
460
461
462
463
464
465
466
467
468
my @heading1 = ('              ');
my @heading2 = ('Port          ');
my @format   = ('@<<<<<<<<<<<<<');

foreach my $line (@oids{@oids}) {
    my ($heading,$format) = @$line;
    push @heading1, $$heading[0];
    push @heading2, $$heading[1];
    push @format,   $format;
}
469

470
471
472
473
474
475
476
my $heading1 = join(" ",@heading1);
my $heading2 = join(" ",@heading2);

my $format = "format stats =\n" . join(" ",@format) . "\n";
$format .= join(",",map {"\$\$::line[$_]"} (0 .. @oids)) . "\n.\n";

eval $format;
477
478
$FORMAT_NAME = 'stats';

479
480
481
482
483
484
485
#
# Print out the heading
#
print "$heading1\n";
print "$heading2\n";
print "-" x length($heading1),"\n";

486
487
488
#
# Finally, print out the results
#
489
foreach $::line (sort {$$a[0] cmp $$b[0]} @stats) {
490
491
    write;
}