From e8f392c3f12013d399411d78469b606e9ee03173 Mon Sep 17 00:00:00 2001 From: Daniel Markstedt Date: Sat, 26 Feb 2022 21:46:35 -0800 Subject: [PATCH] Create SysCmds common class, and refactor Python codebase (#697) * Move the oled script's PiCmds module to common, and rename it SysCmds. * Use sys_cmds.get_ip_and_host() in web UI code. * Move the auth_active() method to device_utils * Rename device_utils to web_utils. Make auth_active() method take the group as argument. * Migrate all pi_cmds methods to the SysCmds common class. * Display hostname and ip in Web UI. * Resolve or suppress pylint warnings. * Resolve a pylint warning. * Resolve or suppress pylint warnings. * Import libraries at the top level for readability. In my testing on a Pi3B+, this leads to ~1.5k more memory being used by the python3 process. * Change page title as requested by akuker. * Reenable the import-outside-toplevel pylint rule. * Resolve pylint warnings. * Fix error following refactoring. * Minor UI tweaks. * Cleanup. * Break out bridge config validation into a utility method. * Move the dropzonejs method into the web_utils package * Move get_logs method into SysCmds class. * Improve get logs UI. * Resolve pylint warning. * Standardize class instance name. --- python/.pylintrc | 3 +- python/common/src/rascsi/file_cmds.py | 28 +-- python/common/src/rascsi/ractl_cmds.py | 2 +- python/common/src/rascsi/socket_cmds.py | 5 +- python/common/src/rascsi/sys_cmds.py | 170 ++++++++++++++ python/oled/src/pi_cmds.py | 21 -- python/oled/src/rascsi_oled_monitor.py | 7 +- python/web/src/device_utils.py | 90 -------- python/web/src/pi_cmds.py | 156 ------------- python/web/src/return_code_mapper.py | 5 +- python/web/src/templates/base.html | 17 +- python/web/src/templates/index.html | 11 +- python/web/src/web.py | 283 +++++++++--------------- python/web/src/web_utils.py | 183 +++++++++++++++ 14 files changed, 502 insertions(+), 479 deletions(-) create mode 100644 python/common/src/rascsi/sys_cmds.py delete mode 100644 python/oled/src/pi_cmds.py delete mode 100644 python/web/src/device_utils.py delete mode 100644 python/web/src/pi_cmds.py create mode 100644 python/web/src/web_utils.py diff --git a/python/.pylintrc b/python/.pylintrc index b1a8aa6b..99c84cb5 100644 --- a/python/.pylintrc +++ b/python/.pylintrc @@ -140,8 +140,7 @@ disable=print-statement, xreadlines-attribute, deprecated-sys-function, exception-escape, - comprehension-escape, - import-outside-toplevel + comprehension-escape # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/python/common/src/rascsi/file_cmds.py b/python/common/src/rascsi/file_cmds.py index b3e493ec..bfc71af6 100644 --- a/python/common/src/rascsi/file_cmds.py +++ b/python/common/src/rascsi/file_cmds.py @@ -4,8 +4,16 @@ Module for methods reading from and writing to the file system import os import logging -from pathlib import PurePath import asyncio +from pathlib import PurePath +from zipfile import ZipFile, is_zipfile +from re import escape, findall +from time import time +from subprocess import run, CalledProcessError +from json import dump, load + +import requests + import rascsi_interface_pb2 as proto from rascsi.common_settings import CFG_DIR, CONFIG_FILE_SUFFIX, PROPERTIES_SUFFIX, RESERVATIONS from rascsi.ractl_cmds import RaCtlCmds @@ -79,7 +87,6 @@ class FileCmds: prop_data = self.list_files(PROPERTIES_SUFFIX, CFG_DIR) prop_files = [PurePath(x[0]).stem for x in prop_data] - from zipfile import ZipFile, is_zipfile server_info = self.ractl.get_server_info() files = [] for file in result.image_files_info.image_files: @@ -225,12 +232,11 @@ class FileCmds: members contains all of the full paths to each of the zip archive members Returns (dict) with (boolean) status and (list of str) msg """ - from asyncio import run server_info = self.ractl.get_server_info() prop_flag = False if not member: - unzip_proc = run(self.run_async( + unzip_proc = asyncio.run(self.run_async( f"unzip -d {server_info['image_dir']} -n -j " f"{server_info['image_dir']}/{file_name}" )) @@ -241,14 +247,13 @@ class FileCmds: self.rename_file(f"{server_info['image_dir']}/{name}", f"{CFG_DIR}/{name}") prop_flag = True else: - from re import escape member = escape(member) - unzip_proc = run(self.run_async( + unzip_proc = asyncio.run(self.run_async( f"unzip -d {server_info['image_dir']} -n -j " f"{server_info['image_dir']}/{file_name} {member}" )) # Attempt to unzip a properties file in the same archive dir - unzip_prop = run(self.run_async( + unzip_prop = asyncio.run(self.run_async( f"unzip -d {CFG_DIR} -n -j " f"{server_info['image_dir']}/{file_name} {member}.{PROPERTIES_SUFFIX}" )) @@ -258,7 +263,6 @@ class FileCmds: logging.warning("Unzipping failed: %s", unzip_proc["stderr"]) return {"status": False, "msg": unzip_proc["stderr"]} - from re import findall unzipped = findall( "(?:inflating|extracting):(.+)\n", unzip_proc["stdout"] @@ -270,8 +274,6 @@ class FileCmds: Takes (str) url and one or more (str) *iso_args Returns (dict) with (bool) status and (str) msg """ - from time import time - from subprocess import run, CalledProcessError server_info = self.ractl.get_server_info() @@ -287,7 +289,6 @@ class FileCmds: if not req_proc["status"]: return {"status": False, "msg": req_proc["msg"]} - from zipfile import is_zipfile, ZipFile if is_zipfile(tmp_full_path): if "XtraStuf.mac" in str(ZipFile(tmp_full_path).namelist()): logging.info("MacZip file format detected. Will not unzip to retain resource fork.") @@ -339,7 +340,6 @@ class FileCmds: Takes (str) url, (str) save_dir, (str) file_name Returns (dict) with (bool) status and (str) msg """ - import requests logging.info("Making a request to download %s", url) try: @@ -371,7 +371,6 @@ class FileCmds: Takes (str) file_name Returns (dict) with (bool) status and (str) msg """ - from json import dump file_name = f"{CFG_DIR}/{file_name}" try: with open(file_name, "w", encoding="ISO-8859-1") as json_file: @@ -433,7 +432,6 @@ class FileCmds: Takes (str) file_name Returns (dict) with (bool) status and (str) msg """ - from json import load file_name = f"{CFG_DIR}/{file_name}" try: with open(file_name, encoding="ISO-8859-1") as json_file: @@ -512,7 +510,6 @@ class FileCmds: Takes file name base (str) and (list of dicts) conf as arguments Returns (dict) with (bool) status and (str) msg """ - from json import dump file_path = f"{CFG_DIR}/{file_name}" try: with open(file_path, "w") as json_file: @@ -548,7 +545,6 @@ class FileCmds: Takes (str) file_path as argument. Returns (dict) with (bool) status, (str) msg, (dict) conf """ - from json import load try: with open(file_path) as json_file: conf = load(json_file) diff --git a/python/common/src/rascsi/ractl_cmds.py b/python/common/src/rascsi/ractl_cmds.py index 3e0576fe..cd0dc2bb 100644 --- a/python/common/src/rascsi/ractl_cmds.py +++ b/python/common/src/rascsi/ractl_cmds.py @@ -162,7 +162,7 @@ class RaCtlCmds: def get_disk_device_types(self): """ - Returns a (list) of (str) of four letter device acronyms + Returns a (list) of (str) of four letter device acronyms that take image files as arguments. """ device_types = self.get_device_types() diff --git a/python/common/src/rascsi/socket_cmds.py b/python/common/src/rascsi/socket_cmds.py index ea3b867f..5b5a7f04 100644 --- a/python/common/src/rascsi/socket_cmds.py +++ b/python/common/src/rascsi/socket_cmds.py @@ -3,7 +3,10 @@ Module for sending and receiving data over a socket connection with the RaSCSI b """ import logging +import socket from time import sleep +from struct import pack, unpack + from rascsi.exceptions import (EmptySocketChunkException, InvalidProtobufResponse, FailedSocketConnectionException) @@ -27,7 +30,6 @@ class SocketCmds: tries = 20 error_msg = "" - import socket while counter < tries: try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: @@ -56,7 +58,6 @@ class SocketCmds: Tries to extract and interpret the protobuf header to get response size. Reads data from socket in 2048 bytes chunks until all data is received. """ - from struct import pack, unpack # Sending the magic word "RASCSI" to authenticate with the server sock.send(b"RASCSI") diff --git a/python/common/src/rascsi/sys_cmds.py b/python/common/src/rascsi/sys_cmds.py new file mode 100644 index 00000000..7fd79e8d --- /dev/null +++ b/python/common/src/rascsi/sys_cmds.py @@ -0,0 +1,170 @@ +""" +Module with methods that interact with the Pi system +""" +import subprocess +import logging +from subprocess import run +from shutil import disk_usage +from re import findall, match +from socket import socket, gethostname, AF_INET, SOCK_DGRAM + + +class SysCmds: + """ + Class for commands sent to the Pi's Linux system. + """ + + @staticmethod + def running_env(): + """ + Returns (str) git and (str) env + git contains the git hash of the checked out code + env is the various system information where this app is running + """ + try: + ra_git_version = ( + subprocess.run( + ["git", "rev-parse", "HEAD"], + capture_output=True, + check=True, + ) + .stdout.decode("utf-8") + .strip() + ) + except subprocess.CalledProcessError as error: + logging.warning("Executed shell command: %s", " ".join(error.cmd)) + logging.warning("Got error: %s", error.stderr.decode("utf-8")) + ra_git_version = "" + + try: + pi_version = ( + subprocess.run( + ["uname", "-a"], + capture_output=True, + check=True, + ) + .stdout.decode("utf-8") + .strip() + ) + except subprocess.CalledProcessError as error: + logging.warning("Executed shell command: %s", " ".join(error.cmd)) + logging.warning("Got error: %s", error.stderr.decode("utf-8")) + pi_version = "Unknown" + + return {"git": ra_git_version, "env": pi_version} + + @staticmethod + def running_proc(daemon): + """ + Takes (str) daemon + Returns (int) proc, which is the number of processes currently running + """ + try: + processes = ( + subprocess.run( + ["ps", "aux"], + capture_output=True, + check=True, + ) + .stdout.decode("utf-8") + .strip() + ) + except subprocess.CalledProcessError as error: + logging.warning("Executed shell command: %s", " ".join(error.cmd)) + logging.warning("Got error: %s", error.stderr.decode("utf-8")) + processes = "" + + matching_processes = findall(daemon, processes) + return len(matching_processes) + + @staticmethod + def is_bridge_setup(): + """ + Returns (bool) True if the rascsi_bridge network interface exists + """ + try: + bridges = ( + subprocess.run( + ["brctl", "show"], + capture_output=True, + check=True, + ) + .stdout.decode("utf-8") + .strip() + ) + except subprocess.CalledProcessError as error: + logging.warning("Executed shell command: %s", " ".join(error.cmd)) + logging.warning("Got error: %s", error.stderr.decode("utf-8")) + bridges = "" + + if "rascsi_bridge" in bridges: + return True + return False + + @staticmethod + def disk_space(): + """ + Returns a (dict) with (int) total (int) used (int) free + This is the disk space information of the volume where this app is running + """ + total, used, free = disk_usage(__file__) + return {"total": total, "used": used, "free": free} + + @staticmethod + def introspect_file(file_path, re_term): + """ + Takes a (str) file_path and (str) re_term in regex format + Will introspect file_path for the existance of re_term + and return True if found, False if not found + """ + try: + ifile = open(file_path, "r", encoding="ISO-8859-1") + except (IOError, ValueError, EOFError, TypeError) as error: + logging.error(str(error)) + return False + for line in ifile: + if match(re_term, line): + return True + return False + + # pylint: disable=broad-except + @staticmethod + def get_ip_and_host(): + """ + Use a mock socket connection to identify the Pi's hostname and IP address + """ + host = gethostname() + sock = socket(AF_INET, SOCK_DGRAM) + try: + # mock ip address; doesn't have to be reachable + sock.connect(('10.255.255.255', 1)) + ip_addr = sock.getsockname()[0] + except Exception: + ip_addr = False + finally: + sock.close() + return ip_addr, host + + @staticmethod + def get_logs(lines, scope): + """ + Takes (int) lines and (str) scope. + Returns either the decoded log output, or the stderr output of journalctl. + """ + if scope != "all": + process = run( + ["journalctl", "-n", lines, "-u", scope], + capture_output=True, + check=True, + ) + else: + process = run( + ["journalctl", "-n", lines], + capture_output=True, + check=True, + ) + + if process.returncode == 0: + return process.returncode, process.stdout.decode("utf-8") + + return process.returncode, process.stderr.decode("utf-8") diff --git a/python/oled/src/pi_cmds.py b/python/oled/src/pi_cmds.py deleted file mode 100644 index a33913db..00000000 --- a/python/oled/src/pi_cmds.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -Module with methods that interact with the Pi's Linux system -""" - - -def get_ip_and_host(): - """ - Use a mock socket connection to identify the Pi's hostname and IP address - """ - from socket import socket, gethostname, AF_INET, SOCK_DGRAM - host = gethostname() - sock = socket(AF_INET, SOCK_DGRAM) - try: - # mock ip address; doesn't have to be reachable - sock.connect(('10.255.255.255', 1)) - ip_addr = sock.getsockname()[0] - except Exception: - ip_addr = False - finally: - sock.close() - return ip_addr, host diff --git a/python/oled/src/rascsi_oled_monitor.py b/python/oled/src/rascsi_oled_monitor.py index 80d0e86d..3201e768 100755 --- a/python/oled/src/rascsi_oled_monitor.py +++ b/python/oled/src/rascsi_oled_monitor.py @@ -39,9 +39,9 @@ from board import I2C from adafruit_ssd1306 import SSD1306_I2C from PIL import Image, ImageDraw, ImageFont from interrupt_handler import GracefulInterruptHandler -from pi_cmds import get_ip_and_host from rascsi.ractl_cmds import RaCtlCmds from rascsi.socket_cmds import SocketCmds +from rascsi.sys_cmds import SysCmds parser = argparse.ArgumentParser(description="RaSCSI OLED Monitor script") parser.add_argument( @@ -99,6 +99,7 @@ TOKEN = args.password sock_cmd = SocketCmds(host=args.rascsi_host, port=args.rascsi_port) ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=TOKEN) +sys_cmd = SysCmds() WIDTH = 128 BORDER = 5 @@ -159,7 +160,7 @@ LINE_SPACING = 8 # Some other nice fonts to try: http://www.dafont.com/bitmap.php FONT = ImageFont.truetype('resources/type_writer.ttf', FONT_SIZE) -IP_ADDR, HOSTNAME = get_ip_and_host() +IP_ADDR, HOSTNAME = sys_cmd.get_ip_and_host() REMOVABLE_DEVICE_TYPES = ractl_cmd.get_removable_device_types() PERIPHERAL_DEVICE_TYPES = ractl_cmd.get_peripheral_device_types() @@ -183,7 +184,7 @@ def formatted_output(): else: output.append(f"{line['id']} {line['device_type'][2:4]} {line['status']}") # Special handling of devices that don't use image files - elif line["device_type"] in (PERIPHERAL_DEVICE_TYPES): + elif line["device_type"] in PERIPHERAL_DEVICE_TYPES: if line["vendor"] == "RaSCSI": output.append(f"{line['id']} {line['device_type'][2:4]} {line['product']}") else: diff --git a/python/web/src/device_utils.py b/python/web/src/device_utils.py deleted file mode 100644 index 0d1d6cf8..00000000 --- a/python/web/src/device_utils.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Module for RaSCSI device management utility methods -""" - -from flask_babel import _ - - -def get_valid_scsi_ids(devices, reserved_ids): - """ - Takes a list of (dict)s devices, and list of (int)s reserved_ids. - Returns: - - (list) of (int)s valid_ids, which are the SCSI ids that are not reserved - - (int) recommended_id, which is the id that the Web UI should default to recommend - """ - occupied_ids = [] - for device in devices: - occupied_ids.append(device["id"]) - - unoccupied_ids = [i for i in list(range(8)) if i not in reserved_ids + occupied_ids] - unoccupied_ids.sort() - valid_ids = [i for i in list(range(8)) if i not in reserved_ids] - valid_ids.sort(reverse=True) - - if unoccupied_ids: - recommended_id = unoccupied_ids[-1] - else: - recommended_id = occupied_ids.pop(0) - - return valid_ids, recommended_id - - -def sort_and_format_devices(devices): - """ - Takes a (list) of (dict)s devices and returns a (list) of (dict)s. - Sorts by SCSI ID acending (0 to 7). - For SCSI IDs where no device is attached, inject a (dict) with placeholder text. - """ - occupied_ids = [] - for device in devices: - occupied_ids.append(device["id"]) - - formatted_devices = devices - - # Add padding devices and sort the list - for i in range(8): - if i not in occupied_ids: - formatted_devices.append({"id": i, "device_type": "-", \ - "status": "-", "file": "-", "product": "-"}) - # Sort list of devices by id - formatted_devices.sort(key=lambda dic: str(dic["id"])) - - return formatted_devices - - -def map_device_types_and_names(device_types): - """ - Takes a (dict) corresponding to the data structure returned by RaCtlCmds.get_device_types() - Returns a (dict) of device_type:device_name mappings of localized device names - """ - for key, value in device_types.items(): - device_types[key]["name"] = get_device_name(key) - - return device_types - - -def get_device_name(device_type): - """ - Takes a four letter device acronym (str) device_type. - Returns the human-readable name for the device type. - """ - if device_type == "SAHD": - return _("SASI Hard Disk") - elif device_type == "SCHD": - return _("SCSI Hard Disk") - elif device_type == "SCRM": - return _("Removable Disk") - elif device_type == "SCMO": - return _("Magneto-Optical") - elif device_type == "SCCD": - return _("CD / DVD") - elif device_type == "SCBR": - return _("X68000 Host Bridge") - elif device_type == "SCDP": - return _("DaynaPORT SCSI/Link") - elif device_type == "SCLP": - return _("Printer") - elif device_type == "SCHS": - return _("Host Services") - else: - return device_type diff --git a/python/web/src/pi_cmds.py b/python/web/src/pi_cmds.py deleted file mode 100644 index 55fe4d86..00000000 --- a/python/web/src/pi_cmds.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Module for methods controlling and getting information about the Pi's Linux system -""" - -import subprocess -import logging -from flask_babel import _ -from settings import AUTH_GROUP - - -def running_env(): - """ - Returns (str) git and (str) env - git contains the git hash of the checked out code - env is the various system information where this app is running - """ - try: - ra_git_version = ( - subprocess.run( - ["git", "rev-parse", "HEAD"], - capture_output=True, - check=True, - ) - .stdout.decode("utf-8") - .strip() - ) - except subprocess.CalledProcessError as error: - logging.warning("Executed shell command: %s", " ".join(error.cmd)) - logging.warning("Got error: %s", error.stderr.decode("utf-8")) - ra_git_version = "" - - try: - pi_version = ( - subprocess.run( - ["uname", "-a"], - capture_output=True, - check=True, - ) - .stdout.decode("utf-8") - .strip() - ) - except subprocess.CalledProcessError as error: - logging.warning("Executed shell command: %s", " ".join(error.cmd)) - logging.warning("Got error: %s", error.stderr.decode("utf-8")) - pi_version = "Unknown" - - return {"git": ra_git_version, "env": pi_version} - - -def running_proc(daemon): - """ - Takes (str) daemon - Returns (int) proc, which is the number of processes currently running - """ - try: - processes = ( - subprocess.run( - ["ps", "aux"], - capture_output=True, - check=True, - ) - .stdout.decode("utf-8") - .strip() - ) - except subprocess.CalledProcessError as error: - logging.warning("Executed shell command: %s", " ".join(error.cmd)) - logging.warning("Got error: %s", error.stderr.decode("utf-8")) - processes = "" - - from re import findall - matching_processes = findall(daemon, processes) - return len(matching_processes) - - -def is_bridge_setup(): - """ - Returns (bool) True if the rascsi_bridge network interface exists - """ - try: - bridges = ( - subprocess.run( - ["brctl", "show"], - capture_output=True, - check=True, - ) - .stdout.decode("utf-8") - .strip() - ) - except subprocess.CalledProcessError as error: - logging.warning("Executed shell command: %s", " ".join(error.cmd)) - logging.warning("Got error: %s", error.stderr.decode("utf-8")) - bridges = "" - - if "rascsi_bridge" in bridges: - return True - return False - - -def disk_space(): - """ - Returns a (dict) with (int) total (int) used (int) free - This is the disk space information of the volume where this app is running - """ - from shutil import disk_usage - total, used, free = disk_usage(__file__) - return {"total": total, "used": used, "free": free} - - -def get_ip_address(): - """ - Use a mock socket connection to identify the Pi's IP address - """ - from socket import socket, AF_INET, SOCK_DGRAM - sock = socket(AF_INET, SOCK_DGRAM) - try: - # mock ip address; doesn't have to be reachable - sock.connect(('10.255.255.255', 1)) - ip_addr = sock.getsockname()[0] - except Exception: - ip_addr = '127.0.0.1' - finally: - sock.close() - return ip_addr - - -def introspect_file(file_path, re_term): - """ - Takes a (str) file_path and (str) re_term in regex format - Will introspect file_path for the existance of re_term - and return True if found, False if not found - """ - from re import match - try: - ifile = open(file_path, "r", encoding="ISO-8859-1") - except: - return False - for line in ifile: - if match(re_term, line): - return True - return False - - -def auth_active(): - """ - Inspects if the group defined in AUTH_GROUP exists on the system. - If it exists, tell the webapp to enable authentication. - Returns a (dict) with (bool) status and (str) msg - """ - from grp import getgrall - groups = [g.gr_name for g in getgrall()] - if AUTH_GROUP in groups: - return { - "status": True, - "msg": _("You must log in to use this function"), - } - return {"status": False, "msg": ""} diff --git a/python/web/src/return_code_mapper.py b/python/web/src/return_code_mapper.py index a25a3f11..d42fad93 100644 --- a/python/web/src/return_code_mapper.py +++ b/python/web/src/return_code_mapper.py @@ -41,5 +41,8 @@ class ReturnCodeMapper: parameters = payload["parameters"] - payload["msg"] = lazy_gettext(ReturnCodeMapper.MESSAGES[payload["return_code"]], **parameters) + payload["msg"] = lazy_gettext( + ReturnCodeMapper.MESSAGES[payload["return_code"]], + **parameters, + ) return payload diff --git a/python/web/src/templates/base.html b/python/web/src/templates/base.html index da316687..9871e8d8 100644 --- a/python/web/src/templates/base.html +++ b/python/web/src/templates/base.html @@ -1,7 +1,7 @@ - {{ _("RaSCSI Control Page") }} + {{ _("RaSCSI Control Page") }} [{{ host }}] @@ -52,22 +52,27 @@
{{ _("Log In to Use Web Interface") }}
- + {% endif %} {% else %} {{ _("Web Interface Authentication Disabled") }} – {{ _("See Wiki for more information", url="https://github.com/akuker/RASCSI/wiki/Web-Interface#enable-authentication") }} {% endif %} - +
- - + + + +
+
-

RaSCSI - 68kmla Edition

+

{{ _("RaSCSI Control Page") }}

+ hostname: {{ host }} ip: {{ ip_addr }} +
diff --git a/python/web/src/templates/index.html b/python/web/src/templates/index.html index 43d0a328..017c2dfa 100644 --- a/python/web/src/templates/index.html +++ b/python/web/src/templates/index.html @@ -65,7 +65,7 @@ + diff --git a/python/web/src/web.py b/python/web/src/web.py index 59a89862..27001f2c 100644 --- a/python/web/src/web.py +++ b/python/web/src/web.py @@ -6,6 +6,13 @@ import logging import argparse from pathlib import Path from functools import wraps +from grp import getgrall +from ast import literal_eval + +import bjoern +from werkzeug.utils import secure_filename +from simplepam import authenticate +from flask_babel import Babel, Locale, refresh, _ from flask import ( Flask, @@ -20,26 +27,30 @@ from flask import ( session, abort, ) -from flask_babel import Babel, Locale, refresh, _ -from pi_cmds import ( - running_env, - running_proc, - is_bridge_setup, - disk_space, - get_ip_address, - introspect_file, - auth_active, +from rascsi.ractl_cmds import RaCtlCmds +from rascsi.file_cmds import FileCmds +from rascsi.sys_cmds import SysCmds + +from rascsi.common_settings import ( + CFG_DIR, + CONFIG_FILE_SUFFIX, + PROPERTIES_SUFFIX, + RESERVATIONS, ) -from device_utils import ( +from return_code_mapper import ReturnCodeMapper +from socket_cmds_flask import SocketCmdsFlask + +from web_utils import ( sort_and_format_devices, get_valid_scsi_ids, map_device_types_and_names, get_device_name, + auth_active, + is_bridge_configured, + upload_with_dropzonejs, ) -from return_code_mapper import ReturnCodeMapper - from settings import ( AFP_DIR, MAX_FILE_SIZE, @@ -50,17 +61,6 @@ from settings import ( LANGUAGES, ) -from rascsi.common_settings import ( - CFG_DIR, - CONFIG_FILE_SUFFIX, - PROPERTIES_SUFFIX, - RESERVATIONS, -) -from rascsi.ractl_cmds import RaCtlCmds -from rascsi.file_cmds import FileCmds - -from socket_cmds_flask import SocketCmdsFlask - APP = Flask(__name__) BABEL = Babel(APP) @@ -93,12 +93,13 @@ def get_supported_locales(): return sorted_locales +# pylint: disable=too-many-locals @APP.route("/") def index(): """ Sets up data structures for and renders the index page """ - if not ractl.is_token_auth()["status"] and not APP.config["TOKEN"]: + if not ractl_cmd.is_token_auth()["status"] and not APP.config["TOKEN"]: abort( 403, _( @@ -107,11 +108,12 @@ def index(): ), ) - server_info = ractl.get_server_info() - devices = ractl.list_devices() - device_types = map_device_types_and_names(ractl.get_device_types()["device_types"]) - image_files = file_cmds.list_images() - config_files = file_cmds.list_config_files() + server_info = ractl_cmd.get_server_info() + devices = ractl_cmd.list_devices() + device_types = map_device_types_and_names(ractl_cmd.get_device_types()["device_types"]) + image_files = file_cmd.list_images() + config_files = file_cmd.list_config_files() + ip_addr, host = sys_cmd.get_ip_and_host() extended_image_files = [] for image in image_files["files"]: @@ -147,10 +149,11 @@ def index(): return render_template( "index.html", locales=get_supported_locales(), - bridge_configured=is_bridge_setup(), - netatalk_configured=running_proc("afpd"), - macproxy_configured=running_proc("macproxy"), - ip_addr=get_ip_address(), + bridge_configured=sys_cmd.is_bridge_setup(), + netatalk_configured=sys_cmd.running_proc("afpd"), + macproxy_configured=sys_cmd.running_proc("macproxy"), + ip_addr=ip_addr, + host=host, devices=formatted_devices, files=extended_image_files, config_files=config_files, @@ -165,24 +168,24 @@ def index(): reserved_scsi_ids=reserved_scsi_ids, RESERVATIONS=RESERVATIONS, max_file_size=int(int(MAX_FILE_SIZE) / 1024 / 1024), - running_env=running_env(), + running_env=sys_cmd.running_env(), version=server_info["version"], log_levels=server_info["log_levels"], current_log_level=server_info["current_log_level"], - netinfo=ractl.get_network_info(), + netinfo=ractl_cmd.get_network_info(), device_types=device_types, - free_disk=int(disk_space()["free"] / 1024 / 1024), + free_disk=int(sys_cmd.disk_space()["free"] / 1024 / 1024), valid_file_suffix=valid_file_suffix, cdrom_file_suffix=tuple(server_info["sccd"]), removable_file_suffix=tuple(server_info["scrm"]), mo_file_suffix=tuple(server_info["scmo"]), username=username, - auth_active=auth_active()["status"], + auth_active=auth_active(AUTH_GROUP)["status"], ARCHIVE_FILE_SUFFIX=ARCHIVE_FILE_SUFFIX, PROPERTIES_SUFFIX=PROPERTIES_SUFFIX, - REMOVABLE_DEVICE_TYPES=ractl.get_removable_device_types(), - DISK_DEVICE_TYPES=ractl.get_disk_device_types(), - PERIPHERAL_DEVICE_TYPES=ractl.get_peripheral_device_types(), + REMOVABLE_DEVICE_TYPES=ractl_cmd.get_removable_device_types(), + DISK_DEVICE_TYPES=ractl_cmd.get_disk_device_types(), + PERIPHERAL_DEVICE_TYPES=ractl_cmd.get_peripheral_device_types(), ) @@ -195,7 +198,7 @@ def drive_list(): # The file resides in the current dir of the web ui process drive_properties = Path(DRIVE_PROPERTIES_FILE) if drive_properties.is_file(): - process = file_cmds.read_drive_properties(str(drive_properties)) + process = file_cmd.read_drive_properties(str(drive_properties)) process = ReturnCodeMapper.add_msg(process) if not process["status"]: flash(process["msg"], "error") @@ -215,7 +218,6 @@ def drive_list(): cd_conf = [] rm_conf = [] - from werkzeug.utils import secure_filename for device in conf: if device["device_type"] == "SCHD": device["secure_name"] = secure_filename(device["name"]) @@ -234,21 +236,21 @@ def drive_list(): else: username = None - server_info = ractl.get_server_info() + server_info = ractl_cmd.get_server_info() return render_template( "drives.html", - files=file_cmds.list_images()["files"], + files=file_cmd.list_images()["files"], base_dir=server_info["image_dir"], hd_conf=hd_conf, cd_conf=cd_conf, rm_conf=rm_conf, - running_env=running_env(), + running_env=sys_cmd.running_env(), version=server_info["version"], - free_disk=int(disk_space()["free"] / 1024 / 1024), + free_disk=int(sys_cmd.disk_space()["free"] / 1024 / 1024), cdrom_file_suffix=tuple(server_info["sccd"]), username=username, - auth_active=auth_active()["status"], + auth_active=auth_active(AUTH_GROUP)["status"], ) @@ -260,9 +262,6 @@ def login(): username = request.form["username"] password = request.form["password"] - from simplepam import authenticate - from grp import getgrall - groups = [g.gr_name for g in getgrall() if username in g.gr_mem] if AUTH_GROUP in groups: if authenticate(str(username), str(password)): @@ -287,12 +286,12 @@ def logout(): return redirect(url_for("index")) -@APP.route("/pwa/") -def send_pwa_files(path): +@APP.route("/pwa/") +def send_pwa_files(pwa_path): """ Sets up mobile web resources """ - return send_from_directory("pwa", path) + return send_from_directory("pwa", pwa_path) def login_required(func): @@ -301,7 +300,7 @@ def login_required(func): """ @wraps(func) def decorated_function(*args, **kwargs): - auth = auth_active() + auth = auth_active(AUTH_GROUP) if auth["status"] and "username" not in session: flash(auth["msg"], "error") return redirect(url_for("index")) @@ -325,7 +324,7 @@ def drive_create(): full_file_name = file_name + "." + file_type # Creating the image file - process = file_cmds.create_new_image(file_name, file_type, size) + process = file_cmd.create_new_image(file_name, file_type, size) if process["status"]: flash(_("Image file created: %(file_name)s", file_name=full_file_name)) else: @@ -340,7 +339,7 @@ def drive_create(): "revision": revision, "block_size": block_size, } - process = file_cmds.write_drive_properties(prop_file_name, properties) + process = file_cmd.write_drive_properties(prop_file_name, properties) process = ReturnCodeMapper.add_msg(process) if process["status"]: flash(process["msg"]) @@ -370,7 +369,7 @@ def drive_cdrom(): "revision": revision, "block_size": block_size, } - process = file_cmds.write_drive_properties(file_name, properties) + process = file_cmd.write_drive_properties(file_name, properties) process = ReturnCodeMapper.add_msg(process) if process["status"]: flash(process["msg"]) @@ -389,7 +388,7 @@ def config_save(): file_name = request.form.get("name") or "default" file_name = f"{file_name}.{CONFIG_FILE_SUFFIX}" - process = file_cmds.write_config(file_name) + process = file_cmd.write_config(file_name) process = ReturnCodeMapper.add_msg(process) if process["status"]: flash(process["msg"]) @@ -408,7 +407,7 @@ def config_load(): file_name = request.form.get("name") if "load" in request.form: - process = file_cmds.read_config(file_name) + process = file_cmd.read_config(file_name) process = ReturnCodeMapper.add_msg(process) if process["status"]: flash(process["msg"]) @@ -417,7 +416,7 @@ def config_load(): flash(process['msg'], "error") return redirect(url_for("index")) if "delete" in request.form: - process = file_cmds.delete_file(f"{CFG_DIR}/{file_name}") + process = file_cmd.delete_file(f"{CFG_DIR}/{file_name}") process = ReturnCodeMapper.add_msg(process) if process["status"]: flash(process["msg"]) @@ -437,28 +436,15 @@ def show_logs(): Displays system logs """ lines = request.form.get("lines") or "200" - scope = request.form.get("scope") or "default" + scope = request.form.get("scope") or "all" - from subprocess import run - if scope != "default": - process = run( - ["journalctl", "-n", lines, "-u", scope], - capture_output=True, - check=True, - ) - else: - process = run( - ["journalctl", "-n", lines], - capture_output=True, - check=True, - ) - - if process.returncode == 0: + returncode, logs = sys_cmd.get_logs(lines, scope) + if returncode == 0: headers = {"content-type": "text/plain"} - return process.stdout.decode("utf-8"), int(lines), headers + return logs, int(lines), headers flash(_("An error occurred when fetching logs.")) - flash(process.stderr.decode("utf-8"), "stderr") + flash(logs, "stderr") return redirect(url_for("index")) @@ -470,7 +456,7 @@ def log_level(): """ level = request.form.get("level") or "info" - process = ractl.set_log_level(level) + process = ractl_cmd.set_log_level(level) if process["status"]: flash(_("Log level set to %(value)s", value=level)) return redirect(url_for("index")) @@ -502,31 +488,18 @@ def attach_device(): error_msg = _("Please follow the instructions at %(url)s", url=error_url) if "interface" in params.keys(): - if params["interface"].startswith("wlan"): - if not introspect_file("/etc/sysctl.conf", r"^net\.ipv4\.ip_forward=1$"): - flash(_("Configure IPv4 forwarding before using a wireless network device."), "error") - flash(error_msg, "error") - return redirect(url_for("index")) - if not Path("/etc/iptables/rules.v4").is_file(): - flash(_("Configure NAT before using a wireless network device."), "error") - flash(error_msg, "error") - return redirect(url_for("index")) - else: - if not introspect_file("/etc/dhcpcd.conf", r"^denyinterfaces " + params["interface"] + r"$"): - flash(_("Configure the network bridge before using a wired network device."), "error") - flash(error_msg, "error") - return redirect(url_for("index")) - if not Path("/etc/network/interfaces.d/rascsi_bridge").is_file(): - flash(_("Configure the network bridge before using a wired network device."), "error") - flash(error_msg, "error") - return redirect(url_for("index")) + bridge_status = is_bridge_configured(params["interface"]) + if bridge_status: + flash(bridge_status, "error") + flash(error_msg, "error") + return redirect(url_for("index")) kwargs = { "unit": int(unit), "device_type": device_type, "params": params, } - process = ractl.attach_device(scsi_id, **kwargs) + process = ractl_cmd.attach_device(scsi_id, **kwargs) process = ReturnCodeMapper.add_msg(process) if process["status"]: flash(_( @@ -557,14 +530,14 @@ def attach_image(): if device_type: kwargs["device_type"] = device_type - device_types = ractl.get_device_types() + device_types = ractl_cmd.get_device_types() expected_block_size = min(device_types["device_types"][device_type]["block_sizes"]) # Attempt to load the device properties file: # same file name with PROPERTIES_SUFFIX appended drive_properties = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}" if Path(drive_properties).is_file(): - process = file_cmds.read_drive_properties(drive_properties) + process = file_cmd.read_drive_properties(drive_properties) process = ReturnCodeMapper.add_msg(process) if not process["status"]: flash(process["msg"], "error") @@ -576,7 +549,7 @@ def attach_image(): kwargs["block_size"] = conf["block_size"] expected_block_size = conf["block_size"] - process = ractl.attach_device(scsi_id, **kwargs) + process = ractl_cmd.attach_device(scsi_id, **kwargs) process = ReturnCodeMapper.add_msg(process) if process["status"]: flash(_(( @@ -607,7 +580,7 @@ def detach_all_devices(): """ Detaches all currently attached devices """ - process = ractl.detach_all() + process = ractl_cmd.detach_all() if process["status"]: flash(_("Detached all SCSI devices")) return redirect(url_for("index")) @@ -624,7 +597,7 @@ def detach(): """ scsi_id = request.form.get("scsi_id") unit = request.form.get("unit") - process = ractl.detach_by_id(scsi_id, unit) + process = ractl_cmd.detach_by_id(scsi_id, unit) if process["status"]: flash(_("Detached SCSI ID %(id_number)s LUN %(unit_number)s", id_number=scsi_id, unit_number=unit)) @@ -645,7 +618,7 @@ def eject(): scsi_id = request.form.get("scsi_id") unit = request.form.get("unit") - process = ractl.eject_by_id(scsi_id, unit) + process = ractl_cmd.eject_by_id(scsi_id, unit) if process["status"]: flash(_("Ejected SCSI ID %(id_number)s LUN %(unit_number)s", id_number=scsi_id, unit_number=unit)) @@ -664,7 +637,7 @@ def device_info(): scsi_id = request.form.get("scsi_id") unit = request.form.get("unit") - devices = ractl.list_devices(scsi_id, unit) + devices = ractl_cmd.list_devices(scsi_id, unit) # First check if any device at all was returned if not devices["status"]: @@ -700,9 +673,9 @@ def reserve_id(): """ scsi_id = request.form.get("scsi_id") memo = request.form.get("memo") - reserved_ids = ractl.get_reserved_ids()["ids"] + reserved_ids = ractl_cmd.get_reserved_ids()["ids"] reserved_ids.extend(scsi_id) - process = ractl.reserve_scsi_ids(reserved_ids) + process = ractl_cmd.reserve_scsi_ids(reserved_ids) if process["status"]: RESERVATIONS[int(scsi_id)] = memo flash(_("Reserved SCSI ID %(id_number)s", id_number=scsi_id)) @@ -719,9 +692,9 @@ def release_id(): Releases the reservation of a SCSI ID as well as the memo for the reservation """ scsi_id = request.form.get("scsi_id") - reserved_ids = ractl.get_reserved_ids()["ids"] + reserved_ids = ractl_cmd.get_reserved_ids()["ids"] reserved_ids.remove(scsi_id) - process = ractl.reserve_scsi_ids(reserved_ids) + process = ractl_cmd.reserve_scsi_ids(reserved_ids) if process["status"]: RESERVATIONS[int(scsi_id)] = "" flash(_("Released the reservation for SCSI ID %(id_number)s", id_number=scsi_id)) @@ -738,7 +711,7 @@ def restart(): """ Restarts the Pi """ - ractl.shutdown_pi("reboot") + ractl_cmd.shutdown_pi("reboot") return redirect(url_for("index")) @@ -748,7 +721,7 @@ def shutdown(): """ Shuts down the Pi """ - ractl.shutdown_pi("system") + ractl_cmd.shutdown_pi("system") return redirect(url_for("index")) @@ -762,7 +735,7 @@ def download_to_iso(): url = request.form.get("url") iso_args = request.form.get("type").split() - process = file_cmds.download_file_to_iso(url, *iso_args) + process = file_cmd.download_file_to_iso(url, *iso_args) process = ReturnCodeMapper.add_msg(process) if process["status"]: flash(process["msg"]) @@ -772,7 +745,7 @@ def download_to_iso(): flash(process["msg"], "error") return redirect(url_for("index")) - process_attach = ractl.attach_device( + process_attach = ractl_cmd.attach_device( scsi_id, device_type="SCCD", params={"file": process["file_name"]}, @@ -795,8 +768,8 @@ def download_img(): Downloads a remote file onto the images dir on the Pi """ url = request.form.get("url") - server_info = ractl.get_server_info() - process = file_cmds.download_to_dir(url, server_info["image_dir"], Path(url).name) + server_info = ractl_cmd.get_server_info() + process = file_cmd.download_to_dir(url, server_info["image_dir"], Path(url).name) process = ReturnCodeMapper.add_msg(process) if process["status"]: flash(process["msg"]) @@ -815,7 +788,7 @@ def download_afp(): """ url = request.form.get("url") file_name = Path(url).name - process = file_cmds.download_to_dir(url, AFP_DIR, file_name) + process = file_cmd.download_to_dir(url, AFP_DIR, file_name) process = ReturnCodeMapper.add_msg(process) if process["status"]: flash(process["msg"]) @@ -833,55 +806,12 @@ def upload_file(): Depending on the Dropzone.js JavaScript library """ # Due to the embedded javascript library, we cannot use the @login_required decorator - auth = auth_active() + auth = auth_active(AUTH_GROUP) if auth["status"] and "username" not in session: return make_response(auth["msg"], 403) - from werkzeug.utils import secure_filename - from os import path - - log = logging.getLogger("pydrop") - file_object = request.files["file"] - file_name = secure_filename(file_object.filename) - - server_info = ractl.get_server_info() - - save_path = path.join(server_info["image_dir"], file_name) - current_chunk = int(request.form['dzchunkindex']) - - # Makes sure not to overwrite an existing file, - # but continues writing to a file transfer in progress - if path.exists(save_path) and current_chunk == 0: - return make_response(_("The file already exists!"), 400) - - try: - with open(save_path, "ab") as save: - save.seek(int(request.form["dzchunkbyteoffset"])) - save.write(file_object.stream.read()) - except OSError: - log.exception("Could not write to file") - return make_response(_("Unable to write the file to disk!"), 500) - - total_chunks = int(request.form["dztotalchunkcount"]) - - if current_chunk + 1 == total_chunks: - # Validate the resulting file size after writing the last chunk - if path.getsize(save_path) != int(request.form["dztotalfilesize"]): - log.error( - "Finished transferring %s, " - "but it has a size mismatch with the original file. " - "Got %s but we expected %s.", - file_object.filename, - path.getsize(save_path), - request.form['dztotalfilesize'], - ) - return make_response(_("Transferred file corrupted!"), 500) - - log.info("File %s has been uploaded successfully", file_object.filename) - log.debug("Chunk %s of %s for file %s completed.", - current_chunk + 1, total_chunks, file_object.filename) - - return make_response(_("File upload successful!"), 200) + server_info = ractl_cmd.get_server_info() + return upload_with_dropzonejs(server_info["image_dir"]) @APP.route("/files/create", methods=["POST"]) @@ -895,7 +825,7 @@ def create_file(): file_type = request.form.get("type") full_file_name = file_name + "." + file_type - process = file_cmds.create_new_image(file_name, file_type, size) + process = file_cmd.create_new_image(file_name, file_type, size) if process["status"]: flash(_("Image file created: %(file_name)s", file_name=full_file_name)) return redirect(url_for("index")) @@ -922,7 +852,7 @@ def delete(): """ file_name = request.form.get("file_name") - process = file_cmds.delete_image(file_name) + process = file_cmd.delete_image(file_name) if process["status"]: flash(_("Image file deleted: %(file_name)s", file_name=file_name)) else: @@ -932,7 +862,7 @@ def delete(): # Delete the drive properties file, if it exists prop_file_path = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}" if Path(prop_file_path).is_file(): - process = file_cmds.delete_file(prop_file_path) + process = file_cmd.delete_file(prop_file_path) process = ReturnCodeMapper.add_msg(process) if process["status"]: @@ -954,7 +884,7 @@ def rename(): file_name = request.form.get("file_name") new_file_name = request.form.get("new_file_name") - process = file_cmds.rename_image(file_name, new_file_name) + process = file_cmd.rename_image(file_name, new_file_name) if process["status"]: flash(_("Image file renamed to: %(file_name)s", file_name=new_file_name)) else: @@ -965,7 +895,7 @@ def rename(): prop_file_path = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}" new_prop_file_path = f"{CFG_DIR}/{new_file_name}.{PROPERTIES_SUFFIX}" if Path(prop_file_path).is_file(): - process = file_cmds.rename_file(prop_file_path, new_prop_file_path) + process = file_cmd.rename_file(prop_file_path, new_prop_file_path) process = ReturnCodeMapper.add_msg(process) if process["status"]: flash(process["msg"]) @@ -987,11 +917,10 @@ def unzip(): zip_member = request.form.get("zip_member") or False zip_members = request.form.get("zip_members") or False - from ast import literal_eval if zip_members: zip_members = literal_eval(zip_members) - process = file_cmds.unzip_file(zip_file, zip_member, zip_members) + process = file_cmd.unzip_file(zip_file, zip_member, zip_members) if process["status"]: if not process["msg"]: flash(_("Aborted unzip: File(s) with the same name already exists."), "error") @@ -1015,8 +944,8 @@ def change_language(): """ locale = request.form.get("locale") session["language"] = locale - ractl.locale = session["language"] - file_cmds.locale = session["language"] + ractl_cmd.locale = session["language"] + file_cmd.locale = session["language"] refresh() language = Locale.parse(locale) @@ -1032,8 +961,8 @@ def detect_locale(): This requires the Flask app to have started first. """ session["language"] = get_locale() - ractl.locale = session["language"] - file_cmds.locale = session["language"] + ractl_cmd.locale = session["language"] + file_cmd.locale = session["language"] if __name__ == "__main__": @@ -1074,12 +1003,12 @@ if __name__ == "__main__": APP.config["TOKEN"] = arguments.password sock_cmd = SocketCmdsFlask(host=arguments.rascsi_host, port=arguments.rascsi_port) - ractl = RaCtlCmds(sock_cmd=sock_cmd, token=APP.config["TOKEN"]) - file_cmds = FileCmds(sock_cmd=sock_cmd, ractl=ractl, token=APP.config["TOKEN"]) + ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=APP.config["TOKEN"]) + file_cmd = FileCmds(sock_cmd=sock_cmd, ractl=ractl_cmd, token=APP.config["TOKEN"]) + sys_cmd = SysCmds() if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file(): - file_cmds.read_config(DEFAULT_CONFIG) + file_cmd.read_config(DEFAULT_CONFIG) - import bjoern print("Serving rascsi-web...") bjoern.run(APP, "0.0.0.0", arguments.port) diff --git a/python/web/src/web_utils.py b/python/web/src/web_utils.py new file mode 100644 index 00000000..0d6403d9 --- /dev/null +++ b/python/web/src/web_utils.py @@ -0,0 +1,183 @@ +""" +Module for RaSCSI Web Interface utility methods +""" + +import logging +from grp import getgrall +from os import path +from pathlib import Path + +from flask import request, make_response +from flask_babel import _ +from werkzeug.utils import secure_filename + +from rascsi.sys_cmds import SysCmds + +def get_valid_scsi_ids(devices, reserved_ids): + """ + Takes a list of (dict)s devices, and list of (int)s reserved_ids. + Returns: + - (list) of (int)s valid_ids, which are the SCSI ids that are not reserved + - (int) recommended_id, which is the id that the Web UI should default to recommend + """ + occupied_ids = [] + for device in devices: + occupied_ids.append(device["id"]) + + unoccupied_ids = [i for i in list(range(8)) if i not in reserved_ids + occupied_ids] + unoccupied_ids.sort() + valid_ids = [i for i in list(range(8)) if i not in reserved_ids] + valid_ids.sort(reverse=True) + + if unoccupied_ids: + recommended_id = unoccupied_ids[-1] + else: + recommended_id = occupied_ids.pop(0) + + return valid_ids, recommended_id + + +def sort_and_format_devices(devices): + """ + Takes a (list) of (dict)s devices and returns a (list) of (dict)s. + Sorts by SCSI ID acending (0 to 7). + For SCSI IDs where no device is attached, inject a (dict) with placeholder text. + """ + occupied_ids = [] + for device in devices: + occupied_ids.append(device["id"]) + + formatted_devices = devices + + # Add padding devices and sort the list + for i in range(8): + if i not in occupied_ids: + formatted_devices.append({"id": i, "device_type": "-", \ + "status": "-", "file": "-", "product": "-"}) + # Sort list of devices by id + formatted_devices.sort(key=lambda dic: str(dic["id"])) + + return formatted_devices + + +def map_device_types_and_names(device_types): + """ + Takes a (dict) corresponding to the data structure returned by RaCtlCmds.get_device_types() + Returns a (dict) of device_type:device_name mappings of localized device names + """ + for device in device_types.keys(): + device_types[device]["name"] = get_device_name(device) + + return device_types + + +# pylint: disable=too-many-return-statements +def get_device_name(device_type): + """ + Takes a four letter device acronym (str) device_type. + Returns the human-readable name for the device type. + """ + if device_type == "SAHD": + return _("SASI Hard Disk") + if device_type == "SCHD": + return _("SCSI Hard Disk") + if device_type == "SCRM": + return _("Removable Disk") + if device_type == "SCMO": + return _("Magneto-Optical") + if device_type == "SCCD": + return _("CD / DVD") + if device_type == "SCBR": + return _("X68000 Host Bridge") + if device_type == "SCDP": + return _("DaynaPORT SCSI/Link") + if device_type == "SCLP": + return _("Printer") + if device_type == "SCHS": + return _("Host Services") + return device_type + + +def auth_active(group): + """ + Inspects if the group defined in (str) group exists on the system. + If it exists, tell the webapp to enable authentication. + Returns a (dict) with (bool) status and (str) msg + """ + groups = [g.gr_name for g in getgrall()] + if group in groups: + return { + "status": True, + "msg": _("You must log in to use this function"), + } + return {"status": False, "msg": ""} + + +def is_bridge_configured(interface): + """ + Takes (str) interface of a network device being attached. + Returns (bool) False if the network bridge is configured. + Returns (str) with an error message if the network bridge is not configured. + """ + sys_cmd = SysCmds() + if interface.startswith("wlan"): + if not sys_cmd.introspect_file("/etc/sysctl.conf", r"^net\.ipv4\.ip_forward=1$"): + return _("Configure IPv4 forwarding before using a wireless network device.") + if not Path("/etc/iptables/rules.v4").is_file(): + return _("Configure NAT before using a wireless network device.") + else: + if not sys_cmd.introspect_file( + "/etc/dhcpcd.conf", + r"^denyinterfaces " + interface + r"$", + ): + return _("Configure the network bridge before using a wired network device.") + if not Path("/etc/network/interfaces.d/rascsi_bridge").is_file(): + return _("Configure the network bridge before using a wired network device.") + return False + + +def upload_with_dropzonejs(image_dir): + """ + Takes (str) image_dir which is the path to the image dir to store files. + Opens a stream to transfer a file via the embedded dropzonejs library. + """ + log = logging.getLogger("pydrop") + file_object = request.files["file"] + file_name = secure_filename(file_object.filename) + + save_path = path.join(image_dir, file_name) + current_chunk = int(request.form['dzchunkindex']) + + # Makes sure not to overwrite an existing file, + # but continues writing to a file transfer in progress + if path.exists(save_path) and current_chunk == 0: + return make_response(_("The file already exists!"), 400) + + try: + with open(save_path, "ab") as save: + save.seek(int(request.form["dzchunkbyteoffset"])) + save.write(file_object.stream.read()) + except OSError: + log.exception("Could not write to file") + return make_response(_("Unable to write the file to disk!"), 500) + + total_chunks = int(request.form["dztotalchunkcount"]) + + if current_chunk + 1 == total_chunks: + # Validate the resulting file size after writing the last chunk + if path.getsize(save_path) != int(request.form["dztotalfilesize"]): + log.error( + "Finished transferring %s, " + "but it has a size mismatch with the original file. " + "Got %s but we expected %s.", + file_object.filename, + path.getsize(save_path), + request.form['dztotalfilesize'], + ) + return make_response(_("Transferred file corrupted!"), 500) + + log.info("File %s has been uploaded successfully", file_object.filename) + log.debug("Chunk %s of %s for file %s completed.", + current_chunk + 1, total_chunks, file_object.filename) + + return make_response(_("File upload successful!"), 200)