Compare commits

...

5 Commits

Author SHA1 Message Date
dw-0
9857d8f9f2 Merge be228210bd into f2691f33d3 2024-05-01 21:35:15 +02:00
dw-0
be228210bd refactor: use utils to handle service actions
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-05-01 21:33:12 +02:00
dw-0
b70ac0dfd7 refactor: move config related helper methods into own util module
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-05-01 21:12:37 +02:00
dw-0
af48738221 refactor: use util function to handle service controls
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-05-01 19:04:50 +02:00
dw-0
9d2cb72aa4 feat: implement crowsnest (#462)
* feat: add crowsnest install/remove


Signed-off-by: Dominik Willner <th33xitus@gmail.com>

* feat: add crowsnest update

Signed-off-by: Dominik Willner <th33xitus@gmail.com>

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-05-01 18:46:58 +02:00
15 changed files with 349 additions and 174 deletions

View File

@@ -0,0 +1,13 @@
# ======================================================================= #
# 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
CROWSNEST_DIR = Path.home().joinpath("crowsnest")
CROWSNEST_REPO = "https://github.com/mainsail-crew/crowsnest.git"

View File

@@ -0,0 +1,168 @@
# ======================================================================= #
# 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 __future__ import annotations
import shutil
import textwrap
from pathlib import Path
from subprocess import run, CalledProcessError
from typing import List, Dict, Literal, Union
from components.crowsnest import CROWSNEST_REPO, CROWSNEST_DIR
from components.klipper.klipper import Klipper
from core.instance_manager.instance_manager import InstanceManager
from utils.common import get_install_status
from utils.constants import COLOR_CYAN, RESET_FORMAT, CURRENT_USER
from utils.git_utils import (
git_clone_wrapper,
get_repo_name,
get_local_commit,
get_remote_commit,
git_pull_wrapper,
)
from utils.input_utils import get_confirm
from utils.logger import Logger
from utils.system_utils import (
check_package_install,
install_system_packages,
parse_packages_from_file,
update_system_package_lists,
control_systemd_service,
)
def install_crowsnest() -> None:
# Step 1: Clone crowsnest repo
git_clone_wrapper(CROWSNEST_REPO, "master", CROWSNEST_DIR)
# Step 2: Install dependencies
requirements: List[str] = check_package_install(["make"])
if requirements:
install_system_packages(requirements)
# Step 3: Check for Multi Instance
im = InstanceManager(Klipper)
instances: List[Klipper] = im.find_instances()
if len(instances) > 1:
Logger.print_status("Multi instance install detected ...")
info = textwrap.dedent("""
Crowsnest is NOT designed to support multi instances.
A workaround for this is to choose the most used instance as a 'master'
Use this instance to set up your 'crowsnest.conf' and steering it's service.
Found the following instances:
""")[:-1]
print(info, end="")
for instance in instances:
print(f"{instance.data_dir_name}")
Logger.print_status("\nLaunching crowsnest's configuration tool ...")
if not get_confirm("Continue with configuration?", False, allow_go_back=True):
Logger.print_info("Installation aborted by user ... Exiting!")
return
config = Path(CROWSNEST_DIR).joinpath("tools/.config")
try:
run(
"make config",
cwd=CROWSNEST_DIR,
shell=True,
check=True,
)
except CalledProcessError as e:
Logger.print_error(f"Something went wrong! Please try again...\n{e}")
if config.exists():
Path.unlink(config)
return
if not config.exists():
Logger.print_error("Generating .config failed, installation aborted")
return
# Step 4: Launch crowsnest installer
print(f"{COLOR_CYAN}Installer will prompt you for sudo password!{RESET_FORMAT}")
Logger.print_status("Launching crowsnest installer ...")
try:
run(
f"sudo make install BASE_USER={CURRENT_USER}",
cwd=CROWSNEST_DIR,
shell=True,
check=True,
)
except CalledProcessError as e:
Logger.print_error(f"Something went wrong! Please try again...\n{e}")
return
def update_crowsnest() -> None:
try:
control_systemd_service("crowsnest", "stop")
if not CROWSNEST_DIR.exists():
git_clone_wrapper(CROWSNEST_REPO, "master", CROWSNEST_DIR)
else:
Logger.print_status("Updating Crowsnest ...")
git_pull_wrapper(CROWSNEST_REPO, CROWSNEST_DIR)
script = CROWSNEST_DIR.joinpath("tools/install.sh")
deps = parse_packages_from_file(script)
packages = check_package_install(deps)
update_system_package_lists(silent=False)
install_system_packages(packages)
control_systemd_service("crowsnest", "restart")
Logger.print_ok("Crowsnest updated successfully.", end="\n\n")
except CalledProcessError as e:
Logger.print_error(f"Something went wrong! Please try again...\n{e}")
return
def get_crowsnest_status() -> (
Dict[
Literal["status", "status_code", "repo", "local", "remote"],
Union[str, int],
]
):
files = [
Path("/usr/local/bin/crowsnest"),
Path("/etc/logrotate.d/crowsnest"),
Path("/etc/systemd/system/crowsnest.service"),
]
status = get_install_status(CROWSNEST_DIR, files)
return {
"status": status.get("status"),
"status_code": status.get("status_code"),
"repo": get_repo_name(CROWSNEST_DIR),
"local": get_local_commit(CROWSNEST_DIR),
"remote": get_remote_commit(CROWSNEST_DIR),
}
def remove_crowsnest() -> None:
if not CROWSNEST_DIR.exists():
Logger.print_info("Crowsnest does not seem to be installed! Skipping ...")
return
try:
run(
"make uninstall",
cwd=CROWSNEST_DIR,
shell=True,
check=True,
)
except CalledProcessError as e:
Logger.print_error(f"Something went wrong! Please try again...\n{e}")
return
Logger.print_status("Removing crowsnest directory ...")
shutil.rmtree(CROWSNEST_DIR)
Logger.print_ok("Directory removed!")

View File

@@ -16,7 +16,8 @@ from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker import Moonraker
from components.webui_client.base_data import BaseWebClientConfig from components.webui_client.base_data import BaseWebClientConfig
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from utils.filesystem_utils import remove_file, remove_config_section from utils.config_utils import remove_config_section
from utils.filesystem_utils import remove_file
from utils.logger import Logger from utils.logger import Logger

View File

@@ -26,11 +26,8 @@ from components.webui_client.client_utils import (
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from utils.common import backup_printer_config_dir from utils.common import backup_printer_config_dir
from utils.filesystem_utils import ( from utils.config_utils import add_config_section, add_config_section_at_top
create_symlink, from utils.filesystem_utils import create_symlink
add_config_section,
add_config_section_at_top,
)
from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.git_utils import git_clone_wrapper, git_pull_wrapper
from utils.input_utils import get_confirm from utils.input_utils import get_confirm
from utils.logger import Logger from utils.logger import Logger

View File

@@ -23,10 +23,10 @@ from components.webui_client.client_config.client_config_remove import (
from components.webui_client.client_utils import backup_mainsail_config_json from components.webui_client.client_utils import backup_mainsail_config_json
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from utils.config_utils import remove_config_section
from utils.filesystem_utils import ( from utils.filesystem_utils import (
remove_nginx_config, remove_nginx_config,
remove_nginx_logs, remove_nginx_logs,
remove_config_section,
) )
from utils.logger import Logger from utils.logger import Logger
@@ -49,7 +49,7 @@ def run_client_removal(
client_name = client.name client_name = client.name
remove_client_dir(client) remove_client_dir(client)
remove_nginx_config(client_name) remove_nginx_config(client_name)
remove_nginx_logs(client_name) remove_nginx_logs(client_name, kl_instances)
section = f"update_manager {client_name}" section = f"update_manager {client_name}"
remove_config_section(section, mr_instances) remove_config_section(section, mr_instances)

View File

@@ -37,6 +37,7 @@ from core.instance_manager.instance_manager import InstanceManager
from core.settings.kiauh_settings import KiauhSettings from core.settings.kiauh_settings import KiauhSettings
from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED
from utils.common import check_install_dependencies from utils.common import check_install_dependencies
from utils.config_utils import add_config_section
from utils.filesystem_utils import ( from utils.filesystem_utils import (
unzip, unzip,
copy_upstream_nginx_cfg, copy_upstream_nginx_cfg,
@@ -44,7 +45,6 @@ from utils.filesystem_utils import (
create_nginx_cfg, create_nginx_cfg,
create_symlink, create_symlink,
remove_file, remove_file,
add_config_section,
read_ports_from_nginx_configs, read_ports_from_nginx_configs,
is_valid_port, is_valid_port,
get_next_free_port, get_next_free_port,

View File

@@ -15,6 +15,7 @@ from typing import List, Optional, Union, TypeVar
from core.instance_manager.base_instance import BaseInstance from core.instance_manager.base_instance import BaseInstance
from utils.constants import SYSTEMD from utils.constants import SYSTEMD
from utils.logger import Logger from utils.logger import Logger
from utils.system_utils import control_systemd_service
T = TypeVar(name="T", bound=BaseInstance, covariant=True) T = TypeVar(name="T", bound=BaseInstance, covariant=True)
@@ -108,14 +109,7 @@ class InstanceManager:
def enable_instance(self) -> None: def enable_instance(self) -> None:
Logger.print_status(f"Enabling {self.instance_service_full} ...") Logger.print_status(f"Enabling {self.instance_service_full} ...")
try: try:
command = [ control_systemd_service(self.instance_service_full, "enable")
"sudo",
"systemctl",
"enable",
self.instance_service_full,
]
if subprocess.run(command, check=True):
Logger.print_ok(f"{self.instance_service_full} enabled.")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
Logger.print_error(f"Error enabling service {self.instance_service_full}:") Logger.print_error(f"Error enabling service {self.instance_service_full}:")
Logger.print_error(f"{e}") Logger.print_error(f"{e}")
@@ -123,14 +117,7 @@ class InstanceManager:
def disable_instance(self) -> None: def disable_instance(self) -> None:
Logger.print_status(f"Disabling {self.instance_service_full} ...") Logger.print_status(f"Disabling {self.instance_service_full} ...")
try: try:
command = [ control_systemd_service(self.instance_service_full, "disable")
"sudo",
"systemctl",
"disable",
self.instance_service_full,
]
if subprocess.run(command, check=True):
Logger.print_ok(f"{self.instance_service_full} disabled.")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
Logger.print_error(f"Error disabling {self.instance_service_full}:") Logger.print_error(f"Error disabling {self.instance_service_full}:")
Logger.print_error(f"{e}") Logger.print_error(f"{e}")
@@ -138,14 +125,7 @@ class InstanceManager:
def start_instance(self) -> None: def start_instance(self) -> None:
Logger.print_status(f"Starting {self.instance_service_full} ...") Logger.print_status(f"Starting {self.instance_service_full} ...")
try: try:
command = [ control_systemd_service(self.instance_service_full, "start")
"sudo",
"systemctl",
"start",
self.instance_service_full,
]
if subprocess.run(command, check=True):
Logger.print_ok(f"{self.instance_service_full} started.")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
Logger.print_error(f"Error starting {self.instance_service_full}:") Logger.print_error(f"Error starting {self.instance_service_full}:")
Logger.print_error(f"{e}") Logger.print_error(f"{e}")
@@ -153,14 +133,7 @@ class InstanceManager:
def restart_instance(self) -> None: def restart_instance(self) -> None:
Logger.print_status(f"Restarting {self.instance_service_full} ...") Logger.print_status(f"Restarting {self.instance_service_full} ...")
try: try:
command = [ control_systemd_service(self.instance_service_full, "restart")
"sudo",
"systemctl",
"restart",
self.instance_service_full,
]
if subprocess.run(command, check=True):
Logger.print_ok(f"{self.instance_service_full} restarted.")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
Logger.print_error(f"Error restarting {self.instance_service_full}:") Logger.print_error(f"Error restarting {self.instance_service_full}:")
Logger.print_error(f"{e}") Logger.print_error(f"{e}")
@@ -178,9 +151,7 @@ class InstanceManager:
def stop_instance(self) -> None: def stop_instance(self) -> None:
Logger.print_status(f"Stopping {self.instance_service_full} ...") Logger.print_status(f"Stopping {self.instance_service_full} ...")
try: try:
command = ["sudo", "systemctl", "stop", self.instance_service_full] control_systemd_service(self.instance_service_full, "stop")
if subprocess.run(command, check=True):
Logger.print_ok(f"{self.instance_service_full} stopped.")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
Logger.print_error(f"Error stopping {self.instance_service_full}:") Logger.print_error(f"Error stopping {self.instance_service_full}:")
Logger.print_error(f"{e}") Logger.print_error(f"{e}")

View File

@@ -10,6 +10,7 @@
import textwrap import textwrap
from typing import Type, Optional from typing import Type, Optional
from components.crowsnest.crowsnest import install_crowsnest
from components.klipper import klipper_setup from components.klipper import klipper_setup
from components.moonraker import moonraker_setup from components.moonraker import moonraker_setup
from components.webui_client import client_setup from components.webui_client import client_setup
@@ -44,6 +45,7 @@ class InstallMenu(BaseMenu):
"4": Option(method=self.install_fluidd, menu=False), "4": Option(method=self.install_fluidd, menu=False),
"5": Option(method=self.install_mainsail_config, menu=False), "5": Option(method=self.install_mainsail_config, menu=False),
"6": Option(method=self.install_fluidd_config, menu=False), "6": Option(method=self.install_fluidd_config, menu=False),
"9": Option(method=self.install_crowsnest, menu=False),
} }
def print_menu(self): def print_menu(self):
@@ -88,3 +90,6 @@ class InstallMenu(BaseMenu):
def install_fluidd_config(self, **kwargs): def install_fluidd_config(self, **kwargs):
client_config_setup.install_client_config(FluiddData()) client_config_setup.install_client_config(FluiddData())
def install_crowsnest(self, **kwargs):
install_crowsnest()

View File

@@ -10,6 +10,7 @@
import textwrap import textwrap
from typing import Type, Optional from typing import Type, Optional
from components.crowsnest.crowsnest import get_crowsnest_status
from components.klipper.klipper_utils import get_klipper_status from components.klipper.klipper_utils import get_klipper_status
from components.log_uploads.menus.log_upload_menu import LogUploadMenu from components.log_uploads.menus.log_upload_menu import LogUploadMenu
from components.moonraker.moonraker_utils import get_moonraker_status from components.moonraker.moonraker_utils import get_moonraker_status
@@ -90,16 +91,23 @@ class MainMenu(BaseMenu):
self.ms_status = get_client_status(MainsailData()) self.ms_status = get_client_status(MainsailData())
self.fl_status = get_client_status(FluiddData()) self.fl_status = get_client_status(FluiddData())
self.cc_status = get_current_client_config([MainsailData(), FluiddData()]) self.cc_status = get_current_client_config([MainsailData(), FluiddData()])
self._update_status("cn", get_crowsnest_status)
def _update_status(self, status_name: str, status_fn: callable) -> None: def _update_status(self, status_name: str, status_fn: callable) -> None:
status_data = status_fn() status_data = status_fn()
status = status_data.get("status") status = status_data.get("status")
code = status_data.get("status_code") code = status_data.get("status_code")
instances = f" {status_data.get('instances')}" if code == 1 else ""
instance_count = status_data.get("instances")
count: str = ""
if instance_count and code == 1:
count = f" {instance_count}"
setattr( setattr(
self, self,
f"{status_name}_status", f"{status_name}_status",
self._format_status_by_code(code, status, instances), self._format_status_by_code(code, status, count),
) )
setattr( setattr(
self, self,

View File

@@ -10,6 +10,7 @@
import textwrap import textwrap
from typing import Type, Optional from typing import Type, Optional
from components.crowsnest.crowsnest import remove_crowsnest
from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu
from components.moonraker.menus.moonraker_remove_menu import ( from components.moonraker.menus.moonraker_remove_menu import (
MoonrakerRemoveMenu, MoonrakerRemoveMenu,
@@ -42,6 +43,7 @@ class RemoveMenu(BaseMenu):
"2": Option(method=self.remove_moonraker, menu=True), "2": Option(method=self.remove_moonraker, menu=True),
"3": Option(method=self.remove_mainsail, menu=True), "3": Option(method=self.remove_mainsail, menu=True),
"4": Option(method=self.remove_fluidd, menu=True), "4": Option(method=self.remove_fluidd, menu=True),
"6": Option(method=self.remove_crowsnest, menu=True),
} }
def print_menu(self): def print_menu(self):
@@ -78,3 +80,6 @@ class RemoveMenu(BaseMenu):
def remove_fluidd(self, **kwargs): def remove_fluidd(self, **kwargs):
ClientRemoveMenu(previous_menu=self.__class__, client=FluiddData()).run() ClientRemoveMenu(previous_menu=self.__class__, client=FluiddData()).run()
def remove_crowsnest(self, **kwargs):
remove_crowsnest()

View File

@@ -10,6 +10,7 @@
import textwrap import textwrap
from typing import Type, Optional from typing import Type, Optional
from components.crowsnest.crowsnest import get_crowsnest_status, update_crowsnest
from components.klipper.klipper_setup import update_klipper from components.klipper.klipper_setup import update_klipper
from components.klipper.klipper_utils import ( from components.klipper.klipper_utils import (
get_klipper_status, get_klipper_status,
@@ -57,6 +58,8 @@ class UpdateMenu(BaseMenu):
self.mc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.mc_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}"
self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
self.cn_local = f"{COLOR_WHITE}{RESET_FORMAT}"
self.cn_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
self.mainsail_client = MainsailData() self.mainsail_client = MainsailData()
self.fluidd_client = FluiddData() self.fluidd_client = FluiddData()
@@ -111,7 +114,7 @@ class UpdateMenu(BaseMenu):
| Other: |---------------|---------------| | Other: |---------------|---------------|
| 7) KlipperScreen | | | | 7) KlipperScreen | | |
| 8) Mobileraker | | | | 8) Mobileraker | | |
| 9) Crowsnest | | | | 9) Crowsnest | {self.cn_local:<22} | {self.cn_remote:<22} |
| |-------------------------------| | |-------------------------------|
| 10) System | | | 10) System | |
""" """
@@ -143,7 +146,8 @@ class UpdateMenu(BaseMenu):
def update_mobileraker(self, **kwargs): ... def update_mobileraker(self, **kwargs): ...
def update_crowsnest(self, **kwargs): ... def update_crowsnest(self, **kwargs):
update_crowsnest()
def upgrade_system_packages(self, **kwargs): ... def upgrade_system_packages(self, **kwargs): ...
@@ -189,6 +193,13 @@ class UpdateMenu(BaseMenu):
) )
self.fc_remote = f"{COLOR_GREEN}{fc_status.get('remote')}{RESET_FORMAT}" self.fc_remote = f"{COLOR_GREEN}{fc_status.get('remote')}{RESET_FORMAT}"
# crowsnest
cn_status = get_crowsnest_status()
self.cn_local = self.format_local_status(
cn_status.get("local"), cn_status.get("remote")
)
self.cn_remote = f"{COLOR_GREEN}{cn_status.get('remote')}{RESET_FORMAT}"
def format_local_status(self, local_version, remote_version) -> str: def format_local_status(self, local_version, remote_version) -> str:
if local_version == remote_version: if local_version == remote_version:
return f"{COLOR_GREEN}{local_version}{RESET_FORMAT}" return f"{COLOR_GREEN}{local_version}{RESET_FORMAT}"

View File

@@ -55,6 +55,22 @@ def check_install_dependencies(deps: List[str]) -> None:
install_system_packages(requirements) install_system_packages(requirements)
def get_install_status(
repo_dir: Path, opt_files: List[Path]
) -> Dict[Literal["status", "status_code", "instances"], Union[str, int]]:
status = [repo_dir.exists()]
for f in opt_files:
status.append(f.exists())
if all(status):
return {"status": "Installed!", "status_code": 1}
elif not any(status):
return {"status": "Not installed!", "status_code": 2}
else:
return {"status": "Incomplete!", "status_code": 3}
def get_install_status_common( def get_install_status_common(
instance_type: Type[BaseInstance], repo_dir: Path, env_dir: Path instance_type: Type[BaseInstance], repo_dir: Path, env_dir: Path
) -> Dict[Literal["status", "status_code", "instances"], Union[str, int]]: ) -> Dict[Literal["status", "status_code", "instances"], Union[str, int]]:

View File

@@ -0,0 +1,83 @@
# ======================================================================= #
# 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 tempfile
from pathlib import Path
from typing import List, TypeVar, Tuple, Optional
from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker
from core.config_manager.config_manager import ConfigManager
from utils.logger import Logger
B = TypeVar("B", Klipper, Moonraker)
ConfigOption = Tuple[str, str]
def add_config_section(
section: str,
instances: List[B],
options: Optional[List[ConfigOption]] = None,
) -> None:
for instance in instances:
cfg_file = instance.cfg_file
Logger.print_status(f"Add section '[{section}]' to '{cfg_file}' ...")
if not Path(cfg_file).exists():
Logger.print_warn(f"'{cfg_file}' not found!")
continue
cm = ConfigManager(cfg_file)
if cm.config.has_section(section):
Logger.print_info("Section already exist. Skipped ...")
continue
cm.config.add_section(section)
if options is not None:
for option in options:
cm.config.set(section, option[0], option[1])
cm.write_config()
def add_config_section_at_top(section: str, instances: List[B]):
for instance in instances:
tmp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=False)
tmp_cfg_path = Path(tmp_cfg.name)
cmt = ConfigManager(tmp_cfg_path)
cmt.config.add_section(section)
cmt.write_config()
tmp_cfg.close()
cfg_file = instance.cfg_file
with open(cfg_file, "r") as org:
org_content = org.readlines()
with open(tmp_cfg_path, "a") as tmp:
tmp.writelines(org_content)
cfg_file.unlink()
tmp_cfg_path.rename(cfg_file)
def remove_config_section(section: str, instances: List[B]) -> None:
for instance in instances:
cfg_file = instance.cfg_file
Logger.print_status(f"Remove section '[{section}]' from '{cfg_file}' ...")
if not Path(cfg_file).exists():
Logger.print_warn(f"'{cfg_file}' not found!")
continue
cm = ConfigManager(cfg_file)
if not cm.config.has_section(section):
Logger.print_info("Section does not exist. Skipped ...")
continue
cm.config.remove_section(section)
cm.write_config()

View File

@@ -11,17 +11,13 @@
import re import re
import shutil import shutil
import subprocess
import tempfile
from pathlib import Path from pathlib import Path
from zipfile import ZipFile from zipfile import ZipFile
from subprocess import run, check_output, CalledProcessError, PIPE, DEVNULL
from typing import List, TypeVar, Tuple, Optional from typing import List
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker
from core.config_manager.config_manager import ConfigManager
from core.instance_manager.instance_manager import InstanceManager
from utils import ( from utils import (
NGINX_SITES_AVAILABLE, NGINX_SITES_AVAILABLE,
MODULE_PATH, MODULE_PATH,
@@ -31,10 +27,6 @@ from utils import (
from utils.logger import Logger from utils.logger import Logger
B = TypeVar("B", Klipper, Moonraker)
ConfigOption = Tuple[str, str]
def check_file_exist(file_path: Path, sudo=False) -> bool: def check_file_exist(file_path: Path, sudo=False) -> bool:
""" """
Helper function for checking the existence of a file | Helper function for checking the existence of a file |
@@ -45,9 +37,9 @@ def check_file_exist(file_path: Path, sudo=False) -> bool:
if sudo: if sudo:
try: try:
command = ["sudo", "find", file_path] command = ["sudo", "find", file_path]
subprocess.check_output(command, stderr=subprocess.DEVNULL) check_output(command, stderr=DEVNULL)
return True return True
except subprocess.CalledProcessError: except CalledProcessError:
return False return False
else: else:
if file_path.exists(): if file_path.exists():
@@ -61,8 +53,8 @@ def create_symlink(source: Path, target: Path, sudo=False) -> None:
cmd = ["ln", "-sf", source, target] cmd = ["ln", "-sf", source, target]
if sudo: if sudo:
cmd.insert(0, "sudo") cmd.insert(0, "sudo")
subprocess.run(cmd, stderr=subprocess.PIPE, check=True) run(cmd, stderr=PIPE, check=True)
except subprocess.CalledProcessError as e: except CalledProcessError as e:
Logger.print_error(f"Failed to create symlink: {e}") Logger.print_error(f"Failed to create symlink: {e}")
raise raise
@@ -70,8 +62,8 @@ def create_symlink(source: Path, target: Path, sudo=False) -> None:
def remove_file(file_path: Path, sudo=False) -> None: def remove_file(file_path: Path, sudo=False) -> None:
try: try:
cmd = f"{'sudo ' if sudo else ''}rm -f {file_path}" cmd = f"{'sudo ' if sudo else ''}rm -f {file_path}"
subprocess.run(cmd, stderr=subprocess.PIPE, check=True, shell=True) run(cmd, stderr=PIPE, check=True, shell=True)
except subprocess.CalledProcessError as e: except CalledProcessError as e:
log = f"Cannot remove file {file_path}: {e.stderr.decode()}" log = f"Cannot remove file {file_path}: {e.stderr.decode()}"
Logger.print_error(log) Logger.print_error(log)
raise raise
@@ -97,8 +89,8 @@ def copy_upstream_nginx_cfg() -> None:
target = NGINX_CONFD.joinpath("upstreams.conf") target = NGINX_CONFD.joinpath("upstreams.conf")
try: try:
command = ["sudo", "cp", source, target] command = ["sudo", "cp", source, target]
subprocess.run(command, stderr=subprocess.PIPE, check=True) run(command, stderr=PIPE, check=True)
except subprocess.CalledProcessError as e: except CalledProcessError as e:
log = f"Unable to create upstreams.conf: {e.stderr.decode()}" log = f"Unable to create upstreams.conf: {e.stderr.decode()}"
Logger.print_error(log) Logger.print_error(log)
raise raise
@@ -113,8 +105,8 @@ def copy_common_vars_nginx_cfg() -> None:
target = NGINX_CONFD.joinpath("common_vars.conf") target = NGINX_CONFD.joinpath("common_vars.conf")
try: try:
command = ["sudo", "cp", source, target] command = ["sudo", "cp", source, target]
subprocess.run(command, stderr=subprocess.PIPE, check=True) run(command, stderr=PIPE, check=True)
except subprocess.CalledProcessError as e: except CalledProcessError as e:
log = f"Unable to create upstreams.conf: {e.stderr.decode()}" log = f"Unable to create upstreams.conf: {e.stderr.decode()}"
Logger.print_error(log) Logger.print_error(log)
raise raise
@@ -142,8 +134,8 @@ def create_nginx_cfg(name: str, port: int, root_dir: Path) -> None:
target = NGINX_SITES_AVAILABLE.joinpath(name) target = NGINX_SITES_AVAILABLE.joinpath(name)
try: try:
command = ["sudo", "mv", tmp, target] command = ["sudo", "mv", tmp, target]
subprocess.run(command, stderr=subprocess.PIPE, check=True) run(command, stderr=PIPE, check=True)
except subprocess.CalledProcessError as e: except CalledProcessError as e:
log = f"Unable to create '{target}': {e.stderr.decode()}" log = f"Unable to create '{target}': {e.stderr.decode()}"
Logger.print_error(log) Logger.print_error(log)
raise raise
@@ -183,117 +175,23 @@ def get_next_free_port(ports_in_use: List[int]) -> int:
return min(valid_ports - used_ports) return min(valid_ports - used_ports)
def add_config_section(
section: str,
instances: List[B],
options: Optional[List[ConfigOption]] = None,
) -> None:
for instance in instances:
cfg_file = instance.cfg_file
Logger.print_status(f"Add section '[{section}]' to '{cfg_file}' ...")
if not Path(cfg_file).exists():
Logger.print_warn(f"'{cfg_file}' not found!")
continue
cm = ConfigManager(cfg_file)
if cm.config.has_section(section):
Logger.print_info("Section already exist. Skipped ...")
continue
cm.config.add_section(section)
if options is not None:
for option in options:
cm.config.set(section, option[0], option[1])
cm.write_config()
def add_config_section_at_top(section: str, instances: List[B]):
for instance in instances:
tmp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=False)
tmp_cfg_path = Path(tmp_cfg.name)
cmt = ConfigManager(tmp_cfg_path)
cmt.config.add_section(section)
cmt.write_config()
tmp_cfg.close()
cfg_file = instance.cfg_file
with open(cfg_file, "r") as org:
org_content = org.readlines()
with open(tmp_cfg_path, "a") as tmp:
tmp.writelines(org_content)
cfg_file.unlink()
tmp_cfg_path.rename(cfg_file)
def remove_config_section(section: str, instances: List[B]) -> None:
for instance in instances:
cfg_file = instance.cfg_file
Logger.print_status(f"Remove section '[{section}]' from '{cfg_file}' ...")
if not Path(cfg_file).exists():
Logger.print_warn(f"'{cfg_file}' not found!")
continue
cm = ConfigManager(cfg_file)
if not cm.config.has_section(section):
Logger.print_info("Section does not exist. Skipped ...")
continue
cm.config.remove_section(section)
cm.write_config()
def patch_moonraker_conf(
moonraker_instances: List[Moonraker],
name: str,
section_name: str,
template_file: str,
) -> None:
for instance in moonraker_instances:
cfg_file = instance.cfg_file
Logger.print_status(f"Add {name} update section to '{cfg_file}' ...")
if not Path(cfg_file).exists():
Logger.print_warn(f"'{cfg_file}' not found!")
return
cm = ConfigManager(cfg_file)
if cm.config.has_section(section_name):
Logger.print_info("Section already exist. Skipped ...")
return
template = MODULE_PATH.joinpath("assets", template_file)
with open(template, "r") as t:
template_content = "\n"
template_content += t.read()
with open(cfg_file, "a") as f:
f.write(template_content)
def remove_nginx_config(name: str) -> None: def remove_nginx_config(name: str) -> None:
Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...") Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...")
try: try:
remove_file(NGINX_SITES_AVAILABLE.joinpath(name), True) remove_file(NGINX_SITES_AVAILABLE.joinpath(name), True)
remove_file(NGINX_SITES_ENABLED.joinpath(name), True) remove_file(NGINX_SITES_ENABLED.joinpath(name), True)
except subprocess.CalledProcessError as e: except CalledProcessError as e:
log = f"Unable to remove NGINX config '{name}':\n{e.stderr.decode()}" log = f"Unable to remove NGINX config '{name}':\n{e.stderr.decode()}"
Logger.print_error(log) Logger.print_error(log)
def remove_nginx_logs(name: str) -> None: def remove_nginx_logs(name: str, instances: List[Klipper]) -> None:
Logger.print_status(f"Removing NGINX logs for {name.capitalize()} ...") Logger.print_status(f"Removing NGINX logs for {name.capitalize()} ...")
try: try:
remove_file(Path(f"/var/log/nginx/{name}-access.log"), True) remove_file(Path(f"/var/log/nginx/{name}-access.log"), True)
remove_file(Path(f"/var/log/nginx/{name}-error.log"), True) remove_file(Path(f"/var/log/nginx/{name}-error.log"), True)
im = InstanceManager(Klipper)
instances: List[Klipper] = im.instances
if not instances: if not instances:
return return
@@ -301,5 +199,5 @@ def remove_nginx_logs(name: str) -> None:
remove_file(instance.log_dir.joinpath(f"{name}-access.log")) remove_file(instance.log_dir.joinpath(f"{name}-access.log"))
remove_file(instance.log_dir.joinpath(f"{name}-error.log")) remove_file(instance.log_dir.joinpath(f"{name}-error.log"))
except (OSError, subprocess.CalledProcessError) as e: except (OSError, CalledProcessError) as e:
Logger.print_error(f"Unable to remove NGINX logs:\n{e}") Logger.print_error(f"Unable to remove NGINX logs:\n{e}")

View File

@@ -21,9 +21,9 @@ from typing import List, Literal
import select import select
from utils.filesystem_utils import check_file_exist
from utils.input_utils import get_confirm from utils.input_utils import get_confirm
from utils.logger import Logger from utils.logger import Logger
from utils.filesystem_utils import check_file_exist
def kill(opt_err_msg: str = "") -> None: def kill(opt_err_msg: str = "") -> None:
@@ -240,8 +240,7 @@ def mask_system_service(service_name: str) -> None:
:return: None :return: None
""" """
try: try:
command = ["sudo", "systemctl", "mask", service_name] control_systemd_service(service_name, "mask")
run(command, stderr=PIPE, check=True)
except CalledProcessError as e: except CalledProcessError as e:
log = f"Unable to mask system service {service_name}: {e.stderr.decode()}" log = f"Unable to mask system service {service_name}: {e.stderr.decode()}"
Logger.print_error(log) Logger.print_error(log)
@@ -330,7 +329,7 @@ def set_nginx_permissions() -> None:
def control_systemd_service( def control_systemd_service(
name: str, action: Literal["start", "stop", "restart", "disable"] name: str, action: Literal["start", "stop", "restart", "enable", "disable", "mask"]
) -> None: ) -> None:
""" """
Helper method to execute several actions for a specific systemd service. | Helper method to execute several actions for a specific systemd service. |
@@ -339,12 +338,12 @@ def control_systemd_service(
:return: None :return: None
""" """
try: try:
Logger.print_status(f"{action.capitalize()} {name}.service ...") Logger.print_status(f"{action.capitalize()} {name} ...")
command = ["sudo", "systemctl", action, f"{name}.service"] command = ["sudo", "systemctl", action, name]
run(command, stderr=PIPE, check=True) run(command, stderr=PIPE, check=True)
Logger.print_ok("OK!") Logger.print_ok("OK!")
except CalledProcessError as e: except CalledProcessError as e:
log = f"Failed to {action} {name}.service: {e.stderr.decode()}" log = f"Failed to {action} {name}: {e.stderr.decode()}"
Logger.print_error(log) Logger.print_error(log)
raise raise