mirror of
https://github.com/akuker/RASCSI.git
synced 2025-01-13 22:30:52 +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
9
.gitignore
vendored
9
.gitignore
vendored
@ -5,11 +5,12 @@ core
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
__pycache__
|
__pycache__
|
||||||
python/web/current
|
current
|
||||||
python/web/src/rascsi_interface_pb2.py
|
rascsi_interface_pb2.py
|
||||||
python/oled/current
|
|
||||||
python/oled/src/rascsi_interface_pb2.py
|
|
||||||
src/raspberrypi/hfdisk/
|
src/raspberrypi/hfdisk/
|
||||||
*~
|
*~
|
||||||
messages.pot
|
messages.pot
|
||||||
messages.mo
|
messages.mo
|
||||||
|
|
||||||
|
# temporary user files
|
||||||
|
s.sh
|
@ -52,6 +52,7 @@ VIRTUAL_DRIVER_PATH="$HOME/images"
|
|||||||
CFG_PATH="$HOME/.config/rascsi"
|
CFG_PATH="$HOME/.config/rascsi"
|
||||||
WEB_INSTALL_PATH="$BASE/python/web"
|
WEB_INSTALL_PATH="$BASE/python/web"
|
||||||
OLED_INSTALL_PATH="$BASE/python/oled"
|
OLED_INSTALL_PATH="$BASE/python/oled"
|
||||||
|
PYTHON_COMMON_PATH="$BASE/python/common"
|
||||||
SYSTEMD_PATH="/etc/systemd/system"
|
SYSTEMD_PATH="/etc/systemd/system"
|
||||||
HFS_FORMAT=/usr/bin/hformat
|
HFS_FORMAT=/usr/bin/hformat
|
||||||
HFDISK_BIN=/usr/bin/hfdisk
|
HFDISK_BIN=/usr/bin/hfdisk
|
||||||
@ -94,15 +95,17 @@ function installRaScsi() {
|
|||||||
sudo make install CONNECT_TYPE="${CONNECT_TYPE:-FULLSPEC}" </dev/null
|
sudo make install CONNECT_TYPE="${CONNECT_TYPE:-FULLSPEC}" </dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# install everything required to run an HTTP server (Nginx + Python Flask App)
|
function preparePythonCommon() {
|
||||||
function installRaScsiWebInterface() {
|
if [ -f "$PYTHON_COMMON_PATH/rascsi_interface_pb2.py" ]; then
|
||||||
if [ -f "$WEB_INSTALL_PATH/rascsi_interface_pb2.py" ]; then
|
sudo rm "$PYTHON_COMMON_PATH/rascsi_interface_pb2.py"
|
||||||
sudo rm "$WEB_INSTALL_PATH/rascsi_interface_pb2.py"
|
|
||||||
echo "Deleting old Python protobuf library rascsi_interface_pb2.py"
|
echo "Deleting old Python protobuf library rascsi_interface_pb2.py"
|
||||||
fi
|
fi
|
||||||
echo "Compiling the Python protobuf library rascsi_interface_pb2.py..."
|
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/nginx-default.conf" /etc/nginx/sites-available/default
|
||||||
sudo cp -f "$WEB_INSTALL_PATH/service-infra/502.html" /var/www/html/502.html
|
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
|
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
|
if [[ $(grep -c "^dtparam=i2c_arm=on" /boot/config.txt) -ge 1 ]]; then
|
||||||
echo "NOTE: I2C support seems to have been configured already."
|
echo "NOTE: I2C support seems to have been configured already."
|
||||||
REBOOT=0
|
REBOOT=0
|
||||||
@ -882,6 +878,7 @@ function runChoice() {
|
|||||||
backupRaScsiService
|
backupRaScsiService
|
||||||
installRaScsi
|
installRaScsi
|
||||||
enableRaScsiService
|
enableRaScsiService
|
||||||
|
preparePythonCommon
|
||||||
if [[ $(isRaScsiScreenInstalled) -eq 0 ]]; then
|
if [[ $(isRaScsiScreenInstalled) -eq 0 ]]; then
|
||||||
echo "Detected rascsi oled service; will run the installation steps for the OLED monitor."
|
echo "Detected rascsi oled service; will run the installation steps for the OLED monitor."
|
||||||
installRaScsiScreen
|
installRaScsiScreen
|
||||||
@ -913,6 +910,7 @@ function runChoice() {
|
|||||||
stopRaScsi
|
stopRaScsi
|
||||||
compileRaScsi
|
compileRaScsi
|
||||||
backupRaScsiService
|
backupRaScsiService
|
||||||
|
preparePythonCommon
|
||||||
installRaScsi
|
installRaScsi
|
||||||
enableRaScsiService
|
enableRaScsiService
|
||||||
if [[ $(isRaScsiScreenInstalled) -eq 0 ]]; then
|
if [[ $(isRaScsiScreenInstalled) -eq 0 ]]; then
|
||||||
@ -931,6 +929,7 @@ function runChoice() {
|
|||||||
echo "- Add and modify systemd services"
|
echo "- Add and modify systemd services"
|
||||||
echo "- Modify the Raspberry Pi boot configuration (may require a reboot)"
|
echo "- Modify the Raspberry Pi boot configuration (may require a reboot)"
|
||||||
sudoCheck
|
sudoCheck
|
||||||
|
preparePythonCommon
|
||||||
installRaScsiScreen
|
installRaScsiScreen
|
||||||
showRaScsiScreenStatus
|
showRaScsiScreenStatus
|
||||||
echo "Installing / Updating RaSCSI OLED Screen - Complete!"
|
echo "Installing / Updating RaSCSI OLED Screen - Complete!"
|
||||||
@ -1018,6 +1017,7 @@ function runChoice() {
|
|||||||
updateRaScsiGit
|
updateRaScsiGit
|
||||||
createCfgDir
|
createCfgDir
|
||||||
installPackages
|
installPackages
|
||||||
|
preparePythonCommon
|
||||||
installRaScsiWebInterface
|
installRaScsiWebInterface
|
||||||
echo "Configuring RaSCSI Web Interface stand-alone - Complete!"
|
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"
|
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
|
# Add files or directories to the blacklist. They should be base names, not
|
||||||
# paths.
|
# paths.
|
||||||
ignore=CVS
|
ignore=CVS,rascsi_interface_pb2.py
|
||||||
|
|
||||||
# Add files or directories matching the regex patterns to the blacklist. The
|
# Add files or directories matching the regex patterns to the blacklist. The
|
||||||
# regex matches against base names, not paths.
|
# regex matches against base names, not paths.
|
||||||
@ -15,7 +15,7 @@ ignore-patterns=
|
|||||||
|
|
||||||
# Python code to execute, usually for sys.path manipulation such as
|
# Python code to execute, usually for sys.path manipulation such as
|
||||||
# pygtk.require().
|
# 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
|
# venv hook for pylint
|
||||||
# Requires pylint-venv package:
|
# Requires pylint-venv package:
|
||||||
# $ pip install pylint-venv
|
# $ 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
|
$ ./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
|
## Credits
|
||||||
### type_writer.ttf
|
### type_writer.ttf
|
||||||
* _Type Writer_ TrueType font by Mandy Smith
|
* _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
|
import signal
|
||||||
|
|
||||||
class GracefulInterruptHandler():
|
|
||||||
|
class GracefulInterruptHandler:
|
||||||
"""
|
"""
|
||||||
Class for handling Linux signal interrupts
|
Class for handling Linux signal interrupts
|
||||||
"""
|
"""
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
Module with methods that interact with the Pi's Linux system
|
Module with methods that interact with the Pi's Linux system
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_ip_and_host():
|
def get_ip_and_host():
|
||||||
"""
|
"""
|
||||||
Use a mock socket connection to identify the Pi's hostname and IP address
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
from time import sleep
|
from time import sleep
|
||||||
@ -39,7 +40,8 @@ from adafruit_ssd1306 import SSD1306_I2C
|
|||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
from interrupt_handler import GracefulInterruptHandler
|
from interrupt_handler import GracefulInterruptHandler
|
||||||
from pi_cmds import get_ip_and_host
|
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 = argparse.ArgumentParser(description="RaSCSI OLED Monitor script")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -65,6 +67,20 @@ parser.add_argument(
|
|||||||
action="store",
|
action="store",
|
||||||
help="Token password string for authenticating with RaSCSI",
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.rotation == 0:
|
if args.rotation == 0:
|
||||||
@ -81,6 +97,9 @@ elif args.height == 32:
|
|||||||
|
|
||||||
TOKEN = args.password
|
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
|
WIDTH = 128
|
||||||
BORDER = 5
|
BORDER = 5
|
||||||
|
|
||||||
@ -148,10 +167,10 @@ def formatted_output():
|
|||||||
Formats the strings to be displayed on the Screen
|
Formats the strings to be displayed on the Screen
|
||||||
Returns a (list) of (str) output
|
Returns a (list) of (str) output
|
||||||
"""
|
"""
|
||||||
rascsi_list = device_list(TOKEN)
|
rascsi_list = ractl_cmd.list_devices()['device_list']
|
||||||
output = []
|
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!")
|
output.append("Permission denied!")
|
||||||
elif rascsi_list:
|
elif rascsi_list:
|
||||||
for line in 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
|
else
|
||||||
echo "Starting with parameter $HEIGHT"
|
echo "Starting with parameter $HEIGHT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
PYTHON_COMMON_PATH=$(dirname $PWD)/common/src
|
||||||
|
export PYTHONPATH=$PWD/src:${PYTHON_COMMON_PATH}
|
||||||
python3 src/rascsi_oled_monitor.py ${ROTATION} ${HEIGHT} ${PASSWORD}
|
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.
|
TODO: rascsi-web uses protobuf commands to send and receive data from rascsi.
|
||||||
A separate mocking solution will be needed for this interface.
|
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
|
## Pushing to the Pi via git
|
||||||
|
|
||||||
Setup a bare repo on the rascsi
|
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
|
Module for RaSCSI device management utility methods
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_valid_scsi_ids(devices, reserved_ids):
|
def get_valid_scsi_ids(devices, reserved_ids):
|
||||||
"""
|
"""
|
||||||
Takes a list of (dict)s devices, and list of (int)s 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 subprocess
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
from flask_babel import _
|
from flask_babel import _
|
||||||
from settings import AUTH_GROUP
|
from settings import AUTH_GROUP
|
||||||
@ -132,7 +131,7 @@ def introspect_file(file_path, re_term):
|
|||||||
"""
|
"""
|
||||||
from re import match
|
from re import match
|
||||||
try:
|
try:
|
||||||
ifile = open(file_path, "r")
|
ifile = open(file_path, "r", encoding="ISO-8859-1")
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
for line in ifile:
|
for line in ifile:
|
||||||
@ -141,30 +140,6 @@ def introspect_file(file_path, re_term):
|
|||||||
return False
|
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():
|
def auth_active():
|
||||||
"""
|
"""
|
||||||
Inspects if the group defined in AUTH_GROUP exists on the system.
|
Inspects if the group defined in AUTH_GROUP exists on the system.
|
||||||
@ -176,6 +151,6 @@ def auth_active():
|
|||||||
if AUTH_GROUP in groups:
|
if AUTH_GROUP in groups:
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"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": ""}
|
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
|
from os import getenv, getcwd
|
||||||
|
import rascsi.common_settings
|
||||||
|
|
||||||
WEB_DIR = getcwd()
|
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])
|
HOME_DIR = "/".join(WEB_DIR.split("/")[0:3])
|
||||||
CFG_DIR = f"{HOME_DIR}/.config/rascsi"
|
|
||||||
AFP_DIR = f"{HOME_DIR}/afpshare"
|
AFP_DIR = f"{HOME_DIR}/afpshare"
|
||||||
|
|
||||||
MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", str(1024 * 1024 * 1024 * 4)) # 4gb
|
MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", str(1024 * 1024 * 1024 * 4)) # 4gb
|
||||||
|
|
||||||
ARCHIVE_FILE_SUFFIX = "zip"
|
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
|
# 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
|
# File containing canonical drive properties
|
||||||
DRIVE_PROPERTIES_FILE = WEB_DIR + "/drive_properties.json"
|
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
|
# The user group that is used for webapp authentication
|
||||||
AUTH_GROUP = "rascsi"
|
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 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 (
|
from pi_cmds import (
|
||||||
running_env,
|
running_env,
|
||||||
running_proc,
|
running_proc,
|
||||||
@ -48,43 +32,40 @@ from pi_cmds import (
|
|||||||
introspect_file,
|
introspect_file,
|
||||||
auth_active,
|
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 (
|
from device_utils import (
|
||||||
sort_and_format_devices,
|
sort_and_format_devices,
|
||||||
get_valid_scsi_ids,
|
get_valid_scsi_ids,
|
||||||
)
|
)
|
||||||
|
from return_code_mapper import ReturnCodeMapper
|
||||||
|
|
||||||
from settings import (
|
from settings import (
|
||||||
CFG_DIR,
|
|
||||||
AFP_DIR,
|
AFP_DIR,
|
||||||
MAX_FILE_SIZE,
|
MAX_FILE_SIZE,
|
||||||
ARCHIVE_FILE_SUFFIX,
|
ARCHIVE_FILE_SUFFIX,
|
||||||
CONFIG_FILE_SUFFIX,
|
|
||||||
PROPERTIES_SUFFIX,
|
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
DRIVE_PROPERTIES_FILE,
|
DRIVE_PROPERTIES_FILE,
|
||||||
REMOVABLE_DEVICE_TYPES,
|
|
||||||
RESERVATIONS,
|
|
||||||
AUTH_GROUP,
|
AUTH_GROUP,
|
||||||
LANGUAGES,
|
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__)
|
APP = Flask(__name__)
|
||||||
BABEL = Babel(APP)
|
BABEL = Babel(APP)
|
||||||
|
|
||||||
|
|
||||||
@BABEL.localeselector
|
@BABEL.localeselector
|
||||||
def get_locale():
|
def get_locale():
|
||||||
"""
|
"""
|
||||||
@ -117,22 +98,22 @@ def index():
|
|||||||
"""
|
"""
|
||||||
Sets up data structures for and renders the index page
|
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(
|
abort(
|
||||||
403,
|
403,
|
||||||
_(
|
_(
|
||||||
u"RaSCSI is password protected. "
|
"RaSCSI is password protected. "
|
||||||
u"Start the Web Interface with the --password parameter."
|
"Start the Web Interface with the --password parameter."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
locales = get_supported_locales()
|
locales = get_supported_locales()
|
||||||
server_info = get_server_info()
|
server_info = ractl.get_server_info()
|
||||||
disk = disk_space()
|
disk = disk_space()
|
||||||
devices = list_devices()
|
devices = ractl.list_devices()
|
||||||
device_types = get_device_types()
|
device_types = ractl.get_device_types()
|
||||||
image_files = list_images()
|
image_files = file_cmds.list_images()
|
||||||
config_files = list_config_files()
|
config_files = file_cmds.list_config_files()
|
||||||
|
|
||||||
sorted_image_files = sorted(image_files["files"], key=lambda x: x["name"].lower())
|
sorted_image_files = sorted(image_files["files"], key=lambda x: x["name"].lower())
|
||||||
sorted_config_files = sorted(config_files, key=lambda x: x.lower())
|
sorted_config_files = sorted(config_files, key=lambda x: x.lower())
|
||||||
@ -187,7 +168,7 @@ def index():
|
|||||||
version=server_info["version"],
|
version=server_info["version"],
|
||||||
log_levels=server_info["log_levels"],
|
log_levels=server_info["log_levels"],
|
||||||
current_log_level=server_info["current_log_level"],
|
current_log_level=server_info["current_log_level"],
|
||||||
netinfo=get_network_info(),
|
netinfo=ractl.get_network_info(),
|
||||||
device_types=device_types["device_types"],
|
device_types=device_types["device_types"],
|
||||||
free_disk=int(disk["free"] / 1024 / 1024),
|
free_disk=int(disk["free"] / 1024 / 1024),
|
||||||
valid_file_suffix=valid_file_suffix,
|
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
|
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()
|
disk = disk_space()
|
||||||
|
|
||||||
# Reads the canonical drive properties into a dict
|
# Reads the canonical drive properties into a dict
|
||||||
# The file resides in the current dir of the web ui process
|
# The file resides in the current dir of the web ui process
|
||||||
drive_properties = Path(DRIVE_PROPERTIES_FILE)
|
drive_properties = Path(DRIVE_PROPERTIES_FILE)
|
||||||
if drive_properties.is_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"]:
|
if not process["status"]:
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -247,7 +229,7 @@ def drive_list():
|
|||||||
device["size_mb"] = "{:,.2f}".format(device["size"] / 1024 / 1024)
|
device["size_mb"] = "{:,.2f}".format(device["size"] / 1024 / 1024)
|
||||||
rm_conf.append(device)
|
rm_conf.append(device)
|
||||||
|
|
||||||
files = list_images()
|
files = file_cmds.list_images()
|
||||||
sorted_image_files = sorted(files["files"], key=lambda x: x["name"].lower())
|
sorted_image_files = sorted(files["files"], key=lambda x: x["name"].lower())
|
||||||
hd_conf = sorted(hd_conf, 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())
|
cd_conf = sorted(cd_conf, key=lambda x: x["name"].lower())
|
||||||
@ -292,7 +274,7 @@ def login():
|
|||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
flash(
|
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,
|
group=AUTH_GROUP,
|
||||||
),
|
),
|
||||||
"error",
|
"error",
|
||||||
@ -347,9 +329,9 @@ def drive_create():
|
|||||||
full_file_name = file_name + "." + file_type
|
full_file_name = file_name + "." + file_type
|
||||||
|
|
||||||
# Creating the image file
|
# 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"]:
|
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:
|
else:
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -362,7 +344,8 @@ def drive_create():
|
|||||||
"revision": revision,
|
"revision": revision,
|
||||||
"block_size": block_size,
|
"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"]:
|
if process["status"]:
|
||||||
flash(process["msg"])
|
flash(process["msg"])
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -391,7 +374,8 @@ def drive_cdrom():
|
|||||||
"revision": revision,
|
"revision": revision,
|
||||||
"block_size": block_size,
|
"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"]:
|
if process["status"]:
|
||||||
flash(process["msg"])
|
flash(process["msg"])
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -409,7 +393,8 @@ def config_save():
|
|||||||
file_name = request.form.get("name") or "default"
|
file_name = request.form.get("name") or "default"
|
||||||
file_name = f"{file_name}.{CONFIG_FILE_SUFFIX}"
|
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"]:
|
if process["status"]:
|
||||||
flash(process["msg"])
|
flash(process["msg"])
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -427,7 +412,8 @@ def config_load():
|
|||||||
file_name = request.form.get("name")
|
file_name = request.form.get("name")
|
||||||
|
|
||||||
if "load" in request.form:
|
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"]:
|
if process["status"]:
|
||||||
flash(process["msg"])
|
flash(process["msg"])
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -435,7 +421,8 @@ def config_load():
|
|||||||
flash(process['msg'], "error")
|
flash(process['msg'], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
if "delete" in request.form:
|
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"]:
|
if process["status"]:
|
||||||
flash(process["msg"])
|
flash(process["msg"])
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -474,7 +461,7 @@ def show_logs():
|
|||||||
headers = {"content-type": "text/plain"}
|
headers = {"content-type": "text/plain"}
|
||||||
return process.stdout.decode("utf-8"), int(lines), headers
|
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")
|
flash(process.stderr.decode("utf-8"), "stderr")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
@ -487,9 +474,9 @@ def log_level():
|
|||||||
"""
|
"""
|
||||||
level = request.form.get("level") or "info"
|
level = request.form.get("level") or "info"
|
||||||
|
|
||||||
process = set_log_level(level)
|
process = ractl.set_log_level(level)
|
||||||
if process["status"]:
|
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"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
@ -508,24 +495,24 @@ def daynaport_attach():
|
|||||||
mask = request.form.get("mask")
|
mask = request.form.get("mask")
|
||||||
|
|
||||||
error_url = "https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link"
|
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 interface.startswith("wlan"):
|
||||||
if not introspect_file("/etc/sysctl.conf", r"^net\.ipv4\.ip_forward=1$"):
|
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")
|
flash(error_msg, "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
if not Path("/etc/iptables/rules.v4").is_file():
|
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")
|
flash(error_msg, "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
else:
|
else:
|
||||||
if not introspect_file("/etc/dhcpcd.conf", r"^denyinterfaces " + interface + r"$"):
|
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")
|
flash(error_msg, "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
if not Path("/etc/network/interfaces.d/rascsi_bridge").is_file():
|
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")
|
flash(error_msg, "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
@ -536,9 +523,10 @@ def daynaport_attach():
|
|||||||
arg += (":" + ip_addr + "/" + mask)
|
arg += (":" + ip_addr + "/" + mask)
|
||||||
kwargs["interfaces"] = arg
|
kwargs["interfaces"] = arg
|
||||||
|
|
||||||
process = attach_image(scsi_id, **kwargs)
|
process = ractl.attach_image(scsi_id, **kwargs)
|
||||||
|
process = ReturnCodeMapper.add_msg(process)
|
||||||
if process["status"]:
|
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"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
@ -573,7 +561,8 @@ def attach():
|
|||||||
# same file name with PROPERTIES_SUFFIX appended
|
# same file name with PROPERTIES_SUFFIX appended
|
||||||
drive_properties = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
|
drive_properties = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
|
||||||
if Path(drive_properties).is_file():
|
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"]:
|
if not process["status"]:
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -584,18 +573,19 @@ def attach():
|
|||||||
kwargs["block_size"] = conf["block_size"]
|
kwargs["block_size"] = conf["block_size"]
|
||||||
expected_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"]:
|
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))
|
file_name=file_name, id_number=scsi_id, unit_number=unit))
|
||||||
if int(file_size) % int(expected_block_size):
|
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"%(block_size)s. RaSCSI will ignore the trailing data. "
|
||||||
u"The image may be corrupted, so proceed with caution.",
|
u"The image may be corrupted, so proceed with caution.",
|
||||||
file_size=file_size, block_size=expected_block_size), "error")
|
file_size=file_size, block_size=expected_block_size), "error")
|
||||||
return redirect(url_for("index"))
|
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")
|
file_name=file_name, id_number=scsi_id, unit_number=unit), "error")
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -607,9 +597,9 @@ def detach_all_devices():
|
|||||||
"""
|
"""
|
||||||
Detaches all currently attached devices
|
Detaches all currently attached devices
|
||||||
"""
|
"""
|
||||||
process = detach_all()
|
process = ractl.detach_all()
|
||||||
if process["status"]:
|
if process["status"]:
|
||||||
flash(_(u"Detached all SCSI devices"))
|
flash(_("Detached all SCSI devices"))
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
@ -624,13 +614,13 @@ def detach():
|
|||||||
"""
|
"""
|
||||||
scsi_id = request.form.get("scsi_id")
|
scsi_id = request.form.get("scsi_id")
|
||||||
unit = request.form.get("unit")
|
unit = request.form.get("unit")
|
||||||
process = detach_by_id(scsi_id, unit)
|
process = ractl.detach_by_id(scsi_id, unit)
|
||||||
if process["status"]:
|
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))
|
id_number=scsi_id, unit_number=unit))
|
||||||
return redirect(url_for("index"))
|
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")
|
id_number=scsi_id, unit_number=unit), "error")
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -645,13 +635,13 @@ def eject():
|
|||||||
scsi_id = request.form.get("scsi_id")
|
scsi_id = request.form.get("scsi_id")
|
||||||
unit = request.form.get("unit")
|
unit = request.form.get("unit")
|
||||||
|
|
||||||
process = eject_by_id(scsi_id, unit)
|
process = ractl.eject_by_id(scsi_id, unit)
|
||||||
if process["status"]:
|
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))
|
id_number=scsi_id, unit_number=unit))
|
||||||
return redirect(url_for("index"))
|
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")
|
id_number=scsi_id, unit_number=unit), "error")
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -664,7 +654,7 @@ def device_info():
|
|||||||
scsi_id = request.form.get("scsi_id")
|
scsi_id = request.form.get("scsi_id")
|
||||||
unit = request.form.get("unit")
|
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
|
# First check if any device at all was returned
|
||||||
if not devices["status"]:
|
if not devices["status"]:
|
||||||
@ -674,19 +664,19 @@ def device_info():
|
|||||||
# the one and only device that should have been returned
|
# the one and only device that should have been returned
|
||||||
device = devices["device_list"][0]
|
device = devices["device_list"][0]
|
||||||
if str(device["id"]) == scsi_id:
|
if str(device["id"]) == scsi_id:
|
||||||
flash(_(u"DEVICE INFO"))
|
flash(_("DEVICE INFO"))
|
||||||
flash("===========")
|
flash("===========")
|
||||||
flash(_(u"SCSI ID: %(id_number)s", id_number=device["id"]))
|
flash(_("SCSI ID: %(id_number)s", id_number=device["id"]))
|
||||||
flash(_(u"LUN: %(unit_number)s", unit_number=device["unit"]))
|
flash(_("LUN: %(unit_number)s", unit_number=device["unit"]))
|
||||||
flash(_(u"Type: %(device_type)s", device_type=device["device_type"]))
|
flash(_("Type: %(device_type)s", device_type=device["device_type"]))
|
||||||
flash(_(u"Status: %(device_status)s", device_status=device["status"]))
|
flash(_("Status: %(device_status)s", device_status=device["status"]))
|
||||||
flash(_(u"File: %(image_file)s", image_file=device["image"]))
|
flash(_("File: %(image_file)s", image_file=device["image"]))
|
||||||
flash(_(u"Parameters: %(value)s", value=device["params"]))
|
flash(_("Parameters: %(value)s", value=device["params"]))
|
||||||
flash(_(u"Vendor: %(value)s", value=device["vendor"]))
|
flash(_("Vendor: %(value)s", value=device["vendor"]))
|
||||||
flash(_(u"Product: %(value)s", value=device["product"]))
|
flash(_("Product: %(value)s", value=device["product"]))
|
||||||
flash(_(u"Revision: %(revision_number)s", revision_number=device["revision"]))
|
flash(_("Revision: %(revision_number)s", revision_number=device["revision"]))
|
||||||
flash(_(u"Block Size: %(value)s bytes", value=device["block_size"]))
|
flash(_("Block Size: %(value)s bytes", value=device["block_size"]))
|
||||||
flash(_(u"Image Size: %(value)s bytes", value=device["size"]))
|
flash(_("Image Size: %(value)s bytes", value=device["size"]))
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
flash(devices["msg"], "error")
|
flash(devices["msg"], "error")
|
||||||
@ -700,15 +690,15 @@ def reserve_id():
|
|||||||
"""
|
"""
|
||||||
scsi_id = request.form.get("scsi_id")
|
scsi_id = request.form.get("scsi_id")
|
||||||
memo = request.form.get("memo")
|
memo = request.form.get("memo")
|
||||||
reserved_ids = get_reserved_ids()["ids"]
|
reserved_ids = ractl.get_reserved_ids()["ids"]
|
||||||
reserved_ids.extend(scsi_id)
|
reserved_ids.extend(scsi_id)
|
||||||
process = reserve_scsi_ids(reserved_ids)
|
process = ractl.reserve_scsi_ids(reserved_ids)
|
||||||
if process["status"]:
|
if process["status"]:
|
||||||
RESERVATIONS[int(scsi_id)] = memo
|
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"))
|
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")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
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
|
Removes the reservation of a SCSI ID as well as the memo for the reservation
|
||||||
"""
|
"""
|
||||||
scsi_id = request.form.get("scsi_id")
|
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)
|
reserved_ids.remove(scsi_id)
|
||||||
process = reserve_scsi_ids(reserved_ids)
|
process = ractl.reserve_scsi_ids(reserved_ids)
|
||||||
if process["status"]:
|
if process["status"]:
|
||||||
RESERVATIONS[int(scsi_id)] = ""
|
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"))
|
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")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
@ -738,7 +728,7 @@ def restart():
|
|||||||
"""
|
"""
|
||||||
Restarts the Pi
|
Restarts the Pi
|
||||||
"""
|
"""
|
||||||
shutdown_pi("reboot")
|
ractl.shutdown_pi("reboot")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
@ -748,7 +738,7 @@ def shutdown():
|
|||||||
"""
|
"""
|
||||||
Shuts down the Pi
|
Shuts down the Pi
|
||||||
"""
|
"""
|
||||||
shutdown_pi("system")
|
ractl.shutdown_pi("system")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
@ -762,21 +752,23 @@ def download_to_iso():
|
|||||||
url = request.form.get("url")
|
url = request.form.get("url")
|
||||||
iso_args = request.form.get("type").split()
|
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"]:
|
if process["status"]:
|
||||||
flash(process["msg"])
|
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:
|
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")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
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"]:
|
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"))
|
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")
|
id_number=scsi_id), "error")
|
||||||
flash(process_attach["msg"], "error")
|
flash(process_attach["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -789,13 +781,14 @@ def download_img():
|
|||||||
Downloads a remote file onto the images dir on the Pi
|
Downloads a remote file onto the images dir on the Pi
|
||||||
"""
|
"""
|
||||||
url = request.form.get("url")
|
url = request.form.get("url")
|
||||||
server_info = get_server_info()
|
server_info = ractl.get_server_info()
|
||||||
process = download_to_dir(url, server_info["image_dir"], Path(url).name)
|
process = file_cmds.download_to_dir(url, server_info["image_dir"], Path(url).name)
|
||||||
|
process = ReturnCodeMapper.add_msg(process)
|
||||||
if process["status"]:
|
if process["status"]:
|
||||||
flash(process["msg"])
|
flash(process["msg"])
|
||||||
return redirect(url_for("index"))
|
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")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
@ -819,12 +812,13 @@ def download_afp():
|
|||||||
stem_remainder = file_name_stem[:(26 - len(file_name_suffix))]
|
stem_remainder = file_name_stem[:(26 - len(file_name_suffix))]
|
||||||
appendix_hash = (md5(discarded_portion.encode("utf-8"))).hexdigest().upper()
|
appendix_hash = (md5(discarded_portion.encode("utf-8"))).hexdigest().upper()
|
||||||
file_name = stem_remainder + "#" + appendix_hash[:4] + file_name_suffix
|
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"]:
|
if process["status"]:
|
||||||
flash(process["msg"])
|
flash(process["msg"])
|
||||||
return redirect(url_for("index"))
|
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")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
@ -847,7 +841,7 @@ def upload_file():
|
|||||||
file_object = request.files["file"]
|
file_object = request.files["file"]
|
||||||
file_name = secure_filename(file_object.filename)
|
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)
|
save_path = path.join(server_info["image_dir"], file_name)
|
||||||
current_chunk = int(request.form['dzchunkindex'])
|
current_chunk = int(request.form['dzchunkindex'])
|
||||||
@ -855,7 +849,7 @@ def upload_file():
|
|||||||
# Makes sure not to overwrite an existing file,
|
# Makes sure not to overwrite an existing file,
|
||||||
# but continues writing to a file transfer in progress
|
# but continues writing to a file transfer in progress
|
||||||
if path.exists(save_path) and current_chunk == 0:
|
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:
|
try:
|
||||||
with open(save_path, "ab") as save:
|
with open(save_path, "ab") as save:
|
||||||
@ -863,7 +857,7 @@ def upload_file():
|
|||||||
save.write(file_object.stream.read())
|
save.write(file_object.stream.read())
|
||||||
except OSError:
|
except OSError:
|
||||||
log.exception("Could not write to file")
|
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"])
|
total_chunks = int(request.form["dztotalchunkcount"])
|
||||||
|
|
||||||
@ -878,13 +872,13 @@ def upload_file():
|
|||||||
path.getsize(save_path),
|
path.getsize(save_path),
|
||||||
request.form['dztotalfilesize'],
|
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.info("File %s has been uploaded successfully", file_object.filename)
|
||||||
log.debug("Chunk %s of %s for file %s completed.",
|
log.debug("Chunk %s of %s for file %s completed.",
|
||||||
current_chunk + 1, total_chunks, file_object.filename)
|
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"])
|
@APP.route("/files/create", methods=["POST"])
|
||||||
@ -898,9 +892,9 @@ def create_file():
|
|||||||
file_type = request.form.get("type")
|
file_type = request.form.get("type")
|
||||||
full_file_name = file_name + "." + file_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"]:
|
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"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
@ -925,9 +919,9 @@ def delete():
|
|||||||
"""
|
"""
|
||||||
file_name = request.form.get("file_name")
|
file_name = request.form.get("file_name")
|
||||||
|
|
||||||
process = delete_image(file_name)
|
process = file_cmds.delete_image(file_name)
|
||||||
if process["status"]:
|
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:
|
else:
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -935,7 +929,9 @@ def delete():
|
|||||||
# Delete the drive properties file, if it exists
|
# Delete the drive properties file, if it exists
|
||||||
prop_file_path = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
|
prop_file_path = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
|
||||||
if Path(prop_file_path).is_file():
|
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"]:
|
if process["status"]:
|
||||||
flash(process["msg"])
|
flash(process["msg"])
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -955,9 +951,9 @@ def rename():
|
|||||||
file_name = request.form.get("file_name")
|
file_name = request.form.get("file_name")
|
||||||
new_file_name = request.form.get("new_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"]:
|
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:
|
else:
|
||||||
flash(process["msg"], "error")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -966,7 +962,8 @@ def rename():
|
|||||||
prop_file_path = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
|
prop_file_path = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
|
||||||
new_prop_file_path = f"{CFG_DIR}/{new_file_name}.{PROPERTIES_SUFFIX}"
|
new_prop_file_path = f"{CFG_DIR}/{new_file_name}.{PROPERTIES_SUFFIX}"
|
||||||
if Path(prop_file_path).is_file():
|
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"]:
|
if process["status"]:
|
||||||
flash(process["msg"])
|
flash(process["msg"])
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
@ -991,19 +988,19 @@ def unzip():
|
|||||||
if zip_members:
|
if zip_members:
|
||||||
zip_members = literal_eval(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 process["status"]:
|
||||||
if not process["msg"]:
|
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"))
|
return redirect(url_for("index"))
|
||||||
flash(_(u"Unzipped the following files:"))
|
flash(_("Unzipped the following files:"))
|
||||||
for msg in process["msg"]:
|
for msg in process["msg"]:
|
||||||
flash(msg)
|
flash(msg)
|
||||||
if process["prop_flag"]:
|
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"))
|
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")
|
flash(process["msg"], "error")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
@ -1015,9 +1012,11 @@ def change_language():
|
|||||||
"""
|
"""
|
||||||
locale = request.form.get("locale")
|
locale = request.form.get("locale")
|
||||||
session["language"] = locale
|
session["language"] = locale
|
||||||
|
ractl.locale = session["language"]
|
||||||
|
file_cmds.locale = session["language"]
|
||||||
refresh()
|
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"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
@ -1029,8 +1028,10 @@ def load_default_config():
|
|||||||
- Load the default configuration file, if found
|
- Load the default configuration file, if found
|
||||||
"""
|
"""
|
||||||
session["language"] = get_locale()
|
session["language"] = get_locale()
|
||||||
|
ractl.locale = session["language"]
|
||||||
|
file_cmds.locale = session["language"]
|
||||||
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
|
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
|
||||||
read_config(DEFAULT_CONFIG)
|
file_cmds.read_config(DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@ -1053,9 +1054,27 @@ if __name__ == "__main__":
|
|||||||
action="store",
|
action="store",
|
||||||
help="Token password string for authenticating with RaSCSI",
|
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()
|
arguments = parser.parse_args()
|
||||||
APP.config["TOKEN"] = arguments.password
|
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
|
import bjoern
|
||||||
print("Serving rascsi-web...")
|
print("Serving rascsi-web...")
|
||||||
bjoern.run(APP, "0.0.0.0", arguments.port)
|
bjoern.run(APP, "0.0.0.0", arguments.port)
|
||||||
|
@ -104,6 +104,8 @@ while [ "$1" != "" ]; do
|
|||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
|
PYTHON_COMMON_PATH=$(dirname $PWD)/common/src
|
||||||
echo "Starting web server for RaSCSI Web Interface..."
|
echo "Starting web server for RaSCSI Web Interface..."
|
||||||
|
export PYTHONPATH=$PWD/src:${PYTHON_COMMON_PATH}
|
||||||
cd src
|
cd src
|
||||||
python3 web.py ${PORT} ${PASSWORD}
|
python3 web.py ${PORT} ${PASSWORD}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user