2019-01-22 20:25:04 +00:00
|
|
|
import json
|
|
|
|
import os
|
2019-04-18 13:12:13 +00:00
|
|
|
import sys
|
2019-01-22 20:25:04 +00:00
|
|
|
|
|
|
|
import click
|
|
|
|
|
2019-05-11 19:20:09 +00:00
|
|
|
from .. import exceptions
|
|
|
|
from .. import env
|
|
|
|
from .. import fmt
|
|
|
|
from .. import opts
|
|
|
|
from .. import serialize
|
|
|
|
from .. import utils
|
|
|
|
from ..__about__ import __version__
|
2019-01-22 20:25:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
@click.group(
|
|
|
|
short_help="Configure Open edX",
|
2019-05-05 09:45:24 +00:00
|
|
|
help="""Configure Open edX and store configuration values in $TUTOR_ROOT/config.yml""",
|
2019-01-22 20:25:04 +00:00
|
|
|
)
|
2019-04-23 07:57:55 +00:00
|
|
|
def config_command():
|
2019-01-22 20:25:04 +00:00
|
|
|
pass
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-03-06 16:49:15 +00:00
|
|
|
@click.command(help="Create and save configuration interactively")
|
2019-01-22 20:25:04 +00:00
|
|
|
@opts.root
|
2019-04-09 18:17:19 +00:00
|
|
|
@click.option("-y", "--yes", "silent1", is_flag=True, help="Run non-interactively")
|
|
|
|
@click.option("--silent", "silent2", is_flag=True, hidden=True)
|
2019-01-22 20:25:04 +00:00
|
|
|
@opts.key_value
|
2019-04-09 18:17:19 +00:00
|
|
|
def save_command(root, silent1, silent2, set_):
|
|
|
|
silent = silent1 or silent2
|
|
|
|
save(root, silent=silent, keyvalues=set_)
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-04-09 18:17:19 +00:00
|
|
|
def save(root, silent=False, keyvalues=None):
|
|
|
|
keyvalues = keyvalues or []
|
2019-05-11 22:11:44 +00:00
|
|
|
defaults = load_defaults()
|
|
|
|
config = load_current(root, defaults)
|
2019-04-09 18:17:19 +00:00
|
|
|
for k, v in keyvalues:
|
2019-01-22 20:25:04 +00:00
|
|
|
config[k] = v
|
2019-03-06 16:49:15 +00:00
|
|
|
if not silent:
|
2019-05-11 22:11:44 +00:00
|
|
|
load_interactive(config, defaults)
|
2019-03-18 16:26:37 +00:00
|
|
|
save_config(root, config)
|
2019-05-11 22:11:44 +00:00
|
|
|
merge(config, defaults)
|
2019-03-18 16:26:37 +00:00
|
|
|
save_env(root, config)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-05 09:45:24 +00:00
|
|
|
@click.command(help="Print the project root")
|
2019-01-22 20:25:04 +00:00
|
|
|
@opts.root
|
|
|
|
def printroot(root):
|
|
|
|
click.echo(root)
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-03-18 21:38:13 +00:00
|
|
|
@click.command(help="Print a configuration value")
|
|
|
|
@opts.root
|
|
|
|
@click.argument("key")
|
|
|
|
def printvalue(root, key):
|
2019-05-11 22:11:44 +00:00
|
|
|
defaults = load_defaults()
|
|
|
|
config = load_current(root, defaults)
|
|
|
|
merge(config, defaults)
|
2019-03-18 21:38:13 +00:00
|
|
|
try:
|
|
|
|
print(config[key])
|
|
|
|
except KeyError:
|
|
|
|
raise exceptions.TutorError("Missing configuration value: {}".format(key))
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
def load(root):
|
|
|
|
"""
|
|
|
|
Load configuration, and generate it interactively if the file does not
|
|
|
|
exist.
|
|
|
|
"""
|
2019-05-11 22:11:44 +00:00
|
|
|
defaults = load_defaults()
|
|
|
|
config = load_current(root, defaults)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-03-18 16:26:37 +00:00
|
|
|
should_update_env = False
|
2019-01-22 20:25:04 +00:00
|
|
|
if not os.path.exists(config_path(root)):
|
2019-05-11 22:11:44 +00:00
|
|
|
load_interactive(config, defaults)
|
2019-03-18 16:26:37 +00:00
|
|
|
should_update_env = True
|
|
|
|
save_config(root, config)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-03-18 16:26:37 +00:00
|
|
|
if not env.is_up_to_date(root):
|
|
|
|
should_update_env = True
|
2019-04-19 22:02:47 +00:00
|
|
|
pre_upgrade_announcement(root)
|
2019-03-18 16:26:37 +00:00
|
|
|
|
|
|
|
if should_update_env:
|
|
|
|
save_env(root, config)
|
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
return config
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
|
|
|
def pre_upgrade_announcement(root):
|
|
|
|
"""
|
|
|
|
Inform the user that the current environment is not up-to-date. Crash if running in
|
|
|
|
non-interactive mode.
|
|
|
|
"""
|
2019-05-05 09:45:24 +00:00
|
|
|
click.echo(
|
|
|
|
fmt.alert(
|
|
|
|
"The current environment stored at {} is not up-to-date: it is at "
|
|
|
|
"v{} while the 'tutor' binary is at v{}.".format(
|
|
|
|
env.base_dir(root), env.version(root), __version__
|
|
|
|
)
|
2019-04-19 22:02:47 +00:00
|
|
|
)
|
2019-05-05 09:45:24 +00:00
|
|
|
)
|
2019-04-19 22:02:47 +00:00
|
|
|
if os.isatty(sys.stdin.fileno()):
|
|
|
|
# Interactive mode: ask the user permission to proceed
|
2019-05-05 09:45:24 +00:00
|
|
|
click.confirm(
|
|
|
|
fmt.question(
|
|
|
|
# every patch you take, every change you make, I'll be watching you
|
|
|
|
"Would you like to upgrade the environment? If you do, any change you"
|
|
|
|
" might have made will be overwritten."
|
|
|
|
),
|
|
|
|
default=True,
|
|
|
|
abort=True,
|
|
|
|
)
|
2019-04-19 22:02:47 +00:00
|
|
|
else:
|
|
|
|
# Non-interactive mode with no authorization: abort
|
|
|
|
raise exceptions.TutorError(
|
|
|
|
"Running in non-interactive mode, the environment will not be upgraded"
|
|
|
|
" automatically. To upgrade the environment manually, run:\n"
|
|
|
|
"\n"
|
|
|
|
" tutor config save -y"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
def load_current(root, defaults):
|
2019-01-22 20:25:04 +00:00
|
|
|
convert_json2yml(root)
|
2019-05-11 22:11:44 +00:00
|
|
|
config = load_user(root)
|
|
|
|
load_env(config, defaults)
|
|
|
|
load_required(config, defaults)
|
2019-04-23 07:57:55 +00:00
|
|
|
return config
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
def load_user(root):
|
|
|
|
path = config_path(root)
|
|
|
|
if not os.path.exists(path):
|
|
|
|
return {}
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
with open(path) as fi:
|
|
|
|
config = serialize.load(fi.read())
|
|
|
|
upgrade_obsolete(config)
|
|
|
|
return config
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-03-18 20:19:03 +00:00
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
def load_env(config, defaults):
|
|
|
|
for k in defaults.keys():
|
2019-03-18 20:19:03 +00:00
|
|
|
env_var = "TUTOR_" + k
|
|
|
|
if env_var in os.environ:
|
2019-03-24 21:34:50 +00:00
|
|
|
config[k] = serialize.parse_value(os.environ[env_var])
|
2019-03-18 20:19:03 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
def load_required(config, defaults):
|
|
|
|
"""
|
|
|
|
All these keys must be present in the user's config.yml. This includes all important
|
|
|
|
values, such as LMS_HOST, and randomly-generated values, such as passwords.
|
|
|
|
"""
|
|
|
|
for key in [
|
|
|
|
"LMS_HOST",
|
|
|
|
"CMS_HOST",
|
|
|
|
"CONTACT_EMAIL",
|
|
|
|
"SECRET_KEY",
|
|
|
|
"MYSQL_ROOT_PASSWORD",
|
|
|
|
"OPENEDX_MYSQL_PASSWORD",
|
|
|
|
"NOTES_MYSQL_PASSWORD",
|
|
|
|
"NOTES_SECRET_KEY",
|
|
|
|
"NOTES_OAUTH2_SECRET",
|
|
|
|
"XQUEUE_AUTH_PASSWORD",
|
|
|
|
"XQUEUE_MYSQL_PASSWORD",
|
|
|
|
"XQUEUE_SECRET_KEY",
|
|
|
|
"ANDROID_OAUTH2_SECRET",
|
|
|
|
"ID",
|
|
|
|
]:
|
|
|
|
if key not in config:
|
|
|
|
config[key] = env.render_str(config, defaults[key])
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-03-22 16:58:41 +00:00
|
|
|
def upgrade_obsolete(config):
|
|
|
|
# Openedx-specific mysql passwords
|
2019-03-20 17:45:20 +00:00
|
|
|
if "MYSQL_PASSWORD" in config:
|
|
|
|
config["MYSQL_ROOT_PASSWORD"] = config["MYSQL_PASSWORD"]
|
|
|
|
config["OPENEDX_MYSQL_PASSWORD"] = config["MYSQL_PASSWORD"]
|
|
|
|
config.pop("MYSQL_PASSWORD")
|
|
|
|
if "MYSQL_DATABASE" in config:
|
|
|
|
config["OPENEDX_MYSQL_DATABASE"] = config.pop("MYSQL_DATABASE")
|
|
|
|
if "MYSQL_USERNAME" in config:
|
|
|
|
config["OPENEDX_MYSQL_USERNAME"] = config.pop("MYSQL_USERNAME")
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
def load_interactive(config, defaults):
|
|
|
|
ask("Your website domain name for students (LMS)", "LMS_HOST", config, defaults)
|
|
|
|
ask("Your website domain name for teachers (CMS)", "CMS_HOST", config, defaults)
|
|
|
|
ask("Your platform name/title", "PLATFORM_NAME", config, defaults)
|
|
|
|
ask("Your public contact email address", "CONTACT_EMAIL", config, defaults)
|
2019-01-22 20:25:04 +00:00
|
|
|
ask_choice(
|
|
|
|
"The default language code for the platform",
|
2019-05-05 09:45:24 +00:00
|
|
|
"LANGUAGE_CODE",
|
|
|
|
config,
|
2019-05-11 22:11:44 +00:00
|
|
|
defaults,
|
2019-05-05 09:45:24 +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",
|
|
|
|
],
|
2019-01-22 20:25:04 +00:00
|
|
|
)
|
|
|
|
ask_bool(
|
2019-05-05 09:45:24 +00:00
|
|
|
(
|
|
|
|
"Activate SSL/TLS certificates for HTTPS access? Important note:"
|
2019-05-15 08:19:51 +00:00
|
|
|
" this will NOT work in a development environment."
|
2019-05-05 09:45:24 +00:00
|
|
|
),
|
|
|
|
"ACTIVATE_HTTPS",
|
|
|
|
config,
|
2019-05-11 22:11:44 +00:00
|
|
|
defaults,
|
2019-01-22 20:25:04 +00:00
|
|
|
)
|
|
|
|
ask_bool(
|
|
|
|
"Activate Student Notes service (https://open.edx.org/features/student-notes)?",
|
2019-05-05 09:45:24 +00:00
|
|
|
"ACTIVATE_NOTES",
|
|
|
|
config,
|
2019-05-11 22:11:44 +00:00
|
|
|
defaults,
|
2019-01-22 20:25:04 +00:00
|
|
|
)
|
|
|
|
ask_bool(
|
|
|
|
"Activate Xqueue for external grader services (https://github.com/edx/xqueue)?",
|
2019-05-05 09:45:24 +00:00
|
|
|
"ACTIVATE_XQUEUE",
|
|
|
|
config,
|
2019-05-11 22:11:44 +00:00
|
|
|
defaults,
|
2019-01-22 20:25:04 +00:00
|
|
|
)
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
def load_defaults():
|
|
|
|
return serialize.load(env.read("config.yml"))
|
2019-03-23 23:07:50 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
def ask(question, key, config, defaults):
|
|
|
|
default = env.render_str(config, config.get(key, defaults[key]))
|
2019-01-22 20:25:04 +00:00
|
|
|
config[key] = click.prompt(
|
2019-05-05 09:45:24 +00:00
|
|
|
fmt.question(question), prompt_suffix=" ", default=default, show_default=True
|
2019-01-22 20:25:04 +00:00
|
|
|
)
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
def ask_bool(question, key, config, defaults):
|
|
|
|
default = config.get(key, defaults[key])
|
|
|
|
config[key] = click.confirm(fmt.question(question), prompt_suffix=" ", default=default)
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
def ask_choice(question, key, config, defaults, choices):
|
|
|
|
default = config.get(key, defaults[key])
|
2019-01-22 20:25:04 +00:00
|
|
|
answer = click.prompt(
|
|
|
|
fmt.question(question),
|
|
|
|
type=click.Choice(choices),
|
2019-04-19 22:02:47 +00:00
|
|
|
prompt_suffix=" ",
|
|
|
|
default=default,
|
|
|
|
show_choices=False,
|
2019-01-22 20:25:04 +00:00
|
|
|
)
|
|
|
|
config[key] = answer
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
def convert_json2yml(root):
|
|
|
|
json_path = os.path.join(root, "config.json")
|
|
|
|
if not os.path.exists(json_path):
|
|
|
|
return
|
|
|
|
if os.path.exists(config_path(root)):
|
|
|
|
raise exceptions.TutorError(
|
2019-05-05 09:45:24 +00:00
|
|
|
"Both config.json and config.yml exist in {}: only one of these files must exist to continue".format(
|
|
|
|
root
|
|
|
|
)
|
2019-01-22 20:25:04 +00:00
|
|
|
)
|
|
|
|
with open(json_path) as fi:
|
|
|
|
config = json.load(fi)
|
2019-03-18 16:26:37 +00:00
|
|
|
save_config(root, config)
|
2019-01-22 20:25:04 +00:00
|
|
|
os.remove(json_path)
|
2019-05-05 09:45:24 +00:00
|
|
|
click.echo(
|
|
|
|
fmt.info(
|
|
|
|
"File config.json detected in {} and converted to config.yml".format(root)
|
|
|
|
)
|
|
|
|
)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-03-18 16:26:37 +00:00
|
|
|
def save_config(root, config):
|
2019-01-22 20:25:04 +00:00
|
|
|
path = config_path(root)
|
2019-05-11 22:11:44 +00:00
|
|
|
utils.ensure_file_directory_exists(path)
|
2019-01-22 20:25:04 +00:00
|
|
|
with open(path, "w") as of:
|
2019-03-24 21:34:50 +00:00
|
|
|
serialize.dump(config, of)
|
2019-01-22 20:25:04 +00:00
|
|
|
click.echo(fmt.info("Configuration saved to {}".format(path)))
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-03-18 16:26:37 +00:00
|
|
|
def save_env(root, config):
|
|
|
|
env.render_full(root, config)
|
|
|
|
click.echo(fmt.info("Environment generated in {}".format(env.base_dir(root))))
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
def config_path(root):
|
|
|
|
return os.path.join(root, "config.yml")
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-04-23 07:57:55 +00:00
|
|
|
config_command.add_command(save_command, name="save")
|
|
|
|
config_command.add_command(printroot)
|
|
|
|
config_command.add_command(printvalue)
|