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>
This commit was merged in pull request #462.
This commit is contained in:
13
kiauh/components/crowsnest/__init__.py
Normal file
13
kiauh/components/crowsnest/__init__.py
Normal 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"
|
||||||
172
kiauh/components/crowsnest/crowsnest.py
Normal file
172
kiauh/components/crowsnest/crowsnest.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
stop_cn = "sudo systemctl stop crowsnest"
|
||||||
|
restart_cn = "sudo systemctl restart crowsnest"
|
||||||
|
|
||||||
|
Logger.print_status("Stopping Crowsnest service ...")
|
||||||
|
run(stop_cn, shell=True, check=True)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
Logger.print_status("Restarting Crowsnest service ...")
|
||||||
|
run(restart_cn, shell=True, check=True)
|
||||||
|
|
||||||
|
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!")
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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]]:
|
||||||
|
|||||||
Reference in New Issue
Block a user