56
oper_func = call_operator(operation)
57
return oper_func(argv[2:])
61
print """Usage: python setup.py operation [options]
67
For help and options for a specific operation use 'help [operation]'."""
70
oper_func = call_operator(operator)
71
oper_func(['operator','--help'])
73
def call_operator(operation):
129
# Disallow run as root unless installing
130
if operation != 'install' and os.geteuid() == 0:
131
print >>sys.stderr, "I do not want to run this stage as root."
132
print >>sys.stderr, "Please run as a normal user."
74
134
# Call the requested operation's function
78
'build' : setup.build.build,
79
'install' : setup.install.install,
140
'listmake' : listmake,
82
144
print >>sys.stderr, (
83
145
"""Invalid operation '%s'. Try python setup.py help."""
148
return oper_func(argv[2:])
150
# Operation functions
154
print """Usage: python setup.py operation [args]
155
Operation (and args) can be:
157
listmake (developer use only)
160
install [--nojail] [-n|--dry]
164
print """Usage: python setup.py help [operation]"""
169
if operation == 'help':
170
print """python setup.py help [operation]
171
Prints the usage message or detailed help on an operation, then exits."""
172
elif operation == 'listmake':
173
print """python setup.py listmake
174
(For developer use only)
175
Recurses through the source tree and builds a list of all files which should
176
be copied upon installation. This should be run by the developer before
177
cutting a distribution, and the listfile it generates should be included in
178
the distribution, avoiding the administrator having to run it."""
179
elif operation == 'conf':
180
print """python setup.py conf [args]
181
Configures IVLE with machine-specific details, most notably, various paths.
182
Either prompts the administrator for these details or accepts them as
183
command-line args. Will be interactive only if there are no arguments given.
184
Takes defaults from existing conf file if it exists.
185
Creates www/conf/conf.py and trampoline/conf.h.
191
As explained in the interactive prompt or conf.py.
193
elif operation == 'build':
194
print """python -O setup.py build [--dry|-n]
195
Compiles all files and sets up a jail template in the source directory.
196
-O is recommended to cause compilation to be optimised.
198
Compiles (GCC) trampoline/trampoline.c to trampoline/trampoline.
200
Creates standard subdirs inside the jail, eg bin, opt, home, tmp.
201
Copies console/ to a location within the jail.
202
Copies OS programs and files to corresponding locations within the jail
203
(eg. python and Python libs, ld.so, etc).
204
Generates .pyc or .pyo files for all the IVLE .py files.
206
--dry | -n Print out the actions but don't do anything."""
207
elif operation == 'install':
208
print """sudo python setup.py install [--nojail] [--dry|-n]
210
Create target install directory ($target).
212
Copy trampoline/trampoline to $target/bin.
213
chown and chmod the installed trampoline.
214
Copy www/ to $target.
215
Copy jail/ to jails template directory (unless --nojail specified).
217
--nojail Do not copy the jail.
218
--dry | -n Print out the actions but don't do anything."""
220
print >>sys.stderr, (
221
"""Invalid operation '%s'. Try python setup.py help."""
226
# We build two separate lists, by walking www and console
227
list_www = build_list_py_files('www')
228
list_console = build_list_py_files('console')
229
# Make sure that the files generated by conf are in the list
230
# (since listmake is typically run before conf)
231
if "www/conf/conf.py" not in list_www:
232
list_www.append("www/conf/conf.py")
233
# Make sure that console/python-console is in the list
234
if "console/python-console" not in list_console:
235
list_console.append("console/python-console")
236
# Write these out to a file
238
# the files that will be created/overwritten
239
listfile = os.path.join(cwd, "install_list.py")
242
file = open(listfile, "w")
244
file.write("""# IVLE Configuration File
246
# Provides lists of all files to be installed by `setup.py install' from
247
# certain directories.
248
# Note that any files with the given filename plus 'c' or 'o' (that is,
249
# compiled .pyc or .pyo files) will be copied as well.
251
# List of all installable files in www directory.
253
writelist_pretty(file, list_www)
255
# List of all installable files in console directory.
257
writelist_pretty(file, list_console)
260
except IOError, (errno, strerror):
261
print "IO error(%s): %s" % (errno, strerror)
264
print "Successfully wrote install_list.py"
267
print ("You may modify the set of installable files before cutting the "
274
def build_list_py_files(dir):
275
"""Builds a list of all py files found in a directory and its
276
subdirectories. Returns this as a list of strings."""
278
for (dirpath, dirnames, filenames) in os.walk(dir):
279
# Exclude directories beginning with a '.' (such as '.svn')
280
filter_mutate(lambda x: x[0] != '.', dirnames)
281
# All *.py files are added to the list
282
pylist += [os.path.join(dirpath, item) for item in filenames
283
if mimetypes.guess_type(item)[0] in listmake_mimetypes]
286
def writelist_pretty(file, list):
287
"""Writes a list one element per line, to a file."""
293
file.write(' %s,\n' % repr(elem))
297
global root_dir, ivle_install_dir, jail_base, allowed_uids
298
# Set up some variables
301
# the files that will be created/overwritten
302
conffile = os.path.join(cwd, "www/conf/conf.py")
303
conf_hfile = os.path.join(cwd, "trampoline/conf.h")
305
# Fixed config options that we don't ask the admin
306
default_app = "dummy"
308
# Get command-line arguments to avoid asking questions.
310
(opts, args) = getopt.gnu_getopt(args, "", ['root_dir=',
311
'ivle_install_dir=', 'jail_base=', 'allowed_uids='])
314
print >>sys.stderr, "Invalid arguments:", string.join(args, ' ')
318
# Interactive mode. Prompt the user for all the values.
320
print """This tool will create the following files:
323
prompting you for details about your configuration. The file will be
324
overwritten if it already exists. It will *not* install or deploy IVLE.
326
Please hit Ctrl+C now if you do not wish to do this.
327
""" % (conffile, conf_hfile)
329
# Get information from the administrator
330
# If EOF is encountered at any time during the questioning, just exit
333
root_dir = query_user(root_dir,
334
"""Root directory where IVLE is located (in URL space):""")
335
ivle_install_dir = query_user(ivle_install_dir,
336
'Root directory where IVLE will be installed (on the local file '
338
jail_base = query_user(jail_base,
339
"""Root directory where the jails (containing user files) are stored
340
(on the local file system):""")
341
allowed_uids = query_user(allowed_uids,
342
"""UID of the web server process which will run IVLE.
343
Only this user may execute the trampoline. May specify multiple users as
344
a comma-separated list.
349
# Non-interactive mode. Parse the options.
350
if '--root_dir' in opts:
351
root_dir = opts['--root_dir']
352
if '--ivle_install_dir' in opts:
353
ivle_install_dir = opts['--ivle_install_dir']
354
if '--jail_base' in opts:
355
jail_base = opts['--jail_base']
356
if '--allowed_uids' in opts:
357
allowed_uids = opts['--allowed_uids']
359
# Error handling on input values
361
allowed_uids = map(int, allowed_uids.split(','))
363
print >>sys.stderr, (
364
"Invalid UID list (%s).\n"
365
"Must be a comma-separated list of integers." % allowed_uids)
368
# Write www/conf/conf.py
371
conf = open(conffile, "w")
373
conf.write("""# IVLE Configuration File
375
# Miscellaneous application settings
378
# In URL space, where in the site is IVLE located. (All URLs will be prefixed
380
# eg. "/" or "/ivle".
383
# In the local file system, where IVLE is actually installed.
384
# This directory should contain the "www" and "bin" directories.
385
ivle_install_dir = "%s"
387
# In the local file system, where are the student/user file spaces located.
388
# The user jails are expected to be located immediately in subdirectories of
392
# Which application to load by default (if the user navigates to the top level
393
# of the site). This is the app's URL name.
394
# Note that if this app requires authentication, the user will first be
395
# presented with the login screen.
397
""" % (root_dir, ivle_install_dir, jail_base, default_app))
400
except IOError, (errno, strerror):
401
print "IO error(%s): %s" % (errno, strerror)
404
print "Successfully wrote www/conf/conf.py"
406
# Write trampoline/conf.h
409
conf = open(conf_hfile, "w")
411
conf.write("""/* IVLE Configuration File
413
* Administrator settings required by trampoline.
414
* Note: trampoline will have to be rebuilt in order for changes to this file
418
/* In the local file system, where are the jails located.
419
* The trampoline does not allow the creation of a jail anywhere besides
420
* jail_base or a subdirectory of jail_base.
422
static const char* jail_base = "%s";
424
/* Which user IDs are allowed to run the trampoline.
425
* This list should be limited to the web server user.
426
* (Note that root is an implicit member of this list).
428
static const int allowed_uids[] = { %s };
429
""" % (jail_base, repr(allowed_uids)[1:-1]))
432
except IOError, (errno, strerror):
433
print "IO error(%s): %s" % (errno, strerror)
436
print "Successfully wrote trampoline/conf.h"
439
print "You may modify the configuration at any time by editing"
446
# Get "dry" variable from command line
447
(opts, args) = getopt.gnu_getopt(args, "n", ['dry'])
449
dry = '-n' in opts or '--dry' in opts
452
print "Dry run (no actions will be executed\n"
454
# Compile the trampoline
455
action_runprog('gcc', ['-Wall', '-o', 'trampoline/trampoline',
456
'trampoline/trampoline.c'], dry)
458
# Create the jail and its subdirectories
459
# Note: Other subdirs will be made by copying files
460
action_mkdir('jail', dry)
461
action_mkdir('jail/home', dry)
462
action_mkdir('jail/tmp', dry)
464
# Copy all console and operating system files into the jail
465
action_copylist(install_list.list_console, 'jail/opt/ivle', dry)
466
copy_os_files_jail(dry)
468
# Compile .py files into .pyc or .pyo files
469
compileall.compile_dir('www', quiet=True)
470
compileall.compile_dir('console', quiet=True)
474
def copy_os_files_jail(dry):
475
"""Copies necessary Operating System files from their usual locations
476
into the jail/ directory of the cwd."""
477
# Currently source paths are configured for Ubuntu.
478
copy_file_to_jail('/lib/ld-linux.so.2', dry)
479
copy_file_to_jail('/lib/tls/i686/cmov/libc.so.6', dry)
480
copy_file_to_jail('/lib/tls/i686/cmov/libdl.so.2', dry)
481
copy_file_to_jail('/lib/tls/i686/cmov/libm.so.6', dry)
482
copy_file_to_jail('/lib/tls/i686/cmov/libpthread.so.0', dry)
483
copy_file_to_jail('/lib/tls/i686/cmov/libutil.so.1', dry)
484
copy_file_to_jail('/usr/bin/python2.5', dry)
485
action_symlink('python2.5', 'jail/usr/bin/python', dry)
486
action_copytree('/usr/lib/python2.5', 'jail/usr/lib/python2.5', dry)
488
def copy_file_to_jail(src, dry):
489
"""Copies a single file from an absolute location into the same location
490
within the jail. src must begin with a '/'. The jail will be located
491
in a 'jail' subdirectory of the current path."""
492
action_copyfile(src, 'jail' + src, dry)
495
# Get "dry" and "nojail" variables from command line
496
(opts, args) = getopt.gnu_getopt(args, "n", ['dry', 'nojail'])
498
dry = '-n' in opts or '--dry' in opts
499
nojail = '--nojail' in opts
502
print "Dry run (no actions will be executed\n"
504
if not dry and os.geteuid() != 0:
505
print >>sys.stderr, "Must be root to run install"
506
print >>sys.stderr, "(I need to chown some files)."
509
# Create the target (install) directory
510
action_mkdir(ivle_install_dir, dry)
512
# Create bin and copy the compiled files there
513
action_mkdir(os.path.join(ivle_install_dir, 'bin'), dry)
514
tramppath = os.path.join(ivle_install_dir, 'bin/trampoline')
515
action_copyfile('trampoline/trampoline', tramppath, dry)
516
# chown trampoline to root and set setuid bit
517
action_chown_setuid(tramppath, dry)
519
# Copy the www directory using the list
520
action_copylist(install_list.list_www, ivle_install_dir, dry)
523
# Copy the local jail directory built by the build action
524
# to the jails template directory (it will be used as a template
525
# for all the students' jails).
526
action_copytree('jail', os.path.join(jail_base, 'template'), dry)
530
# The actions call Python os functions but print actions and handle dryness.
531
# May still throw os exceptions if errors occur.
534
"""Represents an error when running a program (nonzero return)."""
535
def __init__(self, prog, retcode):
537
self.retcode = retcode
539
return str(self.prog) + " returned " + repr(self.retcode)
541
def action_runprog(prog, args, dry):
542
"""Runs a unix program. Searches in $PATH. Synchronous (waits for the
543
program to return). Runs in the current environment. First prints the
544
action as a "bash" line.
546
Throws a RunError with a retcode of the return value of the program,
547
if the program did not return 0.
549
prog: String. Name of the program. (No path required, if in $PATH).
550
args: [String]. Arguments to the program.
551
dry: Bool. If True, prints but does not execute.
553
print prog, string.join(args, ' ')
555
ret = os.spawnvp(os.P_WAIT, prog, args)
557
raise RunError(prog, ret)
559
def action_mkdir(path, dry):
560
"""Calls mkdir. Silently ignored if the directory already exists.
561
Creates all parent directories as necessary."""
562
print "mkdir -p", path
566
except OSError, (err, msg):
567
if err != errno.EEXIST:
570
def action_copytree(src, dst, dry):
571
"""Copies an entire directory tree. Symlinks are seen as normal files and
572
copies of the entire file (not the link) are made. Creates all parent
573
directories as necessary.
575
See shutil.copytree."""
576
if os.access(dst, os.F_OK):
579
shutil.rmtree(dst, True)
580
print "cp -r", src, dst
582
shutil.copytree(src, dst, True)
584
def action_copylist(srclist, dst, dry):
585
"""Copies all files in a list to a new location. The files in the list
586
are read relative to the current directory, and their destinations are the
587
same paths relative to dst. Creates all parent directories as necessary.
589
for srcfile in srclist:
590
dstfile = os.path.join(dst, srcfile)
591
dstdir = os.path.split(dstfile)[0]
592
if not os.path.isdir(dstdir):
593
action_mkdir(dstdir, dry)
594
print "cp -f", srcfile, dstfile
596
shutil.copyfile(srcfile, dstfile)
598
def action_copyfile(src, dst, dry):
599
"""Copies one file to a new location. Creates all parent directories
602
dstdir = os.path.split(dst)[0]
603
if not os.path.isdir(dstdir):
604
action_mkdir(dstdir, dry)
605
print "cp -f", src, dst
607
shutil.copyfile(src, dst)
608
shutil.copymode(src, dst)
610
def action_symlink(src, dst, dry):
611
"""Creates a symlink in a given location. Creates all parent directories
614
dstdir = os.path.split(dst)[0]
615
if not os.path.isdir(dstdir):
616
action_mkdir(dstdir, dry)
617
# Delete existing file
618
if os.path.exists(dst):
620
print "ln -fs", src, dst
624
def action_chown_setuid(file, dry):
625
"""Chowns a file to root, and sets the setuid bit on the file.
626
Calling this function requires the euid to be root.
627
The actual mode of path is set to: rws--s--s
629
print "chown root:root", file
632
print "chmod a+xs", file
633
print "chmod u+rw", file
635
os.chmod(file, stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
636
| stat.S_ISUID | stat.S_IRUSR | stat.S_IWUSR)
638
def query_user(default, prompt):
639
"""Prompts the user for a string, which is read from a line of stdin.
640
Exits silently if EOF is encountered. Returns the string, with spaces
641
removed from the beginning and end.
643
Returns default if a 0-length line (after spaces removed) was read.
645
sys.stdout.write('%s\n (default: "%s")\n>' % (prompt, default))
647
val = sys.stdin.readline()
648
except KeyboardInterrupt:
650
sys.stdout.write("\n")
652
sys.stdout.write("\n")
654
if val == '': sys.exit(1)
655
# If empty line, return default
657
if val == '': return default
660
def filter_mutate(function, list):
661
"""Like built-in filter, but mutates the given list instead of returning a
662
new one. Returns None."""
665
# Delete elements which do not match
666
if not function(list[i]):
88
670
if __name__ == "__main__":