Commit 77540494 authored by Leigh Stoller's avatar Leigh Stoller

Rework the newosid web page as an example of how I want all of our current

form processing to be done.

The gist is that I have moved all of the data checking and DB work to
the backend into a new script called utils/newosid. This script does
all the field checking that used to be done in php. It takes a simple
XML file as input and returns a set of strings to format as errors (if
there are any).

The overall goal to make a big push to move this code out of PHP and
perl.  A nice side effect is that many operations that are current
only available via the web interface will also become available
command line (and also XMLRPC with a little moew work).
parent 1d9b8885
#!/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;
#
# Create a new osid from a XML description.
#
sub usage()
{
print("Usage: newosid [-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(;$);
#
# 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 create new osids")
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) will
# sets this.
$this_user = User->ImpliedUser();
if (! defined($this_user)) {
fatal("Cannot determind 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
("description" => ["description", $SLOT_REQUIRED],
"osname" => ["osname" , $SLOT_REQUIRED],
"project" => ["pid", $SLOT_REQUIRED],
"OS" => ["OS", $SLOT_REQUIRED],
"version" => ["version", $SLOT_OPTIONAL, ""],
"path" => ["path", $SLOT_OPTIONAL, "NULL"],
"magic", => ["magic", $SLOT_OPTIONAL, ""],
"op_mode", => ["op_mode", $SLOT_REQUIRED],
"features", => ["osfeatures", $SLOT_OPTIONAL, ""],
"shared", => ["shared", $SLOT_ADMINONLY, 0],
"mustclean", => ["mustclean", $SLOT_ADMINONLY, 1],
"nextosid", => ["nextosid", $SLOT_ADMINONLY],
"reboot_waittime", => ["reboot_waittime", $SLOT_ADMINONLY]);
#
# 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.
#
foreach my $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 OSinfo->Create() as we check
# the attributes.
#
my %newosid_args = ();
foreach my $key (keys(%{ $xmlparse->{'attribute'} })) {
my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'};
if ($debug) {
print STDERR "User attribute: '$key' -> '$value'\n";
}
$errors{$key} = "Unknown attribute"
if (!exists($xmlfields{$key}));
my ($dbslot, $required, $default) = @{$xmlfields{$key}};
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, "os_info", $dbslot, TBDB_CHECKDBSLOT_ERROR)) {
$errors{$key} = TBFieldErrorString();
next;
}
$newosid_args{$dbslot} = $value;
}
UserError()
if (keys(%errors));
#
# Now do special checks.
#
my $project = Project->Lookup($newosid_args{"pid"});
if (!defined($project)) {
UserError("Project: No such project");
}
if (!$project->AccessCheck($this_user, TB_PROJECT_MAKEOSID())) {
UserError("Project: Not enough permission");
}
# OS must be in the allowed list.
if (! OSinfo->ValidOS($newosid_args{"OS"})) {
UserError("OS: Invalid");
}
# Ditto the opmode.
if (! OSinfo->ValidOpMode($newosid_args{"op_mode"})) {
UserError("OpMode: Invalid");
}
# Nextosid check. Must exist. admin check done above.
if (exists($newosid_args{"nextosid"})) {
my $nextos = OSinfo->Lookup($newosid_args{"nextosid"});
if (!defined($nextos)) {
UserError("Nextosid: Does not exist");
}
}
# Mere users have to supply a version, but admin people do not.
if (! $this_user->IsAdmin() &&
(!exists($newosid_args{"version"}) || $newosid_args{"version"} eq "")) {
UserError("Version: Required value not provided");
}
# reboot waittime default value is not set by an admin user.
if (! exists($newosid_args{"reboot_waittime"})) {
$newosid_args{"reboot_waittime"} =
OSinfo->RebootWaitTime($newosid_args{"OS"});
}
exit(0)
if ($verify);
#
# Now safe to create the OSID.
#
# We pass the osname along as an argument to Create(), so remove it from
# the argument array.
#
my $osname = $newosid_args{"osname"};
delete($newosid_args{"osname"});
# Ditto the pid.
delete($newosid_args{"pid"});
my $new_osinfo = OSinfo->Create($project, $this_user, $osname, \%newosid_args);
if (!defined($new_osinfo)) {
fatal("Could not create new OSID!");
}
my $osid = $new_osinfo->osid();
# The web interface requires this line to be printed.
print "OSID $osname/$osid 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);
}
......@@ -448,7 +448,8 @@ sub AccessCheck($$$)
if ($access_type == TB_PROJECT_READINFO) {
$mintrust = PROJMEMBERTRUST_USER;
}
elsif ($access_type == TB_PROJECT_CREATEEXPT) {
elsif ($access_type == TB_PROJECT_CREATEEXPT ||
$access_type == TB_PROJECT_MAKEOSID) {
$mintrust = PROJMEMBERTRUST_LOCALROOT;
}
elsif ($access_type == TB_PROJECT_DELUSER) {
......
......@@ -34,8 +34,45 @@ my $TBBASE = "@TBBASE@";
my $TBWWW = "@TBWWW@";
# Cache of instances to avoid regenerating them.
my %osids = ();
my $debug = 0;
my %osids = ();
my $debug = 0;
# Valid features. Mirrored in the web interface. The value is a user-okay flag.
my %FEATURES = ( "ping" => 1,
"ssh", => 1,
"ipod" => 1,
"isup" => 1,
"veths" => 0,
"mlinks" => 0,
"linktest" => 1,
"linkdelays" => 0 );
# Valid OS names. Mirrored in the web interface. The value is a user-okay flag.
my %OSLIST = ( "Linux" => 1,
"Fedora" => 1,
"FreeBSD" => 1,
"NetBSD" => 1,
"Windows" => 1,
"TinyOS" => 1,
"Oskit" => 0,
"Other" => 1 );
# Default OSID boot wait timeouts in seconds. Mirrored in the web interface.
my %WAITTIMES = ("Linux" => 120,
"Fedora" => 120,
"FreeBSD" => 120,
"NetBSD" => 120,
"Windows" => 240,
"TinyOS" => 60,
"Oskit" => 60,
"Other" => 60 );
# OP modes. Mirrored in the web interface. The value is a user-okay flag.
my %OPMODES = ("NORMALv2" => 1,
"NORMALv1" => 0,
"MINIMAL" => 1,
"NORMAL" => 1,
"ALWAYSUP" => 1 );
# Little helper and debug function.
sub mysystem($)
......@@ -136,6 +173,82 @@ sub max_concurrent($) { return field($_[0], "max_concurrent"); }
sub mfs($) { return field($_[0], "mfs"); }
sub reboot_waittime($) { return field($_[0], "reboot_waittime"); }
#
# Create a new experiment. This installs the new record in the DB,
# and returns an instance. There is some bookkeeping along the way.
#
sub Create($$$$)
{
my ($class, $project, $creator, $osname, $argref) = @_;
my $idx;
my $now = time();
return undef
if (ref($class) || !ref($project));
my $pid = $project->pid();
my $pid_idx = $project->pid_idx();
my $uid = $creator->uid();
my $uid_idx = $creator->uid_idx();
#
# The pid/osid has to be unique, so lock the table for the check/insert.
#
DBQueryWarn("lock tables os_info write, emulab_indicies write")
or return undef;
my $query_result =
DBQueryWarn("select osname from os_info ".
"where pid_idx='$pid_idx' and osname='$osname'");
if ($query_result->numrows) {
DBQueryWarn("unlock tables");
tberror("OS $osname in project $pid already exists!");
return undef;
}
#
# Grab unique ID. Table already locked.
#
my $osid = TBGetUniqueIndex("next_osid", undef, 1);
my $uuid = NewUUID();
my $desc = "''";
my $magic = "''";
# Some fields special cause of quoting.
#
if (exists($argref->{'description'})) {
$desc = DBQuoteSpecial($argref->{'description'});
delete($argref->{'description'});
}
if (exists($argref->{'magic'})) {
$magic = DBQuoteSpecial($argref->{'magic'});
delete($argref->{'magic'});
}
my $query = "insert into os_info set ".
join(",", map("$_='" . $argref->{$_} . "'", keys(%{$argref})));
# Append the rest
$query .= ",osname='$osname'";
$query .= ",osid='$osid'";
$query .= ",uuid='$uuid'";
$query .= ",pid='$pid',pid_idx='$pid_idx'";
$query .= ",creator='$uid',creator_idx='$uid_idx'";
$query .= ",created=now()";
$query .= ",description=$desc";
$query .= ",magic=$magic";
if (! DBQueryWarn($query)) {
DBQueryWarn("unlock tables");
tberror("Error inserting new os_info record for $pid/$osname!");
return undef;
}
DBQueryWarn("unlock tables");
return OSinfo->Lookup($osid);
}
#
# Refresh a class instance by reloading from the DB.
#
......@@ -265,5 +378,38 @@ sub AccessCheck($$$)
return TBMinTrust($project->Trust($user), $mintrust);
}
#
# Class method to get the default reboot time for an os type.
#
sub RebootWaitTime($$)
{
my ($self, $os) = @_;
return $WAITTIMES{"other"}
if (!exists($WAITTIMES{$os}));
return $WAITTIMES{$os};
}
#
# Class method to check the OS is legal.
#
sub ValidOS($$)
{
my ($self, $os) = @_;
return (exists($OSLIST{$os}) ? 1 : 0);
}
#
# Class method to check the OPmode is legal.
#
sub ValidOpMode($$)
{
my ($self, $opmode) = @_;
return (exists($OPMODES{$opmode}) ? 1 : 0);
}
# _Always_ make sure that this 1 is at the end of the file...
1;
......@@ -571,7 +571,6 @@ REPLACE INTO table_regex VALUES ('nseconfigs','pid','text','redirect','projects:
REPLACE INTO table_regex VALUES ('nseconfigs','eid','text','redirect','experiments:eid',0,0,NULL);
REPLACE INTO table_regex VALUES ('nseconfigs','vname','text','redirect','virt_nodes:vname',0,0,NULL);
REPLACE INTO table_regex VALUES ('nseconfigs','nseconfig','text','regex','^[\\040-\\176\\012\\011\\015]*$',0,16777215,NULL);
REPLACE INTO table_regex VALUES ('os_info','osname','text','regex','^[-\\w\\.+]+$',2,20,NULL);
REPLACE INTO table_regex VALUES ('projects','newpid','text','regex','^[a-zA-Z][-a-zA-Z0-9]+$',2,12,NULL);
REPLACE INTO table_regex VALUES ('projects','head_uid','text','redirect','users:uid',0,0,NULL);
REPLACE INTO table_regex VALUES ('projects','name','text','redirect','default:tinytext',0,256,NULL);
......@@ -714,7 +713,6 @@ REPLACE INTO table_regex VALUES ('experiments','jail_osname','text','redirect','
REPLACE INTO table_regex VALUES ('experiments','delay_osname','text','redirect','os_info:osname',0,0,NULL);
REPLACE INTO table_regex VALUES ('experiments','use_ipassign','int','redirect','default:boolean',0,0,NULL);
REPLACE INTO table_regex VALUES ('experiments','ipassign_args','text','regex','^[\\w\\s-]*$',0,255,NULL);
REPLACE INTO table_regex VALUES ('os_info','osid','text','regex','^[-\\w\\.+]+$',2,35,NULL);
REPLACE INTO table_regex VALUES ('experiments','expt_name','text','redirect','default:tinytext',1,255,NULL);
REPLACE INTO table_regex VALUES ('experiments','noswap_reason','text','redirect','default:tinytext',1,255,NULL);
REPLACE INTO table_regex VALUES ('experiments','noidleswap_reason','text','redirect','default:tinytext',1,255,NULL);
......@@ -828,6 +826,20 @@ REPLACE INTO table_regex VALUES ('virt_node_motelog','logfileid','text','regex',
REPLACE INTO table_regex VALUES ('virt_node_motelog','pid','text','redirect','projects:pid',0,0,NULL);
REPLACE INTO table_regex VALUES ('virt_node_motelog','eid','text','redirect','experiments:eid',0,0,NULL);
REPLACE INTO `table_regex` VALUES ('virt_nodes','plab_plcnet','text','regex','^[\\w\\_\\d]+$',0,0,NULL);
REPLACE INTO table_regex VALUES ('os_info','osid','text','regex','^[-\\w\\.+]+$',2,35,NULL);
REPLACE INTO table_regex VALUES ('os_info','pid','text','redirect','projects:pid',0,0,NULL);
REPLACE INTO table_regex VALUES ('os_info','osname','text','regex','^[-\\w\\.+]+$',2,20,NULL);
REPLACE INTO table_regex VALUES ('os_info','description','text','regex','^[\\040-\\176\\012\\015\\011]*$',1,256,NULL);
REPLACE INTO table_regex VALUES ('os_info','OS','text','regex','^[-\\w]*$',1,32,NULL);
REPLACE INTO table_regex VALUES ('os_info','version','text','regex','^[-\\w\\.]*$',1,12,NULL);
REPLACE INTO table_regex VALUES ('os_info','path','text','regex','^[-\\w\\.\\/:]*$',1,256,NULL);
REPLACE INTO table_regex VALUES ('os_info','magic','text','redirect','default:tinytext',0,256,NULL);
REPLACE INTO table_regex VALUES ('os_info','shared','int','redirect','default:tinyint',0,1,NULL);
REPLACE INTO table_regex VALUES ('os_info','mustclean','int','redirect','default:tinyint',0,1,NULL);
REPLACE INTO table_regex VALUES ('os_info','osfeatures','text','regex','^[-\\w,]*$',1,128,NULL);
REPLACE INTO table_regex VALUES ('os_info','op_mode','text','regex','^[-\\w]*$',1,20,NULL);
REPLACE INTO table_regex VALUES ('os_info','nextosid','text','redirect','os_info:osid',0,0,NULL);
REPLACE INTO table_regex VALUES ('os_info','reboot_waittime','int','redirect','default:int',0,2000,NULL);
--
-- Dumping data for table `testsuite_preentables`
......
#!/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;
#
# Create a new osid from a XML description.
#
sub usage()
{
print("Usage: newosid [-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(;$);
#
# 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 create new osids")
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) will
# sets this.
$this_user = User->ImpliedUser();
if (! defined($this_user)) {
fatal("Cannot determind 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
("description" => ["description", $SLOT_REQUIRED],
"osname" => ["osname" , $SLOT_REQUIRED],
"project" => ["pid", $SLOT_REQUIRED],
"OS" => ["OS", $SLOT_REQUIRED],
"version" => ["version", $SLOT_OPTIONAL, ""],
"path" => ["path", $SLOT_OPTIONAL, "NULL"],
"magic", => ["magic", $SLOT_OPTIONAL, ""],
"op_mode", => ["op_mode", $SLOT_REQUIRED],
"features", => ["osfeatures", $SLOT_OPTIONAL, ""],
"shared", => ["shared", $SLOT_ADMINONLY, 0],
"mustclean", => ["mustclean", $SLOT_ADMINONLY, 1],
"nextosid", => ["nextosid", $SLOT_ADMINONLY],
"reboot_waittime", => ["reboot_waittime", $SLOT_ADMINONLY]);
#
# 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.
#
foreach my $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 OSinfo->Create() as we check
# the attributes.
#
my %newosid_args = ();
foreach my $key (keys(%{ $xmlparse->{'attribute'} })) {
my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'};
if ($debug) {
print STDERR "User attribute: '$key' -> '$value'\n";
}
$errors{$key} = "Unknown attribute"
if (!exists($xmlfields{$key}));
my ($dbslot, $required, $default) = @{$xmlfields{$key}};