From c2e7ee98dfe309c13a3b512e863d4b187349feda Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 18:08:18 +0100 Subject: [PATCH] feat(Mainsail): implement Mainsail installer Signed-off-by: Dominik Willner --- kiauh.cfg.example | 4 + kiauh/core/menus/install_menu.py | 3 +- kiauh/modules/mainsail/__init__.py | 26 ++ kiauh/modules/mainsail/mainsail_dialogs.py | 95 ++++++ kiauh/modules/mainsail/mainsail_setup.py | 272 ++++++++++++++++++ .../mainsail/res/mainsail-config-updater.conf | 6 + .../mainsail/res/mainsail-updater.conf | 5 + 7 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 kiauh/modules/mainsail/__init__.py create mode 100644 kiauh/modules/mainsail/mainsail_dialogs.py create mode 100644 kiauh/modules/mainsail/mainsail_setup.py create mode 100644 kiauh/modules/mainsail/res/mainsail-config-updater.conf create mode 100644 kiauh/modules/mainsail/res/mainsail-updater.conf diff --git a/kiauh.cfg.example b/kiauh.cfg.example index 72f4a21..e1724d2 100644 --- a/kiauh.cfg.example +++ b/kiauh.cfg.example @@ -10,3 +10,7 @@ method: https repository_url: https://github.com/Arksine/moonraker branch: master method: https + +[mainsail] +default_port: 80 +unstable_releases: False diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 03c82f1..cd72444 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -14,6 +14,7 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper import klipper_setup +from kiauh.modules.mainsail import mainsail_setup from kiauh.modules.moonraker import moonraker_setup from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT @@ -69,7 +70,7 @@ class InstallMenu(BaseMenu): moonraker_setup.run_moonraker_setup(install=True) def install_mainsail(self): - print("install_mainsail") + mainsail_setup.run_mainsail_setup(install=True) def install_fluidd(self): print("install_fluidd") diff --git a/kiauh/modules/mainsail/__init__.py b/kiauh/modules/mainsail/__init__.py new file mode 100644 index 0000000..06f2027 --- /dev/null +++ b/kiauh/modules/mainsail/__init__.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 Dominik Willner # +# # +# This file is part of KIAUH - Klipper Installation And Update Helper # +# https://github.com/dw-0/kiauh # +# # +# This file may be distributed under the terms of the GNU GPLv3 license # +# ======================================================================= # + +from pathlib import Path + +import os + +MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) +MAINSAIL_DIR = os.path.join(Path.home(), "mainsail") +MAINSAIL_CONFIG_DIR = os.path.join(Path.home(), "mainsail-config") +MAINSAIL_CONFIG_JSON = os.path.join(MAINSAIL_DIR, "config.json") +MAINSAIL_URL = ( + "https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip" +) +MAINSAIL_UNSTABLE_URL = ( + "https://github.com/mainsail-crew/mainsail/releases/download/%TAG%/mainsail.zip" +) +MAINSAIL_CONFIG_REPO_URL = "https://github.com/mainsail-crew/mainsail-config.git" diff --git a/kiauh/modules/mainsail/mainsail_dialogs.py b/kiauh/modules/mainsail/mainsail_dialogs.py new file mode 100644 index 0000000..0c60bbf --- /dev/null +++ b/kiauh/modules/mainsail/mainsail_dialogs.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 Dominik Willner # +# # +# This file is part of KIAUH - Klipper Installation And Update Helper # +# https://github.com/dw-0/kiauh # +# # +# This file may be distributed under the terms of the GNU GPLv3 license # +# ======================================================================= # + +import textwrap + +from kiauh.core.menus.base_menu import print_back_footer +from kiauh.utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN + + +def print_moonraker_not_found_dialog(): + line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" + line2 = f"{COLOR_YELLOW}No local Moonraker installation was found!{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | {line1:<63}| + | {line2:<63}| + |-------------------------------------------------------| + | It is possible to install Mainsail without a local | + | Moonraker installation. If you continue, you need to | + | make sure, that Moonraker is installed on another | + | machine in your network. Otherwise Mainsail will NOT | + | work correctly. | + """ + )[1:] + + print(dialog, end="") + print_back_footer() + + +def print_mainsail_already_installed_dialog(): + line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" + line2 = f"{COLOR_YELLOW}Mainsail seems to be already installed!{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | {line1:<63}| + | {line2:<63}| + |-------------------------------------------------------| + | If you continue, your current Mainsail installation | + | will be overwritten. You will not loose any printer | + | configurations and the Moonraker database will remain | + | untouched. | + """ + )[1:] + + print(dialog, end="") + print_back_footer() + + +def print_install_mainsail_config_dialog(): + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | It is recommended to use special macros in order to | + | have Mainsail fully functional and working. | + | | + | The recommended macros for Mainsail can be seen here: | + | https://github.com/mainsail-crew/mainsail-config | + | | + | If you already use these macros skip this step. | + | Otherwise you should consider to answer with 'Y' to | + | download the recommended macros. | + \\=======================================================/ + """ + )[1:] + + print(dialog, end="") + + +def print_mainsail_port_select_dialog(port: str): + port = f"{COLOR_CYAN}{port}{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | Please select the port, Mainsail should be served on. | + | If you are unsure what to select, hit Enter to apply | + | the suggested value of: {port:38} | + | | + | In case you need Mainsail to be served on a specific | + | port, you can set it now. Make sure the port is not | + | used by any other application on your system! | + \\=======================================================/ + """ + )[1:] + + print(dialog, end="") diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py new file mode 100644 index 0000000..3dc3607 --- /dev/null +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 Dominik Willner # +# # +# This file is part of KIAUH - Klipper Installation And Update Helper # +# https://github.com/dw-0/kiauh # +# # +# This file may be distributed under the terms of the GNU GPLv3 license # +# ======================================================================= # + +import json +import os.path +import shutil +from pathlib import Path +from typing import List + +from kiauh import KIAUH_CFG +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.core.repo_manager.repo_manager import RepoManager +from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.mainsail import ( + MAINSAIL_URL, + MAINSAIL_DIR, + MAINSAIL_CONFIG_DIR, + MAINSAIL_CONFIG_REPO_URL, + MAINSAIL_CONFIG_JSON, + MODULE_PATH, +) +from kiauh.modules.mainsail.mainsail_dialogs import ( + print_moonraker_not_found_dialog, + print_mainsail_already_installed_dialog, + print_install_mainsail_config_dialog, + print_mainsail_port_select_dialog, +) +from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.utils.common import check_install_dependencies +from kiauh.utils.input_utils import get_confirm, get_number_input +from kiauh.utils.logger import Logger +from kiauh.utils.system_utils import ( + download_file, + unzip, + create_upstream_nginx_cfg, + create_nginx_cfg, + delete_default_nginx_cfg, + enable_nginx_cfg, + set_nginx_permissions, + get_ipv4_addr, + control_systemd_service, + create_common_vars_nginx_cfg, +) + + +def run_mainsail_setup(install: bool) -> None: + im_mr = InstanceManager(Moonraker) + is_moonraker_installed = len(im_mr.instances) > 0 + + enable_remotemode = False + if not is_moonraker_installed: + print_moonraker_not_found_dialog() + do_continue = get_confirm("Continue Mainsail installation?", allow_go_back=True) + if do_continue: + enable_remotemode = True + else: + return + + is_mainsail_installed = Path(f"{Path.home()}/mainsail").exists() + do_reinstall = False + if is_mainsail_installed: + print_mainsail_already_installed_dialog() + do_reinstall = get_confirm("Re-install Mainsail?", allow_go_back=True) + if do_reinstall: + backup_config_json() + else: + return + + im_kl = InstanceManager(Klipper) + is_klipper_installed = len(im_kl.instances) > 0 + install_ms_config = False + if is_klipper_installed: + print_install_mainsail_config_dialog() + question = "Download the recommended macros?" + install_ms_config = get_confirm(question, allow_go_back=False) + + cm = ConfigManager(cfg_file=KIAUH_CFG) + cm.read_config() + default_port = cm.get_value("mainsail", "default_port") + mainsail_port = default_port if default_port else 80 + if not default_port: + print_mainsail_port_select_dialog(f"{mainsail_port}") + mainsail_port = get_number_input( + "Configure Mainsail for port", + min_count=mainsail_port, + default=mainsail_port, + ) + + check_install_dependencies(["nginx"]) + + try: + download_mainsail() + if do_reinstall: + restore_config_json() + if enable_remotemode: + enable_mainsail_remotemode() + if is_moonraker_installed: + patch_moonraker_conf( + im_mr.instances, + "Mainsail", + "update_manager mainsail", + "mainsail-updater.conf", + ) + im_mr.restart_all_instance() + if is_klipper_installed and install_ms_config: + download_mainsail_config() + patch_moonraker_conf( + im_mr.instances, + "mainsail-config", + "update_manager mainsail-config", + "mainsail-config-updater.conf", + ) + patch_printer_config(im_kl.instances) + im_kl.restart_all_instance() + + create_upstream_nginx_cfg() + create_common_vars_nginx_cfg() + create_mainsail_nginx_cfg(mainsail_port) + if is_klipper_installed: + symlink_webui_nginx_log(im_kl.instances) + control_systemd_service("nginx", "restart") + + except Exception as e: + Logger.print_error(f"Mainsail installation failed!\n{e}") + return + + log = f"Open Mainsail now on: http://{get_ipv4_addr()}:{mainsail_port}" + Logger.print_ok("Mainsail installation complete!", start="\n") + Logger.print_ok(log, prefix=False, end="\n\n") + + +def download_mainsail() -> None: + try: + Logger.print_status("Downloading Mainsail ...") + download_file(MAINSAIL_URL, f"{Path.home()}", "mainsail.zip") + Logger.print_ok("Download complete!") + + Logger.print_status("Extracting mainsail.zip ...") + unzip(f"{Path.home()}/mainsail.zip", MAINSAIL_DIR) + Logger.print_ok("OK!") + + except Exception: + Logger.print_error("Downloading Mainsail failed!") + raise + + +def download_mainsail_config() -> None: + try: + Logger.print_status("Downloading mainsail-config ...") + rm = RepoManager(MAINSAIL_CONFIG_REPO_URL, target_dir=MAINSAIL_CONFIG_DIR) + rm.clone_repo() + except Exception: + Logger.print_error("Downloading mainsail-config failed!") + raise + + +def create_mainsail_nginx_cfg(port: int) -> None: + try: + Logger.print_status("Creating NGINX config for Mainsail ...") + root_dir = MAINSAIL_DIR + delete_default_nginx_cfg() + create_nginx_cfg("mainsail", port, root_dir) + enable_nginx_cfg("mainsail") + set_nginx_permissions() + Logger.print_ok("NGINX config for Mainsail successfully created.") + except Exception: + Logger.print_error("Creating NGINX config for Mainsail failed!") + raise + + +def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: + Logger.print_status("Link NGINX logs into log directory ...") + access_log = Path("/var/log/nginx/mainsail-access.log") + error_log = Path("/var/log/nginx/mainsail-error.log") + + for instance in klipper_instances: + desti_access = Path(instance.log_dir).joinpath("mainsail-access.log") + if not desti_access.exists(): + desti_access.symlink_to(access_log) + + desti_error = Path(instance.log_dir).joinpath("mainsail-error.log") + if not desti_error.exists(): + desti_error.symlink_to(error_log) + + +def patch_moonraker_conf( + moonraker_instances: List[Moonraker], + name: str, + section_name: str, + template_file: str, +) -> None: + for instance in moonraker_instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Add {name} update section to '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + return + + cm = ConfigManager(cfg_file) + cm.read_config() + if cm.config.has_section(section_name): + Logger.print_info("Section already exist. Skipped ...") + return + + template = os.path.join(MODULE_PATH, "res", template_file) + with open(template, "r") as t: + template_content = "\n" + template_content += t.read() + + with open(cfg_file, "a") as f: + f.write(template_content) + + +def patch_printer_config(klipper_instances: List[Klipper]) -> None: + for instance in klipper_instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Including mainsail-config in '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + return + + cm = ConfigManager(cfg_file) + cm.read_config() + if cm.config.has_section("include mainsail.cfg"): + Logger.print_info("Section already exist. Skipped ...") + return + + with open(cfg_file, "a") as f: + f.write("\n[include mainsail.cfg]") + + +def backup_config_json() -> None: + try: + Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") + target = os.path.join(Path.home(), "config.json.kiauh.bak") + shutil.copy(MAINSAIL_CONFIG_JSON, target) + except OSError: + Logger.print_info(f"Unable to backup config.json. Skipped ...") + + +def restore_config_json() -> None: + try: + Logger.print_status(f"Restore '{MAINSAIL_CONFIG_JSON}' ...") + source = os.path.join(Path.home(), "config.json.kiauh.bak") + shutil.copy(source, MAINSAIL_CONFIG_JSON) + except OSError: + Logger.print_info(f"Unable to restore config.json. Skipped ...") + + +def enable_mainsail_remotemode() -> None: + with open(MAINSAIL_CONFIG_JSON, "r") as f: + config_data = json.load(f) + + if config_data["instancesDB"] == "browser": + return + + Logger.print_status("Setting instance storage location to 'browser' ...") + config_data["instancesDB"] = "browser" + + with open(MAINSAIL_CONFIG_JSON, "w") as f: + json.dump(config_data, f, indent=4) diff --git a/kiauh/modules/mainsail/res/mainsail-config-updater.conf b/kiauh/modules/mainsail/res/mainsail-config-updater.conf new file mode 100644 index 0000000..02bb789 --- /dev/null +++ b/kiauh/modules/mainsail/res/mainsail-config-updater.conf @@ -0,0 +1,6 @@ +[update_manager mainsail-config] +type: git_repo +primary_branch: master +path: ~/mainsail-config +origin: https://github.com/mainsail-crew/mainsail-config.git +managed_services: klipper diff --git a/kiauh/modules/mainsail/res/mainsail-updater.conf b/kiauh/modules/mainsail/res/mainsail-updater.conf new file mode 100644 index 0000000..f668332 --- /dev/null +++ b/kiauh/modules/mainsail/res/mainsail-updater.conf @@ -0,0 +1,5 @@ +[update_manager mainsail] +type: web +channel: stable +repo: mainsail-crew/mainsail +path: ~/mainsail