feat(klipper): implement instance manager and klipper installer in python

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
This commit is contained in:
dw-0
2023-10-26 13:44:43 +02:00
parent f45da66e9e
commit ce0daa52ae
28 changed files with 1821 additions and 69 deletions

5
.gitignore vendored
View File

@@ -1,3 +1,8 @@
.vscode
.idea
.pytest_cache
.kiauh-env
*.code-workspace
klipper_repos.txt
klipper_repos.json

15
kiauh.py Normal file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 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 kiauh.main import main
if __name__ == "__main__":
main()

158
kiauh.sh
View File

@@ -12,77 +12,97 @@
set -e
clear
### sourcing all additional scripts
KIAUH_SRCDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")"
for script in "${KIAUH_SRCDIR}/scripts/"*.sh; do . "${script}"; done
for script in "${KIAUH_SRCDIR}/scripts/ui/"*.sh; do . "${script}"; done
function main() {
local python_command
local entrypoint
#===================================================#
#=================== UPDATE KIAUH ==================#
#===================================================#
function update_kiauh() {
status_msg "Updating KIAUH ..."
cd "${KIAUH_SRCDIR}"
git reset --hard && git pull
ok_msg "Update complete! Please restart KIAUH."
exit 0
}
#===================================================#
#=================== KIAUH STATUS ==================#
#===================================================#
function kiauh_update_avail() {
[[ ! -d "${KIAUH_SRCDIR}/.git" ]] && return
local origin head
cd "${KIAUH_SRCDIR}"
### abort if not on master branch
! git branch -a | grep -q "\* master" && return
### compare commit hash
git fetch -q
origin=$(git rev-parse --short=8 origin/master)
head=$(git rev-parse --short=8 HEAD)
if [[ ${origin} != "${head}" ]]; then
echo "true"
if command -v python3 &>/dev/null; then
python_command="python3"
elif command -v python &>/dev/null; then
python_command="python"
else
echo "Python is not installed. Please install Python and try again."
exit 1
fi
entrypoint=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
${python_command} "${entrypoint}/kiauh.py"
}
function kiauh_update_dialog() {
[[ ! $(kiauh_update_avail) == "true" ]] && return
top_border
echo -e "|${green} New KIAUH update available! ${white}|"
hr
echo -e "|${green} View Changelog: https://git.io/JnmlX ${white}|"
blank_line
echo -e "|${yellow} It is recommended to keep KIAUH up to date. Updates ${white}|"
echo -e "|${yellow} usually contain bugfixes, important changes or new ${white}|"
echo -e "|${yellow} features. Please consider updating! ${white}|"
bottom_border
main
local yn
read -p "${cyan}###### Do you want to update now? (Y/n):${white} " yn
while true; do
case "${yn}" in
Y|y|Yes|yes|"")
do_action "update_kiauh"
break;;
N|n|No|no)
break;;
*)
deny_action "kiauh_update_dialog";;
esac
done
}
check_euid
init_logfile
set_globals
kiauh_update_dialog
main_menu
#### sourcing all additional scripts
#KIAUH_SRCDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")"
#for script in "${KIAUH_SRCDIR}/scripts/"*.sh; do . "${script}"; done
#for script in "${KIAUH_SRCDIR}/scripts/ui/"*.sh; do . "${script}"; done
#
##===================================================#
##=================== UPDATE KIAUH ==================#
##===================================================#
#
#function update_kiauh() {
# status_msg "Updating KIAUH ..."
#
# cd "${KIAUH_SRCDIR}"
# git reset --hard && git pull
#
# ok_msg "Update complete! Please restart KIAUH."
# exit 0
#}
#
##===================================================#
##=================== KIAUH STATUS ==================#
##===================================================#
#
#function kiauh_update_avail() {
# [[ ! -d "${KIAUH_SRCDIR}/.git" ]] && return
# local origin head
#
# cd "${KIAUH_SRCDIR}"
#
# ### abort if not on master branch
# ! git branch -a | grep -q "\* master" && return
#
# ### compare commit hash
# git fetch -q
# origin=$(git rev-parse --short=8 origin/master)
# head=$(git rev-parse --short=8 HEAD)
#
# if [[ ${origin} != "${head}" ]]; then
# echo "true"
# fi
#}
#
#function kiauh_update_dialog() {
# [[ ! $(kiauh_update_avail) == "true" ]] && return
# top_border
# echo -e "|${green} New KIAUH update available! ${white}|"
# hr
# echo -e "|${green} View Changelog: https://git.io/JnmlX ${white}|"
# blank_line
# echo -e "|${yellow} It is recommended to keep KIAUH up to date. Updates ${white}|"
# echo -e "|${yellow} usually contain bugfixes, important changes or new ${white}|"
# echo -e "|${yellow} features. Please consider updating! ${white}|"
# bottom_border
#
# local yn
# read -p "${cyan}###### Do you want to update now? (Y/n):${white} " yn
# while true; do
# case "${yn}" in
# Y|y|Yes|yes|"")
# do_action "update_kiauh"
# break;;
# N|n|No|no)
# break;;
# *)
# deny_action "kiauh_update_dialog";;
# esac
# done
#}
#
#check_euid
#init_logfile
#set_globals
#kiauh_update_dialog
#main_menu

0
kiauh/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 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 abc import abstractmethod, ABC
from pathlib import Path
from typing import List, Optional
class BaseInstance(ABC):
@classmethod
def blacklist(cls) -> List[str]:
return []
def __init__(self, prefix: Optional[str], name: Optional[str],
user: Optional[str], data_dir_name: Optional[str]):
self._prefix = prefix
self._name = name
self._user = user
self._data_dir_name = data_dir_name
self.data_dir = f"{Path.home()}/{self._data_dir_name}_data"
self.cfg_dir = f"{self.data_dir}/config"
self.log_dir = f"{self.data_dir}/logs"
self.comms_dir = f"{self.data_dir}/comms"
self.sysd_dir = f"{self.data_dir}/systemd"
@property
def prefix(self) -> str:
return self._prefix
@prefix.setter
def prefix(self, value) -> None:
self._prefix = value
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value) -> None:
self._name = value
@property
def user(self) -> str:
return self._user
@user.setter
def user(self, value) -> None:
self._user = value
@property
def data_dir_name(self) -> str:
return self._data_dir_name
@data_dir_name.setter
def data_dir_name(self, value) -> None:
self._data_dir_name = value
@abstractmethod
def create(self) -> None:
raise NotImplementedError("Subclasses must implement the create method")
@abstractmethod
def read(self) -> None:
raise NotImplementedError("Subclasses must implement the read method")
@abstractmethod
def update(self) -> None:
raise NotImplementedError("Subclasses must implement the update method")
@abstractmethod
def delete(self, del_remnants: bool) -> None:
raise NotImplementedError("Subclasses must implement the delete method")
@abstractmethod
def get_service_file_name(self) -> str:
raise NotImplementedError(
"Subclasses must implement the get_service_file_name method")

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import os
import re
import subprocess
from pathlib import Path
from typing import Optional, List, Type, Union
from kiauh.instance_manager.base_instance import BaseInstance
from kiauh.utils.constants import SYSTEMD
from kiauh.utils.logger import Logger
# noinspection PyMethodMayBeStatic
class InstanceManager:
def __init__(self, instance_type: Type[BaseInstance],
current_instance: Optional[BaseInstance] = None) -> None:
self.instance_type = instance_type
self.current_instance = current_instance
self.instance_name = current_instance.name if current_instance is not None else None
self.instances = []
def get_current_instance(self) -> BaseInstance:
return self.current_instance
def set_current_instance(self, instance: BaseInstance) -> None:
self.current_instance = instance
self.instance_name = f"{instance.prefix}-{instance.name}" if instance.name else instance.prefix
def create_instance(self) -> None:
if self.current_instance is not None:
try:
self.current_instance.create()
except (OSError, subprocess.CalledProcessError) as e:
Logger.print_error(f"Creating instance failed: {e}")
raise
else:
raise ValueError("current_instance cannot be None")
def delete_instance(self, del_remnants=False) -> None:
if self.current_instance is not None:
try:
self.current_instance.delete(del_remnants)
except (OSError, subprocess.CalledProcessError) as e:
Logger.print_error(f"Removing instance failed: {e}")
raise
else:
raise ValueError("current_instance cannot be None")
def enable_instance(self) -> None:
Logger.print_info(f"Enabling {self.instance_name}.service ...")
try:
command = ["sudo", "systemctl", "enable",
f"{self.instance_name}.service"]
if subprocess.run(command, check=True):
Logger.print_ok(f"{self.instance_name}.service enabled.")
except subprocess.CalledProcessError as e:
Logger.print_error(
f"Error enabling service {self.instance_name}.service:")
Logger.print_error(f"{e}")
def disable_instance(self) -> None:
Logger.print_info(f"Disabling {self.instance_name}.service ...")
try:
command = ["sudo", "systemctl", "disable",
f"{self.instance_name}.service"]
if subprocess.run(command, check=True):
Logger.print_ok(f"{self.instance_name}.service disabled.")
except subprocess.CalledProcessError as e:
Logger.print_error(
f"Error disabling service {self.instance_name}.service:")
Logger.print_error(f"{e}")
def start_instance(self) -> None:
Logger.print_info(f"Starting {self.instance_name}.service ...")
try:
command = ["sudo", "systemctl", "start",
f"{self.instance_name}.service"]
if subprocess.run(command, check=True):
Logger.print_ok(f"{self.instance_name}.service started.")
except subprocess.CalledProcessError as e:
Logger.print_error(
f"Error starting service {self.instance_name}.service:")
Logger.print_error(f"{e}")
def stop_instance(self) -> None:
Logger.print_info(f"Stopping {self.instance_name}.service ...")
try:
command = ["sudo", "systemctl", "stop",
f"{self.instance_name}.service"]
if subprocess.run(command, check=True):
Logger.print_ok(f"{self.instance_name}.service stopped.")
except subprocess.CalledProcessError as e:
Logger.print_error(
f"Error stopping service {self.instance_name}.service:")
Logger.print_error(f"{e}")
raise
def reload_daemon(self) -> None:
Logger.print_info("Reloading systemd manager configuration ...")
try:
command = ["sudo", "systemctl", "daemon-reload"]
if subprocess.run(command, check=True):
Logger.print_ok("Systemd manager configuration reloaded")
except subprocess.CalledProcessError as e:
Logger.print_error("Error reloading systemd manager configuration:")
Logger.print_error(f"{e}")
raise
def get_instances(self) -> List[BaseInstance]:
if not self.instances:
self._find_instances()
return sorted(self.instances,
key=lambda x: self._sort_instance_list(x.name))
def _find_instances(self) -> None:
prefix = self.instance_type.__name__.lower()
pattern = re.compile(f"{prefix}(-[0-9a-zA-Z]+)?.service")
excluded = self.instance_type.blacklist()
service_list = [
os.path.join(SYSTEMD, service)
for service in os.listdir(SYSTEMD)
if pattern.search(service)
and not any(s in service for s in excluded)]
instance_list = [
self.instance_type(name=self._get_instance_name(Path(service)))
for service in service_list]
self.instances = instance_list
def _get_instance_name(self, file_path: Path) -> Union[str, None]:
full_name = str(file_path).split("/")[-1].split(".")[0]
if full_name.isalnum():
return None
return full_name.split("-")[-1]
def _sort_instance_list(self, s):
return int(s) if s.isdigit() else s

20
kiauh/main.py Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 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 kiauh.menus.main_menu import MainMenu
from kiauh.utils.logger import Logger
def main():
try:
MainMenu().start()
except KeyboardInterrupt:
Logger.print_ok("\nHappy printing!\n", prefix=False)

0
kiauh/menus/__init__.py Normal file
View File

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import textwrap
from kiauh.menus.base_menu import BaseMenu
from kiauh.utils.constants import COLOR_YELLOW, RESET_FORMAT
class AdvancedMenu(BaseMenu):
def __init__(self):
super().__init__(
header=True,
options={},
footer_type="back"
)
def print_menu(self):
menu = textwrap.dedent(f"""
/=======================================================\\
| {COLOR_YELLOW}~~~~~~~~~~~~~ [ Advanced Menu ] ~~~~~~~~~~~~~{RESET_FORMAT} |
|-------------------------------------------------------|
| Klipper & API: | Mainsail: |
| 0) [Rollback] | 5) [Theme installer] |
| | |
| Firmware: | System: |
| 1) [Build only] | 6) [Change hostname] |
| 2) [Flash only] | |
| 3) [Build + Flash] | Extras: |
| 4) [Get MCU ID] | 7) [G-Code Shell Command] |
""")[1:]
print(menu, end="")

177
kiauh/menus/base_menu.py Normal file
View File

@@ -0,0 +1,177 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import subprocess
import sys
import textwrap
from abc import abstractmethod, ABC
from typing import Dict, Any
from kiauh.utils.constants import COLOR_GREEN, COLOR_YELLOW, COLOR_RED, \
COLOR_CYAN, RESET_FORMAT
def clear():
subprocess.call("clear", shell=True)
def print_header():
header = textwrap.dedent(f"""
/=======================================================\\
| {COLOR_CYAN}~~~~~~~~~~~~~~~~~ [ KIAUH ] ~~~~~~~~~~~~~~~~~{RESET_FORMAT} |
| {COLOR_CYAN} Klipper Installation And Update Helper {RESET_FORMAT} |
| {COLOR_CYAN}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{RESET_FORMAT} |
\=======================================================/
""")[1:]
print(header, end="")
def print_quit_footer():
footer = textwrap.dedent(f"""
|-------------------------------------------------------|
| {COLOR_RED}Q) Quit{RESET_FORMAT} |
\=======================================================/
""")[1:]
print(footer, end="")
def print_back_footer():
footer = textwrap.dedent(f"""
|-------------------------------------------------------|
| {COLOR_GREEN}B) « Back{RESET_FORMAT} |
\=======================================================/
""")[1:]
print(footer, end="")
def print_back_help_footer():
footer = textwrap.dedent(f"""
|-------------------------------------------------------|
| {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_RED}Q) Quit{RESET_FORMAT} |
\=======================================================/
""")[1:]
print(footer, end="")
def print_back_quit_footer():
footer = textwrap.dedent(f"""
|-------------------------------------------------------|
| {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_YELLOW}H) Help [?]{RESET_FORMAT} |
\=======================================================/
""")[1:]
print(footer, end="")
def print_back_quit_help_footer():
footer = textwrap.dedent(f"""
|-------------------------------------------------------|
| {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_RED}Q) Quit{RESET_FORMAT} | {COLOR_YELLOW}H) Help [?]{RESET_FORMAT} |
\=======================================================/
""")[1:]
print(footer, end="")
class BaseMenu(ABC):
QUIT_FOOTER = "quit"
BACK_FOOTER = "back"
BACK_HELP_FOOTER = "back_help"
BACK_QUIT_FOOTER = "back_quit"
BACK_QUIT_HELP_FOOTER = "back_quit_help"
def __init__(self, options: Dict[int, Any], options_offset=0, header=True,
footer_type="quit"):
self.options = options
self.options_offset = options_offset
self.header = header
self.footer_type = footer_type
@abstractmethod
def print_menu(self):
raise NotImplementedError(
"Subclasses must implement the print_menu method")
def print_footer(self):
footer_type_map = {
self.QUIT_FOOTER: print_quit_footer,
self.BACK_FOOTER: print_back_footer,
self.BACK_HELP_FOOTER: print_back_help_footer,
self.BACK_QUIT_FOOTER: print_back_quit_footer,
self.BACK_QUIT_HELP_FOOTER: print_back_quit_help_footer
}
footer_function = footer_type_map.get(self.footer_type,
print_quit_footer)
footer_function()
def display(self):
# clear()
if self.header:
print_header()
self.print_menu()
self.print_footer()
def handle_user_input(self):
while True:
choice = input(f"{COLOR_CYAN}###### Perform action: {RESET_FORMAT}")
error_msg = f"{COLOR_RED}Invalid input.{RESET_FORMAT}" \
if choice.isalpha() \
else f"{COLOR_RED}Invalid input. Select a number between {min(self.options)} and {max(self.options)}.{RESET_FORMAT}"
if choice.isdigit() and 0 <= int(choice) < len(self.options):
return choice
elif choice.isalpha():
allowed_input = {
"quit": ["q"],
"back": ["b"],
"back_help": ["b", "h"],
"back_quit": ["b", "q"],
"back_quit_help": ["b", "q", "h"]
}
if self.footer_type in allowed_input and choice.lower() in \
allowed_input[self.footer_type]:
return choice
else:
print(error_msg)
else:
print(error_msg)
def start(self):
while True:
self.display()
choice = self.handle_user_input()
if choice == "q":
print(f"{COLOR_GREEN}###### Happy printing!{RESET_FORMAT}")
sys.exit(0)
elif choice == "b":
return
elif choice == "p":
print("help!")
else:
self.execute_option(int(choice))
def execute_option(self, choice):
option = self.options.get(choice, None)
if isinstance(option, type) and issubclass(option, BaseMenu):
self.navigate_to_submenu(option)
elif callable(option):
option()
elif option is None:
raise NotImplementedError(f"No implementation for option {choice}")
else:
raise TypeError(
f"Type {type(option)} of option {choice} not of type BaseMenu or Method")
def navigate_to_submenu(self, submenu_class):
submenu = submenu_class()
submenu.previous_menu = self
submenu.start()

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import textwrap
from kiauh.menus.base_menu import BaseMenu
from kiauh.modules.klipper import klipper_setup
from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT
# noinspection PyMethodMayBeStatic
class InstallMenu(BaseMenu):
def __init__(self):
super().__init__(
header=True,
options={
1: self.install_klipper,
2: self.install_moonraker,
3: self.install_mainsail,
4: self.install_fluidd,
5: self.install_klipperscreen,
6: self.install_pretty_gcode,
7: self.install_telegram_bot,
8: self.install_obico,
9: self.install_octoeverywhere,
10: self.install_mobileraker,
11: self.install_crowsnest
},
footer_type="back"
)
def print_menu(self):
menu = textwrap.dedent(f"""
/=======================================================\\
| {COLOR_GREEN}~~~~~~~~~~~ [ Installation Menu ] ~~~~~~~~~~~{RESET_FORMAT} |
|-------------------------------------------------------|
| You need this menu usually only for installing |
| all necessary dependencies for the various |
| functions on a completely fresh system. |
|-------------------------------------------------------|
| Firmware & API: | Other: |
| 1) [Klipper] | 6) [PrettyGCode] |
| 2) [Moonraker] | 7) [Telegram Bot] |
| | 8) $(obico_install_title) |
| Klipper Webinterface: | 9) [OctoEverywhere] |
| 3) [Mainsail] | 10) [Mobileraker] |
| 4) [Fluidd] | |
| | Webcam Streamer: |
| Touchscreen GUI: | 11) [Crowsnest] |
| 5) [KlipperScreen] | |
""")[1:]
print(menu, end="")
def install_klipper(self):
klipper_setup.run_klipper_setup(install=True)
def install_moonraker(self):
print("install_moonraker")
def install_mainsail(self):
print("install_mainsail")
def install_fluidd(self):
print("install_fluidd")
def install_klipperscreen(self):
print("install_klipperscreen")
def install_pretty_gcode(self):
print("install_pretty_gcode")
def install_telegram_bot(self):
print("install_telegram_bot")
def install_obico(self):
print("install_obico")
def install_octoeverywhere(self):
print("install_octoeverywhere")
def install_mobileraker(self):
print("install_mobileraker")
def install_crowsnest(self):
print("install_crowsnest")

65
kiauh/menus/main_menu.py Normal file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import textwrap
from kiauh.menus.advanced_menu import AdvancedMenu
from kiauh.menus.base_menu import BaseMenu
from kiauh.menus.install_menu import InstallMenu
from kiauh.menus.remove_menu import RemoveMenu
from kiauh.menus.settings_menu import SettingsMenu
from kiauh.menus.update_menu import UpdateMenu
from kiauh.utils.constants import COLOR_MAGENTA, COLOR_CYAN, RESET_FORMAT
class MainMenu(BaseMenu):
def __init__(self):
super().__init__(
header=True,
options={
0: self.test,
1: InstallMenu,
2: UpdateMenu,
3: RemoveMenu,
4: AdvancedMenu,
5: None,
6: SettingsMenu
},
footer_type="quit"
)
def print_menu(self):
menu = textwrap.dedent(f"""
/=======================================================\\
| {COLOR_CYAN}~~~~~~~~~~~~~~~ [ Main Menu ] ~~~~~~~~~~~~~~~{RESET_FORMAT} |
|-------------------------------------------------------|
| 0) [Log-Upload] | Klipper: <TODO> |
| | Repo: <TODO> |
| 1) [Install] | |
| 2) [Update] | Moonraker: <TODO> |
| 3) [Remove] | Repo: <TODO> |
| 4) [Advanced] | |
| 5) [Backup] | Mainsail: <TODO> |
| | Fluidd: <TODO> |
| 6) [Settings] | KlipperScreen: <TODO> |
| | Mobileraker: <TODO> |
| | |
| | Crowsnest: <TODO> |
| | Telegram Bot: <TODO> |
| | Obico: <TODO> |
| | OctoEverywhere: <TODO> |
|-------------------------------------------------------|
| {COLOR_CYAN}KIAUH v6.0.0{RESET_FORMAT} | Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT} |
""")[1:]
print(menu, end="")
def test(self):
print("blub")

109
kiauh/menus/remove_menu.py Normal file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import textwrap
from kiauh.menus.base_menu import BaseMenu
from kiauh.modules.klipper import klipper_setup
from kiauh.utils.constants import COLOR_RED, RESET_FORMAT
# noinspection PyMethodMayBeStatic
class RemoveMenu(BaseMenu):
def __init__(self):
super().__init__(
header=True,
options={
1: self.remove_klipper,
2: self.remove_moonraker,
3: self.remove_mainsail,
4: self.remove_mainsail_config,
5: self.remove_fluidd,
6: self.remove_fluidd_config,
7: self.remove_klipperscreen,
8: self.remove_crowsnest,
9: self.remove_mjpgstreamer,
10: self.remove_pretty_gcode,
11: self.remove_telegram_bot,
12: self.remove_obico,
13: self.remove_octoeverywhere,
14: self.remove_mobileraker,
15: self.remove_nginx,
},
footer_type="back"
)
def print_menu(self):
menu = textwrap.dedent(f"""
/=======================================================\\
| {COLOR_RED}~~~~~~~~~~~~~~ [ Remove Menu ] ~~~~~~~~~~~~~~{RESET_FORMAT} |
|-------------------------------------------------------|
| INFO: Configurations and/or any backups will be kept! |
|-------------------------------------------------------|
| Firmware & API: | Webcam Streamer: |
| 1) [Klipper] | 8) [Crowsnest] |
| 2) [Moonraker] | 9) [MJPG-Streamer] |
| | |
| Klipper Webinterface: | Other: |
| 3) [Mainsail] | 10) [PrettyGCode] |
| 4) [Mainsail-Config] | 11) [Telegram Bot] |
| 5) [Fluidd] | 12) [Obico for Klipper] |
| 6) [Fluidd-Config] | 13) [OctoEverywhere] |
| | 14) [Mobileraker] |
| Touchscreen GUI: | 15) [NGINX] |
| 7) [KlipperScreen] | |
""")[1:]
print(menu, end="")
def remove_klipper(self):
klipper_setup.run_klipper_setup(install=False)
def remove_moonraker(self):
print("remove_moonraker")
def remove_mainsail(self):
print("remove_mainsail")
def remove_mainsail_config(self):
print("remove_mainsail_config")
def remove_fluidd(self):
print("remove_fluidd")
def remove_fluidd_config(self):
print("remove_fluidd_config")
def remove_klipperscreen(self):
print("remove_klipperscreen")
def remove_crowsnest(self):
print("remove_crowsnest")
def remove_mjpgstreamer(self):
print("remove_mjpgstreamer")
def remove_pretty_gcode(self):
print("remove_pretty_gcode")
def remove_telegram_bot(self):
print("remove_telegram_bot")
def remove_obico(self):
print("remove_obico")
def remove_octoeverywhere(self):
print("remove_octoeverywhere")
def remove_mobileraker(self):
print("remove_mobileraker")
def remove_nginx(self):
print("remove_nginx")

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 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 kiauh.menus.base_menu import BaseMenu
# noinspection PyMethodMayBeStatic
class SettingsMenu(BaseMenu):
def __init__(self):
super().__init__(
header=True,
options={}
)
def print_menu(self):
print("self")
def execute_option_p(self):
# Implement the functionality for Option P
print("Executing Option P")
def execute_option_q(self):
# Implement the functionality for Option Q
print("Executing Option Q")
def execute_option_r(self):
# Implement the functionality for Option R
print("Executing Option R")

108
kiauh/menus/update_menu.py Normal file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import textwrap
from kiauh.menus.base_menu import BaseMenu
from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT
# noinspection PyMethodMayBeStatic
class UpdateMenu(BaseMenu):
def __init__(self):
super().__init__(
header=True,
options={
0: self.update_all,
1: self.update_klipper,
2: self.update_moonraker,
3: self.update_mainsail,
4: self.update_fluidd,
5: self.update_klipperscreen,
6: self.update_pgc_for_klipper,
7: self.update_telegram_bot,
8: self.update_moonraker_obico,
9: self.update_octoeverywhere,
10: self.update_mobileraker,
11: self.update_crowsnest,
12: self.upgrade_system_packages,
},
footer_type="back"
)
def print_menu(self):
menu = textwrap.dedent(f"""
/=======================================================\\
| {COLOR_GREEN}~~~~~~~~~~~~~~ [ Update Menu ] ~~~~~~~~~~~~~~{RESET_FORMAT} |
|-------------------------------------------------------|
| 0) [Update all] | | |
| | Current: | Latest: |
| Klipper & API: |--------------|--------------|
| 1) [Klipper] | | |
| 2) [Moonraker] | | |
| | | |
| Klipper Webinterface: |--------------|--------------|
| 3) [Mainsail] | | |
| 4) [Fluidd] | | |
| | | |
| Touchscreen GUI: |--------------|--------------|
| 5) [KlipperScreen] | | |
| | | |
| Other: |--------------|--------------|
| 6) [PrettyGCode] | | |
| 7) [Telegram Bot] | | |
| 8) [Obico for Klipper] | | |
| 9) [OctoEverywhere] | | |
| 10) [Mobileraker] | | |
| 11) [Crowsnest] | | |
| |-----------------------------|
| 12) [System] | | |
""")[1:]
print(menu, end="")
def update_all(self):
print("update_all")
def update_klipper(self):
print("update_klipper")
def update_moonraker(self):
print("update_moonraker")
def update_mainsail(self):
print("update_mainsail")
def update_fluidd(self):
print("update_fluidd")
def update_klipperscreen(self):
print("update_klipperscreen")
def update_pgc_for_klipper(self):
print("update_pgc_for_klipper")
def update_telegram_bot(self):
print("update_telegram_bot")
def update_moonraker_obico(self):
print("update_moonraker_obico")
def update_octoeverywhere(self):
print("update_octoeverywhere")
def update_mobileraker(self):
print("update_mobileraker")
def update_crowsnest(self):
print("update_crowsnest")
def upgrade_system_packages(self):
print("upgrade_system_packages")

View File

View File

View File

@@ -0,0 +1,164 @@
# !/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import os
import pwd
import shutil
import subprocess
from pathlib import Path
from typing import List
from kiauh.instance_manager.base_instance import BaseInstance
from kiauh.utils.constants import SYSTEMD, KLIPPER_DIR, KLIPPER_ENV_DIR
from kiauh.utils.logger import Logger
from kiauh.utils.system_utils import create_directory
# noinspection PyMethodMayBeStatic
class Klipper(BaseInstance):
@classmethod
def blacklist(cls) -> List[str]:
return ["None", "mcu"]
def __init__(self, name: str):
super().__init__(name=name,
prefix="klipper",
user=pwd.getpwuid(os.getuid())[0],
data_dir_name=self._get_data_dir_from_name(name))
self.klipper_dir = KLIPPER_DIR
self.env_dir = KLIPPER_ENV_DIR
self.cfg_file = f"{self.cfg_dir}/printer.cfg"
self.log = f"{self.log_dir}/klippy.log"
self.serial = f"{self.comms_dir}/klippy.serial"
self.uds = f"{self.comms_dir}/klippy.sock"
def create(self) -> None:
Logger.print_info("Creating Klipper Instance")
module_path = os.path.dirname(os.path.abspath(__file__))
service_template_path = os.path.join(module_path, "res",
"klipper.service")
env_template_file_path = os.path.join(module_path, "res", "klipper.env")
service_file_name = self.get_service_file_name(extension=True)
service_file_target = f"{SYSTEMD}/{service_file_name}"
env_file_target = os.path.abspath(f"{self.sysd_dir}/klipper.env")
# create folder structure
dirs = [self.data_dir, self.cfg_dir, self.log_dir,
self.comms_dir, self.sysd_dir]
for _dir in dirs:
create_directory(Path(_dir))
try:
# writing the klipper service file (requires sudo!)
service_content = self._prep_service_file(service_template_path,
env_file_target)
command = ["sudo", "tee", service_file_target]
subprocess.run(command, input=service_content.encode(),
stdout=subprocess.DEVNULL, check=True)
Logger.print_ok(f"Service file created: {service_file_target}")
# writing the klipper.env file
env_file_content = self._prep_env_file(env_template_file_path)
with open(env_file_target, "w") as env_file:
env_file.write(env_file_content)
Logger.print_ok(f"Env file created: {env_file_target}")
except subprocess.CalledProcessError as e:
Logger.print_error(
f"Error creating service file {service_file_target}: {e}")
raise
except OSError as e:
Logger.print_error(
f"Error creating env file {env_file_target}: {e}")
raise
def read(self) -> None:
print("Reading Klipper Instance")
def update(self) -> None:
print("Updating Klipper Instance")
def delete(self, del_remnants: bool) -> None:
service_file = self.get_service_file_name(extension=True)
service_file_path = self._get_service_file_path()
Logger.print_info(f"Deleting Klipper Instance: {service_file}")
try:
command = ["sudo", "rm", "-f", service_file_path]
subprocess.run(command, check=True)
Logger.print_ok(f"Service file deleted: {service_file_path}")
except subprocess.CalledProcessError as e:
Logger.print_error(f"Error deleting service file: {e}")
raise
if del_remnants:
self._delete_klipper_remnants()
def _delete_klipper_remnants(self) -> None:
try:
Logger.print_info(f"Delete {self.klipper_dir} ...")
shutil.rmtree(Path(self.klipper_dir))
Logger.print_info(f"Delete {self.env_dir} ...")
shutil.rmtree(Path(self.env_dir))
except FileNotFoundError:
Logger.print_info("Cannot delete Klipper directories. Not found.")
except PermissionError as e:
Logger.print_error(f"Error deleting Klipper directories: {e}")
raise
Logger.print_ok("Directories successfully deleted.")
def get_service_file_name(self, extension=False) -> str:
name = self.prefix if self.name is None else self.prefix + '-' + self.name
return name if not extension else f"{name}.service"
def _get_service_file_path(self):
return f"{SYSTEMD}/{self.get_service_file_name(extension=True)}"
def _get_data_dir_from_name(self, name: str) -> str:
if name is None:
return "printer"
elif int(name.isdigit()):
return f"printer_{name}"
else:
return name
def _prep_service_file(self, service_template_path, env_file_path):
try:
with open(service_template_path, "r") as template_file:
template_content = template_file.read()
except FileNotFoundError:
Logger.print_error(
f"Unable to open {service_template_path} - File not found")
raise
service_content = template_content.replace("%USER%", self.user)
service_content = service_content.replace("%KLIPPER_DIR%",
self.klipper_dir)
service_content = service_content.replace("%ENV%", self.env_dir)
service_content = service_content.replace("%ENV_FILE%", env_file_path)
return service_content
def _prep_env_file(self, env_template_file_path):
try:
with open(env_template_file_path, "r") as env_file:
env_template_file_content = env_file.read()
except FileNotFoundError:
Logger.print_error(
f"Unable to open {env_template_file_path} - File not found")
raise
env_file_content = env_template_file_content.replace("%KLIPPER_DIR%",
self.klipper_dir)
env_file_content = env_file_content.replace("%CFG%", self.cfg_file)
env_file_content = env_file_content.replace("%SERIAL%", self.serial)
env_file_content = env_file_content.replace("%LOG%", self.log)
env_file_content = env_file_content.replace("%UDS%", self.uds)
return env_file_content

View File

@@ -0,0 +1,236 @@
# !/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import os
import re
import subprocess
from pathlib import Path
from typing import Optional, List, Union
from kiauh.instance_manager.instance_manager import InstanceManager
from kiauh.modules.klipper.klipper import Klipper
from kiauh.modules.klipper.klipper_utils import print_instance_overview
from kiauh.utils.constants import KLIPPER_DIR, KLIPPER_ENV_DIR
from kiauh.utils.input_utils import get_user_confirm, get_user_number_input, \
get_user_string_input, get_user_selection_input
from kiauh.utils.logger import Logger
from kiauh.utils.system_utils import parse_packages_from_file, \
clone_repo, create_python_venv, \
install_python_requirements, update_system_package_lists, \
install_system_packages
def run_klipper_setup(install: bool) -> None:
instance_manager = InstanceManager(Klipper)
instance_list = instance_manager.get_instances()
instances_installed = len(instance_list)
is_klipper_installed = check_klipper_installation(instance_manager)
if not install and not is_klipper_installed:
Logger.print_warn("Klipper not installed!")
return
if install:
add_additional = handle_existing_instances(instance_manager)
if is_klipper_installed and not add_additional:
Logger.print_info("Exiting Klipper setup ...")
return
install_klipper(instance_manager)
if not install:
if instances_installed == 1:
remove_single_instance(instance_manager)
else:
remove_multi_instance(instance_manager)
def check_klipper_installation(instance_manager: InstanceManager) -> bool:
instance_list = instance_manager.get_instances()
instances_installed = len(instance_list)
if instances_installed < 1:
return False
return True
def handle_existing_instances(instance_manager: InstanceManager) -> bool:
instance_list = instance_manager.get_instances()
instance_count = len(instance_list)
if instance_count > 0:
print_instance_overview(instance_list)
if not get_user_confirm("Add new instances?"):
return False
return True
def install_klipper(instance_manager: InstanceManager) -> None:
instance_list = instance_manager.get_instances()
if_adding = " additional" if len(instance_list) > 0 else ""
install_count = get_user_number_input(
f"Number of{if_adding} Klipper instances to set up",
1, default=1)
instance_names = set_instance_names(instance_list, install_count)
if len(instance_list) < 1:
setup_klipper_prerequesites()
for name in instance_names:
current_instance = Klipper(name=name)
instance_manager.set_current_instance(current_instance)
instance_manager.create_instance()
instance_manager.enable_instance()
instance_manager.start_instance()
instance_manager.reload_daemon()
# step 4: check/handle conflicting packages/services
# step 5: check for required group membership
def setup_klipper_prerequesites() -> None:
# clone klipper TODO: read branch and url from json to allow forks
url = "https://github.com/Klipper3D/klipper"
branch = "master"
clone_repo(Path(KLIPPER_DIR), url, branch)
# install klipper dependencies and create python virtualenv
install_klipper_packages(Path(KLIPPER_DIR))
create_python_venv(Path(KLIPPER_ENV_DIR))
klipper_py_req = Path(f"{KLIPPER_DIR}/scripts/klippy-requirements.txt")
install_python_requirements(Path(KLIPPER_ENV_DIR), klipper_py_req)
def install_klipper_packages(klipper_dir: Path) -> None:
script = f"{klipper_dir}/scripts/install-debian.sh"
packages = parse_packages_from_file(script)
packages = [pkg.replace("python-dev", "python3-dev") for pkg in packages]
# Add dfu-util for octopi-images
packages.append("dfu-util")
# Add dbus requirement for DietPi distro
if os.path.exists("/boot/dietpi/.version"):
packages.append("dbus")
update_system_package_lists(silent=False)
install_system_packages(packages)
def set_instance_names(instance_list, install_count: int) -> List[
Union[str, None]]:
instance_count = len(instance_list)
# default single instance install
if instance_count == 0 and install_count == 1:
return [None]
# new multi instance install
elif ((instance_count == 0 and install_count > 1)
# or convert single instance install to multi instance install
or (instance_count == 1 and install_count >= 1)):
if get_user_confirm("Assign custom names?", False):
return assign_custom_names(instance_count, install_count, None)
else:
_range = range(1, install_count + 1)
return [str(i) for i in _range]
# existing multi instance install
elif instance_count > 1:
if has_custom_names(instance_list):
return assign_custom_names(instance_count, install_count,
instance_list)
else:
start = get_highest_index(instance_list) + 1
_range = range(start, start + install_count)
return [str(i) for i in _range]
def has_custom_names(instance_list: List[Klipper]) -> bool:
pattern = re.compile("^\d+$")
for instance in instance_list:
if not pattern.match(instance.name):
return True
return False
def assign_custom_names(instance_count: int, install_count: int,
instance_list: Optional[List[Klipper]]) -> List[str]:
instance_names = []
exclude = Klipper.blacklist()
# if an instance_list is provided, exclude all existing instance names
if instance_list is not None:
for instance in instance_list:
exclude.append(instance.name)
for i in range(instance_count + install_count):
question = f"Enter name for instance {i + 1}"
name = get_user_string_input(question, exclude=exclude)
instance_names.append(name)
exclude.append(name)
return instance_names
def get_highest_index(instance_list: List[Klipper]) -> int:
indices = [int(instance.name.split('-')[-1]) for instance in instance_list]
return max(indices)
def remove_single_instance(instance_manager: InstanceManager) -> None:
instance_list = instance_manager.get_instances()
try:
instance_manager.set_current_instance(instance_list[0])
instance_manager.stop_instance()
instance_manager.disable_instance()
instance_manager.delete_instance(del_remnants=True)
instance_manager.reload_daemon()
except (OSError, subprocess.CalledProcessError):
Logger.print_error("Removing instance failed!")
return
def remove_multi_instance(instance_manager: InstanceManager) -> None:
instance_list = instance_manager.get_instances()
print_instance_overview(instance_list, show_index=True,
show_select_all=True)
options = [str(i) for i in range(len(instance_list))]
options.extend(["a", "A", "b", "B"])
selection = get_user_selection_input(
"Select Klipper instance to remove", options)
print(selection)
if selection == "b".lower():
return
elif selection == "a".lower():
Logger.print_info("Removing all Klipper instances ...")
for instance in instance_list:
instance_manager.set_current_instance(instance)
instance_manager.stop_instance()
instance_manager.disable_instance()
instance_manager.delete_instance(del_remnants=True)
else:
instance = instance_list[int(selection)]
Logger.print_info(
f"Removing Klipper instance: {instance.get_service_file_name()}")
instance_manager.set_current_instance(instance)
instance_manager.stop_instance()
instance_manager.disable_instance()
instance_manager.delete_instance(del_remnants=False)
instance_manager.reload_daemon()

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from typing import List
from kiauh.instance_manager.base_instance import BaseInstance
from kiauh.menus.base_menu import print_back_footer
from kiauh.utils.constants import COLOR_GREEN, COLOR_CYAN, COLOR_YELLOW, \
RESET_FORMAT
def print_instance_overview(instances: List[BaseInstance], show_index=False,
show_select_all=False):
headline = f"{COLOR_GREEN}The following Klipper instances were found:{RESET_FORMAT}"
print("/=======================================================\\")
print(f"|{'{:^64}'.format(headline)}|")
print("|-------------------------------------------------------|")
if show_select_all:
select_all = f" {COLOR_YELLOW}a) Select all{RESET_FORMAT}"
print(f"|{'{:64}'.format(select_all)}|")
print("| |")
for i, s in enumerate(instances):
index = f"{i})" if show_index else ""
instance = s.get_service_file_name()
line = f"{'{:53}'.format(f'{index} {instance}')}"
print(f"| {COLOR_CYAN}{line}{RESET_FORMAT}|")
print_back_footer()

View File

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

View File

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

0
kiauh/utils/__init__.py Normal file
View File

29
kiauh/utils/constants.py Normal file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 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
# text colors and formats
COLOR_MAGENTA = "\033[35m" # magenta
COLOR_GREEN = "\033[92m" # bright green
COLOR_YELLOW = "\033[93m" # bright yellow
COLOR_RED = "\033[91m" # bright red
COLOR_CYAN = "\033[96m" # bright cyan
RESET_FORMAT = "\033[0m" # reset format
SYSTEMD = "/etc/systemd/system"
KLIPPER_DIR = f"{Path.home()}/klipper"
KLIPPER_ENV_DIR = f"{Path.home()}/klippy-env"
MOONRAKER_DIR = f"{Path.home()}/moonraker"
MOONRAKER_ENV_DIR = f"{Path.home()}/moonraker-env"
MAINSAIL_DIR = f"{Path.home()}/mainsail"
FLUIDD_DIR = f"{Path.home()}/fluidd"

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from typing import Optional, List
from kiauh.utils.logger import Logger
from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT
def get_user_confirm(question: str, default_choice=True) -> bool:
options_confirm = ["y", "yes"]
options_decline = ["n", "no"]
if default_choice:
def_choice = "(Y/n)"
options_confirm.append("")
else:
def_choice = "(y/N)"
options_decline.append("")
while True:
choice = (
input(f"{COLOR_CYAN}###### {question} {def_choice} {RESET_FORMAT}")
.strip()
.lower())
if choice in options_confirm:
return True
elif choice in options_decline:
return False
else:
Logger.print_error("Invalid choice. Please select 'y' or 'n'.")
def get_user_number_input(question: str, min_count: int, max_count=None,
default=None) -> int:
_question = question + f" (default={default})" if default else question
_question = f"{COLOR_CYAN}###### {_question}: {RESET_FORMAT}"
while True:
try:
num = input(_question)
if num == "":
return default
if max_count is not None:
if min_count <= int(num) <= max_count:
return int(num)
else:
raise ValueError
elif int(num) >= min_count:
return int(num)
else:
raise ValueError
except ValueError:
Logger.print_error("Invalid choice. Please select a valid number.")
def get_user_string_input(question: str, exclude=Optional[List]) -> str:
while True:
_input = (input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}")
.strip())
if _input.isalnum() and _input not in exclude:
return _input
Logger.print_error("Invalid choice. Please enter a valid value.")
if _input in exclude:
Logger.print_error("This value is already in use/reserved.")
def get_user_selection_input(question: str, option_list: List) -> str:
while True:
_input = (input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}")
.strip())
if _input in option_list:
return _input
Logger.print_error("Invalid choice. Please enter a valid value.")

51
kiauh/utils/logger.py Normal file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 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 kiauh.utils.constants import COLOR_GREEN, COLOR_YELLOW, COLOR_RED, \
COLOR_MAGENTA, RESET_FORMAT
class Logger:
@staticmethod
def info(msg):
# log to kiauh.log
pass
@staticmethod
def warn(msg):
# log to kiauh.log
pass
@staticmethod
def error(msg):
# log to kiauh.log
pass
@staticmethod
def print_ok(msg, prefix=True, end="\n") -> None:
message = f"[OK] {msg}" if prefix else msg
print(f"{COLOR_GREEN}{message}{RESET_FORMAT}", end=end)
@staticmethod
def print_warn(msg, prefix=True, end="\n") -> None:
message = f"[WARN] {msg}" if prefix else msg
print(f"{COLOR_YELLOW}{message}{RESET_FORMAT}", end=end)
@staticmethod
def print_error(msg, prefix=True, end="\n") -> None:
message = f"[ERROR] {msg}" if prefix else msg
print(f"{COLOR_RED}{message}{RESET_FORMAT}", end=end)
@staticmethod
def print_info(msg, prefix=True, end="\n") -> None:
message = f"###### {msg}" if prefix else msg
print(f"{COLOR_MAGENTA}{message}{RESET_FORMAT}", end=end)

203
kiauh/utils/system_utils.py Normal file
View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python
# ======================================================================= #
# Copyright (C) 2020 - 2023 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
from typing import List
from kiauh.utils.constants import COLOR_RED, RESET_FORMAT
from kiauh.utils.logger import Logger
from kiauh.utils.input_utils import get_user_confirm
def kill(opt_err_msg=None) -> None:
"""
Kill the application.
Parameters
----------
opt_err_msg : str
optional, additional error message to display
Returns
----------
None
"""
if opt_err_msg:
Logger.print_error(opt_err_msg)
Logger.print_error("A critical error has occured. KIAUH was terminated.")
sys.exit(1)
def clone_repo(target_dir: Path, url: str, branch: str) -> None:
Logger.print_info(f"Cloning repository from {url}")
if not target_dir.exists():
try:
command = ["git", "clone", f"{url}"]
subprocess.run(command, check=True)
command = ["git", "checkout", f"{branch}"]
subprocess.run(command, cwd=target_dir, check=True)
Logger.print_ok("Clone successfull!")
except subprocess.CalledProcessError as e:
print("Error cloning repository:", e.output.decode())
else:
overwrite_target = get_user_confirm(
"Target directory already exists. Overwrite?")
if overwrite_target:
try:
shutil.rmtree(target_dir)
clone_repo(target_dir, url, branch)
except OSError as e:
print("Error removing existing repository:", e.strerror)
else:
print("Skipping re-clone of repository ...")
def parse_packages_from_file(source_file) -> List[str]:
packages = []
print("Reading dependencies...")
with open(source_file, 'r') as file:
for line in file:
line = line.strip()
if line.startswith("PKGLIST="):
line = line.replace("\"", "")
line = line.replace("PKGLIST=", "")
line = line.replace('${PKGLIST}', '')
packages.extend(line.split())
return packages
def create_python_venv(target: Path) -> None:
Logger.print_info("Set up Python virtual environment ...")
if not target.exists():
try:
command = ["python3", "-m", "venv", f"{target}"]
result = subprocess.run(command, stderr=subprocess.PIPE, text=True)
if result.returncode != 0 or result.stderr:
print(f"{COLOR_RED}{result.stderr}{RESET_FORMAT}")
Logger.print_error("Setup of virtualenv failed!")
return
Logger.print_ok("Setup of virtualenv successfull!")
except subprocess.CalledProcessError as e:
print("Error setting up virtualenv:", e.output.decode())
else:
overwrite_venv = get_user_confirm(
"Virtualenv already exists. Re-create?")
if overwrite_venv:
try:
shutil.rmtree(target)
create_python_venv(target)
except OSError as e:
Logger.print_error(
f"Error removing existing virtualenv: {e.strerror}",
False)
else:
print("Skipping re-creation of virtualenv ...")
def update_python_pip(target: Path) -> None:
Logger.print_info("Updating pip ...")
try:
command = [f"{target}/bin/pip", "install", "-U", "pip"]
result = subprocess.run(command, stderr=subprocess.PIPE, text=True)
if result.returncode != 0 or result.stderr:
Logger.print_error(f"{result.stderr}", False)
Logger.print_error("Updating pip failed!")
return
Logger.print_ok("Updating pip successfull!")
except subprocess.CalledProcessError as e:
print("Error updating pip:", e.output.decode())
def install_python_requirements(target: Path, requirements: Path) -> None:
update_python_pip(target)
Logger.print_info("Installing Python requirements ...")
try:
command = [f"{target}/bin/pip", "install", "-r", f"{requirements}"]
result = subprocess.run(command, stderr=subprocess.PIPE, text=True)
if result.returncode != 0 or result.stderr:
Logger.print_error(f"{result.stderr}", False)
Logger.print_error("Installing Python requirements failed!")
return
Logger.print_ok("Installing Python requirements successfull!")
except subprocess.CalledProcessError as e:
print("Error installing Python requirements:", e.output.decode())
def update_system_package_lists(silent: bool, rls_info_change=False) -> None:
cache_mtime = 0
cache_files = [
"/var/lib/apt/periodic/update-success-stamp",
"/var/lib/apt/lists"
]
for cache_file in cache_files:
if Path(cache_file).exists():
cache_mtime = max(cache_mtime, os.path.getmtime(cache_file))
update_age = int(time.time() - cache_mtime)
update_interval = 6 * 3600 # 48hrs
if update_age <= update_interval:
return
if not silent:
print("Updating package list...")
try:
command = ["sudo", "apt-get", "update"]
if rls_info_change:
command.append("--allow-releaseinfo-change")
result = subprocess.run(command, stderr=subprocess.PIPE, text=True)
if result.returncode != 0 or result.stderr:
Logger.print_error(f"{result.stderr}", False)
Logger.print_error("Updating system package list failed!")
return
Logger.print_ok("System package list updated successfully!")
except subprocess.CalledProcessError as e:
kill(f"Error updating system package list:\n{e.stderr.decode()}")
def install_system_packages(packages: List) -> None:
try:
command = ["sudo", "apt-get", "install", "-y"]
for pkg in packages:
command.append(pkg)
subprocess.run(command, stderr=subprocess.PIPE, check=True)
Logger.print_ok("Packages installed successfully.")
except subprocess.CalledProcessError as e:
kill(f"Error installing packages:\n{e.stderr.decode()}")
def create_directory(_dir: Path) -> None:
try:
if not os.path.isdir(_dir):
Logger.print_info(f"Create directory: {_dir}")
os.makedirs(_dir, exist_ok=True)
Logger.print_ok("Directory created!")
else:
Logger.print_info(
f"Directory already exists: {_dir}\nSkip creation ...")
except OSError as e:
Logger.print_error(f"Error creating folder: {e}")
raise