Commit bd20dd17 authored by Timothy Stack's avatar Timothy Stack

First cut at a daemon that does regular checkups of the testbed
hardware/software.

	* configure, configure.in: Add tbsetup/checkup directory.

	* db/audit.in: Add a listing of stuck checkups.

	* install/boss-install.in: Add 'elabckup' user.

	* rc.d/3.testbed.sh.in: Startup the checkup_daemon.

	* sql/database-create.sql, sql/database-migrate.txt: Add the
	checkups tables.

	* tbsetup/GNUmakefile.in: Descend into the checkup directory.

	* tbsetup/checkup: The checkup daemon, man page, and
	  associated scripts.

	* tbsetup/ptopgen.in: Add a feature with a value of 0.9 to
	  prereserved nodes to keep them from being allocated unless
	  they're really wanted.

	* utils/firstuser.in: Add some other options so the script can be
	  used to create other pseudo users.
parent 3d64914b
......@@ -2297,6 +2297,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
tbsetup/ipassign/ipassign_wrapper tbsetup/assign_prepass \
tbsetup/panic tbsetup/webpanic \
tbsetup/tbrsync tbsetup/nfstrace \
tbsetup/checkup/GNUmakefile tbsetup/checkup/checkup_daemon \
tip/GNUmakefile tip/console \
tmcd/GNUmakefile tmcd/tmcd.restart \
tmcd/common/GNUmakefile tmcd/common/config/GNUmakefile \
......
......@@ -735,6 +735,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
tbsetup/ipassign/ipassign_wrapper tbsetup/assign_prepass \
tbsetup/panic tbsetup/webpanic \
tbsetup/tbrsync tbsetup/nfstrace \
tbsetup/checkup/GNUmakefile tbsetup/checkup/checkup_daemon \
tip/GNUmakefile tip/console \
tmcd/GNUmakefile tmcd/tmcd.restart \
tmcd/common/GNUmakefile tmcd/common/config/GNUmakefile \
......
#!/usr/bin/perl -wT
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2005 University of Utah and the Flux Group.
# Copyright (c) 2000-2006 University of Utah and the Flux Group.
# All rights reserved.
#
use English;
......@@ -247,6 +247,31 @@ if ($query_result->numrows) {
print "\n";
}
#
# Look for stuck checkups.
#
if (! ($query_result =
DBQueryWarn("select object,type,state,start from active_checkups ".
"where start < DATE_SUB(NOW(), INTERVAL 1 DAY)"))) {
fatal("Error accessing the database.");
}
if ($query_result->numrows) {
print "\n";
print "----------------------------------------------------------------\n";
print "\n";
print "Checkups running for more than a day:\n";
printf("%-16s %-16s %-16s %-22s\n", "Object", "Type", "State", "Start");
print "---------------- ---------------- ---------------- ".
"--------------------\n";
while (my ($object,$type,$state,$start) = $query_result->fetchrow()) {
printf("%-16s %-16s %-16s %-20s\n", $object, $type, $state, $start);
}
print "\n";
}
#
# Send email if anything was reported.
#
......
......@@ -2,7 +2,7 @@
#
# EMULAB-COPYRIGHT
# Copyright (c) 2003, 2004, 2005 University of Utah and the Flux Group.
# Copyright (c) 2003, 2004, 2005, 2006 University of Utah and the Flux Group.
# All rights reserved.
#
......@@ -981,6 +981,15 @@ Phase "firstuser", "Setting up initial user (elabman)", sub {
(defined($password) ? " -p $password" : ""));
};
Phase "chkupuser", "Setting up checkup user (elabckup)", sub {
PhaseSkip("elabckup already created")
if (-d "/users/elabckup");
ExecQuietFatal("perl $TOP_OBJDIR/utils/firstuser -b ".
(defined($password) ? " -p $password" : "").
"-u elabckup -n 'Emulab Checkup User' ".
"-e '@TBTESTSUITEEMAIL@'");
};
Phase "experiments", "Setting up system experiments", sub {
foreach my $eid (keys(%EXPERIMENTS)) {
my $pid = $EXPERIMENTS{$eid}->{"pid"};
......
......@@ -76,6 +76,11 @@ case "$1" in
@prefix@/sbin/repos_daemon
fi
if [ -x @prefix@/sbin/checkup_daemon ]; then
echo -n " checkupd"
@prefix@/sbin/checkup_daemon
fi
if [ -x @prefix@/sbin/robomonitord ]; then
echo -n " robomonitord"
@prefix@/sbin/robomonitord
......
......@@ -15,6 +15,18 @@ CREATE TABLE accessed_files (
KEY idx (idx)
) TYPE=MyISAM;
--
-- Table structure for table `active_checkups`
--
CREATE TABLE active_checkups (
object varchar(128) NOT NULL default '',
type varchar(64) NOT NULL default '',
state varchar(16) NOT NULL default 'new',
start datetime default NULL,
PRIMARY KEY (object)
) TYPE=MyISAM;
--
-- Table structure for table `archive_tags`
--
......@@ -99,6 +111,40 @@ CREATE TABLE cdroms (
PRIMARY KEY (cdkey)
) TYPE=MyISAM;
--
-- Table structure for table `checkup_types`
--
CREATE TABLE checkup_types (
object_type varchar(64) NOT NULL default '',
checkup_type varchar(64) NOT NULL default '',
major_type varchar(64) NOT NULL default '',
expiration int(10) NOT NULL default '86400',
PRIMARY KEY (object_type,checkup_type)
) TYPE=MyISAM;
--
-- Table structure for table `checkups`
--
CREATE TABLE checkups (
object varchar(128) NOT NULL default '',
type varchar(64) NOT NULL default '',
next datetime default NULL,
PRIMARY KEY (object,type)
) TYPE=MyISAM;
--
-- Table structure for table `checkups_temp`
--
CREATE TABLE checkups_temp (
object varchar(128) NOT NULL default '',
type varchar(64) NOT NULL default '',
next datetime default NULL,
PRIMARY KEY (object,type)
) TYPE=MyISAM;
--
-- Table structure for table `comments`
--
......@@ -199,7 +245,7 @@ CREATE TABLE delays (
) TYPE=MyISAM;
--
-- Table structure for table `delta_inst`
-- Table structure for table `deleted_users`
--
CREATE TABLE deleted_users (
......
......@@ -3034,3 +3034,35 @@ last_net_act,last_cpu_act,last_ext_act);
alter table scripts change name script_name varchar(24) NOT NULL default '';
alter table priorities change name priority_name varchar(8) NOT NULL;
4.26: Add tables for the checkup_daemon.
CREATE TABLE active_checkups (
object varchar(128) NOT NULL default '',
type varchar(64) NOT NULL default '',
state varchar(16) NOT NULL default 'new',
start datetime default NULL,
PRIMARY KEY (object)
) TYPE=MyISAM;
CREATE TABLE checkup_types (
object_type varchar(64) NOT NULL default '',
checkup_type varchar(64) NOT NULL default '',
major_type varchar(64) NOT NULL default '',
expiration int(10) NOT NULL default '86400',
PRIMARY KEY (object_type,checkup_type)
) TYPE=MyISAM;
CREATE TABLE checkups (
object varchar(128) NOT NULL default '',
type varchar(64) NOT NULL default '',
next datetime default NULL,
PRIMARY KEY (object,type)
) TYPE=MyISAM;
CREATE TABLE checkups_temp (
object varchar(128) NOT NULL default '',
type varchar(64) NOT NULL default '',
next datetime default NULL,
PRIMARY KEY (object,type)
) TYPE=MyISAM;
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2005 University of Utah and the Flux Group.
# Copyright (c) 2000-2006 University of Utah and the Flux Group.
# All rights reserved.
#
......@@ -13,7 +13,7 @@ PLABSUPPORT = @PLABSUPPORT@
include $(OBJDIR)/Makeconf
SUBDIRS = checkpass ns2ir ipassign nseparse
SUBDIRS = checkpass ns2ir ipassign nseparse checkup
BIN_STUFF = power snmpit tbend tbprerun tbreport \
os_load endexp batchexp swapexp \
......@@ -75,7 +75,7 @@ wanlinksolve: wanlinksolve.cc
routecalc: routecalc.cc
${CXX} $< ${CXXFLAGS} -o $@ ${LIBS} -lm -lstdc++ ${LDFLAGS}
.PHONY: checkpass ns2ir plab ipassign
.PHONY: checkpass ns2ir plab ipassign checkup
checkpass:
@$(MAKE) -C checkpass all
......@@ -91,6 +91,9 @@ plab:
ipassign:
@$(MAKE) -C ipassign all
checkup:
@$(MAKE) -C checkup all
install: all script-install subdir-install
@echo "Don't forget to do a post-install as root"
......@@ -116,6 +119,7 @@ subdir-install:
@$(MAKE) -C nseparse install
$(PLAB_INSTALL)
@$(MAKE) -C ipassign install
@$(MAKE) -C checkup install
script-install: $(addprefix $(INSTALL_BINDIR)/, $(BIN_STUFF)) \
$(addprefix $(INSTALL_SBINDIR)/, $(SBIN_STUFF)) \
......@@ -210,6 +214,7 @@ subdir-clean:
@$(MAKE) -C nseparse clean
@$(MAKE) -C plab clean
@$(MAKE) -C ipassign clean
@$(MAKE) -C checkup clean
distclean: subdir-distclean
......@@ -219,6 +224,7 @@ subdir-distclean:
@$(MAKE) -C nseparse distclean
@$(MAKE) -C plab distclean
@$(MAKE) -C ipassign distclean
@$(MAKE) -C checkup distclean
#
# XXX Create non .tcl files.
#
......
#
# EMULAB-COPYRIGHT
# Copyright (c) 2006 University of Utah and the Flux Group.
# All rights reserved.
#
#
# Insert Copyright Here.
#
SRCDIR = @srcdir@
TESTBED_SRCDIR = @top_srcdir@
OBJDIR = ../..
SUBDIR = tbsetup/checkup
include $(OBJDIR)/Makeconf
LIB_STUFF = linktest.ns
LIBEXEC_STUFF = example_checkup
SBIN_STUFF = checkup_daemon
MAN_STUFF = checkup_daemon.8
#
# Force dependencies on the scripts so that they will be rerun through
# configure if the .in file is changed.
#
all: $(LIB_STUFF) $(SBIN_STUFF)
include $(TESTBED_SRCDIR)/GNUmakerules
install: $(addprefix $(INSTALL_SBINDIR)/, $(SBIN_STUFF)) \
$(addprefix $(INSTALL_LIBDIR)/checkup/, $(LIB_STUFF)) \
$(addprefix $(INSTALL_LIBEXECDIR)/checkup/, $(LIBEXEC_STUFF)) \
$(addprefix $(INSTALL_DIR)/man/man8/, $(MAN_STUFF))
$(INSTALL_DIR)/man/man8/%: %
@echo "Installing $<"
-mkdir -p $(INSTALL_DIR)/man/man8
$(INSTALL_DATA) $< $@
$(INSTALL_LIBDIR)/checkup/%: %
@echo "Installing $<"
-mkdir -p $(INSTALL_LIBDIR)/checkup
$(INSTALL) $< $@
$(INSTALL_LIBEXECDIR)/checkup/%: %
@echo "Installing $<"
-mkdir -p $(INSTALL_LIBEXECDIR)/checkup
$(INSTALL) $< $@
clean:
.\"
.\" EMULAB-COPYRIGHT
.\" Copyright (c) 2006 University of Utah and the Flux Group.
.\" All rights reserved.
.\"
.TH CHECKUP_DAEMON 8 "Jan 2, 2006" "Emulab" "Emulab Commands Manual"
.OS
.SH NAME
checkup_daemon \- Daemon that periodically performs checkups on the testbed
hardware and software.
.SH SYNOPSIS
.BI checkup_daemon
[\fB-hd\fR]
.SH DESCRIPTION
The
.B checkup_daemon
executes regular checkups on the testbed so as to proactively uncover any
hardware or software problems. The daemon itself acts primarily as a manager
and relies on NS files or scripts to perform the necessary testing of objects
in the testbed. For example, to perform a checkup on the experimental
interfaces of a node, the daemon creates an experiment that runs linktest and
terminates itself if there were no problems. If there was a problem, an email
is sent to testbed-ops and the checkup stays in a locked state until a human
can have a look at the problem. Once the checkup finishes, either because
there was no problem or it was resolved, the next checkup is scheduled.
.SH CONFIGURATION
The configuration for the
.B checkup_daemon
is derived from the testbed database. The schedule for the objects to be
checked and the type of checks is stored in the
.I checkups
table and has the following fields:
.TP
.I object
The "object" identifier. The objects under test are pretty loosely defined at
the moment, so it is possible to schedule checkups for things that have no real
representation in the rest of the testbed.
.TP
.I type
The type of checkup to perform on this object.
.TP
.I next
The next time this checkup should be run on this object.
.P
The
.I checkup_types
table provides additional information for each type of check to be applied to
an object and has the following fields:
.TP
.I checkup_type
The checkup type identifier and the name of the script or NS file to execute.
Scripts must be stored in "/usr/testbed/libexec/checkup" and NS files must
be stored in "/usr/testbed/lib/checkup" with a ".ns" extension.
.TP
.I object_type
The type of object this checkup can be applied to. This field is usually
used in conjunction with the major_type field, for example, to automatically
schedule checkups for nodes of a given type.
.TP
.I major_type
The "major" type of an object. This field is used by the daemon to perform
some additional actions, for example, doing some setup before executing the
checkup on an object. Currently, the actions are hardcoded into the daemon
itself.
.TP
.I expiration
The number of seconds after a checkup that the next checkup should be run.
.SH MAJOR TYPES
The currently recognized major types are as follows:
.TP
node
The
.I node
major type indicates that the object identifier is a physical node name and
should be prereserved so the checkup can be run in a timely fashion.
.SH NS\-BASED CHECKUPS
The first method of performing a checkup is to use an NS file that can be run
as a batch experiment. The daemon will create an experiment with the NS file
and wait for it to terminate itself or for an error to be reported through the
event system. If an error is reported, the experiment will stay swapped in
until a testbed operator can diagnose the problem and swap out the experiment.
.P
Arguments are passed to the NS file through the following TCL variables:
.TP
opt(CHECKUP_OBJECT)
The identifier of the object to be tested.
.TP
opt(OBJECT_TYPE)
The object type as listed in the checkup_types table.
.TP
opt(MAJOR_TYPE)
The major type as listed in the checkup_types table.
.SH SCRIPT\-BASED CHECKUPS
Checkups that don't make sense as NS file can be implemented using a script.
The daemon executes the script in a polling fashion, therefore, scripts that
take longer than a few seconds to execute should daemonize and report when the
daemon finishes the next time the script is executed. The script is started in
its own working directory and passed the following arguments:
.TP
.I object
The object to operate on.
.TP
.I state
The current state of the checkup. See
.B ACTIVE CHECKUP STATES
below for an explanation of the states.
.P
The script is then expected to return one of the following error codes:
.TP
0
The checkup has finished successfully.
.TP
10
The checkup is still running.
.TP
.I other
The checkup failed. An email containing the output of the script will be sent
to testbed-ops and the checkup will be placed in the "locked" state.
.SH ACTIVE CHECKUP STATES
.TP
.B new
The checkup was just made active. For NS based checkups, the experiment is
created here and added to the batch queue. For script based checkups, the
script should report success immediately or daemonize and exit with status 10.
.TP
.B running
The checkup is still running. Script based checkups should poll the status of
the daemon here.
.TP
.B locked
The checkup is locked and waiting for human intervention. Unlocking an NS
based checkup is done by swapping out the experiment, the daemon will then take
care of terminating the experiment. Script based checkups should provide their
own method of unlocking the checkup.
.SH EXAMPLES
.PP
To add a new checkup type that is rerun every seconds on an object:
.PP
.RS
mysql> insert into checkup_types set checkup_type='mytest', expiration=60;
.RE
.PP
To schedule a "mytest" checkup for the "foo" object:
.PP
.RS
mysql> insert into checkups set object='foo', type='mytest', next=NOW();
.RE
.PP
To add a new checkup for all pc600's that is rerun every day:
.PP
.RS
mysql> insert into checkup_types set object_type='pc600', major_type='node',
checkup_type='hwtest.ns';
.RE
.SH FILES
.TP
/usr/testbed/lib/checkup/
Directory that holds NS files that can perform a checkup.
.TP
/usr/testbed/lib/checkup/linktest.ns
NS file used to check the experimental interfaces on a node. XXX It is
currently hardwired to test for four interfaces.
.TP
/usr/testbed/libexec/checkup/
Directory that holds scripts that can perform a checkup.
.TP
/usr/testbed/libexec/checkup/example_checkup
Example checkup script that demonstrates the "protocol" between the
checkup_daemon and a checkup script.
.TP
/users/elabckup/ckup-*
The working directories for any active script based checkups.
.SH AUTHOR
The Emulab project at the University of Utah.
.SH NOTES
The Emulab project can be found on the web at
.IR http://www.emulab.net
This diff is collapsed.
#! /usr/bin/env python
import sys
import os, os.path
import getopt
def usage():
print "Usage: example_checkup [-h] <object> <state>"
return
try:
opts, req_args = getopt.getopt(sys.argv[1:],
"h",
[ "help", ])
for opt, val in opts:
if opt in ("-h", "--help"):
usage()
sys.exit()
pass
pass
if len(req_args) < 2:
raise getopt.error('error: too few arguments')
if len(req_args) > 2:
raise getopt.error('error: too many arguments')
object, state = req_args
if state not in ("new", "running", "locked"):
raise getopt.error('error: unknown state - %s' % state)
pass
except getopt.error, e:
print e.args[0]
usage()
sys.exit(2)
pass
if state == "new":
file = open("counter", "w")
file.write("0")
file.close()
rc = 10
pass
elif state == "running":
value = int(open("counter", "r").read())
if value < 2:
file = open("counter", "w")
file.write(str(value + 1))
file.close()
rc = 10
pass
else:
sys.stderr.write("error: value not less than 2 - %d\n" % value)
sys.stderr.write("info: remove %s/counter to unlock this checkup.\n" %
os.getcwd())
rc = 1
pass
pass
elif state == "locked":
if os.path.exists("counter"):
rc = 10
pass
else:
rc = 0
pass
pass
sys.exit(rc)
# XXX Assumes four ethernet interfaces.
set ns [new Simulator]
source tb_compat.tcl
tb-set-colocate-factor 20
# Nodes
set node0 [$ns node]
tb-fix-node $node0 $opt(CHECKUP_OBJECT)
tb-set-node-os $node0 FBSD-STD
set node1 [$ns node]
tb-set-hardware $node1 pcvm
set node2 [$ns node]
tb-set-hardware $node2 pcvm
set node3 [$ns node]
tb-set-hardware $node3 pcvm
set node4 [$ns node]
tb-set-hardware $node4 pcvm
# Links
set link0 [$ns duplex-link $node0 $node1 100000.0kb 0.0ms DropTail]
set link1 [$ns duplex-link $node0 $node2 100000.0kb 0.0ms DropTail]
set link2 [$ns duplex-link $node0 $node3 100000.0kb 0.0ms DropTail]
set link3 [$ns duplex-link $node0 $node4 100000.0kb 0.0ms DropTail]
set doit [$ns event-sequence {
$ns linktest
$ns terminate
}]
$ns at 0.0 "$doit start"
$ns rtproto Static
$ns run
......@@ -2,7 +2,7 @@
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2005 University of Utah and the Flux Group.
# Copyright (c) 2000-2006 University of Utah and the Flux Group.
# All rights reserved.
#
......@@ -340,7 +340,8 @@ if ($allnodes) {
$result =
DBQueryFatal("select a.node_id,a.type,a.phys_nodeid,t.class,t.issubnode," .
"a.def_boot_osid, (b.pid is not null and b.eid is not null) " .
"a.def_boot_osid, (b.pid is not null and b.eid is not null), " .
" np.reserved_pid is not null ".
"from nodes as a ".
"left join reserved as b on a.node_id=b.node_id ".
"left join reserved as m on a.phys_nodeid=m.node_id ".
......@@ -355,8 +356,8 @@ $result =
# to use all nodes), or if there is no entry in the perms table for
# the type/class of node.
#
while (($node,$type,$physnode,$class,$issubnode,$def_boot_osid,$reserved)
= $result->fetchrow_array) {
while (($node,$type,$physnode,$class,$issubnode,$def_boot_osid,$reserved,
$prereserved) = $result->fetchrow_array) {
$nodes{$node} = $type
if (!defined($pid) ||
($permissions{$type} && $permissions{$class}));
......@@ -370,6 +371,8 @@ while (($node,$type,$physnode,$class,$issubnode,$def_boot_osid,$reserved)
} else {
$is_reserved{$node} = 0;
}
$is_prereserved{$node} = $prereserved;
}
#
......@@ -555,6 +558,10 @@ foreach $node (keys(%nodes)) {
push(@features,"already_reserved:0");
}
if ($is_prereserved{$node}) {
push(@features,"prereserved:0.9");
}
# Add in modelnet stuff.
if ($mnetcores) {
push(@types, "modelnet-core:$mnetcores");
......
......@@ -2,7 +2,7 @@
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2005 University of Utah and the Flux Group.
# Copyright (c) 2000-2006 University of Utah and the Flux Group.
# All rights reserved.
#
......@@ -32,18 +32,23 @@ my $HOMEDIR = "/users";
my $protoproj = 'emulab-ops';
my $protoproj_desc = 'Operations Meta-Project';
my $batchmode = 0;
my $webonly = 1;
my $trust = "project_root";
my $password;
my $encpass;
my %opts;
my $CONTROL = "@USERNODE@";
my $BOSSNODE = "@BOSSNODE@";
#
# Handle command-line options
#
sub usage {
print "Usage: firstuser [-b] [-p password]\n";
print "Usage: firstuser [-b] [-p password] [-u user] [-n name] [-e email]\n";
exit(1);
}
if (! getopts("bp:", \%opts)) {
if (! getopts("bp:u:n:e:", \%opts)) {
usage();
}
if (defined($opts{b})) {
......@@ -52,6 +57,17 @@ if (defined($opts{b})) {
if (defined($opts{p})) {
$password = $opts{p};
}
if (defined($opts{u})) {
$protouser = $opts{u};
$webonly = 0;
$trust = "local_root";
}
if (defined($opts{n})) {
$protouser_name = $opts{n};
}
if (defined($opts{e})) {
$protouser_email = $opts{e};
}
my $result = DBQueryFatal("select * from users where uid='$protouser'");
if ($result->num_rows()) {
......@@ -62,9 +78,11 @@ if ($UID != 0) {
die "Sorry, this must be run as root\n";
}
print "This script will create the 'proto-user' $protouser, which you will\n";
print "use to bootstrap other users. It also creates the emulab-ops\n";
print "meta-project.\n\n";
if (!defined($opts{u})) {
print "This script will create the 'proto-user' $protouser, which you will\n";
print "use to bootstrap other users. It also creates the emulab-ops\n";
print "meta-project.\n\n";
}
# Get a password for the user
if (!defined($password)) {
......@@ -107,6 +125,10 @@ if (!defined $agid) {
}
# And in the wheel group so elabman can sudo to root.
my $Ggid = "wheel";
if (defined($opts{u})) {
$Ggid .= " $protoproj";
}
if (!$batchmode) {
print "Creating user/project: Are you sure? (Y/N) ";
......@@ -116,33 +138,48 @@ if (!$batchmode) {
}
print "Creating user on boss...\n";
if (system "/usr/sbin/pw useradd $protouser -u $uid -g $agid -G $Ggid -h - " .
if (system "/usr/sbin/pw useradd $protouser -u $uid -g $agid -G \"$Ggid\" -h - " .
"-m -d $HOMEDIR/$protouser -s /bin/nologin -c \"$protouser_name\"\n") {
die "Unable to add user to the password file!\n";
}
if ($CONTROL ne $BOSSNODE) {
print "Creating user on ops...\n";
if (system("ssh $CONTROL ".
"'/usr/sbin/pw useradd $protouser -u $uid -g $agid ".
"-G \"$Ggid\" -h - -d $HOMEDIR/$protouser -s /bin/nologin ".
"-c \"$protouser_name\"'")) {
die "Unable to add user to the ops password file!\n";
}
}
# Initialize the index value;
DBQueryFatal("replace into emulab_indicies set name='next_uid',idx=$uid");
print "Creating user in database...\n";
DBQueryFatal("insert into users set uid='$protouser', usr_created=now(), " .
"usr_name='$protouser_name', usr_pswd='$encpass', unix_uid=$uid, ".
"usr_modified=now(), admin=1, webonly=1, status='active', ".
"usr_modified=now(), admin=1, webonly=$webonly, status='active', ".
"usr_shell='$protouser_shell', usr_email='$protouser_email'");
print "Creating project in database...\n";
DBQueryFatal("insert into projects set pid='$protoproj', created=now(), " .
"name='$protoproj_desc', head_uid='$protouser', unix_gid=$gid, " .
"approved=1");
print "Creating group in database...\n";
DBQueryFatal("insert into groups set pid='$protoproj', gid='$protoproj', " .
"leader='$protouser', created=now(), description='Default Group', " .