mirror of
https://github.com/akuker/RASCSI.git
synced 2025-04-07 14:38:14 +00:00
ID reservation in Web UI (#416)
* Remove dead code * Clean up indentation * Cleanup * Move socket commands into its own file * Move non-rascsi command methods into its own file * Refactoring * Bring back list_config_files * Cleanup * Cleanup of status messages * Remove unused libraries * Resolve pylint warnings * Resolve pylint warnings * Remove unused library * Resolve pylint warnings * Clean up status messages * Add requests lib to requirements.txt * Clean up status messages * Resolve interpolation warnings for logging * Resolve pylint warnings * Resolve pylint warnings * Resolve pylint warnings * Resolve pylint warnings * Resolve pylint warnings * Resolve pylint warnings * Resolve pylint warnings * Cleanup * Add html/head/body tags to the base document * Resolve pylint warnings * Resolve pylint warnings * Resolve pylint warnings * Resolve pylint warnings * Resolve pylint warnings * Resolve pylint warnings * Resolve pylint warnings * Add .pylintrc and suppress warnings for the generated protobuf module * Resolve pylint warnings * Clean up docstrings * Fix error * Cleanup * Add info on pylint to README * Store .pylintrc in parent dir to allow other Python packages to use it * Tidy index.html * Cleanup * Resolve jinja-ninja warnings * Cleanup * Cleanup * Cleanup * Cleanup * Cleanup * Save and load id reservations in config file * Reserve and unreserve in the web ui * TODO * Add backwards compatibility with 21.10 config files * Comment cleanup * Save and load reservation memos into the config file * Cleanup * Resolve pylint warnings * Fix bugs * Fix bug * Fix bugs * Cleanup * Fix typo * Fix successful return clause * Cleanup * Fix bugs
This commit is contained in:
parent
54b3e480a5
commit
7e546e2cb8
@ -8,12 +8,14 @@ from pathlib import PurePath
|
||||
|
||||
from ractl_cmds import (
|
||||
get_server_info,
|
||||
get_reserved_ids,
|
||||
attach_image,
|
||||
detach_all,
|
||||
list_devices,
|
||||
send_pb_command,
|
||||
reserve_scsi_ids,
|
||||
)
|
||||
from settings import CFG_DIR, CONFIG_FILE_SUFFIX, PROPERTIES_SUFFIX
|
||||
from socket_cmds import send_pb_command
|
||||
from settings import CFG_DIR, CONFIG_FILE_SUFFIX, PROPERTIES_SUFFIX, RESERVATIONS
|
||||
import rascsi_interface_pb2 as proto
|
||||
|
||||
|
||||
@ -252,9 +254,8 @@ def write_config(file_name):
|
||||
file_name = CFG_DIR + file_name
|
||||
try:
|
||||
with open(file_name, "w") as json_file:
|
||||
version = get_server_info()["version"]
|
||||
devices = list_devices()["device_list"]
|
||||
if not devices:
|
||||
return {"status": False, "msg": "No attached devices."}
|
||||
for device in devices:
|
||||
# Remove keys that we don't want to store in the file
|
||||
del device["status"]
|
||||
@ -270,7 +271,15 @@ def write_config(file_name):
|
||||
device["block_size"] = None
|
||||
# Convert to a data type that can be serialized
|
||||
device["params"] = dict(device["params"])
|
||||
dump(devices, json_file, indent=4)
|
||||
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": f"Saved config to {file_name}"}
|
||||
except (IOError, ValueError, EOFError, TypeError) as error:
|
||||
logging.error(str(error))
|
||||
@ -291,25 +300,53 @@ def read_config(file_name):
|
||||
file_name = CFG_DIR + file_name
|
||||
try:
|
||||
with open(file_name) as json_file:
|
||||
detach_all()
|
||||
devices = load(json_file)
|
||||
for row in devices:
|
||||
kwargs = {
|
||||
"device_type": row["device_type"],
|
||||
"image": row["image"],
|
||||
"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]
|
||||
process = attach_image(row["id"], **kwargs)
|
||||
if process["status"]:
|
||||
return {"status": process["status"], "msg": f"Loaded config from: {file_name}"}
|
||||
return {"status": process["status"], "msg": process["msg"]}
|
||||
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": "Invalid config file format."}
|
||||
return {"status": True, "msg": f"Loaded config from: {file_name}"}
|
||||
except (IOError, ValueError, EOFError, TypeError) as error:
|
||||
logging.error(str(error))
|
||||
return {"status": False, "msg": str(error)}
|
||||
|
@ -66,6 +66,26 @@ def get_server_info():
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
@ -288,7 +308,7 @@ def list_devices(scsi_id=None, unit=None):
|
||||
|
||||
device_list.append({
|
||||
"id": did,
|
||||
"un": dunit,
|
||||
"unit": dunit,
|
||||
"device_type": dtype,
|
||||
"status": ", ".join(dstat_msg),
|
||||
"image": dpath,
|
||||
@ -305,6 +325,21 @@ def list_devices(scsi_id=None, unit=None):
|
||||
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)
|
||||
|
||||
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.
|
||||
|
@ -24,3 +24,7 @@ DEFAULT_CONFIG = f"default.{CONFIG_FILE_SUFFIX}"
|
||||
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)]
|
||||
|
@ -53,7 +53,7 @@
|
||||
{% if device["id"] not in reserved_scsi_ids %}
|
||||
<td style="text-align:center">{{ device.id }}</td>
|
||||
{% if units %}
|
||||
<td style="text-align:center">{{ device.un }}</td>
|
||||
<td style="text-align:center">{{ device.unit }}</td>
|
||||
{% endif %}
|
||||
<td style="text-align:center">{{ device.device_type }}</td>
|
||||
<td style="text-align:center">{{ device.status }}</td>
|
||||
@ -63,26 +63,32 @@
|
||||
{% else %}
|
||||
<td style="text-align:center">{{ device.vendor }} {{ device.product }}</td>
|
||||
{% endif %}
|
||||
<td style="text-align:left">
|
||||
<td style="text-align:center">
|
||||
{% if device.device_type != "-" %}
|
||||
{% if device.device_type in REMOVABLE_DEVICE_TYPES and "No Media" not in device.status %}
|
||||
<form action="/scsi/eject" method="post" onsubmit="return confirm('Eject Disk?')">
|
||||
<input name="scsi_id" type="hidden" value="{{ device.id }}">
|
||||
<input name="unit" type="hidden" value="{{ device.un }}">
|
||||
<input name="unit" type="hidden" value="{{ device.unit }}">
|
||||
<input type="submit" value="Eject">
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/scsi/detach" method="post" onsubmit="return confirm('Detach Device?')">
|
||||
<input name="scsi_id" type="hidden" value="{{ device.id }}">
|
||||
<input name="unit" type="hidden" value="{{ device.un }}">
|
||||
<input name="unit" type="hidden" value="{{ device.unit }}">
|
||||
<input type="submit" value="Detach">
|
||||
</form>
|
||||
{% endif %}
|
||||
<form action="/scsi/info" method="post">
|
||||
<input name="scsi_id" type="hidden" value="{{ device.id }}">
|
||||
<input name="unit" type="hidden" value="{{ device.un }}">
|
||||
<input name="unit" type="hidden" value="{{ device.unit }}">
|
||||
<input type="submit" value="Info">
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/scsi/reserve" method="post" onsubmit="var memo = prompt('Enter a memo for this reservation'); document.getElementById('memo_{{ device.id }}').value = memo;">
|
||||
<input name="scsi_id" type="hidden" value="{{ device.id }}">
|
||||
<input name="memo" id="memo_{{ device.id }}" type="hidden" value="">
|
||||
<input type="submit" value="Reserve">
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
@ -92,9 +98,14 @@
|
||||
{% endif %}
|
||||
<td class="inactive"></td>
|
||||
<td class="inactive">Reserved ID</td>
|
||||
<td class="inactive">{{ RESERVATIONS[device.id] }}</td>
|
||||
<td class="inactive"></td>
|
||||
<td class="inactive"></td>
|
||||
<td class="inactive"></td>
|
||||
<td class="inactive">
|
||||
<form action="/scsi/unreserve" method="post">
|
||||
<input name="scsi_id" type="hidden" value="{{ device.id }}">
|
||||
<input type="submit" value="Unreserve">
|
||||
</form>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -47,8 +47,10 @@ from ractl_cmds import (
|
||||
eject_by_id,
|
||||
detach_all,
|
||||
get_server_info,
|
||||
get_reserved_ids,
|
||||
get_network_info,
|
||||
get_device_types,
|
||||
reserve_scsi_ids,
|
||||
set_log_level,
|
||||
)
|
||||
from device_utils import (
|
||||
@ -65,6 +67,7 @@ from settings import (
|
||||
DEFAULT_CONFIG,
|
||||
DRIVE_PROPERTIES_FILE,
|
||||
REMOVABLE_DEVICE_TYPES,
|
||||
RESERVATIONS,
|
||||
)
|
||||
|
||||
APP = Flask(__name__)
|
||||
@ -90,7 +93,7 @@ def index():
|
||||
# If there are more than 0 logical unit numbers, display in the Web UI
|
||||
for device in devices["device_list"]:
|
||||
attached_images.append(Path(device["image"]).name)
|
||||
units += int(device["un"])
|
||||
units += int(device["unit"])
|
||||
|
||||
reserved_scsi_ids = server_info["reserved_ids"]
|
||||
scsi_ids, recommended_id = get_valid_scsi_ids(devices["device_list"], reserved_scsi_ids)
|
||||
@ -120,6 +123,7 @@ def index():
|
||||
attached_images=attached_images,
|
||||
units=units,
|
||||
reserved_scsi_ids=reserved_scsi_ids,
|
||||
RESERVATIONS=RESERVATIONS,
|
||||
max_file_size=int(int(MAX_FILE_SIZE) / 1024 / 1024),
|
||||
running_env=running_env(),
|
||||
version=server_info["version"],
|
||||
@ -498,7 +502,7 @@ def device_info():
|
||||
if str(device["id"]) == scsi_id:
|
||||
flash("=== DEVICE INFO ===")
|
||||
flash(f"SCSI ID: {device['id']}")
|
||||
flash(f"LUN: {device['un']}")
|
||||
flash(f"LUN: {device['unit']}")
|
||||
flash(f"Type: {device['device_type']}")
|
||||
flash(f"Status: {device['status']}")
|
||||
flash(f"File: {device['image']}")
|
||||
@ -513,6 +517,41 @@ def device_info():
|
||||
flash(devices["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@APP.route("/scsi/reserve", methods=["POST"])
|
||||
def reserve_id():
|
||||
"""
|
||||
Reserves a SCSI ID and stores the memo for that reservation
|
||||
"""
|
||||
scsi_id = request.form.get("scsi_id")
|
||||
memo = request.form.get("memo")
|
||||
reserved_ids = get_reserved_ids()["ids"]
|
||||
reserved_ids.extend(scsi_id)
|
||||
process = reserve_scsi_ids(reserved_ids)
|
||||
if process["status"]:
|
||||
RESERVATIONS[int(scsi_id)] = memo
|
||||
flash(f"Reserved SCSI ID {scsi_id}")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
flash(process["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@APP.route("/scsi/unreserve", methods=["POST"])
|
||||
def unreserve_id():
|
||||
"""
|
||||
Removes the reservation of a SCSI ID as well as the memo for the reservation
|
||||
"""
|
||||
scsi_id = request.form.get("scsi_id")
|
||||
reserved_ids = get_reserved_ids()["ids"]
|
||||
reserved_ids.remove(scsi_id)
|
||||
process = reserve_scsi_ids(reserved_ids)
|
||||
if process["status"]:
|
||||
RESERVATIONS[int(scsi_id)] = ""
|
||||
flash(f"Released the reservation for SCSI ID {scsi_id}")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
flash(process["msg"], "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@APP.route("/pi/reboot", methods=["POST"])
|
||||
def restart():
|
||||
"""
|
||||
@ -746,7 +785,7 @@ if __name__ == "__main__":
|
||||
APP.config["MAX_CONTENT_LENGTH"] = int(MAX_FILE_SIZE)
|
||||
|
||||
# Load the default configuration file, if found
|
||||
if Path(DEFAULT_CONFIG).is_file():
|
||||
if Path(CFG_DIR + DEFAULT_CONFIG).is_file():
|
||||
read_config(DEFAULT_CONFIG)
|
||||
|
||||
import bjoern
|
||||
|
Loading…
x
Reference in New Issue
Block a user