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:
Daniel Markstedt 2021-11-06 19:11:17 -07:00 committed by GitHub
parent 54b3e480a5
commit 7e546e2cb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 35 deletions

View File

@ -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)}

View File

@ -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.

View File

@ -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)]

View File

@ -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 %}

View File

@ -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