diff --git a/backend/GNUmakefile.in b/backend/GNUmakefile.in
index 4c64d1a81e8d06e4d8267b702fd58c8d1da3fc17..0920c92f4fe38a2eecda3fb55e05dda27bc3d425 100644
--- a/backend/GNUmakefile.in
+++ b/backend/GNUmakefile.in
@@ -12,8 +12,10 @@ UNIFIED         = @UNIFIED_BOSS_AND_OPS@
 
 include $(OBJDIR)/Makeconf
 
-BIN_SCRIPTS	= moduserinfo newgroup newmmlist editexp editimageid
-WEB_BIN_SCRIPTS = webmoduserinfo webnewgroup webnewmmlist webeditimageid
+BIN_SCRIPTS	= moduserinfo newgroup newmmlist editexp editimageid \
+		  editnodetype
+WEB_BIN_SCRIPTS = webmoduserinfo webnewgroup webnewmmlist webeditimageid \
+		  webeditnodetype
 WEB_SBIN_SCRIPTS= 
 LIBEXEC_SCRIPTS	= $(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS)
 
diff --git a/backend/editnodetype.in b/backend/editnodetype.in
new file mode 100644
index 0000000000000000000000000000000000000000..01b3d4ce12de9f4294190dd750c9186fc5d7ae5a
--- /dev/null
+++ b/backend/editnodetype.in
@@ -0,0 +1,552 @@
+#!/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 or edit a nodetype.
+#
+sub usage()
+{
+    print("Usage: editnodetype [-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@";
+
+#
+# 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 OSinfo;
+
+# 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!");
+    }
+    fatal("You must have admin privledges to ...")
+	if (!$this_user->IsAdmin());
+}
+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
+    ("node_type"	=> ["node_type",	$SLOT_REQUIRED],
+
+     # Presence of new_type commands creation of a new nodetype.
+     "new_type"		=> ["attr_boolean",	$SLOT_OPTIONAL],
+
+     # Class may only be changed while making a new class. 
+     "class"		=> ["class",		$SLOT_OPTIONAL],
+
+     # Fixed attributes.
+     "isvirtnode"	=> ["boolean",		$SLOT_OPTIONAL],
+     "isjailed"		=> ["boolean",		$SLOT_OPTIONAL],
+     "isdynamic"	=> ["boolean",		$SLOT_OPTIONAL],
+     "isremotenode"	=> ["boolean",		$SLOT_OPTIONAL],
+     "issubnode"	=> ["boolean",		$SLOT_OPTIONAL],
+     "isplabdslice"	=> ["boolean",		$SLOT_OPTIONAL],
+     "issimnode"	=> ["boolean",		$SLOT_OPTIONAL],
+
+     # Dynamic attributes with wildcards.
+     "attr_boolean_*"	=> ["attr_boolean",	$SLOT_OPTIONAL],
+     "attr_integer_*"	=> ["attr_int",		$SLOT_OPTIONAL],
+     "attr_float_*"	=> ["attr_float",	$SLOT_OPTIONAL],
+     "attr_string_*"	=> ["attr_string",	$SLOT_OPTIONAL],
+
+     # Old-style osid and image names.
+     "attr_string_*_osid"    => ["attr_osid",	$SLOT_OPTIONAL],
+     "attr_string_*_imageid" => ["attr_imageid",$SLOT_OPTIONAL],
+     # New-style indices.
+     "attr_integer_*_osid"   => ["attr_int",	$SLOT_OPTIONAL],
+     "attr_integer_*_imageid"=> ["attr_int",	$SLOT_OPTIONAL],
+
+     # The name of a single attribute to add to the list.
+     "new_attr"		=> ["attr_name",	$SLOT_OPTIONAL],
+     # Multiple attributes can be deleted from the list.
+     "delete_*"		=> ["attr_boolean",	$SLOT_OPTIONAL]);
+
+#
+# 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 () as we check
+# the attributes.
+#
+my %editnodetype_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, "node_types", $dbslot, TBDB_CHECKDBSLOT_ERROR)) {
+	$errors{$key} = TBFieldErrorString();
+	next;
+    }
+
+    $editnodetype_args{$key} = $value;
+}
+UserError()
+    if (keys(%errors));
+
+#
+# Now do special checks.
+#
+my $query_result;
+my $node_type = $editnodetype_args{'node_type'};
+my $new_type = exists($editnodetype_args{"new_type"});
+
+#
+# Check whether the node type already exists.
+#
+$query_result =
+    DBQueryFatal("select * from node_types where type='$node_type'");
+my $node_type_exists = $query_result->numrows;
+my $prev_nodetype_data;
+if ($new_type) {
+
+    # Found.  But it's supposed to be new.
+    UserError("New NodeType: $node_type is already used!")
+	if ($node_type_exists);
+}
+else {
+    # Not found, but it was supposed to be old.
+    UserError("NodeType: $node_type is not a valid nodetype!")
+	if (!$node_type_exists);
+
+    # Found an existing one, grab its data.
+    $prev_nodetype_data = $query_result->fetchrow_hashref();
+}
+
+#
+# Check attributes of the node type, building an insert list as we go.
+#
+my @nodetype_data;
+
+# First check fixed (non-attr*) attributes that are in the node_types table.
+# Class may only be set while making a new nodetype.
+my $class;
+if (exists($editnodetype_args{"class"})) {
+    my $newclass = $editnodetype_args{"class"};
+    if ($new_type) {
+	$class = $newclass;
+	if ($class eq "") {
+	    $class = "pc";	# Default to pc class.
+	}
+	push(@nodetype_data, "class='$class'");
+    }
+    else {
+	$class = $prev_nodetype_data->{"class"};
+
+	# It's okay to specify it to be the same as it was before.
+	UserError("NodeType: Can't change class ($class) of existing node.")
+	    if ($class ne $newclass);
+    }
+}
+
+# The rest of them all have names starting with "is" at present.
+my @fixed_args   = grep(/^is/, keys(%editnodetype_args));
+foreach my $name (@fixed_args) {
+    if (exists($editnodetype_args{$name})) {
+	my $value = $editnodetype_args{$name};
+	push(@nodetype_data, "$name='$value'");
+    }
+}
+# Needed below.
+my $isremotenode = exists($editnodetype_args{"isremotenode"}) ?
+    $editnodetype_args{"isremotenode"} : 
+    $prev_nodetype_data->{"isremotenode"};
+
+# Get previous dynamic attrs from the node_type_attributes table.
+$query_result =
+    DBQueryFatal("select * from node_type_attributes ".
+		 "where type='$node_type'");
+my $prev_attrs = $query_result->fetchall_hashref("attrkey");
+
+# Dynamic attributes to be changed or deleted.  Possibly one new one added.
+my $new_attr_name = "";
+if (my $new_attr = exists($editnodetype_args{'new_attr'})) {
+    $new_attr_name = $editnodetype_args{'new_attr'};
+
+    # The new attr must not already exist.
+    UserError("New NodeType Attr: $new_attr_name is already used!")
+	if (exists($prev_attrs->{$new_attr_name}));
+}
+
+# Get lists of ids for checking the special "attr_*_*id" attributes.
+$query_result =
+    DBQueryFatal("select osid,osname,pid from os_info ".
+		 "where (path='' or path is NULL) ".
+		 "order by pid,osname");
+my $osids = $query_result->fetchall_hashref("osid");
+$query_result =
+    DBQueryFatal("select osid,osname,pid from os_info ".
+		 "where (path is not NULL and path!='') ".
+		 "order by pid,osname");
+my $mfsosids = $query_result->fetchall_hashref("osid");
+$query_result =
+    DBQueryFatal("select imageid,imagename,pid from images ".
+		 "order by pid,imagename");
+my $imageids = $query_result->fetchall_hashref("imageid");
+
+# Separate out the attr types and names from the other argument keys.
+my ($attr_name, $attr_type, $attr_value);
+my (@attr_names, %attr_types, %attr_values, %attr_dels);
+foreach my $argkey (keys(%editnodetype_args)) {
+    next
+	if (!($argkey =~ /^attr_/));
+
+    $attr_name = $attr_type = $argkey;
+    $attr_name =~ s/^attr_${wordpat}_(.*)$/$1/;
+    if ($argkey =~ /_(os|image)id$/) {
+	# Special case: the type is the LAST part of the name for ID attrs.
+	$attr_type = $1;
+    }
+    else {
+	# Normal ones are like "attr_type_name".
+	$attr_type =~ s/^attr_($wordpat)_.*$/$1/;
+    }
+    $attr_value = $editnodetype_args{$argkey};
+
+    if ($debug) {
+	print STDERR "Dynamic attr: $attr_name($attr_type) = '$attr_value'\n";
+    }
+
+    push(@attr_names, $attr_name);
+    $attr_types{$attr_name} = $attr_type;
+    $attr_values{$attr_name} = $attr_value;
+}
+
+# Check all of the dynamic attrs that are to be set.
+foreach $attr_name (@attr_names) {
+    
+    # Skip checks on attrs that are scheduled for deletion anyway.
+    my $del = $attr_dels{$attr_name} = 
+	exists($editnodetype_args{"delete_${attr_name}"}) &&
+	    $editnodetype_args{"delete_${attr_name}"} eq "1";
+    next
+	if $del;
+
+    # An attr to be set must pre-exist, unless it's the new attr.
+    UserError("NodeType Attr: $attr_name is not set in nodetype $node_type!")
+	if (!exists($prev_attrs->{$attr_name}) && 
+	    $attr_name ne $new_attr_name);
+
+    $attr_type = $attr_types{$attr_name};
+    $attr_value = $attr_values{$attr_name};
+
+    # Check the osid and imageid attribute values against the id lists.
+    # Under the web page interface, these come to us from selectors.
+    if ($attr_type eq "osid") {
+	if ($attr_name =~ /mfs$/) {
+	    UserError("NodeType MFS OSID Attr: $attr_name is not an mfs_osid.")
+		if (!exists($mfsosids->{$attr_value}));
+	}
+	else {
+	    UserError("NodeType OSID Attr: $attr_name is not an osid.")
+		if (!exists($osids->{$attr_value}));
+	}
+    }
+    elsif ($attr_type eq "imageid") {
+	UserError("NodeType Image ID Attr: $attr_name is not an imageid.")
+	    if (!exists($imageids->{$attr_value}));
+    }
+}
+
+exit(0)
+    if ($verify);
+
+#
+# Now safe to put the nodetype info into the DB.
+#
+my ($type, $value);
+if ($new_type) {
+    DBQueryFatal("insert into node_types set type='$node_type', ".
+		 join(",", @nodetype_data));
+
+    if ($class eq "pc" || $isremotenode eq "1") {
+	my $vnode_type = $node_type;
+	$vnode_type =~ s/pc/pcvm/;
+	if ($vnode_type eq $node_type) {
+	    $vnode_type = "$vnode_type-vm";
+	}
+	my $pcvmtype = ($isremotenode eq "1" ? "pcvwa" : "pcvm");
+
+	DBQueryFatal("insert into node_types_auxtypes set " .
+		     "  auxtype='$vnode_type', type='$pcvmtype'");
+    }
+
+    foreach $attr_name (@attr_names) {
+	# Skip adding an attr if it is also scheduled for deletion.
+	next
+	    if ($attr_dels{$attr_name});
+
+	$key   = escapeshellarg($attr_name);
+	$type  = escapeshellarg($attr_types{$attr_name});
+	$value = escapeshellarg($attr_values{$attr_name});
+
+	DBQueryFatal("insert into node_type_attributes set ".
+		     "   type='$node_type', ".
+		     "   attrkey='$key', attrtype='$type', ".
+		     "   attrvalue='$value' ");
+    }
+}
+else {
+    DBQueryFatal("update node_types set ".
+		 join(",", @nodetype_data) . " ".
+		 "where type='$node_type'");
+
+    foreach $attr_name (@attr_names) {
+	$key   = escapeshellarg($attr_name);
+	$type  = escapeshellarg($attr_types{$attr_name});
+	$value = escapeshellarg($attr_values{$attr_name});
+
+	# Remove an attr from the DB if scheduled for deletion.
+	if ($attr_dels{$attr_name}) {
+	    DBQueryFatal("delete from node_type_attributes ".
+			 "where type='$node_type' and attrkey='$key'");
+	}
+	else {
+	    DBQueryFatal("replace into node_type_attributes set ".
+			 "   type='$node_type', ".
+			 "   attrkey='$key', attrtype='$type', ".
+			 "   attrvalue='$value' ");
+	}
+    }
+}
+
+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 bd60efa9b12db5a44ba03d3ed38a9ca5799229ba..725c8ead1e0cd4d4b92a30ddc561c62e6c5c4946 100755
--- a/configure
+++ b/configure
@@ -2428,7 +2428,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
 	account/addpubkey account/addsfskey account/genpubkeys \
 	account/quotamail account/mkusercert account/newproj account/newuser \
 	backend/GNUmakefile backend/moduserinfo backend/newgroup \
-	backend/newmmlist backend/editexp backend/editimageid \
+	backend/newmmlist backend/editexp backend/editimageid backend/editnodetype \
 	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 6436cb5f669cdb23511253c56cb55d638fe6b860..78230730aa97bdefa0049cea66df719341ea48cc 100755
--- a/configure.in
+++ b/configure.in
@@ -808,7 +808,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
 	account/addpubkey account/addsfskey account/genpubkeys \
 	account/quotamail account/mkusercert account/newproj account/newuser \
 	backend/GNUmakefile backend/moduserinfo backend/newgroup \
-	backend/newmmlist backend/editexp backend/editimageid \
+	backend/newmmlist backend/editexp backend/editimageid backend/editnodetype \
 	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/sql/database-fill.sql b/sql/database-fill.sql
index 3af4b603b36e7faef0d9d54b854fd92de11e394a..67e41d8c849fc83855beaa4350656e1a4ce7e7c1 100644
--- a/sql/database-fill.sql
+++ b/sql/database-fill.sql
@@ -798,6 +798,18 @@ REPLACE INTO table_regex VALUES ('images','osid','text','redirect','os_info:osid
 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);
 
+REPLACE INTO table_regex VALUES ('node_types','new_type','text','redirect','default:tinytext',0,0,NULL);
+REPLACE INTO table_regex VALUES ('node_types','node_type','text','regex','^[-\\w]+$',1,30,NULL);
+REPLACE INTO table_regex VALUES ('node_types','class','text','regex','^[\\w]+$',1,30,NULL);
+REPLACE INTO table_regex VALUES ('node_types','boolean','text','redirect','default:boolean',0,0,NULL);
+REPLACE INTO table_regex VALUES ('node_types','attr_name','text','regex','^[-\\w]+$',1,32,NULL);
+REPLACE INTO table_regex VALUES ('node_types','attr_osid','text','redirect','os_info:osid',0,0,NULL);
+REPLACE INTO table_regex VALUES ('node_types','attr_imageid','text','redirect','images:imageid',0,0,NULL);
+REPLACE INTO table_regex VALUES ('node_types','attr_boolean','text','redirect','default:boolean',0,0,NULL);
+REPLACE INTO table_regex VALUES ('node_types','attr_integer','text','redirect','default:int',0,0,NULL);
+REPLACE INTO table_regex VALUES ('node_types','attr_float','text','redirect','default:float',0,0,NULL);
+REPLACE INTO table_regex VALUES ('node_types','attr_string','text','redirect','default:tinytext',0,0,NULL);
+
 REPLACE INTO table_regex VALUES ('experiments','security_level','int','redirect','default:tinyuint',0,4,NULL);
 REPLACE INTO table_regex VALUES ('experiments','elabinelab_eid','text','redirect','experiments:eid',0,0,NULL);
 REPLACE INTO table_regex VALUES ('virt_node_startloc','pid','text','redirect','projects:pid',0,0,NULL);
diff --git a/www/editnodetype.php3 b/www/editnodetype.php3
index 0cc78d6f1dda3cb3ddc31c435e0746dbc6f54f35..5cdc26a70857a2a1a3bd3e80a1404225328f7e7b 100644
--- a/www/editnodetype.php3
+++ b/www/editnodetype.php3
@@ -23,9 +23,14 @@ if (! $isadmin) {
 }
 
 $optargs = OptionalPageArguments("submit",     PAGEARG_STRING,
-				 "node_type",  PAGEARG_STRING,
-				 "new_type",   PAGEARG_STRING,
 				 "formfields", PAGEARG_ARRAY,
+
+				 # Send new_type=1 to create new nodetype.
+				 "new_type",   PAGEARG_STRING,
+				 # Optional if new_type, required if not.
+				 "node_type",  PAGEARG_STRING,
+
+				 # Attribute creation and deletion.
 				 "deletes",    PAGEARG_ARRAY,
 				 "attributes", PAGEARG_ARRAY,
 				 "newattribute_type",  PAGEARG_STRING,
@@ -249,6 +254,10 @@ function SPITFORM($node_type, $formfields, $attributes, $deletes, $errors)
            <td align=center colspan=2><b>Node Attributes</b></td></tr>\n";
 
     while (list ($key, $val) = each ($attributes)) {
+	if (!isset($deletes[$key])) {
+	    # Somehow this doesn't get initialized in the Create Node case.
+	    $deletes[$key] = "";
+	}
 	if ($key == "default_osid" ||
 	    $key == "jail_osid" ||
 	    $key == "delay_osid") {
@@ -385,7 +394,7 @@ else {
 }
 
 #
-# We need a list of osids and imageids.
+# We need lists of osids and imageids for selection.
 #
 $osid_result =
     DBQueryFatal("select osid,osname,pid from os_info ".
@@ -419,251 +428,212 @@ if (!isset($new_type)) {
 }
 
 #
-# Otherwise, must validate and redisplay if errors. Build up a DB insert
-# string as we go. 
+# Otherwise, must validate and redisplay if errors.
 #
 $errors  = array();
-$inserts = array();
 
-# Class (only for new types)
-if (isset($new_type) &&
-    isset($formfields['class']) && $formfields['class'] != "") {
-    if (! TBvalid_description($formfields['class'])) {
-	$errors["Class"] = TBFieldErrorString();
+# Check the attributes.
+while (list ($key, $val) = each ($attributes)) {
+    # Skip checks if scheduled for deletion
+    if (isset($deletes[$key]) && $deletes[$key] == "checked") 
+	continue;
+
+    if (!isset($attribute_types[$key])) {
+	$errors[$key] = "Unknown Attribute";
+	continue;
     }
-    else {
-	$inserts["class"] = addslashes($formfields["class"]);
+    
+    if ($val == "") {
+	$errors[$key] = "No value provided for $key";
+	continue;
+    }
+
+    # Probably redundant with the XML keyfields checking...
+    $attrtype = $attribute_types[$key];
+    if ($attrtype == "") {	# Shouldn't happen...
+	$attrtype = $attribute_types[$key] = "integer";
+    }
+    if (strpos(":boolean:float:integer:string:", ":$attrtype:")===FALSE) {
+	$errors[$key] = "Invalid type information: $attrtype";
+	continue;
+    }
+
+    # New attributes require type and value.
+    if (isset($newattribute_name) && $newattribute_name != "" &&
+	!(isset($newattribute_type) && $newattribute_type != "")) {
+	$errors[$newattribute_name] = "Missing type";
+    }
+    if (isset($newattribute_name) && $newattribute_name != "" &&
+	!(isset($newattribute_value) && $newattribute_value != "")) {
+	$errors[$newattribute_name] = "Missing value";
     }
 }
 
+#
+# If any errors, respit the form with the current values and the
+# error messages displayed. Iterate until happy.
+# 
+if (count($errors)) {
+    SPITFORM($node_type, $formfields, $attributes, $deletes, $errors);
+    PAGEFOOTER();
+    return;
+}
+
+#
+# Build up argument array to pass along.
+#
+$args = array();
+
+# Class (only for new types.)
+if (isset($new_type) &&
+    isset($formfields['class']) && $formfields['class'] != "") {
+    $args["new_type"] = "1";
+    $args["class"] = $formfields["class"];
+}
+
 # isvirtnode
 if (isset($formfields["isvirtnode"]) && $formfields["isvirtnode"] != "") {
-    if (! TBvalid_boolean($formfields["isvirtnode"])) {
-	$errors["isvirtnode"] = TBFieldErrorString();
-    }
-    else {
-	$inserts["isvirtnode"] = $formfields["isvirtnode"];
-    }
+    $args["isvirtnode"] = $formfields["isvirtnode"];
 }
 
 # isjailed
 if (isset($formfields["isjailed"]) && $formfields["isjailed"] != "") {
-    if (! TBvalid_boolean($formfields["isjailed"])) {
-	$errors["isjailed"] = TBFieldErrorString();
-    }
-    else {
-	$inserts["isjailed"] = $formfields["isjailed"];
-    }
+    $args["isjailed"] = $formfields["isjailed"];
 }
 
 # isdynamic
 if (isset($formfields["isdynamic"]) && $formfields["isdynamic"] != "") {
-    if (! TBvalid_boolean($formfields["isdynamic"])) {
-	$errors["isdynamic"] = TBFieldErrorString();
-    }
-    else {
-	$inserts["isdynamic"] = $formfields["isdynamic"];
-    }
+    $args["isdynamic"] = $formfields["isdynamic"];
 }
 
 # isremotenode
 if (isset($formfields["isremotenode"]) && $formfields["isremotenode"] != "") {
-    if (! TBvalid_boolean($formfields["isremotenode"])) {
-	$errors["isremotenode"] = TBFieldErrorString();
-    }
-    else {
-	$inserts["isremotenode"] = $formfields["isremotenode"];
-    }
+    $args["isremotenode"] = $formfields["isremotenode"];
 }
 
 # issubnode
 if (isset($formfields["issubnode"]) && $formfields["issubnode"] != "") {
-    if (! TBvalid_boolean($formfields["issubnode"])) {
-	$errors["issubnode"] = TBFieldErrorString();
-    }
-    else {
-	$inserts["issubnode"] = $formfields["issubnode"];
-    }
+    $args["issubnode"] = $formfields["issubnode"];
 }
 
 # isplabdslice
 if (isset($formfields["isplabdslice"]) && $formfields["isplabdslice"] != "") {
-    if (! TBvalid_boolean($formfields["isplabdslice"])) {
-	$errors["isplabdslice"] = TBFieldErrorString();
-    }
-    else {
-	$inserts["isplabdslice"] = $formfields["isplabdslice"];
-    }
+    $args["isplabdslice"] = $formfields["isplabdslice"];
 }
 
 # issimnode
 if (isset($formfields["issimnode"]) && $formfields["issimnode"] != "") {
-    if (! TBvalid_boolean($formfields["issimnode"])) {
-	$errors["issimnode"] = TBFieldErrorString();
-    }
-    else {
-	$inserts["issimnode"] = $formfields["issimnode"];
-    }
+    $args["issimnode"] = $formfields["issimnode"];
 }
 
-# Check the attributes
-while (list ($key, $val) = each ($attributes)) {
-    # Skip checks if scheduled for deletion
-    if (isset($deletes[$key]) && $deletes[$key] == "checked") 
-	continue;
-
-    if (!isset($attribute_types[$key])) {
-	$errors[$key] = "Unknown Attribute";
-	continue;
-    }
-    
-    $attrtype = $attribute_types[$key];
-    $valid    = 1;
-
-    if ($val == "") {
-	$errors[$key] = "No value provided for $key";
-	continue;
-    }
-    elseif ($attrtype == "boolean") {
-	$valid = TBvalid_boolean($val);
-    }
-    elseif ($attrtype == "float") {
-	$valid = TBvalid_float($val);
-    }
-    elseif ($attrtype == "integer") {
-	$valid = TBvalid_integer($val);
-    }
-    elseif ($attrtype == "string") {
-	$valid = TBvalid_description($val);
-    }
-    else {
-	$errors[$key] = "Invalid type information: $attrtype";
-	continue;
-    }
-    if (!$valid) {
-	$errors[$key] = TBFieldErrorString();
-    }
+# Existing attributes.
+foreach ($attributes as $attr_key => $attr_val) {
+    if (isset($deletes[$attr_key]) && $deletes[$attr_key] == "checked") 
+	$args["delete_${attr_key}"] = "1";
+    $attr_type = $attribute_types[$attr_key];
+    $args["attr_${attr_type}_${attr_key}"] = $attr_val;
 }
 
 #
-# Spit any errors now.
+# Form allows for adding a single new attribute, but someday be more fancy.
 #
-if (count($errors)) {
+if (isset($newattribute_name) && $newattribute_name != "" &&
+    isset($newattribute_value) && $newattribute_value != "" &&
+    isset($newattribute_type) && $newattribute_type != "") {
+
+    $args["new_attr"] = $newattribute_name;
+    # The following is matched by wildcards on the other side of XML,
+    # including checking its type and value, just like existing attributes.
+    $args["attr_${newattribute_type}_$newattribute_name"] = $newattribute_value;
+}
+
+if (! ($result = SetNodeType($node_type, $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($node_type, $formfields, $attributes, $deletes, $errors);
     PAGEFOOTER();
     return;
 }
 
+PAGEHEADER(isset($new_type) ? "Create" : "Edit" . "Node Type");
+
 #
-# Form allows for a single new attribute, but someday be more fancy.
+# Spit out a redirect so that the history does not include a post
+# in it. The back button skips over the post and to the form.
 #
-if (isset($newattribute_name) && $newattribute_name != "" &&
-    isset($newattribute_value) && $newattribute_value != "" &&
-    isset($newattribute_type) && $newattribute_type != "") {
+PAGEREPLACE("editnodetype.php3?node_type=$node_type");
 
-    if (!preg_match("/^[-\w]+$/", $newattribute_name)) {
-	$errors["New Attribute Name"] = "Invalid characters in attribute name";
-    }
-    else {
-	$valid    = 1;
-
-	if ($newattribute_type == "boolean") {
-	    $valid = TBvalid_boolean($newattribute_value);
-	}
-	elseif ($newattribute_type == "float") {
-	    $valid = TBvalid_float($newattribute_value);
-	}
-	elseif ($newattribute_type == "integer") {
-	    $valid = TBvalid_integer($newattribute_value);
-	}
-	elseif ($newattribute_type == "string") {
-	    $valid = TBvalid_description($newattribute_value);
-	}
-	else {
-	    $errors["New Attribute Type"] = "Invalid type: $newattribute_type";
-	}
-	if (!$valid) {
-	    $errors["New Attribute Type"] = TBFieldErrorString();
-	}
-    }
-
-    #
-    # Spit any errors now.
-    #
-    if (count($errors)) {
-	SPITFORM($node_type, $formfields, $attributes, $deletes, $errors);
-	PAGEFOOTER();
-	return;
-    }
-    # Set up for loops below.
-    $attributes[$newattribute_name] = $newattribute_value;
-    $attribute_types[$newattribute_name] = $newattribute_type;
-}
+#
+# Standard Testbed Footer
+# 
+PAGEFOOTER();
 
 #
-# Otherwise, do the inserts.
+# Create or edit a nodetype.  (No class for that at present.)
 #
-$insert_data = array();
-foreach ($inserts as $name => $value) {
-    $insert_data[] = "$name='$value'";
-}
+function SetNodeType($node_type, $args, &$errors) {
+    global $suexec_output, $suexec_output_array;
 
-if (isset($new_type)) {
-    DBQueryFatal("insert into node_types set type='$node_type', ".
-		 implode(",", $insert_data));
-    if ($formfields["class"] == "pc" || $formfields["isremotenode"] == 1) {
-	$vnode_type = $node_type;
-	$vnode_type = preg_replace("/pc/","pcvm",$vnode_type);
-	if ($vnode_type == $node_type) {
-	    $vnode_type = "$vnode_type-vm";
-	}
-	$pcvmtype = ($formfields["isremotenode"] == 1 ? "pcvwa" : "pcvm");
-	
-	DBQueryFatal("insert into node_types_auxtypes set " .
-		     "  auxtype='$vnode_type', type='$pcvmtype'");
+    #
+    # Generate a temporary file and write in the XML goo.
+    #
+    $xmlname = tempnam("/tmp", "editnodetype");
+    if (! $xmlname) {
+	TBERROR("Could not create temporary filename", 0);
+	$errors[] = "Transient error(1); please try again later.";
+	return null;
     }
-    foreach ($attributes as $key => $value) {
-        # Skip if scheduled for deletion
-	if (isset($deletes[$key]) && $deletes[$key] == "checked") 
-	    continue;
-	
-	$key   = addslashes($key);
-	$type  = addslashes($attribute_types[$key]);
-	$value = addslashes($value);
-	
-	DBQueryFatal("insert into node_type_attributes set ".
-		     "   type='$node_type', ".
-		     "   attrkey='$key', attrtype='$type', ".
-		     "   attrvalue='$value' ");
+    if (! ($fp = fopen($xmlname, "w"))) {
+	TBERROR("Could not open temp file $xmlname", 0);
+	$errors[] = "Transient error(2); please try again later.";
+	return null;
     }
-} else {
-    DBQueryFatal("update node_types set ".
-		 implode(",", $insert_data) . " ".
-		 "where type='$node_type'");
-
-    foreach ($attributes as $key => $value) {
-	$key   = addslashes($key);
-	$type  = addslashes($attribute_types[$key]);
-	$value = addslashes($value);
-
-        # Remove if scheduled for deletion
-	if (isset($deletes[$key]) && $deletes[$key] == "checked") {
-	    DBQueryFatal("delete from node_type_attributes ".
-			 "where type='$node_type' and attrkey='$key'");
+
+    # Add these. Maybe caller should do this?
+    $args["node_type"] = $node_type;
+    
+    fwrite($fp, "<nodetype>\n");
+    foreach ($args as $name => $value) {
+	fwrite($fp, "<attribute name=\"$name\">");
+	fwrite($fp, "  <value>" . htmlspecialchars($value) . "</value>");
+	fwrite($fp, "</attribute>\n");
+    }
+    fwrite($fp, "</nodetype>\n");
+    fclose($fp);
+    chmod($xmlname, 0666);
+
+    $retval = SUEXEC("nobody", "nobody", "webeditnodetype $xmlname",
+		     SUEXEC_ACTION_IGNORE);
+
+    if ($retval) {
+	if ($retval < 0) {
+	    $errors[] = "Transient error(3); please try again later.";
+	    SUEXECERROR(SUEXEC_ACTION_CONTINUE);
 	}
 	else {
-	    DBQueryFatal("replace into node_type_attributes set ".
-			 "   type='$node_type', ".
-			 "   attrkey='$key', attrtype='$type', ".
-			 "   attrvalue='$value' ");
+	    # 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;
     }
-}
+    # There are no return value(s) to parse at the end of the output.
 
-#
-# Spit out a redirect so that the history does not include a post
-# in it. The back button skips over the post and to the form.
-#
-header("Location: editnodetype.php3?node_type=$node_type");
+    # Unlink this here, so that the file is left behind in case of error.
+    # We can then create the nodetype by hand from the xmlfile, if desired.
+    unlink($xmlname);
+    return true;
+}
 
 ?>