23
23
# This is a command-line application, for use by the administrator.
24
# This program configures, builds and installs IVLE in three separate steps.
24
# This program is a frontend for the modules in the setup packages that
25
# build and install IVLE in separate steps.
25
26
# 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
public_host = confmodule.public_host
147
public_host = "public.localhost"
149
jail_base = confmodule.jail_base
151
jail_base = "/home/informatics/jails"
153
# Just set reasonable defaults
155
ivle_install_dir = "/opt/ivle"
156
public_host = "public.localhost"
157
jail_base = "/home/informatics/jails"
161
# Try importing install_list, but don't fail if we can't, because listmake can
162
# function without it.
168
# Mime types which will automatically be placed in the list by listmake.
169
# Note that listmake is not intended to be run by the final user (the system
170
# administrator who installs this), so the developers can customize the list
171
# as necessary, and include it in the distribution.
172
listmake_mimetypes = ['text/x-python', 'text/html',
173
'application/x-javascript', 'application/javascript',
174
'text/css', 'image/png']
176
# Main function skeleton from Guido van Rossum
177
# http://www.artima.com/weblogs/viewpost.jsp?thread=4829
179
33
def main(argv=None):
202
# Disallow run as root unless installing
203
if (operation != 'install' and operation != 'updatejails'
204
and os.geteuid() == 0):
205
print >>sys.stderr, "I do not want to run this stage as root."
206
print >>sys.stderr, "Please run as a normal user."
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):
208
74
# Call the requested operation's function
214
'listmake' : listmake,
216
'updatejails' : updatejails,
78
'build' : setup.build.build,
79
'install' : setup.install.install,
219
82
print >>sys.stderr, (
220
83
"""Invalid operation '%s'. Try python setup.py help."""
223
return oper_func(argv[2:])
225
# Operation functions
229
print """Usage: python setup.py operation [args]
230
Operation (and args) can be:
232
listmake (developer use only)
235
install [--nojail] [-n|--dry]
239
print """Usage: python setup.py help [operation]"""
244
if operation == 'help':
245
print """python setup.py help [operation]
246
Prints the usage message or detailed help on an operation, then exits."""
247
elif operation == 'listmake':
248
print """python setup.py listmake
249
(For developer use only)
250
Recurses through the source tree and builds a list of all files which should
251
be copied upon installation. This should be run by the developer before
252
cutting a distribution, and the listfile it generates should be included in
253
the distribution, avoiding the administrator having to run it."""
254
elif operation == 'conf':
255
print """python setup.py conf [args]
256
Configures IVLE with machine-specific details, most notably, various paths.
257
Either prompts the administrator for these details or accepts them as
258
command-line args. Will be interactive only if there are no arguments given.
259
Takes defaults from existing conf file if it exists.
261
To run IVLE out of the source directory (allowing development without having
262
to rebuild/install), just provide ivle_install_dir as the IVLE trunk
263
directory, and run build/install one time.
265
Creates www/conf/conf.py and trampoline/conf.h.
273
As explained in the interactive prompt or conf.py.
275
elif operation == 'build':
276
print """python -O setup.py build [--dry|-n]
277
Compiles all files and sets up a jail template in the source directory.
278
-O is recommended to cause compilation to be optimised.
280
Compiles (GCC) trampoline/trampoline.c to trampoline/trampoline.
282
Creates standard subdirs inside the jail, eg bin, opt, home, tmp.
283
Copies console/ to a location within the jail.
284
Copies OS programs and files to corresponding locations within the jail
285
(eg. python and Python libs, ld.so, etc).
286
Generates .pyc or .pyo files for all the IVLE .py files.
288
--dry | -n Print out the actions but don't do anything."""
289
elif operation == 'install':
290
print """sudo python setup.py install [--nojail] [--dry|-n]
292
Create target install directory ($target).
294
Copy trampoline/trampoline to $target/bin.
295
chown and chmod the installed trampoline.
296
Copy www/ to $target.
297
Copy jail/ to jails template directory (unless --nojail specified).
299
--nojail Do not copy the jail.
300
--dry | -n Print out the actions but don't do anything."""
301
elif operation == 'updatejails':
302
print """sudo python setup.py updatejails [--dry|-n]
304
Copy jail/ to each subdirectory in jails directory.
306
--dry | -n Print out the actions but don't do anything."""
308
print >>sys.stderr, (
309
"""Invalid operation '%s'. Try python setup.py help."""
314
# We build two separate lists, by walking www and console
315
list_www = build_list_py_files('www')
316
list_console = build_list_py_files('console')
317
# Make sure that the files generated by conf are in the list
318
# (since listmake is typically run before conf)
319
if "www/conf/conf.py" not in list_www:
320
list_www.append("www/conf/conf.py")
321
# Make sure that console/python-console is in the list
322
if "console/python-console" not in list_console:
323
list_console.append("console/python-console")
324
# Write these out to a file
326
# the files that will be created/overwritten
327
listfile = os.path.join(cwd, "install_list.py")
330
file = open(listfile, "w")
332
file.write("""# IVLE Configuration File
334
# Provides lists of all files to be installed by `setup.py install' from
335
# certain directories.
336
# Note that any files with the given filename plus 'c' or 'o' (that is,
337
# compiled .pyc or .pyo files) will be copied as well.
339
# List of all installable files in www directory.
341
writelist_pretty(file, list_www)
343
# List of all installable files in console directory.
345
writelist_pretty(file, list_console)
348
except IOError, (errno, strerror):
349
print "IO error(%s): %s" % (errno, strerror)
352
print "Successfully wrote install_list.py"
355
print ("You may modify the set of installable files before cutting the "
362
def build_list_py_files(dir):
363
"""Builds a list of all py files found in a directory and its
364
subdirectories. Returns this as a list of strings."""
366
for (dirpath, dirnames, filenames) in os.walk(dir):
367
# Exclude directories beginning with a '.' (such as '.svn')
368
filter_mutate(lambda x: x[0] != '.', dirnames)
369
# All *.py files are added to the list
370
pylist += [os.path.join(dirpath, item) for item in filenames
371
if mimetypes.guess_type(item)[0] in listmake_mimetypes]
374
def writelist_pretty(file, list):
375
"""Writes a list one element per line, to a file."""
381
file.write(' %s,\n' % repr(elem))
385
global root_dir, ivle_install_dir, jail_base, public_host, allowed_uids
386
# Set up some variables
389
# the files that will be created/overwritten
390
conffile = os.path.join(cwd, "www/conf/conf.py")
391
conf_hfile = os.path.join(cwd, "trampoline/conf.h")
393
# Get command-line arguments to avoid asking questions.
395
(opts, args) = getopt.gnu_getopt(args, "", ['root_dir=',
396
'ivle_install_dir=', 'jail_base=', 'allowed_uids='])
399
print >>sys.stderr, "Invalid arguments:", string.join(args, ' ')
403
# Interactive mode. Prompt the user for all the values.
405
print """This tool will create the following files:
408
prompting you for details about your configuration. The file will be
409
overwritten if it already exists. It will *not* install or deploy IVLE.
411
Please hit Ctrl+C now if you do not wish to do this.
412
""" % (conffile, conf_hfile)
414
# Get information from the administrator
415
# If EOF is encountered at any time during the questioning, just exit
418
root_dir = query_user(root_dir,
419
"""Root directory where IVLE is located (in URL space):""")
420
ivle_install_dir = query_user(ivle_install_dir,
421
'Root directory where IVLE will be installed (on the local file '
423
jail_base = query_user(jail_base,
424
"""Root directory where the jails (containing user files) are stored
425
(on the local file system):""")
426
public_host = query_user(public_host,
427
"""Hostname which will cause the server to go into "public mode",
428
providing login-free access to student's published work:""")
429
allowed_uids = query_user(allowed_uids,
430
"""UID of the web server process which will run IVLE.
431
Only this user may execute the trampoline. May specify multiple users as
432
a comma-separated list.
437
# Non-interactive mode. Parse the options.
438
if '--root_dir' in opts:
439
root_dir = opts['--root_dir']
440
if '--ivle_install_dir' in opts:
441
ivle_install_dir = opts['--ivle_install_dir']
442
if '--jail_base' in opts:
443
jail_base = opts['--jail_base']
444
if '--public_host' in opts:
445
public_host = opts['--public_host']
446
if '--allowed_uids' in opts:
447
allowed_uids = opts['--allowed_uids']
449
# Error handling on input values
451
allowed_uids = map(int, allowed_uids.split(','))
453
print >>sys.stderr, (
454
"Invalid UID list (%s).\n"
455
"Must be a comma-separated list of integers." % allowed_uids)
458
# Write www/conf/conf.py
461
conf = open(conffile, "w")
463
conf.write("""# IVLE Configuration File
465
# Miscellaneous application settings
468
# In URL space, where in the site is IVLE located. (All URLs will be prefixed
470
# eg. "/" or "/ivle".
473
# In the local file system, where IVLE is actually installed.
474
# This directory should contain the "www" and "bin" directories.
475
ivle_install_dir = "%s"
477
# The server goes into "public mode" if the browser sends a request with this
478
# host. This is for security reasons - we only serve public student files on a
479
# separate domain to the main IVLE site.
480
# Public mode does not use cookies, and serves only public content.
481
# Private mode (normal mode) requires login, and only serves files relevant to
482
# the logged-in user.
485
# In the local file system, where are the student/user file spaces located.
486
# The user jails are expected to be located immediately in subdirectories of
489
""" % (root_dir, ivle_install_dir, public_host, jail_base))
492
except IOError, (errno, strerror):
493
print "IO error(%s): %s" % (errno, strerror)
496
print "Successfully wrote www/conf/conf.py"
498
# Write trampoline/conf.h
501
conf = open(conf_hfile, "w")
503
conf.write("""/* IVLE Configuration File
505
* Administrator settings required by trampoline.
506
* Note: trampoline will have to be rebuilt in order for changes to this file
510
/* In the local file system, where are the jails located.
511
* The trampoline does not allow the creation of a jail anywhere besides
512
* jail_base or a subdirectory of jail_base.
514
static const char* jail_base = "%s";
516
/* Which user IDs are allowed to run the trampoline.
517
* This list should be limited to the web server user.
518
* (Note that root is an implicit member of this list).
520
static const int allowed_uids[] = { %s };
521
""" % (jail_base, repr(allowed_uids)[1:-1]))
524
except IOError, (errno, strerror):
525
print "IO error(%s): %s" % (errno, strerror)
528
print "Successfully wrote trampoline/conf.h"
531
print "You may modify the configuration at any time by editing"
538
# Get "dry" variable from command line
539
(opts, args) = getopt.gnu_getopt(args, "n", ['dry'])
541
dry = '-n' in opts or '--dry' in opts
544
print "Dry run (no actions will be executed\n"
546
# Compile the trampoline
547
action_runprog('gcc', ['-Wall', '-o', 'trampoline/trampoline',
548
'trampoline/trampoline.c'], dry)
550
# Create the jail and its subdirectories
551
# Note: Other subdirs will be made by copying files
552
action_mkdir('jail', dry)
553
action_mkdir('jail/home', dry)
554
action_mkdir('jail/tmp', dry)
556
# Copy all console and operating system files into the jail
557
action_copylist(install_list.list_console, 'jail/opt/ivle', dry)
558
copy_os_files_jail(dry)
559
# Chmod the python console
560
action_chmod_x('jail/opt/ivle/console/python-console', dry)
563
# Compile .py files into .pyc or .pyo files
564
compileall.compile_dir('www', quiet=True)
565
compileall.compile_dir('console', quiet=True)
569
def copy_os_files_jail(dry):
570
"""Copies necessary Operating System files from their usual locations
571
into the jail/ directory of the cwd."""
572
# Currently source paths are configured for Ubuntu.
573
for filename in JAIL_FILES:
574
copy_file_to_jail(filename, dry)
575
for src, dst in JAIL_LINKS.items():
576
action_symlink(src, dst, dry)
577
for src, dst in JAIL_COPYTREES.items():
578
action_copytree(src, dst, dry)
580
def copy_file_to_jail(src, dry):
581
"""Copies a single file from an absolute location into the same location
582
within the jail. src must begin with a '/'. The jail will be located
583
in a 'jail' subdirectory of the current path."""
584
action_copyfile(src, 'jail' + src, dry)
587
# Get "dry" and "nojail" variables from command line
588
(opts, args) = getopt.gnu_getopt(args, "n", ['dry', 'nojail'])
590
dry = '-n' in opts or '--dry' in opts
591
nojail = '--nojail' in opts
594
print "Dry run (no actions will be executed\n"
596
if not dry and os.geteuid() != 0:
597
print >>sys.stderr, "Must be root to run install"
598
print >>sys.stderr, "(I need to chown some files)."
601
# Create the target (install) directory
602
action_mkdir(ivle_install_dir, dry)
604
# Create bin and copy the compiled files there
605
action_mkdir(os.path.join(ivle_install_dir, 'bin'), dry)
606
tramppath = os.path.join(ivle_install_dir, 'bin/trampoline')
607
action_copyfile('trampoline/trampoline', tramppath, dry)
608
# chown trampoline to root and set setuid bit
609
action_chown_setuid(tramppath, dry)
611
# Copy the www directory using the list
612
action_copylist(install_list.list_www, ivle_install_dir, dry)
615
# Copy the local jail directory built by the build action
616
# to the jails template directory (it will be used as a template
617
# for all the students' jails).
618
action_copytree('jail', os.path.join(jail_base, 'template'), dry)
620
# Append IVLE path to ivle.pth in python site packages
621
# (Unless it's already there)
622
ivle_pth = os.path.join(sys.prefix,
623
"lib/python2.5/site-packages/ivle.pth")
624
ivle_www = os.path.join(ivle_install_dir, "www")
625
write_ivle_pth = True
627
file = open(ivle_pth, 'r')
629
if line.strip() == ivle_www:
630
write_ivle_pth = False
632
except (IOError, OSError):
635
action_append(ivle_pth, ivle_www)
639
def updatejails(args):
640
# Get "dry" variable from command line
641
(opts, args) = getopt.gnu_getopt(args, "n", ['dry'])
643
dry = '-n' in opts or '--dry' in opts
646
print "Dry run (no actions will be executed\n"
648
if not dry and os.geteuid() != 0:
649
print >>sys.stderr, "Must be root to run install"
650
print >>sys.stderr, "(I need to chown some files)."
653
# Update the template jail directory in case it hasn't been installed
655
action_copytree('jail', os.path.join(jail_base, 'template'), dry)
657
# Re-link all the files in all students jails.
658
for dir in os.listdir(jail_base):
659
if dir == 'template': continue
660
# First back up the student's home directory
661
temp_home = os.tmpnam()
662
action_rename(os.path.join(jail_base, dir, 'home'), temp_home, dry)
663
# Delete the student's jail and relink the jail files
664
action_linktree(os.path.join(jail_base, 'template'),
665
os.path.join(jail_base, dir), dry)
666
# Restore the student's home directory
667
action_rename(temp_home, os.path.join(jail_base, dir, 'home'), dry)
668
# Set up the user's home directory just in case they don't have a
669
# directory for this yet
670
action_mkdir(os.path.join(jail_base, dir, 'home', dir), dry)
674
# The actions call Python os functions but print actions and handle dryness.
675
# May still throw os exceptions if errors occur.
678
"""Represents an error when running a program (nonzero return)."""
679
def __init__(self, prog, retcode):
681
self.retcode = retcode
683
return str(self.prog) + " returned " + repr(self.retcode)
685
def action_runprog(prog, args, dry):
686
"""Runs a unix program. Searches in $PATH. Synchronous (waits for the
687
program to return). Runs in the current environment. First prints the
688
action as a "bash" line.
690
Throws a RunError with a retcode of the return value of the program,
691
if the program did not return 0.
693
prog: String. Name of the program. (No path required, if in $PATH).
694
args: [String]. Arguments to the program.
695
dry: Bool. If True, prints but does not execute.
697
print prog, string.join(args, ' ')
699
ret = os.spawnvp(os.P_WAIT, prog, args)
701
raise RunError(prog, ret)
703
def action_rename(src, dst, dry):
704
"""Calls rename. Deletes the target if it already exists."""
705
if os.access(dst, os.F_OK):
708
shutil.rmtree(dst, True)
709
print "mv ", src, dst
713
except OSError, (err, msg):
714
if err != errno.EEXIST:
717
def action_mkdir(path, dry):
718
"""Calls mkdir. Silently ignored if the directory already exists.
719
Creates all parent directories as necessary."""
720
print "mkdir -p", path
724
except OSError, (err, msg):
725
if err != errno.EEXIST:
728
def action_copytree(src, dst, dry):
729
"""Copies an entire directory tree. Symlinks are seen as normal files and
730
copies of the entire file (not the link) are made. Creates all parent
731
directories as necessary.
733
See shutil.copytree."""
734
if os.access(dst, os.F_OK):
737
shutil.rmtree(dst, True)
738
print "cp -r", src, dst
740
shutil.copytree(src, dst, True)
742
def action_linktree(src, dst, dry):
743
"""Hard-links an entire directory tree. Same as copytree but the created
744
files are hard-links not actual copies. Removes the existing destination.
746
if os.access(dst, os.F_OK):
749
shutil.rmtree(dst, True)
750
print "<cp with hardlinks> -r", src, dst
752
common.makeuser.linktree(src, dst)
754
def action_copylist(srclist, dst, dry):
755
"""Copies all files in a list to a new location. The files in the list
756
are read relative to the current directory, and their destinations are the
757
same paths relative to dst. Creates all parent directories as necessary.
759
for srcfile in srclist:
760
dstfile = os.path.join(dst, srcfile)
761
dstdir = os.path.split(dstfile)[0]
762
if not os.path.isdir(dstdir):
763
action_mkdir(dstdir, dry)
764
print "cp -f", srcfile, dstfile
767
shutil.copyfile(srcfile, dstfile)
768
shutil.copymode(srcfile, dstfile)
772
def action_copyfile(src, dst, dry):
773
"""Copies one file to a new location. Creates all parent directories
776
dstdir = os.path.split(dst)[0]
777
if not os.path.isdir(dstdir):
778
action_mkdir(dstdir, dry)
779
print "cp -f", src, dst
782
shutil.copyfile(src, dst)
783
shutil.copymode(src, dst)
787
def action_symlink(src, dst, dry):
788
"""Creates a symlink in a given location. Creates all parent directories
791
dstdir = os.path.split(dst)[0]
792
if not os.path.isdir(dstdir):
793
action_mkdir(dstdir, dry)
794
# Delete existing file
795
if os.path.exists(dst):
797
print "ln -fs", src, dst
801
def action_append(ivle_pth, ivle_www):
802
file = open(ivle_pth, 'a+')
803
file.write(ivle_www + '\n')
806
def action_chown_setuid(file, dry):
807
"""Chowns a file to root, and sets the setuid bit on the file.
808
Calling this function requires the euid to be root.
809
The actual mode of path is set to: rws--s--s
811
print "chown root:root", file
814
print "chmod a+xs", file
815
print "chmod u+rw", file
817
os.chmod(file, stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
818
| stat.S_ISUID | stat.S_IRUSR | stat.S_IWUSR)
820
def action_chmod_x(file, dry):
821
"""Chmod +xs a file (sets execute permission)."""
822
print "chmod u+rwx", file
824
os.chmod(file, stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR)
826
def query_user(default, prompt):
827
"""Prompts the user for a string, which is read from a line of stdin.
828
Exits silently if EOF is encountered. Returns the string, with spaces
829
removed from the beginning and end.
831
Returns default if a 0-length line (after spaces removed) was read.
833
sys.stdout.write('%s\n (default: "%s")\n>' % (prompt, default))
835
val = sys.stdin.readline()
836
except KeyboardInterrupt:
838
sys.stdout.write("\n")
840
sys.stdout.write("\n")
842
if val == '': sys.exit(1)
843
# If empty line, return default
845
if val == '': return default
848
def filter_mutate(function, list):
849
"""Like built-in filter, but mutates the given list instead of returning a
850
new one. Returns None."""
853
# Delete elements which do not match
854
if not function(list[i]):
858
88
if __name__ == "__main__":