2
# IVLE - Informatics Virtual Learning Environment
3
# Copyright (C) 2007-2008 The University of Melbourne
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 2 of the License, or
8
# (at your option) any later version.
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23
# This is a command-line application, for use by the administrator.
24
# This program configures, builds and installs IVLE in three separate steps.
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
# Try importing existing conf, but if we can't just set up defaults
72
# The reason for this is that these settings are used by other phases
73
# of setup besides conf, so we need to know them.
74
# Also this allows you to hit Return to accept the existing value.
76
confmodule = __import__("www/conf/conf")
78
root_dir = confmodule.root_dir
82
ivle_install_dir = confmodule.ivle_install_dir
84
ivle_install_dir = "/opt/ivle"
86
jail_base = confmodule.jail_base
88
jail_base = "/home/informatics/jails"
90
# Just set reasonable defaults
92
ivle_install_dir = "/opt/ivle"
93
jail_base = "/home/informatics/jails"
97
# Try importing install_list, but don't fail if we can't, because listmake can
98
# function without it.
104
# Mime types which will automatically be placed in the list by listmake.
105
# Note that listmake is not intended to be run by the final user (the system
106
# administrator who installs this), so the developers can customize the list
107
# as necessary, and include it in the distribution.
108
listmake_mimetypes = ['text/x-python', 'text/html',
109
'application/x-javascript', 'application/javascript',
110
'text/css', 'image/png']
112
# Main function skeleton from Guido van Rossum
113
# http://www.artima.com/weblogs/viewpost.jsp?thread=4829
119
# Print the opening spiel including the GPL notice
121
print """IVLE - Informatics Virtual Learning Environment Setup
122
Copyright (C) 2007-2008 The University of Melbourne
123
IVLE comes with ABSOLUTELY NO WARRANTY.
124
This is free software, and you are welcome to redistribute it
125
under certain conditions. See LICENSE.txt for details.
130
# First argument is the name of the setup operation
134
# Print usage message and exit
138
# Disallow run as root unless installing
139
if operation != 'install' and os.geteuid() == 0:
140
print >>sys.stderr, "I do not want to run this stage as root."
141
print >>sys.stderr, "Please run as a normal user."
143
# Call the requested operation's function
149
'listmake' : listmake,
153
print >>sys.stderr, (
154
"""Invalid operation '%s'. Try python setup.py help."""
157
return oper_func(argv[2:])
159
# Operation functions
163
print """Usage: python setup.py operation [args]
164
Operation (and args) can be:
166
listmake (developer use only)
169
install [--nojail] [-n|--dry]
173
print """Usage: python setup.py help [operation]"""
178
if operation == 'help':
179
print """python setup.py help [operation]
180
Prints the usage message or detailed help on an operation, then exits."""
181
elif operation == 'listmake':
182
print """python setup.py listmake
183
(For developer use only)
184
Recurses through the source tree and builds a list of all files which should
185
be copied upon installation. This should be run by the developer before
186
cutting a distribution, and the listfile it generates should be included in
187
the distribution, avoiding the administrator having to run it."""
188
elif operation == 'conf':
189
print """python setup.py conf [args]
190
Configures IVLE with machine-specific details, most notably, various paths.
191
Either prompts the administrator for these details or accepts them as
192
command-line args. Will be interactive only if there are no arguments given.
193
Takes defaults from existing conf file if it exists.
195
To run IVLE out of the source directory (allowing development without having
196
to rebuild/install), just provide ivle_install_dir as the IVLE trunk
197
directory, and run build/install one time.
199
Creates www/conf/conf.py and trampoline/conf.h.
206
As explained in the interactive prompt or conf.py.
208
elif operation == 'build':
209
print """python -O setup.py build [--dry|-n]
210
Compiles all files and sets up a jail template in the source directory.
211
-O is recommended to cause compilation to be optimised.
213
Compiles (GCC) trampoline/trampoline.c to trampoline/trampoline.
215
Creates standard subdirs inside the jail, eg bin, opt, home, tmp.
216
Copies console/ to a location within the jail.
217
Copies OS programs and files to corresponding locations within the jail
218
(eg. python and Python libs, ld.so, etc).
219
Generates .pyc or .pyo files for all the IVLE .py files.
221
--dry | -n Print out the actions but don't do anything."""
222
elif operation == 'install':
223
print """sudo python setup.py install [--nojail] [--dry|-n]
225
Create target install directory ($target).
227
Copy trampoline/trampoline to $target/bin.
228
chown and chmod the installed trampoline.
229
Copy www/ to $target.
230
Copy jail/ to jails template directory (unless --nojail specified).
232
--nojail Do not copy the jail.
233
--dry | -n Print out the actions but don't do anything."""
235
print >>sys.stderr, (
236
"""Invalid operation '%s'. Try python setup.py help."""
241
# We build two separate lists, by walking www and console
242
list_www = build_list_py_files('www')
243
list_console = build_list_py_files('console')
244
# Make sure that the files generated by conf are in the list
245
# (since listmake is typically run before conf)
246
if "www/conf/conf.py" not in list_www:
247
list_www.append("www/conf/conf.py")
248
# Make sure that console/python-console is in the list
249
if "console/python-console" not in list_console:
250
list_console.append("console/python-console")
251
# Write these out to a file
253
# the files that will be created/overwritten
254
listfile = os.path.join(cwd, "install_list.py")
257
file = open(listfile, "w")
259
file.write("""# IVLE Configuration File
261
# Provides lists of all files to be installed by `setup.py install' from
262
# certain directories.
263
# Note that any files with the given filename plus 'c' or 'o' (that is,
264
# compiled .pyc or .pyo files) will be copied as well.
266
# List of all installable files in www directory.
268
writelist_pretty(file, list_www)
270
# List of all installable files in console directory.
272
writelist_pretty(file, list_console)
275
except IOError, (errno, strerror):
276
print "IO error(%s): %s" % (errno, strerror)
279
print "Successfully wrote install_list.py"
282
print ("You may modify the set of installable files before cutting the "
289
def build_list_py_files(dir):
290
"""Builds a list of all py files found in a directory and its
291
subdirectories. Returns this as a list of strings."""
293
for (dirpath, dirnames, filenames) in os.walk(dir):
294
# Exclude directories beginning with a '.' (such as '.svn')
295
filter_mutate(lambda x: x[0] != '.', dirnames)
296
# All *.py files are added to the list
297
pylist += [os.path.join(dirpath, item) for item in filenames
298
if mimetypes.guess_type(item)[0] in listmake_mimetypes]
301
def writelist_pretty(file, list):
302
"""Writes a list one element per line, to a file."""
308
file.write(' %s,\n' % repr(elem))
312
global root_dir, ivle_install_dir, jail_base, allowed_uids
313
# Set up some variables
316
# the files that will be created/overwritten
317
conffile = os.path.join(cwd, "www/conf/conf.py")
318
conf_hfile = os.path.join(cwd, "trampoline/conf.h")
320
# Get command-line arguments to avoid asking questions.
322
(opts, args) = getopt.gnu_getopt(args, "", ['root_dir=',
323
'ivle_install_dir=', 'jail_base=', 'allowed_uids='])
326
print >>sys.stderr, "Invalid arguments:", string.join(args, ' ')
330
# Interactive mode. Prompt the user for all the values.
332
print """This tool will create the following files:
335
prompting you for details about your configuration. The file will be
336
overwritten if it already exists. It will *not* install or deploy IVLE.
338
Please hit Ctrl+C now if you do not wish to do this.
339
""" % (conffile, conf_hfile)
341
# Get information from the administrator
342
# If EOF is encountered at any time during the questioning, just exit
345
root_dir = query_user(root_dir,
346
"""Root directory where IVLE is located (in URL space):""")
347
ivle_install_dir = query_user(ivle_install_dir,
348
'Root directory where IVLE will be installed (on the local file '
350
jail_base = query_user(jail_base,
351
"""Root directory where the jails (containing user files) are stored
352
(on the local file system):""")
353
allowed_uids = query_user(allowed_uids,
354
"""UID of the web server process which will run IVLE.
355
Only this user may execute the trampoline. May specify multiple users as
356
a comma-separated list.
361
# Non-interactive mode. Parse the options.
362
if '--root_dir' in opts:
363
root_dir = opts['--root_dir']
364
if '--ivle_install_dir' in opts:
365
ivle_install_dir = opts['--ivle_install_dir']
366
if '--jail_base' in opts:
367
jail_base = opts['--jail_base']
368
if '--allowed_uids' in opts:
369
allowed_uids = opts['--allowed_uids']
371
# Error handling on input values
373
allowed_uids = map(int, allowed_uids.split(','))
375
print >>sys.stderr, (
376
"Invalid UID list (%s).\n"
377
"Must be a comma-separated list of integers." % allowed_uids)
380
# Write www/conf/conf.py
383
conf = open(conffile, "w")
385
conf.write("""# IVLE Configuration File
387
# Miscellaneous application settings
390
# In URL space, where in the site is IVLE located. (All URLs will be prefixed
392
# eg. "/" or "/ivle".
395
# In the local file system, where IVLE is actually installed.
396
# This directory should contain the "www" and "bin" directories.
397
ivle_install_dir = "%s"
399
# In the local file system, where are the student/user file spaces located.
400
# The user jails are expected to be located immediately in subdirectories of
403
""" % (root_dir, ivle_install_dir, jail_base))
406
except IOError, (errno, strerror):
407
print "IO error(%s): %s" % (errno, strerror)
410
print "Successfully wrote www/conf/conf.py"
412
# Write trampoline/conf.h
415
conf = open(conf_hfile, "w")
417
conf.write("""/* IVLE Configuration File
419
* Administrator settings required by trampoline.
420
* Note: trampoline will have to be rebuilt in order for changes to this file
424
/* In the local file system, where are the jails located.
425
* The trampoline does not allow the creation of a jail anywhere besides
426
* jail_base or a subdirectory of jail_base.
428
static const char* jail_base = "%s";
430
/* Which user IDs are allowed to run the trampoline.
431
* This list should be limited to the web server user.
432
* (Note that root is an implicit member of this list).
434
static const int allowed_uids[] = { %s };
435
""" % (jail_base, repr(allowed_uids)[1:-1]))
438
except IOError, (errno, strerror):
439
print "IO error(%s): %s" % (errno, strerror)
442
print "Successfully wrote trampoline/conf.h"
445
print "You may modify the configuration at any time by editing"
452
# Get "dry" variable from command line
453
(opts, args) = getopt.gnu_getopt(args, "n", ['dry'])
455
dry = '-n' in opts or '--dry' in opts
458
print "Dry run (no actions will be executed\n"
460
# Compile the trampoline
461
action_runprog('gcc', ['-Wall', '-o', 'trampoline/trampoline',
462
'trampoline/trampoline.c'], dry)
464
# Create the jail and its subdirectories
465
# Note: Other subdirs will be made by copying files
466
action_mkdir('jail', dry)
467
action_mkdir('jail/home', dry)
468
action_mkdir('jail/tmp', dry)
470
# Copy all console and operating system files into the jail
471
action_copylist(install_list.list_console, 'jail/opt/ivle', dry)
472
copy_os_files_jail(dry)
474
# Compile .py files into .pyc or .pyo files
475
compileall.compile_dir('www', quiet=True)
476
compileall.compile_dir('console', quiet=True)
480
def copy_os_files_jail(dry):
481
"""Copies necessary Operating System files from their usual locations
482
into the jail/ directory of the cwd."""
483
# Currently source paths are configured for Ubuntu.
484
copy_file_to_jail('/lib/ld-linux.so.2', dry)
485
copy_file_to_jail('/lib/tls/i686/cmov/libc.so.6', dry)
486
copy_file_to_jail('/lib/tls/i686/cmov/libdl.so.2', dry)
487
copy_file_to_jail('/lib/tls/i686/cmov/libm.so.6', dry)
488
copy_file_to_jail('/lib/tls/i686/cmov/libpthread.so.0', dry)
489
copy_file_to_jail('/lib/tls/i686/cmov/libutil.so.1', dry)
490
copy_file_to_jail('/usr/bin/python2.5', dry)
491
action_symlink('python2.5', 'jail/usr/bin/python', dry)
492
action_copytree('/usr/lib/python2.5', 'jail/usr/lib/python2.5', dry)
494
def copy_file_to_jail(src, dry):
495
"""Copies a single file from an absolute location into the same location
496
within the jail. src must begin with a '/'. The jail will be located
497
in a 'jail' subdirectory of the current path."""
498
action_copyfile(src, 'jail' + src, dry)
501
# Get "dry" and "nojail" variables from command line
502
(opts, args) = getopt.gnu_getopt(args, "n", ['dry', 'nojail'])
504
dry = '-n' in opts or '--dry' in opts
505
nojail = '--nojail' in opts
508
print "Dry run (no actions will be executed\n"
510
if not dry and os.geteuid() != 0:
511
print >>sys.stderr, "Must be root to run install"
512
print >>sys.stderr, "(I need to chown some files)."
515
# Create the target (install) directory
516
action_mkdir(ivle_install_dir, dry)
518
# Create bin and copy the compiled files there
519
action_mkdir(os.path.join(ivle_install_dir, 'bin'), dry)
520
tramppath = os.path.join(ivle_install_dir, 'bin/trampoline')
521
action_copyfile('trampoline/trampoline', tramppath, dry)
522
# chown trampoline to root and set setuid bit
523
action_chown_setuid(tramppath, dry)
525
# Copy the www directory using the list
526
action_copylist(install_list.list_www, ivle_install_dir, dry)
529
# Copy the local jail directory built by the build action
530
# to the jails template directory (it will be used as a template
531
# for all the students' jails).
532
action_copytree('jail', os.path.join(jail_base, 'template'), dry)
536
# The actions call Python os functions but print actions and handle dryness.
537
# May still throw os exceptions if errors occur.
540
"""Represents an error when running a program (nonzero return)."""
541
def __init__(self, prog, retcode):
543
self.retcode = retcode
545
return str(self.prog) + " returned " + repr(self.retcode)
547
def action_runprog(prog, args, dry):
548
"""Runs a unix program. Searches in $PATH. Synchronous (waits for the
549
program to return). Runs in the current environment. First prints the
550
action as a "bash" line.
552
Throws a RunError with a retcode of the return value of the program,
553
if the program did not return 0.
555
prog: String. Name of the program. (No path required, if in $PATH).
556
args: [String]. Arguments to the program.
557
dry: Bool. If True, prints but does not execute.
559
print prog, string.join(args, ' ')
561
ret = os.spawnvp(os.P_WAIT, prog, args)
563
raise RunError(prog, ret)
565
def action_mkdir(path, dry):
566
"""Calls mkdir. Silently ignored if the directory already exists.
567
Creates all parent directories as necessary."""
568
print "mkdir -p", path
572
except OSError, (err, msg):
573
if err != errno.EEXIST:
576
def action_copytree(src, dst, dry):
577
"""Copies an entire directory tree. Symlinks are seen as normal files and
578
copies of the entire file (not the link) are made. Creates all parent
579
directories as necessary.
581
See shutil.copytree."""
582
if os.access(dst, os.F_OK):
585
shutil.rmtree(dst, True)
586
print "cp -r", src, dst
588
shutil.copytree(src, dst, True)
590
def action_copylist(srclist, dst, dry):
591
"""Copies all files in a list to a new location. The files in the list
592
are read relative to the current directory, and their destinations are the
593
same paths relative to dst. Creates all parent directories as necessary.
595
for srcfile in srclist:
596
dstfile = os.path.join(dst, srcfile)
597
dstdir = os.path.split(dstfile)[0]
598
if not os.path.isdir(dstdir):
599
action_mkdir(dstdir, dry)
600
print "cp -f", srcfile, dstfile
603
shutil.copyfile(srcfile, dstfile)
604
shutil.copymode(srcfile, dstfile)
608
def action_copyfile(src, dst, dry):
609
"""Copies one file to a new location. Creates all parent directories
612
dstdir = os.path.split(dst)[0]
613
if not os.path.isdir(dstdir):
614
action_mkdir(dstdir, dry)
615
print "cp -f", src, dst
618
shutil.copyfile(src, dst)
619
shutil.copymode(src, dst)
623
def action_symlink(src, dst, dry):
624
"""Creates a symlink in a given location. Creates all parent directories
627
dstdir = os.path.split(dst)[0]
628
if not os.path.isdir(dstdir):
629
action_mkdir(dstdir, dry)
630
# Delete existing file
631
if os.path.exists(dst):
633
print "ln -fs", src, dst
637
def action_chown_setuid(file, dry):
638
"""Chowns a file to root, and sets the setuid bit on the file.
639
Calling this function requires the euid to be root.
640
The actual mode of path is set to: rws--s--s
642
print "chown root:root", file
645
print "chmod a+xs", file
646
print "chmod u+rw", file
648
os.chmod(file, stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
649
| stat.S_ISUID | stat.S_IRUSR | stat.S_IWUSR)
651
def query_user(default, prompt):
652
"""Prompts the user for a string, which is read from a line of stdin.
653
Exits silently if EOF is encountered. Returns the string, with spaces
654
removed from the beginning and end.
656
Returns default if a 0-length line (after spaces removed) was read.
658
sys.stdout.write('%s\n (default: "%s")\n>' % (prompt, default))
660
val = sys.stdin.readline()
661
except KeyboardInterrupt:
663
sys.stdout.write("\n")
665
sys.stdout.write("\n")
667
if val == '': sys.exit(1)
668
# If empty line, return default
670
if val == '': return default
673
def filter_mutate(function, list):
674
"""Like built-in filter, but mutates the given list instead of returning a
675
new one. Returns None."""
678
# Delete elements which do not match
679
if not function(list[i]):
683
if __name__ == "__main__":