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).
60
# TODO: List in help, and handle, args for the conf operation
72
# Try importing existing conf, but if we can't just set up defaults
73
# The reason for this is that these settings are used by other phases
74
# of setup besides conf, so we need to know them.
75
# Also this allows you to hit Return to accept the existing value.
77
confmodule = __import__("www/conf/conf")
78
root_dir = confmodule.root_dir
79
ivle_install_dir = confmodule.ivle_install_dir
80
jail_base = confmodule.jail_base
82
# Just set reasonable defaults
84
ivle_install_dir = "/opt/ivle"
85
jail_base = "/home/informatics/jails"
89
# Try importing install_list, but don't fail if we can't, because listmake can
90
# function without it.
96
# Mime types which will automatically be placed in the list by listmake.
97
# Note that listmake is not intended to be run by the final user (the system
98
# administrator who installs this), so the developers can customize the list
99
# as necessary, and include it in the distribution.
100
listmake_mimetypes = ['text/x-python', 'text/html',
101
'application/x-javascript', 'application/javascript',
102
'text/css', 'image/png']
104
# Main function skeleton from Guido van Rossum
105
# http://www.artima.com/weblogs/viewpost.jsp?thread=4829
107
class Usage(Exception):
108
def __init__(self, msg):
115
# Print the opening spiel including the GPL notice
117
print """IVLE - Informatics Virtual Learning Environment Setup
118
Copyright (C) 2007-2008 The University of Melbourne
119
IVLE comes with ABSOLUTELY NO WARRANTY.
120
This is free software, and you are welcome to redistribute it
121
under certain conditions. See LICENSE.txt for details.
126
# First argument is the name of the setup operation
130
# Print usage message and exit
134
# Call the requested operation's function
140
'listmake' : listmake,
142
}[operation](argv[2:])
144
print >>sys.stderr, (
145
"""Invalid operation '%s'. Try python setup.py help."""
150
opts, args = getopt.getopt(argv[1:], "h", ["help"])
151
except getopt.error, msg:
153
# more code, unchanged
155
print >>sys.stderr, err.msg
156
print >>sys.stderr, "for help use --help"
159
# Operation functions
163
print """Usage: python setup.py operation [args]
164
Operation (and args) can be:
168
install [--nojail] [-n|--dry]
172
print """Usage: python setup.py help [operation]"""
177
if operation == 'help':
178
print """python setup.py help [operation]
179
Prints the usage message or detailed help on an operation, then exits."""
180
elif operation == 'listmake':
181
print """python setup.py listmake
182
(For developer use only)
183
Recurses through the source tree and builds a list of all files which should
184
be copied upon installation. This should be run by the developer before
185
cutting a distribution, and the listfile it generates should be included in
186
the distribution, avoiding the administrator having to run it."""
187
elif operation == 'conf':
188
print """python setup.py conf [args]
189
Configures IVLE with machine-specific details, most notably, various paths.
190
Either prompts the administrator for these details or accepts them as
192
Creates www/conf/conf.py and trampoline/conf.h.
195
elif operation == 'build':
196
print """python -O setup.py build [--dry|-n]
197
Compiles all files and sets up a jail template in the source directory.
198
-O is recommended to cause compilation to be optimised.
200
Compiles (GCC) trampoline/trampoline.c to trampoline/trampoline.
202
Creates standard subdirs inside the jail, eg bin, opt, home, tmp.
203
Copies console/ to a location within the jail.
204
Copies OS programs and files to corresponding locations within the jail
205
(eg. python and Python libs, ld.so, etc).
206
Generates .pyc or .pyo files for all the IVLE .py files.
208
--dry | -n Print out the actions but don't do anything."""
209
elif operation == 'install':
210
print """sudo python setup.py install [--nojail] [--dry|-n]
212
Create target install directory ($target).
214
Copy trampoline/trampoline to $target/bin.
215
chown and chmod the installed trampoline.
216
Copy www/ to $target.
217
Copy jail/ to jails template directory (unless --nojail specified).
219
--nojail Do not copy the jail.
220
--dry | -n Print out the actions but don't do anything."""
222
print >>sys.stderr, (
223
"""Invalid operation '%s'. Try python setup.py help."""
228
# We build two separate lists, by walking www and console
229
list_www = build_list_py_files('www')
230
list_console = build_list_py_files('console')
231
# Make sure that the files generated by conf are in the list
232
# (since listmake is typically run before conf)
233
if "www/conf/conf.py" not in list_www:
234
list_www.append("www/conf/conf.py")
235
# Make sure that console/python-console is in the list
236
if "console/python-console" not in list_console:
237
list_console.append("console/python-console")
238
# Write these out to a file
240
# the files that will be created/overwritten
241
listfile = os.path.join(cwd, "install_list.py")
244
file = open(listfile, "w")
246
file.write("""# IVLE Configuration File
248
# Provides lists of all files to be installed by `setup.py install' from
249
# certain directories.
250
# Note that any files with the given filename plus 'c' or 'o' (that is,
251
# compiled .pyc or .pyo files) will be copied as well.
253
# List of all installable files in www directory.
255
writelist_pretty(file, list_www)
257
# List of all installable files in console directory.
259
writelist_pretty(file, list_console)
262
except IOError, (errno, strerror):
263
print "IO error(%s): %s" % (errno, strerror)
266
print "Successfully wrote install_list.py"
269
print ("You may modify the set of installable files before cutting the "
276
def build_list_py_files(dir):
277
"""Builds a list of all py files found in a directory and its
278
subdirectories. Returns this as a list of strings."""
280
for (dirpath, dirnames, filenames) in os.walk(dir):
281
# Exclude directories beginning with a '.' (such as '.svn')
282
filter_mutate(lambda x: x[0] != '.', dirnames)
283
# All *.py files are added to the list
284
pylist += [os.path.join(dirpath, item) for item in filenames
285
if mimetypes.guess_type(item)[0] in listmake_mimetypes]
288
def writelist_pretty(file, list):
289
"""Writes a list one element per line, to a file."""
295
file.write(' %s,\n' % repr(elem))
299
global root_dir, ivle_install_dir, jail_base, allowed_uids
300
# Set up some variables
303
# the files that will be created/overwritten
304
conffile = os.path.join(cwd, "www/conf/conf.py")
305
conf_hfile = os.path.join(cwd, "trampoline/conf.h")
307
# Fixed config options that we don't ask the admin
309
default_app = "dummy"
311
print """This tool will create the following files:
314
prompting you for details about your configuration. The file will be
315
overwritten if it already exists. It will *not* install or deploy IVLE.
317
Please hit Ctrl+C now if you do not wish to do this.
318
""" % (conffile, conf_hfile)
320
# Get information from the administrator
321
# If EOF is encountered at any time during the questioning, just exit
324
root_dir = query_user(root_dir,
325
"""Root directory where IVLE is located (in URL space):""")
326
ivle_install_dir = query_user(ivle_install_dir,
327
'Root directory where IVLE will be installed (on the local file '
329
jail_base = query_user(jail_base,
330
"""Root directory where the jails (containing user files) are stored
331
(on the local file system):""")
332
allowed_uids = query_user(allowed_uids,
333
"""UID of the web server process which will run IVLE.
334
Only this user may execute the trampoline. May specify multiple users as
335
a comma-separated list.
338
# Error handling on input values
341
allowed_uids = map(int, allowed_uids.split(','))
343
print >>sys.stderr, (
344
"Invalid UID list (%s).\n"
345
"Must be a comma-separated list of integers." % allowed_uids)
348
# Write www/conf/conf.py
351
conf = open(conffile, "w")
353
conf.write("""# IVLE Configuration File
355
# Miscellaneous application settings
358
# In URL space, where in the site is IVLE located. (All URLs will be prefixed
360
# eg. "/" or "/ivle".
363
# In the local file system, where IVLE is actually installed.
364
# This directory should contain the "www" and "bin" directories.
365
ivle_install_dir = "%s"
367
# In the local file system, where are the student/user file spaces located.
368
# The user jails are expected to be located immediately in subdirectories of
372
# Which application to load by default (if the user navigates to the top level
373
# of the site). This is the app's URL name.
374
# Note that if this app requires authentication, the user will first be
375
# presented with the login screen.
377
""" % (root_dir, ivle_install_dir, jail_base, default_app))
380
except IOError, (errno, strerror):
381
print "IO error(%s): %s" % (errno, strerror)
384
print "Successfully wrote www/conf/conf.py"
386
# Write trampoline/conf.h
389
conf = open(conf_hfile, "w")
391
conf.write("""/* IVLE Configuration File
393
* Administrator settings required by trampoline.
394
* Note: trampoline will have to be rebuilt in order for changes to this file
398
/* In the local file system, where are the jails located.
399
* The trampoline does not allow the creation of a jail anywhere besides
400
* jail_base or a subdirectory of jail_base.
402
static const char* jail_base = "%s";
404
/* Which user IDs are allowed to run the trampoline.
405
* This list should be limited to the web server user.
406
* (Note that root is an implicit member of this list).
408
static const int allowed_uids[] = { %s };
409
""" % (jail_base, repr(allowed_uids)[1:-1]))
412
except IOError, (errno, strerror):
413
print "IO error(%s): %s" % (errno, strerror)
416
print "Successfully wrote trampoline/conf.h"
419
print "You may modify the configuration at any time by editing"
426
dry = False # Set to True later if --dry
428
# Compile the trampoline
429
action_runprog('gcc', ['-Wall', '-o', 'trampoline/trampoline',
430
'trampoline/trampoline.c'], dry)
432
# Create the jail and its subdirectories
433
action_mkdir('jail', dry)
434
action_mkdir('jail/bin', dry)
435
action_mkdir('jail/lib', dry)
436
action_mkdir('jail/usr/bin', dry)
437
action_mkdir('jail/usr/lib', dry)
438
action_mkdir('jail/opt/ivle', dry)
439
action_mkdir('jail/home', dry)
440
action_mkdir('jail/tmp', dry)
442
# Copy all console files into the jail
443
action_copylist(install_list.list_console, 'jail/opt/ivle', dry)
445
# TODO: Copy operating system files into the jail
447
# Compile .py files into .pyc or .pyo files
448
compileall.compile_dir('www', quiet=True)
449
compileall.compile_dir('console', quiet=True)
454
# Create the target directory
455
nojail = False # Set to True later if --nojail
456
dry = False # Set to True later if --dry
458
if not dry and os.geteuid() != 0:
459
print >>sys.stderr, "Must be root to run install"
460
print >>sys.stderr, "(I need to chown some files)."
463
# Create the target (install) directory
464
action_mkdir(ivle_install_dir, dry)
466
# Create bin and copy the compiled files there
467
action_mkdir(os.path.join(ivle_install_dir, 'bin'), dry)
468
tramppath = os.path.join(ivle_install_dir, 'bin/trampoline')
469
action_copyfile('trampoline/trampoline', tramppath, dry)
470
# chown trampoline to root and set setuid bit
471
action_chown_setuid(tramppath, dry)
473
# Copy the www directory using the list
474
action_copylist(install_list.list_www, ivle_install_dir, dry)
477
# Copy the local jail directory built by the build action
478
# to the jails template directory (it will be used as a template
479
# for all the students' jails).
480
action_copytree('jail', os.path.join(jail_base, 'template'), dry)
484
# The actions call Python os functions but print actions and handle dryness.
485
# May still throw os exceptions if errors occur.
488
"""Represents an error when running a program (nonzero return)."""
489
def __init__(self, prog, retcode):
491
self.retcode = retcode
493
return str(self.prog) + " returned " + repr(self.retcode)
495
def action_runprog(prog, args, dry):
496
"""Runs a unix program. Searches in $PATH. Synchronous (waits for the
497
program to return). Runs in the current environment. First prints the
498
action as a "bash" line.
500
Throws a RunError with a retcode of the return value of the program,
501
if the program did not return 0.
503
prog: String. Name of the program. (No path required, if in $PATH).
504
args: [String]. Arguments to the program.
505
dry: Bool. If True, prints but does not execute.
507
print prog, string.join(args, ' ')
509
ret = os.spawnvp(os.P_WAIT, prog, args)
511
raise RunError(prog, ret)
513
def action_mkdir(path, dry):
514
"""Calls mkdir. Silently ignored if the directory already exists.
515
Creates all parent directories as necessary."""
516
print "mkdir -p", path
520
except OSError, (err, msg):
521
if err != errno.EEXIST:
524
def action_copytree(src, dst, dry):
525
"""Copies an entire directory tree. Symlinks are seen as normal files and
526
copies of the entire file (not the link) are made. Creates all parent
527
directories as necessary.
529
See shutil.copytree."""
530
if os.access(dst, os.F_OK):
533
shutil.rmtree(dst, True)
534
print "cp -r", src, dst
536
shutil.copytree(src, dst)
538
def action_copylist(srclist, dst, dry):
539
"""Copies all files in a list to a new location. The files in the list
540
are read relative to the current directory, and their destinations are the
541
same paths relative to dst. Creates all parent directories as necessary.
543
for srcfile in srclist:
544
dstfile = os.path.join(dst, srcfile)
545
dstdir = os.path.split(dstfile)[0]
546
if not os.path.isdir(dstdir):
547
action_mkdir(dstdir, dry)
548
print "cp -f", srcfile, dstfile
550
shutil.copyfile(srcfile, dstfile)
552
def action_copyfile(src, dst, dry):
553
"""Copies one file to a new location. Creates all parent directories
556
dstdir = os.path.split(dst)[0]
557
if not os.path.isdir(dstdir):
558
action_mkdir(dstdir, dry)
559
print "cp -f", src, dst
561
shutil.copyfile(src, dst)
563
def action_chown_setuid(file, dry):
564
"""Chowns a file to root, and sets the setuid bit on the file.
565
Calling this function requires the euid to be root.
566
The actual mode of path is set to: rws--s--s
568
print "chown root:root", file
571
print "chmod a+xs", file
572
print "chmod u+rw", file
574
os.chmod(file, stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
575
| stat.S_ISUID | stat.S_IRUSR | stat.S_IWUSR)
577
def query_user(default, prompt):
578
"""Prompts the user for a string, which is read from a line of stdin.
579
Exits silently if EOF is encountered. Returns the string, with spaces
580
removed from the beginning and end.
582
Returns default if a 0-length line (after spaces removed) was read.
584
sys.stdout.write('%s\n (default: "%s")\n>' % (prompt, default))
586
val = sys.stdin.readline()
587
except KeyboardInterrupt:
589
sys.stdout.write("\n")
591
sys.stdout.write("\n")
593
if val == '': sys.exit(1)
594
# If empty line, return default
596
if val == '': return default
599
def filter_mutate(function, list):
600
"""Like built-in filter, but mutates the given list instead of returning a
601
new one. Returns None."""
604
# Delete elements which do not match
605
if not function(list[i]):
609
if __name__ == "__main__":