Commit 980aa180 authored by Leigh Stoller's avatar Leigh Stoller

Add prototype EC2 image import plumbing.

To create a new descriptor that will be an import from EC2 (and thus
run under XEN), add ?ec2=1 to newimage_ez.php3. Eventually will link
it in someplace. The form will create a XEN based VM, but instead of
node to snapshot from, provide user@host for the EC2 instance.

On the image snapshot page, instead of node use user@host for the EC2
instance.

The backend script (create_image) will call over to ops and invoke
Srikanth's code. I have called that script ec2import-image.pl. See
create_image for how arguments are passed to the script.
parent 03febd79
......@@ -1883,6 +1883,7 @@ CREATE TABLE `images` (
`locker_pid` int(11) default '0',
`metadata_url` tinytext,
`imagefile_url` tinytext,
`logfileid` varchar(40) default NULL,
PRIMARY KEY (`imageid`),
UNIQUE KEY `pid` (`pid`,`imagename`),
KEY `gid` (`gid`),
......
#
# Add logfiles to image.
#
use strict;
use libdb;
my $impotent = 0;
sub DoUpdate($$$)
{
my ($dbhandle, $dbname, $version) = @_;
if (!DBSlotExists("images", "logfileid")) {
DBQueryFatal("alter table images add ".
" `logfileid` varchar(40) default NULL");
}
return 0;
}
# Local Variables:
# mode:perl
# End:
......@@ -60,7 +60,7 @@ WEB_BIN_SCRIPTS = webcreate_image websetdest weblinkmon_ctl webspewevents \
LIBEXEC_SCRIPTS = spewleds webcopy spewsource webcvsweb xlogin webviewvc \
$(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS)
CTRLSBIN_SCRIPTS= opsdb_control.proxy daemon_wrapper
CTRLSBIN_SCRIPTS= opsdb_control.proxy daemon_wrapper ec2import.proxy
# These scripts installed setuid, with sudo.
SETUID_BIN_SCRIPTS = create_image
......
......@@ -21,6 +21,7 @@
#
# }}}
#
use strict;
use English;
use Getopt::Std;
use POSIX qw(setsid :sys_wait_h);
......@@ -124,6 +125,7 @@ sub run_with_ssh($$);
my $nodereboot = "$TB/bin/node_reboot";
my $createimage = "/usr/local/bin/create-image";
my $reboot_prep = "@CLIENT_BINDIR@/reboot_prepare";
my $EC2SNAP = "$TB/sbin/ec2import.proxy";
my $friskiller = "$TB/sbin/frisbeehelper";
my $osselect = "$TB/bin/os_select";
my $checkquota = "$TB/sbin/checkquota";
......@@ -144,8 +146,12 @@ my $oldlogfile;
my $needcleanup = 0;
my $needunlock = 0;
my $isvirtnode = 0;
my $isec2node = 0;
my $onsharednode= 0;
my $didbackup = 0;
my $node_id;
my $node;
my ($experiment,$pid);
#
# Parse command arguments. Once we return from getopts, all that should be
......@@ -186,7 +192,7 @@ if (@ARGV != 2) {
}
my $imagename = $ARGV[0];
my $node_id = $ARGV[1];
my $target = $ARGV[1];
#
# There is no reason to run as root unless we are taking a snapshot
......@@ -198,14 +204,6 @@ $EUID = $UID;
#
# Untaint the arguments.
#
if ($node_id =~ /^([-\w]+)$/) {
$node_id = $1;
}
else {
die("*** $0:\n".
" Bad data in $node_id\n");
}
if ($imagename =~ /^([-\w\.\+]+)$/) {
$imagename = $1;
}
......@@ -257,25 +255,6 @@ my $user_uid = $this_user->uid();
my $user_name = $this_user->name();
my $user_email = $this_user->email();
# Check node and permission
my $node = Node->Lookup($node_id);
if (!defined($node)) {
die("*** $0:\n".
" Invalid node name $node_id!\n");
}
$isvirtnode = $node->isvirtnode();
$onsharednode = $node->sharing_mode()
if ($isvirtnode);
if ($UID && ! $this_user->IsAdmin()) {
$mereuser = 1;
if (! $node->AccessCheck($this_user, TB_NODEACCESS_LOADIMAGE)) {
die("*** $0:\n".
" You do not have permission to create an image from $node\n");
}
}
#
# Before doing anything else, check for overquota ... lets not waste
# our time. Make sure user sees the error by exiting with 1.
......@@ -285,35 +264,8 @@ if (system("$checkquota $user_uid") != 0) {
" You are over your disk quota on $CONTROL; ".
"please login there and cleanup!\n");
}
#
# We need the project id for test below. The target directory for the
# output file has to be the node project directory, since that is the
# directory that is going to be NFS mounted by default.
#
my $experiment = $node->Reservation();
if (!defined($experiment)) {
die("*** $0:\n".
" Could not map $node to its experiment object!\n");
}
my $pid = $experiment->pid();
my $eid = $experiment->eid();
#
# To avoid blowing a cavernous hole ("allow all TCP ports to boss")
# in the per-experiment firewall, we don't use the frisbee uploader if
# the node is firewalled.
#
if ($usefup && $experiment->IsFirewalled()) {
print "*** WARNING: $node_id is firewalled, not using Frisbee uploader\n";
$usefup = 0;
if ($NONFS) {
$usenfs = 0;
$usessh = 1;
} else {
$usenfs = 1;
$usessh = 0;
}
if ($UID && ! $this_user->IsAdmin()) {
$mereuser = 1;
}
#
......@@ -334,6 +286,76 @@ if ($mereuser &&
" You do not have permission to use imageid $imageid!\n");
}
#
# Is it a local node or a remote EC2 node (need to generalize).
#
if ($target =~ /^.*@.*$/) {
if ($target =~ /^([-\w\@\+\.]+)$/) {
$target = $1;
}
else {
die("*** $0:\n".
" Bad data in $target\n");
}
$isec2node = 1;
$usefup = $usessh = 0;
$pid = $image->pid();
}
else {
if ($target =~ /^([-\w]+)$/) {
$node_id = $1;
}
else {
die("*** $0:\n".
" Bad data in $target\n");
}
# Check node and permission
$node = Node->Lookup($node_id);
if (!defined($node)) {
die("*** $0:\n".
" Invalid node name $node_id!\n");
}
$isvirtnode = $node->isvirtnode();
$onsharednode = $node->sharing_mode()
if ($isvirtnode);
if (! $node->AccessCheck($this_user, TB_NODEACCESS_LOADIMAGE)) {
die("*** $0:\n".
" You do not have permission to create an image from $node\n");
}
#
# We need the project id for test below. The target directory for the
# output file has to be the node project directory, since that is the
# directory that is going to be NFS mounted by default.
#
$experiment = $node->Reservation();
if (!defined($experiment)) {
die("*** $0:\n".
" Could not map $node to its experiment object!\n");
}
$pid = $experiment->pid();
#
# To avoid blowing a cavernous hole ("allow all TCP ports to boss")
# in the per-experiment firewall, we don't use the frisbee uploader if
# the node is firewalled.
#
if ($usefup && $experiment->IsFirewalled()) {
print "*** WARNING: $node_id is firewalled, not using Frisbee uploader\n";
$usefup = 0;
if ($NONFS) {
$usenfs = 0;
$usessh = 1;
} else {
$usenfs = 1;
$usessh = 0;
}
}
}
#
# Make sure that the directory exists and is writeable for the user.
# We test this by creating the file. Its going to get wiped anyway.
......@@ -436,7 +458,7 @@ open(FILE, "> $tmp") or
close(FILE) or
fatal("Could not truncate $tmp: $!");
if (!$isvirtnode) {
if (! ($isvirtnode || $isec2node)) {
#
# Get the disktype for this node
#
......@@ -475,7 +497,10 @@ if ($usefup) {
$command .= " -S $BOSSIP -F $id";
}
if ($isvirtnode) {
if ($isec2node) {
$command = "$EC2SNAP ";
}
elsif ($isvirtnode) {
$command .= " $node_id";
}
else {
......@@ -498,21 +523,22 @@ if ($usefup || $usessh) {
# Go to the background since this is going to take a while.
#
if (! $foreground) {
$logfile = Logfile->Create($experiment->gid_idx());
$logfile = Logfile->Create((defined($experiment) ?
$experiment->gid_idx() : $image->gid_idx()));
fatal("Could not create a logfile")
if (!defined($logfile));
# Mark it open since we are going to start using it right away.
$logfile->Open();
# Logfile becomes the current spew, but save off the old spew.
$experiment->SetLogFile($logfile, \$oldlogfile);
# Logfile becomes the current spew.
$image->SetLogFile($logfile);
if (my $childpid = TBBackGround($logfile->filename())) {
#
# Parent exits normally, except if in waitmode.
#
if (!$waitmode) {
print("Your image from $node_id is being created\n".
print("Your image from $target is being created\n".
"You will be notified via email when the image has been\n".
"completed, and you can load the image on another node.\n");
exit(0);
......@@ -558,7 +584,8 @@ if (! $foreground) {
$needcleanup = 1;
# Clear the bootlog; see below.
$node->ClearBootLog();
$node->ClearBootLog()
if (defined($node));
# check_progress state
my $runticks = 0;
......@@ -570,9 +597,26 @@ my $lastsize = 0;
my $result;
#
# We can skip a lot of the stuff below for virtnodes.
# We can skip a lot of the stuff below for virtnodes and ec2 nodes.
#
if ($isvirtnode) {
if ($isec2node) {
my $safe_target = User::escapeshellarg($target);
my $cmd = "$TB/bin/sshtb -host $CONTROL $EC2SNAP -u $user_uid ".
"$safe_target $pid $user_uid $imageid $filename";
print STDERR "About to: '$cmd'\n" if (1 || $debug);
my $SAVEUID = $UID;
$EUID = $UID = 0;
system($cmd);
fatal("'$cmd' failed")
if ($?);
$EUID = $UID = $SAVEUID;
goto ec2done;
}
elsif ($isvirtnode) {
#
# XEN creates a problem; the physical host cannot actually
# execute a command inside the guest, but we need to run
......@@ -724,6 +768,7 @@ if ($result != 0) {
fatal("FAILED: Returned error code $result generating image ... \n");
}
ec2done:
#
# Everything worked, create the hash signature file.
#
......@@ -786,8 +831,8 @@ if (defined($bootlog)) {
}
SENDMAIL("$user_name <$user_email>",
"Image Creation on $node_id Completed: $pid/$imagename",
"Image creation on $node_id has completed. As you requested, the\n".
"Image Creation on $target Completed: $pid/$imagename",
"Image creation on $target has completed. As you requested, the\n".
"image has been written to $filename.\n".
"You may now os_load this image on other nodes in your experiment.\n".
"$swmsg",
......@@ -798,10 +843,7 @@ SENDMAIL("$user_name <$user_email>",
if (defined($logfile)) {
# Close up the log file so the webpage stops.
$logfile->Close();
# And restore the original logfile as current spew.
$experiment->SetLogFile($oldlogfile)
if (defined($oldlogfile));
$logfile->Delete(1);
$image->ClearLogFile();
}
$image->Unlock();
exit 0;
......@@ -810,7 +852,7 @@ sub cleanup ()
{
$needcleanup = 0;
if ($isvirtnode) {
if ($isvirtnode || $isec2node) {
#
# Nothing to do; the clientside script rebooted the container.
#
......@@ -858,7 +900,7 @@ sub fatal($)
# Send a message to the testbed list.
#
SENDMAIL("$user_name <$user_email>",
"Image Creation Failure on $node_id: $pid/$imagename",
"Image Creation Failure on $target: $pid/$imagename",
$mesg,
"$user_name <$user_email>",
"Cc: $TBOPS",
......@@ -867,12 +909,7 @@ sub fatal($)
if (defined($logfile)) {
# Close up the log file so the webpage stops.
$logfile->Close();
# And restore the original logfile as current spew.
$experiment->SetLogFile($oldlogfile)
if (defined($oldlogfile));
$logfile->Delete();
# This was mailed so no longer needed.
unlink("$logfile->filename()");
$image->ClearLogFile();
}
$image->Unlock()
if ($needunlock);
......
#!/usr/bin/perl -w
#
# Copyright (c) 2000-2013 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 <http://www.gnu.org/licenses/>.
#
# }}}
#
use strict;
use English;
use Getopt::Std;
use BSD::Resource;
use POSIX qw(:signal_h);
#
# Wrapper for the EC2 image import script.
#
sub usage()
{
print STDOUT "Usage: ec2import.proxy -u user ...\n";
exit(-1);
}
#
# Configure variables
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $EC2SNAP = "$TB/sbin/ec2import-image.pl";
my $errors = 0;
#
# Turn off line buffering on output
#
$| = 1;
#
# Untaint the path
#
$ENV{'PATH'} = "/bin:/usr/bin:/sbin:/usr/sbin";
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
#
# Testbed Support libraries
#
use lib "@prefix@/lib";
use libtestbed;
#
# First option has to be the -u option, the user to run this script as.
#
if ($UID != 0 || $EUID != 0) {
die("*** $0:\n".
" Must be root to run this script!\n");
}
if ($ARGV[0] ne "-u") {
usage();
}
my $user = $ARGV[1];
shift(@ARGV);
shift(@ARGV);
my (undef,undef,$unix_uid) = getpwnam($user) or
die("*** $0:\n".
" No such user $user\n");
#
# Need the entire group list for the user, cause of subgroups, and
# cause thats the correct thing to do. Too bad perl does not have a
# getgrouplist function like the C library.
#
my $glist = `/usr/bin/id -G $user`;
if ($glist =~ /^([\d ]*)$/) {
$glist = $1;
}
else {
die("*** $0:\n".
" Unexpected results from 'id -G $user': $glist\n");
}
# Need to split off the first group and create a proper list for $GUID.
my @gglist = split(" ", $glist);
my $unix_gid = $gglist[0];
$glist = "$unix_gid $glist";
# Flip to user and never go back!
$GID = $unix_gid;
$EGID = $glist;
$EUID = $UID = $unix_uid;
$ENV{'USER'} = $user;
$ENV{'LOGNAME'} = $user;
#
# Invoke script with the rest of the args.
#
system("$EC2SNAP @ARGV");
exit($? >> 8);
......@@ -126,7 +126,7 @@ class Image
$this->imageid = NULL;
return -1;
}
$this->imageid = mysql_fetch_array($query_result);
$this->image = mysql_fetch_array($query_result);
#
# Reload the type info.
......@@ -363,6 +363,7 @@ class Image
function hash() { return $this->field("hash"); }
function metadata_url() { return $this->field("metadata_url"); }
function imagefile_url() { return $this->field("imagefile_url"); }
function logfileid() { return $this->field("logfileid"); }
# Return the DB data.
function DBData() { return $this->image; }
......@@ -840,4 +841,12 @@ class Image
}
return 0;
}
function GetLogfile() {
$this->Refresh();
if ($this->logfileid())
return Logfile::Lookup($this->logfileid());
return null;
}
}
<?php
#
# Copyright (c) 2000-2012 University of Utah and the Flux Group.
# Copyright (c) 2000-2013 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -39,7 +39,7 @@ include("showlogfile_sup.php3");
# Verify page arguments.
#
$reqargs = RequiredPageArguments("image", PAGEARG_IMAGE);
$optargs = OptionalPageArguments("node", PAGEARG_NODE,
$optargs = OptionalPageArguments("target", PAGEARG_STRING,
"canceled", PAGEARG_STRING,
"confirmed", PAGEARG_STRING);
......@@ -54,13 +54,12 @@ $image_pid = $image->pid();
$image_gid = $image->gid();
$image_name = $image->imagename();
$image_path = $image->path();
$node_id = (isset($node) ? $node->node_id() : "");
if (!$image->AccessCheck($this_user, $TB_IMAGEID_MODIFYINFO )) {
USERERROR("You do not have permission to modify image '$imageid'.", 1);
}
if (! isset($node) || isset($canceled)) {
if (! isset($target) || isset($canceled)) {
echo "<center>";
if (isset($canceled)) {
......@@ -72,7 +71,7 @@ if (! isset($node) || isset($canceled)) {
echo "<form action='$url' method='post'>\n".
"<font size=+1>Node to snapshot into image '$image_name':</font> ".
"<input type='text' name='node_id' value='$node_id'></input>\n".
"<input type='text' name='target' value='$target' size=32></input>\n".
"<input type='submit' name='submit' value='Go!'></input>\n".
"</form><br>";
echo "<font size=+1>Information for Image Descriptor '$image_name':</font>\n";
......@@ -84,33 +83,51 @@ if (! isset($node) || isset($canceled)) {
return;
}
if (!$node->AccessCheck($this_user, $TB_NODEACCESS_LOADIMAGE)) {
USERERROR("You do not have permission to ".
"snapshot an image from node '$node_id'.", 1);
#
# A node or something else to pass through?
#
if (preg_match("/^[-\w]+$/", "$target")) {
$node = Node::Lookup($target);
if (!$node) {
USERERROR("No such node $node_id!", 1);
}
if (!$node->AccessCheck($this_user, $TB_NODEACCESS_LOADIMAGE)) {
USERERROR("You do not have permission to ".
"snapshot an image from node '$node_id'.", 1);
}
$experiment = $node->Reservation();
if (!$experiment) {
USERERROR("$node_id is not currently reserved to an experiment!", 1);
}
$node_pid = $experiment->pid();
$unix_gid = $experiment->UnixGID();
$project = $experiment->Project();
$unix_groups = $project->unix_gid() . "," . $unix_gid;
if ($project->pid() != $node_pid) {
$unix_groups = "$unix_groups,$node_pid";
}
}
elseif (preg_match("/^[-\w\@\.\+]+$/", "$target")) {
$unix_groups = "$image_pid,$image_gid";
}
$experiment = $node->Reservation();
if (!$experiment) {
USERERROR("$node_id is not currently reserved to an experiment!", 1);
else {
USERERROR("Illegal characters in '$target'.", 1);
}
$node_pid = $experiment->pid();
$node_eid = $experiment->eid();
$unix_gid = $experiment->UnixGID();
$project = $experiment->Project();
$unix_pid = $project->unix_gid();
# Should check for file file_exists($image_path),
# but too messy.
if (! isset($confirmed)) {
$url = CreateURL("loadimage", $image);
$newurl = CreateURL("newimageid_ez", $node);
$newurl = CreateURL("newimageid_ez", "target", $target);
echo "<center><form action='$url' method='post'>\n".
"<h2><b>Warning!</b></h2>".
"<b>Doing a snapshot of node '$node_id' into image '$image_name' ".
"<b>Doing a snapshot of '$target' into image '$image_name' ".
"will overwrite any previous snapshot for that image.<br><br> ".
"Are you sure you want to continue?</b><br>".
"<input type='hidden' name='node_id' value='$node_id'></input>".
"<input type='hidden' name='target' value='$target'></input>".
"<input type='submit' name='confirmed' value='Confirm'></input>".
"&nbsp;".
"<input type='submit' name='canceled' value='Cancel'></input>\n".
......@@ -125,24 +142,26 @@ if (! isset($confirmed)) {
}
echo "<br>
Taking a snapshot of node '$node_id' into image '$image_name' ...
Taking a snapshot of '$target' into image '$image_name' ...
<br><br>\n";
flush();
SUEXEC($uid,
"$unix_pid,$unix_gid" . ($image_pid != $node_pid ? ",$node_pid" : ""),
"webcreate_image -p $image_pid $image_name $node_id",
$unix_groups,
"webcreate_image -p $image_pid $image_name " . escapeshellarg($target),
SUEXEC_ACTION_DUPDIE);
echo "This will take 10 minutes or more; you will receive email
notification when the snapshot is complete. In the meantime,
<b>PLEASE DO NOT</b> delete the imageid or the experiment
$node_id is in. In fact, it is best if you do not mess with
the node or the experiment at all until you receive email.<br>\n";
echo "This will take as little as 10 minutes or as much as an hour;
you will receive email
notification when the image is complete. In the meantime,
<b>PLEASE DO NOT</b> delete the imageid or mess with
the node at all!<br>\n";
flush();
STARTLOG($experiment);
$logfile = $image->GetLogfile();
if ($logfile) {
STARTLOG($logfile);
}
#
# Standard Testbed Footer
......
......@@ -53,11 +53,17 @@ $optargs = OptionalPageArguments("submit", PAGEARG_STRING,
"nodeclass", PAGEARG_STRING,
"canceled", PAGEARG_BOOLEAN,
"confirmed", PAGEARG_BOOLEAN,
"ec2", PAGEARG_BOOLEAN,
"node", PAGEARG_NODE,
"baseimage", PAGEARG_IMAGE,
"baseosinfo", PAGEARG_OSINFO,
"formfields", PAGEARG_ARRAY);
# Flag to import an EC2 image.
if (!isset($ec2)) {
$ec2 = 0;
}
#
# If starting from a specific node we can derive the type and possibly
# the baseimage from it.
......@@ -78,6 +84,9 @@ if (isset($node)) {
$baseosinfo = OSinfo::Lookup($node->def_boot_osid());
}
}
elseif ($ec2) {
$nodetype = "pcvm";
}
#
# Options for using this page with different types of nodes.
......@@ -191,7 +200,7 @@ function SPITFORM($formfields, $errors)
global $nodeclass, $node;
global $TBDB_OSID_OSNAMELEN, $TBDB_NODEIDLEN;
global $TBDB_OSID_VERSLEN, $TBBASE, $TBPROJ_DIR, $TBGROUP_DIR;
global $view;
global $view, $ec2;
#
# Explanation of the $view argument: used to turn on and off display of
......@@ -318,6 +327,9 @@ function SPITFORM($formfields, $errors)
$id = $node->node_id();
echo "<input type=hidden name=node_id value='$id'>";
}
if ($ec2) {
echo "<input type=hidden name=ec2 value=true>";
}
#
# Select Project
......@@ -491,7 +503,18 @@ function SPITFORM($formfields, $errors)
#
# Node to Snapshot image from.
#
if (isset($view["hide_snapshot"])) {
if ($ec2) {
echo "<tr>
<td>EC2 User@Node Info:</td>
<td class=left>
<input type=text
name=\"formfields[ec2_info]\"
value=\"" . $formfields["ec2_info"] . "\"
size=64>
</td>
</tr>\n";
}
elseif (isset($view["hide_snapshot"])) {
spithidden($formfields, 'node');
} else {
echo "<tr>
......@@ -504,6 +527,7 @@ function SPITFORM($formfields, $errors)
</td>
</tr>\n";
}
#
# OS Features.
......@@ -955,16 +979,27 @@ if (!isset($submit)) {
$defaults["reboot_waittime"] = "240";
$defaults["os_feature_ping"] = "checked";
$defaults["os_feature_ssh"] = "checked";
$defaults["os_feature_isup"] = "checked";
$defaults["os_feature_linktest"] = "checked";
#
# XXX At the moment we have only one parent that can run openvz
# container images, so rather then give the user a choice, just
# hardwire it. Needs more thought, cause we also have the os_submap
# table and not sure how to deal with that either.
#
$def_parentosinfo =
OSinfo::LookupByName("emulab-ops". "FEDORA15-OPENVZ-STD");
if ($ec2) {
#
# XXX Need to fix this.
#
$def_parentosinfo =
OSinfo::LookupByName("emulab-ops", "XEN41-64-STD");