Commit 8266ae51 authored by Leigh Stoller's avatar Leigh 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) = @_;
......
This diff is collapsed.
......@@ -1105,13 +1105,18 @@ sub OutstandingReservation($$;$) {
# Return a list of (pid, nodetype, reserved, used) hashes for any currently
# active reservations belonging to a listed project.
sub CurrentReservations($$) {
my ($class, $projlist) = @_;
sub CurrentReservations($;$$) {
my ($class, $projlist, $activeonly) = @_;
my @answer = ();
foreach ( @$projlist ) {
# reject illegal PIDs
return undef unless /^[-\w]+$/;
my $pclause = "";
$activeonly = 0 if (!defined($activeonly));
if (defined($projlist) && @$projlist) {
foreach ( @$projlist ) {
# reject illegal PIDs
return undef unless /^[-\w]+$/;
}
$pclause = "r.pid IN ('" . join( "','", @$projlist ) . "') AND ";
}
my $query_result = DBQueryFatal(
......@@ -1124,9 +1129,11 @@ sub CurrentReservations($$) {
"(SELECT COUNT(*) FROM nodes AS n, reserved AS res " .
"WHERE n.node_id=res.node_id AND res.pid='emulab-ops' AND " .
"res.eid IN ('reloading', 'reloadpending')) " .
"FROM future_reservations AS r WHERE r.pid IN ('" .
join( "','", @$projlist ) .
"') AND r.approved IS NOT NULL GROUP BY r.pid, r.type" );
"FROM future_reservations AS r WHERE $pclause ".
"r.approved IS NOT NULL ".
($activeonly ?
"AND r.start < NOW() AND r.end > NOW() " : " ") .
"GROUP BY r.pid, r.type" );
while( my($pid, $type, $reserved, $used, $ready, $reloading) =
$query_result->fetchrow_array() ) {
......@@ -1168,6 +1175,115 @@ sub UpcomingReservations($$) {
return @answer;
}
#
# Return a list of project reservations. Optional type and activeonly.
# Type can be a single type or a list reference of types.
#
sub ProjectReservations($$$;$$) {
my ($class, $project, $user, $type, $activeonly) = @_;
my $typeclause = "";
my $activeclause = "";
my $userclause = "";
my $pidclause = "";
my @answer = ();
if (defined($type)) {
if (ref($type)) {
my @types = @{$type};
$typeclause = "AND type IN ('" . join( "','", @types ) . "')" ;
}
else {
$typeclause = "AND type='$type'";
}
}
if (defined($activeonly) && $activeonly) {
$activeclause = "AND start < NOW() AND end > NOW()";
}
if (defined($user)) {
my $uid = $user->uid();
$userclause = "AND uid='$uid'";
}
if (defined($project)) {
my $pid = ref($project) ? $project->pid() : $project;
$pidclause = "AND pid='$pid'";
}
my $query_result =
DBQueryFatal("select idx from future_reservations ".
"where approved IS NOT NULL $pidclause ".
" $typeclause $activeclause $userclause ".
"order by created desc");
while (my ($idx) = $query_result->fetchrow_array()) {
my $res = Reservation->Lookup($idx);
push(@answer, $res)
if (defined($res));
}
return @answer;
}
#
# Return a list of historical project reservations. Optional type.
# Type can be a single type or a list reference of types.
#
sub HistoricalReservations($$$;$) {
my ($class, $project, $user, $type) = @_;
my $pid;
my @clauses = ();
my @answer = ();
if (defined($type)) {
if (ref($type)) {
my @types = @{$type};
push(@clauses,
"type IN ('" . join( "','", @types ) . "')");
}
else {
push(@clauses, "type='$type'");
}
}
if (defined($user)) {
my $uid = $user->uid();
push(@clauses, "uid='$uid'");
}
if (defined($project)) {
my $pid = $project->pid();
push(@clauses, "pid='$pid'");
}
my $query_result =
DBQueryFatal("select *,UNIX_TIMESTAMP(start) AS s, " .
" UNIX_TIMESTAMP(end), AS e ".
" UNIX_TIMESTAMP(created) AS c, " .
" UNIX_TIMESTAMP(canceled) AS d, " .
" UNIX_TIMESTAMP(deleted) AS k " .
" from reservation_history ".
"where " . join(" AND ", @clauses) . " " .
"order by start asc");
while (my $record = $query_result->fetchrow_hashref()) {
my $self = {};
$self->{'PID'} = $record->{'pid'};
$self->{'PID_IDX'} = $record->{'pid_idx'};
$self->{'EID'} = undef;
$self->{'START'} = $record->{'s'};
$self->{'END'} = $record->{'e'};
$self->{'CREATED'} = $record->{'c'};
$self->{'DELETED'} = $record->{'k'};
$self->{'CANCEL'} = $record->{'d'};
$self->{'TYPE'} = $record->{'type'};
$self->{'NODES'} = $record->{'nodes'};
$self->{'UID'} = $record->{'uid'};
$self->{'UID_IDX'} = $record->{'uid_idx'};
$self->{'NOTES'} = $record->{'notes'};
$self->{'ADMIN_NOTES'} = $record->{'admin_notes'};
$self->{'APPROVED'} = undef;
$self->{'APPROVER'} = undef;
$self->{'UUID'} = $record->{'uuid'};
bless($self, $class);
push(@answer, $self);
}
return @answer;
}
sub ExptTypes($) {
my ($exptidx) = @_;
......
......@@ -51,6 +51,7 @@ use GeniCM;
use GeniHRN;
use GeniUtil;
use Reservation;
use ResUtil;
use libadminctrl;
use GeniCredential;
use GeniStd;
......@@ -692,11 +693,101 @@ sub SliceMaxExtension($)
if (Reservation->MaxSliceExtension($slice, \$max, \$reserror, 3600)) {
Reservation->FlushAll();
return GeniResponse->Create(GENIRESPONSE_REFUSED, undef, $reserror);
return GeniResponse->Create(GENIRESPONSE_ERROR, undef, $reserror);
}
Reservation->FlushAll();
$max = TBDateStringGMT($max);
return GeniResponse->Create(GENIRESPONSE_SUCCESS, $max);
my $blob = {"maxextension" => TBDateStringGMT($max)};
my $project = Project->Lookup($experiment->pid());
if (!defined($project)) {
return GeniResponse->Create(GENIRESPONSE_REFUSED, undef,
"Cannot find project for experiment");
}
#
# See if we can locate an approved/active reservation that is
# "associated" with this experiment. Note that this is somewhat
# ambiguous since reservations are by project/nodetype, not for
# specific users. And there can be multiple reservations per
# project, by multiple users.
#
my %reslist = ();
my $pidlist = [$experiment->pid()];
my @typelist = $experiment->TypesInUse();
my @curres = Reservation->CurrentReservations($pidlist, 1);
my %current = ();
if (@curres) {
foreach my $ref (@curres) {
if (grep {$_ eq $ref->{'nodetype'}} @typelist) {
my $type = $ref->{'nodetype'};
#
# We have active reservation(s) for this pid/type. We
# want to return into about those reservations to the
# caller.
#
my @projres =
Reservation->ProjectReservations($experiment->pid(), undef,
$type, 1);
if (!exists($current{$type})) {
$current{$type} = $project->TypeInUseCount($type);
}
foreach my $res (@projres) {
my $uid = $res->uid();
# we know these are active reservations.
my $using = $current{$type};
if ($using >= $res->nodes()) {
$using = $res->nodes();
$current{$type} -= $using;
}
my @timeline = ();
if ($res->approved() && $res->start() < time()) {
@timeline = ResUtil::CreateTimeline($project, $res);
# We do not want to send back more then we need.
if (@timeline) {
@timeline = map {
{"t" => $_->{'t'},
"allocated" => $_->{'allocated'} || {},
"reserved" => $_->{'reserved'} || {},
} } @timeline;
}
}
my $user = User->Lookup($uid);
if (!defined($user)) {
print STDERR "maxextension: No such user $uid\n";
next;
}
my $blob = {};
$blob->{"uuid"} = $res->uuid();
$blob->{"project"} = $project->nonlocalurn();
$blob->{"user"} = $user->nonlocalurn();
$blob->{"nodes"} = $res->nodes();
$blob->{"using"} = $using;
$blob->{"type"} = $res->type();
$blob->{"created"} = TBDateStringGMT($res->created());
$blob->{"start"} = TBDateStringGMT($res->start());
$blob->{"end"} = TBDateStringGMT($res->end());
$blob->{"notes"} = $res->notes() || "";
$blob->{"approved"} = "";
if (defined($res->approved())) {
$blob->{"approved"} =
TBDateStringGMT($res->approved());
}
if (defined($res->cancel())) {
$blob->{"cancel"} = TBDateStringGMT($res->cancel());
}
$blob->{"history"} = \@timeline;
$reslist{$res->uuid()} = $blob;
}
}
}
}
$blob->{'reservations'} = \%reslist;
return GeniResponse->Create(GENIRESPONSE_SUCCESS, $blob);
}
#
......@@ -1049,7 +1140,8 @@ sub Reservations($)
" UNIX_TIMESTAMP(cancel) as cancel ".
" from future_reservations ".
"where pid='$pid' ".
(defined($target) ? "and $target" : ""));
(defined($target) ? "and $target " : "").
"order BY start");
}
else {
my $uid = $geniuser->uid();
......@@ -1060,7 +1152,8 @@ sub Reservations($)
" UNIX_TIMESTAMP(cancel) as cancel ".
" from future_reservations ".
"where uid='$uid' ".
(defined($target) ? "and $target" : ""));
(defined($target) ? "and $target " : "").
"order BY start");
}
}
return GeniResponse->Create(GENIRESPONSE_ERROR)
......@@ -1073,7 +1166,8 @@ sub Reservations($)
# how many are actually reserved now.
my %current = ();
# Project usage numbers;
my %history = ();
my %projusage = ();
my %projects = ();
while (my $row = $query_result->fetchrow_hashref()) {
my $blob = {};
......@@ -1090,6 +1184,8 @@ sub Reservations($)
next;
}
my $projurn = $project->nonlocalurn();
# Remember for usage numbers below.
$projects{$projurn} = $project;
my $user = User->Lookup($uid);
if (!defined($user)) {
......@@ -1106,10 +1202,36 @@ sub Reservations($)
$current{"$pid:$type"} -= $using;
}
}
my $pstart;
my @timeline = ();
my $reservation = Reservation->Lookup($idx);
next
if (!defined($reservation));
if ($reservation->approved() && $reservation->start() < time()) {
@timeline = ResUtil::CreateTimeline($project, $reservation);
# We do not want to send back more then we need.
if (@timeline) {
@timeline = map { {"t" => $_->{'t'},
"allocated" => $_->{'allocated'} || {},
"reserved" => $_->{'reserved'} || {},
} } @timeline;
}
#print STDERR Dumper(@timeline);
# We want experiment history back to the beginning of the res.
$pstart = $reservation->start() - (3600 * 24);
}
else {
$pstart = time() - (3600 * 24 * 4);
}
# We want proj usage starting at earliest reservation start.
if (!exists($projusage{$projurn}) || $pstart < $projusage{$projurn}) {
$projusage{$projurn} = $pstart;
}
$blob->{"idx"} = $idx;
$blob->{"uuid"} = $uuid;
$blob->{"project"} = $projurn,
$blob->{"project"} = $projurn;
$blob->{"user"} = $user->nonlocalurn();
$blob->{"nodes"} = $row->{'nodes'};
$blob->{"type"} = $type;
......@@ -1118,19 +1240,26 @@ sub Reservations($)
$blob->{"start"} = TBDateStringGMT($row->{'start'});
$blob->{"end"} = TBDateStringGMT($row->{'end'});
$blob->{"notes"} = $row->{'notes'} || "";
$blob->{"approved"} = $row->{'approved'} || "";
$blob->{"approved"} = "";
if (defined($row->{'approved'})) {
$blob->{"approved"} = TBDateStringGMT($row->{'approved'});
}
if (defined($row->{'cancel'})) {
$blob->{"cancel"} = TBDateStringGMT($row->{'cancel'});
}
$blob->{"history"<