23
23
# This is a command-line application, for use by the administrator.
24
# This program is a frontend for the modules in the setup packages that
25
# build and install IVLE in separate steps.
24
# This program configures, builds and installs IVLE in three separate steps.
26
25
# It is called with at least one argument, which specifies which operation to
28
# setup.py listmake (for developer use only)
29
# Recurses through the source tree and builds a list of all files which should
30
# be copied upon installation. This should be run by the developer before
31
# cutting a distribution, and the listfile it generates should be included in
32
# the distribution, avoiding the administrator having to run it.
34
# setup.py conf [args]
35
# Configures IVLE with machine-specific details, most notably, various paths.
36
# Either prompts the administrator for these details or accepts them as
38
# Creates www/conf/conf.py and trampoline/conf.h.
41
# Compiles all files and sets up a jail template in the source directory.
43
# Compiles (GCC) trampoline/trampoline.c to trampoline/trampoline.
45
# Creates standard subdirs inside the jail, eg bin, opt, home, tmp.
46
# Copies console/ to a location within the jail.
47
# Copies OS programs and files to corresponding locations within the jail
48
# (eg. python and Python libs, ld.so, etc).
49
# Generates .pyc files for all the IVLE .py files.
51
# setup.py install [--nojail] [--dry|n]
53
# Create target install directory ($target).
55
# Copy trampoline/trampoline to $target/bin.
56
# chown and chmod the installed trampoline.
57
# Copy www/ to $target.
58
# Copy jail/ to jails template directory (unless --nojail specified).
71
# Import modules from the website is tricky since they're in the www
73
sys.path.append(os.path.join(os.getcwd(), 'www'))
75
import common.makeuser
77
# Operating system files to copy over into the jail.
78
# These will be copied from the given place on the OS file system into the
79
# same place within the jail.
82
'/lib/tls/i686/cmov/libc.so.6',
83
'/lib/tls/i686/cmov/libdl.so.2',
84
'/lib/tls/i686/cmov/libm.so.6',
85
'/lib/tls/i686/cmov/libpthread.so.0',
86
'/lib/tls/i686/cmov/libutil.so.1',
89
# These 2 files do not exist in Ubuntu
90
#'/etc/ld.so.preload',
91
#'/etc/ld.so.nohwcap',
98
# Needed by matplotlib
99
'/usr/lib/i686/cmov/libssl.so.0.9.8',
100
'/usr/lib/i686/cmov/libcrypto.so.0.9.8',
101
'/lib/tls/i686/cmov/libnsl.so.1',
102
'/usr/lib/libz.so.1',
103
'/usr/lib/atlas/liblapack.so.3',
104
'/usr/lib/atlas/libblas.so.3',
105
'/usr/lib/libg2c.so.0',
106
'/usr/lib/libstdc++.so.6',
107
'/usr/lib/libfreetype.so.6',
108
'/usr/lib/libpng12.so.0',
109
'/usr/lib/libBLT.2.4.so.8.4',
110
'/usr/lib/libtk8.4.so.0',
111
'/usr/lib/libtcl8.4.so.0',
112
'/usr/lib/tcl8.4/init.tcl',
113
'/usr/lib/libX11.so.6',
114
'/usr/lib/libXau.so.6',
115
'/usr/lib/libXdmcp.so.6',
116
'/lib/libgcc_s.so.1',
119
# Symlinks to make within the jail. Src mapped to dst.
121
'python2.5': 'jail/usr/bin/python',
123
# Trees to copy. Src mapped to dst (these will be passed to action_copytree).
125
'/usr/lib/python2.5': 'jail/usr/lib/python2.5',
126
'/usr/share/matplotlib': 'jail/usr/share/matplotlib',
127
'/etc/ld.so.conf.d': 'jail/etc/ld.so.conf.d',
130
# Try importing existing conf, but if we can't just set up defaults
131
# The reason for this is that these settings are used by other phases
132
# of setup besides conf, so we need to know them.
133
# Also this allows you to hit Return to accept the existing value.
135
confmodule = __import__("www/conf/conf")
137
root_dir = confmodule.root_dir
141
ivle_install_dir = confmodule.ivle_install_dir
143
ivle_install_dir = "/opt/ivle"
145
jail_base = confmodule.jail_base
147
jail_base = "/home/informatics/jails"
149
# Just set reasonable defaults
151
ivle_install_dir = "/opt/ivle"
152
jail_base = "/home/informatics/jails"
156
# Try importing install_list, but don't fail if we can't, because listmake can
157
# function without it.
163
# Mime types which will automatically be placed in the list by listmake.
164
# Note that listmake is not intended to be run by the final user (the system
165
# administrator who installs this), so the developers can customize the list
166
# as necessary, and include it in the distribution.
167
listmake_mimetypes = ['text/x-python', 'text/html',
168
'application/x-javascript', 'application/javascript',
169
'text/css', 'image/png']
171
# Main function skeleton from Guido van Rossum
172
# http://www.artima.com/weblogs/viewpost.jsp?thread=4829
33
174
def main(argv=None):
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):
197
# Disallow run as root unless installing
198
if (operation != 'install' and operation != 'updatejails'
199
and os.geteuid() == 0):
200
print >>sys.stderr, "I do not want to run this stage as root."
201
print >>sys.stderr, "Please run as a normal user."
74
203
# Call the requested operation's function
78
'build' : setup.build.build,
79
'install' : setup.install.install,
209
'listmake' : listmake,
211
'updatejails' : updatejails,
82
214
print >>sys.stderr, (
83
215
"""Invalid operation '%s'. Try python setup.py help."""
218
return oper_func(argv[2:])
220
# Operation functions
224
print """Usage: python setup.py operation [args]
225
Operation (and args) can be:
227
listmake (developer use only)
230
install [--nojail] [-n|--dry]
234
print """Usage: python setup.py help [operation]"""
239
if operation == 'help':
240
print """python setup.py help [operation]
241
Prints the usage message or detailed help on an operation, then exits."""
242
elif operation == 'listmake':
243
print """python setup.py listmake
244
(For developer use only)
245
Recurses through the source tree and builds a list of all files which should
246
be copied upon installation. This should be run by the developer before
247
cutting a distribution, and the listfile it generates should be included in
248
the distribution, avoiding the administrator having to run it."""
249
elif operation == 'conf':
250
print """python setup.py conf [args]
251
Configures IVLE with machine-specific details, most notably, various paths.
252
Either prompts the administrator for these details or accepts them as
253
command-line args. Will be interactive only if there are no arguments given.
254
Takes defaults from existing conf file if it exists.
256
To run IVLE out of the source directory (allowing development without having
257
to rebuild/install), just provide ivle_install_dir as the IVLE trunk
258
directory, and run build/install one time.
260
Creates www/conf/conf.py and trampoline/conf.h.
267
As explained in the interactive prompt or conf.py.
269
elif operation == 'build':
270
print """python -O setup.py build [--dry|-n]
271
Compiles all files and sets up a jail template in the source directory.
272
-O is recommended to cause compilation to be optimised.
274
Compiles (GCC) trampoline/trampoline.c to trampoline/trampoline.
276
Creates standard subdirs inside the jail, eg bin, opt, home, tmp.
277
Copies console/ to a location within the jail.
278
Copies OS programs and files to corresponding locations within the jail
279
(eg. python and Python libs, ld.so, etc).
280
Generates .pyc or .pyo files for all the IVLE .py files.
282
--dry | -n Print out the actions but don't do anything."""
283
elif operation == 'install':
284
print """sudo python setup.py install [--nojail] [--dry|-n]
286
Create target install directory ($target).
288
Copy trampoline/trampoline to $target/bin.
289
chown and chmod the installed trampoline.
290
Copy www/ to $target.
291
Copy jail/ to jails template directory (unless --nojail specified).
293
--nojail Do not copy the jail.
294
--dry | -n Print out the actions but don't do anything."""
295
elif operation == 'updatejails':
296
print """sudo python setup.py updatejails [--dry|-n]
298
Copy jail/ to each subdirectory in jails directory.
300
--dry | -n Print out the actions but don't do anything."""
302
print >>sys.stderr, (
303
"""Invalid operation '%s'. Try python setup.py help."""
308
# We build two separate lists, by walking www and console
309
list_www = build_list_py_files('www')
310
list_console = build_list_py_files('console')
311
# Make sure that the files generated by conf are in the list
312
# (since listmake is typically run before conf)
313
if "www/conf/conf.py" not in list_www:
314
list_www.append("www/conf/conf.py")
315
# Make sure that console/python-console is in the list
316
if "console/python-console" not in list_console:
317
list_console.append("console/python-console")
318
# Write these out to a file
320
# the files that will be created/overwritten
321
listfile = os.path.join(cwd, "install_list.py")
324
file = open(listfile, "w")
326
file.write("""# IVLE Configuration File
328
# Provides lists of all files to be installed by `setup.py install' from
329
# certain directories.
330
# Note that any files with the given filename plus 'c' or 'o' (that is,
331
# compiled .pyc or .pyo files) will be copied as well.
333
# List of all installable files in www directory.
335
writelist_pretty(file, list_www)
337
# List of all installable files in console directory.
339
writelist_pretty(file, list_console)
342
except IOError, (errno, strerror):
343
print "IO error(%s): %s" % (errno, strerror)
346
print "Successfully wrote install_list.py"
349
print ("You may modify the set of installable files before cutting the "
356
def build_list_py_files(dir):
357
"""Builds a list of all py files found in a directory and its
358
subdirectories. Returns this as a list of strings."""
360
for (dirpath, dirnames, filenames) in os.walk(dir):
361
# Exclude directories beginning with a '.' (such as '.svn')
362
filter_mutate(lambda x: x[0] != '.', dirnames)
363
# All *.py files are added to the list
364
pylist += [os.path.join(dirpath, item) for item in filenames
365
if mimetypes.guess_type(item)[0] in listmake_mimetypes]
368
def writelist_pretty(file, list):
369
"""Writes a list one element per line, to a file."""
375
file.write(' %s,\n' % repr(elem))
379
global root_dir, ivle_install_dir, jail_base, allowed_uids
380
# Set up some variables
383
# the files that will be created/overwritten
384
conffile = os.path.join(cwd, "www/conf/conf.py")
385
conf_hfile = os.path.join(cwd, "trampoline/conf.h")
387
# Get command-line arguments to avoid asking questions.
389
(opts, args) = getopt.gnu_getopt(args, "", ['root_dir=',
390
'ivle_install_dir=', 'jail_base=', 'allowed_uids='])
393
print >>sys.stderr, "Invalid arguments:", string.join(args, ' ')
397
# Interactive mode. Prompt the user for all the values.
399
print """This tool will create the following files:
402
prompting you for details about your configuration. The file will be
403
overwritten if it already exists. It will *not* install or deploy IVLE.
405
Please hit Ctrl+C now if you do not wish to do this.
406
""" % (conffile, conf_hfile)
408
# Get information from the administrator
409
# If EOF is encountered at any time during the questioning, just exit
412
root_dir = query_user(root_dir,
413
"""Root directory where IVLE is located (in URL space):""")
414
ivle_install_dir = query_user(ivle_install_dir,
415
'Root directory where IVLE will be installed (on the local file '
417
jail_base = query_user(jail_base,
418
"""Root directory where the jails (containing user files) are stored
419
(on the local file system):""")
420
allowed_uids = query_user(allowed_uids,
421
"""UID of the web server process which will run IVLE.
422
Only this user may execute the trampoline. May specify multiple users as
423
a comma-separated list.
428
# Non-interactive mode. Parse the options.
429
if '--root_dir' in opts:
430
root_dir = opts['--root_dir']
431
if '--ivle_install_dir' in opts:
432
ivle_install_dir = opts['--ivle_install_dir']
433
if '--jail_base' in opts:
434
jail_base = opts['--jail_base']
435
if '--allowed_uids' in opts:
436
allowed_uids = opts['--allowed_uids']
438
# Error handling on input values
440
allowed_uids = map(int, allowed_uids.split(','))
442
print >>sys.stderr, (
443
"Invalid UID list (%s).\n"
444
"Must be a comma-separated list of integers." % allowed_uids)
447
# Write www/conf/conf.py
450
conf = open(conffile, "w")
452
conf.write("""# IVLE Configuration File
454
# Miscellaneous application settings
457
# In URL space, where in the site is IVLE located. (All URLs will be prefixed
459
# eg. "/" or "/ivle".
462
# In the local file system, where IVLE is actually installed.
463
# This directory should contain the "www" and "bin" directories.
464
ivle_install_dir = "%s"
466
# In the local file system, where are the student/user file spaces located.
467
# The user jails are expected to be located immediately in subdirectories of
470
""" % (root_dir, ivle_install_dir, jail_base))
473
except IOError, (errno, strerror):
474
print "IO error(%s): %s" % (errno, strerror)
477
print "Successfully wrote www/conf/conf.py"
479
# Write trampoline/conf.h
482
conf = open(conf_hfile, "w")
484
conf.write("""/* IVLE Configuration File
486
* Administrator settings required by trampoline.
487
* Note: trampoline will have to be rebuilt in order for changes to this file
491
/* In the local file system, where are the jails located.
492
* The trampoline does not allow the creation of a jail anywhere besides
493
* jail_base or a subdirectory of jail_base.
495
static const char* jail_base = "%s";
497
/* Which user IDs are allowed to run the trampoline.
498
* This list should be limited to the web server user.
499
* (Note that root is an implicit member of this list).
501
static const int allowed_uids[] = { %s };
502
""" % (jail_base, repr(allowed_uids)[1:-1]))
505
except IOError, (errno, strerror):
506
print "IO error(%s): %s" % (errno, strerror)
509
print "Successfully wrote trampoline/conf.h"
512
print "You may modify the configuration at any time by editing"
519
# Get "dry" variable from command line
520
(opts, args) = getopt.gnu_getopt(args, "n", ['dry'])
522
dry = '-n' in opts or '--dry' in opts
525
print "Dry run (no actions will be executed\n"
527
# Compile the trampoline
528
action_runprog('gcc', ['-Wall', '-o', 'trampoline/trampoline',
529
'trampoline/trampoline.c'], dry)
531
# Create the jail and its subdirectories
532
# Note: Other subdirs will be made by copying files
533
action_mkdir('jail', dry)
534
action_mkdir('jail/home', dry)
535
action_mkdir('jail/tmp', dry)
537
# Copy all console and operating system files into the jail
538
action_copylist(install_list.list_console, 'jail/opt/ivle', dry)
539
copy_os_files_jail(dry)
540
# Chmod the python console
541
action_chmod_x('jail/opt/ivle/console/python-console', dry)
544
# Compile .py files into .pyc or .pyo files
545
compileall.compile_dir('www', quiet=True)
546
compileall.compile_dir('console', quiet=True)
550
def copy_os_files_jail(dry):
551
"""Copies necessary Operating System files from their usual locations
552
into the jail/ directory of the cwd."""
553
# Currently source paths are configured for Ubuntu.
554
for filename in JAIL_FILES:
555
copy_file_to_jail(filename, dry)
556
for src, dst in JAIL_LINKS.items():
557
action_symlink(src, dst, dry)
558
for src, dst in JAIL_COPYTREES.items():
559
action_copytree(src, dst, dry)
561
def copy_file_to_jail(src, dry):
562
"""Copies a single file from an absolute location into the same location
563
within the jail. src must begin with a '/'. The jail will be located
564
in a 'jail' subdirectory of the current path."""
565
action_copyfile(src, 'jail' + src, dry)
568
# Get "dry" and "nojail" variables from command line
569
(opts, args) = getopt.gnu_getopt(args, "n", ['dry', 'nojail'])
571
dry = '-n' in opts or '--dry' in opts
572
nojail = '--nojail' in opts
575
print "Dry run (no actions will be executed\n"
577
if not dry and os.geteuid() != 0:
578
print >>sys.stderr, "Must be root to run install"
579
print >>sys.stderr, "(I need to chown some files)."
582
# Create the target (install) directory
583
action_mkdir(ivle_install_dir, dry)
585
# Create bin and copy the compiled files there
586
action_mkdir(os.path.join(ivle_install_dir, 'bin'), dry)
587
tramppath = os.path.join(ivle_install_dir, 'bin/trampoline')
588
action_copyfile('trampoline/trampoline', tramppath, dry)
589
# chown trampoline to root and set setuid bit
590
action_chown_setuid(tramppath, dry)
592
# Copy the www directory using the list
593
action_copylist(install_list.list_www, ivle_install_dir, dry)
596
# Copy the local jail directory built by the build action
597
# to the jails template directory (it will be used as a template
598
# for all the students' jails).
599
action_copytree('jail', os.path.join(jail_base, 'template'), dry)
601
# Append IVLE path to ivle.pth in python site packages
602
# (Unless it's already there)
603
ivle_pth = os.path.join(sys.prefix,
604
"lib/python2.5/site-packages/ivle.pth")
605
ivle_www = os.path.join(ivle_install_dir, "www")
606
write_ivle_pth = True
608
file = open(ivle_pth, 'r')
610
if line.strip() == ivle_www:
611
write_ivle_pth = False
613
except (IOError, OSError):
616
action_append(ivle_pth, ivle_www)
620
def updatejails(args):
621
# Get "dry" variable from command line
622
(opts, args) = getopt.gnu_getopt(args, "n", ['dry'])
624
dry = '-n' in opts or '--dry' in opts
627
print "Dry run (no actions will be executed\n"
629
if not dry and os.geteuid() != 0:
630
print >>sys.stderr, "Must be root to run install"
631
print >>sys.stderr, "(I need to chown some files)."
634
# Update the template jail directory in case it hasn't been installed
636
action_copytree('jail', os.path.join(jail_base, 'template'), dry)
638
# Re-link all the files in all students jails.
639
for dir in os.listdir(jail_base):
640
if dir == 'template': continue
641
# First back up the student's home directory
642
temp_home = os.tmpnam()
643
action_rename(os.path.join(jail_base, dir, 'home'), temp_home, dry)
644
# Delete the student's jail and relink the jail files
645
action_linktree(os.path.join(jail_base, 'template'),
646
os.path.join(jail_base, dir), dry)
647
# Restore the student's home directory
648
action_rename(temp_home, os.path.join(jail_base, dir, 'home'), dry)
649
# Set up the user's home directory just in case they don't have a
650
# directory for this yet
651
action_mkdir(os.path.join(jail_base, dir, 'home', dir), dry)
655
# The actions call Python os functions but print actions and handle dryness.
656
# May still throw os exceptions if errors occur.
659
"""Represents an error when running a program (nonzero return)."""
660
def __init__(self, prog, retcode):
662
self.retcode = retcode
664
return str(self.prog) + " returned " + repr(self.retcode)
666
def action_runprog(prog, args, dry):
667
"""Runs a unix program. Searches in $PATH. Synchronous (waits for the
668
program to return). Runs in the current environment. First prints the
669
action as a "bash" line.
671
Throws a RunError with a retcode of the return value of the program,
672
if the program did not return 0.
674
prog: String. Name of the program. (No path required, if in $PATH).
675
args: [String]. Arguments to the program.
676
dry: Bool. If True, prints but does not execute.
678
print prog, string.join(args, ' ')
680
ret = os.spawnvp(os.P_WAIT, prog, args)
682
raise RunError(prog, ret)
684
def action_rename(src, dst, dry):
685
"""Calls rename. Deletes the target if it already exists."""
686
if os.access(dst, os.F_OK):
689
shutil.rmtree(dst, True)
690
print "mv ", src, dst
694
except OSError, (err, msg):
695
if err != errno.EEXIST:
698
def action_mkdir(path, dry):
699
"""Calls mkdir. Silently ignored if the directory already exists.
700
Creates all parent directories as necessary."""
701
print "mkdir -p", path
705
except OSError, (err, msg):
706
if err != errno.EEXIST:
709
def action_copytree(src, dst, dry):
710
"""Copies an entire directory tree. Symlinks are seen as normal files and
711
copies of the entire file (not the link) are made. Creates all parent
712
directories as necessary.
714
See shutil.copytree."""
715
if os.access(dst, os.F_OK):
718
shutil.rmtree(dst, True)
719
print "cp -r", src, dst
721
shutil.copytree(src, dst, True)
723
def action_linktree(src, dst, dry):
724
"""Hard-links an entire directory tree. Same as copytree but the created
725
files are hard-links not actual copies. Removes the existing destination.
727
if os.access(dst, os.F_OK):
730
shutil.rmtree(dst, True)
731
print "<cp with hardlinks> -r", src, dst
733
common.makeuser.linktree(src, dst)
735
def action_copylist(srclist, dst, dry):
736
"""Copies all files in a list to a new location. The files in the list
737
are read relative to the current directory, and their destinations are the
738
same paths relative to dst. Creates all parent directories as necessary.
740
for srcfile in srclist:
741
dstfile = os.path.join(dst, srcfile)
742
dstdir = os.path.split(dstfile)[0]
743
if not os.path.isdir(dstdir):
744
action_mkdir(dstdir, dry)
745
print "cp -f", srcfile, dstfile
748
shutil.copyfile(srcfile, dstfile)
749
shutil.copymode(srcfile, dstfile)
753
def action_copyfile(src, dst, dry):
754
"""Copies one file to a new location. Creates all parent directories
757
dstdir = os.path.split(dst)[0]
758
if not os.path.isdir(dstdir):
759
action_mkdir(dstdir, dry)
760
print "cp -f", src, dst
763
shutil.copyfile(src, dst)
764
shutil.copymode(src, dst)
768
def action_symlink(src, dst, dry):
769
"""Creates a symlink in a given location. Creates all parent directories
772
dstdir = os.path.split(dst)[0]
773
if not os.path.isdir(dstdir):
774
action_mkdir(dstdir, dry)
775
# Delete existing file
776
if os.path.exists(dst):
778
print "ln -fs", src, dst
782
def action_append(ivle_pth, ivle_www):
783
file = open(ivle_pth, 'a+')
784
file.write(ivle_www + '\n')
787
def action_chown_setuid(file, dry):
788
"""Chowns a file to root, and sets the setuid bit on the file.
789
Calling this function requires the euid to be root.
790
The actual mode of path is set to: rws--s--s
792
print "chown root:root", file
795
print "chmod a+xs", file
796
print "chmod u+rw", file
798
os.chmod(file, stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
799
| stat.S_ISUID | stat.S_IRUSR | stat.S_IWUSR)
801
def action_chmod_x(file, dry):
802
"""Chmod +xs a file (sets execute permission)."""
803
print "chmod u+rwx", file
805
os.chmod(file, stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR)
807
def query_user(default, prompt):
808
"""Prompts the user for a string, which is read from a line of stdin.
809
Exits silently if EOF is encountered. Returns the string, with spaces
810
removed from the beginning and end.
812
Returns default if a 0-length line (after spaces removed) was read.
814
sys.stdout.write('%s\n (default: "%s")\n>' % (prompt, default))
816
val = sys.stdin.readline()
817
except KeyboardInterrupt:
819
sys.stdout.write("\n")
821
sys.stdout.write("\n")
823
if val == '': sys.exit(1)
824
# If empty line, return default
826
if val == '': return default
829
def filter_mutate(function, list):
830
"""Like built-in filter, but mutates the given list instead of returning a
831
new one. Returns None."""
834
# Delete elements which do not match
835
if not function(list[i]):
88
839
if __name__ == "__main__":