predict.in 8.34 KB
Newer Older
1 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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
#!/usr/bin/perl -w
#
# Copyright (c) 2016 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 <http://www.gnu.org/licenses/>.
# 
# }}}
#
use strict;
use English;
use Getopt::Std;
use Date::Parse;
use POSIX;

#
# Configure variables
#
my $TB		 = "@prefix@";
my $TBOPS        = "@TBOPSEMAIL@";

#
# Testbed Support libraries
#
use lib "@prefix@/lib";
use emdb;
use libtestbed;
use Project;
use Reservation;

sub usage()
{
    print STDERR "Usage: predict [-p] [-u] [-t time] type\n";
48
    print STDERR "       predict -c type [pid...]\n";
49
    print STDERR "       predict -l type [pid...]\n";
50
    print STDERR "       predict -n [-t time] [-D duration] type [pid...]\n";
51
    print STDERR "       predict -P type [pid...]\n";
52
    print STDERR "       predict -x [-T type] pid...\n";
53
    print STDERR "   -h   This message\n";
54
    print STDERR "   -c   Give an oversimplified free node count\n";
55 56
    print STDERR "   -l   Give a list of node allocation status counts " .
	"over time\n";
57 58
    print STDERR "   -n   Compute the minimum free pool size over a time " .
	"period\n";
59 60
    print STDERR "   -P   Identify periods of node pressure\n";
    print STDERR "   -x   Show earliest unfulfilled reservation\n";
61 62 63 64 65 66 67
    print STDERR "   -p   Identify by pid only, not pid/eid\n";
    print STDERR "   -t   Give time/date for prediction (defaults to now)\n";
    print STDERR "   -u   Interpret/display all times in UTC\n";
    
    exit( -1 );
}

68
my $optlist     = "cdD:hlnpPt:T:x";
69
my $debug       = 0;
70
my $duration    = 365 * 24 * 60 * 60; # default to 1 year ~= infinity
71
my $time        = time; # default to now
72
my $minfree     = 0;
73 74 75 76 77
my $pidonly     = 0;
my $countonly   = 0;
my $pressure    = 0;
my $timeseries  = 0;
my $unfulfilled = 0;
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 110 111 112 113 114 115 116 117 118 119 120 121
my $type;

sub fatal($)
{
    my ($mesg) = $_[0];

    die("*** $0:\n".
	"    $mesg\n");
}

sub convert($) {
    my ($unixtime) = @_;

    return strftime( "%Y-%m-%d %H:%M", localtime( $unixtime ) );
}

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

#
# Untaint the path
# 
$ENV{'PATH'} = "/bin:/sbin:/usr/bin:";

#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
my %options = ();
if (! getopts($optlist, \%options)) {
    usage();
}
if (defined($options{"u"})) {
    # handle this option ASAP, since it affects parsing of other options!
    $ENV{ "TZ" } = "UTC";
}
if (defined($options{h})) {
    usage();
}
if (defined($options{p})) {
    $pidonly = 1;
}
122 123 124
if (defined($options{P})) {
    $pressure = 1;
}
125 126 127 128 129 130 131 132 133
if (defined($options{"t"})) {
    $time = $options{"t"};
    if ($time !~ /^\d+$/) {
	$time = str2time($time);
	if( !defined( $time ) ) {
	    fatal("Could not parse -t option.");
	}
    }
}
134 135 136 137 138 139 140
if (defined($options{"D"})) {
    $duration = $options{"D"};
    if ($duration !~ /^\d+$/) {
	fatal("Could not parse -D option.");
    }
    $duration *= 60 * 60;
}
141 142 143
if (defined($options{T})) {
    $type = $options{T};
}
144 145 146
if (defined($options{"c"})) {
    $countonly = 1;
}
147 148 149
if (defined($options{"l"})) {
    $timeseries = 1;
}
150 151 152
if (defined($options{"n"})) {
    $minfree = 1;
}
153 154 155
if (defined($options{"x"})) {
    $unfulfilled = 1;
}
156 157
usage() if( ( $countonly || $timeseries || $pressure || $unfulfilled ||
	      $minfree ) ?
158 159 160 161 162 163
	    @ARGV < 1 : @ARGV != 1 );
if( !$unfulfilled ) {
    $type = shift( @ARGV );
    unless( $type =~ /^[-\w]+$/ ) {
	fatal( "Invalid node type." );
    }
164 165
}

166 167 168 169 170 171
if( $countonly ) {
    print( int( Reservation->FreeCount( $type, \@ARGV ) + 0.5 ) . "\n" );
    
    exit( 0 );
}

172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
if( $minfree ) {
    my $min = 0xFFFF;
    foreach my $rec ( Reservation->Forecast( $type, \@ARGV ) ) {
	my $sample = $rec->{'held'} + $rec->{'free'}; 
	if( $rec->{'t'} < $time ) {
	    # record is before period of interest; retain only the
	    # newest available
	    $min = $sample;
	} elsif( $rec->{'t'} <= $time + $duration ) {
	    # record is during period of interest; compute the minimum
	    $min = $sample if( $sample < $min );
	} else {
	    # we've gone past the end of the period and no longer care
	    # about the future
	    last;
	}
    }

    print "$min\n";

    exit( 0 );
}

195 196 197 198 199 200 201 202 203 204 205 206 207
if( $timeseries ) {
    print "Time       Unavailable  Held  Free\n";
    print "----------       -----  ----  ----\n";
    
    foreach my $rec ( Reservation->Forecast( $type, \@ARGV ) ) {
	printf( "%s %5d %5d %5d\n",
		strftime( "%Y-%m-%d %H:%M", localtime( $rec->{'t'} ) ),
		$rec->{'unavailable'}, $rec->{'held'}, $rec->{'free'} );
    }
    
    exit( 0 );
}

208 209 210 211 212 213 214 215 216 217 218 219
if( $pressure ) {
    foreach ( Reservation->FuturePressure( [ $type ], \@ARGV ) ) {
        my ( $start, $end ) = @$_;

	print strftime( "%Y-%m-%d %H:%M-", localtime( $start ) ) .
	    strftime( "%Y-%m-%d %H:%M\n", localtime( $end ) );
    }
    
    exit( 0 );
}

if( $unfulfilled ) {
220 221 222
    my $t = Reservation->OutstandingReservation( \@ARGV,
						 defined( $type ) ? [ $type ] :
						 undef );
223 224 225 226 227 228 229 230 231 232

    if( defined( $t ) ) {
	print strftime( "%Y-%m-%d %H:%M\n", localtime( $t ) );
    } else {
	print "No outstanding reservations found.\n";
    }
    
    exit( 0 );
}

233 234
my $reservations = Reservation->LookupAll( $type );
my @timeline = ();
235
my $free = 0;
236 237
my %used = ();
my %reserved = ();
238
my %usedexp = ();
239 240 241 242 243 244 245 246 247

foreach my $reservation ( @$reservations ) {
    my $start;
    my $end;

    if( defined( $reservation->eid() ) ) {
	# A swapped-in experiment.  Already using nodes (so no
	# need to save a start event), and will later release real nodes.
	my $pid = $reservation->pid();
248 249 250
	my $exp = $reservation->pid() . "/" . $reservation->eid();
	if( !exists( $usedexp{ $exp } ) ) {
	    $usedexp{ $exp } = 0;
251
	}
252 253 254 255 256 257 258 259
	if( !exists( $used{ $pid } ) ) {
	    $used{ $pid } = 0;
	    $reserved{ $pid } = 0;
	}
	$used{ $pid } += $reservation->nodes();
	$usedexp{ $exp } += $reservation->nodes();
	$end = { 'pid' => $pid,
		 'exp' => $exp,
260 261 262 263 264
		 't' => $reservation->end(),
		 'used' => -$reservation->nodes(),
		 'reserved' => 0 };
    } elsif( defined( $reservation->pid() ) ) {
	# A reservation.  Uses then releases reserved nodes.
265
	$start = { 'pid' => $reservation->pid(),
266 267 268
		   't' => $reservation->start(),
		   'used' => 0,
		   'reserved' => $reservation->nodes() };
269
	$end = { 'pid' => $reservation->pid(),
270 271 272 273 274
		 't' => $reservation->end(),
		 'used' => 0,
		 'reserved' => -$reservation->nodes() };
    } else {
	# Available resources.  Provides nodes for all time.
275
	$free += $reservation->nodes();
276 277 278 279 280 281 282 283 284 285 286
    }

    push( @timeline, $start ) if( defined( $start->{'t'} ) );
    push( @timeline, $end ) if( defined( $end->{'t'} ) );
}

my @events = sort { $a->{'t'} <=> $b->{'t'} } @timeline;
    
foreach my $event ( @events ) {
    last if( $event->{'t'} > $time );
		     
287 288 289 290
    my $pid = $event->{'pid'};
    if( !exists( $used{ $pid } ) ) {
	$used{ $pid } = 0;
	$reserved{ $pid } = 0;
291 292
    }

293 294
    my $oldsum = $used{ $pid } > $reserved{ $pid } ?
	$used{ $pid }: $reserved{ $pid };
295

296 297 298 299 300
    $used{ $pid } += $event->{ 'used' };
    $reserved{ $pid } += $event->{ 'reserved' };
    if( exists( $event->{ 'exp' } ) ) {
	$usedexp{ $event->{ 'exp' } } += $event->{ 'used' };
    }
301

302 303
    my $newsum = $used{ $pid } > $reserved{ $pid } ?
	$used{ $pid }: $reserved{ $pid };
304 305 306 307

    $free += $oldsum - $newsum;
}

308 309 310 311 312 313 314 315 316 317
if( $pidonly ) {
    foreach my $used ( sort { $used{$b} <=> $used{$a} } keys( %used ) ) {
	my $val = $used{ $used };
	print "$used: $val\n" if( $val > 0 );
    }
} else {
    foreach my $used ( sort { $usedexp{$b} <=> $usedexp{$a} } keys( %usedexp ) ) {
	my $val = $usedexp{ $used };
	print "$used: $val\n" if( $val > 0 );
    }
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
}

foreach my $reserved ( sort { $reserved{$b} <=> $reserved{$a} }
		       keys( %reserved ) ) {
    my $val = $reserved{ $reserved };
    print "[$reserved: $val]\n" if( $val > 0 );
}

if( $free >= 0 ) {
    print $free . " free node";
    print ( $free == 1 ? ".\n" : "s.\n" );
} else {
    print "Overbooked by " . -$free . " node";
    print ( $free == -1 ? ".\n" : "s.\n" );
}
333 334

exit( 0 );