From eb7fe19b017bf1c23c052438391d7fea1b13dfed Mon Sep 17 00:00:00 2001
From: Leigh B Stoller <stoller@flux.utah.edu>
Date: Wed, 29 Jan 2014 12:32:00 -0700
Subject: [PATCH] First cut a new page to create new profiles, plus backend
 scripts and libraries. Rough, needs plenty more work.

---
 apt/APT_Profile.pm.in        | 223 ++++++++++++++++++++++++++++
 apt/GNUmakefile.in           |  92 ++++++++++++
 apt/manage_profile.in        | 271 +++++++++++++++++++++++++++++++++++
 sql/database-create.sql      |  22 +++
 sql/database-fill.sql        |   6 +
 sql/updates/4/379            |  59 ++++++++
 www/aptui/js/main.js         |  30 ++--
 www/aptui/js/quickvm_sup.js  |  69 +++++----
 www/aptui/login.php          |   2 +-
 www/aptui/logout.php         |  56 ++++++++
 www/aptui/manage_profile.php | 235 +++++++++++++++++++++++++++---
 www/aptui/quickvm.css        |  35 ++++-
 www/aptui/quickvm.php        |  18 ++-
 www/aptui/quickvm_sup.php    |  45 +++---
 14 files changed, 1068 insertions(+), 95 deletions(-)
 create mode 100644 apt/APT_Profile.pm.in
 create mode 100644 apt/GNUmakefile.in
 create mode 100644 apt/manage_profile.in
 create mode 100644 sql/updates/4/379
 create mode 100644 www/aptui/logout.php

diff --git a/apt/APT_Profile.pm.in b/apt/APT_Profile.pm.in
new file mode 100644
index 0000000000..569b07ecc5
--- /dev/null
+++ b/apt/APT_Profile.pm.in
@@ -0,0 +1,223 @@
+#!/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;
diff --git a/apt/GNUmakefile.in b/apt/GNUmakefile.in
new file mode 100644
index 0000000000..4721a338b0
--- /dev/null
+++ b/apt/GNUmakefile.in
@@ -0,0 +1,92 @@
+#
+# 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
diff --git a/apt/manage_profile.in b/apt/manage_profile.in
new file mode 100644
index 0000000000..5cb0984eb0
--- /dev/null
+++ b/apt/manage_profile.in
@@ -0,0 +1,271 @@
+#!/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;
+}
diff --git a/sql/database-create.sql b/sql/database-create.sql
index 2a232bde6e..a8f036653d 100644
--- a/sql/database-create.sql
+++ b/sql/database-create.sql
@@ -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`
 --
diff --git a/sql/database-fill.sql b/sql/database-fill.sql
index 0dbda77688..0b532cb1ca 100644
--- a/sql/database-fill.sql
+++ b/sql/database-fill.sql
@@ -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`
diff --git a/sql/updates/4/379 b/sql/updates/4/379
new file mode 100644
index 0000000000..20d2151836
--- /dev/null
+++ b/sql/updates/4/379
@@ -0,0 +1,59 @@
+#
+# 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:
diff --git a/www/aptui/js/main.js b/www/aptui/js/main.js
index 6ce07b17da..d526b0d6a6 100644
--- a/www/aptui/js/main.js
+++ b/www/aptui/js/main.js
@@ -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);
diff --git a/www/aptui/js/quickvm_sup.js b/www/aptui/js/quickvm_sup.js
index 740485b4df..f28d2afb55 100755
--- a/www/aptui/js/quickvm_sup.js
+++ b/www/aptui/js/quickvm_sup.js
@@ -142,7 +142,8 @@ function ShowTopo(uuid)
 
 function UpdateProfileSelection(selectedElement)
 {
-	var profile = $(selectedElement).attr('value');
+    console.log(selectedElement);
+        var profile = $(selectedElement).text();
 	$('#selected_profile').attr('value', profile);
 	$('#selected_profile_text').html("" + profile);
 
@@ -178,15 +179,15 @@ function ShowProfileList(selectedElement)
 	var topo   = ConvertManifestToJSON(profile, xml);
 	console.info(topo);
     
-	$('#showtopo_title').html("<h3>" + profile + "</h3>");
+	$('#showtopo_title').html("<h3>" + json.value.name + "</h3>");
 	$('#showtopo_description').html(json.value.description);
 
 	maketopmap("#showtopo_div",
 		   ($("#showtopo_div").outerWidth()),
 		   300, topo);
-    }
-    var $xmlthing = CallMethod("getprofile", null, 0, profile);
-    $xmlthing.done(callback);
+	}
+        var $xmlthing = CallMethod("getprofile", null, 0, profile);
+        $xmlthing.done(callback);
 }
 
 function ShowProfile(direction)
@@ -236,29 +237,6 @@ function ShowProfile(direction)
     $xmlthing.done(callback);
 }
 
-function ShowProfileSlider(direction)
-{
-    console.info(direction);
-    
-    var callback = function(json) {
-	console.info(json.value);
-	var xmlDoc = $.parseXML(json.value.rspec);
-	var xml    = $(xmlDoc);
-	var topo   = ConvertManifestToJSON(null, xml);
-	console.info(topo);
-
-	$('#showtopo_title').html("<h3>" + json.value.name + "</h3>");
-	$('#showtopo_description').html(json.value.description);
-	
-	$("#slider_container").removeClass("invisible");
-	maketopmap("#slider_div",
-		   ($("#slider_div").outerWidth()) - 90,
-		   200, topo);
-    }
-    var $xmlthing = CallMethod("getprofile", null, 0, null);
-    $xmlthing.done(callback);
-}
-
 function InitProfileSelector()
 {
     $('#scrollleft').click(function () {
@@ -976,6 +954,38 @@ function LoginByModal()
     xmlthing.done(callback);
 }
 
+/*
+ * log the user out via an ajax call.
+ */
+function Logout()
+{
+    var callback = function(json) {
+	if (json.code) {
+	    alert("Logout failed!");
+	}
+	else {
+	    // Need to stick the button back in ...
+	    $("#loginbutton").html("");
+	}
+    }
+    var xmlthing = $.ajax({
+	// the URL for the request
+	url: "logout.php",
+ 
+	// the data to send (will be converted to a query string)
+	data: {
+	    ajax_request: 1,
+	},
+ 
+	// whether this is a POST or GET request
+	type: "GET",
+ 
+	// the type of data we expect back
+	dataType : "json",
+    });
+    xmlthing.done(callback);
+}
+
 // Exports from this module for use elsewhere
 return {
     Extend: Extend,
@@ -990,6 +1000,7 @@ return {
     Terminate: Terminate,
     UpdateProfileSelection: UpdateProfileSelection,
     ShowUploadedRspec: ShowUploadedRspec,
-    LoginByModal: LoginByModal
+    LoginByModal: LoginByModal,
+    Logout: Logout
 };
 });
diff --git a/www/aptui/login.php b/www/aptui/login.php
index ba8e44401c..bc702b9941 100644
--- a/www/aptui/login.php
+++ b/www/aptui/login.php
@@ -203,6 +203,6 @@ else {
     #
     # Zap back to front page in secure mode.
     # 
-    header("Location: $APTBASE");
+    header("Location: $APTBASE/quickvm.php");
 }
 ?>
diff --git a/www/aptui/logout.php b/www/aptui/logout.php
new file mode 100644
index 0000000000..ecb58ef5f8
--- /dev/null
+++ b/www/aptui/logout.php
@@ -0,0 +1,56 @@
+<?php
+#
+# 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/>.
+# 
+# }}}
+#
+chdir("..");
+include("defs.php3");
+chdir("apt");
+include("quickvm_sup.php");
+
+#
+# Verify page arguments.
+#
+$optargs = OptionalPageArguments("ajax_request", PAGEARG_BOOLEAN);
+
+#
+# Get current user.
+#
+$this_user = CheckLogin($check_status);
+if ($this_user) {
+    if (DOLOGOUT($this_user) != 0) {
+	if ($ajax_request) {
+	    SPITAJAX_ERROR(1, "Logout failed");
+	    exit();
+	}
+	else {
+	    SPITHEADER();
+	    echo "<center><font color=red>Logout failed!</font></failed>\n";
+	    SPITFOOTER();
+	}
+    }
+}
+if ($ajax_request) {
+    SPITAJAX_RESPONSE("");
+    exit();
+}
+header("Location: quickvm.php");
+?>
diff --git a/www/aptui/manage_profile.php b/www/aptui/manage_profile.php
index 645fda2684..0113e6395f 100644
--- a/www/aptui/manage_profile.php
+++ b/www/aptui/manage_profile.php
@@ -35,15 +35,14 @@ $this_user = CheckLogin($check_status);
 # Verify page arguments.
 #
 $optargs = OptionalPageArguments("create",      PAGEARG_STRING,
-				 "profile",     PAGEARG_STRING,
 				 "formfields",  PAGEARG_ARRAY);
 
 #
 # Spit the form
 #
-function SPITFORM($formfields, $needlogin, $errors)
+function SPITFORM($formfields, $errors)
 {
-    global $this_user;
+    global $this_user, $projlist;
 
     # XSS prevention.
     while (list ($key, $val) = each ($formfields)) {
@@ -56,15 +55,20 @@ function SPITFORM($formfields, $needlogin, $errors)
 	}
     }
 
-    $formatter = function($field, $label, $html) use ($errors) {
+    $formatter = function($field, $label, $html, $help = null) use ($errors) {
 	$class = "form-group";
 	if ($errors && array_key_exists($field, $errors)) {
 	    $class .= " has-error";
 	}
 	echo "<div class='$class'>\n";
 	echo "  <label for='$field' ".
-	"         class='col-sm-2 control-label'>$label</label>\n";
-	echo "  <div class='col-sm-10'>\n";
+	"         class='col-sm-3 control-label'>$label ";
+	if ($help) {
+	    echo "<a href='#' data-toggle='tooltip' title='$help'>".
+		"<span class='glyphicon glyphicon-question-sign'></span></a>";
+	}
+	echo "  </label>\n";
+	echo "  <div class='col-sm-7'>\n";
 	echo "     $html\n";
 	if ($errors && array_key_exists($field, $errors)) {
 	    echo "<label class='control-label' for='inputError'>" .
@@ -88,6 +92,7 @@ function SPITFORM($formfields, $needlogin, $errors)
               </h3>
              </div>
              <div class='panel-body'>\n";
+
     echo "   <form id='quickvm_create_profile_form'
                    class='form-horizontal' role='form'
                    enctype='multipart/form-data'
@@ -96,13 +101,45 @@ function SPITFORM($formfields, $needlogin, $errors)
     echo "     <div class='col-sm-12'>\n";
     echo "      <fieldset>\n";
 
+    #
+    # Look for non-specific error.
+    #
+    if ($errors && array_key_exists("error", $errors)) {
+	echo "<font color=red>" . $errors["error"] . "</font>";
+    }
 
     $formatter("profile_name", "Profile Name",
 	       "<input name=\"formfields[profile_name]\"
 		       id='profile_name'
 		       value='" . $formfields["profile_name"] . "'
                        class='form-control'
-                       placeholder='' type='text'>");
+                       placeholder='' type='text'>",
+	       "alphanumeric, dash, underscore, no whitespace");
+    #
+    # If user is a member of only one project, then just pass it
+    # through, no need for the user to see it. Otherwise we have
+    # to let the user choose.
+    #
+    if (count($projlist) == 1) {
+	echo "<input type='hidden' name='formfields[profile_pid]' ".
+	    "value='" . $projlist[0] . "'>\n";
+    }
+    else {
+	$pid_options = "";
+	while (list($project) = each($projlist)) {
+	    $selected = "";
+	    if ($formfields["profile_pid"] == $project) {
+		$selected = "selected";
+	    }
+	    $pid_options .= 
+		"<option $selected value='$project'>$project</option>\n";
+	}
+	$formatter("profile_pid", "Project",
+		   "<select name=\"formfields[profile_pid]\"
+		            id='profile_pid' class='form-control'
+                            placeholder='Please Select'>$pid_options</select>");
+    }
+       
     $formatter("profile_description", "Description",
 	       "<textarea name=\"formfields[profile_description]\"
 		          id='profile_description'
@@ -115,16 +152,24 @@ function SPITFORM($formfields, $needlogin, $errors)
 	       "<input name='rspecfile' id='rspecfile'
                        type=file class='form-control'>");
 
+    $formatter("profile_public", "Public?",
+	       "<div class='checkbox'>
+                <label><input name=\"formfields[profile_public]\" ".
+	               $formfields["profile_public"] .
+	       "       id='profile_public' value=checked
+                       type=checkbox> ".
+	       "List on the public page for anyone to use?</label></div>");
+
     echo "      </fieldset>\n";
 
     echo "<div class='form-group'>
             <div class='col-sm-offset-2 col-sm-10'>
-               <button class='btn btn-primary btm-sm'
+               <button class='btn btn-primary btm-sm pull-right'
                    id='profile_submit_button'
                    type='submit' name='create'>Create</button>
             </div>
           </div>\n";
-    
+
     echo "     </div>\n";
     echo "    </div>\n";
     echo "   </form></div>\n";
@@ -163,38 +208,182 @@ function SPITFORM($formfields, $needlogin, $errors)
     
     SPITFOOTER();
 }
+
 #
-# If not clicked, then put up a form. We use a session variable to
-# save the form data in case we have to force the user to login.
+# The user must be logged in.
 #
-session_start();
+if (!$this_user) {
+    if (isset($formfields)) {
+	$_SESSION["formfields"] = $formfields;
+    }
+    # HTTP_REFERER will not work reliably when redirecting so
+    # pass in the URI for this page as an argument
+    header("Location: login.php?referrer=".
+	   urlencode($_SERVER['REQUEST_URI']));
+    # Use exit because of the session. 
+    exit();
+}
+
+#
+# See what projects the user can do this in.
+#
+$projlist = $this_user->ProjectAccessList($TB_PROJECT_MAKEIMAGEID);
 
 if (! isset($create)) {
+    $errors = array();
+    
     if (isset($_SESSION["formfields"])) {
 	$defaults = $_SESSION["formfields"];
     }
     else {
 	$defaults = array();
     }
+    if (! (isset($projlist) && count($projlist))) {
+	$errors["error"] =
+	    "You do not appear to be a member of any projects in which ".
+	    "you have permission to create new profiles";
+    }
+    SPITFORM($defaults, $errors);
+    return;
+}
+
+#
+# Otherwise, must validate and redisplay if errors
+#
+$errors = array();
+
+#
+# Quick check for required fields.
+#
+$required = array("pid", "name", "description");
+		  
+foreach ($required as $key) {
+    if (!isset($formfields["profile_${key}"]) ||
+	strcmp($formfields["profile_${key}"], "") == 0) {
+	$errors["profile_${key}"] = "Missing Field";
+    }
+    elseif (! TBcheck_dbslot($formfields["profile_${key}"], "apt_profiles", $key,
+			     TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)) {
+	$errors["profile_${key}"] = TBFieldErrorString();
+    }
+}
+
+#
+# The rspec has to be treated specially of course.
+#
+if (isset($_FILES['rspecfile']) &&
+    $_FILES['rspecfile']['name'] != "" &&
+    $_FILES['rspecfile']['name'] != "none") {
+
+    $rspec = file_get_contents($_FILES['rspecfile']['tmp_name']);
+    if (!$rspec) {
+	$errors["rspecfile"] = "Could not process file";
+    }
+    elseif (! TBvalid_html_fulltext($rspec)) {
+	$errors["rspecfile"] = TBFieldErrorString();	
+    }
+}
+else {
+    $errors["rspecfile"] = "Missing Field";
+}
+
+#
+# Project has to exist. We need to know it for the SUEXEC call
+# below. 
+#
+$project = Project::LookupByPid($formfields["profile_pid"]);
+if (!$project) {
+    $errors["profile_pid"] = "No such project";
+}
 
-    SPITFORM($defaults, 0, null);
+# Present these errors before we call out to do anything else.
+if (count($errors)) {
+    SPITFORM($formfields, $errors);
     return;
 }
 
 #
-# The user must be logged in.
+# Pass to the backend as an XML data file. If this gets too complicated,
+# we might eed to do all the checking in the backend and have it pass
+# back the error set. 
 #
-if (!$this_user) {
-    if (isset($formfields)) {
-	$_SESSION["formfields"] = $formfields;
+# Generate a temporary file and write in the XML goo.
+#
+$xmlname = tempnam("/tmp", "newprofile");
+if (! $xmlname) {
+    TBERROR("Could not create temporary filename", 0);
+    $errors["error"] = "Internal error; Could not create temp file";
+}
+elseif (! ($fp = fopen($xmlname, "w"))) {
+    TBERROR("Could not open temp file $xmlname", 0);
+    $errors["error"] = "Internal error; Could not open temp file";
+}
+else {
+    fwrite($fp, "<profile>\n");
+    fwrite($fp, "<attribute name='profile_pid'>");
+    fwrite($fp, "  <value>" . $formfields["profile_pid"] . "</value>");
+    fwrite($fp, "</attribute>\n");
+    fwrite($fp, "<attribute name='profile_name'>");
+    fwrite($fp, "  <value>" .
+	   htmlspecialchars($formfields["profile_name"]) . "</value>");
+    fwrite($fp, "</attribute>\n");
+    fwrite($fp, "<attribute name='profile_description'>");
+    fwrite($fp, "  <value>" .
+	   htmlspecialchars($formfields["profile_description"]) . "</value>");
+    fwrite($fp, "</attribute>\n");
+    fwrite($fp, "<attribute name='rspec'>");
+    fwrite($fp, "  <value>" . htmlspecialchars($rspec) . "</value>");
+    fwrite($fp, "</attribute>\n");
+    if (isset($formfields["profile_public"]) &&
+	$formfields["profile_public"] == "checked") {
+	fwrite($fp, "<attribute name='profile_public'>");
+	fwrite($fp, "  <value>1</value>");
+	fwrite($fp, "</attribute>\n");
     }
-    header("Location: login.php?refer=1");
-    # Use exit because of the session. 
-    exit();
+    fwrite($fp, "</profile>\n");
+    fclose($fp);
+    chmod($xmlname, 0666);
+}
+if (count($errors)) {
+    unlink($xmlname);
+    SPITFORM($formfields, $errors);
+    return;
 }
-# No longer needed, the user is logged in. 
-session_destroy();
 
-SPITFORM($formfields, 0, null);
+#
+# Call out to the backend.
+#
+$retval = SUEXEC($this_user->uid(), $project->unix_gid(),
+		 "webmanage_profile $xmlname",
+		 SUEXEC_ACTION_IGNORE);
+if ($retval) {
+    if ($retval < 0) {
+	$errors["error"] = "Internal Error; please try again later.";
+	SUEXECERROR(SUEXEC_ACTION_CONTINUE);
+    }
+    else {
+	#
+	# Decode simple XML that is returned. 
+	#
+	$parsed = simplexml_load_string($suexec_output);
+	if (!$parsed) {
+	    $errors["error"] = "Internal Error; please try again later.";
+	    TBERROR("Could not parse XML output:\n$suexec_output\n", 0);
+	}
+	else {
+	    foreach ($parsed->error as $error) {
+		$errors[(string)$error['name']] = $error;
+	    }
+	}
+    }
+}
+if (count($errors)) {
+    unlink($xmlname);
+    SPITFORM($formfields, $errors);
+    return;
+}
+
+SPITFORM($formfields, $errors);
+
 
 ?>
diff --git a/www/aptui/quickvm.css b/www/aptui/quickvm.css
index 028c0a3580..4f95f5bc4f 100644
--- a/www/aptui/quickvm.css
+++ b/www/aptui/quickvm.css
@@ -17,13 +17,36 @@ body {
   padding: 0 0 2.5em;
 }
 
-#loginbutton {
-  display:inline-block;
-  padding-right:15px;
-  padding-top:15px;
-  position:absolute;
-  right:0;
+.navbar img {
+  display: block;
+  margin: 0 auto;
+  height: 75px;
+}
+
+.navbar-static-top {
+  background-color: #ff6600;
+}
+
+.navbar-inner {
+  background-color: #ff6600;
+  height: 75px;
+  width: 95%;
+  margin: 0 auto;
+}
+
+.navbar .brand {
+  position: absolute;
   top:0;
+  left:0;
+  width: 100%;
+}
+
+.navbar-nav > li > a {
+  margin-top: 20px;
+}
+
+.navbar-btn {
+  margin-top: 20px;
 }
 
 /* Set the fixed height of the footer here */
diff --git a/www/aptui/quickvm.php b/www/aptui/quickvm.php
index 19ee215e76..a6a0238a90 100755
--- a/www/aptui/quickvm.php
+++ b/www/aptui/quickvm.php
@@ -54,19 +54,20 @@ $optargs = OptionalPageArguments("create",      PAGEARG_STRING,
 #
 if (isset($ajax_request)) {
     if ($ajax_method == "getprofile") {
-	$profile_name = addslashes($ajax_argument);
+	$profile_idx = addslashes($ajax_argument);
 	$query_result =
-	    DBQueryWarn("select * from quickvm_rspecs ".
-			"where name='$profile_name'", $dblink);
+	    DBQueryWarn("select * from apt_profiles ".
+			"where idx='$profile_idx'");
 
 	if (!$query_result || !mysql_num_rows($query_result)) {
-	    SPITAJAX_ERROR(1, "No such profile!");
+	    SPITAJAX_ERROR(1, "No such profile $profile_idx!");
 	    exit();
 	}
 	$row = mysql_fetch_array($query_result);
 	
 	SPITAJAX_RESPONSE(array('rspec' => $row['rspec'],
 				'name'  => $row['name'],
+				'idx'   => $row['idx'],
 				'description' => $row['description']));
     }
     exit();
@@ -76,13 +77,16 @@ if (isset($ajax_request)) {
 $username_default = "Pick a user name";
 $email_default    = "Your email address";
 $sshkey_default   = "Your SSH public key";
-$profile_default  = "UBUNTU12-64-STD";
+$profile_default  = "ThreeVMs";
 $profile_array    = array();
 
 $query_result =
-    DBQueryFatal("select * from quickvm_rspecs", $dblink);
+    DBQueryFatal("select * from apt_profiles where public=1");
 while ($row = mysql_fetch_array($query_result)) {
-    $profile_array[$row["name"]] = $row["name"];
+    $profile_array[$row["idx"]] = $row["name"];
+    if ($row["pid"] == $TBOPSPID && $row["name"] == $profile_default) {
+	$profile_default = $row["idx"];
+    }
 }
 
 function SPITFORM($username, $email, $sshkey, $profile, $newuser, $errors)
diff --git a/www/aptui/quickvm_sup.php b/www/aptui/quickvm_sup.php
index b9125f4fad..b06ab3f799 100644
--- a/www/aptui/quickvm_sup.php
+++ b/www/aptui/quickvm_sup.php
@@ -72,32 +72,43 @@ function SPITHEADER($thinheader = 0)
     if ($TBMAINSITE && file_exists("../google-analytics.php")) {
 	readfile("../google-analytics.php");
     }
-    
+
     echo "
     <!-- Container for body, needed for sticky footer -->
     <div id='wrap'>
-      <div style='background-color: #ff6600'>";
+         <div class='navbar navbar-static-top' role='navigation'>
+           <div class='navbar-inner'>
+             <div class='brand'>
+                 <img src='aptlogo.png'/>
+             </div>
+             <ul class='nav navbar-nav navbar-right'>";
     if (!$disable_accounts) {
 	if ($login_user) {
-	    echo "<div id='loginbutton'>
-                      $login_uid logged in<br>
-                  </div>\n";
+	    echo "<li><a>$login_uid logged in</a></li>\n";
 	}
 	elseif (!NOLOGINS()) {
-	    echo "<div id='loginbutton'>
-                   <button class='btn btn-primary'
-                           id='login_button' type=button
-	                   data-toggle='modal' data-target='#quickvm_login_modal'>
-                        Login</button>
-                 </div>\n";
+	    echo "<li><a class='btn btn-primary navbar-btn'
+                           id='login_button'
+	                   data-toggle='modal'
+                           href='#quickvm_login_modal'
+                           data-target='#quickvm_login_modal'>
+                        Login</a></li>
+                  \n";
 	}
     }
-    echo "<img class='align-center' style='width: ${height}px'
-               src='aptlogo.png'/>
-      </div>
-     <!-- Page content -->
-     <div class='container'>\n";
+    echo "   </ul>
+             <ul class='nav navbar-nav navbar-left'>
+                <li><a href='quickvm.php'>Home</a></li>\n";
+    if (!$disable_accounts && $login_user) {
+	echo "  <li><a href='#' id='logout_button'>Logout</a></li>\n";
+    }
+    echo "   </ul>
+           </div>
+         </div>\n";
+
     SpitLoginModal("quickvm_login_modal");
+    echo " <!-- Page content -->
+           <div class='container'>\n";
 }
 
 function SPITFOOTER()
@@ -195,7 +206,7 @@ function SpitVerifyModal($id, $label)
 function SpitLoginModal($id, $embedded = 0)
 {
     echo "<!-- This is the login modal -->
-          <div id='$id' class='modal fade'>
+          <div id='$id' class='modal fade' role='dialog'>
             <div class='modal-dialog'>
             <div class='modal-content'>
                <div class='modal-header'>
-- 
GitLab