#!/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"); }