mirror of https://github.com/akuker/RASCSI.git
620 lines
23 KiB
Python
620 lines
23 KiB
Python
"""
|
|
Module for commands sent to the PiSCSI backend service.
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import PurePath, Path
|
|
from functools import lru_cache
|
|
|
|
import piscsi_interface_pb2 as proto
|
|
from piscsi.return_codes import ReturnCodes
|
|
from piscsi.socket_cmds import SocketCmds
|
|
|
|
from piscsi.common_settings import (
|
|
CFG_DIR,
|
|
PROPERTIES_SUFFIX,
|
|
ARCHIVE_FILE_SUFFIXES,
|
|
)
|
|
|
|
from util import unarchiver
|
|
|
|
|
|
class PiscsiCmds:
|
|
"""
|
|
Class for commands sent to the PiSCSI backend service.
|
|
"""
|
|
|
|
def __init__(self, sock_cmd: SocketCmds, token=None, locale="en"):
|
|
self.sock_cmd = sock_cmd
|
|
self.token = token
|
|
self.locale = locale
|
|
|
|
def send_pb_command(self, command):
|
|
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
|
logging.debug(self.format_pb_command(command))
|
|
|
|
return self.sock_cmd.send_pb_command(command.SerializeToString())
|
|
|
|
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
|
|
"""
|
|
from piscsi.file_cmds import FileCmds
|
|
|
|
self.file_cmd = FileCmds(piscsi=self)
|
|
|
|
command = proto.PbCommand()
|
|
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO
|
|
command.params["token"] = self.token
|
|
command.params["locale"] = self.locale
|
|
|
|
data = self.send_pb_command(command)
|
|
result = proto.PbResult()
|
|
result.ParseFromString(data)
|
|
|
|
server_info = self.get_server_info()
|
|
files = []
|
|
for file in result.image_files_info.image_files:
|
|
prop_file_path = Path(CFG_DIR) / f"{file.name}.{PROPERTIES_SUFFIX}"
|
|
# Add properties meta data for the image, if matching prop file is found
|
|
if prop_file_path.exists():
|
|
process = self.file_cmd.read_drive_properties(prop_file_path)
|
|
prop = process["conf"]
|
|
else:
|
|
prop = False
|
|
|
|
archive_contents = []
|
|
if PurePath(file.name).suffix.lower()[1:] in ARCHIVE_FILE_SUFFIXES:
|
|
try:
|
|
archive_info = self._get_archive_info(
|
|
f"{server_info['image_dir']}/{file.name}",
|
|
_cache_extra_key=file.size,
|
|
)
|
|
|
|
properties_files = [
|
|
x["path"]
|
|
for x in archive_info["members"]
|
|
if x["path"].endswith(PROPERTIES_SUFFIX)
|
|
]
|
|
|
|
for member in archive_info["members"]:
|
|
if member["is_dir"] or member["is_resource_fork"]:
|
|
continue
|
|
|
|
if PurePath(member["path"]).suffix.lower()[1:] == PROPERTIES_SUFFIX:
|
|
member["is_properties_file"] = True
|
|
elif f"{member['path']}.{PROPERTIES_SUFFIX}" in properties_files:
|
|
member["related_properties_file"] = (
|
|
f"{member['path']}.{PROPERTIES_SUFFIX}"
|
|
)
|
|
|
|
archive_contents.append(member)
|
|
except (unarchiver.LsarCommandError, unarchiver.LsarOutputError):
|
|
pass
|
|
|
|
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,
|
|
"archive_contents": archive_contents,
|
|
}
|
|
)
|
|
|
|
return {"status": result.status, "msg": result.msg, "files": files}
|
|
|
|
def get_server_info(self):
|
|
"""
|
|
Sends a SERVER_INFO command to the server.
|
|
Returns a dict with:
|
|
- (bool) status
|
|
- (str) version (PiSCSI version number)
|
|
- (list) of (str) log_levels (the log levels PiSCSI 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 PiSCSI
|
|
"""
|
|
command = proto.PbCommand()
|
|
command.operation = proto.PbOperation.SERVER_INFO
|
|
command.params["token"] = self.token
|
|
command.params["locale"] = self.locale
|
|
|
|
data = self.send_pb_command(command)
|
|
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 = list(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 PiSCSI
|
|
mappings = result.server_info.mapping_info.mapping
|
|
schd = []
|
|
scrm = []
|
|
scmo = []
|
|
sccd = []
|
|
for dtype in mappings:
|
|
if 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,
|
|
"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.send_pb_command(command)
|
|
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 PiSCSI)
|
|
"""
|
|
command = proto.PbCommand()
|
|
command.operation = proto.PbOperation.NETWORK_INTERFACES_INFO
|
|
command.params["token"] = self.token
|
|
command.params["locale"] = self.locale
|
|
|
|
data = self.send_pb_command(command)
|
|
result = proto.PbResult()
|
|
result.ParseFromString(data)
|
|
ifs = result.network_interfaces_info.name
|
|
return {"status": result.status, "ifs": list(ifs)}
|
|
|
|
def get_device_types(self):
|
|
"""
|
|
Sends a DEVICE_TYPES_INFO command to the server.
|
|
Returns a dict with:
|
|
- (bool) status
|
|
- (dict) device_types, where keys are the four letter device type acronym,
|
|
and the value is a (dict) of supported parameters and their default values.
|
|
"""
|
|
command = proto.PbCommand()
|
|
command.operation = proto.PbOperation.DEVICE_TYPES_INFO
|
|
command.params["token"] = self.token
|
|
command.params["locale"] = self.locale
|
|
|
|
data = self.send_pb_command(command)
|
|
result = proto.PbResult()
|
|
result.ParseFromString(data)
|
|
device_types = {}
|
|
for device in result.device_types_info.properties:
|
|
params = {}
|
|
for key, value in device.properties.default_params.items():
|
|
params[key] = value
|
|
device_types[proto.PbDeviceType.Name(device.type)] = {
|
|
"removable": device.properties.removable,
|
|
"supports_file": device.properties.supports_file,
|
|
"params": params,
|
|
"block_sizes": list(device.properties.block_sizes),
|
|
}
|
|
return {"status": result.status, "device_types": device_types}
|
|
|
|
def get_removable_device_types(self):
|
|
"""
|
|
Returns a (list) of (str) of four letter device acronyms
|
|
that are of the removable type.
|
|
"""
|
|
device_types = self.get_device_types()
|
|
removable_device_types = []
|
|
for device, value in device_types["device_types"].items():
|
|
if value["removable"]:
|
|
removable_device_types.append(device)
|
|
return removable_device_types
|
|
|
|
def get_disk_device_types(self):
|
|
"""
|
|
Returns a (list) of (str) of four letter device acronyms
|
|
that take image files as arguments.
|
|
"""
|
|
device_types = self.get_device_types()
|
|
disk_device_types = []
|
|
for device, value in device_types["device_types"].items():
|
|
if value["supports_file"]:
|
|
disk_device_types.append(device)
|
|
return disk_device_types
|
|
|
|
def get_peripheral_device_types(self):
|
|
"""
|
|
Returns a (list) of (str) of four letter device acronyms
|
|
that don't take image files as arguments.
|
|
"""
|
|
device_types = self.get_device_types()
|
|
image_device_types = self.get_disk_device_types()
|
|
peripheral_device_types = [
|
|
x for x in device_types["device_types"] if x not in image_device_types
|
|
]
|
|
return peripheral_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.send_pb_command(command)
|
|
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_device(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 kwargs.get("device_type"):
|
|
devices.type = proto.PbDeviceType.Value(str(kwargs["device_type"]))
|
|
if kwargs.get("unit"):
|
|
devices.unit = kwargs["unit"]
|
|
if kwargs.get("params") and isinstance(kwargs["params"], dict):
|
|
for param in kwargs["params"]:
|
|
devices.params[param] = kwargs["params"][param]
|
|
|
|
# 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
|
|
|
|
removable_device_types = self.get_removable_device_types()
|
|
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 kwargs.get("vendor"):
|
|
devices.vendor = kwargs["vendor"]
|
|
if kwargs.get("product"):
|
|
devices.product = kwargs["product"]
|
|
if kwargs.get("revision"):
|
|
devices.revision = kwargs["revision"]
|
|
if kwargs.get("block_size"):
|
|
devices.block_size = int(kwargs["block_size"])
|
|
|
|
command.devices.append(devices)
|
|
|
|
data = self.send_pb_command(command)
|
|
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.send_pb_command(command)
|
|
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.send_pb_command(command)
|
|
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.send_pb_command(command)
|
|
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.send_pb_command(command)
|
|
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 = dict(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.send_pb_command(command)
|
|
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.send_pb_command(command)
|
|
result = proto.PbResult()
|
|
result.ParseFromString(data)
|
|
return {"status": result.status, "msg": result.msg}
|
|
|
|
def shutdown(self, mode):
|
|
"""
|
|
Sends a SHUT_DOWN command to the server.
|
|
Takes (str) mode as an argument.
|
|
The backend will use system calls to reboot or shut down the system.
|
|
It can also shut down the backend process itself.
|
|
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.send_pb_command(command)
|
|
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 PiSCSI 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.send_pb_command(command)
|
|
result = proto.PbResult()
|
|
result.ParseFromString(data)
|
|
return {"status": result.status, "msg": result.msg}
|
|
|
|
def format_pb_command(self, command):
|
|
"""
|
|
Formats the Protobuf command for output
|
|
"""
|
|
message = f"Sending: {proto.PbOperation.Name(command.operation)}"
|
|
|
|
params = {
|
|
name: "***" if name == "token" else value
|
|
for (name, value) in sorted(command.params.items())
|
|
}
|
|
message += f", params: {params}"
|
|
|
|
for device in command.devices:
|
|
formatted_device = {
|
|
key: value
|
|
for (key, value) in {
|
|
"id": device.id,
|
|
"unit": device.unit,
|
|
"type": proto.PbDeviceType.Name(device.type) if device.type else None,
|
|
"params": device.params,
|
|
"vendor": device.vendor,
|
|
"product": device.product,
|
|
"revision": device.revision,
|
|
}.items()
|
|
if key == "id" or value
|
|
}
|
|
message += f", device: {formatted_device}"
|
|
|
|
return message
|
|
|
|
# noinspection PyMethodMayBeStatic
|
|
@lru_cache(maxsize=32)
|
|
def _get_archive_info(self, file_path, **kwargs):
|
|
"""
|
|
Cached wrapper method to improve performance, e.g. on index screen
|
|
"""
|
|
try:
|
|
return unarchiver.inspect_archive(file_path)
|
|
except (unarchiver.LsarCommandError, unarchiver.LsarOutputError) as error:
|
|
logging.error(str(error))
|
|
raise
|