RASCSI/python/common/src/piscsi/piscsi_cmds.py

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