Commit f586ad16 authored by Leigh Stoller's avatar Leigh Stoller

A slew of image/dataset changes for secure (credentialed) images and

image relocation.
parent 168949a9
......@@ -63,6 +63,7 @@ use GeniCredential;
use GeniUser;
use GeniHRN;
use GeniXML;
use GeniImage;
use WebTask;
use Logfile;
use overload ('""' => 'Stringify');
......@@ -74,6 +75,7 @@ my $TBAUDIT = "@TBAUDITEMAIL@";
my $OURDOMAIN = "@OURDOMAIN@";
my $GENEXTENDCRED = "$TB/sbin/protogeni/genextendcred";
my $MANAGEDATASET = "$TB/bin/manage_dataset";
my $MANAGEIMAGES = "$TB/bin/manage_images";
my $GENIUSER = "geniuser";
my $MAINSITE = @TBMAINSITE@;
my $PROTOGENI_LOCALUSER= @PROTOGENI_LOCALUSER@;
......@@ -1498,15 +1500,15 @@ sub CreateDatasetCreds($$$)
my $dataset_urn = GeniHRN->new($dataset_id);
my $dataset = APT_Dataset->LookupByRemoteURN($dataset_urn);
if (!defined($dataset)) {
if ($dataset_urn->domain() eq $OURDOMAIN) {
if ($dataset_urn->IsOurDomain()) {
#
# Local image backed dataset or lease.
#
my ($image,$lease);
my $pid = $dataset_urn->project();
my $id = $dataset_urn->id();
my $pid = $dataset_urn->dsetpid();
my $id = $dataset_urn->dsetname();
if ($dataset_urn->type() eq "imdataset") {
if ($dataset_urn->IsIMDataset()) {
$image = Image->Lookup($pid, $id);
if ($image && !$image->isdataset()) {
$$pmsg = "$dataset_urn is an image not a dataset ";
......@@ -1520,7 +1522,7 @@ sub CreateDatasetCreds($$$)
if (!$image->global() &&
$PROTOGENI_LOCALUSER && $geniuser->IsLocal() &&
!$image->AccessCheck($geniuser->emulab_user(),
TB_IMAGEID_ACCESS())) {
TB_IMAGEID_READINFO())) {
$$pmsg = "No permission to use $dataset_urn";
return 1;
}
......@@ -1544,7 +1546,7 @@ sub CreateDatasetCreds($$$)
# are applied at the CM.
#
next
if ($dataset->type() ne "imdataset");
if (!$dataset->IsIMDataset());
#
# For image backed datasets, we need to send along a credential
......@@ -1577,6 +1579,205 @@ sub CreateDatasetCreds($$$)
return 0;
}
#
# Create credentials required by this instance, to access restricted images.
# Access means import and/or use the image.
#
sub CreateImageCreds($$$;$)
{
my ($self, $pmsg, $pref) = @_;
my $rspecstr = $self->rspec();
my $project = $self->GetProject();
my $geniuser = $self->GetGeniUser();
my %imagecache = ();
my %credentials = ();
my $rspec = GeniXML::Parse($rspecstr);
if (! defined($rspec)) {
print STDERR "CreateDatasetCreds: Could not parse rspec\n";
return -1;
}
foreach my $ref (GeniXML::FindNodes("n:node", $rspec)->get_nodelist()) {
my $manager_urn = GeniHRN->new(GetManagerId($ref));
my $diskref = GeniXML::GetDiskImage($ref);
my $credstr;
if (! (defined($manager_urn) && $manager_urn->IsCM())) {
$$pmsg = "$manager_urn is not a valid CM URN";
return 1;
}
# Using the default (system) image.
next
if (!defined($diskref));
my $image_url = GeniXML::GetText("url", $diskref);
my $image_urn = GeniXML::GetText("name", $diskref);
next
if (!defined($image_urn));
$image_urn = GeniHRN->new($image_urn);
if (! (defined($image_urn) && $image_urn->IsImage())) {
$$pmsg = "$image_urn is not a valid URN";
return 1;
}
# System images are global (well, it would be rare if not).
next
if ($image_urn->ospid() eq TBOPSPID());
#
# If the image is local, then local permissions apply to the
# user. Implicit assumption: PROTOGENI_LOCALUSER=1. We do the
# check here to catch permission errors early.
#
# If the manager is the local aggregate, then no credential
# is needed, the CM is going to do a local permission check too.
#
if ($image_urn->IsOurDomain()) {
my $image = Image->Lookup($image_urn->ospid(), $image_urn->osname());
if (!defined($image)) {
$$pmsg = "Could not lookup local image $image_urn";
return 1;
}
if (! ($image->global() ||
$image->AccessCheck($geniuser->emulab_user(),
TB_IMAGEID_READINFO()))) {
$$pmsg = "No permission to use $image_urn";
return 1;
}
#
# If the image is restricted, the experiment has to be created
# in the same project as the image, or in a project that has
# been granted permission to use the image.
#
if (!$image->global() && $image->pid() ne $project->pid()) {
my $allowed = 0;
if ($image->LookupAccess($project->GetProjectGroup(),
\$allowed) != 0) {
$$pmsg = "Could not lookup access for $image";
return -1;
}
if (!$allowed) {
$$pmsg = "Not allowed to use restricted image $image_urn ".
"in project " . $project->pid();
return 1;
}
}
# No cred needed for local image at local CM.
next
if ($manager_urn->IsOurDomain());
}
#
# Basically, only the Mothership will get this far, every other
# site is going to be talking to the local portal only, not
# a remote cluster. No IMS either.
#
next
if (exists($credentials{$image_urn}));
#
# Generate a credential that allows the user to use a local
# image at a remote cluster (if it was the local cluster,
# that case was handled above). We know this is allowed as
# per the access check above.
#
if ($image_urn->IsOurDomain()) {
#
# We can generate the credential inline.
#
my $image = Image->Lookup($image_urn->ospid(),$image_urn->osname());
if (!defined($image)) {
$$pmsg = "Could not lookup local image $image_urn";
return 1;
}
my $authority = APT_Geni::GetAuthority($manager_urn);
if (!defined($authority)) {
$$pmsg = "Could not look up authority $manager_urn";
return -1;
}
my $credential = GeniImage::CreateImageCredential($image,
$authority);
if (!defined($credential)) {
$$pmsg = "Could not create credential for $image_urn";
return -1;
}
$credentials{$image_urn} = $credential->asString();
next;
}
#
# Temporary, need to talk IMS directly instead of issuing an RPC
# for every image.
#
my $blob;
if (exists($imagecache{$image_urn})) {
$blob = $imagecache{$image_urn};
}
else {
$blob = GeniImage::GetImageData($image_urn, $pmsg);
if (!defined($blob)) {
print STDERR $pmsg . "\n";
next;
}
$imagecache{$image_urn} = $blob;
}
# IMS says the image is public (global) so no credential needed.
next
if ($blob->{"visibility"} eq "public");
#
# The IMS tells us the project urn of the image. 99% of the time,
# that is for a project here (where the IMS is). The other 1% the
# image was created by some other SA (say, ch.geni.net), and we
# cannot do anything with those. Just throw an error, I doubt a
# Portal user would be using one of those (and global=0), revisit
# later if it happens.
#
my $project_urn = GeniHRN->new($blob->{'project_urn'});
if ($project_urn->domain() ne $OURDOMAIN) {
$$pmsg = "Cannot create a credential for ".
"external image: $image_urn";
return 1;
}
#
# Since we are ignoring images created by other domains, and
# since the image server is not giving us permission info, we
# basically have to say that the experiment and the image have
# to be in the same local project.
#
if ($project_urn->project() ne $project->pid()) {
$$pmsg = "No permission to use $image_urn in project ".
$project->pid();
return 1;
}
#
# Get a credential from the remote cluster via the admin path.
#
my $cmd = "$MANAGEIMAGES getcredential -a $manager_urn $image_urn";
my $output = emutil::ExecQuiet($cmd);
if ($?) {
$$pmsg = "Could not generate credential for $image_urn";
if (($? >> 8) > 0) {
if ($output ne "") {
$$pmsg = "$image_urn" . ": " . $output;
}
return 1;
}
print STDERR "$cmd\n";
print STDERR $output . "\n";
return -1;
}
$credentials{$image_urn} = $output;
}
@$pref = values(%credentials);
return 0;
}
#
# Defer aggregate setup until missing aggregates come online.
# Optional start time indicates we are deferring the entire experiment
......@@ -3030,11 +3231,11 @@ sub ConsoleURL($$)
#
# Create an Image,
#
sub CreateImage($$$$;$$$$$$)
sub CreateImage($$$$;$$$$$$$)
{
my ($self, $sliver_urn, $imagename, $update_prepare,
$copyback_uuid, $bsname, $nosnapshot,
$mustnotexist, $wholedisk, $description) = @_;
$mustnotexist, $wholedisk, $description, $relocate) = @_;
my $authority = $self->GetGeniAuthority();
my $geniuser = $self->instance()->GetGeniUser();
my $slice = $self->instance()->GetGeniSlice();
......@@ -3057,7 +3258,6 @@ sub CreateImage($$$$;$$$$$$)
"slice_urn" => $slice->urn(),
"imagename" => $imagename,
"sliver_urn" => $sliver_urn,
"global" => 1,
"credentials" => [$slice_credential->asString(),
$speaksfor_credential->asString()],
};
......@@ -3075,6 +3275,8 @@ sub CreateImage($$$$;$$$$$$)
if ($wholedisk);
$args->{'description'} = $description
if (defined($description));
$args->{'relocate_urn'} = $relocate
if (defined($relocate));
my $cmurl = $authority->url();
$cmurl = devurl($cmurl) if ($usemydevtree);
......
......@@ -853,15 +853,6 @@ if ($instance->ApplyExtensionPolicies()) {
fatal("Error applying policies");
}
# Generate the extra credentials that tells the backend this experiment
# can access the datasets.
my $dataset_credentials = {};
$retval = $instance->CreateDatasetCreds(\$errmsg, \$dataset_credentials);
if ($retval) {
$instance->Delete();
($retval < 0 ? fatal($errmsg) : UserError($errmsg));
}
# We use this list of references for ParRun below.
my @aggregate_list = ();
foreach my $aggregate_urn (@aggregate_urns) {
......
......@@ -275,7 +275,13 @@ if (! (defined($speaksfor_credential) &&
my $dataset_credentials = {};
my $retval = $instance->CreateDatasetCreds(\$errmsg, \$dataset_credentials);
if ($retval) {
fatal("Could not generate dataset credentials");
fatal("Could not generate dataset credentials: $errmsg");
}
# Ditto images that are not global (also checks user permission).
my @image_credentials = ();
$retval = $instance->CreateImageCreds(\$errmsg, \@image_credentials);
if ($retval) {
fatal("Could not generate image credentials: $errmsg");
}
#
......@@ -635,6 +641,7 @@ sub CreateSliver($)
[$slice_credential->asString(),
$speaksfor_credential->asString(),
@dsetcreds
@image_credentials
],
"certificate" => $instance->cert(),
"key" => $instance->privkey(),
......@@ -1272,6 +1279,8 @@ sub fatal($)
$genislice->UnLock()
if (defined($genislice));
$instance->SetStatus("failed")
if (defined($instance));
if (defined($webtask)) {
$webtask->output($mesg);
......
#!/usr/bin/perl -w
#
# Copyright (c) 2000-2018 University of Utah and the Flux Group.
# Copyright (c) 2000-2019 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -248,6 +248,15 @@ sub DoCreate()
fatal("Could not find aggregate for $nodeid");
}
$aggregate_urn = $aggregate->aggregate_urn();
#
# Not allowed to create a dataset on aggregates that are marked as
# nolocalimages. Maybe add "relocation" later, but for now just
# throw a user error.
#
if ($aggregate->nolocalimages()) {
UserError("Not allowed to create a new dataset on this cluster");
}
}
else {
if (!APT_Dataset::ValidBlockstoreBackend($aggregate_urn)) {
......
#!/usr/bin/perl -w
#
# Copyright (c) 2000-2018 University of Utah and the Flux Group.
# Copyright (c) 2000-2019 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -33,8 +33,10 @@ use JSON;
#
sub usage()
{
print STDERR "Usage: manage_images [options --] list ...\n";
print STDERR "Usage: manage_images [options --] delete <urn> ...\n";
print STDERR "Usage: manage_images [options] list ...\n";
print STDERR "Usage: manage_images [options] delete <urn> ...\n";
print STDERR "Usage: manage_images [options] getcredential <urn>\n";
print STDERR "Usage: manage_images [options] relocate ...\n";
exit(-1);
}
my $optlist = "dt:";
......@@ -49,7 +51,9 @@ my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $OURDOMAIN = "@OURDOMAIN@";
my $MYURN = "urn:publicid:IDN+${OURDOMAIN}+authority+cm";
my $CMCERT = "$TB/etc/genicm.pem";
my $MANAGEPROFILE = "$TB/bin/manage_profile";
my $IMPORTER = "$TB/sbin/image_import";
my $MAINSITE = @TBMAINSITE@;
# For development.
......@@ -77,6 +81,9 @@ use Project;
use User;
use WebTask;
use GeniResponse;
use GeniCertificate;
use GeniCredential;
use GeniImage;
use GeniXML;
use GeniUser;
use APT_Geni;
......@@ -88,6 +95,8 @@ sub fatal($);
sub UserError($);
sub DoListImages();
sub DoDeleteImage();
sub DoGetCredential();
sub DoRelocate();
sub ExitWithError($);
#
......@@ -128,6 +137,12 @@ if ($action eq "list") {
elsif ($action eq "delete") {
exit(DoDeleteImage());
}
elsif ($action eq "getcredential") {
exit(DoGetCredential());
}
elsif ($action eq "relocate") {
exit(DoRelocate());
}
else {
usage();
}
......@@ -588,6 +603,226 @@ sub DoDeleteImage()
exit(0);
}
sub GetCredentialInternal($$$)
{
my ($image_urn, $context, $perror) = @_;
# Convert the image urn into the authority URN.
my $hrn = GeniHRN->new($image_urn);
my $authurn = GeniHRN::Generate($hrn->domain(), "authority", "cm");
my $manager = APT_Geni::GetAuthority($authurn);
if (!defined($manager)) {
$$perror = "Could not lookup authority $authurn";
return undef;
}
my $args = {"image_urn" => $image_urn};
my $response = APT_Geni::PortalRPC($manager, $context,
"GetImageCredential", $args);
if (GeniResponse::IsError($response)) {
return $response;
}
my $credential = GeniCredential->CreateFromSigned($response->value());
if (!defined($credential)) {
$$perror = "Could not parse new credential";
return undef;
}
return $credential;
}
#
# Request an image credential via the Portal fast path.
#
sub DoGetCredential()
{
my $usage = sub {
print STDERR "Usage: manage_images getcredential [-a urn] ".
"<image_urn>\n";
print STDERR "Options:\n";
print STDERR " -a urn - URN of the remote cluster (cm)\n";
print STDERR " image_urn - URN of the image at the remote cluster\n";
exit(-1);
};
my $optlist = "a:";
my $authority;
my $errmsg;
my %options = ();
if (! getopts($optlist, \%options)) {
&$usage();
}
if (defined($options{"a"})) {
my $urn = $options{"a"};
$authority = APT_Geni::GetAuthority($urn);
if (!defined($authority)) {
fatal("Could not lookup authority $urn");
}
}
&$usage()
if (!@ARGV);
my $image_urn = shift(@ARGV);
if (!GeniHRN::IsValid($image_urn)) {
fatal("Not a valid urn");
}
# Convert the image urn into the authority URN.
my $hrn = GeniHRN->new($image_urn);
if (!$hrn->IsImage()) {
fatal("Not an image urn");
}
my $context = APT_Geni::GeniContext();
my $credential = GetCredentialInternal($image_urn, $context, \$errmsg);
if (!defined($credential)) {
fatal($errmsg);
}
if (GeniResponse::IsError($credential)) {
ExitWithError($credential);
}
if (defined($authority)) {
my $delegated = $credential->Delegate($authority);
$delegated->Sign($context);
$credential = $delegated;
}
print $credential->asString();
exit(0);
}
#
# Relocate an image back to this cluster.
#
sub DoRelocate()
{
$debug = 1;
my $usage = sub {
print STDERR "Usage: manage_images relocate [-s] [-u user] -p pid ".
"-i imagename <image_urn> <url>\n";
print STDERR " manage_images relocate -p pid -i imagename\n";
print STDERR "Use the -s option to *also* schedule.\n";
print STDERR "Use the -S option to *only* schedule.\n";
print STDERR "Use the second form to start a scheduled relocation\n";
exit(-1);
};
my $optlist = "p:i:u:sS";
my $schedule = 0;
my $project;
my $group;
my $pid;
my $user;
my $image_urn;
my $image_url;
my $imagename;
my $errmsg;
my %options = ();
if (! getopts($optlist, \%options)) {
&$usage();
}
if (defined($options{"s"}) || defined($options{"S"})) {
$schedule = 1;
}
if (defined($options{"p"})) {
$pid = $options{"p"};
$project = Project->Lookup($pid);
if (!defined($project)) {
fatal("No such project");
}
$group = $project->GetProjectGroup();
}
if (defined($options{"u"})) {
$user = User->Lookup($options{"u"});
if (!defined($user)) {
fatal("No such user");
}
}
else {
$user = $this_user;
}
if (defined($options{"i"})) {
$imagename = $options{"i"};
if (!TBcheck_dbslot($imagename, "images", "imagename",
TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)){
fatal("Not a valid imagename");
}
}
&$usage()
if (!(defined($pid) && defined($imagename)));
if (@ARGV == 2) {
$image_urn = shift(@ARGV);
if (!GeniHRN::IsValid($image_urn)) {
fatal("Not a valid urn");
}
$image_url = shift(@ARGV);
}
else {
my $blob;
my $val = GeniImage::ImageRelocationPending($imagename, $group, \$blob);
if (!$val || !defined($blob)) {
fatal("Could not lookup scheduled relocation");
}
print Dumper($blob);
$image_urn = $blob->{'remote_urn'};
$image_url = $blob->{'metadata_url'};
$user = User->Lookup($blob->{'uid_idx'});
if (!defined($user)) {
fatal("Not a valid user: " . $blob->{'uid'});
}
}
# Convert the image urn into the authority URN.
my $hrn = GeniHRN->new($image_urn);
if (!$hrn->IsImage()) {
fatal("Not an image urn");
}
if ($schedule) {
if (GeniImage::ImageRelocationSchedule($imagename, $user, $group,
$image_urn, $image_url)) {
fatal("Could not schedule incoming relocation for $image_urn");
}
if (exists($options{"S"})) {
exit(0);
}
}
#
# The context for image import is the CM.
#
my $cmcert = GeniCertificate->LoadFromFile($CMCERT);
if (!defined($cmcert)) {
fatal("Could not load certificate from $CMCERT\n");
}
my $context = Genixmlrpc->Context($cmcert);
if (!defined($context)) {
fatal("Could not create RPC context");
}
my $credential = GetCredentialInternal($image_urn, $context, \$errmsg);
if (!defined($credential)) {
fatal("Could not generate credential: $errmsg");
}
if (GeniResponse::IsError($credential)) {
ExitWithError($credential);
}
# This will autodelete
my $credfile = $credential->WriteToFile();
if (!defined($credfile)) {
fatal("Could not write credential to file");
}
if (GeniImage::ImageRelocationLock($imagename, $group)) {
print STDERR "Could not lock image relocation record\n";
exit(1);
}
my $cmd = "$IMPORTER ";
$cmd .= "-d " if ($debug);
$cmd .= "-R -C $credfile -p $pid -u " . $user->uid() . " " ;
$cmd .= "-i '$imagename' '$image_url'";
if ($debug) {
print "Running '$cmd'\n";
}
system($cmd);
if ($?) {
GeniImage::ImageRelocationUnlock($imagename, $group);
fatal("Could not relocate image");
}
GeniImage::ImageRelocationFinished($imagename, $group);
exit(0);
}
sub fatal($)
{
my ($mesg) = @_;
......
......@@ -79,9 +79,13 @@ my $geniuser;
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $OURDOMAIN = "@OURDOMAIN@";
my $MYURN = "urn:publicid:IDN+${OURDOMAIN}+authority+cm";
my $PROTOUSER = "elabman";
my $SUDO = "/usr/local/bin/sudo";
my $MANAGEINSTANCE = "$TB/bin/manage_instance";
my $MANAGEIMAGES = "$TB/bin/manage_images";
my $IMPORTER = "$TB/sbin/image_import";
my $WAP = "$TB/sbin/wap";
my $TBACCT = "$TB/sbin/tbacct";
......@@ -151,13 +155,14 @@ sub DoApplyExtensionPolicy();
sub WriteCredentials();
sub StartMonitor();
sub StartMonitorInternal(;$);
sub DoImageTrackerStuff($$$$$$$);
sub DoImageTrackerStuff($$$$$$$$);
sub DoWarn();
sub DoDelete();
sub DenyExtensionInternal($);
sub ExtendInternal($$$$$);
sub CallMethodOnAggregates($$$@);
sub ResponseErrorMessage($$);
sub RelocateImage($$$$);
#
# Parse command arguments. Once we return from getopts, all that should be
......@@ -325,6 +330,7 @@ sub DoSnapshot()
my $update_prepare = 0;
my $doversions = 0;
my $usetracker = 0;
my $relocate = 0;
my $operation = "image-only"; # Default to just snapshot.
my $optlist = "n:i:u:Uc:O:SseD:";
......@@ -523,7 +529,7 @@ sub DoSnapshot()
my $rval = DoImageTrackerStuff($aggregate, $node, $project,
$imagename,
\$copyback_uuid, \$copyback_urn,
\$errmsg);
\$relocate, \$errmsg);
if ($rval) {
if ($rval < 0) {
fatal($errmsg);
......@@ -535,9 +541,12 @@ sub DoSnapshot()
}
}
}
elsif ($aggregate->GetAptAggregate()->nolocalimages()) {
$relocate = 1;
}
}
if (0) {
fatal("$copyback_uuid, $copyback_urn\n");
fatal("$copyback_uuid, $copyback_urn, $relocate\n");
}
#
......@@ -617,6 +626,18 @@ sub DoSnapshot()
# Shorten default timeout
Genixmlrpc->SetTimeout(60);
if ($relocate) {
#
# When relocating an image back here, the URN we get back from
# the cluster is not the URN the user is going to use. We need
# to form a local URN to use after the copy back.
#
# XXX Need to check if we can use the URN earlier in the process.
#
$copyback_urn = GeniHRN::GenerateImage($OURDOMAIN, $project->pid(),
$imagename, undef);
}
#
# This returns pretty fast, and then the imaging takes place in