mirror of
https://github.com/ChristianLight/tutor.git
synced 2024-12-12 14:17:46 +00:00
Run code formatting on the entire code base
This commit is contained in:
parent
4ac7dff06a
commit
f2f714ad23
@ -7,51 +7,59 @@ from . import opts
|
|||||||
from . import utils
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
@click.group(
|
@click.group(help="Build an Android app for your Open edX platform [BETA FEATURE]")
|
||||||
help="Build an Android app for your Open edX platform [BETA FEATURE]"
|
|
||||||
)
|
|
||||||
def android():
|
def android():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@click.group(
|
|
||||||
help="Build the application"
|
@click.group(help="Build the application")
|
||||||
)
|
|
||||||
def build():
|
def build():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@click.command(
|
|
||||||
help="Build the application in debug mode"
|
@click.command(help="Build the application in debug mode")
|
||||||
)
|
|
||||||
@opts.root
|
@opts.root
|
||||||
def debug(root):
|
def debug(root):
|
||||||
docker_run(root)
|
docker_run(root)
|
||||||
click.echo(fmt.info("The debuggable APK file is available in {}".format(tutor_env.data_path(root, "android"))))
|
click.echo(
|
||||||
|
fmt.info(
|
||||||
@click.command(
|
"The debuggable APK file is available in {}".format(
|
||||||
help="Build the application in release mode"
|
tutor_env.data_path(root, "android")
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(help="Build the application in release mode")
|
||||||
@opts.root
|
@opts.root
|
||||||
def release(root):
|
def release(root):
|
||||||
docker_run(root, "./gradlew", "assembleProdRelease")
|
docker_run(root, "./gradlew", "assembleProdRelease")
|
||||||
click.echo(fmt.info("The production APK file is available in {}".format(tutor_env.data_path(root, "android"))))
|
click.echo(
|
||||||
|
fmt.info(
|
||||||
@click.command(
|
"The production APK file is available in {}".format(
|
||||||
help="Pull the docker image"
|
tutor_env.data_path(root, "android")
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(help="Pull the docker image")
|
||||||
@opts.root
|
@opts.root
|
||||||
def pullimage(root):
|
def pullimage(root):
|
||||||
config = tutor_config.load(root)
|
config = tutor_config.load(root)
|
||||||
utils.execute("docker", "pull", config['DOCKER_IMAGE_ANDROID'])
|
utils.execute("docker", "pull", config["DOCKER_IMAGE_ANDROID"])
|
||||||
|
|
||||||
|
|
||||||
def docker_run(root, *command):
|
def docker_run(root, *command):
|
||||||
config = tutor_config.load(root)
|
config = tutor_config.load(root)
|
||||||
utils.docker_run(
|
utils.docker_run(
|
||||||
"--volume={}:/openedx/config/".format(tutor_env.pathjoin(root, "android")),
|
"--volume={}:/openedx/config/".format(tutor_env.pathjoin(root, "android")),
|
||||||
"--volume={}:/openedx/data/".format(tutor_env.data_path(root, "android")),
|
"--volume={}:/openedx/data/".format(tutor_env.data_path(root, "android")),
|
||||||
config['DOCKER_IMAGE_ANDROID'],
|
config["DOCKER_IMAGE_ANDROID"],
|
||||||
*command
|
*command
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
build.add_command(debug)
|
build.add_command(debug)
|
||||||
build.add_command(release)
|
build.add_command(release)
|
||||||
android.add_command(build)
|
android.add_command(build)
|
||||||
|
@ -25,16 +25,13 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@click.group(context_settings={'help_option_names': ['-h', '--help', 'help']})
|
@click.group(context_settings={"help_option_names": ["-h", "--help", "help"]})
|
||||||
@click.version_option(version=__version__)
|
@click.version_option(version=__version__)
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(help="Print this help", name="help")
|
||||||
help="Print this help",
|
|
||||||
name="help",
|
|
||||||
)
|
|
||||||
def print_help():
|
def print_help():
|
||||||
with click.Context(cli) as context:
|
with click.Context(cli) as context:
|
||||||
click.echo(cli.get_help(context))
|
click.echo(cli.get_help(context))
|
||||||
|
149
tutor/config.py
149
tutor/config.py
@ -15,7 +15,7 @@ from .__about__ import __version__
|
|||||||
|
|
||||||
@click.group(
|
@click.group(
|
||||||
short_help="Configure Open edX",
|
short_help="Configure Open edX",
|
||||||
help="""Configure Open edX and store configuration values in $TUTOR_ROOT/config.yml"""
|
help="""Configure Open edX and store configuration values in $TUTOR_ROOT/config.yml""",
|
||||||
)
|
)
|
||||||
def config_command():
|
def config_command():
|
||||||
pass
|
pass
|
||||||
@ -44,9 +44,7 @@ def save(root, silent=False, keyvalues=None):
|
|||||||
save_env(root, config)
|
save_env(root, config)
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(help="Print the project root")
|
||||||
help="Print the project root",
|
|
||||||
)
|
|
||||||
@opts.root
|
@opts.root
|
||||||
def printroot(root):
|
def printroot(root):
|
||||||
click.echo(root)
|
click.echo(root)
|
||||||
@ -93,19 +91,25 @@ def pre_upgrade_announcement(root):
|
|||||||
Inform the user that the current environment is not up-to-date. Crash if running in
|
Inform the user that the current environment is not up-to-date. Crash if running in
|
||||||
non-interactive mode.
|
non-interactive mode.
|
||||||
"""
|
"""
|
||||||
click.echo(fmt.alert(
|
click.echo(
|
||||||
|
fmt.alert(
|
||||||
"The current environment stored at {} is not up-to-date: it is at "
|
"The current environment stored at {} is not up-to-date: it is at "
|
||||||
"v{} while the 'tutor' binary is at v{}.".format(
|
"v{} while the 'tutor' binary is at v{}.".format(
|
||||||
env.base_dir(root), env.version(root), __version__
|
env.base_dir(root), env.version(root), __version__
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
|
)
|
||||||
if os.isatty(sys.stdin.fileno()):
|
if os.isatty(sys.stdin.fileno()):
|
||||||
# Interactive mode: ask the user permission to proceed
|
# Interactive mode: ask the user permission to proceed
|
||||||
click.confirm(fmt.question(
|
click.confirm(
|
||||||
|
fmt.question(
|
||||||
# every patch you take, every change you make, I'll be watching you
|
# 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"
|
"Would you like to upgrade the environment? If you do, any change you"
|
||||||
" might have made will be overwritten."
|
" might have made will be overwritten."
|
||||||
), default=True, abort=True)
|
),
|
||||||
|
default=True,
|
||||||
|
abort=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Non-interactive mode with no authorization: abort
|
# Non-interactive mode with no authorization: abort
|
||||||
raise exceptions.TutorError(
|
raise exceptions.TutorError(
|
||||||
@ -171,29 +175,105 @@ def load_interactive(config):
|
|||||||
ask("Your public contact email address", "CONTACT_EMAIL", config)
|
ask("Your public contact email address", "CONTACT_EMAIL", config)
|
||||||
ask_choice(
|
ask_choice(
|
||||||
"The default language code for the platform",
|
"The default language code for the platform",
|
||||||
"LANGUAGE_CODE", config,
|
"LANGUAGE_CODE",
|
||||||
['en', 'am', 'ar', 'az', 'bg-bg', 'bn-bd', 'bn-in', 'bs', 'ca',
|
config,
|
||||||
'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',
|
"en",
|
||||||
'et-ee', 'eu-es', 'fa', 'fa-ir', 'fi-fi', 'fil', 'fr', 'gl', 'gu',
|
"am",
|
||||||
'he', 'hi', 'hr', 'hu', 'hy-am', 'id', 'it-it', 'ja-jp', 'kk-kz',
|
"ar",
|
||||||
'km-kh', 'kn', 'ko-kr', 'lt-lt', 'ml', 'mn', 'mr', 'ms', 'nb', 'ne',
|
"az",
|
||||||
'nl-nl', 'or', 'pl', 'pt-br', 'pt-pt', 'ro', 'ru', 'si', 'sk', 'sl',
|
"bg-bg",
|
||||||
'sq', 'sr', 'sv', 'sw', 'ta', 'te', 'th', 'tr-tr', 'uk', 'ur', 'vi',
|
"bn-bd",
|
||||||
'uz', 'zh-cn', 'zh-hk', 'zh-tw'],
|
"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",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
ask_bool(
|
ask_bool(
|
||||||
("Activate SSL/TLS certificates for HTTPS access? Important note:"
|
(
|
||||||
"this will NOT work in a development environment."),
|
"Activate SSL/TLS certificates for HTTPS access? Important note:"
|
||||||
"ACTIVATE_HTTPS", config
|
"this will NOT work in a development environment."
|
||||||
|
),
|
||||||
|
"ACTIVATE_HTTPS",
|
||||||
|
config,
|
||||||
)
|
)
|
||||||
ask_bool(
|
ask_bool(
|
||||||
"Activate Student Notes service (https://open.edx.org/features/student-notes)?",
|
"Activate Student Notes service (https://open.edx.org/features/student-notes)?",
|
||||||
"ACTIVATE_NOTES", config
|
"ACTIVATE_NOTES",
|
||||||
|
config,
|
||||||
)
|
)
|
||||||
ask_bool(
|
ask_bool(
|
||||||
"Activate Xqueue for external grader services (https://github.com/edx/xqueue)?",
|
"Activate Xqueue for external grader services (https://github.com/edx/xqueue)?",
|
||||||
"ACTIVATE_XQUEUE", config
|
"ACTIVATE_XQUEUE",
|
||||||
|
config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -204,24 +284,21 @@ def load_defaults(config):
|
|||||||
config[k] = v
|
config[k] = v
|
||||||
|
|
||||||
# Add extra configuration parameters that need to be computed separately
|
# Add extra configuration parameters that need to be computed separately
|
||||||
config["lms_cms_common_domain"] = utils.common_domain(config["LMS_HOST"], config["CMS_HOST"])
|
config["lms_cms_common_domain"] = utils.common_domain(
|
||||||
|
config["LMS_HOST"], config["CMS_HOST"]
|
||||||
|
)
|
||||||
config["lms_host_reverse"] = ".".join(config["LMS_HOST"].split(".")[::-1])
|
config["lms_host_reverse"] = ".".join(config["LMS_HOST"].split(".")[::-1])
|
||||||
|
|
||||||
|
|
||||||
def ask(question, key, config):
|
def ask(question, key, config):
|
||||||
default = env.render_str(config, config[key])
|
default = env.render_str(config, config[key])
|
||||||
config[key] = click.prompt(
|
config[key] = click.prompt(
|
||||||
fmt.question(question),
|
fmt.question(question), prompt_suffix=" ", default=default, show_default=True
|
||||||
prompt_suffix=" ", default=default, show_default=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def ask_bool(question, key, config):
|
def ask_bool(question, key, config):
|
||||||
return click.confirm(
|
return click.confirm(fmt.question(question), prompt_suffix=" ", default=config[key])
|
||||||
fmt.question(question),
|
|
||||||
prompt_suffix=' ',
|
|
||||||
default=config[key],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def ask_choice(question, key, config, choices):
|
def ask_choice(question, key, config, choices):
|
||||||
@ -242,13 +319,19 @@ def convert_json2yml(root):
|
|||||||
return
|
return
|
||||||
if os.path.exists(config_path(root)):
|
if os.path.exists(config_path(root)):
|
||||||
raise exceptions.TutorError(
|
raise exceptions.TutorError(
|
||||||
"Both config.json and config.yml exist in {}: only one of these files must exist to continue".format(root)
|
"Both config.json and config.yml exist in {}: only one of these files must exist to continue".format(
|
||||||
|
root
|
||||||
|
)
|
||||||
)
|
)
|
||||||
with open(json_path) as fi:
|
with open(json_path) as fi:
|
||||||
config = json.load(fi)
|
config = json.load(fi)
|
||||||
save_config(root, config)
|
save_config(root, config)
|
||||||
os.remove(json_path)
|
os.remove(json_path)
|
||||||
click.echo(fmt.info("File config.json detected in {} and converted to config.yml".format(root)))
|
click.echo(
|
||||||
|
fmt.info(
|
||||||
|
"File config.json detected in {} and converted to config.yml".format(root)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_config(root, config):
|
def save_config(root, config):
|
||||||
|
67
tutor/dev.py
67
tutor/dev.py
@ -7,9 +7,7 @@ from . import opts
|
|||||||
from . import utils
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
@click.group(
|
@click.group(help="Run Open edX platform with development settings")
|
||||||
help="Run Open edX platform with development settings",
|
|
||||||
)
|
|
||||||
def dev():
|
def dev():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -36,9 +34,7 @@ def run(root, edx_platform_path, edx_platform_settings, service, command, args):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(help="Run a development server")
|
||||||
help="Run a development server",
|
|
||||||
)
|
|
||||||
@opts.root
|
@opts.root
|
||||||
@opts.edx_platform_path
|
@opts.edx_platform_path
|
||||||
@opts.edx_platform_settings
|
@opts.edx_platform_settings
|
||||||
@ -46,47 +42,70 @@ def run(root, edx_platform_path, edx_platform_settings, service, command, args):
|
|||||||
def runserver(root, edx_platform_path, edx_platform_settings, service):
|
def runserver(root, edx_platform_path, edx_platform_settings, service):
|
||||||
port = service_port(service)
|
port = service_port(service)
|
||||||
docker_compose_run_with_port(
|
docker_compose_run_with_port(
|
||||||
root, edx_platform_path, edx_platform_settings, port,
|
root,
|
||||||
service, "./manage.py", service, "runserver", "0.0.0.0:{}".format(port),
|
edx_platform_path,
|
||||||
|
edx_platform_settings,
|
||||||
|
port,
|
||||||
|
service,
|
||||||
|
"./manage.py",
|
||||||
|
service,
|
||||||
|
"runserver",
|
||||||
|
"0.0.0.0:{}".format(port),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@click.command(help="Stop a running development platform",)
|
@click.command(help="Stop a running development platform")
|
||||||
@opts.root
|
@opts.root
|
||||||
def stop(root):
|
def stop(root):
|
||||||
docker_compose(root, "rm", "--stop", "--force")
|
docker_compose(root, "rm", "--stop", "--force")
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(help="Watch for changes in your themes and recompile assets when needed")
|
||||||
help="Watch for changes in your themes and recompile assets when needed"
|
|
||||||
)
|
|
||||||
@opts.root
|
@opts.root
|
||||||
@opts.edx_platform_path
|
@opts.edx_platform_path
|
||||||
@opts.edx_platform_settings
|
@opts.edx_platform_settings
|
||||||
def watchthemes(root, edx_platform_path, edx_platform_settings):
|
def watchthemes(root, edx_platform_path, edx_platform_settings):
|
||||||
docker_compose_run(
|
docker_compose_run(
|
||||||
root, edx_platform_path, edx_platform_settings,
|
root,
|
||||||
"--no-deps", "lms", "openedx-assets", "watch-themes", "--env", "dev"
|
edx_platform_path,
|
||||||
|
edx_platform_settings,
|
||||||
|
"--no-deps",
|
||||||
|
"lms",
|
||||||
|
"openedx-assets",
|
||||||
|
"watch-themes",
|
||||||
|
"--env",
|
||||||
|
"dev",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def docker_compose_run_with_port(root, edx_platform_path, edx_platform_settings, port, *command):
|
def docker_compose_run_with_port(
|
||||||
|
root, edx_platform_path, edx_platform_settings, port, *command
|
||||||
|
):
|
||||||
docker_compose_run(
|
docker_compose_run(
|
||||||
root, edx_platform_path, edx_platform_settings,
|
root,
|
||||||
"-p", "{port}:{port}".format(port=port), *command
|
edx_platform_path,
|
||||||
|
edx_platform_settings,
|
||||||
|
"-p",
|
||||||
|
"{port}:{port}".format(port=port),
|
||||||
|
*command
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def docker_compose_run(root, edx_platform_path, edx_platform_settings, *command):
|
def docker_compose_run(root, edx_platform_path, edx_platform_settings, *command):
|
||||||
run_command = [
|
run_command = [
|
||||||
"run", "--rm",
|
"run",
|
||||||
"-e", "SETTINGS={}".format(edx_platform_settings),
|
"--rm",
|
||||||
"--volume={}:/openedx/themes".format(tutor_env.pathjoin(root, "build", "openedx", "themes")),
|
"-e",
|
||||||
|
"SETTINGS={}".format(edx_platform_settings),
|
||||||
|
"--volume={}:/openedx/themes".format(
|
||||||
|
tutor_env.pathjoin(root, "build", "openedx", "themes")
|
||||||
|
),
|
||||||
]
|
]
|
||||||
if edx_platform_path:
|
if edx_platform_path:
|
||||||
run_command += [
|
run_command += [
|
||||||
"--volume={}:/openedx/edx-platform".format(edx_platform_path),
|
"--volume={}:/openedx/edx-platform".format(edx_platform_path),
|
||||||
"-e", "USERID={}".format(subprocess.check_output(["id", "-u"]).strip().decode())
|
"-e",
|
||||||
|
"USERID={}".format(subprocess.check_output(["id", "-u"]).strip().decode()),
|
||||||
]
|
]
|
||||||
run_command += command
|
run_command += command
|
||||||
docker_compose(root, *run_command)
|
docker_compose(root, *run_command)
|
||||||
@ -94,8 +113,10 @@ def docker_compose_run(root, edx_platform_path, edx_platform_settings, *command)
|
|||||||
|
|
||||||
def docker_compose(root, *command):
|
def docker_compose(root, *command):
|
||||||
return utils.docker_compose(
|
return utils.docker_compose(
|
||||||
"-f", tutor_env.pathjoin(root, "local", "docker-compose.yml"),
|
"-f",
|
||||||
"--project-name", "tutor_dev",
|
tutor_env.pathjoin(root, "local", "docker-compose.yml"),
|
||||||
|
"--project-name",
|
||||||
|
"tutor_dev",
|
||||||
*command
|
*command
|
||||||
)
|
)
|
||||||
|
|
||||||
|
15
tutor/env.py
15
tutor/env.py
@ -20,7 +20,7 @@ def render_full(root, config):
|
|||||||
for target in ["android", "apps", "k8s", "local", "webui"]:
|
for target in ["android", "apps", "k8s", "local", "webui"]:
|
||||||
render_target(root, config, target)
|
render_target(root, config, target)
|
||||||
copy_target(root, "build")
|
copy_target(root, "build")
|
||||||
with open(pathjoin(root, VERSION_FILENAME), 'w') as f:
|
with open(pathjoin(root, VERSION_FILENAME), "w") as f:
|
||||||
f.write(__version__)
|
f.write(__version__)
|
||||||
|
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ def render_target(root, config, target):
|
|||||||
|
|
||||||
|
|
||||||
def render_file(config, path):
|
def render_file(config, path):
|
||||||
with codecs.open(path, encoding='utf-8') as fi:
|
with codecs.open(path, encoding="utf-8") as fi:
|
||||||
try:
|
try:
|
||||||
return render_str(config, fi.read())
|
return render_str(config, fi.read())
|
||||||
except jinja2.exceptions.TemplateError:
|
except jinja2.exceptions.TemplateError:
|
||||||
@ -77,9 +77,7 @@ def render_str(config, text):
|
|||||||
template = jinja2.Template(text, undefined=jinja2.StrictUndefined)
|
template = jinja2.Template(text, undefined=jinja2.StrictUndefined)
|
||||||
try:
|
try:
|
||||||
return template.render(
|
return template.render(
|
||||||
RAND8=utils.random_string(8),
|
RAND8=utils.random_string(8), RAND24=utils.random_string(24), **config
|
||||||
RAND24=utils.random_string(24),
|
|
||||||
**config
|
|
||||||
)
|
)
|
||||||
except jinja2.exceptions.UndefinedError as e:
|
except jinja2.exceptions.UndefinedError as e:
|
||||||
raise exceptions.TutorError("Missing configuration value: {}".format(e.args[0]))
|
raise exceptions.TutorError("Missing configuration value: {}".format(e.args[0]))
|
||||||
@ -113,7 +111,7 @@ def read(*path):
|
|||||||
Read template content located at `path`.
|
Read template content located at `path`.
|
||||||
"""
|
"""
|
||||||
src = template_path(*path)
|
src = template_path(*path)
|
||||||
with codecs.open(src, encoding='utf-8') as fi:
|
with codecs.open(src, encoding="utf-8") as fi:
|
||||||
return fi.read()
|
return fi.read()
|
||||||
|
|
||||||
|
|
||||||
@ -129,10 +127,7 @@ def walk_templates(root, target):
|
|||||||
for dirpath, _, filenames in os.walk(target_root):
|
for dirpath, _, filenames in os.walk(target_root):
|
||||||
if not is_part_of_env(dirpath):
|
if not is_part_of_env(dirpath):
|
||||||
continue
|
continue
|
||||||
dst_dir = pathjoin(
|
dst_dir = pathjoin(root, target, os.path.relpath(dirpath, target_root))
|
||||||
root, target,
|
|
||||||
os.path.relpath(dirpath, target_root)
|
|
||||||
)
|
|
||||||
if not os.path.exists(dst_dir):
|
if not os.path.exists(dst_dir):
|
||||||
os.makedirs(dst_dir)
|
os.makedirs(dst_dir)
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
|
10
tutor/fmt.py
10
tutor/fmt.py
@ -1,26 +1,30 @@
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
def title(text):
|
def title(text):
|
||||||
indent = 8
|
indent = 8
|
||||||
separator = "=" * (len(text) + 2 * indent)
|
separator = "=" * (len(text) + 2 * indent)
|
||||||
message = "{separator}\n{indent}{text}\n{separator}".format(
|
message = "{separator}\n{indent}{text}\n{separator}".format(
|
||||||
separator=separator,
|
separator=separator, indent=" " * indent, text=text
|
||||||
indent=" " * indent,
|
|
||||||
text=text,
|
|
||||||
)
|
)
|
||||||
return click.style(message, fg="green")
|
return click.style(message, fg="green")
|
||||||
|
|
||||||
|
|
||||||
def info(text):
|
def info(text):
|
||||||
return click.style(text, fg="blue")
|
return click.style(text, fg="blue")
|
||||||
|
|
||||||
|
|
||||||
def error(text):
|
def error(text):
|
||||||
return click.style(text, fg="red")
|
return click.style(text, fg="red")
|
||||||
|
|
||||||
|
|
||||||
def command(text):
|
def command(text):
|
||||||
return click.style(text, fg="magenta")
|
return click.style(text, fg="magenta")
|
||||||
|
|
||||||
|
|
||||||
def question(text):
|
def question(text):
|
||||||
return click.style(text, fg="yellow")
|
return click.style(text, fg="yellow")
|
||||||
|
|
||||||
|
|
||||||
def alert(text):
|
def alert(text):
|
||||||
return click.style("⚠️ " + text, fg="yellow", bold=True)
|
return click.style("⚠️ " + text, fg="yellow", bold=True)
|
||||||
|
@ -13,38 +13,43 @@ def images_command():
|
|||||||
|
|
||||||
|
|
||||||
OPENEDX_IMAGES = ["openedx", "forum", "notes", "xqueue", "android"]
|
OPENEDX_IMAGES = ["openedx", "forum", "notes", "xqueue", "android"]
|
||||||
VENDOR_IMAGES = ["elasticsearch", "memcached", "mongodb", "mysql", "nginx", "rabbitmq", "smtp"]
|
VENDOR_IMAGES = [
|
||||||
|
"elasticsearch",
|
||||||
|
"memcached",
|
||||||
|
"mongodb",
|
||||||
|
"mysql",
|
||||||
|
"nginx",
|
||||||
|
"rabbitmq",
|
||||||
|
"smtp",
|
||||||
|
]
|
||||||
argument_openedx_image = click.argument(
|
argument_openedx_image = click.argument(
|
||||||
"image", type=click.Choice(["all"] + OPENEDX_IMAGES),
|
"image", type=click.Choice(["all"] + OPENEDX_IMAGES)
|
||||||
)
|
)
|
||||||
argument_image = click.argument(
|
argument_image = click.argument(
|
||||||
"image", type=click.Choice(["all"] + OPENEDX_IMAGES + VENDOR_IMAGES),
|
"image", type=click.Choice(["all"] + OPENEDX_IMAGES + VENDOR_IMAGES)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(
|
||||||
short_help="Build docker images",
|
short_help="Build docker images",
|
||||||
help="Build the docker images necessary for an Open edX platform."
|
help="Build the docker images necessary for an Open edX platform.",
|
||||||
)
|
)
|
||||||
@opts.root
|
@opts.root
|
||||||
@argument_openedx_image
|
@argument_openedx_image
|
||||||
@click.option(
|
@click.option(
|
||||||
"-a", "--build-arg", multiple=True,
|
"-a",
|
||||||
help="Set build-time docker ARGS in the form 'myarg=value'. This option may be specified multiple times."
|
"--build-arg",
|
||||||
|
multiple=True,
|
||||||
|
help="Set build-time docker ARGS in the form 'myarg=value'. This option may be specified multiple times.",
|
||||||
)
|
)
|
||||||
def build(root, image, build_arg):
|
def build(root, image, build_arg):
|
||||||
config = tutor_config.load(root)
|
config = tutor_config.load(root)
|
||||||
for img in openedx_image_names(config, image):
|
for img in openedx_image_names(config, image):
|
||||||
tag = get_tag(config, img)
|
tag = get_tag(config, img)
|
||||||
click.echo(fmt.info("Building image {}".format(tag)))
|
click.echo(fmt.info("Building image {}".format(tag)))
|
||||||
command = [
|
command = ["build", "-t", tag, tutor_env.pathjoin(root, "build", img)]
|
||||||
"build", "-t", tag,
|
|
||||||
tutor_env.pathjoin(root, "build", img)
|
|
||||||
]
|
|
||||||
for arg in build_arg:
|
for arg in build_arg:
|
||||||
command += [
|
command += ["--build-arg", arg]
|
||||||
"--build-arg", arg
|
|
||||||
]
|
|
||||||
utils.docker(*command)
|
utils.docker(*command)
|
||||||
|
|
||||||
|
|
||||||
@ -71,10 +76,7 @@ def push(root, image):
|
|||||||
|
|
||||||
def get_tag(config, name):
|
def get_tag(config, name):
|
||||||
image = config["DOCKER_IMAGE_" + name.upper()]
|
image = config["DOCKER_IMAGE_" + name.upper()]
|
||||||
return "{registry}{image}".format(
|
return "{registry}{image}".format(registry=config["DOCKER_REGISTRY"], image=image)
|
||||||
registry=config["DOCKER_REGISTRY"],
|
|
||||||
image=image,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def image_names(config, image):
|
def image_names(config, image):
|
||||||
@ -89,10 +91,10 @@ def openedx_image_tags(config, image):
|
|||||||
def openedx_image_names(config, image):
|
def openedx_image_names(config, image):
|
||||||
if image == "all":
|
if image == "all":
|
||||||
images = OPENEDX_IMAGES[:]
|
images = OPENEDX_IMAGES[:]
|
||||||
if not config['ACTIVATE_XQUEUE']:
|
if not config["ACTIVATE_XQUEUE"]:
|
||||||
images.remove('xqueue')
|
images.remove("xqueue")
|
||||||
if not config['ACTIVATE_NOTES']:
|
if not config["ACTIVATE_NOTES"]:
|
||||||
images.remove('notes')
|
images.remove("notes")
|
||||||
return images
|
return images
|
||||||
return [image]
|
return [image]
|
||||||
|
|
||||||
@ -100,16 +102,16 @@ def openedx_image_names(config, image):
|
|||||||
def vendor_image_names(config, image):
|
def vendor_image_names(config, image):
|
||||||
if image == "all":
|
if image == "all":
|
||||||
images = VENDOR_IMAGES[:]
|
images = VENDOR_IMAGES[:]
|
||||||
if not config['ACTIVATE_ELASTICSEARCH']:
|
if not config["ACTIVATE_ELASTICSEARCH"]:
|
||||||
images.remove('elasticsearch')
|
images.remove("elasticsearch")
|
||||||
if not config['ACTIVATE_MEMCACHED']:
|
if not config["ACTIVATE_MEMCACHED"]:
|
||||||
images.remove('memcached')
|
images.remove("memcached")
|
||||||
if not config['ACTIVATE_MONGODB']:
|
if not config["ACTIVATE_MONGODB"]:
|
||||||
images.remove('mongodb')
|
images.remove("mongodb")
|
||||||
if not config['ACTIVATE_MYSQL']:
|
if not config["ACTIVATE_MYSQL"]:
|
||||||
images.remove('mysql')
|
images.remove("mysql")
|
||||||
if not config['ACTIVATE_RABBITMQ']:
|
if not config["ACTIVATE_RABBITMQ"]:
|
||||||
images.remove('rabbitmq')
|
images.remove("rabbitmq")
|
||||||
return images
|
return images
|
||||||
return [image]
|
return [image]
|
||||||
|
|
||||||
|
96
tutor/k8s.py
96
tutor/k8s.py
@ -14,9 +14,7 @@ def k8s():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(help="Configure and run Open edX from scratch")
|
||||||
help="Configure and run Open edX from scratch"
|
|
||||||
)
|
|
||||||
@opts.root
|
@opts.root
|
||||||
def quickstart(root):
|
def quickstart(root):
|
||||||
click.echo(fmt.title("Interactive platform configuration"))
|
click.echo(fmt.title("Interactive platform configuration"))
|
||||||
@ -25,7 +23,11 @@ def quickstart(root):
|
|||||||
stop.callback()
|
stop.callback()
|
||||||
click.echo(fmt.title("Starting the platform"))
|
click.echo(fmt.title("Starting the platform"))
|
||||||
start.callback(root)
|
start.callback(root)
|
||||||
click.echo(fmt.title("Running migrations. NOTE: this might fail. If it does, please retry 'tutor k8s databases' later"))
|
click.echo(
|
||||||
|
fmt.title(
|
||||||
|
"Running migrations. NOTE: this might fail. If it does, please retry 'tutor k8s databases' later"
|
||||||
|
)
|
||||||
|
)
|
||||||
databases.callback(root)
|
databases.callback(root)
|
||||||
|
|
||||||
|
|
||||||
@ -35,12 +37,42 @@ def start(root):
|
|||||||
config = tutor_config.load(root)
|
config = tutor_config.load(root)
|
||||||
kubectl_no_fail("create", "-f", tutor_env.pathjoin(root, "k8s", "namespace.yml"))
|
kubectl_no_fail("create", "-f", tutor_env.pathjoin(root, "k8s", "namespace.yml"))
|
||||||
|
|
||||||
kubectl("create", "configmap", "nginx-config", "--from-file", tutor_env.pathjoin(root, "apps", "nginx"))
|
kubectl(
|
||||||
if config['ACTIVATE_MYSQL']:
|
"create",
|
||||||
kubectl("create", "configmap", "mysql-config", "--from-env-file", tutor_env.pathjoin(root, "apps", "mysql", "auth.env"))
|
"configmap",
|
||||||
kubectl("create", "configmap", "openedx-settings-lms", "--from-file", tutor_env.pathjoin(root, "apps", "openedx", "settings", "lms"))
|
"nginx-config",
|
||||||
kubectl("create", "configmap", "openedx-settings-cms", "--from-file", tutor_env.pathjoin(root, "apps", "openedx", "settings", "cms"))
|
"--from-file",
|
||||||
kubectl("create", "configmap", "openedx-config", "--from-file", tutor_env.pathjoin(root, "apps", "openedx", "config"))
|
tutor_env.pathjoin(root, "apps", "nginx"),
|
||||||
|
)
|
||||||
|
if config["ACTIVATE_MYSQL"]:
|
||||||
|
kubectl(
|
||||||
|
"create",
|
||||||
|
"configmap",
|
||||||
|
"mysql-config",
|
||||||
|
"--from-env-file",
|
||||||
|
tutor_env.pathjoin(root, "apps", "mysql", "auth.env"),
|
||||||
|
)
|
||||||
|
kubectl(
|
||||||
|
"create",
|
||||||
|
"configmap",
|
||||||
|
"openedx-settings-lms",
|
||||||
|
"--from-file",
|
||||||
|
tutor_env.pathjoin(root, "apps", "openedx", "settings", "lms"),
|
||||||
|
)
|
||||||
|
kubectl(
|
||||||
|
"create",
|
||||||
|
"configmap",
|
||||||
|
"openedx-settings-cms",
|
||||||
|
"--from-file",
|
||||||
|
tutor_env.pathjoin(root, "apps", "openedx", "settings", "cms"),
|
||||||
|
)
|
||||||
|
kubectl(
|
||||||
|
"create",
|
||||||
|
"configmap",
|
||||||
|
"openedx-config",
|
||||||
|
"--from-file",
|
||||||
|
tutor_env.pathjoin(root, "apps", "openedx", "config"),
|
||||||
|
)
|
||||||
|
|
||||||
kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "volumes.yml"))
|
kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "volumes.yml"))
|
||||||
kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "ingress.yml"))
|
kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "ingress.yml"))
|
||||||
@ -57,13 +89,14 @@ def stop():
|
|||||||
@click.option("-y", "--yes", is_flag=True, help="Do not ask for confirmation")
|
@click.option("-y", "--yes", is_flag=True, help="Do not ask for confirmation")
|
||||||
def delete(yes):
|
def delete(yes):
|
||||||
if not yes:
|
if not yes:
|
||||||
click.confirm('Are you sure you want to delete the platform? All data will be removed.', abort=True)
|
click.confirm(
|
||||||
|
"Are you sure you want to delete the platform? All data will be removed.",
|
||||||
|
abort=True,
|
||||||
|
)
|
||||||
kubectl("delete", "namespace", K8s.NAMESPACE)
|
kubectl("delete", "namespace", K8s.NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(help="Create databases and run database migrations")
|
||||||
help="Create databases and run database migrations",
|
|
||||||
)
|
|
||||||
@opts.root
|
@opts.root
|
||||||
def databases(root):
|
def databases(root):
|
||||||
scripts.migrate(root, run_sh)
|
scripts.migrate(root, run_sh)
|
||||||
@ -96,9 +129,7 @@ def indexcourses(root):
|
|||||||
scripts.index_courses(root, run_sh)
|
scripts.index_courses(root, run_sh)
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(help="Launch a shell in LMS or CMS")
|
||||||
help="Launch a shell in LMS or CMS",
|
|
||||||
)
|
|
||||||
@click.argument("service", type=click.Choice(["lms", "cms"]))
|
@click.argument("service", type=click.Choice(["lms", "cms"]))
|
||||||
def shell(service):
|
def shell(service):
|
||||||
K8s().execute(service, "bash")
|
K8s().execute(service, "bash")
|
||||||
@ -121,9 +152,7 @@ def kubectl(*command):
|
|||||||
ignored, to avoid stopping on "AlreadyExists" errors.
|
ignored, to avoid stopping on "AlreadyExists" errors.
|
||||||
"""
|
"""
|
||||||
args = list(command)
|
args = list(command)
|
||||||
args += [
|
args += ["--namespace", K8s.NAMESPACE]
|
||||||
"--namespace", K8s.NAMESPACE
|
|
||||||
]
|
|
||||||
kubectl_no_fail(*args)
|
kubectl_no_fail(*args)
|
||||||
|
|
||||||
|
|
||||||
@ -150,6 +179,7 @@ class K8s:
|
|||||||
if self.CLIENT is None:
|
if self.CLIENT is None:
|
||||||
# Import moved here for performance reasons
|
# Import moved here for performance reasons
|
||||||
import kubernetes
|
import kubernetes
|
||||||
|
|
||||||
kubernetes.config.load_kube_config()
|
kubernetes.config.load_kube_config()
|
||||||
self.CLIENT = kubernetes.client.CoreV1Api()
|
self.CLIENT = kubernetes.client.CoreV1Api()
|
||||||
return self.CLIENT
|
return self.CLIENT
|
||||||
@ -157,23 +187,37 @@ class K8s:
|
|||||||
def pod_name(self, app):
|
def pod_name(self, app):
|
||||||
selector = "app=" + app
|
selector = "app=" + app
|
||||||
try:
|
try:
|
||||||
return self.client.list_namespaced_pod("openedx", label_selector=selector).items[0].metadata.name
|
return (
|
||||||
|
self.client.list_namespaced_pod("openedx", label_selector=selector)
|
||||||
|
.items[0]
|
||||||
|
.metadata.name
|
||||||
|
)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise exceptions.TutorError("Pod with app {} does not exist. Make sure that the pod is running.")
|
raise exceptions.TutorError(
|
||||||
|
"Pod with app {} does not exist. Make sure that the pod is running."
|
||||||
|
)
|
||||||
|
|
||||||
def admin_token(self):
|
def admin_token(self):
|
||||||
# Note: this is a HORRIBLE way of looking for a secret
|
# Note: this is a HORRIBLE way of looking for a secret
|
||||||
try:
|
try:
|
||||||
secret = [
|
secret = [
|
||||||
s for s in self.client.list_namespaced_secret("kube-system").items if s.metadata.name.startswith("admin-user-token")
|
s
|
||||||
|
for s in self.client.list_namespaced_secret("kube-system").items
|
||||||
|
if s.metadata.name.startswith("admin-user-token")
|
||||||
][0]
|
][0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise exceptions.TutorError("Secret 'admin-user-token'. Make sure that admin user was created.")
|
raise exceptions.TutorError(
|
||||||
return self.client.read_namespaced_secret(secret.metadata.name, "kube-system").data["token"]
|
"Secret 'admin-user-token'. Make sure that admin user was created."
|
||||||
|
)
|
||||||
|
return self.client.read_namespaced_secret(
|
||||||
|
secret.metadata.name, "kube-system"
|
||||||
|
).data["token"]
|
||||||
|
|
||||||
def execute(self, app, *command):
|
def execute(self, app, *command):
|
||||||
podname = self.pod_name(app)
|
podname = self.pod_name(app)
|
||||||
kubectl_no_fail("exec", "--namespace", self.NAMESPACE, "-it", podname, "--", *command)
|
kubectl_no_fail(
|
||||||
|
"exec", "--namespace", self.NAMESPACE, "-it", podname, "--", *command
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_sh(root, service, command): # pylint: disable=unused-argument
|
def run_sh(root, service, command): # pylint: disable=unused-argument
|
||||||
|
131
tutor/local.py
131
tutor/local.py
@ -23,7 +23,9 @@ def local():
|
|||||||
|
|
||||||
|
|
||||||
@click.command(help="Configure and run Open edX from scratch")
|
@click.command(help="Configure and run Open edX from scratch")
|
||||||
@click.option("-p", "--pullimages", "pullimages_", is_flag=True, help="Update docker images")
|
@click.option(
|
||||||
|
"-p", "--pullimages", "pullimages_", is_flag=True, help="Update docker images"
|
||||||
|
)
|
||||||
@opts.root
|
@opts.root
|
||||||
def quickstart(pullimages_, root):
|
def quickstart(pullimages_, root):
|
||||||
click.echo(fmt.title("Interactive platform configuration"))
|
click.echo(fmt.title("Interactive platform configuration"))
|
||||||
@ -64,30 +66,38 @@ def start(root, detach):
|
|||||||
http = "https" if config["ACTIVATE_HTTPS"] else "http"
|
http = "https" if config["ACTIVATE_HTTPS"] else "http"
|
||||||
urls = []
|
urls = []
|
||||||
if not config["ACTIVATE_HTTPS"] and not config["WEB_PROXY"]:
|
if not config["ACTIVATE_HTTPS"] and not config["WEB_PROXY"]:
|
||||||
urls += [
|
urls += ["http://localhost", "http://studio.localhost"]
|
||||||
"http://localhost",
|
urls.append(
|
||||||
"http://studio.localhost",
|
"{http}://{lms_host}".format(http=http, lms_host=config["LMS_HOST"])
|
||||||
]
|
)
|
||||||
urls.append("{http}://{lms_host}".format(http=http, lms_host=config["LMS_HOST"]))
|
urls.append(
|
||||||
urls.append("{http}://{cms_host}".format(http=http, cms_host=config["CMS_HOST"]))
|
"{http}://{cms_host}".format(http=http, cms_host=config["CMS_HOST"])
|
||||||
click.echo(fmt.info("""Your Open edX platform is ready and can be accessed at the following urls:
|
)
|
||||||
|
click.echo(
|
||||||
|
fmt.info(
|
||||||
|
"""Your Open edX platform is ready and can be accessed at the following urls:
|
||||||
|
|
||||||
{}""".format("\n ".join(urls))))
|
{}""".format(
|
||||||
|
"\n ".join(urls)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@click.command(help="Stop a running platform",)
|
@click.command(help="Stop a running platform")
|
||||||
@opts.root
|
@opts.root
|
||||||
def stop(root):
|
def stop(root):
|
||||||
config = tutor_config.load(root)
|
config = tutor_config.load(root)
|
||||||
docker_compose(root, config, "rm", "--stop", "--force")
|
docker_compose(root, config, "rm", "--stop", "--force")
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(
|
||||||
help="""Restart some components from a running platform.
|
help="""Restart some components from a running platform.
|
||||||
You may specify 'openedx' to restart the lms, cms and workers, or 'all' to
|
You may specify 'openedx' to restart the lms, cms and workers, or 'all' to
|
||||||
restart all services.""",
|
restart all services."""
|
||||||
)
|
)
|
||||||
@opts.root
|
@opts.root
|
||||||
@click.argument('service')
|
@click.argument("service")
|
||||||
def restart(root, service):
|
def restart(root, service):
|
||||||
config = tutor_config.load(root)
|
config = tutor_config.load(root)
|
||||||
command = ["restart"]
|
command = ["restart"]
|
||||||
@ -110,11 +120,7 @@ def restart(root, service):
|
|||||||
@click.argument("command", default=None, required=False)
|
@click.argument("command", default=None, required=False)
|
||||||
@click.argument("args", nargs=-1, required=False)
|
@click.argument("args", nargs=-1, required=False)
|
||||||
def run(root, service, command, args):
|
def run(root, service, command, args):
|
||||||
run_command = [
|
run_command = ["run", "--rm", service]
|
||||||
"run",
|
|
||||||
"--rm",
|
|
||||||
service
|
|
||||||
]
|
|
||||||
if command:
|
if command:
|
||||||
run_command.append(command)
|
run_command.append(command)
|
||||||
if args:
|
if args:
|
||||||
@ -123,9 +129,7 @@ def run(root, service, command, args):
|
|||||||
docker_compose(root, config, *run_command)
|
docker_compose(root, config, *run_command)
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(help="Create databases and run database migrations")
|
||||||
help="Create databases and run database migrations",
|
|
||||||
)
|
|
||||||
@opts.root
|
@opts.root
|
||||||
def databases(root):
|
def databases(root):
|
||||||
init_mysql(root)
|
init_mysql(root)
|
||||||
@ -134,7 +138,7 @@ def databases(root):
|
|||||||
|
|
||||||
def init_mysql(root):
|
def init_mysql(root):
|
||||||
config = tutor_config.load(root)
|
config = tutor_config.load(root)
|
||||||
if not config['ACTIVATE_MYSQL']:
|
if not config["ACTIVATE_MYSQL"]:
|
||||||
return
|
return
|
||||||
mysql_data_path = tutor_env.data_path(root, "mysql", "mysql")
|
mysql_data_path = tutor_env.data_path(root, "mysql", "mysql")
|
||||||
if os.path.exists(mysql_data_path):
|
if os.path.exists(mysql_data_path):
|
||||||
@ -145,10 +149,17 @@ def init_mysql(root):
|
|||||||
click.echo(fmt.info(" waiting for mysql initialization"))
|
click.echo(fmt.info(" waiting for mysql initialization"))
|
||||||
# TODO this is duplicate code with the docker_compose function. We
|
# TODO this is duplicate code with the docker_compose function. We
|
||||||
# should rely on a dedicated function in utils module.
|
# should rely on a dedicated function in utils module.
|
||||||
mysql_logs = subprocess.check_output([
|
mysql_logs = subprocess.check_output(
|
||||||
"docker-compose", "-f", tutor_env.pathjoin(root, "local", "docker-compose.yml"),
|
[
|
||||||
"--project-name", config["LOCAL_PROJECT_NAME"], "logs", "mysql",
|
"docker-compose",
|
||||||
])
|
"-f",
|
||||||
|
tutor_env.pathjoin(root, "local", "docker-compose.yml"),
|
||||||
|
"--project-name",
|
||||||
|
config["LOCAL_PROJECT_NAME"],
|
||||||
|
"logs",
|
||||||
|
"mysql",
|
||||||
|
]
|
||||||
|
)
|
||||||
# pylint: disable=unsupported-membership-test
|
# pylint: disable=unsupported-membership-test
|
||||||
if b"MySQL init process done. Ready for start up." in mysql_logs:
|
if b"MySQL init process done. Ready for start up." in mysql_logs:
|
||||||
click.echo(fmt.info("MySQL database initialized"))
|
click.echo(fmt.info("MySQL database initialized"))
|
||||||
@ -173,28 +184,38 @@ def https_create(root):
|
|||||||
2. On certificate renewal, nginx is not reloaded
|
2. On certificate renewal, nginx is not reloaded
|
||||||
"""
|
"""
|
||||||
config = tutor_config.load(root)
|
config = tutor_config.load(root)
|
||||||
if not config['ACTIVATE_HTTPS']:
|
if not config["ACTIVATE_HTTPS"]:
|
||||||
click.echo(fmt.info("HTTPS is not activated: certificate generation skipped"))
|
click.echo(fmt.info("HTTPS is not activated: certificate generation skipped"))
|
||||||
return
|
return
|
||||||
|
|
||||||
script = scripts.render_template(config, 'https_create.sh')
|
script = scripts.render_template(config, "https_create.sh")
|
||||||
|
|
||||||
if config['WEB_PROXY']:
|
if config["WEB_PROXY"]:
|
||||||
click.echo(fmt.info("""You are running Tutor behind a web proxy (WEB_PROXY=true): SSL/TLS
|
click.echo(
|
||||||
|
fmt.info(
|
||||||
|
"""You are running Tutor behind a web proxy (WEB_PROXY=true): SSL/TLS
|
||||||
certificates must be generated on the host. For instance, to generate
|
certificates must be generated on the host. For instance, to generate
|
||||||
certificates with Let's Encrypt, run:
|
certificates with Let's Encrypt, run:
|
||||||
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
See the official certbot documentation for your platform: https://certbot.eff.org/""".format(indent(script, " "))))
|
See the official certbot documentation for your platform: https://certbot.eff.org/""".format(
|
||||||
|
indent(script, " ")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
utils.docker_run(
|
utils.docker_run(
|
||||||
"--volume", "{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")),
|
"--volume",
|
||||||
"-p", "80:80",
|
"{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")),
|
||||||
|
"-p",
|
||||||
|
"80:80",
|
||||||
"--entrypoint=sh",
|
"--entrypoint=sh",
|
||||||
"certbot/certbot:latest",
|
"certbot/certbot:latest",
|
||||||
"-e", "-c", script,
|
"-e",
|
||||||
|
"-c",
|
||||||
|
script,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -202,22 +223,29 @@ See the official certbot documentation for your platform: https://certbot.eff.or
|
|||||||
@opts.root
|
@opts.root
|
||||||
def https_renew(root):
|
def https_renew(root):
|
||||||
config = tutor_config.load(root)
|
config = tutor_config.load(root)
|
||||||
if not config['ACTIVATE_HTTPS']:
|
if not config["ACTIVATE_HTTPS"]:
|
||||||
click.echo(fmt.info("HTTPS is not activated: certificate renewal skipped"))
|
click.echo(fmt.info("HTTPS is not activated: certificate renewal skipped"))
|
||||||
return
|
return
|
||||||
if config['WEB_PROXY']:
|
if config["WEB_PROXY"]:
|
||||||
click.echo(fmt.info("""You are running Tutor behind a web proxy (WEB_PROXY=true): SSL/TLS
|
click.echo(
|
||||||
|
fmt.info(
|
||||||
|
"""You are running Tutor behind a web proxy (WEB_PROXY=true): SSL/TLS
|
||||||
certificates must be renewed on the host. For instance, to renew Let's Encrypt
|
certificates must be renewed on the host. For instance, to renew Let's Encrypt
|
||||||
certificates, run:
|
certificates, run:
|
||||||
|
|
||||||
certbot renew
|
certbot renew
|
||||||
|
|
||||||
See the official certbot documentation for your platform: https://certbot.eff.org/"""))
|
See the official certbot documentation for your platform: https://certbot.eff.org/"""
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
docker_run = [
|
docker_run = [
|
||||||
"--volume", "{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")),
|
"--volume",
|
||||||
"-p", "80:80",
|
"{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")),
|
||||||
"certbot/certbot:latest", "renew"
|
"-p",
|
||||||
|
"80:80",
|
||||||
|
"certbot/certbot:latest",
|
||||||
|
"renew",
|
||||||
]
|
]
|
||||||
utils.docker_run(*docker_run)
|
utils.docker_run(*docker_run)
|
||||||
|
|
||||||
@ -274,22 +302,31 @@ def indexcourses(root):
|
|||||||
short_help="Run Portainer, a UI for container supervision",
|
short_help="Run Portainer, a UI for container supervision",
|
||||||
)
|
)
|
||||||
@opts.root
|
@opts.root
|
||||||
@click.option("-p", "--port", type=int, default=9000, show_default=True, help="Bind port")
|
@click.option(
|
||||||
|
"-p", "--port", type=int, default=9000, show_default=True, help="Bind port"
|
||||||
|
)
|
||||||
def portainer(root, port):
|
def portainer(root, port):
|
||||||
docker_run = [
|
docker_run = [
|
||||||
"--volume=/var/run/docker.sock:/var/run/docker.sock",
|
"--volume=/var/run/docker.sock:/var/run/docker.sock",
|
||||||
"--volume={}:/data".format(tutor_env.data_path(root, "portainer")),
|
"--volume={}:/data".format(tutor_env.data_path(root, "portainer")),
|
||||||
"-p", "{port}:{port}".format(port=port),
|
"-p",
|
||||||
|
"{port}:{port}".format(port=port),
|
||||||
"portainer/portainer:latest",
|
"portainer/portainer:latest",
|
||||||
"--bind=:{}".format(port),
|
"--bind=:{}".format(port),
|
||||||
]
|
]
|
||||||
click.echo(fmt.info("View the Portainer UI at http://localhost:{port}".format(port=port)))
|
click.echo(
|
||||||
|
fmt.info("View the Portainer UI at http://localhost:{port}".format(port=port))
|
||||||
|
)
|
||||||
utils.docker_run(*docker_run)
|
utils.docker_run(*docker_run)
|
||||||
|
|
||||||
|
|
||||||
def check_service_is_activated(config, service):
|
def check_service_is_activated(config, service):
|
||||||
if not config["ACTIVATE_" + service.upper()]:
|
if not config["ACTIVATE_" + service.upper()]:
|
||||||
raise exceptions.TutorError("This command may only be executed on the server where the {} is running".format(service))
|
raise exceptions.TutorError(
|
||||||
|
"This command may only be executed on the server where the {} is running".format(
|
||||||
|
service
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_sh(root, service, command):
|
def run_sh(root, service, command):
|
||||||
@ -299,8 +336,10 @@ def run_sh(root, service, command):
|
|||||||
|
|
||||||
def docker_compose(root, config, *command):
|
def docker_compose(root, config, *command):
|
||||||
return utils.docker_compose(
|
return utils.docker_compose(
|
||||||
"-f", tutor_env.pathjoin(root, "local", "docker-compose.yml"),
|
"-f",
|
||||||
"--project-name", config["LOCAL_PROJECT_NAME"],
|
tutor_env.pathjoin(root, "local", "docker-compose.yml"),
|
||||||
|
"--project-name",
|
||||||
|
config["LOCAL_PROJECT_NAME"],
|
||||||
*command
|
*command
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,27 +5,32 @@ from . import serialize
|
|||||||
|
|
||||||
|
|
||||||
root = click.option(
|
root = click.option(
|
||||||
"-r", "--root",
|
"-r",
|
||||||
|
"--root",
|
||||||
envvar="TUTOR_ROOT",
|
envvar="TUTOR_ROOT",
|
||||||
default=appdirs.user_data_dir(appname="tutor"), show_default=True,
|
default=appdirs.user_data_dir(appname="tutor"),
|
||||||
|
show_default=True,
|
||||||
type=click.Path(resolve_path=True),
|
type=click.Path(resolve_path=True),
|
||||||
help="Root project directory (environment variable: TUTOR_ROOT)"
|
help="Root project directory (environment variable: TUTOR_ROOT)",
|
||||||
)
|
)
|
||||||
|
|
||||||
edx_platform_path = click.option(
|
edx_platform_path = click.option(
|
||||||
"-P", "--edx-platform-path",
|
"-P",
|
||||||
|
"--edx-platform-path",
|
||||||
envvar="TUTOR_EDX_PLATFORM_PATH",
|
envvar="TUTOR_EDX_PLATFORM_PATH",
|
||||||
type=click.Path(exists=True, dir_okay=True, resolve_path=True),
|
type=click.Path(exists=True, dir_okay=True, resolve_path=True),
|
||||||
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)"
|
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)",
|
||||||
)
|
)
|
||||||
|
|
||||||
edx_platform_settings = click.option(
|
edx_platform_settings = click.option(
|
||||||
"-S", "--edx-platform-settings",
|
"-S",
|
||||||
|
"--edx-platform-settings",
|
||||||
envvar="TUTOR_EDX_PLATFORM_SETTINGS",
|
envvar="TUTOR_EDX_PLATFORM_SETTINGS",
|
||||||
default="tutor.development",
|
default="tutor.development",
|
||||||
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)"
|
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class YamlParamType(click.ParamType):
|
class YamlParamType(click.ParamType):
|
||||||
name = "yaml"
|
name = "yaml"
|
||||||
|
|
||||||
@ -36,7 +41,13 @@ class YamlParamType(click.ParamType):
|
|||||||
self.fail("'{}' is not of the form 'key=value'.".format(value), param, ctx)
|
self.fail("'{}' is not of the form 'key=value'.".format(value), param, ctx)
|
||||||
return k, serialize.parse_value(v)
|
return k, serialize.parse_value(v)
|
||||||
|
|
||||||
|
|
||||||
key_value = click.option(
|
key_value = click.option(
|
||||||
"-s", "--set", "set_", type=YamlParamType(), multiple=True, metavar="KEY=VAL",
|
"-s",
|
||||||
help="Set a configuration value (can be used multiple times)"
|
"--set",
|
||||||
|
"set_",
|
||||||
|
type=YamlParamType(),
|
||||||
|
multiple=True,
|
||||||
|
metavar="KEY=VAL",
|
||||||
|
help="Set a configuration value (can be used multiple times)",
|
||||||
)
|
)
|
||||||
|
@ -4,6 +4,7 @@ from . import config as tutor_config
|
|||||||
from . import env
|
from . import env
|
||||||
from . import fmt
|
from . import fmt
|
||||||
|
|
||||||
|
|
||||||
def migrate(root, run_func):
|
def migrate(root, run_func):
|
||||||
config = tutor_config.load(root)
|
config = tutor_config.load(root)
|
||||||
|
|
||||||
@ -31,29 +32,30 @@ def migrate(root, run_func):
|
|||||||
run_template(root, config, "lms", "oauth2.sh", run_func)
|
run_template(root, config, "lms", "oauth2.sh", run_func)
|
||||||
click.echo(fmt.info("Databases ready."))
|
click.echo(fmt.info("Databases ready."))
|
||||||
|
|
||||||
|
|
||||||
def create_user(root, run_func, superuser, staff, name, email):
|
def create_user(root, run_func, superuser, staff, name, email):
|
||||||
config = {
|
config = {"OPTS": "", "USERNAME": name, "EMAIL": email}
|
||||||
"OPTS": "",
|
|
||||||
"USERNAME": name,
|
|
||||||
"EMAIL": email,
|
|
||||||
}
|
|
||||||
if superuser:
|
if superuser:
|
||||||
config["OPTS"] += " --superuser"
|
config["OPTS"] += " --superuser"
|
||||||
if staff:
|
if staff:
|
||||||
config["OPTS"] += " --staff"
|
config["OPTS"] += " --staff"
|
||||||
run_template(root, config, "lms", "create_user.sh", run_func)
|
run_template(root, config, "lms", "create_user.sh", run_func)
|
||||||
|
|
||||||
|
|
||||||
def import_demo_course(root, run_func):
|
def import_demo_course(root, run_func):
|
||||||
run_template(root, {}, "cms", "import_demo_course.sh", run_func)
|
run_template(root, {}, "cms", "import_demo_course.sh", run_func)
|
||||||
|
|
||||||
|
|
||||||
def index_courses(root, run_func):
|
def index_courses(root, run_func):
|
||||||
run_template(root, {}, "cms", "index_courses.sh", run_func)
|
run_template(root, {}, "cms", "index_courses.sh", run_func)
|
||||||
|
|
||||||
|
|
||||||
def run_template(root, config, service, template, run_func):
|
def run_template(root, config, service, template, run_func):
|
||||||
command = render_template(config, template)
|
command = render_template(config, template)
|
||||||
if command:
|
if command:
|
||||||
run_func(root, service, command)
|
run_func(root, service, command)
|
||||||
|
|
||||||
|
|
||||||
def render_template(config, template):
|
def render_template(config, template):
|
||||||
path = env.template_path("scripts", template)
|
path = env.template_path("scripts", template)
|
||||||
return env.render_file(config, path).strip()
|
return env.render_file(config, path).strip()
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
def load(stream):
|
def load(stream):
|
||||||
return yaml.load(stream, Loader=yaml.SafeLoader)
|
return yaml.load(stream, Loader=yaml.SafeLoader)
|
||||||
|
|
||||||
|
|
||||||
def dump(content, fileobj):
|
def dump(content, fileobj):
|
||||||
yaml.dump(content, fileobj, default_flow_style=False)
|
yaml.dump(content, fileobj, default_flow_style=False)
|
||||||
|
|
||||||
|
|
||||||
def parse_value(v):
|
def parse_value(v):
|
||||||
"""
|
"""
|
||||||
Parse a yaml-formatted string. This is fairly basic and should only be used
|
Parse a yaml-formatted string. This is fairly basic and should only be used
|
||||||
@ -16,5 +19,5 @@ def parse_value(v):
|
|||||||
elif v == "null":
|
elif v == "null":
|
||||||
v = None
|
v = None
|
||||||
elif v in ["true", "false"]:
|
elif v in ["true", "false"]:
|
||||||
v = (v == "true")
|
v = v == "true"
|
||||||
return v
|
return v
|
||||||
|
@ -1,28 +1,34 @@
|
|||||||
from .common import *
|
from .common import *
|
||||||
|
|
||||||
SECRET_KEY = '{{ NOTES_SECRET_KEY }}'
|
SECRET_KEY = "{{ NOTES_SECRET_KEY }}"
|
||||||
ALLOWED_HOSTS = ['localhost', 'notes', 'notes.openedx', 'notes.localhost', 'notes.{{ LMS_HOST }}']
|
ALLOWED_HOSTS = [
|
||||||
|
"localhost",
|
||||||
|
"notes",
|
||||||
|
"notes.openedx",
|
||||||
|
"notes.localhost",
|
||||||
|
"notes.{{ LMS_HOST }}",
|
||||||
|
]
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
"ENGINE": "django.db.backends.mysql",
|
||||||
'HOST': '{{ MYSQL_HOST }}',
|
"HOST": "{{ MYSQL_HOST }}",
|
||||||
'PORT': {{ MYSQL_PORT }},
|
"PORT": {{MYSQL_PORT}},
|
||||||
'NAME': '{{ NOTES_MYSQL_DATABASE }}',
|
"NAME": "{{ NOTES_MYSQL_DATABASE }}",
|
||||||
'USER': '{{ NOTES_MYSQL_USERNAME }}',
|
"USER": "{{ NOTES_MYSQL_USERNAME }}",
|
||||||
'PASSWORD': '{{ NOTES_MYSQL_PASSWORD }}',
|
"PASSWORD": "{{ NOTES_MYSQL_PASSWORD }}",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CLIENT_ID = 'notes'
|
CLIENT_ID = "notes"
|
||||||
CLIENT_SECRET = '{{ NOTES_OAUTH2_SECRET }}'
|
CLIENT_SECRET = "{{ NOTES_OAUTH2_SECRET }}"
|
||||||
|
|
||||||
HAYSTACK_CONNECTIONS = {
|
HAYSTACK_CONNECTIONS = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': 'notesserver.highlight.ElasticsearchSearchEngine',
|
"ENGINE": "notesserver.highlight.ElasticsearchSearchEngine",
|
||||||
'URL': 'http://{{ ELASTICSEARCH_HOST }}:{{ ELASTICSEARCH_PORT }}/',
|
"URL": "http://{{ ELASTICSEARCH_HOST }}:{{ ELASTICSEARCH_PORT }}/",
|
||||||
'INDEX_NAME': 'notes',
|
"INDEX_NAME": "notes",
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGING['handlers']['local'] = LOGGING['handlers']['console'].copy()
|
LOGGING["handlers"]["local"] = LOGGING["handlers"]["console"].copy()
|
||||||
|
@ -5,18 +5,18 @@ update_module_store_settings(MODULESTORE, doc_store_settings=DOC_STORE_CONFIG)
|
|||||||
MEDIA_ROOT = "/openedx/data/uploads/"
|
MEDIA_ROOT = "/openedx/data/uploads/"
|
||||||
|
|
||||||
# Video settings
|
# Video settings
|
||||||
VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['location'] = MEDIA_ROOT
|
VIDEO_IMAGE_SETTINGS["STORAGE_KWARGS"]["location"] = MEDIA_ROOT
|
||||||
VIDEO_TRANSCRIPTS_SETTINGS['STORAGE_KWARGS']['location'] = MEDIA_ROOT
|
VIDEO_TRANSCRIPTS_SETTINGS["STORAGE_KWARGS"]["location"] = MEDIA_ROOT
|
||||||
|
|
||||||
# Change syslog-based loggers which don't work inside docker containers
|
# Change syslog-based loggers which don't work inside docker containers
|
||||||
LOGGING['handlers']['local'] = {'class': 'logging.NullHandler'}
|
LOGGING["handlers"]["local"] = {"class": "logging.NullHandler"}
|
||||||
LOGGING['handlers']['tracking'] = {
|
LOGGING["handlers"]["tracking"] = {
|
||||||
'level': 'DEBUG',
|
"level": "DEBUG",
|
||||||
'class': 'logging.StreamHandler',
|
"class": "logging.StreamHandler",
|
||||||
'formatter': 'standard',
|
"formatter": "standard",
|
||||||
}
|
}
|
||||||
|
|
||||||
LOCALE_PATHS.append('/openedx/locale')
|
LOCALE_PATHS.append("/openedx/locale")
|
||||||
|
|
||||||
# Create folders if necessary
|
# Create folders if necessary
|
||||||
for folder in [LOG_DIR, MEDIA_ROOT, STATIC_ROOT_BASE]:
|
for folder in [LOG_DIR, MEDIA_ROOT, STATIC_ROOT_BASE]:
|
||||||
|
@ -3,7 +3,7 @@ from cms.envs.devstack import *
|
|||||||
|
|
||||||
|
|
||||||
# Execute the contents of common.py in this context
|
# Execute the contents of common.py in this context
|
||||||
execfile(os.path.join(os.path.dirname(__file__), 'common.py'), globals())
|
execfile(os.path.join(os.path.dirname(__file__), "common.py"), globals())
|
||||||
|
|
||||||
# Setup correct webpack configuration file for development
|
# Setup correct webpack configuration file for development
|
||||||
WEBPACK_CONFIG_PATH = 'webpack.dev.config.js'
|
WEBPACK_CONFIG_PATH = "webpack.dev.config.js"
|
||||||
|
@ -3,15 +3,19 @@ from cms.envs.production import *
|
|||||||
|
|
||||||
|
|
||||||
# Execute the contents of common.py in this context
|
# Execute the contents of common.py in this context
|
||||||
execfile(os.path.join(os.path.dirname(__file__), 'common.py'), globals())
|
execfile(os.path.join(os.path.dirname(__file__), "common.py"), globals())
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = [
|
||||||
ENV_TOKENS.get('CMS_BASE'),
|
ENV_TOKENS.get("CMS_BASE"),
|
||||||
'127.0.0.1', 'localhost', 'studio.localhost',
|
"127.0.0.1",
|
||||||
'127.0.0.1:8000', 'localhost:8000',
|
"localhost",
|
||||||
'127.0.0.1:8001', 'localhost:8001',
|
"studio.localhost",
|
||||||
|
"127.0.0.1:8000",
|
||||||
|
"localhost:8000",
|
||||||
|
"127.0.0.1:8001",
|
||||||
|
"localhost:8001",
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_FROM_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
DEFAULT_FROM_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
SERVER_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
SERVER_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
|
@ -7,36 +7,38 @@ update_module_store_settings(MODULESTORE, doc_store_settings=DOC_STORE_CONFIG)
|
|||||||
MEDIA_ROOT = "/openedx/data/uploads/"
|
MEDIA_ROOT = "/openedx/data/uploads/"
|
||||||
|
|
||||||
# Video settings
|
# Video settings
|
||||||
VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['location'] = MEDIA_ROOT
|
VIDEO_IMAGE_SETTINGS["STORAGE_KWARGS"]["location"] = MEDIA_ROOT
|
||||||
VIDEO_TRANSCRIPTS_SETTINGS['STORAGE_KWARGS']['location'] = MEDIA_ROOT
|
VIDEO_TRANSCRIPTS_SETTINGS["STORAGE_KWARGS"]["location"] = MEDIA_ROOT
|
||||||
|
|
||||||
# Change syslog-based loggers which don't work inside docker containers
|
# Change syslog-based loggers which don't work inside docker containers
|
||||||
LOGGING['handlers']['local'] = {'class': 'logging.NullHandler'}
|
LOGGING["handlers"]["local"] = {"class": "logging.NullHandler"}
|
||||||
LOGGING['handlers']['tracking'] = {
|
LOGGING["handlers"]["tracking"] = {
|
||||||
'level': 'DEBUG',
|
"level": "DEBUG",
|
||||||
'class': 'logging.StreamHandler',
|
"class": "logging.StreamHandler",
|
||||||
'formatter': 'standard',
|
"formatter": "standard",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fix media files paths
|
# Fix media files paths
|
||||||
VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['location'] = MEDIA_ROOT
|
VIDEO_IMAGE_SETTINGS["STORAGE_KWARGS"]["location"] = MEDIA_ROOT
|
||||||
VIDEO_TRANSCRIPTS_SETTINGS['STORAGE_KWARGS']['location'] = MEDIA_ROOT
|
VIDEO_TRANSCRIPTS_SETTINGS["STORAGE_KWARGS"]["location"] = MEDIA_ROOT
|
||||||
PROFILE_IMAGE_BACKEND['options']['location'] = os.path.join(MEDIA_ROOT, 'profile-images/')
|
PROFILE_IMAGE_BACKEND["options"]["location"] = os.path.join(
|
||||||
|
MEDIA_ROOT, "profile-images/"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ORA2_FILEUPLOAD_BACKEND = 'filesystem'
|
ORA2_FILEUPLOAD_BACKEND = "filesystem"
|
||||||
ORA2_FILEUPLOAD_ROOT = '/openedx/data/ora2'
|
ORA2_FILEUPLOAD_ROOT = "/openedx/data/ora2"
|
||||||
ORA2_FILEUPLOAD_CACHE_NAME = 'ora2-storage'
|
ORA2_FILEUPLOAD_CACHE_NAME = "ora2-storage"
|
||||||
|
|
||||||
GRADES_DOWNLOAD = {
|
GRADES_DOWNLOAD = {
|
||||||
'STORAGE_TYPE': '',
|
"STORAGE_TYPE": "",
|
||||||
'STORAGE_KWARGS': {
|
"STORAGE_KWARGS": {
|
||||||
'base_url': "/media/grades/",
|
"base_url": "/media/grades/",
|
||||||
'location': os.path.join(MEDIA_ROOT, 'grades'),
|
"location": os.path.join(MEDIA_ROOT, "grades"),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
LOCALE_PATHS.append('/openedx/locale')
|
LOCALE_PATHS.append("/openedx/locale")
|
||||||
|
|
||||||
# Create folders if necessary
|
# Create folders if necessary
|
||||||
for folder in [LOG_DIR, MEDIA_ROOT, STATIC_ROOT_BASE, ORA2_FILEUPLOAD_ROOT]:
|
for folder in [LOG_DIR, MEDIA_ROOT, STATIC_ROOT_BASE, ORA2_FILEUPLOAD_ROOT]:
|
||||||
|
@ -3,8 +3,8 @@ from lms.envs.devstack import *
|
|||||||
|
|
||||||
|
|
||||||
# Execute the contents of common.py in this context
|
# Execute the contents of common.py in this context
|
||||||
execfile(os.path.join(os.path.dirname(__file__), 'common.py'), globals())
|
execfile(os.path.join(os.path.dirname(__file__), "common.py"), globals())
|
||||||
|
|
||||||
|
|
||||||
# Setup correct webpack configuration file for development
|
# Setup correct webpack configuration file for development
|
||||||
WEBPACK_CONFIG_PATH = 'webpack.dev.config.js'
|
WEBPACK_CONFIG_PATH = "webpack.dev.config.js"
|
||||||
|
@ -3,13 +3,17 @@ from lms.envs.production import *
|
|||||||
|
|
||||||
|
|
||||||
# Execute the contents of common.py in this context
|
# Execute the contents of common.py in this context
|
||||||
execfile(os.path.join(os.path.dirname(__file__), 'common.py'), globals())
|
execfile(os.path.join(os.path.dirname(__file__), "common.py"), globals())
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = [
|
||||||
ENV_TOKENS.get('LMS_BASE'),
|
ENV_TOKENS.get("LMS_BASE"),
|
||||||
FEATURES['PREVIEW_LMS_BASE'],
|
FEATURES["PREVIEW_LMS_BASE"],
|
||||||
'127.0.0.1', 'localhost', 'preview.localhost',
|
"127.0.0.1",
|
||||||
'127.0.0.1:8000', 'localhost:8000', 'preview.localhost:8000',
|
"localhost",
|
||||||
|
"preview.localhost",
|
||||||
|
"127.0.0.1:8000",
|
||||||
|
"localhost:8000",
|
||||||
|
"preview.localhost:8000",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Required to display all courses on start page
|
# Required to display all courses on start page
|
||||||
@ -18,15 +22,15 @@ SEARCH_SKIP_ENROLLMENT_START_DATE_FILTERING = True
|
|||||||
# Allow insecure oauth2 for local interaction with local containers
|
# Allow insecure oauth2 for local interaction with local containers
|
||||||
OAUTH_ENFORCE_SECURE = False
|
OAUTH_ENFORCE_SECURE = False
|
||||||
|
|
||||||
DEFAULT_FROM_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
DEFAULT_FROM_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
SERVER_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
SERVER_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
TECH_SUPPORT_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
TECH_SUPPORT_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
CONTACT_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
CONTACT_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
BUGS_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
BUGS_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
UNIVERSITY_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
UNIVERSITY_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
PRESS_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
PRESS_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
PAYMENT_SUPPORT_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
PAYMENT_SUPPORT_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
BULK_EMAIL_DEFAULT_FROM_EMAIL = 'no-reply@' + ENV_TOKENS['LMS_BASE']
|
BULK_EMAIL_DEFAULT_FROM_EMAIL = "no-reply@" + ENV_TOKENS["LMS_BASE"]
|
||||||
API_ACCESS_MANAGER_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
API_ACCESS_MANAGER_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
API_ACCESS_FROM_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
|
API_ACCESS_FROM_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
|
||||||
|
@ -1,24 +1,18 @@
|
|||||||
from .settings import *
|
from .settings import *
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
"ENGINE": "django.db.backends.mysql",
|
||||||
'HOST': '{{ MYSQL_HOST }}',
|
"HOST": "{{ MYSQL_HOST }}",
|
||||||
'PORT': {{ MYSQL_PORT }},
|
"PORT": {{MYSQL_PORT}},
|
||||||
'NAME': '{{ XQUEUE_MYSQL_DATABASE }}',
|
"NAME": "{{ XQUEUE_MYSQL_DATABASE }}",
|
||||||
'USER': '{{ XQUEUE_MYSQL_USERNAME }}',
|
"USER": "{{ XQUEUE_MYSQL_USERNAME }}",
|
||||||
'PASSWORD': '{{ XQUEUE_MYSQL_PASSWORD }}',
|
"PASSWORD": "{{ XQUEUE_MYSQL_PASSWORD }}",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGING = get_logger_config(
|
LOGGING = get_logger_config(log_dir="/openedx/data/", logging_env="tutor", dev_env=True)
|
||||||
log_dir="/openedx/data/",
|
|
||||||
logging_env="tutor",
|
|
||||||
dev_env=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
SECRET_KEY = '{{ XQUEUE_SECRET_KEY }}'
|
SECRET_KEY = "{{ XQUEUE_SECRET_KEY }}"
|
||||||
|
|
||||||
XQUEUE_USERS = {
|
XQUEUE_USERS = {"{{ XQUEUE_AUTH_USERNAME }}": "{{ XQUEUE_AUTH_PASSWORD}}"}
|
||||||
'{{ XQUEUE_AUTH_USERNAME }}': '{{ XQUEUE_AUTH_PASSWORD}}'
|
|
||||||
}
|
|
||||||
|
@ -4,13 +4,15 @@ import click_repl
|
|||||||
|
|
||||||
@click.command(
|
@click.command(
|
||||||
short_help="Interactive shell",
|
short_help="Interactive shell",
|
||||||
help="Launch an interactive shell for launching Tutor commands"
|
help="Launch an interactive shell for launching Tutor commands",
|
||||||
)
|
)
|
||||||
def ui():
|
def ui():
|
||||||
click.echo("""Welcome to the Tutor interactive shell UI!
|
click.echo(
|
||||||
|
"""Welcome to the Tutor interactive shell UI!
|
||||||
Type "help" to view all available commands.
|
Type "help" to view all available commands.
|
||||||
Type "local quickstart" to configure and launch a new platform from scratch.
|
Type "local quickstart" to configure and launch a new platform from scratch.
|
||||||
Type <ctrl-d> to exit.""")
|
Type <ctrl-d> to exit."""
|
||||||
|
)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
click_repl.repl(click.get_current_context())
|
click_repl.repl(click.get_current_context())
|
||||||
|
@ -10,7 +10,9 @@ from . import fmt
|
|||||||
|
|
||||||
|
|
||||||
def random_string(length):
|
def random_string(length):
|
||||||
return "".join([random.choice(string.ascii_letters + string.digits) for _ in range(length)])
|
return "".join(
|
||||||
|
[random.choice(string.ascii_letters + string.digits) for _ in range(length)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def common_domain(d1, d2):
|
def common_domain(d1, d2):
|
||||||
@ -36,13 +38,17 @@ def docker_run(*command):
|
|||||||
|
|
||||||
def docker(*command):
|
def docker(*command):
|
||||||
if shutil.which("docker") is None:
|
if shutil.which("docker") is None:
|
||||||
raise exceptions.TutorError("docker is not installed. Please follow instructions from https://docs.docker.com/install/")
|
raise exceptions.TutorError(
|
||||||
|
"docker is not installed. Please follow instructions from https://docs.docker.com/install/"
|
||||||
|
)
|
||||||
return execute("docker", *command)
|
return execute("docker", *command)
|
||||||
|
|
||||||
|
|
||||||
def docker_compose(*command):
|
def docker_compose(*command):
|
||||||
if shutil.which("docker-compose") is None:
|
if shutil.which("docker-compose") is None:
|
||||||
raise exceptions.TutorError("docker-compose is not installed. Please follow instructions from https://docs.docker.com/compose/install/")
|
raise exceptions.TutorError(
|
||||||
|
"docker-compose is not installed. Please follow instructions from https://docs.docker.com/compose/install/"
|
||||||
|
)
|
||||||
return execute("docker-compose", *command)
|
return execute("docker-compose", *command)
|
||||||
|
|
||||||
|
|
||||||
@ -66,11 +72,8 @@ def execute(*command):
|
|||||||
except Exception:
|
except Exception:
|
||||||
p.kill()
|
p.kill()
|
||||||
p.wait()
|
p.wait()
|
||||||
raise exceptions.TutorError("Command failed: {}".format(
|
raise exceptions.TutorError("Command failed: {}".format(" ".join(command)))
|
||||||
" ".join(command)
|
|
||||||
))
|
|
||||||
if result > 0:
|
if result > 0:
|
||||||
raise exceptions.TutorError("Command failed with status {}: {}".format(
|
raise exceptions.TutorError(
|
||||||
result,
|
"Command failed with status {}: {}".format(result, " ".join(command))
|
||||||
" ".join(command)
|
)
|
||||||
))
|
|
||||||
|
@ -15,24 +15,26 @@ from . import opts
|
|||||||
from . import env as tutor_env
|
from . import env as tutor_env
|
||||||
from . import serialize
|
from . import serialize
|
||||||
|
|
||||||
|
|
||||||
@click.group(
|
@click.group(
|
||||||
short_help="Web user interface",
|
short_help="Web user interface", help="""Run Tutor commands from a web terminal"""
|
||||||
help="""Run Tutor commands from a web terminal"""
|
|
||||||
)
|
)
|
||||||
def webui():
|
def webui():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@click.command(
|
|
||||||
help="Start the web UI",
|
@click.command(help="Start the web UI")
|
||||||
)
|
|
||||||
@opts.root
|
@opts.root
|
||||||
@click.option(
|
@click.option(
|
||||||
"-p", "--port", default=3737, type=int, show_default=True,
|
"-p",
|
||||||
|
"--port",
|
||||||
|
default=3737,
|
||||||
|
type=int,
|
||||||
|
show_default=True,
|
||||||
help="Port number to listen",
|
help="Port number to listen",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"-h", "--host", default="0.0.0.0", show_default=True,
|
"-h", "--host", default="0.0.0.0", show_default=True, help="Host address to listen"
|
||||||
help="Host address to listen",
|
|
||||||
)
|
)
|
||||||
def start(root, port, host):
|
def start(root, port, host):
|
||||||
check_gotty_binary(root)
|
check_gotty_binary(root)
|
||||||
@ -42,15 +44,24 @@ def start(root, port, host):
|
|||||||
user = config["user"]
|
user = config["user"]
|
||||||
password = config["password"]
|
password = config["password"]
|
||||||
command = [
|
command = [
|
||||||
gotty_path(root), "--permit-write",
|
gotty_path(root),
|
||||||
"--address", host, "--port", str(port),
|
"--permit-write",
|
||||||
"--title-format", "Tutor web UI - {{ .Command }} ({{ .Hostname }})",
|
"--address",
|
||||||
|
host,
|
||||||
|
"--port",
|
||||||
|
str(port),
|
||||||
|
"--title-format",
|
||||||
|
"Tutor web UI - {{ .Command }} ({{ .Hostname }})",
|
||||||
]
|
]
|
||||||
if user and password:
|
if user and password:
|
||||||
credential = "{}:{}".format(user, password)
|
credential = "{}:{}".format(user, password)
|
||||||
command += ["--credential", credential]
|
command += ["--credential", credential]
|
||||||
else:
|
else:
|
||||||
click.echo(fmt.alert("Running web UI without user authentication. Run 'tutor webui configure' to setup authentication"))
|
click.echo(
|
||||||
|
fmt.alert(
|
||||||
|
"Running web UI without user authentication. Run 'tutor webui configure' to setup authentication"
|
||||||
|
)
|
||||||
|
)
|
||||||
command += [sys.argv[0], "ui"]
|
command += [sys.argv[0], "ui"]
|
||||||
p = subprocess.Popen(command)
|
p = subprocess.Popen(command)
|
||||||
while True:
|
while True:
|
||||||
@ -59,29 +70,35 @@ def start(root, port, host):
|
|||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
new_config = load_config(root)
|
new_config = load_config(root)
|
||||||
if new_config != config:
|
if new_config != config:
|
||||||
click.echo("WARNING configuration changed. Tutor web UI is now going to restart. Reload this page to continue.")
|
click.echo(
|
||||||
|
"WARNING configuration changed. Tutor web UI is now going to restart. Reload this page to continue."
|
||||||
|
)
|
||||||
p.kill()
|
p.kill()
|
||||||
p.wait()
|
p.wait()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
@click.command(help="Configure authentication")
|
@click.command(help="Configure authentication")
|
||||||
@opts.root
|
@opts.root
|
||||||
@click.option("-u", "--user", prompt="User name", help="Authentication user name")
|
@click.option("-u", "--user", prompt="User name", help="Authentication user name")
|
||||||
@click.option(
|
@click.option(
|
||||||
"-p", "--password",
|
"-p",
|
||||||
prompt=True, hide_input=True, confirmation_prompt=True,
|
"--password",
|
||||||
help="Authentication password"
|
prompt=True,
|
||||||
|
hide_input=True,
|
||||||
|
confirmation_prompt=True,
|
||||||
|
help="Authentication password",
|
||||||
)
|
)
|
||||||
def configure(root, user, password):
|
def configure(root, user, password):
|
||||||
save_config(root, {
|
save_config(root, {"user": user, "password": password})
|
||||||
"user": user,
|
click.echo(
|
||||||
"password": password,
|
fmt.info(
|
||||||
})
|
|
||||||
click.echo(fmt.info(
|
|
||||||
"The web UI configuration has been updated. "
|
"The web UI configuration has been updated. "
|
||||||
"If at any point you wish to reset your username and password, "
|
"If at any point you wish to reset your username and password, "
|
||||||
"just delete the following file:\n\n {}".format(config_path(root))
|
"just delete the following file:\n\n {}".format(config_path(root))
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_gotty_binary(root):
|
def check_gotty_binary(root):
|
||||||
path = gotty_path(root)
|
path = gotty_path(root)
|
||||||
@ -93,8 +110,7 @@ def check_gotty_binary(root):
|
|||||||
# Note: I don't know how to handle arm
|
# Note: I don't know how to handle arm
|
||||||
architecture = "amd64" if platform.architecture()[0] == "64bit" else "386"
|
architecture = "amd64" if platform.architecture()[0] == "64bit" else "386"
|
||||||
url = "https://github.com/yudai/gotty/releases/download/v1.0.1/gotty_{system}_{architecture}.tar.gz".format(
|
url = "https://github.com/yudai/gotty/releases/download/v1.0.1/gotty_{system}_{architecture}.tar.gz".format(
|
||||||
system=platform.system().lower(),
|
system=platform.system().lower(), architecture=architecture
|
||||||
architecture=architecture,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Download
|
# Download
|
||||||
@ -107,16 +123,15 @@ def check_gotty_binary(root):
|
|||||||
compressed = tarfile.open(fileobj=io.BytesIO(response.read()))
|
compressed = tarfile.open(fileobj=io.BytesIO(response.read()))
|
||||||
compressed.extract("./gotty", dirname)
|
compressed.extract("./gotty", dirname)
|
||||||
|
|
||||||
|
|
||||||
def load_config(root):
|
def load_config(root):
|
||||||
path = config_path(root)
|
path = config_path(root)
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
save_config(root, {
|
save_config(root, {"user": None, "password": None})
|
||||||
"user": None,
|
|
||||||
"password": None,
|
|
||||||
})
|
|
||||||
with open(config_path(root)) as f:
|
with open(config_path(root)) as f:
|
||||||
return serialize.load(f)
|
return serialize.load(f)
|
||||||
|
|
||||||
|
|
||||||
def save_config(root, config):
|
def save_config(root, config):
|
||||||
path = config_path(root)
|
path = config_path(root)
|
||||||
directory = os.path.dirname(path)
|
directory = os.path.dirname(path)
|
||||||
@ -125,14 +140,18 @@ def save_config(root, config):
|
|||||||
with open(path, "w") as of:
|
with open(path, "w") as of:
|
||||||
serialize.dump(config, of)
|
serialize.dump(config, of)
|
||||||
|
|
||||||
|
|
||||||
def gotty_path(root):
|
def gotty_path(root):
|
||||||
return get_path(root, "gotty")
|
return get_path(root, "gotty")
|
||||||
|
|
||||||
|
|
||||||
def config_path(root):
|
def config_path(root):
|
||||||
return get_path(root, "config.yml")
|
return get_path(root, "config.yml")
|
||||||
|
|
||||||
|
|
||||||
def get_path(root, filename):
|
def get_path(root, filename):
|
||||||
return tutor_env.pathjoin(root, "webui", filename)
|
return tutor_env.pathjoin(root, "webui", filename)
|
||||||
|
|
||||||
|
|
||||||
webui.add_command(start)
|
webui.add_command(start)
|
||||||
webui.add_command(configure)
|
webui.add_command(configure)
|
||||||
|
Loading…
Reference in New Issue
Block a user