feat: KIAUH v6 - full rewrite of KIAUH in Python (#428)

This commit is contained in:
dw-0
2024-08-31 19:16:52 +02:00
committed by GitHub
parent 8547942986
commit 0ee0fa3325
159 changed files with 13461 additions and 54 deletions

View File

@@ -0,0 +1,12 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
MODULE_PATH = Path(__file__).resolve().parent

View File

@@ -0,0 +1,6 @@
# /etc/nginx/conf.d/common_vars.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

View File

@@ -0,0 +1,95 @@
server {
listen %PORT%;
# uncomment the next line to activate IPv6
# listen [::]:%PORT%;
access_log /var/log/nginx/%NAME%-access.log;
error_log /var/log/nginx/%NAME%-error.log;
# disable this section on smaller hardware like a pi zero
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_proxied expired no-cache no-store private auth;
gzip_comp_level 4;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/json application/xml;
# web_path from %NAME% static files
root %ROOT_DIR%;
index index.html;
server_name _;
# disable max upload size checks
client_max_body_size 0;
# disable proxy request buffering
proxy_request_buffering off;
location / {
try_files $uri $uri/ /index.html;
}
location = /index.html {
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
location /websocket {
proxy_pass http://apiserver/websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 86400;
}
location ~ ^/(printer|api|access|machine|server)/ {
proxy_pass http://apiserver$request_uri;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
}
location /webcam/ {
postpone_output 0;
proxy_buffering off;
proxy_ignore_headers X-Accel-Buffering;
access_log off;
error_log off;
proxy_pass http://mjpgstreamer1/;
}
location /webcam2/ {
postpone_output 0;
proxy_buffering off;
proxy_ignore_headers X-Accel-Buffering;
access_log off;
error_log off;
proxy_pass http://mjpgstreamer2/;
}
location /webcam3/ {
postpone_output 0;
proxy_buffering off;
proxy_ignore_headers X-Accel-Buffering;
access_log off;
error_log off;
proxy_pass http://mjpgstreamer3/;
}
location /webcam4/ {
postpone_output 0;
proxy_buffering off;
proxy_ignore_headers X-Accel-Buffering;
access_log off;
error_log off;
proxy_pass http://mjpgstreamer4/;
}
}

View File

@@ -0,0 +1,25 @@
# /etc/nginx/conf.d/upstreams.conf
upstream apiserver {
ip_hash;
server 127.0.0.1:7125;
}
upstream mjpgstreamer1 {
ip_hash;
server 127.0.0.1:8080;
}
upstream mjpgstreamer2 {
ip_hash;
server 127.0.0.1:8081;
}
upstream mjpgstreamer3 {
ip_hash;
server 127.0.0.1:8082;
}
upstream mjpgstreamer4 {
ip_hash;
server 127.0.0.1:8083;
}

View File

@@ -0,0 +1,56 @@
# ======================================================================= #
# 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
from abc import ABC
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
class WebClientType(Enum):
MAINSAIL: str = "mainsail"
FLUIDD: str = "fluidd"
class WebClientConfigType(Enum):
MAINSAIL: str = "mainsail-config"
FLUIDD: str = "fluidd-config"
@dataclass()
class BaseWebClient(ABC):
"""Base class for webclient data"""
client: WebClientType
name: str
display_name: str
client_dir: Path
config_file: Path
backup_dir: Path
repo_path: str
download_url: str
nginx_access_log: Path
nginx_error_log: Path
client_config: BaseWebClientConfig
@dataclass()
class BaseWebClientConfig(ABC):
"""Base class for webclient config data"""
client_config: WebClientConfigType
name: str
display_name: str
config_filename: str
config_dir: Path
backup_dir: Path
repo_url: str
config_section: str

View File

@@ -0,0 +1,43 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from typing import List
from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker
from components.webui_client.base_data import BaseWebClientConfig
from core.logger import Logger
from utils.config_utils import remove_config_section
from utils.fs_utils import run_remove_routines
from utils.instance_utils import get_instances
def run_client_config_removal(
client_config: BaseWebClientConfig,
kl_instances: List[Klipper],
mr_instances: List[Moonraker],
) -> None:
remove_client_config_dir(client_config)
remove_client_config_symlink(client_config)
remove_config_section(f"update_manager {client_config.name}", mr_instances)
remove_config_section(client_config.config_section, kl_instances)
def remove_client_config_dir(client_config: BaseWebClientConfig) -> None:
Logger.print_status(f"Removing {client_config.display_name} ...")
run_remove_routines(client_config.config_dir)
def remove_client_config_symlink(client_config: BaseWebClientConfig) -> None:
instances: List[Klipper] = get_instances(Klipper)
for instance in instances:
run_remove_routines(
instance.base.cfg_dir.joinpath(client_config.config_filename)
)

View File

@@ -0,0 +1,125 @@
# ======================================================================= #
# 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 subprocess
from pathlib import Path
from typing import List
from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker
from components.webui_client.base_data import BaseWebClient, BaseWebClientConfig
from components.webui_client.client_dialogs import (
print_client_already_installed_dialog,
)
from components.webui_client.client_utils import (
backup_client_config_data,
detect_client_cfg_conflict,
)
from core.instance_manager.instance_manager import InstanceManager
from core.logger import Logger
from core.settings.kiauh_settings import KiauhSettings
from utils.common import backup_printer_config_dir
from utils.config_utils import add_config_section, add_config_section_at_top
from utils.fs_utils import create_symlink
from utils.git_utils import git_clone_wrapper, git_pull_wrapper
from utils.input_utils import get_confirm
from utils.instance_utils import get_instances
def install_client_config(client_data: BaseWebClient) -> None:
client_config: BaseWebClientConfig = client_data.client_config
display_name = client_config.display_name
if detect_client_cfg_conflict(client_data):
Logger.print_info("Another Client-Config is already installed! Skipped ...")
return
if client_config.config_dir.exists():
print_client_already_installed_dialog(display_name)
if get_confirm(f"Re-install {display_name}?", allow_go_back=True):
shutil.rmtree(client_config.config_dir)
else:
return
mr_instances: List[Moonraker] = get_instances(Moonraker)
kl_instances = get_instances(Klipper)
try:
download_client_config(client_config)
create_client_config_symlink(client_config, kl_instances)
backup_printer_config_dir()
add_config_section(
section=f"update_manager {client_config.name}",
instances=mr_instances,
options=[
("type", "git_repo"),
("primary_branch", "master"),
("path", str(client_config.config_dir)),
("origin", str(client_config.repo_url)),
("managed_services", "klipper"),
],
)
add_config_section_at_top(client_config.config_section, kl_instances)
InstanceManager.restart_all(kl_instances)
except Exception as e:
Logger.print_error(f"{display_name} installation failed!\n{e}")
return
Logger.print_ok(f"{display_name} installation complete!", start="\n")
def download_client_config(client_config: BaseWebClientConfig) -> None:
try:
Logger.print_status(f"Downloading {client_config.display_name} ...")
repo = client_config.repo_url
target_dir = client_config.config_dir
git_clone_wrapper(repo, target_dir)
except Exception:
Logger.print_error(f"Downloading {client_config.display_name} failed!")
raise
def update_client_config(client: BaseWebClient) -> None:
client_config: BaseWebClientConfig = client.client_config
Logger.print_status(f"Updating {client_config.display_name} ...")
if not client_config.config_dir.exists():
Logger.print_info(
f"Unable to update {client_config.display_name}. Directory does not exist! Skipping ..."
)
return
settings = KiauhSettings()
if settings.kiauh.backup_before_update:
backup_client_config_data(client)
git_pull_wrapper(client_config.repo_url, client_config.config_dir)
Logger.print_ok(f"Successfully updated {client_config.display_name}.")
Logger.print_info("Restart Klipper to reload the configuration!")
def create_client_config_symlink(
client_config: BaseWebClientConfig, klipper_instances: List[Klipper]
) -> None:
for instance in klipper_instances:
Logger.print_status(f"Create symlink for {client_config.config_filename} ...")
source = Path(client_config.config_dir, client_config.config_filename)
target = instance.base.cfg_dir
Logger.print_status(f"Linking {source} to {target}")
try:
create_symlink(source, target)
except subprocess.CalledProcessError:
Logger.print_error("Creating symlink failed!")

View File

@@ -0,0 +1,88 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from typing import List
from components.webui_client.base_data import BaseWebClient
from core.logger import DialogType, Logger
def print_moonraker_not_found_dialog() -> None:
Logger.print_dialog(
DialogType.WARNING,
[
"No local Moonraker installation was found!",
"\n\n",
"It is possible to install Mainsail without a local Moonraker installation. "
"If you continue, you need to make sure, that Moonraker is installed on "
"another machine in your network. Otherwise Mainsail will NOT work "
"correctly.",
],
)
def print_client_already_installed_dialog(name: str) -> None:
Logger.print_dialog(
DialogType.WARNING,
[
f"{name} seems to be already installed!",
f"If you continue, your current {name} installation will be overwritten.",
],
)
def print_client_port_select_dialog(
name: str, port: int, ports_in_use: List[int]
) -> None:
Logger.print_dialog(
DialogType.CUSTOM,
[
f"Please select the port, {name} should be served on. If your are unsure "
f"what to select, hit Enter to apply the suggested value of: {port}",
"\n\n",
f"In case you need {name} to be served on a specific port, you can set it "
f"now. Make sure that the port is not already used by another application "
f"on your system!",
"\n\n",
"The following ports were found to be in use already:",
*[f"{port}" for port in ports_in_use],
],
)
def print_install_client_config_dialog(client: BaseWebClient) -> None:
name = client.display_name
url = client.client_config.repo_url.replace(".git", "")
Logger.print_dialog(
DialogType.INFO,
[
f"It is recommended to use special macros in order to have {name} fully "
f"functional and working.",
"\n\n",
f"The recommended macros for {name} can be seen here:",
url,
"\n\n",
"If you already use these macros skip this step. Otherwise you should "
"consider to answer with 'Y' to download the recommended macros.",
],
)
def print_ipv6_warning_dialog() -> None:
Logger.print_dialog(
DialogType.WARNING,
[
"It looks like IPv6 is enabled on this system!",
"This may cause issues with the installation of NGINX in the following "
"steps! It is recommended to disable IPv6 on your system to avoid this issue.",
"\n\n",
"If you think this warning is a false alarm, and you are sure that "
"IPv6 is disabled, you can continue with the installation.",
],
)

View File

@@ -0,0 +1,85 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from typing import List
from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker
from components.webui_client.base_data import (
BaseWebClient,
)
from components.webui_client.client_config.client_config_remove import (
run_client_config_removal,
)
from core.backup_manager.backup_manager import BackupManager
from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED
from core.logger import Logger
from utils.config_utils import remove_config_section
from utils.fs_utils import (
remove_with_sudo,
run_remove_routines,
)
from utils.instance_utils import get_instances
def run_client_removal(
client: BaseWebClient,
remove_client: bool,
remove_client_cfg: bool,
backup_config: bool,
) -> None:
mr_instances: List[Moonraker] = get_instances(Moonraker)
kl_instances: List[Klipper] = get_instances(Klipper)
if backup_config:
bm = BackupManager()
bm.backup_file(client.config_file)
if remove_client:
client_name = client.name
remove_client_dir(client)
remove_client_nginx_config(client_name)
remove_client_nginx_logs(client, kl_instances)
section = f"update_manager {client_name}"
remove_config_section(section, mr_instances)
if remove_client_cfg:
run_client_config_removal(
client.client_config,
kl_instances,
mr_instances,
)
def remove_client_dir(client: BaseWebClient) -> None:
Logger.print_status(f"Removing {client.display_name} ...")
run_remove_routines(client.client_dir)
def remove_client_nginx_config(name: str) -> None:
Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...")
remove_with_sudo(NGINX_SITES_AVAILABLE.joinpath(name))
remove_with_sudo(NGINX_SITES_ENABLED.joinpath(name))
def remove_client_nginx_logs(client: BaseWebClient, instances: List[Klipper]) -> None:
Logger.print_status(f"Removing NGINX logs for {client.display_name} ...")
remove_with_sudo(client.nginx_access_log)
remove_with_sudo(client.nginx_error_log)
if not instances:
return
for instance in instances:
run_remove_routines(
instance.base.log_dir.joinpath(client.nginx_access_log.name)
)
run_remove_routines(instance.base.log_dir.joinpath(client.nginx_error_log.name))

View File

@@ -0,0 +1,190 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import shutil
import tempfile
from pathlib import Path
from typing import List
from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker
from components.webui_client import MODULE_PATH
from components.webui_client.base_data import (
BaseWebClient,
BaseWebClientConfig,
WebClientType,
)
from components.webui_client.client_config.client_config_setup import (
install_client_config,
)
from components.webui_client.client_dialogs import (
print_client_port_select_dialog,
print_install_client_config_dialog,
print_moonraker_not_found_dialog,
)
from components.webui_client.client_utils import (
copy_common_vars_nginx_cfg,
copy_upstream_nginx_cfg,
create_nginx_cfg,
detect_client_cfg_conflict,
enable_mainsail_remotemode,
get_next_free_port,
is_valid_port,
read_ports_from_nginx_configs,
symlink_webui_nginx_log,
)
from core.instance_manager.instance_manager import InstanceManager
from core.logger import Logger
from core.settings.kiauh_settings import KiauhSettings
from utils.common import check_install_dependencies
from utils.config_utils import add_config_section
from utils.fs_utils import unzip
from utils.input_utils import get_confirm, get_number_input
from utils.instance_utils import get_instances
from utils.sys_utils import (
cmd_sysctl_service,
download_file,
get_ipv4_addr,
)
def install_client(client: BaseWebClient) -> None:
if client is None:
raise ValueError("Missing parameter client_data!")
if client.client_dir.exists():
Logger.print_info(
f"{client.display_name} seems to be already installed! Skipped ..."
)
return
mr_instances: List[Moonraker] = get_instances(Moonraker)
enable_remotemode = False
if not mr_instances:
print_moonraker_not_found_dialog()
if not get_confirm(f"Continue {client.display_name} installation?"):
return
# if moonraker is not installed or multiple instances
# are installed we enable mainsails remote mode
if (
client.client == WebClientType.MAINSAIL
and not mr_instances
or len(mr_instances) > 1
):
enable_remotemode = True
kl_instances = get_instances(Klipper)
install_client_cfg = False
client_config: BaseWebClientConfig = client.client_config
if (
kl_instances
and not client_config.config_dir.exists()
and not detect_client_cfg_conflict(client)
):
print_install_client_config_dialog(client)
question = f"Download the recommended {client_config.display_name}?"
install_client_cfg = get_confirm(question, allow_go_back=False)
settings = KiauhSettings()
port: int = settings.get(client.name, "port")
ports_in_use: List[int] = read_ports_from_nginx_configs()
# check if configured port is a valid number and not in use already
valid_port = is_valid_port(port, ports_in_use)
while not valid_port:
next_port = get_next_free_port(ports_in_use)
print_client_port_select_dialog(client.display_name, next_port, ports_in_use)
port = get_number_input(
f"Configure {client.display_name} for port",
min_count=int(next_port),
default=next_port,
)
valid_port = is_valid_port(port, ports_in_use)
check_install_dependencies({"nginx"})
try:
download_client(client)
if enable_remotemode and client.client == WebClientType.MAINSAIL:
enable_mainsail_remotemode()
if mr_instances:
add_config_section(
section=f"update_manager {client.name}",
instances=mr_instances,
options=[
("type", "web"),
("channel", "stable"),
("repo", str(client.repo_path)),
("path", str(client.client_dir)),
],
)
InstanceManager.restart_all(mr_instances)
if install_client_cfg and kl_instances:
install_client_config(client)
copy_upstream_nginx_cfg()
copy_common_vars_nginx_cfg()
create_nginx_cfg(
display_name=client.display_name,
cfg_name=client.name,
template_src=MODULE_PATH.joinpath("assets/nginx_cfg"),
PORT=port,
ROOT_DIR=client.client_dir,
NAME=client.name,
)
if kl_instances:
symlink_webui_nginx_log(client, kl_instances)
cmd_sysctl_service("nginx", "restart")
except Exception as e:
Logger.print_error(f"{client.display_name} installation failed!\n{e}")
return
log = f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{port}"
Logger.print_ok(f"{client.display_name} installation complete!", start="\n")
Logger.print_ok(log, prefix=False, end="\n\n")
def download_client(client: BaseWebClient) -> None:
zipfile = f"{client.name.lower()}.zip"
target = Path().home().joinpath(zipfile)
try:
Logger.print_status(
f"Downloading {client.display_name} from {client.download_url} ..."
)
download_file(client.download_url, target, True)
Logger.print_ok("Download complete!")
Logger.print_status(f"Extracting {zipfile} ...")
unzip(target, client.client_dir)
target.unlink(missing_ok=True)
Logger.print_ok("OK!")
except Exception:
Logger.print_error(f"Downloading {client.display_name} failed!")
raise
def update_client(client: BaseWebClient) -> None:
Logger.print_status(f"Updating {client.display_name} ...")
if not client.client_dir.exists():
Logger.print_info(
f"Unable to update {client.display_name}. Directory does not exist! Skipping ..."
)
return
with tempfile.NamedTemporaryFile(suffix=".json") as tmp_file:
Logger.print_status(
f"Creating temporary backup of {client.config_file} as {tmp_file.name} ..."
)
shutil.copy(client.config_file, tmp_file.name)
download_client(client)
shutil.copy(tmp_file.name, client.config_file)

View File

@@ -0,0 +1,343 @@
# ======================================================================= #
# 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 json
import re
import shutil
from pathlib import Path
from subprocess import PIPE, CalledProcessError, run
from typing import List, get_args
from components.klipper.klipper import Klipper
from components.webui_client import MODULE_PATH
from components.webui_client.base_data import (
BaseWebClient,
WebClientType,
)
from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData
from core.backup_manager.backup_manager import BackupManager
from core.constants import (
COLOR_CYAN,
COLOR_YELLOW,
NGINX_CONFD,
NGINX_SITES_AVAILABLE,
NGINX_SITES_ENABLED,
RESET_FORMAT,
)
from core.logger import Logger
from core.settings.kiauh_settings import KiauhSettings
from core.types import ComponentStatus
from utils.common import get_install_status
from utils.fs_utils import create_symlink, remove_file
from utils.git_utils import (
get_latest_remote_tag,
get_latest_unstable_tag,
)
def get_client_status(
client: BaseWebClient, fetch_remote: bool = False
) -> ComponentStatus:
files = [
NGINX_SITES_AVAILABLE.joinpath(client.name),
NGINX_CONFD.joinpath("upstreams.conf"),
NGINX_CONFD.joinpath("common_vars.conf"),
]
comp_status: ComponentStatus = get_install_status(client.client_dir, files=files)
# if the client dir does not exist, set the status to not
# installed even if the other files are present
if not client.client_dir.exists():
comp_status.status = 0
comp_status.local = get_local_client_version(client)
comp_status.remote = get_remote_client_version(client) if fetch_remote else None
return comp_status
def get_client_config_status(client: BaseWebClient) -> ComponentStatus:
return get_install_status(client.client_config.config_dir)
def get_current_client_config(clients: List[BaseWebClient]) -> str:
installed = []
for client in clients:
client_config = client.client_config
if client_config.config_dir.exists():
installed.append(client)
if len(installed) > 1:
return f"{COLOR_YELLOW}Conflict!{RESET_FORMAT}"
elif len(installed) == 1:
cfg = installed[0].client_config
return f"{COLOR_CYAN}{cfg.display_name}{RESET_FORMAT}"
return f"{COLOR_CYAN}-{RESET_FORMAT}"
def enable_mainsail_remotemode() -> None:
Logger.print_status("Enable Mainsails remote mode ...")
c_json = MainsailData().client_dir.joinpath("config.json")
with open(c_json, "r") as f:
config_data = json.load(f)
if config_data["instancesDB"] == "browser":
Logger.print_info("Remote mode already configured. Skipped ...")
return
Logger.print_status("Setting instance storage location to 'browser' ...")
config_data["instancesDB"] = "browser"
with open(c_json, "w") as f:
json.dump(config_data, f, indent=4)
Logger.print_ok("Mainsails remote mode enabled!")
def symlink_webui_nginx_log(
client: BaseWebClient, klipper_instances: List[Klipper]
) -> None:
Logger.print_status("Link NGINX logs into log directory ...")
access_log = client.nginx_access_log
error_log = client.nginx_error_log
for instance in klipper_instances:
desti_access = instance.base.log_dir.joinpath(access_log.name)
if not desti_access.exists():
desti_access.symlink_to(access_log)
desti_error = instance.base.log_dir.joinpath(error_log.name)
if not desti_error.exists():
desti_error.symlink_to(error_log)
def get_local_client_version(client: BaseWebClient) -> str | None:
relinfo_file = client.client_dir.joinpath("release_info.json")
version_file = client.client_dir.joinpath(".version")
if not client.client_dir.exists():
return None
if not relinfo_file.is_file() and not version_file.is_file():
return "n/a"
if relinfo_file.is_file():
with open(relinfo_file, "r") as f:
return str(json.load(f)["version"])
else:
with open(version_file, "r") as f:
return f.readlines()[0]
def get_remote_client_version(client: BaseWebClient) -> str | None:
try:
if (tag := get_latest_remote_tag(client.repo_path)) != "":
return str(tag)
return None
except Exception:
return None
def backup_client_data(client: BaseWebClient) -> None:
name = client.name
src = client.client_dir
dest = client.backup_dir
with open(src.joinpath(".version"), "r") as v:
version = v.readlines()[0]
bm = BackupManager()
bm.backup_directory(f"{name}-{version}", src, dest)
bm.backup_file(client.config_file, dest)
bm.backup_file(NGINX_SITES_AVAILABLE.joinpath(name), dest)
def backup_client_config_data(client: BaseWebClient) -> None:
client_config = client.client_config
name = client_config.name
source = client_config.config_dir
target = client_config.backup_dir
bm = BackupManager()
bm.backup_directory(name, source, target)
def get_existing_clients() -> List[BaseWebClient]:
clients = list(get_args(WebClientType))
installed_clients: List[BaseWebClient] = []
for client in clients:
if client.client_dir.exists():
installed_clients.append(client)
return installed_clients
def detect_client_cfg_conflict(curr_client: BaseWebClient) -> bool:
"""
Check if any other client configs are present on the system.
It is usually not harmful, but chances are they can conflict each other.
Multiple client configs are, at least, redundant to have them installed
:param curr_client: The client name to check for the conflict
:return: True, if other client configs were found, else False
"""
mainsail_cfg_status: ComponentStatus = get_client_config_status(MainsailData())
fluidd_cfg_status: ComponentStatus = get_client_config_status(FluiddData())
if curr_client.client == WebClientType.MAINSAIL and fluidd_cfg_status.status == 2:
return True
if curr_client.client == WebClientType.FLUIDD and mainsail_cfg_status.status == 2:
return True
return False
def get_download_url(base_url: str, client: BaseWebClient) -> str:
settings = KiauhSettings()
use_unstable = settings.get(client.name, "unstable_releases")
stable_url = f"{base_url}/latest/download/{client.name}.zip"
if not use_unstable:
return stable_url
try:
unstable_tag = get_latest_unstable_tag(client.repo_path)
if unstable_tag == "":
raise Exception
return f"{base_url}/download/{unstable_tag}/{client.name}.zip"
except Exception:
return stable_url
#################################################
## NGINX RELATED FUNCTIONS
#################################################
def copy_upstream_nginx_cfg() -> None:
"""
Creates an upstream.conf in /etc/nginx/conf.d
:return: None
"""
source = MODULE_PATH.joinpath("assets/upstreams.conf")
target = NGINX_CONFD.joinpath("upstreams.conf")
try:
command = ["sudo", "cp", source, target]
run(command, stderr=PIPE, check=True)
except CalledProcessError as e:
log = f"Unable to create upstreams.conf: {e.stderr.decode()}"
Logger.print_error(log)
raise
def copy_common_vars_nginx_cfg() -> None:
"""
Creates a common_vars.conf in /etc/nginx/conf.d
:return: None
"""
source = MODULE_PATH.joinpath("assets/common_vars.conf")
target = NGINX_CONFD.joinpath("common_vars.conf")
try:
command = ["sudo", "cp", source, target]
run(command, stderr=PIPE, check=True)
except CalledProcessError as e:
log = f"Unable to create upstreams.conf: {e.stderr.decode()}"
Logger.print_error(log)
raise
def generate_nginx_cfg_from_template(name: str, template_src: Path, **kwargs) -> None:
"""
Creates an NGINX config from a template file and
replaces all placeholders passed as kwargs. A placeholder must be defined
in the template file as %{placeholder}%.
:param name: name of the config to create
:param template_src: the path to the template file
:return: None
"""
tmp = Path.home().joinpath(f"{name}.tmp")
shutil.copy(template_src, tmp)
with open(tmp, "r+") as f:
content = f.read()
for key, value in kwargs.items():
content = content.replace(f"%{key}%", str(value))
f.seek(0)
f.write(content)
f.truncate()
target = NGINX_SITES_AVAILABLE.joinpath(name)
try:
command = ["sudo", "mv", tmp, target]
run(command, stderr=PIPE, check=True)
except CalledProcessError as e:
log = f"Unable to create '{target}': {e.stderr.decode()}"
Logger.print_error(log)
raise
def create_nginx_cfg(
display_name: str,
cfg_name: str,
template_src: Path,
**kwargs,
) -> None:
from utils.sys_utils import set_nginx_permissions
try:
Logger.print_status(f"Creating NGINX config for {display_name} ...")
source = NGINX_SITES_AVAILABLE.joinpath(cfg_name)
target = NGINX_SITES_ENABLED.joinpath(cfg_name)
remove_file(Path("/etc/nginx/sites-enabled/default"), True)
generate_nginx_cfg_from_template(cfg_name, template_src=template_src, **kwargs)
create_symlink(source, target, True)
set_nginx_permissions()
Logger.print_ok(f"NGINX config for {display_name} successfully created.")
except Exception:
Logger.print_error(f"Creating NGINX config for {display_name} failed!")
raise
def read_ports_from_nginx_configs() -> List[int]:
"""
Helper function to iterate over all NGINX configs and read all ports defined for listen
:return: A sorted list of listen ports
"""
if not NGINX_SITES_ENABLED.exists():
return []
port_list = []
for config in NGINX_SITES_ENABLED.iterdir():
if not config.is_file():
continue
with open(config, "r") as cfg:
lines = cfg.readlines()
for line in lines:
line = line.replace("default_server", "")
line = re.sub(r"[;:\[\]]", "", line.strip())
if line.startswith("listen") and line.split()[-1] not in port_list:
port_list.append(line.split()[-1])
ports_to_ints_list = [int(port) for port in port_list]
return sorted(ports_to_ints_list, key=lambda x: int(x))
def is_valid_port(port: int, ports_in_use: List[int]) -> bool:
return port not in ports_in_use
def get_next_free_port(ports_in_use: List[int]) -> int:
valid_ports = set(range(80, 7125))
used_ports = set(map(int, ports_in_use))
return min(valid_ports - used_ports)

View File

@@ -0,0 +1,56 @@
# ======================================================================= #
# 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
from dataclasses import dataclass
from pathlib import Path
from components.webui_client.base_data import (
BaseWebClient,
BaseWebClientConfig,
WebClientConfigType,
WebClientType,
)
from core.backup_manager import BACKUP_ROOT_DIR
@dataclass()
class FluiddConfigWeb(BaseWebClientConfig):
client_config: WebClientConfigType = WebClientConfigType.FLUIDD
name: str = client_config.value
display_name: str = name.title()
config_dir: Path = Path.home().joinpath("fluidd-config")
config_filename: str = "fluidd.cfg"
config_section: str = f"include {config_filename}"
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-config-backups")
repo_url: str = "https://github.com/fluidd-core/fluidd-config.git"
@dataclass()
class FluiddData(BaseWebClient):
BASE_DL_URL = "https://github.com/fluidd-core/fluidd/releases"
client: WebClientType = WebClientType.FLUIDD
name: str = client.value
display_name: str = name.capitalize()
client_dir: Path = Path.home().joinpath("fluidd")
config_file: Path = client_dir.joinpath("config.json")
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-backups")
repo_path: str = "fluidd-core/fluidd"
nginx_access_log: Path = Path("/var/log/nginx/fluidd-access.log")
nginx_error_log: Path = Path("/var/log/nginx/fluidd-error.log")
client_config: BaseWebClientConfig = None
download_url: str | None = None
def __post_init__(self):
from components.webui_client.client_utils import get_download_url
self.client_config = FluiddConfigWeb()
self.download_url = get_download_url(self.BASE_DL_URL, self)

View File

@@ -0,0 +1,56 @@
# ======================================================================= #
# 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
from dataclasses import dataclass
from pathlib import Path
from components.webui_client.base_data import (
BaseWebClient,
BaseWebClientConfig,
WebClientConfigType,
WebClientType,
)
from core.backup_manager import BACKUP_ROOT_DIR
@dataclass()
class MainsailConfigWeb(BaseWebClientConfig):
client_config: WebClientConfigType = WebClientConfigType.MAINSAIL
name: str = client_config.value
display_name: str = name.title()
config_dir: Path = Path.home().joinpath("mainsail-config")
config_filename: str = "mainsail.cfg"
config_section: str = f"include {config_filename}"
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-config-backups")
repo_url: str = "https://github.com/mainsail-crew/mainsail-config.git"
@dataclass()
class MainsailData(BaseWebClient):
BASE_DL_URL: str = "https://github.com/mainsail-crew/mainsail/releases"
client: WebClientType = WebClientType.MAINSAIL
name: str = WebClientType.MAINSAIL.value
display_name: str = name.capitalize()
client_dir: Path = Path.home().joinpath("mainsail")
config_file: Path = client_dir.joinpath("config.json")
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-backups")
repo_path: str = "mainsail-crew/mainsail"
nginx_access_log: Path = Path("/var/log/nginx/mainsail-access.log")
nginx_error_log: Path = Path("/var/log/nginx/mainsail-error.log")
client_config: BaseWebClientConfig = None
download_url: str | None = None
def __post_init__(self):
from components.webui_client.client_utils import get_download_url
self.client_config = MainsailConfigWeb()
self.download_url = get_download_url(self.BASE_DL_URL, self)

View File

@@ -0,0 +1,126 @@
# ======================================================================= #
# 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 textwrap
from typing import Type
from components.webui_client import client_remove
from components.webui_client.base_data import BaseWebClient
from core.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT
from core.menus import Option
from core.menus.base_menu import BaseMenu
# noinspection PyUnusedLocal
class ClientRemoveMenu(BaseMenu):
def __init__(
self, client: BaseWebClient, previous_menu: Type[BaseMenu] | None = None
):
super().__init__()
self.previous_menu: Type[BaseMenu] | None = previous_menu
self.client: BaseWebClient = client
self.remove_client: bool = False
self.remove_client_cfg: bool = False
self.backup_config_json: bool = False
self.selection_state: bool = False
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.remove_menu import RemoveMenu
self.previous_menu = previous_menu if previous_menu is not None else RemoveMenu
def set_options(self) -> None:
self.options = {
"a": Option(method=self.toggle_all),
"1": Option(method=self.toggle_rm_client),
"2": Option(method=self.toggle_rm_client_config),
"3": Option(method=self.toggle_backup_config_json),
"c": Option(method=self.run_removal_process),
}
def print_menu(self) -> None:
client_name = self.client.display_name
client_config = self.client.client_config
client_config_name = client_config.display_name
header = f" [ Remove {client_name} ] "
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
unchecked = "[ ]"
o1 = checked if self.remove_client else unchecked
o2 = checked if self.remove_client_cfg else unchecked
o3 = checked if self.backup_config_json else unchecked
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
║ Enter a number and hit enter to select / deselect ║
║ the specific option for removal. ║
╟───────────────────────────────────────────────────────╢
║ a) {self._get_selection_state_str():37}
╟───────────────────────────────────────────────────────╢
║ 1) {o1} Remove {client_name:16}
║ 2) {o2} Remove {client_config_name:24}
║ 3) {o3} Backup config.json ║
╟───────────────────────────────────────────────────────╢
║ C) Continue ║
╟───────────────────────────────────────────────────────╢
"""
)[1:]
print(menu, end="")
def toggle_all(self, **kwargs) -> None:
self.selection_state = not self.selection_state
self.remove_client = self.selection_state
self.remove_client_cfg = self.selection_state
self.backup_config_json = self.selection_state
def toggle_rm_client(self, **kwargs) -> None:
self.remove_client = not self.remove_client
def toggle_rm_client_config(self, **kwargs) -> None:
self.remove_client_cfg = not self.remove_client_cfg
def toggle_backup_config_json(self, **kwargs) -> None:
self.backup_config_json = not self.backup_config_json
def run_removal_process(self, **kwargs) -> None:
if (
not self.remove_client
and not self.remove_client_cfg
and not self.backup_config_json
):
error = f"{COLOR_RED}Nothing selected ...{RESET_FORMAT}"
print(error)
return
client_remove.run_client_removal(
client=self.client,
remove_client=self.remove_client,
remove_client_cfg=self.remove_client_cfg,
backup_config=self.backup_config_json,
)
self.remove_client = False
self.remove_client_cfg = False
self.backup_config_json = False
self._go_back()
def _get_selection_state_str(self) -> str:
return (
"Select everything" if not self.selection_state else "Deselect everything"
)
def _go_back(self, **kwargs) -> None:
if self.previous_menu is not None:
self.previous_menu().run()