Commit b01c991d authored by Leigh B Stoller's avatar Leigh B Stoller

New script, clone_image to simplify create/snapshot from a node.

clone_image is a wrapper around newimageid_ez and create_image, that
simplifies the most common operation; creating a new imageid derived
from the image/os that is currently running in the node, and then
taking a snapshot of the node. So for example, if node pcXXX is
running image FREEBSD, and you want to create a custom image from that
node, all you need to do:

	boss> clone_image myfreebsd pcXXX

which will create the new descriptor, deriving everything from the
FREEBSD image on the node, and then take a snapshot from pcXXX. If
the descriptor already exists, just take the snapshot.

So what if you do:

	boss> clone_image FREEBSD pcXXX

well, the image is always looked up in the project the node is
currently attached to, so in fact a new descriptor is created in that
project, and you do not actually overwrite an image from some other
project. 

I've added some locking to images to prevent concurrent snapshots.
This seemed like a good idea since this script is going to be used
from the ProtoGeni interface. More on this in another commit.
parent 516dec51
......@@ -136,6 +136,7 @@ sub mbr_version($) { return field($_[0], "mbr_version"); }
sub access_key($) { return field($_[0], "access_key"); }
sub uuid($) { return field($_[0], "uuid"); }
sub hash($) { return field($_[0], "hash"); }
sub locked($) { return field($_[0], "locked"); }
#
# Get a list of all running frisbee images.
......@@ -768,19 +769,68 @@ sub SetHash($$)
}
#
# Get the type list.
# Lock and Unlock
#
sub TypeList($)
sub Lock($)
{
my ($self) = @_;
# Must be a real reference.
return -1
if (! ref($self));
return -1
if (!DBQueryWarn("lock tables images write"));
my $imageid = $self->imageid();
my $query_result =
DBQueryWarn("update images set locked=now() " .
"where imageid='$imageid' and locked is null");
if (! $query_result ||
$query_result->numrows == 0) {
DBQueryWarn("unlock tables");
return -1;
}
DBQueryWarn("unlock tables");
$self->{'IMAGE'}->{'locked'} = time();
return 0;
}
sub Unlock($)
{
my ($self) = @_;
# Must be a real reference.
return -1
if (! ref($self));
my $imageid = $self->imageid();
return -1
if (! DBQueryWarn("update images set locked=null " .
"where imageid='$imageid'"));
$self->{'IMAGE'}->{'locked'} = 0;
return 0;
}
#
# Get the type list.
#
sub TypeList($;$)
{
my ($self, $osinfo) = @_;
require NodeType;
my @result = ();
my $imageid = $self->imageid();
my $clause = (defined($osinfo) ?
"and osid='" . $osinfo->osid() . "'" : "");
my $query_result =
DBQueryWarn("select distinct type from osidtoimageid ".
"where imageid='$imageid'");
"where imageid='$imageid' $clause");
return undef
if (!defined($query_result));
......
......@@ -3390,5 +3390,34 @@ sub GetOutletAuthInfo($$)
return ($login, $auxinfo);
}
#
# Get the currently running os/image for a node.
#
sub RunningOsImage($)
{
require OSinfo;
my ($self) = @_;
my $nodeid = $self->node_id();
my $osid = $self->def_boot_osid();
my $osinfo = OSinfo->Lookup($osid);
return ()
if (!defined($osinfo));
my $query_result =
DBQueryWarn("select imageid from partitions as p ".
"where p.node_id='$nodeid' and p.osid='$osid'");
return ()
if (!$query_result || !$query_result->numrows);
my ($imageid) = $query_result->fetchrow_array();
my $image = Image->Lookup($imageid);
return ()
if (!defined($image));
return ($osinfo, $image);
}
# _Always_ make sure that this 1 is at the end of the file...
1;
......@@ -1747,6 +1747,7 @@ CREATE TABLE `images` (
`auth_key` varchar(512) default NULL,
`decryption_key` varchar(256) default NULL,
`hash` varchar(64) default NULL,
`locked` datetime default NULL,
PRIMARY KEY (`imageid`),
UNIQUE KEY `pid` (`pid`,`imagename`),
KEY `gid` (`gid`),
......
#
# Add busy bit to images table. Used by create_image to mark an
# image as being in the process of a snapshot.
#
use strict;
use libdb;
sub DoUpdate($$$)
{
my ($dbhandle, $dbname, $version) = @_;
if (! DBSlotExists("images", "locked")) {
DBQueryFatal("alter table images add ".
" `locked` datetime default NULL");
}
return 0;
}
1;
# Local Variables:
# mode:perl
# End:
......@@ -29,7 +29,7 @@ SBIN_SCRIPTS = vlandiff vlansync withadminprivs export_tables cvsupd.pl \
archive-expinfo grantfeature emulabfeature addblob readblob \
prereserve grantimage getimages localize_mfs \
management_iface sharevlan check-shared-bw \
addspecialdevice addspecialiface imagehash
addspecialdevice addspecialiface imagehash clone_image
WEB_SBIN_SCRIPTS= webnewnode webdeletenode webspewconlog webarchive_list \
webwanodecheckin webspewimage
......
#!/usr/bin/perl -w
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2012 University of Utah and the Flux Group.
# All rights reserved.
#
use English;
use strict;
use Getopt::Std;
use Data::Dumper;
use File::Temp qw(tempfile);
use CGI;
#
# 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 [-d] [-e] [-n | -s] <imagename> <node_id>\n".
"Options:\n".
" -d Turn on debug mode\n".
" -e Create a whole disk image\n".
" -s Create descriptor but do not snapshot\n".
" -n Impotent mode\n");
exit(-1);
}
my $optlist = "dens";
my $debug = 0;
my $wholedisk = 0;
my $impotent = 0;
my $nosnapshot = 0;
#
# Configure variables
#
my $TB = "@prefix@";
my $PROJROOT = "@PROJROOT_DIR@";
my $CREATEIMAGE = "$TB/bin/create_image";
my $NEWIMAGEEZ = "$TB/bin/newimageid_ez";
#
# 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;
# 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;
}
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");
}
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 $project = $experiment->GetProject();
if (! (defined($project) && defined($group))) {
fatal("Could not get project/group for $experiment");
}
my $image = Image->Lookup($project->pid(), $imagename);
#
# The simple case is that the descriptor already exists. So it is just
# a simple snapshot to the image file.
#
if (defined($image)) {
#
# 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.
#
# But we do not allow emulab-ops images to ever be overwritten.
# Might remove this later. Just being careful since this is going
# to be used from the ProtoGENI RPC interface.
#
if ($image->pid eq TBOPSPID()) {
fatal("Not allowed to snapshot a system image");
}
if ($impotent) {
print "Not doing anything in impotent mode\n";
exit(0);
}
if ($nosnapshot) {
print "Not taking a snapshot, as directed\n";
exit(0);
}
my $output = emutil::ExecQuiet("$CREATEIMAGE -p $pid $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);
}
#
# 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();
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);
#
# 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_image->description(),
"loadpart" => $base_image->loadpart(),
"OS" => $base_osinfo->OS(),
"version" => $base_osinfo->version(),
"path" => "$PROJROOT/$pid/images/${imagename}.ndz",
"node_id" => $node_id,
"osfeatures", => $base_osinfo->osfeatures(),
"op_mode", => $base_osinfo->op_mode(),
"wholedisk", => $wholedisk,
"mbr_version", => $base_image->mbr_version(),
);
#
# Grab the existing type list and generate new mtype_* variables.
#
my @typelist = $base_image->TypeList($base_osinfo);
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 "<image>\n";
foreach my $key (keys(%xmlfields)) {
my $value = $xmlfields{$key};
print $fh "<attribute name=\"$key\">";
print $fh "<value>" . CGI::escapeHTML($value) . "</value>";
print $fh "</attribute>\n";
}
print $fh "</image>\n";
close($fh);
my $output = emutil::ExecQuiet("$NEWIMAGEEZ -v -a $filename");
if ($?) {
print STDERR $output;
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 -a $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 ($debug) {
print "Created $osinfo\n";
print "Created $image\n";
}
if ($nosnapshot) {
print "Not taking a snapshot, as directed\n";
exit(0);
}
$output = emutil::ExecQuiet("$CREATEIMAGE -p $pid $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");
}
......@@ -50,7 +50,7 @@ sub usage()
"<node> - nodeid to create the image from\n");
exit(-1);
}
my $optlist = "p:wsNd";
my $optlist = "p:wsNdf";
my $waitmode = 0;
my $usessh = 0;
my $usenfs = 0;
......@@ -108,10 +108,12 @@ my $devtype;
my $devnum;
my $mereuser = 0;
my $debug = 0;
my $foreground = 0;
my $imagepid = TB_OPSPID;
my $logfile;
my $oldlogfile;
my $needcleanup = 0;
my $needunlock = 0;
#
# Parse command arguments. Once we return from getopts, all that should be
......@@ -141,6 +143,9 @@ if (defined($options{"d"})) {
$debug = 1;
$waitmode = 0;
}
if (defined($options{"f"})) {
$foreground = 1;
}
if (@ARGV != 2) {
usage();
}
......@@ -338,28 +343,33 @@ if (! TBValidUserDir($filename, 0)) {
" $filename does not resolve to an allowed directory!\n");
}
#
# Before we do anything destructive, we lock the descriptor.
#
if ($image->Lock()) {
die("*** $0:\n".
" Image is locked, try again later!\n");
}
$needunlock = 1;
#
# Be sure to kill off running frisbee. If a node is trying to load that
# image, well tough.
#
system("$friskiller -k $imageid");
if ($?) {
die("*** $0:\n".
" Could not kill running frisbee for $imageid!\n");
fatal("Could not kill running frisbee for $imageid!");
}
if (-e $filename) {
unlink($filename) or
die("*** $0:\n".
" Could not delete $filename: $!\n");
fatal("Could not delete $filename: $!");
}
open(FILE, "> $filename") or
die("*** $0:\n".
" Could not create $filename: $!\n");
fatal("Could not create $filename: $!");
close(FILE) or
die("*** $0:\n".
" Could not truncate $filename: $!\n");
fatal("Could not truncate $filename: $!");
#
# Get the disktype for this node
......@@ -378,8 +388,7 @@ my $device = "/dev/${devtype}${devnum}";
# revision of the testbed image it was based off.
#
$image->MarkUpdateTime() == 0 or
die("*** $0:\n".
" Could not mark the update time in $image\n");
fatal("Could not mark the update time in $image");
#
# Okay, we want to build up a command line that will run the script on
......@@ -410,7 +419,7 @@ if ($usefup || $usessh) {
#
# Go to the background since this is going to take a while.
#
if (!$debug) {
if (! ($debug || $foreground)) {
$logfile = Logfile->Create($experiment->gid_idx());
fatal("Could not create a logfile")
if (!defined($logfile));
......@@ -456,10 +465,11 @@ if (!$debug) {
}
#
# When in waitmode, must put ourselves in another process group so that
# an interrupt to the parent will not have any effect on the backend.
# New process group since we get called from the web interface,
# and so the child does not get zapped if the user types ^C
# in waitmode.
#
if ($waitmode) {
if (! ($debug || $foreground)) {
POSIX::setsid();
}
......@@ -674,6 +684,7 @@ if (defined($logfile)) {
if (defined($oldlogfile));
$logfile->Delete(1);
}
$image->Unlock();
exit 0;
sub cleanup ()
......@@ -737,6 +748,8 @@ sub fatal($)
# This was mailed so no longer needed.
unlink("$logfile->filename()");
}
$image->Unlock()
if ($needunlock);
exit(-1);
}
......@@ -901,3 +914,4 @@ sub run_with_ssh($$)
return $stat;
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment