restructuring towards python client library #455 (#613)

* python client library clean branch for PR. #455

* removed superfluous file. #455

* removed one more superfluous file. #455

* README.md, .pylintrc and pylint based fixes. #455

* updated wrt. to the review comments. #455

* removed pylint documentation duplication. #455
This commit is contained in:
Benjamin Zeiss 2022-01-22 00:08:29 +01:00 committed by GitHub
parent 7f362c9308
commit 089dc302e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1573 additions and 1467 deletions

9
.gitignore vendored
View File

@ -5,11 +5,12 @@ core
.DS_Store
*.swp
__pycache__
python/web/current
python/web/src/rascsi_interface_pb2.py
python/oled/current
python/oled/src/rascsi_interface_pb2.py
current
rascsi_interface_pb2.py
src/raspberrypi/hfdisk/
*~
messages.pot
messages.mo
# temporary user files
s.sh

View File

@ -52,6 +52,7 @@ VIRTUAL_DRIVER_PATH="$HOME/images"
CFG_PATH="$HOME/.config/rascsi"
WEB_INSTALL_PATH="$BASE/python/web"
OLED_INSTALL_PATH="$BASE/python/oled"
PYTHON_COMMON_PATH="$BASE/python/common"
SYSTEMD_PATH="/etc/systemd/system"
HFS_FORMAT=/usr/bin/hformat
HFDISK_BIN=/usr/bin/hfdisk
@ -94,15 +95,17 @@ function installRaScsi() {
sudo make install CONNECT_TYPE="${CONNECT_TYPE:-FULLSPEC}" </dev/null
}
# install everything required to run an HTTP server (Nginx + Python Flask App)
function installRaScsiWebInterface() {
if [ -f "$WEB_INSTALL_PATH/rascsi_interface_pb2.py" ]; then
sudo rm "$WEB_INSTALL_PATH/rascsi_interface_pb2.py"
function preparePythonCommon() {
if [ -f "$PYTHON_COMMON_PATH/rascsi_interface_pb2.py" ]; then
sudo rm "$PYTHON_COMMON_PATH/rascsi_interface_pb2.py"
echo "Deleting old Python protobuf library rascsi_interface_pb2.py"
fi
echo "Compiling the Python protobuf library rascsi_interface_pb2.py..."
protoc -I="$BASE/src/raspberrypi/" --python_out="$WEB_INSTALL_PATH/src" rascsi_interface.proto
protoc -I="$BASE/src/raspberrypi/" --python_out="$PYTHON_COMMON_PATH/src" rascsi_interface.proto
}
# install everything required to run an HTTP server (Nginx + Python Flask App)
function installRaScsiWebInterface() {
sudo cp -f "$WEB_INSTALL_PATH/service-infra/nginx-default.conf" /etc/nginx/sites-available/default
sudo cp -f "$WEB_INSTALL_PATH/service-infra/502.html" /var/www/html/502.html
@ -779,13 +782,6 @@ function installRaScsiScreen() {
sudo apt-get update && sudo apt-get install libjpeg-dev libpng-dev libopenjp2-7-dev i2c-tools raspi-config -y </dev/null
if [ -f "$OLED_INSTALL_PATH/src/rascsi_interface_pb2.py" ]; then
sudo rm "$OLED_INSTALL_PATH/src/rascsi_interface_pb2.py"
echo "Deleting old Python protobuf library rascsi_interface_pb2.py"
fi
echo "Compiling the Python protobuf library rascsi_interface_pb2.py..."
protoc -I="$BASE/src/raspberrypi/" --python_out="$OLED_INSTALL_PATH/src" rascsi_interface.proto
if [[ $(grep -c "^dtparam=i2c_arm=on" /boot/config.txt) -ge 1 ]]; then
echo "NOTE: I2C support seems to have been configured already."
REBOOT=0
@ -882,6 +878,7 @@ function runChoice() {
backupRaScsiService
installRaScsi
enableRaScsiService
preparePythonCommon
if [[ $(isRaScsiScreenInstalled) -eq 0 ]]; then
echo "Detected rascsi oled service; will run the installation steps for the OLED monitor."
installRaScsiScreen
@ -913,6 +910,7 @@ function runChoice() {
stopRaScsi
compileRaScsi
backupRaScsiService
preparePythonCommon
installRaScsi
enableRaScsiService
if [[ $(isRaScsiScreenInstalled) -eq 0 ]]; then
@ -931,6 +929,7 @@ function runChoice() {
echo "- Add and modify systemd services"
echo "- Modify the Raspberry Pi boot configuration (may require a reboot)"
sudoCheck
preparePythonCommon
installRaScsiScreen
showRaScsiScreenStatus
echo "Installing / Updating RaSCSI OLED Screen - Complete!"
@ -1018,6 +1017,7 @@ function runChoice() {
updateRaScsiGit
createCfgDir
installPackages
preparePythonCommon
installRaScsiWebInterface
echo "Configuring RaSCSI Web Interface stand-alone - Complete!"
echo "Launch the Web Interface with the 'start.sh' script. To use a custom port for the web server: 'start.sh --port=8081"

View File

@ -7,7 +7,7 @@ extension-pkg-whitelist=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
ignore=CVS,rascsi_interface_pb2.py
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
@ -15,7 +15,7 @@ ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
init-hook=
init-hook=import sys; sys.path.append("common/src"); sys.path.append("web/src"); sys.path.append("oled/src");
# venv hook for pylint
# Requires pylint-venv package:
# $ pip install pylint-venv

32
python/README.md Normal file
View File

@ -0,0 +1,32 @@
# RaSCSI Python Apps
This directory contains Python-based clients for RaSCSI as well as common
packages that are shared among the clients.
The following paragraphs in this README contain instructions that are shared
among all Python apps.
### Static analysis with pylint
It is recommended to run pylint against new code to protect against bugs
and keep the code readable and maintainable.
The local pylint configuration lives in .pylintrc.
In order for pylint to recognize venv libraries, the pylint-venv package is required.
```
sudo apt install pylint3
sudo pip install pylint-venv
source venv/bin/activate
pylint3 python_source_file.py
```
Examples:
```
# check a single file
pylint web/src/web.py
# check the python modules
pylint common/src
pylint web/src
pylint oled/src
```

View File

@ -0,0 +1,2 @@
protobuf==3.19.3
requests==2.26.0

View File

@ -0,0 +1,30 @@
# RaSCSI Common Python Module
The common module contains python modules that are shared among multiple Python
applications such as the OLED or the Web application. It contains shared functionality.
For example, the rascsi python module provides functionality for accessing rascsi through its
protobuf interface and provides convenient classes for that purpose.
### Usage
To make use of the rascsi python module, it needs to be found by the Python scripts using it.
This can be achieved in multiple ways. One way is to simply adapt the PYTHONPATH environment
variable to include the common/src directory:
```
PYTHON_COMMON_PATH=${path_to_common_directory}/common/src
export PYTHONPATH=$PWD:${PYTHON_COMMON_PATH}
python3 myapp.py
```
The most interesting functions are likely found in the classes RaCtlCmds and FileCmds. Classes
can be instantiated, for example, as follows
(assuming that rascsi host, rascsi port and token are somehow retrieved from a command line
argument):
```
sock_cmd = SocketCmds(host=args.rascsi_host, port=args.rascsi_port)
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=args.token)
```
Usage examples can be found in the existing RaSCSI Python applications.

View File

View File

View File

@ -0,0 +1,21 @@
"""
Module for general settings used in the rascsi module
"""
from os import getcwd
WORK_DIR = getcwd()
REMOVABLE_DEVICE_TYPES = ("SCCD", "SCRM", "SCMO")
# There may be a more elegant way to get the HOME dir of the user that installed RaSCSI
HOME_DIR = "/".join(WORK_DIR.split("/")[0:3])
CFG_DIR = f"{HOME_DIR}/.config/rascsi"
CONFIG_FILE_SUFFIX = "json"
# File ending used for drive properties files
PROPERTIES_SUFFIX = "properties"
# The RESERVATIONS list is used to keep track of the reserved ID memos.
# Initialize with a list of 8 empty strings.
RESERVATIONS = ["" for x in range(0, 8)]

View File

@ -0,0 +1,15 @@
"""
Module for custom exceptions raised by the rascsi module
"""
class FailedSocketConnectionException(Exception):
"""Raise when a rascsi protobuf socket connection cannot be established after multiple tries."""
class EmptySocketChunkException(Exception):
"""Raise when a socket payload contains an empty chunk which implies a possible problem. """
class InvalidProtobufResponse(Exception):
"""Raise when a rascsi socket payload contains unpexpected data. """

View File

@ -0,0 +1,601 @@
"""
Module for methods reading from and writing to the file system
"""
import os
import logging
from pathlib import PurePath
import asyncio
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
from rascsi.return_codes import ReturnCodes
from rascsi.socket_cmds import SocketCmds
class FileCmds:
"""
class for methods reading from and writing to the file system
"""
def __init__(self, sock_cmd: SocketCmds, ractl: RaCtlCmds, token=None, locale=None):
self.sock_cmd = sock_cmd
self.ractl = ractl
self.token = token
self.locale = locale
# noinspection PyMethodMayBeStatic
# pylint: disable=no-self-use
def list_files(self, file_types, dir_path):
"""
Takes a (list) or (tuple) of (str) file_types - e.g. ('hda', 'hds')
Returns (list) of (list)s files_list:
index 0 is (str) file name and index 1 is (int) size in bytes
"""
files_list = []
for path, _dirs, files in os.walk(dir_path):
# Only list selected file types
files = [f for f in files if f.lower().endswith(file_types)]
files_list.extend(
[
(
file,
os.path.getsize(os.path.join(path, file))
)
for file in files
]
)
return files_list
# noinspection PyMethodMayBeStatic
def list_config_files(self):
"""
Finds fils with file ending CONFIG_FILE_SUFFIX in CFG_DIR.
Returns a (list) of (str) files_list
"""
files_list = []
for _root, _dirs, files in os.walk(CFG_DIR):
for file in files:
if file.endswith("." + CONFIG_FILE_SUFFIX):
files_list.append(file)
return files_list
def list_images(self):
"""
Sends a IMAGE_FILES_INFO command to the server
Returns a (dict) with (bool) status, (str) msg, and (list) of (dict)s files
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO
command.params["token"] = self.token
command.params["locale"] = self.locale
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
# Get a list of all *.properties files in CFG_DIR
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:
# Add properties meta data for the image, if applicable
if file.name in prop_files:
process = self.read_drive_properties(f"{CFG_DIR}/{file.name}.{PROPERTIES_SUFFIX}")
prop = process["conf"]
else:
prop = False
if file.name.lower().endswith(".zip"):
zip_path = f"{server_info['image_dir']}/{file.name}"
if is_zipfile(zip_path):
zipfile = ZipFile(zip_path)
# Get a list of (str) containing all zipfile members
zip_members = zipfile.namelist()
# Strip out directories from the list
zip_members = [x for x in zip_members if not x.endswith("/")]
else:
logging.warning("%s is an invalid zip file", zip_path)
zip_members = False
else:
zip_members = False
size_mb = "{:,.1f}".format(file.size / 1024 / 1024)
dtype = proto.PbDeviceType.Name(file.type)
files.append({
"name": file.name,
"size": file.size,
"size_mb": size_mb,
"detected_type": dtype,
"prop": prop,
"zip_members": zip_members,
})
return {"status": result.status, "msg": result.msg, "files": files}
def create_new_image(self, file_name, file_type, size):
"""
Takes (str) file_name, (str) file_type, and (int) size
Sends a CREATE_IMAGE command to the server
Returns (dict) with (bool) status and (str) msg
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CREATE_IMAGE
command.params["token"] = self.token
command.params["locale"] = self.locale
command.params["file"] = file_name + "." + file_type
command.params["size"] = str(size)
command.params["read_only"] = "false"
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def delete_image(self, file_name):
"""
Takes (str) file_name
Sends a DELETE_IMAGE command to the server
Returns (dict) with (bool) status and (str) msg
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DELETE_IMAGE
command.params["token"] = self.ractl.token
command.params["locale"] = self.ractl.locale
command.params["file"] = file_name
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def rename_image(self, file_name, new_file_name):
"""
Takes (str) file_name, (str) new_file_name
Sends a RENAME_IMAGE command to the server
Returns (dict) with (bool) status and (str) msg
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.RENAME_IMAGE
command.params["token"] = self.ractl.token
command.params["locale"] = self.ractl.locale
command.params["from"] = file_name
command.params["to"] = new_file_name
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
# noinspection PyMethodMayBeStatic
def delete_file(self, file_path):
"""
Takes (str) file_path with the full path to the file to delete
Returns (dict) with (bool) status and (str) msg
"""
parameters = {
"file_path": file_path
}
if os.path.exists(file_path):
os.remove(file_path)
return {
"status": True,
"return_code": ReturnCodes.DELETEFILE_SUCCESS,
"parameters": parameters,
}
return {
"status": False,
"return_code": ReturnCodes.DELETEFILE_FILE_NOT_FOUND,
"parameters": parameters,
}
# noinspection PyMethodMayBeStatic
def rename_file(self, file_path, target_path):
"""
Takes (str) file_path and (str) target_path
Returns (dict) with (bool) status and (str) msg
"""
parameters = {
"target_path": target_path
}
if os.path.exists(PurePath(target_path).parent):
os.rename(file_path, target_path)
return {
"status": True,
"return_code": ReturnCodes.RENAMEFILE_SUCCESS,
"parameters": parameters,
}
return {
"status": False,
"return_code": ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE,
"parameters": parameters,
}
def unzip_file(self, file_name, member=False, members=False):
"""
Takes (str) file_name, optional (str) member, optional (list) of (str) members
file_name is the name of the zip file to unzip
member is the full path to the particular file in the zip file to unzip
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(
f"unzip -d {server_info['image_dir']} -n -j "
f"{server_info['image_dir']}/{file_name}"
))
if members:
for path in members:
if path.endswith(PROPERTIES_SUFFIX):
name = PurePath(path).name
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(
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(
f"unzip -d {CFG_DIR} -n -j "
f"{server_info['image_dir']}/{file_name} {member}.{PROPERTIES_SUFFIX}"
))
if unzip_prop["returncode"] == 0:
prop_flag = True
if unzip_proc["returncode"] != 0:
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"]
)
return {"status": True, "msg": unzipped, "prop_flag": prop_flag}
def download_file_to_iso(self, url, *iso_args):
"""
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()
file_name = PurePath(url).name
tmp_ts = int(time())
tmp_dir = "/tmp/" + str(tmp_ts) + "/"
os.mkdir(tmp_dir)
tmp_full_path = tmp_dir + file_name
iso_filename = f"{server_info['image_dir']}/{file_name}.iso"
req_proc = self.download_to_dir(url, tmp_dir, file_name)
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.")
else:
logging.info(
"%s is a zipfile! Will attempt to unzip and store the resulting files.",
tmp_full_path,
)
unzip_proc = asyncio.run(self.run_async(
f"unzip -d {tmp_dir} -n {tmp_full_path}"
))
if not unzip_proc["returncode"]:
logging.info(
"%s was successfully unzipped. Deleting the zipfile.",
tmp_full_path,
)
self.delete_file(tmp_full_path)
try:
run(
[
"genisoimage",
*iso_args,
"-o",
iso_filename,
tmp_dir,
],
capture_output=True,
check=True,
)
except CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
return {"status": False, "msg": error.stderr.decode("utf-8")}
parameters = {
"value": " ".join(iso_args)
}
return {
"status": True,
"return_code": ReturnCodes.DOWNLOADFILETOISO_SUCCESS,
"parameters": parameters,
"file_name": iso_filename,
}
# noinspection PyMethodMayBeStatic
def download_to_dir(self, url, save_dir, file_name):
"""
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:
with requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"}) as req:
req.raise_for_status()
with open(f"{save_dir}/{file_name}", "wb") as download:
for chunk in req.iter_content(chunk_size=8192):
download.write(chunk)
except requests.exceptions.RequestException as error:
logging.warning("Request failed: %s", str(error))
return {"status": False, "msg": str(error)}
logging.info("Response encoding: %s", req.encoding)
logging.info("Response content-type: %s", req.headers["content-type"])
logging.info("Response status code: %s", req.status_code)
parameters = {
"file_name": file_name,
"save_dir": save_dir
}
return {
"status": True,
"return_code": ReturnCodes.DOWNLOADTODIR_SUCCESS,
"parameters": parameters,
}
def write_config(self, file_name):
"""
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:
version = self.ractl.get_server_info()["version"]
devices = self.ractl.list_devices()["device_list"]
for device in devices:
# Remove keys that we don't want to store in the file
del device["status"]
del device["file"]
# It's cleaner not to store an empty parameter for every device without media
if device["image"] == "":
device["image"] = None
# RaSCSI product names will be generated on the fly by RaSCSI
if device["vendor"] == "RaSCSI":
device["vendor"] = device["product"] = device["revision"] = None
# A block size of 0 is how RaSCSI indicates N/A for block size
if device["block_size"] == 0:
device["block_size"] = None
# Convert to a data type that can be serialized
device["params"] = dict(device["params"])
reserved_ids_and_memos = []
reserved_ids = self.ractl.get_reserved_ids()["ids"]
for scsi_id in reserved_ids:
reserved_ids_and_memos.append({"id": scsi_id,
"memo": RESERVATIONS[int(scsi_id)]})
dump(
{"version": version,
"devices": devices,
"reserved_ids": reserved_ids_and_memos},
json_file,
indent=4
)
parameters = {
"file_name": file_name
}
return {
"status": True,
"return_code": ReturnCodes.WRITECONFIG_SUCCESS,
"parameters": parameters,
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
self.delete_file(file_name)
return {"status": False, "msg": str(error)}
except:
logging.error("Could not write to file: %s", file_name)
self.delete_file(file_name)
parameters = {
"file_name": file_name
}
return {
"status": False,
"return_code": ReturnCodes.WRITECONFIG_COULD_NOT_WRITE,
"parameters": parameters,
}
def read_config(self, file_name):
"""
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:
config = load(json_file)
# If the config file format changes again in the future,
# introduce more sophisticated format detection logic here.
if isinstance(config, dict):
self.ractl.detach_all()
ids_to_reserve = []
for item in config["reserved_ids"]:
ids_to_reserve.append(item["id"])
RESERVATIONS[int(item["id"])] = item["memo"]
self.ractl.reserve_scsi_ids(ids_to_reserve)
for row in config["devices"]:
kwargs = {
"device_type": row["device_type"],
"image": row["image"],
"unit": int(row["unit"]),
"vendor": row["vendor"],
"product": row["product"],
"revision": row["revision"],
"block_size": row["block_size"],
}
params = dict(row["params"])
for param in params.keys():
kwargs[param] = params[param]
self.ractl.attach_image(row["id"], **kwargs)
# The config file format in RaSCSI 21.10 is using a list data type at the top level.
# If future config file formats return to the list data type,
# introduce more sophisticated format detection logic here.
elif isinstance(config, list):
self.ractl.detach_all()
for row in config:
kwargs = {
"device_type": row["device_type"],
"image": row["image"],
# "un" for backwards compatibility
"unit": int(row["un"]),
"vendor": row["vendor"],
"product": row["product"],
"revision": row["revision"],
"block_size": row["block_size"],
}
params = dict(row["params"])
for param in params.keys():
kwargs[param] = params[param]
self.ractl.attach_image(row["id"], **kwargs)
else:
return {"status": False,
"return_code": ReturnCodes.READCONFIG_INVALID_CONFIG_FILE_FORMAT}
parameters = {
"file_name": file_name
}
return {
"status": True,
"return_code": ReturnCodes.READCONFIG_SUCCESS,
"parameters": parameters
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
return {"status": False, "msg": str(error)}
except:
logging.error("Could not read file: %s", file_name)
parameters = {
"file_name": file_name
}
return {
"status": False,
"return_code": ReturnCodes.READCONFIG_COULD_NOT_READ,
"parameters": parameters
}
def write_drive_properties(self, file_name, conf):
"""
Writes a drive property configuration file to the config dir.
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:
dump(conf, json_file, indent=4)
parameters = {
"file_path": file_path
}
return {
"status": True,
"return_code": ReturnCodes.WRITEDRIVEPROPS_SUCCESS,
"parameters": parameters,
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
self.delete_file(file_path)
return {"status": False, "msg": str(error)}
except:
logging.error("Could not write to file: %s", file_path)
self.delete_file(file_path)
parameters = {
"file_path": file_path
}
return {
"status": False,
"return_code": ReturnCodes.WRITEDRIVEPROPS_COULD_NOT_WRITE,
"parameters": parameters,
}
# noinspection PyMethodMayBeStatic
def read_drive_properties(self, file_path):
"""
Reads drive properties from json formatted file.
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)
parameters = {
"file_path": file_path
}
return {
"status": True,
"return_codes": ReturnCodes.READDRIVEPROPS_SUCCESS,
"parameters": parameters,
"conf": conf,
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
return {"status": False, "msg": str(error)}
except:
logging.error("Could not read file: %s", file_path)
parameters = {
"file_path": file_path
}
return {
"status": False,
"return_codes": ReturnCodes.READDRIVEPROPS_COULD_NOT_READ,
"parameters": parameters,
}
# noinspection PyMethodMayBeStatic
async def run_async(self, cmd):
"""
Takes (str) cmd with the shell command to execute
Executes shell command and captures output
Returns (dict) with (int) returncode, (str) stdout, (str) stderr
"""
proc = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
stdout, stderr = await proc.communicate()
logging.info("Executed command \"%s\" with status code %d", cmd, proc.returncode)
if stdout:
stdout = stdout.decode()
logging.info("stdout: %s", stdout)
if stderr:
stderr = stderr.decode()
logging.info("stderr: %s", stderr)
return {"returncode": proc.returncode, "stdout": stdout, "stderr": stderr}

View File

@ -0,0 +1,444 @@
"""
Module for commands sent to the RaSCSI backend service.
"""
import rascsi_interface_pb2 as proto
from rascsi.common_settings import REMOVABLE_DEVICE_TYPES
from rascsi.return_codes import ReturnCodes
from rascsi.socket_cmds import SocketCmds
class RaCtlCmds:
"""
Class for commands sent to the RaSCSI backend service.
"""
def __init__(self, sock_cmd: SocketCmds, token=None, locale="en"):
self.sock_cmd = sock_cmd
self.token = token
self.locale = locale
def get_server_info(self):
"""
Sends a SERVER_INFO command to the server.
Returns a dict with:
- (bool) status
- (str) version (RaSCSI version number)
- (list) of (str) log_levels (the log levels RaSCSI supports)
- (str) current_log_level
- (list) of (int) reserved_ids
- (str) image_dir, path to the default images directory
- (int) scan_depth, the current images directory scan depth
- 5 distinct (list)s of (str)s with file endings recognized by RaSCSI
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.SERVER_INFO
command.params["token"] = self.token
command.params["locale"] = self.locale
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
version = (str(result.server_info.version_info.major_version) + "." +
str(result.server_info.version_info.minor_version) + "." +
str(result.server_info.version_info.patch_version))
log_levels = result.server_info.log_level_info.log_levels
current_log_level = result.server_info.log_level_info.current_log_level
reserved_ids = list(result.server_info.reserved_ids_info.ids)
image_dir = result.server_info.image_files_info.default_image_folder
scan_depth = result.server_info.image_files_info.depth
# Creates lists of file endings recognized by RaSCSI
mappings = result.server_info.mapping_info.mapping
sahd = []
schd = []
scrm = []
scmo = []
sccd = []
for dtype in mappings:
if mappings[dtype] == proto.PbDeviceType.SAHD:
sahd.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCHD:
schd.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCRM:
scrm.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCMO:
scmo.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCCD:
sccd.append(dtype)
return {
"status": result.status,
"version": version,
"log_levels": log_levels,
"current_log_level": current_log_level,
"reserved_ids": reserved_ids,
"image_dir": image_dir,
"scan_depth": scan_depth,
"sahd": sahd,
"schd": schd,
"scrm": scrm,
"scmo": scmo,
"sccd": sccd,
}
def get_reserved_ids(self):
"""
Sends a RESERVED_IDS_INFO command to the server.
Returns a dict with:
- (bool) status
- (list) of (int) ids -- currently reserved SCSI IDs
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.RESERVED_IDS_INFO
command.params["token"] = self.token
command.params["locale"] = self.locale
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
scsi_ids = []
for scsi_id in result.reserved_ids_info.ids:
scsi_ids.append(str(scsi_id))
return {"status": result.status, "ids": scsi_ids}
def get_network_info(self):
"""
Sends a NETWORK_INTERFACES_INFO command to the server.
Returns a dict with:
- (bool) status
- (list) of (str) ifs (network interfaces detected by RaSCSI)
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.NETWORK_INTERFACES_INFO
command.params["token"] = self.token
command.params["locale"] = self.locale
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
ifs = result.network_interfaces_info.name
return {"status": result.status, "ifs": ifs}
def get_device_types(self):
"""
Sends a DEVICE_TYPES_INFO command to the server.
Returns a dict with:
- (bool) status
- (list) of (str) device_types (device types that RaSCSI supports, ex. SCHD, SCCD, etc)
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICE_TYPES_INFO
command.params["token"] = self.token
command.params["locale"] = self.locale
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
device_types = []
for prop in result.device_types_info.properties:
device_types.append(proto.PbDeviceType.Name(prop.type))
return {"status": result.status, "device_types": device_types}
def get_image_files_info(self):
"""
Sends a DEFAULT_IMAGE_FILES_INFO command to the server.
Returns a dict with:
- (bool) status
- (str) images_dir, path to images dir
- (list) of (str) image_files
- (int) scan_depth, the current scan depth
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO
command.params["token"] = self.token
command.params["locale"] = self.token
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
images_dir = result.image_files_info.default_image_folder
image_files = result.image_files_info.image_files
scan_depth = result.image_files_info.depth
return {
"status": result.status,
"images_dir": images_dir,
"image_files": image_files,
"scan_depth": scan_depth,
}
def attach_image(self, scsi_id, **kwargs):
"""
Takes (int) scsi_id and kwargs containing 0 or more device properties
If the current attached device is a removable device wihout media inserted,
this sends a INJECT command to the server.
If there is no currently attached device, this sends the ATTACH command to the server.
Returns (bool) status and (str) msg
"""
command = proto.PbCommand()
command.params["token"] = self.token
command.params["locale"] = self.locale
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if "device_type" in kwargs.keys():
if kwargs["device_type"] not in [None, ""]:
devices.type = proto.PbDeviceType.Value(str(kwargs["device_type"]))
if "unit" in kwargs.keys():
if kwargs["unit"] not in [None, ""]:
devices.unit = kwargs["unit"]
if "image" in kwargs.keys():
if kwargs["image"] not in [None, ""]:
devices.params["file"] = kwargs["image"]
# Handling the inserting of media into an attached removable type device
device_type = kwargs.get("device_type", None)
currently_attached = self.list_devices(scsi_id, kwargs.get("unit"))["device_list"]
if currently_attached:
current_type = currently_attached[0]["device_type"]
else:
current_type = None
if device_type in REMOVABLE_DEVICE_TYPES and current_type in REMOVABLE_DEVICE_TYPES:
if current_type != device_type:
parameters = {
"device_type": device_type,
"current_device_type": current_type
}
return {
"status": False,
"return_code": ReturnCodes.ATTACHIMAGE_COULD_NOT_ATTACH,
"parameters": parameters,
}
command.operation = proto.PbOperation.INSERT
# Handling attaching a new device
else:
command.operation = proto.PbOperation.ATTACH
if "interfaces" in kwargs.keys():
if kwargs["interfaces"] not in [None, ""]:
devices.params["interfaces"] = kwargs["interfaces"]
if "vendor" in kwargs.keys():
if kwargs["vendor"] is not None:
devices.vendor = kwargs["vendor"]
if "product" in kwargs.keys():
if kwargs["product"] is not None:
devices.product = kwargs["product"]
if "revision" in kwargs.keys():
if kwargs["revision"] is not None:
devices.revision = kwargs["revision"]
if "block_size" in kwargs.keys():
if kwargs["block_size"] not in [None, ""]:
devices.block_size = int(kwargs["block_size"])
command.devices.append(devices)
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def detach_by_id(self, scsi_id, unit=None):
"""
Takes (int) scsi_id and optional (int) unit.
Sends a DETACH command to the server.
Returns (bool) status and (str) msg.
"""
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if unit is not None:
devices.unit = int(unit)
command = proto.PbCommand()
command.operation = proto.PbOperation.DETACH
command.devices.append(devices)
command.params["token"] = self.token
command.params["locale"] = self.locale
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def detach_all(self):
"""
Sends a DETACH_ALL command to the server.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DETACH_ALL
command.params["token"] = self.token
command.params["locale"] = self.locale
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def eject_by_id(self, scsi_id, unit=None):
"""
Takes (int) scsi_id and optional (int) unit.
Sends an EJECT command to the server.
Returns (bool) status and (str) msg.
"""
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if unit is not None:
devices.unit = int(unit)
command = proto.PbCommand()
command.operation = proto.PbOperation.EJECT
command.devices.append(devices)
command.params["token"] = self.token
command.params["locale"] = self.locale
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def list_devices(self, scsi_id=None, unit=None):
"""
Takes optional (int) scsi_id and optional (int) unit.
Sends a DEVICES_INFO command to the server.
If no scsi_id is provided, returns a (list) of (dict)s of all attached devices.
If scsi_id is is provided, returns a (list) of one (dict) for the given device.
If no attached device is found, returns an empty (list).
Returns (bool) status, (list) of dicts device_list
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICES_INFO
command.params["token"] = self.token
command.params["locale"] = self.locale
# If method is called with scsi_id parameter, return the info on those devices
# Otherwise, return the info on all attached devices
if scsi_id is not None:
device = proto.PbDeviceDefinition()
device.id = int(scsi_id)
if unit is not None:
device.unit = int(unit)
command.devices.append(device)
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
device_list = []
# Return an empty (list) if no devices are attached
if not result.devices_info.devices:
return {"status": False, "device_list": []}
image_files_info = self.get_image_files_info()
i = 0
while i < len(result.devices_info.devices):
did = result.devices_info.devices[i].id
dunit = result.devices_info.devices[i].unit
dtype = proto.PbDeviceType.Name(result.devices_info.devices[i].type)
dstat = result.devices_info.devices[i].status
dprop = result.devices_info.devices[i].properties
# Building the status string
dstat_msg = []
if dprop.read_only:
dstat_msg.append("Read-Only")
if dstat.protected and dprop.protectable:
dstat_msg.append("Write-Protected")
if dstat.removed and dprop.removable:
dstat_msg.append("No Media")
if dstat.locked and dprop.lockable:
dstat_msg.append("Locked")
dpath = result.devices_info.devices[i].file.name
dfile = dpath.replace(image_files_info["images_dir"] + "/", "")
dparam = result.devices_info.devices[i].params
dven = result.devices_info.devices[i].vendor
dprod = result.devices_info.devices[i].product
drev = result.devices_info.devices[i].revision
dblock = result.devices_info.devices[i].block_size
dsize = int(result.devices_info.devices[i].block_count) * int(dblock)
device_list.append({
"id": did,
"unit": dunit,
"device_type": dtype,
"status": ", ".join(dstat_msg),
"image": dpath,
"file": dfile,
"params": dparam,
"vendor": dven,
"product": dprod,
"revision": drev,
"block_size": dblock,
"size": dsize,
})
i += 1
return {"status": result.status, "msg": result.msg, "device_list": device_list}
def reserve_scsi_ids(self, reserved_scsi_ids):
"""
Sends the RESERVE_IDS command to the server to reserve SCSI IDs.
Takes a (list) of (str) as argument.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.RESERVE_IDS
command.params["ids"] = ",".join(reserved_scsi_ids)
command.params["token"] = self.token
command.params["locale"] = self.locale
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def set_log_level(self, log_level):
"""
Sends a LOG_LEVEL command to the server.
Takes (str) log_level as an argument.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.LOG_LEVEL
command.params["level"] = str(log_level)
command.params["token"] = self.token
command.params["locale"] = self.locale
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def shutdown_pi(self, mode):
"""
Sends a SHUT_DOWN command to the server.
Takes (str) mode as an argument.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.SHUT_DOWN
command.params["mode"] = str(mode)
command.params["token"] = self.token
command.params["locale"] = self.locale
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def is_token_auth(self):
"""
Sends a CHECK_AUTHENTICATION command to the server.
Tells you whether RaSCSI backend is protected by a token password or not.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CHECK_AUTHENTICATION
data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}

View File

@ -0,0 +1,25 @@
"""
Module for return codes that are refrenced in the return payloads of the rascsi module.
"""
from typing import Final
# pylint: disable=too-few-public-methods
class ReturnCodes:
"""Class for the return codes used within the rascsi module."""
DELETEFILE_SUCCESS: Final = 0
DELETEFILE_FILE_NOT_FOUND: Final = 1
RENAMEFILE_SUCCESS: Final = 10
RENAMEFILE_UNABLE_TO_MOVE: Final = 11
DOWNLOADFILETOISO_SUCCESS: Final = 20
DOWNLOADTODIR_SUCCESS: Final = 30
WRITECONFIG_SUCCESS: Final = 40
WRITECONFIG_COULD_NOT_WRITE: Final = 41
READCONFIG_SUCCESS: Final = 50
READCONFIG_COULD_NOT_READ: Final = 51
READCONFIG_INVALID_CONFIG_FILE_FORMAT: Final = 51
WRITEDRIVEPROPS_SUCCESS: Final = 60
WRITEDRIVEPROPS_COULD_NOT_WRITE: Final = 61
READDRIVEPROPS_SUCCESS: Final = 70
READDRIVEPROPS_COULD_NOT_READ: Final = 71
ATTACHIMAGE_COULD_NOT_ATTACH: Final = 80

View File

@ -0,0 +1,91 @@
"""
Module for sending and receiving data over a socket connection with the RaSCSI backend
"""
import logging
from time import sleep
from rascsi.exceptions import (EmptySocketChunkException,
InvalidProtobufResponse,
FailedSocketConnectionException)
class SocketCmds:
"""
Class for sending and receiving data over a socket connection with the RaSCSI backend
"""
def __init__(self, host="localhost", port=6868):
self.host = host
self.port = port
def send_pb_command(self, payload):
"""
Takes a (str) containing a serialized protobuf as argument.
Establishes a socket connection with RaSCSI.
"""
counter = 0
tries = 20
error_msg = ""
import socket
while counter < tries:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((self.host, self.port))
response = self.send_over_socket(sock, payload)
return response
except socket.error as error:
counter += 1
logging.warning("The RaSCSI service is not responding - attempt %s/%s",
str(counter), str(tries))
error_msg = str(error)
sleep(0.2)
except EmptySocketChunkException as ex:
raise ex
except InvalidProtobufResponse as ex:
raise ex
logging.error(error_msg)
raise FailedSocketConnectionException(error_msg)
# pylint: disable=no-self-use
def send_over_socket(self, sock, payload):
"""
Takes a socket object and (str) payload with serialized protobuf.
Sends payload to RaSCSI over socket and captures the response.
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")
# Prepending a little endian 32bit header with the message size
sock.send(pack("<i", len(payload)))
sock.send(payload)
# Receive the first 4 bytes to get the response header
response = sock.recv(4)
if len(response) >= 4:
# Extracting the response header to get the length of the response message
response_length = unpack("<i", response)[0]
# Reading in chunks, to handle a case where the response message is very large
chunks = []
bytes_recvd = 0
while bytes_recvd < response_length:
chunk = sock.recv(min(response_length - bytes_recvd, 2048))
if chunk == b'':
error_message = ("Read an empty chunk from the socket. Socket connection has "
"dropped unexpectedly. RaSCSI may have crashed.")
logging.error(error_message)
raise EmptySocketChunkException(error_message)
chunks.append(chunk)
bytes_recvd = bytes_recvd + len(chunk)
response_message = b''.join(chunks)
return response_message
error_message = ("The response from RaSCSI did not contain a protobuf header. "
"RaSCSI may have crashed.")
logging.error(error_message)
raise InvalidProtobufResponse(error_message)

View File

@ -1 +0,0 @@
../.pylintrc

View File

@ -33,17 +33,6 @@ The start.sh script can also be run standalone, and will handle the venv creatio
$ ./start.sh --rotation=180 --height=64
```
## Static analysis with pylint
It is recommended to run pylint against new code to protect against bugs
and keep the code readable and maintainable.
The local pylint configuration lives in .pylintrc (symlink to ../.pylintrc)
```
$ sudo apt install pylint3
$ pylint3 python_source_file.py
```
## Credits
### type_writer.ttf
* _Type Writer_ TrueType font by Mandy Smith

View File

View File

@ -3,7 +3,8 @@ Linux interrupt handling module
"""
import signal
class GracefulInterruptHandler():
class GracefulInterruptHandler:
"""
Class for handling Linux signal interrupts
"""

View File

@ -2,6 +2,7 @@
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

View File

@ -1,70 +0,0 @@
"""
Module for commands sent to the RaSCSI backend service.
"""
from os import path
from unidecode import unidecode
from socket_cmds import send_pb_command
import rascsi_interface_pb2 as proto
def device_list(token):
"""
Sends a DEVICES_INFO command to the server.
Returns a list of dicts with info on all attached devices.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICES_INFO
command.params["token"] = token
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
dlist = []
i = 0
while i < len(result.devices_info.devices):
did = result.devices_info.devices[i].id
dtype = proto.PbDeviceType.Name(result.devices_info.devices[i].type)
dstat = result.devices_info.devices[i].status
dprop = result.devices_info.devices[i].properties
# Building the status string
dstat_msg = []
if dstat.protected and dprop.protectable:
dstat_msg.append("Write-Protected")
if dstat.removed and dprop.removable:
dstat_msg.append("No Media")
if dstat.locked and dprop.lockable:
dstat_msg.append("Locked")
# Transliterate non-ASCII chars in the file name to ASCII
dfile = unidecode(path.basename(result.devices_info.devices[i].file.name))
dven = result.devices_info.devices[i].vendor
dprod = result.devices_info.devices[i].product
dlist.append({
"id": did,
"device_type": dtype,
"status": ", ".join(dstat_msg),
"file": dfile,
"vendor": dven,
"product": dprod,
})
i += 1
return dlist
def is_token_auth(token):
"""
Sends a CHECK_AUTHENTICATION command to the server.
Tells you whether RaSCSI backend is protected by a token password or not.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CHECK_AUTHENTICATION
command.params["token"] = token
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}

View File

@ -30,6 +30,7 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import argparse
import sys
from time import sleep
@ -39,7 +40,8 @@ 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 ractl_cmds import device_list, is_token_auth
from rascsi.ractl_cmds import RaCtlCmds
from rascsi.socket_cmds import SocketCmds
parser = argparse.ArgumentParser(description="RaSCSI OLED Monitor script")
parser.add_argument(
@ -65,6 +67,20 @@ parser.add_argument(
action="store",
help="Token password string for authenticating with RaSCSI",
)
parser.add_argument(
"--rascsi-host",
type=str,
default="localhost",
action="store",
help="RaSCSI host. Default: localhost",
)
parser.add_argument(
"--rascsi-port",
type=str,
default=6868,
action="store",
help="RaSCSI port. Default: 6868",
)
args = parser.parse_args()
if args.rotation == 0:
@ -81,6 +97,9 @@ elif args.height == 32:
TOKEN = args.password
sock_cmd = SocketCmds(host=args.rascsi_host, port=args.rascsi_port)
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=TOKEN)
WIDTH = 128
BORDER = 5
@ -148,10 +167,10 @@ def formatted_output():
Formats the strings to be displayed on the Screen
Returns a (list) of (str) output
"""
rascsi_list = device_list(TOKEN)
rascsi_list = ractl_cmd.list_devices()['device_list']
output = []
if not TOKEN and not is_token_auth(TOKEN)["status"]:
if not TOKEN and not ractl_cmd.is_token_auth()["status"]:
output.append("Permission denied!")
elif rascsi_list:
for line in rascsi_list:

View File

@ -1,73 +0,0 @@
"""
Module for handling socket connections for sending commands
and receiving results from the RaSCSI backend
"""
import socket
from struct import pack, unpack
from time import sleep
def send_pb_command(payload):
"""
Takes a (str) containing a serialized protobuf as argument.
Establishes a socket connection with RaSCSI.
"""
# Host and port number where rascsi is listening for socket connections
host = 'localhost'
port = 6868
counter = 0
tries = 20
error_msg = ""
while counter < tries:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((host, port))
return send_over_socket(sock, payload)
except socket.error as error:
counter += 1
print("The RaSCSI service is not responding - attempt %s/%s",
str(counter), str(tries))
error_msg = str(error)
sleep(0.2)
exit(error_msg)
def send_over_socket(sock, payload):
"""
Takes a socket object and (str) payload with serialized protobuf.
Sends payload to RaSCSI over socket and captures the response.
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.
"""
# Sending the magic word "RASCSI" to authenticate with the server
sock.send(b"RASCSI")
# Prepending a little endian 32bit header with the message size
sock.send(pack("<i", len(payload)))
sock.send(payload)
# Receive the first 4 bytes to get the response header
response = sock.recv(4)
if len(response) >= 4:
# Extracting the response header to get the length of the response message
response_length = unpack("<i", response)[0]
# Reading in chunks, to handle a case where the response message is very large
chunks = []
bytes_recvd = 0
while bytes_recvd < response_length:
chunk = sock.recv(min(response_length - bytes_recvd, 2048))
if chunk == b'':
exit("Socket connection has dropped unexpectedly. "
"RaSCSI may have crashed."
)
chunks.append(chunk)
bytes_recvd = bytes_recvd + len(chunk)
response_message = b''.join(chunks)
return response_message
exit("The response from RaSCSI did not contain a protobuf header. "
"RaSCSI may have crashed."
)

View File

@ -149,4 +149,7 @@ if [ -z ${HEIGHT+x} ]; then
else
echo "Starting with parameter $HEIGHT"
fi
PYTHON_COMMON_PATH=$(dirname $PWD)/common/src
export PYTHONPATH=$PWD/src:${PYTHON_COMMON_PATH}
python3 src/rascsi_oled_monitor.py ${ROTATION} ${HEIGHT} ${PASSWORD}

View File

@ -1 +0,0 @@
../.pylintrc

View File

@ -19,20 +19,6 @@ You may edit the files under `mock/bin` to simulate Linux command responses.
TODO: rascsi-web uses protobuf commands to send and receive data from rascsi.
A separate mocking solution will be needed for this interface.
### Static analysis with pylint
It is recommended to run pylint against new code to protect against bugs
and keep the code readable and maintainable.
The local pylint configuration lives in .pylintrc
In order for pylint to recognize venv libraries, the pylint-venv package is required.
```
sudo apt install pylint3
sudo pip install pylint-venv
source venv/bin/activate
pylint3 python_source_file.py
```
## Pushing to the Pi via git
Setup a bare repo on the rascsi

0
python/web/__init__.py Normal file
View File

View File

View File

@ -2,6 +2,7 @@
Module for RaSCSI device management utility methods
"""
def get_valid_scsi_ids(devices, reserved_ids):
"""
Takes a list of (dict)s devices, and list of (int)s reserved_ids.

View File

@ -1,538 +0,0 @@
"""
Module for methods reading from and writing to the file system
"""
import os
import logging
from pathlib import PurePath
from flask import current_app, session
from flask_babel import _
from ractl_cmds import (
get_server_info,
get_reserved_ids,
attach_image,
detach_all,
list_devices,
reserve_scsi_ids,
)
from pi_cmds import run_async
from socket_cmds import send_pb_command
from settings import CFG_DIR, CONFIG_FILE_SUFFIX, PROPERTIES_SUFFIX, RESERVATIONS
import rascsi_interface_pb2 as proto
def list_files(file_types, dir_path):
"""
Takes a (list) or (tuple) of (str) file_types - e.g. ('hda', 'hds')
Returns (list) of (list)s files_list:
index 0 is (str) file name and index 1 is (int) size in bytes
"""
files_list = []
for path, dirs, files in os.walk(dir_path):
# Only list selected file types
files = [f for f in files if f.lower().endswith(file_types)]
files_list.extend(
[
(
file,
os.path.getsize(os.path.join(path, file))
)
for file in files
]
)
return files_list
def list_config_files():
"""
Finds fils with file ending CONFIG_FILE_SUFFIX in CFG_DIR.
Returns a (list) of (str) files_list
"""
files_list = []
for root, dirs, files in os.walk(CFG_DIR):
for file in files:
if file.endswith("." + CONFIG_FILE_SUFFIX):
files_list.append(file)
return files_list
def list_images():
"""
Sends a IMAGE_FILES_INFO command to the server
Returns a (dict) with (bool) status, (str) msg, and (list) of (dict)s files
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
# Get a list of all *.properties files in CFG_DIR
prop_data = 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 = get_server_info()
files = []
for file in result.image_files_info.image_files:
# Add properties meta data for the image, if applicable
if file.name in prop_files:
process = read_drive_properties(f"{CFG_DIR}/{file.name}.{PROPERTIES_SUFFIX}")
prop = process["conf"]
else:
prop = False
if file.name.lower().endswith(".zip"):
zip_path = f"{server_info['image_dir']}/{file.name}"
if is_zipfile(zip_path):
zipfile = ZipFile(zip_path)
# Get a list of (str) containing all zipfile members
zip_members = zipfile.namelist()
# Strip out directories from the list
zip_members = [x for x in zip_members if not x.endswith("/")]
else:
logging.warning("%s is an invalid zip file", zip_path)
zip_members = False
else:
zip_members = False
size_mb = "{:,.1f}".format(file.size / 1024 / 1024)
dtype = proto.PbDeviceType.Name(file.type)
files.append({
"name": file.name,
"size": file.size,
"size_mb": size_mb,
"detected_type": dtype,
"prop": prop,
"zip_members": zip_members,
})
return {"status": result.status, "msg": result.msg, "files": files}
def create_new_image(file_name, file_type, size):
"""
Takes (str) file_name, (str) file_type, and (int) size
Sends a CREATE_IMAGE command to the server
Returns (dict) with (bool) status and (str) msg
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CREATE_IMAGE
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
command.params["file"] = file_name + "." + file_type
command.params["size"] = str(size)
command.params["read_only"] = "false"
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def delete_image(file_name):
"""
Takes (str) file_name
Sends a DELETE_IMAGE command to the server
Returns (dict) with (bool) status and (str) msg
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DELETE_IMAGE
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
command.params["file"] = file_name
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def rename_image(file_name, new_file_name):
"""
Takes (str) file_name, (str) new_file_name
Sends a RENAME_IMAGE command to the server
Returns (dict) with (bool) status and (str) msg
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.RENAME_IMAGE
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
command.params["from"] = file_name
command.params["to"] = new_file_name
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def delete_file(file_path):
"""
Takes (str) file_path with the full path to the file to delete
Returns (dict) with (bool) status and (str) msg
"""
if os.path.exists(file_path):
os.remove(file_path)
return {
"status": True,
"msg": _(u"File deleted: %(file_path)s", file_path=file_path),
}
return {
"status": False,
"msg": _(u"File to delete not found: %(file_path)s", file_path=file_path),
}
def rename_file(file_path, target_path):
"""
Takes (str) file_path and (str) target_path
Returns (dict) with (bool) status and (str) msg
"""
if os.path.exists(PurePath(target_path).parent):
os.rename(file_path, target_path)
return {
"status": True,
"msg": _(u"File moved to: %(target_path)s", target_path=target_path),
}
return {
"status": False,
"msg": _(u"Unable to move file to: %(target_path)s", target_path=target_path),
}
def unzip_file(file_name, member=False, members=False):
"""
Takes (str) file_name, optional (str) member, optional (list) of (str) members
file_name is the name of the zip file to unzip
member is the full path to the particular file in the zip file to unzip
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 = get_server_info()
prop_flag = False
if not member:
unzip_proc = run(run_async(
f"unzip -d {server_info['image_dir']} -n -j "
f"{server_info['image_dir']}/{file_name}"
))
if members:
for path in members:
if path.endswith(PROPERTIES_SUFFIX):
name = PurePath(path).name
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(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(run_async(
f"unzip -d {CFG_DIR} -n -j "
f"{server_info['image_dir']}/{file_name} {member}.{PROPERTIES_SUFFIX}"
))
if unzip_prop["returncode"] == 0:
prop_flag = True
if unzip_proc["returncode"] != 0:
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"]
)
return {"status": True, "msg": unzipped, "prop_flag": prop_flag}
def download_file_to_iso(url, *iso_args):
"""
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
import asyncio
server_info = get_server_info()
file_name = PurePath(url).name
tmp_ts = int(time())
tmp_dir = "/tmp/" + str(tmp_ts) + "/"
os.mkdir(tmp_dir)
tmp_full_path = tmp_dir + file_name
iso_filename = f"{server_info['image_dir']}/{file_name}.iso"
req_proc = download_to_dir(url, tmp_dir, file_name)
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.")
else:
logging.info(
"%s is a zipfile! Will attempt to unzip and store the resulting files.",
tmp_full_path,
)
unzip_proc = asyncio.run(run_async(
f"unzip -d {tmp_dir} -n {tmp_full_path}"
))
if not unzip_proc["returncode"]:
logging.info(
"%s was successfully unzipped. Deleting the zipfile.",
tmp_full_path,
)
delete_file(tmp_full_path)
try:
run(
[
"genisoimage",
*iso_args,
"-o",
iso_filename,
tmp_dir,
],
capture_output=True,
check=True,
)
except CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
return {"status": False, "msg": error.stderr.decode("utf-8")}
return {
"status": True,
"msg": _(
u"Created CD-ROM ISO image with arguments \"%(value)s\"",
value=" ".join(iso_args),
),
"file_name": iso_filename,
}
def download_to_dir(url, save_dir, file_name):
"""
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:
with requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"}) as req:
req.raise_for_status()
with open(f"{save_dir}/{file_name}", "wb") as download:
for chunk in req.iter_content(chunk_size=8192):
download.write(chunk)
except requests.exceptions.RequestException as error:
logging.warning("Request failed: %s", str(error))
return {"status": False, "msg": str(error)}
logging.info("Response encoding: %s", req.encoding)
logging.info("Response content-type: %s", req.headers["content-type"])
logging.info("Response status code: %s", req.status_code)
return {
"status": True,
"msg": _(
u"%(file_name)s downloaded to %(save_dir)s",
file_name=file_name,
save_dir=save_dir,
),
}
def write_config(file_name):
"""
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") as json_file:
version = get_server_info()["version"]
devices = list_devices()["device_list"]
for device in devices:
# Remove keys that we don't want to store in the file
del device["status"]
del device["file"]
# It's cleaner not to store an empty parameter for every device without media
if device["image"] == "":
device["image"] = None
# RaSCSI product names will be generated on the fly by RaSCSI
if device["vendor"] == "RaSCSI":
device["vendor"] = device["product"] = device["revision"] = None
# A block size of 0 is how RaSCSI indicates N/A for block size
if device["block_size"] == 0:
device["block_size"] = None
# Convert to a data type that can be serialized
device["params"] = dict(device["params"])
reserved_ids_and_memos = []
reserved_ids = get_reserved_ids()["ids"]
for scsi_id in reserved_ids:
reserved_ids_and_memos.append({"id": scsi_id, "memo": RESERVATIONS[int(scsi_id)]})
dump(
{"version": version, "devices": devices, "reserved_ids": reserved_ids_and_memos},
json_file,
indent=4
)
return {
"status": True,
"msg": _(u"Saved configuration file to %(file_name)s", file_name=file_name),
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
delete_file(file_name)
return {"status": False, "msg": str(error)}
except:
logging.error("Could not write to file: %s", file_name)
delete_file(file_name)
return {
"status": False,
"msg": _(u"Could not write to file: %(file_name)s", file_name=file_name),
}
def read_config(file_name):
"""
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) as json_file:
config = load(json_file)
# If the config file format changes again in the future,
# introduce more sophisticated format detection logic here.
if isinstance(config, dict):
detach_all()
ids_to_reserve = []
for item in config["reserved_ids"]:
ids_to_reserve.append(item["id"])
RESERVATIONS[int(item["id"])] = item["memo"]
reserve_scsi_ids(ids_to_reserve)
for row in config["devices"]:
kwargs = {
"device_type": row["device_type"],
"image": row["image"],
"unit": int(row["unit"]),
"vendor": row["vendor"],
"product": row["product"],
"revision": row["revision"],
"block_size": row["block_size"],
}
params = dict(row["params"])
for param in params.keys():
kwargs[param] = params[param]
attach_image(row["id"], **kwargs)
# The config file format in RaSCSI 21.10 is using a list data type at the top level.
# If future config file formats return to the list data type,
# introduce more sophisticated format detection logic here.
elif isinstance(config, list):
detach_all()
for row in config:
kwargs = {
"device_type": row["device_type"],
"image": row["image"],
# "un" for backwards compatibility
"unit": int(row["un"]),
"vendor": row["vendor"],
"product": row["product"],
"revision": row["revision"],
"block_size": row["block_size"],
}
params = dict(row["params"])
for param in params.keys():
kwargs[param] = params[param]
attach_image(row["id"], **kwargs)
else:
return {"status": False, "msg": _(u"Invalid configuration file format")}
return {
"status": True,
"msg": _(u"Loaded configurations from: %(file_name)s", file_name=file_name),
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
return {"status": False, "msg": str(error)}
except:
logging.error("Could not read file: %s", file_name)
return {
"status": False,
"msg": _(u"Could not read configuration file: %(file_name)s", file_name=file_name),
}
def write_drive_properties(file_name, conf):
"""
Writes a drive property configuration file to the config dir.
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:
dump(conf, json_file, indent=4)
return {
"status": True,
"msg": _(u"Created properties file: %(file_path)s", file_path=file_path),
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
delete_file(file_path)
return {"status": False, "msg": str(error)}
except:
logging.error("Could not write to file: %s", file_path)
delete_file(file_path)
return {
"status": False,
"msg": _(u"Could not write to properties file: %(file_path)s", file_path=file_path),
}
def read_drive_properties(file_path):
"""
Reads drive properties from json formatted file.
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)
return {
"status": True,
"msg": _(u"Read properties from file: %(file_path)s", file_path=file_path),
"conf": conf,
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
return {"status": False, "msg": str(error)}
except:
logging.error("Could not read file: %s", file_path)
return {
"status": False,
"msg": _(u"Could not read properties from file: %(file_path)s", file_path=file_path),
}

View File

@ -3,7 +3,6 @@ Module for methods controlling and getting information about the Pi's Linux syst
"""
import subprocess
import asyncio
import logging
from flask_babel import _
from settings import AUTH_GROUP
@ -132,7 +131,7 @@ def introspect_file(file_path, re_term):
"""
from re import match
try:
ifile = open(file_path, "r")
ifile = open(file_path, "r", encoding="ISO-8859-1")
except:
return False
for line in ifile:
@ -141,30 +140,6 @@ def introspect_file(file_path, re_term):
return False
async def run_async(cmd):
"""
Takes (str) cmd with the shell command to execute
Executes shell command and captures output
Returns (dict) with (int) returncode, (str) stdout, (str) stderr
"""
proc = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
stdout, stderr = await proc.communicate()
logging.info("Executed command \"%s\" with status code %d", cmd, proc.returncode)
if stdout:
stdout = stdout.decode()
logging.info("stdout: %s", stdout)
if stderr:
stderr = stderr.decode()
logging.info("stderr: %s", stderr)
return {"returncode": proc.returncode, "stdout": stdout, "stderr": stderr}
def auth_active():
"""
Inspects if the group defined in AUTH_GROUP exists on the system.
@ -176,6 +151,6 @@ def auth_active():
if AUTH_GROUP in groups:
return {
"status": True,
"msg": _(u"You must log in to use this function"),
"msg": _("You must log in to use this function"),
}
return {"status": False, "msg": ""}

View File

@ -1,465 +0,0 @@
"""
Module for commands sent to the RaSCSI backend service.
"""
from flask import current_app, session
from flask_babel import _
import rascsi_interface_pb2 as proto
from settings import REMOVABLE_DEVICE_TYPES
from socket_cmds import send_pb_command
def get_server_info():
"""
Sends a SERVER_INFO command to the server.
Returns a dict with:
- (bool) status
- (str) version (RaSCSI version number)
- (list) of (str) log_levels (the log levels RaSCSI supports)
- (str) current_log_level
- (list) of (int) reserved_ids
- (str) image_dir, path to the default images directory
- (int) scan_depth, the current images directory scan depth
- 5 distinct (list)s of (str)s with file endings recognized by RaSCSI
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.SERVER_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
version = str(result.server_info.version_info.major_version) + "." +\
str(result.server_info.version_info.minor_version) + "." +\
str(result.server_info.version_info.patch_version)
log_levels = result.server_info.log_level_info.log_levels
current_log_level = result.server_info.log_level_info.current_log_level
reserved_ids = list(result.server_info.reserved_ids_info.ids)
image_dir = result.server_info.image_files_info.default_image_folder
scan_depth = result.server_info.image_files_info.depth
# Creates lists of file endings recognized by RaSCSI
mappings = result.server_info.mapping_info.mapping
sahd = []
schd = []
scrm = []
scmo = []
sccd = []
for dtype in mappings:
if mappings[dtype] == proto.PbDeviceType.SAHD:
sahd.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCHD:
schd.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCRM:
scrm.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCMO:
scmo.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCCD:
sccd.append(dtype)
return {
"status": result.status,
"version": version,
"log_levels": log_levels,
"current_log_level": current_log_level,
"reserved_ids": reserved_ids,
"image_dir": image_dir,
"scan_depth": scan_depth,
"sahd": sahd,
"schd": schd,
"scrm": scrm,
"scmo": scmo,
"sccd": sccd,
}
def get_reserved_ids():
"""
Sends a RESERVED_IDS_INFO command to the server.
Returns a dict with:
- (bool) status
- (list) of (int) ids -- currently reserved SCSI IDs
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.RESERVED_IDS_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
scsi_ids = []
for scsi_id in result.reserved_ids_info.ids:
scsi_ids.append(str(scsi_id))
return {"status": result.status, "ids": scsi_ids}
def get_network_info():
"""
Sends a NETWORK_INTERFACES_INFO command to the server.
Returns a dict with:
- (bool) status
- (list) of (str) ifs (network interfaces detected by RaSCSI)
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.NETWORK_INTERFACES_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
ifs = result.network_interfaces_info.name
return {"status": result.status, "ifs": ifs}
def get_device_types():
"""
Sends a DEVICE_TYPES_INFO command to the server.
Returns a dict with:
- (bool) status
- (list) of (str) device_types (device types that RaSCSI supports, ex. SCHD, SCCD, etc)
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICE_TYPES_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
device_types = []
for prop in result.device_types_info.properties:
device_types.append(proto.PbDeviceType.Name(prop.type))
return {"status": result.status, "device_types": device_types}
def get_image_files_info():
"""
Sends a DEFAULT_IMAGE_FILES_INFO command to the server.
Returns a dict with:
- (bool) status
- (str) images_dir, path to images dir
- (list) of (str) image_files
- (int) scan_depth, the current scan depth
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
images_dir = result.image_files_info.default_image_folder
image_files = result.image_files_info.image_files
scan_depth = result.image_files_info.depth
return {
"status": result.status,
"images_dir": images_dir,
"image_files": image_files,
"scan_depth": scan_depth,
}
def attach_image(scsi_id, **kwargs):
"""
Takes (int) scsi_id and kwargs containing 0 or more device properties
If the current attached device is a removable device wihout media inserted,
this sends a INJECT command to the server.
If there is no currently attached device, this sends the ATTACH command to the server.
Returns (bool) status and (str) msg
"""
command = proto.PbCommand()
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if "device_type" in kwargs.keys():
if kwargs["device_type"] not in [None, ""]:
devices.type = proto.PbDeviceType.Value(str(kwargs["device_type"]))
if "unit" in kwargs.keys():
if kwargs["unit"] not in [None, ""]:
devices.unit = kwargs["unit"]
if "image" in kwargs.keys():
if kwargs["image"] not in [None, ""]:
devices.params["file"] = kwargs["image"]
# Handling the inserting of media into an attached removable type device
device_type = kwargs.get("device_type", None)
currently_attached = list_devices(scsi_id, kwargs.get("unit"))["device_list"]
if currently_attached:
current_type = currently_attached[0]["device_type"]
else:
current_type = None
if device_type in REMOVABLE_DEVICE_TYPES and current_type in REMOVABLE_DEVICE_TYPES:
if current_type != device_type:
return {
"status": False,
"msg": _(
u"Cannot insert an image for %(device_type)s into a "
u"%(current_device_type)s device",
device_type=device_type,
current_device_type=current_type
),
}
command.operation = proto.PbOperation.INSERT
# Handling attaching a new device
else:
command.operation = proto.PbOperation.ATTACH
if "interfaces" in kwargs.keys():
if kwargs["interfaces"] not in [None, ""]:
devices.params["interfaces"] = kwargs["interfaces"]
if "vendor" in kwargs.keys():
if kwargs["vendor"] is not None:
devices.vendor = kwargs["vendor"]
if "product" in kwargs.keys():
if kwargs["product"] is not None:
devices.product = kwargs["product"]
if "revision" in kwargs.keys():
if kwargs["revision"] is not None:
devices.revision = kwargs["revision"]
if "block_size" in kwargs.keys():
if kwargs["block_size"] not in [None, ""]:
devices.block_size = int(kwargs["block_size"])
command.devices.append(devices)
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def detach_by_id(scsi_id, unit=None):
"""
Takes (int) scsi_id and optional (int) unit.
Sends a DETACH command to the server.
Returns (bool) status and (str) msg.
"""
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if unit is not None:
devices.unit = int(unit)
command = proto.PbCommand()
command.operation = proto.PbOperation.DETACH
command.devices.append(devices)
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def detach_all():
"""
Sends a DETACH_ALL command to the server.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DETACH_ALL
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def eject_by_id(scsi_id, unit=None):
"""
Takes (int) scsi_id and optional (int) unit.
Sends an EJECT command to the server.
Returns (bool) status and (str) msg.
"""
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if unit is not None:
devices.unit = int(unit)
command = proto.PbCommand()
command.operation = proto.PbOperation.EJECT
command.devices.append(devices)
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def list_devices(scsi_id=None, unit=None):
"""
Takes optional (int) scsi_id and optional (int) unit.
Sends a DEVICES_INFO command to the server.
If no scsi_id is provided, returns a (list) of (dict)s of all attached devices.
If scsi_id is is provided, returns a (list) of one (dict) for the given device.
If no attached device is found, returns an empty (list).
Returns (bool) status, (list) of dicts device_list
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICES_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
# If method is called with scsi_id parameter, return the info on those devices
# Otherwise, return the info on all attached devices
if scsi_id is not None:
device = proto.PbDeviceDefinition()
device.id = int(scsi_id)
if unit is not None:
device.unit = int(unit)
command.devices.append(device)
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
device_list = []
# Return an empty (list) if no devices are attached
if not result.devices_info.devices:
return {"status": False, "device_list": []}
image_files_info = get_image_files_info()
i = 0
while i < len(result.devices_info.devices):
did = result.devices_info.devices[i].id
dunit = result.devices_info.devices[i].unit
dtype = proto.PbDeviceType.Name(result.devices_info.devices[i].type)
dstat = result.devices_info.devices[i].status
dprop = result.devices_info.devices[i].properties
# Building the status string
dstat_msg = []
if dprop.read_only:
dstat_msg.append("Read-Only")
if dstat.protected and dprop.protectable:
dstat_msg.append("Write-Protected")
if dstat.removed and dprop.removable:
dstat_msg.append("No Media")
if dstat.locked and dprop.lockable:
dstat_msg.append("Locked")
dpath = result.devices_info.devices[i].file.name
dfile = dpath.replace(image_files_info["images_dir"] + "/", "")
dparam = result.devices_info.devices[i].params
dven = result.devices_info.devices[i].vendor
dprod = result.devices_info.devices[i].product
drev = result.devices_info.devices[i].revision
dblock = result.devices_info.devices[i].block_size
dsize = int(result.devices_info.devices[i].block_count) * int(dblock)
device_list.append({
"id": did,
"unit": dunit,
"device_type": dtype,
"status": ", ".join(dstat_msg),
"image": dpath,
"file": dfile,
"params": dparam,
"vendor": dven,
"product": dprod,
"revision": drev,
"block_size": dblock,
"size": dsize,
})
i += 1
return {"status": result.status, "msg": result.msg, "device_list": device_list}
def reserve_scsi_ids(reserved_scsi_ids):
"""
Sends the RESERVE_IDS command to the server to reserve SCSI IDs.
Takes a (list) of (str) as argument.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.RESERVE_IDS
command.params["ids"] = ",".join(reserved_scsi_ids)
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def set_log_level(log_level):
"""
Sends a LOG_LEVEL command to the server.
Takes (str) log_level as an argument.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.LOG_LEVEL
command.params["level"] = str(log_level)
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def shutdown_pi(mode):
"""
Sends a SHUT_DOWN command to the server.
Takes (str) mode as an argument.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.SHUT_DOWN
command.params["mode"] = str(mode)
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def is_token_auth():
"""
Sends a CHECK_AUTHENTICATION command to the server.
Tells you whether RaSCSI backend is protected by a token password or not.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CHECK_AUTHENTICATION
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}

View File

@ -0,0 +1,45 @@
"""Module for mapping between rascsi return codes and translated strings"""
from rascsi.return_codes import ReturnCodes
from flask_babel import _
# pylint: disable=too-few-public-methods
class ReturnCodeMapper:
"""Class for mapping between rascsi return codes and translated strings"""
MESSAGES = {
ReturnCodes.DELETEFILE_SUCCESS: _("File deleted: %(file_path)s"),
ReturnCodes.DELETEFILE_FILE_NOT_FOUND: _("File to delete not found: %(file_path)s"),
ReturnCodes.RENAMEFILE_SUCCESS: _("File moved to: %(target_path)s"),
ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE: _("Unable to move file to: %(target_path)s"),
ReturnCodes.DOWNLOADFILETOISO_SUCCESS: _("Created CD-ROM ISO image with "
"arguments \"%(value)s\""),
ReturnCodes.DOWNLOADTODIR_SUCCESS: _("%(file_name)s downloaded to %(save_dir)s"),
ReturnCodes.WRITECONFIG_SUCCESS: _("Saved configuration file to %(file_name)s"),
ReturnCodes.WRITECONFIG_COULD_NOT_WRITE: _("Could not write to file: %(file_name)s"),
ReturnCodes.READCONFIG_SUCCESS: _("Loaded configurations from: %(file_name)s"),
ReturnCodes.READCONFIG_COULD_NOT_READ: _("Could not read configuration "
"file: %(file_name)s"),
ReturnCodes.READCONFIG_INVALID_CONFIG_FILE_FORMAT: _("Invalid configuration file format"),
ReturnCodes.WRITEDRIVEPROPS_SUCCESS: _("Created properties file: %(file_path)s"),
ReturnCodes.WRITEDRIVEPROPS_COULD_NOT_WRITE: _("Could not write to properties "
"file: %(file_path)s"),
ReturnCodes.READDRIVEPROPS_SUCCESS: _("Read properties from file: %(file_path)s"),
ReturnCodes.READDRIVEPROPS_COULD_NOT_READ: _("Could not read properties from "
"file: %(file_path)s"),
ReturnCodes.ATTACHIMAGE_COULD_NOT_ATTACH: _("Cannot insert an image for %(device_type)s "
"into a %(current_device_type)s device"),
}
@staticmethod
def add_msg(payload):
"""adds a msg key to a given payload with a rascsi module return code
with a translated return code message string. """
if "return_code" not in payload:
return payload
parameters = payload["parameters"]
payload["msg"] = _(ReturnCodeMapper.MESSAGES[payload["return_code"]], **parameters)
return payload

View File

@ -3,31 +3,22 @@ Constant definitions used by other modules
"""
from os import getenv, getcwd
import rascsi.common_settings
WEB_DIR = getcwd()
# There may be a more elegant way to get the HOME dir of the user that installed RaSCSI
HOME_DIR = "/".join(WEB_DIR.split("/")[0:3])
CFG_DIR = f"{HOME_DIR}/.config/rascsi"
AFP_DIR = f"{HOME_DIR}/afpshare"
MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", str(1024 * 1024 * 1024 * 4)) # 4gb
ARCHIVE_FILE_SUFFIX = "zip"
CONFIG_FILE_SUFFIX = "json"
# File ending used for drive properties files
PROPERTIES_SUFFIX = "properties"
# The file name of the default config file that loads when rascsi-web starts
DEFAULT_CONFIG = f"default.{CONFIG_FILE_SUFFIX}"
DEFAULT_CONFIG = f"default.{rascsi.common_settings.CONFIG_FILE_SUFFIX}"
# File containing canonical drive properties
DRIVE_PROPERTIES_FILE = WEB_DIR + "/drive_properties.json"
REMOVABLE_DEVICE_TYPES = ("SCCD", "SCRM", "SCMO")
# The RESERVATIONS list is used to keep track of the reserved ID memos.
# Initialize with a list of 8 empty strings.
RESERVATIONS = ["" for x in range(0, 8)]
# The user group that is used for webapp authentication
AUTH_GROUP = "rascsi"

View File

@ -1,100 +0,0 @@
"""
Module for sending and receiving data over a socket connection with the RaSCSI backend
"""
import logging
from time import sleep
from flask import abort
from flask_babel import _
def send_pb_command(payload):
"""
Takes a (str) containing a serialized protobuf as argument.
Establishes a socket connection with RaSCSI.
"""
# Host and port number where rascsi is listening for socket connections
host = 'localhost'
port = 6868
counter = 0
tries = 20
error_msg = ""
import socket
while counter < tries:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((host, port))
return send_over_socket(sock, payload)
except socket.error as error:
counter += 1
logging.warning("The RaSCSI service is not responding - attempt %s/%s",
str(counter), str(tries))
error_msg = str(error)
sleep(0.2)
logging.error(error_msg)
# After failing all attempts, throw a 404 error
abort(404, _(
u"The RaSCSI Web Interface failed to connect to RaSCSI at %(host)s:%(port)s "
u"with error: %(error_msg)s. The RaSCSI process is not running or may have crashed.",
host=host, port=port, error_msg=error_msg,
)
)
def send_over_socket(sock, payload):
"""
Takes a socket object and (str) payload with serialized protobuf.
Sends payload to RaSCSI over socket and captures the response.
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")
# Prepending a little endian 32bit header with the message size
sock.send(pack("<i", len(payload)))
sock.send(payload)
# Receive the first 4 bytes to get the response header
response = sock.recv(4)
if len(response) >= 4:
# Extracting the response header to get the length of the response message
response_length = unpack("<i", response)[0]
# Reading in chunks, to handle a case where the response message is very large
chunks = []
bytes_recvd = 0
while bytes_recvd < response_length:
chunk = sock.recv(min(response_length - bytes_recvd, 2048))
if chunk == b'':
logging.error(
"Read an empty chunk from the socket. "
"Socket connection has dropped unexpectedly. "
"RaSCSI may have crashed."
)
abort(
503, _(
u"The RaSCSI Web Interface lost connection to RaSCSI. "
u"Please go back and try again. "
u"If the issue persists, please report a bug."
)
)
chunks.append(chunk)
bytes_recvd = bytes_recvd + len(chunk)
response_message = b''.join(chunks)
return response_message
logging.error(
"The response from RaSCSI did not contain a protobuf header. "
"RaSCSI may have crashed."
)
abort(
500, _(
u"The RaSCSI Web Interface did not get a valid response from RaSCSI. "
u"Please go back and try again. "
u"If the issue persists, please report a bug."
)
)

View File

@ -0,0 +1,60 @@
"""
Module for sending and receiving data over a socket connection with the RaSCSI backend
"""
from flask import abort
from flask_babel import _
from rascsi.exceptions import (EmptySocketChunkException,
InvalidProtobufResponse,
FailedSocketConnectionException)
from rascsi.socket_cmds import SocketCmds
class SocketCmdsFlask(SocketCmds):
"""
Class for sending and receiving data over a socket connection with the RaSCSI backend
"""
# pylint: disable=useless-super-delegation
def __init__(self, host="localhost", port=6868):
super().__init__(host, port)
def send_pb_command(self, payload):
"""
Takes a (str) containing a serialized protobuf as argument.
Establishes a socket connection with RaSCSI.
"""
try:
return super().send_pb_command(payload)
except FailedSocketConnectionException as err:
# After failing all attempts, throw a 404 error
abort(404, _(
"The RaSCSI Web Interface failed to connect to RaSCSI at %(host)s:%(port)s "
"with error: %(error_msg)s. The RaSCSI process is not running or may have crashed.",
host=self.host, port=self.port, error_msg=str(err),
)
)
return None
def send_over_socket(self, sock, payload):
"""Sends a payload over a given socket"""
try:
return super().send_over_socket(sock, payload)
except EmptySocketChunkException:
abort(
503, _(
"The RaSCSI Web Interface lost connection to RaSCSI. "
"Please go back and try again. "
"If the issue persists, please report a bug."
)
)
return None
except InvalidProtobufResponse:
abort(
500, _(
"The RaSCSI Web Interface did not get a valid response from RaSCSI. "
"Please go back and try again. "
"If the issue persists, please report a bug."
)
)
return None

View File

@ -23,22 +23,6 @@ from flask import (
)
from flask_babel import Babel, Locale, refresh, _
from file_cmds import (
list_images,
list_config_files,
create_new_image,
download_file_to_iso,
delete_image,
rename_image,
delete_file,
rename_file,
unzip_file,
download_to_dir,
write_config,
read_config,
write_drive_properties,
read_drive_properties,
)
from pi_cmds import (
running_env,
running_proc,
@ -48,43 +32,40 @@ from pi_cmds import (
introspect_file,
auth_active,
)
from ractl_cmds import (
attach_image,
list_devices,
detach_by_id,
eject_by_id,
detach_all,
get_server_info,
get_reserved_ids,
get_network_info,
get_device_types,
reserve_scsi_ids,
set_log_level,
shutdown_pi,
is_token_auth,
)
from device_utils import (
sort_and_format_devices,
get_valid_scsi_ids,
)
from return_code_mapper import ReturnCodeMapper
from settings import (
CFG_DIR,
AFP_DIR,
MAX_FILE_SIZE,
ARCHIVE_FILE_SUFFIX,
CONFIG_FILE_SUFFIX,
PROPERTIES_SUFFIX,
DEFAULT_CONFIG,
DRIVE_PROPERTIES_FILE,
REMOVABLE_DEVICE_TYPES,
RESERVATIONS,
AUTH_GROUP,
LANGUAGES,
)
from rascsi.common_settings import (
CFG_DIR,
CONFIG_FILE_SUFFIX,
PROPERTIES_SUFFIX,
REMOVABLE_DEVICE_TYPES,
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)
@BABEL.localeselector
def get_locale():
"""
@ -117,22 +98,22 @@ def index():
"""
Sets up data structures for and renders the index page
"""
if not is_token_auth()["status"] and not APP.config["TOKEN"]:
if not ractl.is_token_auth()["status"] and not APP.config["TOKEN"]:
abort(
403,
_(
u"RaSCSI is password protected. "
u"Start the Web Interface with the --password parameter."
"RaSCSI is password protected. "
"Start the Web Interface with the --password parameter."
),
)
locales = get_supported_locales()
server_info = get_server_info()
server_info = ractl.get_server_info()
disk = disk_space()
devices = list_devices()
device_types = get_device_types()
image_files = list_images()
config_files = list_config_files()
devices = ractl.list_devices()
device_types = ractl.get_device_types()
image_files = file_cmds.list_images()
config_files = file_cmds.list_config_files()
sorted_image_files = sorted(image_files["files"], key=lambda x: x["name"].lower())
sorted_config_files = sorted(config_files, key=lambda x: x.lower())
@ -187,7 +168,7 @@ def index():
version=server_info["version"],
log_levels=server_info["log_levels"],
current_log_level=server_info["current_log_level"],
netinfo=get_network_info(),
netinfo=ractl.get_network_info(),
device_types=device_types["device_types"],
free_disk=int(disk["free"] / 1024 / 1024),
valid_file_suffix=valid_file_suffix,
@ -207,14 +188,15 @@ def drive_list():
"""
Sets up the data structures and kicks off the rendering of the drive list page
"""
server_info = get_server_info()
server_info = ractl.get_server_info()
disk = disk_space()
# Reads the canonical drive properties into a dict
# The file resides in the current dir of the web ui process
drive_properties = Path(DRIVE_PROPERTIES_FILE)
if drive_properties.is_file():
process = read_drive_properties(str(drive_properties))
process = file_cmds.read_drive_properties(str(drive_properties))
process = ReturnCodeMapper.add_msg(process)
if not process["status"]:
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -247,7 +229,7 @@ def drive_list():
device["size_mb"] = "{:,.2f}".format(device["size"] / 1024 / 1024)
rm_conf.append(device)
files = list_images()
files = file_cmds.list_images()
sorted_image_files = sorted(files["files"], key=lambda x: x["name"].lower())
hd_conf = sorted(hd_conf, key=lambda x: x["name"].lower())
cd_conf = sorted(cd_conf, key=lambda x: x["name"].lower())
@ -292,7 +274,7 @@ def login():
return redirect(url_for("index"))
flash(
_(
u"You must log in with credentials for a user in the '%(group)s' group",
"You must log in with credentials for a user in the '%(group)s' group",
group=AUTH_GROUP,
),
"error",
@ -347,9 +329,9 @@ def drive_create():
full_file_name = file_name + "." + file_type
# Creating the image file
process = create_new_image(file_name, file_type, size)
process = file_cmds.create_new_image(file_name, file_type, size)
if process["status"]:
flash(_(u"Image file created: %(file_name)s", file_name=full_file_name))
flash(_("Image file created: %(file_name)s", file_name=full_file_name))
else:
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -362,7 +344,8 @@ def drive_create():
"revision": revision,
"block_size": block_size,
}
process = write_drive_properties(prop_file_name, properties)
process = file_cmds.write_drive_properties(prop_file_name, properties)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
@ -391,7 +374,8 @@ def drive_cdrom():
"revision": revision,
"block_size": block_size,
}
process = write_drive_properties(file_name, properties)
process = file_cmds.write_drive_properties(file_name, properties)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
@ -409,7 +393,8 @@ def config_save():
file_name = request.form.get("name") or "default"
file_name = f"{file_name}.{CONFIG_FILE_SUFFIX}"
process = write_config(file_name)
process = file_cmds.write_config(file_name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
@ -427,7 +412,8 @@ def config_load():
file_name = request.form.get("name")
if "load" in request.form:
process = read_config(file_name)
process = file_cmds.read_config(file_name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
@ -435,7 +421,8 @@ def config_load():
flash(process['msg'], "error")
return redirect(url_for("index"))
if "delete" in request.form:
process = delete_file(f"{CFG_DIR}/{file_name}")
process = file_cmds.delete_file(f"{CFG_DIR}/{file_name}")
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
@ -474,7 +461,7 @@ def show_logs():
headers = {"content-type": "text/plain"}
return process.stdout.decode("utf-8"), int(lines), headers
flash(_(u"An error occurred when fetching logs."))
flash(_("An error occurred when fetching logs."))
flash(process.stderr.decode("utf-8"), "stderr")
return redirect(url_for("index"))
@ -487,9 +474,9 @@ def log_level():
"""
level = request.form.get("level") or "info"
process = set_log_level(level)
process = ractl.set_log_level(level)
if process["status"]:
flash(_(u"Log level set to %(value)s", value=level))
flash(_("Log level set to %(value)s", value=level))
return redirect(url_for("index"))
flash(process["msg"], "error")
@ -508,24 +495,24 @@ def daynaport_attach():
mask = request.form.get("mask")
error_url = "https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link"
error_msg = _(u"Please follow the instructions at %(url)s", url=error_url)
error_msg = _("Please follow the instructions at %(url)s", url=error_url)
if interface.startswith("wlan"):
if not introspect_file("/etc/sysctl.conf", r"^net\.ipv4\.ip_forward=1$"):
flash(_(u"Configure IPv4 forwarding before using a wireless network device."), "error")
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(_(u"Configure NAT before using a wireless network device."), "error")
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 " + interface + r"$"):
flash(_(u"Configure the network bridge before using a wired network device."), "error")
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(_(u"Configure the network bridge before using a wired network device."), "error")
flash(_("Configure the network bridge before using a wired network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index"))
@ -536,9 +523,10 @@ def daynaport_attach():
arg += (":" + ip_addr + "/" + mask)
kwargs["interfaces"] = arg
process = attach_image(scsi_id, **kwargs)
process = ractl.attach_image(scsi_id, **kwargs)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(_(u"Attached DaynaPORT to SCSI ID %(id_number)s", id_number=scsi_id))
flash(_("Attached DaynaPORT to SCSI ID %(id_number)s", id_number=scsi_id))
return redirect(url_for("index"))
flash(process["msg"], "error")
@ -573,7 +561,8 @@ def attach():
# same file name with PROPERTIES_SUFFIX appended
drive_properties = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
if Path(drive_properties).is_file():
process = read_drive_properties(drive_properties)
process = file_cmds.read_drive_properties(drive_properties)
process = ReturnCodeMapper.add_msg(process)
if not process["status"]:
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -584,18 +573,19 @@ def attach():
kwargs["block_size"] = conf["block_size"]
expected_block_size = conf["block_size"]
process = attach_image(scsi_id, **kwargs)
process = ractl.attach_image(scsi_id, **kwargs)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(_(u"Attached %(file_name)s to SCSI ID %(id_number)s LUN %(unit_number)s",
flash(_("Attached %(file_name)s to SCSI ID %(id_number)s LUN %(unit_number)s",
file_name=file_name, id_number=scsi_id, unit_number=unit))
if int(file_size) % int(expected_block_size):
flash(_(u"The image file size %(file_size)s bytes is not a multiple of "
flash(_("The image file size %(file_size)s bytes is not a multiple of "
u"%(block_size)s. RaSCSI will ignore the trailing data. "
u"The image may be corrupted, so proceed with caution.",
file_size=file_size, block_size=expected_block_size), "error")
return redirect(url_for("index"))
flash(_(u"Failed to attach %(file_name)s to SCSI ID %(id_number)s LUN %(unit_number)s",
flash(_("Failed to attach %(file_name)s to SCSI ID %(id_number)s LUN %(unit_number)s",
file_name=file_name, id_number=scsi_id, unit_number=unit), "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -607,9 +597,9 @@ def detach_all_devices():
"""
Detaches all currently attached devices
"""
process = detach_all()
process = ractl.detach_all()
if process["status"]:
flash(_(u"Detached all SCSI devices"))
flash(_("Detached all SCSI devices"))
return redirect(url_for("index"))
flash(process["msg"], "error")
@ -624,13 +614,13 @@ def detach():
"""
scsi_id = request.form.get("scsi_id")
unit = request.form.get("unit")
process = detach_by_id(scsi_id, unit)
process = ractl.detach_by_id(scsi_id, unit)
if process["status"]:
flash(_(u"Detached SCSI ID %(id_number)s LUN %(unit_number)s",
flash(_("Detached SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id, unit_number=unit))
return redirect(url_for("index"))
flash(_(u"Failed to detach SCSI ID %(id_number)s LUN %(unit_number)s",
flash(_("Failed to detach SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id, unit_number=unit), "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -645,13 +635,13 @@ def eject():
scsi_id = request.form.get("scsi_id")
unit = request.form.get("unit")
process = eject_by_id(scsi_id, unit)
process = ractl.eject_by_id(scsi_id, unit)
if process["status"]:
flash(_(u"Ejected SCSI ID %(id_number)s LUN %(unit_number)s",
flash(_("Ejected SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id, unit_number=unit))
return redirect(url_for("index"))
flash(_(u"Failed to eject SCSI ID %(id_number)s LUN %(unit_number)s",
flash(_("Failed to eject SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id, unit_number=unit), "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -664,7 +654,7 @@ def device_info():
scsi_id = request.form.get("scsi_id")
unit = request.form.get("unit")
devices = list_devices(scsi_id, unit)
devices = ractl.list_devices(scsi_id, unit)
# First check if any device at all was returned
if not devices["status"]:
@ -674,19 +664,19 @@ def device_info():
# the one and only device that should have been returned
device = devices["device_list"][0]
if str(device["id"]) == scsi_id:
flash(_(u"DEVICE INFO"))
flash(_("DEVICE INFO"))
flash("===========")
flash(_(u"SCSI ID: %(id_number)s", id_number=device["id"]))
flash(_(u"LUN: %(unit_number)s", unit_number=device["unit"]))
flash(_(u"Type: %(device_type)s", device_type=device["device_type"]))
flash(_(u"Status: %(device_status)s", device_status=device["status"]))
flash(_(u"File: %(image_file)s", image_file=device["image"]))
flash(_(u"Parameters: %(value)s", value=device["params"]))
flash(_(u"Vendor: %(value)s", value=device["vendor"]))
flash(_(u"Product: %(value)s", value=device["product"]))
flash(_(u"Revision: %(revision_number)s", revision_number=device["revision"]))
flash(_(u"Block Size: %(value)s bytes", value=device["block_size"]))
flash(_(u"Image Size: %(value)s bytes", value=device["size"]))
flash(_("SCSI ID: %(id_number)s", id_number=device["id"]))
flash(_("LUN: %(unit_number)s", unit_number=device["unit"]))
flash(_("Type: %(device_type)s", device_type=device["device_type"]))
flash(_("Status: %(device_status)s", device_status=device["status"]))
flash(_("File: %(image_file)s", image_file=device["image"]))
flash(_("Parameters: %(value)s", value=device["params"]))
flash(_("Vendor: %(value)s", value=device["vendor"]))
flash(_("Product: %(value)s", value=device["product"]))
flash(_("Revision: %(revision_number)s", revision_number=device["revision"]))
flash(_("Block Size: %(value)s bytes", value=device["block_size"]))
flash(_("Image Size: %(value)s bytes", value=device["size"]))
return redirect(url_for("index"))
flash(devices["msg"], "error")
@ -700,15 +690,15 @@ def reserve_id():
"""
scsi_id = request.form.get("scsi_id")
memo = request.form.get("memo")
reserved_ids = get_reserved_ids()["ids"]
reserved_ids = ractl.get_reserved_ids()["ids"]
reserved_ids.extend(scsi_id)
process = reserve_scsi_ids(reserved_ids)
process = ractl.reserve_scsi_ids(reserved_ids)
if process["status"]:
RESERVATIONS[int(scsi_id)] = memo
flash(_(u"Reserved SCSI ID %(id_number)s", id_number=scsi_id))
flash(_("Reserved SCSI ID %(id_number)s", id_number=scsi_id))
return redirect(url_for("index"))
flash(_(u"Failed to reserve SCSI ID %(id_number)s", id_number=scsi_id))
flash(_("Failed to reserve SCSI ID %(id_number)s", id_number=scsi_id))
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -719,15 +709,15 @@ def unreserve_id():
Removes the reservation of a SCSI ID as well as the memo for the reservation
"""
scsi_id = request.form.get("scsi_id")
reserved_ids = get_reserved_ids()["ids"]
reserved_ids = ractl.get_reserved_ids()["ids"]
reserved_ids.remove(scsi_id)
process = reserve_scsi_ids(reserved_ids)
process = ractl.reserve_scsi_ids(reserved_ids)
if process["status"]:
RESERVATIONS[int(scsi_id)] = ""
flash(_(u"Released the reservation for SCSI ID %(id_number)s", id_number=scsi_id))
flash(_("Released the reservation for SCSI ID %(id_number)s", id_number=scsi_id))
return redirect(url_for("index"))
flash(_(u"Failed to release the reservation for SCSI ID %(id_number)s", id_number=scsi_id))
flash(_("Failed to release the reservation for SCSI ID %(id_number)s", id_number=scsi_id))
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -738,7 +728,7 @@ def restart():
"""
Restarts the Pi
"""
shutdown_pi("reboot")
ractl.shutdown_pi("reboot")
return redirect(url_for("index"))
@ -748,7 +738,7 @@ def shutdown():
"""
Shuts down the Pi
"""
shutdown_pi("system")
ractl.shutdown_pi("system")
return redirect(url_for("index"))
@ -762,21 +752,23 @@ def download_to_iso():
url = request.form.get("url")
iso_args = request.form.get("type").split()
process = download_file_to_iso(url, *iso_args)
process = file_cmds.download_file_to_iso(url, *iso_args)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
flash(_(u"Saved image as: %(file_name)s", file_name=process['file_name']))
flash(_("Saved image as: %(file_name)s", file_name=process['file_name']))
else:
flash(_(u"Failed to create CD-ROM image from %(url)s", url=url), "error")
flash(_("Failed to create CD-ROM image from %(url)s", url=url), "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
process_attach = attach_image(scsi_id, device_type="SCCD", image=process["file_name"])
process_attach = ractl.attach_image(scsi_id, device_type="SCCD", image=process["file_name"])
process_attach = ReturnCodeMapper.add_msg(process_attach)
if process_attach["status"]:
flash(_(u"Attached to SCSI ID %(id_number)s", id_number=scsi_id))
flash(_("Attached to SCSI ID %(id_number)s", id_number=scsi_id))
return redirect(url_for("index"))
flash(_(u"Failed to attach image to SCSI ID %(id_number)s. Try attaching it manually.",
flash(_("Failed to attach image to SCSI ID %(id_number)s. Try attaching it manually.",
id_number=scsi_id), "error")
flash(process_attach["msg"], "error")
return redirect(url_for("index"))
@ -789,13 +781,14 @@ def download_img():
Downloads a remote file onto the images dir on the Pi
"""
url = request.form.get("url")
server_info = get_server_info()
process = download_to_dir(url, server_info["image_dir"], Path(url).name)
server_info = ractl.get_server_info()
process = file_cmds.download_to_dir(url, server_info["image_dir"], Path(url).name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
flash(_(u"Failed to download file from %(url)s", url=url), "error")
flash(_("Failed to download file from %(url)s", url=url), "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -819,12 +812,13 @@ def download_afp():
stem_remainder = file_name_stem[:(26 - len(file_name_suffix))]
appendix_hash = (md5(discarded_portion.encode("utf-8"))).hexdigest().upper()
file_name = stem_remainder + "#" + appendix_hash[:4] + file_name_suffix
process = download_to_dir(url, AFP_DIR, file_name)
process = file_cmds.download_to_dir(url, AFP_DIR, file_name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
flash(_(u"Failed to download file from %(url)s", url=url), "error")
flash(_("Failed to download file from %(url)s", url=url), "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -847,7 +841,7 @@ def upload_file():
file_object = request.files["file"]
file_name = secure_filename(file_object.filename)
server_info = get_server_info()
server_info = ractl.get_server_info()
save_path = path.join(server_info["image_dir"], file_name)
current_chunk = int(request.form['dzchunkindex'])
@ -855,7 +849,7 @@ def upload_file():
# 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(_(u"The file already exists!"), 400)
return make_response(_("The file already exists!"), 400)
try:
with open(save_path, "ab") as save:
@ -863,7 +857,7 @@ def upload_file():
save.write(file_object.stream.read())
except OSError:
log.exception("Could not write to file")
return make_response(_(u"Unable to write the file to disk!"), 500)
return make_response(_("Unable to write the file to disk!"), 500)
total_chunks = int(request.form["dztotalchunkcount"])
@ -878,13 +872,13 @@ def upload_file():
path.getsize(save_path),
request.form['dztotalfilesize'],
)
return make_response(_(u"Transferred file corrupted!"), 500)
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(_(u"File upload successful!"), 200)
return make_response(_("File upload successful!"), 200)
@APP.route("/files/create", methods=["POST"])
@ -898,9 +892,9 @@ def create_file():
file_type = request.form.get("type")
full_file_name = file_name + "." + file_type
process = create_new_image(file_name, file_type, size)
process = file_cmds.create_new_image(file_name, file_type, size)
if process["status"]:
flash(_(u"Image file created: %(file_name)s", file_name=full_file_name))
flash(_("Image file created: %(file_name)s", file_name=full_file_name))
return redirect(url_for("index"))
flash(process["msg"], "error")
@ -925,9 +919,9 @@ def delete():
"""
file_name = request.form.get("file_name")
process = delete_image(file_name)
process = file_cmds.delete_image(file_name)
if process["status"]:
flash(_(u"Image file deleted: %(file_name)s", file_name=file_name))
flash(_("Image file deleted: %(file_name)s", file_name=file_name))
else:
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -935,7 +929,9 @@ 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 = delete_file(prop_file_path)
process = file_cmds.delete_file(prop_file_path)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
@ -955,9 +951,9 @@ def rename():
file_name = request.form.get("file_name")
new_file_name = request.form.get("new_file_name")
process = rename_image(file_name, new_file_name)
process = file_cmds.rename_image(file_name, new_file_name)
if process["status"]:
flash(_(u"Image file renamed to: %(file_name)s", file_name=new_file_name))
flash(_("Image file renamed to: %(file_name)s", file_name=new_file_name))
else:
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -966,7 +962,8 @@ 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 = rename_file(prop_file_path, new_prop_file_path)
process = file_cmds.rename_file(prop_file_path, new_prop_file_path)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
@ -991,19 +988,19 @@ def unzip():
if zip_members:
zip_members = literal_eval(zip_members)
process = unzip_file(zip_file, zip_member, zip_members)
process = file_cmds.unzip_file(zip_file, zip_member, zip_members)
if process["status"]:
if not process["msg"]:
flash(_(u"Aborted unzip: File(s) with the same name already exists."), "error")
flash(_("Aborted unzip: File(s) with the same name already exists."), "error")
return redirect(url_for("index"))
flash(_(u"Unzipped the following files:"))
flash(_("Unzipped the following files:"))
for msg in process["msg"]:
flash(msg)
if process["prop_flag"]:
flash(_(u"Properties file(s) have been moved to %(directory)s", directory=CFG_DIR))
flash(_("Properties file(s) have been moved to %(directory)s", directory=CFG_DIR))
return redirect(url_for("index"))
flash(_(u"Failed to unzip %(zip_file)s", zip_file=zip_file), "error")
flash(_("Failed to unzip %(zip_file)s", zip_file=zip_file), "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -1015,9 +1012,11 @@ def change_language():
"""
locale = request.form.get("locale")
session["language"] = locale
ractl.locale = session["language"]
file_cmds.locale = session["language"]
refresh()
flash(_(u"Changed Web Interface language to %(locale)s", locale=locale))
flash(_("Changed Web Interface language to %(locale)s", locale=locale))
return redirect(url_for("index"))
@ -1029,8 +1028,10 @@ def load_default_config():
- Load the default configuration file, if found
"""
session["language"] = get_locale()
ractl.locale = session["language"]
file_cmds.locale = session["language"]
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
read_config(DEFAULT_CONFIG)
file_cmds.read_config(DEFAULT_CONFIG)
if __name__ == "__main__":
@ -1053,9 +1054,27 @@ if __name__ == "__main__":
action="store",
help="Token password string for authenticating with RaSCSI",
)
parser.add_argument(
"--rascsi-host",
type=str,
default="localhost",
action="store",
help="RaSCSI host. Default: localhost",
)
parser.add_argument(
"--rascsi-port",
type=str,
default=6868,
action="store",
help="RaSCSI port. Default: 6868",
)
arguments = parser.parse_args()
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"])
import bjoern
print("Serving rascsi-web...")
bjoern.run(APP, "0.0.0.0", arguments.port)

View File

@ -104,6 +104,8 @@ while [ "$1" != "" ]; do
shift
done
PYTHON_COMMON_PATH=$(dirname $PWD)/common/src
echo "Starting web server for RaSCSI Web Interface..."
export PYTHONPATH=$PWD/src:${PYTHON_COMMON_PATH}
cd src
python3 web.py ${PORT} ${PASSWORD}