#!/usr/bin/perl -w
#
# Copyright (c) 2000-2015 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 Data::Dumper;
use File::Temp qw(tempfile);
use CGI;
use File::Basename;
#
# Clone an image (descriptor) from a node and then snapshot
# that node into the descriptor. Creates the descriptor if
# if it does not exist. The idea is to use all of the info
# from the current image descriptor that is loaded on the node
# to quickly create a new descriptor by inheriting all of the
# attributes of the original.
#
# We also want to support taking a snapshot of a previously
# created clone. To make everything work properly, require
# that the imagename exist in the experiment project, which
# ensures that we are operating on a clone, not an image in
# some other project or a system image.
#
sub usage()
{
print("Usage: clone_image [-dwe] [-n | -s] \n".
"Options:\n".
" -d Turn on debug mode\n".
" -e Create a whole disk image\n".
" -g 0,1 Override base image global setting\n".
" -r 0,1 Override base image shared (within project) setting\n".
" -s Create descriptor but do not snapshot\n".
" -n Impotent mode\n".
" -F Create a full image even if deltas are on\n".
" -w Wait for image to be created\n");
exit(-1);
}
my $optlist = "densg:wFr:";
my $debug = 0;
my $wholedisk = 0;
my $impotent = 0;
my $nosnapshot = 0;
my $isvirtnode = 0;
my $waitmode = 0;
my $nodelta = 0; # To pass to create_image.
my $global = 0;
my $shared = 0;
#
# Configure variables
#
my $TB = "@prefix@";
my $PROJROOT = "@PROJROOT_DIR@";
my $GROUPROOT = "@GROUPSROOT_DIR@";
my $CREATEIMAGE = "$TB/bin/create_image";
my $NEWIMAGEEZ = "$TB/bin/newimageid_ez";
my $DOPROVENANCE = @IMAGEPROVENANCE@;
my $doprovenance = 0;
#
# 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;
#
# Load the Testbed support stuff.
#
use lib "@prefix@/lib";
use EmulabConstants;
use emutil;
use User;
use Project;
use Image;
use OSinfo;
use Node;
use EmulabFeatures;
# Protos
sub fatal($);
#
# 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{"e"})) {
$wholedisk = 1;
}
if (defined($options{"n"})) {
$impotent = 1;
}
if (defined($options{"s"})) {
$nosnapshot = 1;
}
if (defined($options{"w"})) {
$waitmode = 1;
}
if (defined($options{"F"})) {
$nodelta = 1;
}
if (defined($options{"g"})) {
$global = $options{"g"};
}
elsif (defined($options{"r"})) {
$shared = $options{"r"};
}
usage()
if (@ARGV != 2);
my $imagename = shift(@ARGV);
my $node_id = shift(@ARGV);
#
# Map invoking user to object.
#
my $this_user = User->ThisUser();
if (! defined($this_user)) {
fatal("You ($UID) do not exist!");
}
#
# The node must of course be allocated and the user must have
# permission to clone it.
#
my $node = Node->Lookup($node_id);
if (!defined($node)) {
fatal("No such node");
}
$isvirtnode = $node->isvirtnode();
if (!$node->AccessCheck($this_user, TB_NODEACCESS_LOADIMAGE())) {
fatal("Not enough permission");
}
my $experiment = $node->Reservation();
if (!defined($experiment)) {
fatal("Node is not reserved");
}
my $pid = $experiment->pid();
my $group = $experiment->GetGroup();
my $gid = $group->gid();
my $project = $experiment->GetProject();
if (! (defined($project) && defined($group))) {
fatal("Could not get project/group for $experiment");
}
my $image = Image->Lookup($project->pid(), $imagename);
#
# Need to look up the base image; the image that is currently running
# on the node and being cloned.
#
my ($base_osinfo, $base_image) = $node->RunningOsImage();
# No support for cloning MFSs, so there will always be a base image.
if (! (defined($base_osinfo) && defined($base_image))) {
fatal("Could not determine osid/imageid for $node_id");
}
print "$node_id is running $base_osinfo,$base_image\n"
if ($debug);
# See if enabled.
$doprovenance =
EmulabFeatures->FeatureEnabled("ImageProvenance", $this_user, $project);
# Temporary override for all geni projects until we can export deltas.
if ($project->IsNonLocal()) {
my $feature = EmulabFeatures->Lookup("ImageProvenance");
if ($feature && $feature->enabled()) {
# If globally enabled, disable. If not globally enabled but
# doprovenance is on, then it was a per-project enable which
# we want to respect.
$doprovenance = 0;
}
}
#
# The simple case is that the descriptor already exists. So it is just
# a simple snapshot to the image file.
#
if (defined($image)) {
my $needdelete = 0;
#
# Only EZ images via this interface.
#
if (!$image->ezid()) {
fatal("Cannot clone a non-ez image");
}
#
# The access check above determines if the caller has permission
# to overwrite the image file.
# Not that this matters, cause create_image is going to make the
# same checks.
#
if ($impotent) {
print "Not doing anything in impotent mode\n";
exit(0);
}
#
# Before we do anything destructive, we lock the image.
#
if ($image->Lock()) {
fatal("Image is locked, please try again later!\n");
}
if ($DOPROVENANCE && $doprovenance) {
#
# This will include unreleased images (in image_versions, but
# not the one pointed to by the images table).
#
$image = $image->LookupMostRecent();
if (!defined($image)) {
$image->Unlock();
fatal("Cannot lookup osinfo for $image");
}
#
# We create a new version of the image descriptor for the new
# snapshot. We mark it as not ready so that others know it is
# in transition. When we later call createimage, it will make
# sure the ready bit is clear before trying to use it.
#
my $needclone = 1;
#
# Does the most recent version in the table not have its ready bit set?
# If so it means something went wrong with a previous image creation.
# We can reuse it, but reset the provenance just in case the node got
# reloaded.
#
if (!$image->ready()) {
my $osinfo = OSinfo->Lookup($image->imageid(), $image->version());
if (!defined($osinfo)) {
$image->Unlock();
fatal("Cannot lookup osinfo for $image");
}
$image->SetProvenance($base_image);
$osinfo->SetProvenance($base_osinfo);
$needclone = 0;
print "Reusing image version " . $image->version() . ", ".
"since it was never marked ready.\n";
}
#
# If the new image is going to based on the exact same base,
# lets not create another new version, but overwrite the current
# one. Save lots of space this way. We miss saving intermediate
# versions, but this is a typical approach to getting an image
# ready for use; change, snapshot, test, change, snapshot ...
#
if ($needclone && !$image->released() &&
($image->parent_imageid() == $base_image->imageid() &&
$image->parent_version() == $base_image->version())) {
# For create_image to be happy.
$image->ClearReady(0);
$needclone = 0;
print "Reusing image version " . $image->version() . ", ".
"since the base is the same and it was not released.\n";
}
if ($needclone) {
my $clone_error;
my $clone = $image->NewVersion($this_user,
$base_image, \$clone_error);
if (!defined($clone)) {
$image->Unlock();
fatal("Could not clone image descriptor" .
(defined($clone_error) ? ": $clone_error" : "") . "\n");
}
$image = $clone;
$needdelete = 1;
#
# Watch for a system image that is saved elsewhere; see equiv code
# in create_image. We change the path to point over to the /proj
# directory so that we do not burn up space on boss until it is
# officially "released". We *can* use this version of the image
# by explicitly using its version number, before it is released.
#
if ($image->path() =~ /^$TB/) {
my $path = $PROJROOT . "/" . $image->pid() . "/images/" .
basename($image->path());
if ($image->Update({"path" => $path})) {
$image->DeleteVersion();
fatal("Could not update path!");
}
}
}
}
$image->Unlock();
if ($nosnapshot) {
print "Not taking a snapshot, as directed\n"
if ($debug);
exit(0);
}
my $opts = "-p $pid ";
$opts .= "-w " if ($waitmode);
$opts .= "-F " if ($nodelta);
#
# Mike says do not pass versioned imagenames to create_image when
# provenance is turned off.
#
$imagename = $image->imagename();
$imagename .= ":" . $image->version() if ($DOPROVENANCE && $doprovenance);
my $output = emutil::ExecQuiet("$CREATEIMAGE $opts $imagename $node_id");
if ($?) {
$image->DeleteVersion()
if ($needdelete);
print STDERR $output;
fatal("Failed to create image");
}
print "Image is being created. This can take 15-30 minutes.\n";
exit(0);
}
DoNew:
#
# Only EZ images via this interface.
#
if (!$base_image->ezid()) {
fatal("Cannot clone a non-ez image");
}
#
# Not allowed to derive an image from one that has not been released.
# Maybe relax this in the future, but this is a good simplification for
# now.
#
if ($DOPROVENANCE && $doprovenance && !$base_image->released()) {
fatal("Not allowed to derive a new image from unreleased ".
"base $base_image");
}
#
# To avoid confusion, we do not allow users to shadow system images
# in their own project.
#
if (Image->LookupByName($imagename) && !$this_user->IsAdmin()) {
fatal("Not allowed to shadow snapshot a system image");
}
# Subgroups change the path, but a global image should still
# go into the project image directory.
my $path = ($experiment->pid() eq $experiment->gid() || $global ?
"$PROJROOT/$pid/images/${imagename}.ndz" :
"$GROUPROOT/$pid/$gid/images/${imagename}.ndz");
#
# Create the image descriptor. We use the backend script to do the
# heavy lifting, but we have to cons up an XML file based on the image
# descriptor that is being cloned.
#
# These are the fields we have to come up with, plus a number
# of mtype_* entries.
#
my %xmlfields =
("imagename" => $imagename,
"pid" => $project->pid(),
"gid" => $experiment->gid(),
"description" => $base_osinfo->description(),
"OS" => $base_osinfo->OS(),
"version" => $base_osinfo->version(),
"path" => $path,
"op_mode", => $base_osinfo->op_mode(),
"wholedisk", => $wholedisk,
);
$xmlfields{"reboot_waittime"} = $base_osinfo->reboot_waittime()
if (defined($base_osinfo->reboot_waittime()));
$xmlfields{"osfeatures"} = $base_osinfo->osfeatures()
if (defined($base_osinfo->osfeatures()) &&
$base_osinfo->osfeatures() ne "");
if ($global) {
$xmlfields{"global"} = 1;
}
elsif ($shared) {
$xmlfields{"shared"} = 1;
}
if (defined($base_image)) {
$xmlfields{"mbr_version"} = $base_image->mbr_version();
$xmlfields{"loadpart"} = $base_image->loadpart();
$xmlfields{"noexport"} = $base_image->noexport();
# Short form uses wholedisk instead. Should fix this.
if ($base_image->loadpart() == 0 && $base_image->loadlength() == 4) {
$xmlfields{"wholedisk"} = 1;
$xmlfields{"loadpart"} = 1;
#
# Ick. we have to tell newimageid_ez the correct loadpart, since
# it uses that as the boot partition.
#
for (my $i = 1; $i <= 4; $i++) {
my $func = "part${i}_osid";
my $foo = $base_image->$func();
if (defined($foo) && $foo == $base_image->default_osid()) {
$xmlfields{"loadpart"} = $i;
last;
}
}
}
}
elsif ($isvirtnode) {
$xmlfields{"reboot_waittime"} = 240;
$xmlfields{"loadpart"} = 1;
$xmlfields{"mtype_pcvm"} = 1;
$xmlfields{"wholedisk"} = 1;
}
else {
fatal("No base image for $node_id");
}
# This needs more thought.
if (($isvirtnode || $base_osinfo) && $base_osinfo->def_parentosid()) {
my $parentosinfo = OSinfo->Lookup($base_osinfo->def_parentosid());
if (!defined($parentosinfo)) {
fatal("Could not lookup object for parent osid of $base_osinfo");
}
$xmlfields{"def_parentosid"} =
$parentosinfo->pid() . "," . $parentosinfo->osname();
# And this is just plain bogus.
#$xmlfields{"mbr_version"} = 99;
}
#
# Grab the existing type list and generate new mtype_* variables.
#
if (defined($base_image)) {
my @typelist = $base_image->TypeList($base_osinfo);
if (! @typelist && defined($base_image->deleted())) {
my $sysimage = Image->LookupByName($base_image->imagename());
if (defined($sysimage)) {
@typelist = $sysimage->TypeList();
}
}
if (! @typelist) {
fatal("$base_image does not run on any types");
}
foreach my $type (@typelist) {
my $type_id = $type->type();
$xmlfields{"mtype_${type_id}"} = 1;
}
}
#
# Create the XML file to pass to newimageid_ez.
#
my ($fh, $filename) = tempfile(UNLINK => 1);
fatal("Could not create temporary file")
if (!defined($fh));
print $fh "\n";
foreach my $key (keys(%xmlfields)) {
my $value = $xmlfields{$key};
print $fh "";
print $fh "" . CGI::escapeHTML($value) . "";
print $fh "\n";
}
print $fh "\n";
close($fh);
if ($debug) {
system("/bin/cat $filename");
}
my $output = emutil::ExecQuiet("$NEWIMAGEEZ -s -v $filename");
if ($?) {
print STDERR $output;
my $foo = `cat $filename`;
print STDERR $foo;
fatal("Failed to verify image descriptor from $filename");
}
if ($impotent) {
print "Not doing anything in impotent mode\n";
system("cat $filename");
exit(0);
}
$output = emutil::ExecQuiet("$NEWIMAGEEZ -s $filename");
if ($?) {
print STDERR $output;
my $foo = `cat $filename`;
print STDERR $foo;
fatal("Failed to create image descriptor");
}
$image = Image->Lookup($project->pid(), $imagename);
if (!defined($image)) {
fatal("Cannot lookup newly created image for $imagename");
}
my $osinfo = OSinfo->Lookup($image->imageid());
if (!defined($osinfo)) {
fatal("Cannot lookup newly created osinfo for $image");
}
if ($DOPROVENANCE && $doprovenance) {
$image->SetProvenance($base_image);
$osinfo->SetProvenance($base_osinfo);
}
if ($debug) {
print "Created $osinfo\n";
print "Created $image\n";
}
if ($nosnapshot) {
print "Not taking a snapshot, as directed\n"
if ($debug);
exit(0);
}
my $opts = "-p $pid ";
$opts .= "-w " if ($waitmode);
$opts .= "-F " if ($nodelta);
$output = emutil::ExecQuiet("$CREATEIMAGE $opts $imagename $node_id");
if ($?) {
print STDERR $output;
fatal("Failed to create image");
}
print "Image is being created. This can take 15-30 minutes.\n";
exit(0);
sub fatal($)
{
my ($mesg) = @_;
die("*** $0:\n".
" $mesg\n");
}