2018-08-05 14:26:19 +00:00
|
|
|
#! /usr/bin/env python3
|
|
|
|
# coding: utf8
|
|
|
|
import argparse
|
|
|
|
import codecs
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import random
|
|
|
|
import string
|
|
|
|
import sys
|
|
|
|
|
|
|
|
from collections import OrderedDict
|
|
|
|
|
2018-08-05 14:40:51 +00:00
|
|
|
import jinja2
|
|
|
|
|
2018-08-05 14:26:19 +00:00
|
|
|
|
|
|
|
class Configurator:
|
|
|
|
|
|
|
|
def __init__(self, **default_overrides):
|
2018-09-15 13:51:41 +00:00
|
|
|
"""
|
|
|
|
Default values are read, in decreasing order of priority, from:
|
|
|
|
- SETTING_<name> environment variable
|
|
|
|
- Existing config file (in `default_overrides`)
|
|
|
|
- Value passed to add()
|
|
|
|
"""
|
2018-08-05 14:26:19 +00:00
|
|
|
self.__values = OrderedDict()
|
|
|
|
self.__default_values = default_overrides
|
2018-12-03 18:59:09 +00:00
|
|
|
if os.environ.get('SILENT'):
|
|
|
|
self.__input = None
|
|
|
|
else:
|
2018-08-05 14:26:19 +00:00
|
|
|
self.__input = input
|
2018-12-03 18:59:09 +00:00
|
|
|
print("====================================\n"
|
|
|
|
" Interactive configuration \n"
|
|
|
|
"====================================")
|
2018-08-05 14:26:19 +00:00
|
|
|
|
|
|
|
def as_dict(self):
|
2018-08-16 13:59:31 +00:00
|
|
|
return self.__values
|
2018-08-05 14:26:19 +00:00
|
|
|
|
2018-09-15 14:19:35 +00:00
|
|
|
def get_default_value(self, name, default):
|
2018-09-15 13:51:41 +00:00
|
|
|
setting_name = 'SETTING_' + name.upper()
|
2018-09-15 14:19:35 +00:00
|
|
|
if os.environ.get(setting_name):
|
2018-09-15 13:51:41 +00:00
|
|
|
return os.environ[setting_name]
|
2018-09-15 14:19:35 +00:00
|
|
|
if name in self.__default_values:
|
|
|
|
return self.__default_values[name]
|
|
|
|
return default
|
2018-09-15 13:51:41 +00:00
|
|
|
|
2018-11-28 17:27:52 +00:00
|
|
|
def add(self, name, default, question=""):
|
|
|
|
default = self.get_default_value(name, default)
|
|
|
|
if not self.__input or not question:
|
|
|
|
return self.set(name, default)
|
2018-12-03 18:59:09 +00:00
|
|
|
return self.set(name, self.ask(question, default))
|
2018-09-30 12:27:24 +00:00
|
|
|
|
2018-11-28 17:27:52 +00:00
|
|
|
def add_bool(self, name, default, question=""):
|
|
|
|
default = self.get_default_value(name, default)
|
|
|
|
if default in [1, '1']:
|
|
|
|
default = True
|
|
|
|
if default in [0, '0', '']:
|
|
|
|
default = False
|
|
|
|
if not self.__input or not question:
|
|
|
|
return self.set(name, default)
|
2018-12-03 18:59:09 +00:00
|
|
|
question += " (y/n)"
|
2018-11-28 17:27:52 +00:00
|
|
|
while True:
|
2018-12-03 18:59:09 +00:00
|
|
|
answer = self.ask(question, 'y' if default else 'n')
|
2018-11-28 17:27:52 +00:00
|
|
|
if answer is None or answer == '':
|
|
|
|
return self.set(name, default)
|
|
|
|
if answer.lower() in ['y', 'yes']:
|
|
|
|
return self.set(name, True)
|
|
|
|
if answer.lower() in ['n', 'no']:
|
|
|
|
return self.set(name, False)
|
|
|
|
|
|
|
|
def add_choice(self, name, default, choices, question=""):
|
|
|
|
default = self.get_default_value(name, default)
|
|
|
|
if not self.__input or not question:
|
|
|
|
return self.set(name, default)
|
|
|
|
while True:
|
2018-12-03 18:59:09 +00:00
|
|
|
answer = self.ask(question, default)
|
2018-11-28 17:27:52 +00:00
|
|
|
if answer in choices:
|
|
|
|
return self.set(name, answer)
|
|
|
|
print("Invalid value. Choices are: {}".format(", ".join(choices)))
|
2018-09-30 12:27:24 +00:00
|
|
|
|
2018-12-03 18:59:09 +00:00
|
|
|
def ask(self, question, default):
|
|
|
|
return self.__input('\1\2\x1b[35m> {} [{}] \x1b[39;49;00m'.format(question, default)) or default
|
|
|
|
|
2018-08-05 14:26:19 +00:00
|
|
|
def get(self, name):
|
|
|
|
return self.__values.get(name)
|
|
|
|
|
|
|
|
def set(self, name, value):
|
|
|
|
self.__values[name] = value
|
2018-08-16 13:59:31 +00:00
|
|
|
return self
|
2018-08-05 14:26:19 +00:00
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
parser = argparse.ArgumentParser("Config file generator for Open edX")
|
|
|
|
parser.add_argument('-c', '--config', default=os.path.join("/", "openedx", "config", "config.json"),
|
|
|
|
help="Load default values from this file. Config values will be saved there.")
|
|
|
|
subparsers = parser.add_subparsers()
|
|
|
|
|
|
|
|
parser_interactive = subparsers.add_parser('interactive')
|
|
|
|
parser_interactive.set_defaults(func=interactive)
|
|
|
|
|
|
|
|
parser_substitute = subparsers.add_parser('substitute')
|
2018-09-15 14:19:35 +00:00
|
|
|
parser_substitute.add_argument('src', help="Template source directory")
|
|
|
|
parser_substitute.add_argument('dst', help="Destination configuration directory")
|
2018-08-05 14:26:19 +00:00
|
|
|
parser_substitute.set_defaults(func=substitute)
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
2018-08-16 13:59:31 +00:00
|
|
|
args.func(args)
|
2018-08-05 14:26:19 +00:00
|
|
|
|
2018-12-03 18:59:09 +00:00
|
|
|
def load_config(path):
|
|
|
|
if os.path.exists(path):
|
|
|
|
with open(path) as f:
|
2018-08-16 13:59:31 +00:00
|
|
|
return json.load(f)
|
|
|
|
return {}
|
2018-08-05 14:26:19 +00:00
|
|
|
|
2018-08-16 13:59:31 +00:00
|
|
|
def interactive(args):
|
2018-12-03 18:59:09 +00:00
|
|
|
interactive_configuration(args.config)
|
2018-08-05 14:26:19 +00:00
|
|
|
|
2018-12-03 18:59:09 +00:00
|
|
|
def interactive_configuration(config_path):
|
|
|
|
configurator = Configurator(**load_config(config_path))
|
2018-08-05 14:26:19 +00:00
|
|
|
configurator.add(
|
2018-12-03 18:59:09 +00:00
|
|
|
'LMS_HOST', 'www.myopenedx.com', "Your website domain name for students (LMS)"
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-12-03 18:59:09 +00:00
|
|
|
'CMS_HOST', 'studio.' + configurator.get('LMS_HOST'), "Your website domain name for teachers (CMS)"
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'PLATFORM_NAME', "My Open edX", "Your platform name/title"
|
2018-09-05 10:24:07 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'CONTACT_EMAIL', 'contact@' + configurator.get('LMS_HOST'), "Your public contact email address",
|
|
|
|
).add_choice(
|
|
|
|
'LANGUAGE_CODE', 'en',
|
2018-12-03 18:59:09 +00:00
|
|
|
['en', 'am', 'ar', 'az', 'bg-bg', 'bn-bd', 'bn-in', 'bs', 'ca',
|
|
|
|
'ca@valencia', 'cs', 'cy', 'da', 'de-de', 'el', 'en-uk', 'en@lolcat',
|
|
|
|
'en@pirate', 'es-419', 'es-ar', 'es-ec', 'es-es', 'es-mx', 'es-pe',
|
|
|
|
'et-ee', 'eu-es', 'fa', 'fa-ir', 'fi-fi', 'fil', 'fr', 'gl', 'gu',
|
|
|
|
'he', 'hi', 'hr', 'hu', 'hy-am', 'id', 'it-it', 'ja-jp', 'kk-kz',
|
|
|
|
'km-kh', 'kn', 'ko-kr', 'lt-lt', 'ml', 'mn', 'mr', 'ms', 'nb', 'ne',
|
|
|
|
'nl-nl', 'or', 'pl', 'pt-br', 'pt-pt', 'ro', 'ru', 'si', 'sk', 'sl',
|
|
|
|
'sq', 'sr', 'sv', 'sw', 'ta', 'te', 'th', 'tr-tr', 'uk', 'ur', 'vi',
|
|
|
|
'uz', 'zh-cn', 'zh-hk', 'zh-tw'],
|
2018-11-28 17:27:52 +00:00
|
|
|
"The default language code for the platform"
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'SECRET_KEY', random_string(24)
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'MYSQL_DATABASE', 'openedx'
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'MYSQL_USERNAME', 'openedx'
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'MYSQL_PASSWORD', random_string(8)
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'MONGODB_DATABASE', 'openedx'
|
2018-09-15 13:19:57 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'NOTES_MYSQL_DATABASE', 'notes',
|
2018-09-15 13:19:57 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'NOTES_MYSQL_USERNAME', 'notes',
|
2018-09-15 13:19:57 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'NOTES_MYSQL_PASSWORD', random_string(8)
|
2018-09-15 13:19:57 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'NOTES_SECRET_KEY', random_string(24)
|
2018-09-15 13:19:57 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'NOTES_OAUTH2_SECRET', random_string(24)
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'XQUEUE_AUTH_USERNAME', 'lms'
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'XQUEUE_AUTH_PASSWORD', random_string(8)
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'XQUEUE_MYSQL_DATABASE', 'xqueue',
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'XQUEUE_MYSQL_USERNAME', 'xqueue',
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'XQUEUE_MYSQL_PASSWORD', random_string(8)
|
2018-08-05 14:26:19 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'XQUEUE_SECRET_KEY', random_string(24)
|
2018-09-29 13:51:48 +00:00
|
|
|
).add_bool(
|
2018-11-28 17:27:52 +00:00
|
|
|
'ACTIVATE_HTTPS', False, "Activate SSL/TLS certificates for HTTPS access? Important note: this will NOT work in a development environment.",
|
2018-09-15 13:51:41 +00:00
|
|
|
).add_bool(
|
2018-11-28 17:27:52 +00:00
|
|
|
'ACTIVATE_NOTES', False, "Activate Student Notes service (https://open.edx.org/features/student-notes)?",
|
2018-09-15 13:51:41 +00:00
|
|
|
).add_bool(
|
2018-11-28 17:27:52 +00:00
|
|
|
'ACTIVATE_XQUEUE', False, "Activate Xqueue for external grader services? (https://github.com/edx/xqueue)",
|
2018-09-18 18:17:42 +00:00
|
|
|
).add(
|
2018-11-28 17:27:52 +00:00
|
|
|
'ID', random_string(8)
|
2018-08-05 14:26:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
# Save values
|
2018-12-03 18:59:09 +00:00
|
|
|
with open(config_path, 'w') as f:
|
2018-08-05 14:26:19 +00:00
|
|
|
json.dump(configurator.as_dict(), f, sort_keys=True, indent=4)
|
2018-12-25 19:33:27 +00:00
|
|
|
set_owner(config_path)
|
2018-08-05 14:26:19 +00:00
|
|
|
|
|
|
|
|
2018-08-16 13:59:31 +00:00
|
|
|
def substitute(args):
|
2018-12-03 18:59:09 +00:00
|
|
|
config = load_config(args.config)
|
2018-09-15 14:19:35 +00:00
|
|
|
for root, _, filenames in os.walk(args.src):
|
|
|
|
for filename in filenames:
|
|
|
|
if filename.startswith('.'):
|
|
|
|
# Skip hidden files, such as files generated by the IDE
|
|
|
|
continue
|
|
|
|
src_file = os.path.join(root, filename)
|
|
|
|
dst_file = os.path.join(args.dst, os.path.relpath(src_file, args.src))
|
|
|
|
substitute_file(config, src_file, dst_file)
|
|
|
|
|
|
|
|
def substitute_file(config, src, dst):
|
|
|
|
with codecs.open(src, encoding='utf-8') as fi:
|
2018-08-05 14:40:51 +00:00
|
|
|
template = jinja2.Template(fi.read(), undefined=jinja2.StrictUndefined)
|
2018-08-05 14:26:19 +00:00
|
|
|
try:
|
2018-08-16 13:59:31 +00:00
|
|
|
substituted = template.render(**config)
|
2018-08-05 14:40:51 +00:00
|
|
|
except jinja2.exceptions.UndefinedError as e:
|
2018-09-15 14:19:35 +00:00
|
|
|
sys.stderr.write("ERROR Missing config value '{}' for template {}\n".format(e.args[0], src))
|
2018-08-05 14:26:19 +00:00
|
|
|
sys.exit(1)
|
|
|
|
|
2018-09-15 14:19:35 +00:00
|
|
|
dst_dir = os.path.dirname(dst)
|
2018-12-25 19:33:27 +00:00
|
|
|
ensure_path_exists(dst_dir)
|
2018-09-15 14:19:35 +00:00
|
|
|
with codecs.open(dst, encoding='utf-8', mode='w') as fo:
|
2018-08-05 14:26:19 +00:00
|
|
|
fo.write(substituted)
|
2018-12-25 19:33:27 +00:00
|
|
|
set_owner(dst)
|
2018-08-05 14:26:19 +00:00
|
|
|
|
2018-08-16 13:59:31 +00:00
|
|
|
# Set same permissions as original file
|
2018-09-15 14:19:35 +00:00
|
|
|
os.chmod(dst, os.stat(src).st_mode)
|
2018-08-16 13:59:31 +00:00
|
|
|
|
2018-12-25 19:33:27 +00:00
|
|
|
def ensure_path_exists(directory):
|
|
|
|
"""
|
|
|
|
Create the required subfolders one by one, if necessary, and assign them
|
|
|
|
the right uid/gid.
|
|
|
|
"""
|
|
|
|
to_create = []
|
|
|
|
while not os.path.exists(directory):
|
|
|
|
to_create.append(directory)
|
|
|
|
directory = os.path.dirname(directory)
|
|
|
|
for d in to_create[::-1]:
|
|
|
|
os.mkdir(d)
|
|
|
|
set_owner(d)
|
|
|
|
|
|
|
|
def set_owner(path):
|
|
|
|
"""
|
|
|
|
If the USER_ID environment variable is defined, use it to set the
|
|
|
|
owner/group of the given file path.
|
|
|
|
"""
|
|
|
|
user_id = int(os.environ.get('USERID', 0))
|
|
|
|
if user_id:
|
|
|
|
os.chown(path, user_id, user_id)
|
2018-08-05 14:26:19 +00:00
|
|
|
|
|
|
|
def random_string(length):
|
|
|
|
return "".join([random.choice(string.ascii_letters + string.digits) for _ in range(length)])
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|