-
Notifications
You must be signed in to change notification settings - Fork 78
/
install
executable file
·270 lines (218 loc) · 10.5 KB
/
install
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
#!/usr/bin/env python3
# This installer sets up the required jobe users (i.e. the "users"
# that run submitted jobs) and compiles and adjusts the runguard sandbox.
# It must be run as root.
# If run with the parameter --purge, all files and users set up by a previous
# run of the script are deleted at the start.
import os
import subprocess
import re
import sys
import argparse
class LoginDefsException (Exception):
pass
JOBE_DIRS = ['/var/www/jobe', '/var/www/html/jobe']
LANGUAGE_CACHE_FILE = '/tmp/jobe_language_cache_file'
FILE_CACHE_BASE = '/home/jobe/files'
def get_config(param_name, install_dir):
'''Get a config parameter from <<install_dir>>/app/Config/Jobe.php.
An exception occurs if either the file or the required parameter
is not found.
'''
with open(f'{install_dir}/app/Config/Jobe.php') as config:
lines = config.readlines()
dollar_param = r'\$' + param_name # PHP variable name for config parameter
patterns = [
rf"\s*public\s+(string)\s+{dollar_param}\s*=\s*\"([^\"]+)\";.*\n", # Double-quoted string
rf"\s*public\s+(string)\s+{dollar_param}\s*=\s*'([^']+)';.*\n", # Single-quoted string
rf"\s*public\s+(int|bool|float)\s+{dollar_param}\s*=\s*([^;]+);.*\n" # Non-string literal
]
for line in lines:
for pattern in patterns:
match = re.match(pattern, line)
if match:
return match.group(2)
raise Exception('Config param ' + param_name + ' not found')
def fail():
"""We're dead, Fred"""
print("Install failed")
sys.exit(1);
def get_webserver():
'''Find the user name used to run the Apache or nginx web server'''
ps_cmd = "ps aux | grep -E '/usr/sbin/(apache2|httpd)|nginx: worker'"
try:
ps_lines = subprocess.check_output(ps_cmd, shell=True).decode('utf8').split('\n')
except subprocess.CalledProcessError:
raise Exception("ps command to find web-server user id failed. Is the web server running?")
names = {ps_line.split(' ')[0] for ps_line in ps_lines}
candidates = names.intersection(set(['apache', 'www-data', 'nginx']))
if len(candidates) != 1:
raise Exception("Couldn't determine web-server user id. Is the web server running?")
webserver_user = list(candidates)[0]
print("Web server is", webserver_user)
return webserver_user
def do_command(cmd, ignore_errors=False):
'''Execute the given OS command user subprocess.call.
Raise an exception on failure unless ignore_errors is True.
'''
try:
returncode = subprocess.call(cmd, shell=True)
except subprocess.SubprocessError:
returncode = -1
if returncode != 0:
if ignore_errors:
print("Command '{}' failed. Ignoring.".format(cmd))
else:
raise OSError("Command ({}) failed".format(cmd))
def check_php_version():
'''Check that the installed PHP version is at least 7.4.
Raise an exception on failure.
'''
if subprocess.call("php -r 'exit(version_compare(PHP_VERSION, \"7.4.0\", \"lt\"));'", shell=True) != 0:
raise OSError("Jobe requires at least PHP 7.4.")
def make_sudoers(install_dir, webserver_user, num_jobe_users):
'''Build a custom jobe-sudoers file for include in /etc/sudoers.d.
It allows the webserver to run the runguard program as root and also to
kill any jobe tasks and delete the run directories.
'''
commands = [install_dir + '/runguard/runguard']
commands.append('/bin/rm -R /home/jobe/runs/*')
for i in range(num_jobe_users):
commands.append('/usr/bin/pkill -9 -u jobe{:02d}'.format(i))
for directory in get_config('clean_up_path', install_dir).split(';'):
commands.append('/usr/bin/find {}/ -user jobe{:02d} -delete'.format(directory, i))
sudoers_file_name = '/etc/sudoers.d/jobe-sudoers'
with open(sudoers_file_name, 'w') as sudoers:
os.chmod(sudoers_file_name, 0o440)
for cmd in commands:
sudoers.write('{} ALL=(root) NOPASSWD: {}\n'.format(webserver_user, cmd))
def make_user(username, comment, make_home_dir=False, group='jobe', uid=None):
''' Check if user exists. If not, add the named user with the given comment.
Make a home directory only if make_home_dir is true.
'''
try:
do_command('id ' + username + '> /dev/null 2>&1')
print(username, 'already exists')
except:
opt = '--home /home/jobe -m' if make_home_dir else ' -M'
group_opt = '' if group is None else f" -g {group}"
uid_opt = '' if uid is None else f" -u {uid}"
do_command(f'useradd -r {opt} -s "/bin/false"{group_opt}{uid_opt} -c "{comment}" {username}')
def make_directory(dirpath, owner, group, permissions=771):
'''If dirpath doesn't exist, make a directory and give it
the given owner, group and permissions'''
if not os.path.exists(dirpath):
os.makedirs(dirpath)
do_command('chown {0}:{1} {2}; chmod {3} {2}'.format(owner, group, dirpath, permissions))
def make_runguard(install_dir):
"""Make the runguard application"""
runguard_commands = [
"cd {0}".format(install_dir + '/runguard'),
"gcc -o runguard runguard.c -lm",
"chmod 700 runguard"
]
cmd = ';'.join(runguard_commands)
try:
do_command(cmd)
except OSError as e:
print("Compile of runguard failed.")
print("Check all dependencies are installed, particularly libcgroup-dev")
raise OSError("Runguard build failed")
def do_purge(install_dir):
'''Purge existing users and extra files set up by a previous install'''
with open('/etc/passwd') as infile:
lines = infile.read().splitlines()
jobe_users = [line.split(':')[0] for line in lines if re.match(r'jobe\d\d', line)]
for jobe_user in jobe_users:
do_command(f'userdel {jobe_user}', ignore_errors=True)
do_command('userdel jobe', ignore_errors=True)
#do_command('groupdel jobe', ignore_errors=True)
do_command('rm -rf /home/jobe')
do_command('rm -rf /var/log/jobe')
do_command('rm -rf {}/files'.format(install_dir), ignore_errors=True)
do_command('rm -rf ' + FILE_CACHE_BASE + '/*', ignore_errors=True)
def update_runguard_config(install_dir, num_jobe_users):
# Make sure the number of valid users are matching our num jobe users
print("Setting up runguard config")
runguard_users = ["domjudge", "jobe"] + ['jobe{:02d}'.format(i) for i in range(num_jobe_users)]
with open(os.path.join(install_dir, "runguard/runguard-config.h"), "r") as runguard_config_in:
runguard_config = runguard_config_in.read()
runguard_valid_users = ",".join(runguard_users)
runguard_config = re.sub(r"#define\s+VALID_USERS[^\n]+", '#define VALID_USERS "' + runguard_valid_users + '"', runguard_config)
with open(os.path.join(install_dir, "runguard/runguard-config.h"), "w") as runguard_config_out:
runguard_config_out.write(runguard_config)
def make_workers(num_jobe_users, max_uid):
"""Make the Jobe worker users"""
print(f"Making {num_jobe_users} Jobe workers")
uid = None if max_uid is None else max_uid - 1 # First uid is one less than max_uid (used by Jobe)
for i in range(num_jobe_users):
username = 'jobe{:02d}'.format(i)
make_user(username, 'Jobe server task runner', uid=uid)
if uid is not None:
uid -= 1
def create_jobe_user_and_files(install_dir, webserver_user, max_uid):
"""Create the user jobe and set up the home directory for runs"""
print("Making user jobe")
make_user('jobe', 'Jobe user. Provides home for runs and files.', True, None, max_uid)
# make sure webuser can reach /home/jobe/runs despite its not being in jobe group
do_command("chmod 751 /home/jobe")
print("Setting up Jobe runs directory (/home/jobe/runs)")
make_directory('/home/jobe/runs', 'jobe', webserver_user)
print("Setting up Jobe files directory (/home/jobe/files)")
make_directory('/home/jobe/files', 'jobe', webserver_user)
print("Setting up Jobe log directory (/var/log/jobe)")
make_directory('/var/log/jobe', 'jobe', webserver_user)
def process_command_line_args():
"""Command line argument handler."""
def max_uid(s):
max_uid = int(s)
if max_uid < 200 or max_uid > 998:
raise argparse.ArgumentTypeError("max_uid must be in the range 200 to 998")
return max_uid
parser = argparse.ArgumentParser(description='Jobe installer')
parser.add_argument('--purge', action="store_true", help='Purge prior Jobe settings and files before installing')
parser.add_argument('--uninstall', action="store_true", help='Uninstall jobe')
parser.add_argument('--max_uid', action="store", type=max_uid, default=None,
help="Set the maximum system UID to be used by Jobe and its workers. For use in containers only.")
args = parser.parse_args(sys.argv[1:])
return args
def main():
"""Every home should have one."""
args = process_command_line_args()
install_dir = os.getcwd()
if install_dir not in JOBE_DIRS:
print("WARNING: Jobe appears not to have been installed in /var/www")
print ("or /var/www/html as expected. Things might turn ugly.")
if subprocess.check_output('whoami', shell=True) != b'root\n':
print("****This script must be run by root*****")
fail()
try:
if args.purge or args.uninstall:
print('Purging all prior jobe users and files')
do_purge(install_dir)
if args.uninstall:
print("Jobe has been uninstalled")
sys.exit(0)
check_php_version()
webserver_user = get_webserver()
# Ensure install directory subtree owned by webuser and not readable by others
do_command('chown -R {0}:{0} {1}'.format(webserver_user, install_dir))
do_command('chmod -R o-rwx,g+w {}'.format(install_dir))
num_jobe_users = int(get_config('jobe_max_users', install_dir))
create_jobe_user_and_files(install_dir, webserver_user, args.max_uid)
make_workers(num_jobe_users, args.max_uid)
print("Setting up file cache")
make_directory(FILE_CACHE_BASE, 'jobe', webserver_user)
print("Building runguard")
update_runguard_config(install_dir, num_jobe_users)
make_runguard(install_dir)
make_sudoers(install_dir, webserver_user, num_jobe_users)
try:
os.remove(LANGUAGE_CACHE_FILE)
except:
pass
print("Jobe installation complete")
except Exception as e:
print("Exception during install: " + str(e))
fail()
main()