def build( context: Context, image_names: t.List[str], no_cache: bool, build_args: t.List[str], add_hosts: t.List[str], target: str, docker_args: t.List[str], ) -> None: config = tutor_config.load(context.root) command_args = [] if no_cache: command_args.append("--no-cache") for build_arg in build_args: command_args += ["--build-arg", build_arg] for add_host in add_hosts: command_args += ["--add-host", add_host] if target: command_args += ["--target", target] if docker_args: command_args += docker_args for image in image_names: for _name, path, tag, custom_args in find_images_to_build( config, image): images.build( tutor_env.pathjoin(context.root, *path), tag, *command_args, *custom_args, )
def copyfrom( context: BaseComposeContext, service: str, container_path: str, host_path: str ) -> None: # Path management container_root_path = "/tmp/mount" container_dst_path = container_root_path if not os.path.exists(host_path): # Emulate cp semantics, where if the destination path does not exist # then we copy to its parent and rename to the destination folder container_dst_path += "/" + os.path.basename(host_path) host_path = os.path.dirname(host_path) if not os.path.exists(host_path): raise TutorError( f"Cannot create directory {host_path}. No such file or directory." ) # cp/mv commands command = f"cp --recursive --preserve {container_path} {container_dst_path}" config = tutor_config.load(context.root) runner = context.job_runner(config) runner.docker_compose( "run", "--rm", "--no-deps", "--user=0", f"--volume={host_path}:{container_root_path}", service, "sh", "-e", "-c", command, )
def settheme( context: BaseComposeContext, domains: t.List[str], theme_name: str ) -> None: config = tutor_config.load(context.root) runner = context.job_runner(config) domains = domains or jobs.get_all_openedx_domains(config) jobs.set_theme(theme_name, domains, runner)
def stop(context: K8sContext, names: List[str]) -> None: config = tutor_config.load(context.root) names = names or ["all"] for name in names: if name == "all": delete_resources(config) else: delete_resources(config, name=name)
def init(context: K8sContext, limit: Optional[str]) -> None: config = tutor_config.load(context.root) runner = context.job_runner(config) wait_for_pod_ready(config, "caddy") for name in ["elasticsearch", "mysql", "mongodb"]: if tutor_config.is_service_activated(config, name): wait_for_pod_ready(config, name) jobs.initialise(runner, limit_to=limit)
def init( context: BaseComposeContext, limit: str, mounts: t.Tuple[t.List[MountParam.MountType]], ) -> None: process_mount_arguments(mounts) config = tutor_config.load(context.root) runner = context.job_runner(config) jobs.initialise(runner, limit_to=limit)
def scale(context: K8sContext, deployment: str, replicas: int) -> None: config = tutor_config.load(context.root) utils.kubectl( "scale", # Note that we don't use the full resource selector because selectors # are not compatible with the deployment/<name> argument. *resource_namespace_selector(config, ), f"--replicas={replicas}", f"deployment/{deployment}", )
def createuser( context: BaseComposeContext, superuser: str, staff: bool, password: str, name: str, email: str, ) -> None: config = tutor_config.load(context.root) runner = context.job_runner(config) command = jobs.create_user_command(superuser, staff, name, email, password=password) runner.run_job("lms", command)
def bindmount_command(context: BaseComposeContext, service: str, path: str) -> None: """ This command is made obsolete by the --mount arguments. """ fmt.echo_alert( "The 'bindmount' command is deprecated and will be removed in a later release. Use 'copyfrom' instead." ) config = tutor_config.load(context.root) host_path = bindmounts.create(context.job_runner(config), service, path) fmt.echo_info( f"Bind-mount volume created at {host_path}. You can now use it in all `local` and `dev` " f"commands with the `--volume={path}` option." )
def quickstart(context: click.Context, non_interactive: bool) -> None: run_upgrade_from_release = tutor_env.should_upgrade_from_release( context.obj.root) if run_upgrade_from_release is not None: click.echo(fmt.title("Upgrading from an older release")) context.invoke( upgrade, from_release=tutor_env.get_env_release(context.obj.root), ) click.echo(fmt.title("Interactive platform configuration")) config = tutor_config.load_minimal(context.obj.root) if not non_interactive: interactive_config.ask_questions(config, run_for_prod=True) tutor_config.save_config_file(context.obj.root, config) config = tutor_config.load_full(context.obj.root) tutor_env.save(context.obj.root, config) if run_upgrade_from_release and not non_interactive: question = f"""Your platform is being upgraded from {run_upgrade_from_release.capitalize()}. If you run custom Docker images, you must rebuild and push them to your private repository now by running the following commands in a different shell: tutor images build all # add your custom images here tutor images push all Press enter when you are ready to continue""" click.confirm(fmt.question(question), default=True, abort=True, prompt_suffix=" ") click.echo(fmt.title("Starting the platform")) context.invoke(start) click.echo(fmt.title("Database creation and migrations")) context.invoke(init, limit=None) config = tutor_config.load(context.obj.root) fmt.echo_info( """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"], ))
def restart(context: BaseComposeContext, services: t.List[str]) -> None: config = tutor_config.load(context.root) command = ["restart"] if "all" in services: pass else: for service in services: if service == "openedx": if config["RUN_LMS"]: command += ["lms", "lms-worker"] if config["RUN_CMS"]: command += ["cms", "cms-worker"] else: command.append(service) context.job_runner(config).docker_compose(*command)
def dc_command(context: BaseComposeContext, command: str, args: t.List[str]) -> None: config = tutor_config.load(context.root) volumes, non_volume_args = bindmounts.parse_volumes(args) volume_args = [] for volume_arg in volumes: if ":" not in volume_arg: # This is a bind-mounted volume from the "volumes/" folder. host_bind_path = bindmounts.get_path(context.root, volume_arg) if not os.path.exists(host_bind_path): raise TutorError( f"Bind-mount volume directory {host_bind_path} does not exist. It must first be created " f"with the '{bindmount_command.name}' command." ) volume_arg = f"{host_bind_path}:{volume_arg}" volume_args += ["--volume", volume_arg] context.job_runner(config).docker_compose(command, *volume_args, *non_volume_args)
def logs(context: K8sContext, container: str, follow: bool, tail: bool, service: str) -> None: config = tutor_config.load(context.root) command = ["logs"] selectors = ["app.kubernetes.io/name=" + service] if service else [] command += resource_selector(config, *selectors) if container: command += ["-c", container] if follow: command += ["--follow"] if tail is not None: command += ["--tail", str(tail)] utils.kubectl(*command)
def start( context: BaseComposeContext, skip_build: bool, detach: bool, mounts: t.Tuple[t.List[MountParam.MountType]], services: t.List[str], ) -> None: command = ["up", "--remove-orphans"] if not skip_build: command.append("--build") if detach: command.append("-d") process_mount_arguments(mounts) # Start services config = tutor_config.load(context.root) context.job_runner(config).docker_compose(*command, *services)
def upgrade_from(context: Context, from_release: str) -> None: config = tutor_config.load(context.root) running_release = from_release if running_release == "ironwood": upgrade_from_ironwood(config) running_release = "juniper" if running_release == "juniper": upgrade_from_juniper(config) running_release = "koa" if running_release == "koa": upgrade_from_koa(config) running_release = "lilac" if running_release == "lilac": upgrade_from_lilac(config) running_release = "maple"
def runserver( context: click.Context, mounts: t.Tuple[t.List[compose.MountParam.MountType]], options: t.List[str], service: str, ) -> None: depr_warning = "'runserver' is deprecated and will be removed in a future release. Use 'start' instead." for option in options: if option.startswith("-v") or option.startswith("--volume"): depr_warning += " Bind-mounts can be specified using '-m/--mount'." break fmt.echo_alert(depr_warning) config = tutor_config.load(context.obj.root) if service in ["lms", "cms"]: port = 8000 if service == "lms" else 8001 host = config["LMS_HOST"] if service == "lms" else config["CMS_HOST"] fmt.echo_info( f"The {service} service will be available at http://{host}:{port}" ) args = ["--service-ports", *options, service] context.invoke(compose.run, mounts=mounts, args=args)
def start(context: K8sContext, names: List[str]) -> None: config = tutor_config.load(context.root) # Create namespace, if necessary # Note that this step should not be run for some users, in particular those # who do not have permission to edit the namespace. try: utils.kubectl("get", "namespaces", k8s_namespace(config)) fmt.echo_info("Namespace already exists: skipping creation.") except exceptions.TutorError: fmt.echo_info("Namespace does not exist: now creating it...") kubectl_apply( context.root, "--wait", "--selector", "app.kubernetes.io/component=namespace", ) names = names or ["all"] for name in names: if name == "all": # Create volumes kubectl_apply( context.root, "--wait", "--selector", "app.kubernetes.io/component=volume", ) # Create everything else except jobs kubectl_apply( context.root, "--selector", "app.kubernetes.io/component notin (job,volume,namespace)", ) else: kubectl_apply( context.root, "--selector", f"app.kubernetes.io/name={name}", )
def exec_command(context: K8sContext, service: str, args: List[str]) -> None: config = tutor_config.load(context.root) kubectl_exec(config, service, args)
def importdemocourse(context: BaseComposeContext) -> None: config = tutor_config.load(context.root) runner = context.job_runner(config) fmt.echo_info("Importing demo course") jobs.import_demo_course(runner)
def wait(context: K8sContext, name: str) -> None: config = tutor_config.load(context.root) wait_for_pod_ready(config, name)
def status(context: K8sContext) -> int: config = tutor_config.load(context.root) return utils.kubectl("get", "all", *resource_namespace_selector(config))
def stop(context: BaseComposeContext, services: t.List[str]) -> None: config = tutor_config.load(context.root) context.job_runner(config).docker_compose("stop", *services)
def quickstart( context: click.Context, mounts: t.Tuple[t.List[compose.MountParam.MountType]], non_interactive: bool, pullimages: bool, ) -> None: try: utils.check_macos_docker_memory() except exceptions.TutorError as e: fmt.echo_alert( f"""Could not verify sufficient RAM allocation in Docker: {e} Tutor may not work if Docker is configured with < 4 GB RAM. Please follow instructions from: https://docs.tutor.overhang.io/install.html""") run_upgrade_from_release = tutor_env.should_upgrade_from_release( context.obj.root) if run_upgrade_from_release is not None: click.echo(fmt.title("Upgrading from an older release")) if not non_interactive: to_release = tutor_env.get_package_release() question = f"""You are about to upgrade your Open edX platform from {run_upgrade_from_release.capitalize()} to {to_release.capitalize()} 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=" ") context.invoke( upgrade, from_release=run_upgrade_from_release, ) click.echo(fmt.title("Interactive platform configuration")) config = tutor_config.load_minimal(context.obj.root) if not non_interactive: interactive_config.ask_questions(config) tutor_config.save_config_file(context.obj.root, config) config = tutor_config.load_full(context.obj.root) tutor_env.save(context.obj.root, config) if run_upgrade_from_release and not non_interactive: question = f"""Your platform is being upgraded from {run_upgrade_from_release.capitalize()}. If you run custom Docker images, you must rebuild them now by running the following command in a different shell: tutor images build all # list your custom images here See the documentation for more information: https://docs.tutor.overhang.io/install.html#upgrading-to-a-new-open-edx-release Press enter when you are ready to continue""" click.confirm(fmt.question(question), default=True, abort=True, prompt_suffix=" ") 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, mounts=mounts, detach=True) click.echo(fmt.title("Database creation and migrations")) context.invoke(compose.init, mounts=mounts) 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"], ))
def print_result(context, client_func_name, *args, **kwargs): user_config = tutor_config.load(context.root) client = Client(user_config, url=context.url) func = getattr(client, client_func_name) result = func(*args, **kwargs) print(json.dumps(result, indent=2))