Attach empty removable drives in the Web UI (#877)

* Read the drive properties file once and store it in the Flask app config. Spin out the drive properties formatting to a helper method.

* Add empty removable disk drives to the attach peripherals UI

* Refinement of UI labels and help text, moving some context to the wiki
This commit is contained in:
Daniel Markstedt 2022-10-01 16:51:30 -07:00 committed by GitHub
parent 255a6e139f
commit d969fbdcce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 187 additions and 84 deletions

View File

@ -426,9 +426,9 @@
"revision": "1.0k",
"block_size": 2048,
"size": null,
"name": "Apple CD 600e",
"name": "AppleCD 600e (Matsushita CR-8005)",
"file_type": null,
"description": "Emulates Apple CD ROM drive for use with Macintosh computers.",
"description": "Emulates an Apple CD-ROM drive for use with Macintosh computers.",
"url": ""
},
{
@ -438,7 +438,7 @@
"revision": null,
"block_size": 512,
"size": null,
"name": "Generic CD-ROM 512 block size",
"name": "Generic CD-ROM block size 512",
"file_type": null,
"description": "For use with host systems that expect the non-standard 512 byte block size for CD-ROM drives, such as Akai samplers.",
"url": ""

View File

@ -3,7 +3,7 @@
{% block content %}
<h2>{{ _("Disclaimer") }}</h2>
<p>{{ _("These device profiles are provided as-is with no guarantee to work equally to the actual physical device they are named after. You may need to provide appropirate device drivers and/or configuration parameters for them to function properly. If you would like to see data modified, or have additional devices to add to the list, please raise an issue ticket at <a href=\"%(url)s\">GitHub</a>.", url="https://github.com/akuker/RASCSI/issues") }}</p>
<h2>{{ _("Hard Drives") }}</h2>
<h2>{{ _("Hard Disk Drives") }}</h2>
<table cellpadding="3" border="black">
<tbody>
@ -14,7 +14,7 @@
<td><b>{{ _("Ref.") }}</b></td>
<td><b>{{ _("Action") }}</b></td>
</tr>
{% for hd in hd_conf|sort(attribute='name') %}
{% for hd in drive_properties['hd_conf']|sort(attribute='name') %}
<tr>
<td style="text-align:center">{{ hd.name }}</td>
<td style="text-align:center">{{ hd.size_mb }}</td>
@ -47,8 +47,8 @@
<hr/>
<h2>{{ _("CD-ROM Drives") }}</h2>
<p><em>{{ _("This will create a properties file for the given CD-ROM image. No new image file will be created.") }}</em></p>
<h2>{{ _("CD/DVD Drives") }}</h2>
<p><em>{{ _("This will create a properties file for the given CD-ROM or DVD image. No new image file will be created.") }}</em></p>
<table cellpadding="3" border="black">
<tbody>
<tr>
@ -58,7 +58,7 @@
<td><b>{{ _("Ref.") }}</b></td>
<td><b>{{ _("Action") }}</b></td>
</tr>
{% for cd in cd_conf|sort(attribute='name') %}
{% for cd in drive_properties['cd_conf']|sort(attribute='name') %}
<tr>
<td style="text-align:center">{{ cd.name }}</td>
<td style="text-align:center">{{ cd.size_mb }}</td>
@ -94,7 +94,7 @@
<hr/>
<h2>{{ _("Removable Drives") }}</h2>
<h2>{{ _("Removable Disk Drives") }}</h2>
<table cellpadding="3" border="black">
<tbody>
<tr>
@ -104,7 +104,7 @@
<td><b>{{ _("Ref.") }}</b></td>
<td><b>{{ _("Action") }}</b></td>
</tr>
{% for rm in rm_conf|sort(attribute='name') %}
{% for rm in drive_properties['rm_conf']|sort(attribute='name') %}
<tr>
<td style="text-align:center">{{ rm.name }}</td>
<td style="text-align:center">{{ rm.size_mb }}</td>

View File

@ -138,15 +138,15 @@
<table style="border: none;" cellpadding="3">
<tr style="border: none;">
<td style="border: none;">
<form action="/scsi/detach_all" method="post" onsubmit="return confirm('{{ _("Detach all SCSI Devices?") }}')">
<form action="/scsi/detach_all" method="post" onsubmit="return confirm('{{ _("Detach all SCSI Devices?") }}')">
<input type="submit" value="{{ _("Detach All Devices") }}">
</form>
</td>
</form>
</td>
<td style="border: none;">
<form action="/scsi/info" method="post">
<form action="/scsi/info" method="post">
<input type="submit" value="{{ _("Show Device Info") }}">
</form>
</td>
</form>
</td>
</tr>
</table>
@ -326,31 +326,36 @@
{{ _("Attach Peripheral Device") }}
</summary>
<ul>
<li>{{ _("<a href=\"%(url1)s\">DaynaPORT SCSI/Link</a> and <a href=\"%(url2)s\">X68000 Host Bridge</a> are network devices.", url1="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link", url2="https://github.com/akuker/RASCSI/wiki/X68000#Host_File_System_driver") }}
<li>{{ _("Before using a networking device, it is recommended to run easyinstall.sh from the command line to configure your Raspberry Pi.") }}
</li>
<ul>
<li>{{ _("If you have a DHCP setup, choose only the interface you have configured the bridge with. You can ignore the inet field when attaching.") }}</li>
<li>{{ _("Configure the network bridge by running easyinstall.sh, or follow the <a href=\"%(url)s\">manual steps in the wiki</a>.", url="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#manual-setup") }}
{% if bridge_configured %}
<li>{{ _("The <tt>rascsi_bridge</tt> network bridge is active and ready to be used by an emulated network adapter!") }}</li>
{% else %}
<li>{{ _("Please configure the <tt>rascsi_bridge</tt> network bridge before attaching an emulated network adapter!") }}</li>
{% endif %}
<li>{{ _("To browse the modern web, install a vintage web proxy like <a href=\"%(url)s\">Macproxy</a>.", url="https://github.com/akuker/RASCSI/wiki/Vintage-Web-Proxy#macproxy") }}</li>
<li>{{ _("If you have a DHCP setup, choose only the interface you have configured the bridge with. You can ignore the inet field when attaching.") }}</li>
<li>{{ _("To browse the modern web, install a vintage web proxy such as <a href=\"%(url)s\">Macproxy</a>.", url="https://github.com/akuker/RASCSI/wiki/Vintage-Web-Proxy#macproxy") }}</li>
</li>
</ul>
<li>{{ _("The Printer and Host Services device are currently supported on compatible Atari systems, and require <a href=\"%(url)s\">driver software</a> to be installed on the host system.", url="https://www.hddriver.net/en/rascsi_tools.html") }}
<li>{{ _("Read more about <a href=\"%(url)s\">supported device types</a> on the wiki.", url="https://github.com/akuker/RASCSI/wiki/Supported-Device-Types") }}
</li>
</ul>
</details>
<table border="black" cellpadding="3">
<tr style="font-weight: bold;">
<td>{{ _("Peripheral") }}</td>
<td>{{ _("Device") }}</td>
<td>{{ _("Code") }}</td>
<td>{{ _("Parameters and Actions") }}</td>
</tr>
{% for type in PERIPHERAL_DEVICE_TYPES %}
{% for type in REMOVABLE_DEVICE_TYPES + PERIPHERAL_DEVICE_TYPES %}
<tr>
<td>
<div>{{ device_types[type]["name"] }}</div>
</td>
<td>
<div>{{ type }}</div>
</td>
<td>
<form action="/scsi/attach_device" method="post">
<input name="type" type="hidden" value="{{ type }}">
@ -370,6 +375,35 @@
<input name="{{ key }}" type="text" size="{{ value|length }}" placeholder="{{ value }}">
{% endif %}
{% endfor %}
{% if type in REMOVABLE_DEVICE_TYPES %}
<label for="drive_name">{{ _("Masquerade as:") }}</label>
<select name="drive_name">
<option value="">
{{ _("None") }}
</option>
{% if type == "SCCD" %}
{% for drive in drive_properties["cd_conf"] | sort(attribute='name') %}
<option value="{{ drive.name }}">
{{ drive.name }}
</option>
{% endfor %}
{% endif %}
{% if type == "SCRM" %}
{% for drive in drive_properties["rm_conf"] | sort(attribute='name') %}
<option value="{{ drive.name }}">
{{ drive.name }}
</option>
{% endfor %}
{% endif %}
{% if type == "SCMO" %}
{% for drive in drive_properties["mo_conf"] | sort(attribute='name') %}
<option value="{{ drive.name }}">
{{ drive.name }}
</option>
{% endfor %}
{% endif %}
</select>
{% endif %}
<label for="scsi_id">{{ _("SCSI ID:") }}</label>
<select name="scsi_id">
{% for id in scsi_ids %}
@ -410,7 +444,7 @@
<option value="afp">{{ AFP_DIR }}</option>
</select>
</p>
</form>
</form>
</td>
</tr>
</table>
@ -459,7 +493,7 @@
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/download_url" method="post">
<label for="destination">{{ _("Target directory:") }}</label>
<label for="destination">{{ _("Target directory:") }}</label>
<select name="destination">
<option value="images">{{ base_dir }}</option>
<option value="afp">{{ AFP_DIR }}</option>
@ -481,7 +515,6 @@
<ul>
<li>{{ _("Create an ISO file system CD-ROM image with the downloaded file, and mount it on the given SCSI ID.") }}</li>
<li>{{ _("HFS is for Mac OS, Joliet for Windows, and Rock Ridge for POSIX.") }}</li>
<li>{{ _("On Mac OS, a <a href=\"%(url)s\">compatible CD-ROM driver</a> is required.", url="https://github.com/akuker/RASCSI/wiki/Drive-Setup#Mounting_CD_ISO_or_MO_images") }}</li>
<li>{{ _("If the downloaded file is a zip archive, we will attempt to unzip it and store the resulting files.") }}</li>
</ul>
</details>
@ -701,7 +734,7 @@
<center><tt>
{% if netatalk_configured == 1 %}
{{ _("The AppleShare server is running. No active connections.") }}
{% endif %}
{% endif %}
{% if netatalk_configured == 2 %}
{{ _("%(value)d active AFP connection", value=(netatalk_configured - 1)) }}
{% elif netatalk_configured > 2 %}

View File

@ -51,6 +51,7 @@ from web_utils import (
map_device_types_and_names,
get_device_name,
map_image_file_descriptions,
format_drive_properties,
auth_active,
is_bridge_configured,
upload_with_dropzonejs,
@ -164,7 +165,7 @@ def index():
"""
Sets up data structures for and renders the index page
"""
if not ractl_cmd.is_token_auth()["status"] and not APP.config["TOKEN"]:
if not ractl_cmd.is_token_auth()["status"] and not APP.config["RASCSI_TOKEN"]:
abort(
403,
_(
@ -217,6 +218,16 @@ def index():
server_info["sccd"]
)
try:
drive_properties = format_drive_properties(APP.config["RASCSI_DRIVE_PROPERTIES"])
except:
drive_properties = {
"hd_conf": [],
"cd_conf": [],
"rm_conf": [],
"mo_conf": [],
}
return response(
template="index.html",
locales=get_supported_locales(),
@ -247,6 +258,7 @@ def index():
cdrom_file_suffix=tuple(server_info["sccd"]),
removable_file_suffix=tuple(server_info["scrm"]),
mo_file_suffix=tuple(server_info["scmo"]),
drive_properties=drive_properties,
PROPERTIES_SUFFIX=PROPERTIES_SUFFIX,
ARCHIVE_FILE_SUFFIXES=ARCHIVE_FILE_SUFFIXES,
REMOVABLE_DEVICE_TYPES=ractl_cmd.get_removable_device_types(),
@ -268,39 +280,15 @@ def drive_list():
"""
Sets up the data structures and kicks off the rendering of the drive list page
"""
# Reads the canonical drive properties into a dict
# The file resides in the current dir of the web ui process
drive_properties = Path(DRIVE_PROPERTIES_FILE)
if not drive_properties.is_file():
return response(
error=True,
message=_("Could not read drive properties from %(properties_file)s",
properties_file=drive_properties),
)
process = file_cmd.read_drive_properties(str(drive_properties))
process = ReturnCodeMapper.add_msg(process)
if not process["status"]:
return response(error=True, message=process["msg"])
conf = process["conf"]
hd_conf = []
cd_conf = []
rm_conf = []
for device in conf:
if device["device_type"] == "SCHD":
device["secure_name"] = secure_filename(device["name"])
device["size_mb"] = "{:,.2f}".format(device["size"] / 1024 / 1024)
hd_conf.append(device)
elif device["device_type"] == "SCCD":
device["size_mb"] = "N/A"
cd_conf.append(device)
elif device["device_type"] == "SCRM":
device["secure_name"] = secure_filename(device["name"])
device["size_mb"] = "{:,.2f}".format(device["size"] / 1024 / 1024)
rm_conf.append(device)
try:
drive_properties = format_drive_properties(APP.config["RASCSI_DRIVE_PROPERTIES"])
except:
drive_properties = {
"hd_conf": [],
"cd_conf": [],
"rm_conf": [],
"mo_conf": [],
}
server_info = ractl_cmd.get_server_info()
@ -308,9 +296,7 @@ def drive_list():
template="drives.html",
files=file_cmd.list_images()["files"],
base_dir=server_info["image_dir"],
hd_conf=hd_conf,
cd_conf=cd_conf,
rm_conf=rm_conf,
drive_properties=drive_properties,
version=server_info["version"],
cdrom_file_suffix=tuple(server_info["sccd"]),
)
@ -548,6 +534,7 @@ def attach_device():
Attaches a peripheral device that doesn't take an image file as argument
"""
params = {}
drive_props = None
for item in request.form:
if item == "scsi_id":
scsi_id = request.form.get(item)
@ -555,6 +542,12 @@ def attach_device():
unit = request.form.get(item)
elif item == "type":
device_type = request.form.get(item)
elif item == "drive_name":
drive_name = request.form.get(item)
for drive in APP.config["RASCSI_DRIVE_PROPERTIES"]:
if drive["name"] == drive_name:
drive_props = drive
break
else:
param = request.form.get(item)
if param:
@ -577,6 +570,12 @@ def attach_device():
"device_type": device_type,
"params": params,
}
if drive_props:
kwargs["vendor"] = drive_props["vendor"]
kwargs["product"] = drive_props["product"]
kwargs["revision"] = drive_props["revision"]
kwargs["block_size"] = drive_props["block_size"]
process = ractl_cmd.attach_device(scsi_id, **kwargs)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
@ -1099,15 +1098,23 @@ if __name__ == "__main__":
)
arguments = parser.parse_args()
APP.config["TOKEN"] = arguments.password
APP.config["RASCSI_TOKEN"] = arguments.password
sock_cmd = SocketCmdsFlask(host=arguments.rascsi_host, port=arguments.rascsi_port)
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=APP.config["TOKEN"])
file_cmd = FileCmds(sock_cmd=sock_cmd, ractl=ractl_cmd, token=APP.config["TOKEN"])
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=APP.config["RASCSI_TOKEN"])
file_cmd = FileCmds(sock_cmd=sock_cmd, ractl=ractl_cmd, token=APP.config["RASCSI_TOKEN"])
sys_cmd = SysCmds()
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
file_cmd.read_config(DEFAULT_CONFIG)
if Path(f"{DRIVE_PROPERTIES_FILE}").is_file():
process = file_cmd.read_drive_properties(DRIVE_PROPERTIES_FILE)
if process["status"]:
APP.config["RASCSI_DRIVE_PROPERTIES"] = process["conf"]
else:
logging.error(process["msg"])
else:
logging.warning("Could not read drive properties from %s", DRIVE_PROPERTIES_FILE)
logging.basicConfig(stream=sys.stdout,
format="%(asctime)s %(levelname)s %(filename)s:%(lineno)s %(message)s",

View File

@ -81,15 +81,15 @@ def get_device_name(device_type):
Returns the human-readable name for the device type.
"""
if device_type == "SCHD":
return _("Hard Disk")
return _("Hard Disk Drive")
if device_type == "SCRM":
return _("Removable Disk")
return _("Removable Disk Drive")
if device_type == "SCMO":
return _("Magneto-Optical Disk")
return _("Magneto-Optical Drive")
if device_type == "SCCD":
return _("CD / DVD")
return _("CD/DVD Drive")
if device_type == "SCBR":
return _("X68000 Host Bridge")
return _("Host Bridge")
if device_type == "SCDP":
return _("DaynaPORT SCSI/Link")
if device_type == "SCLP":
@ -132,6 +132,41 @@ def get_image_description(file_suffix):
return file_suffix
def format_drive_properties(drive_properties):
"""
Takes a (dict) with structured drive properties data
Returns a (dict) with the formatted properties, one (list) per device type
"""
hd_conf = []
cd_conf = []
rm_conf = []
mo_conf = []
FORMAT_FILTER = "{:,.2f}"
for device in drive_properties:
if device["device_type"] == "SCHD":
device["secure_name"] = secure_filename(device["name"])
device["size_mb"] = FORMAT_FILTER.format(device["size"] / 1024 / 1024)
hd_conf.append(device)
elif device["device_type"] == "SCCD":
device["size_mb"] = _("N/A")
cd_conf.append(device)
elif device["device_type"] == "SCRM":
device["secure_name"] = secure_filename(device["name"])
device["size_mb"] = FORMAT_FILTER.format(device["size"] / 1024 / 1024)
rm_conf.append(device)
elif device["device_type"] == "SCMO":
device["secure_name"] = secure_filename(device["name"])
device["size_mb"] = FORMAT_FILTER.format(device["size"] / 1024 / 1024)
mo_conf.append(device)
return {
"hd_conf": hd_conf,
"cd_conf": cd_conf,
"rm_conf": rm_conf,
"mo_conf": mo_conf,
}
def auth_active(group):
"""
Inspects if the group defined in (str) group exists on the system.

View File

@ -1,7 +1,6 @@
import pytest
from conftest import (
IMAGES_DIR,
SCSI_ID,
FILE_SIZE_1_MIB,
STATUS_SUCCESS,
@ -27,7 +26,7 @@ def test_attach_image(http_client, create_test_image, detach_devices):
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == (
f"Attached {test_image} as Hard Disk to SCSI ID {SCSI_ID} LUN 0"
f"Attached {test_image} as Hard Disk Drive to SCSI ID {SCSI_ID} LUN 0"
)
# Cleanup
@ -38,8 +37,43 @@ def test_attach_image(http_client, create_test_image, detach_devices):
@pytest.mark.parametrize(
"device_name,device_config",
[
# TODO: Fix networking in container, else SCBR attachment fails
# ("X68000 Host Bridge", {"type": "SCBR", "interface": "eth0", "inet": "10.10.20.1/24"}),
(
"Removable Disk Drive",
{
"type": "SCRM",
"drive_props": {
"vendor": "VENDOR",
"product": "PRODUCT",
"revision": "0123",
"block_size": "512",
},
},
),
(
"Magneto-Optical Drive",
{
"type": "SCMO",
"drive_props": {
"vendor": "VENDOR",
"product": "PRODUCT",
"revision": "0123",
"block_size": "512",
},
},
),
(
"CD/DVD Drive",
{
"type": "SCCD",
"drive_props": {
"vendor": "VENDOR",
"product": "PRODUCT",
"revision": "0123",
"block_size": "512",
},
},
),
("Host Bridge", {"type": "SCBR", "interface": "eth0", "inet": "10.10.20.1/24"}),
("DaynaPORT SCSI/Link", {"type": "SCDP", "interface": "eth0", "inet": "10.10.20.1/24"}),
("Host Services", {"type": "SCHS"}),
("Printer", {"type": "SCLP", "timeout": 30, "cmd": "lp -oraw %f"}),

View File

@ -4,7 +4,6 @@ import os
from conftest import (
IMAGES_DIR,
AFP_DIR,
SCSI_ID,
FILE_SIZE_1_MIB,
STATUS_SUCCESS,
@ -201,10 +200,7 @@ def test_upload_file(http_client, delete_file):
def test_download_file(http_client, create_test_image):
file_name = create_test_image()
response = http_client.post(
"/files/download",
data={"file": f"{IMAGES_DIR}/{file_name}"}
)
response = http_client.post("/files/download", data={"file": f"{IMAGES_DIR}/{file_name}"})
assert response.status_code == 200
assert response.headers["content-type"] == "application/octet-stream"

View File

@ -42,9 +42,7 @@ def test_show_named_drive_presets(http_client):
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert "cd_conf" in response_data["data"]
assert "hd_conf" in response_data["data"]
assert "rm_conf" in response_data["data"]
assert "drive_properties" in response_data["data"]
# route("/drive/cdrom", methods=["POST"])