From a80f0bb0e822e46886c16c1c16ae13401e95c185 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 14:42:53 +0100 Subject: [PATCH] feat(utils): add several util methods Signed-off-by: Dominik Willner --- kiauh/utils/__init__.py | 7 ++ kiauh/utils/res/common_vars.conf | 6 ++ kiauh/utils/res/nginx_cfg | 96 +++++++++++++++++ kiauh/utils/res/upstreams.conf | 25 +++++ kiauh/utils/system_utils.py | 179 ++++++++++++++++++++++++++++++- 5 files changed, 310 insertions(+), 3 deletions(-) create mode 100644 kiauh/utils/res/common_vars.conf create mode 100644 kiauh/utils/res/nginx_cfg create mode 100644 kiauh/utils/res/upstreams.conf diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py index 203c66a..108b521 100644 --- a/kiauh/utils/__init__.py +++ b/kiauh/utils/__init__.py @@ -8,5 +8,12 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import os +MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) INVALID_CHOICE = "Invalid choice. Please select a valid value." + +# ================== NGINX =====================# +NGINX_SITES_AVAILABLE = "/etc/nginx/sites-available" +NGINX_SITES_ENABLED = "/etc/nginx/sites-enabled" +NGINX_CONFD = "/etc/nginx/conf.d" diff --git a/kiauh/utils/res/common_vars.conf b/kiauh/utils/res/common_vars.conf new file mode 100644 index 0000000..9c3f85e --- /dev/null +++ b/kiauh/utils/res/common_vars.conf @@ -0,0 +1,6 @@ +# /etc/nginx/conf.d/common_vars.conf + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} \ No newline at end of file diff --git a/kiauh/utils/res/nginx_cfg b/kiauh/utils/res/nginx_cfg new file mode 100644 index 0000000..6303674 --- /dev/null +++ b/kiauh/utils/res/nginx_cfg @@ -0,0 +1,96 @@ +# /etc/nginx/sites-available/%NAME% + +server { + 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; + proxy_read_timeout 600; + } + + 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/; + } +} diff --git a/kiauh/utils/res/upstreams.conf b/kiauh/utils/res/upstreams.conf new file mode 100644 index 0000000..d04e04a --- /dev/null +++ b/kiauh/utils/res/upstreams.conf @@ -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; +} \ No newline at end of file diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 9b6fbce..f14fbb2 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -18,8 +18,15 @@ import time import urllib.error import urllib.request from pathlib import Path -from typing import List +from typing import List, Literal +from zipfile import ZipFile +from kiauh.utils import ( + NGINX_CONFD, + MODULE_PATH, + NGINX_SITES_AVAILABLE, + NGINX_SITES_ENABLED, +) from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger @@ -273,11 +280,22 @@ def get_ipv4_addr() -> str: s.close() -def download_file(url: str, target_folder: str, target_name: str, show_progress=True): +def download_file( + url: str, target_folder: str, target_name: str, show_progress=True +) -> None: + """ + Helper method for downloading files from a provided URL | + :param url: the url to the file + :param target_folder: the target folder to download the file into + :param target_name: the name of the downloaded file + :param show_progress: show download progress or not + :return: None + """ target_path = os.path.join(target_folder, target_name) try: if show_progress: urllib.request.urlretrieve(url, target_path, download_progress) + sys.stdout.write("\n") else: urllib.request.urlretrieve(url, target_path) except urllib.error.HTTPError as e: @@ -291,7 +309,14 @@ def download_file(url: str, target_folder: str, target_name: str, show_progress= raise -def download_progress(block_num, block_size, total_size): +def download_progress(block_num, block_size, total_size) -> None: + """ + Reporthook method for urllib.request.urlretrieve() method call in download_file() | + :param block_num: + :param block_size: + :param total_size: total filesize in bytes + :return: None + """ downloaded = block_num * block_size percent = 100 if downloaded >= total_size else downloaded / total_size * 100 mb = 1024 * 1024 @@ -300,3 +325,151 @@ def download_progress(block_num, block_size, total_size): dl = f"\rDownloading: [{'#' * progress}{remaining}]{percent:.2f}% ({downloaded/mb:.2f}/{total_size/mb:.2f}MB)" sys.stdout.write(dl) sys.stdout.flush() + + +def unzip(file: str, target_dir: str) -> None: + """ + Helper function to unzip a zip-archive into a target directory | + :param file: the zip-file to unzip + :param target_dir: the target directory to extract the files into + :return: None + """ + with ZipFile(file, "r") as _zip: + _zip.extractall(target_dir) + + +def create_upstream_nginx_cfg() -> None: + """ + Creates an upstream.conf in /etc/nginx/conf.d + :return: None + """ + source = os.path.join(MODULE_PATH, "res", "upstreams.conf") + target = os.path.join(NGINX_CONFD, "upstreams.conf") + try: + command = ["sudo", "cp", source, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create upstreams.conf: {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def create_common_vars_nginx_cfg() -> None: + """ + Creates a common_vars.conf in /etc/nginx/conf.d + :return: None + """ + source = os.path.join(MODULE_PATH, "res", "common_vars.conf") + target = os.path.join(NGINX_CONFD, "common_vars.conf") + try: + command = ["sudo", "cp", source, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create upstreams.conf: {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def create_nginx_cfg(name: str, port: int, root_dir: str) -> None: + """ + Creates an NGINX config from a template file and replaces all placeholders + :param name: name of the config to create + :param port: listen port + :param root_dir: directory of the static files + :return: None + """ + tmp = f"{Path.home()}/{name}.tmp" + shutil.copy(os.path.join(MODULE_PATH, "res", "nginx_cfg"), tmp) + with open(tmp, "r+") as f: + content = f.read() + content = content.replace("%NAME%", name) + content = content.replace("%PORT%", str(port)) + content = content.replace("%ROOT_DIR%", root_dir) + f.seek(0) + f.write(content) + f.truncate() + + target = os.path.join(NGINX_SITES_AVAILABLE, name) + try: + command = ["sudo", "mv", tmp, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create '{target}': {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def delete_default_nginx_cfg() -> None: + """ + Deletes a default NGINX config + :return: None + """ + default_cfg = Path("/etc/nginx/sites-enabled/default") + if not check_file_exists(default_cfg): + return + + try: + command = ["sudo", "rm", default_cfg] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to delete '{default_cfg}': {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def enable_nginx_cfg(name: str) -> None: + """ + Helper method to enable an NGINX config | + :param name: name of the config to enable + :return: None + """ + source = os.path.join(NGINX_SITES_AVAILABLE, name) + target = os.path.join(NGINX_SITES_ENABLED, name) + if check_file_exists(Path(target)): + return + + try: + command = ["sudo", "ln", "-s", source, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create symlink: {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def set_nginx_permissions() -> None: + """ + Check if permissions of the users home directory + grant execution rights to group and other and set them if not set. + Required permissions for NGINX to be able to serve Mainsail/Fluidd. + This seems to have become necessary with Ubuntu 21+. | + :return: None + """ + cmd1 = f"ls -ld {Path.home()} | cut -d' ' -f1" + homedir_perm = subprocess.run(cmd1, shell=True, stdout=subprocess.PIPE, text=True) + homedir_perm = homedir_perm.stdout + + if homedir_perm.count("x") < 3: + Logger.print_status("Granting NGINX the required permissions ...") + subprocess.run(["chmod", "og+x", Path.home()]) + Logger.print_ok("Permissions granted.") + + +def control_systemd_service( + name: str, action: Literal["start", "stop", "restart", "disable"] +) -> None: + """ + Helper method to execute several actions for a specific systemd service. | + :param name: the service name + :param action: Either "start", "stop", "restart" or "disable" + :return: None + """ + try: + Logger.print_status(f"{action.capitalize()} {name}.service ...") + command = ["sudo", "systemctl", action, f"{name}.service"] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + Logger.print_ok(f"OK!") + except subprocess.CalledProcessError as e: + log = f"Failed to {action} {name}.service: {e.stderr.decode()}" + Logger.print_error(log) + raise