mirror of
https://github.com/akuker/RASCSI.git
synced 2024-12-06 15:49:29 +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
|
||||
*.swp
|
||||
__pycache__
|
||||
python/web/current
|
||||
python/web/src/rascsi_interface_pb2.py
|
||||
python/oled/current
|
||||
python/oled/src/rascsi_interface_pb2.py
|
||||
current
|
||||
rascsi_interface_pb2.py
|
||||
src/raspberrypi/hfdisk/
|
||||
*~
|
||||
messages.pot
|
||||
messages.mo
|
||||
|
||||
# temporary user files
|
||||
s.sh
|
@ -52,6 +52,7 @@ VIRTUAL_DRIVER_PATH="$HOME/images"
|
||||
CFG_PATH="$HOME/.config/rascsi"
|
||||
WEB_INSTALL_PATH="$BASE/python/web"
|
||||
OLED_INSTALL_PATH="$BASE/python/oled"
|
||||
PYTHON_COMMON_PATH="$BASE/python/common"
|
||||
SYSTEMD_PATH="/etc/systemd/system"
|
||||
HFS_FORMAT=/usr/bin/hformat
|
||||
HFDISK_BIN=/usr/bin/hfdisk
|
||||
@ -94,15 +95,17 @@ function installRaScsi() {
|
||||
sudo make install CONNECT_TYPE="${CONNECT_TYPE:-FULLSPEC}" </dev/null
|
||||
}
|
||||
|
||||
# install everything required to run an HTTP server (Nginx + Python Flask App)
|
||||
function installRaScsiWebInterface() {
|
||||
if [ -f "$WEB_INSTALL_PATH/rascsi_interface_pb2.py" ]; then
|
||||
sudo rm "$WEB_INSTALL_PATH/rascsi_interface_pb2.py"
|
||||
function preparePythonCommon() {
|
||||
if [ -f "$PYTHON_COMMON_PATH/rascsi_interface_pb2.py" ]; then
|
||||
sudo rm "$PYTHON_COMMON_PATH/rascsi_interface_pb2.py"
|
||||
echo "Deleting old Python protobuf library rascsi_interface_pb2.py"
|
||||
fi
|
||||
echo "Compiling the Python protobuf library rascsi_interface_pb2.py..."
|
||||
protoc -I="$BASE/src/raspberrypi/" --python_out="$WEB_INSTALL_PATH/src" rascsi_interface.proto
|
||||
protoc -I="$BASE/src/raspberrypi/" --python_out="$PYTHON_COMMON_PATH/src" rascsi_interface.proto
|
||||
}
|
||||
|
||||
# install everything required to run an HTTP server (Nginx + Python Flask App)
|
||||
function installRaScsiWebInterface() {
|
||||
sudo cp -f "$WEB_INSTALL_PATH/service-infra/nginx-default.conf" /etc/nginx/sites-available/default
|
||||
sudo cp -f "$WEB_INSTALL_PATH/service-infra/502.html" /var/www/html/502.html
|
||||
|
||||
@ -779,13 +782,6 @@ function installRaScsiScreen() {
|
||||
|
||||
sudo apt-get update && sudo apt-get install libjpeg-dev libpng-dev libopenjp2-7-dev i2c-tools raspi-config -y </dev/null
|
||||
|
||||
if [ -f "$OLED_INSTALL_PATH/src/rascsi_interface_pb2.py" ]; then
|
||||
sudo rm "$OLED_INSTALL_PATH/src/rascsi_interface_pb2.py"
|
||||
echo "Deleting old Python protobuf library rascsi_interface_pb2.py"
|
||||
fi
|
||||
echo "Compiling the Python protobuf library rascsi_interface_pb2.py..."
|
||||
protoc -I="$BASE/src/raspberrypi/" --python_out="$OLED_INSTALL_PATH/src" rascsi_interface.proto
|
||||
|
||||
if [[ $(grep -c "^dtparam=i2c_arm=on" /boot/config.txt) -ge 1 ]]; then
|
||||
echo "NOTE: I2C support seems to have been configured already."
|
||||
REBOOT=0
|
||||
@ -882,6 +878,7 @@ function runChoice() {
|
||||
backupRaScsiService
|
||||
installRaScsi
|
||||
enableRaScsiService
|
||||
preparePythonCommon
|
||||
if [[ $(isRaScsiScreenInstalled) -eq 0 ]]; then
|
||||
echo "Detected rascsi oled service; will run the installation steps for the OLED monitor."
|
||||
installRaScsiScreen
|
||||
@ -913,6 +910,7 @@ function runChoice() {
|
||||
stopRaScsi
|
||||
compileRaScsi
|
||||
backupRaScsiService
|
||||
preparePythonCommon
|
||||
installRaScsi
|
||||
enableRaScsiService
|
||||
if [[ $(isRaScsiScreenInstalled) -eq 0 ]]; then
|
||||
@ -931,6 +929,7 @@ function runChoice() {
|
||||
echo "- Add and modify systemd services"
|
||||
echo "- Modify the Raspberry Pi boot configuration (may require a reboot)"
|
||||
sudoCheck
|
||||
preparePythonCommon
|
||||
installRaScsiScreen
|
||||
showRaScsiScreenStatus
|
||||
echo "Installing / Updating RaSCSI OLED Screen - Complete!"
|
||||
@ -1018,6 +1017,7 @@ function runChoice() {
|
||||
updateRaScsiGit
|
||||
createCfgDir
|
||||
installPackages
|
||||
preparePythonCommon
|
||||
installRaScsiWebInterface
|
||||
echo "Configuring RaSCSI Web Interface stand-alone - Complete!"
|
||||
echo "Launch the Web Interface with the 'start.sh' script. To use a custom port for the web server: 'start.sh --port=8081"
|
||||
|
@ -7,7 +7,7 @@ extension-pkg-whitelist=
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
ignore=CVS,rascsi_interface_pb2.py
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
@ -15,7 +15,7 @@ ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
init-hook=
|
||||
init-hook=import sys; sys.path.append("common/src"); sys.path.append("web/src"); sys.path.append("oled/src");
|
||||
# venv hook for pylint
|
||||
# Requires pylint-venv package:
|
||||
# $ pip install pylint-venv
|
||||
|
32
python/README.md
Normal file
32
python/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
# RaSCSI Python Apps
|
||||
|
||||
This directory contains Python-based clients for RaSCSI as well as common
|
||||
packages that are shared among the clients.
|
||||
|
||||
The following paragraphs in this README contain instructions that are shared
|
||||
among all Python apps.
|
||||
|
||||
### Static analysis with pylint
|
||||
|
||||
It is recommended to run pylint against new code to protect against bugs
|
||||
and keep the code readable and maintainable.
|
||||
The local pylint configuration lives in .pylintrc.
|
||||
In order for pylint to recognize venv libraries, the pylint-venv package is required.
|
||||
|
||||
```
|
||||
sudo apt install pylint3
|
||||
sudo pip install pylint-venv
|
||||
source venv/bin/activate
|
||||
pylint3 python_source_file.py
|
||||
```
|
||||
|
||||
Examples:
|
||||
```
|
||||
# check a single file
|
||||
pylint web/src/web.py
|
||||
|
||||
# check the python modules
|
||||
pylint common/src
|
||||
pylint web/src
|
||||
pylint oled/src
|
||||
```
|
2
python/common/requirements.txt
Normal file
2
python/common/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
protobuf==3.19.3
|
||||
requests==2.26.0
|
30
python/common/src/README.md
Normal file
30
python/common/src/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# RaSCSI Common Python Module
|
||||
|
||||
The common module contains python modules that are shared among multiple Python
|
||||
applications such as the OLED or the Web application. It contains shared functionality.
|
||||
For example, the rascsi python module provides functionality for accessing rascsi through its
|
||||
protobuf interface and provides convenient classes for that purpose.
|
||||
|
||||
### Usage
|
||||
|
||||
To make use of the rascsi python module, it needs to be found by the Python scripts using it.
|
||||
This can be achieved in multiple ways. One way is to simply adapt the PYTHONPATH environment
|
||||
variable to include the common/src directory:
|
||||
|
||||
```
|
||||
PYTHON_COMMON_PATH=${path_to_common_directory}/common/src
|
||||
export PYTHONPATH=$PWD:${PYTHON_COMMON_PATH}
|
||||
python3 myapp.py
|
||||
```
|
||||
|
||||
The most interesting functions are likely found in the classes RaCtlCmds and FileCmds. Classes
|
||||
can be instantiated, for example, as follows
|
||||
(assuming that rascsi host, rascsi port and token are somehow retrieved from a command line
|
||||
argument):
|
||||
|
||||
```
|
||||
sock_cmd = SocketCmds(host=args.rascsi_host, port=args.rascsi_port)
|
||||
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=args.token)
|
||||
```
|
||||
|
||||
Usage examples can be found in the existing RaSCSI Python applications.
|
0
python/common/src/__init__.py
Normal file
0
python/common/src/__init__.py
Normal file
0
python/common/src/rascsi/__init__.py
Normal file
0
python/common/src/rascsi/__init__.py
Normal file
21
python/common/src/rascsi/common_settings.py
Normal file
21
python/common/src/rascsi/common_settings.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""
|
||||
Module for general settings used in the rascsi module
|
||||
"""
|
||||
|
||||
from os import getcwd
|
||||
|
||||
WORK_DIR = getcwd()
|
||||
|
||||
REMOVABLE_DEVICE_TYPES = ("SCCD", "SCRM", "SCMO")
|
||||
|
||||
# There may be a more elegant way to get the HOME dir of the user that installed RaSCSI
|
||||
HOME_DIR = "/".join(WORK_DIR.split("/")[0:3])
|
||||
CFG_DIR = f"{HOME_DIR}/.config/rascsi"
|
||||
CONFIG_FILE_SUFFIX = "json"
|
||||
|
||||
# File ending used for drive properties files
|
||||
PROPERTIES_SUFFIX = "properties"
|
||||
|
||||
# The RESERVATIONS list is used to keep track of the reserved ID memos.
|
||||
# Initialize with a list of 8 empty strings.
|
||||
RESERVATIONS = ["" for x in range(0, 8)]
|
15
python/common/src/rascsi/exceptions.py
Normal file
15
python/common/src/rascsi/exceptions.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""
|
||||
Module for custom exceptions raised by the rascsi module
|
||||
"""
|
||||
|
||||
|
||||
class FailedSocketConnectionException(Exception):
|
||||
"""Raise when a rascsi protobuf socket connection cannot be established after multiple tries."""
|
||||
|
||||
|
||||
class EmptySocketChunkException(Exception):
|
||||
"""Raise when a socket payload contains an empty chunk which implies a possible problem. """
|
||||
|
||||
|
||||
class InvalidProtobufResponse(Exception):
|
||||
"""Raise when a rascsi socket payload contains unpexpected data. """
|
601
python/common/src/rascsi/file_cmds.py
Normal file
601
python/common/src/rascsi/file_cmds.py
Normal file
@ -0,0 +1,601 @@
|
||||
"""
|
||||
Module for methods reading from and writing to the file system
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import PurePath
|
||||
import asyncio
|
||||
import rascsi_interface_pb2 as proto
|
||||
from rascsi.common_settings import CFG_DIR, CONFIG_FILE_SUFFIX, PROPERTIES_SUFFIX, RESERVATIONS
|
||||
from rascsi.ractl_cmds import RaCtlCmds
|
||||
from rascsi.return_codes import ReturnCodes
|
||||
from rascsi.socket_cmds import SocketCmds
|
||||
|
||||
|
||||
class FileCmds:
|
||||
"""
|
||||
class for methods reading from and writing to the file system
|
||||
"""
|
||||
|
||||
def __init__(self, sock_cmd: SocketCmds, ractl: RaCtlCmds, token=None, locale=None):
|
||||
self.sock_cmd = sock_cmd
|
||||
self.ractl = ractl
|
||||
self.token = token
|
||||
self.locale = locale
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
# pylint: disable=no-self-use
|
||||
def list_files(self, file_types, dir_path):
|
||||
"""
|
||||
Takes a (list) or (tuple) of (str) file_types - e.g. ('hda', 'hds')
|
||||
Returns (list) of (list)s files_list:
|
||||
index 0 is (str) file name and index 1 is (int) size in bytes
|
||||
"""
|
||||
files_list = []
|
||||
for path, _dirs, files in os.walk(dir_path):
|
||||
# Only list selected file types
|
||||
files = [f for f in files if f.lower().endswith(file_types)]
|
||||
files_list.extend(
|
||||
[
|
||||
(
|
||||
file,
|
||||
os.path.getsize(os.path.join(path, file))
|
||||
)
|
||||
for file in files
|
||||
]
|
||||
)
|
||||
return files_list
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def list_config_files(self):
|
||||
"""
|
||||
Finds fils with file ending CONFIG_FILE_SUFFIX in CFG_DIR.
|
||||
Returns a (list) of (str) files_list
|
||||
"""
|
||||
files_list = []
|
||||
for _root, _dirs, files in os.walk(CFG_DIR):
|
||||
for file in files:
|
||||
if file.endswith("." + CONFIG_FILE_SUFFIX):
|
||||
files_list.append(file)
|
||||
return files_list
|
||||
|
||||
def list_images(self):
|
||||
"""
|
||||
Sends a IMAGE_FILES_INFO command to the server
|
||||
Returns a (dict) with (bool) status, (str) msg, and (list) of (dict)s files
|
||||
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
|
||||
# Get a list of all *.properties files in CFG_DIR
|
||||
prop_data = self.list_files(PROPERTIES_SUFFIX, CFG_DIR)
|
||||
prop_files = [PurePath(x[0]).stem for x in prop_data]
|
||||
|
||||
from zipfile import ZipFile, is_zipfile
|
||||
server_info = self.ractl.get_server_info()
|
||||
files = []
|
||||
for file in result.image_files_info.image_files:
|
||||
# Add properties meta data for the image, if applicable
|
||||
if file.name in prop_files:
|
||||
process = self.read_drive_properties(f"{CFG_DIR}/{file.name}.{PROPERTIES_SUFFIX}")
|
||||
prop = process["conf"]
|
||||
else:
|
||||
prop = False
|
||||
if file.name.lower().endswith(".zip"):
|
||||
zip_path = f"{server_info['image_dir']}/{file.name}"
|
||||
if is_zipfile(zip_path):
|
||||
zipfile = ZipFile(zip_path)
|
||||
# Get a list of (str) containing all zipfile members
|
||||
zip_members = zipfile.namelist()
|
||||
# Strip out directories from the list
|
||||
zip_members = [x for x in zip_members if not x.endswith("/")]
|
||||
else:
|
||||
logging.warning("%s is an invalid zip file", zip_path)
|
||||
zip_members = False
|
||||
else:
|
||||
zip_members = False
|
||||
|
||||
size_mb = "{:,.1f}".format(file.size / 1024 / 1024)
|
||||
dtype = proto.PbDeviceType.Name(file.type)
|
||||
files.append({
|
||||
"name": file.name,
|
||||
"size": file.size,
|
||||
"size_mb": size_mb,
|
||||
"detected_type": dtype,
|
||||
"prop": prop,
|
||||
"zip_members": zip_members,
|
||||
})
|
||||
|
||||
return {"status": result.status, "msg": result.msg, "files": files}
|
||||
|
||||
def create_new_image(self, file_name, file_type, size):
|
||||
"""
|
||||
Takes (str) file_name, (str) file_type, and (int) size
|
||||
Sends a CREATE_IMAGE command to the server
|
||||
Returns (dict) with (bool) status and (str) msg
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.CREATE_IMAGE
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
command.params["file"] = file_name + "." + file_type
|
||||
command.params["size"] = str(size)
|
||||
command.params["read_only"] = "false"
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
return {"status": result.status, "msg": result.msg}
|
||||
|
||||
def delete_image(self, file_name):
|
||||
"""
|
||||
Takes (str) file_name
|
||||
Sends a DELETE_IMAGE command to the server
|
||||
Returns (dict) with (bool) status and (str) msg
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.DELETE_IMAGE
|
||||
command.params["token"] = self.ractl.token
|
||||
command.params["locale"] = self.ractl.locale
|
||||
|
||||
command.params["file"] = file_name
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
return {"status": result.status, "msg": result.msg}
|
||||
|
||||
def rename_image(self, file_name, new_file_name):
|
||||
"""
|
||||
Takes (str) file_name, (str) new_file_name
|
||||
Sends a RENAME_IMAGE command to the server
|
||||
Returns (dict) with (bool) status and (str) msg
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.RENAME_IMAGE
|
||||
command.params["token"] = self.ractl.token
|
||||
command.params["locale"] = self.ractl.locale
|
||||
|
||||
command.params["from"] = file_name
|
||||
command.params["to"] = new_file_name
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
return {"status": result.status, "msg": result.msg}
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def delete_file(self, file_path):
|
||||
"""
|
||||
Takes (str) file_path with the full path to the file to delete
|
||||
Returns (dict) with (bool) status and (str) msg
|
||||
"""
|
||||
parameters = {
|
||||
"file_path": file_path
|
||||
}
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
return {
|
||||
"status": True,
|
||||
"return_code": ReturnCodes.DELETEFILE_SUCCESS,
|
||||
"parameters": parameters,
|
||||
}
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.DELETEFILE_FILE_NOT_FOUND,
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def rename_file(self, file_path, target_path):
|
||||
"""
|
||||
Takes (str) file_path and (str) target_path
|
||||
Returns (dict) with (bool) status and (str) msg
|
||||
"""
|
||||
parameters = {
|
||||
"target_path": target_path
|
||||
}
|
||||
if os.path.exists(PurePath(target_path).parent):
|
||||
os.rename(file_path, target_path)
|
||||
return {
|
||||
"status": True,
|
||||
"return_code": ReturnCodes.RENAMEFILE_SUCCESS,
|
||||
"parameters": parameters,
|
||||
}
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE,
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
def unzip_file(self, file_name, member=False, members=False):
|
||||
"""
|
||||
Takes (str) file_name, optional (str) member, optional (list) of (str) members
|
||||
file_name is the name of the zip file to unzip
|
||||
member is the full path to the particular file in the zip file to unzip
|
||||
members contains all of the full paths to each of the zip archive members
|
||||
Returns (dict) with (boolean) status and (list of str) msg
|
||||
"""
|
||||
from asyncio import run
|
||||
server_info = self.ractl.get_server_info()
|
||||
prop_flag = False
|
||||
|
||||
if not member:
|
||||
unzip_proc = run(self.run_async(
|
||||
f"unzip -d {server_info['image_dir']} -n -j "
|
||||
f"{server_info['image_dir']}/{file_name}"
|
||||
))
|
||||
if members:
|
||||
for path in members:
|
||||
if path.endswith(PROPERTIES_SUFFIX):
|
||||
name = PurePath(path).name
|
||||
self.rename_file(f"{server_info['image_dir']}/{name}", f"{CFG_DIR}/{name}")
|
||||
prop_flag = True
|
||||
else:
|
||||
from re import escape
|
||||
member = escape(member)
|
||||
unzip_proc = run(self.run_async(
|
||||
f"unzip -d {server_info['image_dir']} -n -j "
|
||||
f"{server_info['image_dir']}/{file_name} {member}"
|
||||
))
|
||||
# Attempt to unzip a properties file in the same archive dir
|
||||
unzip_prop = run(self.run_async(
|
||||
f"unzip -d {CFG_DIR} -n -j "
|
||||
f"{server_info['image_dir']}/{file_name} {member}.{PROPERTIES_SUFFIX}"
|
||||
))
|
||||
if unzip_prop["returncode"] == 0:
|
||||
prop_flag = True
|
||||
if unzip_proc["returncode"] != 0:
|
||||
logging.warning("Unzipping failed: %s", unzip_proc["stderr"])
|
||||
return {"status": False, "msg": unzip_proc["stderr"]}
|
||||
|
||||
from re import findall
|
||||
unzipped = findall(
|
||||
"(?:inflating|extracting):(.+)\n",
|
||||
unzip_proc["stdout"]
|
||||
)
|
||||
return {"status": True, "msg": unzipped, "prop_flag": prop_flag}
|
||||
|
||||
def download_file_to_iso(self, url, *iso_args):
|
||||
"""
|
||||
Takes (str) url and one or more (str) *iso_args
|
||||
Returns (dict) with (bool) status and (str) msg
|
||||
"""
|
||||
from time import time
|
||||
from subprocess import run, CalledProcessError
|
||||
|
||||
server_info = self.ractl.get_server_info()
|
||||
|
||||
file_name = PurePath(url).name
|
||||
tmp_ts = int(time())
|
||||
tmp_dir = "/tmp/" + str(tmp_ts) + "/"
|
||||
os.mkdir(tmp_dir)
|
||||
tmp_full_path = tmp_dir + file_name
|
||||
iso_filename = f"{server_info['image_dir']}/{file_name}.iso"
|
||||
|
||||
req_proc = self.download_to_dir(url, tmp_dir, file_name)
|
||||
|
||||
if not req_proc["status"]:
|
||||
return {"status": False, "msg": req_proc["msg"]}
|
||||
|
||||
from zipfile import is_zipfile, ZipFile
|
||||
if is_zipfile(tmp_full_path):
|
||||
if "XtraStuf.mac" in str(ZipFile(tmp_full_path).namelist()):
|
||||
logging.info("MacZip file format detected. Will not unzip to retain resource fork.")
|
||||
else:
|
||||
logging.info(
|
||||
"%s is a zipfile! Will attempt to unzip and store the resulting files.",
|
||||
tmp_full_path,
|
||||
)
|
||||
unzip_proc = asyncio.run(self.run_async(
|
||||
f"unzip -d {tmp_dir} -n {tmp_full_path}"
|
||||
))
|
||||
if not unzip_proc["returncode"]:
|
||||
logging.info(
|
||||
"%s was successfully unzipped. Deleting the zipfile.",
|
||||
tmp_full_path,
|
||||
)
|
||||
self.delete_file(tmp_full_path)
|
||||
|
||||
try:
|
||||
run(
|
||||
[
|
||||
"genisoimage",
|
||||
*iso_args,
|
||||
"-o",
|
||||
iso_filename,
|
||||
tmp_dir,
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
except CalledProcessError as error:
|
||||
logging.warning("Executed shell command: %s", " ".join(error.cmd))
|
||||
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
|
||||
return {"status": False, "msg": error.stderr.decode("utf-8")}
|
||||
|
||||
parameters = {
|
||||
"value": " ".join(iso_args)
|
||||
}
|
||||
return {
|
||||
"status": True,
|
||||
"return_code": ReturnCodes.DOWNLOADFILETOISO_SUCCESS,
|
||||
"parameters": parameters,
|
||||
"file_name": iso_filename,
|
||||
}
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def download_to_dir(self, url, save_dir, file_name):
|
||||
"""
|
||||
Takes (str) url, (str) save_dir, (str) file_name
|
||||
Returns (dict) with (bool) status and (str) msg
|
||||
"""
|
||||
import requests
|
||||
logging.info("Making a request to download %s", url)
|
||||
|
||||
try:
|
||||
with requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"}) as req:
|
||||
req.raise_for_status()
|
||||
with open(f"{save_dir}/{file_name}", "wb") as download:
|
||||
for chunk in req.iter_content(chunk_size=8192):
|
||||
download.write(chunk)
|
||||
except requests.exceptions.RequestException as error:
|
||||
logging.warning("Request failed: %s", str(error))
|
||||
return {"status": False, "msg": str(error)}
|
||||
|
||||
logging.info("Response encoding: %s", req.encoding)
|
||||
logging.info("Response content-type: %s", req.headers["content-type"])
|
||||
logging.info("Response status code: %s", req.status_code)
|
||||
|
||||
parameters = {
|
||||
"file_name": file_name,
|
||||
"save_dir": save_dir
|
||||
}
|
||||
return {
|
||||
"status": True,
|
||||
"return_code": ReturnCodes.DOWNLOADTODIR_SUCCESS,
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
def write_config(self, file_name):
|
||||
"""
|
||||
Takes (str) file_name
|
||||
Returns (dict) with (bool) status and (str) msg
|
||||
"""
|
||||
from json import dump
|
||||
file_name = f"{CFG_DIR}/{file_name}"
|
||||
try:
|
||||
with open(file_name, "w", encoding="ISO-8859-1") as json_file:
|
||||
version = self.ractl.get_server_info()["version"]
|
||||
devices = self.ractl.list_devices()["device_list"]
|
||||
for device in devices:
|
||||
# Remove keys that we don't want to store in the file
|
||||
del device["status"]
|
||||
del device["file"]
|
||||
# It's cleaner not to store an empty parameter for every device without media
|
||||
if device["image"] == "":
|
||||
device["image"] = None
|
||||
# RaSCSI product names will be generated on the fly by RaSCSI
|
||||
if device["vendor"] == "RaSCSI":
|
||||
device["vendor"] = device["product"] = device["revision"] = None
|
||||
# A block size of 0 is how RaSCSI indicates N/A for block size
|
||||
if device["block_size"] == 0:
|
||||
device["block_size"] = None
|
||||
# Convert to a data type that can be serialized
|
||||
device["params"] = dict(device["params"])
|
||||
reserved_ids_and_memos = []
|
||||
reserved_ids = self.ractl.get_reserved_ids()["ids"]
|
||||
for scsi_id in reserved_ids:
|
||||
reserved_ids_and_memos.append({"id": scsi_id,
|
||||
"memo": RESERVATIONS[int(scsi_id)]})
|
||||
dump(
|
||||
{"version": version,
|
||||
"devices": devices,
|
||||
"reserved_ids": reserved_ids_and_memos},
|
||||
json_file,
|
||||
indent=4
|
||||
)
|
||||
parameters = {
|
||||
"file_name": file_name
|
||||
}
|
||||
return {
|
||||
"status": True,
|
||||
"return_code": ReturnCodes.WRITECONFIG_SUCCESS,
|
||||
"parameters": parameters,
|
||||
}
|
||||
except (IOError, ValueError, EOFError, TypeError) as error:
|
||||
logging.error(str(error))
|
||||
self.delete_file(file_name)
|
||||
return {"status": False, "msg": str(error)}
|
||||
except:
|
||||
logging.error("Could not write to file: %s", file_name)
|
||||
self.delete_file(file_name)
|
||||
parameters = {
|
||||
"file_name": file_name
|
||||
}
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.WRITECONFIG_COULD_NOT_WRITE,
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
def read_config(self, file_name):
|
||||
"""
|
||||
Takes (str) file_name
|
||||
Returns (dict) with (bool) status and (str) msg
|
||||
"""
|
||||
from json import load
|
||||
file_name = f"{CFG_DIR}/{file_name}"
|
||||
try:
|
||||
with open(file_name, encoding="ISO-8859-1") as json_file:
|
||||
config = load(json_file)
|
||||
# If the config file format changes again in the future,
|
||||
# introduce more sophisticated format detection logic here.
|
||||
if isinstance(config, dict):
|
||||
self.ractl.detach_all()
|
||||
ids_to_reserve = []
|
||||
for item in config["reserved_ids"]:
|
||||
ids_to_reserve.append(item["id"])
|
||||
RESERVATIONS[int(item["id"])] = item["memo"]
|
||||
self.ractl.reserve_scsi_ids(ids_to_reserve)
|
||||
for row in config["devices"]:
|
||||
kwargs = {
|
||||
"device_type": row["device_type"],
|
||||
"image": row["image"],
|
||||
"unit": int(row["unit"]),
|
||||
"vendor": row["vendor"],
|
||||
"product": row["product"],
|
||||
"revision": row["revision"],
|
||||
"block_size": row["block_size"],
|
||||
}
|
||||
params = dict(row["params"])
|
||||
for param in params.keys():
|
||||
kwargs[param] = params[param]
|
||||
self.ractl.attach_image(row["id"], **kwargs)
|
||||
# The config file format in RaSCSI 21.10 is using a list data type at the top level.
|
||||
# If future config file formats return to the list data type,
|
||||
# introduce more sophisticated format detection logic here.
|
||||
elif isinstance(config, list):
|
||||
self.ractl.detach_all()
|
||||
for row in config:
|
||||
kwargs = {
|
||||
"device_type": row["device_type"],
|
||||
"image": row["image"],
|
||||
# "un" for backwards compatibility
|
||||
"unit": int(row["un"]),
|
||||
"vendor": row["vendor"],
|
||||
"product": row["product"],
|
||||
"revision": row["revision"],
|
||||
"block_size": row["block_size"],
|
||||
}
|
||||
params = dict(row["params"])
|
||||
for param in params.keys():
|
||||
kwargs[param] = params[param]
|
||||
self.ractl.attach_image(row["id"], **kwargs)
|
||||
else:
|
||||
return {"status": False,
|
||||
"return_code": ReturnCodes.READCONFIG_INVALID_CONFIG_FILE_FORMAT}
|
||||
|
||||
parameters = {
|
||||
"file_name": file_name
|
||||
}
|
||||
return {
|
||||
"status": True,
|
||||
"return_code": ReturnCodes.READCONFIG_SUCCESS,
|
||||
"parameters": parameters
|
||||
}
|
||||
except (IOError, ValueError, EOFError, TypeError) as error:
|
||||
logging.error(str(error))
|
||||
return {"status": False, "msg": str(error)}
|
||||
except:
|
||||
logging.error("Could not read file: %s", file_name)
|
||||
parameters = {
|
||||
"file_name": file_name
|
||||
}
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.READCONFIG_COULD_NOT_READ,
|
||||
"parameters": parameters
|
||||
}
|
||||
|
||||
def write_drive_properties(self, file_name, conf):
|
||||
"""
|
||||
Writes a drive property configuration file to the config dir.
|
||||
Takes file name base (str) and (list of dicts) conf as arguments
|
||||
Returns (dict) with (bool) status and (str) msg
|
||||
"""
|
||||
from json import dump
|
||||
file_path = f"{CFG_DIR}/{file_name}"
|
||||
try:
|
||||
with open(file_path, "w") as json_file:
|
||||
dump(conf, json_file, indent=4)
|
||||
parameters = {
|
||||
"file_path": file_path
|
||||
}
|
||||
return {
|
||||
"status": True,
|
||||
"return_code": ReturnCodes.WRITEDRIVEPROPS_SUCCESS,
|
||||
"parameters": parameters,
|
||||
}
|
||||
except (IOError, ValueError, EOFError, TypeError) as error:
|
||||
logging.error(str(error))
|
||||
self.delete_file(file_path)
|
||||
return {"status": False, "msg": str(error)}
|
||||
except:
|
||||
logging.error("Could not write to file: %s", file_path)
|
||||
self.delete_file(file_path)
|
||||
parameters = {
|
||||
"file_path": file_path
|
||||
}
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.WRITEDRIVEPROPS_COULD_NOT_WRITE,
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def read_drive_properties(self, file_path):
|
||||
"""
|
||||
Reads drive properties from json formatted file.
|
||||
Takes (str) file_path as argument.
|
||||
Returns (dict) with (bool) status, (str) msg, (dict) conf
|
||||
"""
|
||||
from json import load
|
||||
try:
|
||||
with open(file_path) as json_file:
|
||||
conf = load(json_file)
|
||||
parameters = {
|
||||
"file_path": file_path
|
||||
}
|
||||
return {
|
||||
"status": True,
|
||||
"return_codes": ReturnCodes.READDRIVEPROPS_SUCCESS,
|
||||
"parameters": parameters,
|
||||
"conf": conf,
|
||||
}
|
||||
except (IOError, ValueError, EOFError, TypeError) as error:
|
||||
logging.error(str(error))
|
||||
return {"status": False, "msg": str(error)}
|
||||
except:
|
||||
logging.error("Could not read file: %s", file_path)
|
||||
parameters = {
|
||||
"file_path": file_path
|
||||
}
|
||||
return {
|
||||
"status": False,
|
||||
"return_codes": ReturnCodes.READDRIVEPROPS_COULD_NOT_READ,
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
async def run_async(self, cmd):
|
||||
"""
|
||||
Takes (str) cmd with the shell command to execute
|
||||
Executes shell command and captures output
|
||||
Returns (dict) with (int) returncode, (str) stdout, (str) stderr
|
||||
"""
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE)
|
||||
|
||||
stdout, stderr = await proc.communicate()
|
||||
|
||||
logging.info("Executed command \"%s\" with status code %d", cmd, proc.returncode)
|
||||
if stdout:
|
||||
stdout = stdout.decode()
|
||||
logging.info("stdout: %s", stdout)
|
||||
if stderr:
|
||||
stderr = stderr.decode()
|
||||
logging.info("stderr: %s", stderr)
|
||||
|
||||
return {"returncode": proc.returncode, "stdout": stdout, "stderr": stderr}
|
444
python/common/src/rascsi/ractl_cmds.py
Normal file
444
python/common/src/rascsi/ractl_cmds.py
Normal file
@ -0,0 +1,444 @@
|
||||
"""
|
||||
Module for commands sent to the RaSCSI backend service.
|
||||
"""
|
||||
|
||||
import rascsi_interface_pb2 as proto
|
||||
from rascsi.common_settings import REMOVABLE_DEVICE_TYPES
|
||||
from rascsi.return_codes import ReturnCodes
|
||||
from rascsi.socket_cmds import SocketCmds
|
||||
|
||||
|
||||
class RaCtlCmds:
|
||||
"""
|
||||
Class for commands sent to the RaSCSI backend service.
|
||||
"""
|
||||
def __init__(self, sock_cmd: SocketCmds, token=None, locale="en"):
|
||||
self.sock_cmd = sock_cmd
|
||||
self.token = token
|
||||
self.locale = locale
|
||||
|
||||
def get_server_info(self):
|
||||
"""
|
||||
Sends a SERVER_INFO command to the server.
|
||||
Returns a dict with:
|
||||
- (bool) status
|
||||
- (str) version (RaSCSI version number)
|
||||
- (list) of (str) log_levels (the log levels RaSCSI supports)
|
||||
- (str) current_log_level
|
||||
- (list) of (int) reserved_ids
|
||||
- (str) image_dir, path to the default images directory
|
||||
- (int) scan_depth, the current images directory scan depth
|
||||
- 5 distinct (list)s of (str)s with file endings recognized by RaSCSI
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.SERVER_INFO
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
version = (str(result.server_info.version_info.major_version) + "." +
|
||||
str(result.server_info.version_info.minor_version) + "." +
|
||||
str(result.server_info.version_info.patch_version))
|
||||
log_levels = result.server_info.log_level_info.log_levels
|
||||
current_log_level = result.server_info.log_level_info.current_log_level
|
||||
reserved_ids = list(result.server_info.reserved_ids_info.ids)
|
||||
image_dir = result.server_info.image_files_info.default_image_folder
|
||||
scan_depth = result.server_info.image_files_info.depth
|
||||
|
||||
# Creates lists of file endings recognized by RaSCSI
|
||||
mappings = result.server_info.mapping_info.mapping
|
||||
sahd = []
|
||||
schd = []
|
||||
scrm = []
|
||||
scmo = []
|
||||
sccd = []
|
||||
for dtype in mappings:
|
||||
if mappings[dtype] == proto.PbDeviceType.SAHD:
|
||||
sahd.append(dtype)
|
||||
elif mappings[dtype] == proto.PbDeviceType.SCHD:
|
||||
schd.append(dtype)
|
||||
elif mappings[dtype] == proto.PbDeviceType.SCRM:
|
||||
scrm.append(dtype)
|
||||
elif mappings[dtype] == proto.PbDeviceType.SCMO:
|
||||
scmo.append(dtype)
|
||||
elif mappings[dtype] == proto.PbDeviceType.SCCD:
|
||||
sccd.append(dtype)
|
||||
|
||||
return {
|
||||
"status": result.status,
|
||||
"version": version,
|
||||
"log_levels": log_levels,
|
||||
"current_log_level": current_log_level,
|
||||
"reserved_ids": reserved_ids,
|
||||
"image_dir": image_dir,
|
||||
"scan_depth": scan_depth,
|
||||
"sahd": sahd,
|
||||
"schd": schd,
|
||||
"scrm": scrm,
|
||||
"scmo": scmo,
|
||||
"sccd": sccd,
|
||||
}
|
||||
|
||||
def get_reserved_ids(self):
|
||||
"""
|
||||
Sends a RESERVED_IDS_INFO command to the server.
|
||||
Returns a dict with:
|
||||
- (bool) status
|
||||
- (list) of (int) ids -- currently reserved SCSI IDs
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.RESERVED_IDS_INFO
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
scsi_ids = []
|
||||
for scsi_id in result.reserved_ids_info.ids:
|
||||
scsi_ids.append(str(scsi_id))
|
||||
|
||||
return {"status": result.status, "ids": scsi_ids}
|
||||
|
||||
def get_network_info(self):
|
||||
"""
|
||||
Sends a NETWORK_INTERFACES_INFO command to the server.
|
||||
Returns a dict with:
|
||||
- (bool) status
|
||||
- (list) of (str) ifs (network interfaces detected by RaSCSI)
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.NETWORK_INTERFACES_INFO
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
ifs = result.network_interfaces_info.name
|
||||
return {"status": result.status, "ifs": ifs}
|
||||
|
||||
def get_device_types(self):
|
||||
"""
|
||||
Sends a DEVICE_TYPES_INFO command to the server.
|
||||
Returns a dict with:
|
||||
- (bool) status
|
||||
- (list) of (str) device_types (device types that RaSCSI supports, ex. SCHD, SCCD, etc)
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.DEVICE_TYPES_INFO
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
device_types = []
|
||||
for prop in result.device_types_info.properties:
|
||||
device_types.append(proto.PbDeviceType.Name(prop.type))
|
||||
return {"status": result.status, "device_types": device_types}
|
||||
|
||||
def get_image_files_info(self):
|
||||
"""
|
||||
Sends a DEFAULT_IMAGE_FILES_INFO command to the server.
|
||||
Returns a dict with:
|
||||
- (bool) status
|
||||
- (str) images_dir, path to images dir
|
||||
- (list) of (str) image_files
|
||||
- (int) scan_depth, the current scan depth
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.token
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
images_dir = result.image_files_info.default_image_folder
|
||||
image_files = result.image_files_info.image_files
|
||||
scan_depth = result.image_files_info.depth
|
||||
return {
|
||||
"status": result.status,
|
||||
"images_dir": images_dir,
|
||||
"image_files": image_files,
|
||||
"scan_depth": scan_depth,
|
||||
}
|
||||
|
||||
def attach_image(self, scsi_id, **kwargs):
|
||||
"""
|
||||
Takes (int) scsi_id and kwargs containing 0 or more device properties
|
||||
|
||||
If the current attached device is a removable device wihout media inserted,
|
||||
this sends a INJECT command to the server.
|
||||
If there is no currently attached device, this sends the ATTACH command to the server.
|
||||
|
||||
Returns (bool) status and (str) msg
|
||||
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
devices = proto.PbDeviceDefinition()
|
||||
devices.id = int(scsi_id)
|
||||
|
||||
if "device_type" in kwargs.keys():
|
||||
if kwargs["device_type"] not in [None, ""]:
|
||||
devices.type = proto.PbDeviceType.Value(str(kwargs["device_type"]))
|
||||
if "unit" in kwargs.keys():
|
||||
if kwargs["unit"] not in [None, ""]:
|
||||
devices.unit = kwargs["unit"]
|
||||
if "image" in kwargs.keys():
|
||||
if kwargs["image"] not in [None, ""]:
|
||||
devices.params["file"] = kwargs["image"]
|
||||
|
||||
# Handling the inserting of media into an attached removable type device
|
||||
device_type = kwargs.get("device_type", None)
|
||||
currently_attached = self.list_devices(scsi_id, kwargs.get("unit"))["device_list"]
|
||||
if currently_attached:
|
||||
current_type = currently_attached[0]["device_type"]
|
||||
else:
|
||||
current_type = None
|
||||
|
||||
if device_type in REMOVABLE_DEVICE_TYPES and current_type in REMOVABLE_DEVICE_TYPES:
|
||||
if current_type != device_type:
|
||||
parameters = {
|
||||
"device_type": device_type,
|
||||
"current_device_type": current_type
|
||||
}
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.ATTACHIMAGE_COULD_NOT_ATTACH,
|
||||
"parameters": parameters,
|
||||
}
|
||||
command.operation = proto.PbOperation.INSERT
|
||||
# Handling attaching a new device
|
||||
else:
|
||||
command.operation = proto.PbOperation.ATTACH
|
||||
if "interfaces" in kwargs.keys():
|
||||
if kwargs["interfaces"] not in [None, ""]:
|
||||
devices.params["interfaces"] = kwargs["interfaces"]
|
||||
if "vendor" in kwargs.keys():
|
||||
if kwargs["vendor"] is not None:
|
||||
devices.vendor = kwargs["vendor"]
|
||||
if "product" in kwargs.keys():
|
||||
if kwargs["product"] is not None:
|
||||
devices.product = kwargs["product"]
|
||||
if "revision" in kwargs.keys():
|
||||
if kwargs["revision"] is not None:
|
||||
devices.revision = kwargs["revision"]
|
||||
if "block_size" in kwargs.keys():
|
||||
if kwargs["block_size"] not in [None, ""]:
|
||||
devices.block_size = int(kwargs["block_size"])
|
||||
|
||||
command.devices.append(devices)
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
return {"status": result.status, "msg": result.msg}
|
||||
|
||||
def detach_by_id(self, scsi_id, unit=None):
|
||||
"""
|
||||
Takes (int) scsi_id and optional (int) unit.
|
||||
Sends a DETACH command to the server.
|
||||
Returns (bool) status and (str) msg.
|
||||
"""
|
||||
devices = proto.PbDeviceDefinition()
|
||||
devices.id = int(scsi_id)
|
||||
if unit is not None:
|
||||
devices.unit = int(unit)
|
||||
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.DETACH
|
||||
command.devices.append(devices)
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
return {"status": result.status, "msg": result.msg}
|
||||
|
||||
def detach_all(self):
|
||||
"""
|
||||
Sends a DETACH_ALL command to the server.
|
||||
Returns (bool) status and (str) msg.
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.DETACH_ALL
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
return {"status": result.status, "msg": result.msg}
|
||||
|
||||
def eject_by_id(self, scsi_id, unit=None):
|
||||
"""
|
||||
Takes (int) scsi_id and optional (int) unit.
|
||||
Sends an EJECT command to the server.
|
||||
Returns (bool) status and (str) msg.
|
||||
"""
|
||||
devices = proto.PbDeviceDefinition()
|
||||
devices.id = int(scsi_id)
|
||||
if unit is not None:
|
||||
devices.unit = int(unit)
|
||||
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.EJECT
|
||||
command.devices.append(devices)
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
return {"status": result.status, "msg": result.msg}
|
||||
|
||||
def list_devices(self, scsi_id=None, unit=None):
|
||||
"""
|
||||
Takes optional (int) scsi_id and optional (int) unit.
|
||||
Sends a DEVICES_INFO command to the server.
|
||||
If no scsi_id is provided, returns a (list) of (dict)s of all attached devices.
|
||||
If scsi_id is is provided, returns a (list) of one (dict) for the given device.
|
||||
If no attached device is found, returns an empty (list).
|
||||
Returns (bool) status, (list) of dicts device_list
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.DEVICES_INFO
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
# If method is called with scsi_id parameter, return the info on those devices
|
||||
# Otherwise, return the info on all attached devices
|
||||
if scsi_id is not None:
|
||||
device = proto.PbDeviceDefinition()
|
||||
device.id = int(scsi_id)
|
||||
if unit is not None:
|
||||
device.unit = int(unit)
|
||||
command.devices.append(device)
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
|
||||
device_list = []
|
||||
|
||||
# Return an empty (list) if no devices are attached
|
||||
if not result.devices_info.devices:
|
||||
return {"status": False, "device_list": []}
|
||||
|
||||
image_files_info = self.get_image_files_info()
|
||||
i = 0
|
||||
while i < len(result.devices_info.devices):
|
||||
did = result.devices_info.devices[i].id
|
||||
dunit = result.devices_info.devices[i].unit
|
||||
dtype = proto.PbDeviceType.Name(result.devices_info.devices[i].type)
|
||||
dstat = result.devices_info.devices[i].status
|
||||
dprop = result.devices_info.devices[i].properties
|
||||
|
||||
# Building the status string
|
||||
dstat_msg = []
|
||||
if dprop.read_only:
|
||||
dstat_msg.append("Read-Only")
|
||||
if dstat.protected and dprop.protectable:
|
||||
dstat_msg.append("Write-Protected")
|
||||
if dstat.removed and dprop.removable:
|
||||
dstat_msg.append("No Media")
|
||||
if dstat.locked and dprop.lockable:
|
||||
dstat_msg.append("Locked")
|
||||
|
||||
dpath = result.devices_info.devices[i].file.name
|
||||
dfile = dpath.replace(image_files_info["images_dir"] + "/", "")
|
||||
dparam = result.devices_info.devices[i].params
|
||||
dven = result.devices_info.devices[i].vendor
|
||||
dprod = result.devices_info.devices[i].product
|
||||
drev = result.devices_info.devices[i].revision
|
||||
dblock = result.devices_info.devices[i].block_size
|
||||
dsize = int(result.devices_info.devices[i].block_count) * int(dblock)
|
||||
|
||||
device_list.append({
|
||||
"id": did,
|
||||
"unit": dunit,
|
||||
"device_type": dtype,
|
||||
"status": ", ".join(dstat_msg),
|
||||
"image": dpath,
|
||||
"file": dfile,
|
||||
"params": dparam,
|
||||
"vendor": dven,
|
||||
"product": dprod,
|
||||
"revision": drev,
|
||||
"block_size": dblock,
|
||||
"size": dsize,
|
||||
})
|
||||
i += 1
|
||||
|
||||
return {"status": result.status, "msg": result.msg, "device_list": device_list}
|
||||
|
||||
def reserve_scsi_ids(self, reserved_scsi_ids):
|
||||
"""
|
||||
Sends the RESERVE_IDS command to the server to reserve SCSI IDs.
|
||||
Takes a (list) of (str) as argument.
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.RESERVE_IDS
|
||||
command.params["ids"] = ",".join(reserved_scsi_ids)
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
return {"status": result.status, "msg": result.msg}
|
||||
|
||||
def set_log_level(self, log_level):
|
||||
"""
|
||||
Sends a LOG_LEVEL command to the server.
|
||||
Takes (str) log_level as an argument.
|
||||
Returns (bool) status and (str) msg.
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.LOG_LEVEL
|
||||
command.params["level"] = str(log_level)
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
return {"status": result.status, "msg": result.msg}
|
||||
|
||||
def shutdown_pi(self, mode):
|
||||
"""
|
||||
Sends a SHUT_DOWN command to the server.
|
||||
Takes (str) mode as an argument.
|
||||
Returns (bool) status and (str) msg.
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.SHUT_DOWN
|
||||
command.params["mode"] = str(mode)
|
||||
command.params["token"] = self.token
|
||||
command.params["locale"] = self.locale
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
return {"status": result.status, "msg": result.msg}
|
||||
|
||||
def is_token_auth(self):
|
||||
"""
|
||||
Sends a CHECK_AUTHENTICATION command to the server.
|
||||
Tells you whether RaSCSI backend is protected by a token password or not.
|
||||
Returns (bool) status and (str) msg.
|
||||
"""
|
||||
command = proto.PbCommand()
|
||||
command.operation = proto.PbOperation.CHECK_AUTHENTICATION
|
||||
|
||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||
result = proto.PbResult()
|
||||
result.ParseFromString(data)
|
||||
return {"status": result.status, "msg": result.msg}
|
25
python/common/src/rascsi/return_codes.py
Normal file
25
python/common/src/rascsi/return_codes.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""
|
||||
Module for return codes that are refrenced in the return payloads of the rascsi module.
|
||||
"""
|
||||
from typing import Final
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ReturnCodes:
|
||||
"""Class for the return codes used within the rascsi module."""
|
||||
DELETEFILE_SUCCESS: Final = 0
|
||||
DELETEFILE_FILE_NOT_FOUND: Final = 1
|
||||
RENAMEFILE_SUCCESS: Final = 10
|
||||
RENAMEFILE_UNABLE_TO_MOVE: Final = 11
|
||||
DOWNLOADFILETOISO_SUCCESS: Final = 20
|
||||
DOWNLOADTODIR_SUCCESS: Final = 30
|
||||
WRITECONFIG_SUCCESS: Final = 40
|
||||
WRITECONFIG_COULD_NOT_WRITE: Final = 41
|
||||
READCONFIG_SUCCESS: Final = 50
|
||||
READCONFIG_COULD_NOT_READ: Final = 51
|
||||
READCONFIG_INVALID_CONFIG_FILE_FORMAT: Final = 51
|
||||
WRITEDRIVEPROPS_SUCCESS: Final = 60
|
||||
WRITEDRIVEPROPS_COULD_NOT_WRITE: Final = 61
|
||||
READDRIVEPROPS_SUCCESS: Final = 70
|
||||
READDRIVEPROPS_COULD_NOT_READ: Final = 71
|
||||
ATTACHIMAGE_COULD_NOT_ATTACH: Final = 80
|
91
python/common/src/rascsi/socket_cmds.py
Normal file
91
python/common/src/rascsi/socket_cmds.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""
|
||||
Module for sending and receiving data over a socket connection with the RaSCSI backend
|
||||
"""
|
||||
|
||||
import logging
|
||||
from time import sleep
|
||||
from rascsi.exceptions import (EmptySocketChunkException,
|
||||
InvalidProtobufResponse,
|
||||
FailedSocketConnectionException)
|
||||
|
||||
|
||||
class SocketCmds:
|
||||
"""
|
||||
Class for sending and receiving data over a socket connection with the RaSCSI backend
|
||||
"""
|
||||
def __init__(self, host="localhost", port=6868):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
def send_pb_command(self, payload):
|
||||
"""
|
||||
Takes a (str) containing a serialized protobuf as argument.
|
||||
Establishes a socket connection with RaSCSI.
|
||||
"""
|
||||
|
||||
counter = 0
|
||||
tries = 20
|
||||
error_msg = ""
|
||||
|
||||
import socket
|
||||
while counter < tries:
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.connect((self.host, self.port))
|
||||
response = self.send_over_socket(sock, payload)
|
||||
return response
|
||||
except socket.error as error:
|
||||
counter += 1
|
||||
logging.warning("The RaSCSI service is not responding - attempt %s/%s",
|
||||
str(counter), str(tries))
|
||||
error_msg = str(error)
|
||||
sleep(0.2)
|
||||
except EmptySocketChunkException as ex:
|
||||
raise ex
|
||||
except InvalidProtobufResponse as ex:
|
||||
raise ex
|
||||
|
||||
logging.error(error_msg)
|
||||
raise FailedSocketConnectionException(error_msg)
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def send_over_socket(self, sock, payload):
|
||||
"""
|
||||
Takes a socket object and (str) payload with serialized protobuf.
|
||||
Sends payload to RaSCSI over socket and captures the response.
|
||||
Tries to extract and interpret the protobuf header to get response size.
|
||||
Reads data from socket in 2048 bytes chunks until all data is received.
|
||||
"""
|
||||
from struct import pack, unpack
|
||||
|
||||