chore(kiauh): rename "modules" to "components"

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
This commit is contained in:
dw-0
2024-01-29 21:10:14 +01:00
parent ad56b51e70
commit 5a3d21c40b
39 changed files with 66 additions and 67 deletions

View File

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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
MODULE_PATH = Path(__file__).resolve().parent
KLIPPER_DIR = Path.home().joinpath("klipper")
KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env")
KLIPPER_REQUIREMENTS_TXT = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt")
DEFAULT_KLIPPER_REPO_URL = "https://github.com/Klipper3D/klipper"
EXIT_KLIPPER_SETUP = "Exiting Klipper setup ..."

View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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.core.instance_manager.base_instance import BaseInstance
from kiauh.components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH
from kiauh.utils.constants import SYSTEMD
from kiauh.utils.logger import Logger
# noinspection PyMethodMayBeStatic
class Klipper(BaseInstance):
@classmethod
def blacklist(cls) -> List[str]:
return ["None", "mcu"]
def __init__(self, suffix: str = ""):
super().__init__(instance_type=self, suffix=suffix)
self.klipper_dir: Path = KLIPPER_DIR
self.env_dir: Path = KLIPPER_ENV_DIR
self._cfg_file = self.cfg_dir.joinpath("printer.cfg")
self._log = self.log_dir.joinpath("klippy.log")
self._serial = self.comms_dir.joinpath("klippy.serial")
self._uds = self.comms_dir.joinpath("klippy.sock")
@property
def cfg_file(self) -> Path:
return self._cfg_file
@property
def log(self) -> Path:
return self._log
@property
def serial(self) -> Path:
return self._serial
@property
def uds(self) -> Path:
return self._uds
def create(self) -> None:
Logger.print_status("Creating new Klipper Instance ...")
service_template_path = MODULE_PATH.joinpath("res/klipper.service")
service_file_name = self.get_service_file_name(extension=True)
service_file_target = SYSTEMD.joinpath(service_file_name)
env_template_file_path = MODULE_PATH.joinpath("res/klipper.env")
env_file_target = self.sysd_dir.joinpath("klipper.env")
try:
self.create_folders()
self.write_service_file(
service_template_path, service_file_target, env_file_target
)
self.write_env_file(env_template_file_path, env_file_target)
except subprocess.CalledProcessError as e:
Logger.print_error(
f"Error creating service file {service_file_target}: {e}"
)
raise
except OSError as e:
Logger.print_error(f"Error creating env file {env_file_target}: {e}")
raise
def delete(self) -> None:
service_file = self.get_service_file_name(extension=True)
service_file_path = self.get_service_file_path()
Logger.print_status(f"Deleting Klipper Instance: {service_file}")
try:
command = ["sudo", "rm", "-f", service_file_path]
subprocess.run(command, check=True)
Logger.print_ok(f"Service file deleted: {service_file_path}")
except subprocess.CalledProcessError as e:
Logger.print_error(f"Error deleting service file: {e}")
raise
def write_service_file(
self,
service_template_path: Path,
service_file_target: Path,
env_file_target: Path,
) -> None:
service_content = self._prep_service_file(
service_template_path, env_file_target
)
command = ["sudo", "tee", service_file_target]
subprocess.run(
command,
input=service_content.encode(),
stdout=subprocess.DEVNULL,
check=True,
)
Logger.print_ok(f"Service file created: {service_file_target}")
def write_env_file(
self, env_template_file_path: Path, env_file_target: Path
) -> None:
env_file_content = self._prep_env_file(env_template_file_path)
with open(env_file_target, "w") as env_file:
env_file.write(env_file_content)
Logger.print_ok(f"Env file created: {env_file_target}")
def _prep_service_file(
self, service_template_path: Path, env_file_path: Path
) -> str:
try:
with open(service_template_path, "r") as template_file:
template_content = template_file.read()
except FileNotFoundError:
Logger.print_error(
f"Unable to open {service_template_path} - File not found"
)
raise
service_content = template_content.replace("%USER%", self.user)
service_content = service_content.replace(
"%KLIPPER_DIR%", str(self.klipper_dir)
)
service_content = service_content.replace("%ENV%", str(self.env_dir))
service_content = service_content.replace("%ENV_FILE%", str(env_file_path))
return service_content
def _prep_env_file(self, env_template_file_path: Path) -> str:
try:
with open(env_template_file_path, "r") as env_file:
env_template_file_content = env_file.read()
except FileNotFoundError:
Logger.print_error(
f"Unable to open {env_template_file_path} - File not found"
)
raise
env_file_content = env_template_file_content.replace(
"%KLIPPER_DIR%", str(self.klipper_dir)
)
env_file_content = env_file_content.replace(
"%CFG%", f"{self.cfg_dir}/printer.cfg"
)
env_file_content = env_file_content.replace("%SERIAL%", str(self.serial))
env_file_content = env_file_content.replace("%LOG%", str(self.log))
env_file_content = env_file_content.replace("%UDS%", str(self.uds))
return env_file_content

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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 List
from kiauh.core.instance_manager.base_instance import BaseInstance
from kiauh.core.menus.base_menu import print_back_footer
from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN
def print_instance_overview(
instances: List[BaseInstance], show_index=False, show_select_all=False
):
headline = f"{COLOR_GREEN}The following Klipper instances were found:{RESET_FORMAT}"
dialog = textwrap.dedent(
f"""
/=======================================================\\
|{headline:^64}|
|-------------------------------------------------------|
"""
)[1:]
if show_select_all:
select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}"
dialog += f"| {select_all:<63}|\n"
dialog += "| |\n"
for i, s in enumerate(instances):
line = f"{COLOR_CYAN}{f'{i})' if show_index else ''} {s.get_service_file_name()}{RESET_FORMAT}"
dialog += f"| {line:<63}|\n"
print(dialog, end="")
print_back_footer()
def print_select_instance_count_dialog():
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
line2 = f"{COLOR_YELLOW}Setting up too many instances may crash your system.{RESET_FORMAT}"
dialog = textwrap.dedent(
f"""
/=======================================================\\
| Please select the number of Klipper instances to set |
| up. The number of Klipper instances will determine |
| the amount of printers you can run from this host. |
| |
| {line1:<63}|
| {line2:<63}|
"""
)[1:]
print(dialog, end="")
print_back_footer()
def print_select_custom_name_dialog():
line1 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}"
line2 = f"{COLOR_YELLOW}Only alphanumeric characters are allowed!{RESET_FORMAT}"
dialog = textwrap.dedent(
f"""
/=======================================================\\
| You can now assign a custom name to each instance. |
| If skipped, each instance will get an index assigned |
| in ascending order, starting at index '1'. |
| |
| {line1:<63}|
| {line2:<63}|
"""
)[1:]
print(dialog, end="")
print_back_footer()
def print_missing_usergroup_dialog(missing_groups) -> None:
line1 = f"{COLOR_YELLOW}WARNING: Your current user is not in group:{RESET_FORMAT}"
line2 = f"{COLOR_CYAN}● tty{RESET_FORMAT}"
line3 = f"{COLOR_CYAN}● dialout{RESET_FORMAT}"
line4 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}"
line5 = f"{COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT}"
dialog = textwrap.dedent(
f"""
/=======================================================\\
| {line1:<63}|
"""
)[1:]
if "tty" in missing_groups:
dialog += f"| {line2:<63}|\n"
if "dialout" in missing_groups:
dialog += f"| {line3:<63}|\n"
dialog += textwrap.dedent(
f"""
| |
| It is possible that you won't be able to successfully |
| connect and/or flash the controller board without |
| your user being a member of that group. |
| If you want to add the current user to the group(s) |
| listed above, answer with 'Y'. Else skip with 'n'. |
| |
| {line4:<63}|
| {line5:<63}|
\\=======================================================/
"""
)[1:]
print(dialog, end="")
def print_update_warn_dialog() -> None:
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
line2 = f"{COLOR_YELLOW}Do NOT continue if there are ongoing prints running!{RESET_FORMAT}"
line3 = f"{COLOR_YELLOW}All Klipper instances will be restarted during the {RESET_FORMAT}"
line4 = f"{COLOR_YELLOW}update process and ongoing prints WILL FAIL.{RESET_FORMAT}"
dialog = textwrap.dedent(
f"""
/=======================================================\\
| {line1:<63}|
| {line2:<63}|
| {line3:<63}|
| {line4:<63}|
\\=======================================================/
"""
)[1:]
print(dialog, end="")

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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, Union
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR
from kiauh.components.klipper.klipper import Klipper
from kiauh.components.klipper.klipper_dialogs import print_instance_overview
from kiauh.utils.filesystem_utils import remove_file
from kiauh.utils.input_utils import get_selection_input
from kiauh.utils.logger import Logger
def run_klipper_removal(
remove_service: bool,
remove_dir: bool,
remove_env: bool,
delete_logs: bool,
) -> None:
im = InstanceManager(Klipper)
if remove_service:
Logger.print_status("Removing Klipper instances ...")
if im.instances:
instances_to_remove = select_instances_to_remove(im.instances)
remove_instances(im, instances_to_remove)
else:
Logger.print_info("No Klipper Services installed! Skipped ...")
if (remove_dir or remove_env) and im.instances:
Logger.print_warn("There are still other Klipper services installed!")
Logger.print_warn("Therefor the following parts cannot be removed:")
Logger.print_warn(
"""
● Klipper local repository
● Klipper Python environment
""",
False,
)
else:
if remove_dir:
Logger.print_status("Removing Klipper local repository ...")
remove_klipper_dir()
if remove_env:
Logger.print_status("Removing Klipper Python environment ...")
remove_klipper_env()
# delete klipper logs of all instances
if delete_logs:
Logger.print_status("Removing all Klipper logs ...")
delete_klipper_logs(im.instances)
def select_instances_to_remove(
instances: List[Klipper],
) -> Union[List[Klipper], None]:
print_instance_overview(instances, True, True)
options = [str(i) for i in range(len(instances))]
options.extend(["a", "A", "b", "B"])
selection = get_selection_input("Select Klipper instance to remove", options)
instances_to_remove = []
if selection == "b".lower():
return None
elif selection == "a".lower():
instances_to_remove.extend(instances)
else:
instance = instances[int(selection)]
instances_to_remove.append(instance)
return instances_to_remove
def remove_instances(
instance_manager: InstanceManager,
instance_list: List[Klipper],
) -> None:
for instance in instance_list:
Logger.print_status(f"Removing instance {instance.get_service_file_name()} ...")
instance_manager.current_instance = instance
instance_manager.stop_instance()
instance_manager.disable_instance()
instance_manager.delete_instance()
instance_manager.reload_daemon()
def remove_klipper_dir() -> None:
if not KLIPPER_DIR.exists():
Logger.print_info(f"'{KLIPPER_DIR}' does not exist. Skipped ...")
return
try:
shutil.rmtree(KLIPPER_DIR)
except OSError as e:
Logger.print_error(f"Unable to delete '{KLIPPER_DIR}':\n{e}")
def remove_klipper_env() -> None:
if not KLIPPER_ENV_DIR.exists():
Logger.print_info(f"'{KLIPPER_ENV_DIR}' does not exist. Skipped ...")
return
try:
shutil.rmtree(KLIPPER_ENV_DIR)
except OSError as e:
Logger.print_error(f"Unable to delete '{KLIPPER_ENV_DIR}':\n{e}")
def delete_klipper_logs(instances: List[Klipper]) -> None:
all_logfiles = []
for instance in instances:
all_logfiles = list(instance.log_dir.glob("klippy.log*"))
if not all_logfiles:
Logger.print_info("No Klipper logs found. Skipped ...")
return
for log in all_logfiles:
Logger.print_status(f"Remove '{log}'")
remove_file(log)

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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 kiauh import KIAUH_CFG
from kiauh.core.backup_manager.backup_manager import BackupManager
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.components.klipper import (
EXIT_KLIPPER_SETUP,
DEFAULT_KLIPPER_REPO_URL,
KLIPPER_DIR,
KLIPPER_ENV_DIR,
KLIPPER_REQUIREMENTS_TXT,
)
from kiauh.components.klipper.klipper import Klipper
from kiauh.components.klipper.klipper_dialogs import print_update_warn_dialog
from kiauh.components.klipper.klipper_utils import (
handle_disruptive_system_packages,
check_user_groups,
handle_to_multi_instance_conversion,
create_example_printer_cfg,
add_to_existing,
get_install_count,
init_name_scheme,
check_is_single_to_multi_conversion,
update_name_scheme,
handle_instance_naming,
)
from kiauh.core.repo_manager.repo_manager import RepoManager
from kiauh.components.moonraker.moonraker import Moonraker
from kiauh.utils.input_utils import get_confirm
from kiauh.utils.logger import Logger
from kiauh.utils.system_utils import (
parse_packages_from_file,
create_python_venv,
install_python_requirements,
update_system_package_lists,
install_system_packages,
)
def install_klipper() -> None:
kl_im = InstanceManager(Klipper)
# ask to add new instances, if there are existing ones
if kl_im.instances and not add_to_existing():
Logger.print_status(EXIT_KLIPPER_SETUP)
return
install_count = get_install_count()
if install_count is None:
Logger.print_status(EXIT_KLIPPER_SETUP)
return
# create a dict of the size of the existing instances + install count
name_dict = {c: "" for c in range(len(kl_im.instances) + install_count)}
name_scheme = init_name_scheme(kl_im.instances, install_count)
mr_im = InstanceManager(Moonraker)
name_scheme = update_name_scheme(
name_scheme, name_dict, kl_im.instances, mr_im.instances
)
handle_instance_naming(name_dict, name_scheme)
create_example_cfg = get_confirm("Create example printer.cfg?")
try:
if not kl_im.instances:
setup_klipper_prerequesites()
count = 0
for name in name_dict:
if name_dict[name] in [n.suffix for n in kl_im.instances]:
continue
if check_is_single_to_multi_conversion(kl_im.instances):
handle_to_multi_instance_conversion(name_dict[name])
continue
count += 1
create_klipper_instance(name_dict[name], create_example_cfg)
if count == install_count:
break
kl_im.reload_daemon()
except Exception:
Logger.print_error("Klipper installation failed!")
return
# step 4: check/handle conflicting packages/services
handle_disruptive_system_packages()
# step 5: check for required group membership
check_user_groups()
def setup_klipper_prerequesites() -> None:
cm = ConfigManager(cfg_file=KIAUH_CFG)
repo = str(cm.get_value("klipper", "repository_url") or DEFAULT_KLIPPER_REPO_URL)
branch = str(cm.get_value("klipper", "branch") or "master")
repo_manager = RepoManager(
repo=repo,
branch=branch,
target_dir=KLIPPER_DIR,
)
repo_manager.clone_repo()
# install klipper dependencies and create python virtualenv
try:
install_klipper_packages(KLIPPER_DIR)
create_python_venv(KLIPPER_ENV_DIR)
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT)
except Exception:
Logger.print_error("Error during installation of Klipper requirements!")
raise
def install_klipper_packages(klipper_dir: Path) -> None:
script = klipper_dir.joinpath("scripts/install-debian.sh")
packages = parse_packages_from_file(script)
packages = [pkg.replace("python-dev", "python3-dev") for pkg in packages]
# Add dfu-util for octopi-images
packages.append("dfu-util")
# Add dbus requirement for DietPi distro
if Path("/boot/dietpi/.version").exists():
packages.append("dbus")
update_system_package_lists(silent=False)
install_system_packages(packages)
def update_klipper() -> None:
print_update_warn_dialog()
if not get_confirm("Update Klipper now?"):
return
cm = ConfigManager(cfg_file=KIAUH_CFG)
if cm.get_value("kiauh", "backup_before_update"):
bm = BackupManager()
bm.backup_directory("klipper", KLIPPER_DIR)
bm.backup_directory("klippy-env", KLIPPER_ENV_DIR)
instance_manager = InstanceManager(Klipper)
instance_manager.stop_all_instance()
repo = str(cm.get_value("klipper", "repository_url") or DEFAULT_KLIPPER_REPO_URL)
branch = str(cm.get_value("klipper", "branch") or "master")
repo_manager = RepoManager(
repo=repo,
branch=branch,
target_dir=KLIPPER_DIR,
)
repo_manager.pull_repo()
# install possible new system packages
install_klipper_packages(KLIPPER_DIR)
# install possible new python dependencies
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT)
instance_manager.start_all_instance()
def create_klipper_instance(name: str, create_example_cfg: bool) -> None:
kl_im = InstanceManager(Klipper)
new_instance = Klipper(suffix=name)
kl_im.current_instance = new_instance
kl_im.create_instance()
kl_im.enable_instance()
if create_example_cfg:
create_example_printer_cfg(new_instance)
kl_im.start_instance()

View File

@@ -0,0 +1,278 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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 os
import re
import grp
import shutil
import subprocess
import textwrap
from pathlib import Path
from typing import List, Union, Literal, Dict
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.base_instance import BaseInstance
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.core.instance_manager.name_scheme import NameScheme
from kiauh.core.repo_manager.repo_manager import RepoManager
from kiauh.components.klipper import MODULE_PATH, KLIPPER_DIR, KLIPPER_ENV_DIR
from kiauh.components.klipper.klipper import Klipper
from kiauh.components.klipper.klipper_dialogs import (
print_missing_usergroup_dialog,
print_instance_overview,
print_select_instance_count_dialog,
print_select_custom_name_dialog,
)
from kiauh.components.moonraker.moonraker import Moonraker
from kiauh.components.moonraker.moonraker_utils import moonraker_to_multi_conversion
from kiauh.utils.common import get_install_status_common
from kiauh.utils.constants import CURRENT_USER
from kiauh.utils.input_utils import get_confirm, get_string_input, get_number_input
from kiauh.utils.logger import Logger
from kiauh.utils.system_utils import mask_system_service
def get_klipper_status() -> (
Dict[
Literal["status", "status_code", "instances", "repo", "local", "remote"],
Union[str, int],
]
):
status = get_install_status_common(Klipper, KLIPPER_DIR, KLIPPER_ENV_DIR)
return {
"status": status.get("status"),
"status_code": status.get("status_code"),
"instances": status.get("instances"),
"repo": RepoManager.get_repo_name(KLIPPER_DIR),
"local": RepoManager.get_local_commit(KLIPPER_DIR),
"remote": RepoManager.get_remote_commit(KLIPPER_DIR),
}
def check_is_multi_install(
existing_instances: List[Klipper], install_count: int
) -> bool:
return not existing_instances and install_count > 1
def check_is_single_to_multi_conversion(existing_instances: List[Klipper]) -> bool:
return len(existing_instances) == 1 and existing_instances[0].suffix == ""
def init_name_scheme(
existing_instances: List[Klipper], install_count: int
) -> NameScheme:
if check_is_multi_install(
existing_instances, install_count
) or check_is_single_to_multi_conversion(existing_instances):
print_select_custom_name_dialog()
if get_confirm("Assign custom names?", False, allow_go_back=True):
return NameScheme.CUSTOM
else:
return NameScheme.INDEX
else:
return NameScheme.SINGLE
def update_name_scheme(
name_scheme: NameScheme,
name_dict: Dict[int, str],
klipper_instances: List[Klipper],
moonraker_instances: List[Moonraker],
) -> NameScheme:
# if there are more moonraker instances installed than klipper, we
# load their names into the name_dict, as we will detect and enforce that naming scheme
if len(moonraker_instances) > len(klipper_instances):
update_name_dict(name_dict, moonraker_instances)
return detect_name_scheme(moonraker_instances)
elif len(klipper_instances) > 1:
update_name_dict(name_dict, klipper_instances)
return detect_name_scheme(klipper_instances)
else:
return name_scheme
def update_name_dict(name_dict: Dict[int, str], instances: List[BaseInstance]) -> None:
for k, v in enumerate(instances):
name_dict[k] = v.suffix
def handle_instance_naming(name_dict: Dict[int, str], name_scheme: NameScheme) -> None:
if name_scheme == NameScheme.SINGLE:
return
for k in name_dict:
if name_dict[k] == "" and name_scheme == NameScheme.INDEX:
name_dict[k] = str(k + 1)
elif name_dict[k] == "" and name_scheme == NameScheme.CUSTOM:
assign_custom_name(k, name_dict)
def add_to_existing() -> bool:
kl_instances = InstanceManager(Klipper).instances
print_instance_overview(kl_instances)
return get_confirm("Add new instances?", allow_go_back=True)
def get_install_count() -> Union[int, None]:
"""
Print a dialog for selecting the amount of Klipper instances
to set up with an option to navigate back. Returns None if the
user selected to go back, otherwise an integer greater or equal than 1 |
:return: Integer >= 1 or None
"""
kl_instances = InstanceManager(Klipper).instances
print_select_instance_count_dialog()
question = f"Number of{' additional' if len(kl_instances) > 0 else ''} Klipper instances to set up"
return get_number_input(question, 1, default=1, allow_go_back=True)
def assign_custom_name(key: int, name_dict: Dict[int, str]) -> None:
existing_names = []
existing_names.extend(Klipper.blacklist())
existing_names.extend(name_dict[n] for n in name_dict)
question = f"Enter name for instance {key + 1}"
name_dict[key] = get_string_input(question, exclude=existing_names)
def handle_to_multi_instance_conversion(new_name: str) -> None:
Logger.print_status("Converting single instance to multi instances ...")
klipper_to_multi_conversion(new_name)
moonraker_to_multi_conversion(new_name)
def klipper_to_multi_conversion(new_name: str) -> None:
Logger.print_status("Convert Klipper single to multi instance ...")
im = InstanceManager(Klipper)
im.current_instance = im.instances[0]
# temporarily store the data dir path
old_data_dir = im.instances[0].data_dir
# remove the old single instance
im.stop_instance()
im.disable_instance()
im.delete_instance()
# create a new klipper instance with the new name
im.current_instance = Klipper(suffix=new_name)
new_data_dir: Path = im.current_instance.data_dir
# rename the old data dir and use it for the new instance
Logger.print_status(f"Rename '{old_data_dir}' to '{new_data_dir}' ...")
if not new_data_dir.is_dir():
old_data_dir.rename(new_data_dir)
else:
Logger.print_info(f"'{new_data_dir}' already exist. Skipped ...")
im.create_instance()
im.enable_instance()
im.start_instance()
def check_user_groups():
current_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()]
missing_groups = []
if "tty" not in current_groups:
missing_groups.append("tty")
if "dialout" not in current_groups:
missing_groups.append("dialout")
if not missing_groups:
return
print_missing_usergroup_dialog(missing_groups)
if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"):
log = "Skipped adding user to required groups. You might encounter issues."
Logger.warn(log)
return
try:
for group in missing_groups:
Logger.print_status(f"Adding user '{CURRENT_USER}' to group {group} ...")
command = ["sudo", "usermod", "-a", "-G", group, CURRENT_USER]
subprocess.run(command, check=True)
Logger.print_ok(f"Group {group} assigned to user '{CURRENT_USER}'.")
except subprocess.CalledProcessError as e:
Logger.print_error(f"Unable to add user to usergroups: {e}")
raise
log = "Remember to relog/restart this machine for the group(s) to be applied!"
Logger.print_warn(log)
def handle_disruptive_system_packages() -> None:
services = []
command = ["systemctl", "is-enabled", "brltty"]
brltty_status = subprocess.run(command, capture_output=True, text=True)
command = ["systemctl", "is-enabled", "brltty-udev"]
brltty_udev_status = subprocess.run(command, capture_output=True, text=True)
command = ["systemctl", "is-enabled", "ModemManager"]
modem_manager_status = subprocess.run(command, capture_output=True, text=True)
if "enabled" in brltty_status.stdout:
services.append("brltty")
if "enabled" in brltty_udev_status.stdout:
services.append("brltty-udev")
if "enabled" in modem_manager_status.stdout:
services.append("ModemManager")
for service in services if services else []:
try:
log = f"{service} service detected! Masking {service} service ..."
Logger.print_status(log)
mask_system_service(service)
Logger.print_ok(f"{service} service masked!")
except subprocess.CalledProcessError:
warn_msg = textwrap.dedent(
f"""
KIAUH was unable to mask the {service} system service.
Please fix the problem manually. Otherwise, this may have
undesirable effects on the operation of Klipper.
"""
)[1:]
Logger.print_warn(warn_msg)
def detect_name_scheme(instance_list: List[BaseInstance]) -> NameScheme:
pattern = re.compile("^\d+$")
for instance in instance_list:
if not pattern.match(instance.suffix):
return NameScheme.CUSTOM
return NameScheme.INDEX
def get_highest_index(instance_list: List[Klipper]) -> int:
indices = [int(instance.suffix.split("-")[-1]) for instance in instance_list]
return max(indices)
def create_example_printer_cfg(instance: Klipper) -> None:
Logger.print_status(f"Creating example printer.cfg in '{instance.cfg_dir}'")
if instance.cfg_file.is_file():
Logger.print_info(f"'{instance.cfg_file}' already exists.")
return
source = MODULE_PATH.joinpath("res/printer.cfg")
target = instance.cfg_file
try:
shutil.copy(source, target)
except OSError as e:
Logger.print_error(f"Unable to create example printer.cfg:\n{e}")
return
cm = ConfigManager(target)
cm.set_value("virtual_sdcard", "path", str(instance.gcodes_dir))
cm.write_config()
Logger.print_ok(f"Example printer.cfg created in '{instance.cfg_dir}'")

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import textwrap
from kiauh.core.menus import BACK_HELP_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.components.klipper import klipper_remove
from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN
# noinspection PyUnusedLocal
class KlipperRemoveMenu(BaseMenu):
def __init__(self):
super().__init__(
header=False,
options={
0: self.toggle_all,
1: self.toggle_remove_klipper_service,
2: self.toggle_remove_klipper_dir,
3: self.toggle_remove_klipper_env,
4: self.toggle_delete_klipper_logs,
5: self.run_removal_process,
},
footer_type=BACK_HELP_FOOTER,
)
self.remove_klipper_service = False
self.remove_klipper_dir = False
self.remove_klipper_env = False
self.delete_klipper_logs = False
def print_menu(self) -> None:
header = " [ Remove Klipper ] "
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
unchecked = "[ ]"
o1 = checked if self.remove_klipper_service else unchecked
o2 = checked if self.remove_klipper_dir else unchecked
o3 = checked if self.remove_klipper_env else unchecked
o4 = checked if self.delete_klipper_logs 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 Service |
| 2) {o2} Remove Local Repository |
| 3) {o3} Remove Python Environment |
| 4) {o4} Delete all Log-Files |
|-------------------------------------------------------|
| 5) Continue |
"""
)[1:]
print(menu, end="")
def toggle_all(self, **kwargs) -> None:
self.remove_klipper_service = True
self.remove_klipper_dir = True
self.remove_klipper_env = True
self.delete_klipper_logs = True
def toggle_remove_klipper_service(self, **kwargs) -> None:
self.remove_klipper_service = not self.remove_klipper_service
def toggle_remove_klipper_dir(self, **kwargs) -> None:
self.remove_klipper_dir = not self.remove_klipper_dir
def toggle_remove_klipper_env(self, **kwargs) -> None:
self.remove_klipper_env = not self.remove_klipper_env
def toggle_delete_klipper_logs(self, **kwargs) -> None:
self.delete_klipper_logs = not self.delete_klipper_logs
def run_removal_process(self, **kwargs) -> None:
if (
not self.remove_klipper_service
and not self.remove_klipper_dir
and not self.remove_klipper_env
and not self.delete_klipper_logs
):
error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}"
print(error)
return
klipper_remove.run_klipper_removal(
self.remove_klipper_service,
self.remove_klipper_dir,
self.remove_klipper_env,
self.delete_klipper_logs,
)
self.remove_klipper_service = False
self.remove_klipper_dir = False
self.remove_klipper_env = False
self.delete_klipper_logs = False

View File

@@ -0,0 +1 @@
KLIPPER_ARGS="%KLIPPER_DIR%/klippy/klippy.py %CFG% -I %SERIAL% -l %LOG% -a %UDS%"

View File

@@ -0,0 +1,18 @@
[Unit]
Description=Klipper 3D Printer Firmware SV1
Documentation=https://www.klipper3d.org/
After=network-online.target
Wants=udev.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
User=%USER%
RemainAfterExit=yes
WorkingDirectory=%KLIPPER_DIR%
EnvironmentFile=%ENV_FILE%
ExecStart=%ENV%/bin/python $KLIPPER_ARGS
Restart=always
RestartSec=10

View File

@@ -0,0 +1,11 @@
[mcu]
serial: /dev/serial/by-id/<your-mcu-id>
[virtual_sdcard]
path: %GCODES_DIR%
on_error_gcode: CANCEL_PRINT
[printer]
kinematics: none
max_velocity: 1000
max_accel: 1000

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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 Dict, Union, Literal
FileKey = Literal["filepath", "display_name"]
LogFile = Dict[FileKey, Union[str, Path]]

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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 typing import List
from pathlib import Path
import urllib.request
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.components.klipper.klipper import Klipper
from kiauh.components.log_uploads import LogFile
from kiauh.utils.logger import Logger
def get_logfile_list() -> List[LogFile]:
cm = InstanceManager(Klipper)
log_dirs: List[Path] = [instance.log_dir for instance in cm.instances]
logfiles: List[LogFile] = []
for _dir in log_dirs:
for f in _dir.iterdir():
logfiles.append({"filepath": f, "display_name": get_display_name(f)})
return logfiles
def get_display_name(filepath: Path) -> str:
printer = " ".join(filepath.parts[-3].split("_")[:-1])
name = filepath.name
return f"{printer}: {name}"
def upload_logfile(logfile: LogFile) -> None:
file = logfile.get("filepath")
name = logfile.get("display_name")
Logger.print_status(f"Uploading the following logfile from {name} ...")
with open(file, "rb") as f:
headers = {"x-random": ""}
req = urllib.request.Request("http://paste.c-net.org/", headers=headers, data=f)
try:
response = urllib.request.urlopen(req)
link = response.read().decode("utf-8")
Logger.print_ok("Upload successfull! Access it via the following link:")
Logger.print_ok(f">>>> {link}", False)
except Exception as e:
Logger.print_error(f"Uploading logfile failed!")
Logger.print_error(str(e))

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import textwrap
from kiauh.core.menus import BACK_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.components.log_uploads.log_upload_utils import upload_logfile
from kiauh.components.log_uploads.log_upload_utils import get_logfile_list
from kiauh.utils.constants import RESET_FORMAT, COLOR_YELLOW
# noinspection PyMethodMayBeStatic
class LogUploadMenu(BaseMenu):
def __init__(self):
self.logfile_list = get_logfile_list()
options = {index: self.upload for index in range(len(self.logfile_list))}
super().__init__(
header=True,
options=options,
footer_type=BACK_FOOTER,
)
def print_menu(self):
header = " [ Log Upload ] "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent(
f"""
/=======================================================\\
| {color}{header:~^{count}}{RESET_FORMAT} |
|-------------------------------------------------------|
| You can select the following logfiles for uploading: |
| |
"""
)[1:]
logfile_list = get_logfile_list()
for logfile in enumerate(logfile_list):
line = f"{logfile[0]}) {logfile[1].get('display_name')}"
menu += f"| {line:<54}|\n"
print(menu, end="")
def upload(self, **kwargs):
upload_logfile(self.logfile_list[kwargs.get("opt_index")])

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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
MODULE_PATH = Path(__file__).resolve().parent
MAINSAIL_DIR = Path(Path.home(), "mainsail")
MAINSAIL_CONFIG_DIR = Path(Path.home(), "mainsail-config")
MAINSAIL_CONFIG_JSON = Path(MAINSAIL_DIR, "config.json")
MAINSAIL_URL = (
"https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip"
)
MAINSAIL_UNSTABLE_URL = (
"https://github.com/mainsail-crew/mainsail/releases/download/%TAG%/mainsail.zip"
)
MAINSAIL_CONFIG_REPO_URL = "https://github.com/mainsail-crew/mainsail-config.git"

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import textwrap
from kiauh.core.menus.base_menu import print_back_footer
from kiauh.utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN
def print_moonraker_not_found_dialog():
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
line2 = f"{COLOR_YELLOW}No local Moonraker installation was found!{RESET_FORMAT}"
dialog = textwrap.dedent(
f"""
/=======================================================\\
| {line1:<63}|
| {line2:<63}|
|-------------------------------------------------------|
| It is possible to install Mainsail without a local |
| Moonraker installation. If you continue, you need to |
| make sure, that Moonraker is installed on another |
| machine in your network. Otherwise Mainsail will NOT |
| work correctly. |
"""
)[1:]
print(dialog, end="")
print_back_footer()
def print_mainsail_already_installed_dialog():
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
line2 = f"{COLOR_YELLOW}Mainsail seems to be already installed!{RESET_FORMAT}"
dialog = textwrap.dedent(
f"""
/=======================================================\\
| {line1:<63}|
| {line2:<63}|
|-------------------------------------------------------|
| If you continue, your current Mainsail installation |
| will be overwritten. You will not loose any printer |
| configurations and the Moonraker database will remain |
| untouched. |
"""
)[1:]
print(dialog, end="")
print_back_footer()
def print_install_mainsail_config_dialog():
dialog = textwrap.dedent(
f"""
/=======================================================\\
| It is recommended to use special macros in order to |
| have Mainsail fully functional and working. |
| |
| The recommended macros for Mainsail can be seen here: |
| https://github.com/mainsail-crew/mainsail-config |
| |
| If you already use these macros skip this step. |
| Otherwise you should consider to answer with 'Y' to |
| download the recommended macros. |
\\=======================================================/
"""
)[1:]
print(dialog, end="")
def print_mainsail_port_select_dialog(port: str):
port = f"{COLOR_CYAN}{port}{RESET_FORMAT}"
dialog = textwrap.dedent(
f"""
/=======================================================\\
| Please select the port, Mainsail should be served on. |
| If you are unsure what to select, hit Enter to apply |
| the suggested value of: {port:38} |
| |
| In case you need Mainsail to be served on a specific |
| port, you can set it now. Make sure the port is not |
| used by any other application on your system! |
\\=======================================================/
"""
)[1:]
print(dialog, end="")

View File

@@ -0,0 +1,164 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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 kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.components.klipper.klipper import Klipper
from kiauh.components.mainsail import MAINSAIL_DIR, MAINSAIL_CONFIG_DIR
from kiauh.components.mainsail.mainsail_utils import backup_config_json
from kiauh.components.moonraker.moonraker import Moonraker
from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED
from kiauh.utils.filesystem_utils import remove_file
from kiauh.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()

View File

@@ -0,0 +1,256 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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 kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.core.repo_manager.repo_manager import RepoManager
from kiauh.components.klipper.klipper import Klipper
from kiauh.components.mainsail import (
MAINSAIL_URL,
MAINSAIL_DIR,
MAINSAIL_CONFIG_DIR,
MAINSAIL_CONFIG_REPO_URL,
MODULE_PATH,
)
from kiauh.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 kiauh.components.mainsail.mainsail_utils import (
restore_config_json,
enable_mainsail_remotemode,
backup_config_json,
symlink_webui_nginx_log,
)
from kiauh.components.moonraker.moonraker import Moonraker
from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED
from kiauh.utils.common import check_install_dependencies
from kiauh.utils.filesystem_utils import (
unzip,
copy_upstream_nginx_cfg,
copy_common_vars_nginx_cfg,
create_nginx_cfg,
create_symlink,
remove_file,
)
from kiauh.utils.input_utils import get_confirm, get_number_input
from kiauh.utils.logger import Logger
from kiauh.utils.system_utils import (
download_file,
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("res", template_file)
with open(template, "r") as t:
template_content = "\n"
template_content += t.read()
with open(cfg_file, "a") as f:
f.write(template_content)
def patch_printer_config(klipper_instances: List[Klipper]) -> None:
for instance in klipper_instances:
cfg_file = instance.cfg_file
Logger.print_status(f"Including mainsail-config in '{cfg_file}' ...")
if not Path(cfg_file).exists():
Logger.print_warn(f"'{cfg_file}' not found!")
return
cm = ConfigManager(cfg_file)
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]")

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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
import requests
from pathlib import Path
from typing import List
from kiauh.core.backup_manager.backup_manager import BackupManager
from kiauh.components.klipper.klipper import Klipper
from kiauh.components.mainsail import MAINSAIL_CONFIG_JSON, MAINSAIL_DIR
from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_CONFD
from kiauh.utils.common import get_install_status_webui
from kiauh.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"
response = requests.get(url)
data = json.loads(response.text)
return data[0]["name"]

View File

@@ -0,0 +1,122 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import textwrap
from kiauh.core.menus import BACK_HELP_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.components.mainsail import mainsail_remove
from kiauh.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

View File

@@ -0,0 +1,6 @@
[update_manager mainsail-config]
type: git_repo
primary_branch: master
path: ~/mainsail-config
origin: https://github.com/mainsail-crew/mainsail-config.git
managed_services: klipper

View File

@@ -0,0 +1,5 @@
[update_manager mainsail]
type: web
channel: stable
repo: mainsail-crew/mainsail
path: ~/mainsail

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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
MODULE_PATH = Path(__file__).resolve().parent
MOONRAKER_DIR = Path.home().joinpath("moonraker")
MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env")
MOONRAKER_REQUIREMENTS_TXT = MOONRAKER_DIR.joinpath(
"scripts/moonraker-requirements.txt"
)
DEFAULT_MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker"
DEFAULT_MOONRAKER_PORT = 7125
# introduced due to
# https://github.com/Arksine/moonraker/issues/349
# https://github.com/Arksine/moonraker/pull/346
POLKIT_LEGACY_FILE = Path("/etc/polkit-1/localauthority/50-local.d/10-moonraker.pkla")
POLKIT_FILE = Path("/etc/polkit-1/rules.d/moonraker.rules")
POLKIT_USR_FILE = Path("/usr/share/polkit-1/rules.d/moonraker.rules")
POLKIT_SCRIPT = Path.home().joinpath("moonraker/scripts/set-policykit-rules.sh")
EXIT_MOONRAKER_SETUP = "Exiting Moonraker setup ..."

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import textwrap
from kiauh.core.menus import BACK_HELP_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.components.moonraker import moonraker_remove
from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN
# noinspection PyUnusedLocal
class MoonrakerRemoveMenu(BaseMenu):
def __init__(self):
super().__init__(
header=False,
options={
0: self.toggle_all,
1: self.toggle_remove_moonraker_service,
2: self.toggle_remove_moonraker_dir,
3: self.toggle_remove_moonraker_env,
4: self.toggle_remove_moonraker_polkit,
5: self.toggle_delete_moonraker_logs,
6: self.run_removal_process,
},
footer_type=BACK_HELP_FOOTER,
)
self.remove_moonraker_service = False
self.remove_moonraker_dir = False
self.remove_moonraker_env = False
self.remove_moonraker_polkit = False
self.delete_moonraker_logs = False
def print_menu(self) -> None:
header = " [ Remove Moonraker ] "
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
unchecked = "[ ]"
o1 = checked if self.remove_moonraker_service else unchecked
o2 = checked if self.remove_moonraker_dir else unchecked
o3 = checked if self.remove_moonraker_env else unchecked
o4 = checked if self.remove_moonraker_polkit else unchecked
o5 = checked if self.delete_moonraker_logs 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 Service |
| 2) {o2} Remove Local Repository |
| 3) {o3} Remove Python Environment |
| 4) {o4} Remove Policy Kit Rules |
| 5) {o5} Delete all Log-Files |
|-------------------------------------------------------|
| 6) Continue |
"""
)[1:]
print(menu, end="")
def toggle_all(self, **kwargs) -> None:
self.remove_moonraker_service = True
self.remove_moonraker_dir = True
self.remove_moonraker_env = True
self.remove_moonraker_polkit = True
self.delete_moonraker_logs = True
def toggle_remove_moonraker_service(self, **kwargs) -> None:
self.remove_moonraker_service = not self.remove_moonraker_service
def toggle_remove_moonraker_dir(self, **kwargs) -> None:
self.remove_moonraker_dir = not self.remove_moonraker_dir
def toggle_remove_moonraker_env(self, **kwargs) -> None:
self.remove_moonraker_env = not self.remove_moonraker_env
def toggle_remove_moonraker_polkit(self, **kwargs) -> None:
self.remove_moonraker_polkit = not self.remove_moonraker_polkit
def toggle_delete_moonraker_logs(self, **kwargs) -> None:
self.delete_moonraker_logs = not self.delete_moonraker_logs
def run_removal_process(self, **kwargs) -> None:
if (
not self.remove_moonraker_service
and not self.remove_moonraker_dir
and not self.remove_moonraker_env
and not self.remove_moonraker_polkit
and not self.delete_moonraker_logs
):
error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}"
print(error)
return
moonraker_remove.run_moonraker_removal(
self.remove_moonraker_service,
self.remove_moonraker_dir,
self.remove_moonraker_env,
self.remove_moonraker_polkit,
self.delete_moonraker_logs,
)
self.remove_moonraker_service = False
self.remove_moonraker_dir = False
self.remove_moonraker_env = False
self.remove_moonraker_polkit = False
self.delete_moonraker_logs = False

View File

@@ -0,0 +1,147 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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, Union
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.base_instance import BaseInstance
from kiauh.components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR, MODULE_PATH
from kiauh.utils.constants import SYSTEMD
from kiauh.utils.logger import Logger
# noinspection PyMethodMayBeStatic
class Moonraker(BaseInstance):
@classmethod
def blacklist(cls) -> List[str]:
return ["None", "mcu"]
def __init__(self, suffix: str = ""):
super().__init__(instance_type=self, suffix=suffix)
self.moonraker_dir: Path = MOONRAKER_DIR
self.env_dir: Path = MOONRAKER_ENV_DIR
self.cfg_file = self.cfg_dir.joinpath("moonraker.conf")
self.port = self._get_port()
self.backup_dir = self.data_dir.joinpath("backup")
self.certs_dir = self.data_dir.joinpath("certs")
self.db_dir = self.data_dir.joinpath("database")
self.log = self.log_dir.joinpath("moonraker.log")
def create(self, create_example_cfg: bool = False) -> None:
Logger.print_status("Creating new Moonraker Instance ...")
service_template_path = MODULE_PATH.joinpath("res/moonraker.service")
env_template_file_path = MODULE_PATH.joinpath("res/moonraker.env")
service_file_name = self.get_service_file_name(extension=True)
service_file_target = SYSTEMD.joinpath(service_file_name)
env_file_target = self.sysd_dir.joinpath("moonraker.env")
try:
self.create_folders([self.backup_dir, self.certs_dir, self.db_dir])
self.write_service_file(
service_template_path, service_file_target, env_file_target
)
self.write_env_file(env_template_file_path, env_file_target)
except subprocess.CalledProcessError as e:
Logger.print_error(
f"Error creating service file {service_file_target}: {e}"
)
raise
except OSError as e:
Logger.print_error(f"Error writing file: {e}")
raise
def delete(self) -> None:
service_file = self.get_service_file_name(extension=True)
service_file_path = self.get_service_file_path()
Logger.print_status(f"Deleting Moonraker Instance: {service_file}")
try:
command = ["sudo", "rm", "-f", service_file_path]
subprocess.run(command, check=True)
Logger.print_ok(f"Service file deleted: {service_file_path}")
except subprocess.CalledProcessError as e:
Logger.print_error(f"Error deleting service file: {e}")
raise
def write_service_file(
self,
service_template_path: Path,
service_file_target: Path,
env_file_target: Path,
) -> None:
service_content = self._prep_service_file(
service_template_path, env_file_target
)
command = ["sudo", "tee", service_file_target]
subprocess.run(
command,
input=service_content.encode(),
stdout=subprocess.DEVNULL,
check=True,
)
Logger.print_ok(f"Service file created: {service_file_target}")
def write_env_file(
self, env_template_file_path: Path, env_file_target: Path
) -> None:
env_file_content = self._prep_env_file(env_template_file_path)
with open(env_file_target, "w") as env_file:
env_file.write(env_file_content)
Logger.print_ok(f"Env file created: {env_file_target}")
def _prep_service_file(
self, service_template_path: Path, env_file_path: Path
) -> str:
try:
with open(service_template_path, "r") as template_file:
template_content = template_file.read()
except FileNotFoundError:
Logger.print_error(
f"Unable to open {service_template_path} - File not found"
)
raise
service_content = template_content.replace("%USER%", self.user)
service_content = service_content.replace(
"%MOONRAKER_DIR%", str(self.moonraker_dir)
)
service_content = service_content.replace("%ENV%", str(self.env_dir))
service_content = service_content.replace("%ENV_FILE%", str(env_file_path))
return service_content
def _prep_env_file(self, env_template_file_path: Path) -> str:
try:
with open(env_template_file_path, "r") as env_file:
env_template_file_content = env_file.read()
except FileNotFoundError:
Logger.print_error(
f"Unable to open {env_template_file_path} - File not found"
)
raise
env_file_content = env_template_file_content.replace(
"%MOONRAKER_DIR%", str(self.moonraker_dir)
)
env_file_content = env_file_content.replace(
"%PRINTER_DATA%", str(self.data_dir)
)
return env_file_content
def _get_port(self) -> Union[int, None]:
if not self.cfg_file.is_file():
return None
cm = ConfigManager(cfg_file=self.cfg_file)
port = cm.get_value("server", "port")
return int(port) if port is not None else port

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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 List
from kiauh.core.menus.base_menu import print_back_footer
from kiauh.components.klipper.klipper import Klipper
from kiauh.components.moonraker.moonraker import Moonraker
from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN
def print_moonraker_overview(
klipper_instances: List[Klipper],
moonraker_instances: List[Moonraker],
show_index=False,
show_select_all=False,
):
headline = f"{COLOR_GREEN}The following instances were found:{RESET_FORMAT}"
dialog = textwrap.dedent(
f"""
/=======================================================\\
|{headline:^64}|
|-------------------------------------------------------|
"""
)[1:]
if show_select_all:
select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}"
dialog += f"| {select_all:<63}|\n"
dialog += "| |\n"
instance_map = {
k.get_service_file_name(): k.get_service_file_name().replace(
"klipper", "moonraker"
)
if k.suffix in [m.suffix for m in moonraker_instances]
else ""
for k in klipper_instances
}
for i, k in enumerate(instance_map):
mr_name = instance_map.get(k)
m = f"<-> {mr_name}" if mr_name != "" else ""
line = f"{COLOR_CYAN}{f'{i})' if show_index else ''} {k} {m} {RESET_FORMAT}"
dialog += f"| {line:<63}|\n"
warn_l1 = f"{COLOR_YELLOW}PLEASE NOTE: {RESET_FORMAT}"
warn_l2 = f"{COLOR_YELLOW}If you select an instance with an existing Moonraker{RESET_FORMAT}"
warn_l3 = f"{COLOR_YELLOW}instance, that Moonraker instance will be re-created!{RESET_FORMAT}"
warning = textwrap.dedent(
f"""
| |
|-------------------------------------------------------|
| {warn_l1:<63}|
| {warn_l2:<63}|
| {warn_l3:<63}|
"""
)[1:]
dialog += warning
print(dialog, end="")
print_back_footer()

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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, Union
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.components.klipper.klipper_dialogs import print_instance_overview
from kiauh.components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR
from kiauh.components.moonraker.moonraker import Moonraker
from kiauh.utils.filesystem_utils import remove_file
from kiauh.utils.input_utils import get_selection_input
from kiauh.utils.logger import Logger
def run_moonraker_removal(
remove_service: bool,
remove_dir: bool,
remove_env: bool,
remove_polkit: bool,
delete_logs: bool,
) -> None:
im = InstanceManager(Moonraker)
if remove_service:
Logger.print_status("Removing Moonraker instances ...")
if im.instances:
instances_to_remove = select_instances_to_remove(im.instances)
remove_instances(im, instances_to_remove)
else:
Logger.print_info("No Moonraker Services installed! Skipped ...")
if (remove_polkit or remove_dir or remove_env) and im.instances:
Logger.print_warn("There are still other Moonraker services installed!")
Logger.print_warn("Therefor the following parts cannot be removed:")
Logger.print_warn(
"""
● Moonraker PolicyKit rules
● Moonraker local repository
● Moonraker Python environment
""",
False,
)
else:
if remove_polkit:
Logger.print_status("Removing all Moonraker policykit rules ...")
remove_polkit_rules()
if remove_dir:
Logger.print_status("Removing Moonraker local repository ...")
remove_moonraker_dir()
if remove_env:
Logger.print_status("Removing Moonraker Python environment ...")
remove_moonraker_env()
# delete moonraker logs of all instances
if delete_logs:
Logger.print_status("Removing all Moonraker logs ...")
delete_moonraker_logs(im.instances)
def select_instances_to_remove(
instances: List[Moonraker],
) -> Union[List[Moonraker], None]:
print_instance_overview(instances, True, True)
options = [str(i) for i in range(len(instances))]
options.extend(["a", "A", "b", "B"])
selection = get_selection_input("Select Moonraker instance to remove", options)
instances_to_remove = []
if selection == "b".lower():
return None
elif selection == "a".lower():
instances_to_remove.extend(instances)
else:
instance = instances[int(selection)]
instances_to_remove.append(instance)
return instances_to_remove
def remove_instances(
instance_manager: InstanceManager,
instance_list: List[Moonraker],
) -> None:
for instance in instance_list:
Logger.print_status(f"Removing instance {instance.get_service_file_name()} ...")
instance_manager.current_instance = instance
instance_manager.stop_instance()
instance_manager.disable_instance()
instance_manager.delete_instance()
instance_manager.reload_daemon()
def remove_moonraker_dir() -> None:
if not MOONRAKER_DIR.exists():
Logger.print_info(f"'{MOONRAKER_DIR}' does not exist. Skipped ...")
return
try:
shutil.rmtree(MOONRAKER_DIR)
except OSError as e:
Logger.print_error(f"Unable to delete '{MOONRAKER_DIR}':\n{e}")
def remove_moonraker_env() -> None:
if not MOONRAKER_ENV_DIR.exists():
Logger.print_info(f"'{MOONRAKER_ENV_DIR}' does not exist. Skipped ...")
return
try:
shutil.rmtree(MOONRAKER_ENV_DIR)
except OSError as e:
Logger.print_error(f"Unable to delete '{MOONRAKER_ENV_DIR}':\n{e}")
def remove_polkit_rules() -> None:
if not MOONRAKER_DIR.exists():
log = "Cannot remove policykit rules. Moonraker directory not found."
Logger.print_warn(log)
return
try:
command = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"]
subprocess.run(
command, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, check=True
)
except subprocess.CalledProcessError as e:
Logger.print_error(f"Error while removing policykit rules: {e}")
Logger.print_ok("Policykit rules successfully removed!")
def delete_moonraker_logs(instances: List[Moonraker]) -> None:
all_logfiles = []
for instance in instances:
all_logfiles = list(instance.log_dir.glob("moonraker.log*"))
if not all_logfiles:
Logger.print_info("No Moonraker logs found. Skipped ...")
return
for log in all_logfiles:
Logger.print_status(f"Remove '{log}'")
remove_file(log)

View File

@@ -0,0 +1,233 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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
import sys
from pathlib import Path
from typing import List
from kiauh import KIAUH_CFG
from kiauh.core.backup_manager.backup_manager import BackupManager
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.components.klipper.klipper import Klipper
from kiauh.components.klipper.klipper_dialogs import print_instance_overview
from kiauh.core.repo_manager.repo_manager import RepoManager
from kiauh.components.mainsail import MAINSAIL_DIR
from kiauh.components.mainsail.mainsail_utils import enable_mainsail_remotemode
from kiauh.components.moonraker import (
EXIT_MOONRAKER_SETUP,
DEFAULT_MOONRAKER_REPO_URL,
MOONRAKER_DIR,
MOONRAKER_ENV_DIR,
MOONRAKER_REQUIREMENTS_TXT,
POLKIT_LEGACY_FILE,
POLKIT_FILE,
POLKIT_USR_FILE,
POLKIT_SCRIPT,
)
from kiauh.components.moonraker.moonraker import Moonraker
from kiauh.components.moonraker.moonraker_dialogs import print_moonraker_overview
from kiauh.components.moonraker.moonraker_utils import create_example_moonraker_conf
from kiauh.utils.filesystem_utils import check_file_exist
from kiauh.utils.input_utils import (
get_confirm,
get_selection_input,
)
from kiauh.utils.logger import Logger
from kiauh.utils.system_utils import (
parse_packages_from_file,
create_python_venv,
install_python_requirements,
update_system_package_lists,
install_system_packages,
)
def install_moonraker() -> None:
if not check_moonraker_install_requirements():
return
kl_im = InstanceManager(Klipper)
klipper_instances = kl_im.instances
mr_im = InstanceManager(Moonraker)
moonraker_instances = mr_im.instances
selected_klipper_instance = 0
if len(klipper_instances) > 1:
print_moonraker_overview(
klipper_instances,
moonraker_instances,
show_index=True,
show_select_all=True,
)
options = [str(i) for i in range(len(klipper_instances))]
options.extend(["a", "A", "b", "B"])
question = "Select Klipper instance to setup Moonraker for"
selected_klipper_instance = get_selection_input(question, options).lower()
instance_names = []
if selected_klipper_instance == "b":
Logger.print_status(EXIT_MOONRAKER_SETUP)
return
elif selected_klipper_instance == "a":
for instance in klipper_instances:
instance_names.append(instance.suffix)
else:
index = int(selected_klipper_instance)
instance_names.append(klipper_instances[index].suffix)
create_example_cfg = get_confirm("Create example moonraker.conf?")
setup_moonraker_prerequesites()
install_moonraker_polkit()
used_ports_map = {
instance.suffix: instance.port for instance in moonraker_instances
}
for name in instance_names:
current_instance = Moonraker(suffix=name)
mr_im.current_instance = current_instance
mr_im.create_instance()
mr_im.enable_instance()
if create_example_cfg:
create_example_moonraker_conf(current_instance, used_ports_map)
mr_im.start_instance()
mr_im.reload_daemon()
# if mainsail is installed, and we installed
# multiple moonraker instances, we enable mainsails remote mode
if MAINSAIL_DIR.exists() and len(mr_im.instances) > 1:
enable_mainsail_remotemode()
def check_moonraker_install_requirements() -> bool:
if not (sys.version_info.major >= 3 and sys.version_info.minor >= 7):
Logger.print_error("Versioncheck failed!")
Logger.print_error("Python 3.7 or newer required to run Moonraker.")
return False
kl_instance_count = len(InstanceManager(Klipper).instances)
if kl_instance_count < 1:
Logger.print_warn("Klipper not installed!")
Logger.print_warn("Moonraker cannot be installed! Install Klipper first.")
return False
mr_instance_count = len(InstanceManager(Moonraker).instances)
if mr_instance_count >= kl_instance_count:
Logger.print_warn("Unable to install more Moonraker instances!")
Logger.print_warn("More Klipper instances required.")
return False
return True
def setup_moonraker_prerequesites() -> None:
cm = ConfigManager(cfg_file=KIAUH_CFG)
repo = str(
cm.get_value("moonraker", "repository_url") or DEFAULT_MOONRAKER_REPO_URL
)
branch = str(cm.get_value("moonraker", "branch") or "master")
repo_manager = RepoManager(
repo=repo,
branch=branch,
target_dir=MOONRAKER_DIR,
)
repo_manager.clone_repo()
# install moonraker dependencies and create python virtualenv
install_moonraker_packages(MOONRAKER_DIR)
create_python_venv(MOONRAKER_ENV_DIR)
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT)
def install_moonraker_packages(moonraker_dir: Path) -> None:
script = moonraker_dir.joinpath("scripts/install-moonraker.sh")
packages = parse_packages_from_file(script)
update_system_package_lists(silent=False)
install_system_packages(packages)
def install_moonraker_polkit() -> None:
Logger.print_status("Installing Moonraker policykit rules ...")
legacy_file_exists = check_file_exist(POLKIT_LEGACY_FILE, True)
polkit_file_exists = check_file_exist(POLKIT_FILE, True)
usr_file_exists = check_file_exist(POLKIT_USR_FILE, True)
if legacy_file_exists or (polkit_file_exists and usr_file_exists):
Logger.print_info("Moonraker policykit rules are already installed.")
return
try:
command = [POLKIT_SCRIPT, "--disable-systemctl"]
result = subprocess.run(
command, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True
)
if result.returncode != 0 or result.stderr:
Logger.print_error(f"{result.stderr}", False)
Logger.print_error("Installing Moonraker policykit rules failed!")
return
Logger.print_ok("Moonraker policykit rules successfully installed!")
except subprocess.CalledProcessError as e:
log = f"Error while installing Moonraker policykit rules: {e.stderr.decode()}"
Logger.print_error(log)
def handle_existing_instances(instance_list: List[Klipper]) -> bool:
instance_count = len(instance_list)
if instance_count > 0:
print_instance_overview(instance_list)
if not get_confirm("Add new instances?", allow_go_back=True):
return False
return True
def update_moonraker() -> None:
if not get_confirm("Update Moonraker now?"):
return
cm = ConfigManager(cfg_file=KIAUH_CFG)
if cm.get_value("kiauh", "backup_before_update"):
bm = BackupManager()
bm.backup_directory("moonraker", MOONRAKER_DIR)
bm.backup_directory("moonraker-env", MOONRAKER_ENV_DIR)
instance_manager = InstanceManager(Moonraker)
instance_manager.stop_all_instance()
repo = str(
cm.get_value("moonraker", "repository_url") or DEFAULT_MOONRAKER_REPO_URL
)
branch = str(cm.get_value("moonraker", "branch") or "master")
repo_manager = RepoManager(
repo=repo,
branch=branch,
target_dir=MOONRAKER_DIR,
)
repo_manager.pull_repo()
# install possible new system packages
install_moonraker_packages(MOONRAKER_DIR)
# install possible new python dependencies
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT)
instance_manager.start_all_instance()

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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 Dict, Literal, List, Union
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.core.repo_manager.repo_manager import RepoManager
from kiauh.components.mainsail import MAINSAIL_DIR
from kiauh.components.mainsail.mainsail_utils import enable_mainsail_remotemode
from kiauh.components.moonraker import (
DEFAULT_MOONRAKER_PORT,
MODULE_PATH,
MOONRAKER_DIR,
MOONRAKER_ENV_DIR,
)
from kiauh.components.moonraker.moonraker import Moonraker
from kiauh.utils.common import get_install_status_common
from kiauh.utils.logger import Logger
from kiauh.utils.system_utils import (
get_ipv4_addr,
)
def get_moonraker_status() -> (
Dict[
Literal["status", "status_code", "instances", "repo", "local", "remote"],
Union[str, int],
]
):
status = get_install_status_common(Moonraker, MOONRAKER_DIR, MOONRAKER_ENV_DIR)
return {
"status": status.get("status"),
"status_code": status.get("status_code"),
"instances": status.get("instances"),
"repo": RepoManager.get_repo_name(MOONRAKER_DIR),
"local": RepoManager.get_local_commit(MOONRAKER_DIR),
"remote": RepoManager.get_remote_commit(MOONRAKER_DIR),
}
def create_example_moonraker_conf(
instance: Moonraker, ports_map: Dict[str, int]
) -> None:
Logger.print_status(f"Creating example moonraker.conf in '{instance.cfg_dir}'")
if instance.cfg_file.is_file():
Logger.print_info(f"'{instance.cfg_file}' already exists.")
return
source = MODULE_PATH.joinpath("res/moonraker.conf")
target = instance.cfg_file
try:
shutil.copy(source, target)
except OSError as e:
Logger.print_error(f"Unable to create example moonraker.conf:\n{e}")
return
ports = [
ports_map.get(instance)
for instance in ports_map
if ports_map.get(instance) is not None
]
if ports_map.get(instance.suffix) is None:
# this could be improved to not increment the max value of the ports list and assign it as the port
# as it can lead to situation where the port for e.g. instance moonraker-2 becomes 7128 if the port
# of moonraker-1 is 7125 and moonraker-3 is 7127 and there are moonraker.conf files for moonraker-1
# and moonraker-3 already. though, there does not seem to be a very reliable way of always assigning
# the correct port to each instance and the user will likely be required to correct the value manually.
port = max(ports) + 1 if ports else DEFAULT_MOONRAKER_PORT
else:
port = ports_map.get(instance.suffix)
ports_map[instance.suffix] = port
ip = get_ipv4_addr().split(".")[:2]
ip.extend(["0", "0/16"])
uds = instance.comms_dir.joinpath("klippy.sock")
cm = ConfigManager(target)
trusted_clients = f"\n{'.'.join(ip)}"
trusted_clients += cm.get_value("authorization", "trusted_clients")
cm.set_value("server", "port", str(port))
cm.set_value("server", "klippy_uds_address", str(uds))
cm.set_value("authorization", "trusted_clients", trusted_clients)
cm.write_config()
Logger.print_ok(f"Example moonraker.conf created in '{instance.cfg_dir}'")
def moonraker_to_multi_conversion(new_name: str) -> None:
"""
Converts the first instance in the List of Moonraker instances to an instance
with a new name. This method will be called when converting from a single Klipper
instance install to a multi instance install when Moonraker is also already
installed with a single instance.
:param new_name: new name the previous single instance is renamed to
:return: None
"""
im = InstanceManager(Moonraker)
instances: List[Moonraker] = im.instances
if not instances:
return
# in case there are multiple Moonraker instances, we don't want to do anything
if len(instances) > 1:
Logger.print_info("More than a single Moonraker instance found. Skipped ...")
return
Logger.print_status("Convert Moonraker single to multi instance ...")
# remove the old single instance
im.current_instance = im.instances[0]
im.stop_instance()
im.disable_instance()
im.delete_instance()
# create a new klipper instance with the new name
im.current_instance = Moonraker(suffix=new_name)
# create, enable and start the new moonraker instance
im.create_instance()
im.enable_instance()
im.start_instance()
# if mainsail is installed, we enable mainsails remote mode
if MAINSAIL_DIR.exists() and len(im.instances) > 1:
enable_mainsail_remotemode()

View File

@@ -0,0 +1,29 @@
[server]
host: 0.0.0.0
port: %PORT%
klippy_uds_address: %UDS%
[authorization]
trusted_clients:
10.0.0.0/8
127.0.0.0/8
169.254.0.0/16
172.16.0.0/12
192.168.0.0/16
FE80::/10
::1/128
cors_domains:
*.lan
*.local
*://localhost
*://localhost:*
*://my.mainsail.xyz
*://app.fluidd.xyz
[octoprint_compat]
[history]
[update_manager]
channel: dev
refresh_interval: 168

View File

@@ -0,0 +1 @@
MOONRAKER_ARGS="%MOONRAKER_DIR%/moonraker/moonraker.py -d %PRINTER_DATA%"

View File

@@ -0,0 +1,19 @@
[Unit]
Description=API Server for Klipper SV1
Documentation=https://moonraker.readthedocs.io/
Requires=network-online.target
After=network-online.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
User=%USER%
SupplementaryGroups=moonraker-admin
RemainAfterExit=yes
WorkingDirectory=%MOONRAKER_DIR%
EnvironmentFile=%ENV_FILE%
ExecStart=%ENV%/bin/python $MOONRAKER_ARGS
Restart=always
RestartSec=10