mirror of
https://github.com/dw-0/kiauh.git
synced 2025-12-25 16:53:36 +05:00
Compare commits
2 Commits
ea8621af0c
...
v6.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c91816d13f | ||
|
|
1a6f06eaf2 |
@@ -11,5 +11,5 @@ end_of_line = lf
|
|||||||
[*.py]
|
[*.py]
|
||||||
max_line_length = 88
|
max_line_length = 88
|
||||||
|
|
||||||
[*.{sh,yml,yaml}]
|
[*.{sh,yml,yaml,json}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
@@ -102,6 +102,8 @@ class KlipperSetupService:
|
|||||||
self.moonraker_list = self.misvc.get_all_instances()
|
self.moonraker_list = self.misvc.get_all_instances()
|
||||||
|
|
||||||
def install(self) -> None:
|
def install(self) -> None:
|
||||||
|
self.__refresh_state()
|
||||||
|
|
||||||
Logger.print_status("Installing Klipper ...")
|
Logger.print_status("Installing Klipper ...")
|
||||||
|
|
||||||
match_moonraker: bool = False
|
match_moonraker: bool = False
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu):
|
|||||||
self.flash_options.selected_baudrate = get_number_input(
|
self.flash_options.selected_baudrate = get_number_input(
|
||||||
question="Please set the baud rate",
|
question="Please set the baud rate",
|
||||||
default=250000,
|
default=250000,
|
||||||
min_count=0,
|
min_value=0,
|
||||||
allow_go_back=True,
|
allow_go_back=True,
|
||||||
)
|
)
|
||||||
KlipperFlashOverviewMenu(previous_menu=self.__class__).run()
|
KlipperFlashOverviewMenu(previous_menu=self.__class__).run()
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ from __future__ import annotations
|
|||||||
import textwrap
|
import textwrap
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from components.moonraker import moonraker_remove
|
from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService
|
||||||
from core.menus import Option
|
from core.menus import FooterType, Option
|
||||||
from core.menus.base_menu import BaseMenu
|
from core.menus.base_menu import BaseMenu
|
||||||
from core.types.color import Color
|
from core.types.color import Color
|
||||||
|
|
||||||
@@ -21,14 +21,19 @@ from core.types.color import Color
|
|||||||
class MoonrakerRemoveMenu(BaseMenu):
|
class MoonrakerRemoveMenu(BaseMenu):
|
||||||
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
|
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.title = "Remove Moonraker"
|
self.title = "Remove Moonraker"
|
||||||
self.title_color = Color.RED
|
self.title_color = Color.RED
|
||||||
self.previous_menu: Type[BaseMenu] | None = previous_menu
|
self.previous_menu: Type[BaseMenu] | None = previous_menu
|
||||||
self.remove_moonraker_service = False
|
self.footer_type = FooterType.BACK
|
||||||
self.remove_moonraker_dir = False
|
|
||||||
self.remove_moonraker_env = False
|
self.rm_svc = False
|
||||||
self.remove_moonraker_polkit = False
|
self.rm_dir = False
|
||||||
self.selection_state = False
|
self.rm_env = False
|
||||||
|
self.rm_pk = False
|
||||||
|
self.select_state = False
|
||||||
|
|
||||||
|
self.mrsvc = MoonrakerSetupService()
|
||||||
|
|
||||||
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
|
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
|
||||||
from core.menus.remove_menu import RemoveMenu
|
from core.menus.remove_menu import RemoveMenu
|
||||||
@@ -48,17 +53,18 @@ class MoonrakerRemoveMenu(BaseMenu):
|
|||||||
def print_menu(self) -> None:
|
def print_menu(self) -> None:
|
||||||
checked = f"[{Color.apply('x', Color.CYAN)}]"
|
checked = f"[{Color.apply('x', Color.CYAN)}]"
|
||||||
unchecked = "[ ]"
|
unchecked = "[ ]"
|
||||||
o1 = checked if self.remove_moonraker_service else unchecked
|
o1 = checked if self.rm_svc else unchecked
|
||||||
o2 = checked if self.remove_moonraker_dir else unchecked
|
o2 = checked if self.rm_dir else unchecked
|
||||||
o3 = checked if self.remove_moonraker_env else unchecked
|
o3 = checked if self.rm_env else unchecked
|
||||||
o4 = checked if self.remove_moonraker_polkit else unchecked
|
o4 = checked if self.rm_pk else unchecked
|
||||||
|
sel_state = f"{'Select' if not self.select_state else 'Deselect'} everything"
|
||||||
menu = textwrap.dedent(
|
menu = textwrap.dedent(
|
||||||
f"""
|
f"""
|
||||||
╟───────────────────────────────────────────────────────╢
|
╟───────────────────────────────────────────────────────╢
|
||||||
║ Enter a number and hit enter to select / deselect ║
|
║ Enter a number and hit enter to select / deselect ║
|
||||||
║ the specific option for removal. ║
|
║ the specific option for removal. ║
|
||||||
╟───────────────────────────────────────────────────────╢
|
╟───────────────────────────────────────────────────────╢
|
||||||
║ a) {self._get_selection_state_str():37} ║
|
║ a) {sel_state:49} ║
|
||||||
╟───────────────────────────────────────────────────────╢
|
╟───────────────────────────────────────────────────────╢
|
||||||
║ 1) {o1} Remove Service ║
|
║ 1) {o1} Remove Service ║
|
||||||
║ 2) {o2} Remove Local Repository ║
|
║ 2) {o2} Remove Local Repository ║
|
||||||
@@ -72,57 +78,33 @@ class MoonrakerRemoveMenu(BaseMenu):
|
|||||||
print(menu, end="")
|
print(menu, end="")
|
||||||
|
|
||||||
def toggle_all(self, **kwargs) -> None:
|
def toggle_all(self, **kwargs) -> None:
|
||||||
self.selection_state = not self.selection_state
|
self.select_state = not self.select_state
|
||||||
self.remove_moonraker_service = self.selection_state
|
self.rm_svc = self.select_state
|
||||||
self.remove_moonraker_dir = self.selection_state
|
self.rm_dir = self.select_state
|
||||||
self.remove_moonraker_env = self.selection_state
|
self.rm_env = self.select_state
|
||||||
self.remove_moonraker_polkit = self.selection_state
|
self.rm_pk = self.select_state
|
||||||
|
|
||||||
def toggle_remove_moonraker_service(self, **kwargs) -> None:
|
def toggle_remove_moonraker_service(self, **kwargs) -> None:
|
||||||
self.remove_moonraker_service = not self.remove_moonraker_service
|
self.rm_svc = not self.rm_svc
|
||||||
|
|
||||||
def toggle_remove_moonraker_dir(self, **kwargs) -> None:
|
def toggle_remove_moonraker_dir(self, **kwargs) -> None:
|
||||||
self.remove_moonraker_dir = not self.remove_moonraker_dir
|
self.rm_dir = not self.rm_dir
|
||||||
|
|
||||||
def toggle_remove_moonraker_env(self, **kwargs) -> None:
|
def toggle_remove_moonraker_env(self, **kwargs) -> None:
|
||||||
self.remove_moonraker_env = not self.remove_moonraker_env
|
self.rm_env = not self.rm_env
|
||||||
|
|
||||||
def toggle_remove_moonraker_polkit(self, **kwargs) -> None:
|
def toggle_remove_moonraker_polkit(self, **kwargs) -> None:
|
||||||
self.remove_moonraker_polkit = not self.remove_moonraker_polkit
|
self.rm_pk = not self.rm_pk
|
||||||
|
|
||||||
def run_removal_process(self, **kwargs) -> None:
|
def run_removal_process(self, **kwargs) -> None:
|
||||||
if (
|
if not self.rm_svc and not self.rm_dir and not self.rm_env and not self.rm_pk:
|
||||||
not self.remove_moonraker_service
|
msg = "Nothing selected! Select options to remove first."
|
||||||
and not self.remove_moonraker_dir
|
print(Color.apply(msg, Color.RED))
|
||||||
and not self.remove_moonraker_env
|
|
||||||
and not self.remove_moonraker_polkit
|
|
||||||
):
|
|
||||||
print(
|
|
||||||
Color.apply(
|
|
||||||
"Nothing selected! Select options to remove first.", Color.RED
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
moonraker_remove.run_moonraker_removal(
|
self.mrsvc.remove(self.rm_svc, self.rm_dir, self.rm_env, self.rm_pk)
|
||||||
self.remove_moonraker_service,
|
|
||||||
self.remove_moonraker_dir,
|
|
||||||
self.remove_moonraker_env,
|
|
||||||
self.remove_moonraker_polkit,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.remove_moonraker_service = False
|
self.rm_svc = False
|
||||||
self.remove_moonraker_dir = False
|
self.rm_dir = False
|
||||||
self.remove_moonraker_env = False
|
self.rm_env = False
|
||||||
self.remove_moonraker_polkit = False
|
self.rm_pk = False
|
||||||
|
|
||||||
self._go_back()
|
|
||||||
|
|
||||||
def _get_selection_state_str(self) -> str:
|
|
||||||
return (
|
|
||||||
"Select everything" if not self.selection_state else "Deselect everything"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _go_back(self, **kwargs) -> None:
|
|
||||||
if self.previous_menu is not None:
|
|
||||||
self.previous_menu().run()
|
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
# ======================================================================= #
|
|
||||||
# Copyright (C) 2020 - 2025 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 __future__ import annotations
|
|
||||||
|
|
||||||
from subprocess import DEVNULL, PIPE, CalledProcessError, run
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from components.klipper.klipper_dialogs import print_instance_overview
|
|
||||||
from components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR
|
|
||||||
from components.moonraker.moonraker import Moonraker
|
|
||||||
from core.instance_manager.instance_manager import InstanceManager
|
|
||||||
from core.logger import Logger
|
|
||||||
from utils.fs_utils import run_remove_routines
|
|
||||||
from utils.input_utils import get_selection_input
|
|
||||||
from utils.instance_utils import get_instances
|
|
||||||
from utils.sys_utils import unit_file_exists
|
|
||||||
|
|
||||||
|
|
||||||
def run_moonraker_removal(
|
|
||||||
remove_service: bool,
|
|
||||||
remove_dir: bool,
|
|
||||||
remove_env: bool,
|
|
||||||
remove_polkit: bool,
|
|
||||||
) -> None:
|
|
||||||
instances = get_instances(Moonraker)
|
|
||||||
|
|
||||||
if remove_service:
|
|
||||||
Logger.print_status("Removing Moonraker instances ...")
|
|
||||||
if instances:
|
|
||||||
instances_to_remove = select_instances_to_remove(instances)
|
|
||||||
remove_instances(instances_to_remove)
|
|
||||||
else:
|
|
||||||
Logger.print_info("No Moonraker Services installed! Skipped ...")
|
|
||||||
|
|
||||||
delete_remaining: bool = remove_polkit or remove_dir or remove_env
|
|
||||||
if delete_remaining and unit_file_exists("moonraker", suffix="service"):
|
|
||||||
Logger.print_info("There are still other Moonraker services installed")
|
|
||||||
Logger.print_info(
|
|
||||||
"● Moonraker PolicyKit rules were not removed.", prefix=False
|
|
||||||
)
|
|
||||||
Logger.print_info(f"● '{MOONRAKER_DIR}' was not removed.", prefix=False)
|
|
||||||
Logger.print_info(f"● '{MOONRAKER_ENV_DIR}' was not removed.", prefix=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 ...")
|
|
||||||
run_remove_routines(MOONRAKER_DIR)
|
|
||||||
if remove_env:
|
|
||||||
Logger.print_status("Removing Moonraker Python environment ...")
|
|
||||||
run_remove_routines(MOONRAKER_ENV_DIR)
|
|
||||||
|
|
||||||
|
|
||||||
def select_instances_to_remove(
|
|
||||||
instances: List[Moonraker],
|
|
||||||
) -> List[Moonraker] | None:
|
|
||||||
start_index = 1
|
|
||||||
options = [str(i + start_index) for i in range(len(instances))]
|
|
||||||
options.extend(["a", "b"])
|
|
||||||
instance_map = {options[i]: instances[i] for i in range(len(instances))}
|
|
||||||
|
|
||||||
print_instance_overview(
|
|
||||||
instances,
|
|
||||||
start_index=start_index,
|
|
||||||
show_index=True,
|
|
||||||
show_select_all=True,
|
|
||||||
)
|
|
||||||
selection = get_selection_input("Select Moonraker instance to remove", options)
|
|
||||||
|
|
||||||
instances_to_remove = []
|
|
||||||
if selection == "b":
|
|
||||||
return None
|
|
||||||
elif selection == "a":
|
|
||||||
instances_to_remove.extend(instances)
|
|
||||||
else:
|
|
||||||
instances_to_remove.append(instance_map[selection])
|
|
||||||
|
|
||||||
return instances_to_remove
|
|
||||||
|
|
||||||
|
|
||||||
def remove_instances(
|
|
||||||
instance_list: List[Moonraker] | None,
|
|
||||||
) -> None:
|
|
||||||
if not instance_list:
|
|
||||||
Logger.print_info("No Moonraker instances found. Skipped ...")
|
|
||||||
return
|
|
||||||
for instance in instance_list:
|
|
||||||
Logger.print_status(f"Removing instance {instance.service_file_path.stem} ...")
|
|
||||||
InstanceManager.remove(instance)
|
|
||||||
delete_moonraker_env_file(instance)
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
cmd = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"]
|
|
||||||
run(cmd, stderr=PIPE, stdout=DEVNULL, check=True)
|
|
||||||
except CalledProcessError as e:
|
|
||||||
Logger.print_error(f"Error while removing policykit rules: {e}")
|
|
||||||
|
|
||||||
Logger.print_ok("Policykit rules successfully removed!")
|
|
||||||
|
|
||||||
|
|
||||||
def delete_moonraker_env_file(instance: Moonraker):
|
|
||||||
Logger.print_status(f"Remove '{instance.env_file}'")
|
|
||||||
if not instance.env_file.exists():
|
|
||||||
msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..."
|
|
||||||
Logger.print_info(msg)
|
|
||||||
return
|
|
||||||
run_remove_routines(instance.env_file)
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
# ======================================================================= #
|
|
||||||
# Copyright (C) 2020 - 2025 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 __future__ import annotations
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from components.klipper.klipper import Klipper
|
|
||||||
from components.moonraker import (
|
|
||||||
EXIT_MOONRAKER_SETUP,
|
|
||||||
MOONRAKER_DEPS_JSON_FILE,
|
|
||||||
MOONRAKER_DIR,
|
|
||||||
MOONRAKER_ENV_DIR,
|
|
||||||
MOONRAKER_INSTALL_SCRIPT,
|
|
||||||
MOONRAKER_REPO_URL,
|
|
||||||
MOONRAKER_REQ_FILE,
|
|
||||||
MOONRAKER_SPEEDUPS_REQ_FILE,
|
|
||||||
POLKIT_FILE,
|
|
||||||
POLKIT_LEGACY_FILE,
|
|
||||||
POLKIT_SCRIPT,
|
|
||||||
POLKIT_USR_FILE,
|
|
||||||
)
|
|
||||||
from components.moonraker.moonraker import Moonraker
|
|
||||||
from components.moonraker.moonraker_dialogs import print_moonraker_overview
|
|
||||||
from components.moonraker.services.moonraker_instance_service import (
|
|
||||||
MoonrakerInstanceService,
|
|
||||||
)
|
|
||||||
from components.moonraker.utils.sysdeps_parser import SysDepsParser
|
|
||||||
from components.moonraker.utils.utils import (
|
|
||||||
backup_moonraker_dir,
|
|
||||||
create_example_moonraker_conf,
|
|
||||||
load_sysdeps_json,
|
|
||||||
)
|
|
||||||
from components.webui_client.client_utils import (
|
|
||||||
enable_mainsail_remotemode,
|
|
||||||
get_existing_clients,
|
|
||||||
)
|
|
||||||
from components.webui_client.mainsail_data import MainsailData
|
|
||||||
from core.instance_manager.instance_manager import InstanceManager
|
|
||||||
from core.logger import DialogType, Logger
|
|
||||||
from core.settings.kiauh_settings import KiauhSettings
|
|
||||||
from core.types.color import Color
|
|
||||||
from utils.common import check_install_dependencies
|
|
||||||
from utils.fs_utils import check_file_exist
|
|
||||||
from utils.git_utils import git_clone_wrapper, git_pull_wrapper
|
|
||||||
from utils.input_utils import (
|
|
||||||
get_confirm,
|
|
||||||
get_selection_input,
|
|
||||||
)
|
|
||||||
from utils.instance_utils import get_instances
|
|
||||||
from utils.sys_utils import (
|
|
||||||
check_python_version,
|
|
||||||
cmd_sysctl_manage,
|
|
||||||
cmd_sysctl_service,
|
|
||||||
create_python_venv,
|
|
||||||
get_ipv4_addr,
|
|
||||||
install_python_requirements,
|
|
||||||
parse_packages_from_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def install_moonraker() -> None:
|
|
||||||
klipper_list: List[Klipper] = get_instances(Klipper)
|
|
||||||
|
|
||||||
if not check_moonraker_install_requirements(klipper_list):
|
|
||||||
return
|
|
||||||
|
|
||||||
instance_service = MoonrakerInstanceService()
|
|
||||||
instance_service.load_instances()
|
|
||||||
|
|
||||||
moonraker_list: List[Moonraker] = instance_service.get_all_instances()
|
|
||||||
new_instances: List[Moonraker] = []
|
|
||||||
selected_option: str | Klipper
|
|
||||||
|
|
||||||
if len(klipper_list) == 1:
|
|
||||||
suffix: str = klipper_list[0].suffix
|
|
||||||
new_inst = instance_service.create_new_instance(suffix)
|
|
||||||
new_instances.append(new_inst)
|
|
||||||
|
|
||||||
else:
|
|
||||||
print_moonraker_overview(
|
|
||||||
klipper_list,
|
|
||||||
moonraker_list,
|
|
||||||
show_index=True,
|
|
||||||
show_select_all=True,
|
|
||||||
)
|
|
||||||
options = {str(i + 1): k for i, k in enumerate(klipper_list)}
|
|
||||||
additional_options = {"a": None, "b": None}
|
|
||||||
options = {**options, **additional_options}
|
|
||||||
question = "Select Klipper instance to setup Moonraker for"
|
|
||||||
selected_option = get_selection_input(question, options)
|
|
||||||
|
|
||||||
if selected_option == "b":
|
|
||||||
Logger.print_status(EXIT_MOONRAKER_SETUP)
|
|
||||||
return
|
|
||||||
|
|
||||||
if selected_option == "a":
|
|
||||||
new_inst_list: List[Moonraker] = [
|
|
||||||
instance_service.create_new_instance(k.suffix) for k in klipper_list
|
|
||||||
]
|
|
||||||
new_instances.extend(new_inst_list)
|
|
||||||
else:
|
|
||||||
klipper_instance: Klipper | None = options.get(selected_option)
|
|
||||||
if klipper_instance is None:
|
|
||||||
raise Exception("Error selecting instance!")
|
|
||||||
new_inst = instance_service.create_new_instance(klipper_instance.suffix)
|
|
||||||
new_instances.append(new_inst)
|
|
||||||
|
|
||||||
create_example_cfg = get_confirm("Create example moonraker.conf?")
|
|
||||||
|
|
||||||
try:
|
|
||||||
check_install_dependencies()
|
|
||||||
setup_moonraker_prerequesites()
|
|
||||||
install_moonraker_polkit()
|
|
||||||
|
|
||||||
ports_map = instance_service.get_instance_port_map()
|
|
||||||
for instance in new_instances:
|
|
||||||
instance.create()
|
|
||||||
cmd_sysctl_service(instance.service_file_path.name, "enable")
|
|
||||||
|
|
||||||
if create_example_cfg:
|
|
||||||
# if a webclient and/or it's config is installed, patch
|
|
||||||
# its update section to the config
|
|
||||||
clients = get_existing_clients()
|
|
||||||
create_example_moonraker_conf(instance, ports_map, clients)
|
|
||||||
|
|
||||||
cmd_sysctl_service(instance.service_file_path.name, "start")
|
|
||||||
|
|
||||||
cmd_sysctl_manage("daemon-reload")
|
|
||||||
|
|
||||||
# if mainsail is installed, and we installed
|
|
||||||
# multiple moonraker instances, we enable mainsails remote mode
|
|
||||||
if MainsailData().client_dir.exists() and len(moonraker_list) > 1:
|
|
||||||
enable_mainsail_remotemode()
|
|
||||||
|
|
||||||
instance_service.load_instances()
|
|
||||||
new_instances = [
|
|
||||||
instance_service.get_instance_by_suffix(i.suffix) for i in new_instances
|
|
||||||
]
|
|
||||||
|
|
||||||
ip: str = get_ipv4_addr()
|
|
||||||
# noinspection HttpUrlsUsage
|
|
||||||
url_list = [
|
|
||||||
f"● {i.service_file_path.stem}: http://{ip}:{i.port}"
|
|
||||||
for i in new_instances
|
|
||||||
if i.port
|
|
||||||
]
|
|
||||||
dialog_content = []
|
|
||||||
if url_list:
|
|
||||||
dialog_content.append("You can access Moonraker via the following URL:")
|
|
||||||
dialog_content.extend(url_list)
|
|
||||||
|
|
||||||
Logger.print_dialog(
|
|
||||||
DialogType.CUSTOM,
|
|
||||||
custom_title="Moonraker successfully installed!",
|
|
||||||
custom_color=Color.GREEN,
|
|
||||||
content=dialog_content,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
Logger.print_error(f"Error while installing Moonraker: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def check_moonraker_install_requirements(klipper_list: List[Klipper]) -> bool:
|
|
||||||
def check_klipper_instances() -> bool:
|
|
||||||
if len(klipper_list) >= 1:
|
|
||||||
return True
|
|
||||||
|
|
||||||
Logger.print_warn("Klipper not installed!")
|
|
||||||
Logger.print_warn("Moonraker cannot be installed! Install Klipper first.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return check_python_version(3, 7) and check_klipper_instances()
|
|
||||||
|
|
||||||
|
|
||||||
def setup_moonraker_prerequesites() -> None:
|
|
||||||
settings = KiauhSettings()
|
|
||||||
default_repo = (MOONRAKER_REPO_URL, "master")
|
|
||||||
repo = settings.moonraker.repositories
|
|
||||||
# pull the first repo defined in kiauh.cfg or fallback to the official Moonraker repo
|
|
||||||
repo, branch = (repo[0].url, repo[0].branch) if repo else default_repo
|
|
||||||
git_clone_wrapper(repo, MOONRAKER_DIR, branch)
|
|
||||||
|
|
||||||
# install moonraker dependencies and create python virtualenv
|
|
||||||
install_moonraker_packages()
|
|
||||||
if create_python_venv(MOONRAKER_ENV_DIR):
|
|
||||||
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE)
|
|
||||||
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_SPEEDUPS_REQ_FILE)
|
|
||||||
|
|
||||||
|
|
||||||
def install_moonraker_packages() -> None:
|
|
||||||
Logger.print_status("Parsing Moonraker system dependencies ...")
|
|
||||||
|
|
||||||
moonraker_deps = []
|
|
||||||
if MOONRAKER_DEPS_JSON_FILE.exists():
|
|
||||||
Logger.print_info(
|
|
||||||
f"Parsing system dependencies from {MOONRAKER_DEPS_JSON_FILE.name} ..."
|
|
||||||
)
|
|
||||||
parser = SysDepsParser()
|
|
||||||
sysdeps = load_sysdeps_json(MOONRAKER_DEPS_JSON_FILE)
|
|
||||||
moonraker_deps.extend(parser.parse_dependencies(sysdeps))
|
|
||||||
|
|
||||||
elif MOONRAKER_INSTALL_SCRIPT.exists():
|
|
||||||
Logger.print_warn(f"{MOONRAKER_DEPS_JSON_FILE.name} not found!")
|
|
||||||
Logger.print_info(
|
|
||||||
f"Parsing system dependencies from {MOONRAKER_INSTALL_SCRIPT.name} ..."
|
|
||||||
)
|
|
||||||
moonraker_deps = parse_packages_from_file(MOONRAKER_INSTALL_SCRIPT)
|
|
||||||
|
|
||||||
if not moonraker_deps:
|
|
||||||
raise ValueError("Error parsing Moonraker dependencies!")
|
|
||||||
|
|
||||||
check_install_dependencies({*moonraker_deps})
|
|
||||||
|
|
||||||
|
|
||||||
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 update_moonraker() -> None:
|
|
||||||
if not get_confirm("Update Moonraker now?"):
|
|
||||||
return
|
|
||||||
|
|
||||||
settings = KiauhSettings()
|
|
||||||
if settings.kiauh.backup_before_update:
|
|
||||||
backup_moonraker_dir()
|
|
||||||
|
|
||||||
instances = get_instances(Moonraker)
|
|
||||||
InstanceManager.stop_all(instances)
|
|
||||||
|
|
||||||
git_pull_wrapper(MOONRAKER_DIR)
|
|
||||||
|
|
||||||
# install possible new system packages
|
|
||||||
install_moonraker_packages()
|
|
||||||
# install possible new python dependencies
|
|
||||||
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE)
|
|
||||||
|
|
||||||
InstanceManager.start_all(instances)
|
|
||||||
407
kiauh/components/moonraker/services/moonraker_setup_service.py
Normal file
407
kiauh/components/moonraker/services/moonraker_setup_service.py
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2025 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 __future__ import annotations
|
||||||
|
|
||||||
|
from copy import copy
|
||||||
|
from subprocess import DEVNULL, PIPE, CalledProcessError, run
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper.klipper_dialogs import print_instance_overview
|
||||||
|
from components.klipper.services.klipper_instance_service import KlipperInstanceService
|
||||||
|
from components.moonraker import (
|
||||||
|
EXIT_MOONRAKER_SETUP,
|
||||||
|
MOONRAKER_DIR,
|
||||||
|
MOONRAKER_ENV_DIR,
|
||||||
|
MOONRAKER_REPO_URL,
|
||||||
|
MOONRAKER_REQ_FILE,
|
||||||
|
MOONRAKER_SPEEDUPS_REQ_FILE,
|
||||||
|
POLKIT_FILE,
|
||||||
|
POLKIT_LEGACY_FILE,
|
||||||
|
POLKIT_SCRIPT,
|
||||||
|
POLKIT_USR_FILE,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.moonraker.moonraker_dialogs import print_moonraker_overview
|
||||||
|
from components.moonraker.services.moonraker_instance_service import (
|
||||||
|
MoonrakerInstanceService,
|
||||||
|
)
|
||||||
|
from components.moonraker.utils.utils import (
|
||||||
|
backup_moonraker_dir,
|
||||||
|
create_example_moonraker_conf,
|
||||||
|
install_moonraker_packages,
|
||||||
|
remove_polkit_rules,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
enable_mainsail_remotemode,
|
||||||
|
get_existing_clients,
|
||||||
|
)
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.logger import DialogType, Logger
|
||||||
|
from core.services.message_service import Message, MessageService
|
||||||
|
from core.settings.kiauh_settings import KiauhSettings
|
||||||
|
from core.types.color import Color
|
||||||
|
from utils.common import check_install_dependencies
|
||||||
|
from utils.fs_utils import check_file_exist, run_remove_routines
|
||||||
|
from utils.git_utils import git_clone_wrapper, git_pull_wrapper
|
||||||
|
from utils.input_utils import (
|
||||||
|
get_confirm,
|
||||||
|
get_selection_input,
|
||||||
|
)
|
||||||
|
from utils.sys_utils import (
|
||||||
|
check_python_version,
|
||||||
|
cmd_sysctl_manage,
|
||||||
|
cmd_sysctl_service,
|
||||||
|
create_python_venv,
|
||||||
|
get_ipv4_addr,
|
||||||
|
install_python_requirements,
|
||||||
|
unit_file_exists,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class MoonrakerSetupService:
|
||||||
|
__cls_instance = None
|
||||||
|
|
||||||
|
kisvc: KlipperInstanceService
|
||||||
|
misvc: MoonrakerInstanceService
|
||||||
|
msgsvc = MessageService
|
||||||
|
|
||||||
|
settings: KiauhSettings
|
||||||
|
klipper_list: List[Klipper]
|
||||||
|
moonraker_list: List[Moonraker]
|
||||||
|
|
||||||
|
def __new__(cls) -> "MoonrakerSetupService":
|
||||||
|
if cls.__cls_instance is None:
|
||||||
|
cls.__cls_instance = super(MoonrakerSetupService, cls).__new__(cls)
|
||||||
|
return cls.__cls_instance
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
if not hasattr(self, "__initialized"):
|
||||||
|
self.__initialized = False
|
||||||
|
if self.__initialized:
|
||||||
|
return
|
||||||
|
self.__initialized = True
|
||||||
|
self.__init_state()
|
||||||
|
|
||||||
|
def __init_state(self) -> None:
|
||||||
|
self.settings = KiauhSettings()
|
||||||
|
|
||||||
|
self.kisvc = KlipperInstanceService()
|
||||||
|
self.kisvc.load_instances()
|
||||||
|
self.klipper_list = self.kisvc.get_all_instances()
|
||||||
|
|
||||||
|
self.misvc = MoonrakerInstanceService()
|
||||||
|
self.misvc.load_instances()
|
||||||
|
self.moonraker_list = self.misvc.get_all_instances()
|
||||||
|
|
||||||
|
self.msgsvc = MessageService()
|
||||||
|
|
||||||
|
def __refresh_state(self) -> None:
|
||||||
|
self.kisvc.load_instances()
|
||||||
|
self.klipper_list = self.kisvc.get_all_instances()
|
||||||
|
|
||||||
|
self.misvc.load_instances()
|
||||||
|
self.moonraker_list = self.misvc.get_all_instances()
|
||||||
|
|
||||||
|
def install(self) -> None:
|
||||||
|
self.__refresh_state()
|
||||||
|
|
||||||
|
if not self.__check_requirements(self.klipper_list):
|
||||||
|
return
|
||||||
|
|
||||||
|
new_instances: List[Moonraker] = []
|
||||||
|
selected_option: str | Klipper
|
||||||
|
|
||||||
|
if len(self.klipper_list) == 1:
|
||||||
|
suffix: str = self.klipper_list[0].suffix
|
||||||
|
new_inst = self.misvc.create_new_instance(suffix)
|
||||||
|
new_instances.append(new_inst)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print_moonraker_overview(
|
||||||
|
self.klipper_list,
|
||||||
|
self.moonraker_list,
|
||||||
|
show_index=True,
|
||||||
|
show_select_all=True,
|
||||||
|
)
|
||||||
|
options = {str(i + 1): k for i, k in enumerate(self.klipper_list)}
|
||||||
|
additional_options = {"a": None, "b": None}
|
||||||
|
options = {**options, **additional_options}
|
||||||
|
question = "Select Klipper instance to setup Moonraker for"
|
||||||
|
selected_option = get_selection_input(question, options)
|
||||||
|
|
||||||
|
if selected_option == "b":
|
||||||
|
Logger.print_status(EXIT_MOONRAKER_SETUP)
|
||||||
|
return
|
||||||
|
|
||||||
|
if selected_option == "a":
|
||||||
|
new_inst_list: List[Moonraker] = [
|
||||||
|
self.misvc.create_new_instance(k.suffix) for k in self.klipper_list
|
||||||
|
]
|
||||||
|
new_instances.extend(new_inst_list)
|
||||||
|
else:
|
||||||
|
klipper_instance: Klipper | None = options.get(selected_option)
|
||||||
|
if klipper_instance is None:
|
||||||
|
raise Exception("Error selecting instance!")
|
||||||
|
new_inst = self.misvc.create_new_instance(klipper_instance.suffix)
|
||||||
|
new_instances.append(new_inst)
|
||||||
|
|
||||||
|
create_example_cfg = get_confirm("Create example moonraker.conf?")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.__run_setup(new_instances, create_example_cfg)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"Error while installing Moonraker: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.WARNING,
|
||||||
|
[
|
||||||
|
"Be careful if there are ongoing prints running!",
|
||||||
|
"All Moonraker instances will be restarted during the update process and "
|
||||||
|
"ongoing prints COULD FAIL.",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not get_confirm("Update Moonraker now?"):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.__refresh_state()
|
||||||
|
|
||||||
|
if self.settings.kiauh.backup_before_update:
|
||||||
|
backup_moonraker_dir()
|
||||||
|
|
||||||
|
InstanceManager.stop_all(self.moonraker_list)
|
||||||
|
git_pull_wrapper(MOONRAKER_DIR)
|
||||||
|
install_moonraker_packages()
|
||||||
|
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE)
|
||||||
|
InstanceManager.start_all(self.moonraker_list)
|
||||||
|
|
||||||
|
def remove(
|
||||||
|
self,
|
||||||
|
remove_service: bool,
|
||||||
|
remove_dir: bool,
|
||||||
|
remove_env: bool,
|
||||||
|
remove_polkit: bool,
|
||||||
|
) -> None:
|
||||||
|
self.__refresh_state()
|
||||||
|
|
||||||
|
completion_msg = Message(
|
||||||
|
title="Moonraker Removal Process completed",
|
||||||
|
color=Color.GREEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
if remove_service:
|
||||||
|
Logger.print_status("Removing Moonraker instances ...")
|
||||||
|
if self.moonraker_list:
|
||||||
|
instances_to_remove = self.__get_instances_to_remove()
|
||||||
|
self.__remove_instances(instances_to_remove)
|
||||||
|
if instances_to_remove:
|
||||||
|
instance_names = [
|
||||||
|
i.service_file_path.stem for i in instances_to_remove
|
||||||
|
]
|
||||||
|
txt = f"● Moonraker instances removed: {', '.join(instance_names)}"
|
||||||
|
completion_msg.text.append(txt)
|
||||||
|
else:
|
||||||
|
Logger.print_info("No Moonraker Services installed! Skipped ...")
|
||||||
|
|
||||||
|
if (remove_polkit or remove_dir or remove_env) and unit_file_exists(
|
||||||
|
"moonraker", suffix="service"
|
||||||
|
):
|
||||||
|
completion_msg.text = [
|
||||||
|
"Some Klipper services are still installed:",
|
||||||
|
"● Moonraker PolicyKit rules were not removed, even though selected for removal.",
|
||||||
|
f"● '{MOONRAKER_DIR}' was not removed, even though selected for removal.",
|
||||||
|
f"● '{MOONRAKER_ENV_DIR}' was not removed, even though selected for removal.",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
if remove_polkit:
|
||||||
|
Logger.print_status("Removing all Moonraker policykit rules ...")
|
||||||
|
if remove_polkit_rules():
|
||||||
|
completion_msg.text.append("● Moonraker policykit rules removed")
|
||||||
|
if remove_dir:
|
||||||
|
Logger.print_status("Removing Moonraker local repository ...")
|
||||||
|
if run_remove_routines(MOONRAKER_DIR):
|
||||||
|
completion_msg.text.append("● Moonraker local repository removed")
|
||||||
|
if remove_env:
|
||||||
|
Logger.print_status("Removing Moonraker Python environment ...")
|
||||||
|
if run_remove_routines(MOONRAKER_ENV_DIR):
|
||||||
|
completion_msg.text.append("● Moonraker Python environment removed")
|
||||||
|
|
||||||
|
if completion_msg.text:
|
||||||
|
completion_msg.text.insert(0, "The following actions were performed:")
|
||||||
|
else:
|
||||||
|
completion_msg.color = Color.YELLOW
|
||||||
|
completion_msg.centered = True
|
||||||
|
completion_msg.text = ["Nothing to remove."]
|
||||||
|
|
||||||
|
self.msgsvc.set_message(completion_msg)
|
||||||
|
|
||||||
|
def __run_setup(
|
||||||
|
self, new_instances: List[Moonraker], create_example_cfg: bool
|
||||||
|
) -> None:
|
||||||
|
check_install_dependencies()
|
||||||
|
self.__install_deps()
|
||||||
|
|
||||||
|
ports_map = self.misvc.get_instance_port_map()
|
||||||
|
for i in new_instances:
|
||||||
|
i.create()
|
||||||
|
cmd_sysctl_service(i.service_file_path.name, "enable")
|
||||||
|
|
||||||
|
if create_example_cfg:
|
||||||
|
# if a webclient and/or it's config is installed, patch
|
||||||
|
# its update section to the config
|
||||||
|
clients = get_existing_clients()
|
||||||
|
create_example_moonraker_conf(i, ports_map, clients)
|
||||||
|
|
||||||
|
cmd_sysctl_service(i.service_file_path.name, "start")
|
||||||
|
|
||||||
|
cmd_sysctl_manage("daemon-reload")
|
||||||
|
|
||||||
|
# if mainsail is installed, and we installed
|
||||||
|
# multiple moonraker instances, we enable mainsails remote mode
|
||||||
|
if MainsailData().client_dir.exists() and len(self.moonraker_list) > 1:
|
||||||
|
enable_mainsail_remotemode()
|
||||||
|
|
||||||
|
self.misvc.load_instances()
|
||||||
|
new_instances = [
|
||||||
|
self.misvc.get_instance_by_suffix(i.suffix) for i in new_instances
|
||||||
|
]
|
||||||
|
|
||||||
|
ip: str = get_ipv4_addr()
|
||||||
|
# noinspection HttpUrlsUsage
|
||||||
|
url_list = [
|
||||||
|
f"● {i.service_file_path.stem}: http://{ip}:{i.port}"
|
||||||
|
for i in new_instances
|
||||||
|
if i.port
|
||||||
|
]
|
||||||
|
dialog_content = []
|
||||||
|
if url_list:
|
||||||
|
dialog_content.append("You can access Moonraker via the following URL:")
|
||||||
|
dialog_content.extend(url_list)
|
||||||
|
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.CUSTOM,
|
||||||
|
custom_title="Moonraker successfully installed!",
|
||||||
|
custom_color=Color.GREEN,
|
||||||
|
content=dialog_content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __check_requirements(self, klipper_list: List[Klipper]) -> bool:
|
||||||
|
is_klipper_installed = len(klipper_list) >= 1
|
||||||
|
if not is_klipper_installed:
|
||||||
|
Logger.print_warn("Klipper not installed!")
|
||||||
|
Logger.print_warn("Moonraker cannot be installed! Install Klipper first.")
|
||||||
|
|
||||||
|
is_python_ok = check_python_version(3, 7)
|
||||||
|
|
||||||
|
return is_klipper_installed and is_python_ok
|
||||||
|
|
||||||
|
def __install_deps(self) -> None:
|
||||||
|
default_repo = (MOONRAKER_REPO_URL, "master")
|
||||||
|
repo = self.settings.moonraker.repositories
|
||||||
|
# pull the first repo defined in kiauh.cfg or fallback to the official Moonraker repo
|
||||||
|
repo, branch = (repo[0].url, repo[0].branch) if repo else default_repo
|
||||||
|
git_clone_wrapper(repo, MOONRAKER_DIR, branch)
|
||||||
|
|
||||||
|
try:
|
||||||
|
install_moonraker_packages()
|
||||||
|
if create_python_venv(MOONRAKER_ENV_DIR):
|
||||||
|
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE)
|
||||||
|
install_python_requirements(
|
||||||
|
MOONRAKER_ENV_DIR, MOONRAKER_SPEEDUPS_REQ_FILE
|
||||||
|
)
|
||||||
|
self.__install_polkit()
|
||||||
|
except Exception:
|
||||||
|
Logger.print_error("Error during installation of Moonraker requirements!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def __install_polkit(self) -> 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 = run(
|
||||||
|
command,
|
||||||
|
stderr=PIPE,
|
||||||
|
stdout=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 CalledProcessError as e:
|
||||||
|
log = (
|
||||||
|
f"Error while installing Moonraker policykit rules: {e.stderr.decode()}"
|
||||||
|
)
|
||||||
|
Logger.print_error(log)
|
||||||
|
|
||||||
|
def __get_instances_to_remove(self) -> List[Moonraker] | None:
|
||||||
|
start_index = 1
|
||||||
|
curr_instances: List[Moonraker] = self.moonraker_list
|
||||||
|
instance_count = len(curr_instances)
|
||||||
|
|
||||||
|
options = [str(i + start_index) for i in range(instance_count)]
|
||||||
|
options.extend(["a", "b"])
|
||||||
|
instance_map = {
|
||||||
|
options[i]: self.moonraker_list[i] for i in range(instance_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
print_instance_overview(
|
||||||
|
self.moonraker_list,
|
||||||
|
start_index=start_index,
|
||||||
|
show_index=True,
|
||||||
|
show_select_all=True,
|
||||||
|
)
|
||||||
|
selection = get_selection_input("Select Moonraker instance to remove", options)
|
||||||
|
|
||||||
|
if selection == "b":
|
||||||
|
return None
|
||||||
|
elif selection == "a":
|
||||||
|
return copy(self.moonraker_list)
|
||||||
|
|
||||||
|
return [instance_map[selection]]
|
||||||
|
|
||||||
|
def __remove_instances(
|
||||||
|
self,
|
||||||
|
instance_list: List[Moonraker] | None,
|
||||||
|
) -> None:
|
||||||
|
if not instance_list:
|
||||||
|
return
|
||||||
|
|
||||||
|
for instance in instance_list:
|
||||||
|
Logger.print_status(
|
||||||
|
f"Removing instance {instance.service_file_path.stem} ..."
|
||||||
|
)
|
||||||
|
InstanceManager.remove(instance)
|
||||||
|
self.__delete_env_file(instance)
|
||||||
|
|
||||||
|
self.__refresh_state()
|
||||||
|
|
||||||
|
def __delete_env_file(self, instance: Moonraker):
|
||||||
|
Logger.print_status(f"Remove '{instance.env_file}'")
|
||||||
|
if not instance.env_file.exists():
|
||||||
|
msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..."
|
||||||
|
Logger.print_info(msg)
|
||||||
|
return
|
||||||
|
run_remove_routines(instance.env_file)
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from subprocess import DEVNULL, PIPE, CalledProcessError, run
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from components.moonraker import (
|
from components.moonraker import (
|
||||||
@@ -16,10 +17,13 @@ from components.moonraker import (
|
|||||||
MOONRAKER_BACKUP_DIR,
|
MOONRAKER_BACKUP_DIR,
|
||||||
MOONRAKER_DB_BACKUP_DIR,
|
MOONRAKER_DB_BACKUP_DIR,
|
||||||
MOONRAKER_DEFAULT_PORT,
|
MOONRAKER_DEFAULT_PORT,
|
||||||
|
MOONRAKER_DEPS_JSON_FILE,
|
||||||
MOONRAKER_DIR,
|
MOONRAKER_DIR,
|
||||||
MOONRAKER_ENV_DIR,
|
MOONRAKER_ENV_DIR,
|
||||||
|
MOONRAKER_INSTALL_SCRIPT,
|
||||||
)
|
)
|
||||||
from components.moonraker.moonraker import Moonraker
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.moonraker.utils.sysdeps_parser import SysDepsParser
|
||||||
from components.webui_client.base_data import BaseWebClient
|
from components.webui_client.base_data import BaseWebClient
|
||||||
from core.backup_manager.backup_manager import BackupManager
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
from core.logger import Logger
|
from core.logger import Logger
|
||||||
@@ -27,10 +31,11 @@ from core.submodules.simple_config_parser.src.simple_config_parser.simple_config
|
|||||||
SimpleConfigParser,
|
SimpleConfigParser,
|
||||||
)
|
)
|
||||||
from core.types.component_status import ComponentStatus
|
from core.types.component_status import ComponentStatus
|
||||||
from utils.common import get_install_status
|
from utils.common import check_install_dependencies, get_install_status
|
||||||
from utils.instance_utils import get_instances
|
from utils.instance_utils import get_instances
|
||||||
from utils.sys_utils import (
|
from utils.sys_utils import (
|
||||||
get_ipv4_addr,
|
get_ipv4_addr,
|
||||||
|
parse_packages_from_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -38,6 +43,46 @@ def get_moonraker_status() -> ComponentStatus:
|
|||||||
return get_install_status(MOONRAKER_DIR, MOONRAKER_ENV_DIR, Moonraker)
|
return get_install_status(MOONRAKER_DIR, MOONRAKER_ENV_DIR, Moonraker)
|
||||||
|
|
||||||
|
|
||||||
|
def install_moonraker_packages() -> None:
|
||||||
|
Logger.print_status("Parsing Moonraker system dependencies ...")
|
||||||
|
|
||||||
|
moonraker_deps = []
|
||||||
|
if MOONRAKER_DEPS_JSON_FILE.exists():
|
||||||
|
Logger.print_info(
|
||||||
|
f"Parsing system dependencies from {MOONRAKER_DEPS_JSON_FILE.name} ..."
|
||||||
|
)
|
||||||
|
parser = SysDepsParser()
|
||||||
|
sysdeps = load_sysdeps_json(MOONRAKER_DEPS_JSON_FILE)
|
||||||
|
moonraker_deps.extend(parser.parse_dependencies(sysdeps))
|
||||||
|
|
||||||
|
elif MOONRAKER_INSTALL_SCRIPT.exists():
|
||||||
|
Logger.print_warn(f"{MOONRAKER_DEPS_JSON_FILE.name} not found!")
|
||||||
|
Logger.print_info(
|
||||||
|
f"Parsing system dependencies from {MOONRAKER_INSTALL_SCRIPT.name} ..."
|
||||||
|
)
|
||||||
|
moonraker_deps = parse_packages_from_file(MOONRAKER_INSTALL_SCRIPT)
|
||||||
|
|
||||||
|
if not moonraker_deps:
|
||||||
|
raise ValueError("Error parsing Moonraker dependencies!")
|
||||||
|
|
||||||
|
check_install_dependencies({*moonraker_deps})
|
||||||
|
|
||||||
|
|
||||||
|
def remove_polkit_rules() -> bool:
|
||||||
|
if not MOONRAKER_DIR.exists():
|
||||||
|
log = "Cannot remove policykit rules. Moonraker directory not found."
|
||||||
|
Logger.print_warn(log)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"]
|
||||||
|
run(cmd, stderr=PIPE, stdout=DEVNULL, check=True)
|
||||||
|
return True
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error while removing policykit rules: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def create_example_moonraker_conf(
|
def create_example_moonraker_conf(
|
||||||
instance: Moonraker,
|
instance: Moonraker,
|
||||||
ports_map: Dict[str, int],
|
ports_map: Dict[str, int],
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ def get_client_port_selection(
|
|||||||
while True:
|
while True:
|
||||||
_type = "Reconfigure" if reconfigure else "Configure"
|
_type = "Reconfigure" if reconfigure else "Configure"
|
||||||
question = f"{_type} {client.display_name} for port"
|
question = f"{_type} {client.display_name} for port"
|
||||||
port_input = get_number_input(question, min_count=80, default=port)
|
port_input = get_number_input(question, min_value=80, default=port)
|
||||||
|
|
||||||
if port_input not in ports_in_use:
|
if port_input not in ports_in_use:
|
||||||
client_settings: WebUiSettings = settings[client.name]
|
client_settings: WebUiSettings = settings[client.name]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from typing import Type
|
|||||||
from components.crowsnest.crowsnest import install_crowsnest
|
from components.crowsnest.crowsnest import install_crowsnest
|
||||||
from components.klipper.services.klipper_setup_service import KlipperSetupService
|
from components.klipper.services.klipper_setup_service import KlipperSetupService
|
||||||
from components.klipperscreen.klipperscreen import install_klipperscreen
|
from components.klipperscreen.klipperscreen import install_klipperscreen
|
||||||
from components.moonraker import moonraker_setup
|
from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService
|
||||||
from components.webui_client.client_config.client_config_setup import (
|
from components.webui_client.client_config.client_config_setup import (
|
||||||
install_client_config,
|
install_client_config,
|
||||||
)
|
)
|
||||||
@@ -37,6 +37,7 @@ class InstallMenu(BaseMenu):
|
|||||||
self.title_color = Color.GREEN
|
self.title_color = Color.GREEN
|
||||||
self.previous_menu: Type[BaseMenu] | None = previous_menu
|
self.previous_menu: Type[BaseMenu] | None = previous_menu
|
||||||
self.klsvc = KlipperSetupService()
|
self.klsvc = KlipperSetupService()
|
||||||
|
self.mrsvc = MoonrakerSetupService()
|
||||||
|
|
||||||
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
|
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
|
||||||
from core.menus.main_menu import MainMenu
|
from core.menus.main_menu import MainMenu
|
||||||
@@ -79,7 +80,7 @@ class InstallMenu(BaseMenu):
|
|||||||
self.klsvc.install()
|
self.klsvc.install()
|
||||||
|
|
||||||
def install_moonraker(self, **kwargs) -> None:
|
def install_moonraker(self, **kwargs) -> None:
|
||||||
moonraker_setup.install_moonraker()
|
self.mrsvc.install()
|
||||||
|
|
||||||
def install_mainsail(self, **kwargs) -> None:
|
def install_mainsail(self, **kwargs) -> None:
|
||||||
client: MainsailData = MainsailData()
|
client: MainsailData = MainsailData()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from components.klipperscreen.klipperscreen import (
|
|||||||
get_klipperscreen_status,
|
get_klipperscreen_status,
|
||||||
update_klipperscreen,
|
update_klipperscreen,
|
||||||
)
|
)
|
||||||
from components.moonraker.moonraker_setup import update_moonraker
|
from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService
|
||||||
from components.moonraker.utils.utils import get_moonraker_status
|
from components.moonraker.utils.utils import get_moonraker_status
|
||||||
from components.webui_client.client_config.client_config_setup import (
|
from components.webui_client.client_config.client_config_setup import (
|
||||||
update_client_config,
|
update_client_config,
|
||||||
@@ -197,7 +197,8 @@ class UpdateMenu(BaseMenu):
|
|||||||
self._run_update_routine("klipper", klsvc.update)
|
self._run_update_routine("klipper", klsvc.update)
|
||||||
|
|
||||||
def update_moonraker(self, **kwargs) -> None:
|
def update_moonraker(self, **kwargs) -> None:
|
||||||
self._run_update_routine("moonraker", update_moonraker)
|
mrsvc = MoonrakerSetupService()
|
||||||
|
self._run_update_routine("moonraker", mrsvc.update)
|
||||||
|
|
||||||
def update_mainsail(self, **kwargs) -> None:
|
def update_mainsail(self, **kwargs) -> None:
|
||||||
self._run_update_routine(
|
self._run_update_routine(
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class PrettyGcodeExtension(BaseExtension):
|
|||||||
|
|
||||||
port = get_number_input(
|
port = get_number_input(
|
||||||
"On which port should PrettyGCode run",
|
"On which port should PrettyGCode run",
|
||||||
min_count=0,
|
min_value=0,
|
||||||
default=7136,
|
default=7136,
|
||||||
allow_go_back=True,
|
allow_go_back=True,
|
||||||
)
|
)
|
||||||
|
|||||||
16
kiauh/extensions/spoolman/__init__.py
Normal file
16
kiauh/extensions/spoolman/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2025 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
|
||||||
|
SPOOLMAN_DOCKER_IMAGE = "ghcr.io/donkie/spoolman:latest"
|
||||||
|
SPOOLMAN_DIR = Path.home().joinpath("spoolman")
|
||||||
|
SPOOLMAN_DATA_DIR = SPOOLMAN_DIR.joinpath("data")
|
||||||
|
SPOOLMAN_COMPOSE_FILE = SPOOLMAN_DIR.joinpath("docker-compose.yml")
|
||||||
|
SPOOLMAN_DEFAULT_PORT = 7912
|
||||||
14
kiauh/extensions/spoolman/assets/docker-compose.yml
Normal file
14
kiauh/extensions/spoolman/assets/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
spoolman:
|
||||||
|
image: ghcr.io/donkie/spoolman:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# Mount the host machine's ./data directory into the container's /home/app/.local/share/spoolman directory
|
||||||
|
- type: bind
|
||||||
|
source: ./data # This is where the data will be stored locally. Could also be set to for example `source: /home/pi/printer_data/spoolman`.
|
||||||
|
target: /home/app/.local/share/spoolman # Do NOT modify this line
|
||||||
|
ports:
|
||||||
|
# Map the host machine's port 7912 to the container's port 8000
|
||||||
|
- "7912:8000"
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Stockholm # Optional, defaults to UTC
|
||||||
18
kiauh/extensions/spoolman/metadata.json
Normal file
18
kiauh/extensions/spoolman/metadata.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"index": 11,
|
||||||
|
"module": "spoolman_extension",
|
||||||
|
"maintained_by": "dw-0",
|
||||||
|
"display_name": "Spoolman (Docker)",
|
||||||
|
"description": [
|
||||||
|
"Filament manager for 3D printing",
|
||||||
|
"- Track your filament inventory",
|
||||||
|
"- Monitor filament usage",
|
||||||
|
"- Manage vendors, materials, and spools",
|
||||||
|
"- Integrates with Moonraker",
|
||||||
|
"\n\n",
|
||||||
|
"Note: This extension installs Spoolman using Docker. Docker must be installed on your system before installing Spoolman."
|
||||||
|
],
|
||||||
|
"updates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
190
kiauh/extensions/spoolman/spoolman.py
Normal file
190
kiauh/extensions/spoolman/spoolman.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2025 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 __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import CalledProcessError, run
|
||||||
|
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from core.logger import Logger
|
||||||
|
from extensions.spoolman import (
|
||||||
|
MODULE_PATH,
|
||||||
|
SPOOLMAN_COMPOSE_FILE,
|
||||||
|
SPOOLMAN_DIR,
|
||||||
|
SPOOLMAN_DOCKER_IMAGE,
|
||||||
|
)
|
||||||
|
from utils.sys_utils import get_system_timezone
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Spoolman:
|
||||||
|
suffix: str
|
||||||
|
base: BaseInstance = field(init=False, repr=False)
|
||||||
|
dir: Path = SPOOLMAN_DIR
|
||||||
|
data_dir: Path = field(init=False)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.base: BaseInstance = BaseInstance(Moonraker, self.suffix)
|
||||||
|
self.data_dir = self.base.data_dir
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_container_running() -> bool:
|
||||||
|
"""Check if the Spoolman container is running"""
|
||||||
|
try:
|
||||||
|
result = run(
|
||||||
|
["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "ps", "-q"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return bool(result.stdout.strip())
|
||||||
|
except CalledProcessError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_docker_available() -> bool:
|
||||||
|
"""Check if Docker is installed and available"""
|
||||||
|
try:
|
||||||
|
run(["docker", "--version"], capture_output=True, check=True)
|
||||||
|
return True
|
||||||
|
except (CalledProcessError, FileNotFoundError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_docker_compose_available() -> bool:
|
||||||
|
"""Check if Docker Compose is installed and available"""
|
||||||
|
try:
|
||||||
|
# Try modern docker compose command
|
||||||
|
run(["docker", "compose", "version"], capture_output=True, check=True)
|
||||||
|
return True
|
||||||
|
except (CalledProcessError, FileNotFoundError):
|
||||||
|
# Try legacy docker-compose command
|
||||||
|
try:
|
||||||
|
run(["docker-compose", "--version"], capture_output=True, check=True)
|
||||||
|
return True
|
||||||
|
except (CalledProcessError, FileNotFoundError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_docker_compose() -> bool:
|
||||||
|
"""Copy the docker-compose.yml file for Spoolman and set system timezone"""
|
||||||
|
try:
|
||||||
|
shutil.copy(
|
||||||
|
MODULE_PATH.joinpath("assets/docker-compose.yml"),
|
||||||
|
SPOOLMAN_COMPOSE_FILE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# get system timezone
|
||||||
|
timezone = get_system_timezone()
|
||||||
|
|
||||||
|
with open(SPOOLMAN_COMPOSE_FILE, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
content = content.replace("TZ=Europe/Stockholm", f"TZ={timezone}")
|
||||||
|
|
||||||
|
with open(SPOOLMAN_COMPOSE_FILE, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"Error creating Docker Compose file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def start_container() -> bool:
|
||||||
|
"""Start the Spoolman container"""
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "up", "-d"],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Failed to start Spoolman container: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_container() -> bool:
|
||||||
|
"""Update the Spoolman container"""
|
||||||
|
|
||||||
|
def __get_image_id() -> str:
|
||||||
|
"""Get the image ID of the Spoolman Docker image"""
|
||||||
|
try:
|
||||||
|
result = run(
|
||||||
|
["docker", "images", "-q", SPOOLMAN_DOCKER_IMAGE],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except CalledProcessError:
|
||||||
|
raise Exception("Failed to get Spoolman Docker image ID")
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_image_id = __get_image_id()
|
||||||
|
Logger.print_status("Pulling latest Spoolman image...")
|
||||||
|
Spoolman.pull_image()
|
||||||
|
new_image_id = __get_image_id()
|
||||||
|
Logger.print_status("Tearing down old Spoolman container...")
|
||||||
|
Spoolman.tear_down_container()
|
||||||
|
Logger.print_status("Spinning up new Spoolman container...")
|
||||||
|
Spoolman.start_container()
|
||||||
|
if old_image_id != new_image_id:
|
||||||
|
Logger.print_status("Removing old Spoolman image...")
|
||||||
|
run(["docker", "rmi", old_image_id], check=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Failed to update Spoolman container: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def tear_down_container() -> bool:
|
||||||
|
"""Stop and remove the Spoolman container"""
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "down"],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Failed to tear down Spoolman container: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def pull_image() -> bool:
|
||||||
|
"""Pull the Spoolman Docker image"""
|
||||||
|
try:
|
||||||
|
run(["docker", "pull", SPOOLMAN_DOCKER_IMAGE], check=True)
|
||||||
|
return True
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Failed to pull Spoolman Docker image: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_image() -> bool:
|
||||||
|
"""Remove the Spoolman Docker image"""
|
||||||
|
try:
|
||||||
|
image_exists = run(
|
||||||
|
["docker", "images", "-q", SPOOLMAN_DOCKER_IMAGE],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
).stdout.strip()
|
||||||
|
if not image_exists:
|
||||||
|
Logger.print_info("Spoolman Docker image not found. Nothing to remove.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
run(["docker", "rmi", SPOOLMAN_DOCKER_IMAGE], check=True)
|
||||||
|
return True
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Failed to remove Spoolman Docker image: {e}")
|
||||||
|
return False
|
||||||
344
kiauh/extensions/spoolman/spoolman_extension.py
Normal file
344
kiauh/extensions/spoolman/spoolman_extension.py
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2025 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 re
|
||||||
|
from subprocess import CalledProcessError, run
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.moonraker.services.moonraker_instance_service import (
|
||||||
|
MoonrakerInstanceService,
|
||||||
|
)
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.logger import DialogType, Logger
|
||||||
|
from extensions.base_extension import BaseExtension
|
||||||
|
from extensions.spoolman import (
|
||||||
|
SPOOLMAN_COMPOSE_FILE,
|
||||||
|
SPOOLMAN_DATA_DIR,
|
||||||
|
SPOOLMAN_DEFAULT_PORT,
|
||||||
|
SPOOLMAN_DIR,
|
||||||
|
)
|
||||||
|
from extensions.spoolman.spoolman import Spoolman
|
||||||
|
from utils.config_utils import (
|
||||||
|
add_config_section,
|
||||||
|
remove_config_section,
|
||||||
|
)
|
||||||
|
from utils.fs_utils import run_remove_routines
|
||||||
|
from utils.input_utils import get_confirm, get_number_input
|
||||||
|
from utils.sys_utils import get_ipv4_addr
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class SpoolmanExtension(BaseExtension):
|
||||||
|
ip: str = ""
|
||||||
|
port: int = SPOOLMAN_DEFAULT_PORT
|
||||||
|
|
||||||
|
def install_extension(self, **kwargs) -> None:
|
||||||
|
Logger.print_status("Installing Spoolman using Docker...")
|
||||||
|
|
||||||
|
docker_available, docker_compose_available = self.__check_docker_prereqs()
|
||||||
|
if not docker_available or not docker_compose_available:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.__handle_existing_installation():
|
||||||
|
self.ip: str = get_ipv4_addr()
|
||||||
|
self.__run_setup()
|
||||||
|
|
||||||
|
# noinspection HttpUrlsUsage
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.SUCCESS,
|
||||||
|
[
|
||||||
|
"Spoolman successfully installed using Docker!",
|
||||||
|
"You can access Spoolman via the following URL:",
|
||||||
|
f"http://{self.ip}:{self.port}",
|
||||||
|
],
|
||||||
|
center_content=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_extension(self, **kwargs) -> None:
|
||||||
|
Logger.print_status("Updating Spoolman Docker container...")
|
||||||
|
|
||||||
|
if not SPOOLMAN_DIR.exists() or not SPOOLMAN_COMPOSE_FILE.exists():
|
||||||
|
Logger.print_error("Spoolman installation not found or incomplete.")
|
||||||
|
return
|
||||||
|
|
||||||
|
docker_available, docker_compose_available = self.__check_docker_prereqs()
|
||||||
|
if not docker_available or not docker_compose_available:
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status("Updating Spoolman container...")
|
||||||
|
if not Spoolman.update_container():
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.SUCCESS,
|
||||||
|
["Spoolman Docker container successfully updated!"],
|
||||||
|
center_content=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def remove_extension(self, **kwargs) -> None:
|
||||||
|
Logger.print_status("Removing Spoolman Docker container...")
|
||||||
|
|
||||||
|
if not SPOOLMAN_DIR.exists():
|
||||||
|
Logger.print_info("Spoolman is not installed. Nothing to remove.")
|
||||||
|
return
|
||||||
|
|
||||||
|
docker_available, docker_compose_available = self.__check_docker_prereqs()
|
||||||
|
if not docker_available or not docker_compose_available:
|
||||||
|
return
|
||||||
|
|
||||||
|
# remove moonraker integration
|
||||||
|
mrsvc = MoonrakerInstanceService()
|
||||||
|
mrsvc.load_instances()
|
||||||
|
mr_instances: List[Moonraker] = mrsvc.get_all_instances()
|
||||||
|
|
||||||
|
Logger.print_status("Removing Spoolman configuration from moonraker.conf...")
|
||||||
|
remove_config_section("spoolman", mr_instances)
|
||||||
|
|
||||||
|
Logger.print_status("Removing Spoolman from moonraker.asvc...")
|
||||||
|
self.__remove_from_moonraker_asvc()
|
||||||
|
|
||||||
|
# stop and remove the container if docker-compose exists
|
||||||
|
if SPOOLMAN_COMPOSE_FILE.exists():
|
||||||
|
Logger.print_status("Stopping and removing Spoolman container...")
|
||||||
|
|
||||||
|
if Spoolman.tear_down_container():
|
||||||
|
Logger.print_ok("Spoolman container removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_error(
|
||||||
|
"Failed to remove Spoolman container! Please remove it manually."
|
||||||
|
)
|
||||||
|
|
||||||
|
if Spoolman.remove_image():
|
||||||
|
Logger.print_ok("Spoolman container and image removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_error(
|
||||||
|
"Failed to remove Spoolman image! Please remove it manually."
|
||||||
|
)
|
||||||
|
|
||||||
|
# backup Spoolman directory to ~/spoolman_data-<timestamp> before removing it
|
||||||
|
try:
|
||||||
|
bm = BackupManager()
|
||||||
|
result = bm.backup_directory(
|
||||||
|
f"{SPOOLMAN_DIR.name}_data",
|
||||||
|
source=SPOOLMAN_DIR,
|
||||||
|
target=SPOOLMAN_DIR.parent,
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
Logger.print_ok(f"Spoolman data backed up to {result}")
|
||||||
|
Logger.print_status("Removing Spoolman directory...")
|
||||||
|
if run_remove_routines(SPOOLMAN_DIR):
|
||||||
|
Logger.print_ok("Spoolman directory removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_error(
|
||||||
|
"Failed to remove Spoolman directory! Please remove it manually."
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"Failed to backup Spoolman directory: {e}")
|
||||||
|
Logger.print_info("Skipping Spoolman directory removal...")
|
||||||
|
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.SUCCESS,
|
||||||
|
["Spoolman successfully removed!"],
|
||||||
|
center_content=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __run_setup(self) -> None:
|
||||||
|
# Create Spoolman directory and data directory
|
||||||
|
Logger.print_status("Setting up Spoolman directories...")
|
||||||
|
SPOOLMAN_DIR.mkdir(parents=True)
|
||||||
|
Logger.print_ok(f"Directory {SPOOLMAN_DIR} created!")
|
||||||
|
SPOOLMAN_DATA_DIR.mkdir(parents=True)
|
||||||
|
Logger.print_ok(f"Directory {SPOOLMAN_DATA_DIR} created!")
|
||||||
|
|
||||||
|
# Set correct permissions for data directory
|
||||||
|
try:
|
||||||
|
Logger.print_status("Setting permissions for Spoolman data directory...")
|
||||||
|
run(["chown", "1000:1000", str(SPOOLMAN_DATA_DIR)], check=True)
|
||||||
|
Logger.print_ok("Permissions set!")
|
||||||
|
except CalledProcessError:
|
||||||
|
Logger.print_warn(
|
||||||
|
"Could not set permissions on data directory. This might cause issues."
|
||||||
|
)
|
||||||
|
|
||||||
|
Logger.print_status("Creating Docker Compose file...")
|
||||||
|
if Spoolman.create_docker_compose():
|
||||||
|
Logger.print_ok("Docker Compose file created!")
|
||||||
|
else:
|
||||||
|
Logger.print_error("Failed to create Docker Compose file!")
|
||||||
|
|
||||||
|
self.__port_config_prompt()
|
||||||
|
|
||||||
|
Logger.print_status("Spinning up Spoolman container...")
|
||||||
|
if Spoolman.start_container():
|
||||||
|
Logger.print_ok("Spoolman container started!")
|
||||||
|
else:
|
||||||
|
Logger.print_error("Failed to start Spoolman container!")
|
||||||
|
|
||||||
|
if self.__add_moonraker_integration():
|
||||||
|
Logger.print_ok("Spoolman integration added to Moonraker!")
|
||||||
|
else:
|
||||||
|
Logger.print_info("Moonraker integration skipped.")
|
||||||
|
|
||||||
|
def __check_docker_prereqs(self) -> Tuple[bool, bool]:
|
||||||
|
# check if Docker is available
|
||||||
|
is_docker_available = Spoolman.is_docker_available()
|
||||||
|
if not is_docker_available:
|
||||||
|
Logger.print_error("Docker is not installed or not available.")
|
||||||
|
Logger.print_info(
|
||||||
|
"Please install Docker first: https://docs.docker.com/engine/install/"
|
||||||
|
)
|
||||||
|
|
||||||
|
# check if Docker Compose is available
|
||||||
|
is_docker_compose_available = Spoolman.is_docker_compose_available()
|
||||||
|
if not is_docker_compose_available:
|
||||||
|
Logger.print_error("Docker Compose is not installed or not available.")
|
||||||
|
|
||||||
|
return is_docker_available, is_docker_compose_available
|
||||||
|
|
||||||
|
def __port_config_prompt(self) -> None:
|
||||||
|
"""Prompt for advanced configuration options"""
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.INFO,
|
||||||
|
[
|
||||||
|
"You can configure Spoolman to run on a different port than the default. "
|
||||||
|
"Make sure you don't select a port which is already in use by "
|
||||||
|
"another application. Your input will not be validated! "
|
||||||
|
"The default port is 7912.",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if not get_confirm("Continue with default port 7912?", default_choice=True):
|
||||||
|
self.__set_port()
|
||||||
|
|
||||||
|
def __set_port(self) -> None:
|
||||||
|
"""Configure advanced options for Spoolman Docker container"""
|
||||||
|
port = get_number_input(
|
||||||
|
"Which port should Spoolman run on?",
|
||||||
|
default=SPOOLMAN_DEFAULT_PORT,
|
||||||
|
min_value=1024,
|
||||||
|
max_value=65535,
|
||||||
|
)
|
||||||
|
|
||||||
|
if port != SPOOLMAN_DEFAULT_PORT:
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
with open(SPOOLMAN_COMPOSE_FILE, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
port_mapping_pattern = r'"(\d+):8000"'
|
||||||
|
content = re.sub(port_mapping_pattern, f'"{port}:8000"', content)
|
||||||
|
|
||||||
|
with open(SPOOLMAN_COMPOSE_FILE, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
Logger.print_ok(f"Port set to {port}...")
|
||||||
|
|
||||||
|
def __handle_existing_installation(self) -> bool:
|
||||||
|
if not (SPOOLMAN_DIR.exists() and SPOOLMAN_DIR.is_dir()):
|
||||||
|
return False
|
||||||
|
|
||||||
|
compose_file_exists = SPOOLMAN_COMPOSE_FILE.exists()
|
||||||
|
container_running = Spoolman.is_container_running()
|
||||||
|
|
||||||
|
if container_running and compose_file_exists:
|
||||||
|
Logger.print_info("Spoolman is already installed!")
|
||||||
|
return True
|
||||||
|
elif container_running and not compose_file_exists:
|
||||||
|
Logger.print_status(
|
||||||
|
"Spoolman container is running but Docker Compose file is missing..."
|
||||||
|
)
|
||||||
|
if get_confirm(
|
||||||
|
"Do you want to recreate the Docker Compose file?",
|
||||||
|
default_choice=True,
|
||||||
|
):
|
||||||
|
Spoolman.create_docker_compose()
|
||||||
|
self.__port_config_prompt()
|
||||||
|
return True
|
||||||
|
elif not container_running and compose_file_exists:
|
||||||
|
Logger.print_status(
|
||||||
|
"Docker Compose file exists but container is not running..."
|
||||||
|
)
|
||||||
|
Spoolman.start_container()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __add_moonraker_integration(self) -> bool:
|
||||||
|
"""Enable Moonraker integration for Spoolman Docker container"""
|
||||||
|
if not get_confirm("Add Moonraker integration?", default_choice=True):
|
||||||
|
return False
|
||||||
|
|
||||||
|
Logger.print_status("Adding Spoolman integration to Moonraker...")
|
||||||
|
|
||||||
|
# read port from the docker-compose file
|
||||||
|
port = SPOOLMAN_DEFAULT_PORT
|
||||||
|
if SPOOLMAN_COMPOSE_FILE.exists():
|
||||||
|
with open(SPOOLMAN_COMPOSE_FILE, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
# Extract port from the port mapping
|
||||||
|
port_match = re.search(r'"(\d+):8000"', content)
|
||||||
|
if port_match:
|
||||||
|
port = port_match.group(1)
|
||||||
|
|
||||||
|
mrsvc = MoonrakerInstanceService()
|
||||||
|
mrsvc.load_instances()
|
||||||
|
mr_instances = mrsvc.get_all_instances()
|
||||||
|
|
||||||
|
# noinspection HttpUrlsUsage
|
||||||
|
add_config_section(
|
||||||
|
section="spoolman",
|
||||||
|
instances=mr_instances,
|
||||||
|
options=[("server", f"http://{self.ip}:{port}")],
|
||||||
|
)
|
||||||
|
|
||||||
|
Logger.print_status("Adding Spoolman to moonraker.asvc...")
|
||||||
|
self.__add_to_moonraker_asvc()
|
||||||
|
|
||||||
|
InstanceManager.restart_all(mr_instances)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __add_to_moonraker_asvc(self) -> None:
|
||||||
|
"""Add Spoolman to moonraker.asvc"""
|
||||||
|
mrsvc = MoonrakerInstanceService()
|
||||||
|
mrsvc.load_instances()
|
||||||
|
mr_instances = mrsvc.get_all_instances()
|
||||||
|
for instance in mr_instances:
|
||||||
|
asvc_path = instance.data_dir.joinpath("moonraker.asvc")
|
||||||
|
if asvc_path.exists():
|
||||||
|
if "Spoolman" in open(asvc_path).read():
|
||||||
|
Logger.print_info(f"Spoolman already in {asvc_path}. Skipping...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(asvc_path, "a") as f:
|
||||||
|
f.write("Spoolman\n")
|
||||||
|
|
||||||
|
Logger.print_ok(f"Spoolman added to {asvc_path}!")
|
||||||
|
|
||||||
|
def __remove_from_moonraker_asvc(self) -> None:
|
||||||
|
"""Remove Spoolman from moonraker.asvc"""
|
||||||
|
mrsvc = MoonrakerInstanceService()
|
||||||
|
mrsvc.load_instances()
|
||||||
|
mr_instances = mrsvc.get_all_instances()
|
||||||
|
for instance in mr_instances:
|
||||||
|
asvc_path = instance.data_dir.joinpath("moonraker.asvc")
|
||||||
|
if asvc_path.exists():
|
||||||
|
if "Spoolman" not in open(asvc_path).read():
|
||||||
|
Logger.print_info(f"Spoolman not in {asvc_path}. Skipping...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(asvc_path, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
new_lines = [line for line in lines if "Spoolman" not in line]
|
||||||
|
|
||||||
|
with open(asvc_path, "w") as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
|
|
||||||
|
Logger.print_ok(f"Spoolman removed from {asvc_path}!")
|
||||||
@@ -27,7 +27,9 @@ from components.moonraker import (
|
|||||||
MOONRAKER_REQ_FILE,
|
MOONRAKER_REQ_FILE,
|
||||||
)
|
)
|
||||||
from components.moonraker.moonraker import Moonraker
|
from components.moonraker.moonraker import Moonraker
|
||||||
from components.moonraker.moonraker_setup import install_moonraker_packages
|
from components.moonraker.services.moonraker_setup_service import (
|
||||||
|
install_moonraker_packages,
|
||||||
|
)
|
||||||
from core.backup_manager.backup_manager import BackupManager, BackupManagerException
|
from core.backup_manager.backup_manager import BackupManager, BackupManagerException
|
||||||
from core.instance_manager.instance_manager import InstanceManager
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
from core.logger import Logger
|
from core.logger import Logger
|
||||||
|
|||||||
@@ -52,16 +52,16 @@ def get_confirm(question: str, default_choice=True, allow_go_back=False) -> bool
|
|||||||
|
|
||||||
def get_number_input(
|
def get_number_input(
|
||||||
question: str,
|
question: str,
|
||||||
min_count: int,
|
min_value: int,
|
||||||
max_count: int | None = None,
|
max_value: int | None = None,
|
||||||
default: int | None = None,
|
default: int | None = None,
|
||||||
allow_go_back: bool = False,
|
allow_go_back: bool = False,
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
"""
|
"""
|
||||||
Helper method to get a number input from the user
|
Helper method to get a number input from the user
|
||||||
:param question: The question to display
|
:param question: The question to display
|
||||||
:param min_count: The lowest allowed value
|
:param min_value: The lowest allowed value
|
||||||
:param max_count: The highest allowed value (or None)
|
:param max_value: The highest allowed value (or None)
|
||||||
:param default: Optional default value
|
:param default: Optional default value
|
||||||
:param allow_go_back: Navigate back to a previous dialog
|
:param allow_go_back: Navigate back to a previous dialog
|
||||||
:return: Either the validated number input, or None on go_back
|
:return: Either the validated number input, or None on go_back
|
||||||
@@ -77,7 +77,7 @@ def get_number_input(
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return validate_number_input(_input, min_count, max_count)
|
return validate_number_input(_input, min_value, max_value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
Logger.print_error(INVALID_CHOICE)
|
Logger.print_error(INVALID_CHOICE)
|
||||||
|
|
||||||
|
|||||||
@@ -359,11 +359,12 @@ def get_ipv4_addr() -> str:
|
|||||||
try:
|
try:
|
||||||
# doesn't even have to be reachable
|
# doesn't even have to be reachable
|
||||||
s.connect(("192.255.255.255", 1))
|
s.connect(("192.255.255.255", 1))
|
||||||
return str(s.getsockname()[0])
|
ipv4: str = str(s.getsockname()[0])
|
||||||
except Exception:
|
|
||||||
return "127.0.0.1"
|
|
||||||
finally:
|
|
||||||
s.close()
|
s.close()
|
||||||
|
return ipv4
|
||||||
|
except Exception:
|
||||||
|
s.close()
|
||||||
|
return "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
def download_file(url: str, target: Path, show_progress=True) -> None:
|
def download_file(url: str, target: Path, show_progress=True) -> None:
|
||||||
@@ -600,3 +601,33 @@ def get_distro_info() -> Tuple[str, str]:
|
|||||||
raise ValueError("Error reading distro version!")
|
raise ValueError("Error reading distro version!")
|
||||||
|
|
||||||
return distro_id.lower(), distro_version
|
return distro_id.lower(), distro_version
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_timezone() -> str:
|
||||||
|
timezone = "UTC"
|
||||||
|
try:
|
||||||
|
with open("/etc/timezone", "r") as f:
|
||||||
|
timezone = f.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
# fallback to reading timezone from timedatectl
|
||||||
|
try:
|
||||||
|
result = run(
|
||||||
|
["timedatectl", "show", "--property=Timezone"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
timezone = result.stdout.strip().split("=")[1]
|
||||||
|
except CalledProcessError:
|
||||||
|
# fallback if timedatectl fails, try reading from readlink
|
||||||
|
try:
|
||||||
|
result = run(
|
||||||
|
["readlink", "-f", "/etc/localtime"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
timezone = result.stdout.strip().split("zoneinfo/")[1]
|
||||||
|
except (CalledProcessError, IndexError):
|
||||||
|
Logger.print_warn("Could not determine system timezone, using UTC")
|
||||||
|
return timezone
|
||||||
|
|||||||
Reference in New Issue
Block a user