mirror of
https://github.com/akuker/RASCSI.git
synced 2025-03-30 20:30:01 +00:00
* 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:
parent
7f362c9308
commit
089dc302e5
.gitignoreeasyinstall.sh
python
9
.gitignore
vendored
9
.gitignore
vendored
@ -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
|
@ -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"
|
||||
|
@ -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
32
python/README.md
Normal 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
|
||||
```
|
2
python/common/requirements.txt
Normal file
2
python/common/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
protobuf==3.19.3
|
||||
requests==2.26.0
|
30
python/common/src/README.md
Normal file
30
python/common/src/README.md
Normal 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.
|
0
python/common/src/__init__.py
Normal file
0
python/common/src/__init__.py
Normal file
0
python/common/src/rascsi/__init__.py
Normal file
0
python/common/src/rascsi/__init__.py
Normal file
21
python/common/src/rascsi/common_settings.py
Normal file
21
python/common/src/rascsi/common_settings.py
Normal 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)]
|
15
python/common/src/rascsi/exceptions.py
Normal file
15
python/common/src/rascsi/exceptions.py
Normal 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. """
|
601
python/common/src/rascsi/file_cmds.py
Normal file
601
python/common/src/rascsi/file_cmds.py
Normal 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}
|
444
python/common/src/rascsi/ractl_cmds.py
Normal file
444
python/common/src/rascsi/ractl_cmds.py
Normal 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}
|
25
python/common/src/rascsi/return_codes.py
Normal file
25
python/common/src/rascsi/return_codes.py
Normal 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
|
91
python/common/src/rascsi/socket_cmds.py
Normal file
91
python/common/src/rascsi/socket_cmds.py
Normal 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)
|
@ -1 +0,0 @@
|
||||
../.pylintrc
|
@ -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
|
||||
|
0
python/oled/src/__init__.py
Normal file
0
python/oled/src/__init__.py
Normal file
@ -3,7 +3,8 @@ Linux interrupt handling module
|
||||
"""
|
||||
import signal
|
||||
|
||||
class GracefulInterruptHandler():
|
||||
|
||||
class GracefulInterruptHandler:
|
||||
"""
|
||||
Class for handling Linux signal interrupts
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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}
|
@ -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:
|
||||
|
@ -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."
|
||||
)
|
@ -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}
|
||||
|
@ -1 +0,0 @@
|
||||
../.pylintrc
|
@ -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
0
python/web/__init__.py
Normal file
0
python/web/src/__init__.py
Normal file
0
python/web/src/__init__.py
Normal 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.
|
||||
|
@ -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),
|
||||
}
|
@ -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": ""}
|
||||
|
@ -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}
|
45
python/web/src/return_code_mapper.py
Normal file
45
python/web/src/return_code_mapper.py
Normal 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
|
@ -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"
|
||||
|
||||
|
@ -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."
|
||||
)
|
||||
)
|
60
python/web/src/socket_cmds_flask.py
Normal file
60
python/web/src/socket_cmds_flask.py
Normal 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
|
@ -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)
|
||||
|
@ -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}
|
||||
|
Loading…
x
Reference in New Issue
Block a user