Commit a3e565f4 authored by Leigh B Stoller's avatar Leigh B Stoller
Browse files

Big cleanup of the "quickvm" script, in that everything except the create

function has been moved into manage_instance and its associated library
(APT_Instance). Lots of cleanup of the code and more use of webtasks to
communicate with the web server.
parent 7bcc61cb
......@@ -25,6 +25,9 @@ package APT_Instance;
use strict;
use Carp;
use English;
use Data::Dumper;
use POSIX qw(tmpnam);
use Exporter;
use vars qw(@ISA @EXPORT $AUTOLOAD);
......@@ -37,13 +40,17 @@ use emdb;
use libtestbed;
use Brand;
use APT_Profile;
use English;
use Data::Dumper;
use APT_Geni;
use Genixmlrpc;
use GeniResponse;
use GeniCertificate;
use GeniHRN;
use overload ('""' => 'Stringify');
# Configure variables
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $GENEXTENDCRED = "$TB/sbin/protogeni/genextendcred";
# Cache of instances to avoid regenerating them.
my %instances = ();
......@@ -84,7 +91,7 @@ sub Lookup($$;$)
AUTOLOAD {
my $self = $_[0];
my $type = ref($self) or croak "$self is not an object";
my $type = ref($self) or confess "$self is not an object";
my $name = $AUTOLOAD;
$name =~ s/.*://; # strip fully-qualified portion
......@@ -187,15 +194,30 @@ sub Stringify($)
sub LookupBySlice($$)
{
my ($class, $slice_uuid) = @_;
my ($class, $token) = @_;
my $slice_uuid;
if ($slice_uuid !~ /^\w+\-\w+\-\w+\-\w+\-\w+$/) {
if ($token =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/) {
$slice_uuid = $token;
}
elsif (GeniHRN::IsValid($token)) {
#
# We should put the slice_urn into the apt_instances table.
#
require GeniSlice;
my $slice = GeniSlice->Lookup($token);
return undef
if (!defined($slice));
$slice_uuid = $slice->uuid();
}
else {
return undef;
}
my $query_result =
DBQueryWarn("select uuid from apt_instances ".
"where slice_uuid='$slice_uuid'");
return undef
if (!$query_result || !$query_result->numrows);
......@@ -311,6 +333,33 @@ sub RecordHistory($)
return 0;
}
#
# Locate Geni objects for creator and slice.
#
sub GetGeniSlice($)
{
my ($self) = @_;
require GeniSlice;
return GeniSlice->Lookup($self->slice_uuid());
}
sub GetGeniUser($)
{
my ($self) = @_;
require GeniUser;
return GeniUser->Lookup($self->creator_uuid(), 1);
}
sub GetGeniAuthority($)
{
my ($self) = @_;
require GeniAuthority;
return APT_Geni::GetAuthority($self->aggregate_urn());
}
#
# Warn creator that the experiment is going to expire. This is hooked
# in from the sa_daemon, so we can send a message that is less geni like
......@@ -353,5 +402,234 @@ sub WarnExpiring($$)
return 0;
}
#
# Ask aggregate for the console URL for a node.
#
sub ConsoleURL($$)
{
my ($self, $sliver_urn) = @_;
my $authority = $self->GetGeniAuthority();
my $geniuser = $self->GetGeniUser();
my $slice = $self->GetGeniSlice();
my $context = APT_Geni::GeniContext();
return undef
if (! (defined($geniuser) && defined($authority) &&
defined($slice) && defined($context)));
my ($slice_credential, $speaksfor_credential) =
APT_Geni::GenCredentials($slice, $geniuser);
return undef
if (! (defined($speaksfor_credential) &&
defined($slice_credential)));
my $args = {
"slice_urn" => $slice->urn(),
"sliver_urn" => $sliver_urn,
"credentials" => [$slice_credential->asString(),
$speaksfor_credential->asString()],
};
return Genixmlrpc::CallMethod($authority->url(),
$context, "ConsoleURL", $args);
}
#
# Ask aggregate to terminate.
#
sub Terminate($)
{
my ($self) = @_;
my $authority = $self->GetGeniAuthority();
my $geniuser = $self->GetGeniUser();
my $slice = $self->GetGeniSlice();
my $context = APT_Geni::GeniContext();
return undef
if (! (defined($geniuser) && defined($authority) &&
defined($slice) && defined($context)));
my ($slice_credential, $speaksfor_credential) =
APT_Geni::GenCredentials($slice, $geniuser);
return undef
if (! (defined($speaksfor_credential) &&
defined($slice_credential)));
my $args = {
"slice_urn" => $slice->urn(),
"credentials" => [$slice_credential->asString(),
$speaksfor_credential->asString()],
};
#
# We have to watch for resource busy errors, and retry. For a while
# at least. Eventually give up cause it might be a permanently locked
# slice cause of earlier error.
#
my $response;
my $tries = 10;
while ($tries) {
$response =
Genixmlrpc::CallMethod($authority->url(),
$context, "DeleteSlice", $args);
# SEARCHFAILED is success.
return $response
if ($response->code() == GENIRESPONSE_SUCCESS ||
$response->code() == GENIRESPONSE_SEARCHFAILED);
return $response
if ($response->code() != GENIRESPONSE_BUSY);
#
# Wait for a while and try again.
#
$tries--;
if ($tries) {
print STDERR "Slice is busy, will retry again in a bit ...\n";
sleep(30);
}
}
return $response;
}
#
# Ask to extend.
#
sub Extend($$)
{
my ($self, $new_expires) = @_;
my $authority = $self->GetGeniAuthority();
my $geniuser = $self->GetGeniUser();
my $slice = $self->GetGeniSlice();
my $context = APT_Geni::GeniContext();
return undef
if (! (defined($geniuser) && defined($authority) &&
defined($slice) && defined($context)));
#
# We need a special credentential in case the aggregate is enforcing
# limits (as do Utah aggregates).
#
my $slice_urn = $slice->urn();
my $extcred = "";
my $credname = tmpnam();
my $userarg = "-u " . $geniuser->urn();
my ($fh,$certfile);
#
# But if a nonlocal user from Geni, then the user we have in the database
# is not in the same domain as the speaksfor, so we use the geni certificate
# that the trusted signer gave us and is stored in the DB.
#
if ($geniuser->IsLocal() && $geniuser->emulab_user()->IsNonLocal()) {
my (undef, $certificate_string) =
$geniuser->emulab_user()->GetStoredCredential();
if (! defined($certificate_string)) {
print STDERR "Could not get stored certificate for $geniuser\n";
return undef;
}
my $certificate = GeniCertificate->LoadFromString($certificate_string);
if (!defined($certificate)) {
print STDERR "Could not load stored certificate for $geniuser\n";
return undef;
}
$certfile = $certificate->WriteToFile();
$userarg = "-c $certfile";
}
system("$GENEXTENDCRED -a -o $credname -s $slice_urn -t 90 $userarg");
if ($?) {
print STDERR "Could not create extended credential\n";
return undef;
}
if (!open(EXT, $credname)) {
print STDERR "Could not open ext credfile $credname\n";
return undef;
}
while (<EXT>) {
$extcred .= $_;
}
close(EXT);
unlink($credname);
chomp($extcred);
my ($slice_credential, $speaksfor_credential) =
APT_Geni::GenCredentials($slice, $geniuser);
return undef
if (! (defined($speaksfor_credential) &&
defined($slice_credential)));
my $args = {
"slice_urn" => $slice->urn(),
"expiration" => $new_expires,
"credentials" => [$slice_credential->asString(),
$speaksfor_credential->asString(),
$extcred],
};
return Genixmlrpc::CallMethod($authority->url(),
$context, "RenewSlice", $args);
}
#
# Create an Image,
#
sub CreateImage($$$)
{
my ($self, $sliver_urn, $imagename) = @_;
my $authority = $self->GetGeniAuthority();
my $geniuser = $self->GetGeniUser();
my $slice = $self->GetGeniSlice();
my $context = APT_Geni::GeniContext();
return undef
if (! (defined($geniuser) && defined($authority) &&
defined($slice) && defined($context)));
my ($slice_credential, $speaksfor_credential) =
APT_Geni::GenCredentials($slice, $geniuser);
return undef
if (! (defined($speaksfor_credential) &&
defined($slice_credential)));
my $args = {
"slice_urn" => $slice->urn(),
"imagename" => $imagename,
"sliver_urn" => $sliver_urn,
"credentials" => [$slice_credential->asString(),
$speaksfor_credential->asString()],
};
return Genixmlrpc::CallMethod($authority->url(),
$context, "CreateImage", $args);
}
#
# Ask for status.
#
sub SliceStatus($)
{
my ($self) = @_;
my $authority = $self->GetGeniAuthority();
my $geniuser = $self->GetGeniUser();
my $slice = $self->GetGeniSlice();
my $context = APT_Geni::GeniContext();
return undef
if (! (defined($geniuser) && defined($authority) &&
defined($slice) && defined($context)));
my ($slice_credential, $speaksfor_credential) =
APT_Geni::GenCredentials($slice, $geniuser);
return undef
if (! (defined($speaksfor_credential) &&
defined($slice_credential)));
my $args = {
"slice_urn" => $slice->urn(),
"credentials" => [$slice_credential->asString(),
$speaksfor_credential->asString()],
};
return Genixmlrpc::CallMethod($authority->url(),
$context, "SliverStatus", $args);
}
# _Always_ make sure that this 1 is at the end of the file...
1;
......@@ -35,19 +35,17 @@ use POSIX qw(setsid close);
#
sub usage()
{
print("Usage: manage_instance -s instance [imagename]\n");
print("Usage: manage_instance -r instance\n");
print("Usage: manage_instance -c instance node\n");
print("Usage: manage_instance snapshot instance [imagename node_id]\n");
print("Usage: manage_instance consoleurl instance node\n");
print("Usage: manage_instance extend instance seconds\n");
print("Usage: manage_instance terminate instance\n");
print("Usage: manage_instance refresh instance\n");
exit(-1);
}
my $optlist = "dsrt:c";
my $optlist = "dt:";
my $debug = 0;
my $delete = 0;
my $snapshot = 0;
my $doconsole = 0;
my $webtask;
my $webtask_id;
my $webtask_delete;
my $webtask;
#
# Configure variables
......@@ -55,6 +53,7 @@ my $webtask_delete;
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $QUICKVM = "$TB/sbin/protogeni/quickvm";
my $VERSIONING = @PROFILEVERSIONS@;
#
# Untaint the path
......@@ -74,18 +73,26 @@ use lib "@prefix@/lib";
use EmulabConstants;
use emdb;
use emutil;
use libtestbed;
use User;
use Project;
use APT_Profile;
use APT_Instance;
use APT_Geni;
use GeniXML;
use GeniHRN;
use Genixmlrpc;
use GeniResponse;
use GeniSlice;
use WebTask;
# Protos
sub fatal($);
sub DoSnapshot();
sub DoConsole();
sub DoTerminate();
sub DoExtend();
sub DoRefresh();
#
# Parse command arguments. Once we return from getopts, all that should be
......@@ -95,19 +102,13 @@ my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"s"})) {
$snapshot = 1;
}
if (defined($options{"c"})) {
$doconsole = 1;
}
if (defined($options{"r"})) {
$delete = 1;
}
if (defined($options{"t"})) {
$webtask_id = $options{"t"};
}
if (@ARGV < 1) {
if (defined($options{"d"})) {
$debug++;
}
if (@ARGV < 2) {
usage();
}
# The web interface (and in the future the xmlrpc interface) sets this.
......@@ -119,18 +120,34 @@ if (! defined($this_user)) {
}
}
my $action = shift(@ARGV);
my $uuid = shift(@ARGV);
my $instance = APT_Instance->Lookup($uuid);
if (!defined($instance)) {
$instance = APT_Instance->LookupBySlice($uuid);
}
if (!defined($instance)) {
fatal("No such instance $uuid");
}
if ($snapshot) {
if ($action eq "snapshot") {
DoSnapshot();
}
elsif ($doconsole) {
if ($action eq "extend") {
DoExtend();
}
elsif ($action eq "consoleurl") {
DoConsole()
}
elsif ($action eq "terminate") {
DoTerminate()
}
elsif ($action eq "refresh") {
DoRefresh()
}
else {
usage();
}
exit(0);
#
......@@ -138,6 +155,20 @@ exit(0);
#
sub DoSnapshot()
{
my $errmsg;
my $logfile;
my $errcode = 1;
my $needunlock = 0;
my $old_status = $instance->status();
if ($old_status ne "ready") {
fatal("Instance must be in the ready state to take a snapshot");
}
my $slice = $instance->GetGeniSlice();
if (!defined($slice)) {
fatal("No slice for quick VM: $uuid");
}
#
# If we get an imagename on the command line, the caller is
# saying it is responsible. If we do not get one, we create
......@@ -145,8 +176,9 @@ sub DoSnapshot()
# urn.
#
my $imagename;
my $node_id;
my $sliver_urn;
my $update_profile = 0;
my $rspec;
my $profile = APT_Profile->Lookup($instance->profile_id());
if (!defined($profile)) {
......@@ -154,6 +186,9 @@ sub DoSnapshot()
}
if (@ARGV) {
$imagename = shift(@ARGV);
if (@ARGV) {
$node_id = shift(@ARGV);
}
}
else {
$imagename = $profile->name();
......@@ -168,122 +203,242 @@ sub DoSnapshot()
fatal("Could not parse manifest");
}
my @nodes = GeniXML::FindNodes("n:node", $manifest)->get_nodelist();
if (@nodes != 1) {
fatal("Too many nodes (> 1) to snapshot");
if (!defined($node_id)) {
if (@nodes != 1) {
fatal("Too many nodes (> 1) to snapshot");
}
my ($node) = @nodes;
$sliver_urn = GeniXML::GetSliverId($node);
}
my ($node) = @nodes;
my $sliver_urn = GeniXML::GetSliverId($node);
#
# But we eventually update the rspec, so get that.
#
if ($update_profile) {
$rspec = GeniXML::Parse($profile->rspec());
if (! defined($rspec)) {
fatal("Could not parse rspec for profile");
else {
foreach my $node (@nodes) {
my $client_id = GeniXML::GetVirtualId($node);
if ($node_id eq $client_id) {
$sliver_urn = GeniXML::GetSliverId($node);
last;
}
}
if (!defined($sliver_urn)) {
fatal("Could not find node '$node_id' in manifest");
}
($node) = GeniXML::FindNodes("n:node", $rspec)->get_nodelist();
}
if ($slice->Lock()) {
fatal("Slice is busy, cannot lock it");
}
$needunlock = 1;
#
# Create the webtask object.
#
if (defined($webtask_id)) {
$webtask = WebTask->Lookup($webtask_id);
$webtask_delete = 0;
}
#
# We always create one for the called script. Makes it easy to
# communicate.
#
if (!defined($webtask)) {
$webtask = WebTask->Create($instance->uuid(), $webtask_id);
if (!defined($webtask)) {
fatal("Could not create webtask object");
}
# We created this cause caller did not specify it needed one,
# so we will delete it when we are done.
if (!defined($webtask_id)) {
$webtask_id = $webtask_id->task_id();
$webtask_delete = 1;
}
$webtask = WebTask->LookupOrCreate($instance->uuid(), $webtask_id);
# Convenient.
$webtask->AutoStore(1);
}
# Convenient.
$webtask->AutoStore(1);
my $command = "$QUICKVM -t $webtask_id -s $uuid $sliver_urn $imagename";
my $authority = $instance->GetGeniAuthority();
my $geniuser = $instance->GetGeniUser();
my $context = APT_Geni::GeniContext();
if (! (defined($geniuser) && defined($authority) &&
defined($slice) && defined($context))) {
$errmsg = "Internal error getting instance objects";
goto bad;
}
my ($slice_credential, $speaksfor_credential) =
APT_Geni::GenCredentials($slice, $geniuser);
if (! (defined($speaksfor_credential) && defined($slice_credential))) {
$errmsg = "Internal error getting credentials";
goto bad;
}
$instance->SetStatus("imaging");
my $args = {
"slice_urn" => $slice->urn(),
"imagename" => $imagename,
"sliver_urn" => $sliver_urn,
"credentials" => [$slice_credential->asString(),
$speaksfor_credential->asString()],
};
#
# This returns pretty fast, and then the imaging takes place in
# the background at the aggregate. quickvm keeps a process running
# in the background waiting for the sliver to unlock and the
# sliverstatus to indicate the node is running again.
# the background at the aggregate.
#
my $output = emutil::ExecQuiet($command);
if ($?) {
$webtask->Delete()
if ($webtask_delete);
print STDERR $output;
fatal("Failed to create disk image");
my $response = Genixmlrpc::CallMethod($authority->url(),
$context, "CreateImage", $args);
if (!defined($response)) {
$errmsg = "Internal error creating image";
$instance->SetStatus($old_status);
goto bad;
}
#
# Parse the output to get the new image urn, we will stick that
# into the rspec if the operation succeeds.
#
my $image_urn;