From 1620efe56c79cb0fa965ea10be8f172646e0e798 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 2 Mar 2024 17:22:37 +0100 Subject: [PATCH] refactor(KIAUH): full refactor of client and client-config installation Signed-off-by: Dominik Willner --- kiauh/components/fluidd/__init__.py | 26 -- .../fluidd/assets/fluidd-config-updater.conf | 6 - .../fluidd/assets/fluidd-updater.conf | 5 - kiauh/components/fluidd/fluidd_remove.py | 160 ----------- kiauh/components/fluidd/fluidd_setup.py | 242 ----------------- kiauh/components/fluidd/fluidd_utils.py | 79 ------ .../fluidd/menus/fluidd_remove_menu.py | 111 -------- .../klipper/menus/klipper_remove_menu.py | 4 +- kiauh/components/mainsail/__init__.py | 27 -- .../assets/mainsail-config-updater.conf | 6 - .../mainsail/assets/mainsail-updater.conf | 5 - kiauh/components/mainsail/mainsail_dialogs.py | 95 ------- kiauh/components/mainsail/mainsail_remove.py | 164 ----------- kiauh/components/mainsail/mainsail_setup.py | 256 ------------------ kiauh/components/mainsail/mainsail_utils.py | 117 -------- .../mainsail/menus/mainsail_remove_menu.py | 122 --------- .../moonraker/menus/moonraker_remove_menu.py | 4 +- kiauh/components/moonraker/moonraker_setup.py | 4 +- kiauh/components/moonraker/moonraker_utils.py | 4 +- kiauh/components/webui_client/__init__.py | 77 ++++++ .../client_config}/__init__.py | 0 .../client_config/client_config_remove.py | 64 +++++ .../client_config/client_config_setup.py | 145 ++++++++++ .../client_dialogs.py} | 69 ++--- .../components/webui_client/client_remove.py | 75 +++++ kiauh/components/webui_client/client_setup.py | 207 ++++++++++++++ kiauh/components/webui_client/client_utils.py | 237 ++++++++++++++++ .../menus/__init__.py | 0 .../webui_client/menus/client_remove_menu.py | 149 ++++++++++ kiauh/core/menus/backup_menu.py | 41 ++- kiauh/core/menus/install_menu.py | 67 ++--- kiauh/core/menus/main_menu.py | 31 ++- kiauh/core/menus/remove_menu.py | 8 +- kiauh/core/menus/update_menu.py | 116 ++++---- kiauh/utils/filesystem_utils.py | 112 +++++++- 35 files changed, 1248 insertions(+), 1587 deletions(-) delete mode 100644 kiauh/components/fluidd/__init__.py delete mode 100644 kiauh/components/fluidd/assets/fluidd-config-updater.conf delete mode 100644 kiauh/components/fluidd/assets/fluidd-updater.conf delete mode 100644 kiauh/components/fluidd/fluidd_remove.py delete mode 100644 kiauh/components/fluidd/fluidd_setup.py delete mode 100644 kiauh/components/fluidd/fluidd_utils.py delete mode 100644 kiauh/components/fluidd/menus/fluidd_remove_menu.py delete mode 100644 kiauh/components/mainsail/__init__.py delete mode 100644 kiauh/components/mainsail/assets/mainsail-config-updater.conf delete mode 100644 kiauh/components/mainsail/assets/mainsail-updater.conf delete mode 100644 kiauh/components/mainsail/mainsail_dialogs.py delete mode 100644 kiauh/components/mainsail/mainsail_remove.py delete mode 100644 kiauh/components/mainsail/mainsail_setup.py delete mode 100644 kiauh/components/mainsail/mainsail_utils.py delete mode 100644 kiauh/components/mainsail/menus/mainsail_remove_menu.py create mode 100644 kiauh/components/webui_client/__init__.py rename kiauh/components/{fluidd/menus => webui_client/client_config}/__init__.py (100%) create mode 100644 kiauh/components/webui_client/client_config/client_config_remove.py create mode 100644 kiauh/components/webui_client/client_config/client_config_setup.py rename kiauh/components/{fluidd/fluidd_dialogs.py => webui_client/client_dialogs.py} (75%) create mode 100644 kiauh/components/webui_client/client_remove.py create mode 100644 kiauh/components/webui_client/client_setup.py create mode 100644 kiauh/components/webui_client/client_utils.py rename kiauh/components/{mainsail => webui_client}/menus/__init__.py (100%) create mode 100644 kiauh/components/webui_client/menus/client_remove_menu.py diff --git a/kiauh/components/fluidd/__init__.py b/kiauh/components/fluidd/__init__.py deleted file mode 100644 index 70bc91f..0000000 --- a/kiauh/components/fluidd/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 - -from core.backup_manager import BACKUP_ROOT_DIR - -MODULE_PATH = Path(__file__).resolve().parent -FLUIDD_DIR = Path.home().joinpath("fluidd") -FLUIDD_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("fluidd-backups") -FLUIDD_CONFIG_DIR = Path.home().joinpath("fluidd-config") -FLUIDD_NGINX_CFG = Path("/etc/nginx/sites-enabled/fluidd") -FLUIDD_URL = "https://github.com/fluidd-core/fluidd/releases/latest/download/fluidd.zip" -FLUIDD_UNSTABLE_URL = ( - "https://github.com/fluidd-core/fluidd/releases/download/%TAG%/fluidd.zip" -) -FLUIDD_CONFIG_REPO_URL = "https://github.com/fluidd-core/fluidd-config.git" - diff --git a/kiauh/components/fluidd/assets/fluidd-config-updater.conf b/kiauh/components/fluidd/assets/fluidd-config-updater.conf deleted file mode 100644 index bc4a5cc..0000000 --- a/kiauh/components/fluidd/assets/fluidd-config-updater.conf +++ /dev/null @@ -1,6 +0,0 @@ -[update_manager fluidd-config] -type: git_repo -primary_branch: master -path: ~/fluidd-config -origin: https://github.com/fluidd-core/fluidd-config.git -managed_services: klipper diff --git a/kiauh/components/fluidd/assets/fluidd-updater.conf b/kiauh/components/fluidd/assets/fluidd-updater.conf deleted file mode 100644 index d9d1e7d..0000000 --- a/kiauh/components/fluidd/assets/fluidd-updater.conf +++ /dev/null @@ -1,5 +0,0 @@ -[update_manager fluidd] -type: web -channel: stable -repo: fluidd-core/fluidd -path: ~/fluidd diff --git a/kiauh/components/fluidd/fluidd_remove.py b/kiauh/components/fluidd/fluidd_remove.py deleted file mode 100644 index b617cec..0000000 --- a/kiauh/components/fluidd/fluidd_remove.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 shutil -import subprocess -from pathlib import Path -from typing import List - -from components.klipper.klipper import Klipper -from components.fluidd import FLUIDD_DIR, FLUIDD_CONFIG_DIR -from components.moonraker.moonraker import Moonraker -from core.config_manager.config_manager import ConfigManager -from core.instance_manager.instance_manager import InstanceManager -from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from utils.filesystem_utils import remove_file -from utils.logger import Logger - - -def run_fluidd_removal( - remove_fluidd: bool, - remove_fl_config: bool, - remove_mr_updater_section: bool, - remove_flc_printer_cfg_include: bool, -) -> None: - if remove_fluidd: - remove_fluidd_dir() - remove_nginx_config() - remove_nginx_logs() - if remove_mr_updater_section: - remove_updater_section("update_manager fluidd") - if remove_fl_config: - remove_fluidd_cfg_dir() - remove_fluidd_cfg_symlink() - if remove_mr_updater_section: - remove_updater_section("update_manager fluidd-config") - if remove_flc_printer_cfg_include: - remove_printer_cfg_include() - - -def remove_fluidd_dir() -> None: - Logger.print_status("Removing Fluidd ...") - if not FLUIDD_DIR.exists(): - Logger.print_info(f"'{FLUIDD_DIR}' does not exist. Skipping ...") - return - - try: - shutil.rmtree(FLUIDD_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{FLUIDD_DIR}':\n{e}") - - -def remove_nginx_config() -> None: - Logger.print_status("Removing Fluidd NGINX config ...") - try: - remove_file(NGINX_SITES_AVAILABLE.joinpath("fluidd"), True) - remove_file(NGINX_SITES_ENABLED.joinpath("fluidd"), True) - - except subprocess.CalledProcessError as e: - log = f"Unable to remove Fluidd NGINX config:\n{e.stderr.decode()}" - Logger.print_error(log) - - -def remove_nginx_logs() -> None: - Logger.print_status("Removing Fluidd NGINX logs ...") - try: - remove_file(Path("/var/log/nginx/fluidd-access.log"), True) - remove_file(Path("/var/log/nginx/fluidd-error.log"), True) - - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - if not instances: - return - - for instance in instances: - remove_file(instance.log_dir.joinpath("fluidd-access.log")) - remove_file(instance.log_dir.joinpath("fluidd-error.log")) - - except (OSError, subprocess.CalledProcessError) as e: - Logger.print_error(f"Unable to NGINX logs:\n{e}") - - -def remove_updater_section(name: str) -> None: - Logger.print_status("Remove updater section from moonraker.conf ...") - im = InstanceManager(Moonraker) - instances: List[Moonraker] = im.instances - if not instances: - Logger.print_info("Moonraker not installed. Skipped ...") - return - - for instance in instances: - Logger.print_status(f"Remove section '{name}' in '{instance.cfg_file}' ...") - - if not instance.cfg_file.is_file(): - Logger.print_info(f"'{instance.cfg_file}' does not exist. Skipped ...") - continue - - cm = ConfigManager(instance.cfg_file) - if not cm.config.has_section(name): - Logger.print_info("Section not present. Skipped ...") - continue - - cm.config.remove_section(name) - cm.write_config() - - -def remove_fluidd_cfg_dir() -> None: - Logger.print_status("Removing fluidd-config ...") - if not FLUIDD_CONFIG_DIR.exists(): - Logger.print_info(f"'{FLUIDD_CONFIG_DIR}' does not exist. Skipping ...") - return - - try: - shutil.rmtree(FLUIDD_CONFIG_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{FLUIDD_CONFIG_DIR}':\n{e}") - - -def remove_fluidd_cfg_symlink() -> None: - Logger.print_status("Removing fluidd.cfg symlinks ...") - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - for instance in instances: - Logger.print_status(f"Removing symlink from '{instance.cfg_file}' ...") - try: - remove_file(instance.cfg_dir.joinpath("fluidd.cfg")) - except subprocess.CalledProcessError: - Logger.print_error("Failed to remove symlink!") - - -def remove_printer_cfg_include() -> None: - Logger.print_status("Remove fluidd-config include from printer.cfg ...") - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - if not instances: - Logger.print_info("Klipper not installed. Skipping ...") - return - - for instance in instances: - log = f"Removing include from '{instance.cfg_file}' ..." - Logger.print_status(log) - - if not instance.cfg_file.is_file(): - continue - - cm = ConfigManager(instance.cfg_file) - if not cm.config.has_section("include fluidd.cfg"): - Logger.print_info("Section not present. Skipped ...") - continue - - cm.config.remove_section("include fluidd.cfg") - cm.write_config() diff --git a/kiauh/components/fluidd/fluidd_setup.py b/kiauh/components/fluidd/fluidd_setup.py deleted file mode 100644 index 7c23bb5..0000000 --- a/kiauh/components/fluidd/fluidd_setup.py +++ /dev/null @@ -1,242 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 subprocess -from pathlib import Path -from typing import List - -from components.fluidd import ( - FLUIDD_URL, - FLUIDD_CONFIG_REPO_URL, - FLUIDD_CONFIG_DIR, - FLUIDD_DIR, - MODULE_PATH, -) -from components.fluidd.fluidd_dialogs import ( - print_fluidd_already_installed_dialog, - print_install_fluidd_config_dialog, - print_fluidd_port_select_dialog, - print_moonraker_not_found_dialog, -) -from components.fluidd.fluidd_utils import symlink_webui_nginx_log -from kiauh import KIAUH_CFG -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.config_manager.config_manager import ConfigManager -from core.instance_manager.instance_manager import InstanceManager -from core.repo_manager.repo_manager import RepoManager -from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from utils.common import check_install_dependencies -from utils.filesystem_utils import ( - unzip, - copy_upstream_nginx_cfg, - copy_common_vars_nginx_cfg, - create_nginx_cfg, - create_symlink, - remove_file, - read_ports_from_nginx_configs, - get_next_free_port, is_valid_port, - ) -from utils.input_utils import get_confirm, get_number_input -from utils.logger import Logger -from utils.system_utils import ( - download_file, - set_nginx_permissions, - get_ipv4_addr, - control_systemd_service, -) - - -def install_fluidd() -> None: - mr_im = InstanceManager(Moonraker) - mr_instances: List[Moonraker] = mr_im.instances - - if not mr_instances: - print_moonraker_not_found_dialog() - if not get_confirm("Continue Fluidd installation?", allow_go_back=True): - return - - if Path.home().joinpath("fluidd").exists(): - print_fluidd_already_installed_dialog() - do_reinstall = get_confirm("Re-install Fluidd?", allow_go_back=True) - if not do_reinstall: - return - - kl_im = InstanceManager(Klipper) - kl_instances = kl_im.instances - install_fl_config = False - if kl_instances: - print_install_fluidd_config_dialog() - question = "Download the recommended macros?" - install_fl_config = get_confirm(question, allow_go_back=False) - - cm = ConfigManager(cfg_file=KIAUH_CFG) - fluidd_port = cm.get_value("fluidd", "port") - ports_in_use = read_ports_from_nginx_configs() - - # check if configured port is a valid number and not in use already - valid_port = is_valid_port(fluidd_port, ports_in_use) - while not valid_port: - next_port = get_next_free_port(ports_in_use) - print_fluidd_port_select_dialog(next_port, ports_in_use) - fluidd_port = str(get_number_input( - "Configure Fluidd for port", - min_count=int(next_port), - default=next_port, - )) - valid_port = is_valid_port(fluidd_port, ports_in_use) - - check_install_dependencies(["nginx"]) - - try: - download_fluidd() - if mr_instances: - patch_moonraker_conf( - mr_instances, - "Fluidd", - "update_manager fluidd", - "fluidd-updater.conf", - ) - mr_im.restart_all_instance() - if install_fl_config and kl_instances: - download_fluidd_cfg() - create_fluidd_cfg_symlink(kl_instances) - patch_moonraker_conf( - mr_instances, - "fluidd-config", - "update_manager fluidd-config", - "fluidd-config-updater.conf", - ) - patch_printer_config(kl_instances) - kl_im.restart_all_instance() - - copy_upstream_nginx_cfg() - copy_common_vars_nginx_cfg() - create_fluidd_nginx_cfg(fluidd_port) - if kl_instances: - symlink_webui_nginx_log(kl_instances) - control_systemd_service("nginx", "restart") - - except Exception as e: - Logger.print_error(f"Fluidd installation failed!\n{e}") - return - - log = f"Open Fluidd now on: http://{get_ipv4_addr()}:{fluidd_port}" - Logger.print_ok("Fluidd installation complete!", start="\n") - Logger.print_ok(log, prefix=False, end="\n\n") - - -def download_fluidd() -> None: - try: - Logger.print_status("Downloading Fluidd ...") - target = Path.home().joinpath("fluidd.zip") - download_file(FLUIDD_URL, target, True) - Logger.print_ok("Download complete!") - - Logger.print_status("Extracting fluidd.zip ...") - unzip(Path.home().joinpath("fluidd.zip"), FLUIDD_DIR) - target.unlink(missing_ok=True) - Logger.print_ok("OK!") - - except Exception: - Logger.print_error("Downloading Fluidd failed!") - raise - - -def update_fluidd() -> None: - Logger.print_status("Updating Fluidd ...") - download_fluidd() - - -def download_fluidd_cfg() -> None: - try: - Logger.print_status("Downloading fluidd-config ...") - rm = RepoManager(FLUIDD_CONFIG_REPO_URL, target_dir=FLUIDD_CONFIG_DIR) - rm.clone_repo() - except Exception: - Logger.print_error("Downloading fluidd-config failed!") - raise - - -def create_fluidd_cfg_symlink(klipper_instances: List[Klipper]) -> None: - Logger.print_status("Create symlink of fluidd.cfg ...") - source = Path(FLUIDD_CONFIG_DIR, "fluidd.cfg") - for instance in klipper_instances: - target = instance.cfg_dir - Logger.print_status(f"Linking {source} to {target}") - try: - create_symlink(source, target) - except subprocess.CalledProcessError: - Logger.print_error("Creating symlink failed!") - - -def create_fluidd_nginx_cfg(port: int) -> None: - root_dir = FLUIDD_DIR - source = NGINX_SITES_AVAILABLE.joinpath("fluidd") - target = NGINX_SITES_ENABLED.joinpath("fluidd") - try: - Logger.print_status("Creating NGINX config for Fluidd ...") - remove_file(Path("/etc/nginx/sites-enabled/default"), True) - create_nginx_cfg("fluidd", port, root_dir) - create_symlink(source, target, True) - set_nginx_permissions() - Logger.print_ok("NGINX config for Fluidd successfully created.") - except Exception: - Logger.print_error("Creating NGINX config for Fluidd failed!") - raise - - -# TODO: could be fully extracted, its webui agnostic, and used for mainsail and fluidd -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) - if cm.config.has_section(section_name): - Logger.print_info("Section already exist. Skipped ...") - return - - template = MODULE_PATH.joinpath("assets", 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) - - -# TODO: could be made fully webui agnostic and extracted, and used for mainsail and fluidd -def patch_printer_config(klipper_instances: List[Klipper]) -> None: - for instance in klipper_instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Including fluidd-config in '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - return - - cm = ConfigManager(cfg_file) - if cm.config.has_section("include fluidd.cfg"): - Logger.print_info("Section already exist. Skipped ...") - return - - with open(cfg_file, "a") as f: - f.write("\n[include fluidd.cfg]") diff --git a/kiauh/components/fluidd/fluidd_utils.py b/kiauh/components/fluidd/fluidd_utils.py deleted file mode 100644 index c9c672b..0000000 --- a/kiauh/components/fluidd/fluidd_utils.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 urllib.request -from json import JSONDecodeError -from pathlib import Path -from typing import List - -from components.fluidd import FLUIDD_DIR, FLUIDD_BACKUP_DIR -from components.klipper.klipper import Klipper -from core.backup_manager.backup_manager import BackupManager -from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD -from utils.common import get_install_status_webui -from utils.logger import Logger - - -# TODO: could be extracted and made generic -def get_fluidd_status() -> str: - return get_install_status_webui( - FLUIDD_DIR, - NGINX_SITES_AVAILABLE.joinpath("fluidd"), - NGINX_CONFD.joinpath("upstreams.conf"), - NGINX_CONFD.joinpath("common_vars.conf"), - ) - - -# TODO: could be extracted and made generic -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/fluidd-access.log") - error_log = Path("/var/log/nginx/fluidd-error.log") - - for instance in klipper_instances: - desti_access = instance.log_dir.joinpath("fluidd-access.log") - if not desti_access.exists(): - desti_access.symlink_to(access_log) - - desti_error = instance.log_dir.joinpath("fluidd-error.log") - if not desti_error.exists(): - desti_error.symlink_to(error_log) - - -# TODO: could be extracted and made generic -def get_fluidd_local_version() -> str: - relinfo_file = FLUIDD_DIR.joinpath("release_info.json") - if not relinfo_file.is_file(): - return "-" - - with open(relinfo_file, "r") as f: - return json.load(f)["version"] - - -# TODO: could be extracted and made generic -def get_fluidd_remote_version() -> str: - url = "https://api.github.com/repos/fluidd-core/fluidd/tags" - try: - with urllib.request.urlopen(url) as response: - data = json.loads(response.read()) - return data[0]["name"] - except (JSONDecodeError, TypeError): - return "ERROR" - - -# TODO: could be extracted and made generic -def backup_fluidd_data() -> None: - with open(FLUIDD_DIR.joinpath(".version"), "r") as v: - version = v.readlines()[0] - bm = BackupManager() - bm.backup_directory(f"fluidd-{version}", FLUIDD_DIR, FLUIDD_BACKUP_DIR) - bm.backup_file(NGINX_SITES_AVAILABLE.joinpath("fluidd"), FLUIDD_BACKUP_DIR) diff --git a/kiauh/components/fluidd/menus/fluidd_remove_menu.py b/kiauh/components/fluidd/menus/fluidd_remove_menu.py deleted file mode 100644 index 4671799..0000000 --- a/kiauh/components/fluidd/menus/fluidd_remove_menu.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 components.fluidd import fluidd_remove -from core.menus import BACK_HELP_FOOTER -from core.menus.base_menu import BaseMenu -from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN - - -# noinspection PyUnusedLocal -class FluiddRemoveMenu(BaseMenu): - def __init__(self): - super().__init__( - header=False, - options={ - "0": self.toggle_all, - "1": self.toggle_remove_fluidd, - "2": self.toggle_remove_fl_config, - "3": self.toggle_remove_updater_section, - "4": self.toggle_remove_printer_cfg_include, - "5": self.run_removal_process, - }, - footer_type=BACK_HELP_FOOTER, - ) - self.remove_fluidd = False - self.remove_fl_config = False - self.remove_updater_section = False - self.remove_printer_cfg_include = False - - def print_menu(self) -> None: - header = " [ Remove Fluidd ] " - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) - checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" - unchecked = "[ ]" - o1 = checked if self.remove_fluidd else unchecked - o2 = checked if self.remove_fl_config else unchecked - o3 = checked if self.remove_updater_section else unchecked - o4 = checked if self.remove_printer_cfg_include else unchecked - menu = textwrap.dedent( - f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Enter a number and hit enter to select / deselect | - | the specific option for removal. | - |-------------------------------------------------------| - | 0) Select everything | - |-------------------------------------------------------| - | 1) {o1} Remove Fluidd | - | 2) {o2} Remove fluidd-config | - | | - | printer.cfg & moonraker.conf | - | 3) {o3} Remove Moonraker update section | - | 4) {o4} Remove printer.cfg include | - |-------------------------------------------------------| - | 5) Continue | - """ - )[1:] - print(menu, end="") - - def toggle_all(self, **kwargs) -> None: - self.remove_fluidd = True - self.remove_fl_config = True - self.remove_updater_section = True - self.remove_printer_cfg_include = True - - def toggle_remove_fluidd(self, **kwargs) -> None: - self.remove_fluidd = not self.remove_fluidd - - def toggle_remove_fl_config(self, **kwargs) -> None: - self.remove_fl_config = not self.remove_fl_config - - def toggle_remove_updater_section(self, **kwargs) -> None: - self.remove_updater_section = not self.remove_updater_section - - def toggle_remove_printer_cfg_include(self, **kwargs) -> None: - self.remove_printer_cfg_include = not self.remove_printer_cfg_include - - def run_removal_process(self, **kwargs) -> None: - if ( - not self.remove_fluidd - and not self.remove_fl_config - and not self.remove_updater_section - and not self.remove_printer_cfg_include - ): - error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" - print(error) - return - - fluidd_remove.run_fluidd_removal( - remove_fluidd=self.remove_fluidd, - remove_fl_config=self.remove_fl_config, - remove_mr_updater_section=self.remove_updater_section, - remove_flc_printer_cfg_include=self.remove_printer_cfg_include, - ) - - self.remove_fluidd = False - self.remove_fl_config = False - self.remove_updater_section = False - self.remove_printer_cfg_include = False diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index fc953ab..dc92b33 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -28,7 +28,7 @@ class KlipperRemoveMenu(BaseMenu): "2": self.toggle_remove_klipper_dir, "3": self.toggle_remove_klipper_env, "4": self.toggle_delete_klipper_logs, - "5": self.run_removal_process, + "c": self.run_removal_process, }, footer_type=BACK_HELP_FOOTER, ) @@ -62,7 +62,7 @@ class KlipperRemoveMenu(BaseMenu): | 3) {o3} Remove Python Environment | | 4) {o4} Delete all Log-Files | |-------------------------------------------------------| - | 5) Continue | + | C) Continue | """ )[1:] print(menu, end="") diff --git a/kiauh/components/mainsail/__init__.py b/kiauh/components/mainsail/__init__.py deleted file mode 100644 index 53a9924..0000000 --- a/kiauh/components/mainsail/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 - -from core.backup_manager import BACKUP_ROOT_DIR - -MODULE_PATH = Path(__file__).resolve().parent -MAINSAIL_DIR = Path.home().joinpath("mainsail") -MAINSAIL_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mainsail-backups") -MAINSAIL_CONFIG_DIR = Path.home().joinpath("mainsail-config") -MAINSAIL_CONFIG_JSON = MAINSAIL_DIR.joinpath("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/components/mainsail/assets/mainsail-config-updater.conf b/kiauh/components/mainsail/assets/mainsail-config-updater.conf deleted file mode 100644 index 02bb789..0000000 --- a/kiauh/components/mainsail/assets/mainsail-config-updater.conf +++ /dev/null @@ -1,6 +0,0 @@ -[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/components/mainsail/assets/mainsail-updater.conf b/kiauh/components/mainsail/assets/mainsail-updater.conf deleted file mode 100644 index f668332..0000000 --- a/kiauh/components/mainsail/assets/mainsail-updater.conf +++ /dev/null @@ -1,5 +0,0 @@ -[update_manager mainsail] -type: web -channel: stable -repo: mainsail-crew/mainsail -path: ~/mainsail diff --git a/kiauh/components/mainsail/mainsail_dialogs.py b/kiauh/components/mainsail/mainsail_dialogs.py deleted file mode 100644 index 36453ff..0000000 --- a/kiauh/components/mainsail/mainsail_dialogs.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 core.menus.base_menu import print_back_footer -from 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/components/mainsail/mainsail_remove.py b/kiauh/components/mainsail/mainsail_remove.py deleted file mode 100644 index 204e8a6..0000000 --- a/kiauh/components/mainsail/mainsail_remove.py +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 shutil -import subprocess -from pathlib import Path -from typing import List - -from components.klipper.klipper import Klipper -from components.mainsail import MAINSAIL_DIR, MAINSAIL_CONFIG_DIR -from components.mainsail.mainsail_utils import backup_config_json -from components.moonraker.moonraker import Moonraker -from core.config_manager.config_manager import ConfigManager -from core.instance_manager.instance_manager import InstanceManager -from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from utils.filesystem_utils import remove_file -from utils.logger import Logger - - -def run_mainsail_removal( - remove_mainsail: bool, - remove_ms_config: bool, - backup_ms_config_json: bool, - remove_mr_updater_section: bool, - remove_msc_printer_cfg_include: bool, -) -> None: - if backup_ms_config_json: - backup_config_json() - if remove_mainsail: - remove_mainsail_dir() - remove_nginx_config() - remove_nginx_logs() - if remove_mr_updater_section: - remove_updater_section("update_manager mainsail") - if remove_ms_config: - remove_mainsail_cfg_dir() - remove_mainsail_cfg_symlink() - if remove_mr_updater_section: - remove_updater_section("update_manager mainsail-config") - if remove_msc_printer_cfg_include: - remove_printer_cfg_include() - - -def remove_mainsail_dir() -> None: - Logger.print_status("Removing Mainsail ...") - if not MAINSAIL_DIR.exists(): - Logger.print_info(f"'{MAINSAIL_DIR}' does not exist. Skipping ...") - return - - try: - shutil.rmtree(MAINSAIL_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{MAINSAIL_DIR}':\n{e}") - - -def remove_nginx_config() -> None: - Logger.print_status("Removing Mainsails NGINX config ...") - try: - remove_file(NGINX_SITES_AVAILABLE.joinpath("mainsail"), True) - remove_file(NGINX_SITES_ENABLED.joinpath("mainsail"), True) - - except subprocess.CalledProcessError as e: - log = f"Unable to remove Mainsail NGINX config:\n{e.stderr.decode()}" - Logger.print_error(log) - - -def remove_nginx_logs() -> None: - Logger.print_status("Removing Mainsails NGINX logs ...") - try: - remove_file(Path("/var/log/nginx/mainsail-access.log"), True) - remove_file(Path("/var/log/nginx/mainsail-error.log"), True) - - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - if not instances: - return - - for instance in instances: - remove_file(instance.log_dir.joinpath("mainsail-access.log")) - remove_file(instance.log_dir.joinpath("mainsail-error.log")) - - except (OSError, subprocess.CalledProcessError) as e: - Logger.print_error(f"Unable to NGINX logs:\n{e}") - - -def remove_updater_section(name: str) -> None: - Logger.print_status("Remove updater section from moonraker.conf ...") - im = InstanceManager(Moonraker) - instances: List[Moonraker] = im.instances - if not instances: - Logger.print_info("Moonraker not installed. Skipped ...") - return - - for instance in instances: - Logger.print_status(f"Remove section '{name}' in '{instance.cfg_file}' ...") - - if not instance.cfg_file.is_file(): - Logger.print_info(f"'{instance.cfg_file}' does not exist. Skipped ...") - continue - - cm = ConfigManager(instance.cfg_file) - if not cm.config.has_section(name): - Logger.print_info("Section not present. Skipped ...") - continue - - cm.config.remove_section(name) - cm.write_config() - - -def remove_mainsail_cfg_dir() -> None: - Logger.print_status("Removing mainsail-config ...") - if not MAINSAIL_CONFIG_DIR.exists(): - Logger.print_info(f"'{MAINSAIL_CONFIG_DIR}' does not exist. Skipping ...") - return - - try: - shutil.rmtree(MAINSAIL_CONFIG_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{MAINSAIL_CONFIG_DIR}':\n{e}") - - -def remove_mainsail_cfg_symlink() -> None: - Logger.print_status("Removing mainsail.cfg symlinks ...") - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - for instance in instances: - Logger.print_status(f"Removing symlink from '{instance.cfg_file}' ...") - try: - remove_file(instance.cfg_dir.joinpath("mainsail.cfg")) - except subprocess.CalledProcessError: - Logger.print_error("Failed to remove symlink!") - - -def remove_printer_cfg_include() -> None: - Logger.print_status("Remove mainsail-config include from printer.cfg ...") - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - if not instances: - Logger.print_info("Klipper not installed. Skipping ...") - return - - for instance in instances: - log = f"Removing include from '{instance.cfg_file}' ..." - Logger.print_status(log) - - if not instance.cfg_file.is_file(): - continue - - cm = ConfigManager(instance.cfg_file) - if not cm.config.has_section("include mainsail.cfg"): - Logger.print_info("Section not present. Skipped ...") - continue - - cm.config.remove_section("include mainsail.cfg") - cm.write_config() diff --git a/kiauh/components/mainsail/mainsail_setup.py b/kiauh/components/mainsail/mainsail_setup.py deleted file mode 100644 index 210dc9e..0000000 --- a/kiauh/components/mainsail/mainsail_setup.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 subprocess -from pathlib import Path -from typing import List - -from kiauh import KIAUH_CFG -from components.klipper.klipper import Klipper -from components.mainsail import ( - MAINSAIL_URL, - MAINSAIL_DIR, - MAINSAIL_CONFIG_DIR, - MAINSAIL_CONFIG_REPO_URL, - MODULE_PATH, -) -from components.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 components.mainsail.mainsail_utils import ( - restore_config_json, - enable_mainsail_remotemode, - backup_config_json, - symlink_webui_nginx_log, -) -from components.moonraker.moonraker import Moonraker -from core.config_manager.config_manager import ConfigManager -from core.instance_manager.instance_manager import InstanceManager -from core.repo_manager.repo_manager import RepoManager -from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from utils.common import check_install_dependencies -from utils.filesystem_utils import ( - unzip, - copy_upstream_nginx_cfg, - copy_common_vars_nginx_cfg, - create_nginx_cfg, - create_symlink, - remove_file, -) -from utils.input_utils import get_confirm, get_number_input -from utils.logger import Logger -from utils.system_utils import ( - download_file, - set_nginx_permissions, - get_ipv4_addr, - control_systemd_service, -) - - -def install_mainsail() -> None: - mr_im = InstanceManager(Moonraker) - mr_instances: List[Moonraker] = mr_im.instances - - enable_remotemode = False - if not mr_instances: - print_moonraker_not_found_dialog() - if not get_confirm("Continue Mainsail installation?", allow_go_back=True): - return - - # if moonraker is not installed or multiple instances - # are installed we enable mainsails remote mode - if not mr_instances or len(mr_instances) > 1: - enable_remotemode = True - - do_reinstall = False - if Path.home().joinpath("mainsail").exists(): - print_mainsail_already_installed_dialog() - do_reinstall = get_confirm("Re-install Mainsail?", allow_go_back=True) - if do_reinstall: - backup_config_json(is_temp=True) - else: - return - - kl_im = InstanceManager(Klipper) - kl_instances = kl_im.instances - install_ms_config = False - if kl_instances: - print_install_mainsail_config_dialog() - question = "Download the recommended macros?" - install_ms_config = get_confirm(question, allow_go_back=False) - - # if a default port is configured in the kiauh.cfg, we use that for the port - # otherwise we default to port 80, but show the user a dialog to confirm/change that port - cm = ConfigManager(cfg_file=KIAUH_CFG) - default_port = cm.get_value("mainsail", "default_port") - is_valid_port = default_port and default_port.isdigit() - mainsail_port = default_port if is_valid_port else "80" - if not is_valid_port: - print_mainsail_port_select_dialog(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 mr_instances: - patch_moonraker_conf( - mr_instances, - "Mainsail", - "update_manager mainsail", - "mainsail-updater.conf", - ) - mr_im.restart_all_instance() - if install_ms_config and kl_instances: - download_mainsail_cfg() - create_mainsail_cfg_symlink(kl_instances) - patch_moonraker_conf( - mr_instances, - "mainsail-config", - "update_manager mainsail-config", - "mainsail-config-updater.conf", - ) - patch_printer_config(kl_instances) - kl_im.restart_all_instance() - - copy_upstream_nginx_cfg() - copy_common_vars_nginx_cfg() - create_mainsail_nginx_cfg(mainsail_port) - if kl_instances: - symlink_webui_nginx_log(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 ...") - target = Path.home().joinpath("mainsail.zip") - download_file(MAINSAIL_URL, target, True) - Logger.print_ok("Download complete!") - - Logger.print_status("Extracting mainsail.zip ...") - unzip(Path.home().joinpath("mainsail.zip"), MAINSAIL_DIR) - target.unlink(missing_ok=True) - Logger.print_ok("OK!") - - except Exception: - Logger.print_error("Downloading Mainsail failed!") - raise - - -def update_mainsail() -> None: - Logger.print_status("Updating Mainsail ...") - backup_config_json(is_temp=True) - download_mainsail() - restore_config_json() - - -def download_mainsail_cfg() -> 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_cfg_symlink(klipper_instances: List[Klipper]) -> None: - Logger.print_status("Create symlink of mainsail.cfg ...") - source = Path(MAINSAIL_CONFIG_DIR, "mainsail.cfg") - for instance in klipper_instances: - target = instance.cfg_dir - Logger.print_status(f"Linking {source} to {target}") - try: - create_symlink(source, target) - except subprocess.CalledProcessError: - Logger.print_error("Creating symlink failed!") - - -def create_mainsail_nginx_cfg(port: int) -> None: - root_dir = MAINSAIL_DIR - source = NGINX_SITES_AVAILABLE.joinpath("mainsail") - target = NGINX_SITES_ENABLED.joinpath("mainsail") - try: - Logger.print_status("Creating NGINX config for Mainsail ...") - remove_file(Path("/etc/nginx/sites-enabled/default"), True) - create_nginx_cfg("mainsail", port, root_dir) - create_symlink(source, target, True) - 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 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) - if cm.config.has_section(section_name): - Logger.print_info("Section already exist. Skipped ...") - return - - template = MODULE_PATH.joinpath("assets", 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) - 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]") diff --git a/kiauh/components/mainsail/mainsail_utils.py b/kiauh/components/mainsail/mainsail_utils.py deleted file mode 100644 index 321f326..0000000 --- a/kiauh/components/mainsail/mainsail_utils.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 shutil -from json import JSONDecodeError -from pathlib import Path -from typing import List - -import urllib.request - -from components.klipper.klipper import Klipper -from components.mainsail import ( - MAINSAIL_CONFIG_JSON, - MAINSAIL_DIR, - MAINSAIL_BACKUP_DIR, -) -from core.backup_manager.backup_manager import BackupManager -from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD -from utils.common import get_install_status_webui -from utils.logger import Logger - - -def get_mainsail_status() -> str: - return get_install_status_webui( - MAINSAIL_DIR, - NGINX_SITES_AVAILABLE.joinpath("mainsail"), - NGINX_CONFD.joinpath("upstreams.conf"), - NGINX_CONFD.joinpath("common_vars.conf"), - ) - - -def backup_config_json(is_temp=False) -> None: - Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") - bm = BackupManager() - if is_temp: - fn = Path.home().joinpath("config.json.kiauh.bak") - bm.backup_file(MAINSAIL_CONFIG_JSON, custom_filename=fn) - else: - bm.backup_file(MAINSAIL_CONFIG_JSON) - - -def restore_config_json() -> None: - try: - Logger.print_status(f"Restore '{MAINSAIL_CONFIG_JSON}' ...") - source = Path.home().joinpath("config.json.kiauh.bak") - shutil.copy(source, MAINSAIL_CONFIG_JSON) - except OSError: - Logger.print_info("Unable to restore config.json. Skipped ...") - - -def enable_mainsail_remotemode() -> None: - Logger.print_status("Enable Mainsails remote mode ...") - with open(MAINSAIL_CONFIG_JSON, "r") as f: - config_data = json.load(f) - - if config_data["instancesDB"] == "browser": - Logger.print_info("Remote mode already configured. Skipped ...") - 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) - Logger.print_ok("Mainsails remote mode enabled!") - - -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 = instance.log_dir.joinpath("mainsail-access.log") - if not desti_access.exists(): - desti_access.symlink_to(access_log) - - desti_error = instance.log_dir.joinpath("mainsail-error.log") - if not desti_error.exists(): - desti_error.symlink_to(error_log) - - -def get_mainsail_local_version() -> str: - relinfo_file = MAINSAIL_DIR.joinpath("release_info.json") - if not relinfo_file.is_file(): - return "-" - - with open(relinfo_file, "r") as f: - return json.load(f)["version"] - - -def get_mainsail_remote_version() -> str: - url = "https://api.github.com/repos/mainsail-crew/mainsail/tags" - try: - with urllib.request.urlopen(url) as response: - data = json.loads(response.read()) - return data[0]["name"] - except (JSONDecodeError, TypeError): - return "ERROR" - - -def backup_mainsail_data() -> None: - with open(MAINSAIL_DIR.joinpath(".version"), "r") as v: - version = v.readlines()[0] - bm = BackupManager() - bm.backup_directory(f"mainsail-{version}", MAINSAIL_DIR, MAINSAIL_BACKUP_DIR) - bm.backup_file(MAINSAIL_CONFIG_JSON, MAINSAIL_BACKUP_DIR) - bm.backup_file(NGINX_SITES_AVAILABLE.joinpath("mainsail"), MAINSAIL_BACKUP_DIR) diff --git a/kiauh/components/mainsail/menus/mainsail_remove_menu.py b/kiauh/components/mainsail/menus/mainsail_remove_menu.py deleted file mode 100644 index 48cfb4b..0000000 --- a/kiauh/components/mainsail/menus/mainsail_remove_menu.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 components.mainsail import mainsail_remove -from core.menus import BACK_HELP_FOOTER -from core.menus.base_menu import BaseMenu -from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN - - -# noinspection PyUnusedLocal -class MainsailRemoveMenu(BaseMenu): - def __init__(self): - super().__init__( - header=False, - options={ - "0": self.toggle_all, - "1": self.toggle_remove_mainsail, - "2": self.toggle_remove_ms_config, - "3": self.toggle_backup_config_json, - "4": self.toggle_remove_updater_section, - "5": self.toggle_remove_printer_cfg_include, - "6": self.run_removal_process, - }, - footer_type=BACK_HELP_FOOTER, - ) - self.remove_mainsail = False - self.remove_ms_config = False - self.backup_config_json = False - self.remove_updater_section = False - self.remove_printer_cfg_include = False - - def print_menu(self) -> None: - header = " [ Remove Mainsail ] " - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) - checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" - unchecked = "[ ]" - o1 = checked if self.remove_mainsail else unchecked - o2 = checked if self.remove_ms_config else unchecked - o3 = checked if self.backup_config_json else unchecked - o4 = checked if self.remove_updater_section else unchecked - o5 = checked if self.remove_printer_cfg_include else unchecked - menu = textwrap.dedent( - f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Enter a number and hit enter to select / deselect | - | the specific option for removal. | - |-------------------------------------------------------| - | 0) Select everything | - |-------------------------------------------------------| - | 1) {o1} Remove Mainsail | - | 2) {o2} Remove mainsail-config | - | 3) {o3} Backup config.json | - | | - | printer.cfg & moonraker.conf | - | 4) {o4} Remove Moonraker update section | - | 5) {o5} Remove printer.cfg include | - |-------------------------------------------------------| - | 6) Continue | - """ - )[1:] - print(menu, end="") - - def toggle_all(self, **kwargs) -> None: - self.remove_mainsail = True - self.remove_ms_config = True - self.backup_config_json = True - self.remove_updater_section = True - self.remove_printer_cfg_include = True - - def toggle_remove_mainsail(self, **kwargs) -> None: - self.remove_mainsail = not self.remove_mainsail - - def toggle_remove_ms_config(self, **kwargs) -> None: - self.remove_ms_config = not self.remove_ms_config - - def toggle_backup_config_json(self, **kwargs) -> None: - self.backup_config_json = not self.backup_config_json - - def toggle_remove_updater_section(self, **kwargs) -> None: - self.remove_updater_section = not self.remove_updater_section - - def toggle_remove_printer_cfg_include(self, **kwargs) -> None: - self.remove_printer_cfg_include = not self.remove_printer_cfg_include - - def run_removal_process(self, **kwargs) -> None: - if ( - not self.remove_mainsail - and not self.remove_ms_config - and not self.backup_config_json - and not self.remove_updater_section - and not self.remove_printer_cfg_include - ): - error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" - print(error) - return - - mainsail_remove.run_mainsail_removal( - remove_mainsail=self.remove_mainsail, - remove_ms_config=self.remove_ms_config, - backup_ms_config_json=self.backup_config_json, - remove_mr_updater_section=self.remove_updater_section, - remove_msc_printer_cfg_include=self.remove_printer_cfg_include, - ) - - self.remove_mainsail = False - self.remove_ms_config = False - self.backup_config_json = False - self.remove_updater_section = False - self.remove_printer_cfg_include = False diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 652d028..f740b45 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -29,7 +29,7 @@ class MoonrakerRemoveMenu(BaseMenu): "3": self.toggle_remove_moonraker_env, "4": self.toggle_remove_moonraker_polkit, "5": self.toggle_delete_moonraker_logs, - "6": self.run_removal_process, + "c": self.run_removal_process, }, footer_type=BACK_HELP_FOOTER, ) @@ -66,7 +66,7 @@ class MoonrakerRemoveMenu(BaseMenu): | 4) {o4} Remove Policy Kit Rules | | 5) {o5} Delete all Log-Files | |-------------------------------------------------------| - | 6) Continue | + | C) Continue | """ )[1:] print(menu, end="") diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index fe635f8..a4c6cf2 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -14,11 +14,11 @@ import sys from pathlib import Path from typing import List +from components.webui_client import MAINSAIL_DIR +from components.webui_client.client_utils import enable_mainsail_remotemode from kiauh import KIAUH_CFG from components.klipper.klipper import Klipper from components.klipper.klipper_dialogs import print_instance_overview -from components.mainsail import MAINSAIL_DIR -from components.mainsail.mainsail_utils import enable_mainsail_remotemode from components.moonraker import ( EXIT_MOONRAKER_SETUP, DEFAULT_MOONRAKER_REPO_URL, diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index 3321781..8f89364 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -12,8 +12,6 @@ import shutil from typing import Dict, Literal, List, Union -from components.mainsail import MAINSAIL_DIR -from components.mainsail.mainsail_utils import enable_mainsail_remotemode from components.moonraker import ( DEFAULT_MOONRAKER_PORT, MODULE_PATH, @@ -23,6 +21,8 @@ from components.moonraker import ( MOONRAKER_DB_BACKUP_DIR, ) from components.moonraker.moonraker import Moonraker +from components.webui_client import MAINSAIL_DIR +from components.webui_client.client_utils import enable_mainsail_remotemode from core.backup_manager.backup_manager import BackupManager from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager diff --git a/kiauh/components/webui_client/__init__.py b/kiauh/components/webui_client/__init__.py new file mode 100644 index 0000000..b0d44f3 --- /dev/null +++ b/kiauh/components/webui_client/__init__.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 +from typing import Literal, TypedDict, Set + +from core.backup_manager import BACKUP_ROOT_DIR + +MODULE_PATH = Path(__file__).resolve().parent + +########### +# MAINSAIL +########### +MAINSAIL_DIR = Path.home().joinpath("mainsail") +MAINSAIL_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mainsail-backups") +MAINSAIL_CONFIG_DIR = Path.home().joinpath("mainsail-config") +MAINSAIL_CONFIG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mainsail-config-backups") +MAINSAIL_CONFIG_REPO_URL = "https://github.com/mainsail-crew/mainsail-config.git" +MAINSAIL_CONFIG_JSON = MAINSAIL_DIR.joinpath("config.json") +MAINSAIL_URL = ( + "https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip" +) +MAINSAIL_PRE_RLS_URL = ( + "https://github.com/mainsail-crew/mainsail/releases/download/%TAG%/mainsail.zip" +) +MAINSAIL_TAGS_URL = "https://api.github.com/repos/mainsail-crew/mainsail/tags" + +######### +# FLUIDD +######### +FLUIDD_DIR = Path.home().joinpath("fluidd") +FLUIDD_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("fluidd-backups") +FLUIDD_CONFIG_DIR = Path.home().joinpath("fluidd-config") +FLUIDD_CONFIG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("fluidd-config-backups") +FLUIDD_CONFIG_REPO_URL = "https://github.com/fluidd-core/fluidd-config.git" +FLUIDD_URL = "https://github.com/fluidd-core/fluidd/releases/latest/download/fluidd.zip" +FLUIDD_PRE_RLS_URL = ( + "https://github.com/fluidd-core/fluidd/releases/download/%TAG%/fluidd.zip" +) +FLUIDD_TAGS_URL = "https://api.github.com/repos/fluidd-core/fluidd/tags" + +ClientName = Literal["mainsail", "fluidd"] +ClientConfigName = Literal["mainsail-config", "fluidd-config"] + + +class ClientData(TypedDict): + name: ClientName + display_name: str + dir: Path + backup_dir: Path + url: str + pre_release_url: str + tags_url: str + remote_mode: bool # required only for Mainsail + mr_conf_repo: str + mr_conf_path: str + client_config: "ClientConfigData" + + +class ClientConfigData(TypedDict): + name: ClientConfigName + display_name: str + cfg_filename: str + dir: Path + backup_dir: Path + url: str + printer_cfg_section: str + mr_conf_path: str + mr_conf_origin: str diff --git a/kiauh/components/fluidd/menus/__init__.py b/kiauh/components/webui_client/client_config/__init__.py similarity index 100% rename from kiauh/components/fluidd/menus/__init__.py rename to kiauh/components/webui_client/client_config/__init__.py diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py new file mode 100644 index 0000000..41203a3 --- /dev/null +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 shutil +import subprocess +from typing import List + +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from components.webui_client import ClientConfigData +from core.instance_manager.instance_manager import InstanceManager +from utils.filesystem_utils import remove_file, remove_config_section +from utils.logger import Logger + + +def run_client_config_removal( + client_config: ClientConfigData, + remove_moonraker_conf_section: bool, + remove_printer_cfg_include: bool, + kl_instances: List[Klipper], + mr_instances: List[Moonraker], +) -> None: + remove_client_config_dir(client_config) + remove_client_config_symlink(client_config) + if remove_moonraker_conf_section: + remove_config_section( + f"update_manager {client_config.get('name')}", mr_instances + ) + if remove_printer_cfg_include: + remove_config_section(client_config.get("printer_cfg_section"), kl_instances) + + +def remove_client_config_dir(client_config: ClientConfigData) -> None: + Logger.print_status(f"Removing {client_config.get('name')} ...") + client_config_dir = client_config.get("dir") + if not client_config_dir.exists(): + Logger.print_info(f"'{client_config_dir}' does not exist. Skipping ...") + return + + try: + shutil.rmtree(client_config_dir) + except OSError as e: + Logger.print_error(f"Unable to delete '{client_config_dir}':\n{e}") + + +def remove_client_config_symlink(client_config: ClientConfigData) -> None: + Logger.print_status(f"Removing {client_config.get('cfg_filename')} symlinks ...") + im = InstanceManager(Klipper) + instances: List[Klipper] = im.instances + for instance in instances: + Logger.print_status(f"Removing symlink from '{instance.cfg_file}' ...") + try: + remove_file(instance.cfg_dir.joinpath(client_config.get("cfg_filename"))) + except subprocess.CalledProcessError: + Logger.print_error("Failed to remove symlink!") diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py new file mode 100644 index 0000000..d893198 --- /dev/null +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 shutil +import subprocess +from pathlib import Path +from typing import List, get_args + +from kiauh import KIAUH_CFG +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from components.webui_client import ClientConfigData, ClientName, ClientData +from components.webui_client.client_dialogs import print_client_already_installed_dialog +from components.webui_client.client_utils import ( + load_client_data, + backup_client_config_data, +) +from core.config_manager.config_manager import ConfigManager + +from core.instance_manager.instance_manager import InstanceManager +from core.repo_manager.repo_manager import RepoManager +from utils.filesystem_utils import ( + create_symlink, + add_config_section, +) +from utils.input_utils import get_confirm +from utils.logger import Logger + + +def install_client_config(client_name: ClientName) -> None: + client: ClientData = load_client_data(client_name) + client_config: ClientConfigData = client.get("client_config") + d_name = client_config.get("display_name") + + if check_existing_client_config_install(client_name): + Logger.print_info("Another Client-Config is already installed! Skipped ...") + return + + if client_config.get("dir").exists(): + print_client_already_installed_dialog(d_name) + if get_confirm(f"Re-install {d_name}?", allow_go_back=True): + shutil.rmtree(client_config.get("dir")) + else: + return + + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + kl_im = InstanceManager(Klipper) + kl_instances = kl_im.instances + + try: + download_client_config(client_config) + create_client_config_symlink(client_config, kl_instances) + add_config_section( + section=f"update_manager {client_config.get('name')}", + instances=mr_instances, + options=[ + ("type", "git_repo"), + ("primary_branch", "master"), + ("path", client_config.get("mr_conf_path")), + ("origin", client_config.get("mr_conf_origin")), + ("managed_services", "klipper"), + ], + ) + add_config_section(client_config.get("printer_cfg_section"), kl_instances) + kl_im.restart_all_instance() + + except Exception as e: + Logger.print_error(f"{d_name} installation failed!\n{e}") + return + + Logger.print_ok(f"{d_name} installation complete!", start="\n") + + +def check_existing_client_config_install(client_name: ClientName) -> bool: + # check if any other client-configs are present + # as they can conflict each other, or are at least + # redundant to have, so we skip the installation + client_list = list(get_args(ClientName)) + client_list.remove(client_name) + for c in client_list: + c_data: ClientData = load_client_data(c) + c_config_data: ClientConfigData = c_data.get("client_config") + if c_config_data.get("dir").exists(): + return True + + return False + + +def download_client_config(client_config: ClientConfigData) -> None: + try: + Logger.print_status(f"Downloading {client_config.get('display_name')} ...") + rm = RepoManager( + client_config.get("url"), target_dir=str(client_config.get("dir")) + ) + rm.clone_repo() + except Exception: + Logger.print_error(f"Downloading {client_config.get('display_name')} failed!") + raise + + +def update_client_config(client: ClientData) -> None: + client_config: ClientConfigData = client.get("client_config") + + Logger.print_status(f"Updating {client_config.get('display_name')} ...") + + cm = ConfigManager(cfg_file=KIAUH_CFG) + if cm.get_value("kiauh", "backup_before_update"): + backup_client_config_data(client) + + repo_manager = RepoManager( + repo=client_config.get("url"), + branch="master", + target_dir=str(client_config.get("dir")), + ) + repo_manager.pull_repo() + + Logger.print_ok(f"Successfully updated {client_config.get('display_name')}.") + Logger.print_warn("Remember to restart Klipper to reload the configurations!") + + +def create_client_config_symlink( + client_config: ClientConfigData, klipper_instances: List[Klipper] = None +) -> None: + if klipper_instances is None: + kl_im = InstanceManager(Klipper) + klipper_instances = kl_im.instances + + Logger.print_status(f"Create symlink for {client_config.get('cfg_filename')} ...") + source = Path(client_config.get("dir"), client_config.get("cfg_filename")) + for instance in klipper_instances: + target = instance.cfg_dir + Logger.print_status(f"Linking {source} to {target}") + try: + create_symlink(source, target) + except subprocess.CalledProcessError: + Logger.print_error("Creating symlink failed!") diff --git a/kiauh/components/fluidd/fluidd_dialogs.py b/kiauh/components/webui_client/client_dialogs.py similarity index 75% rename from kiauh/components/fluidd/fluidd_dialogs.py rename to kiauh/components/webui_client/client_dialogs.py index deec016..9fb98e9 100644 --- a/kiauh/components/fluidd/fluidd_dialogs.py +++ b/kiauh/components/webui_client/client_dialogs.py @@ -12,6 +12,7 @@ import textwrap from typing import List +from components.webui_client import ClientData from core.menus.base_menu import print_back_footer from utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN @@ -25,10 +26,10 @@ def print_moonraker_not_found_dialog(): | {line1:<63}| | {line2:<63}| |-------------------------------------------------------| - | It is possible to install Fluidd without a local | + | 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 Fluidd will NOT | + | machine in your network. Otherwise Mainsail will NOT | | work correctly. | """ )[1:] @@ -37,19 +38,18 @@ def print_moonraker_not_found_dialog(): print_back_footer() -def print_fluidd_already_installed_dialog(): +def print_client_already_installed_dialog(name: str): line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" - line2 = f"{COLOR_YELLOW}Fluidd seems to be already installed!{RESET_FORMAT}" + line2 = f"{COLOR_YELLOW}{name} seems to be already installed!{RESET_FORMAT}" + line3 = f"If you continue, your current {name}" dialog = textwrap.dedent( f""" /=======================================================\\ | {line1:<63}| | {line2:<63}| |-------------------------------------------------------| - | If you continue, your current Fluidd installation | - | will be overwritten. You will not loose any printer | - | configurations and the Moonraker database will remain | - | untouched. | + | {line3:<54}| + | installation will be overwritten. | """ )[1:] @@ -57,38 +57,21 @@ def print_fluidd_already_installed_dialog(): print_back_footer() -def print_install_fluidd_config_dialog(): - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | It is recommended to use special macros in order to | - | have Fluidd fully functional and working. | - | | - | The recommended macros for Fluidd can be seen here: | - | https://github.com/fluidd-core/fluidd-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_fluidd_port_select_dialog(port: str, ports_in_use: List[str]): +def print_client_port_select_dialog(name: str, port: str, ports_in_use: List[str]): port = f"{COLOR_CYAN}{port}{RESET_FORMAT}" + line1 = f"Please select the port, {name} should be served on." + line2 = f"In case you need {name} to be served on a specific" dialog = textwrap.dedent( f""" /=======================================================\\ - | Please select the port, Fluidd should be served on. | + | {line1:<54}| | If you are unsure what to select, hit Enter to apply | | the suggested value of: {port:38} | | | - | In case you need Fluidd to be served on a specific | + | {line2:<54}| | port, you can set it now. Make sure the port is not | | used by any other application on your system! | + \\=======================================================/ """ )[1:] @@ -102,3 +85,27 @@ def print_fluidd_port_select_dialog(port: str, ports_in_use: List[str]): dialog += "\\=======================================================/\n" print(dialog, end="") + + +def print_install_client_config_dialog(client: ClientData): + name = client.get("display_name") + url = client.get("client_config").get("url").replace(".git", "") + line1 = f"have {name} fully functional and working." + line2 = f"The recommended macros for {name} can be seen here:" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | It is recommended to use special macros in order to | + | {line1:<54}| + | | + | {line2:<54}| + | {url:<54}| + | | + | 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="") diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py new file mode 100644 index 0000000..49bc694 --- /dev/null +++ b/kiauh/components/webui_client/client_remove.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 shutil +from typing import List + +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from components.webui_client import ClientData +from components.webui_client.client_config.client_config_remove import ( + run_client_config_removal, +) +from components.webui_client.client_utils import backup_mainsail_config_json + +from core.instance_manager.instance_manager import InstanceManager +from utils.filesystem_utils import ( + remove_nginx_config, + remove_nginx_logs, + remove_config_section, +) +from utils.logger import Logger + + +def run_client_removal( + client: ClientData, + rm_client: bool, + rm_client_config: bool, + backup_ms_config_json: bool, + rm_moonraker_conf_section: bool, + rm_printer_cfg_section: bool, +) -> None: + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + kl_im = InstanceManager(Klipper) + kl_instances: List[Klipper] = kl_im.instances + if backup_ms_config_json and client.get("name") == "mainsail": + backup_mainsail_config_json() + if rm_client: + client_name = client.get("name") + remove_client_dir(client) + remove_nginx_config(client_name) + remove_nginx_logs(client_name) + if rm_moonraker_conf_section: + section = f"update_manager {client_name}" + remove_config_section(section, mr_instances) + if rm_client_config: + run_client_config_removal( + client.get("client_config"), + rm_moonraker_conf_section, + rm_printer_cfg_section, + kl_instances, + mr_instances, + ) + + +def remove_client_dir(client: ClientData) -> None: + Logger.print_status(f"Removing {client.get('display_name')} ...") + client_dir = client.get("dir") + if not client.get("dir").exists(): + Logger.print_info(f"'{client_dir}' does not exist. Skipping ...") + return + + try: + shutil.rmtree(client_dir) + except OSError as e: + Logger.print_error(f"Unable to delete '{client_dir}':\n{e}") diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py new file mode 100644 index 0000000..a783507 --- /dev/null +++ b/kiauh/components/webui_client/client_setup.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 +from typing import List + +from components.klipper.klipper import Klipper +from components.webui_client import ( + ClientName, + ClientData, +) + +from components.moonraker.moonraker import Moonraker +from components.webui_client.client_config.client_config_setup import ( + install_client_config, + check_existing_client_config_install, +) +from components.webui_client.client_dialogs import ( + print_moonraker_not_found_dialog, + print_client_port_select_dialog, + print_install_client_config_dialog, +) +from components.webui_client.client_utils import ( + backup_mainsail_config_json, + restore_mainsail_config_json, + enable_mainsail_remotemode, + symlink_webui_nginx_log, + load_client_data, +) +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.instance_manager import InstanceManager +from kiauh import KIAUH_CFG +from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED +from utils.common import check_install_dependencies +from utils.filesystem_utils import ( + unzip, + copy_upstream_nginx_cfg, + copy_common_vars_nginx_cfg, + create_nginx_cfg, + create_symlink, + remove_file, + add_config_section, + read_ports_from_nginx_configs, + is_valid_port, + get_next_free_port, +) +from utils.input_utils import get_confirm, get_number_input +from utils.logger import Logger +from utils.system_utils import ( + download_file, + set_nginx_permissions, + get_ipv4_addr, + control_systemd_service, +) + + +def install_client(client_name: ClientName) -> None: + client: ClientData = load_client_data(client_name) + d_name = client.get("display_name") + + if client is None: + Logger.print_error("Missing parameter client_name!") + return + + if client.get("dir").exists(): + Logger.print_info( + f"{client.get('display_name')} seems to be already installed! Skipped ..." + ) + return + + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + + enable_remotemode = False + if not mr_instances: + print_moonraker_not_found_dialog() + if not get_confirm( + f"Continue {d_name} installation?", + allow_go_back=True, + ): + return + + # if moonraker is not installed or multiple instances + # are installed we enable mainsails remote mode + if client.get("remote_mode") and not mr_instances or len(mr_instances) > 1: + enable_remotemode = True + + kl_im = InstanceManager(Klipper) + kl_instances = kl_im.instances + install_client_cfg = False + client_config = client.get("client_config") + if ( + kl_instances + and not client_config.get("dir").exists() + and not check_existing_client_config_install(client.get("name")) + ): + print_install_client_config_dialog(client) + question = f"Download the recommended {client_config.get('display_name')}?" + install_client_cfg = get_confirm(question, allow_go_back=False) + + cm = ConfigManager(cfg_file=KIAUH_CFG) + client_port = cm.get_value(client.get("name"), "port") + ports_in_use = read_ports_from_nginx_configs() + + # check if configured port is a valid number and not in use already + valid_port = is_valid_port(client_port, ports_in_use) + while not valid_port: + next_port = get_next_free_port(ports_in_use) + print_client_port_select_dialog(d_name, next_port, ports_in_use) + client_port = str( + get_number_input( + f"Configure {d_name} for port", + min_count=int(next_port), + default=next_port, + ) + ) + valid_port = is_valid_port(client_port, ports_in_use) + + check_install_dependencies(["nginx"]) + + try: + download_client(client) + if enable_remotemode and client.get("name") == "mainsail": + enable_mainsail_remotemode() + if mr_instances: + add_config_section( + section=f"update_manager {client.get('name')}", + instances=mr_instances, + options=[ + ("type", "web"), + ("channel", "stable"), + ("repo", client.get("mr_conf_repo")), + ("path", client.get("mr_conf_path")), + ], + ) + mr_im.restart_all_instance() + if install_client_cfg and kl_instances: + install_client_config(client.get("name")) + + copy_upstream_nginx_cfg() + copy_common_vars_nginx_cfg() + create_client_nginx_cfg(client, client_port) + if kl_instances: + symlink_webui_nginx_log(kl_instances) + control_systemd_service("nginx", "restart") + + except Exception as e: + Logger.print_error(f"{d_name} installation failed!\n{e}") + return + + log = f"Open {d_name} now on: http://{get_ipv4_addr()}:{client_port}" + Logger.print_ok(f"{d_name} installation complete!", start="\n") + Logger.print_ok(log, prefix=False, end="\n\n") + + +def download_client(client: ClientData) -> None: + zipfile = f"{client.get('name').lower()}.zip" + target = Path().home().joinpath(zipfile) + try: + Logger.print_status(f"Downloading {zipfile} ...") + download_file(client.get("url"), target, True) + Logger.print_ok("Download complete!") + + Logger.print_status(f"Extracting {zipfile} ...") + unzip(target, client.get("dir")) + target.unlink(missing_ok=True) + Logger.print_ok("OK!") + + except Exception: + Logger.print_error(f"Downloading {zipfile} failed!") + raise + + +def update_client(client: ClientData) -> None: + Logger.print_status(f"Updating {client.get('display_name')} ...") + if client.get("name") == "mainsail": + backup_mainsail_config_json(is_temp=True) + + download_client(client) + + if client.get("name") == "mainsail": + restore_mainsail_config_json() + + +def create_client_nginx_cfg(client: ClientData, port: int) -> None: + d_name = client.get("display_name") + root_dir = client.get("dir") + source = NGINX_SITES_AVAILABLE.joinpath(client.get("name")) + target = NGINX_SITES_ENABLED.joinpath(client.get("name")) + try: + Logger.print_status(f"Creating NGINX config for {d_name} ...") + remove_file(Path("/etc/nginx/sites-enabled/default"), True) + create_nginx_cfg(client.get("name"), port, root_dir) + create_symlink(source, target, True) + set_nginx_permissions() + Logger.print_ok(f"NGINX config for {d_name} successfully created.") + except Exception: + Logger.print_error(f"Creating NGINX config for {d_name} failed!") + raise diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py new file mode 100644 index 0000000..40207e5 --- /dev/null +++ b/kiauh/components/webui_client/client_utils.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 shutil +from json import JSONDecodeError +from pathlib import Path +from typing import List, Optional, Dict, Literal, Union + +import urllib.request + +from components.klipper.klipper import Klipper +from components.webui_client import ( + MAINSAIL_CONFIG_JSON, + MAINSAIL_DIR, + MAINSAIL_BACKUP_DIR, + FLUIDD_PRE_RLS_URL, + FLUIDD_BACKUP_DIR, + FLUIDD_URL, + FLUIDD_DIR, + ClientData, + FLUIDD_CONFIG_REPO_URL, + FLUIDD_CONFIG_DIR, + ClientConfigData, + MAINSAIL_PRE_RLS_URL, + MAINSAIL_URL, + MAINSAIL_CONFIG_REPO_URL, + MAINSAIL_CONFIG_DIR, + ClientName, + MAINSAIL_TAGS_URL, + FLUIDD_TAGS_URL, + FLUIDD_CONFIG_BACKUP_DIR, + MAINSAIL_CONFIG_BACKUP_DIR, +) +from core.backup_manager.backup_manager import BackupManager +from core.repo_manager.repo_manager import RepoManager +from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD +from utils.common import get_install_status_webui, get_install_status_common +from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW +from utils.logger import Logger + + +def load_client_data(client_name: ClientName) -> Optional[ClientData]: + client_data = None + + if client_name == "mainsail": + client_config_data = ClientConfigData( + name="mainsail-config", + display_name="Mainsail-Config", + cfg_filename="mainsail.cfg", + dir=MAINSAIL_CONFIG_DIR, + backup_dir=MAINSAIL_CONFIG_BACKUP_DIR, + url=MAINSAIL_CONFIG_REPO_URL, + printer_cfg_section="include mainsail.cfg", + mr_conf_path="~/mainsail-config", + mr_conf_origin=MAINSAIL_CONFIG_REPO_URL, + ) + client_data = ClientData( + name=client_name, + display_name=client_name.capitalize(), + dir=MAINSAIL_DIR, + backup_dir=MAINSAIL_BACKUP_DIR, + url=MAINSAIL_URL, + pre_release_url=MAINSAIL_PRE_RLS_URL, + tags_url=MAINSAIL_TAGS_URL, + remote_mode=True, + mr_conf_repo="mainsail-crew/mainsail", + mr_conf_path="~/mainsail", + client_config=client_config_data, + ) + elif client_name == "fluidd": + client_config_data = ClientConfigData( + name="fluidd-config", + display_name="Fluidd-Config", + cfg_filename="fluidd.cfg", + dir=FLUIDD_CONFIG_DIR, + backup_dir=FLUIDD_CONFIG_BACKUP_DIR, + url=FLUIDD_CONFIG_REPO_URL, + printer_cfg_section="include fluidd.cfg", + mr_conf_path="~/fluidd-config", + mr_conf_origin=FLUIDD_CONFIG_REPO_URL, + ) + client_data = ClientData( + name=client_name, + display_name=client_name.capitalize(), + dir=FLUIDD_DIR, + backup_dir=FLUIDD_BACKUP_DIR, + url=FLUIDD_URL, + pre_release_url=FLUIDD_PRE_RLS_URL, + tags_url=FLUIDD_TAGS_URL, + remote_mode=False, + mr_conf_repo="fluidd-core/fluidd", + mr_conf_path="~/fluidd", + client_config=client_config_data, + ) + + return client_data + + +def get_client_status(client: ClientData) -> str: + return get_install_status_webui( + client.get("dir"), + NGINX_SITES_AVAILABLE.joinpath(client.get("name")), + NGINX_CONFD.joinpath("upstreams.conf"), + NGINX_CONFD.joinpath("common_vars.conf"), + ) + + +def get_client_config_status(client: ClientData) -> Dict[ + Literal["repo", "local", "remote"], + Union[str, int], +]: + client_config = client.get("client_config") + client_config = client_config.get("dir") + + return { + "repo": RepoManager.get_repo_name(client_config), + "local": RepoManager.get_local_commit(client_config), + "remote": RepoManager.get_remote_commit(client_config), + } + + +def get_current_client_config(clients: List[ClientData]) -> str: + installed = [] + for client in clients: + client_config = client.get("client_config") + if client_config.get("dir").exists(): + installed.append(client) + + if len(installed) > 1: + return f"{COLOR_YELLOW}Conflict!{RESET_FORMAT}" + elif len(installed) == 1: + cfg = installed[0].get("client_config") + return f"{COLOR_CYAN}{cfg.get('display_name')}{RESET_FORMAT}" + + return f"{COLOR_CYAN}-{RESET_FORMAT}" + + +def backup_mainsail_config_json(is_temp=False) -> None: + Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") + bm = BackupManager() + if is_temp: + fn = Path.home().joinpath("config.json.kiauh.bak") + bm.backup_file(MAINSAIL_CONFIG_JSON, custom_filename=fn) + else: + bm.backup_file(MAINSAIL_CONFIG_JSON) + + +def restore_mainsail_config_json() -> None: + try: + Logger.print_status(f"Restore '{MAINSAIL_CONFIG_JSON}' ...") + source = Path.home().joinpath("config.json.kiauh.bak") + shutil.copy(source, MAINSAIL_CONFIG_JSON) + except OSError: + Logger.print_info("Unable to restore config.json. Skipped ...") + + +def enable_mainsail_remotemode() -> None: + Logger.print_status("Enable Mainsails remote mode ...") + with open(MAINSAIL_CONFIG_JSON, "r") as f: + config_data = json.load(f) + + if config_data["instancesDB"] == "browser": + Logger.print_info("Remote mode already configured. Skipped ...") + 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) + Logger.print_ok("Mainsails remote mode enabled!") + + +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 = instance.log_dir.joinpath("mainsail-access.log") + if not desti_access.exists(): + desti_access.symlink_to(access_log) + + desti_error = instance.log_dir.joinpath("mainsail-error.log") + if not desti_error.exists(): + desti_error.symlink_to(error_log) + + +def get_local_client_version(client: ClientData) -> str: + relinfo_file = client.get("dir").joinpath("release_info.json") + if not relinfo_file.is_file(): + return "-" + + with open(relinfo_file, "r") as f: + return json.load(f)["version"] + + +def get_remote_client_version(client: ClientData) -> str: + try: + with urllib.request.urlopen(client.get("tags_url")) as response: + data = json.loads(response.read()) + return data[0]["name"] + except (JSONDecodeError, TypeError): + return "ERROR" + + +def backup_client_data(client: ClientData) -> None: + name = client.get("name") + src = client.get("dir") + dest = client.get("backup_dir") + + with open(src.joinpath(".version"), "r") as v: + version = v.readlines()[0] + + bm = BackupManager() + bm.backup_directory(f"{name}-{version}", src, dest) + if name == "mainsail": + bm.backup_file(MAINSAIL_CONFIG_JSON, dest) + bm.backup_file(NGINX_SITES_AVAILABLE.joinpath(name), dest) + + +def backup_client_config_data(client: ClientData) -> None: + client_config = client.get("client_config") + name = client_config.get("name") + source = client_config.get("dir") + target = client_config.get("backup_dir") + bm = BackupManager() + bm.backup_directory(name, source, target) diff --git a/kiauh/components/mainsail/menus/__init__.py b/kiauh/components/webui_client/menus/__init__.py similarity index 100% rename from kiauh/components/mainsail/menus/__init__.py rename to kiauh/components/webui_client/menus/__init__.py diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py new file mode 100644 index 0000000..b4648d6 --- /dev/null +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 typing import Callable, Dict + +from components.webui_client import client_remove, ClientData +from core.menus import BACK_HELP_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN + + +# noinspection PyUnusedLocal +class ClientRemoveMenu(BaseMenu): + def __init__(self, client: ClientData): + self.client = client + self.rm_client = False + self.rm_client_config = False + self.backup_mainsail_config_json = False + self.rm_moonraker_conf_section = False + self.rm_printer_cfg_section = False + + super().__init__( + header=False, + options=self.get_options(), + footer_type=BACK_HELP_FOOTER, + ) + + def get_options(self) -> Dict[str, Callable]: + options = { + "0": self.toggle_all, + "1": self.toggle_rm_client, + "2": self.toggle_rm_client_config, + "3": self.toggle_rm_printer_cfg_section, + "4": self.toggle_rm_moonraker_conf_section, + "c": self.run_removal_process, + } + if self.client.get("name") == "mainsail": + options["5"] = self.toggle_backup_mainsail_config_json + + return options + + def print_menu(self) -> None: + client_name = self.client.get("display_name") + client_config = self.client.get("client_config") + client_config_name = client_config.get("display_name") + + header = f" [ Remove {client_name} ] " + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) + checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" + unchecked = "[ ]" + o1 = checked if self.rm_client else unchecked + o2 = checked if self.rm_client_config else unchecked + o3 = checked if self.rm_printer_cfg_section else unchecked + o4 = checked if self.rm_moonraker_conf_section else unchecked + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | Enter a number and hit enter to select / deselect | + | the specific option for removal. | + |-------------------------------------------------------| + | 0) Select everything | + |-------------------------------------------------------| + | 1) {o1} Remove {client_name:16} | + | 2) {o2} Remove {client_config_name:24} | + | | + | printer.cfg & moonraker.conf | + | 3) {o3} Remove printer.cfg include | + | 4) {o4} Remove Moonraker update section | + """ + )[1:] + + if self.client.get("name") == "mainsail": + o5 = checked if self.backup_mainsail_config_json else unchecked + menu += textwrap.dedent( + f""" + | | + | Mainsail config.json | + | 5) {o5} Backup config.json | + """ + )[1:] + + menu += textwrap.dedent( + """ + |-------------------------------------------------------| + | C) Continue | + """ + )[1:] + print(menu, end="") + + def toggle_all(self, **kwargs) -> None: + self.rm_client = True + self.rm_client_config = True + self.backup_mainsail_config_json = True + self.rm_moonraker_conf_section = True + self.rm_printer_cfg_section = True + + def toggle_rm_client(self, **kwargs) -> None: + self.rm_client = not self.rm_client + + def toggle_rm_client_config(self, **kwargs) -> None: + self.rm_client_config = not self.rm_client_config + + def toggle_backup_mainsail_config_json(self, **kwargs) -> None: + self.backup_mainsail_config_json = not self.backup_mainsail_config_json + + def toggle_rm_moonraker_conf_section(self, **kwargs) -> None: + self.rm_moonraker_conf_section = not self.rm_moonraker_conf_section + + def toggle_rm_printer_cfg_section(self, **kwargs) -> None: + self.rm_printer_cfg_section = not self.rm_printer_cfg_section + + def run_removal_process(self, **kwargs) -> None: + if ( + not self.rm_client + and not self.rm_client_config + and not self.backup_mainsail_config_json + and not self.rm_moonraker_conf_section + and not self.rm_printer_cfg_section + ): + error = f"{COLOR_RED}Nothing selected ...{RESET_FORMAT}" + print(error) + return + + client_remove.run_client_removal( + client=self.client, + rm_client=self.rm_client, + rm_client_config=self.rm_client_config, + backup_ms_config_json=self.backup_mainsail_config_json, + rm_moonraker_conf_section=self.rm_moonraker_conf_section, + rm_printer_cfg_section=self.rm_printer_cfg_section, + ) + + self.rm_client = False + self.rm_client_config = False + self.backup_mainsail_config_json = False + self.rm_moonraker_conf_section = False + self.rm_printer_cfg_section = False diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index 8b92978..459461a 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -12,11 +12,15 @@ import textwrap from components.klipper.klipper_utils import backup_klipper_dir -from components.mainsail.mainsail_utils import backup_mainsail_data from components.moonraker.moonraker_utils import ( backup_moonraker_dir, backup_moonraker_db_dir, ) +from components.webui_client.client_utils import ( + backup_client_data, + load_client_data, + backup_client_config_data, +) from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.common import backup_printer_config_dir @@ -35,6 +39,10 @@ class BackupMenu(BaseMenu): "3": self.backup_printer_config, "4": self.backup_moonraker_db, "5": self.backup_mainsail, + "6": self.backup_fluidd, + "7": self.backup_mainsail_config, + "8": self.backup_fluidd_config, + "9": self.backup_klipperscreen, }, footer_type=BACK_FOOTER, ) @@ -51,15 +59,15 @@ class BackupMenu(BaseMenu): |-------------------------------------------------------| | {line1:^62} | |-------------------------------------------------------| - | Klipper & Moonraker API: | Touchscreen GUI: | - | 1) [Klipper] | 7) [KlipperScreen] | - | 2) [Moonraker] | | - | 3) [Config Folder] | Other: | - | 4) [Moonraker Database] | 9) [Telegram Bot] | - | | | - | Klipper Webinterface: | | - | 5) [Mainsail] | | - | 6) [Fluidd] | | + | Klipper & Moonraker API: | Client-Config: | + | 1) [Klipper] | 7) [Mainsail-Config] | + | 2) [Moonraker] | 8) [Fluidd-Config] | + | 3) [Config Folder] | | + | 4) [Moonraker Database] | Touchscreen GUI: | + | | 9) [KlipperScreen] | + | Webinterface: | | + | 5) [Mainsail] | | + | 6) [Fluidd] | | """ )[1:] print(menu, end="") @@ -77,13 +85,16 @@ class BackupMenu(BaseMenu): backup_moonraker_db_dir() def backup_mainsail(self, **kwargs): - backup_mainsail_data() + backup_client_data(load_client_data("mainsail")) def backup_fluidd(self, **kwargs): - pass + backup_client_data(load_client_data("fluidd")) + + def backup_mainsail_config(self, **kwargs): + backup_client_config_data(load_client_data("mainsail")) + + def backup_fluidd_config(self, **kwargs): + backup_client_config_data(load_client_data("fluidd")) def backup_klipperscreen(self, **kwargs): pass - - def backup_telegram_bot(self, **kwargs): - pass diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index c466fdf..a90e235 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -11,10 +11,10 @@ import textwrap -from components.fluidd import fluidd_setup from components.klipper import klipper_setup -from components.mainsail import mainsail_setup from components.moonraker import moonraker_setup +from components.webui_client import client_setup +from components.webui_client.client_config import client_config_setup from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import COLOR_GREEN, RESET_FORMAT @@ -31,13 +31,11 @@ class InstallMenu(BaseMenu): "2": self.install_moonraker, "3": self.install_mainsail, "4": self.install_fluidd, - "5": self.install_klipperscreen, - "6": self.install_pretty_gcode, - "7": self.install_telegram_bot, - "8": self.install_obico, - "9": self.install_octoeverywhere, - "10": self.install_mobileraker, - "11": self.install_crowsnest, + "5": self.install_mainsail_config, + "6": self.install_fluidd_config, + "7": None, + "8": None, + "9": None, }, footer_type=BACK_FOOTER, ) @@ -51,16 +49,18 @@ class InstallMenu(BaseMenu): /=======================================================\\ | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| - | Firmware & API: | Other: | - | 1) [Klipper] | 6) [PrettyGCode] | - | 2) [Moonraker] | 7) [Telegram Bot] | - | | 8) $(obico_install_title) | - | Klipper Webinterface: | 9) [OctoEverywhere] | - | 3) [Mainsail] | 10) [Mobileraker] | - | 4) [Fluidd] | | - | | Webcam Streamer: | - | Touchscreen GUI: | 11) [Crowsnest] | - | 5) [KlipperScreen] | | + | Firmware & API: | Touchscreen GUI: | + | 1) [Klipper] | 7) [KlipperScreen] | + | 2) [Moonraker] | | + | | Android / iOS: | + | Webinterface: | 8) [Mobileraker] | + | 3) [Mainsail] | | + | 4) [Fluidd] | Webcam Streamer: | + | | 9) [Crowsnest] | + | Client-Config: | | + | 5) [Mainsail-Config] | | + | 6) [Fluidd-Config] | | + | | | """ )[1:] print(menu, end="") @@ -72,28 +72,13 @@ class InstallMenu(BaseMenu): moonraker_setup.install_moonraker() def install_mainsail(self, **kwargs): - mainsail_setup.install_mainsail() + client_setup.install_client(client_name="mainsail") + + def install_mainsail_config(self, **kwargs): + client_config_setup.install_client_config(client_name="mainsail") def install_fluidd(self, **kwargs): - fluidd_setup.install_fluidd() + client_setup.install_client(client_name="fluidd") - def install_klipperscreen(self, **kwargs): - print("install_klipperscreen") - - def install_pretty_gcode(self, **kwargs): - print("install_pretty_gcode") - - def install_telegram_bot(self, **kwargs): - print("install_telegram_bot") - - def install_obico(self, **kwargs): - print("install_obico") - - def install_octoeverywhere(self, **kwargs): - print("install_octoeverywhere") - - def install_mobileraker(self, **kwargs): - print("install_mobileraker") - - def install_crowsnest(self, **kwargs): - print("install_crowsnest") + def install_fluidd_config(self, **kwargs): + client_config_setup.install_client_config(client_name="fluidd") diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 716e046..4ab3b31 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -11,11 +11,14 @@ import textwrap -from components.fluidd.fluidd_utils import get_fluidd_status from components.klipper.klipper_utils import get_klipper_status from components.log_uploads.menus.log_upload_menu import LogUploadMenu -from components.mainsail.mainsail_utils import get_mainsail_status from components.moonraker.moonraker_utils import get_moonraker_status +from components.webui_client.client_utils import ( + get_client_status, + load_client_data, + get_current_client_config, +) from core.menus import QUIT_FOOTER from core.menus.advanced_menu import AdvancedMenu from core.menus.backup_menu import BackupMenu @@ -61,13 +64,11 @@ class MainMenu(BaseMenu): self.ks_status = "" self.mb_status = "" self.cn_status = "" - self.tg_status = "" - self.ob_status = "" - self.oe_status = "" + self.cc_status = "" self.init_status() def init_status(self) -> None: - status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn", "tg", "ob", "oe"] + status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn"] for var in status_vars: setattr(self, f"{var}_status", f"{COLOR_RED}Not installed!{RESET_FORMAT}") @@ -87,9 +88,15 @@ class MainMenu(BaseMenu): self.mr_status = self.format_status_by_code(mr_code, mr_status, mr_instances) self.mr_repo = f"{COLOR_CYAN}{moonraker_status.get('repo')}{RESET_FORMAT}" # mainsail - self.ms_status = get_mainsail_status() + mainsail_client_data = load_client_data("mainsail") + self.ms_status = get_client_status(mainsail_client_data) # fluidd - self.fl_status = get_fluidd_status() + fluidd_client_data = load_client_data("fluidd") + self.fl_status = get_client_status(fluidd_client_data) + # client-config + self.cc_status = get_current_client_config( + [mainsail_client_data, fluidd_client_data] + ) def format_status_by_code(self, code: int, status: str, count: str) -> str: if code == 1: @@ -120,13 +127,11 @@ class MainMenu(BaseMenu): | 4) [Advanced] |------------------------------------| | 5) [Backup] | Mainsail: {self.ms_status:<26} | | | Fluidd: {self.fl_status:<26} | - | E) [Extensions] | KlipperScreen: {self.ks_status:<26} | - | | Mobileraker: {self.mb_status:<26} | + | S) [Settings] | Client-Config: {self.cc_status:<26} | | | | + | Community: | KlipperScreen: {self.ks_status:<26} | + | E) [Extensions] | Mobileraker: {self.mb_status:<26} | | | Crowsnest: {self.cn_status:<26} | - | | Telegram Bot: {self.tg_status:<26} | - | | Obico: {self.ob_status:<26} | - | S) [Settings] | OctoEverywhere: {self.oe_status:<26} | |-------------------------------------------------------| | {COLOR_CYAN}{footer1:^16}{RESET_FORMAT} | {footer2:^43} | """ diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 8df2380..ad53c94 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -11,10 +11,10 @@ import textwrap -from components.fluidd.menus.fluidd_remove_menu import FluiddRemoveMenu from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu -from components.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu from components.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu +from components.webui_client.client_utils import load_client_data +from components.webui_client.menus.client_remove_menu import ClientRemoveMenu from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import COLOR_RED, RESET_FORMAT @@ -29,8 +29,8 @@ class RemoveMenu(BaseMenu): options={ "1": KlipperRemoveMenu, "2": MoonrakerRemoveMenu, - "3": MainsailRemoveMenu, - "4": FluiddRemoveMenu, + "3": ClientRemoveMenu(client=load_client_data("mainsail")), + "4": ClientRemoveMenu(client=load_client_data("fluidd")), "5": None, "6": None, "7": None, diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 04d6de5..ffa74ba 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -11,22 +11,22 @@ import textwrap -from components.fluidd.fluidd_setup import update_fluidd -from components.fluidd.fluidd_utils import ( - get_fluidd_local_version, - get_fluidd_remote_version, -) from components.klipper.klipper_setup import update_klipper from components.klipper.klipper_utils import ( get_klipper_status, ) -from components.mainsail.mainsail_setup import update_mainsail -from components.mainsail.mainsail_utils import ( - get_mainsail_local_version, - get_mainsail_remote_version, -) from components.moonraker.moonraker_setup import update_moonraker from components.moonraker.moonraker_utils import get_moonraker_status +from components.webui_client.client_config.client_config_setup import ( + update_client_config, +) +from components.webui_client.client_setup import update_client +from components.webui_client.client_utils import ( + get_local_client_version, + get_remote_client_version, + load_client_data, + get_client_config_status, +) from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import ( @@ -50,14 +50,12 @@ class UpdateMenu(BaseMenu): "2": self.update_moonraker, "3": self.update_mainsail, "4": self.update_fluidd, - "5": self.update_klipperscreen, - "6": self.update_pgc_for_klipper, - "7": self.update_telegram_bot, - "8": self.update_moonraker_obico, - "9": self.update_octoeverywhere, - "10": self.update_mobileraker, - "11": self.update_crowsnest, - "12": self.upgrade_system_packages, + "5": self.update_mainsail_config, + "6": self.update_fluidd_config, + "7": self.update_klipperscreen, + "8": self.update_mobileraker, + "9": self.update_crowsnest, + "10": self.upgrade_system_packages, }, footer_type=BACK_FOOTER, ) @@ -69,6 +67,10 @@ class UpdateMenu(BaseMenu): self.ms_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.fl_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.fl_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.mc_local = f"{COLOR_WHITE}{RESET_FORMAT}" + self.mc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}" + self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" def print_menu(self): self.fetch_update_status() @@ -87,22 +89,20 @@ class UpdateMenu(BaseMenu): | 1) Klipper | {self.kl_local:<22} | {self.kl_remote:<22} | | 2) Moonraker | {self.mr_local:<22} | {self.mr_remote:<22} | | | | | - | Klipper Webinterface: |---------------|---------------| + | Webinterface: |---------------|---------------| | 3) Mainsail | {self.ms_local:<22} | {self.ms_remote:<22} | | 4) Fluidd | {self.fl_local:<22} | {self.fl_remote:<22} | | | | | - | Touchscreen GUI: |---------------|---------------| - | 5) KlipperScreen | | | + | Client-Config: |---------------|---------------| + | 5) Mainsail-Config | {self.mc_local:<22} | {self.mc_remote:<22} | + | 6) Fluidd-Config | {self.fc_local:<22} | {self.fc_remote:<22} | | | | | | Other: |---------------|---------------| - | 6) PrettyGCode | | | - | 7) Telegram Bot | | | - | 8) Obico for Klipper | | | - | 9) OctoEverywhere | | | - | 10) Mobileraker | | | - | 11) Crowsnest | | | + | 7) KlipperScreen | | | + | 8) Mobileraker | | | + | 9) Crowsnest | | | | |-------------------------------| - | 12) System | | + | 10) System | | """ )[1:] print(menu, end="") @@ -117,34 +117,24 @@ class UpdateMenu(BaseMenu): update_moonraker() def update_mainsail(self, **kwargs): - update_mainsail() + update_client(load_client_data("mainsail")) + + def update_mainsail_config(self, **kwargs): + update_client_config(load_client_data("mainsail")) def update_fluidd(self, **kwargs): - update_fluidd() + update_client(load_client_data("fluidd")) - def update_klipperscreen(self, **kwargs): - print("update_klipperscreen") + def update_fluidd_config(self, **kwargs): + update_client_config(load_client_data("fluidd")) - def update_pgc_for_klipper(self, **kwargs): - print("update_pgc_for_klipper") + def update_klipperscreen(self, **kwargs): ... - def update_telegram_bot(self, **kwargs): - print("update_telegram_bot") + def update_mobileraker(self, **kwargs): ... - def update_moonraker_obico(self, **kwargs): - print("update_moonraker_obico") + def update_crowsnest(self, **kwargs): ... - def update_octoeverywhere(self, **kwargs): - print("update_octoeverywhere") - - def update_mobileraker(self, **kwargs): - print("update_mobileraker") - - def update_crowsnest(self, **kwargs): - print("update_crowsnest") - - def upgrade_system_packages(self, **kwargs): - print("upgrade_system_packages") + def upgrade_system_packages(self, **kwargs): ... def fetch_update_status(self): # klipper @@ -166,18 +156,38 @@ class UpdateMenu(BaseMenu): self.mr_local = f"{COLOR_YELLOW}{self.mr_local}{RESET_FORMAT}" self.mr_remote = f"{COLOR_GREEN}{self.mr_remote}{RESET_FORMAT}" # mainsail - self.ms_local = get_mainsail_local_version() - self.ms_remote = get_mainsail_remote_version() + mainsail_client_data = load_client_data("mainsail") + self.ms_local = get_local_client_version(mainsail_client_data) + self.ms_remote = get_remote_client_version(mainsail_client_data) if self.ms_local == self.ms_remote: self.ms_local = f"{COLOR_GREEN}{self.ms_local}{RESET_FORMAT}" else: self.ms_local = f"{COLOR_YELLOW}{self.ms_local}{RESET_FORMAT}" self.ms_remote = f"{COLOR_GREEN if self.ms_remote != 'ERROR' else COLOR_RED}{self.ms_remote}{RESET_FORMAT}" # fluidd - self.fl_local = get_fluidd_local_version() - self.fl_remote = get_fluidd_remote_version() + fluidd_client_data = load_client_data("fluidd") + self.fl_local = get_local_client_version(fluidd_client_data) + self.fl_remote = get_remote_client_version(fluidd_client_data) if self.fl_local == self.fl_remote: self.fl_local = f"{COLOR_GREEN}{self.fl_local}{RESET_FORMAT}" else: self.fl_local = f"{COLOR_YELLOW}{self.fl_local}{RESET_FORMAT}" self.fl_remote = f"{COLOR_GREEN if self.fl_remote != 'ERROR' else COLOR_RED}{self.fl_remote}{RESET_FORMAT}" + # mainsail-config + mc_status = get_client_config_status(load_client_data("mainsail")) + self.mc_local = mc_status.get("local") + self.mc_remote = mc_status.get("remote") + if self.mc_local == self.mc_remote: + self.mc_local = f"{COLOR_GREEN}{self.mc_local}{RESET_FORMAT}" + else: + self.mc_local = f"{COLOR_YELLOW}{self.mc_local}{RESET_FORMAT}" + self.mc_remote = f"{COLOR_GREEN}{self.mc_remote}{RESET_FORMAT}" + # fluidd-config + fc_status = get_client_config_status(load_client_data("fluidd")) + self.fc_local = fc_status.get("local") + self.fc_remote = fc_status.get("remote") + if self.fc_local == self.mc_remote: + self.fc_local = f"{COLOR_GREEN}{self.fc_local}{RESET_FORMAT}" + else: + self.fc_local = f"{COLOR_YELLOW}{self.fc_local}{RESET_FORMAT}" + self.fc_remote = f"{COLOR_GREEN}{self.fc_remote}{RESET_FORMAT}" diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 14ad26f..b7fed2f 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 + # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # @@ -7,14 +8,20 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # + import re import shutil import subprocess from pathlib import Path from zipfile import ZipFile -from typing import List +from typing import List, Type, TypeVar, Union, Tuple +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.base_instance import BaseInstance +from core.instance_manager.instance_manager import InstanceManager from utils import ( NGINX_SITES_AVAILABLE, MODULE_PATH, @@ -24,6 +31,10 @@ from utils import ( from utils.logger import Logger +B = TypeVar('B', bound='BaseInstance') +ConfigOption = Tuple[str, str] + + def check_file_exist(file_path: Path, sudo=False) -> bool: """ Helper function for checking the existence of a file | @@ -169,3 +180,102 @@ def get_next_free_port(ports_in_use: List[str]) -> str: used_ports = set(map(int, ports_in_use)) return str(min(valid_ports - used_ports)) + + +def add_config_section(section: str, instances: List[B], options: List[ConfigOption] = None) -> None: + for instance in instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Add section '[{section}]' to '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + continue + + cm = ConfigManager(cfg_file) + if cm.config.has_section(section): + Logger.print_info("Section already exist. Skipped ...") + continue + + cm.config.add_section(section) + + if options is not None: + for option in options: + cm.config.set(section, option[0], option[1]) + + cm.write_config() + + +def remove_config_section(section: str, instances: List[B]) -> None: + for instance in instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Remove section '[{section}]' from '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + continue + + cm = ConfigManager(cfg_file) + if not cm.config.has_section(section): + Logger.print_info("Section does not exist. Skipped ...") + continue + + cm.config.remove_section(section) + cm.write_config() + + +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) + if cm.config.has_section(section_name): + Logger.print_info("Section already exist. Skipped ...") + return + + template = MODULE_PATH.joinpath("assets", 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 remove_nginx_config(name: str) -> None: + Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...") + try: + remove_file(NGINX_SITES_AVAILABLE.joinpath(name), True) + remove_file(NGINX_SITES_ENABLED.joinpath(name), True) + + except subprocess.CalledProcessError as e: + log = f"Unable to remove NGINX config '{name}':\n{e.stderr.decode()}" + Logger.print_error(log) + + +def remove_nginx_logs(name: str) -> None: + Logger.print_status(f"Removing NGINX logs for {name.capitalize()} ...") + try: + remove_file(Path(f"/var/log/nginx/{name}-access.log"), True) + remove_file(Path(f"/var/log/nginx/{name}-error.log"), True) + + im = InstanceManager(Klipper) + instances: List[Klipper] = im.instances + if not instances: + return + + for instance in instances: + remove_file(instance.log_dir.joinpath(f"{name}-access.log")) + remove_file(instance.log_dir.joinpath(f"{name}-error.log")) + + except (OSError, subprocess.CalledProcessError) as e: + Logger.print_error(f"Unable to remove NGINX logs:\n{e}")