#!/usr/bin/perl -w
#
# Copyright (c) 2000-2012 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see .
#
# }}}
#
# Utility routines for Emulab.
#
package emutil;
use strict;
use Exporter;
use SelfLoader;
use vars qw(@ISA @EXPORT);
@ISA = qw(Exporter SelfLoader);
@EXPORT = qw(TBDB_CHECKDBSLOT_NOFLAGS TBDB_CHECKDBSLOT_WARN
TBDB_CHECKDBSLOT_ERROR TBcheck_dbslot TBFieldErrorString
TBGetUniqueIndex ParRun VersionInfo UpdateVersionInfo
SpanningTree GenFakeMac BackTraceOnWarning PassWordHash);
use emdb;
use English;
use Carp;
#
# Support for checking field values against what is specified.
#
use vars qw(%DBFieldData $DBFieldErrstr);
%DBFieldData = ();
$DBFieldErrstr = "";
#
# A helper function for checking lossrates. Bad place for this, I know.
# Needs to be before the __DATA__ cause we used defined() on it. Sigh.
#
sub _checklossrate($)
{
my ($token) = @_;
return 1
if ("$token" eq "0");
# floating point, no exponent. Stole this out of the perl tutorial.
if (! ($token =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+)([eE][+-]?\d+)?$/)) {
$DBFieldErrstr = "Improper floating number";
return 0;
}
if ($token > 1.0) {
$DBFieldErrstr = "Too big; must be < 1.0";
return 0;
}
if ($token < 0.0) {
$DBFieldErrstr = "Too small; must be > 0.0";
return 0;
}
if ($token > 0.0 && $token < 0.000001) {
$DBFieldErrstr = "Too small; must be >= 0.000001";
return 0;
}
return 1;
}
1;
@SELFLOADER_DATA@
# Constants for checkslot code.
sub TBDB_CHECKDBSLOT_NOFLAGS() { 0x0; }
sub TBDB_CHECKDBSLOT_WARN() { 0x1; }
sub TBDB_CHECKDBSLOT_ERROR() { 0x2; }
sub TBFieldErrorString() { return $DBFieldErrstr; }
#
# Download all data from the DB and store in hash for latter access.
#
sub TBGrabFieldData()
{
%DBFieldData = ();
my $query_result =
emdb::DBQueryFatal("select * from table_regex");
while (my %row = $query_result->fetchhash()) {
my $table_name = $row{"table_name"};
my $column_name = $row{"column_name"};
$DBFieldData{$table_name . ":" . $column_name} =
{ "check" => $row{"check"},
"check_type" => $row{"check_type"},
"column_type" => $row{"column_type"},
"min" => $row{"min"},
"max" => $row{"max"}
};
}
}
#
# Return the field data for a specific table/slot. If none, return the default
# entry.
#
# The top level entry defines some stuff that is not to be overidden by the
# redirected entries. For example, the top level entry is the only place we
# can specify a field is optional when inserting a record. We could do this
# with default entries in the DB table defintion, but I do not like that idea.
# The min/max lengths also override, unless they are both zero in which case
# let the first non-zero defs set them.
#
sub TBFieldData($$;$)
{
my ($table, $column, $flag) = @_;
my $toplevel;
my $fielddata;
if (! %DBFieldData) {
TBGrabFieldData();
}
my $key = $table . ":" . $column;
while (exists($DBFieldData{$key})) {
$fielddata = $DBFieldData{$key};
#
# See if a redirect to another entry.
#
if ($fielddata->{"check_type"} eq "redirect") {
if (!defined($toplevel)) {
$toplevel = $fielddata;
}
$key = $fielddata->{"check"};
# print STDERR "Redirecting to $key for $table/$column!\n";
next;
}
last;
}
# Resort to a default entry.
if (!defined($fielddata)) {
$DBFieldErrstr = "Error-checking pattern missing from the database";
if (defined($flag)) {
if ($flag & TBDB_CHECKDBSLOT_WARN()) {
print STDERR "*** $0:\n" .
" WARNING: No slot data for $table/$column!\n";
}
return undef
if ($flag & TBDB_CHECKDBSLOT_ERROR());
}
$fielddata = $DBFieldData{"default:default"};
}
# Return both entries.
if (defined($toplevel) &&
($toplevel->{"min"} || $toplevel->{"max"})) {
return ($fielddata, $toplevel);
}
return ($fielddata);
}
#
# Generic wrapper to check a slot.
#
sub TBcheck_dbslot($$$;$)
{
my ($token, $table, $column, $flag) = @_;
$DBFieldErrstr = "Unknown Error";
my ($fielddata,$toplevel) = TBFieldData($table, $column, $flag);
return 0
if (!defined($fielddata));
my $check = $fielddata->{"check"};
my $check_type = $fielddata->{"check_type"};
my $column_type = $fielddata->{"column_type"};
my $min = (defined($toplevel) ?
$toplevel->{"min"} : $fielddata->{"min"});
my $max = (defined($toplevel) ?
$toplevel->{"max"} : $fielddata->{"max"});
# print STDERR "Using $check/$check_type/$column_type/$min/$max for ".
# "$table/$column\n";
#
# Functional checks partly implemented. Needs work.
#
if ($check_type eq "function") {
if (defined(&$check)) {
my $func = \&$check;
return &$func($token);
}
else {
die("*** $0:\n" .
" Functional DB check not implemented: ".
"$table/$column/$check\n");
}
}
# Make sure the regex is anchored. Its a mistake not to be!
$check = "^" . $check
if (! ($check =~ /^\^/));
$check = $check . "\$"
if (! ($check =~ /\Q$/));
# Check regex.
if (! ("$token" =~ /$check/)) {
$DBFieldErrstr = "Illegal Characters";
return 0;
}
# Check min/max.
if ($column_type eq "text") {
my $len = length($token);
# Any length is okay if no min or max.
return 1
if ((!($min || $max)) ||
($len >= $min && $len <= $max));
$DBFieldErrstr = "Too Short"
if ($min && $len < $min);
$DBFieldErrstr = "Too Long"
if ($max && $len > $max);
}
elsif ($column_type eq "int" ||
$column_type eq "float") {
# If both min/max are zero, then skip check; allow anything.
return 1
if ((!($min || $max)) || ($token >= $min && $token <= $max));
$DBFieldErrstr = "Too Small"
if ($min && $token < $min);
$DBFieldErrstr = "Too Big"
if ($max && $token > $max);
}
else {
die("*** $0:\n" .
" Unrecognized column_type $column_type\n");
}
return 0;
}
#
# Return a unique index from emulab_indicies for the indicated name.
# Updates the index to be, well, unique.
# Eats flaming death on error.
#
# WARNING: this will unlock all locked tables, be careful where you call it!
#
sub TBGetUniqueIndex($;$$)
{
my ($name, $initval, $nolock) = @_;
#
# Lock the table to avoid conflict, but not if the caller already did it.
#
$nolock = 0
if (!defined($nolock));
DBQueryFatal("lock tables emulab_indicies write")
if (!$nolock);
my $query_result =
DBQueryFatal("select idx from emulab_indicies ".
"where name='$name'");
my ($curidx) = $query_result->fetchrow_array();
if (!defined($curidx)) {
$curidx = (defined($initval) ? $initval : 1);
}
my $nextidx = $curidx + 1;
DBQueryFatal("replace into emulab_indicies (name, idx) ".
"values ('$name', $nextidx)");
DBQueryFatal("unlock tables")
if (!$nolock);
return $curidx;
}
#
# A utility function for forking off a bunch of children and
# waiting for them.
#
# TODO: A fatal error will leave children. Need to catch that.
#
sub ParRun($$$@)
{
my ($options, $pref, $function, @objects) = @_;
my %children = ();
my @results = ();
my $counter = 0;
my $signaled = 0;
# We need this below.
require event;
# options.
my $maxchildren = 10;
my $maxwaittime = 200;
if (defined($options)) {
$maxchildren = $options->{'maxchildren'}
if (exists($options->{'maxchildren'}));
$maxwaittime = $options->{'maxwaittime'}
if (exists($options->{'maxwaittime'}));
}
#
# Set up a signal handler in the parent to handle termination.
#
my $coderef = sub {
my ($signame) = @_;
print STDERR "Caught SIG${signame}! Killing parrun ...";
$SIG{TERM} = 'IGNORE';
$signaled = 1;
foreach my $pid (keys(%children)) {
kill('TERM', $pid);
}
sleep(1);
};
local $SIG{QUIT} = $coderef;
local $SIG{TERM} = $coderef;
local $SIG{HUP} = $coderef;
local $SIG{INT} = 'IGNORE';
#
# Initialize return.
#
for (my $i = 0; $i < scalar(@objects); $i++) {
$results[$i] = -1;
}
while (@objects || keys(%children)) {
#
# Something to do and still have free slots.
#
if (@objects && keys(%children) < $maxchildren && !$signaled) {
# Space out the invocation of child processes a little.
sleep(1);
#
# Run command in a child process, protected by an alarm to
# ensure that whatever happens is not hung up forever in
# some funky state.
#
my $object = shift(@objects);
my $syspid = fork();
if ($syspid) {
#
# Just keep track of it, we'll wait for it finish down below
#
$children{$syspid} = [$object, $counter, time()];
$counter++;
}
else {
$SIG{TERM} = 'DEFAULT';
$SIG{QUIT} = 'DEFAULT';
$SIG{HUP} = 'DEFAULT';
# So we get the event system fork too ...
event::EventFork();
exit(&$function($object));
}
}
elsif ($signaled) {
my $childpid = wait();
my $exitstatus = $?;
if (exists($children{$childpid})) {
delete($children{$childpid});
}
}
else {
#
# We have too many of the little rugrats, wait for one to die
#
#
# Set up a timer - we want to kill processes after they
# hit timeout, so we find the first one marked for death.
#
my $oldest;
my $oldestpid = 0;
my $oldestobj;
while (my ($pid, $aref) = each %children) {
my ($object, $which, $birthtime) = @$aref;
if ((!$oldestpid) || ($birthtime < $oldest)) {
$oldest = $birthtime;
$oldestpid = $pid;
$oldestobj = $object;
}
}
#
# Sanity check
#
if (!$oldest) {
print STDERR
"*** ParRun: ".
"Uh oh, I have no children left, something is wrong!\n";
}
#
# If the oldest has already expired, just kill it off
# right now, and go back around the loop
#
my $now = time();
my $waittime = ($oldest + $maxwaittime) - time();
#
# Kill off the oldest if it gets too old while we are waiting.
#
my $childpid = -1;
my $exitstatus = -1;
eval {
local $SIG{ALRM} = sub { die "alarm clock" };
if ($waittime <= 0) {
print STDERR
"*** ParRun: timeout waiting for child: $oldestpid\n";
kill("TERM", $oldestpid);
}
else {
alarm($waittime);
}
$childpid = wait();
alarm 0;
$exitstatus = $?;
};
if ($@) {
die unless $@ =~ /alarm clock/;
next;
}
#
# Another sanity check
#
if ($childpid < 0) {
print STDERR
"*** ParRun:\n".
"wait() returned <0, something is wrong!\n";
next;
}
#
# Look up to see what object this was associated with - if we
# do not know about this child, ignore it
#
my $aref = $children{$childpid};
next unless @$aref;
my ($object, $which, $birthtime) = @$aref;
delete($children{$childpid});
$results[$which] = $exitstatus;
}
}
@$pref = @results
if (defined($pref));
return -1
if ($signaled);
return 0;
}
#
# Version Info
#
sub VersionInfo($)
{
my ($name) = @_;
my $query_result =
DBQueryWarn("select value from version_info ".
"where name='$name'");
return undef
if (!$query_result || !$query_result->numrows);
my ($value) = $query_result->fetchrow_array();
return $value;
}
#
# Version Info
#
sub UpdateVersionInfo($$)
{
my ($name, $value) = @_;
my $safe_name = DBQuoteSpecial($name);
my $safe_value = DBQuoteSpecial($value);
return -1
if (!DBQueryWarn("replace into version_info set ".
" name=$safe_name, value=$safe_value"));
return 0;
}
#
# Run a command, being sure to capture all output.
#
sub ExecQuiet($)
{
#
# Use a pipe read, so that we save away the output
#
my ($command) = @_;
my $output = "";
open(PIPE,"$command 2>&1 |") or return -1;
while () {
$output .= $_;
}
close(PIPE);
return $output;
}
#
# Given a set of edges: [[cisco1, cisco3], [cisco3, cisco4]].
# Return a spanning tree. Deadly simple algorithm.
#
sub SpanningTree($)
{
my ($edges) = @_;
my %vertices = ();
my %edges = ();
#
# Get the unique set of vertices. Also form a hash of edges we can mark.
#
foreach my $edge (@$edges) {
my ($a, $b) = @$edge;
$vertices{$a} = 0
if (!exists($vertices{$a}));
$vertices{$b} = 0
if (!exists($vertices{$b}));
$edges{"$a:$b"} = 0;
}
#print Dumper(\%vertices);
#print Dumper(\%edges);
#
# Pick the first vertex and mark it.
#
$vertices{(keys(%vertices))[0]} = 1;
#
# Loop according to Prims algorithm.
#
while (1) {
#
# Get the set of marked vertices;
#
my %marked = ();
foreach my $vertex (keys(%vertices)) {
$marked{$vertex} = 1
if ($vertices{$vertex});
}
# Done if all vertices are marked.
last
if (scalar(keys(%marked)) == scalar(keys(%vertices)));
#
# Find the first unmarked vertex that connects to any of the
# marked ones. Mark that edge; that is an edge we want in the
# final set.
#
foreach my $vertex (keys(%vertices)) {
next
if ($marked{$vertex});
foreach my $marked (keys(%marked)) {
if (exists($edges{"$vertex:$marked"})) {
$edges{"$vertex:$marked"} = 1;
$vertices{$vertex} = 1;
goto loop;
}
elsif (exists($edges{"$marked:$vertex"})) {
$edges{"$marked:$vertex"} = 1;
$vertices{$vertex} = 1;
goto loop;
}
}
}
loop:
#print Dumper(\%edges);
#sleep(1);
}
#
# Return a new set of *marked* edges.
#
my @newedges = ();
foreach my $edge (keys(%edges)) {
next
if (!$edges{$edge});
my ($a, $b) = split(":", $edge);
push(@newedges, [$a, $b]);
}
return @newedges;
}
#
# Toggle backtrace on warning.
#
sub BackTraceOnWarning($)
{
my ($enable) = @_;
if ($enable) {
$SIG{__WARN__} = sub { Carp::cluck(@_); };
}
else {
$SIG{__WARN__} = 'DEFAULT';
}
}
#
# Convert to an encrypted hash.
#
sub PassWordHash($)
{
my ($password) = @_;
my @salt_chars = ('a'..'z','A'..'Z','0'..'9');
my $salt = $salt_chars[rand(@salt_chars)] .
$salt_chars[rand(@salt_chars)];
my $passhash = crypt($password, "\$1\$${salt}");
return $passhash;
}
#
# Generate a hopefully unique mac address that is suitable for use
# on a shared node where uniqueness matters.
#
sub GenFakeMac()
{
my $mac;
#
# Random number for lower 4 octets.
#
my $ran=`/bin/dd if=/dev/urandom count=32 bs=1 2>/dev/null | /sbin/md5`;
return undef
if ($?);
if ($ran =~ /^\w\w\w(\w\w\w\w\w\w\w\w\w\w)/) {
$mac = $1;
}
#
# Set the "locally administered" bit, good practice.
#
return "02" . $mac;
}
# _Always_ make sure that this 1 is at the end of the file...
1;