diff --git a/backend/GNUmakefile.in b/backend/GNUmakefile.in
index e5c818c4d1a3e5a624f78671db89385b8466f599..38d744c1349126c82da103e1b873b43e7fb3d992 100644
--- a/backend/GNUmakefile.in
+++ b/backend/GNUmakefile.in
@@ -13,9 +13,9 @@ UNIFIED         = @UNIFIED_BOSS_AND_OPS@
 include $(OBJDIR)/Makeconf
 
 BIN_SCRIPTS	= moduserinfo newgroup newmmlist editexp editimageid \
-		  editnodetype editsitevars
+		  editnodetype editsitevars newimageid
 WEB_BIN_SCRIPTS = webmoduserinfo webnewgroup webnewmmlist webeditimageid \
-		  webeditnodetype webeditsitevars
+		  webeditnodetype webeditsitevars webnewimageid
 WEB_SBIN_SCRIPTS= 
 LIBEXEC_SCRIPTS	= $(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS)
 
diff --git a/backend/newimageid.in b/backend/newimageid.in
new file mode 100644
index 0000000000000000000000000000000000000000..6283e3bddf3aa95c50d341e04b716507378b3c24
--- /dev/null
+++ b/backend/newimageid.in
@@ -0,0 +1,580 @@
+#!/usr/bin/perl -wT
+#
+# EMULAB-COPYRIGHT
+# Copyright (c) 2000-2007 University of Utah and the Flux Group.
+# All rights reserved.
+#
+use English;
+use strict;
+use Getopt::Std;
+use XML::Simple;
+use Data::Dumper;
+
+#
+# Back-end script to create new Image descriptors.
+#
+sub usage()
+{
+    print("Usage: newimageid [-v] <xmlfile>\n");
+    exit(-1);
+}
+my $optlist = "dv";
+my $debug   = 0;
+my $verify  = 0;	# Check data and return status only. 
+
+#
+# Configure variables
+#
+my $TB		= "@prefix@";
+my $TBOPS       = "@TBOPSEMAIL@";
+my $TBAUDIT	= "@TBAUDITEMAIL@";
+my $TBGROUP_DIR	= "@GROUPSROOT_DIR@";
+my $TBPROJ_DIR	= "@PROJROOT_DIR@";
+
+#
+# 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 libdb;
+use libtestbed;
+use User;
+use Project;
+use Image;
+
+# Protos
+sub fatal($);
+sub UserError(;$);
+sub escapeshellarg($);
+
+#
+# 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{"v"})) {
+    $verify = 1;
+}
+if (@ARGV != 1) {
+    usage();
+}
+my $xmlfile  = shift(@ARGV);
+
+#
+# Map invoking user to object. 
+# If invoked as "nobody" we are coming from the web interface and the
+# current user context is "implied" (see tbauth.php3).
+#
+my $this_user;
+
+if (getpwuid($UID) ne "nobody") {
+    $this_user = User->ThisUser();
+
+    if (! defined($this_user)) {
+	fatal("You ($UID) do not exist!");
+    }
+    # You don't need admin privileges to create new Image descriptors.
+}
+else {
+    #
+    # Check the filename when invoked from the web interface; must be a
+    # file in /tmp.
+    #
+    if ($xmlfile =~ /^([-\w\.\/]+)$/) {
+	$xmlfile = $1;
+    }
+    else {
+	fatal("Bad data in pathname: $xmlfile");
+    }
+
+    # Use realpath to resolve any symlinks.
+    my $translated = `realpath $xmlfile`;
+    if ($translated =~ /^(\/tmp\/[-\w\.\/]+)$/) {
+	$xmlfile = $1;
+    }
+    else {
+	fatal("Bad data in translated pathname: $xmlfile");
+    }
+
+    # The web interface (and in the future the xmlrpc interface) sets this.
+    $this_user = User->ImpliedUser();
+
+    if (! defined($this_user)) {
+	fatal("Cannot determine implied user!");
+    }
+}
+
+#
+# These are the fields that we allow to come in from the XMLfile.
+#
+my $SLOT_OPTIONAL	= 0x1;	# The field is not required.
+my $SLOT_REQUIRED	= 0x2;  # The field is required and must be non-null.
+my $SLOT_ADMINONLY	= 0x4;  # Only admins can set this field.
+#
+# XXX We should encode all of this in the DB so that we can generate the
+# forms on the fly, as well as this checking code.
+#
+my %xmlfields =
+    # XML Field Name        DB slot name         Flags             Default
+    ("imagename"	=> ["imagename"	,	$SLOT_REQUIRED],
+     "pid"		=> ["pid",		$SLOT_REQUIRED],
+     "gid"		=> ["gid",		$SLOT_OPTIONAL],
+     "description"	=> ["description",	$SLOT_REQUIRED],
+     "loadpart"		=> ["loadpart",		$SLOT_REQUIRED],
+     "loadlength"	=> ["loadlength",	$SLOT_REQUIRED],
+     "part1_osid"	=> ["part1_osid",	$SLOT_OPTIONAL],
+     "part2_osid"	=> ["part2_osid",	$SLOT_OPTIONAL],
+     "part3_osid"	=> ["part3_osid",	$SLOT_OPTIONAL],
+     "part4_osid"	=> ["part4_osid",	$SLOT_OPTIONAL],
+     "default_osid"	=> ["default_osid",	$SLOT_REQUIRED],
+     "path"		=> ["path",		$SLOT_OPTIONAL,  ""],
+     "mtype_*"		=> ["mtype",		$SLOT_OPTIONAL],
+     "node_id"		=> ["node_id",		$SLOT_OPTIONAL,  ""],
+     "shared",		=> ["shared",		$SLOT_OPTIONAL,   0],
+     "global",		=> ["global",		$SLOT_ADMINONLY,  0],
+     "makedefault",	=> ["makedefault",	$SLOT_ADMINONLY,  0],
+);
+
+#
+# Must wrap the parser in eval since it exits on error.
+#
+my $xmlparse = eval { XMLin($xmlfile,
+			    VarAttr => 'name',
+			    ContentKey => '-content',
+			    SuppressEmpty => undef); };
+fatal($@)
+    if ($@);
+
+#
+# Process and dump the errors (formatted for the web interface).
+# We should probably XML format the errors instead but not sure I want
+# to go there yet.
+#
+my %errors = ();
+
+#
+# Make sure all the required arguments were provided.
+#
+my $key;
+foreach $key (keys(%xmlfields)) {
+    my (undef, $required, undef) = @{$xmlfields{$key}};
+
+    $errors{$key} = "Required value not provided"
+	if ($required & $SLOT_REQUIRED  &&
+	    ! exists($xmlparse->{'attribute'}->{"$key"}));
+}
+UserError()
+    if (keys(%errors));
+
+#
+# We build up an array of arguments to pass to Image->Create() as we check
+# the attributes.
+#
+my %newimageid_args = ();
+
+#
+# Wildcard keys have one or more *'s in them like simple glob patterns.
+# This allows multiple key instances for categories of attributes, and
+# putting a "type signature" in the key for arg checking, as well.
+#
+# Wildcards are made into regex's by anchoring the ends and changing each * to
+# a "word" (group of alphahumeric.)  A tail * means "the rest", allowing
+# multiple words separated by underscores or dashes.
+#
+my $wordpat = '[a-zA-Z0-9]+';
+my $tailpat = '[-\w]+';
+my %wildcards;
+foreach $key (keys(%xmlfields)) {
+    if (index($key, "*") >= 0) {
+	my $regex = '^' . $key . '$';
+	$regex =~ s/\*\$$/$tailpat/;
+	$regex =~ s/\*/$wordpat/g;
+	$wildcards{$key} = $regex;
+    }
+}
+# Key ordering is lost in a hash.
+# Put longer matching wildcard keys before their prefix.
+my @wildkeys = reverse(sort(keys(%wildcards)));
+
+foreach $key (keys(%{ $xmlparse->{'attribute'} })) {
+    my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'};
+
+    print STDERR "User attribute: '$key' -> '$value'\n"
+	if ($debug);
+
+    my $field = $key;
+    my $wild;
+    if (!exists($xmlfields{$key})) {
+
+	# Not a regular key; look for a wildcard regex match.
+        foreach my $wildkey (@wildkeys) {
+	    my $regex = $wildcards{$wildkey};
+	    if ($wild = $key =~ /$regex/) {
+		$field = $wildkey;
+		print STDERR "Wildcard: '$key' matches '$wildkey'\n"
+		    if ($debug);
+		last; # foreach $wildkey
+	    }
+	}
+	if (!$wild) {
+	    $errors{$key} = "Unknown attribute";
+	    next; # foreach $key
+	}
+    }
+
+    my ($dbslot, $required, $default) = @{$xmlfields{$field}};
+
+    if ($required & $SLOT_REQUIRED) {
+	# A slot that must be provided, so do not allow a null value.
+	if (!defined($value)) {
+	    $errors{$key} = "Must provide a non-null value";
+	    next;
+	}
+    }
+    if ($required & $SLOT_OPTIONAL) {
+	# Optional slot. If value is null skip it. Might not be the correct
+	# thing to do all the time?
+	if (!defined($value)) {
+	    next
+		if (!defined($default));
+	    $value = $default;
+	}
+    }
+    if ($required & $SLOT_ADMINONLY) {
+	# Admin implies optional, but thats probably not correct approach.
+	$errors{$key} = "Administrators only"
+	    if (! $this_user->IsAdmin());
+    }
+	
+    # Now check that the value is legal.
+    if (! TBcheck_dbslot($value, "images", $dbslot, TBDB_CHECKDBSLOT_ERROR)) {
+	$errors{$key} = TBFieldErrorString();
+	next;
+    }
+
+    $newimageid_args{$key} = $value;
+}
+UserError()
+    if (keys(%errors));
+
+#
+# Need a list of node types. We join this over the nodes table so that
+# we get a list of just the nodes that are currently in the testbed, not
+# just in the node_types table.
+#
+my $types_result =
+    DBQueryFatal("select distinct n.type from nodes as n ".
+		 "left join node_type_attributes as a on a.type=n.type ".
+		 "where a.attrkey='imageable' and ".
+		 "      a.attrvalue!='0'");
+# Save the valid types in a new array for later.
+my @mtypes_array;
+while (my ($type) = $types_result->fetchrow_array()) {
+    push(@mtypes_array, $type);
+    $xmlfields{"mtype_$type"} = ["mtype", $SLOT_OPTIONAL];
+}
+## printf "%s mtypes\n", $#mtypes_array + 1;
+## foreach my $x (@mtypes_array) { printf "%s\n", $x; }
+## print "\n";
+
+#
+# Now do special checks.
+#
+
+my $isadmin = $this_user->IsAdmin();
+my $imagename = $newimageid_args{"imagename"};
+
+my $project = Project->Lookup($newimageid_args{"pid"});
+if (!defined($project)) {
+    UserError("Project: No such project");
+}
+if (!$project->AccessCheck($this_user, TB_PROJECT_MAKEIMAGEID())) {
+    UserError("Project: Not enough permission");
+}
+
+my $group;
+if (exists($newimageid_args{"group"})) {
+    my $gid = $newimageid_args{"group"};
+    $group = Group->LookupSubgroupByName($gid);
+    if (!defined($group)) {
+	UserError("Group: No such group $gid");
+    }
+}
+else {
+    $group = $project->GetProjectGroup();    
+}
+
+if ($newimageid_args{"loadpart"} != 0 && $newimageid_args{"loadlength"} != 1) {
+    UserError("#of Partitions: Only single slices or partial disks are allowed");
+}
+
+#
+# Check sanity of the OSIDs for each slice. Permission checks not needed.
+# Store the ones we care about and silently forget about the extraneous OSIDs.
+#
+my @osid_array;
+for (my $i = 1; $i <= 4; $i++) {
+    my $foo      = "part${i}_osid";
+
+    if ($newimageid_args{"loadpart"} ?
+	$i == $newimageid_args{"loadpart"} :
+	$i <= $newimageid_args{"loadlength"}) {
+
+	if (!exists($newimageid_args{$foo})) {
+	    UserError("Partition $i OS: Must select an OS");
+	}
+	else {
+	    my $thisosid = $newimageid_args{$foo};
+	    if ($thisosid eq "" || $thisosid eq "X") {
+		UserError("Partition $i OS: Must select an OS");
+	    }
+	    elsif ($thisosid eq "none") {
+		#
+		# Allow admins to specify no OS for a partition.
+		# 
+		UserError("Partition $i OS: Must select an OS")
+		    if (!$isadmin);
+		delete($newimageid_args{$foo});
+	    }
+	    elsif (!OSinfo->Lookup($thisosid)) {
+		UserError("Partition $i OS: No such OS defined");
+	    }
+	    else {
+		push(@osid_array, $thisosid);
+	    }
+	}
+    }
+    else {
+	delete($newimageid_args{$foo});
+    }
+}
+
+#
+# Check the boot OS. Must be one of the OSes selected for a partition.
+# 
+if (!exists($newimageid_args{"default_osid"}) ||
+    $newimageid_args{"default_osid"} eq "" ||
+    $newimageid_args{"default_osid"} eq "none") {
+    UserError("Boot OS: Not Selected");
+}
+elsif (!OSinfo->Lookup($newimageid_args{"default_osid"})) {
+    UserError("Boot OS: No such OS defined");
+}
+else {
+    UserError("Boot OS: Invalid; Must be one of the partitions")
+	if (!grep($_ eq $newimageid_args{"default_osid"}, @osid_array));
+}
+
+#
+# Only admin types can set the global bit for an image. Ignore silently.
+#
+my $global = 0;
+if ($isadmin &&
+    exists($newimageid_args{"global"}) &&
+    $newimageid_args{"global"} eq "1") {
+    $global = 1;
+}
+
+my $shared = 0;
+if (exists($newimageid_args{"shared"}) &&
+    $newimageid_args{"shared"} eq "1") {
+    $shared = 1;
+}
+# Does not make sense to do this. 
+if ($global && $shared) {
+    UserError("Global: Image declared both shared and global");
+}
+
+#
+# The path must not contain illegal chars and it must be more than
+# the original /proj/$pid we gave the user. We allow admins to specify
+# a path outside of /proj though.
+# 
+if (!exists($newimageid_args{"path"}) ||
+    $newimageid_args{"path"} eq "") {
+    UserError("Path: Missing Field");
+}
+elsif (! $isadmin) {
+    my $pdef = "";
+    
+    if (!$shared &&
+	exists($newimageid_args{"gid"}) &&
+	$newimageid_args{"gid"} ne "" &&
+	$newimageid_args{"gid"} ne $newimageid_args{"pid"}) {
+	$pdef = "$TBGROUP_DIR/" .
+	    $newimageid_args{"pid"} . "/" . $newimageid_args{"gid"} . "/";
+    }
+    else {
+	$pdef = "$TBPROJ_DIR/" . $newimageid_args{"pid"} . "/images/";
+    }
+
+    if (index($newimageid_args{"path"}, $pdef) < 0) {
+	UserError("Path: Invalid Path");
+    }
+}
+
+#
+# See what node types this image will work on. Must be at least one!
+#
+UserError("Node Types: Must have at least one node type")
+    if ($#mtypes_array < 0);
+my $typeclause = join(" or ", map("type='$_'", @mtypes_array));
+
+# Check validity of mtype_* args, since the keys are dynamically generated.
+my $node_types_selected = 0;
+my @mtype_keys = grep(/^mtype_/, keys(%newimageid_args));
+foreach my $key (@mtype_keys) {
+    my $value = $newimageid_args{$key};
+    print STDERR "mtype: '$key' -> '$value'\n"
+	if ($debug);
+
+    my $type = $key;
+    $type =~ s/^mtype_//;
+    my $match = grep(/^${type}$/, @mtypes_array);
+    if ($match == 0) {
+	$errors{$key} = "Illegal node type."
+    }
+    elsif ($value eq "1") {
+	$node_types_selected++;
+    }
+}
+UserError("Node Types: Must select at least one node type")
+    if ($node_types_selected == 0);
+
+#
+# Check sanity of node name and that user can create an image from it.
+#
+my ($node, $node_id);
+if (exists($newimageid_args{"node_id"}) &&
+    $newimageid_args{"node_id"} ne "") {
+
+    if (!($node = Node->Lookup($newimageid_args{"node_id"}))) {
+	UserError("Node: Invalid node name");
+    }
+    elsif (!$node->AccessCheck($this_user, TB_NODEACCESS_LOADIMAGE())) {
+	UserError("Node: Not enough permission");
+    }
+    else {
+	$node_id = $node->node_id();
+    }
+}
+
+#
+# Mereusers are not allowed to create more than one osid/imageid mapping
+# for each machinetype. They cannot actually do that through the EZ form
+# since the osid/imageid has to be unique, but it can happen by mixed
+# use of the long form and the short form, or with multiple uses of the
+# long form. 
+#
+my $osidclause;
+foreach my $partn_osid (grep(/^part[1-4]_osid$/, keys(%newimageid_args))) {
+    $osidclause .= " or "
+	if (defined($osidclause));
+    $osidclause .= "osid='$newimageid_args{$partn_osid}'";
+}
+
+DBQueryWarn("lock tables images write, os_info write, osidtoimageid write");
+my $query_result =
+    DBQueryWarn("select osidtoimageid.*,images.pid,images.imagename ".
+		 " from osidtoimageid ".
+		 "left join images on ".
+		 " images.imageid=osidtoimageid.imageid ".
+		 "where ($osidclause) and ($typeclause)");
+DBQueryWarn("unlock tables");
+if ($query_result->numrows) {
+
+    my $msg = 
+	"There are other image descriptors that specify the same OS".
+	"descriptors for the same node types.  There must be a unique".
+	"mapping of OS descriptor to Image descriptor for each node type!".
+	"Perhaps you need to delete one of the images below, or create a".
+	"new OS descriptor to use in this new Image descriptor.  \n\n";
+    my $fmt = "%-20s %-20s %-20s\n";
+    $msg .= sprintf($fmt, "OS ID/name", "Node Type", "Image PID/ID/name");
+    $msg .= sprintf($fmt, "==========", "=========", "=================");
+    while (my ($osid, $type, $imageid, $pid, $imagename) = 
+	   $query_result->fetchrow_array()) {
+	my $osname = OSinfo->Lookup($osid)->osname();
+	$msg .= sprintf($fmt, "$osid/$osname",$type,"$pid/$imageid/$imagename");
+    }
+
+    UserError("Conflict: Please check the other Image descriptors".
+	      " and make the necessary changes!\n $msg");
+}
+
+exit(0)
+    if ($verify);
+
+#
+# Now safe to create new image descriptor.
+#
+# We pass the imagename along as an argument to Create(), so remove it from
+# the argument array.
+#
+delete($newimageid_args{"imagename"});
+
+my $usrerr;
+my $new_image = Image->Create($project, $group, $this_user, $imagename,
+				\%newimageid_args, \$usrerr);
+UserError($usrerr)
+    if (defined($usrerr));
+fatal("Could not create new Image!")
+    if (!defined($new_image));
+
+my $imageid = $new_image->imageid();
+
+# The web interface requires this line to be printed.
+print "IMAGE $imagename/$imageid has been created\n";
+
+exit(0);
+
+sub fatal($)
+{
+    my ($mesg) = @_;
+
+    print STDERR "*** $0:\n".
+	         "    $mesg\n";
+    # Exit with negative status so web interface treats it as system error.
+    exit(-1);
+}
+
+sub UserError(;$)
+{
+    my ($mesg) = @_;
+
+    if (keys(%errors)) {
+	foreach my $key (keys(%errors)) {
+	    my $val = $errors{$key};
+	    print "${key}: $val\n";
+	}
+    }
+    print "$mesg\n"
+	if (defined($mesg));
+
+    # Exit with positive status so web interface treats it as user error.
+    exit(1);
+}
+
+sub escapeshellarg($)
+{
+    my ($str) = @_;
+
+    $str =~ s/[^[:alnum:]]/\\$&/g;
+    return $str;
+}
diff --git a/configure b/configure
index 3c33e5433dfafb67674339a84aa8cb45952ce521..67d73815f6bc067a2ade8173c5c1d60de3cf4d93 100755
--- a/configure
+++ b/configure
@@ -2429,7 +2429,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
 	account/quotamail account/mkusercert account/newproj account/newuser \
 	backend/GNUmakefile backend/moduserinfo backend/newgroup \
 	backend/newmmlist backend/editexp backend/editimageid backend/editnodetype \
-	backend/editsitevars \
+	backend/editsitevars backend/newimageid \
 	tbsetup/GNUmakefile tbsetup/console_setup tbsetup/spewlogfile \
 	tbsetup/spewrpmtar tbsetup/gentopofile tbsetup/power_sgmote.pm \
 	tbsetup/console_reset tbsetup/bwconfig tbsetup/power_rpc27.pm \
diff --git a/configure.in b/configure.in
index a2f954815fb07b639b9dc0cca86a98a99e0ae50d..f71a069e989943df8c89facf51e1a49f6f1da225 100755
--- a/configure.in
+++ b/configure.in
@@ -809,7 +809,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
 	account/quotamail account/mkusercert account/newproj account/newuser \
 	backend/GNUmakefile backend/moduserinfo backend/newgroup \
 	backend/newmmlist backend/editexp backend/editimageid backend/editnodetype \
-	backend/editsitevars \
+	backend/editsitevars backend/newimageid  \
 	tbsetup/GNUmakefile tbsetup/console_setup tbsetup/spewlogfile \
 	tbsetup/spewrpmtar tbsetup/gentopofile tbsetup/power_sgmote.pm \
 	tbsetup/console_reset tbsetup/bwconfig tbsetup/power_rpc27.pm \
diff --git a/db/Image.pm.in b/db/Image.pm.in
index 5c71683b20f644e53cddca20888d636021db81e7..936d7dcaaedc0b28196ed5976c9af412645cd829 100644
--- a/db/Image.pm.in
+++ b/db/Image.pm.in
@@ -164,6 +164,163 @@ sub Refresh($)
     return 0;
 }
 
+#
+# Create a new os_info. This installs the new record in the DB,
+# and returns an instance. There is some bookkeeping along the way.
+#
+sub Create($$$$)
+{
+    my ($class, $project, $group, $creator, $imagename, $argref, $usrerr_ref) = @_;
+    my $idx;
+    my $now = time();
+
+    return undef
+	if (ref($class) || !ref($project));
+
+    my $isadmin = $creator->IsAdmin();
+
+    # We may ignore particular partN_osid's by deleting them.
+    my @arg_slots = grep(/^part[1-4]_osid$/, keys(%{$argref}));
+
+    # Pass-through a bunch of required slots, ignoring any extras
+    # and stuff we handle explicitly.
+    foreach my $key ("loadpart", "loadlength", "default_osid") {
+	if (!exists($argref->{$key})) {
+	    $$usrerr_ref = "Error: $key missing in Image->Create!";
+	    return undef;
+	}
+	push(@arg_slots, $key);
+    }
+    # Pass-through optional slots, otherwise the DB default is used.
+    foreach my $key ("path", "shared", "global") {
+	if (exists($argref->{$key})) {
+	    push(@arg_slots, $key);
+	}
+    }
+
+    my $pid     = $project->pid();
+    my $pid_idx = $project->pid_idx();
+    my $gid     = $group->gid();
+    my $gid_idx = $group->gid_idx();
+    my $uid     = $creator->uid();
+    my $uid_idx = $creator->uid_idx();
+
+    #
+    # The pid/imageid has to be unique, so lock the table for the check/insert.
+    #
+    DBQueryWarn("lock tables images write, emulab_indicies write")
+	or return undef;
+
+    my $query_result =
+	DBQueryWarn("select imagename from images ".
+		    "where pid_idx='$pid_idx' and imagename='$imagename'");
+
+    if ($query_result->numrows) {
+	DBQueryWarn("unlock tables");
+	$$usrerr_ref = "Error: IMAGE $imagename in project $pid already exists!";
+	return undef;
+    }
+
+    #
+    # Grab unique ID. Table already locked.
+    # 
+    my $imageid  = TBGetUniqueIndex("next_imageid", undef, 1);
+    my $uuid  = NewUUID();
+    my $desc  = "''";
+
+    # Some fields special cause of quoting.
+    #
+    if (exists($argref->{'description'})) {
+	$desc = DBQuoteSpecial($argref->{'description'});
+    }
+    
+    my $query = "insert into images set ".
+	join(",", map("$_='" . $argref->{$_} . "'", @arg_slots));
+
+    # Append the rest
+    $query .= ",imagename='$imagename'";
+    $query .= ",imageid='$imageid'";
+    $query .= ",uuid='$uuid'";
+    $query .= ",pid='$pid',pid_idx='$pid_idx'";
+    $query .= ",gid='$gid',gid_idx='$gid_idx'";
+    $query .= ",creator='$uid',creator_idx='$uid_idx'";
+    $query .= ",created=now()";
+    $query .= ",description=$desc";    
+    
+    if (! DBQueryWarn($query)) {
+	DBQueryWarn("unlock tables");
+	tberror("Error inserting new images record for $pid/$imagename!");
+	return undef;
+    }
+
+    my $image = Image->Lookup($imageid);
+    # Create the osidtoimageid mapping.  Admins have an option to do it or not.
+    my $makedefault = exists($argref->{"makedefault"}) && 
+	$argref->{"makedefault"} eq "1";
+    if (!$isadmin || $makedefault) {
+
+	# Lock tables unlocks previous locks as a side-effect.
+	DBQueryWarn("lock tables osidtoimageid write, images write, ".
+		    "nodes as n write, node_type_attributes as a write");
+	#
+	# Need a list of node types. We join this over the nodes table so that
+	# we get a list of just the nodes that are currently in the testbed, not
+	# just in the node_types table.
+	#
+	my $types_result =
+	    DBQueryWarn("select distinct n.type from nodes as n ".
+			 "left join node_type_attributes as a on a.type=n.type ".
+			 "where a.attrkey='imageable' and ".
+			 "      a.attrvalue!='0'");
+	my @mtypes_array;
+	my @map_updates;
+	while (my ($type) = $types_result->fetchrow_array()) {
+	    push(@mtypes_array, $type);
+
+	    # Remember when we get one of the mtype_$type args.  These aren't DB
+	    # columns to update, but instead control re-creating the rows in the
+	    # osidtoimageid table for this imageid, below.
+	    my $mtype = "mtype_$type";
+	    if (exists($argref->{$mtype})) {
+		my $value = $argref->{$mtype};
+		##printf "argref->{$mtype} %s\n", $value;
+		if ($value eq "1") {
+		    push(@map_updates, $type);
+		}
+	    }
+	}
+
+	my @osid_array;
+	for (my $i = 1; $i <= 4; $i++) {
+	    my $foo = $image->field("part${i}_osid");
+	    if (defined($foo)) {
+		push(@osid_array, $foo);
+	    }
+	}
+
+	for (my $i = 0; $i <= $#map_updates; $i++) {
+	    for (my $j = 0; $j <= $#osid_array; $j++) {
+		 my $query = "INSERT INTO osidtoimageid ".
+			     "       (osid, type, imageid) ".
+			     "VALUES ('$osid_array[$j]', ".
+			     "        '$map_updates[$i]', ".
+			     "        '$imageid')";
+		 ##print "$query\n";
+		 if (! DBQueryWarn($query)) {
+		     DBQueryWarn("unlock tables");
+		     tberror("Error inserting new ostoimageid mapping for ".
+			     "$pid/$imagename!");
+		     return undef;
+		 }
+	    }
+	}
+    }
+
+    DBQueryWarn("unlock tables");
+
+    return $image;
+}
+
 #
 # Worker class method to edit image descriptor.
 # Assumes most argument checking was done elsewhere.
@@ -196,7 +353,7 @@ sub EditImageid($$$$)
     # just in the node_types table.
     #
     my $types_result =
-	DBQueryFatal("select distinct n.type from nodes as n ".
+	DBQueryWarn("select distinct n.type from nodes as n ".
 		     "left join node_type_attributes as a on a.type=n.type ".
 		     "where a.attrkey='imageable' and ".
 		     "      a.attrvalue!='0'");
@@ -230,13 +387,13 @@ sub EditImageid($$$$)
     }
 
     if (keys %updates || $redo_map) {
-	DBQueryFatal("lock tables images write, os_info write, ".
+	DBQueryWarn("lock tables images write, os_info write, ".
 		     "osidtoimageid write");
     }
     if (keys %updates) {
 	if ($image->Update(\%updates)) {
 	    $$usrerr_ref = "DB: Error updating the images table";
-	    DBQueryFatal("unlock tables");
+	    DBQueryWarn("unlock tables");
 	    return undef;
 	}
     }
@@ -245,7 +402,7 @@ sub EditImageid($$$$)
 	# Update the osidtoimageid table too.
 	# 
 	# Must delete old entries first.
-	DBQueryFatal("delete from osidtoimageid ".
+	DBQueryWarn("delete from osidtoimageid ".
 		     "where imageid='$imageid'");
 
 	my @osid_array;
@@ -263,12 +420,12 @@ sub EditImageid($$$$)
 			     "        '$map_updates[$i]', ".
 			     "        '$imageid')";
 		 ##print "$query\n";
-		 DBQueryFatal($query);
+		 DBQueryWarn($query);
 	    }
 	}
     }
     if (keys %updates || $redo_map) {
-	DBQueryFatal("unlock tables");
+	DBQueryWarn("unlock tables");
     }
 
     return 1;
diff --git a/sql/database-fill.sql b/sql/database-fill.sql
index 63f8f8029285c5315d4bb3833b0ce6dbf77dc0fb..bcb99bcf34e6e1f5713375e7145d01a731337114 100644
--- a/sql/database-fill.sql
+++ b/sql/database-fill.sql
@@ -789,12 +789,24 @@ REPLACE INTO table_regex VALUES ('experiments','elab_in_elab','int','redirect','
 REPLACE INTO table_regex VALUES ('experiments','elabinelab_singlenet','int','redirect','default:boolean',0,0,NULL);
 REPLACE INTO table_regex VALUES ('experiments','elabinelab_cvstag','text','regex','^[-\\w\\@\\/\\.]+$',0,0,NULL);
 
-REPLACE INTO table_regex VALUES ('images','imageid','text','redirect','default:int',0,100000000,NULL);
 REPLACE INTO table_regex VALUES ('images','imagename','text','regex','^[a-zA-Z0-9][-\\w\\.+]+$',2,30,NULL);
+REPLACE INTO table_regex VALUES ('images','imageid','text','redirect','default:int',0,100000000,NULL);
+REPLACE INTO table_regex VALUES ('images','pid','text','redirect','projects:pid',0,0,NULL);
+REPLACE INTO table_regex VALUES ('images','gid','text','redirect','groups:gid',0,0,NULL);
 REPLACE INTO table_regex VALUES ('images','description','text','regex','^[\\040-\\176\\012\\015\\011]*$',1,256,NULL);
+REPLACE INTO table_regex VALUES ('images','loadpart','text','redirect','default:tinyint',0,4,NULL);
+REPLACE INTO table_regex VALUES ('images','loadlength','text','redirect','default:tinyint',1,4,NULL);
+REPLACE INTO table_regex VALUES ('images','part1_osid','text','redirect','os_info:osid',0,0,NULL);
+REPLACE INTO table_regex VALUES ('images','part2_osid','text','redirect','os_info:osid',0,0,NULL);
+REPLACE INTO table_regex VALUES ('images','part3_osid','text','redirect','os_info:osid',0,0,NULL);
+REPLACE INTO table_regex VALUES ('images','part4_osid','text','redirect','os_info:osid',0,0,NULL);
+REPLACE INTO table_regex VALUES ('images','default_osid','text','redirect','os_info:osid',0,0,NULL);
 REPLACE INTO table_regex VALUES ('images','path','text','regex','^[-_\\w\\.\\/:+]*$',1,256,NULL);
+REPLACE INTO table_regex VALUES ('images','shared','text','redirect','default:boolean',0,0,NULL);
+REPLACE INTO table_regex VALUES ('images','global','text','redirect','default:boolean',0,0,NULL);
+REPLACE INTO table_regex VALUES ('images','makedefault','text','redirect','default:boolean',0,0,NULL);
 REPLACE INTO table_regex VALUES ('images','mtype','text','redirect','default:boolean',0,0,NULL);
-REPLACE INTO table_regex VALUES ('images','osid','text','redirect','os_info:osid',0,0,NULL);
+REPLACE INTO table_regex VALUES ('images','node_id','text','redirect','nodes:node_id',0,0,NULL);
 REPLACE INTO table_regex VALUES ('images','load_address','text','redirect','default:text',0,0,NULL);
 REPLACE INTO table_regex VALUES ('images','frisbee_pid','text','redirect','default:int',0,0,NULL);
 
diff --git a/www/imageid_defs.php b/www/imageid_defs.php
index 1bfe16f74663568f54c0c0801dcc581812b065a9..7467c29e723f9fa9c5c0760f0a1cc95f941bf0a9 100644
--- a/www/imageid_defs.php
+++ b/www/imageid_defs.php
@@ -115,7 +115,94 @@ class Image
     }
 
     #
-    # Class function to edit image descriptor.
+    # Class function to create a new image descriptor.
+    #
+    function NewImageId($imagename, $args, &$errors) {
+	global $suexec_output, $suexec_output_array;
+
+        #
+        # Generate a temporary file and write in the XML goo.
+        #
+	$xmlname = tempnam("/tmp", "newimageid");
+	if (! $xmlname) {
+	    TBERROR("Could not create temporary filename", 0);
+	    $errors[] = "Transient error(1); please try again later.";
+	    return null;
+	}
+	if (! ($fp = fopen($xmlname, "w"))) {
+	    TBERROR("Could not open temp file $xmlname", 0);
+	    $errors[] = "Transient error(2); please try again later.";
+	    return null;
+	}
+
+	# Add these. Maybe caller should do this?
+	$args["imagename"] = $imagename;
+
+	fwrite($fp, "<image>\n");
+	foreach ($args as $name => $value) {
+	    fwrite($fp, "<attribute name=\"$name\">");
+	    fwrite($fp, "  <value>" . htmlspecialchars($value) . "</value>");
+	    fwrite($fp, "</attribute>\n");
+	}
+	fwrite($fp, "</image>\n");
+	fclose($fp);
+	chmod($xmlname, 0666);
+
+	$retval = SUEXEC("nobody", "nobody", "webnewimageid $xmlname",
+			 SUEXEC_ACTION_IGNORE);
+
+	if ($retval) {
+	    if ($retval < 0) {
+		$errors[] = "Transient error(3); please try again later.";
+		SUEXECERROR(SUEXEC_ACTION_CONTINUE);
+	    }
+	    else {
+		# unlink($xmlname);
+		if (count($suexec_output_array)) {
+		    for ($i = 0; $i < count($suexec_output_array); $i++) {
+			$line = $suexec_output_array[$i];
+			if (preg_match("/^([-\w]+):\s*(.*)$/",
+				       $line, $matches)) {
+			    $errors[$matches[1]] = $matches[2];
+			}
+			else
+			    $errors[] = $line;
+		    }
+		}
+		else
+		    $errors[] = "Transient error(4); please try again later.";
+	    }
+	    return null;
+	}
+
+        #
+        # Parse the last line of output. Ick.
+        #
+	unset($matches);
+	
+	if (!preg_match("/^IMAGE\s+([^\/]+)\/(\d+)\s+/",
+			$suexec_output_array[count($suexec_output_array)-1],
+			$matches)) {
+	    $errors[] = "Transient error; please try again later.";
+	    SUEXECERROR(SUEXEC_ACTION_CONTINUE);
+	    return null;
+	}
+	$image = $matches[2];
+	$newimage = image::Lookup($image);
+	if (! $newimage) {
+	    $errors[] = "Transient error; please try again later.";
+	    TBERROR("Could not lookup new image $image", 0);
+	    return null;
+	}
+
+	# Unlink this here, so that the file is left behind in case of error.
+	# We can then create the image by hand from the xmlfile, if desired.
+	unlink($xmlname);
+	return $newimage; 
+    }
+
+    #
+    # Class function to edit an image descriptor.
     #
     function EditImageid($image, $args, &$errors) {
 	global $suexec_output, $suexec_output_array;
diff --git a/www/newimageid.php3 b/www/newimageid.php3
index d0616aa5ccc58bc2133410dc4dce3e545cebab1c..e95c1a58788b1d2fe821f4a7e1956924c090aaf1 100644
--- a/www/newimageid.php3
+++ b/www/newimageid.php3
@@ -1,7 +1,7 @@
 <?php
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2004, 2006, 2007 University of Utah and the Flux Group.
+# Copyright (c) 2000-2007 University of Utah and the Flux Group.
 # All rights reserved.
 #
 include("defs.php3");
@@ -16,7 +16,7 @@ include("osiddefs.php3");
 PAGEHEADER("Create a new Image Descriptor (long form)");
 
 #
-# Only known and logged in users!
+# Only known and logged in users.
 #
 $this_user = CheckLoginOrDie();
 $uid       = $this_user->uid();
@@ -26,8 +26,8 @@ $isadmin   = ISADMIN();
 #
 # Verify page arguments.
 #
-$optargs = OptionalPageArguments("submit",       PAGEARG_STRING,
-				 "formfields",   PAGEARG_ARRAY);
+$optargs = OptionalPageArguments("submit",     PAGEARG_STRING,
+				 "formfields", PAGEARG_ARRAY);
 
 #
 # See what projects the uid can do this in.
@@ -51,14 +51,14 @@ $types_result =
 		 "      a.attrvalue!='0' and n.role='testnode'");
 
 #
-# Spit the form out using the array of data. 
-# 
+# Spit the form out using the array of data.
+#
 function SPITFORM($formfields, $errors)
 {
     global $this_user, $projlist, $isadmin, $types_result;
     global $TBDB_IMAGEID_IMAGENAMELEN, $TBDB_NODEIDLEN;
     global $TBPROJ_DIR, $TBGROUP_DIR;
-    
+
     if ($errors) {
 	echo "<table class=nogrid
                      align=center border=0 cellpadding=6 cellspacing=0>
@@ -162,14 +162,15 @@ function SPITFORM($formfields, $errors)
               }
           </SCRIPT>\n";
 
+    $url = CreateURL("newimageid");
     echo "<br>
-          <table align=center border=1> 
+          <table align=center border=1>
           <tr>
              <td align=center colspan=2>
                  <em>(Fields marked with * are required)</em>
              </td>
           </tr>
-          <form action='newimageid.php3' method=post name=idform>\n";
+          <form action='$url' method=post name=idform>\n";
 
     #
     # Select Project
@@ -225,7 +226,7 @@ function SPITFORM($formfields, $errors)
           </tr>\n";
 
     #
-    # Image Name:
+    # Image Name
     #
     echo "<tr>
               <td>*Descriptor Name (no blanks):</td>
@@ -421,7 +422,7 @@ function SPITFORM($formfields, $errors)
 
     echo "<tr>
               <td align=center colspan=2>
-                  <b><input type=submit name=submit value=Submit></b>
+                 <b><input type=submit name=submit value=Submit></b>
               </td>
           </tr>\n";
 
@@ -472,11 +473,11 @@ if (!isset($submit)) {
     $defaults["part3_osid"]  = "";
     $defaults["part4_osid"]  = "";
     $defaults["default_osid"]= "";
+    $defaults["path"]        = "$TBPROJ_DIR/";
     $defaults["node_id"]     = "";
     $defaults["shared"]      = "No";
     $defaults["global"]      = "No";
     $defaults["makedefault"] = "No";
-    $defaults["path"]        = "$TBPROJ_DIR/";
 
     #
     # For users that are in one project and one subgroup, it is usually
@@ -485,7 +486,7 @@ if (!isset($submit)) {
     # 
     if (count($projlist) == 1) {
 	list($project, $grouplist) = each($projlist);
-	
+
 	if (count($grouplist) <= 2) {
 	    $defaults["pid"] = $project;
 	    if (count($grouplist) == 1 || strcmp($project, $grouplist[0]))
@@ -512,203 +513,108 @@ if (!isset($submit)) {
 # Otherwise, must validate and redisplay if errors
 #
 $errors  = array();
-$project = null;
-$group   = null;
 
+# Be friendly about the form field names.
 if (!isset($formfields["pid"]) ||
     strcmp($formfields["pid"], "") == 0) {
-    $errors["Project"] = "Not Selected";
-}
-elseif (!TBvalid_pid($formfields["pid"])) {
-    $errors["Project"] = "Invalid project name";
-}
-elseif (! ($project = Project::Lookup($formfields["pid"]))) {
-    $errors["Project"] = "Invalid project name";
-}
-elseif (! $project->AccessCheck($this_user, $TB_PROJECT_MAKEIMAGEID)) {
-    $errors["Project"] = "Not enough permission";
-}
-
-if (isset($formfields["gid"]) && $formfields["gid"] != "") {
-    if ($formfields["pid"] == $formfields["gid"] && $project) {
-	$group = $project->DefaultGroup();
-    }
-    elseif (!TBvalid_gid($formfields["gid"])) {
-	$errors["Group"] = "Invalid group name";
-    }
-    elseif ($project &&
-	    ! ($group = $project->LookupSubgroupByName($formfields["gid"]))) {
-	$errors["Group"] = "Invalid group name";
-    }
-}
-elseif ($project) {
-    $group = $project->DefaultGroup();    
+    $errors["Project"] = "Missing Field";
 }
 
 if (!isset($formfields["imagename"]) ||
     strcmp($formfields["imagename"], "") == 0) {
     $errors["Descriptor Name"] = "Missing Field";
 }
-else {
-    if (! ereg("^[a-zA-Z0-9][-_a-zA-Z0-9\.\+]+$", $formfields["imagename"])) {
-	$errors["Descriptor Name"] =
-	    "Must be alphanumeric (includes _, -, +, and .)<br>".
-	    "and must begin with an alphanumeric";
-    }
-    elseif (strlen($formfields["imagename"]) > $TBDB_IMAGEID_IMAGENAMELEN) {
-	$errors["Descriptor Name"] =
-	    "Too long! ".
-	    "Must be less than or equal to $TBDB_IMAGEID_IMAGENAMELEN";
-    }
-}
-
-if (!isset($formfields["description"]) ||
-    strcmp($formfields["description"], "") == 0) {
-    $errors["Description"] = "Missing Field";
-}
 
 if (!isset($formfields["loadpart"]) ||
-    strcmp($formfields["loadpart"], "") == 0 ||
     strcmp($formfields["loadpart"], "X") == 0) {
-    $errors["Starting Partition"] = "Not Selected";
-}
-elseif (! ereg("^[0-9]+$", $formfields["loadpart"]) ||
-	$formfields["loadpart"] < 0 || $formfields["loadpart"] > 4) {
-    $errors["Starting Partition"] = "Must be 0,1,2,3, or 4!";
+    $errors["Starting DOS Partion"] = "Missing Field";
 }
 
 if (!isset($formfields["loadlength"]) ||
-    strcmp($formfields["loadlength"], "") == 0 ||
     strcmp($formfields["loadlength"], "X") == 0) {
-    $errors["#of Partitions"] = "Not Selected";
+    $errors["Number of DOS Partitions"] = "Missing Field";
 }
-elseif (! ereg("^[0-9]+$", $formfields["loadlength"]) ||
-	$formfields["loadlength"] < 1 || $formfields["loadlength"] > 4) {
-    $errors["#of Partitions"] = "Must be 1,2,3, or 4!";
-}
-elseif ($formfields["loadpart"] != 0 && $formfields["loadlength"] != 1) {
-    $errors["#of Partitions"] =
-	"Only single slices or<br> partial disks are allowed";
+
+if (!isset($formfields["default_osid"]) ||
+    strcmp($formfields["default_osid"], "none") == 0) {
+    $errors["Boot OS"] = "Missing Field";
 }
 
 #
-# Check sanity of the OSIDs for each slice. Permission checks not needed.
-# Store the ones we care about and silently forget about the extraneous
-# OSIDs by setting the locals to NULL.
+# Build up argument array to pass along.
 #
-# XXX This loop creates locals part1_osid, part2_osid, part3_osid, and
-#     part4_osid on the fly. Look at $$foo. We use them below.
-#
-$osid_array = array();
+$args = array();
 
-for ($i = 1; $i <= 4; $i++) {
-    $foo      = "part${i}_osid";	# Local variable dynamically created.
-    $thisosid = $formfields[$foo];
+if (isset($formfields["pid"]) && $formfields["pid"] != "") {
+    $args["pid"] = $pid = $formfields["pid"];
+}
 
-    if (($formfields["loadpart"] && $i == $formfields["loadpart"]) ||
-	(!$formfields["loadpart"] && $i <= $formfields["loadlength"])) {
+if (isset($formfields["gid"]) && $formfields["gid"] != "") {
+    $args["gid"] = $gid = $formfields["gid"];
+}
 
-	if (!isset($thisosid) ||
-	    strcmp($thisosid, "") == 0 ||
-	    strcmp($thisosid, "X") == 0) {
-	    $errors["Partition $i OS"] = "Must select an OS";
-	}
-	elseif (strcmp($thisosid, "none") == 0) {
-	    #
-	    # Allow admins to specify no OS for a partition.
-	    # 
-	    if (!$isadmin)	    
-		$errors["Partition $i OS"] = "Must select an OS";
-	    $$foo = "NULL";	    
-	}
-	elseif (! TBvalid_osid($thisosid)) {
-	    $errors["Partition $i OS"] = "Invalid characters in OSID";
-	}
-	elseif (!OSinfo::Lookup($thisosid)) {
-	    $errors["Partition $i OS"] = "No such OS defined";
-	}
-	else {
-	    $$foo = "'$thisosid'";
-	    $osid_array[] = $thisosid;
-	}
-    }
-    else {
-	$$foo = "NULL";
-    }
+if (isset($formfields["imagename"]) && $formfields["imagename"] != "") {
+    $args["imagename"] = $formfields["imagename"];
 }
 
-#
-# Check the boot OS. Must be one of the OSes selected for a partition.
-# 
-if (!isset($formfields["default_osid"]) ||
-    strcmp($formfields["default_osid"], "") == 0 ||
-    strcmp($formfields["default_osid"], "none") == 0) {
-    $errors["Boot OS"] = "Not Selected";
+if (isset($formfields["description"]) && $formfields["description"] != "") {
+    $args["description"] = $formfields["description"];
 }
-elseif (! TBvalid_osid($formfields["default_osid"])) {
-    $errors["Boot OS"] = "Invalid characters in OSID";
+
+if (isset($formfields["loadpart"]) &&
+    $formfields["loadpart"] != "none" && $formfields["loadpart"] != "") {
+    $args["loadpart"] = $formfields["loadpart"];
 }
-elseif (!OSinfo::Lookup($formfields["default_osid"])) {
-    $errors["Boot OS"] = "No such OS defined";
+
+if (isset($formfields["loadlength"]) &&
+    $formfields["loadlength"] != "none" && $formfields["loadlength"] != "") {
+    $args["loadlength"] = $formfields["loadlength"];
 }
-else {
-    for ($i = 0; $i < count($osid_array); $i++) {
-	if (strcmp($osid_array[$i], $formfields["default_osid"]) == 0)
-	    break;
-    }
-    if ($i == count($osid_array)) 
-	$errors["Boot OS"] = "Invalid; Must be one of the partitions";
+
+if (isset($formfields["part1_osid"]) &&
+    $formfields["part1_osid"] != "none" && $formfields["part1_osid"] != "") {
+    $args["part1_osid"] = $formfields["part1_osid"];
 }
 
-#
-# Only admin types can set the global bit for an image. Ignore silently.
-#
-$global = 0;
-if ($isadmin &&
-    isset($formfields["global"]) &&
-    strcmp($formfields["global"], "Yep") == 0) {
-    $global = 1;
+if (isset($formfields["part2_osid"]) &&
+    $formfields["part2_osid"] != "none" && $formfields["part2_osid"] != "") {
+    $args["part2_osid"] = $formfields["part2_osid"];
 }
 
-$shared = 0;
-if (isset($formfields["shared"]) &&
-    strcmp($formfields["shared"], "Yep") == 0) {
-    $shared = 1;
+if (isset($formfields["part3_osid"]) &&
+    $formfields["part3_osid"] != "none" && $formfields["part3_osid"] != "") {
+    $args["part3_osid"] = $formfields["part3_osid"];
 }
-# Does not make sense to do this. 
-if ($global && $shared) {
-    $errors["Global"] = "Image declared both shared and global";
+
+if (isset($formfields["part4_osid"]) &&
+    $formfields["part4_osid"] != "none" && $formfields["part4_osid"] != "") {
+    $args["part4_osid"] = $formfields["part4_osid"];
 }
 
-#
-# The path must not contain illegal chars and it must be more than
-# the original /proj/$pid we gave the user. We allow admins to specify
-# a path outside of /proj though.
-# 
-if (!isset($formfields["path"]) ||
-    strcmp($formfields["path"], "") == 0) {
-    $errors["Path"] = "Missing Field";
+if (isset($formfields["default_osid"]) &&
+    $formfields["default_osid"] != "none" && $formfields["default_osid"] != "") {
+    $args["default_osid"] = $formfields["default_osid"];
 }
-elseif (! ereg("^[-_a-zA-Z0-9\/\.+]+$", $formfields["path"])) {
-    $errors["Path"] = "Contains invalid characters";
+
+if (isset($formfields["path"]) && $formfields["path"] != "") {
+    $args["path"] = $formfields["path"];
 }
-elseif (! $isadmin) {
-    $pdef = "";
-    
-    if (!$shared &&
-	isset($formfields["gid"]) &&
-	strcmp($formfields["gid"], "") &&
-	strcmp($formfields["gid"], $formfields["pid"])) {
-	$pdef = "$TBGROUP_DIR/" .
-	    $formfields["pid"] . "/" . $formfields["gid"] . "/";
-    }
-    else {
-	$pdef = "$TBPROJ_DIR/" . $formfields["pid"] . "/images/";
-    }
 
-    if (strpos($formfields["path"], $pdef) === false) {
-	$errors["Path"] = "Invalid Path";
-    }
+if (isset($formfields["node_id"]) && $formfields["node_id"] != "") {
+    $args["node_id"] = $node_id = $formfields["node_id"];
+}
+
+# Filter booleans from checkboxes to 0 or 1.
+if (isset($formfields["shared"])) {
+   $args["shared"] = strcmp($formfields["shared"], "Yep") ? 0 : 1;
+}
+if (isset($formfields["global"])) {
+   $args["global"] = strcmp($formfields["global"], "Yep") ? 0 : 1;
+}
+$makedefault = 0;
+if (isset($formfields["makedefault"])) {
+   $args["makedefault"] = $makedefault = 
+       strcmp($formfields["makedefault"], "Yep") ? 0 : 1;
 }
 
 #
@@ -716,7 +622,7 @@ elseif (! $isadmin) {
 # Store the valid types in a new array for simplicity.
 #
 $mtypes_array = array();
-
+mysql_data_seek($types_result, 0);
 while ($row = mysql_fetch_array($types_result)) {
     $type = $row["type"];
 
@@ -732,37 +638,13 @@ if (! count($mtypes_array)) {
     $errors["Node Types"] = "Must select at least one type";
 }
 
-#
-# Check sanity of node name and that user can create an image from it.
-#
-unset($node);
-if (isset($formfields["node_id"]) &&
-    strcmp($formfields["node_id"], "")) {
+# The mtype_* checkboxes are dynamically generated.
+foreach ($mtypes_array as $type) {
 
-    if (!TBvalid_node_id($formfields["node_id"])) {
-	$errors["Node"] = "Invalid node name";
-    }
-    elseif (! ($node = Node::Lookup($formfields["node_id"]))) {
-	$errors["Node"] = "Invalid node name";
-    }
-    elseif (!$node->AccessCheck($this_user, $TB_NODEACCESS_LOADIMAGE)) {
-	$errors["Node"] = "Not enough permission";
-    }
-    else {
-	$node_id = $node->node_id();
-    }
-}
-
-#
-# Only admins have this option. Always on for mereusers, but default off
-# for admins. 
-#
-$makedefault = 0;
-
-if (! $isadmin ||
-    (isset($formfields["makedefault"]) &&
-     strcmp($formfields["makedefault"], "Yep") == 0)) {
-    $makedefault = 1;
+    # Filter booleans from checkbox values.
+    $checked = isset($formfields["mtype_$type"]) &&
+	strcmp($formfields["mtype_$type"], "Yep") == 0;
+    $args["mtype_$type"] = $checked ? "1" : "0";
 }
 
 #
@@ -776,67 +658,47 @@ if (count($errors)) {
 }
 
 #
-# For the rest, sanitize and convert to locals to make life easier.
-# 
-$description = addslashes($formfields["description"]);
-$pid         = $project->pid();
-$gid         = $group->gid();
-$pid_idx     = $project->pid_idx();
-$gid_idx     = $group->gid_idx();
-$imagename   = $formfields["imagename"];
-$loadpart    = $formfields["loadpart"];
-$loadlength  = $formfields["loadlength"];
-$default_osid= $formfields["default_osid"];
-$path        = $formfields["path"];
-
-#
-# Grab unique imageid (before locking tables). 
-# 
-$imageid = TBGetUniqueIndex("next_osid");
-$uuid    = NewUUID();
-
-#
-# And insert the record!
-#
-DBQueryFatal("lock tables images write, osidtoimageid write");
-
-#
-# Of course, the Image record may not already exist in the DB.
-#
-if (($image = Image::LookupByName($project, $imagename))) {
-    DBQueryFatal("unlock tables");
+# Mereusers are not allowed to create more than one osid/imageid mapping
+# for each machinetype. They cannot actually do that through the EZ form
+# since the osid/imageid has to be unique, but it can happen by mixed
+# use of the long form and the short form, or with multiple uses of the
+# long form.
 
-    $errors["Descriptor Name"] = "Already in use in selected project";
+# Can't check this unless we have at least one mtype!
+if (!count($mtypes_array)) {
     SPITFORM($formfields, $errors);
     PAGEFOOTER();
     return;
 }
-
-#
-# Mereusers are not allowed to create more than one osid/imageid mapping
-# for each machinetype. They cannot actually do that through the EZ form
-# since the osid/imageid has to be unique, but it can happen by mixed
-# use of the long form and the short form, or with multiple uses of the
-# long form. 
-#
+    
 $typeclause = "type=" . "'$mtypes_array[0]'";
-
 for ($i = 1; $i < count($mtypes_array); $i++) {
     $typeclause = "$typeclause or type=" . "'$mtypes_array[$i]'";
 }
 
-$osidclause = "osid=" . "'$osid_array[0]'";
-    
-for ($i = 1; $i < count($osid_array); $i++) {
-    $osidclause = "$osidclause or osid=" . "'$osid_array[$i]'";
+unset($osidclause);
+for ($i = 1; $i <= 4; $i++) {
+    # Local variable dynamically created.    
+    $foo      = "part${i}_osid";
+
+    if (isset($formfields[$foo])) {
+	if (isset($osidclause))
+	    $osidclause = "$osidclause or osid='" . $formfields[$foo] . "' ";
+	else 
+	    $osidclause = "osid='" . $formfields[$foo] . "' ";
+
+	$osid_array[] = $formfields[$foo];
+    }
 }
     
+DBQueryFatal("lock tables images write, os_info write, osidtoimageid write");
 $query_result =
     DBQueryFatal("select osidtoimageid.*,images.pid,images.imagename ".
 		 " from osidtoimageid ".
 		 "left join images on ".
 		 " images.imageid=osidtoimageid.imageid ".
 		 "where ($osidclause) and ($typeclause)");
+DBQueryFatal("unlock tables");
 
 if (mysql_num_rows($query_result)) {
     if (!$isadmin || $makedefault) {
@@ -855,23 +717,25 @@ if (mysql_num_rows($query_result)) {
 	echo "<table border=1 cellpadding=2 cellspacing=2 align='center'>\n";
 
 	echo "<tr>
-                  <td align=center>OSID</td>
-                  <td align=center>Type</td>
-                  <td align=center>ImageID</td>
+                  <td align=center>OS ID/name</td>
+                  <td align=center>Node Type</td>
+                  <td align=center>Image PID/ID/name</td>
              </tr>\n";
 
 	while ($row = mysql_fetch_array($query_result)) {
-	    $imageid   = $row['imageid'];
-	    $url       = rawurlencode($imageid);
 	    $osid      = $row["osid"];
+            $osinfo    = OSinfo::Lookup($osid);
+            $osname    = $osinfo->osname();
 	    $type      = $row["type"];
+	    $imageid   = $row['imageid'];
+	    $url       = CreateURL("showimageid", URLARG_IMAGEID, $imageid);
+	    $pid       = $row['pid'];
 	    $imagename = $row["imagename"];
 	    
 	    echo "<tr>
-                      <td>$osid</td>
+                      <td>$osid/$osname</td>
 	              <td>$type</td>
-                      <td><A href='showimageid.php3?&imageid=$url'>
-                             $imagename</A></td>
+                      <td>$pid/$imageid/<A href='$url'>$imagename</A></td>
 	          </tr>\n";
 	}
 	echo "</table><br><br>\n";
@@ -881,43 +745,24 @@ if (mysql_num_rows($query_result)) {
     }
 }
 
-$query_result =
-    DBQueryFatal("INSERT INTO images ".
-		 "(imagename, imageid, description, loadpart, loadlength, ".
-		 " part1_osid, part2_osid, part3_osid, part4_osid, ".
-		 " default_osid, path, pid, gid, shared, global, ".
-		 " creator, creator_idx, created, pid_idx, gid_idx, uuid) ".
-		 "VALUES ".
-		 "  ('$imagename', '$imageid', '$description', $loadpart, ".
-		 "   $loadlength, ".
-		 "   $part1_osid, $part2_osid, $part3_osid, $part4_osid, ".
-		 "   '$default_osid', '$path', '$pid', '$gid', $shared, ".
-	         "   $global, '$uid', '$dbid', now(), $pid_idx, $gid_idx, ".
-		 "   '$uuid')");
-
-if (!$isadmin || $makedefault) {
-    for ($i = 0; $i < count($mtypes_array); $i++) {
-	for ($j = 0; $j < count($osid_array); $j++) {
-	    DBQueryFatal("REPLACE INTO osidtoimageid ".
-			 "(osid, type, imageid) ".
-			 "VALUES ('$osid_array[$j]', '$mtypes_array[$i]', ".
-			 "        '$imageid')");
-	}
-    }
+# Send to the backend for more checking, and eventually, to update the DB.
+$imagename = $args["imagename"];
+if (! ($image = Image::NewImageId($imagename, $args, $errors))) {
+    # Always respit the form so that the form fields are not lost.
+    # I just hate it when that happens so lets not be guilty of it ourselves.
+    SPITFORM($formfields, $errors);
+    PAGEFOOTER();
+    return;
 }
 
-DBQueryFatal("unlock tables");
-
-#
-# Get the object for rest of the script.
-#
-if (! ($image = Image::Lookup($imageid))) {
-    TBERROR("Could not look up object for image $imageid", 1);
-}
+$pid = $image->pid();
+$gid_idx = $image->gid_idx();
+$group = Group::Lookup($gid_idx);
 
 SUBPAGESTART();
 SUBMENUSTART("More Options");
-if (! isset($node)) {
+if (! isset($node_id)) {
+    $imageid = $image->imageid();
     $fooid = rawurlencode($imageid);
     WRITESUBMENUBUTTON("Edit this Image Descriptor",
 		       "editimageid.php3?imageid=$fooid");
@@ -940,7 +785,7 @@ SUBMENUEND();
 $image->Show();
 SUBPAGEEND();
 
-if (isset($node)) {
+if (isset($node_id)) {
     #
     # Create the image.
     #
@@ -948,6 +793,11 @@ if (isset($node)) {
     # problem for the descriptor; the script is done with it by the time
     # it returns. However, if the node is freed up, things are going to go
     # awry. 
+    #
+
+    $node = Node::Lookup($node_id); # Already been checked.
+    $node_id = $node->node_id();    # XXX Why?
+
     #
     # Grab the unix GID for running script.
     #
@@ -955,7 +805,7 @@ if (isset($node)) {
     $safe_name = escapeshellarg($imagename);
 
     echo "<br>
-          Creating image using node '$node_id' ...
+          Creating image using node '$node_id'.
           <br><br>\n";
     flush();
 
@@ -966,8 +816,8 @@ if (isset($node)) {
     echo "This will take 10 minutes or more; you will receive email
           notification when the image is complete. In the meantime,
           <b>PLEASE DO NOT</b> delete the imageid or the experiment
-          $node is in. In fact, it is best if you do not mess with 
-          the node at all!<br>\n";
+          $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";
 }
 
 #