From afa5e919edef90680727edddd809b081b0cdff00 Mon Sep 17 00:00:00 2001
From: Mike Hibler <mike@flux.utah.edu>
Date: Fri, 20 Oct 2006 16:09:09 +0000
Subject: [PATCH] Wow, this should make me look important!

Two-day boondoggle to support "/scratch", an optional large, shared filesystem
for users.  To do this, I needed to find all the instances where /proj is used
and behave accordingly.  The boondoggle part was the decision to gather up all
the hardwired instances of shared directory names ("/proj", "/users", etc.)
so that they are set in a common place (via unexposed configure variables).
This is a boondoggle because:

1. I didn't change the client-side scripts.  They need a different mechanism
   (e.g., tmcd) to get the info, configure is the wrong way.

2. Even if I had done #1 it is likely--no, certain--that something would
   fail if you tried to rename "/proj" to be "/mike".  These names are just
   too ingrained.

3. We may not even use "/scratch" as it turns out.

Note, I also didn't fix any of the .html documentation.  Anyway, it is done.
To maintain my illusion in the future you should:

1. Have perl scripts include "use libtestbed" and use the defined PROJROOT(),
   et.al. functions where possible.  If not possible, make sure they run
   through configure and use @PROJROOT_DIR@, etc.

2. Use the configure method for python, C, php and other languages.

3. There are perl (TBValidUserDir) and php (VALIDUSERPATH) functions which
   you should call to determine if an NS, template parameter, tarball or
   other file are in "an acceptable location."  Use these functions where
   possible.  They know about the optional "scratch" filesystem.  Note that
   the perl function is over-engineered to handles cases that don't occur
   in nature.
---
 account/addpubkey.in                  |   5 +-
 account/addsfskey.in                  |   5 +-
 account/quotamail.in                  |  17 ++-
 account/tbacct.in                     |   3 +-
 collab/cvstools/cvsrepo_ctrl.in       |   6 +-
 collab/cvstools/cvsrepo_ctrl.proxy.in |   7 +-
 config.h.in                           |   6 +
 configure                             |  72 +++++++++--
 configure.in                          |  31 ++++-
 db/genelists.in                       |   2 -
 db/libdb.pm.in                        |  10 +-
 defs-default                          |   1 +
 defs-elabinelab                       |   1 +
 defs-example                          |  14 +-
 defs-example-privatecnet              |  14 +-
 doc/setup-fs.txt                      |  16 ++-
 doc/setup-ops.txt                     |  17 ++-
 event/linktest/linktest.pl.in         |  41 +++---
 event/linktest/run_linktest.pl.in     |  22 ++--
 event/nsetrafgen/nseinput.tcl.in      |   7 +-
 install/boss-install.in               |  46 +++++--
 install/fs-install.in                 |  36 ++++--
 install/ops-install.in                |  53 +++++---
 install/smb.conf.head.in              |   2 +-
 sensors/canaryd/feedbacklogs.in       |   4 +-
 sensors/slothd/webfeedback.in         |   8 +-
 tbsetup/archive_control.in            |   4 +-
 tbsetup/batch_daemon.in               |   1 -
 tbsetup/batchexp.in                   |  19 +--
 tbsetup/checkup/checkup_daemon.in     |   5 +-
 tbsetup/elabinelab.in                 |   8 +-
 tbsetup/endexp.in                     |   1 -
 tbsetup/eventsys.proxy.in             |   7 +-
 tbsetup/exports_setup.in              |   9 +-
 tbsetup/fetchtar.proxy.in             |  35 +++--
 tbsetup/libArchive.pm.in              |  19 ++-
 tbsetup/libosload.pm.in               |   7 +-
 tbsetup/libtestbed.pm.in              | 180 +++++++++++++++++++++++++-
 tbsetup/mkgroup.in                    |   8 +-
 tbsetup/mkproj.in                     |  41 ++++--
 tbsetup/nfstrace.in                   |  29 +++--
 tbsetup/ns2ir/parse-ns.in             |   2 +-
 tbsetup/ns2ir/tb_compat.tcl.in        |  35 +++--
 tbsetup/nscheck.in                    |  19 ++-
 tbsetup/nsverify/nstbparse.in         |   2 +-
 tbsetup/rmgroup.in                    |   5 +-
 tbsetup/rmproj.in                     |  30 ++++-
 tbsetup/rmuser.in                     |   5 +-
 tbsetup/spewrpmtar.in                 |  13 +-
 tbsetup/swapexp.in                    |  19 ++-
 tbsetup/tbswap.in                     |   2 +-
 tbsetup/template_create.in            |  16 +--
 tbsetup/template_delete.in            |   1 -
 tbsetup/template_exprun.in            |  15 +--
 tbsetup/template_graph.in             |   1 -
 tbsetup/template_instantiate.in       |  15 +--
 tbsetup/template_metadata.in          |  14 +-
 tbsetup/template_swapin.in            |   1 -
 tmcd/common/config/rc.mkelab          |  44 ++++++-
 tmcd/tmcd.c                           |  70 ++++++++--
 utils/create_image.in                 |   8 +-
 utils/cvsinit.in                      |   4 +-
 utils/firstuser.in                    |   2 +-
 utils/template_record.in              |   5 +-
 www/beginexp_form.php3                |   5 +-
 www/beginexp_xml.php3                 |   8 +-
 www/defs.php3.in                      |  37 +++++-
 www/editimageid.php3                  |   4 +-
 www/groups.html                       |   8 +-
 www/modifyexp.php3                    |  12 +-
 www/newimageid.php3                   |  22 ++--
 www/newimageid_ez.php3                |  24 ++--
 www/nscheck.php3                      |  12 +-
 www/nscheck_form.php3                 |   4 +-
 www/template_create.php               |  12 +-
 www/template_export.php               |   2 +-
 www/template_exprun.php               |  12 +-
 www/template_swapin.php               |  12 +-
 xmlrpc/emulabserver.py.in             |  13 +-
 79 files changed, 955 insertions(+), 379 deletions(-)

diff --git a/account/addpubkey.in b/account/addpubkey.in
index d61bdd8f3c..319600a604 100644
--- a/account/addpubkey.in
+++ b/account/addpubkey.in
@@ -1,7 +1,7 @@
 #!/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;
@@ -42,7 +42,6 @@ my $TB		= "@prefix@";
 my $TBOPS       = "@TBOPSEMAIL@";
 my $TBAUDIT     = "@TBAUDITEMAIL@";
 my $OURDOMAIN   = "@OURDOMAIN@";
-my $HOMEDIR	= "/users";
 my $KEYGEN	= "/usr/bin/ssh-keygen";
 my $USERUID;
 
@@ -71,6 +70,8 @@ sub InitUser();
 sub GenerateKeyFile();
 sub fatal($);
 
+my $HOMEDIR = USERROOT();
+
 #
 # Turn off line buffering on output
 #
diff --git a/account/addsfskey.in b/account/addsfskey.in
index a394dacfe7..ec49b69187 100644
--- a/account/addsfskey.in
+++ b/account/addsfskey.in
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -wT
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2004 University of Utah and the Flux Group.
+# Copyright (c) 2000-2004, 2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 use English;
@@ -34,7 +34,6 @@ my $nobody    = 0;
 my $TB		= "@prefix@";
 my $TBOPS       = "@TBOPSEMAIL@";
 my $TBAUDIT     = "@TBAUDITEMAIL@";
-my $HOMEDIR	= "/users";
 my $SFSUPDATE   = "$TB/sbin/sfskey_update";
 
 # Locals
@@ -54,6 +53,8 @@ use libtestbed;
 #
 sub fatal($);
 
+my $HOMEDIR	= USERROOT();
+
 #
 # Turn off line buffering on output
 #
diff --git a/account/quotamail.in b/account/quotamail.in
index 426d165736..a493716e71 100644
--- a/account/quotamail.in
+++ b/account/quotamail.in
@@ -16,6 +16,16 @@
 # installation script/directions should take care of this now.
 #
 
+PROJSTR="Project directory:    @FSDIR_PROJ@"
+GROUPSSTR="Group directory:      @FSDIR_GROUPS@"
+USERSSTR="User/Home directory:  @FSDIR_USERS@"
+if [ -n "@FSDIR_SCRATCH@" ]; then
+    SCRATCHSTR="Project scratch directory: @FSDIR_SCRATCH@"
+else
+    SCRATCHSTR=""
+fi
+
+
 for i in `/usr/sbin/repquota -v @FS_WITH_QUOTAS@ | awk '$2 ~ /\+/ {print $1}'`
 do
 (
@@ -41,9 +51,10 @@ You can check your usage with the 'quota' command on @USERNODE@:
 
 The directory trees (on @USERNODE@) where you most likely have files are:
 
-Project directory:    @FSDIR_PROJ@
-Group directory:      @FSDIR_GROUPS@
-User/Home directory:  @FSDIR_USERS@   
+$PROJSTR
+$GROUPSSTR
+$USERSSTR
+$SCRATCHSTR
 
 PLEASE NOTE: just login to @USERNODE@ directly to do the cleanup!
 You will not be able to swapin an experiment to access these directories
diff --git a/account/tbacct.in b/account/tbacct.in
index 61b337b597..a4a412961b 100644
--- a/account/tbacct.in
+++ b/account/tbacct.in
@@ -50,7 +50,6 @@ my $PROTOUSER   = 'elabman';
 my $SAMBANODE	= "fs";  # DNS makes this do the right thing in E-in-E.
 my $SMBPASSWD	= "/usr/local/bin/smbpasswd";
 
-my $HOMEDIR	= "/users";
 my $USERPATH	= "$TB/bin";
 my $ADDKEY	= "$TB/sbin/addpubkey";
 my $USERADD	= "/usr/sbin/pw useradd";
@@ -137,6 +136,8 @@ sub CheckDotFiles();
 sub GenerateSFSKey();
 sub fatal($);
 
+my $HOMEDIR	= USERROOT();
+
 #
 # Parse command arguments. Once we return from getopts, all that should be
 # left are the required arguments.
diff --git a/collab/cvstools/cvsrepo_ctrl.in b/collab/cvstools/cvsrepo_ctrl.in
index 4fe089d758..f1740d7a87 100644
--- a/collab/cvstools/cvsrepo_ctrl.in
+++ b/collab/cvstools/cvsrepo_ctrl.in
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -wT
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2005 University of Utah and the Flux Group.
+# Copyright (c) 2005, 2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 use English;
@@ -30,8 +30,6 @@ my $TB		= "@prefix@";
 my $FSNODE      = "@FSNODE@";
 my $TBOPS       = "@TBOPSEMAIL@";
 my $TBAUDIT     = "@TBAUDITEMAIL@";
-my $PROJROOT    = "/proj";
-my $CVSREPOS    = "$PROJROOT/cvsrepos";
 my $CVSSUPPORT  = @CVSSUPPORT@;
 my $TESTMODE    = @TESTMODE@;
 
@@ -187,6 +185,8 @@ my $now = time;
 utime $now, $now, $lockfile;
 
 if (defined($pid)) {
+    my $CVSREPOS = PROJROOT() . "/cvsrepos";
+
     #
     # Grab DB data. 
     #
diff --git a/collab/cvstools/cvsrepo_ctrl.proxy.in b/collab/cvstools/cvsrepo_ctrl.proxy.in
index 70ea4f9024..a60c010bd6 100644
--- a/collab/cvstools/cvsrepo_ctrl.proxy.in
+++ b/collab/cvstools/cvsrepo_ctrl.proxy.in
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -w
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2005 University of Utah and the Flux Group.
+# Copyright (c) 2005, 2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 use English;
@@ -32,8 +32,6 @@ my $CVSDCONFOLD	= "/usr/local/etc/cvsd/cvsd.conf.backup";
 my $HEADFILE    = "$TB/lib/cvsd.conf.head";
 my $STARTPROG   = "/usr/local/etc/rc.d/cvsd.sh";
 my $REPODIR     = "cvsrepos";
-my $PROJREPODIR = "/proj/$REPODIR";
-my $JAILREPODIR = "/var/cvsjail/$REPODIR";
 my $MOUNT	= "/sbin/mount -o ro";
 my $UNMOUNT     = "/sbin/umount";
 my %pubrepos    = ();
@@ -62,6 +60,9 @@ sub fatal($);
 use lib "@prefix@/lib";
 use libtestbed;
 
+my $PROJREPODIR = PROJDIR() . "/$REPODIR";
+my $JAILREPODIR = "/var/cvsjail/$REPODIR";
+
 #
 # Parse command arguments. Once we return from getopts, all that should be
 # left are the required arguments.
diff --git a/config.h.in b/config.h.in
index 7041eec9fa..b944bbc76b 100644
--- a/config.h.in
+++ b/config.h.in
@@ -20,10 +20,16 @@
 #undef CONTROL_ROUTER_IP
 #undef CONTROL_NETWORK
 #undef CONTROL_NETMASK
+#undef USERSROOT_DIR
+#undef PROJROOT_DIR
+#undef GROUPSROOT_DIR
+#undef SCRATCHROOT_DIR
+#undef SHAREROOT_DIR
 #undef FSDIR_PROJ
 #undef FSDIR_GROUPS
 #undef FSDIR_USERS
 #undef FSDIR_SHARE
+#undef FSDIR_SCRATCH
 #undef SFSSUPPORT
 #undef ELABINELAB
 #undef LOG_TESTBED
diff --git a/configure b/configure
index b7e7192363..3a1e25b77c 100755
--- a/configure
+++ b/configure
@@ -1371,6 +1371,11 @@ done
 
 
 
+
+
+
+
+
 
 
 
@@ -1437,7 +1442,6 @@ CHATSUPPORT=0
 ARCHIVESUPPORT=0
 NFSTRACESUPPORT=0
 TBLOGFACIL="local5"
-LINKTEST_NSPATH="/share/linktest-ns"
 BOSSEVENTPORT=2927
 UNIFIED_BOSS_AND_OPS=0
 FRISEBEEMCASTADDR="234.5.6"
@@ -1447,6 +1451,16 @@ MIN_UNIX_GID=6000
 DELAYTHRESH=2
 PELABSUPPORT=0
 
+#
+# XXX You really don't want to change these!
+# They are ingrained and who knows what might break if you change them.
+#
+USERSROOT_DIR="/users"
+PROJROOT_DIR="/proj"
+GROUPSROOT_DIR="/groups"
+SHAREROOT_DIR="/share"
+SCRATCHROOT_DIR=""
+
 #
 # Okay, I know this is improper usage of --with. Too bad.
 #
@@ -1480,6 +1494,11 @@ else
 	{ echo "configure: error: Definitions file $TBDEFS was not found." 1>&2; exit 1; }
 fi
 
+# XXX this can optionally be set in the defs file
+if test -n "$FSDIR_SCRATCH"; then
+    SCRATCHROOT_DIR="/scratch"
+fi
+
 #
 # These must come after the "eval" above, since the variables are not
 # defined until the defs file is taken in.
@@ -1560,6 +1579,26 @@ cat >> confdefs.h <<EOF
 #define CONTROL_NETMASK "$CONTROL_NETMASK"
 EOF
 
+cat >> confdefs.h <<EOF
+#define USERSROOT_DIR "$USERSROOT_DIR"
+EOF
+
+cat >> confdefs.h <<EOF
+#define PROJROOT_DIR "$PROJROOT_DIR"
+EOF
+
+cat >> confdefs.h <<EOF
+#define GROUPSROOT_DIR "$GROUPSROOT_DIR"
+EOF
+
+cat >> confdefs.h <<EOF
+#define SCRATCHROOT_DIR "$SCRATCHROOT_DIR"
+EOF
+
+cat >> confdefs.h <<EOF
+#define SHAREROOT_DIR "$SHAREROOT_DIR"
+EOF
+
 cat >> confdefs.h <<EOF
 #define FSDIR_PROJ "$FSDIR_PROJ"
 EOF
@@ -1577,6 +1616,12 @@ if test -n "$FSDIR_SHARE"; then
 #define FSDIR_SHARE "$FSDIR_SHARE"
 EOF
 
+fi
+if test -n "$FSDIR_SCRATCH"; then
+    cat >> confdefs.h <<EOF
+#define FSDIR_SCRATCH "$FSDIR_SCRATCH"
+EOF
+
 fi
 cat >> confdefs.h <<EOF
 #define BOSSEVENTPORT "$BOSSEVENTPORT"
@@ -1972,17 +2017,17 @@ for ac_hdr in ulxmlrpcpp/ulxr_config.h
 do
 ac_safe=`echo "$ac_hdr" | sed 'y%./+-%__p_%'`
 echo $ac_n "checking for $ac_hdr""... $ac_c" 1>&6
-echo "configure:1976: checking for $ac_hdr" >&5
+echo "configure:2021: checking for $ac_hdr" >&5
 if eval "test \"`echo '$''{'ac_cv_header_$ac_safe'+set}'`\" = set"; then
   echo $ac_n "(cached) $ac_c" 1>&6
 else
   cat > conftest.$ac_ext <<EOF
-#line 1981 "configure"
+#line 2026 "configure"
 #include "confdefs.h"
 #include <$ac_hdr>
 EOF
 ac_try="$ac_cpp conftest.$ac_ext >/dev/null 2>conftest.out"
-{ (eval echo configure:1986: \"$ac_try\") 1>&5; (eval $ac_try) 2>&5; }
+{ (eval echo configure:2031: \"$ac_try\") 1>&5; (eval $ac_try) 2>&5; }
 ac_err=`grep -v '^ *+' conftest.out | grep -v "^conftest.${ac_ext}\$"`
 if test -z "$ac_err"; then
   rm -rf conftest*
@@ -2021,17 +2066,17 @@ for ac_hdr in linux/videodev.h
 do
 ac_safe=`echo "$ac_hdr" | sed 'y%./+-%__p_%'`
 echo $ac_n "checking for $ac_hdr""... $ac_c" 1>&6
-echo "configure:2025: checking for $ac_hdr" >&5
+echo "configure:2070: checking for $ac_hdr" >&5
 if eval "test \"`echo '$''{'ac_cv_header_$ac_safe'+set}'`\" = set"; then
   echo $ac_n "(cached) $ac_c" 1>&6
 else
   cat > conftest.$ac_ext <<EOF
-#line 2030 "configure"
+#line 2075 "configure"
 #include "confdefs.h"
 #include <$ac_hdr>
 EOF
 ac_try="$ac_cpp conftest.$ac_ext >/dev/null 2>conftest.out"
-{ (eval echo configure:2035: \"$ac_try\") 1>&5; (eval $ac_try) 2>&5; }
+{ (eval echo configure:2080: \"$ac_try\") 1>&5; (eval $ac_try) 2>&5; }
 ac_err=`grep -v '^ *+' conftest.out | grep -v "^conftest.${ac_ext}\$"`
 if test -z "$ac_err"; then
   rm -rf conftest*
@@ -2064,7 +2109,7 @@ done
 # Extract the first word of "gtk-config", so it can be a program name with args.
 set dummy gtk-config; ac_word=$2
 echo $ac_n "checking for $ac_word""... $ac_c" 1>&6
-echo "configure:2068: checking for $ac_word" >&5
+echo "configure:2113: checking for $ac_word" >&5
 if eval "test \"`echo '$''{'ac_cv_prog_GTK_CONFIG'+set}'`\" = set"; then
   echo $ac_n "(cached) $ac_c" 1>&6
 else
@@ -2143,7 +2188,7 @@ fi
 # SVR4 /usr/ucb/install, which tries to use the nonexistent group "staff"
 # ./install, which can be erroneously created by make from ./install.sh.
 echo $ac_n "checking for a BSD compatible install""... $ac_c" 1>&6
-echo "configure:2147: checking for a BSD compatible install" >&5
+echo "configure:2192: checking for a BSD compatible install" >&5
 if test -z "$INSTALL"; then
 if eval "test \"`echo '$''{'ac_cv_path_install'+set}'`\" = set"; then
   echo $ac_n "(cached) $ac_c" 1>&6
@@ -2204,7 +2249,7 @@ esac
 # Extract the first word of "rsync", so it can be a program name with args.
 set dummy rsync; ac_word=$2
 echo $ac_n "checking for $ac_word""... $ac_c" 1>&6
-echo "configure:2208: checking for $ac_word" >&5
+echo "configure:2253: checking for $ac_word" >&5
 if eval "test \"`echo '$''{'ac_cv_path_RSYNC'+set}'`\" = set"; then
   echo $ac_n "(cached) $ac_c" 1>&6
 else
@@ -2605,10 +2650,16 @@ s%@FSNODE@%$FSNODE%g
 s%@EVENTSERVER@%$EVENTSERVER%g
 s%@BOSSEVENTPORT@%$BOSSEVENTPORT%g
 s%@OURDOMAIN@%$OURDOMAIN%g
+s%@USERSROOT_DIR@%$USERSROOT_DIR%g
+s%@PROJROOT_DIR@%$PROJROOT_DIR%g
+s%@GROUPSROOT_DIR@%$GROUPSROOT_DIR%g
+s%@SCRATCHROOT_DIR@%$SCRATCHROOT_DIR%g
+s%@SHAREROOT_DIR@%$SHAREROOT_DIR%g
 s%@FSDIR_PROJ@%$FSDIR_PROJ%g
 s%@FSDIR_GROUPS@%$FSDIR_GROUPS%g
 s%@FSDIR_USERS@%$FSDIR_USERS%g
 s%@FSDIR_SHARE@%$FSDIR_SHARE%g
+s%@FSDIR_SCRATCH@%$FSDIR_SCRATCH%g
 s%@FS_WITH_QUOTAS@%$FS_WITH_QUOTAS%g
 s%@TRACK_INTERSWITCH_BANDWIDTH@%$TRACK_INTERSWITCH_BANDWIDTH%g
 s%@TIMESTAMPS@%$TIMESTAMPS%g
@@ -2647,7 +2698,6 @@ s%@NFSTRACESUPPORT@%$NFSTRACESUPPORT%g
 s%@TBLOGFACIL@%$TBLOGFACIL%g
 s%@PLAB_ROOTBALL@%$PLAB_ROOTBALL%g
 s%@PLAB_SLICEPREFIX@%$PLAB_SLICEPREFIX%g
-s%@LINKTEST_NSPATH@%$LINKTEST_NSPATH%g
 s%@TESTBED_NETWORK@%$TESTBED_NETWORK%g
 s%@EXTERNAL_TESTBED_NETWORK@%$EXTERNAL_TESTBED_NETWORK%g
 s%@TESTBED_NETMASK@%$TESTBED_NETMASK%g
diff --git a/configure.in b/configure.in
index 499998d955..410eb8615a 100755
--- a/configure.in
+++ b/configure.in
@@ -78,10 +78,16 @@ AC_SUBST(FSNODE)
 AC_SUBST(EVENTSERVER)
 AC_SUBST(BOSSEVENTPORT)
 AC_SUBST(OURDOMAIN)
+AC_SUBST(USERSROOT_DIR)
+AC_SUBST(PROJROOT_DIR)
+AC_SUBST(GROUPSROOT_DIR)
+AC_SUBST(SCRATCHROOT_DIR)
+AC_SUBST(SHAREROOT_DIR)
 AC_SUBST(FSDIR_PROJ)
 AC_SUBST(FSDIR_GROUPS)
 AC_SUBST(FSDIR_USERS)
 AC_SUBST(FSDIR_SHARE)
+AC_SUBST(FSDIR_SCRATCH)
 AC_SUBST(FS_WITH_QUOTAS)
 AC_SUBST(TRACK_INTERSWITCH_BANDWIDTH)
 AC_SUBST(TIMESTAMPS)
@@ -120,7 +126,6 @@ AC_SUBST(NFSTRACESUPPORT)
 AC_SUBST(TBLOGFACIL)
 AC_SUBST(PLAB_ROOTBALL)
 AC_SUBST(PLAB_SLICEPREFIX)
-AC_SUBST(LINKTEST_NSPATH)
 AC_SUBST(TESTBED_NETWORK)
 AC_SUBST(EXTERNAL_TESTBED_NETWORK)
 AC_SUBST(TESTBED_NETMASK)
@@ -218,7 +223,6 @@ CHATSUPPORT=0
 ARCHIVESUPPORT=0
 NFSTRACESUPPORT=0
 TBLOGFACIL="local5"
-LINKTEST_NSPATH="/share/linktest-ns"
 BOSSEVENTPORT=2927
 UNIFIED_BOSS_AND_OPS=0
 FRISEBEEMCASTADDR="234.5.6"
@@ -228,6 +232,16 @@ MIN_UNIX_GID=6000
 DELAYTHRESH=2
 PELABSUPPORT=0
 
+#
+# XXX You really don't want to change these!
+# They are ingrained and who knows what might break if you change them.
+#
+USERSROOT_DIR="/users"
+PROJROOT_DIR="/proj"
+GROUPSROOT_DIR="/groups"
+SHAREROOT_DIR="/share"
+SCRATCHROOT_DIR=""
+
 #
 # Okay, I know this is improper usage of --with. Too bad.
 #
@@ -256,6 +270,11 @@ else
 	AC_MSG_ERROR([Definitions file $TBDEFS was not found.])
 fi
 
+# XXX this can optionally be set in the defs file
+if test -n "$FSDIR_SCRATCH"; then
+    SCRATCHROOT_DIR="/scratch"
+fi
+
 #
 # These must come after the "eval" above, since the variables are not
 # defined until the defs file is taken in.
@@ -285,12 +304,20 @@ fi
 AC_DEFINE_UNQUOTED(CONTROL_ROUTER_IP, "$CONTROL_ROUTER_IP")
 AC_DEFINE_UNQUOTED(CONTROL_NETWORK, "$CONTROL_NETWORK")
 AC_DEFINE_UNQUOTED(CONTROL_NETMASK, "$CONTROL_NETMASK")
+AC_DEFINE_UNQUOTED(USERSROOT_DIR, "$USERSROOT_DIR")
+AC_DEFINE_UNQUOTED(PROJROOT_DIR, "$PROJROOT_DIR")
+AC_DEFINE_UNQUOTED(GROUPSROOT_DIR, "$GROUPSROOT_DIR")
+AC_DEFINE_UNQUOTED(SCRATCHROOT_DIR, "$SCRATCHROOT_DIR")
+AC_DEFINE_UNQUOTED(SHAREROOT_DIR, "$SHAREROOT_DIR")
 AC_DEFINE_UNQUOTED(FSDIR_PROJ, "$FSDIR_PROJ")
 AC_DEFINE_UNQUOTED(FSDIR_GROUPS, "$FSDIR_GROUPS")
 AC_DEFINE_UNQUOTED(FSDIR_USERS, "$FSDIR_USERS")
 if test -n "$FSDIR_SHARE"; then
     AC_DEFINE_UNQUOTED(FSDIR_SHARE, "$FSDIR_SHARE")
 fi
+if test -n "$FSDIR_SCRATCH"; then
+    AC_DEFINE_UNQUOTED(FSDIR_SCRATCH, "$FSDIR_SCRATCH")
+fi
 AC_DEFINE_UNQUOTED(BOSSEVENTPORT, "$BOSSEVENTPORT")
 
 if test $OPSDBSUPPORT -eq 1; then
diff --git a/db/genelists.in b/db/genelists.in
index e67850bf41..9d4f177c56 100644
--- a/db/genelists.in
+++ b/db/genelists.in
@@ -50,8 +50,6 @@ my $USERS       = "@USERNODE@";
 my $OURDOMAIN   = "@OURDOMAIN@";
 my $TBACTIVE    = "@TBACTIVEARCHIVE@";
 my $TBALL       = "@TBUSERSARCHIVE@";
-my $PROJROOT	= "/proj";
-my $GRPROOT	= "/groups";
 my $ELISTS      = "$TB/lists";
 my $ELABINELAB  = @ELABINELAB@;
 my $MAILMANSUPPORT= @MAILMANSUPPORT@;
diff --git a/db/libdb.pm.in b/db/libdb.pm.in
index caa5b09178..5d5018f29d 100644
--- a/db/libdb.pm.in
+++ b/db/libdb.pm.in
@@ -30,7 +30,7 @@ use vars qw(@ISA @EXPORT);
 	 PROJMEMBERTRUST_ROOT PROJMEMBERTRUST_GROUPROOT
 	 PROJMEMBERTRUST_PROJROOT PROJMEMBERTRUST_LOCALROOT
 
-	 PROJROOT GROUPROOT USERROOT TBOPSPID EXPTLOGNAME
+	 TBOPSPID EXPTLOGNAME
 	 PLABMOND_PID PLABMOND_EID PLABHOLDING_PID PLABHOLDING_EID
          PLABTESTING_PID PLABTESTING_EID PLABDOWN_PID PLABDOWN_EID
 
@@ -255,10 +255,8 @@ my $BOSSNODE    = "@BOSSNODE@";
 my $TESTMODE    = @TESTMODE@;
 my $TBOPSPID	= "emulab-ops";
 my $SCRIPTNAME  = "Unknown";
-my $PROJROOT    = "/proj";
-my $GROUPROOT   = "/groups";
-my $USERROOT    = "/users";
 my $EXPTLOGNAME = "activity.log";
+my $PROJROOT    = "@PROJROOT_DIR@";
 
 if ($EVENTSYS) {
     require event;
@@ -456,9 +454,6 @@ sub OLDRESERVED_PID()		{ $TBOPSPID; }
 sub OLDRESERVED_EID()		{ "oldreserved"; }
 sub NFREELOCKED_PID()		{ $TBOPSPID; }
 sub NFREELOCKED_EID()		{ "nfree-locked"; }
-sub PROJROOT()			{ $PROJROOT; }
-sub GROUPROOT()			{ $GROUPROOT; }
-sub USERROOT()			{ $USERROOT; }
 sub TBOPSPID()			{ $TBOPSPID; }
 sub EXPTLOGNAME()		{ $EXPTLOGNAME; }
 
@@ -3295,6 +3290,7 @@ sub TBExptDestroy($$)
     }
     # Yuck.
     if ($pid ne $gid) {
+	# XXX note that this is not the same as TBExptUserDir
 	my $eidlink = "$PROJROOT/$pid/exp/$eid";
 	unlink($eidlink)
 	    if (-l $eidlink);
diff --git a/defs-default b/defs-default
index 9bf9f0c277..7ba9e339c1 100644
--- a/defs-default
+++ b/defs-default
@@ -30,6 +30,7 @@ FSDIR_GROUPS=/q/groups
 FSDIR_PROJ=/q/proj
 FSDIR_USERS=/users
 FSDIR_SHARE=/share
+FSDIR_SCRATCH=
 FS_WITH_QUOTAS="/q /users"
 WWWHOST=www.emulab.net
 TBMAINSITE=1
diff --git a/defs-elabinelab b/defs-elabinelab
index 059b9692da..795b26f4cf 100644
--- a/defs-elabinelab
+++ b/defs-elabinelab
@@ -28,6 +28,7 @@ FSDIR_GROUPS=/q/groups
 FSDIR_PROJ=/q/proj
 FSDIR_USERS=/q/users
 FSDIR_SHARE=/share
+FSDIR_SCRATCH=changeme
 WWWHOST=changeme
 TBMAINSITE=0
 THISHOMEBASE=changeme
diff --git a/defs-example b/defs-example
index 47a9756d49..ee8c605c56 100644
--- a/defs-example
+++ b/defs-example
@@ -82,19 +82,27 @@ TBACTIVEARCHIVE=testbed-active-users-archive@example.emulab.net
 #
 # Real paths (no symlinks) to the directories that get exported from ops
 #
-FSDIR_GROUPS=/groups
+# FSDIR_SCRATCH is optional.  The intent is that it provides per-project
+# space that is not "guaranteed" (for the Utah Emulab that means we do
+# not back it up to tape).  If defined, you would either set no quotas,
+# or higher quotas than for FSDIR_PROJ, on this filesystem.  If you are
+# not providing guarantees and are not doing quotas, you might as well
+# just put all your space in /proj and leave FSDIR_SCRATCH= blank.
+#
+FSDIR_GROUPS=/q/groups
 FSDIR_PROJ=/q/proj
 FSDIR_USERS=/users
 FSDIR_SHARE=/share
+FSDIR_SCRATCH=
 
 #
 # Filesystems on which quotas should be enforced.
 # Note that if multiple of the FSDIR_* vars above are on the same filesystem
-# (e.g., /q/proj and /q/users) then you should only specify the base of the
+# (e.g., /q/proj and /q/groups) then you should only specify the base of the
 # common filesystem on which they all reside here (e.g., /q).
 # Set to the empty string to turn off quota checking.
 #
-FS_WITH_QUOTAS="/q /groups /users"
+FS_WITH_QUOTAS="/q /users"
 
 #
 # SSL Certificate stuff. Used to customize config files in ssl directory.
diff --git a/defs-example-privatecnet b/defs-example-privatecnet
index 7677c8013a..cd905a178c 100644
--- a/defs-example-privatecnet
+++ b/defs-example-privatecnet
@@ -94,19 +94,27 @@ TBACTIVEARCHIVE=testbed-active-users-archive@example.emulab.net
 #
 # Real paths (no symlinks) to the directories that get exported from ops
 #
-FSDIR_GROUPS=/groups
+# FSDIR_SCRATCH is optional.  The intent is that it provides per-project
+# space that is not "guaranteed" (for the Utah Emulab that means we do
+# not back it up to tape).  If defined, you would either set no quotas,
+# or higher quotas than for FSDIR_PROJ, on this filesystem.  If you are
+# not providing guarantees and are not doing quotas, you might as well
+# just put all your space in /proj and leave FSDIR_SCRATCH= blank.
+#
+FSDIR_GROUPS=/q/groups
 FSDIR_PROJ=/q/proj
 FSDIR_USERS=/users
 FSDIR_SHARE=/share
+FSDIR_SCRATCH=
 
 #
 # Filesystems on which quotas should be enforced.
 # Note that if multiple of the FSDIR_* vars above are on the same filesystem
-# (e.g., /q/proj and /q/users) then you should only specify the base of the
+# (e.g., /q/proj and /q/groups) then you should only specify the base of the
 # common filesystem on which they all reside here (e.g., /q).
 # Set to the empty string to turn off quota checking.
 #
-FS_WITH_QUOTAS="/q /groups /users"
+FS_WITH_QUOTAS="/q /users"
 
 #
 # SSL Certificate stuff. Used to customize config files in ssl directory.
diff --git a/doc/setup-fs.txt b/doc/setup-fs.txt
index 6a17d836e4..0f51486920 100644
--- a/doc/setup-fs.txt
+++ b/doc/setup-fs.txt
@@ -1,6 +1,6 @@
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2002-2005 University of Utah and the Flux Group.
+# Copyright (c) 2002-2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 
@@ -63,6 +63,12 @@ space to hold them:
 			which aids per-project accountability.
 /groups/      - Needs enough space for files shared by the sub-groups of
 			projects. These are primarily used by classes, if any.
+/scratch/     - Optional, large filesystem of "scratch" space.  The intent
+			is that this filesystem provides per-project space
+			that is not "guaranteed" (for the Utah Emulab that
+			means we do not back it up to tape).  If used,
+			you would either set no quotas, or higher quotas
+			than for /proj.
 /share/       - Exported read-only to all nodes, we use it for providing to
 			experimenters the source for the FreeBSD and Linux
 			versions we run as well as common packages and RPMs.
@@ -75,6 +81,11 @@ can have different /users/ and /proj/ quotas.) If you do not think you will
 ever use quotas, then you could make /users and /proj part of the same
 filesystem.
 
+As mentioned /scratch is optional.  If you are not providing "guarantees"
+such as filesaved or RAIDed disk space and you are not using quotas, you
+might as well just put all your space in /proj and not define FSDIR_SCRATCH
+in the defs file.
+
 Note also since /share is exported read-only, FreeBSD requires that it be on
 a separate filesystem from anything that is exported read-write.  So while
 /users, /proj and /groups can be on the same filesystem, /share cannot.
@@ -87,7 +98,8 @@ symlinks to the appropriate places. ie., if you make one big filesystem called
 	ln -s /z/proj /proj
 	... etc.
 
-In other words, we assume the existence of /users, /proj, /group and /share.
+In other words, we assume the existence of /users, /proj, /group and /share
+(but not /scratch).
 
 Do *not* create any user accounts, Emulab does not require that its users
 have login accounts on the fileserver.  For the purposes of this setup, just
diff --git a/doc/setup-ops.txt b/doc/setup-ops.txt
index 53d109490f..fb968ecb34 100644
--- a/doc/setup-ops.txt
+++ b/doc/setup-ops.txt
@@ -1,6 +1,6 @@
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2002-2005 University of Utah and the Flux Group.
+# Copyright (c) 2002-2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 
@@ -52,6 +52,12 @@ server, you will also need:
 			which aids per-project accountability.
 /groups/      - Needs enough space for files shared by the sub-groups of
 			projects. These are primarily used by classes, if any.
+/scratch/     - Optional, large filesystem of "scratch" space.  The intent
+			is that this filesystem provides per-project space
+			that is not "guaranteed" (for the Utah Emulab that
+			means we do not back it up to tape).  If used,
+			you would either set no quotas, or higher quotas
+			than for /proj.
 /share/       - Exported read-only to all nodes, we use it for providing to
 			experimenters the source for the FreeBSD and Linux
 			versions we run as well as common packages and RPMs.
@@ -64,6 +70,11 @@ can have different /users/ and /proj/ quotas.) If you do not think you will
 ever use quotas, then you could make /users and /proj part of the same
 filesystem.
 
+As mentioned /scratch is optional.  If you are not providing "guarantees"
+such as filesaved or RAIDed disk space and you are not using quotas, you
+might as well just put all your space in /proj and not define FSDIR_SCRATCH
+in the defs file.
+
 Note also since /share is exported read-only, FreeBSD requires that it be on
 a separate filesystem from anything that is exported read-write.  So while
 /users, /proj and /groups can be on the same filesystem, /share cannot.
@@ -76,8 +87,8 @@ symlinks to the appropriate places. ie., if you make one big filesystem called
 	ln -s /z/proj /proj
 	... etc.
 
-In other words, we assume the existence of /users, /proj, /group and /share.
-
+In other words, we assume the existence of /users, /proj, /group and /share
+(but not /scratch).
 
 ##### Step 2 - Installing packages
 
diff --git a/event/linktest/linktest.pl.in b/event/linktest/linktest.pl.in
index 3bfbc3109d..5f32e17ed4 100755
--- a/event/linktest/linktest.pl.in
+++ b/event/linktest/linktest.pl.in
@@ -15,6 +15,14 @@ use Socket;
 
 my $LINKTEST_VERSION = "1.2";
 
+#
+# XXX config stuff that does not belong on the client-side
+#
+my $CLIENT_BINDIR = "@CLIENT_BINDIR@";
+my $CLIENT_VARDIR = "@CLIENT_VARDIR@";
+my $EVENTSERVER   = "@EVENTSERVER";
+my $PROJROOT      = "@PROJROOT_DIR@";
+
 #
 # Linktest test script. This script is set up to run as root on
 # experiment nodes. It is invoked by the Linktest daemon after the
@@ -49,19 +57,18 @@ sub usage()
 ##############################################################################
 
 # path to applications and files
-use constant LINKTEST_NSPATH => "@LINKTEST_NSPATH@";
-use constant PATH_NICKNAME => "@CLIENT_VARDIR@/boot/nickname";
-use constant PATH_KEYFILE => "@CLIENT_VARDIR@/boot/eventkey";
-use constant PATH_RUDE => "@CLIENT_BINDIR@/emulab-rude";
-use constant PATH_CRUDE => "@CLIENT_BINDIR@/emulab-crude";
-use constant PATH_IPERF => "@CLIENT_BINDIR@/emulab-iperf";
-use constant PATH_RCTOPO => "@CLIENT_BINDIR@/rc/rc.topomap";
-use constant PATH_EMULAB_SYNC => "@CLIENT_BINDIR@/emulab-sync";
-use constant PATH_LTEVENT => "@CLIENT_BINDIR@/ltevent";
-use constant PATH_TEVC => "@CLIENT_BINDIR@/tevc";
-use constant RUN_PATH => "@CLIENT_BINDIR@"; # where the linktest-ns runs.
-use constant EVENTSERVER => "@EVENTSERVER@";
-use constant PATH_SCHEDFILE => "@CLIENT_VARDIR@/logs/linktest.sched";
+use constant PATH_NICKNAME => "$CLIENT_VARDIR/boot/nickname";
+use constant PATH_KEYFILE => "$CLIENT_VARDIR/boot/eventkey";
+use constant PATH_RUDE => "$CLIENT_BINDIR/emulab-rude";
+use constant PATH_CRUDE => "$CLIENT_BINDIR/emulab-crude";
+use constant PATH_IPERF => "$CLIENT_BINDIR/emulab-iperf";
+use constant PATH_RCTOPO => "$CLIENT_BINDIR/rc/rc.topomap";
+use constant PATH_EMULAB_SYNC => "$CLIENT_BINDIR/emulab-sync";
+use constant PATH_LTEVENT => "$CLIENT_BINDIR/ltevent";
+use constant PATH_TEVC => "$CLIENT_BINDIR/tevc";
+use constant RUN_PATH => "$CLIENT_BINDIR"; # where the linktest-ns runs.
+use constant EVENTSERVER => "$EVENTSERVER";
+use constant PATH_SCHEDFILE => "$CLIENT_VARDIR/logs/linktest.sched";
 
 # log files used by tests.
 use constant CRUDE_DAT => "/tmp/crude.dat"; # binary data
@@ -312,10 +319,10 @@ $gid = $proj_id;
 # Set path variables storing the experiment logging path,
 # the current ns file and the output file for topology info.
 #
-$expt_path = "/proj/$proj_id/exp/$exp_id/tbdata";
+$expt_path = "$PROJROOT/$proj_id/exp/$exp_id/tbdata";
 $linktest_path = "$expt_path/linktest";
-$topology_file = "@CLIENT_VARDIR@/boot/ltmap";
-$ptopology_file = "@CLIENT_VARDIR@/boot/ltpmap";
+$topology_file = "$CLIENT_VARDIR/boot/ltmap";
+$ptopology_file = "$CLIENT_VARDIR/boot/ltpmap";
 
 #
 # Determine what OS we are.  Used for handling the occasional difference
@@ -340,7 +347,7 @@ sleep(int(rand(5)));
 # is not participating, we choose the first node on the host list.
 #
 $synserv = "";
-my $ssname = "@CLIENT_VARDIR@/boot/syncserver";
+my $ssname = "$CLIENT_VARDIR/boot/syncserver";
 if ($ssname) {
     @results = &read_file($ssname);
     ($synserv) = split/\./, $results[0];
diff --git a/event/linktest/run_linktest.pl.in b/event/linktest/run_linktest.pl.in
index 7e62c9e9e5..1b8d2cc651 100644
--- a/event/linktest/run_linktest.pl.in
+++ b/event/linktest/run_linktest.pl.in
@@ -9,6 +9,13 @@ use Getopt::Std;
 use English;
 use POSIX;
 
+#
+# XXX config stuff that does not belong on the client-side
+#
+my $TB            = "@prefix@";
+my $CLIENT_BINDIR = "@CLIENT_BINDIR@";
+my $EVENTSERVER   = "@EVENTSERVER";
+my $PROJROOT      = "@PROJROOT_DIR@";
 
 #
 # Wrapper for running the linktest daemon. This script is currently
@@ -41,9 +48,8 @@ my $startAt = 1; # default start level
 my $stopAt = 4 ; # default stop level
 
 # Local goo
-my $TB          = "@prefix@";
-my $TMCC	= "@CLIENT_BINDIR@/tmcc";
-my $LTEVENT     = "@CLIENT_BINDIR@/ltevent";
+my $TMCC	= "$CLIENT_BINDIR/tmcc";
+my $LTEVENT     = "$CLIENT_BINDIR/ltevent";
 my $LOGHOLE     = "$TB/bin/loghole";
 my $LTEVENTOPS  = "$TB/libexec/ltevent";
 my $STOPEVENT   = "STOP"; # XXX Left in here for backwards compat.
@@ -206,18 +212,18 @@ else {
 # Default to the standard event server.
 #
 if (!defined($server)) {
-    $server = "@EVENTSERVER@";
+    $server = "$EVENTSERVER";
 }
 
 #
 # These days, must use a keyfile!
 #
 if (!defined($keyfile)) {
-    $keyfile = "/proj/$pid/exp/$eid/tbdata/eventkey";
+    $keyfile = "$PROJROOT/$pid/exp/$eid/tbdata/eventkey";
 }
 
-my $linktest_path; # path to linktest data.
-$linktest_path = "/proj/" . $pid . "/exp/" . $eid . "/tbdata/linktest";
+# path to linktest data.
+my $linktest_path = "$PROJROOT/$pid/exp/$eid/tbdata/linktest";
 
 # send the startup event.
 my $args = starter();
@@ -467,7 +473,7 @@ sub kill_linktest_run {
 }
 
 sub run_loghole {
-    my $ltlogs = "/proj/$pid/exp/$eid/tbdata/ltlogs";
+    my $ltlogs = "$PROJROOT/$pid/exp/$eid/tbdata/ltlogs";
 
     print "Downloading logs...\n";
 
diff --git a/event/nsetrafgen/nseinput.tcl.in b/event/nsetrafgen/nseinput.tcl.in
index 07c2d20994..4a3531c3c7 100644
--- a/event/nsetrafgen/nseinput.tcl.in
+++ b/event/nsetrafgen/nseinput.tcl.in
@@ -2,7 +2,7 @@
 
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2004 University of Utah and the Flux Group.
+# Copyright (c) 2000-2004, 2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 
@@ -11,6 +11,7 @@ global CLIENTBINDIR
 set CLIENTVARDIR @CLIENT_VARDIR@
 set CLIENTBINDIR @CLIENT_BINDIR@
 set EVENTSERVER  @EVENTSERVER@
+set PROJDIR	 @PROJROOT_DIR@
 
 # consults tmcc hostnames database to translate hostname to IP
 # returns ip address of name
@@ -439,11 +440,11 @@ set pideidlist [split [exec hostname] "."]
 set vnode [lindex $pideidlist 0]
 set eid [lindex $pideidlist 1]
 set pid [lindex $pideidlist 2]
-set logpath "/proj/$pid/exp/$eid/logs/nse-$vnode.log"
+set logpath "$PROJDIR/$pid/exp/$eid/logs/nse-$vnode.log"
 set simobjname [$ns set tbname]
 set nseswap_cmdline "$CLIENTBINDIR/tevc -s $EVENTSERVER -e $pid/$eid now $simobjname NSESWAP SIMHOST=$vnode"
 
-set pktrate_logpath "/proj/$pid/exp/$eid/logs/nse-vnodepktrate-$vnode.log"
+set pktrate_logpath "$PROJDIR/$pid/exp/$eid/logs/nse-vnodepktrate-$vnode.log"
 
 # Configuring the Scheduler to monitor the event system
 set evsink [new TbEventSink]
diff --git a/install/boss-install.in b/install/boss-install.in
index 26f76d2b59..20205402bf 100644
--- a/install/boss-install.in
+++ b/install/boss-install.in
@@ -40,6 +40,16 @@ my $BOSSNODE   = '@BOSSNODE@';
 my $BOSSNODE_IP= '@BOSSNODE_IP@';
 my $USERNODE_IP= '@USERNODE_IP@';
 my $FSNODE_IP=   '@FSNODE_IP@';
+my $SCRATCHDIR = '@FSDIR_SCRATCH@';
+
+#
+# Fixed paths for clients
+#
+my $PROJROOT    = "@PROJROOT_DIR@";
+my $GROUPROOT   = "@GROUPSROOT_DIR@";
+my $USERROOT    = "@USERSROOT_DIR@";
+my $SCRATCHROOT	= "@SCRATCHROOT_DIR@";
+my $SHAREROOT	= "@SHAREROOT_DIR@";
 
 #
 # Some programs we use
@@ -154,8 +164,11 @@ my $CISCO_MIB_FTP = "ftp://ftp.cisco.com/pub/mibs/v2";
 #
 my @TESTBED_DIRS = ($PREFIX);
 
-my @MOUNTPOINTS = ("$PREFIX/usersvar", "$PREFIX/opsdir",
-		   "/users", "/proj", "/groups", "/share");
+my @MOUNTPOINTS = ("$USERSVAR_DIR", "$OPSDIR_DIR",
+		   "$USERROOT", "$PROJROOT", "$GROUPROOT", "$SHAREROOT");
+if ($SCRATCHDIR) {
+    push(@MOUNTPOINTS, "$SCRATCHROOT");
+}
 
 my @LOGFILES = ("$LOGDIR/bootinfo.log", "$LOGDIR/tmcd.log",
     "$LOGDIR/capture.log", "$LOGDIR/dhcpd.log", "$LOGDIR/capserver.log",
@@ -810,15 +823,20 @@ Phase "NFSmounts", "Setting up NFS mounts", sub {
     };
     Phase "fstab", "Adding NFS mounts to $FSTAB", sub {
 	DoneIfEdited($FSTAB);
-	AppendToFileFatal($FSTAB,
-		"$FSNODE:/users\t\t/users\tnfs\trw,nodev,nosuid\t0\t0",
-		"$FSNODE:/proj\t\t/proj\tnfs\trw,nodev,nosuid\t0\t0",
-		"$FSNODE:/groups\t\t/groups\tnfs\trw,nodev,nosuid\t0\t0",
-		"$FSNODE:/share\t\t/share\tnfs\trw,nodev,nosuid\t0\t0",
-		"$USERNODE:/usr/testbed\t\t$OPSDIR_DIR\tnfs\trw,soft,".
-			  "-b,nodev,nosuid\t0\t0",
-		"$USERNODE:/var\t\t$USERSVAR_DIR\tnfs\tro,soft,".
-			  "-b,nodev,nosuid\t0\t0");
+	my @lines = ("$FSNODE:$USERROOT\t\t$USERROOT\tnfs\trw,nodev,nosuid\t0\t0",
+		     "$FSNODE:$PROJROOT\t\t$PROJROOT\tnfs\trw,nodev,nosuid\t0\t0",
+		     "$FSNODE:$GROUPROOT\t\t$GROUPROOT\tnfs\trw,nodev,nosuid\t0\t0",
+		     "$FSNODE:$SHAREROOT\t\t$SHAREROOT\tnfs\trw,nodev,nosuid\t0\t0");
+	if ($SCRATCHDIR) {
+	    push(@lines,
+		 "$FSNODE:$SCRATCHROOT\t\t$SCRATCHROOT\tnfs\trw,nodev,nosuid\t0\t0");
+	}
+	push(@lines,
+	     "$USERNODE:/usr/testbed\t\t$OPSDIR_DIR\tnfs\trw,soft,".
+	     "-b,nodev,nosuid\t0\t0",
+	     "$USERNODE:/var\t\t$USERSVAR_DIR\tnfs\tro,soft,".
+	     "-b,nodev,nosuid\t0\t0");
+	AppendToFileFatal($FSTAB, @lines);
     };
     Phase "mounts", "Mounting NFS filesystems", sub {
 	foreach my $dir (@MOUNTPOINTS) {
@@ -1160,14 +1178,14 @@ if ($BUGDBSUPPORT) {
 
 Phase "firstuser", "Setting up initial user (elabman)", sub {
     PhaseSkip("elabman already created")
-	if (-d "/users/elabman");
+	if (-d "$USERROOT/elabman");
     ExecQuietFatal("perl $TOP_OBJDIR/utils/firstuser -b ".
 		   (defined($password) ? " -p $password" : ""));
 };
 
 Phase "chkupuser", "Setting up checkup user (elabckup)", sub {
     PhaseSkip("elabckup already created")
-	if (-d "/users/elabckup");
+	if (-d "$USERROOT/elabckup");
     ExecQuietFatal("perl $TOP_OBJDIR/utils/firstuser -b ".
 		   (defined($password) ? " -p $password" : "").
 		   " -u elabckup -n 'Emulab Checkup User' ".
@@ -1181,7 +1199,7 @@ Phase "experiments", "Setting up system experiments", sub {
 	
 	Phase "$pid/$eid", "$pid/$eid", sub {
 	    PhaseSkip("Experiment Created")
-		if (-d "/proj/$pid/exp/$eid");
+		if (-d "$PROJROOT/$pid/exp/$eid");
 	    ExecQuietFatal("$SUDO -u elabman $WAP $BATCHEXP ".
 			   "  -q -i -w -f -n -S 'System Experiment' ".
 			   "  -L 'System Experiment' ".
diff --git a/install/fs-install.in b/install/fs-install.in
index 50617e9817..5b964c3234 100644
--- a/install/fs-install.in
+++ b/install/fs-install.in
@@ -31,6 +31,16 @@ my $LOGFACIL    = '@TBLOGFACIL@';
 my $ELABINELAB  = @ELABINELAB@;
 my $WINSUPPORT  = @WINSUPPORT@;
 my $QUOTA_FSLIST= '@FS_WITH_QUOTAS@';
+my $SCRATCHDIR =  '@FSDIR_SCRATCH@';
+
+#
+# Fixed paths for clients
+#
+my $PROJROOT    = "@PROJROOT_DIR@";
+my $GROUPROOT   = "@GROUPSROOT_DIR@";
+my $USERROOT    = "@USERSROOT_DIR@";
+my $SCRATCHROOT	= "@SCRATCHROOT_DIR@";
+my $SHAREROOT	= "@SHAREROOT_DIR@";
 
 # For /share export below.
 my $CONTROL_NETWORK = "@CONTROL_NETWORK@";
@@ -156,8 +166,15 @@ my $VARRUN        = "/var/run";
 my @LOGFILES           = ("/var/log/logins","/var/log/tiplogs/capture.log",
     "/var/log/mountd.log");
 
-my @TESTBED_DIRS       = ([$PREFIX, "0775"], ["/users", "0755"],
-    ["/proj", "0755"], ["/groups", "0755"], ["/share", "0775"]);
+my @TESTBED_DIRS       = ([$PREFIX, "0775"], [$USERROOT, "0755"],
+    [$PROJROOT, "0755"], [$GROUPROOT, "0755"], [$SHAREROOT, "0775"]);
+my @MOUNTPOINTS        = ($USERROOT, $PROJROOT, $GROUPROOT, $SHAREROOT);
+
+if ($SCRATCHDIR) {
+    push(@TESTBED_DIRS, [$SCRATCHROOT, "0755"]);
+    push(@MOUNTPOINTS, $SCRATCHROOT);
+}
+
 
 #
 # A few files we have to deal with
@@ -320,13 +337,14 @@ Phase "exports", "Setting up exports", sub {
 	# filesystems.  Note: we cannot do /share on the same exports line
 	# as the other filesystems because of the RO mount below (trust me).
 	#
-	my @dirs = ('/users','/groups','/proj');
-	@dirs = map {`realpath $_`} @dirs;
-	chomp @dirs;
 	my %filesystems;
-	foreach my $dir (@dirs) {
-	    my ($dev,@junk) = stat $dir;
-	    push @{$filesystems{$dev}}, $dir;
+	foreach my $dir (@MOUNTPOINTS) {
+	    if ($dir ne $SHAREROOT) {
+		my $dir = `realpath $dir`;
+		chomp($dir);
+		my $dev = (stat($dir))[0];
+		push @{$filesystems{$dev}}, $dir;
+	    }
 	}
 
 	#
@@ -344,7 +362,7 @@ Phase "exports", "Setting up exports", sub {
 	# but to the control network read-only.
 	#
 	my ($a,$b,$c,$d) = ($CONTROL_NETWORK =~ /^(\d*)\.(\d*)\.(\d*)\.(\d*)/);
-	my $realdir = `realpath /share`;
+	my $realdir = `realpath $SHAREROOT`;
 	chomp($realdir);
 	push(@exports_lines,
 	    "$realdir\t$BOSSNODE $USERNODE -maproot=root");
diff --git a/install/ops-install.in b/install/ops-install.in
index 64905c1a5c..b1bd61b9e4 100644
--- a/install/ops-install.in
+++ b/install/ops-install.in
@@ -23,7 +23,7 @@ my $PREFIX = '@prefix@';
 
 my @MAILING_LISTS = ("@TBOPSEMAIL@","@TBLOGSEMAIL@","@TBWWWEMAIL@",
     "@TBAPPROVALEMAIL@","@TBAUDITEMAIL@","@TBSTATEDEMAIL@",
-    "@TBTESTSUITEEMAIL@", "@TBERRORSEMAIL@");
+    "@TBTESTSUITEEMAIL@", "@TBERRORSEMAIL@", "@TBAUTOMAILEMAIL@");
 
 
 my $OURDOMAIN   = '@OURDOMAIN@';
@@ -45,6 +45,16 @@ my $QUOTA_FSLIST= '@FS_WITH_QUOTAS@';
 my $LOGDIR      = "$PREFIX/log";
 my $ETCDIR      = "$PREFIX/etc";
 my $LIBDIR      = "$PREFIX/lib";
+my $SCRATCHDIR =  '@FSDIR_SCRATCH@';
+
+#
+# Fixed paths for clients
+#
+my $PROJROOT    = "@PROJROOT_DIR@";
+my $GROUPROOT   = "@GROUPSROOT_DIR@";
+my $USERROOT    = "@USERSROOT_DIR@";
+my $SCRATCHROOT	= "@SCRATCHROOT_DIR@";
+my $SHAREROOT	= "@SHAREROOT_DIR@";
 
 # True if we are also the FS node
 my $ISFS	= ($USERNODE eq $FSNODE) ? 1 : 0;
@@ -199,10 +209,16 @@ my @LOGFILES           = ("/var/log/logins","/var/log/tiplogs/capture.log",
 my @LOCAL_MAILING_LISTS = grep(/$OURDOMAIN$/,@MAILING_LISTS);
 my @MAILING_LIST_NAMES  = map { /^([\w-]+)\@/ } @LOCAL_MAILING_LISTS;
 
-my @TESTBED_DIRS       = ([$PREFIX, "0775"], ["/users", "0755"],
-    ["/proj", "0755"], ["/groups", "0755"], ["/share", "0775"]);
+my @TESTBED_DIRS       = ([$PREFIX, "0775"], [$USERROOT, "0755"],
+			  [$PROJROOT, "0755"], [$GROUPROOT, "0755"],
+			  [$SHAREROOT, "0775"]);
 
-my @MOUNTPOINTS        = ("/users", "/proj", "/groups", "/share");
+my @MOUNTPOINTS        = ($USERROOT, $PROJROOT, $GROUPROOT, $SHAREROOT);
+
+if ($SCRATCHDIR) {
+    push(@TESTBED_DIRS, [$SCRATCHROOT, "0755"]);
+    push(@MOUNTPOINTS, $SCRATCHROOT);
+}
 
 #
 # A few files we have to deal with
@@ -558,9 +574,9 @@ Phase "exports", "Setting up exports", sub {
 	# filesystems.  Note: we cannot do /share on the same exports line
 	# as the other filesystems because of the RO mount below (trust me).
 	#
-	my @dirs = ('/var','/usr/testbed');
+	my @dirs = ('/var', $PREFIX);
 	if ($ISFS) {
-	    @dirs = ('/users','/groups','/proj',@dirs);
+	    @dirs = (grep(!/^$SHAREROOT$/, @MOUNTPOINTS), @dirs);
 	}
 	@dirs = map {`realpath $_`} @dirs;
 	chomp @dirs;
@@ -587,7 +603,7 @@ Phase "exports", "Setting up exports", sub {
 	    #
 	    my ($a,$b,$c,$d) =
 		($CONTROL_NETWORK =~ /^(\d*)\.(\d*)\.(\d*)\.(\d*)/);
-	    my $realdir = `realpath /share`;
+	    my $realdir = `realpath $SHAREROOT`;
 	    chomp($realdir);
 	    push(@exports_lines,
 		 "$realdir\t$BOSSNODE -maproot=root");
@@ -601,15 +617,15 @@ Phase "exports", "Setting up exports", sub {
 	#
 	if ($CVSSUPPORT) {
 	    my $pdir;
-	    my $dfout = `df /proj | fgrep '/dev'`;
+	    my $dfout = `df $PROJROOT | fgrep '/dev'`;
 	    if ($?) {
-		PhaseFail("'df /proj' failed!");
+		PhaseFail("'df $PROJROOT' failed!");
 	    }
 	    if ($dfout =~ /\s+([\/\w]*)$/) {
 		$pdir = $1;
 	    }
 	    else {
-		PhaseFail("Could not determine where /proj is mounted!");
+		PhaseFail("Could not determine where $PROJROOT is mounted!");
 	    }
 	    push(@exports_lines, "$pdir\tlocalhost -alldirs");
 	}
@@ -645,11 +661,16 @@ Phase "NFSmounts", "Setting up NFS mounts", sub {
     };
     Phase "fstab", "Adding NFS mounts to $FSTAB", sub {
 	DoneIfEdited($FSTAB);
-	AppendToFileFatal($FSTAB,
-		"$FSNODE:/users\t\t/users\tnfs\trw,nodev,nosuid\t0\t0",
-		"$FSNODE:/proj\t\t/proj\tnfs\trw,nodev,nosuid\t0\t0",
-		"$FSNODE:/groups\t\t/groups\tnfs\trw,nodev,nosuid\t0\t0",
-		"$FSNODE:/share\t\t/share\tnfs\trw,nodev,nosuid\t0\t0");
+	my @lines = ("$FSNODE:$USERROOT\t\t$USERROOT\tnfs\trw,nodev,nosuid\t0\t0",
+		     "$FSNODE:$PROJROOT\t\t$PROJROOT\tnfs\trw,nodev,nosuid\t0\t0",
+		     "$FSNODE:$GROUPROOT\t\t$GROUPROOT\tnfs\trw,nodev,nosuid\t0\t0",
+		     "$FSNODE:$SHAREROOT\t\t$SHAREROOT\tnfs\trw,nodev,nosuid\t0\t0");
+	if ($SCRATCHDIR) {
+	    push(@lines,
+		 "$FSNODE:$SCRATCHROOT\t\t$SCRATCHROOT\tnfs\trw,nodev,nosuid\t0\t0");
+	}
+
+	AppendToFileFatal($FSTAB, @lines);
     };
     Phase "mounts", "Mounting NFS filesystems", sub {
 	foreach my $dir (@MOUNTPOINTS) {
@@ -1076,7 +1097,7 @@ if ($CVSSUPPORT) {
     my $CVSDHEAD  = "$LIBDIR/cvsd.conf.head";
     my $CVSDJAIL  = "/var/cvsjail";
     my $BUILDROOT = "/usr/local/sbin/cvsd-buildroot";
-    my $REPOSDIR  = "/proj/cvsrepos";
+    my $REPOSDIR  = "$PROJROOT/cvsrepos";
     
     Phase "cvsd", "Installing cvsd", sub {
 	PhaseSkip("cvsd not supported on ops")
diff --git a/install/smb.conf.head.in b/install/smb.conf.head.in
index 9b4a94a454..4f4642513a 100644
--- a/install/smb.conf.head.in
+++ b/install/smb.conf.head.in
@@ -177,7 +177,7 @@ log level = 2
 
 [share]
                 comment         = Shared files
-                path            = /share
+                path            = @SHAREROOT_DIR@
 		browsable	= yes
                 writable        = no
 #                create mask     = 0770
diff --git a/sensors/canaryd/feedbacklogs.in b/sensors/canaryd/feedbacklogs.in
index 82f617bf6f..356f2f8034 100644
--- a/sensors/canaryd/feedbacklogs.in
+++ b/sensors/canaryd/feedbacklogs.in
@@ -20,6 +20,8 @@ PACKAGE_VERSION = 0.1
 XMLRPC_SERVER = "@BOSSNODE@"
 XMLRPC_PORT   = 3069
 
+PROJROOT = "@PROJROOT_DIR@"
+
 try:
     pw = pwd.getpwuid(os.getuid())
 except KeyError:
@@ -72,7 +74,7 @@ except getopt.error, e:
 
 pid, eid = req_args
 
-expdir = os.path.join("/proj", pid, "exp", eid)
+expdir = os.path.join(PROJROOT, pid, "exp", eid)
 
 sys.path.append(os.path.join(prefix, "lib"))
 from sshxmlrpc import *
diff --git a/sensors/slothd/webfeedback.in b/sensors/slothd/webfeedback.in
index 45df2059c6..675ad05bf0 100644
--- a/sensors/slothd/webfeedback.in
+++ b/sensors/slothd/webfeedback.in
@@ -2,7 +2,7 @@
 
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2004, 2005 University of Utah and the Flux Group.
+# Copyright (c) 2004, 2005, 2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 
@@ -156,8 +156,7 @@ if ($UID) {
     }
 }
 
-# XXX
-$expdir = "/proj/$pid/exp/$eid";
+my $expdir = TBExptUserDir($pid, $eid);
 
 # Figure out which mode we are in and act accordingly.
 if ($mode eq "clear") {
@@ -181,8 +180,7 @@ if ($mode eq "record") {
 
     # Just need to send a START event to all of the canaryds.
     my $handle = event_register_withkeyfile("elvin://boss",
-					    0,
-					    "/proj/$pid/exp/$eid/tbdata/eventkey");
+					    0, "$expdir/tbdata/eventkey");
     if (!$handle) { die "Unable to register with event system\n"; }
 
     my $tuple = address_tuple_alloc();
diff --git a/tbsetup/archive_control.in b/tbsetup/archive_control.in
index c3c4437579..318e680afc 100755
--- a/tbsetup/archive_control.in
+++ b/tbsetup/archive_control.in
@@ -52,6 +52,8 @@ use libtestbed;
 use libaudit;
 use libArchive;
 
+my $SHAREROOT = SHAREROOT();
+
 #
 # Verify user and get his DB uid.
 #
@@ -365,7 +367,7 @@ elsif ($action eq "list" ||
     while (my ($fn) = $query_result->fetchrow_array()) {
 	#print "Accessed: $fn\n";
 	
-	if ($fn =~ /^\/share/ ||
+	if ($fn =~ /^$SHAREROOT/ ||
 	    $fn =~ /\/tbdata\// ||
 	    $fn =~ /^<u:/) {
 	    next;
diff --git a/tbsetup/batch_daemon.in b/tbsetup/batch_daemon.in
index 0daf991ecd..0097e39579 100644
--- a/tbsetup/batch_daemon.in
+++ b/tbsetup/batch_daemon.in
@@ -65,7 +65,6 @@ my $endexp   = "$TB/bin/endexp";
 my $savelogs = "$TB/bin/savelogs";
 my $avail    = "$TB/sbin/avail";
 my $batchlog = "$TB/log/batchlog";
-my $projroot = "/proj";
 my $debug    = 0;
 
 # New template stuff.
diff --git a/tbsetup/batchexp.in b/tbsetup/batchexp.in
index 20e2fbe0c3..aed8164a03 100755
--- a/tbsetup/batchexp.in
+++ b/tbsetup/batchexp.in
@@ -85,7 +85,6 @@ my $copydir;		# Directory extracted from archive, to delete.
 # Configure variables
 #
 my $TB       = "@prefix@";
-my $PROJROOT = "/proj";
 my $EVENTSYS = @EVENTSYS@;
 my $TBOPS    = "@TBOPSEMAIL@";
 my $TBLOGS   = "@TBLOGSEMAIL@";
@@ -938,7 +937,8 @@ sub ParseArgs()
 	}
 
 	#
-	# Called from ops interactively. Make sure NS file in /proj or /users.
+	# Called from ops interactively. Make sure NS file resides in an
+	# appropriate location.
 	#
 	# Use realpath to resolve any symlinks.
 	#
@@ -953,17 +953,18 @@ sub ParseArgs()
 	}
 
 	#
-	# The file must reside in /proj, /groups, or /users. Since this script
-	# runs as the caller, regular file permission checks ensure its a file
-	# the user is allowed to use. /tmp/$pid-$eid.nsfile.XXXXX also allowed
-	# since this script is invoked directly from web interface.
+	# The file must reside in an acceptible location. Since this script
+	# runs as the caller, regular file permission checks ensure it is a
+	# file the user is allowed to use.  So we don't have to be too tight
+	# with the RE matching /tmp and /var/tmp files.  Note that
+	# /tmp/$guid-$nsref.nsfile is also allowed since this script is
+	# invoked directly from web interface which generates a name that
+	# should not be guessable.
 	#
 	if (! ($tempnsfile =~ /^\/tmp\/[-\w]+-\d+\.nsfile/) &&
 	    ! ($tempnsfile =~ /^\/tmp\/\d+\.ns/) &&
 	    ! ($tempnsfile =~ /^\/var\/tmp\/php\w+/) &&
-	    ! ($tempnsfile =~ /^\/proj/) &&
-	    ! ($tempnsfile =~ /^\/groups/) &&
-	    ! ($tempnsfile =~ /^\/users/)) {
+	    ! TBValidUserDir($tempnsfile, 0)) {
 	    tberror({type => 'primary', severity => SEV_ERROR,
 		     error => ['disallowed_directory', $tempnsfile]},
 		    "$tempnsfile does not resolve to an allowed directory!");
diff --git a/tbsetup/checkup/checkup_daemon.in b/tbsetup/checkup/checkup_daemon.in
index 5a69202661..d4f40753f3 100644
--- a/tbsetup/checkup/checkup_daemon.in
+++ b/tbsetup/checkup/checkup_daemon.in
@@ -50,7 +50,7 @@ $libdb::DBQUERY_MAXTRIES = 30;
 my $NODEDEAD_PID= NODEDEAD_PID;
 my $NODEDEAD_EID= NODEDEAD_EID;
 my $TBOPSPID= TBOPSPID;
-my $HOME= "/users/elabckup";
+my $HOME= USERROOT() . "/elabckup";
 
 sub fatal($);
 sub notify($);
@@ -204,7 +204,8 @@ MAINLOOP: while(1) {
 	    }
 	    elsif ($state eq "running") {
 		# Check the experiment's state.
-		my $report = "/proj/$TBOPSPID/exp/$eid/logs/report.mail";
+		my $report = PROJROOT() .
+		    "/$TBOPSPID/exp/$eid/logs/report.mail";
 
 		if (-e $report) {
 		    # Failed...
diff --git a/tbsetup/elabinelab.in b/tbsetup/elabinelab.in
index 3429032119..90f3eabe08 100644
--- a/tbsetup/elabinelab.in
+++ b/tbsetup/elabinelab.in
@@ -79,7 +79,6 @@ if ($EUID != 0) {
 $libdb::DBQUERY_MAXTRIES = 30;
 
 # Locals
-my $PROJROOT	= PROJROOT();
 my $SAVEUID     = $UID;
 my $workdir;
 my %noderoles	= ();
@@ -294,8 +293,9 @@ $UID = 0;
 #
 my $mkelab = "$TB/etc/rc.mkelab";
 
-if (-e "/proj/$pid/exp/$eid/rc.mkelab") {
-    $mkelab = "/proj/$pid/exp/$eid/rc.mkelab";
+my $expdir = PROJROOT() . "/$pid/exp/$eid";
+if (-e "$expdir/rc.mkelab") {
+    $mkelab = "$expdir/rc.mkelab";
 }
 print "Copying $mkelab to ${bossnode}/${opsnode}";
 print "/${fsnode}"
@@ -914,7 +914,7 @@ sub DumpDBGoo()
     $UID = 0;
     system("tar cf - -C $statedir . | ".
 	   "   gzip | $SSH -F /dev/null -host $CONTROL ".
-	   "   '(cat > /$PROJROOT/$pid/exp/$eid/dbstate.tar.gz)'");
+	   "   '(cat > $expdir/dbstate.tar.gz)'");
     if ($?) {
 	die("*** $0:\n".
 	    "    Could not create dbstate.tar.gz\n");
diff --git a/tbsetup/endexp.in b/tbsetup/endexp.in
index 3acca1e3db..03910290bc 100755
--- a/tbsetup/endexp.in
+++ b/tbsetup/endexp.in
@@ -88,7 +88,6 @@ use Experiment;
 $libdb::DBQUERY_MAXTRIES = 0;
 
 my $tbdir       = "$TB/bin/";
-my $projroot    = "/proj";
 my $tbdata      = "tbdata";
 my $archcontrol = "$TB/bin/archive_control";
 my $nextstate;
diff --git a/tbsetup/eventsys.proxy.in b/tbsetup/eventsys.proxy.in
index c7efde4adf..be5289f2ea 100644
--- a/tbsetup/eventsys.proxy.in
+++ b/tbsetup/eventsys.proxy.in
@@ -12,7 +12,8 @@ use Errno;
 use POSIX ":sys_wait_h";
     
 #
-# A wrapper for controlling the event scheduler from boss. 
+# A wrapper for controlling from boss the event scheduler running on ops.
+# This wrapper runs on ops.
 #
 # The first argument option is the user to run this script as, since we
 # get invoked by a root ssh from boss. 
@@ -120,7 +121,7 @@ else {
     usage();
 }
 $PIDFILE = "$PIDDIR/${pid}_${eid}.pid";
-$EXPDIR  = "/proj/$pid/exp/$eid";
+$EXPDIR  = PROJROOT() . "/$pid/exp/$eid";
 
 #
 # Deal with stop and replay.
@@ -193,7 +194,7 @@ if (! -d $LOGDIR) {
 }
 
 #
-# Create a chile whose output is directed into the logfile. Parents waits
+# Create a child whose output is directed into the logfile. Parent waits
 # a moment and then exits.
 #
 if (-e $logfile) {
diff --git a/tbsetup/exports_setup.in b/tbsetup/exports_setup.in
index 8b819d64ef..25cb185345 100644
--- a/tbsetup/exports_setup.in
+++ b/tbsetup/exports_setup.in
@@ -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.
 #
 
@@ -35,6 +35,7 @@ my $FSNODE      = "@FSNODE@";
 my $projdir     = "@FSDIR_PROJ@";
 my $usersdir    = "@FSDIR_USERS@";
 my $groupdir    = "@FSDIR_GROUPS@";
+my $scratchdir  = "@FSDIR_SCRATCH@";
 my $DISABLED	= "@DISABLE_EXPORTS_SETUP@";
 my $WINSUPPORT  = @WINSUPPORT@;
 
@@ -93,6 +94,7 @@ use libtestbed;
 my $PROJROOT  = PROJROOT();
 my $GROUPROOT = GROUPROOT();
 my $USERROOT  = USERROOT();
+my $SCRATCHROOT  = SCRATCHROOT();
 
 #
 # We need to serialize this script to avoid a trashed map file.
@@ -221,6 +223,11 @@ while (@row = $nodes_result->fetchrow_array) {
 	    }
 	}
 
+	if ($scratchdir && -d "$SCRATCHROOT/$pid") {
+	    push(@dirlist, "$scratchdir/$pid");
+            push(@smbshares, ["scratch-$pid", "$scratchdir/$pid"]);
+	}
+
 	# Determine the users that can access this node, and add those
 	# users' directories to the list.
 	# XXX needs to be fixed for shared experiments?
diff --git a/tbsetup/fetchtar.proxy.in b/tbsetup/fetchtar.proxy.in
index 72ef418780..e3f74cf177 100644
--- a/tbsetup/fetchtar.proxy.in
+++ b/tbsetup/fetchtar.proxy.in
@@ -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.
 #
 
@@ -12,7 +12,8 @@ use BSD::Resource;
 use POSIX qw(:signal_h);
     
 #
-# Fetch tarballs and RPMs on behalf of a user
+# Fetch tarballs and RPMs on behalf of a user.
+# This script runs on ops.
 #
 sub usage()
 {
@@ -28,6 +29,8 @@ my $TB       = "@prefix@";
 my $TBOPS    = "@TBOPSEMAIL@";
 my $FSDIR_PROJ = "@FSDIR_PROJ@";
 my $FSDIR_GROUPS = "@FSDIR_GROUPS@";
+my $FSDIR_SCRATCH = "@FSDIR_SCRATCH@";
+my $ISFS = ("@USERNODE_IP@" eq "@FSNODE_IP@") ? 1 : 0;
 
 # Locals
 my $WGET = "/usr/local/bin/wget";
@@ -101,10 +104,12 @@ if (!($localfile =~ /^([\w\.\_\-+\/]+)$/)) {
     $localfile = $1;
     my $realpath = `$REALPATH $localfile`;
     chomp $realpath;
-    if ($realpath !~ /^(($FSDIR_PROJ|$FSDIR_GROUPS)\/.*)$/) {
+    if (!TBValidUserDir($realpath, $ISFS)) {
 	die("*** $0:\n".
-	    "    Local file must be in /proj or /groups: $localfile, $realpath \n");
-    } else {
+	    "    Local file must be in one of " .
+	    join(' or ', TBValidUserDirList()) . ".\n");
+    }
+    if ($realpath =~ /^([\w\.\_\-+\/]+)$/) {
 	$localfile = $1;
     }
 }
@@ -155,7 +160,7 @@ if (! $pid) {
     # Give parent a chance to react.
     sleep(1);
 
-    exec("nice -15 $WGET --timestamping --non-verbose -O $localfile $URL");
+    exec("nice -15 $WGET --timestamping --no-verbose -O $localfile $URL");
     die("Could not exec wget!\n");
 }
 
@@ -174,16 +179,18 @@ if (($exit_status & 0xff) == SIGKILL) {
 
     SENDMAIL($TBOPS, "wget Exceeded CPU Limit", $msg);
     
-    print STDERR "$msg\n";
+    print STDERR "*** $msg\n";
     exit(15);
+} elsif ($exit_status) {
+    print STDERR "*** wget exited with status $exit_status\n";
+} else {
+    #
+    # Change the permissions on the file so that other group members can
+    # overwrite it
+    #
+    system("$CHMOD g+w $localfile") == 0
+	or die ("*** ERROR - Unable to change permissions on $localfile!");
 }
 
-#
-# Change the permissions on the file so that other group members can overwrite
-# it
-#
-system("$CHMOD g+w $localfile") == 0
-    or die ("*** ERROR - Unable to change permissions on $localfile!");
-
 exit($exit_status >> 8);
 
diff --git a/tbsetup/libArchive.pm.in b/tbsetup/libArchive.pm.in
index 9077340227..393c403791 100644
--- a/tbsetup/libArchive.pm.in
+++ b/tbsetup/libArchive.pm.in
@@ -51,10 +51,15 @@ my $inittag     = 'root';
 my $defaultview = 'head';
 my $debug       = 0;
 my $svnopt      = ($debug ? "" : "-q");
-my %ROOTS       = ("proj"   => "proj",
-		   "users"  => "users",
-		   "share"  => "share",
-		   "groups" => "groups");
+my $SHAREROOT   = SHAREROOT();
+my $SCRATCHROOT = SCRATCHROOT();
+my %ROOTS       = (PROJROOT()  => "proj",
+		   USERROOT()  => "users",
+		   $SHAREROOT  => "share",
+		   GROUPROOT() => "groups");
+if ($SCRATCHROOT) {
+    $ROOTS{$SCRATCHROOT} = "scratch";
+}
 
 my $TAGTYPE_USER      = "user";
 my $TAGTYPE_COMMIT    = "commit";
@@ -303,7 +308,7 @@ sub ValidatePath($)
     # the rootdir so we can copy it in.
     #
     if ($realpath =~ /^[\/]+(\w+)\/(.+)$/) {
-	$rootdir  = $1;
+	$rootdir  = "/$1";
 	$pathname = $2;
     }
     else {
@@ -366,7 +371,7 @@ sub ArchiveAdd($$;$$$)
 	# Last part of path must be a directory.
 	#
 	if (! -d $pathname) {
-	    print STDERR "ArchiveAdd: Must be a direcotory: $pathname\n";
+	    print STDERR "ArchiveAdd: Must be a directory: $pathname\n";
 	    return -1;
 	}
 	my ($filename,$directory,undef) = fileparse($pathname);
@@ -1466,7 +1471,7 @@ sub TBExperimentArchiveAddTracedFiles($$)
 		     "LEFT JOIN accessed_files as af on af.idx=fr.fileidx ".
 		     "WHERE fr.rsrcidx=$rsrcidx and fr.type!='l'");
     while (my ($fn) = $query_result->fetchrow_array()) {
-	if ($fn =~ /^\/share/ ||
+	if ($fn =~ /^$SHAREROOT/ ||
 	    $fn =~ /^<u:/) {
 	    next;
 	}
diff --git a/tbsetup/libosload.pm.in b/tbsetup/libosload.pm.in
index 6fcd7f9add..ae8b2d46de 100755
--- a/tbsetup/libosload.pm.in
+++ b/tbsetup/libosload.pm.in
@@ -32,6 +32,7 @@ my $TB		= "@prefix@";
 my $TESTMODE    = @TESTMODE@;
 my $TBOPS       = "@TBOPSEMAIL@";
 my $ELABINELAB  = @ELABINELAB@;
+my $PROJROOT    = "@PROJROOT_DIR@";
 
 # Max number of retries (per node) before its deemed fatal. This allows
 # for the occasional pxeboot failure.
@@ -966,7 +967,7 @@ sub osload_setupswapinfo($$;@)
 	}
 
 	# XXX backward compat
-	my $infodir = "/proj/$rpid/exp/$reid/swapinfo";
+	my $infodir = "/$PROJROOT/$rpid/exp/$reid/swapinfo";
 	if (! -d "$infodir" && !mkdir($infodir, 0770)) {
 	    print "*** swapinfo: no swap info directory $infodir!\n";
 	    next
@@ -989,7 +990,7 @@ sub osload_setupswapinfo($$;@)
 	if ($imagepid eq TBOPSPID()) {
 	    $sigdir = "$TB/images/sigs";
 	} else {
-	    $sigdir = "/proj/$imagepid/images/sigs";
+	    $sigdir = "/$PROJROOT/$imagepid/images/sigs";
 	}
 	$signame = "$imageid.ndz.sig";
 	$signame =~ s/^$imagepid-//;
@@ -1051,7 +1052,7 @@ sub osload_setupswapinfo($$;@)
     # in an experiment (else we don't know whether a sig is used or not).
     #
     if ($allnodes) {
-	my $infodir = "/proj/$pid/exp/$eid/swapinfo";
+	my $infodir = "/$PROJROOT/$pid/exp/$eid/swapinfo";
 	my @allsigs = `ls $infodir/*.sig`;
 	chomp(@allsigs);
 	for my $sig (@allsigs) {
diff --git a/tbsetup/libtestbed.pm.in b/tbsetup/libtestbed.pm.in
index 2ea7cf16e7..153d6448d2 100644
--- a/tbsetup/libtestbed.pm.in
+++ b/tbsetup/libtestbed.pm.in
@@ -16,7 +16,9 @@ use Exporter;
 	 TBDebugTimeStampsOn TBForkCmd TB_BOSSEVENTPORT TB_EVENTSERVER
 	 TBScriptLock TBScriptUnlock TBTimeStampWithDate
 	 TBSCRIPTLOCK_OKAY TBSCRIPTLOCK_TIMEDOUT
-	 TBSCRIPTLOCK_IGNORE TBSCRIPTLOCK_FAILED);
+	 TBSCRIPTLOCK_IGNORE TBSCRIPTLOCK_FAILED
+	 PROJROOT GROUPROOT USERROOT SCRATCHROOT SHAREROOT
+	 TBValidUserDir TBValidUserDirList);
 
 # After package decl.
 use English;
@@ -35,6 +37,31 @@ my $TIMESTAMPS = "@TIMESTAMPS@";
 my $TBOPSEMAIL = "@TBOPSEMAIL@";
 my $SCRIPTNAME = "Unknown";
 
+#
+# Real mount points (on the fileserver) for exported directories.
+# At the moment, we have no reason to export these via functions.
+#
+my $FSDIR_USER = "@FSDIR_USERS@";
+my $FSDIR_PROJ = "@FSDIR_PROJ@";
+my $FSDIR_GROUPS = "@FSDIR_GROUPS@";
+my $FSDIR_SHARE = "@FSDIR_SHARE@";
+my $FSDIR_SCRATCH = "@FSDIR_SCRATCH@";
+
+#
+# Standard mountpoints for exported directories.
+# The scratch directory is optional, hence the FSDIR_SCRATCH check.
+#
+my $PROJROOT    = "@PROJROOT_DIR@";
+my $GROUPROOT   = "@GROUPSROOT_DIR@";
+my $USERROOT    = "@USERSROOT_DIR@";
+my $SCRATCHROOT	= "@SCRATCHROOT_DIR@";
+my $SHAREROOT	= "@SHAREROOT_DIR@";
+sub PROJROOT()	  { $PROJROOT; }
+sub GROUPROOT()	  { $GROUPROOT; }
+sub USERROOT()	  { $USERROOT; }
+sub SHAREROOT()   { $SHAREROOT; }
+sub SCRATCHROOT() { $FSDIR_SCRATCH ? $SCRATCHROOT : ""; }
+
 # Hostname of our boss node
 sub TB_BOSSNODE()	{ $BOSSNODE; }
 
@@ -335,6 +362,157 @@ sub TBForkCmd($;$) {
     return(0);
 }
 
+#
+# Determine if a user-specified path falls within the standard
+# user-accessible directories.
+#
+# If $userealpath is non-zero this will use the real filesystem path on the
+# fileserver (e.g., /q/proj) rather than the conventional mount point (/proj).
+# Obviously, this option should only be used when run on the fileserver.
+#
+sub TBValidUserDir($$;$$$)
+{
+    my ($path, $userealpath, $uid, $pid, $gid, $eid) = @_;
+    my ($uroot, $proot, $groot, $sroot);
+
+    #
+    # Decide whether to test against the "real" (server-side) path
+    # or the user-visible mount point.
+    #
+    if ($userealpath) {
+	$uroot = $FSDIR_USER;
+	$proot = $FSDIR_PROJ;
+	$groot = $FSDIR_GROUPS;
+	if ($FSDIR_SCRATCH) {
+	    $sroot = $FSDIR_SCRATCH;
+	}
+    } else {
+	$uroot = $USERROOT;
+	$proot = $PROJROOT;
+	$groot = $GROUPROOT;
+	if ($FSDIR_SCRATCH) {
+	    $sroot = $SCRATCHROOT;
+	}
+    }
+
+    #
+    # No ids specified, just make sure it starts with an appropriate prefix.
+    #
+    if (!$uid && !$pid && !$gid && !$eid) {
+	if ($path =~ /^$proot\// ||
+	    $path =~ /^$uroot\// ||
+	    $path =~ /^$groot\//) {
+	    return 1;
+	}
+	if (defined($sroot) && $path =~ /^$sroot\//) {
+	    return 1;
+	}
+
+	return 0;
+    }
+    #
+    # Otherwise check for specific directories based on:
+    #
+    #	$uid		/users/$uid
+    #
+    #	$pid		/proj/$pid,
+    #			/scratch/$pid (if present)
+    #
+    #	$pid+$gid	/proj/$pid,
+    #			/groups/$pid/$gid,
+    #			/scratch/$pid (if present)
+    #
+    #	$pid+$eid	/proj/$pid/exp/$eid
+    #
+    #	$pid+$gid+$eid	/groups/$pid/$gid/exp/$eid
+    #
+    if ($uid) {
+	if ($path =~ /^$uroot\/$uid\//) {
+	    return 1;
+	}
+    }
+    if ($pid) {
+	if ($eid) {
+	    if ($gid) {
+		if ($path =~ /^$groot\/$pid\/$gid\/exp\/$eid\//) {
+		    return 1;
+		}
+	    } else {
+		if ($path =~ /^$proot\/$pid\/exp\/$eid\//) {
+		    return 1;
+		}
+	    }
+	} else {
+	    if ($path =~ /^$proot\/$pid\//) {
+		return 1;
+	    }
+	    if ($gid) {
+		if ($path =~ /^$groot\/$pid\/$gid\//) {
+		    return 1;
+		}
+	    }
+
+	    #
+	    # XXX /scratch is currently just per-project
+	    #
+	    if (defined($sroot)) {
+		if ($path =~ /^$sroot\/$pid\//) {
+		    return 1;
+		}
+	    }
+	}
+    }
+
+    return 0;
+}
+
+#
+# Return a list of valid directories based on the specified
+# $uid, $pid, $gid, $eid.  Used for error messages.
+#
+sub TBValidUserDirList(;$$$$)
+{
+    my ($uid, $pid, $gid, $eid) = @_;
+    my @dirs;
+
+    if (!$uid && !$pid && !$gid && !$eid) {
+	@dirs = ($USERROOT, $PROJROOT, $GROUPROOT);
+	if ($FSDIR_SCRATCH) {
+	    push(@dirs, $SCRATCHROOT);
+	}
+	return join(", ", @dirs);
+    }
+
+    if ($uid) {
+	push(@dirs, "$USERROOT/$uid");
+    }
+
+    if ($pid) {
+	if ($eid) {
+	    if ($gid) {
+		push(@dirs, "$GROUPROOT/$pid/$gid/exp/$eid");
+	    } else {
+		push(@dirs, "$PROJROOT/$pid/exp/$eid");
+	    }
+	} else {
+	    push(@dirs, "$PROJROOT/$pid");
+	    # don't confuse for $pid==$gid (/groups/$pid/$gid is /proj/$pid)
+	    if ($gid && $gid ne $pid) {
+		push(@dirs, "$GROUPROOT/$pid/$gid");
+	    }
+
+	    #
+	    # XXX /scratch is currently just per-project
+	    #
+	    if ($FSDIR_SCRATCH) {
+		push(@dirs, "$SCRATCHROOT/$pid");
+	    }
+	}
+    }
+
+    return @dirs;
+}
+
 #
 # Serialize an operation (script).
 #
diff --git a/tbsetup/mkgroup.in b/tbsetup/mkgroup.in
index 942f00a775..13e6435898 100644
--- a/tbsetup/mkgroup.in
+++ b/tbsetup/mkgroup.in
@@ -14,8 +14,7 @@ use Getopt::Std;
 # entries and the group directory. Runs in the foreground all the time;
 # Its quick enough that the user can wait for it.
 #
-# XXX - /proj wired in
-#       control node wired in.
+# XXX - control node wired in.
 #
 sub usage()
 {
@@ -38,8 +37,6 @@ my $ELABINELAB  = @ELABINELAB@;
 my $MAILMANSUPPORT= @MAILMANSUPPORT@;
 my $BUGDBSUPPORT= @BUGDBSUPPORT@;
 my $OPSDBSUPPORT= @OPSDBSUPPORT@;
-my $PROJROOT    = "/proj";
-my $GRPROOT     = "/groups";
 my $SSH         = "$TB/bin/sshtb";
 my $ADDMMLIST   = "$TB/sbin/addmmlist";
 my $OPSDBCONTROL= "$TB/sbin/opsdb_control";
@@ -82,6 +79,9 @@ use libaudit;
 use libdb;
 use libtestbed;
 
+my $PROJROOT    = PROJROOT();
+my $GRPROOT     = GROUPROOT();
+
 #
 # We don't want to run this script unless its the real version.
 #
diff --git a/tbsetup/mkproj.in b/tbsetup/mkproj.in
index b8df8cd766..4dbce9a4bc 100755
--- a/tbsetup/mkproj.in
+++ b/tbsetup/mkproj.in
@@ -40,10 +40,6 @@ my $ADDBUGDBPROJ= "$TB/sbin/addbugdbproj";
 my $ADDMMLIST   = "$TB/sbin/addmmlist";
 my $OPSDBCONTROL= "$TB/sbin/opsdb_control";
 	  
-my $PROJROOT = "/proj";
-my $GRPROOT  = "/groups";
-my $TFTPROOT = "/tftpboot";
-my $CVSREPOS = "$PROJROOT/cvsrepos";
 my @DIRLIST  = ("exp", "images", "logs", "deltas", "tarfiles", "rpms",
 		"groups", "tiplogs", "images/sigs", "templates");
 my $projhead;
@@ -67,6 +63,16 @@ use libaudit;
 use libdb;
 use libtestbed;
 
+my $PROJROOT     = PROJROOT();
+my $GRPROOT      = GROUPROOT();
+my $SCRATCHROOT  = SCRATCHROOT();
+
+#
+# XXX semi-hardwired, oddball paths
+#
+my $TFTPDIR  = "/tftpboot/$PROJROOT";
+my $CVSREPOS = "$PROJROOT/cvsrepos";
+
 #
 # We don't want to run this script unless its the real version.
 #
@@ -190,6 +196,19 @@ if (! -e "$PROJROOT/$pid") {
 	fatal("Could not chown $PROJROOT/$pid to $uid/$gid: $!");
     }
 }
+if ($SCRATCHROOT && ! -e "$SCRATCHROOT/$pid") {
+    if (! mkdir("$SCRATCHROOT/$pid", 0770)) {
+	fatal("Could not make directory $SCRATCHROOT/$pid: $!");
+    }
+
+    if (! chmod(0770, "$SCRATCHROOT/$pid")) {
+	fatal("Could not chmod directory $SCRATCHROOT/$pid: $!");
+    }
+
+    if (! chown($uid, $gid, "$SCRATCHROOT/$pid")) {
+	fatal("Could not chown $SCRATCHROOT/$pid to $uid/$gid: $!");
+    }
+}
 
 #
 # Make project subdirs.
@@ -211,15 +230,15 @@ foreach my $dir (@DIRLIST) {
 #
 # Create a tftp directory for oskit kernels.
 #
-if (! -e "$TFTPROOT/proj/$pid") {
-    if (! mkdir("$TFTPROOT/proj/$pid", 0770)) {
-	fatal("Could not make directory $TFTPROOT/proj/$pid: $!");
+if (! -e "$TFTPDIR/$pid") {
+    if (! mkdir("$TFTPDIR/$pid", 0770)) {
+	fatal("Could not make directory $TFTPDIR/$pid: $!");
     }
-    if (! chmod(0777, "$TFTPROOT/proj/$pid")) {
-	fatal("Could not chmod directory $TFTPROOT/proj/$pid: $!");
+    if (! chmod(0777, "$TFTPDIR/$pid")) {
+	fatal("Could not chmod directory $TFTPDIR/$pid: $!");
     }
-    if (! chown($uid, $gid, "$TFTPROOT/proj/$pid")) {
-	fatal("Could not chown $TFTPROOT/proj/$pid to $uid/$gid: $!");
+    if (! chown($uid, $gid, "$TFTPDIR/$pid")) {
+	fatal("Could not chown $TFTPDIR/$pid to $uid/$gid: $!");
     }
 }
 
diff --git a/tbsetup/nfstrace.in b/tbsetup/nfstrace.in
index de6c512e3a..0e9761c2f9 100644
--- a/tbsetup/nfstrace.in
+++ b/tbsetup/nfstrace.in
@@ -22,6 +22,7 @@ my $FSDIR_GROUPS = "@FSDIR_GROUPS@";
 my $FSDIR_PROJ = "@FSDIR_PROJ@";
 my $FSDIR_USERS = "@FSDIR_USERS@";
 my $FSDIR_SHARE = "@FSDIR_SHARE@";
+my $FSDIR_SCRATCH = "@FSDIR_SCRATCH@";
 
 # Note no -n option. We redirect stdin from the new exports file below.
 my $SSH		= "$TB/bin/sshtb -l root -host $FSNODE";
@@ -113,6 +114,12 @@ use libdb;
 use libtestbed;
 use libArchive;
 
+my $PROJROOT    = PROJROOT();
+my $GROUPROOT   = GROUPROOT();
+my $USERROOT    = USERROOT();
+my $SHAREROOT   = SHAREROOT();
+my $SCRATCHROOT = SCRATCHROOT();
+
 $UID = 0;
 
 if ($cmd eq "add") {
@@ -156,10 +163,13 @@ if ($cmd eq "get" || $cmd eq "transfer") {
 	}
 	($exptidx, $rsrcidx) = $query_result->fetchrow_array();
     }
-    $cmdline .= " -m $FSDIR_GROUPS:/groups";
-    $cmdline .= " -m $FSDIR_PROJ:/proj";
-    $cmdline .= " -m $FSDIR_USERS:/users";
-    $cmdline .= " -m $FSDIR_SHARE:/share";
+    $cmdline .= " -m $FSDIR_GROUPS:$GROUPROOT";
+    $cmdline .= " -m $FSDIR_PROJ:$PROJROOT";
+    $cmdline .= " -m $FSDIR_USERS:$USERROOT";
+    $cmdline .= " -m $FSDIR_SHARE:SHAREROOT";
+    if ($FSDIR_SCRATCH) {
+	$cmdline .= " -m $FSDIR_SCRATCH:$SCRATCHROOT";
+    }
     $cmdline .= " $pid $eid";
 }
 elsif ($cmd eq "gc") {
@@ -200,10 +210,13 @@ sub InsertFile($)
     my ($fn) = @_;
     my $retval = -1;
     
-    $fn =~ s|^$FSDIR_GROUPS|/groups|;
-    $fn =~ s|^$FSDIR_PROJ|/proj|;
-    $fn =~ s|^$FSDIR_USERS|/users|;
-    $fn =~ s|^$FSDIR_SHARE|/share|;
+    $fn =~ s|^$FSDIR_GROUPS|$GROUPROOT|;
+    $fn =~ s|^$FSDIR_PROJ|$PROJROOT|;
+    $fn =~ s|^$FSDIR_USERS|$USERROOT|;
+    $fn =~ s|^$FSDIR_SHARE|$SHAREROOT|;
+    if ($FSDIR_SCRATCH) {
+	$fn =~ s|^$FSDIR_SCRATCH|$SCRATCHROOT|;
+    }
     
     $fn = DBQuoteSpecial($fn);
 
diff --git a/tbsetup/ns2ir/parse-ns.in b/tbsetup/ns2ir/parse-ns.in
index 2ccfed1ba6..9fad1ebdcd 100644
--- a/tbsetup/ns2ir/parse-ns.in
+++ b/tbsetup/ns2ir/parse-ns.in
@@ -637,7 +637,7 @@ sub GenDefsFile($)
     #
     print TCL "# Template goo\n";
     # Does not matter what it is, as long as it is set.
-    print TCL "set ::DATASTORE \"/proj\"\n";
+    print TCL "set ::DATASTORE \"" . PROJROOT() . "\"\n";
     
     if (defined($experiment)) {
 	my $instance =
diff --git a/tbsetup/ns2ir/tb_compat.tcl.in b/tbsetup/ns2ir/tb_compat.tcl.in
index 620220e891..b0a204d827 100644
--- a/tbsetup/ns2ir/tb_compat.tcl.in
+++ b/tbsetup/ns2ir/tb_compat.tcl.in
@@ -32,15 +32,23 @@ namespace eval TBCOMPAT {
 
     # Substitutions for "/proj",
     variable FSDIR_PROJ "@FSDIR_PROJ@"
+    variable PROJROOT	"@PROJROOT_DIR@"
 
     # ... "/groups",
     variable FSDIR_GROUPS "@FSDIR_GROUPS@"
+    variable GROUPROOT	  "@GROUPSROOT_DIR@"
 
-    # ... "/users", and
+    # ... "/users",
     variable FSDIR_USERS "@FSDIR_USERS@"
+    variable USERROOT	 "@USERSROOT_DIR@"
 
-    # ... "/share".
+    # ... "/share", and
     variable FSDIR_SHARE "@FSDIR_SHARE@"
+    variable SHAREROOT	 "@SHAREROOT_DIR@"
+
+    # ... "/scratch".
+    variable FSDIR_SCRATCH "@FSDIR_SCRATCH@"
+    variable SCRATCHROOT   "@SCRATCHROOT_DIR@"
 
     # This is a general procedure that takes a node, an object (lan or link)
     # it is connected to, and an IP address, and sets the IP address
@@ -419,6 +427,7 @@ proc tb-set-node-tarfiles {node args} {
 	set dir [lindex $args 0]
 	set tarfile [lindex $args 1]
 	
+	#
 	# Check the install directory to make sure it is not an NFS mount.
 	# This check can also act as an alert to the user that the arguments
 	# are wrong.  For example, the following line will pass the above
@@ -426,11 +435,15 @@ proc tb-set-node-tarfiles {node args} {
 	#
 	#   tb-set-node-tarfiles $node /proj/foo/bar.tgz /proj/foo/baz.tgz
 	#
-	# XXX /proj is hardcoded since the subst is /q/proj.
-	if {[string match "/proj/*" $dir] ||
-	    [string match "${::TBCOMPAT::FSDIR_GROUPS}/*" $dir] ||
-	    [string match "${::TBCOMPAT::FSDIR_USERS}/*" $dir] ||
-	    [string match "${::TBCOMPAT::FSDIR_SHARE}/*" $dir]} {
+	# XXX This is a hack check because they can specify '/' and have
+	# "proj/foo/..." in the tarball and still clobber themselves.
+	#
+	if {[string match "${::TBCOMPAT::PROJROOT}/*" $dir] ||
+	    [string match "${::TBCOMPAT::GROUPROOT}/*" $dir] ||
+	    [string match "${::TBCOMPAT::USERROOT}/*" $dir] ||
+	    [string match "${::TBCOMPAT::SHAREROOT}/*" $dir] ||
+	    (${::TBCOMPAT::SCRATCHROOT} != "" &&
+	     [string match "${::TBCOMPAT::SCRATCHROOT}/*" $dir])} {
 	    perror "\[tb-set-node-tarfiles] '$dir' refers to an NFS directory instead of the node's local disk."
 	    return
 	} elseif {![string match "/*" $dir]} {
@@ -450,9 +463,11 @@ proc tb-set-node-tarfiles {node args} {
 		perror "\[tb-set-node-tarfiles] '$tarfile' is not an http, https, or ftp URL."
 		return
 	    }
-	} elseif {![string match "/proj/*" $tarfile] &&
-	          ![string match "/groups/*" $tarfile]} {
-	    perror "\[tb-set-node-tarfiles] '$tarfile' is not in '/proj' or '/groups'"
+	} elseif {![string match "${::TBCOMPAT::PROJROOT}/*" $tarfile] &&
+	          ![string match "${::TBCOMPAT::GROUPROOT}/*" $tarfile] &&
+	          (${::TBCOMPAT::SCRATCHROOT} == "" ||
+	           ![string match "${::TBCOMPAT::SCRATCHROOT}/*" $tarfile])} {
+	    perror "\[tb-set-node-tarfiles] '$tarfile' is not in an allowed directory"
 	    return
 	} elseif {![file exists $tarfile]} {
 	    perror "\[tb-set-node-tarfiles] '$tarfile' does not exist."
diff --git a/tbsetup/nscheck.in b/tbsetup/nscheck.in
index e2d1346e87..dac4f7ce3b 100644
--- a/tbsetup/nscheck.in
+++ b/tbsetup/nscheck.in
@@ -2,7 +2,7 @@
 
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2004 University of Utah and the Flux Group.
+# Copyright (c) 2000-2004, 2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 
@@ -43,6 +43,7 @@ my $dirname;
 # Testbed Support libraries
 #
 use lib "@prefix@/lib";
+use libtestbed;
 use libtblog;
 
 #
@@ -94,18 +95,16 @@ else {
 }
 
 #
-# The file must reside in /proj, /groups, or /users. Since this script
-# runs as the caller, regular file permission checks ensure its a file
-# the user is allowed to use. /tmp/$guid-$nsref.nsfile also allowed
-# since this script is invoked directly from web interface, which generates
-# a name that should not be guessable, so as long as it looks to be in
-# proper format, we accept it. 
+# The file must reside in an acceptible location. Since this script
+# runs as the caller, regular file permission checks ensure it is a
+# file the user is allowed to use.  So we don't have to be too tight
+# with the RE matching /tmp and /var/tmp files.  Note that
+# /tmp/$pid-$eid.nsfile.XXXXX is also allowed since this script is
+# invoked directly from web interface.
 #
 if (! ($tempfile =~ /^\/tmp\/[-\w]+-\d+\.nsfile/) &&
     ! ($tempfile =~ /^\/var\/tmp\/php\w+/) &&
-    ! ($tempfile =~ /^\/proj/) &&
-    ! ($tempfile =~ /^\/groups/) &&
-    ! ($tempfile =~ /^\/users/)) {
+    ! TBValidUserDir($tempfile, 0)) {
     fatal("$tempfile does not resolve to an appropriate directory!\n");
 }
 
diff --git a/tbsetup/nsverify/nstbparse.in b/tbsetup/nsverify/nstbparse.in
index 15d3b3d21a..887416c713 100644
--- a/tbsetup/nsverify/nstbparse.in
+++ b/tbsetup/nsverify/nstbparse.in
@@ -14,7 +14,7 @@ variable links
 # optional items
 variable rtproto "none"
 variable simname
-variable DATASTORE "/proj"
+variable DATASTORE "@PROJROOT_DIR@"
 
 rename puts real_puts
 proc puts {args} {
diff --git a/tbsetup/rmgroup.in b/tbsetup/rmgroup.in
index 62e6dccc5c..f15f367b51 100644
--- a/tbsetup/rmgroup.in
+++ b/tbsetup/rmgroup.in
@@ -52,8 +52,6 @@ my $MAILMANSUPPORT= @MAILMANSUPPORT@;
 my $BUGDBSUPPORT  = @BUGDBSUPPORT@;
 my $OPSDBSUPPORT  = @OPSDBSUPPORT@;
 
-my $PROJROOT = "/proj";
-my $GRPROOT  = "/groups";
 my $SSH      = "$TB/bin/sshtb";
 my $GROUPDEL = "/usr/sbin/pw groupdel";
 my $DELMMLIST= "$TB/sbin/delmmlist";
@@ -79,6 +77,9 @@ use libaudit;
 use libdb;
 use libtestbed;
 
+my $PROJROOT    = PROJROOT();
+my $GRPROOT     = GROUPROOT();
+
 #
 # Check args.
 #
diff --git a/tbsetup/rmproj.in b/tbsetup/rmproj.in
index 4fcb81f663..1499c4cbeb 100755
--- a/tbsetup/rmproj.in
+++ b/tbsetup/rmproj.in
@@ -2,7 +2,7 @@
 
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2003, 2005 University of Utah and the Flux Group.
+# Copyright (c) 2000-2003, 2005, 2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 use English;
@@ -24,7 +24,6 @@ my $TB       = "@prefix@";
 my $TBOPS    = "@TBOPSEMAIL@";
 my $CONTROL  = "@USERNODE@";
 
-my $PROJROOT = "/proj";
 my $RMGROUP  = "$TB/sbin/rmgroup";
 my $MODGROUPS= "$TB/sbin/modgroups";
 
@@ -47,6 +46,9 @@ use libaudit;
 use libdb;
 use libtestbed;
 
+my $PROJROOT     = PROJROOT();
+my $SCRATCHROOT  = SCRATCHROOT();
+
 #
 # We don't want to run this script unless its the real version.
 #
@@ -138,6 +140,30 @@ if (-e "$PROJROOT/$pid") {
     }
 }
     
+#
+# and the scratch directory if it exists
+#
+if ($SCRATCHROOT && -e "$SCRATCHROOT/$pid") {
+    my $oldname = "$SCRATCHROOT/$pid";
+    my $newname = "$SCRATCHROOT/$savename";
+
+    if (rename($oldname, $newname)) {
+	#
+	# Chown the owner/group to root and set the permissions so no
+	# one is allowed to look inside.
+	#
+	if (! chmod(0700, $newname)) {
+	    fatal("Could not chmod directory $newname to 0700: $!");
+	}
+	if (! chown(0, 0, $newname)) {
+	    fatal("Could not chown directory $newname to 0/0: $!");
+	}
+    }
+    else {
+	fatal("Could not rename proj directory to $newname: $!");
+    }
+}
+
 #
 # Ditto for the experiment working directory.
 #
diff --git a/tbsetup/rmuser.in b/tbsetup/rmuser.in
index 8f57ff5169..57a6ba2ea2 100755
--- a/tbsetup/rmuser.in
+++ b/tbsetup/rmuser.in
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -wT
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2003, 2005 University of Utah and the Flux Group.
+# Copyright (c) 2000-2003, 2005, 2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 use English;
@@ -27,7 +27,6 @@ my $TBLOGS  = "@TBLOGSEMAIL@";
 my $CONTROL = "@USERNODE@";
 my $BOSSNODE= "@BOSSNODE@";
 
-my $HOMEDIR	= "/users";
 my $MODGROUPS	= "$TB/sbin/modgroups";
 my $DELACCT	= "$TB/sbin/tbacct del";
 
@@ -72,6 +71,8 @@ use libaudit;
 use libdb;
 use libtestbed;
 
+my $HOMEDIR	= USERROOT();
+
 #
 # Check args.
 #
diff --git a/tbsetup/spewrpmtar.in b/tbsetup/spewrpmtar.in
index c66e8fc578..4ac5b55a50 100644
--- a/tbsetup/spewrpmtar.in
+++ b/tbsetup/spewrpmtar.in
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -wT
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2003 University of Utah and the Flux Group.
+# Copyright (c) 2000-2003, 2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 use English;
@@ -207,13 +207,14 @@ sub VerifyFile()
     }
 
     #
-    # The file must reside in /proj/$pid/$eid or /groups/$pid/$eid.
-    # allow anything from /users!
+    # The file must reside in /proj/$pid/$eid, /groups/$pid/$gid
+    # or /scratch/$pid.  Don't allow anything from /users!
     #
-    if (! ($translated =~ /^\/proj\/$pid\//) &&
-	! ($translated =~ /^\/groups\/$pid\/$eid\//)) {
+    if (! TBValidUserDir($translated, 0, undef, $pid, $gid)) {
 	if ($debug) {
-	    print STDERR "$translated is not in /proj or /groups!\n";
+	    print STDERR "$translated is not in ",
+			 join(' or ', TBValidUserDirList(undef, $pid, $gid)),
+			 ".\n";
 	}
 	return 1;
     }
diff --git a/tbsetup/swapexp.in b/tbsetup/swapexp.in
index c0cd0e6bb0..875f87b05f 100644
--- a/tbsetup/swapexp.in
+++ b/tbsetup/swapexp.in
@@ -255,19 +255,18 @@ if ($inout eq "modify" && @ARGV > 2) {
     }
 
     #
-    # The file must reside in /proj, /groups, or /users. Since this script
-    # runs as the caller, regular file permission checks ensure its a file
-    # the user is allowed to use. /tmp/$guid-$nsref.nsfile also allowed
-    # since this script is invoked directly from web interface, which generates
-    # a name that should not be guessable, so as long as it looks to be in
-    # proper format, we accept it. 
+    # The file must reside in an acceptible location. Since this script
+    # runs as the caller, regular file permission checks ensure it is a
+    # file the user is allowed to use.   So we don't have to be too tight
+    # with the RE matching /tmp and /var/tmp files.  Note that
+    # /tmp/$guid-$nsref.nsfile is also allowed since this script is
+    # invoked directly from web interface which generates a name that
+    # should not be guessable.
     #
     if (! ($tempnsfile =~ /^\/tmp\/[-\w]+-\d+\.nsfile/) &&
 	! ($tempnsfile =~ /^\/var\/tmp\/php\w+/) &&
-	! ($tempnsfile =~ /^\/proj/) &&
-	! ($tempnsfile =~ /^\/groups/) &&
-	! ($tempnsfile =~ /^\/users/)) {
-	tbdie("$tempnsfile does not resolve to an appropriate directory!");
+	! TBValidUserDir($tempnsfile, 0)) {
+	tbdie("$tempnsfile does not resolve to an allowed directory!");
     }
 
     if (! -f $tempnsfile || -z $tempnsfile || ! -r $tempnsfile) {
diff --git a/tbsetup/tbswap.in b/tbsetup/tbswap.in
index 7ed0dfc510..cceb1c4d77 100644
--- a/tbsetup/tbswap.in
+++ b/tbsetup/tbswap.in
@@ -1902,7 +1902,7 @@ sub doSwapoutAction($$%)
 	    $state{'_interval'} = $stateinterval;
 	    $state{'_timeout'} = $statetimeout;
 	    $state{'_iterations'} = 0;
-	    my $swapdir = "/proj/$pid/exp/$eid/swapinfo";
+	    my $swapdir = PROJROOT() . "/$pid/exp/$eid/swapinfo";
 	    my @enodes = ();
 	    foreach my $node (@pnodes) {
 		my $vname = $nodes{$node};
diff --git a/tbsetup/template_create.in b/tbsetup/template_create.in
index c8656864c4..8469f0f62b 100644
--- a/tbsetup/template_create.in
+++ b/tbsetup/template_create.in
@@ -57,7 +57,6 @@ my $parent_template;
 # Configure variables
 #
 my $TB		= "@prefix@";
-my $PROJROOT	= "/proj";
 my $EVENTSYS	= @EVENTSYS@;
 my $TBOPS	= "@TBOPSEMAIL@";
 my $TBLOGS	= "@TBLOGSEMAIL@";
@@ -487,17 +486,18 @@ sub ParseArgs()
 	}
 
 	#
-	# The file must reside in /proj, /groups, or /users. Since this script
-	# runs as the caller, regular file permission checks ensure its a file
-	# the user is allowed to use. /tmp/$pid-$eid.nsfile.XXXXX also allowed
-	# since this script is invoked directly from web interface.
+	# The file must reside in an acceptible location. Since this script
+	# runs as the caller, regular file permission checks ensure it is a
+	# file the user is allowed to use.  So we don't have to be too tight
+	# with the RE matching /tmp and /var/tmp files.  Note that
+	# /tmp/$guid-$nsref.nsfile is also allowed since this script is
+	# invoked directly from web interface which generates a name that
+	# should not be guessable.
 	#
 	if (! ($inputfile =~ /^\/tmp\/[-\w]+-\d+\.nsfile/) &&
 	    ! ($inputfile =~ /^\/tmp\/\d+\.ns/) &&
 	    ! ($inputfile =~ /^\/var\/tmp\/php\w+/) &&
-	    ! ($inputfile =~ /^\/proj/) &&
-	    ! ($inputfile =~ /^\/groups/) &&
-	    ! ($inputfile =~ /^\/users/)) {
+	    ! TBValidUserDir($inputfile, 0)) {
 	    tberror("$inputfile does not resolve to an allowed directory!");
 	    # Note positive status; so error goes to user not tbops.
 	    exit(1);
diff --git a/tbsetup/template_delete.in b/tbsetup/template_delete.in
index 007a9fb8b5..93af31efbc 100644
--- a/tbsetup/template_delete.in
+++ b/tbsetup/template_delete.in
@@ -41,7 +41,6 @@ my $recursive    = 0;
 # Configure variables
 #
 my $TB		= "@prefix@";
-my $PROJROOT	= "/proj";
 my $EVENTSYS	= @EVENTSYS@;
 my $TBOPS	= "@TBOPSEMAIL@";
 my $TBLOGS	= "@TBLOGSEMAIL@";
diff --git a/tbsetup/template_exprun.in b/tbsetup/template_exprun.in
index 3d702ffa04..eb573f6a22 100644
--- a/tbsetup/template_exprun.in
+++ b/tbsetup/template_exprun.in
@@ -71,7 +71,6 @@ my $ctoken;
 # Configure variables
 #
 my $TB		= "@prefix@";
-my $PROJROOT	= "/proj";
 my $EVENTSYS	= @EVENTSYS@;
 my $TBOPS	= "@TBOPSEMAIL@";
 my $TBLOGS	= "@TBLOGSEMAIL@";
@@ -784,17 +783,17 @@ sub ParseArgs()
 	}
 
 	#
-	# The file must reside in /proj, /groups, or /users. Since this script
-	# runs as the caller, regular file permission checks ensure its a file
-	# the user is allowed to use. /tmp/$pid-$eid.nsfile.XXXXX also allowed
-	# since this script is invoked directly from web interface.
+	# The file must reside in an acceptible location. Since this script
+	# runs as the caller, regular file permission checks ensure it is a
+	# file the user is allowed to use.  So we don't have to be too tight
+	# with the RE matching /tmp and /var/tmp files.  These .xml files
+	# are allowed since this script is invoked directly from web interface
+	# which generates a name that should not be guessable.
 	#
 	if (! ($inputfile =~ /^\/tmp\/[-\w]+-\d+\.xml/) &&
 	    ! ($inputfile =~ /^\/tmp\/\d+\.xml/) &&
 	    ! ($inputfile =~ /^\/var\/tmp\/php\w+/) &&
-	    ! ($inputfile =~ /^\/proj/) &&
-	    ! ($inputfile =~ /^\/groups/) &&
-	    ! ($inputfile =~ /^\/users/)) {
+	    ! TBValidUserDir($inputfile, 0)) {
 	    tberror("$inputfile does not resolve to an allowed directory!");
 	    # Note positive status; so error goes to user not tbops.
 	    exit(1);
diff --git a/tbsetup/template_graph.in b/tbsetup/template_graph.in
index f9e7fc808d..417f641e51 100644
--- a/tbsetup/template_graph.in
+++ b/tbsetup/template_graph.in
@@ -34,7 +34,6 @@ my $guid;
 # Configure variables
 #
 my $TB		= "@prefix@";
-my $PROJROOT	= "/proj";
 my $EVENTSYS	= @EVENTSYS@;
 my $TBOPS	= "@TBOPSEMAIL@";
 my $TBLOGS	= "@TBLOGSEMAIL@";
diff --git a/tbsetup/template_instantiate.in b/tbsetup/template_instantiate.in
index 3430ddfb3d..9a4a4cb660 100644
--- a/tbsetup/template_instantiate.in
+++ b/tbsetup/template_instantiate.in
@@ -66,7 +66,6 @@ my %parameters   = ();
 # Configure variables
 #
 my $TB		= "@prefix@";
-my $PROJROOT	= "/proj";
 my $EVENTSYS	= @EVENTSYS@;
 my $TBOPS	= "@TBOPSEMAIL@";
 my $TBLOGS	= "@TBLOGSEMAIL@";
@@ -696,17 +695,17 @@ sub ParseArgs()
 	}
 
 	#
-	# The file must reside in /proj, /groups, or /users. Since this script
-	# runs as the caller, regular file permission checks ensure its a file
-	# the user is allowed to use. /tmp/$pid-$eid.nsfile.XXXXX also allowed
-	# since this script is invoked directly from web interface.
+	# The file must reside in an acceptible location. Since this script
+	# runs as the caller, regular file permission checks ensure it is a
+	# file the user is allowed to use.  So we don't have to be too tight
+	# with the RE matching /tmp and /var/tmp files.  These .xml files
+	# are allowed since this script is invoked directly from web interface
+	# which generates a name that should not be guessable.
 	#
 	if (! ($inputfile =~ /^\/tmp\/[-\w]+-\d+\.xml/) &&
 	    ! ($inputfile =~ /^\/tmp\/\d+\.xml/) &&
 	    ! ($inputfile =~ /^\/var\/tmp\/php\w+/) &&
-	    ! ($inputfile =~ /^\/proj/) &&
-	    ! ($inputfile =~ /^\/groups/) &&
-	    ! ($inputfile =~ /^\/users/)) {
+	    ! TBValidUserDir($inputfile, 0)) {
 	    tberror("$inputfile does not resolve to an allowed directory!");
 	    # Note positive status; so error goes to user not tbops.
 	    exit(1);
diff --git a/tbsetup/template_metadata.in b/tbsetup/template_metadata.in
index 584d7a858e..cd50a235de 100644
--- a/tbsetup/template_metadata.in
+++ b/tbsetup/template_metadata.in
@@ -233,17 +233,17 @@ sub ParseArgs()
 	    if (! -f $inputfile);
 
 	#
-	# The file must reside in /proj, /groups, or /users. Since this script
-	# runs as the caller, regular file permission checks ensure its a file
-	# the user is allowed to use. /tmp/$pid-$eid.nsfile.XXXXX also allowed
-	# since this script is invoked directly from web interface.
+	# The file must reside in an acceptible location. Since this script
+	# runs as the caller, regular file permission checks ensure it is a
+	# file the user is allowed to use.  So we don't have to be too tight
+	# with the RE matching /tmp and /var/tmp files.  Note that the .txt
+	# files are allowed since this script is invoked directly from web
+	# interface which generates a name that should not be guessable.
 	#
 	if (! ($inputfile =~ /^\/tmp\/[-\w]+-\d+\.txt/) &&
 	    ! ($inputfile =~ /^\/tmp\/\d+\.txt/) &&
 	    ! ($inputfile =~ /^\/var\/tmp\/php\w+/) &&
-	    ! ($inputfile =~ /^\/proj/) &&
-	    ! ($inputfile =~ /^\/groups/) &&
-	    ! ($inputfile =~ /^\/users/)) {
+	    ! TBValidUserDir($inputfile, 0)) {
 	    tberror("$inputfile does not resolve to an allowed directory!");
 	    # Note positive status; so error goes to user not tbops.
 	    exit(1);
diff --git a/tbsetup/template_swapin.in b/tbsetup/template_swapin.in
index 5f86260738..7de2d10f3e 100644
--- a/tbsetup/template_swapin.in
+++ b/tbsetup/template_swapin.in
@@ -49,7 +49,6 @@ my $eid;
 # Configure variables
 #
 my $TB		= "@prefix@";
-my $PROJROOT	= "/proj";
 my $EVENTSYS	= @EVENTSYS@;
 my $TBOPS	= "@TBOPSEMAIL@";
 my $TBLOGS	= "@TBLOGSEMAIL@";
diff --git a/tmcd/common/config/rc.mkelab b/tmcd/common/config/rc.mkelab
index 5679a2b704..a954b95f14 100755
--- a/tmcd/common/config/rc.mkelab
+++ b/tmcd/common/config/rc.mkelab
@@ -106,6 +106,9 @@ my $WINSUPPORT = 0;
 # This also gets turned on/off below
 my $NOSETUP    = 0;
 
+# Whether we configure a /scratch FS, turned on/off below
+my $SCRATCHFS  = 0;
+
 # This will not change ...
 my $PHP4_PKG = "php4-extensions-1.0";
 
@@ -215,6 +218,7 @@ sub doboot()
     $emulabconfig{"MFSCONSOLE"} = "sio";
     $emulabconfig{"WINSUPPORT"} = 0;
     $emulabconfig{"NOSETUP"}    = 0;
+    $emulabconfig{"SCRATCHFS"}  = 0;
 
     #
     # Turn the tmcc results into a hash first. Then call the boss or ops
@@ -247,6 +251,10 @@ sub doboot()
     if ($emulabconfig{"NOSETUP"}) {
 	$NOSETUP = 1;
     }
+    # and scratch filesystem usage
+    if ($emulabconfig{"SCRATCHFS"}) {
+	$SCRATCHFS = 1;
+    }
 
     # XXX Temporary
     if ($FBSD_VERSION == 5.4) {    
@@ -441,13 +449,14 @@ sub SetupFsNode()
 
     #
     # Clean up a few things on the image and create symlinks into ${TBDIR} for
-    # /proj, /users, and /groups. Also allows /share to be created/
+    # /proj, /users, /groups and /scratch. Also allows /share to be created/
     #
     mysystem("umount -A -t nfs");
     # In case umount fails!
     mysystem("cd /; mv -f users users.old");
     mysystem("cd /; mv -f proj proj.old");
-    # Groups might not exists
+    mysystem("cd /; mv -f share share.old");
+    # This might not exist
     mysystem("cd /; mv -f groups groups.old")
 	if (-d "/groups");
     mysystem("mkdir ${TBDIR}/users ${TBDIR}/proj ${TBDIR}/groups");
@@ -455,9 +464,17 @@ sub SetupFsNode()
     mysystem("ln -s ${TBDIR}/proj /proj");
     mysystem("ln -s ${TBDIR}/groups /groups");
 
+    if ($SCRATCHFS) {
+	mysystem("cd /; mv -f scratch scratch.old")
+	    if (-d "/scratch");
+	mysystem("mkdir ${TBDIR}/scratch");
+	mysystem("ln -s ${TBDIR}/scratch /scratch");
+    }
+
     #
     # Setup a stub /share using slice 2 of the image.
     #
+    mysystem("mkdir /share");
     mysystem("$BINDIR/mkextrafs.pl -f -s 2 /share");
 
     #
@@ -760,14 +777,14 @@ sub SetupOpsNode($)
 
     #
     # Clean up a few things on the image and create symlinks into ${TBDIR} for
-    # /proj, /users, and /groups. Also allows /share to be created/
+    # /proj, /users, /groups and /scratch. Also allows /share to be created/
     #
     mysystem("umount -A -t nfs");
     # In case umount fails!
     mysystem("cd /; mv -f users users.old");
     mysystem("cd /; mv -f proj proj.old");
     mysystem("cd /; mv -f share share.old");
-    # Groups might not exists
+    # This might not exist
     mysystem("cd /; mv -f groups groups.old")
 	if (-d "/groups");
     if ($isfs) {
@@ -785,6 +802,17 @@ sub SetupOpsNode($)
 	mysystem("mkdir /users /proj /groups /share");
     }
 
+    if ($SCRATCHFS) {
+	mysystem("cd /; mv -f scratch scratch.old")
+	    if (-d "/scratch");
+	if ($isfs) {
+	    mysystem("mkdir ${TBDIR}/scratch");
+	    mysystem("ln -s ${TBDIR}/scratch /scratch");
+	} else {
+	    mysystem("mkdir /scratch");
+	}
+    }
+
     #
     # Lets mount the package dir so that we can pass off some stuff to
     # the install scripts;
@@ -1577,6 +1605,14 @@ sub CreateDefsFile($)
 		    print OUTDEFS "TBCOOKIESUFFIX=\"$eid\"\n";
 		    last SWITCH;
 		};
+		/^FSDIR_SCRATCH$/ && do {
+		    if ($SCRATCHFS) {
+			print OUTDEFS "FSDIR_SCRATCH=/q/scratch\n";
+		    } else {
+			print OUTDEFS "FSDIR_SCRATCH=\n";
+		    }
+		    last SWITCH;
+		};
 		
 		print OUTDEFS $_;
 	    }
diff --git a/tmcd/tmcd.c b/tmcd/tmcd.c
index 5d941abb66..5f3046a732 100644
--- a/tmcd/tmcd.c
+++ b/tmcd/tmcd.c
@@ -47,11 +47,15 @@
 #ifdef  FSDIR_SHARE
 #define FSSHAREDIR	FSNODE ":" FSDIR_SHARE
 #endif
-#define PROJDIR		"/proj"
-#define GROUPDIR	"/groups"
-#define USERDIR		"/users"
+#ifdef  FSDIR_SCRATCH
+#define FSSCRATCHDIR	FSNODE ":" FSDIR_SCRATCH
+#endif
+#define PROJDIR		PROJROOT_DIR
+#define GROUPDIR	GROUPSROOT_DIR
+#define USERDIR		USERSROOT_DIR
+#define SCRATCHDIR	SCRATCHROOT_DIR
+#define SHAREDIR	SHAREROOT_DIR
 #define NETBEDDIR	"/netbed"
-#define SHAREDIR	"/share"
 #define PLISALIVELOGDIR "/usr/testbed/log/plabisalive"
 #define RELOADPID	"emulab-ops"
 #define RELOADEID	"reloading"
@@ -2978,6 +2982,16 @@ COMMAND_PROTOTYPE(domounts)
 		/* Leave this logging on all the time for now. */
 		info("MOUNTS: %s", buf);
 		
+#ifdef FSSCRATCHDIR
+		/*
+		 * Return scratch mount if its defined.
+		 */
+		OUTPUT(buf, sizeof(buf), "REMOTE=%s/%s LOCAL=%s/%s\n",
+		       FSSCRATCHDIR, reqp->pid, SCRATCHDIR, reqp->pid);
+		client_writeback(sock, buf, strlen(buf), tcp);
+		/* Leave this logging on all the time for now. */
+		info("MOUNTS: %s", buf);
+#endif
 #ifdef FSSHAREDIR
 		/*
 		 * Return share mount if its defined.
@@ -3030,6 +3044,18 @@ COMMAND_PROTOTYPE(domounts)
 				client_writeback(sock, buf, strlen(buf), tcp);
 				info("MOUNTS: %s", buf);
 			}
+#ifdef FSSCRATCHDIR
+			/*
+			 * Pointer to per-project scratch directory.
+			 */
+			OUTPUT(buf, sizeof(buf),
+			       "SFS REMOTE=%s%s/%s LOCAL=%s/%s\n",
+			       fshostid, FSDIR_SCRATCH, reqp->pid,
+			       SCRATCHDIR, reqp->pid);
+			client_writeback(sock, buf, strlen(buf), tcp);
+			if (verbose)
+				info("MOUNTS: %s", buf);
+#endif
 #ifdef FSSHAREDIR
 			/*
 			 * Pointer to /share.
@@ -3058,16 +3084,19 @@ COMMAND_PROTOTYPE(domounts)
 			 *
 			 * Pointer to /proj.
 			 */
-			OUTPUT(buf, sizeof(buf), "SFS REMOTE=%s%s LOCAL=%s/%s\n",
-				fshostid, FSDIR_PROJ, NETBEDDIR, PROJDIR);
+			OUTPUT(buf, sizeof(buf),
+			       "SFS REMOTE=%s%s LOCAL=%s/%s\n",
+			       fshostid, FSDIR_PROJ, NETBEDDIR, PROJDIR);
 			client_writeback(sock, buf, strlen(buf), tcp);
-			info("MOUNTS: %s", buf);
+			if (verbose)
+				info("MOUNTS: %s", buf);
 
 			/*
 			 * Pointer to /groups
 			 */
-			OUTPUT(buf, sizeof(buf), "SFS REMOTE=%s%s LOCAL=%s%s\n",
-				fshostid, FSDIR_GROUPS, NETBEDDIR, GROUPDIR);
+			OUTPUT(buf, sizeof(buf),
+			       "SFS REMOTE=%s%s LOCAL=%s%s\n",
+			       fshostid, FSDIR_GROUPS, NETBEDDIR, GROUPDIR);
 			client_writeback(sock, buf, strlen(buf), tcp);
 			if (verbose)
 				info("MOUNTS: %s", buf);
@@ -3075,17 +3104,30 @@ COMMAND_PROTOTYPE(domounts)
 			/*
 			 * Pointer to /users
 			 */
-			OUTPUT(buf, sizeof(buf), "SFS REMOTE=%s%s LOCAL=%s%s\n",
-				fshostid, FSDIR_USERS, NETBEDDIR, USERDIR);
+			OUTPUT(buf, sizeof(buf),
+			       "SFS REMOTE=%s%s LOCAL=%s%s\n",
+			       fshostid, FSDIR_USERS, NETBEDDIR, USERDIR);
 			client_writeback(sock, buf, strlen(buf), tcp);
 			if (verbose)
 				info("MOUNTS: %s", buf);
+#ifdef FSSCRATCHDIR
+			/*
+			 * Pointer to per-project scratch directory.
+			 */
+			OUTPUT(buf, sizeof(buf),
+			       "SFS REMOTE=%s%s LOCAL=%s/%s\n",
+			       fshostid, FSDIR_SCRATCH, NETBEDDIR, SCRATCHDIR);
+			client_writeback(sock, buf, strlen(buf), tcp);
+			if (verbose)
+				info("MOUNTS: %s", buf);
+#endif
 #ifdef FSSHAREDIR
 			/*
 			 * Pointer to /share.
 			 */
-			OUTPUT(buf, sizeof(buf), "SFS REMOTE=%s%s LOCAL=%s%s\n",
-				fshostid, FSDIR_SHARE, NETBEDDIR, SHAREDIR);
+			OUTPUT(buf, sizeof(buf),
+			       "SFS REMOTE=%s%s LOCAL=%s%s\n",
+			       fshostid, FSDIR_SHARE, NETBEDDIR, SHAREDIR);
 			client_writeback(sock, buf, strlen(buf), tcp);
 			if (verbose)
 				info("MOUNTS: %s", buf);
@@ -3251,7 +3293,7 @@ COMMAND_PROTOTYPE(dosfshostid)
 	 * Create symlink names
 	 */
 	OUTPUT(sfspath, sizeof(sfspath), "/sfs/%s", nodehostid);
-	OUTPUT(dspath, sizeof(dspath), "/proj/%s/%s.%s.%s", DOTSFS,
+	OUTPUT(dspath, sizeof(dspath), "%s/%s/%s.%s.%s", PROJDIR, DOTSFS,
 	       reqp->nickname, reqp->eid, reqp->pid);
 
 	/*
diff --git a/utils/create_image.in b/utils/create_image.in
index 8f45cf83b7..9218925216 100755
--- a/utils/create_image.in
+++ b/utils/create_image.in
@@ -244,7 +244,7 @@ my $isglobal = $imageid_row{'global'};
 # Redirect pathname for global images.
 #
 if ($isglobal && ($filename =~ /^\/usr\/testbed/)) {
-    $filename = "/proj/$pid/images/" . basename($filename);
+    $filename = PROJDIR() . "/$pid/images/" . basename($filename);
     print "*** WARNING: Writing global descriptor to $filename instead!\n";
 }
 
@@ -274,13 +274,11 @@ else {
 }
 
 #
-# The file must reside in /proj, /groups, or /users. Since this script
+# The file must reside in an allowed directory. Since this script
 # runs as the caller, regular file permission checks ensure its a file
 # the user is allowed to use. 
 #
-if (! ($filename =~ /^\/proj/) &&
-    ! ($filename =~ /^\/groups/) &&
-    ! ($filename =~ /^\/users/)) {
+if (! TBValidUserDir($filename, 0)) {
     die("*** $0:\n".
 	"    $filename does not resolve to an allowed directory!\n");
 }
diff --git a/utils/cvsinit.in b/utils/cvsinit.in
index 12f17051c9..f3ba0240cd 100755
--- a/utils/cvsinit.in
+++ b/utils/cvsinit.in
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -wT
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2005 University of Utah and the Flux Group.
+# Copyright (c) 2005, 2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 use English;
@@ -18,7 +18,6 @@ my $TB       = "@prefix@";
 my $CVSBIN   = "/usr/bin/cvs";
 my $CHOWN    = "/usr/sbin/chown";
 my $CVSSUPPORT  = @CVSSUPPORT@;
-my $PROJROOT = "/proj";
 
 #
 # Untaint the path
@@ -55,6 +54,7 @@ if (! $CVSSUPPORT) {
 my $query_result =
     DBQueryFatal("select pid,head_uid from projects");
 
+my $PROJROOT = PROJROOT();
 while (my ($pid,$projhead) = $query_result->fetchrow_array()) {
     next
 	if (! -d "$PROJROOT/$pid");
diff --git a/utils/firstuser.in b/utils/firstuser.in
index 26269f74cf..332cb5fa80 100755
--- a/utils/firstuser.in
+++ b/utils/firstuser.in
@@ -29,7 +29,7 @@ my $protouser       = 'elabman';
 my $protouser_name  = 'Emulab Manager';
 my $protouser_email = '@TBOPSEMAIL@';
 my $protouser_shell = 'tcsh';
-my $HOMEDIR         = "/users";
+my $HOMEDIR         = USERROOT();
 my $protoproj       = 'emulab-ops';
 my $protoproj_desc  = 'Operations Meta-Project';
 my $batchmode       = 0;
diff --git a/utils/template_record.in b/utils/template_record.in
index 66ece722ee..89b2fe1358 100755
--- a/utils/template_record.in
+++ b/utils/template_record.in
@@ -20,6 +20,7 @@ my $optlist = "-e:";
 
 # Configure variables.
 my $TB          = "@prefix@";
+my $PROJROOT	= "@PROJROOT_DIR@";
 
 # Locals.
 my $pid;
@@ -45,7 +46,7 @@ else {
     #
     # See if we can infer the pid/eid from the path.
     #
-    if (`pwd` =~ /^(?:\/[-\w]+)*\/proj\/([-\w]+)\/exp\/([-\w]+)/) {
+    if (`pwd` =~ /^(?:\/[-\w]+)*\/$PROJROOT\/([-\w]+)\/exp\/([-\w]+)/) {
 	$pid = $1;
 	$eid = $2;
 	print "Using $pid/$eid\n";
@@ -62,7 +63,7 @@ my $scriptname = shift(@ARGV);
 #
 # Grab the user environment variables.
 #
-open(USRENV, "/proj/$pid/exp/$eid/tbdata/environment")
+open(USRENV, "/$PROJROOT/$pid/exp/$eid/tbdata/environment")
     or die("Could not open user environment file!\n");
 
 while (<USRENV>) {
diff --git a/www/beginexp_form.php3 b/www/beginexp_form.php3
index 6948290373..98995f0745 100644
--- a/www/beginexp_form.php3
+++ b/www/beginexp_form.php3
@@ -210,6 +210,7 @@ function SPITFORM($formfields, $errors)
     global $view, $view_style, $projlist, $linktest_levels;
     global $EXPOSELINKTEST, $EXPOSEARCHIVE;
     global $EXPOSESTATESAVE;
+    global $TBVALIDDIRS_HTML;
 
     PAGEHEADER("Begin a Testbed Experiment");
 
@@ -518,8 +519,8 @@ function SPITFORM($formfields, $errors)
                     <td>&nbsp;&nbsp;<b>or</b></td><td></td>
                     </tr><tr>
                       <td class='pad4'>On Server<br>
-                              <font size='-1'>(<code>/proj</code>,
-                        <code>/groups</code>, <code>/users</code>)</font></td>
+                              <font size='-1'>(" . $TBVALIDDIRS_HTML .
+	    		      ")</font></td>
                       <td class='pad4'>
 	                <input type=text
                                name=\"formfields[exp_localnsfile]\"
diff --git a/www/beginexp_xml.php3 b/www/beginexp_xml.php3
index df3c1b87f3..b79e54b8ca 100644
--- a/www/beginexp_xml.php3
+++ b/www/beginexp_xml.php3
@@ -171,11 +171,9 @@ elseif (isset($formfields[exp_localnsfile]) &&
     if (!preg_match("/^([-\@\w\.\/]+)$/", $formfields[exp_localnsfile])) {
 	$errors["Server NS File"] = "Pathname includes illegal characters";
     }
-    elseif (! ereg("^$TBPROJ_DIR/.*",  $formfields[exp_localnsfile]) &&
-	    ! ereg("^$TBUSER_DIR/.*",  $formfields[exp_localnsfile]) &&
-	    ! ereg("^$TBGROUP_DIR/.*", $formfields[exp_localnsfile])) {
-	$errors["Server NS File"] = "Must reside in either ".
-	    "$TBUSER_DIR/, $TBPROJ_DIR/, or $TBGROUP_DIR/";
+    elseif (! VALIDUSERPATH($formfields[exp_localnsfile])) {
+	$errors["Server NS File"] =
+		"Must reside in one of: $TBVALIDDIRS";
     }
     $nsfilelocale = "local";
 }
diff --git a/www/defs.php3.in b/www/defs.php3.in
index 46ecac5ac1..f1bc36bd28 100644
--- a/www/defs.php3.in
+++ b/www/defs.php3.in
@@ -63,12 +63,20 @@ if ($WWWHOST != "www.emulab.net") {
     $TBMAINSITE = 0;
 }
 
-$TBPROJ_DIR     = "/proj";
-$TBUSER_DIR	= "/users";
-$TBGROUP_DIR	= "/groups";
+$TBPROJ_DIR     = "@PROJROOT_DIR@";
+$TBUSER_DIR	= "@USERSROOT_DIR@";
+$TBGROUP_DIR	= "@GROUPSROOT_DIR@";
+$TBSCRATCH_DIR	= "@SCRATCHROOT_DIR@";
 $TBCVSREPO_DIR  = "$TBPROJ_DIR/cvsrepos";
 $TBNSSUBDIR     = "nsdir";
 
+$TBVALIDDIRS	  = "$TBPROJ_DIR, $TBUSER_DIR, $TBGROUP_DIR";
+$TBVALIDDIRS_HTML = "<code>$TBPROJ_DIR</code>, <code>$TBUSER_DIR</code>, <code>$TBGROUP_DIR</code>";
+if ($TBSCRATCH_DIR) {
+    $TBVALIDDIRS .= ", $TBSCRATCH_DIR";
+    $TBVALIDDIRS_HTML .= ", <code>$TBSCRATCH_DIR</code>";
+}
+
 $TBAUTHCOOKIE   = "HashCookie" . $TBCOOKIESUFFIX;
 $TBNAMECOOKIE   = "MyUidCookie" . $TBCOOKIESUFFIX;
 $TBLOGINCOOKIE  = "LoginCookie" . $TBCOOKIESUFFIX;
@@ -396,6 +404,29 @@ function LASTNODELOGIN($node)
 {
 }
 
+function VALIDUSERPATH($path, $uid="", $pid="", $gid="", $eid="")
+{
+    global $TBPROJ_DIR, $TBUSER_DIR, $TBGROUP_DIR, $TBSCRATCH_DIR;
+
+    #
+    # No ids specified, just make sure it starts with an appropriate prefix.
+    #
+    if (!$uid && !$pid && !$gid && !$eid) {
+	if (ereg("^$TBPROJ_DIR/.*", $path) ||
+	    ereg("^$TBUSER_DIR/.*", $path) ||
+	    ereg("^$TBGROUP_DIR/.*", $path)) {
+	    return 1;
+	}
+	if ($TBSCRATCH_DIR && ereg("^$TBSCRATCH_DIR/.*", $path)) {
+	    return 1;
+	}
+	return 0;
+    }
+
+    # XXX for now, see tbsetup/libtestbed.pm for what should happen
+    return 0;
+}
+
 #
 # A function to print the contents of an array (recursively).
 # Mostly useful for debugging.
diff --git a/www/editimageid.php3 b/www/editimageid.php3
index 2a50bc15e2..33fb741ba8 100644
--- a/www/editimageid.php3
+++ b/www/editimageid.php3
@@ -351,10 +351,10 @@ else {
     $gid    = $defaults["gid"];
 	
     if (!$shared && strcmp($gid, $pid)) {
-	$pdef = "/groups/" . $pid . "/" . $gid . "/";
+	$pdef = "$TBGROUP_DIR/" . $pid . "/" . $gid . "/";
     }
     else {
-	$pdef = "/proj/" . $pid . "/";
+	$pdef = "$TBPROJ_DIR/" . $pid . "/";
     }
 
     if (strpos($formfields[path], $pdef) === false) {
diff --git a/www/groups.html b/www/groups.html
index 80b38c7986..6c93ebc71a 100644
--- a/www/groups.html
+++ b/www/groups.html
@@ -1,6 +1,6 @@
 <!--
    EMULAB-COPYRIGHT
-   Copyright (c) 2000-2003, 2005 University of Utah and the Flux Group.
+   Copyright (c) 2000-2003, 2005, 2006 University of Utah and the Flux Group.
    All rights reserved.
   -->
 <center>
@@ -126,8 +126,10 @@ group will get accounts on the nodes; other members of the same
 project, not in the group, will not get accounts.
 </li>
 <li>
-Emulab uses NFS mounted filesystems for <tt>/users</tt>, <tt>/proj</tt>, 
-and <tt>/groups</tt> on the experimental nodes. Because of the nature of
+Emulab uses NFS mounted filesystems for
+<tt>/users</tt>, <tt>/proj</tt>, <tt>/groups</tt>,
+and (optionally) <tt>/scratch</tt>
+on the experimental nodes. Because of the nature of
 NFS, giving root privileges to a user will allow them to read/write
 any files on any filesystems that are mounted, since root access
 allows them to <em>su</em> as any other user. Thus, any files in the
diff --git a/www/modifyexp.php3 b/www/modifyexp.php3
index 1e27860dae..1b71302c7c 100644
--- a/www/modifyexp.php3
+++ b/www/modifyexp.php3
@@ -214,17 +214,15 @@ if ($speclocal) {
     # for the file before going to ground, so the user will get immediate
     # feedback if the filename is bogus.
     #
-    # Do not allow anything outside of /users or /proj. I do not think there
-    # is a security worry, but good to enforce it anyway.
+    # Do not allow anything outside of the usual directories. I do not think
+    # there is a security worry, but good to enforce it anyway.
     #
     if (!preg_match("/^([-\@\w\.\/]+)$/", $exp_localnsfile)) {
 	USERERROR("NS File: Pathname includes illegal characters", 1);
     }
-    if (! ereg("^$TBPROJ_DIR/.*" ,$exp_localnsfile) &&
-        ! ereg("^$TBUSER_DIR/.*" ,$exp_localnsfile) &&
-        ! ereg("^$TBGROUP_DIR/.*" ,$exp_localnsfile)) {
-	USERERROR("NS File: You must specify a server resident file in either ".
-                  "$TBUSER_DIR/ or $TBPROJ_DIR/", 1);
+    if (!VALIDUSERPATH($exp_localnsfile)) {
+	USERERROR("NS File: You must specify a server resident file in " .
+		  "one of: ${TBVALIDDIRS}.", 1);
     }
     
     $nsfile = $exp_localnsfile;
diff --git a/www/newimageid.php3 b/www/newimageid.php3
index 5616c1e6c0..6b847ec322 100644
--- a/www/newimageid.php3
+++ b/www/newimageid.php3
@@ -111,7 +111,7 @@ function SPITFORM($formfields, $errors)
 	echo     "var global = 0;";
 
     echo         "if (pid == '') {
-                      theform['formfields[path]'].value = '/proj';
+                      theform['formfields[path]'].value = '$TBPROJ_DIR';
                   }
                   else if (theform['formfields[imagename]'].value == '') {
 		      theform['formfields[imagename]'].defaultValue = '';
@@ -122,11 +122,11 @@ function SPITFORM($formfields, $errors)
                       }
 		      else if (gid == '' || gid == pid || shared) {
     	                  theform['formfields[path]'].value =
-                                  '/proj/' + pid + '/images/';
+                                  '$TBPROJ_DIR/' + pid + '/images/';
                       }
                       else {
     	                  theform['formfields[path]'].value =
-                                  '/groups/' + pid + '/' + gid + '/images/';
+                                  '$TBGROUP_DIR/' + pid + '/' + gid + '/images/';
                       }
                   }
                   else if (theform['formfields[imagename]'].value != '') {
@@ -139,11 +139,11 @@ function SPITFORM($formfields, $errors)
                       }
 		      else if (gid == '' || gid == pid || shared) {
     	                  theform['formfields[path]'].value =
-                                  '/proj/' + pid + '/images/' + filename;
+                                  '$TBPROJ_DIR/' + pid + '/images/' + filename;
                       }
                       else {
     	                  theform['formfields[path]'].value =
-                                  '/groups/' + pid + '/' + gid + '/images/' +
+                                  '$TBGROUP_DIR/' + pid + '/' + gid + '/images/' +
                                   filename;
                       }
                   }
@@ -299,7 +299,7 @@ function SPITFORM($formfields, $errors)
     #
     echo "<tr>
               <td>Filename (full path) of Image[<b>4</b>]:<br>
-                  (must reside in /proj)</td>
+                  (must reside in $TBPROJ_DIR)</td>
               <td class=left>
                   <input type=text
                          name=\"formfields[path]\"
@@ -448,7 +448,7 @@ function SPITFORM($formfields, $errors)
 if (! $submit) {
     $defaults = array();
     $defaults[loadpart] = "X";
-    $defaults[path]     = "/proj/";
+    $defaults[path]     = "$TBPROJ_DIR/";
 
     #
     # For users that are in one project and one subgroup, it is usually
@@ -468,9 +468,9 @@ if (! $submit) {
 	    $defaults[gid] = $group;
 	    
 	    if (!strcmp($project, $group))
-		$defaults[path]     = "/proj/$project/images/";
+		$defaults[path]     = "$TBPROJ_DIR/$project/images/";
 	    else
-		$defaults[path]     = "/groups/$project/$group/images/";
+		$defaults[path]     = "$TBGROUP_DIR/$project/$group/images/";
 	}
 	reset($projlist);
     }
@@ -645,10 +645,10 @@ elseif (! $isadmin) {
 	isset($formfields[gid]) &&
 	strcmp($formfields[gid], "") &&
 	strcmp($formfields[gid], $formfields[pid])) {
-	$pdef = "/groups/" . $formfields[pid] . "/" . $formfields[gid] . "/";
+	$pdef = "$TBGROUP_DIR/" . $formfields[pid] . "/" . $formfields[gid] . "/";
     }
     else {
-	$pdef = "/proj/" . $formfields[pid] . "/images/";
+	$pdef = "$TBPROJ_DIR/" . $formfields[pid] . "/images/";
     }
 
     if (strpos($formfields[path], $pdef) === false) {
diff --git a/www/newimageid_ez.php3 b/www/newimageid_ez.php3
index 182c9a02bf..dd3850d952 100644
--- a/www/newimageid_ez.php3
+++ b/www/newimageid_ez.php3
@@ -147,7 +147,7 @@ function SPITFORM($formfields, $errors)
 	echo     "var global = 0;";
 
     echo         "if (pid == '') {
-                      theform['formfields[path]'].value = '/proj';
+                      theform['formfields[path]'].value = '$TBPROJ_DIR';
                   }
                   else if (theform['formfields[imagename]'].value == '') {
 		      theform['formfields[imagename]'].defaultValue = '';
@@ -158,11 +158,11 @@ function SPITFORM($formfields, $errors)
                       }
 		      else if (gid == '' || gid == pid || shared) {
     	                  theform['formfields[path]'].value =
-                                  '/proj/' + pid + '/images/';
+                                  '$TBPROJ_DIR/' + pid + '/images/';
                       }
                       else {
     	                  theform['formfields[path]'].value =
-                                  '/groups/' + pid + '/' + gid + '/images/';
+                                  '$TBGROUP_DIR/' + pid + '/' + gid + '/images/';
                       }
                   }
                   else if (theform['formfields[imagename]'].value != '') {
@@ -175,11 +175,11 @@ function SPITFORM($formfields, $errors)
                       }
 		      else if (gid == '' || gid == pid || shared) {
     	                  theform['formfields[path]'].value =
-                                  '/proj/' + pid + '/images/' + filename;
+                                  '$TBPROJ_DIR/' + pid + '/images/' + filename;
                       }
                       else {
     	                  theform['formfields[path]'].value =
-                                  '/groups/' + pid + '/' + gid + '/images/' +
+                                  '$TBGROUP_DIR/' + pid + '/' + gid + '/images/' +
                                   filename;
                       }
                   }
@@ -365,7 +365,7 @@ function SPITFORM($formfields, $errors)
     #
     echo "<tr>
               <td>Filename (full path) of Image:<br>
-                  (must reside in /proj)</td>
+                  (must reside in $TBPROJ_DIR)</td>
               <td class=left>
                   <input type=text
                          name=\"formfields[path]\"
@@ -698,7 +698,7 @@ function spithidden($formfields, $field) {
 #
 if (! $submit) {
     $defaults = array();
-    $defaults[path]     = "/proj/";
+    $defaults[path]     = "$TBPROJ_DIR/";
     $defaults[shared]   = "Nope";
 
     if ($nodetype == "mote") {
@@ -713,7 +713,7 @@ if (! $submit) {
     } else {
 	# Defaulys for PC-type nodes
 	$defaults[loadpart] = "X";
-	$defaults[path]     = "/proj/";
+	$defaults[path]     = "$TBPROJ_DIR/";
 	$defaults[op_mode]  = TBDB_DEFAULT_OSID_OPMODE;
 	$defaults[os_feature_ping] = "checked";
 	$defaults[os_feature_ssh]  = "checked";
@@ -747,9 +747,9 @@ if (! $submit) {
 	    $defaults[gid] = $group;
 	    
 	    if (!strcmp($project, $group))
-		$defaults[path]     = "/proj/$project/images/";
+		$defaults[path]     = "$TBPROJ_DIR/$project/images/";
 	    else
-		$defaults[path]     = "/groups/$project/$group/images/";
+		$defaults[path]     = "$TBGROUP_DIR/$project/$group/images/";
 	}
 	reset($projlist);
     }
@@ -912,10 +912,10 @@ elseif (! $isadmin) {
 	isset($formfields[gid]) &&
 	strcmp($formfields[gid], "") &&
 	strcmp($formfields[gid], $formfields[pid])) {
-	$pdef = "/groups/" . $formfields[pid] . "/" . $formfields[gid] . "/";
+	$pdef = "$TBGROUP_DIR/" . $formfields[pid] . "/" . $formfields[gid] . "/";
     }
     else {
-	$pdef = "/proj/" . $formfields[pid] . "/images/";
+	$pdef = "$TBPROJ_DIR/" . $formfields[pid] . "/images/";
     }
 
     if (strpos($formfields[path], $pdef) === false) {
diff --git a/www/nscheck.php3 b/www/nscheck.php3
index dd2581d610..3417d77156 100644
--- a/www/nscheck.php3
+++ b/www/nscheck.php3
@@ -73,17 +73,15 @@ if ($speclocal) {
     # for the file before going to ground, so the user will get immediate
     # feedback if the filename is bogus.
     #
-    # Do not allow anything outside of /users or /proj. I do not think there
-    # is a security worry, but good to enforce it anyway.
+    # Do not allow anything outside of the usual directories. I do not think
+    # there is a security worry, but good to enforce it anyway.
     #
     if (!preg_match("/^([-\@\w\.\/]+)$/", $exp_localnsfile)) {
 	USERERROR("NS File: Pathname includes illegal characters", 1);
     }
-    if (! ereg("^$TBPROJ_DIR/.*" ,$exp_localnsfile) &&
-        ! ereg("^$TBUSER_DIR/.*" ,$exp_localnsfile) &&
-        ! ereg("^$TBGROUP_DIR/.*" ,$exp_localnsfile)) {
-	USERERROR("NS File: You must specify a server resident file in either ".
-                  "$TBUSER_DIR/ or $TBPROJ_DIR/", 1);
+    if (!VALIDUSERPATH($exp_localnsfile)) {
+	USERERROR("NS File: You must specify a server resident file in " .
+		  "one of: ${TBVALIDDIRS}.", 1);
     }
     
     $nsfile = $exp_localnsfile;
diff --git a/www/nscheck_form.php3 b/www/nscheck_form.php3
index d55be08232..d1a2b7f2b3 100644
--- a/www/nscheck_form.php3
+++ b/www/nscheck_form.php3
@@ -1,7 +1,7 @@
 <?php
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2004 University of Utah and the Flux Group.
+# Copyright (c) 2000-2004, 2006 University of Utah and the Flux Group.
 # All rights reserved.
 #
 include("defs.php3");
@@ -43,7 +43,7 @@ echo "<tr>
                                    <br>
                                    Or<br>
                                    <br>
-                              On Server <br> (/proj, /groups, /users)
+                              On Server <br> (TBVALIDDIRS)
                       </center></td>
 
           <td rowspan>
diff --git a/www/template_create.php b/www/template_create.php
index 71863db16d..3460b504cc 100644
--- a/www/template_create.php
+++ b/www/template_create.php
@@ -25,6 +25,7 @@ function SPITFORM($formfields, $errors)
 {
     global $TBDB_PIDLEN, $TBDB_GIDLEN, $TBDB_EIDLEN, $TBDOCBASE;
     global $projlist;
+    global $TBVALIDDIRS_HTML;
 
     PAGEHEADER("Create an Experiment Template");
 
@@ -177,8 +178,7 @@ function SPITFORM($formfields, $errors)
                     <td>&nbsp;&nbsp;<b>or</b></td><td></td>
                     </tr><tr>
                       <td class='pad4'>On Server<br>
-                              <font size='-1'>(<code>/proj</code>,
-                        <code>/groups</code>, <code>/users</code>)</font></td>
+                              <font size='-1'>($TBVALIDDIRS_HTML)</font></td>
                       <td class='pad4'>
 	                <input type=text
                                name=\"formfields[localnsfile]\"
@@ -352,11 +352,9 @@ elseif (isset($formfields[localnsfile]) && $formfields[localnsfile] != "") {
     if (!preg_match("/^([-\@\w\.\/]+)$/", $formfields[localnsfile])) {
 	$errors["Server NS File"] = "Pathname includes illegal characters";
     }
-    elseif (! ereg("^$TBPROJ_DIR/.*",  $formfields[localnsfile]) &&
-	    ! ereg("^$TBUSER_DIR/.*",  $formfields[localnsfile]) &&
-	    ! ereg("^$TBGROUP_DIR/.*", $formfields[localnsfile])) {
-	$errors["Server NS File"] = "Must reside in either ".
-	    "$TBUSER_DIR/, $TBPROJ_DIR/, or $TBGROUP_DIR/";
+    elseif (! VALIDUSERPATH($formfields[localnsfile])) {
+	$errors["Server NS File"] =
+		"Must reside in one of: $TBVALIDDIRS";
     }
     $nsfilelocale = "local";
 }
diff --git a/www/template_export.php b/www/template_export.php
index 8366dbf785..2f62d2a24d 100644
--- a/www/template_export.php
+++ b/www/template_export.php
@@ -94,7 +94,7 @@ if (!$confirmed) {
     echo "<blockquote><blockquote>
           <ol>
             <li> By default, your instance will be exported to
-                 <tt>/proj/$pid/export/$guid/$version/$exptidx</tt>,
+                 <tt>$TBPROJ_DIR/$pid/export/$guid/$version/$exptidx</tt>,
                  available to
                  other experiments in your project. If you want to export
                  to a local file, click the <em>local disk</em> option
diff --git a/www/template_exprun.php b/www/template_exprun.php
index 51774f6bfc..6e02694ecf 100644
--- a/www/template_exprun.php
+++ b/www/template_exprun.php
@@ -184,6 +184,7 @@ function SPITFORM($instance, $formfields, $parameters, $errors)
 {
     global $TBDB_EIDLEN;
     global $guid, $version, $pid, $eid;
+    global $TBVALIDDIRS_HTML;
 
     PAGEHEADER("Start new Experiment Run");
 
@@ -378,8 +379,7 @@ function SPITFORM($instance, $formfields, $parameters, $errors)
                </td><td></td></tr>\n";
 	echo "<tr>
                   <td class='pad4'>On Server<br>
-                           <font size='-1'>(<code>/proj</code>,
-                      <code>/groups</code>, <code>/users</code>)</font></td>
+                           <font size='-1'>($TBVALIDDIRS_HTML)</font></td>
                   <td class='pad4'>
 	              <input type=text
                              name=\"formfields[parameter_xmlfile]\"
@@ -557,11 +557,9 @@ if (count($parameter_masterlist)) {
 	    $errors["Parameter XML File"] =
 		"Pathname includes illegal characters";
 	}
-	elseif (! ereg("^$TBPROJ_DIR/.*",  $parameter_xmlfile) &&
-		! ereg("^$TBUSER_DIR/.*",  $parameter_xmlfile) &&
-		! ereg("^$TBGROUP_DIR/.*", $parameter_xmlfile)) {
-	    $errors["Parameter XML File"] = "Must reside in either ".
-		"$TBUSER_DIR/, $TBPROJ_DIR/, or $TBGROUP_DIR/";
+	elseif (! VALIDUSERPATH($parameter_xmlfile)) {
+	    $errors["Parameter XML File"] =
+		"Must reside in one of: $TBVALIDDIRS";
 	}
     	$deletexmlfile = 0;
     }
diff --git a/www/template_swapin.php b/www/template_swapin.php
index ba7276fa0c..69b08bc282 100644
--- a/www/template_swapin.php
+++ b/www/template_swapin.php
@@ -32,6 +32,7 @@ $batchmode     = 0;
 function SPITFORM($template, $formfields, $parameters, $errors)
 {
     global $TBDB_EIDLEN, $EXPOSELINKTEST, $EXPOSESTATESAVE;
+    global $TBVALIDDIRS_HTML;
 
     PAGEHEADER("Instantiate an Experiment Template");
 
@@ -222,8 +223,7 @@ function SPITFORM($template, $formfields, $parameters, $errors)
                </td><td></td></tr>\n";
 	echo "<tr>
                   <td class='pad4'>On Server<br>
-                           <font size='-1'>(<code>/proj</code>,
-                      <code>/groups</code>, <code>/users</code>)</font></td>
+                           <font size='-1'>($TBVALIDDIRS_HTML)</font></td>
                   <td class='pad4'>
 	              <input type=text
                              name=\"formfields[parameter_xmlfile]\"
@@ -563,11 +563,9 @@ if (count($parameter_masterlist)) {
 	    $errors["Parameter XML File"] =
 		"Pathname includes illegal characters";
 	}
-	elseif (! ereg("^$TBPROJ_DIR/.*",  $parameter_xmlfile) &&
-		! ereg("^$TBUSER_DIR/.*",  $parameter_xmlfile) &&
-		! ereg("^$TBGROUP_DIR/.*", $parameter_xmlfile)) {
-	    $errors["Parameter XML File"] = "Must reside in either ".
-		"$TBUSER_DIR/, $TBPROJ_DIR/, or $TBGROUP_DIR/";
+	elseif (! VALIDUSERPATH($parameter_xmlfile)) {
+	    $errors["Parameter XML File"] =
+		"Must reside in one of: $TBVALIDDIRS";
 	}
     	$deletexmlfile = 0;
     }
diff --git a/xmlrpc/emulabserver.py.in b/xmlrpc/emulabserver.py.in
index fcb6b340c1..79316b7378 100755
--- a/xmlrpc/emulabserver.py.in
+++ b/xmlrpc/emulabserver.py.in
@@ -43,10 +43,11 @@ from emulabclient import *
 VERSION = 0.1
 
 # Well known directories
-PROJROOT = "/proj"
-GROUPROOT = "/groups"
-SHAREROOT = "/share"
-USERSROOT = "/users"
+PROJROOT = "@PROJROOT_DIR@"
+GROUPROOT = "@GROUPSROOT_DIR@"
+SCRATCHROOT = "@SCRATCHROOT_DIR@"
+SHAREROOT = "@SHAREROOT_DIR@"
+USERSROOT = "@USERSROOT_DIR@"
 
 # List of directories exported to nodes via NFS.
 NFS_EXPORTS = [
@@ -54,6 +55,7 @@ NFS_EXPORTS = [
     GROUPROOT,
     SHAREROOT,
     USERSROOT,
+    SCRATCHROOT,
     ]
 
 #
@@ -794,6 +796,7 @@ class fs:
         for proj in projs:
             if proj[0] == proj[1]:
                 res.append(PROJROOT + "/" + proj[0])
+		res.append(SCRATCHROOT + "/" + proj[0])
                 pass
             else:
                 res.append(GROUPROOT + "/" + proj[0] + "/" + proj[1])
@@ -5092,7 +5095,7 @@ def nfspath(value):
 
     found = False
     for export in NFS_EXPORTS:
-        if retval.startswith(export):
+        if export != "" and retval.startswith(export):
             found = True
             break
         pass
-- 
GitLab