Commit f71d7d95 authored by Leigh B Stoller's avatar Leigh B Stoller

Add extension limiting to manage_extensions and request extension paths.

The limit is the number of hours since the experiment is created, so a
limit of 10 days really just means that experiments can not live past 10
days. I think this makes more sense then anything else. There is an
associated flag with extension limiting that controls whether the user
can even request another extension after the limit. The normal case is
that the user cannot request any more extensions, but when set, the user
is granted no free time and goes through need admin approval path.

Some changes to the email, so that both the user and admin email days
how many days/hours were both requested and granted.

Also UI change; explicitly tell the user when extensions are disabled,
and also when no time is granted (so that the users is more clearly
aware).
parent c751377d
...@@ -398,7 +398,10 @@ sub Update($$) ...@@ -398,7 +398,10 @@ sub Update($$)
my $uuid = $self->uuid(); my $uuid = $self->uuid();
my $query = "update apt_instances set ". my $query = "update apt_instances set ".
join(",", map("$_=" . DBQuoteSpecial($argref->{$_}), keys(%{$argref}))); join(",", map("$_=" .
(defined($argref->{$_}) ?
DBQuoteSpecial($argref->{$_}) : "NULL"),
keys(%{$argref})));
$query .= " where uuid='$uuid'"; $query .= " where uuid='$uuid'";
...@@ -1195,79 +1198,113 @@ sub ApplyExtensionPolicies($) ...@@ -1195,79 +1198,113 @@ sub ApplyExtensionPolicies($)
my $pid_idx = $self->pid_idx(); my $pid_idx = $self->pid_idx();
my $gid_idx = $self->gid_idx(); my $gid_idx = $self->gid_idx();
my $uid_idx = $self->creator_idx(); my $uid_idx = $self->creator_idx();
my $current = $self->extension_disabled(); my $current_disable = $self->extension_disabled();
my $current_limit = $self->extension_limit();
my $policy; my $policy;
my $disabled = 0; my $disabled = 0;
my $limit = 0;
my $admin_after_limit = 0;
my $reason; my $reason;
# #
# Apply in order project, group, then user. # Apply in order project, group, then user.
# #
my $query_result = my $query_result =
DBQueryWarn("select disabled,reason from apt_extension_group_policies ". DBQueryWarn("select disabled,reason,`limit`,admin_after_limit ".
" from apt_extension_group_policies ".
"where pid_idx='$pid_idx' and gid_idx=pid_idx"); "where pid_idx='$pid_idx' and gid_idx=pid_idx");
return -1 return -1
if (!defined($query_result)); if (!defined($query_result));
if ($query_result->numrows) { if ($query_result->numrows) {
($disabled,$reason) = $query_result->fetchrow_array(); ($disabled,$reason,
if ($disabled && !defined($reason)) { $limit,$admin_after_limit) = $query_result->fetchrow_array();
if (($disabled || $limit) && !defined($reason)) {
$reason = "project restriction"; $reason = "project restriction";
} }
$policy = "Project"; $policy = "Project";
} }
$query_result = $query_result =
DBQueryWarn("select disabled,reason from apt_extension_group_policies ". DBQueryWarn("select disabled,reason,`limit`,admin_after_limit ".
" from apt_extension_group_policies ".
"where pid_idx='$pid_idx' and gid_idx='$gid_idx'"); "where pid_idx='$pid_idx' and gid_idx='$gid_idx'");
return -1 return -1
if (!defined($query_result)); if (!defined($query_result));
if ($query_result->numrows) { if ($query_result->numrows) {
my ($d,$r) = $query_result->fetchrow_array(); my ($d,$r,$l,$admin_after_limit) = $query_result->fetchrow_array();
if ($d) { $disabled = $d;
$disabled = 1; $limit = $l;
$reason = (defined($r) ? $r : "group restriction"); $reason = $r;
} $policy = "Group";
else { if (($disabled || $limit) && !defined($reason)) {
$disabled = 0; $reason = "group restriction";
$reason = undef;
} }
$policy = "Group";
} }
$query_result = $query_result =
DBQueryWarn("select disabled,reason from apt_extension_user_policies ". DBQueryWarn("select disabled,reason,`limit`,admin_after_limit ".
" from apt_extension_user_policies ".
"where uid_idx='$uid_idx'"); "where uid_idx='$uid_idx'");
return -1 return -1
if (!defined($query_result)); if (!defined($query_result));
if ($query_result->numrows) { if ($query_result->numrows) {
my ($d,$r) = $query_result->fetchrow_array(); my ($d,$r,$l,$admin_after_limit) = $query_result->fetchrow_array();
if ($d) { $disabled = $d;
$disabled = 1; $limit = $l;
$reason = (defined($r) ? $r : "user restriction"); $reason = $r;
$policy = "User";
if (($disabled || $limit) && !defined($reason)) {
$reason = "user restriction";
} }
else {
$disabled = 0;
$reason = undef;
}
$policy = "User";
} }
# Apply disabled flag # Apply flags
$self->Update({"extension_disabled" => $disabled}) == 0 $self->Update({"extension_disabled" => $disabled,
or return -1; "extension_limit" => $limit,
"extension_admin_after_limit" => $admin_after_limit})
== 0 or return -1;
# Set the reason only if disabled, clear otherwise. # Set the reason only if disabled, clear otherwise.
if ($disabled && defined($reason)) { if ($disabled) {
$self->Update({"extension_disabled_reason" => $reason}) == 0 $self->Update({"extension_disabled_reason" => $reason}) == 0
or return -1;
}
else {
$self->Update({"extension_disabled_reason" => undef}) == 0
or return -1;
}
# Ditto the limit.
if ($limit) {
$self->Update({"extension_limit_reason" => $reason}) == 0
or return -1; or return -1;
} }
else { else {
DBQueryWarn("update apt_instances set extension_disabled_reason=NULL ". $self->Update({"extension_limit_reason" => undef}) == 0
"where uuid='$uuid'")
or return -1; or return -1;
} }
if ($disabled != $current) { #
my $which = ($disabled ? "disabled" : "enabled"); # Basically, disable overrides limit when requesting an extension.
#
$current_limit = -1 if (!defined($current_limit));
$limit = -1 if (!defined($limit));
if ($disabled != $current_disable || $limit != $current_limit) {
my $message = "$policy policy applied to $pid/$name:\n";
if ($disabled != $current_disable) {
my $which = ($disabled ? "disabled" : "enabled");
$message .= "Extensions have been ${which}.\n";
}
if ($limit != $current_limit) {
my $which = ($limit ? "limited to $limit days" :
"set to unlimited");
$message .= "Extensions have been ${which}.\n";
if ($limit && $admin_after_limit) {
$message .=
"Additional extension requests will require ".
"admin approval.\n";
}
}
SENDMAIL($TBAUDIT, SENDMAIL($TBAUDIT,
"Portal experiment $uuid extensions $which", "Portal experiment $uuid extension policy changed",
"$policy policy has $which extensions for $pid/$name\n\n". $message . "\n" .
(defined($reason) ? "Reason:\n$reason\n\n" : ""). (defined($reason) ? "Reason:\n$reason\n\n" : "").
$self->adminURL() . "\n", $self->adminURL() . "\n",
$TBOPS); $TBOPS);
...@@ -1275,6 +1312,34 @@ sub ApplyExtensionPolicies($) ...@@ -1275,6 +1312,34 @@ sub ApplyExtensionPolicies($)
return 0; return 0;
} }
#
# Convert hours to days and hours string.
#
sub HoursToEnglish($)
{
my ($hours) = @_;
my $string;
my $wdays = int($hours / 24);
my $whours = $hours % 24;
if ($wdays) {
$string = "$wdays day";
$string .= "s" if ($wdays > 1);
if ($whours) {
$string .= " ${whours} hour";
$string .= "s" if ($whours > 1);
}
}
elsif ($whours) {
$string = "$whours hour";
$string .= "s" if ($whours > 1);
}
else {
$string = "nothing";
}
return $string;
}
################################################################### ###################################################################
package APT_Instance::ExtensionInfo; package APT_Instance::ExtensionInfo;
use emdb; use emdb;
......
...@@ -38,14 +38,20 @@ sub usage() ...@@ -38,14 +38,20 @@ sub usage()
" manage_extensions disable [-a] [-m reason]". " manage_extensions disable [-a] [-m reason]".
" -p pid | -g pid,gid | -u uid\n". " -p pid | -g pid,gid | -u uid\n".
" manage_extensions enable [-a] -p pid | -g pid,gid | -u uid\n". " manage_extensions enable [-a] -p pid | -g pid,gid | -u uid\n".
" manage_extensions limit [-a] [-m reason] -A ".
" -p pid | -g pid,gid | -u uid <days>\n".
" manage_extensions unlimit [-a] -p pid | -g pid,gid | -u uid\n".
" manage_extensions remove [-a] -p pid | -g pid,gid | -u uid\n". " manage_extensions remove [-a] -p pid | -g pid,gid | -u uid\n".
" manage_extensions show [-p pid | -u uid]\n". " manage_extensions show [-p pid | -u uid]\n".
" manage_extensions apply [-p pid | -u uid]\n".
"Options:\n". "Options:\n".
" -p - Apply to entire project.\n". " -p - Apply to entire project.\n".
" -g - Apply to project group. \n". " -g - Apply to project group. \n".
" -u - Apply to user.\n". " -u - Apply to user.\n".
" -m - Short explanation if desired.\n". " -m - Short explanation if desired.\n".
" -a - Apply changes to matching experiments.\n" " -a - Apply changes to matching experiments.\n".
" -A - For 'limit', extensions require admin approval\n".
" instead of being outright denied.\n"
); );
exit(-1); exit(-1);
} }
...@@ -87,8 +93,9 @@ use APT_Instance; ...@@ -87,8 +93,9 @@ use APT_Instance;
# Protos # Protos
sub fatal($); sub fatal($);
sub DoEnableDisable($); sub DoAction($);
sub DoShow(); sub DoShow();
sub DoApply();
sub Apply($); sub Apply($);
# Locals # Locals
...@@ -118,12 +125,16 @@ if (!$this_user->IsAdmin()) { ...@@ -118,12 +125,16 @@ if (!$this_user->IsAdmin()) {
fatal("Only admins can do this"); fatal("Only admins can do this");
} }
if ($action eq "disable" || $action eq "enable" || $action eq "remove") { if ($action eq "disable" || $action eq "enable" || $action eq "remove" ||
DoEnableDisable($action); $action eq "limit" || $action eq "unlimit") {
DoAction($action);
} }
elsif ($action eq "show") { elsif ($action eq "show") {
DoShow(); DoShow();
} }
elsif ($action eq "apply") {
DoApply();
}
else { else {
usage(); usage();
} }
...@@ -132,11 +143,11 @@ exit(0); ...@@ -132,11 +143,11 @@ exit(0);
# #
# Enable/Disable extensions. # Enable/Disable extensions.
# #
sub DoEnableDisable($) sub DoAction($)
{ {
my ($action) = @_; my ($action) = @_;
my $optlist = "ap:g:u:m:"; my $optlist = "ap:g:u:m:A";
my $apply = 0; my $apply = 0;
my $target; my $target;
my $reason; my $reason;
...@@ -197,7 +208,33 @@ sub DoEnableDisable($) ...@@ -197,7 +208,33 @@ sub DoEnableDisable($)
} }
} }
else { else {
my $disabled = ($action eq "disable" ? 1 : 0); my $clause = "";
if ($action eq "enable" || $action eq "disable") {
my $disabled = ($action eq "disable" ? 1 : 0);
$clause = "disabled='$disabled'";
}
elsif ($action eq "limit" || $action eq "unlimit") {
# When setting a limit, clear the disable flag.
$clause = "disabled='0',`limit`=";
if ($action eq "unlimit") {
$clause .= "'0'";
}
else {
usage()
if (!@ARGV);
my $days = shift(@ARGV);
usage()
if ($days !~ /^\d+$/);
$clause .= "'" . $days * 24 . "'";
if (defined($options{"A"})) {
$clause .= ",admin_after_limit='1'";
}
else {
$clause .= ",admin_after_limit='0'";
}
}
}
my $reason_clause = ""; my $reason_clause = "";
if (defined($reason)) { if (defined($reason)) {
$reason_clause = "reason=" . DBQuoteSpecial($reason) . ", "; $reason_clause = "reason=" . DBQuoteSpecial($reason) . ", ";
...@@ -207,7 +244,7 @@ sub DoEnableDisable($) ...@@ -207,7 +244,7 @@ sub DoEnableDisable($)
my $pid = $target->pid(); my $pid = $target->pid();
my $pid_idx = $target->pid_idx(); my $pid_idx = $target->pid_idx();
DBQueryFatal("replace into apt_extension_group_policies set ". DBQueryFatal("replace into apt_extension_group_policies set ".
" disabled='$disabled', created=now(), ". " $clause, created=now(), ".
" creator='$creator', creator_idx='$creator_idx', ". " creator='$creator', creator_idx='$creator_idx', ".
" $reason_clause ". " $reason_clause ".
" pid='$pid', gid='$pid', ". " pid='$pid', gid='$pid', ".
...@@ -219,7 +256,7 @@ sub DoEnableDisable($) ...@@ -219,7 +256,7 @@ sub DoEnableDisable($)
my $gid = $target->gid(); my $gid = $target->gid();
my $gid_idx = $target->gid_idx(); my $gid_idx = $target->gid_idx();
DBQueryFatal("replace into apt_extension_group_policies set ". DBQueryFatal("replace into apt_extension_group_policies set ".
" disabled='$disabled', created=now(), ". " $clause, created=now(), ".
" creator='$creator', creator_idx='$creator_idx', ". " creator='$creator', creator_idx='$creator_idx', ".
" $reason_clause ". " $reason_clause ".
" pid='$pid', gid='$gid', ". " pid='$pid', gid='$gid', ".
...@@ -229,7 +266,7 @@ sub DoEnableDisable($) ...@@ -229,7 +266,7 @@ sub DoEnableDisable($)
my $uid = $target->uid(); my $uid = $target->uid();
my $uid_idx = $target->uid_idx(); my $uid_idx = $target->uid_idx();
DBQueryFatal("replace into apt_extension_user_policies set ". DBQueryFatal("replace into apt_extension_user_policies set ".
" disabled='$disabled', created=now(), ". " $clause, created=now(), ".
" creator='$creator', creator_idx='$creator_idx', ". " creator='$creator', creator_idx='$creator_idx', ".
" $reason_clause ". " $reason_clause ".
" uid_idx='$uid_idx', uid='$uid'"); " uid_idx='$uid_idx', uid='$uid'");
...@@ -334,6 +371,36 @@ sub DoShow() ...@@ -334,6 +371,36 @@ sub DoShow()
exit(0); exit(0);
} }
#
# Apply policies to target.
#
sub DoApply()
{
my $optlist = "p:u:";
my $target;
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"p"})) {
$target = Project->Lookup($options{"p"});
fatal("No such project")
if (!defined($target));
}
elsif (defined($options{"u"})) {
$target = User->Lookup($options{"u"});
fatal("No such user")
if (!defined($target));
}
else {
usage();
}
Apply($target);
return 0;
}
sub Apply($) sub Apply($)
{ {
my ($target) = @_; my ($target) = @_;
...@@ -356,7 +423,10 @@ sub Apply($) ...@@ -356,7 +423,10 @@ sub Apply($)
my $instance = APT_Instance->Lookup($uuid); my $instance = APT_Instance->Lookup($uuid);
next next
if (!defined($instance)); if (!defined($instance));
my $current = $instance->extension_disabled(); my $current_disabled = $instance->extension_disabled();
my $current_limit = $instance->extension_limit();
# Perl is stupid.
$current_limit = -1 if (!defined($current_limit));
if ($instance->ApplyExtensionPolicies()) { if ($instance->ApplyExtensionPolicies()) {
print STDERR "Could not apply extension policies to $instance\n"; print STDERR "Could not apply extension policies to $instance\n";
...@@ -365,12 +435,19 @@ sub Apply($) ...@@ -365,12 +435,19 @@ sub Apply($)
$instance->Refresh(); $instance->Refresh();
my $disabled = ($instance->extension_disabled() ? my $disabled = ($instance->extension_disabled() ?
"disabled" : "enabled"); "disabled" : "enabled");
my $which = ($current != $instance->extension_disabled() ? my $limit = $instance->extension_limit();
"now" : "still"); $limit = -1 if (!defined($limit));
my $pid = $instance->pid(); my $limited = ($limit > 0 ?
my $name = $instance->name(); "limited to " . APT_Instance::HoursToEnglish($limit) :
"unlimited");
my $pid = $instance->pid();
my $name = $instance->name();
my $which1 = ($current_disabled != $instance->extension_disabled() ?
"now" : "still");
my $which2 = ($current_limit != $limit ? "now" : "still");
print "Extensions for $pid/$name are $which $disabled\n"; print "Extensions for $pid/$name are $which1 $disabled\n";
print "Extensions for $pid/$name are $which2 $limited\n";
} }
} }
......
...@@ -1491,7 +1491,7 @@ sub DoExtend() ...@@ -1491,7 +1491,7 @@ sub DoExtend()
# Need to return this to the web interface via the webtask. # Need to return this to the web interface via the webtask.
return "Your request requires admininstrator approval". return "Your request requires admininstrator approval".
($message ? " because $message" : "") . ". " . ($message ? " $message" : "") . ". " .
"You will receive email if/when your ". "You will receive email if/when your ".
"request is granted (or denied). Thanks!"; "request is granted (or denied). Thanks!";
}; };
...@@ -1505,9 +1505,9 @@ sub DoExtend() ...@@ -1505,9 +1505,9 @@ sub DoExtend()
} }
# #
# Always grant if <= 24 hours. # Always grant if < 24 hours, unless an extension limit.
# #
if ($wanted <= 24) { if ($wanted < 24 && !$instance->extension_limit()) {
$message = "Short extension granted for $wanted hours."; $message = "Short extension granted for $wanted hours.";
$reason = $message if (!defined($reason)); $reason = $message if (!defined($reason));
$granted = $wanted; $granted = $wanted;
...@@ -1520,7 +1520,7 @@ sub DoExtend() ...@@ -1520,7 +1520,7 @@ sub DoExtend()
$granted = $wanted; $granted = $wanted;
} }
elsif (0) { elsif (0) {
$message = "Testing extension stuff"; $message = "because we are testing extension stuff";
$granted = 0; $granted = 0;
$needapproval = 1; $needapproval = 1;
} }
...@@ -1529,7 +1529,9 @@ sub DoExtend() ...@@ -1529,7 +1529,9 @@ sub DoExtend()
my $cdiff = time() - $created_time; my $cdiff = time() - $created_time;
if (! defined($reason)) { if (! defined($reason)) {
fatal("You must supply a reason for this extension"); $errcode = -1;
$errmsg = "You must supply a reason for this extension";
goto bad;
} }
# #
...@@ -1540,6 +1542,57 @@ sub DoExtend() ...@@ -1540,6 +1542,57 @@ sub DoExtend()
$granted = 0; $granted = 0;
} }
# #
# Is there an extension limit.
#
elsif ($instance->extension_limit()) {
my $diff = $expires_time - $created_time;
my $max = 3600 * $instance->extension_limit();
if ($diff >= $max) {
#
# Already out to the limit.
#
$granted = 0;
}
else {
#
# Give them up to as much as the extension limits them to.
# Note that this could turn out to be zero.
#
if ($diff + ($wanted * 3600) > $max) {
$granted = int(($max - $diff) / 3600);
$message = "because your extension is limited by local ".
"policy to " . APT_Instance::HoursToEnglish($granted);
}
else {
$granted = $wanted;
}
}
if ($granted == 0) {
if (defined($instance->extension_limit_reason())) {
$message = $instance->extension_limit_reason();
}
else {
$message = "because you have exceeded the maximum ".
"extension allowed by local policy";
}
#
# Mark as admin disabled now so they do not get to ask again,
# but not if admin_after_limit is set, then it goes through
# the need admin approval path.
#
if ($instance->extension_admin_after_limit()) {
$needapproval = 1;
}
else {
$instance->Update({"extension_disabled" => 1,
"extension_disabled_reason" =>$message});
}
}
# Avoid any change to granted below.
goto grant;
}
#
# After maxage, all extension requests require admin approval. # After maxage, all extension requests require admin approval.
# #
elsif ($cdiff > (3600 * 24 * $autoextend_maxage)) { elsif ($cdiff > (3600 * 24 * $autoextend_maxage)) {
...@@ -1642,31 +1695,8 @@ sub DoExtend() ...@@ -1642,31 +1695,8 @@ sub DoExtend()
localtime($expires_time)); localtime($expires_time));
# Format hours into days/hours. # Format hours into days/hours.
my $wdays = int($wanted / 24); $wantstring = APT_Instance::HoursToEnglish($wanted);
my $whours = $wanted % 24; $grantstring = APT_Instance::HoursToEnglish($granted);
my $gdays = int($granted / 24);
my $ghours = $granted % 24;
if ($wdays) {
$wantstring = "$wdays days";
if ($whours) {
$wantstring .= " ${whours} hours";
}
}
else {
$wantstring = "$whours hours";
}
if ($gdays) {