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")
77
root_dir = confmodule.root_dir
78
ivle_install_dir = confmodule.ivle_install_dir
79
jail_base = confmodule.jail_base
81
# Just set reasonable defaults
83
ivle_install_dir = "/opt/ivle"
84
jail_base = "/home/informatics/jails"
88
# Try importing install_list, but don't fail if we can't, because listmake can
89
# function without it.
95
# Mime types which will automatically be placed in the list by listmake.
96
# Note that listmake is not intended to be run by the final user (the system
97
# administrator who installs this), so the developers can customize the list
98
# as necessary, and include it in the distribution.
99
listmake_mimetypes = ['text/x-python', 'text/html',
100
'application/x-javascript', 'application/javascript',
101
'text/css', 'image/png']
103
# Main function skeleton from Guido van Rossum
104
# http://www.artima.com/weblogs/viewpost.jsp?thread=4829
110
# Print the opening spiel including the GPL notice
112
print """IVLE - Informatics Virtual Learning Environment Setup
113
Copyright (C) 2007-2008 The University of Melbourne
114
IVLE comes with ABSOLUTELY NO WARRANTY.
115
This is free software, and you are welcome to redistribute it
116
under certain conditions. See LICENSE.txt for details.
121
# First argument is the name of the setup operation
125
# Print usage message and exit
129
# Call the requested operation's function
135
'listmake' : listmake,
139
print >>sys.stderr, (
140
"""Invalid operation '%s'. Try python setup.py help."""
143
return oper_func(argv[2:])
145
# Operation functions
149
print """Usage: python setup.py operation [args]
150
Operation (and args) can be:
154
install [--nojail] [-n|--dry]
158
print """Usage: python setup.py help [operation]"""
163
if operation == 'help':
164
print """python setup.py help [operation]
165
Prints the usage message or detailed help on an operation, then exits."""
166
elif operation == 'listmake':
167
print """python setup.py listmake
168
(For developer use only)
169
Recurses through the source tree and builds a list of all files which should
170
be copied upon installation. This should be run by the developer before
171
cutting a distribution, and the listfile it generates should be included in
172
the distribution, avoiding the administrator having to run it."""
173
elif operation == 'conf':
174
print """python setup.py conf [args]
175
Configures IVLE with machine-specific details, most notably, various paths.
176
Either prompts the administrator for these details or accepts them as
177
command-line args. Will be interactive only if there are no arguments given.
178
Takes defaults from existing conf file if it exists.
179
Creates www/conf/conf.py and trampoline/conf.h.
185
As explained in the interactive prompt or conf.py.
187
elif operation == 'build':
188
print """python -O setup.py build [--dry|-n]
189
Compiles all files and sets up a jail template in the source directory.
190
-O is recommended to cause compilation to be optimised.
192
Compiles (GCC) trampoline/trampoline.c to trampoline/trampoline.
194
Creates standard subdirs inside the jail, eg bin, opt, home, tmp.
195
Copies console/ to a location within the jail.
196
Copies OS programs and files to corresponding locations within the jail
197
(eg. python and Python libs, ld.so, etc).
198
Generates .pyc or .pyo files for all the IVLE .py files.
200
--dry | -n Print out the actions but don't do anything."""
201
elif operation == 'install':
202
print """sudo python setup.py install [--nojail] [--dry|-n]
204
Create target install directory ($target).
206
Copy trampoline/trampoline to $target/bin.
207
chown and chmod the installed trampoline.
208
Copy www/ to $target.
209
Copy jail/ to jails template directory (unless --nojail specified).
211
--nojail Do not copy the jail.
212
--dry | -n Print out the actions but don't do anything."""
214
print >>sys.stderr, (
215
"""Invalid operation '%s'. Try python setup.py help."""
220
# We build two separate lists, by walking www and console
221
list_www = build_list_py_files('www')
222
list_console = build_list_py_files('console')
223
# Make sure that the files generated by conf are in the list
224
# (since listmake is typically run before conf)
225
if "www/conf/conf.py" not in list_www:
226
list_www.append("www/conf/conf.py")
227
# Make sure that console/python-console is in the list
228
if "console/python-console" not in list_console:
229
list_console.append("console/python-console")
230
# Write these out to a file
232
# the files that will be created/overwritten
233
listfile = os.path.join(cwd, "install_list.py")
236
file = open(listfile, "w")
238
file.write("""# IVLE Configuration File
240
# Provides lists of all files to be installed by `setup.py install' from
241
# certain directories.
242
# Note that any files with the given filename plus 'c' or 'o' (that is,
243
# compiled .pyc or .pyo files) will be copied as well.
245
# List of all installable files in www directory.
247
writelist_pretty(file, list_www)
249
# List of all installable files in console directory.
251
writelist_pretty(file, list_console)
254
except IOError, (errno, strerror):
255
print "IO error(%s): %s" % (errno, strerror)
258
print "Successfully wrote install_list.py"
261
print ("You may modify the set of installable files before cutting the "
268
def build_list_py_files(dir):
269
"""Builds a list of all py files found in a directory and its
270
subdirectories. Returns this as a list of strings."""
272
for (dirpath, dirnames, filenames) in os.walk(dir):
273
# Exclude directories beginning with a '.' (such as '.svn')
274
filter_mutate(lambda x: x[0] != '.', dirnames)
275
# All *.py files are added to the list
276
pylist += [os.path.join(dirpath, item) for item in filenames
277
if mimetypes.guess_type(item)[0] in listmake_mimetypes]
280
def writelist_pretty(file, list):
281
"""Writes a list one element per line, to a file."""
287
file.write(' %s,\n' % repr(elem))
291
global root_dir, ivle_install_dir, jail_base, allowed_uids
292
# Set up some variables
295
# the files that will be created/overwritten
296
conffile = os.path.join(cwd, "www/conf/conf.py")
297
conf_hfile = os.path.join(cwd, "trampoline/conf.h")
299
# Fixed config options that we don't ask the admin
300
default_app = "dummy"
302
# Get command-line arguments to avoid asking questions.
304
(opts, args) = getopt.gnu_getopt(args, "", ['root_dir=',
305
'ivle_install_dir=', 'jail_base=', 'allowed_uids='])
308
print >>sys.stderr, "Invalid arguments:", string.join(args, ' ')
312
# Interactive mode. Prompt the user for all the values.
314
print """This tool will create the following files:
317
prompting you for details about your configuration. The file will be
318
overwritten if it already exists. It will *not* install or deploy IVLE.
320
Please hit Ctrl+C now if you do not wish to do this.
321
""" % (conffile, conf_hfile)
323
# Get information from the administrator
324
# If EOF is encountered at any time during the questioning, just exit
327
root_dir = query_user(root_dir,
328
"""Root directory where IVLE is located (in URL space):""")
329
ivle_install_dir = query_user(ivle_install_dir,
330
'Root directory where IVLE will be installed (on the local file '
332
jail_base = query_user(jail_base,
333
"""Root directory where the jails (containing user files) are stored
334
(on the local file system):""")
335
allowed_uids = query_user(allowed_uids,
336
"""UID of the web server process which will run IVLE.
337
Only this user may execute the trampoline. May specify multiple users as
338
a comma-separated list.
343
# Non-interactive mode. Parse the options.
344
if '--root_dir' in opts:
345
root_dir = opts['--root_dir']
346
if '--ivle_install_dir' in opts:
347
ivle_install_dir = opts['--ivle_install_dir']
348
if '--jail_base' in opts:
349
jail_base = opts['--jail_base']
350
if '--allowed_uids' in opts:
351
allowed_uids = opts['--allowed_uids']
353
# Error handling on input values
355
allowed_uids = map(int, allowed_uids.split(','))
357
print >>sys.stderr, (
358
"Invalid UID list (%s).\n"
359
"Must be a comma-separated list of integers." % allowed_uids)
362
# Write www/conf/conf.py
365
conf = open(conffile, "w")
367
conf.write("""# IVLE Configuration File
369
# Miscellaneous application settings
372
# In URL space, where in the site is IVLE located. (All URLs will be prefixed
374
# eg. "/" or "/ivle".
377
# In the local file system, where IVLE is actually installed.
378
# This directory should contain the "www" and "bin" directories.
379
ivle_install_dir = "%s"
381
# In the local file system, where are the student/user file spaces located.
382
# The user jails are expected to be located immediately in subdirectories of
386
# Which application to load by default (if the user navigates to the top level
387
# of the site). This is the app's URL name.
388
# Note that if this app requires authentication, the user will first be
389
# presented with the login screen.
391
""" % (root_dir, ivle_install_dir, jail_base, default_app))
394
except IOError, (errno, strerror):
395
print "IO error(%s): %s" % (errno, strerror)
398
print "Successfully wrote www/conf/conf.py"
400
# Write trampoline/conf.h
403
conf = open(conf_hfile, "w")
405
conf.write("""/* IVLE Configuration File
407
* Administrator settings required by trampoline.
408
* Note: trampoline will have to be rebuilt in order for changes to this file
412
/* In the local file system, where are the jails located.
413
* The trampoline does not allow the creation of a jail anywhere besides
414
* jail_base or a subdirectory of jail_base.
416
static const char* jail_base = "%s";
418
/* Which user IDs are allowed to run the trampoline.
419
* This list should be limited to the web server user.
420
* (Note that root is an implicit member of this list).
422
static const int allowed_uids[] = { %s };
423
""" % (jail_base, repr(allowed_uids)[1:-1]))
426
except IOError, (errno, strerror):
427
print "IO error(%s): %s" % (errno, strerror)
430
print "Successfully wrote trampoline/conf.h"
433
print "You may modify the configuration at any time by editing"
440
# Get "dry" variable from command line
441
(opts, args) = getopt.gnu_getopt(args, "n", ['dry'])
443
dry = '-n' in opts or '--dry' in opts
446
print "Dry run (no actions will be executed\n"
448
# Compile the trampoline
449
action_runprog('gcc', ['-Wall', '-o', 'trampoline/trampoline',
450
'trampoline/trampoline.c'], dry)
452
# Create the jail and its subdirectories
453
# Note: Other subdirs will be made by copying files
454
action_mkdir('jail', dry)
455
action_mkdir('jail/home', dry)
456
action_mkdir('jail/tmp', dry)
458
# Copy all console and operating system files into the jail
459
action_copylist(install_list.list_console, 'jail/opt/ivle', dry)
460
copy_os_files_jail(dry)
462
# Compile .py files into .pyc or .pyo files
463
compileall.compile_dir('www', quiet=True)
464
compileall.compile_dir('console', quiet=True)
468
def copy_os_files_jail(dry):
469
"""Copies necessary Operating System files from their usual locations
470
into the jail/ directory of the cwd."""
471
# Currently source paths are configured for Ubuntu.
472
copy_file_to_jail('/lib/ld-linux.so.2', dry)
473
copy_file_to_jail('/lib/tls/i686/cmov/libc.so.6', dry)
474
copy_file_to_jail('/lib/tls/i686/cmov/libdl.so.2', dry)
475
copy_file_to_jail('/lib/tls/i686/cmov/libm.so.6', dry)
476
copy_file_to_jail('/lib/tls/i686/cmov/libpthread.so.0', dry)
477
copy_file_to_jail('/lib/tls/i686/cmov/libutil.so.1', dry)
478
copy_file_to_jail('/usr/bin/python2.5', dry)
479
# TODO: ln -s jail/usr/bin/python2.5 jail/usr/bin/python
480
action_copytree('/usr/lib/python2.5', 'jail/usr/lib/python2.5', dry)
482
def copy_file_to_jail(src, dry):
483
"""Copies a single file from an absolute location into the same location
484
within the jail. src must begin with a '/'. The jail will be located
485
in a 'jail' subdirectory of the current path."""
486
action_copyfile(src, 'jail' + src, dry)
489
# Get "dry" and "nojail" variables from command line
490
(opts, args) = getopt.gnu_getopt(args, "n", ['dry', 'nojail'])
492
dry = '-n' in opts or '--dry' in opts
493
nojail = '--nojail' in opts
496
print "Dry run (no actions will be executed\n"
498
if not dry and os.geteuid() != 0:
499
print >>sys.stderr, "Must be root to run install"
500
print >>sys.stderr, "(I need to chown some files)."
503
# Create the target (install) directory
504
action_mkdir(ivle_install_dir, dry)
506
# Create bin and copy the compiled files there
507
action_mkdir(os.path.join(ivle_install_dir, 'bin'), dry)
508
tramppath = os.path.join(ivle_install_dir, 'bin/trampoline')
509
action_copyfile('trampoline/trampoline', tramppath, dry)
510
# chown trampoline to root and set setuid bit
511
action_chown_setuid(tramppath, dry)
513
# Copy the www directory using the list
514
action_copylist(install_list.list_www, ivle_install_dir, dry)
517
# Copy the local jail directory built by the build action
518
# to the jails template directory (it will be used as a template
519
# for all the students' jails).
520
action_copytree('jail', os.path.join(jail_base, 'template'), dry)
521
# Set up symlinks inside the jail
522
action_symlink(os.path.join(jail_base, 'template/usr/bin/python2.5'),
523
os.path.join(jail_base, 'template/usr/bin/python'), dry)
527
# The actions call Python os functions but print actions and handle dryness.
528
# May still throw os exceptions if errors occur.
531
"""Represents an error when running a program (nonzero return)."""
532
def __init__(self, prog, retcode):
534
self.retcode = retcode
536
return str(self.prog) + " returned " + repr(self.retcode)
538
def action_runprog(prog, args, dry):
539
"""Runs a unix program. Searches in $PATH. Synchronous (waits for the
540
program to return). Runs in the current environment. First prints the
541
action as a "bash" line.
543
Throws a RunError with a retcode of the return value of the program,
544
if the program did not return 0.
546
prog: String. Name of the program. (No path required, if in $PATH).
547
args: [String]. Arguments to the program.
548
dry: Bool. If True, prints but does not execute.
550
print prog, string.join(args, ' ')
552
ret = os.spawnvp(os.P_WAIT, prog, args)
554
raise RunError(prog, ret)
556
def action_mkdir(path, dry):
557
"""Calls mkdir. Silently ignored if the directory already exists.
558
Creates all parent directories as necessary."""
559
print "mkdir -p", path
563
except OSError, (err, msg):
564
if err != errno.EEXIST:
567
def action_copytree(src, dst, dry):
568
"""Copies an entire directory tree. Symlinks are seen as normal files and
569
copies of the entire file (not the link) are made. Creates all parent
570
directories as necessary.
572
See shutil.copytree."""
573
if os.access(dst, os.F_OK):
576
shutil.rmtree(dst, True)
577
print "cp -r", src, dst
579
shutil.copytree(src, dst)
581
def action_copylist(srclist, dst, dry):
582
"""Copies all files in a list to a new location. The files in the list
583
are read relative to the current directory, and their destinations are the
584
same paths relative to dst. Creates all parent directories as necessary.
586
for srcfile in srclist:
587
dstfile = os.path.join(dst, srcfile)
588
dstdir = os.path.split(dstfile)[0]
589
if not os.path.isdir(dstdir):
590
action_mkdir(dstdir, dry)
591
print "cp -f", srcfile, dstfile
593
shutil.copyfile(srcfile, dstfile)
595
def action_copyfile(src, dst, dry):
596
"""Copies one file to a new location. Creates all parent directories
599
dstdir = os.path.split(dst)[0]
600
if not os.path.isdir(dstdir):
601
action_mkdir(dstdir, dry)
602
print "cp -f", src, dst
604
shutil.copyfile(src, dst)
606
def action_symlink(src, dst, dry):
607
"""Creates a symlink in a given location. Creates all parent directories
610
dstdir = os.path.split(dst)[0]
611
if not os.path.isdir(dstdir):
612
action_mkdir(dstdir, dry)
613
print "ln -s", src, dst
617
def action_chown_setuid(file, dry):
618
"""Chowns a file to root, and sets the setuid bit on the file.
619
Calling this function requires the euid to be root.
620
The actual mode of path is set to: rws--s--s
622
print "chown root:root", file
625
print "chmod a+xs", file
626
print "chmod u+rw", file
628
os.chmod(file, stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
629
| stat.S_ISUID | stat.S_IRUSR | stat.S_IWUSR)
631
def query_user(default, prompt):
632
"""Prompts the user for a string, which is read from a line of stdin.
633
Exits silently if EOF is encountered. Returns the string, with spaces
634
removed from the beginning and end.
636
Returns default if a 0-length line (after spaces removed) was read.
638
sys.stdout.write('%s\n (default: "%s")\n>' % (prompt, default))
640
val = sys.stdin.readline()
641
except KeyboardInterrupt:
643
sys.stdout.write("\n")
645
sys.stdout.write("\n")
647
if val == '': sys.exit(1)
648
# If empty line, return default
650
if val == '': return default
653
def filter_mutate(function, list):
654
"""Like built-in filter, but mutates the given list instead of returning a
655
new one. Returns None."""
658
# Delete elements which do not match
659
if not function(list[i]):
663
if __name__ == "__main__":