Commit eb7fe19b authored by Leigh Stoller's avatar Leigh Stoller

First cut a new page to create new profiles, plus backend scripts

and libraries. Rough, needs plenty more work.
parent 72dc3d90
#!/usr/bin/perl -wT
#
# Copyright (c) 2007-2014 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
package APT_Profile;
use strict;
use Carp;
use Exporter;
use vars qw(@ISA @EXPORT $AUTOLOAD);
@ISA = "Exporter";
@EXPORT = qw ( );
# Must come after package declaration!
use EmulabConstants;
use emdb;
use libtestbed;
use English;
use Data::Dumper;
use overload ('""' => 'Stringify');
# Configure variables
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
# Cache of instances to avoid regenerating them.
my %profiles = ();
my $debug = 0;
#
# Lookup by idx or pid,name or uuid, depending on the args.
#
sub Lookup($$;$)
{
my ($class, $arg1, $arg2) = @_;
my $idx;
#
# A single arg is either an index or a "pid,name" or "pid/name" string.
#
if (!defined($arg2)) {
if ($arg1 =~ /^(\d*)$/) {
$idx = $1;
}
elsif ($arg1 =~ /^([-\w]*),([-\w\.\+]*)$/ ||
$arg1 =~ /^([-\w]*)\/([-\w\.\+]*)$/) {
$arg1 = $1;
$arg2 = $2;
}
elsif ($arg1 =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/) {
my $result =
DBQueryWarn("select idx from apt_profiles ".
"where uuid='$arg1'");
return undef
if (! $result || !$result->numrows);
($idx) = $result->fetchrow_array();
}
else {
return undef;
}
}
elsif (! (($arg1 =~ /^[-\w\.\+]*$/) && ($arg2 =~ /^[-\w\.\+]*$/))) {
return undef;
}
#
# Two args means pid/name lookup instead of idx.
#
if (defined($arg2)) {
my $result =
DBQueryWarn("select idx from apt_profiles ".
"where pid='$arg1' and name='$arg2'");
return undef
if (! $result || !$result->numrows);
($idx) = $result->fetchrow_array();
}
# Look in cache first
return $profiles{"$idx"}
if (exists($profiles{"$idx"}));
my $query_result =
DBQueryWarn("select * from apt_profiles where idx='$idx'");
return undef
if (!$query_result || !$query_result->numrows);
my $self = {};
$self->{'PROFILE'} = $query_result->fetchrow_hashref();
bless($self, $class);
# Add to cache.
$profiles{"$idx"} = $self;
return $self;
}
AUTOLOAD {
my $self = $_[0];
my $type = ref($self) or croak "$self is not an object";
my $name = $AUTOLOAD;
$name =~ s/.*://; # strip fully-qualified portion
# A DB row proxy method call.
if (exists($self->{'PROFILE'}->{$name})) {
return $self->{'PROFILE'}->{$name};
}
carp("No such slot '$name' field in class $type");
return undef;
}
# Break circular reference someplace to avoid exit errors.
sub DESTROY {
my $self = shift;
$self->{'PROFILE'} = undef;
}
#
# Refresh a class instance by reloading from the DB.
#
sub Refresh($)
{
my ($self) = @_;
return -1
if (! ref($self));
my $idx = $self->idx();
my $query_result =
DBQueryWarn("select * from apt_profiles where idx=$idx");
return -1
if (!$query_result || !$query_result->numrows);
$self->{'PROFILE'} = $query_result->fetchrow_hashref();
return 0;
}
#
# Create a profile
#
sub Create($$$$$)
{
my ($class, $project, $creator, $argref, $usrerr_ref) = @_;
my $name = DBQuoteSpecial($argref->{'name'});
my $pid = $project->pid();
my $pid_idx = $project->pid_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 apt_profiles write")
or return undef;
my $query_result =
DBQueryWarn("select name from apt_profiles ".
"where pid_idx='$pid_idx' and name=$name");
if ($query_result->numrows) {
DBQueryWarn("unlock tables");
$$usrerr_ref = "Profile already exists in project!";
return undef;
}
my $uuid = NewUUID();
my $desc = DBQuoteSpecial($argref->{'description'});
my $rspec = DBQuoteSpecial($argref->{'rspec'});
my $query = "insert into apt_profiles set created=now()";
# Append the rest
$query .= ",name=$name";
$query .= ",uuid='$uuid'";
$query .= ",pid='$pid',pid_idx='$pid_idx'";
$query .= ",creator='$uid',creator_idx='$uid_idx'";
$query .= ",description=$desc";
$query .= ",rspec=$rspec";
$query .= ",public=1"
if (exists($argref->{'public'}) && $argref->{'public'});
if (! DBQueryWarn($query)) {
DBQueryWarn("unlock tables");
tberror("Error inserting new apt_profile record for $pid/$name!");
return undef;
}
DBQueryWarn("unlock tables");
return Lookup($class, $pid, $argref->{'name'});
}
# _Always_ make sure that this 1 is at the end of the file...
1;
#
# Copyright (c) 2000-2014 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
SRCDIR = @srcdir@
TESTBED_SRCDIR = @top_srcdir@
OBJDIR = ..
SUBDIR = apt
include $(OBJDIR)/Makeconf
SUBDIRS =
BIN_SCRIPTS = manage_profile
SBIN_SCRIPTS =
LIB_SCRIPTS = APT_Profile.pm
WEB_BIN_SCRIPTS = webmanage_profile
WEB_SBIN_SCRIPTS=
LIBEXEC_SCRIPTS = $(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS)
# These scripts installed setuid, with sudo.
SETUID_BIN_SCRIPTS =
SETUID_SBIN_SCRIPTS =
SETUID_SUEXEC_SCRIPTS=
#
# Force dependencies on the scripts so that they will be rerun through
# configure if the .in file is changed.
#
all: $(BIN_SCRIPTS) $(SBIN_SCRIPTS) $(LIBEXEC_SCRIPTS) $(SUBDIRS) \
$(LIB_SCRIPTS) all-subdirs
subboss:
include $(TESTBED_SRCDIR)/GNUmakerules
install: $(addprefix $(INSTALL_BINDIR)/, $(BIN_SCRIPTS)) \
$(addprefix $(INSTALL_SBINDIR)/, $(SBIN_SCRIPTS)) \
$(addprefix $(INSTALL_LIBDIR)/, $(LIB_SCRIPTS)) \
$(addprefix $(INSTALL_LIBEXECDIR)/, $(LIBEXEC_SCRIPTS)) \
boss-install: install install-subdirs
subboss-install:
post-install:
#
# Control node installation (aka, ops)
#
control-install:
# This rule says what web* script depends on which installed binary directory.
$(WEB_SBIN_SCRIPTS): $(INSTALL_SBINDIR)
$(WEB_BIN_SCRIPTS): $(INSTALL_BINDIR)
# Just in case the dirs are not yet created,
$(INSTALL_SBINDIR) $(INSTALL_BINDIR):
# And then how to turn the template into the actual script.
$(WEB_SBIN_SCRIPTS) $(WEB_BIN_SCRIPTS): $(TESTBED_SRCDIR)/WEBtemplate.in
@echo "Generating $@"
cat $< | sed -e 's,@PROGTOINVOKE@,$(word 2,$^)/$(subst web,,$@),' > $@
clean: clean-subdirs
# How to recursively descend into subdirectories to make general
# targets such as `all'.
%.MAKE:
@$(MAKE) -C $(dir $@) $(basename $(notdir $@))
%-subdirs: $(addsuffix /%.MAKE,$(SUBDIRS)) ;
.PHONY: $(SUBDIRS) install
#!/usr/bin/perl -w
#
# Copyright (c) 2000-2014 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
use English;
use strict;
use Getopt::Std;
use XML::Simple;
use Data::Dumper;
use CGI;
#
# Back-end script to manage APT profiles.
#
sub usage()
{
print("Usage: manage_profile [-v] <xmlfile>\n");
print("Usage: manage_profile -r profile\n");
exit(-1);
}
my $optlist = "dv";
my $debug = 0;
my $verify = 0; # Check data and return status only.
my $skipadmin = 0;
#
# Configure variables
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
#
# 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 EmulabConstants;
use emdb;
use emutil;
use User;
use Project;
use APT_Profile;
# 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();
}
# The web interface (and in the future the xmlrpc interface) sets this.
my $this_user = User->ImpliedUser();
if (! defined($this_user)) {
$this_user = User->ThisUser();
if (!defined($this_user)) {
fatal("You ($UID) do not exist!");
}
}
# Remove profile.
if (defined($options{"r"})) {
exit(DeleteProfile());
}
my $xmlfile = shift(@ARGV);
#
# 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
("profile_name" => ["name", $SLOT_REQUIRED],
"profile_pid" => ["pid", $SLOT_REQUIRED],
"profile_creator" => ["creator", $SLOT_OPTIONAL],
"profile_description" => ["description", $SLOT_REQUIRED],
"profile_public" => ["public", $SLOT_OPTIONAL],
"rspec" => ["rspec", $SLOT_REQUIRED],
);
#
# 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 create.
#
my %new_args = ();
foreach $key (keys(%{ $xmlparse->{'attribute'} })) {
my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'};
if (!defined($value)) { # Empty string comes from XML as an undef value.
$xmlparse->{'attribute'}->{"$key"}->{'value'} = $value = "";
}
print STDERR "User attribute: '$key' -> '$value'\n"
if ($debug);
my $field = $key;
if (!exists($xmlfields{$field})) {
next; # Skip it.
}
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 && !$skipadmin) {
# 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, "apt_profiles",
$dbslot, TBDB_CHECKDBSLOT_ERROR)) {
$errors{$key} = TBFieldErrorString();
next;
}
$new_args{$dbslot} = $value;
}
UserError()
if (keys(%errors));
#
# We need to make sure the project exists and is a valid project for
# the creator (current user).
#
my $project = Project->Lookup($new_args{"pid"});
if (!defined($project)) {
$errors{"profile_pid"} = "No such project exists";
}
elsif (!$project->AccessCheck($this_user, TB_PROJECT_MAKEIMAGEID())) {
$errors{"profile_pid"} = "Not enough permission in this project";
}
my $usererror;
my $profile = APT_Profile->Create($project, $this_user, \%new_args, \$usererror);
if (!defined($profile)) {
if (defined($usererror)) {
$errors{"profile_name"} = $usererror;
UserError();
}
fatal("Could not create new profile");
}
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);
}
#
# Generate a simple XML file that PHP can parse. The web interface
# relies on using the same name attributes for the errors, as for
# the incoming values.
#
sub UserError()
{
if (keys(%errors)) {
print "<errors>\n";
foreach my $key (keys(%errors)) {
print "<error name='$key'>" . CGI::escapeHTML($errors{$key});
print "</error>\n";
}
print "</errors>\n";
}
# Exit with positive status so web interface treats it as user error.
exit(1);
}
sub escapeshellarg($)
{
my ($str) = @_;
$str =~ s/[^[:alnum:]]/\\$&/g;
return $str;
}
......@@ -54,6 +54,28 @@ CREATE TABLE `active_checkups` (
PRIMARY KEY (`object`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- Table structure for table `apt_profiles`
--
DROP TABLE IF EXISTS `apt_profiles`;
CREATE TABLE `apt_profiles` (
`name` varchar(64) NOT NULL default '',
`idx` int(10) unsigned NOT NULL auto_increment,
`creator` varchar(8) NOT NULL default '',
`creator_idx` mediumint(8) unsigned NOT NULL default '0',
`pid` varchar(48) NOT NULL default '',
`pid_idx` mediumint(8) unsigned NOT NULL default '0',
`created` datetime default NULL,
`uuid` varchar(40) NOT NULL,
`public` tinyint(1) NOT NULL default '0',
`description` mediumtext,
`rspec` mediumtext,
PRIMARY KEY (`idx`),
UNIQUE KEY `pidname` (`pid_idx`,`name`),
UNIQUE KEY `uuid` (`uuid`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- Table structure for table `archive_revisions`
--
......
......@@ -1269,6 +1269,12 @@ REPLACE INTO table_regex VALUES ('default','tinytext_utf8','text','regex','^(?:[
REPLACE INTO table_regex VALUES ('default','text_utf8','text','regex','^(?:[\\x20-\\x7E]|[\\xC2-\\xDF][\\x80-\\xBF]|\\xE0[\\xA0-\\xBF][\\x80-\\xBF]|[\\xE1-\\xEC\\xEE\\xEF][\\x80-\\xBF]{2}|\\xED[\\x80-\\x9F][\\x80-\\xBF])*$',0,65535,'adopted from http://www.w3.org/International/questions/qa-forms-utf-8.en.php');
REPLACE INTO table_regex VALUES ('default','fulltext_utf8','text','regex','^(?:[\\x09\\x0A\\x0D\\x20-\\x7E]|[\\xC2-\\xDF][\\x80-\\xBF]|\\xE0[\\xA0-\\xBF][\\x80-\\xBF]|[\\xE1-\\xEC\\xEE\\xEF][\\x80-\\xBF]{2}|\\xED[\\x80-\\x9F][\\x80-\\xBF])*$',0,65535,'adopted from http://www.w3.org/International/questions/qa-forms-utf-8.en.php');
REPLACE INTO table_regex VALUES ('apt_profiles','pid','text','redirect','projects:pid',0,0,NULL);
REPLACE INTO table_regex VALUES ('apt_profiles','creator','text','redirect','users:uid',0,0,NULL);
REPLACE INTO table_regex VALUES ('apt_profiles','name','text','redirect','images:imagename',0,64,NULL);
REPLACE INTO table_regex VALUES ('apt_profiles','public','int','redirect','default:boolean',0,0,NULL);
REPLACE INTO table_regex VALUES ('apt_profiles','description','text','redirect','default:html_fulltext',0,512,NULL);
REPLACE INTO table_regex VALUES ('apt_profiles','rspec','text','redirect','default:html_fulltext',0,8192,NULL);
--
-- Dumping data for table `testsuite_preentables`
......
#
# Add APT profiles table.
#
use strict;
use libdb;
sub DoUpdate($$$)
{
my ($dbhandle, $dbname, $version) = @_;
if(!DBTableExists("apt_profiles")) {
DBQueryFatal("CREATE TABLE `apt_profiles` ( ".
" `name` varchar(64) NOT NULL default '', ".
" `idx` int(10) unsigned NOT NULL auto_increment, ".
" `creator` varchar(8) NOT NULL default '', ".
" `creator_idx` mediumint(8) unsigned NOT NULL default '0', ".
" `pid` varchar(48) NOT NULL default '', ".
" `pid_idx` mediumint(8) unsigned NOT NULL default '0', ".
" `created` datetime default NULL, ".
" `uuid` varchar(40) NOT NULL, ".
" `public` tinyint(1) NOT NULL default '0', ".
" `description` mediumtext, ".
" `rspec` mediumtext, ".
" PRIMARY KEY (`idx`), ".
" UNIQUE KEY `pidname` (`pid_idx`,`name`), ".
" UNIQUE KEY `uuid` (`uuid`) ".
") ENGINE=MyISAM DEFAULT CHARSET=latin1");
}
DBQueryFatal("REPLACE INTO table_regex VALUES ".
"('apt_profiles','pid','text','redirect',".
"'projects:pid',0,0,NULL)");
DBQueryFatal("REPLACE INTO table_regex VALUES ".
"('apt_profiles','creator','text','redirect',".
"'users:uid',0,0,NULL)");
DBQueryFatal("REPLACE INTO table_regex VALUES ".
"('apt_profiles','name','text','redirect',".
"'images:imagename',0,64,NULL)");
DBQueryFatal("REPLACE INTO table_regex VALUES ".
"('apt_profiles','public','int','redirect',".
"'default:boolean',0,0,NULL)");
DBQueryFatal("REPLACE INTO table_regex VALUES ".
"('apt_profiles','description','text','redirect',".
"'default:html_fulltext',0,512,NULL)");
DBQueryFatal("REPLACE INTO table_regex VALUES ".
"('apt_profiles','rspec','text','redirect',".
"'default:html_fulltext',0,8192,NULL)");
return 0;
}
# Local Variables:
# mode:perl
# End:
......@@ -68,19 +68,21 @@ function ($, sup)
}
}
else if (pageType == 'manage_profile') {
try {
$('#rspecfile').change(function() {
var reader = new FileReader();
reader.onload = function(event) {
var content = event.target.result;
if (0) {
try {
$('#rspecfile').change(function() {
var reader = new FileReader();
reader.onload = function(event) {
var content = event.target.result;
sup.ShowUploadedRspec(content);
};
reader.readAsText(this.files[0]);
});
}
catch (e) {
alert(e);
sup.ShowUploadedRspec(content);
};
reader.readAsText(this.files[0]);
});
}
catch (e) {
alert(e);
}
}
}
$('body').show();
......@@ -144,6 +146,10 @@ function ($, sup)
event.preventDefault();
sup.LoginByModal();
});
$('#logout_button').click(function (event) {
event.preventDefault();
sup.Logout();
});
}
$(document).ready(initialize);
......
......@@ -142,7 +142,8 @@ function ShowTopo(uuid)
function UpdateProfileSelection(selectedElement)
{
var profile = $(selectedElement).attr('value');
console.log(selectedElement);
var profile = $(selectedElement).text();