6
0
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:
Régis Behmo 2019-05-05 11:45:24 +02:00 committed by Régis Behmo
parent 4ac7dff06a
commit f2f714ad23
23 changed files with 593 additions and 350 deletions

View File

@ -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)

View File

@ -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))

View File

@ -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):

View File

@ -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
) )

View File

@ -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:

View File

@ -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)

View File

@ -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]

View File

@ -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

View File

@ -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
) )

View File

@ -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)",
) )

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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]:

View File

@ -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"

View File

@ -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"]

View File

@ -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]:

View File

@ -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"

View File

@ -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"]

View File

@ -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}}'
}

View File

@ -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())

View File

@ -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) )
))

View File

@ -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)