mirror of
https://github.com/akuker/RASCSI.git
synced 2025-02-16 19:31:09 +00:00
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.
This commit is contained in:
parent
4178d4b845
commit
e8f392c3f1
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
170
python/common/src/rascsi/sys_cmds.py
Normal file
170
python/common/src/rascsi/sys_cmds.py
Normal file
@ -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")
|
@ -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
|
@ -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:
|
||||
|
@ -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
|
@ -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": ""}
|
@ -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
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ _("RaSCSI Control Page") }}</title>
|
||||
<title>{{ _("RaSCSI Control Page") }} [{{ host }}]</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/pwa/apple-icon-57x57.png">
|
||||
@ -52,22 +52,27 @@
|
||||
<div>{{ _("Log In to Use Web Interface") }}</div>
|
||||
<input type="text" name="username" placeholder="{{ _("Username") }}">
|
||||
<input type="password" name="password" placeholder="{{ _("Password") }}">
|
||||
<input type="submit" value="Login">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">{{ _("Web Interface Authentication Disabled") }} – {{ _("See <a href=\"%(url)s\" target=\"_blank\">Wiki</a> for more information", url="https://github.com/akuker/RASCSI/wiki/Web-Interface#enable-authentication") }}</span>
|
||||
{% endif %}
|
||||
<table width="100%">
|
||||
<table width="100%" style="background-color: black;">
|
||||
<tbody>
|
||||
<tr style="background-color: black;">
|
||||
<td style="background-color: black;">
|
||||
<tr align="center">
|
||||
<td>
|
||||
<a href="http://github.com/akuker/RASCSI" target="_blank">
|
||||
<h1>RaSCSI - 68kmla Edition</h1>
|
||||
<h1>{{ _("RaSCSI Control Page") }}</h1>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color: white;">
|
||||
hostname: {{ host }} ip: {{ ip_addr }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -65,7 +65,7 @@
|
||||
<input name="type" type="hidden" value="{{ device.device_type }}">
|
||||
<input name="file_size" type="hidden" value="{{ device.size }}">
|
||||
<select type="select" name="file_name">
|
||||
{% for f in files %}
|
||||
{% for f in files|sort(attribute='name') %}
|
||||
{% if device.device_type == "SCCD" %}
|
||||
{% if f["name"].lower().endswith(cdrom_file_suffix) %}
|
||||
<option value="{{ f["name"] }}">{{ f["name"].replace(base_dir, '') }}</option>
|
||||
@ -601,11 +601,11 @@
|
||||
<td style="border: none; vertical-align:top;">
|
||||
<form action="/logs/show" method="post">
|
||||
<label for="lines">{{ _("Log Lines:") }}</label>
|
||||
<input name="lines" type="number" placeholder="200" min="1" size="4">
|
||||
<input name="lines" type="number" value="200" min="1" size="4">
|
||||
<label for="scope">{{ _("Scope:") }}</label>
|
||||
<select name="scope">
|
||||
<option value="default">
|
||||
default
|
||||
<option value="all">
|
||||
all
|
||||
</option>
|
||||
<option value="rascsi">
|
||||
rascsi.service
|
||||
@ -613,6 +613,9 @@
|
||||
<option value="rascsi-web">
|
||||
rascsi-web.service
|
||||
</option>
|
||||
<option value="rascsi-oled">
|
||||
rascsi-oled.service
|
||||
</option>
|
||||
</select>
|
||||
<input type="submit" value="{{ _("Show Logs") }}">
|
||||
</form>
|
||||
|
@ -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/<path:path>")
|
||||
def send_pwa_files(path):
|
||||
@APP.route("/pwa/<path:pwa_path>")
|
||||
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)
|
||||
|
183
python/web/src/web_utils.py
Normal file
183
python/web/src/web_utils.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user