Completed
Push — master ( e187ac...8aa8f5 )
by Peter
07:52
created

print_help()   A

Complexity

Conditions 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 1
1
'''
2
    This module contains administrative functionality
3
    that is available as command-line tool "opensubmit-web".
4
5
    All functions that demand a working Django ORM are implemented
6
    as Django management command and just called from here.
7
8
    Everything else is implemented here, so this file works without
9
    any of the install dependencies.
10
'''
11
12
import os
13
import pwd
14
import grp
15
import urllib.request
16
import urllib.parse
17
import urllib.error
18
import sys
19
import argparse
20
from base64 import b64encode
21
from configparser import RawConfigParser
22
23
DEFAULT_CONFIG = '''
24
# This is the configuration file for the OpenSubmit tool.
25
# https://github.com/troeger/opensubmit
26
#
27
# It is expected to be located at:
28
# /etc/opensubmit/settings.ini (on production system), or
29
# <project_root>/web/opensubmit/settings_dev.ini (on developer systems)
30
#
31
# For further information, check the output of 'opensubmit-web configcreate -h'.
32
#
33
34
[general]
35
DEBUG: {debug}
36
DEMO: {login_demo}
37
38
[server]
39
HOST: {server_host}
40
HOST_DIR: {server_hostdir}
41
HOST_ALIASES: {server_hostaliases}
42
MEDIA_ROOT: {server_mediaroot}
43
LOG_FILE: {server_logfile}
44
TIME_ZONE: {server_timezone}
45
SECRET_KEY: {server_secretkey}
46
47
[database]
48
DATABASE_ENGINE: {database_engine}
49
DATABASE_NAME: {database_name}
50
DATABASE_USER: {database_user}
51
DATABASE_PASSWORD: {database_password}
52
DATABASE_HOST: {database_host}
53
DATABASE_PORT: {database_port}
54
55
[executor]
56
# The shared secret with the job executor. This ensures that only authorized
57
# machines can fetch submitted solution attachments for validation, and not
58
# every student ...
59
# Change it, the value does not matter.
60
SHARED_SECRET: 49846zut93purfh977TTTiuhgalkjfnk89
61
62
[admin]
63
ADMIN_NAME: {admin_email}
64
ADMIN_EMAIL: {admin_email}
65
66
[login]
67
LOGIN_DESCRIPTION: {login_openid_title}
68
OPENID_PROVIDER: {login_openid_provider}
69
LOGIN_TWITTER_OAUTH_KEY: {login_twitter_oauth_key}
70
LOGIN_TWITTER_OAUTH_SECRET: {login_twitter_oauth_secret}
71
LOGIN_GOOGLE_OAUTH_KEY: {login_google_oauth_key}
72
LOGIN_GOOGLE_OAUTH_SECRET: {login_google_oauth_secret}
73
LOGIN_GITHUB_OAUTH_KEY: {login_github_oauth_key}
74
LOGIN_GITHUB_OAUTH_SECRET: {login_github_oauth_secret}
75
LOGIN_SHIB_DESCRIPTION: {login_shib_title}
76
LOGIN_DEMO: {login_demo}
77
'''
78
79
80
def django_admin(args):
81
    '''
82
        Run something like it would be done through Django's manage.py.
83
    '''
84
    from django.core.management import execute_from_command_line
85
    from django.core.exceptions import ImproperlyConfigured
86
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "opensubmit.settings")
87
    try:
88
        execute_from_command_line([sys.argv[0]] + args)
89
    except ImproperlyConfigured as e:
90
        print(str(e))
91
        exit(-1)
92
93
94
def apache_config(config, outputfile):
95
    '''
96
        Generate a valid Apache configuration file, based on the given settings.
97
    '''
98
    if os.path.exists(outputfile):
99
        os.rename(outputfile, outputfile + ".old")
100
        print("Renamed existing Apache config file to " + outputfile + ".old")
101
102
    from opensubmit import settings
103
    f = open(outputfile, 'w')
104
    print("Generating Apache configuration in " + outputfile)
105
    subdir = (len(settings.HOST_DIR) > 0)
106
    text = """
107
    # OpenSubmit Configuration for Apache 2.4
108
    # These directives are expected to live in some <VirtualHost> block
109
    """
110
    if subdir:
111
        text += "Alias /%s/static/ %s\n" % (settings.HOST_DIR,
112
                                            settings.STATIC_ROOT)
113
        text += "    WSGIScriptAlias /%s %s/wsgi.py\n" % (
114
            settings.HOST_DIR, settings.SCRIPT_ROOT)
115
    else:
116
        text += "Alias /static/ %s\n" % (settings.STATIC_ROOT)
117
        text += "    WSGIScriptAlias / %s/wsgi.py" % (settings.SCRIPT_ROOT)
118
    text += """
119
    WSGIPassAuthorization On
120
    <Directory {static_path}>
121
         Require all granted
122
    </Directory>
123
    <Directory {install_path}>
124
         <Files wsgi.py>
125
              Require all granted
126
         </Files>
127
    </Directory>
128
    """.format(static_path=settings.STATIC_ROOT, install_path=settings.SCRIPT_ROOT)
129
130
    f.write(text)
131
    f.close()
132
133
134
def check_path(file_path):
135
    '''
136
        Checks if the directories for this path exist, and creates them in case.
137
    '''
138
    directory = os.path.dirname(file_path)
139
    if directory != '':
140
        if not os.path.exists(directory):
141
            os.makedirs(directory, 0o775)   # rwxrwxr-x
142
143
144
def check_file(filepath):
145
    '''
146
        - Checks if the parent directories for this path exist.
147
        - Checks that the file exists.
148
        - Donates the file to the web server user.
149
150
        TODO: This is Debian / Ubuntu specific.
151
    '''
152
    check_path(filepath)
153
    if not os.path.exists(filepath):
154
        print("WARNING: File does not exist. Creating it: %s" % filepath)
155
        open(filepath, 'a').close()
156
    try:
157
        print("Setting access rights for %s for www-data user" % (filepath))
158
        uid = pwd.getpwnam("www-data").pw_uid
159
        gid = grp.getgrnam("www-data").gr_gid
160
        os.chown(filepath, uid, gid)
161
        os.chmod(filepath, 0o660)  # rw-rw---
162
    except:
163
        print("WARNING: Could not adjust file system permissions for %s. Make sure your web server can write into it." % filepath)
164
165
166
def check_web_config_consistency(config):
167
    '''
168
        Check the web application config file for consistency.
169
    '''
170
    login_conf_deps = {
171
        'LOGIN_TWITTER_OAUTH_KEY': ['LOGIN_TWITTER_OAUTH_SECRET'],
172
        'LOGIN_GOOGLE_OAUTH_KEY': ['LOGIN_GOOGLE_OAUTH_SECRET'],
173
        'LOGIN_GITHUB_OAUTH_KEY': ['LOGIN_GITHUB_OAUTH_SECRET'],
174
        'LOGIN_TWITTER_OAUTH_SECRET': ['LOGIN_TWITTER_OAUTH_KEY'],
175
        'LOGIN_GOOGLE_OAUTH_SECRET': ['LOGIN_GOOGLE_OAUTH_KEY'],
176
        'LOGIN_GITHUB_OAUTH_SECRET': ['LOGIN_GITHUB_OAUTH_KEY'],
177
    }
178
179
    print("Checking configuration of the OpenSubmit web application...")
180
    # Let Django's manage.py load the settings file, to see if this works in general
181
    django_admin(["check"])
182
    # Check configured host
183
    try:
184
        urllib.request.urlopen(config.get("server", "HOST"))
185
    except Exception as e:
186
        # This may be ok, when the admin is still setting up to server
187
        print("The configured HOST seems to be invalid at the moment: " + str(e))
188
    # Check configuration dependencies
189
    for k, v in list(login_conf_deps.items()):
190
        if config.get('login', k):
191
            for needed in v:
192
                if len(config.get('login', needed)) < 1:
193
                    print(
194
                        "ERROR: You have enabled %s in settings.ini, but %s is not set." % (k, needed))
195
                    return False
196
    # Check media path
197
    check_path(config.get('server', 'MEDIA_ROOT'))
198
    # Prepare empty log file, in case the web server has no creation rights
199
    log_file = config.get('server', 'LOG_FILE')
200
    print("Preparing log file at " + log_file)
201
    check_file(log_file)
202
    # If SQLite database, adjust file system permissions for the web server
203
    if config.get('database', 'DATABASE_ENGINE') == 'sqlite3':
204
        name = config.get('database', 'DATABASE_NAME')
205
        if not os.path.isabs(name):
206
            print("ERROR: Your SQLite database name must be an absolute path. The web server must have directory access permissions for this path.")
207
            return False
208
        check_file(config.get('database', 'DATABASE_NAME'))
209
    # everything ok
210
    return True
211
212
213
def check_web_config(config_fname):
214
    '''
215
        Try to load the Django settings.
216
        If this does not work, than settings file does not exist.
217
218
        Returns:
219
            Loaded configuration, or None.
220
    '''
221
    print("Looking for config file at {0} ...".format(config_fname))
222
    config = RawConfigParser()
223
    try:
224
        config.readfp(open(config_fname))
225
        return config
226
    except IOError:
227
        print("ERROR: Seems like the config file does not exist. Please call 'opensubmit-web configcreate' first, or specify a location with the '-c' option.")
228
        return None
229
230
231
def check_web_db():
232
    '''
233
        Everything related to database checks and updates.
234
    '''
235
    print("Testing for neccessary database migrations...")
236
    django_admin(["migrate"])             # apply schema migrations
237
    print("Checking the OpenSubmit permission system...")
238
    # configure permission system, of needed
239
    django_admin(["fixperms"])
240
    return True
241
242
243
def configcreate(config_fname, settings):
244
    settings['server_secretkey'] = b64encode(os.urandom(64)).decode('utf-8')
245
    url_parts = settings['server_url'].split('/', 3)
246
    settings['server_host'] = url_parts[0] + '//' + url_parts[2]
247
    if len(url_parts) > 3:
248
        settings['server_hostdir'] = url_parts[3]
249
    else:
250
        settings['server_hostdir'] = ''
251
    content = DEFAULT_CONFIG.format(**settings)
252
253
    try:
254
        check_path(config_fname)
255
        f = open(config_fname, 'wt')
256
        f.write(content)
257
        f.close()
258
        print("Config file %s generated" % (config_fname))
259
    except Exception as e:
260
        print("ERROR: Could not create config file at {0}: {1}".format(config_fname, str(e)))
261
262
263
def configtest(config_fname):
264
    print("Inspecting OpenSubmit configuration ...")
265
    config = check_web_config(config_fname)
266
    if not config:
267
        return          # Let them first fix the config file before trying a DB access
268
    if not check_web_config_consistency(config):
269
        return
270
    if not check_web_db():
271
        return
272
    print("Preparing static files for web server...")
273
    django_admin(["collectstatic", "--noinput", "--clear", "-v 0"])
274
275
276
def console_script(fsroot=''):
277
    '''
278
        The main entry point for the production administration script 'opensubmit-web'.
279
        The argument allows the test suite to override the root of all paths used in here.
280
    '''
281
282
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Administration for the OpenSubmit web application.')
283
    parser.add_argument('-c', '--config', default='/etc/opensubmit/settings.ini', help='OpenSubmit configuration file.')
284
    subparsers = parser.add_subparsers(dest='command', help='Supported administrative actions.')
285
    parser_configcreate = subparsers.add_parser('configcreate', help='Create initial config files for the OpenSubmit web server.')
286
    parser_configcreate.add_argument('--debug', default=bool(os.environ.get('OPENSUBMIT_DEBUG', 'False')), action='store_true', help='Enable debug mode, not for production systems.')
287
    parser_configcreate.add_argument('--server_url', default=os.environ.get('OPENSUBMIT_SERVER_URL', 'http://localhost:8000'), help='The main URL of the OpenSubmit installation, including sub-directories.')
288
    parser_configcreate.add_argument('--server_mediaroot', default=os.environ.get('OPENSUBMIT_SERVER_MEDIAROOT', '/tmp/'), help='Storage path for uploadeded files.')
289
    parser_configcreate.add_argument('--server_hostaliases', default=os.environ.get('OPENSUBMIT_SERVER_HOSTALIASES', '127.0.0.1'), help='Comma-separated list of alternative host names for the web server.')
290
    parser_configcreate.add_argument('--server_logfile', default=os.environ.get('OPENSUBMIT_SERVER_LOGFILE', '/tmp/opensubmit.log'), help='Log file for the OpenSubmit application.')
291
    parser_configcreate.add_argument('--server_timezone', default=os.environ.get('OPENSUBMIT_SERVER_TIMEZONE', 'Europe/Berlin'), help='Time zone for all dates and deadlines.')
292
    parser_configcreate.add_argument('--database_name', default=os.environ.get('OPENSUBMIT_DATABASE_NAME', '/tmp/database.sqlite'), help='Name of the database (file).'),
293
    parser_configcreate.add_argument('--database_engine', default=os.environ.get('OPENSUBMIT_DATABASE_ENGINE', 'sqlite3'), choices=['postgresql', 'mysql', 'sqlite3', 'oracle'])
294
    parser_configcreate.add_argument('--database_user', default=os.environ.get('OPENSUBMIT_DATABASE_USER', ''), help='The user name for accessing the database. Not needed for SQLite.')
295
    parser_configcreate.add_argument('--database_password', default=os.environ.get('OPENSUBMIT_DATABASE_PASSWORD', ''), help='The user password for accessing the database. Not needed for SQLite.')
296
    parser_configcreate.add_argument('--database_host', default=os.environ.get('OPENSUBMIT_DATABASE_HOST', ''), help='The host name for accessing the database. Not needed for SQLite. Default is localhost.')
297
    parser_configcreate.add_argument('--database_port', default=os.environ.get('OPENSUBMIT_DATABASE_PORT', ''), help='The port number for accessing the database. Not needed for SQLite.')
298
    parser_configcreate.add_argument('--login_google_oauth_key', default=os.environ.get('OPENSUBMIT_LOGIN_GOOGLE_OAUTH_KEY', ''), help='Google OAuth client key.')
299
    parser_configcreate.add_argument('--login_google_oauth_secret', default=os.environ.get('OPENSUBMIT_LOGIN_GOOGLE_OAUTH_SECRET', ''), help='Google OAuth client secret.')
300
    parser_configcreate.add_argument('--login_twitter_oauth_key', default=os.environ.get('OPENSUBMIT_LOGIN_TWITTER_OAUTH_KEY', ''), help='Twitter OAuth client key.')
301
    parser_configcreate.add_argument('--login_twitter_oauth_secret', default=os.environ.get('OPENSUBMIT_LOGIN_TWITTER_OAUTH_SECRET', ''), help='Twitter OAuth client secret.')
302
    parser_configcreate.add_argument('--login_github_oauth_key', default=os.environ.get('OPENSUBMIT_LOGIN_GITHUB_OAUTH_KEY', ''), help='GitHub OAuth client key.')
303
    parser_configcreate.add_argument('--login_github_oauth_secret', default=os.environ.get('OPENSUBMIT_LOGIN_GITHUB_OAUTH_SECRET', ''), help='GitHUb OAuth client secret.')
304
    parser_configcreate.add_argument('--login_openid_title', default=os.environ.get('OPENSUBMIT_LOGIN_OPENID_TITLE', 'StackExchange'), help='Title of the OpenID login button.')
305
    parser_configcreate.add_argument('--login_openid_provider', default=os.environ.get('OPENSUBMIT_LOGIN_OPENID_PROVIDER', 'https://openid.stackexchange.com'), help='URL of the OpenID provider.')
306
    parser_configcreate.add_argument('--login_shib_title', default=os.environ.get('OPENSUBMIT_LOGIN_SHIB_TITLE', ''), help='Title of the Shibboleth login button.')
307
    parser_configcreate.add_argument('--login_demo', default=bool(os.environ.get('OPENSUBMIT_LOGIN_DEMO', 'False')), action='store_true', help='Title of the Shibboleth login button.')
308
    parser_configcreate.add_argument('--admin_email', default=os.environ.get('OPENSUBMIT_ADMIM_EMAIL', 'root@localhost'), help='eMail target for technical problems with the installation.')
309
310
    parser_configtest = subparsers.add_parser('configtest', aliases=['configure'], help='Check config files and database for correct installation of the OpenSubmit web server.')
311
    parser_democreate = subparsers.add_parser('democreate', aliases=['createdemo'], help='Install some test data (courses, assignments, users).')
312
    parser_apachecreate = subparsers.add_parser('apachecreate', help='Create config file snippet for Apache 2.4.')
313
    parser_fixperms = subparsers.add_parser('fixperms', help='Check and fix student and tutor permissions.')
314
    parser_fixchecksums = subparsers.add_parser('fixchecksums', help='Re-create all student file checksums (for duplicate detection).')
315
316
    parser_makeadmin = subparsers.add_parser('makeadmin', help='Make this user an admin with backend rights.')
317
    parser_makeadmin.add_argument('email')
318
    parser_makeowner = subparsers.add_parser('makeowner', help='Make this user a course owner with backend rights.')
319
    parser_makeowner.add_argument('email')
320
    parser_maketutor = subparsers.add_parser('maketutor', help='Make this user a course tutor with backend rights.')
321
    parser_maketutor.add_argument('email')
322
    parser_makestudent = subparsers.add_parser('makestudent', help='Make this user a student without backend rights.')
323
    parser_makestudent.add_argument('email')
324
    args = parser.parse_args()
325
326
    config_file = fsroot + args.config
327
328
    if args.command == 'apachecreate':
329
        config = check_web_config(config_file)
330
        if config:
331
            apache_config(config, os.path.dirname(config_file) + os.sep + 'apache24.conf')
332
        return
333
334
    if args.command == 'configcreate':
335
        configcreate(config_file, vars(args))
336
        return
337
338
    if args.command == 'configtest':
339
        configtest(config_file)
340
        return
341
342
    if args.command in ['fixperms', 'fixchecksums', 'democreate']:
343
        django_admin([args.command])
344
        return
345
346
    if args.command in ['makeadmin', 'makeowner', 'maketutor', 'makestudent']:
347
        django_admin([args.command, args.email])
348
        return
349
350
351
if __name__ == "__main__":
352
    console_script()
353