#!/usr/bin/perl -w
#
# Copyright (c) 2000-2014 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 Getopt::Std;
#
# Client-side to create a disk image. Caller must have sudo permission!
#
# This is an enhanced version of create-image that knows how to work with
# signature files and created delta images (for versioning). It is a
# separate script just to make backward-compat easier.
#
# In addition, it can create multiple images (e.g., to capture multiple
# partitions or disks) based on the parameters given. Right now these
# parameters are either specified on the command line (allows only
# one image to be made) or come from a file (one line per image to make).
# Eventually, these may come down via tmcc.
#
# For each image, the possible parameters are:
#
# METHOD=
# Method to use for uploading image and up/downloading any metadata
# (just old/new signature files right now). Choices are "frisbee" or
# "file" where file means "across NFS". We may add "http" at some point,
# but currently that would just be a choice for downloading.
#
# SERVER=
# Server to use for uploading image and metadata. May also be used
# for downloading metadata if DOWNSERVER is not set. If METHOD=file,
# SERVER won't be set or will be ignored by the client. Otherwise it
# is the name or IP to use with the frisupload -S option.
#
# DOWNSERVER=
# Server to use for downloading metadata. If not set SERVER is used
# instead. If METHOD=file, DOWNSERVER won't be set or will be ignored
# by the client. Otherwise it is the name or IP to use with the
# frisbee -S option.
#
# IMAGENAME=
# Context-sensitive name of the image to create. If the SERVER is set
# and we are using the frisbee uploader, then this string is the argument
# to present via the -F option (either an imageid or a path). If server
# is not set, then it is a local (NFS) path at which to save the image.
#
# OLDSIGNAME=
# NEWSIGNAME=
# Optional context-sensitive names of the old and new signature files.
# OLDSIGNAME is either a path to read from the filesystem (method=file)
# or the argument to the frisbee -F option. NEWSIGNAME is where to put
# the newly created signature; either in the FS or uploaded via frisupload.
#
# If OLDSIGNAME is given, we are creating a delta image. In this case
# NEWSIGNAME may also be specified if a new signature is desired.
#
# If OLDSIGNAME is not given, then we are creating a full disk image.
# In this case we might or might not create a new signature file for
# the image depending on whether NEWSIGNAME is present.
#
# If both are absent, then we are running in the old, pre-delta image
# world and just creating full disk images always.
#
# DISK=
# BSD-style disk name (e.g., "da0") identifying the disk from which to
# create the image. Used for imagezip disk argument. Note that like
# the argument to loadinfo, this is not at all a sound technique given
# the differences in device names used by BSD and Linux and the fact
# that disk ordering is not deterministic in either! However SNs are
# hard to extract, so we live with a name instead.
#
# PART=
# Partition on DISK from which to load image. Used for imagezip -s option.
# If not set or set to zero, then it is a whole-disk image.
#
# IZOPTS=
# Additional options for imagezip.
#
# PROXY=
# The proxy argument for use on XEN, when acting on behalf of a container.
#
sub usage()
{
print STDERR
"Usage:\n".
"create-versioned-image [-nvx] -f param-file\n".
" or\n".
"create-versioned-image [-nv] [-x vnode_id] KEY=VALUE ...\n";
exit(-1);
}
my $optlist = "f:nvx:";
#
# Turn off line buffering on output
#
$| = 1;
#
# Untaint the path
#
$ENV{'PATH'} = "/bin:/sbin:/usr/bin:";
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
#
# No configure vars.
#
my $sudo = "";
my $zipper = "/usr/local/bin/imagezip";
my $uploader = "/usr/local/bin/frisupload";
my $frisbee = "/usr/local/bin/frisbee";
my $localdir = "/local";
my $impotent = 0;
my $verbose = 0;
my $isxen = 0;
sub process_image($);
sub mysystem($);
my %iinfo = ();
#
# Map DB (BSD-ish) disknames into actual /dev device names on
# FreeBSD or Linux.
#
sub map_diskname($)
{
my ($dev) = @_;
my ($dtype, $dunit);
#
# When called on XEN, the diskname is correct, and in fact we will
# just mess it up.
#
return $dev
if ($isxen);
# strip off /dev/ if it is there
$dev =~ s/^\/dev\///;
if ($dev =~ /^([-a-zA-Z_]+)(\d+)$/) {
($dtype,$dunit) = ($1,$2);
} else {
return undef;
}
# Hack for the Linux MFS: we still use the BSD device
# names in the database so we try to convert them to
# the equivalent Linux devices here. This happens to
# work at the moment, but if device names change again
# it could break.
if ($^O eq 'linux') {
$dtype = "sd";
$dunit -= 4
if ($dtype eq 'ad' && $dunit > 3);
$dunit =~ y/01234567/abcdefgh/;
#
# XXX woeful TPM dongle-boot hack.
# If we are imaging /dev/sda and dmesg reports that
# that device is write-protected, assume it is the boot dongle
# and use /dev/sdb instead!
#
if ($dunit eq "a") {
if (!system("dmesg | fgrep -q '[sda] Write Protect is on'")) {
print STDERR "WARNING: suspect dongle-booted node, using sdb instead of sda\n";
$dunit = "b";
}
}
}
return "/dev/$dtype$dunit";
}
sub parse_params(@)
{
my ($method,$iname,$disk,$part,$sigfile,$nsigfile);
my $errors = 0;
my $iid = 1;
foreach my $line (@_) {
my @kvs = split(' ', $line);
foreach my $kv (@kvs) {
if ($kv =~ /^([-\w]+)=(\S*)$/) {
my $key = lc($1);
my $val = $2;
if ($key eq "method") {
if ($val =~ /^(frisbee|file)$/) {
$iinfo{$iid}{'method'} = $1;
} else {
print STDERR "Bogus METHOD '$val'\n";
$errors++;
}
next;
}
if ($key eq "server") {
$iinfo{$iid}{'server'} = $val;
next;
}
if ($key eq "downserver") {
$iinfo{$iid}{'downserver'} = $val;
next;
}
if ($key eq "imagename") {
$iinfo{$iid}{'iname'} = $val;
next;
}
if ($key eq "disk") {
$iinfo{$iid}{'disk'} = map_diskname($val);
if (!defined($iinfo{$iid}{'disk'})) {
print STDERR "Bogus DISK '$val'\n";
$errors++;
}
next;
}
if ($key eq "part") {
if ($val =~ /^(\d+)$/) {
$iinfo{$iid}{'part'} = $1;
} else {
print STDERR "Bogus PART '$val'\n";
$errors++;
}
next;
}
if ($key eq "oldsigfile") {
$iinfo{$iid}{'sigfile'} = $val;
next;
}
if ($key eq "newsigfile") {
$iinfo{$iid}{'nsigfile'} = $val;
next;
}
if ($key eq "izopts") {
#
# No spaces in string, so options are encoded; e.g.:
# -N -z 9 -d -a SHA1
# would be encoded as:
# N,z=9,d,a=SHA1
# We unencode them here.
#
my $optstr = "";
foreach my $opt (split(',', $val)) {
$optstr .= " -" . join(' ', split('=', $opt));
}
$iinfo{$iid}{'izopts'} = $optstr;
next;
}
if ($key eq "proxy") {
$iinfo{$iid}{'proxy'} = $val;
next;
}
} else {
print STDERR "Bogus parameter: '$kv'\n";
$errors++;
}
}
$iid++;
}
if ($errors) {
print STDERR "Could not parse all arguments\n";
exit(2);
}
}
#
# If we are running as a user, then we will need sudo
#
if ($EUID != 0) {
for my $path (qw#/usr/local/bin /usr/bin#) {
if (-e "$path/sudo") {
$sudo = "$path/sudo";
last;
}
}
}
#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
%options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"n"})) {
$impotent = 1;
}
if (defined($options{"v"})) {
$verbose = 1;
}
if (defined($options{"x"})) {
$isxen = 1;
$localdir = "/capture/" . $options{"x"} . "/frisbee";
}
if (defined($options{"f"})) {
my $pfile = $options{"f"};
if ($pfile =~ /^(\/tmp\/[-\w\.]+)$/) {
$pfile = $1;
if (! -r "$pfile") {
print STDERR "Cannot read paramfile '$pfile'\n";
exit(1);
}
} else {
print STDERR "Bogus '-f' file name\n";
exit(1);
}
my @params = `cat $pfile`;
chomp @params;
parse_params(@params);
} elsif (@ARGV > 0) {
my $pline = join(' ', @ARGV);
parse_params($pline);
} else {
# someday maybe use tmcc to get params
print STDERR "No parameters given!\n";
exit(1);
}
#
# Consistency checks
#
my $dofrisbee = 0;
foreach my $iid (sort keys %iinfo) {
if (!defined($iinfo{$iid}{'disk'})) {
print STDERR "Must specify disk\n";
exit(1);
}
if (!defined($iinfo{$iid}{'part'})) {
$iinfo{$iid}{'part'} = 0;
}
if (!defined($iinfo{$iid}{'iname'})) {
print STDERR "Must specify imagename\n";
exit(1);
}
if (!defined($iinfo{$iid}{'method'})) {
print STDERR "Must specify method\n";
exit(1);
}
if ($iinfo{$iid}{'method'} eq "frisbee") {
$dofrisbee++;
if (!defined($iinfo{$iid}{'server'})) {
print STDERR "Must specify server for frisbee\n";
exit(1);
}
}
}
#
# For uniformity, all sigfiles are rooted here.
# For method=frisbee, the actual files will be here.
# For method=file, a symlink to the actual file will be here.
#
if (! -e $localdir && mysystem("$sudo mkdir -p $localdir")) {
print STDERR "Could not create $localdir\n";
exit(1);
}
#
# If any of the images are using frisbee, we need to ensure that we
# have sufficient local storage for old/new signature files.
#
# XXX with our current MBR3 FS size of 16GB, signature files for
# partition images will be around 8MB. For a 500GB disk and full
# disk image, it would be more like 250MB. We need enough space to
# hold up to two of these signature files (old and new). 512MB of
# memory filesystem is too much for our older machines--let's go
# with 64MB for now.
#
my $MEMFS_SIZE = "64m";
if ($dofrisbee) {
if ($^O eq 'linux') {
if (!$isxen &&
mysystem("$sudo mount -t tmpfs -o size=$MEMFS_SIZE tmpfs $localdir")) {
print STDERR "Could not create $MEMFS_SIZE byte local MFS\n";
# XXX try NFS instead
exit(1);
}
} else {
if (mysystem("$sudo mdconfig -a -t swap -s $MEMFS_SIZE -u 4") ||
mysystem("$sudo newfs -b 8192 -i 25000 -o space /dev/md4") ||
mysystem("$sudo mount /dev/md4 $localdir")) {
print STDERR "Could not create $MEMFS_SIZE byte local MFS\n";
# XXX try NFS instead
exit(1);
}
}
}
if (mysystem("$sudo chmod 1777 $localdir")) {
print STDERR "Could not make $localdir writeable\n";
exit(1);
}
#
# Process each image
#
foreach my $iid (sort keys %iinfo) {
process_image($iid);
}
#
# Get rid of any extra FS.
#
if ($dofrisbee) {
if ($^O eq 'linux') {
if (!$isxen && mysystem("$sudo umount $localdir")) {
print STDERR "WARNING: could not destroy local MFS\n";
}
} else {
if (mysystem("$sudo umount $localdir") ||
mysystem("$sudo mdconfig -d -u 4")) {
print STDERR "WARNING: could not destroy local MFS\n";
}
}
}
exit(0);
sub fetch($$)
{
my ($iid,$file) = @_;
my $lfile = "$localdir/$file";
my $ifile = $iinfo{$iid}{$file};
if ($iinfo{$iid}{'method'} eq "file") {
if (! -r "$ifile" || mysystem("ln -sf $ifile $lfile")) {
return -1;
}
}
elsif ($iinfo{$iid}{'method'} eq "frisbee") {
my $srv = $iinfo{$iid}{'downserver'};
if (!defined($srv)) {
$srv = $iinfo{$iid}{'server'};
}
#
# Since we are fetching small files into an MFS, there is
# not much need for lots of buffer memory. We also don't
# randomize requests.
#
my $opts = "-O -W 1 -C 32";
if (mysystem("$frisbee $opts -B 5 -N -S $srv -F $ifile $lfile")) {
return -1;
}
}
return 0;
}
sub writetest($$)
{
my ($iid,$file) = @_;
my $lfile = "$localdir/$file";
my $ifile = $iinfo{$iid}{$file};
#
# For file (NFS) we make sure we can write the actual file.
# If it doesn't currently exist, we try creating it (if we do create
# it, we remove it again to make sure we don't leave turds behind).
# If all is well, we create a local symlink to use.
#
if ($iinfo{$iid}{'method'} eq "file") {
if (! -e "$ifile") {
if (!$impotent && !open(FD, ">$ifile")) {
return -1;
}
if (!$impotent) {
close(FD);
unlink("$ifile");
}
}
elsif (! -w "$ifile") {
return -1;
}
if (mysystem("ln -sf $ifile $lfile")) {
return -1;
}
}
#
# For frisbee, we just check with the server to ensure we can
# upload to the file. Note: the local file has to exist for this to
# work, so we create that.
#
elsif ($iinfo{$iid}{'method'} eq "frisbee") {
my $srv = $iinfo{$iid}{'server'};
my $cmd = "$uploader -S $srv -Q $ifile $lfile";
if ($impotent) {
print STDERR "Would: '$cmd' ...\n";
} else {
if (open(FD, ">$lfile")) {
close(FD);
}
print STDERR "Doing: '$cmd' ...\n"
if ($verbose);
my $out = `$cmd`;
if ($? != 0 || $out !~ /error=0/) {
return -1;
}
}
}
return 0;
}
sub process_image($)
{
my ($iid) = @_;
if ($verbose) {
print "Image #$iid:\n";
foreach my $k (sort keys %{$iinfo{$iid}}) {
print " $k=", $iinfo{$iid}{$k}, "\n";
}
}
#
# Make sure the image file is writeable.
#
if (writetest($iid, "iname")) {
print STDERR
"Cannot write new image '", $iinfo{$iid}{'iname'}, "'\n";
exit(3);
}
#
# Make sure we can write the new sig file if needed.
#
if (exists($iinfo{$iid}{'nsigfile'}) && writetest($iid, "nsigfile")) {
print STDERR
"Cannot write new signature file '",
$iinfo{$iid}{'nsigfile'}, "'\n";
exit(3);
}
#
# Download the current signature file if needed.
#
if (exists($iinfo{$iid}{'sigfile'}) && fetch($iid, "sigfile")) {
print STDERR
"Cannot fetch/read signature file '",
$iinfo{$iid}{'sigfile'}, "'\n";
exit(3);
}
#
# XXX we don't support GPT right now, so we can only do Linux partition
# images directly from the partition device.
#
my $isgpt = 0;
if ($^O eq 'linux' && -x "/sbin/sgdisk" &&
!mysystem("$sudo /sbin/sgdisk ".$iinfo{$iid}{'disk'}." >/dev/null 2>&1")) {
if ($iinfo{$iid}{'part'} == 0) {
print STDERR
"Cannot take whole disk image of GPT disk\n";
exit(3);
}
$isgpt = 1;
}
#
# Fire off the command:
#
# file:
# imagezip $izopts [-s $part] [-H $sigfile] [-U $nsigfile] $disk $ifile
#
# frisbee:
# imagezip $izopts [-s $part] [-H $sigfile] [-U $localdir/$nsigfile] $disk - | \
# frisupload -S $server -F $ifile -
# [ cat $localdir/$nsigfile | frisupload -S $server -F $nsigfile ]
#
my $cmd = "$sudo $zipper";
if ($verbose) {
$cmd .= " -o";
}
if (exists($iinfo{$iid}{'izopts'})) {
$cmd .= $iinfo{$iid}{'izopts'};
}
if ($isgpt) {
# force imagezip to treat as an ext filesystem
$cmd .= " -S 131";
} else {
if ($iinfo{$iid}{'part'} != 0) {
$cmd .= " -s " . $iinfo{$iid}{'part'};
}
}
if (exists($iinfo{$iid}{'sigfile'})) {
$cmd .= " -H $localdir/sigfile";
}
if (exists($iinfo{$iid}{'nsigfile'})) {
$cmd .= " -U $localdir/nsigfile";
}
$cmd .= " " . $iinfo{$iid}{'disk'};
if ($isgpt) {
# use the slice device
$cmd .= $iinfo{$iid}{'part'};
}
my $image = $iinfo{$iid}{'iname'};
if ($iinfo{$iid}{'method'} eq "file") {
$cmd .= " $image";
} elsif ($iinfo{$iid}{'method'} eq "frisbee") {
my $srv = $iinfo{$iid}{'server'};
# use basic shell sleezy trick to capture exit status from imagezip
$cmd = "( $cmd - || echo \$? > $localdir/imagezip.stat )";
$cmd .= " | $uploader -S $srv -F $image";
if (exists($iinfo{$iid}{'proxy'})) {
$cmd .= " -P " . $iinfo{$iid}{'proxy'};
}
$cmd .= " - ";
}
if (mysystem("$cmd") || -e "$localdir/imagezip.stat") {
my $stat = sprintf("0x%04x", $?);
my $izstat = 0;
if (-e "$localdir/imagezip.stat") {
$izstat = `cat $localdir/imagezip.stat`;
chomp($izstat);
}
$izstat = sprintf("0x%04x", $izstat);
print STDERR "*** Failed to create image!\n";
print STDERR " command: '$cmd'\n";
print STDERR " status: $stat\n";
print STDERR " izstatus: $izstat\n"
if ($izstat);
exit(3);
}
if ($iinfo{$iid}{'method'} eq "frisbee" &&
exists($iinfo{$iid}{'nsigfile'})) {
$cmd = "$uploader -S " . $iinfo{$iid}{'server'} .
" -F " . $iinfo{$iid}{'nsigfile'} . " $localdir/nsigfile";
if (mysystem("$cmd")) {
print STDERR "*** Failed to upload signature for created image!\n";
print STDERR " command: '$cmd'\n";
exit(3);
}
}
mysystem("$sudo rm $localdir/*");
}
sub mysystem($)
{
my ($cmd) = @_;
if ($impotent) {
print STDERR "Would: '$cmd' ...\n";
return 0;
}
print STDERR "Doing: '$cmd' ...\n"
if ($verbose);
return system($cmd);
}