1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
|
# IVLE - Informatics Virtual Learning Environment
# Copyright (C) 2007-2008 The University of Melbourne
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# Module: setup/util
# Author: Matt Giuca, Refactored by David Coles
# Date: 02/07/2008
# setup/util.py
# Contains a set of functions useful for the setup program.
import os
import shutil
import errno
import sys
import string
import stat
import optparse
import mimetypes
__all__ = ['PYTHON_VERSION', 'copy_file_to_jail', 'RunError',
'action_runprog', 'action_remove', 'action_rename', 'action_mkdir',
'action_copytree', 'action_copylist', 'action_copyfile',
'action_symlink', 'action_append', 'action_chown',
'action_chown_setuid', 'action_chmod_x', 'action_make_private',
'query_user', 'filter_mutate', 'get_svn_revision', 'InstallList',
'wwwuid']
# Determine which Python version (2.4 or 2.5, for example) we are running,
# and use that as the filename to the Python directory.
# Just get the first 3 characters of sys.version.
PYTHON_VERSION = sys.version[0:3]
# Location of standard programs
RSYNC = '/usr/bin/rsync'
# UID of the Webserver
wwwuid = 33
def copy_file_to_jail(src, dry):
"""Copies a single file from an absolute location into the same location
within the jail. src must begin with a '/'. The jail will be located
in a 'jail' subdirectory of the current path."""
action_copyfile(src, 'jail' + src, dry)
# The actions call Python os functions but print actions and handle dryness.
# May still throw os exceptions if errors occur.
class RunError:
"""Represents an error when running a program (nonzero return)."""
def __init__(self, prog, retcode):
self.prog = prog
self.retcode = retcode
def __str__(self):
return str(self.prog) + " returned " + repr(self.retcode)
def action_runprog(prog, args, dry):
"""Runs a unix program. Searches in $PATH. Synchronous (waits for the
program to return). Runs in the current environment. First prints the
action as a "bash" line.
Throws a RunError with a retcode of the return value of the program,
if the program did not return 0.
prog: String. Name of the program. (No path required, if in $PATH).
args: [String]. Arguments to the program. (Note, this does not allow you to
set argv[0]; it will always be prog.)
dry: Bool. If True, prints but does not execute.
"""
print prog, string.join(args, ' ')
if dry: return
ret = os.spawnvp(os.P_WAIT, prog, [prog] + args)
if ret != 0:
raise RunError(prog, ret)
def action_remove(path, dry):
"""Calls rmtree, deleting the target file if it exists."""
try:
print "rm -r", path
if not dry:
shutil.rmtree(path, True)
except OSError, (err, msg):
if err != errno.EEXIST:
raise
# Otherwise, didn't exist, so we don't care
def action_rename(src, dst, dry):
"""Calls rename. Deletes the target if it already exists."""
action_remove(dst, dry)
print "mv ", src, dst
if dry: return
try:
os.rename(src, dst)
except OSError, (err, msg):
if err != errno.EEXIST:
raise
def action_mkdir(path, dry):
"""Calls mkdir. Silently ignored if the directory already exists.
Creates all parent directories as necessary."""
print "mkdir -p", path
if dry: return
try:
os.makedirs(path)
except OSError, (err, msg):
if err != errno.EEXIST:
raise
def action_copytree(src, dst, dry):
"""Copies an entire directory tree. Symlinks are seen as normal files and
copies of the entire file (not the link) are made. Creates all parent
directories as necessary.
See shutil.copytree."""
# Allow copying over itself
if (os.path.normpath(os.path.join(os.getcwd(),src)) ==
os.path.normpath(os.path.join(os.getcwd(),dst))):
return
# Try to do the copy with rsync, if that fails just copy
try:
action_runprog(RSYNC, ['-a','--delete',src + '/',dst], dry)
except RunError:
if dry: return
action_remove(dst, dry)
print "cp -r", src, dst
shutil.copytree(src, dst, True)
def action_copylist(srclist, dst, dry, srcdir=".", onlybasename=False):
"""Copies all files in a list to a new location. The files in the list
are read relative to the current directory, and their destinations are the
same paths relative to dst. Creates all parent directories as necessary.
srcdir is "." by default, can be overridden.
If onlybasename is True, only the basename of the source is appended to
the destination.
"""
for srcfile in srclist:
if onlybasename:
dstfile = os.path.join(dst, os.path.basename(srcfile))
else:
dstfile = os.path.join(dst, srcfile)
srcfile = os.path.join(srcdir, srcfile)
dstdir = os.path.split(dstfile)[0]
if not os.path.isdir(dstdir):
action_mkdir(dstdir, dry)
print "cp -f", srcfile, dstfile
if not dry:
try:
shutil.copyfile(srcfile, dstfile)
shutil.copymode(srcfile, dstfile)
except shutil.Error:
pass
def action_copyfile(src, dst, dry):
"""Copies one file to a new location. Creates all parent directories
as necessary.
Warn if file not found.
"""
dstdir = os.path.split(dst)[0]
if not os.path.isdir(dstdir):
action_mkdir(dstdir, dry)
print "cp -f", src, dst
if not dry:
try:
shutil.copyfile(src, dst)
shutil.copymode(src, dst)
except (shutil.Error, IOError), e:
print "Warning: " + str(e)
def action_symlink(src, dst, dry):
"""Creates a symlink in a given location. Creates all parent directories
as necessary.
"""
dstdir = os.path.split(dst)[0]
if not os.path.isdir(dstdir):
action_mkdir(dstdir, dry)
# Delete existing file
if os.path.exists(dst):
os.remove(dst)
print "ln -fs", src, dst
if not dry:
os.symlink(src, dst)
def action_append(ivle_pth, ivle_www):
file = open(ivle_pth, 'a+')
file.write(ivle_www + '\n')
file.close()
def action_chown_setuid(file, dry):
"""Chowns a file to root, and sets the setuid bit on the file.
Calling this function requires the euid to be root.
The actual mode of path is set to: rws--s--s
"""
print "chown root:root", file
if not dry:
os.chown(file, 0, 0)
print "chmod a+xs", file
print "chmod u+rw", file
if not dry:
os.chmod(file, stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
| stat.S_ISUID | stat.S_IRUSR | stat.S_IWUSR)
def action_chmod_x(file, dry):
"""Chmod 755 a file (sets permissions to rwxr-xr-x)."""
print "chmod 755", file
if not dry:
os.chmod(file, stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR
| stat.S_IXGRP | stat.S_IRGRP | stat.S_IXOTH | stat.S_IROTH)
def action_chown(file, uid, gid, dry):
"""Chowns a file to the specified numeric UID and GID."""
print "chown %s:%s %s"%(uid, gid, file)
if not dry:
os.chown(file, uid, gid)
def action_make_private(file, dry):
"""Ensures that a file is private to IVLE (chowns to www-data and chmod to
600)"""
action_chown(file, wwwuid, wwwuid, dry)
print "chmod 600", file
if not dry:
os.chmod(file, stat.S_IRUSR | stat.S_IWUSR)
def query_user(default, prompt):
"""Prompts the user for a string, which is read from a line of stdin.
Exits silently if EOF is encountered. Returns the string, with spaces
removed from the beginning and end.
Returns default if a 0-length line (after spaces removed) was read.
"""
if default is None:
# A default of None means the value will be computed specially, so we
# can't really tell you what it is
defaultstr = "computed"
elif isinstance(default, basestring):
defaultstr = '"%s"' % default
else:
defaultstr = repr(default)
sys.stdout.write('%s\n (default: %s)\n>' % (prompt, defaultstr))
try:
val = sys.stdin.readline()
except KeyboardInterrupt:
# Ctrl+C
sys.stdout.write("\n")
sys.exit(1)
sys.stdout.write("\n")
# If EOF, exit
if val == '': sys.exit(1)
# If empty line, return default
val = val.strip()
if val == '': return default
return val
def filter_mutate(function, list):
"""Like built-in filter, but mutates the given list instead of returning a
new one. Returns None."""
i = len(list)-1
while i >= 0:
# Delete elements which do not match
if not function(list[i]):
del list[i]
i -= 1
def get_svn_revision():
"""Returns either the current SVN revision of this build, or None"""
import pysvn
try:
svn = pysvn.Client()
entry = svn.info('.')
revnum = entry.revision.number
except pysvn.ClientError, e:
revnum = None
return revnum
### InstallList and helpers ###
# Mime types which will automatically be placed in the list by InstallList.
installlist_mimetypes = ['text/x-python', 'text/html',
'application/x-javascript', 'application/javascript',
'text/css', 'image/png', 'image/gif', 'application/xml']
def build_list_py_files(dir, no_top_level=False):
"""Builds a list of all py files found in a directory and its
subdirectories. Returns this as a list of strings.
no_top_level=True means the file paths will not include the top-level
directory.
"""
pylist = []
for (dirpath, dirnames, filenames) in os.walk(dir):
# Exclude directories beginning with a '.' (such as '.svn')
filter_mutate(lambda x: x[0] != '.', dirnames)
# All *.py files are added to the list
pylist += [os.path.join(dirpath, item) for item in filenames
if mimetypes.guess_type(item)[0] in installlist_mimetypes]
if no_top_level:
for i in range(0, len(pylist)):
_, pylist[i] = pylist[i].split(os.sep, 1)
return pylist
class InstallList(object):
# We build two separate lists, by walking www and console
list_www = property(lambda self: build_list_py_files('www'))
list_ivle_lib = property(lambda self: build_list_py_files('ivle'))
list_subjects = property(lambda self: build_list_py_files('subjects',
no_top_level=True))
list_exercises = property(lambda self: build_list_py_files('exercises',
no_top_level=True))
list_services = [
"services/python-console",
"services/fileservice",
"services/serveservice",
"services/interpretservice",
"services/usrmgt-server",
"services/diffservice",
"services/svnlogservice",
]
list_user_binaries = [
"bin/ivle-enrol",
"bin/ivle-enrolallusers",
"bin/ivle-listusers",
"bin/ivle-makeuser",
"bin/ivle-marks",
"bin/ivle-mountallusers",
"bin/ivle-remakeuser",
"bin/ivle-showenrolment",
]
|