#!/usr/bin/perl -w
#
# Copyright (c) 2010-2017 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 .
#
# }}}
#
use English;
use strict;
use Getopt::Std;
use XML::Simple;
use Data::Dumper;
use URI::Escape;
#
# Import an image from an external source.
#
sub usage()
{
print("Usage: image_import [-d] [-v] [-u ] [-g] [-p pid] ".
"[-i name] \n");
print(" image_import [-d] [-u ] [-c] [-g] [url]\n");
print("Options:\n");
print(" -d - Turn on debugging\n");
print(" -v - Verify XML description only\n");
print(" -g - Download image after creating/updating descriptor\n");
print(" -G - Download ALL images instead.\n");
print(" -u uid - Create image as user instead of caller\n");
print(" -p pid - Create image in the specified project.\n".
" Defaults to emulab-ops.\n");
print(" -i name - Use name for imagename.\n".
" Defaults to name in the desciptor\n");
print(" -s - With -r just update the sig file\n");
print(" -c - With -r update ndz file using the updates table\n");
exit(-1);
}
my $optlist = "dvu:p:gGi:Iscr";
my $debug = 0;
my $verify = 0;
my $getimage= 0;
my $update = 0;
my $dosig = 0;
my $force = 0;
my $copyback= 0;
my $getallimages=0;
my $user;
my $group;
#
# Configure variables
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $TBLOGS = "@TBLOGSEMAIL@";
my $TBAUDIT = "@TBAUDITEMAIL@";
my $TBGROUP_DIR = "@GROUPSROOT_DIR@";
my $TBPROJ_DIR = "@PROJROOT_DIR@";
my $TBBASE = "@TBBASE@";
my $CONTROL = "@USERNODE@";
my $WGET = "/usr/local/bin/wget";
my $NEWIMAGE_EZ = "$TB/bin/newimageid_ez";
my $IMAGEDUMP = "$TB/bin/imagedump";
my $SHA1 = "/sbin/sha1";
my $SAVEUID = $UID;
my $IMAGEVALIDATE = "$TB/sbin/imagevalidate";
my $DELETEIMAGE = "$TB/sbin/delete_image";
my $POSTIMAGEINFO = "$TB/sbin/protogeni/postimagedata";
my $WITHPROVENANCE= @IMAGEPROVENANCE@;
my $WITHDELTAS = @IMAGEDELTAS@;
my $DOIMAGEDIRS = @IMAGEDIRECTORIES@;
my $PGENISUPPORT = @PROTOGENI_SUPPORT@;
my $doprovenance = $WITHPROVENANCE;
my $dodeltas = $WITHDELTAS;
#
# When fetching the metadata, we now tell the server what client
# version of the software we are so it gives something we can handle.
# Be sure to update this if you change the version in dumpdescriptor.
#
my $METADATA_CLIENTVERSION = 5;
#
# Untaint the path
#
$ENV{'PATH'} = "$TB/bin:$TB/sbin:/bin:/usr/bin:/usr/bin:/usr/sbin";
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
#
# Turn off line buffering on output
#
$| = 1;
#
# We don't want to run this script unless its the real version.
#
if ($EUID != 0) {
die("*** $0:\n".
" Must be setuid! Maybe its a development version?\n");
}
#
# Load the Testbed support stuff.
#
use lib "@prefix@/lib";
use libdb;
use emutil;
use EmulabConstants;
use EmulabFeatures;
use libtestbed;
use User;
use Project;
use Group;
use OSImage;
# Locals;
my $url;
my $image;
my $imagename;
my $updater_urn;
# Protos
sub fatal($);
sub FetchMetadata($);
sub CreateImage($$$$$);
sub DownLoadImage($$$$);
sub FetchImageFile($$);
sub FetchSigFile($$);
sub CloneFromMetadata($$$);
sub UpdateImageFromMetadata($$);
sub maybeGetImage($$);
#
# There is no reason to run as root unless we need to ssh over
# to ops to fetch the URL.
#
$EUID = $UID;
#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"d"})) {
$debug = 1;
}
if (defined($options{"v"})) {
$verify = 1;
}
if (defined($options{"g"})) {
$getimage = 1;
}
if (defined($options{"G"})) {
$getimage = 1;
$getallimages = 1;
}
if (defined($options{"s"})) {
$dosig = 1;
}
if (defined($options{"i"})) {
$imagename = $options{i};
}
if (defined($options{"r"})) {
$update = 1;
if (defined($options{"c"})) {
$copyback = 1;
}
}
if (defined($options{"R"})) {
$update = 1;
$force = 1;
}
if (defined($options{"u"})) {
$user = User->Lookup($options{"u"});
fatal("User does not exist")
if (!defined($user));
}
if (defined($options{"p"})) {
$group = Group->Lookup($options{"p"});
if (!defined($group)) {
my $project = Project->Lookup($options{"p"});
fatal("Project/Group does not exist")
if (!defined($project));
$group = $project->GetProjectGroup();
fatal("Error getting project group for $project")
if (!defined($group));
}
}
else {
$group = Group->Lookup(TBOPSPID(), TBOPSPID());
fatal("Error getting project group for " . TBOPSPID())
if (!defined($group));
}
if (!defined($user)) {
$user = User->ThisUser();
if (! defined($user)) {
fatal("You ($UID) do not exist!");
}
}
usage()
if (!@ARGV);
#
# If arg is a URL, then image must not exist.
#
if ($ARGV[0] =~ /^http/) {
$url = $ARGV[0];
if (OSImage->LookupByURL($url)) {
fatal("Image already exists for URL, please use the id to update");
}
# We know this is valid, but must taint check anyway for shell command.
if ($url =~/^(.*)$/) {
$url = $1;
}
# If the user is not an admin, must be a member or have perm in
# the group.
if (!$user->IsAdmin() &&
!$group->AccessCheck($user, TB_PROJECT_MAKEIMAGEID())) {
fatal("$user does not have permission to create images in $group");
}
}
else {
$image = OSImage->Lookup(shift(@ARGV));
if (!defined($image)) {
fatal("Image does not exist, maybe you need to import by url first");
}
if ($copyback) {
my ($updater_uid,$updater_idx);
#
# We have to look in the updates table, but we want to do this
# locked so that no one else can mess with it. So lock up here,
# and skip locking below. See fatal(), we will unlock there if
# things go bad.
#
if ($image->Lock()) {
print "$image is currently locked. Please try again later\n";
exit(0);
}
my $imageid = $image->imageid();
my $query_result =
DBQueryWarn("select * from image_updates ".
"where imageid='$imageid'");
if (!$query_result) {
$image->Unlock();
exit(-1);
}
if (!$query_result->numrows) {
print "No update in table. Nothing to do.\n";
$image->Unlock();
exit(0);
}
my $row = $query_result->fetchrow_hashref();
$url = $row->{'url'};
$updater_uid = $row->{'updater'};
$updater_idx = $row->{'updater_idx'};
$updater_urn = $row->{'updater_urn'};
#
# Also want the user doing the import to be the user who actually
# did the update on the remote cluster, if we happen to have that
# record. If not, we have to do it as the creator (someone in the
# project the image belongs to).
#
$user = undef;
if (defined($updater_uid)) {
$user = User->Lookup($updater_idx);
# Ick, setgroups skips nonlocal users, so user does not have
# permission to do this on ops. Needs thought.
if ($user->IsNonLocal()) {
$user = undef;
}
}
if (!defined($user)) {
$user = User->Lookup($image->creator_idx());
}
if (!defined($user)) {
print STDERR "No current user to import image as.\n";
$image->Unlock();
exit(-1);
}
$EUID = 0;
$UID = $SAVEUID = $user->unix_uid();
$EUID = $UID;
}
else {
#
# Allow importing new version of an existing image by a url. This
# allows adding new versions of the image at the origin cluster, to
# an existing image here. Previously, we would have created a brand
# new image name. Of course, we have to compare the origin image
# uuids to confirm they are really from the same image.
#
if (@ARGV) {
$url = $ARGV[0];
# We know this is valid, but must taint check anyway for shell
# command.
if ($url =~/^(.*)$/) {
$url = $1;
}
}
else {
if (!defined($image->metadata_url())) {
fatal("Not an imported image");
}
$url = $image->metadata_url();
}
}
# If the user is not an admin, must have perm on the image.
if (!$user->IsAdmin() &&
!$image->AccessCheck($user, TB_IMAGEID_CREATE())) {
fatal("$user does not have permission to modify $image");
}
# Implied.
$update = 1;
}
my $xmlparse = FetchMetadata($url);
fatal("Could not get metadata!")
if (!defined($xmlparse));
if ($WITHPROVENANCE) {
# But allow feature override.
if (EmulabFeatures->Lookup("ImageProvenance")) {
$doprovenance =
EmulabFeatures->FeatureEnabled("ImageProvenance", undef, $group);
}
}
if (!$doprovenance && $getallimages) {
fatal("-G option requires IMAGE_PROVENANCE to be enabled");
}
#
# Need to watch for two experiments causing this image to
# get created at the same time. It would be pretty silly,
# but it can happen.
#
if (!$update) {
my $safe_url = DBQuoteSpecial($url);
my $query_result = DBQueryWarn("select GET_LOCK($safe_url, 120)");
if (!$query_result ||
!$query_result->numrows) {
fatal("Could not get the SQL lock for a long time!");
}
$image = OSImage->LookupByURL($url);
#
# Look inside the metadata. If we get a non-version specific URL,
# we can use that to look at our images to see if we already have
# a different version of the image. If we do, we can shift to
# update mode and add the later versions to our image history,
# thus bringing them into sync.
#
if (!defined($image)) {
if ($doprovenance &&
exists($xmlparse->{'attribute'}->{"image_metadata_url"})) {
my $image_url =
$xmlparse->{'attribute'}->{"image_metadata_url"}->{'value'};
$image = OSImage->LookupByURL($image_url);
if (defined($image)) {
print "Another version of this image is already imported.\n";
print "Shifting to update mode\n";
$update = 1;
}
}
if (!defined($image)) {
$image = CreateImage($url, $xmlparse, $user, $group, $imagename);
}
}
DBQueryWarn("select RELEASE_LOCK($safe_url)");
}
exit(0)
if ($verify);
#
# We need to get the lock since someone else could already
# be downloading it. Even worse, someone might be taking a local
# snapshot, although at the moment we do not support that.
# If doing a copyback, we already have the lock.
#
if (($update || $getimage) && !$copyback) {
if ($image->Lock()) {
print "$image is currently locked. Waiting ...\n";
my $maxwait = 600;
while ($maxwait > 0 && $image->WaitLock(60)) {
print "$image is currently locked. Waiting ...\n";
$maxwait -= 60;
}
if (!$image->GotLock()) {
fatal("Could not get the image lock after a long time");
}
}
}
#
# If we are doing image versioning and the origin cluster is doing
# versioning, we can import the entire descriptor list. We will not
# import the image files unless we need them, and really, this is
# only useful when the origin is doing deltas where we need all the
# intermediate versions to lay down the correct image.
#
# If we are not doing image versioning, then all we can do is
# import the new image file and overwrite what we have locally.
#
# Origin not doing history, we are NOT doing history:
# Update local descriptor, download new image to overwrite local.
# Must be the full image file, we cannot do deltas.
# Origin not doing history, we ARE doing history:
# Create new local version, download new image file.
# Must be the full image file.
# Origin doing history, we are NOT doing history:
# Update local descriptor, download new image to overwrite local.
# Must be the full image file, we cannot do deltas without history.
# Origin doing history, we ARE doing history:
# In all cases, we want to create local image descriptors for
# the entire history, duplicating the history at the origin cluster,
# so that local users can use earlier versions of the image. But then
# things changed based on deltas:
#
# We are NOT doing deltas:
# Origin IS doing deltas:
# Origin is NOT doing deltas:
# Must download only a full image for the specific image being used.
# We ARE doing deltas:
# Origin IS doing deltas:
# Download the delta files for the current version and all versions
# back to the first version (which is a full image of course).
# Origin is NOT doing deltas:
# Must download only a full image for the specific image being used.
#
#
# First off, if this is an update we want to fetch any prior versions
# of the metadata back to the most recent version we have, so that we
# can build local descriptor versions for everything up to the image
# we actually want to use. Mostly this matters for deltas, but we do it
# anyway in case we ever want to convert from full images into deltas.
#
if ($update) {
if ($doprovenance) {
if (exists($xmlparse->{"version_history"})) {
my @versions = ();
foreach my $vers (keys(%{ $xmlparse->{"version_history"} })) {
my $val = $xmlparse->{"version_history"}->{$vers};
my $parse = FetchMetadata($val);
fatal("Could not fetch metadata for version $vers")
if (!defined($parse));
$versions[$vers] = $parse;
}
#
# Sanity check that there are no gaps.
#
if (scalar(@versions) !=
scalar(keys(%{$xmlparse->{"version_history"}}))){
fatal("Inconsistent number of version history records");
}
#
# Go through the version list and make sure we have a
# local version created.
#
for (my $i = 1; $i <= scalar(@versions); $i++) {
my $clone = OSImage->Lookup($image->imageid(), $i);
if (!defined($clone)) {
$clone = CloneFromMetadata($image, $versions[$i], $user);
if (!defined($clone)) {
$image->Unlock();
fatal("Could not clone image descriptor $image");
}
}
else {
my $attributes = $versions[$i]->{'attribute'};
#
# If the image has diverged locally we are screwed.
#
if ($clone->metadata_url() ne
$attributes->{"metadata_url"}->{'value'}) {
$image->Unlock();
fatal("Image out of sync wrt url at version $i");
}
foreach my $attribute ("hash", "deltahash",
"size", "deltasize") {
my $xmlval = $attributes->{$attribute}->{'value'}
if (exists($attributes->{$attribute}));
my $curval = $image->field($attribute);
if (defined($curval) &&
!(defined($xmlval) || "$xmlval" ne "$curval")) {
$image->Unlock();
fatal("Image out of sync wrt $attribute at ".
"version $i: $xmlval != $curval");
}
}
}
$image = $clone;
}
#
# Image now points to the clone for the highest numbered
# version, which might have been the image we started
# with.
#
}
#
# We are doing provenance (obviously), but the other side might
# not. It is actually hard to know since it might be the only
# version of the image (thus no version history in the metadata) or
# it might just be version zero from a cluster that does versioning
# (again, no version history). In fact, the other side might have
# started out not doing provenance, and then decided to start doing
# it. This last case I need to come back and revisit. But anyway,
# if we are doing provenance and the origin side gives a new hash
# value for the same version of the image, we can assume the other
# side is not doing image versioning.
#
if (defined($image->hash()) &&
$image->hash() ne $xmlparse->{'attribute'}->{"hash"}->{'value'}) {
#
# The other side changed the hash for an image, so it is not
# doing versioning, so the best we can do is clone what we
# have to make a new local version.
#
# This will include unreleased images (in image_versions, but
# not the one pointed to by the images table).
#
$image = $image->LookupMostRecent();
if (!defined($image)) {
fatal("Cannot lookup most recent version of $image");
}
# Reuse if not ready/released. Do not change this test,
# see below in CreateImage().
if ($image->ready() && $image->released()) {
my $clone = CloneFromMetadata($image, $xmlparse, $user);
if (!defined($clone)) {
fatal("Could not clone image descriptor $image");
}
$image = $clone;
}
UpdateImageFromMetadata($image, $xmlparse);
#
# Since the hash changed, clear the ready bit so that we know to
# get the image below.
#
$image->ClearReady();
}
}
else {
#
# No local versioning, we update the image in place.
#
if (defined($image->hash()) &&
$image->hash() ne $xmlparse->{'attribute'}->{"hash"}->{'value'}) {
UpdateImageFromMetadata($image, $xmlparse);
#
# Since the hash changed, clear the ready bit so that we know to
# get the image below.
#
$image->ClearReady();
}
}
}
#
# Now figure out what images we need to actually download.
#
if ($getimage) {
my $didsomething = 0;
#
# When provenance is not enabled, we just get the one image.
#
if (!$doprovenance) {
#
# New images will not have their ready bit set, and if we did an update
# earlier without -g, we cleared the ready bit then.
#
my $rval = maybeGetImage($image, 0);
if ($rval < 0) {
fatal("Could not download $image from server");
}
$didsomething += $rval;
}
else {
my @getlist = ($image);
#
# Get all *prior* versions of the image. Only makes sense when
# provenance is enabled, which we checked for above.
#
if ($getallimages) {
my $tmp = $image;
while (my $parent = $tmp->Parent()) {
# Do not cross image boundry (yet).
last
if ($parent->imageid() ne $tmp->imageid());
push(@getlist, $parent);
$tmp = $parent;
}
}
foreach my $imget (@getlist) {
my $dodelta = 0;
#
# When provenance is enabled, we also have to consider deltas.
# This needs more thought. At the moment, we always get the
# delta if it exists (and we have deltas enabled).
#
if ($imget->deltahash()) {
if ($dodeltas) {
$dodelta = 1;
}
elsif (! $imget->hash()) {
fatal("This image has only deltas, but deltas are ".
"not enabled locally");
}
}
my $rval = maybeGetImage($imget, $dodelta);
if ($rval < 0) {
fatal("Could not download $imget from server");
}
$didsomething += $rval;
}
}
if ($copyback) {
if ($didsomething) {
# Tell image owner that it has been updated.
my $name = $image->pid() . "/" . $image->imagename();
my $project = $image->GetProject();
my $TO;
my $CC = "Bcc: " . $project->LogsEmailAddress();
my $FROM = $project->OpsEmailAddress();
my $versname = $image->versname();
my $creator = User->Lookup($image->creator_idx());
if (defined($creator)) {
$TO = $creator->email();
}
if ($PGENISUPPORT && defined($image->creator_urn())) {
require GeniUser;
my $geniuser = GeniUser->Lookup($image->creator_urn(), 1);
if (defined($geniuser)) {
if (defined($TO)) {
$CC = $CC . "\n" . "CC: " . $geniuser->email();
}
else {
$TO = $geniuser->email();
}
}
}
$TO = $TBLOGS
if (!defined($TO));
$image->GetProject()->SendEmail($TO,
"Image imported: $versname",
"Image $name has been sucessfully imported from\n".
$image->imagefile_url(),
$FROM, $CC);
}
# Delete entry from the updates table while we are still locked.
my $imageid = $image->imageid();
DBQueryWarn("delete from image_updates ".
"where imageid='$imageid'");
# Clear this to make the image gets posted.
$image->ClearIMSReported();
# Mark the updater.
$image->Update({'updater_urn' => $updater_urn})
if (defined($updater_urn));
# Tell the IMS about this new image. If this fails, the daemon
# will take care of it.
system("$POSTIMAGEINFO -d $imageid");
}
}
$image->Unlock()
if ($image->GotLock());
exit(0);
#
# If the image has not been downloaded or if the hash has changed,
# get a new copy.
#
sub maybeGetImage($$)
{
my ($image, $dodelta) = @_;
my $imagefile = ($dodelta ?
$image->DeltaImageFile() : $image->FullImageFile());
# Run as root to access /proj
$EUID = $UID = 0;
if (! -e $imagefile || !$image->ready() || $force) {
$EUID = $UID = $SAVEUID;
if (DownLoadImage($image, $dodelta, $user, $group)) {
$image->Unlock();
return -1;
}
# For imagevalidate, this is wrong I think.
if ($dodelta) {
$image->SetDelta($dodelta);
}
# Update DB info.
my $versname = $image->versname();
# Run as root to access /proj
$EUID = $UID = 0;
if (system("$IMAGEVALIDATE -u $versname")) {
# XXX should this be fatal?
print STDERR "Could not update DB info for $image\n";
}
$EUID = $UID = $SAVEUID;
$image->MarkReady();
# Its more important to know when we brought the new version in.
if ($update) {
$image->MarkUpdate($user);
}
return 1;
}
$EUID = $UID = $SAVEUID;
return 0;
}
#
# Create a new image descriptor. We have to munge the XML file a bit
# though and write it out.
#
sub CreateImage($$$$$)
{
my ($url, $xmlparse, $user, $group, $imagename) = @_;
my $alltypes = "-a";
my $global = 0;
my @versions = ();
my $metadata_url;
print Dumper($xmlparse);
#
# If we are doing image versioning, and the origin has an image
# history, make sure we can get the metadata for those versions.
#
if ($doprovenance &&
exists($xmlparse->{"version_history"})) {
foreach my $vers (keys(%{ $xmlparse->{"version_history"} })) {
my $val = $xmlparse->{"version_history"}->{$vers};
my $parse = FetchMetadata($val);
fatal("Could not metadata for version $vers")
if (!defined($parse));
$versions[$vers] = $parse;
}
#
# Sanity check that there are no gaps.
#
if (scalar(@versions) !=
scalar(keys(%{ $xmlparse->{"version_history"} }))) {
fatal("Inconsistent number of version history records");
}
#
# Okay, if we have an image history, we actually have to first
# create the base version, and then add all the other versions on
# top of it, ending with the metadata we initially fetched.
#
if (scalar(@versions)) {
my $latest =
$xmlparse->{'attribute'}->{"image_version"}->{'value'};
$versions[$latest] = $xmlparse;
$xmlparse = $versions[0];
}
}
$xmlparse->{'attribute'}->{"pid"} = {};
$xmlparse->{'attribute'}->{"gid"} = {};
$xmlparse->{'attribute'}->{"pid"}->{'value'} = $group->pid();
$xmlparse->{'attribute'}->{"gid"}->{'value'} = $group->gid();
#
# If the origin provided an image_metadata_url, this is the non-version
# specific URL which we can store locally in the images table, for users
# that request it via that URL. This avoids a needless duplicate import.
#
if (exists($xmlparse->{'attribute'}->{"image_metadata_url"})) {
my $url = $xmlparse->{'attribute'}->{"image_metadata_url"}->{'value'};
if (! TBcheck_dbslot($url, "images", "metadata_url",
TBDB_CHECKDBSLOT_ERROR)) {
fatal("Bad image_metadata_url: $url");
}
}
#
# Look for a parent osid; this means we should set the type
# to pcvm since the image is for a VM. Well, we also use this
# for subnodes, but I am not going to worry about that case.
#
if (exists($xmlparse->{'attribute'}->{"def_parentosid"})) {
#
# If parent does not exist, then ignore with warning.
# This can be set later via the web interface.
#
my $posid = $xmlparse->{'attribute'}->{"def_parentosid"}->{'value'};
my $parent = OSImage->Lookup($posid);
if (defined($parent)) {
$xmlparse->{'attribute'}->{"mtype_pcvm"} = {};
$xmlparse->{'attribute'}->{"mtype_pcvm"}->{'value'} = 1;
}
else {
delete($xmlparse->{'attribute'}->{"def_parentosid"});
print STDERR
"*** Parent $posid does not exist, skipping parent.\n";
print STDERR
" You can set the parent later via the web interface.\n";
}
}
# For setting the path below.
if (exists($xmlparse->{'attribute'}->{"global"}) &&
$xmlparse->{'attribute'}->{"global"}->{'value'}) {
$global = 1;
}
#
# We check to see if the imagename is already in use. Hopefully
# not, but if not we have to make something up. Note that I am
# not going to worry about concurrent attempts to create a descriptor
# with the same name.
#
if (defined($imagename)) {
$xmlparse->{'attribute'}->{"imagename"}->{'value'} = $imagename;
}
elsif (! exists($xmlparse->{'attribute'}->{"imagename"})) {
$xmlparse->{'attribute'}->{"imagename"}->{'value'} =
substr(TBGenSecretKey(), 0, 12);
}
elsif (OSImage->Lookup($group->pid(),
$xmlparse->{'attribute'}->{"imagename"}->{'value'})) {
my $index = 1;
my $imagename;
do {
$imagename = $xmlparse->{'attribute'}->{"imagename"}->{'value'};
$imagename .= "_" . $index++;
} while ($index < 100 && OSImage->Lookup($group->pid(), $imagename));
if ($index >= 100) {
fatal("Could not generate a unique image name");
}
$xmlparse->{'attribute'}->{"imagename"}->{'value'} = $imagename;
}
$imagename = $xmlparse->{'attribute'}->{"imagename"}->{'value'};
if ($debug) {
print STDERR "Using imagename: $imagename\n";
}
# do not trust path coming in.
if ($global && $user->IsAdmin()) {
$xmlparse->{'attribute'}->{"path"}->{'value'} = "$TB/images/";
}
elsif ($group->pid() eq $group->gid() || $global) {
$xmlparse->{'attribute'}->{"path"}->{'value'} =
"$TBPROJ_DIR/" . $group->pid() . "/images/";
}
else {
$xmlparse->{'attribute'}->{"path"}->{'value'} =
"$TBPROJ_DIR/" . $group->pid() . "/" . $group->gid() . "/images/";
}
if ($DOIMAGEDIRS) {
$xmlparse->{'attribute'}->{"path"}->{'value'} .= "${imagename}/";
}
else {
$xmlparse->{'attribute'}->{"path"}->{'value'} .= "${imagename}.ndz";
}
#
# Generate a new XML description to feed into newimageid.
#
$xmlparse->{'attribute'}->{"imagefile_url"}->{'value'} =
uri_escape($xmlparse->{'attribute'}->{"imagefile_url"}->{'value'});
#
# Old servers do not provide a metadata url in the blob, so we
# have to set it. This is not ideal, since we do not know if the
# URL we have is the version specific or the non-version specific.
#
if (!exists($xmlparse->{'attribute'}->{"metadata_url"})) {
$xmlparse->{'attribute'}->{"metadata_url"}->{'value'} =
uri_escape($url);
}
# We need this to lookup the new image.
$metadata_url =
uri_unescape($xmlparse->{'attribute'}->{"metadata_url"}->{'value'});
my $newxml = "";
foreach my $key (keys(%{ $xmlparse->{'attribute'} })) {
# Skip these, we handle them elsewhere.
next
if ($key =~
/^(image_metadata_url|havefull|havedelta|version_history)$/);
my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'};
$newxml .=
"$value\n";
}
$newxml = "$newxml";
if ($debug) {
print STDERR "$newxml\n";
}
# Verify first, Use skip admin checks option.
open(NEW, "| $NEWIMAGE_EZ $alltypes -f -s -v -")
or fatal("Cannot start $NEWIMAGE_EZ");
print NEW $newxml;
if (!close(NEW)) {
print STDERR "$newxml\n";
fatal("Image xml did not verify");
}
return undef
if ($verify);
open(NEW, "| $NEWIMAGE_EZ $alltypes -f -s -")
or fatal("Cannot start $NEWIMAGE_EZ");
print NEW $newxml;
if (!close(NEW)) {
print STDERR "$newxml\n";
fatal("Could not create new image from xml");
}
my $image = OSImage->LookupByURL($metadata_url);
if (!defined($image)) {
fatal("Could not lookup new image for $url");
}
#
# If the origin provided an image_metadata_url, this is the non-version
# specific URL which we can store locally in the images table, for users
# that request it via that URL. This avoids a needless duplicate import.
#
if (exists($xmlparse->{'attribute'}->{"image_metadata_url"})) {
my $url = $xmlparse->{'attribute'}->{"image_metadata_url"}->{'value'};
$image->SetImageMetadataURL($url);
}
#
# We are going to mark as released but not ready, since this *is* the
# image we want people to run, but we still have to actually get it.
#
$image->MarkReleased();
#
# Store this in case we need it later for -g option.
#
$image->_havedelta(0);
$image->_havefull(0);
if (exists($xmlparse->{'attribute'}->{"havefull"})) {
$image->_havefull($xmlparse->{'attribute'}->{"havefull"}->{'value'});
}
if (exists($xmlparse->{'attribute'}->{"havedelta"})) {
$image->_havedelta($xmlparse->{'attribute'}->{"havedelta"}->{'value'});
}
#
# Now we can create the image history. All of these images are also
# marked as released but not ready. Skip version zero of course, since
# that is what we just created.
#
# @versions will be empty if not doing provenance, see above.
#
if (scalar(@versions)) {
for (my $i = 1; $i < scalar(@versions); $i++) {
my $xmlparse = $versions[$i];
my $clone = CloneFromMetadata($image, $xmlparse, $user);
if (!defined($clone)) {
$image->Unlock();
fatal("Could not clone image descriptor $image");
}
$image = $clone;
}
}
#
# newimageid_ez is not setuid, so it cannot create the new directory.
#
if ($image->CreateImageDir()) {
$image->Unlock();
fatal("Could not create image directory");
}
return $image;
}
#
# Clone an image with additional info from the parsed metadata.
#
sub CloneFromMetadata($$$)
{
my ($image, $xmlparse, $user) = @_;
my $clone = $image->NewVersion($user, $image, undef);
return undef
if (!defined($clone));
#
# So, here is a bit of a quandry. We want this new version of the
# image to be the released version (for mapping), even though we
# do not have image file yet. So we will force the image to be
# released even though it is not "ready" yet. This will happen
# to each version including the last version (the one we are really
# trying to get to).
#
$clone->MarkReady();
$clone->Release();
$clone->ClearReady();
#
# Store this in case we need it later for -g option.
#
$clone->_havedelta(0);
$clone->_havefull(0);
if (exists($xmlparse->{'attribute'}->{"havefull"})) {
$clone->_havefull($xmlparse->{'attribute'}->{"havefull"}->{'value'});
}
if (exists($xmlparse->{'attribute'}->{"havedelta"})) {
$clone->_havedelta($xmlparse->{'attribute'}->{"havedelta"}->{'value'});
}
UpdateImageFromMetadata($clone, $xmlparse) == 0
or return undef;
return $clone;
}
#
# Update descriptor stuff from the metadata, typically after we have
# created a new version (clone) of the descriptor.
#
sub UpdateImageFromMetadata($$)
{
my ($image, $xmlparse) = @_;
my @imslots = ("imagefile_url", "metadata_url", "hash", "deltahash",
"size", "deltasize");
foreach my $key (@imslots) {
next
if (!exists($xmlparse->{'attribute'}->{$key}));
my $value = $xmlparse->{'attribute'}->{$key}->{'value'};
if (! TBcheck_dbslot($value, "images",
$key, TBDB_CHECKDBSLOT_ERROR)) {
print STDERR
"Illegal value for $key: " . TBFieldErrorString() . "\n";
return -1;
}
$image->Update({$key => $value});
}
my @osslots = ("description", "version", "osfeatures");
foreach my $key (@osslots) {
next
if (!exists($xmlparse->{'attribute'}->{$key}));
my $value = $xmlparse->{'attribute'}->{$key}->{'value'};
if (! TBcheck_dbslot($value, "os_info",
$key, TBDB_CHECKDBSLOT_ERROR)) {
print STDERR
"Illegal value for $key: " . TBFieldErrorString() . "\n";
return -1;
}
$image->Update({$key => $value});
}
return 0;
}
#
# Download the image file, which can be a delta.
#
sub DownLoadImage($$$$)
{
my ($image, $dodelta, $user, $group) = @_;
my $image_url = uri_unescape($image->imagefile_url());
my $localfile = ($dodelta ?
$image->DeltaImageFile() :
$image->FullImageFile()) . ".new";
$image_url .= "&delta=1" if ($dodelta);
if (FetchImageFile($image_url, $localfile)) {
return -1;
}
#
# Verify the hash which was created by FetchImageFile().
#
my $newhashfile = $localfile . ".sha1";
print "Verifying the hash ...\n";
my $filehash = `cat $newhashfile`;
if ($?) {
print STDERR "Could not read sha1 hash file $newhashfile\n";
return -1;
}
chomp($filehash);
if ($filehash =~ /^SHA1.*= (\w*)$/) {
$filehash = $1;
}
else {
print STDERR "Could not parse the sha1 hash: '$filehash'\n";
return -1;
}
if ($filehash ne ($dodelta ? $image->deltahash() : $image->hash())) {
print STDERR "sha1 hash of new file did not match\n";
return -1;
}
#
# Use imagedump to verify the ndz file.
#
print "Verifying ndz file format ...\n";
system("$IMAGEDUMP $localfile");
if ($?) {
return -1;
}
return 0
if ($verify);
#
# Now rename the image files and update the hash file.
#
my $hashfile = ($dodelta ?
$image->DeltaImageSHA1File() : $image->FullImageSHA1File());
my $ndzfile = ($dodelta ?
$image->DeltaImageFile() : $image->FullImageFile());
unlink($hashfile)
if (-e $hashfile);
system("/bin/mv -f $newhashfile $hashfile");
if ($?) {
return -1;
}
if (-e $ndzfile) {
system("/bin/mv -f $ndzfile ${ndzfile}.old");
if ($?) {
return -1;
}
}
system("/bin/mv -f $localfile $ndzfile");
if ($?) {
return -1;
}
#
# Try to download a sig file. We have to accept that this might
# fail, which is okay since Mike says we can generate a new one,
# it just takes a while to do.
#
FetchSigFile($image, $dodelta);
return 0;
}
#
# Fetch a file.
#
sub FetchImageFile($$)
{
my ($url, $localfile) = @_;
my $safe_url = User::escapeshellarg($url);
my $user_uid = $user->uid();
#
# Build up a new command line to do the fetch on ops
# But no reason to do this if an admin, which is important
# when the image is going into /usr/testbed/images.
#
if (!$user->IsAdmin()) {
my $cmdargs = "$TB/bin/fetchtar.proxy -h -u $user_uid";
my $glist = `/usr/bin/id -G $user_uid`;
if ($glist =~ /^([\d ]*)$/) {
$glist = join(",", split(/\s+/, $1));
}
else {
print STDERR "Unexpected results from 'id -G $user': $glist\n";
return -1;
}
$cmdargs .= " -g '$glist' \"$safe_url\" $localfile";
print "Downloading $url ...\n";
if ($debug) {
print "$cmdargs\n";
}
$EUID = $UID = 0;
system("sshtb -host $CONTROL $cmdargs ");
if ($?) {
$EUID = $UID = $SAVEUID;
print STDERR "Fetch of image file failed\n";
return -1;
}
$UID = $SAVEUID;
}
else {
print "Downloading $url ...\n";
if (! open(GET, "| nice -15 $WGET --no-check-certificate ".
"--timeout=30 --waitretry=30 --retry-connrefused ".
"-q -O $localfile -i -")) {
print STDERR "Cannot start $WGET\n";
return -1;
}
print GET "$url\n";
return -1
if (!close(GET));
system("$SHA1 $localfile > ${localfile}.sha1");
if ($?) {
print STDERR "Could not generate sha1 hash of $localfile\n";
return -1;
}
}
return 0;
}
#
# Fetch the metadata from the provided URL. Return the XML parse,
#
sub FetchMetadata($)
{
my ($url) = @_;
$url .= "&clientversion=" . $METADATA_CLIENTVERSION;
# We know this is valid, but must taint check anyway for shell command.
if ($url =~/^(.*)$/) {
$url = $1;
}
my $safe_url = User::escapeshellarg($url);
my $xml = "";
my $opts = ($debug ? "" : "-q");
my $cmd = "$WGET $opts --no-check-certificate -O - $safe_url ";
if ($debug) {
print "$cmd\n";
}
if (open(META, "$cmd |")) {
while () {
$xml .= $_;
}
close(META);
}
else {
print STDERR "Could not read metadata from $url\n";
return undef;
}
if ($xml eq "") {
print STDERR "Failed to get metadata from $url\n";
return undef;
}
my $xmlparse = eval { XMLin($xml,
VarAttr => 'name',
ContentKey => '-content',
SuppressEmpty => undef); };
if ($@) {
print STDERR "$@\n";
return undef;
}
if ($debug) {
print STDERR Dumper($xmlparse);
}
#
# Sanity checks; it must have a hash and a url inside. We let
# newimageid do the rest of the checks though.
#
if (!((exists($xmlparse->{'attribute'}->{"hash"}) &&
$xmlparse->{'attribute'}->{"hash"}->{'value'} =~ /^\w{10,}$/) ||
(exists($xmlparse->{'attribute'}->{"deltahash"}) &&
$xmlparse->{'attribute'}->{"deltahash"}->{'value'} =~ /^\w{10,}$/))){
print STDERR "Invalid hash in metadata\n";
return undef;
}
if (! exists($xmlparse->{'attribute'}->{"imagefile_url"})) {
print STDERR "Invalid imagefile url in metadata\n";
return undef;
}
# Silly taint check.
if (exists($xmlparse->{'attribute'}->{"hash"})) {
if ($xmlparse->{'attribute'}->{"hash"}->{'value'} =~ /^(.*)$/) {
$xmlparse->{'attribute'}->{"hash"}->{'value'} = $1;
}
}
else {
if ($xmlparse->{'attribute'}->{"deltahash"}->{'value'} =~ /^(.*)$/) {
$xmlparse->{'attribute'}->{"deltahash"}->{'value'} = $1;
}
}
return $xmlparse;
}
#
# Fetch sig file.
#
sub FetchSigFile($$)
{
my ($image,$dodelta) = @_;
my $image_url = uri_unescape($image->imagefile_url()) . "&sigfile=1";
my $localfile;
my $sigfile;
if ($dodelta) {
$localfile = $image->DeltaImageSigFile() . ".new";
$sigfile = $image->DeltaImageSigFile();
$image_url .= "&delta=1";
}
else {
$localfile = $image->FullImageSigFile() . ".new";
$sigfile = $image->FullImageSigFile();
}
if (! FetchImageFile($image_url, $localfile)) {
system("/bin/mv -f $localfile $sigfile");
# Do not need this.
unlink("${localfile}.sha1")
if (-e "${localfile}.sha1");
}
return 0;
}
sub fatal($)
{
my ($mesg) = @_;
$image->Unlock()
if (defined($image) && $image->GotLock());
print STDERR "*** $0:\n".
" $mesg\n";
exit(-1);
}