def upgrade_from_lilac(config: Config) -> None: if not plugins.is_installed("forum"): fmt.echo_alert( "The Open edX forum feature was moved to a separate plugin in Maple. To keep using this feature, " "you must install and enable the tutor-forum plugin: https://github.com/overhangio/tutor-forum" ) elif not plugins.is_loaded("forum"): fmt.echo_info( "The Open edX forum feature was moved to a separate plugin in Maple. To keep using this feature, " "we will now enable the 'forum' plugin. If you do not want to use this feature, you should disable the " "plugin with: `tutor plugins disable forum`.") plugins.load("forum") tutor_config.save_enabled_plugins(config) if not plugins.is_installed("mfe"): fmt.echo_alert( "In Maple the legacy courseware is no longer supported. You need to install and enable the 'mfe' plugin " "to make use of the new learning microfrontend: https://github.com/overhangio/tutor-mfe" ) elif not plugins.is_loaded("mfe"): fmt.echo_info( "In Maple the legacy courseware is no longer supported. To start using the new learning microfrontend, " "we will now enable the 'mfe' plugin. If you do not want to use this feature, you should disable the " "plugin with: `tutor plugins disable mfe`.") plugins.load("mfe") tutor_config.save_enabled_plugins(config)
def __init__(self, config: t.Optional[Config] = None): config = config or {} self.config = deepcopy(config) self.template_roots = hooks.Filters.ENV_TEMPLATE_ROOTS.apply( [TEMPLATES_ROOT]) # Create environment with extra filters and globals self.environment = JinjaEnvironment(self.template_roots) # Filters plugin_filters: t.Iterator[t.Tuple[ str, JinjaFilter]] = hooks.Filters.ENV_TEMPLATE_FILTERS.iterate() for name, func in plugin_filters: if name in self.environment.filters: fmt.echo_alert( f"Found conflicting template filters named '{name}'") self.environment.filters[name] = func self.environment.filters["walk_templates"] = self.walk_templates # Globals plugin_globals: t.Iterator[t.Tuple[ str, JinjaFilter]] = hooks.Filters.ENV_TEMPLATE_VARIABLES.iterate() for name, value in plugin_globals: if name in self.environment.globals: fmt.echo_alert( f"Found conflicting template variables named '{name}'") self.environment.globals[name] = value self.environment.globals["iter_values_named"] = self.iter_values_named self.environment.globals["patch"] = self.patch
def set_theme(theme_name: str, domain_names: t.List[str], runner: BaseJobRunner) -> None: """ For each domain, get or create a Site object and assign the selected theme. """ if not domain_names: return python_code = "from django.contrib.sites.models import Site" for domain_name in domain_names: if len(domain_name) > 50: fmt.echo_alert( "Assigning a theme to a site with a long (> 50 characters) domain name." " The displayed site name will be truncated to 50 characters.") python_code += """ print('Assigning theme {theme_name} to {domain_name}...') site, _ = Site.objects.get_or_create(domain='{domain_name}') if not site.name: name_max_length = Site._meta.get_field('name').max_length name = '{domain_name}'[:name_max_length] site.name = name site.save() site.themes.all().delete() site.themes.create(theme_dir_name='{theme_name}') """.format(theme_name=theme_name, domain_name=domain_name) command = BASE_OPENEDX_COMMAND + f'./manage.py lms shell -c "{python_code}"' runner.run_job("lms", command)
def __init__( self, config: Config, template_roots: t.List[str], ignore_folders: t.Optional[t.List[str]] = None, ): self.config = deepcopy(config) self.template_roots = template_roots self.ignore_folders = ignore_folders or [] self.ignore_folders.append(".git") # Create environment with extra filters and globals self.environment = JinjaEnvironment(template_roots) # Filters plugin_filters: t.Iterator[ t.Tuple[str, JinjaFilter] ] = hooks.Filters.ENV_TEMPLATE_FILTERS.iterate() for name, func in plugin_filters: if name in self.environment.filters: fmt.echo_alert(f"Found conflicting template filters named '{name}'") self.environment.filters[name] = func self.environment.filters["walk_templates"] = self.walk_templates # Globals plugin_globals: t.Iterator[ t.Tuple[str, JinjaFilter] ] = hooks.Filters.ENV_TEMPLATE_VARIABLES.iterate() for name, value in plugin_globals: if name in self.environment.globals: fmt.echo_alert(f"Found conflicting template variables named '{name}'") self.environment.globals[name] = value self.environment.globals["iter_values_named"] = self.iter_values_named self.environment.globals["patch"] = self.patch
def check_is_up_to_date(root: str) -> None: if not is_up_to_date(root): fmt.echo_alert( f"The current environment stored at {base_dir(root)} is not up-to-date: it is at " f"v{current_version(root)} while the 'tutor' binary is at v{__version__}. You should upgrade " f"the environment by running:\n" f"\n" f" tutor config save")
def cli(context: click.Context, root: str, show_help: bool) -> None: if utils.is_root(): fmt.echo_alert( "You are running Tutor as root. This is strongly not recommended. If you are doing this in order to access" " the Docker daemon, you should instead add your user to the 'docker' group. (see https://docs.docker.com" "/install/linux/linux-postinstall/#manage-docker-as-a-non-root-user)" ) context.obj = Context(root) if context.invoked_subcommand is None or show_help: click.echo(context.get_help())
def upgrade(context: click.Context, from_release: t.Optional[str]) -> None: fmt.echo_alert( "This command only performs a partial upgrade of your Open edX platform. " "To perform a full upgrade, you should run `tutor local quickstart`.") if from_release is None: from_release = tutor_env.get_env_release(context.obj.root) if from_release is None: fmt.echo_info("Your environment is already up-to-date") else: upgrade_from(context, from_release) # We update the environment to update the version context.invoke(config_save_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 load_all(names: t.Iterable[str]) -> None: """ Load all plugins one by one. Plugins are loaded in alphabetical order. We ignore plugins which failed to load. After all plugins have been loaded, the PLUGINS_LOADED action is triggered. """ names = sorted(set(names)) for name in names: try: load(name) except Exception as e: fmt.echo_alert(f"Failed to enable plugin '{name}': {e}") hooks.Actions.PLUGINS_LOADED.do()
def quickstart(context: click.Context, 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""" ) 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=False) tutor_config.save_config_file(context.obj.root, config) config = tutor_config.load_full(context.obj.root) tutor_env.save(context.obj.root, config) 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("Building Docker image for LMS and CMS development")) context.invoke(compose.dc_command, command="build", args=["lms"]) 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) 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}:8000 {http}://{cms_host}:8001 """.format( http="https" if config["ENABLE_HTTPS"] else "http", lms_host=config["LMS_HOST"], cms_host=config["CMS_HOST"], ) )
def get_base() -> Config: """ Load the base configuration. Entries in this configuration are unrendered. """ base = get_template("base.yml") extra_config: t.List[t.Tuple[str, ConfigValue]] = [] extra_config = hooks.Filters.CONFIG_UNIQUE.apply(extra_config) extra_config = hooks.Filters.CONFIG_OVERRIDES.apply(extra_config) for name, value in extra_config: if name in base: fmt.echo_alert( f"Found conflicting values for setting '{name}': '{value}' or '{base[name]}'" ) base[name] = value return base
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 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"], ))