Commit 8266ae51 authored by Leigh B Stoller's avatar Leigh B Stoller

Several backend/RPC changes for reservations:

1. Return current set of reservations (if any) for a user when getting
   the max extension (piggy backing on the call to reduce overhead).

2. Add RPC to get the reservation history for a user (all past
   reservations that were approved).

   Aside; the reservation_history table was not being updated properly,
   only expired reservations were saved, not deleted (but used)
   reservations, so we lost a lot of history. We could regen some of it
   from the history tables I added at the Portal for Dmitry, but not
   sure it is worth the trouble.

3. And then the main content of this commit is that for both of the
   lists above, also return the experiment usage history for the project
   an dthe user who created the reservation. This takes the form of a
   time line of allocation changes so that we can graph node usage
   against the reservation bounds, to show graphically how well utilized
   the reservation is.
parent 735250d3
......@@ -104,6 +104,7 @@ use Project;
use APT_Profile;
use APT_Instance;
use APT_Geni;
use APT_Utility;
use GeniXML;
use GeniHRN;
use Genixmlrpc;
......@@ -1993,10 +1994,18 @@ sub DoMaxExtension()
goto bad;
}
if ($debug) {
print "Max extension: " . TBDateStringLocal($result) . "\n";
print "Max extension: " .
TBDateStringLocal($result->{'maxextension'}) . "\n";
if (exists($result->{'reservations'})) {
if (keys(%{$result->{'reservations'}})) {
print Dumper($result->{'reservations'});
}
}
}
if (defined($webtask)) {
$webtask->MaxExtension(TBDateStringGMT($result));
$webtask->MaxExtension(TBDateStringGMT($result->{'maxextension'}));
$webtask->Reservations($result->{'reservations'})
if (exists($result->{'reservations'}));
$webtask->Exited(0);
}
exit(0);
......@@ -2016,6 +2025,7 @@ sub DoMaxExtensionInternal($$)
my $maxinfo;
my $errmsg;
my $newmax;
my $blob = {"maxextension" => undef, "reservations" => {}};
my @aggregates = ();
foreach my $aggregate ($instance->AggregateList()) {
......@@ -2039,15 +2049,17 @@ sub DoMaxExtensionInternal($$)
# Process the max extension from each aggregate
foreach my $aggregate (@aggregates) {
my $response = shift(@{$responses});
my $aptagg = $aggregate->GetAptAggregate();
my $result = $response->value();
my $code = $response->code();
my $reslist = {};
my $max;
if ($code) {
if ($code == GENIRESPONSE_REFUSED) {
# We want the user to see REFUSED.
$errmsg = "No extension possible at ".
$aggregate->GetAptAggregate()->name() . ": " .
$response->error();
$aptagg->name() . ": " . $response->error();
$errcode = $code;
}
else {
......@@ -2056,8 +2068,15 @@ sub DoMaxExtensionInternal($$)
}
goto bad;
}
my $max = str2time($result);
if (ref($result)) {
$max = $result->{'maxextension'};
$reslist = $result->{'reservations'};
}
else {
$max = $result;
}
$max = str2time($max);
if ($debug) {
print "$aggregate: $result, $max\n";
}
......@@ -2065,8 +2084,49 @@ sub DoMaxExtensionInternal($$)
if (!defined($newmax));
$newmax = $max
if ($max < $newmax);
# Map project/user to local users (if appropriate).
foreach my $res (values(%{$reslist})) {
if (exists($res->{'project'}) && $res->{'project'} &&
GeniHRN::IsValid($res->{'project'})) {
my $project = APT_Utility::MapProjectURN($res->{'project'});
if (defined($project)) {
$res->{'pid'} = $project->pid();
$res->{'pid_idx'} = $project->pid_idx();
}
}
if (exists($res->{'user'}) && $res->{'user'} &&
GeniHRN::IsValid($res->{'user'})) {
my $geniuser = APT_Utility::MapUserURN($res->{'user'});
if (defined($geniuser)) {
$res->{'uid'} = $geniuser->uid();
$res->{'uid_idx'} = $geniuser->uid_idx();
}
}
#
# Add these for the web interface since we are already messing
# with the results.
#
$res->{'cluster'} = $aptagg->urn();
$res->{'cluster_id'} = $aptagg->nickname();
# Backwards compat
if (!exists($res->{'nodes'})) {
$res->{'nodes'} = $res->{'count'};
}
if ($res->{'approved'} eq "") {
# Maps to JSON NULL.
$res->{'approved'} = undef;
}
if (!exists($res->{'cancel'}) || $res->{'cancel'} eq "") {
# Maps to JSON NULL.
$res->{'cancel'} = undef;
}
}
$blob->{'reservations'}->{$aptagg->urn()} = $reslist;
}
$$prval = $newmax;
$blob->{'maxextension'} = $newmax;
$$prval = $blob;
return 0;
bad:
......
......@@ -28,6 +28,7 @@ use XML::Simple;
use Data::Dumper;
use CGI;
use POSIX ":sys_wait_h";
use POSIX qw(:signal_h);
use Date::Parse;
#
......@@ -39,6 +40,8 @@ sub usage()
print("Usage: manage_reservations [-a <urn>] delete pid uuid\n");
print("Usage: manage_reservations [-a <urn>] approve -p portal uuid\n");
print("Usage: manage_reservations [-a <urn>] prediction\n");
print("Usage: manage_reservations [-a <urn>] cancel\n");
print("Usage: manage_reservations [-a <urn>] history [user]\n");
exit(-1);
}
my $optlist = "dt:a:";
......@@ -86,6 +89,7 @@ use WebTask;
use APT_Geni;
use APT_Aggregate;
use APT_Instance;
use APT_Utility;
use GeniResponse;
use GeniUser;
......@@ -99,6 +103,7 @@ sub DoDelete();
sub DoApprove();
sub DoPrediction();
sub DoCancel();
sub DoHistory();
sub readfile($);
sub AddAnnouncement($$$$$$$);
sub DeleteAnnouncement($);
......@@ -183,6 +188,9 @@ elsif ($action eq "approve") {
elsif ($action eq "prediction") {
DoPrediction();
}
elsif ($action eq "history") {
DoHistory();
}
elsif ($action eq "cancel") {
DoCancel();
}
......@@ -419,7 +427,7 @@ sub DoList()
if (!$this_user->IsAdmin() && !$this_user->SameUser($user)) {
fatal("No permission to access reservation list for $user")
}
$geniuser = $this_user->CreateFromLocal($user);
$geniuser = GeniUser->CreateFromLocal($user);
}
if (defined($options{"p"})) {
$project = Project->Lookup($options{"p"});
......@@ -502,6 +510,33 @@ sub DoList()
$details->{'pid'} = $projhrn->id();
}
#
# Add these for the web interface since we are already messing
# with the results.
#
$details->{'cluster_id'} = $aggregate->nickname();
$details->{'cluster_urn'} = $aggregate->urn();
# Backwards compat
if (!exists($details->{'nodes'})) {
$details->{'nodes'} = $details->{'count'};
}
#
# Hmm, undef/null is a pain with XMLRPC.
#
if ($details->{'approved'} eq "") {
# Maps to JSON NULL.
$details->{'approved'} = undef;
}
if ($details->{'cancel'} eq "") {
# Maps to JSON NULL.
$details->{'cancel'} = undef;
}
if (!exists($details->{'uuid'}) || $details->{'uuid'} eq "") {
$details->{'uuid'} = NewUUID();
}
#
# If we have the history, then go through and map the
# experiments to local experiments so we can link to them in
......@@ -513,7 +548,10 @@ sub DoList()
# Local experiment on the remote cluster.
next
if (!(exists($ref->{'urn'}) && exists($ref->{'slice_uuid'})));
if (!(exists($ref->{'urn'}) &&
exists($ref->{'slice_uuid'}) &&
$ref->{'slice_uuid'} ne "" && $ref->{'urn'} ne ""));
# Already processed.
next
if (exists($ref->{'instance_uuid'}));
......@@ -973,12 +1011,21 @@ sub DoPrediction()
my $auth = $ref->{'authority'};
my $agg = $ref->{'aggregate'};
my $wtask = $ref->{'webtask'};
my $response;
$wtask->Refresh();
my $response = $wtask->response();
if ($code == SIGTERM) {
# Need to think about proper handling of this.
$response = GeniResponse->Create(GENIRESPONSE_NETWORK_ERROR,
GENIRESPONSE_NETWORK_ERROR_TIMEDOUT,
"Timed out talking to server");
}
else {
$wtask->Refresh();
$response = $wtask->response();
}
# No longer a blessed object (see above).
$response = GeniResponse->Bless($response);
if ($code) {
#
# If this failed, lets not return a fatal error since
......@@ -1013,6 +1060,120 @@ sub DoPrediction()
fatal($errmsg);
}
#
# Get the history for a specific user.
#
sub DoHistory()
{
my $optlist = "";
my $portal;
my $errmsg;
my $blob = {};
my ($user, $geniuser);
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
usage()
if (@ARGV > 1);
# Argument is a target user.
if (@ARGV) {
$user = User->Lookup($ARGV[0]);
if (!defined($user)) {
fatal("No such user");
}
if (!$this_user->IsAdmin() && !$this_user->SameUser($user)) {
fatal("No permission to access reservation history for $user")
}
$geniuser = GeniUser->CreateFromLocal($user);
}
else {
$geniuser = GeniUser->CreateFromLocal($this_user);
}
if ($aggregate->CheckStatus(\$errmsg, 1)) {
UserError($errmsg);
}
# PortalRPC will use the root context in this case, which is
# essentially saying the caller is an admin. But thats okay
# for this call, it is just informational.
my $response =
APT_Geni::PortalRPC($authority, undef,
"ReservationHistory",
{"user_urn" => $geniuser->urn()->asString()});
if (GeniResponse::IsError($response)) {
ExitWithError($response);
}
#
# Munge the return value like we do above, converting user/proj
# URNs to local names. One minor difference is that we know the
# results are only for local users and projects, but lets be
# consistent.
#
my @reslist = @{ $response->value()->{'history'} };
foreach my $res (@reslist) {
my $userhrn = GeniHRN->new($res->{'user'});
if ($userhrn->domain() eq $OURDOMAIN &&
$userhrn->id eq $geniuser->uid()) {
$res->{'uid'} = $geniuser->uid();
$res->{'uid_idx'} = $geniuser->idx();
}
my $project = APT_Utility::MapProjectURN($res->{'project'});
if (defined($project)) {
$res->{'pid'} = $project->pid();
$res->{'pid_idx'} = $project->pid_idx();
}
else {
my $projhrn = GeniHRN->new($res->{'project'});
$res->{'pid'} = $projhrn->id();
}
#
# Add these for the web interface since we are already messing
# with the results.
#
$res->{'cluster_id'} = $aggregate->nickname();
$res->{'cluster_urn'} = $aggregate->urn();
# Backwards compat
if (!exists($res->{'nodes'})) {
$res->{'nodes'} = $res->{'count'};
}
#
# Hmm, undef/null is a pain with XMLRPC.
#
if (!exists($res->{'approved'}) ||
$res->{'approved'} eq "") {
$res->{'approved'} = undef;
}
if (!exists($res->{'cancel'}) ||
$res->{'cancel'} eq "") {
$res->{'cancel'} = undef;
}
if (!exists($res->{'uuid'}) ||
$res->{'uuid'} eq "") {
$res->{'uuid'} = NewUUID();
}
}
done:
if (defined($webtask)) {
$webtask->value({"reservations" => \@reslist});
$webtask->Exited(0);
}
else {
print Dumper(@reslist);
}
exit(0);
bad:
fatal($errmsg);
}
sub fatal($)
{
my ($mesg) = @_;
......
#!/usr/bin/perl -w
#
# Copyright (c) 2000-2018 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/>.
#
# }}}
#
#
# This is a module so it can be used from the fast RPC path.
#
package ResUtil;
use Exporter;
@ISA = "Exporter";
@EXPORT = qw (CollectReservations CreateTimeline ComputeCounts);
# After package decl.
use strict;
use English;
use Date::Parse;
use Data::Dumper;
use POSIX qw(strftime);
# Emulab
use Reservation;
use Project;
# Debugging.
my $debug = 0;
sub DebugOn() { $debug = 2; };
#
# Collect the reservations we are interested in.
#
sub CollectReservations($$$)
{
my ($project, $useronly, $allres) = @_;
#
# By default we look at all current reservations for a specific project.
# Optionally add all historical reservations for the project.
# Optionally look at reservations for a specific user in the project.
#
my @reservations = Reservation->ProjectReservations($project, $useronly,
undef, 1);
if ($allres) {
#
# Grab all of the historical reservations and append.
#
my @more = Reservation->HistoricalReservations($project,
$useronly, undef);
@reservations = (@reservations, @more);
}
if (!@reservations) {
return ();
}
if ($debug) {
print "There are " . scalar(@reservations) . " reservations\n";
}
# Sort to make sure the earliest is first.
@reservations = sort {$a->start() <=> $b->start()} @reservations;
return @reservations;
}
#
# Create a timeline of alloc/free operations, using the project usage
# since the earliest reservation.
#
sub CreateTimeline($@)
{
my ($project, @reservations) = @_;
# We want the earliest/latest reservation for getting project usage();
my $earliest = $reservations[0];
if ($debug) {
print "The earliest reservation start is " .
POSIX::strftime("%m/%d/20%y %H:%M:%S",
localtime($earliest->start())) . " \n";
}
#
# We want to bound the end of the search to the latest end if we
# have only historial reservations.
#
my @tmp = sort {$a->end() <=> $b->end()} @reservations;
my $latest = $tmp[-1];
if ($debug) {
print "The latest reservation end is " .
POSIX::strftime("%m/%d/20%y %H:%M:%S",
localtime($latest->end())) . " \n";
}
my $end = $latest->end();
$end = time() if ($end > time());
# Get the usage since the beginning of the earliest reservation.
my $usage = $project->Usage($earliest->start() - (3600 * 24), $end);
if ($debug) {
print "There are " . scalar(@$usage) . " usage records\n";
if ($debug > 1 && scalar(@$usage)) {
print Dumper(@$usage);
}
}
if (!scalar(@$usage)) {
print STDERR "There are no usage records to process\n"
if ($debug);
return ();
}
# Form a timeline of changes in allocation.
my @timeline = ();
foreach my $ref (@$usage) {
push(@timeline,
{"t" => $ref->{'start'}, "details" => $ref, "op" => "alloc"});
# Experiment ended so nodes are now free.
push(@timeline,
{"t" => $ref->{'end'}, "details" => $ref, "op" => "free"})
if ($ref->{'end'} ne "");
}
# And sort the new list.
@timeline = sort {$a->{'t'} <=> $b->{'t'}} @timeline;
#
# Remember all the types we care about; when computing the counts
# do not bother with nodes that are not in the set of types reserved.
#
my %typesinuse = ();
#
# Correlate the reservations with the sorted list using the start/end
# of the reservation and the timestamps in the timeline. This tells us
# what reservations are active at each point in the timeline.
#
foreach my $ref (@timeline) {
my $stamp = $ref->{'t'};
my @reslist = ();
#print Dumper($ref);
#
# This will eventually be too inefficient ...
#
foreach my $res (@reservations) {
my $resStart = $res->start();
my $resEnd = $res->end();
push(@reslist, $res)
if ($stamp >= $resStart && $stamp <= $resEnd);
}
next
if (!@reslist);
# Each timestamp gets a list of active reservations.
$ref->{'reservations'} = \@reslist;
# Count up number of node/types reserved by the project and
# by each user.
my $pid = $project->pid();
my $reserved = {$pid => {}};
foreach my $res (@reslist) {
my $type = $res->type();
my $nodes = $res->nodes();
my $uid = $res->uid();
# Remember that we care about this type for later when doing
# allocated counts.
$typesinuse{$type} = $type;
if (!exists($reserved->{$pid}->{$type})) {
$reserved->{$pid}->{$type} = 0;
}
$reserved->{$pid}->{$type} += $nodes;
if (!exists($reserved->{$uid})) {
$reserved->{$uid} = {};
}
if (!exists($reserved->{$uid}->{$type})) {
$reserved->{$uid}->{$type} = 0;
}
$reserved->{$uid}->{$type} += $nodes;
}
$ref->{'reserved'} = $reserved;
}
#
# Kill off timeline entries that do not include the types we care
# about (types that are reserved at some point during the timeline).
# This reduces entries the number of entries that do not show an
# interesting change (ie: same as previous entry).
#
@tmp = ();
foreach my $ref (@timeline) {
foreach my $type (keys(%{$ref->{'details'}->{'types'}})) {
if (exists($typesinuse{$type})) {
push(@tmp, $ref);
last;
}
}
}
@timeline = @tmp;
# Hmm, this happens.
return ()
if (!@timeline);
#
# Now compute the node counts for each entry in the timeline.
#
my @counts = ComputeCounts($project, \%typesinuse, @timeline);
#
# We want to add additional entrys for the start of each reservation.
#
@tmp = ();
foreach my $ref (@counts) {
#
# If this entry is after the timestamp for the first
# reservation on the list (which is also sorted), then
# create a new timeline entry for the very start of
# the reservation so we have counts in use at the very
# start (since often nodes will not be allocated till
# sometime later).
#
while (@reservations &&
$ref->{'t'} >= $reservations[0]->start()) {
# Shallow copy.
my $new = { %{$ref} };
$new->{'t'} = $reservations[0]->start();
push(@tmp, $new);
shift(@reservations);
}
push(@tmp, $ref)
}