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
@click.group(
help="Build an Android app for your Open edX platform [BETA FEATURE]"
)
@click.group(help="Build an Android app for your Open edX platform [BETA FEATURE]")
def android():
pass
@click.group(
help="Build the application"
)
@click.group(help="Build the application")
def build():
pass
@click.command(
help="Build the application in debug mode"
)
@click.command(help="Build the application in debug mode")
@opts.root
def debug(root):
docker_run(root)
click.echo(fmt.info("The debuggable APK file is available in {}".format(tutor_env.data_path(root, "android"))))
@click.command(
help="Build the application in release mode"
click.echo(
fmt.info(
"The debuggable APK file is available in {}".format(
tutor_env.data_path(root, "android")
)
)
)
@click.command(help="Build the application in release mode")
@opts.root
def release(root):
docker_run(root, "./gradlew", "assembleProdRelease")
click.echo(fmt.info("The production APK file is available in {}".format(tutor_env.data_path(root, "android"))))
@click.command(
help="Pull the docker image"
click.echo(
fmt.info(
"The production APK file is available in {}".format(
tutor_env.data_path(root, "android")
)
)
)
@click.command(help="Pull the docker image")
@opts.root
def pullimage(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):
config = tutor_config.load(root)
utils.docker_run(
"--volume={}:/openedx/config/".format(tutor_env.pathjoin(root, "android")),
"--volume={}:/openedx/data/".format(tutor_env.data_path(root, "android")),
config['DOCKER_IMAGE_ANDROID'],
config["DOCKER_IMAGE_ANDROID"],
*command
)
build.add_command(debug)
build.add_command(release)
android.add_command(build)

View File

@ -25,16 +25,13 @@ def main():
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__)
def cli():
pass
@click.command(
help="Print this help",
name="help",
)
@click.command(help="Print this help", name="help")
def print_help():
with click.Context(cli) as context:
click.echo(cli.get_help(context))

View File

@ -15,7 +15,7 @@ from .__about__ import __version__
@click.group(
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():
pass
@ -44,9 +44,7 @@ def save(root, silent=False, keyvalues=None):
save_env(root, config)
@click.command(
help="Print the project root",
)
@click.command(help="Print the project root")
@opts.root
def printroot(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
non-interactive mode.
"""
click.echo(fmt.alert(
click.echo(
fmt.alert(
"The current environment stored at {} is not up-to-date: it is at "
"v{} while the 'tutor' binary is at v{}.".format(
env.base_dir(root), env.version(root), __version__
)
))
)
)
if os.isatty(sys.stdin.fileno()):
# 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
"Would you like to upgrade the environment? If you do, any change you"
" might have made will be overwritten."
), default=True, abort=True)
),
default=True,
abort=True,
)
else:
# Non-interactive mode with no authorization: abort
raise exceptions.TutorError(
@ -171,29 +175,105 @@ def load_interactive(config):
ask("Your public contact email address", "CONTACT_EMAIL", config)
ask_choice(
"The default language code for the platform",
"LANGUAGE_CODE", config,
['en', 'am', 'ar', 'az', 'bg-bg', 'bn-bd', 'bn-in', 'bs', 'ca',
'ca@valencia', 'cs', 'cy', 'da', 'de-de', 'el', 'en-uk', 'en@lolcat',
'en@pirate', 'es-419', 'es-ar', 'es-ec', 'es-es', 'es-mx', 'es-pe',
'et-ee', 'eu-es', 'fa', 'fa-ir', 'fi-fi', 'fil', 'fr', 'gl', 'gu',
'he', 'hi', 'hr', 'hu', 'hy-am', 'id', 'it-it', 'ja-jp', 'kk-kz',
'km-kh', 'kn', 'ko-kr', 'lt-lt', 'ml', 'mn', 'mr', 'ms', 'nb', 'ne',
'nl-nl', 'or', 'pl', 'pt-br', 'pt-pt', 'ro', 'ru', 'si', 'sk', 'sl',
'sq', 'sr', 'sv', 'sw', 'ta', 'te', 'th', 'tr-tr', 'uk', 'ur', 'vi',
'uz', 'zh-cn', 'zh-hk', 'zh-tw'],
"LANGUAGE_CODE",
config,
[
"en",
"am",
"ar",
"az",
"bg-bg",
"bn-bd",
"bn-in",
"bs",
"ca",
"ca@valencia",
"cs",
"cy",
"da",
"de-de",
"el",
"en-uk",
"en@lolcat",
"en@pirate",
"es-419",
"es-ar",
"es-ec",
"es-es",
"es-mx",
"es-pe",
"et-ee",
"eu-es",
"fa",
"fa-ir",
"fi-fi",
"fil",
"fr",
"gl",
"gu",
"he",
"hi",
"hr",
"hu",
"hy-am",
"id",
"it-it",
"ja-jp",
"kk-kz",
"km-kh",
"kn",
"ko-kr",
"lt-lt",
"ml",
"mn",
"mr",
"ms",
"nb",
"ne",
"nl-nl",
"or",
"pl",
"pt-br",
"pt-pt",
"ro",
"ru",
"si",
"sk",
"sl",
"sq",
"sr",
"sv",
"sw",
"ta",
"te",
"th",
"tr-tr",
"uk",
"ur",
"vi",
"uz",
"zh-cn",
"zh-hk",
"zh-tw",
],
)
ask_bool(
("Activate SSL/TLS certificates for HTTPS access? Important note:"
"this will NOT work in a development environment."),
"ACTIVATE_HTTPS", config
(
"Activate SSL/TLS certificates for HTTPS access? Important note:"
"this will NOT work in a development environment."
),
"ACTIVATE_HTTPS",
config,
)
ask_bool(
"Activate Student Notes service (https://open.edx.org/features/student-notes)?",
"ACTIVATE_NOTES", config
"ACTIVATE_NOTES",
config,
)
ask_bool(
"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
# 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])
def ask(question, key, config):
default = env.render_str(config, config[key])
config[key] = click.prompt(
fmt.question(question),
prompt_suffix=" ", default=default, show_default=True,
fmt.question(question), prompt_suffix=" ", default=default, show_default=True
)
def ask_bool(question, key, config):
return click.confirm(
fmt.question(question),
prompt_suffix=' ',
default=config[key],
)
return click.confirm(fmt.question(question), prompt_suffix=" ", default=config[key])
def ask_choice(question, key, config, choices):
@ -242,13 +319,19 @@ def convert_json2yml(root):
return
if os.path.exists(config_path(root)):
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:
config = json.load(fi)
save_config(root, config)
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):

View File

@ -7,9 +7,7 @@ from . import opts
from . import utils
@click.group(
help="Run Open edX platform with development settings",
)
@click.group(help="Run Open edX platform with development settings")
def dev():
pass
@ -36,9 +34,7 @@ def run(root, edx_platform_path, edx_platform_settings, service, command, args):
)
@click.command(
help="Run a development server",
)
@click.command(help="Run a development server")
@opts.root
@opts.edx_platform_path
@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):
port = service_port(service)
docker_compose_run_with_port(
root, edx_platform_path, edx_platform_settings, port,
service, "./manage.py", service, "runserver", "0.0.0.0:{}".format(port),
root,
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
def stop(root):
docker_compose(root, "rm", "--stop", "--force")
@click.command(
help="Watch for changes in your themes and recompile assets when needed"
)
@click.command(help="Watch for changes in your themes and recompile assets when needed")
@opts.root
@opts.edx_platform_path
@opts.edx_platform_settings
def watchthemes(root, edx_platform_path, edx_platform_settings):
docker_compose_run(
root, edx_platform_path, edx_platform_settings,
"--no-deps", "lms", "openedx-assets", "watch-themes", "--env", "dev"
root,
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(
root, edx_platform_path, edx_platform_settings,
"-p", "{port}:{port}".format(port=port), *command
root,
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):
run_command = [
"run", "--rm",
"-e", "SETTINGS={}".format(edx_platform_settings),
"--volume={}:/openedx/themes".format(tutor_env.pathjoin(root, "build", "openedx", "themes")),
"run",
"--rm",
"-e",
"SETTINGS={}".format(edx_platform_settings),
"--volume={}:/openedx/themes".format(
tutor_env.pathjoin(root, "build", "openedx", "themes")
),
]
if edx_platform_path:
run_command += [
"--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
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):
return utils.docker_compose(
"-f", tutor_env.pathjoin(root, "local", "docker-compose.yml"),
"--project-name", "tutor_dev",
"-f",
tutor_env.pathjoin(root, "local", "docker-compose.yml"),
"--project-name",
"tutor_dev",
*command
)

View File

@ -20,7 +20,7 @@ def render_full(root, config):
for target in ["android", "apps", "k8s", "local", "webui"]:
render_target(root, config, target)
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__)
@ -36,7 +36,7 @@ def render_target(root, config, target):
def render_file(config, path):
with codecs.open(path, encoding='utf-8') as fi:
with codecs.open(path, encoding="utf-8") as fi:
try:
return render_str(config, fi.read())
except jinja2.exceptions.TemplateError:
@ -77,9 +77,7 @@ def render_str(config, text):
template = jinja2.Template(text, undefined=jinja2.StrictUndefined)
try:
return template.render(
RAND8=utils.random_string(8),
RAND24=utils.random_string(24),
**config
RAND8=utils.random_string(8), RAND24=utils.random_string(24), **config
)
except jinja2.exceptions.UndefinedError as e:
raise exceptions.TutorError("Missing configuration value: {}".format(e.args[0]))
@ -113,7 +111,7 @@ def read(*path):
Read template content located at `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()
@ -129,10 +127,7 @@ def walk_templates(root, target):
for dirpath, _, filenames in os.walk(target_root):
if not is_part_of_env(dirpath):
continue
dst_dir = pathjoin(
root, target,
os.path.relpath(dirpath, target_root)
)
dst_dir = pathjoin(root, target, os.path.relpath(dirpath, target_root))
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
for filename in filenames:

View File

@ -1,26 +1,30 @@
import click
def title(text):
indent = 8
separator = "=" * (len(text) + 2 * indent)
message = "{separator}\n{indent}{text}\n{separator}".format(
separator=separator,
indent=" " * indent,
text=text,
separator=separator, indent=" " * indent, text=text
)
return click.style(message, fg="green")
def info(text):
return click.style(text, fg="blue")
def error(text):
return click.style(text, fg="red")
def command(text):
return click.style(text, fg="magenta")
def question(text):
return click.style(text, fg="yellow")
def alert(text):
return click.style("⚠️ " + text, fg="yellow", bold=True)

View File

@ -13,38 +13,43 @@ def images_command():
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(
"image", type=click.Choice(["all"] + OPENEDX_IMAGES),
"image", type=click.Choice(["all"] + OPENEDX_IMAGES)
)
argument_image = click.argument(
"image", type=click.Choice(["all"] + OPENEDX_IMAGES + VENDOR_IMAGES),
"image", type=click.Choice(["all"] + OPENEDX_IMAGES + VENDOR_IMAGES)
)
@click.command(
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
@argument_openedx_image
@click.option(
"-a", "--build-arg", multiple=True,
help="Set build-time docker ARGS in the form 'myarg=value'. This option may be specified multiple times."
"-a",
"--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):
config = tutor_config.load(root)
for img in openedx_image_names(config, image):
tag = get_tag(config, img)
click.echo(fmt.info("Building image {}".format(tag)))
command = [
"build", "-t", tag,
tutor_env.pathjoin(root, "build", img)
]
command = ["build", "-t", tag, tutor_env.pathjoin(root, "build", img)]
for arg in build_arg:
command += [
"--build-arg", arg
]
command += ["--build-arg", arg]
utils.docker(*command)
@ -71,10 +76,7 @@ def push(root, image):
def get_tag(config, name):
image = config["DOCKER_IMAGE_" + name.upper()]
return "{registry}{image}".format(
registry=config["DOCKER_REGISTRY"],
image=image,
)
return "{registry}{image}".format(registry=config["DOCKER_REGISTRY"], image=image)
def image_names(config, image):
@ -89,10 +91,10 @@ def openedx_image_tags(config, image):
def openedx_image_names(config, image):
if image == "all":
images = OPENEDX_IMAGES[:]
if not config['ACTIVATE_XQUEUE']:
images.remove('xqueue')
if not config['ACTIVATE_NOTES']:
images.remove('notes')
if not config["ACTIVATE_XQUEUE"]:
images.remove("xqueue")
if not config["ACTIVATE_NOTES"]:
images.remove("notes")
return images
return [image]
@ -100,16 +102,16 @@ def openedx_image_names(config, image):
def vendor_image_names(config, image):
if image == "all":
images = VENDOR_IMAGES[:]
if not config['ACTIVATE_ELASTICSEARCH']:
images.remove('elasticsearch')
if not config['ACTIVATE_MEMCACHED']:
images.remove('memcached')
if not config['ACTIVATE_MONGODB']:
images.remove('mongodb')
if not config['ACTIVATE_MYSQL']:
images.remove('mysql')
if not config['ACTIVATE_RABBITMQ']:
images.remove('rabbitmq')
if not config["ACTIVATE_ELASTICSEARCH"]:
images.remove("elasticsearch")
if not config["ACTIVATE_MEMCACHED"]:
images.remove("memcached")
if not config["ACTIVATE_MONGODB"]:
images.remove("mongodb")
if not config["ACTIVATE_MYSQL"]:
images.remove("mysql")
if not config["ACTIVATE_RABBITMQ"]:
images.remove("rabbitmq")
return images
return [image]

View File

@ -14,9 +14,7 @@ def k8s():
pass
@click.command(
help="Configure and run Open edX from scratch"
)
@click.command(help="Configure and run Open edX from scratch")
@opts.root
def quickstart(root):
click.echo(fmt.title("Interactive platform configuration"))
@ -25,7 +23,11 @@ def quickstart(root):
stop.callback()
click.echo(fmt.title("Starting the platform"))
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)
@ -35,12 +37,42 @@ def start(root):
config = tutor_config.load(root)
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"))
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",
"configmap",
"nginx-config",
"--from-file",
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", "ingress.yml"))
@ -57,13 +89,14 @@ def stop():
@click.option("-y", "--yes", is_flag=True, help="Do not ask for confirmation")
def delete(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)
@click.command(
help="Create databases and run database migrations",
)
@click.command(help="Create databases and run database migrations")
@opts.root
def databases(root):
scripts.migrate(root, run_sh)
@ -96,9 +129,7 @@ def indexcourses(root):
scripts.index_courses(root, run_sh)
@click.command(
help="Launch a shell in LMS or CMS",
)
@click.command(help="Launch a shell in LMS or CMS")
@click.argument("service", type=click.Choice(["lms", "cms"]))
def shell(service):
K8s().execute(service, "bash")
@ -121,9 +152,7 @@ def kubectl(*command):
ignored, to avoid stopping on "AlreadyExists" errors.
"""
args = list(command)
args += [
"--namespace", K8s.NAMESPACE
]
args += ["--namespace", K8s.NAMESPACE]
kubectl_no_fail(*args)
@ -150,6 +179,7 @@ class K8s:
if self.CLIENT is None:
# Import moved here for performance reasons
import kubernetes
kubernetes.config.load_kube_config()
self.CLIENT = kubernetes.client.CoreV1Api()
return self.CLIENT
@ -157,23 +187,37 @@ class K8s:
def pod_name(self, app):
selector = "app=" + app
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:
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):
# Note: this is a HORRIBLE way of looking for a secret
try:
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]
except IndexError:
raise exceptions.TutorError("Secret 'admin-user-token'. Make sure that admin user was created.")
return self.client.read_namespaced_secret(secret.metadata.name, "kube-system").data["token"]
raise exceptions.TutorError(
"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):
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

View File

@ -23,7 +23,9 @@ def local():
@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
def quickstart(pullimages_, root):
click.echo(fmt.title("Interactive platform configuration"))
@ -64,30 +66,38 @@ def start(root, detach):
http = "https" if config["ACTIVATE_HTTPS"] else "http"
urls = []
if not config["ACTIVATE_HTTPS"] and not config["WEB_PROXY"]:
urls += [
"http://localhost",
"http://studio.localhost",
]
urls.append("{http}://{lms_host}".format(http=http, lms_host=config["LMS_HOST"]))
urls.append("{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:
urls += ["http://localhost", "http://studio.localhost"]
urls.append(
"{http}://{lms_host}".format(http=http, lms_host=config["LMS_HOST"])
)
urls.append(
"{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:
{}""".format("\n ".join(urls))))
{}""".format(
"\n ".join(urls)
)
)
)
@click.command(help="Stop a running platform",)
@click.command(help="Stop a running platform")
@opts.root
def stop(root):
config = tutor_config.load(root)
docker_compose(root, config, "rm", "--stop", "--force")
@click.command(
help="""Restart some components from a running platform.
You may specify 'openedx' to restart the lms, cms and workers, or 'all' to
restart all services.""",
restart all services."""
)
@opts.root
@click.argument('service')
@click.argument("service")
def restart(root, service):
config = tutor_config.load(root)
command = ["restart"]
@ -110,11 +120,7 @@ def restart(root, service):
@click.argument("command", default=None, required=False)
@click.argument("args", nargs=-1, required=False)
def run(root, service, command, args):
run_command = [
"run",
"--rm",
service
]
run_command = ["run", "--rm", service]
if command:
run_command.append(command)
if args:
@ -123,9 +129,7 @@ def run(root, service, command, args):
docker_compose(root, config, *run_command)
@click.command(
help="Create databases and run database migrations",
)
@click.command(help="Create databases and run database migrations")
@opts.root
def databases(root):
init_mysql(root)
@ -134,7 +138,7 @@ def databases(root):
def init_mysql(root):
config = tutor_config.load(root)
if not config['ACTIVATE_MYSQL']:
if not config["ACTIVATE_MYSQL"]:
return
mysql_data_path = tutor_env.data_path(root, "mysql", "mysql")
if os.path.exists(mysql_data_path):
@ -145,10 +149,17 @@ def init_mysql(root):
click.echo(fmt.info(" waiting for mysql initialization"))
# TODO this is duplicate code with the docker_compose function. We
# should rely on a dedicated function in utils module.
mysql_logs = subprocess.check_output([
"docker-compose", "-f", tutor_env.pathjoin(root, "local", "docker-compose.yml"),
"--project-name", config["LOCAL_PROJECT_NAME"], "logs", "mysql",
])
mysql_logs = subprocess.check_output(
[
"docker-compose",
"-f",
tutor_env.pathjoin(root, "local", "docker-compose.yml"),
"--project-name",
config["LOCAL_PROJECT_NAME"],
"logs",
"mysql",
]
)
# pylint: disable=unsupported-membership-test
if b"MySQL init process done. Ready for start up." in mysql_logs:
click.echo(fmt.info("MySQL database initialized"))
@ -173,28 +184,38 @@ def https_create(root):
2. On certificate renewal, nginx is not reloaded
"""
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"))
return
script = scripts.render_template(config, 'https_create.sh')
script = scripts.render_template(config, "https_create.sh")
if config['WEB_PROXY']:
click.echo(fmt.info("""You are running Tutor behind a web proxy (WEB_PROXY=true): SSL/TLS
if config["WEB_PROXY"]:
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 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
utils.docker_run(
"--volume", "{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")),
"-p", "80:80",
"--volume",
"{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")),
"-p",
"80:80",
"--entrypoint=sh",
"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
def https_renew(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"))
return
if config['WEB_PROXY']:
click.echo(fmt.info("""You are running Tutor behind a web proxy (WEB_PROXY=true): SSL/TLS
if config["WEB_PROXY"]:
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, run:
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
docker_run = [
"--volume", "{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")),
"-p", "80:80",
"certbot/certbot:latest", "renew"
"--volume",
"{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")),
"-p",
"80:80",
"certbot/certbot:latest",
"renew",
]
utils.docker_run(*docker_run)
@ -274,22 +302,31 @@ def indexcourses(root):
short_help="Run Portainer, a UI for container supervision",
)
@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):
docker_run = [
"--volume=/var/run/docker.sock:/var/run/docker.sock",
"--volume={}:/data".format(tutor_env.data_path(root, "portainer")),
"-p", "{port}:{port}".format(port=port),
"-p",
"{port}:{port}".format(port=port),
"portainer/portainer:latest",
"--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)
def check_service_is_activated(config, service):
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):
@ -299,8 +336,10 @@ def run_sh(root, service, command):
def docker_compose(root, config, *command):
return utils.docker_compose(
"-f", tutor_env.pathjoin(root, "local", "docker-compose.yml"),
"--project-name", config["LOCAL_PROJECT_NAME"],
"-f",
tutor_env.pathjoin(root, "local", "docker-compose.yml"),
"--project-name",
config["LOCAL_PROJECT_NAME"],
*command
)

View File

@ -5,27 +5,32 @@ from . import serialize
root = click.option(
"-r", "--root",
"-r",
"--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),
help="Root project directory (environment variable: TUTOR_ROOT)"
help="Root project directory (environment variable: TUTOR_ROOT)",
)
edx_platform_path = click.option(
"-P", "--edx-platform-path",
"-P",
"--edx-platform-path",
envvar="TUTOR_EDX_PLATFORM_PATH",
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(
"-S", "--edx-platform-settings",
"-S",
"--edx-platform-settings",
envvar="TUTOR_EDX_PLATFORM_SETTINGS",
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):
name = "yaml"
@ -36,7 +41,13 @@ class YamlParamType(click.ParamType):
self.fail("'{}' is not of the form 'key=value'.".format(value), param, ctx)
return k, serialize.parse_value(v)
key_value = click.option(
"-s", "--set", "set_", type=YamlParamType(), multiple=True, metavar="KEY=VAL",
help="Set a configuration value (can be used multiple times)"
"-s",
"--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 fmt
def migrate(root, run_func):
config = tutor_config.load(root)
@ -31,29 +32,30 @@ def migrate(root, run_func):
run_template(root, config, "lms", "oauth2.sh", run_func)
click.echo(fmt.info("Databases ready."))
def create_user(root, run_func, superuser, staff, name, email):
config = {
"OPTS": "",
"USERNAME": name,
"EMAIL": email,
}
config = {"OPTS": "", "USERNAME": name, "EMAIL": email}
if superuser:
config["OPTS"] += " --superuser"
if staff:
config["OPTS"] += " --staff"
run_template(root, config, "lms", "create_user.sh", run_func)
def import_demo_course(root, run_func):
run_template(root, {}, "cms", "import_demo_course.sh", run_func)
def index_courses(root, run_func):
run_template(root, {}, "cms", "index_courses.sh", run_func)
def run_template(root, config, service, template, run_func):
command = render_template(config, template)
if command:
run_func(root, service, command)
def render_template(config, template):
path = env.template_path("scripts", template)
return env.render_file(config, path).strip()

View File

@ -1,11 +1,14 @@
import yaml
def load(stream):
return yaml.load(stream, Loader=yaml.SafeLoader)
def dump(content, fileobj):
yaml.dump(content, fileobj, default_flow_style=False)
def parse_value(v):
"""
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":
v = None
elif v in ["true", "false"]:
v = (v == "true")
v = v == "true"
return v

View File

@ -1,28 +1,34 @@
from .common import *
SECRET_KEY = '{{ NOTES_SECRET_KEY }}'
ALLOWED_HOSTS = ['localhost', 'notes', 'notes.openedx', 'notes.localhost', 'notes.{{ LMS_HOST }}']
SECRET_KEY = "{{ NOTES_SECRET_KEY }}"
ALLOWED_HOSTS = [
"localhost",
"notes",
"notes.openedx",
"notes.localhost",
"notes.{{ LMS_HOST }}",
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': '{{ MYSQL_HOST }}',
'PORT': {{ MYSQL_PORT }},
'NAME': '{{ NOTES_MYSQL_DATABASE }}',
'USER': '{{ NOTES_MYSQL_USERNAME }}',
'PASSWORD': '{{ NOTES_MYSQL_PASSWORD }}',
"default": {
"ENGINE": "django.db.backends.mysql",
"HOST": "{{ MYSQL_HOST }}",
"PORT": {{MYSQL_PORT}},
"NAME": "{{ NOTES_MYSQL_DATABASE }}",
"USER": "{{ NOTES_MYSQL_USERNAME }}",
"PASSWORD": "{{ NOTES_MYSQL_PASSWORD }}",
}
}
CLIENT_ID = 'notes'
CLIENT_SECRET = '{{ NOTES_OAUTH2_SECRET }}'
CLIENT_ID = "notes"
CLIENT_SECRET = "{{ NOTES_OAUTH2_SECRET }}"
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'notesserver.highlight.ElasticsearchSearchEngine',
'URL': 'http://{{ ELASTICSEARCH_HOST }}:{{ ELASTICSEARCH_PORT }}/',
'INDEX_NAME': 'notes',
},
"default": {
"ENGINE": "notesserver.highlight.ElasticsearchSearchEngine",
"URL": "http://{{ ELASTICSEARCH_HOST }}:{{ ELASTICSEARCH_PORT }}/",
"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/"
# Video settings
VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['location'] = MEDIA_ROOT
VIDEO_TRANSCRIPTS_SETTINGS['STORAGE_KWARGS']['location'] = MEDIA_ROOT
VIDEO_IMAGE_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
LOGGING['handlers']['local'] = {'class': 'logging.NullHandler'}
LOGGING['handlers']['tracking'] = {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'standard',
LOGGING["handlers"]["local"] = {"class": "logging.NullHandler"}
LOGGING["handlers"]["tracking"] = {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "standard",
}
LOCALE_PATHS.append('/openedx/locale')
LOCALE_PATHS.append("/openedx/locale")
# Create folders if necessary
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
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
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
execfile(os.path.join(os.path.dirname(__file__), 'common.py'), globals())
execfile(os.path.join(os.path.dirname(__file__), "common.py"), globals())
ALLOWED_HOSTS = [
ENV_TOKENS.get('CMS_BASE'),
'127.0.0.1', 'localhost', 'studio.localhost',
'127.0.0.1:8000', 'localhost:8000',
'127.0.0.1:8001', 'localhost:8001',
ENV_TOKENS.get("CMS_BASE"),
"127.0.0.1",
"localhost",
"studio.localhost",
"127.0.0.1:8000",
"localhost:8000",
"127.0.0.1:8001",
"localhost:8001",
]
DEFAULT_FROM_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
SERVER_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
DEFAULT_FROM_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
DEFAULT_FEEDBACK_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/"
# Video settings
VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['location'] = MEDIA_ROOT
VIDEO_TRANSCRIPTS_SETTINGS['STORAGE_KWARGS']['location'] = MEDIA_ROOT
VIDEO_IMAGE_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
LOGGING['handlers']['local'] = {'class': 'logging.NullHandler'}
LOGGING['handlers']['tracking'] = {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'standard',
LOGGING["handlers"]["local"] = {"class": "logging.NullHandler"}
LOGGING["handlers"]["tracking"] = {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "standard",
}
# Fix media files paths
VIDEO_IMAGE_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/')
VIDEO_IMAGE_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/"
)
ORA2_FILEUPLOAD_BACKEND = 'filesystem'
ORA2_FILEUPLOAD_ROOT = '/openedx/data/ora2'
ORA2_FILEUPLOAD_CACHE_NAME = 'ora2-storage'
ORA2_FILEUPLOAD_BACKEND = "filesystem"
ORA2_FILEUPLOAD_ROOT = "/openedx/data/ora2"
ORA2_FILEUPLOAD_CACHE_NAME = "ora2-storage"
GRADES_DOWNLOAD = {
'STORAGE_TYPE': '',
'STORAGE_KWARGS': {
'base_url': "/media/grades/",
'location': os.path.join(MEDIA_ROOT, 'grades'),
}
"STORAGE_TYPE": "",
"STORAGE_KWARGS": {
"base_url": "/media/grades/",
"location": os.path.join(MEDIA_ROOT, "grades"),
},
}
LOCALE_PATHS.append('/openedx/locale')
LOCALE_PATHS.append("/openedx/locale")
# Create folders if necessary
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
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
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
execfile(os.path.join(os.path.dirname(__file__), 'common.py'), globals())
execfile(os.path.join(os.path.dirname(__file__), "common.py"), globals())
ALLOWED_HOSTS = [
ENV_TOKENS.get('LMS_BASE'),
FEATURES['PREVIEW_LMS_BASE'],
'127.0.0.1', 'localhost', 'preview.localhost',
'127.0.0.1:8000', 'localhost:8000', 'preview.localhost:8000',
ENV_TOKENS.get("LMS_BASE"),
FEATURES["PREVIEW_LMS_BASE"],
"127.0.0.1",
"localhost",
"preview.localhost",
"127.0.0.1:8000",
"localhost:8000",
"preview.localhost:8000",
]
# 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
OAUTH_ENFORCE_SECURE = False
DEFAULT_FROM_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
SERVER_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
TECH_SUPPORT_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
CONTACT_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
BUGS_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
UNIVERSITY_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
PRESS_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
PAYMENT_SUPPORT_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
BULK_EMAIL_DEFAULT_FROM_EMAIL = 'no-reply@' + ENV_TOKENS['LMS_BASE']
API_ACCESS_MANAGER_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
API_ACCESS_FROM_EMAIL = ENV_TOKENS['CONTACT_EMAIL']
DEFAULT_FROM_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
SERVER_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
TECH_SUPPORT_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
CONTACT_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
BUGS_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
UNIVERSITY_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
PRESS_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
PAYMENT_SUPPORT_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
BULK_EMAIL_DEFAULT_FROM_EMAIL = "no-reply@" + ENV_TOKENS["LMS_BASE"]
API_ACCESS_MANAGER_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]
API_ACCESS_FROM_EMAIL = ENV_TOKENS["CONTACT_EMAIL"]

View File

@ -1,24 +1,18 @@
from .settings import *
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': '{{ MYSQL_HOST }}',
'PORT': {{ MYSQL_PORT }},
'NAME': '{{ XQUEUE_MYSQL_DATABASE }}',
'USER': '{{ XQUEUE_MYSQL_USERNAME }}',
'PASSWORD': '{{ XQUEUE_MYSQL_PASSWORD }}',
"default": {
"ENGINE": "django.db.backends.mysql",
"HOST": "{{ MYSQL_HOST }}",
"PORT": {{MYSQL_PORT}},
"NAME": "{{ XQUEUE_MYSQL_DATABASE }}",
"USER": "{{ XQUEUE_MYSQL_USERNAME }}",
"PASSWORD": "{{ XQUEUE_MYSQL_PASSWORD }}",
}
}
LOGGING = get_logger_config(
log_dir="/openedx/data/",
logging_env="tutor",
dev_env=True,
)
LOGGING = get_logger_config(log_dir="/openedx/data/", logging_env="tutor", dev_env=True)
SECRET_KEY = '{{ XQUEUE_SECRET_KEY }}'
SECRET_KEY = "{{ XQUEUE_SECRET_KEY }}"
XQUEUE_USERS = {
'{{ XQUEUE_AUTH_USERNAME }}': '{{ XQUEUE_AUTH_PASSWORD}}'
}
XQUEUE_USERS = {"{{ XQUEUE_AUTH_USERNAME }}": "{{ XQUEUE_AUTH_PASSWORD}}"}

View File

@ -4,13 +4,15 @@ import click_repl
@click.command(
short_help="Interactive shell",
help="Launch an interactive shell for launching Tutor commands"
help="Launch an interactive shell for launching Tutor commands",
)
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 "local quickstart" to configure and launch a new platform from scratch.
Type <ctrl-d> to exit.""")
Type <ctrl-d> to exit."""
)
while True:
try:
click_repl.repl(click.get_current_context())

View File

@ -10,7 +10,9 @@ from . import fmt
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):
@ -36,13 +38,17 @@ def docker_run(*command):
def docker(*command):
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)
def docker_compose(*command):
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)
@ -66,11 +72,8 @@ def execute(*command):
except Exception:
p.kill()
p.wait()
raise exceptions.TutorError("Command failed: {}".format(
" ".join(command)
))
raise exceptions.TutorError("Command failed: {}".format(" ".join(command)))
if result > 0:
raise exceptions.TutorError("Command failed with status {}: {}".format(
result,
" ".join(command)
))
raise exceptions.TutorError(
"Command failed with status {}: {}".format(result, " ".join(command))
)

View File

@ -15,24 +15,26 @@ from . import opts
from . import env as tutor_env
from . import serialize
@click.group(
short_help="Web user interface",
help="""Run Tutor commands from a web terminal"""
short_help="Web user interface", help="""Run Tutor commands from a web terminal"""
)
def webui():
pass
@click.command(
help="Start the web UI",
)
@click.command(help="Start the web UI")
@opts.root
@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",
)
@click.option(
"-h", "--host", default="0.0.0.0", show_default=True,
help="Host address to listen",
"-h", "--host", default="0.0.0.0", show_default=True, help="Host address to listen"
)
def start(root, port, host):
check_gotty_binary(root)
@ -42,15 +44,24 @@ def start(root, port, host):
user = config["user"]
password = config["password"]
command = [
gotty_path(root), "--permit-write",
"--address", host, "--port", str(port),
"--title-format", "Tutor web UI - {{ .Command }} ({{ .Hostname }})",
gotty_path(root),
"--permit-write",
"--address",
host,
"--port",
str(port),
"--title-format",
"Tutor web UI - {{ .Command }} ({{ .Hostname }})",
]
if user and password:
credential = "{}:{}".format(user, password)
command += ["--credential", credential]
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"]
p = subprocess.Popen(command)
while True:
@ -59,29 +70,35 @@ def start(root, port, host):
except subprocess.TimeoutExpired:
new_config = load_config(root)
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.wait()
break
@click.command(help="Configure authentication")
@opts.root
@click.option("-u", "--user", prompt="User name", help="Authentication user name")
@click.option(
"-p", "--password",
prompt=True, hide_input=True, confirmation_prompt=True,
help="Authentication password"
"-p",
"--password",
prompt=True,
hide_input=True,
confirmation_prompt=True,
help="Authentication password",
)
def configure(root, user, password):
save_config(root, {
"user": user,
"password": password,
})
click.echo(fmt.info(
save_config(root, {"user": user, "password": password})
click.echo(
fmt.info(
"The web UI configuration has been updated. "
"If at any point you wish to reset your username and password, "
"just delete the following file:\n\n {}".format(config_path(root))
))
)
)
def check_gotty_binary(root):
path = gotty_path(root)
@ -93,8 +110,7 @@ def check_gotty_binary(root):
# Note: I don't know how to handle arm
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(
system=platform.system().lower(),
architecture=architecture,
system=platform.system().lower(), architecture=architecture
)
# Download
@ -107,16 +123,15 @@ def check_gotty_binary(root):
compressed = tarfile.open(fileobj=io.BytesIO(response.read()))
compressed.extract("./gotty", dirname)
def load_config(root):
path = config_path(root)
if not os.path.exists(path):
save_config(root, {
"user": None,
"password": None,
})
save_config(root, {"user": None, "password": None})
with open(config_path(root)) as f:
return serialize.load(f)
def save_config(root, config):
path = config_path(root)
directory = os.path.dirname(path)
@ -125,14 +140,18 @@ def save_config(root, config):
with open(path, "w") as of:
serialize.dump(config, of)
def gotty_path(root):
return get_path(root, "gotty")
def config_path(root):
return get_path(root, "config.yml")
def get_path(root, filename):
return tutor_env.pathjoin(root, "webui", filename)
webui.add_command(start)
webui.add_command(configure)