tutor/tutor/commands/local.py

248 lines
8.5 KiB
Python

from time import sleep
import click
from .. import config as tutor_config
from .. import env as tutor_env
from .. import exceptions, fmt, utils
from ..types import Config, get_typed
from . import compose
from .config import save as config_save_command
class LocalJobRunner(compose.ComposeJobRunner):
def __init__(self, root: str, config: Config):
"""
Load docker-compose files from local/.
"""
super().__init__(root, config)
self.project_name = get_typed(self.config, "LOCAL_PROJECT_NAME", str)
self.docker_compose_files += [
tutor_env.pathjoin(self.root, "local", "docker-compose.yml"),
tutor_env.pathjoin(self.root, "local", "docker-compose.prod.yml"),
tutor_env.pathjoin(self.root, "local", "docker-compose.override.yml"),
]
self.docker_compose_job_files += [
tutor_env.pathjoin(self.root, "local", "docker-compose.jobs.yml"),
tutor_env.pathjoin(self.root, "local", "docker-compose.jobs.override.yml"),
]
class LocalContext(compose.BaseComposeContext):
def job_runner(self, config: Config) -> LocalJobRunner:
return LocalJobRunner(self.root, config)
@click.group(help="Run Open edX locally with docker-compose")
@click.pass_context
def local(context: click.Context) -> None:
context.obj = LocalContext(context.obj.root)
@click.command(help="Configure and run Open edX from scratch")
@click.option("-I", "--non-interactive", is_flag=True, help="Run non-interactively")
@click.option("-p", "--pullimages", is_flag=True, help="Update docker images")
@click.pass_context
def quickstart(context: click.Context, non_interactive: bool, pullimages: bool) -> None:
try:
utils.check_macos_memory()
except exceptions.TutorError as e:
fmt.echo_alert(
"""Could not verify sufficient RAM allocation in Docker:
{}
Tutor may not work if Docker is configured with < 4 GB RAM. Please follow instructions from:
https://docs.tutor.overhang.io/install.html
""".format(
str(e)
)
)
if tutor_env.needs_major_upgrade(context.obj.root):
click.echo(fmt.title("Upgrading from an older release"))
context.invoke(
upgrade,
from_version=tutor_env.current_release(context.obj.root),
non_interactive=non_interactive,
)
click.echo(fmt.title("Interactive platform configuration"))
context.invoke(
config_save_command,
interactive=(not non_interactive),
set_vars=[],
unset_vars=[],
)
click.echo(fmt.title("Stopping any existing platform"))
context.invoke(compose.stop)
if pullimages:
click.echo(fmt.title("Docker image updates"))
context.invoke(compose.dc_command, command="pull")
click.echo(fmt.title("Starting the platform in detached mode"))
context.invoke(compose.start, detach=True)
click.echo(fmt.title("Database creation and migrations"))
context.invoke(compose.init)
config = tutor_config.load(context.obj.root)
fmt.echo_info(
"""The Open edX platform is now running in detached mode
Your Open edX platform is ready and can be accessed at the following urls:
{http}://{lms_host}
{http}://{cms_host}
""".format(
http="https" if config["ENABLE_HTTPS"] else "http",
lms_host=config["LMS_HOST"],
cms_host=config["CMS_HOST"],
)
)
@click.command(help="Upgrade from a previous Open edX named release")
@click.option(
"--from",
"from_version",
default="koa",
type=click.Choice(["ironwood", "juniper", "koa", "lilac"]),
)
@click.option("-I", "--non-interactive", is_flag=True, help="Run non-interactively")
@click.pass_context
def upgrade(context: click.Context, from_version: str, non_interactive: bool) -> None:
config = tutor_config.load_full(context.obj.root)
if not non_interactive:
question = """You are about to upgrade your Open edX platform. It is strongly recommended to make a backup before upgrading. To do so, run:
tutor local stop
sudo rsync -avr "$(tutor config printroot)"/ /tmp/tutor-backup/
In case of problem, to restore your backup you will then have to run: sudo rsync -avr /tmp/tutor-backup/ "$(tutor config printroot)"/
Are you sure you want to continue?"""
click.confirm(
fmt.question(question), default=True, abort=True, prompt_suffix=" "
)
running_version = from_version
if running_version == "ironwood":
upgrade_from_ironwood(context, config)
running_version = "juniper"
if running_version == "juniper":
upgrade_from_juniper(context, config)
running_version = "koa"
if running_version == "koa":
upgrade_from_koa(context, config)
running_version = "lilac"
if running_version == "lilac":
# Nothing to do here
running_version = "maple"
if not non_interactive:
question = f"""
Your platform was successfuly upgraded from {from_version} to {running_version}. Depending on your setup, you might have to rebuild some of your Docker images. You can do this now by running the following command in a different shell:
tutor images build openedx # add your custom images here
Press enter when you are ready to continue"""
click.confirm(
fmt.question(question), default=True, abort=True, prompt_suffix=" "
)
def upgrade_from_ironwood(context: click.Context, config: Config) -> None:
click.echo(fmt.title("Upgrading from Ironwood"))
tutor_env.save(context.obj.root, config)
click.echo(fmt.title("Stopping any existing platform"))
context.invoke(compose.stop)
if not config["RUN_MONGODB"]:
fmt.echo_info(
"You are not running MongDB (RUN_MONGODB=false). It is your "
"responsibility to upgrade your MongoDb instance to v3.6. There is "
"nothing left to do to upgrade from Ironwood to Juniper."
)
return
upgrade_mongodb(context, config, "3.4", "3.4")
context.invoke(compose.stop)
upgrade_mongodb(context, config, "3.6", "3.6")
context.invoke(compose.stop)
def upgrade_from_juniper(context: click.Context, config: Config) -> None:
click.echo(fmt.title("Upgrading from Juniper"))
tutor_env.save(context.obj.root, config)
click.echo(fmt.title("Stopping any existing platform"))
context.invoke(compose.stop)
if not config["RUN_MYSQL"]:
fmt.echo_info(
"You are not running MySQL (RUN_MYSQL=false). It is your "
"responsibility to upgrade your MySQL instance to v5.7. There is "
"nothing left to do to upgrade from Juniper."
)
return
click.echo(fmt.title("Upgrading MySQL from v5.6 to v5.7"))
context.invoke(compose.start, detach=True, services=["mysql"])
context.invoke(
compose.execute,
args=[
"mysql",
"bash",
"-e",
"-c",
"mysql_upgrade -u {} --password='{}'".format(
config["MYSQL_ROOT_USERNAME"], config["MYSQL_ROOT_PASSWORD"]
),
],
)
context.invoke(compose.stop)
def upgrade_from_koa(context: click.Context, config: Config) -> None:
if not config["RUN_MONGODB"]:
fmt.echo_info(
"You are not running MongDB (RUN_MONGODB=false). It is your "
"responsibility to upgrade your MongoDb instance to v4.0. There is "
"nothing left to do to upgrade from Koa to Lilac."
)
return
upgrade_mongodb(context, config, "4.0.25", "4.0")
def upgrade_mongodb(
context: click.Context,
config: Config,
to_docker_version: str,
to_compatibility_version: str,
) -> None:
click.echo(fmt.title("Upgrading MongoDb to v{}".format(to_docker_version)))
# Note that the DOCKER_IMAGE_MONGODB value is never saved, because we only save the
# environment, not the configuration.
config["DOCKER_IMAGE_MONGODB"] = "mongo:{}".format(to_docker_version)
tutor_env.save(context.obj.root, config)
context.invoke(compose.start, detach=True, services=["mongodb"])
fmt.echo_info("Waiting for mongodb to boot...")
sleep(10)
context.invoke(
compose.execute,
args=[
"mongodb",
"mongo",
"--eval",
'db.adminCommand({ setFeatureCompatibilityVersion: "%s" })'
% to_compatibility_version,
],
)
context.invoke(compose.stop)
local.add_command(quickstart)
local.add_command(upgrade)
compose.add_commands(local)