Disk image profile management using a sidecar config file (#242)

* Handle a case where reserved ids on the Web UI side are not actually reserved on the backend side

* Better error handling when no device is found in list_devices

* Better warning message

* Tag message as error

* Fix device_info

* Get reserved ids from the server instead of storing a client side state, which caused front and backend to get out of sync in certain cases.

* Initial implementation of sidecar configuration reading and writing

* Use bytes for drive image creation internally

* Add named drive section

* Move header to base.html

* Create the disk profile list page

* Make lists of HDDs, CDRs, and Removable drives

* Implement disk image + sidecar creation

* Implement CD-ROM device sidecar creation method

* Add more device configurations

* Add disclaimer

* Hide URL if none is provided

* Make the web ui use the new protobuf parameter maps

* Make daynaport attaching UI more flexible

* Use the protobuf interface to create image files

* Use new create image method for the sidecar flow as well

* Move file deletion logic to protobuf commands; Refactor saving/loading config files

* Update disk image creation

* Fix error

* Disk images the script makes are in Mac format

* Add blurb about the risks with using Lido driver (issue#40) to the easyinstall script, while pushing less for using that feature.

* Make shutdown and reboot async operations

* More informative footer contents

* Wordsmith the Mac drive options

* Link to relevant section in the wiki

* Added GET_IMAGE_FILES

* Added default folder to GET_IMAGE_FILES

* Renaming

* Updated setting image folder

* Lists available net interfaces as a drop down when attaching daynaport

* Macs should use the hds image file ending

* Fixed default image folder handling

* Refer to device properties, instead of sidecars

* Added NETWORK_INTERFACES_INFO

* Filter "lo"

* Use PF_INET in favor of PF_INET6

* Added network interfaces to server info

* Drive property file ending defined in one place; Add handling of common urllib and file system exceptions.

* Use protobuf interface to get network interface info

* Use protobuf interface for list_files

* Repeated field cleanup

* Renaming

* Added DEVICE_TYPES_INFO

* Comment update

* Added -y option to rasctl

* Add the remaining recommended drive profiles provided by rpajarola

* Fix typos

* Add warnings to CD-ROM descriptions

* Add more recommended Sun drives

* Add capacity to name

* Move footer into base.html

* Handle removable drive insertion in the attach method (easy to do with protobuf)

* Limit which arguments to pass to an image injection command

* Cleanup

* Sort image and config files alphabetically

* Make compatible with updated protobuf interface

* Sort drives alphabetically by name

* Decriptive text for CD-ROM section

* Better description

* Hyperlink to disks page instead of button

Co-authored-by: Uwe Seimet <Uwe.Seimet@seimet.de>
This commit is contained in:
Daniel Markstedt 2021-09-19 14:29:01 -07:00 committed by GitHub
parent 6b082447c5
commit 15febd1ee6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 881 additions and 269 deletions

View File

@ -261,7 +261,7 @@ function createDrive() {
driveSize=$1
driveName=$2
mkdir -p $VIRTUAL_DRIVER_PATH
drivePath="${VIRTUAL_DRIVER_PATH}/${driveSize}MB.hda"
drivePath="${VIRTUAL_DRIVER_PATH}/${driveSize}MB.hds"
if [ ! -f $drivePath ]; then
echo "Creating a ${driveSize}MB Drive"
@ -415,19 +415,6 @@ function reserveScsiIds() {
function runChoice() {
case $1 in
0)
echo "Installing / Updating RaSCSI Service (${CONNECT_TYPE-FULLSPEC}) + Web interface + 600MB Drive"
stopOldWebInterface
updateRaScsiGit
createImagesDir
installPackages
installRaScsi
installRaScsiWebInterface
createDrive600MB
showRaScsiStatus
showRaScsiWebStatus
echo "Installing / Updating RaSCSI Service (${CONNECT_TYPE-FULLSPEC}) + Web interface + 600MB Drive - Complete!"
;;
1)
echo "Installing / Updating RaSCSI Service (${CONNECT_TYPE-FULLSPEC}) + Web interface"
stopOldWebInterface
@ -501,11 +488,11 @@ function showMenu() {
echo ""
echo "Choose among the following options:"
echo "INSTALL/UPDATE RASCSI (${CONNECT_TYPE-FULLSPEC} version)"
echo " 0) install or update RaSCSI Service + web interface + 600MB Drive (recommended)"
echo " 1) install or update RaSCSI Service + web interface"
echo " 1) install or update RaSCSI Service + Web Interface"
echo " 2) install or update RaSCSI Service"
echo "CREATE EMPTY DRIVE IMAGE"
echo " 3) 600MB drive (recommended)"
echo "CREATE HFS FORMATTED (MAC) IMAGE WITH LIDO DRIVERS"
echo "** For the Mac Plus, it's better to create an image through the Web Interface **"
echo " 3) 600MB drive (suggested size)"
echo " 4) custom drive size (up to 4000MB)"
echo "NETWORK ASSISTANT"
echo " 5) configure network forwarding over Ethernet (DHCP)"
@ -531,7 +518,7 @@ while [ "$1" != "" ]; do
;;
esac
case $VALUE in
FULLSPEC | STANDARD | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7)
FULLSPEC | STANDARD | 1 | 2 | 3 | 4 | 5 | 6 | 7)
;;
*)
echo "ERROR: unknown option \"$VALUE\""

View File

@ -32,6 +32,7 @@
#include "spdlog/sinks/stdout_color_sinks.h"
#include <spdlog/async.h>
#include <sys/sendfile.h>
#include <ifaddrs.h>
#include <string>
#include <sstream>
#include <iostream>

View File

@ -0,0 +1,266 @@
[
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ55 (C) DEC",
"revision": "",
"block_size": 512,
"blocks": 660960,
"name": "DEC RZ55",
"file_type": "hds",
"description": "Largest recognized drive on Ultrix 3.0",
"url": "http://lastin.dti.supsi.ch/VET/disks/EK-RZXXD-PS.pdf"
},
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ57 (C) DEC",
"revision": "5000",
"block_size": 512,
"blocks": 2050125,
"name": "DEC RZ57",
"file_type": "hds",
"description": "Largest recognized drive on Ultrix 3.1 - 4.3",
"url": "http://lastin.dti.supsi.ch/VET/disks/EK-RZXXD-PS.pdf"
},
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ59 (C) DEC",
"revision": "2000",
"block_size": 512,
"blocks": 17755614,
"name": "DEC RZ59",
"file_type": "hds",
"description": "Largest recognized drive on OSF/1 3.x - 5.x",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ74 (C) DEC",
"revision": "427H",
"block_size": 512,
"blocks": 6950125,
"name": "DEC RZ74",
"file_type": "hds",
"description": "Largest recognized drive on Ultrix 4.4 - 4.5",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "HP",
"product": "A2076",
"revision": "DD24",
"block_size": 512,
"blocks": 2621688,
"name": "HP A2076",
"file_type": "hds",
"description": "Largest recognized drive on HP-UX 8.0",
"url": "http://www.bitsavers.org/pdf/micropolis/105389b_1528_1991.pdf"
},
{
"device_type": "SCHD",
"vendor": "HP",
"product": "C3010",
"revision": "6.0",
"block_size": 512,
"blocks": 3905792,
"name": "HP C3010",
"file_type": "hds",
"description": "Largest recognized drive on HP-UX 9.0",
"url": "https://stason.org/TULARC/pc/hard-drives-hdd/hewlett-packard/HP-C3010-001-2003MB-5-25-FH-SCSI2-FAST.html"
},
{
"device_type": "SCHD",
"vendor": "MICROP",
"product": "1325",
"revision": "",
"block_size": 512,
"blocks": 270336,
"name": "Micropolis 1325",
"file_type": "hds",
"description": "Largest predefined on SunOS 2; Microp 1325 is actually an ESDI disk on an adapter.",
"url": "https://stason.org/TULARC/pc/hard-drives-hdd/micropolis/1325-69MB-5-25-FH-MFM-ST506.html"
},
{
"device_type": "SCHD",
"vendor": "MICROP",
"product": "1588T",
"revision": "",
"block_size": 512,
"blocks": 651840,
"name": "Micropolis 1588-15",
"file_type": "hds",
"description": "Largest predefined on SunOS 3/4 (Sun-3)",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "SEAGATE",
"product": "ST32430N SUN2.1G",
"revision": "0444",
"block_size": 512,
"blocks": 4197405,
"name": "Seagate SUN2.1G",
"file_type": "hds",
"description": "Largest predefined for SunOS 4 (Sun-4) and Solaris 2.0-2.3",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "SEAGATE",
"product": "ST34371W SUN4.2G",
"revision": "7462",
"block_size": 512,
"blocks": 8380800,
"name": "Seagate SUN4.2G",
"file_type": "hds",
"description": "Recommended for Solaris 2.4+",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "SEAGATE",
"product": "ST39173W SUN9.0G",
"revision": "2815",
"block_size": 512,
"blocks": 17689267,
"name": "Seagate SUN9.0G",
"file_type": "hds",
"description": "Recommended for Solaris 2.4+",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "SEAGATE",
"product": "ST914603SSUN146G",
"revision": "0B70",
"block_size": 512,
"blocks": 286739329,
"name": "Seagate SUN146G",
"file_type": "hds",
"description": "Recommended for Solaris 2.4+",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "QUANTUM",
"product": "FIREBALL540S",
"revision": "",
"block_size": 512,
"blocks": 1065235,
"name": "Quantum Fireball 540S",
"file_type": "hds",
"description": "Recommended for older Macintosh systems. Recognized by Apple HD SC Setup.",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "QUANTUM",
"product": "FIREBALL ST4.3S",
"revision": "0F0C",
"block_size": 512,
"blocks": 8471232,
"name": "Quantum Fireball ST4.3S",
"file_type": "hds",
"description": "Recommended for Macintosh System 6 or later. Recognized by Apple HD SC Setup.",
"url": ""
},
{
"device_type": "SCRM",
"vendor": "IOMEGA",
"product": "ZIP 100",
"revision": "D.13",
"block_size": 512,
"blocks": 196608,
"name": "Iomega ZIP 100",
"file_type": "hdr",
"description": "Removable Iomega ZIP drive, 100MB capacity",
"url": "https://www.win.tue.nl/~aeb/linux/zip/zip-1.html"
},
{
"device_type": "SCRM",
"vendor": "IOMEGA",
"product": "ZIP 250",
"revision": "D.58",
"block_size": 512,
"blocks": 489532,
"name": "Iomega ZIP 250",
"file_type": "hdr",
"description": "Removable Iomega ZIP drive, 250MB capacity",
"url": "https://www.win.tue.nl/~aeb/linux/zip/zip-1.html"
},
{
"device_type": "SCRM",
"vendor": "IOMEGA",
"product": "JAZ 2GB",
"revision": "E.16",
"block_size": 512,
"blocks": 3909632,
"name": "Iomega Jaz 2GB",
"file_type": "hdr",
"description": "Removable Iomega Jaz drive, 2GB capacity",
"url": "https://archive.eol.ucar.edu/docs/isf/facilities/iss/manual/creating-data-disks.html"
},
{
"device_type": "SCRM",
"vendor": "SYQUEST",
"product": "SQ5110C",
"revision": "4AA0",
"block_size": 512,
"blocks": 173456,
"name": "SyQuest 88MB",
"file_type": "hdr",
"description": "SyQuest removable hard drive cartridges, 88MB capacity",
"url": ""
},
{
"device_type": "SCCD",
"vendor": "TOSHIBA",
"product": "CD-ROM XM-3401TA",
"revision": "0283",
"block_size": 512,
"blocks": null,
"name": "Toshiba XM-3401TA",
"file_type": null,
"description": "Boots most SGI, Sun, HP, IBM, DEC etc. Use only with Unix workstations of this vintage.",
"url": ""
},
{
"device_type": "SCCD",
"vendor": "SONY",
"product": "CD-ROM CDU-8012",
"revision": "3.1a",
"block_size": 512,
"blocks": null,
"name": "Sony CDU-8012",
"file_type": null,
"description": "Boots Sun-3. Use only with Unix workstations of this vintage.",
"url": ""
},
{
"device_type": "SCCD",
"vendor": "HP",
"product": "A1448A",
"revision": "",
"block_size": 512,
"blocks": null,
"name": "HP A1448A",
"file_type": null,
"description": "Recognized by HP-UX. Use only with Unix workstations of this vintage.",
"url": ""
},
{
"device_type": "SCCD",
"vendor": "DEC",
"product": "RRD42 (C) DEC",
"revision": "4.5d",
"block_size": 512,
"blocks": null,
"name": "DEC RRD42",
"file_type": null,
"description": "Boots DECstations and VAXstations. Use only with Unix workstations of this vintage.",
"url": ""
}
]

View File

@ -1,41 +1,30 @@
import os
import subprocess
import time
import logging
from ractl_cmds import (
attach_image,
detach_all,
list_devices,
send_pb_command,
)
from settings import *
import rascsi_interface_pb2 as proto
def list_files():
from fnmatch import translate
valid_file_types = list(VALID_FILE_SUFFIX)
valid_file_types = ["*." + s for s in valid_file_types]
valid_file_types = r"|".join([translate(x) for x in valid_file_types])
command = proto.PbCommand()
command.operation = proto.PbOperation.IMAGE_FILES_INFO
from re import match, IGNORECASE
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
files = []
for f in result.image_files_info.image_files:
size_mb = "{:,.1f}".format(f.size / 1024 / 1024)
files.append({"name": f.name, "size": f.size, "size_mb": size_mb})
files_list = []
for path, dirs, files in os.walk(base_dir):
# Only list valid file types
files = [f for f in files if match(valid_file_types, f, IGNORECASE)]
files_list.extend(
[
(
os.path.join(path, file),
"{:,.1f}".format(
os.path.getsize(os.path.join(path, file)) / float(1 << 20),
),
os.path.getsize(os.path.join(path, file)),
)
for file in files
]
)
return files_list
return {"status": result.status, "msg": result.msg, "files": files}
def list_config_files():
@ -47,24 +36,30 @@ def list_config_files():
return files_list
def create_new_image(file_name, type, size):
if file_name == "":
file_name = "new_image." + str(int(time.time())) + "." + type
else:
file_name = file_name + "." + type
def create_new_image(file_name, file_type, size):
command = proto.PbCommand()
command.operation = proto.PbOperation.CREATE_IMAGE
return subprocess.run(
["truncate", "--size", f"{size}m", f"{base_dir}{file_name}"],
capture_output=True,
)
command.params["file"] = file_name + "." + file_type
command.params["size"] = str(size)
command.params["read_only"] = "false"
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def delete_file(file_name):
if os.path.exists(file_name):
os.remove(file_name)
return True
else:
return False
command = proto.PbCommand()
command.operation = proto.PbOperation.DELETE_IMAGE
command.params["file"] = file_name
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def unzip_file(file_name):
@ -77,6 +72,8 @@ def unzip_file(file_name):
def download_file_to_iso(scsi_id, url):
import urllib.request
import urllib.error as error
import time
file_name = url.split("/")[-1]
tmp_ts = int(time.time())
@ -87,8 +84,10 @@ def download_file_to_iso(scsi_id, url):
try:
urllib.request.urlretrieve(url, tmp_full_path)
except (error.URLError, error.HTTPError, error.ContentTooShortError) as e:
logging.error(str(e))
return {"status": False, "msg": str(e)}
except:
# TODO: Capture a more descriptive error message
return {"status": False, "msg": "Error loading the URL"}
# iso_filename = make_cd(tmp_full_path, None, None) # not working yet
@ -102,6 +101,7 @@ def download_file_to_iso(scsi_id, url):
def download_image(url):
import urllib.request
import urllib.error as error
file_name = url.split("/")[-1]
full_path = base_dir + file_name
@ -109,16 +109,19 @@ def download_image(url):
try:
urllib.request.urlretrieve(url, full_path)
return {"status": True, "msg": "Downloaded the URL"}
except (error.URLError, error.HTTPError, error.ContentTooShortError) as e:
logging.error(str(e))
return {"status": False, "msg": str(e)}
except:
# TODO: Capture a more descriptive error message
return {"status": False, "msg": "Error loading the URL"}
def write_config(file_name):
from json import dump
file_name = base_dir + file_name
try:
with open(file_name, "w") as json_file:
devices = list_devices()[0]
devices = list_devices()["device_list"]
for device in devices:
# Remove keys that we don't want to store in the file
del device["status"]
@ -136,7 +139,9 @@ def write_config(file_name):
device["params"] = list(device["params"])
dump(devices, json_file, indent=4)
return {"status": True, "msg": f"Successfully wrote to file: {file_name}"}
#TODO: more verbose error handling of file system errors
except (IOError, ValueError, EOFError, TypeError) as e:
logging.error(str(e))
return {"status": False, "msg": str(e)}
except:
logging.error(f"Could not write to file: {file_name}")
return {"status": False, "msg": f"Could not write to file: {file_name}"}
@ -144,31 +149,61 @@ def write_config(file_name):
def read_config(file_name):
from json import load
file_name = base_dir + file_name
try:
with open(file_name) as json_file:
detach_all()
devices = load(json_file)
for row in devices:
process = attach_image(row["id"], device_type=row["device_type"], image=row["image"], unit=int(row["un"]), \
process = attach_image(row["id"], device_type=row["device_type"], \
image=row["image"], unit=int(row["un"]), \
params=row["params"], vendor=row["vendor"], product=row["product"], \
revision=row["revision"], block_size=row["block_size"])
if process["status"] == True:
return {"status": process["status"], "msg": f"Successfully read from file: {file_name}"}
else:
return {"status": process["status"], "msg": process["msg"]}
#TODO: more verbose error handling of file system errors
except (IOError, ValueError, EOFError, TypeError) as e:
logging.error(str(e))
return {"status": False, "msg": str(e)}
except:
logging.error(f"Could not read file: {file_name}")
return {"status": False, "msg": f"Could not read file: {file_name}"}
def read_device_config(file_name):
def write_drive_properties(file_name, conf):
"""
Writes a drive property configuration file to the images dir.
Takes file name base (str) and conf (list of dicts) as arguments
"""
from json import dump
try:
with open(base_dir + file_name, "w") as json_file:
dump(conf, json_file, indent=4)
return {"status": True, "msg": f"Successfully wrote to file: {file_name}"}
except (IOError, ValueError, EOFError, TypeError) as e:
logging.error(str(e))
return {"status": False, "msg": str(e)}
except:
logging.error(f"Could not write to file: {file_name}")
return {"status": False, "msg": f"Could not write to file: {file_name}"}
def read_drive_properties(path_name):
"""
Reads drive properties to any dir.
Either ones deployed to the images dir, or the canonical database.
Takes file path and bas (str) as argument
"""
from json import load
try:
with open(file_name) as json_file:
with open(path_name) as json_file:
conf = load(json_file)
return {"status": True, "msg": f"Read data from file: {file_name}", "conf": conf}
#TODO: more verbose error handling of file system errors
return {"status": True, "msg": f"Read data from file: {path_name}", "conf": conf}
except (IOError, ValueError, EOFError, TypeError) as e:
logging.error(str(e))
return {"status": False, "msg": str(e)}
except:
logging.error(f"Could not read file: {file_name}")
return {"status": False, "msg": f"Could not read file: {file_name}"}
return {"status": False, "msg": f"Could not read file: {path_name}"}

View File

@ -10,15 +10,17 @@ def rascsi_service(action):
def reboot_pi():
return subprocess.run(["sudo", "reboot"]).returncode == 0
subprocess.Popen(["sudo", "reboot"])
return True
def shutdown_pi():
return subprocess.run(["sudo", "shutdown", "-h", "now"]).returncode == 0
subprocess.Popen(["sudo", "shutdown", "-h", "now"])
return True
def running_version():
ra_web_version = (
def running_env():
ra_git_version = (
subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True)
.stdout.decode("utf-8")
.strip()
@ -28,13 +30,12 @@ def running_version():
.stdout.decode("utf-8")
.strip()
)
return ra_web_version + " " + pi_version
return {"git": ra_git_version, "env": pi_version}
def is_bridge_setup():
from subprocess import run
process = run(["brctl", "show"], capture_output=True)
process = subprocess.run(["brctl", "show"], capture_output=True)
output = process.stdout.decode("utf-8")
if "rascsi_bridge" in output:
return True
return False
return False

View File

@ -16,7 +16,19 @@ def get_server_info():
str(result.server_info.patch_version)
log_levels = result.server_info.log_levels
current_log_level = result.server_info.current_log_level
return {"status": result.status, "version": version, "log_levels": log_levels, "current_log_level": current_log_level}
reserved_ids = list(result.server_info.reserved_ids)
return {"status": result.status, "version": version, "log_levels": log_levels, "current_log_level": current_log_level, "reserved_ids": reserved_ids}
def get_network_info():
command = proto.PbCommand()
command.operation = proto.PbOperation.NETWORK_INTERFACES_INFO
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
ifs = result.network_interfaces_info.name
return {"status": result.status, "ifs": ifs}
def validate_scsi_id(scsi_id):
@ -27,18 +39,24 @@ def validate_scsi_id(scsi_id):
return {"status": False, "msg": "Invalid SCSI ID. Should be a number between 0-7"}
def get_valid_scsi_ids(devices, invalid_list, occupied_ids):
for device in devices:
def get_valid_scsi_ids(devices, reserved_ids):
occupied_ids = []
for d in devices:
# Make it possible to insert images on top of a
# removable media device currently without an image attached
if "No Media" in device["status"]:
occupied_ids.remove(device["id"])
if d["device_type"] != "-" and "No Media" not in d["status"]:
occupied_ids.append(d["id"])
# Combine lists and remove duplicates
invalid_ids = list(set(invalid_list + occupied_ids))
invalid_ids = list(set(reserved_ids + occupied_ids))
valid_ids = list(range(8))
for id in invalid_ids:
valid_ids.remove(int(id))
try:
valid_ids.remove(int(id))
except:
# May reach this state if the RaSCSI Web UI thinks an ID
# is reserved but RaSCSI has not actually reserved it.
logging.warning(f"SCSI ID {id} flagged as both valid and invalid. Try restarting the RaSCSI Web UI.")
valid_ids.reverse()
return valid_ids
@ -48,7 +66,7 @@ def get_type(scsi_id):
device.id = int(scsi_id)
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICE_INFO
command.operation = proto.PbOperation.DEVICES_INFO
command.devices.append(device)
data = send_pb_command(command.SerializeToString())
@ -63,6 +81,17 @@ def get_type(scsi_id):
def attach_image(scsi_id, **kwargs):
command = proto.PbCommand()
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if "device_type" in kwargs.keys():
devices.type = proto.PbDeviceType.Value(str(kwargs["device_type"]))
if "unit" in kwargs.keys():
devices.unit = kwargs["unit"]
if "image" in kwargs.keys():
if kwargs["image"] not in [None, ""]:
devices.params["file"] = kwargs["image"]
# Handling the inserting of media into an attached removable type device
currently_attached = get_type(scsi_id)["device_type"]
@ -72,22 +101,13 @@ def attach_image(scsi_id, **kwargs):
if currently_attached != device_type:
return {"status": False, "msg": f"Cannot insert an image for {device_type} into a {currently_attached} device."}
else:
return insert(scsi_id, kwargs.get("image", ""))
command.operation = proto.PbOperation.INSERT
# Handling attaching a new device
else:
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if "device_type" in kwargs.keys():
logging.warning(kwargs["device_type"])
devices.type = proto.PbDeviceType.Value(str(kwargs["device_type"]))
if "unit" in kwargs.keys():
devices.unit = kwargs["unit"]
if "image" in kwargs.keys():
if kwargs["image"] not in [None, ""]:
devices.params.append(kwargs["image"])
if "params" in kwargs.keys():
for p in kwargs["params"]:
devices.params.append(p)
command.operation = proto.PbOperation.ATTACH
if "interfaces" in kwargs.keys():
if kwargs["interfaces"] not in [None, ""]:
devices.params["interfaces"] = kwargs["interfaces"]
if "vendor" in kwargs.keys():
if kwargs["vendor"] not in [None, ""]:
devices.vendor = kwargs["vendor"]
@ -101,14 +121,12 @@ def attach_image(scsi_id, **kwargs):
if kwargs["block_size"] not in [None, ""]:
devices.block_size = int(kwargs["block_size"])
command = proto.PbCommand()
command.operation = proto.PbOperation.ATTACH
command.devices.append(devices)
command.devices.append(devices)
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def detach_by_id(scsi_id):
@ -149,40 +167,10 @@ def eject_by_id(scsi_id):
return {"status": result.status, "msg": result.msg}
def insert(scsi_id, image):
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
devices.params.append(image)
command = proto.PbCommand()
command.operation = proto.PbOperation.INSERT
command.devices.append(devices)
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def attach_daynaport(scsi_id):
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
devices.type = proto.PbDeviceType.SCDP
command = proto.PbCommand()
command.operation = proto.PbOperation.ATTACH
command.devices.append(devices)
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def list_devices(scsi_id=None):
from os import path
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICE_INFO
command.operation = proto.PbOperation.DEVICES_INFO
# If method is called with scsi_id parameter, return the info on those devices
# Otherwise, return the info on all attached devices
@ -196,8 +184,11 @@ def list_devices(scsi_id=None):
result.ParseFromString(data)
device_list = []
occupied_ids = []
n = 0
if len(result.device_info.devices) == 0:
return {"status": False, "device_list": []}
while n < len(result.device_info.devices):
did = result.device_info.devices[n].id
dun = result.device_info.devices[n].unit
@ -228,27 +219,34 @@ def list_devices(scsi_id=None):
device_list.append({"id": did, "un": dun, "device_type": dtype, \
"status": ", ".join(dstat_msg), "image": dpath, "file": dfile, "params": dparam,\
"vendor": dven, "product": dprod, "revision": drev, "block_size": dblock})
occupied_ids.append(did)
n += 1
return device_list, occupied_ids
return {"status": True, "device_list": device_list}
def sort_and_format_devices(device_list, occupied_ids):
def sort_and_format_devices(devices):
occupied_ids = []
for d in devices:
occupied_ids.append(d["id"])
formatted_devices = devices
# Add padding devices and sort the list
for id in range(8):
if id not in occupied_ids:
device_list.append({"id": id, "type": "-", \
formatted_devices.append({"id": id, "device_type": "-", \
"status": "-", "file": "-", "product": "-"})
# Sort list of devices by id
device_list.sort(key=lambda dic: str(dic["id"]))
formatted_devices.sort(key=lambda dic: str(dic["id"]))
return device_list
return formatted_devices
def reserve_scsi_ids(reserved_scsi_ids):
'''Sends a command to the server to reserve SCSI IDs. Takes a list of strings as argument.'''
command = proto.PbCommand()
command.operation = proto.PbOperation.RESERVE
command.params.append(reserved_scsi_ids)
command.params["ids"] = ",".join(reserved_scsi_ids)
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
@ -257,10 +255,10 @@ def reserve_scsi_ids(reserved_scsi_ids):
def set_log_level(log_level):
'''Sends a command to the server to change the log level. Takes target log level as an argument'''
'''Sends a command to the server to change the log level. Takes target log level as an argument.'''
command = proto.PbCommand()
command.operation = proto.PbOperation.LOG_LEVEL
command.params.append(str(log_level))
command.params["level"] = str(log_level)
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()

View File

@ -1,7 +1,9 @@
from os import getenv
from os import getenv, getcwd
base_dir = getenv("BASE_DIR", "/home/pi/images/")
DEFAULT_CONFIG = base_dir + "default.json"
home_dir = getcwd()
DEFAULT_CONFIG = "default.json"
MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", 1024 * 1024 * 1024 * 2) # 2gb
HARDDRIVE_FILE_SUFFIX = ("hda", "hdn", "hdi", "nhd", "hdf", "hds")
@ -11,4 +13,9 @@ ARCHIVE_FILE_SUFFIX = ("zip",)
VALID_FILE_SUFFIX = HARDDRIVE_FILE_SUFFIX + REMOVABLE_FILE_SUFFIX + \
CDROM_FILE_SUFFIX + ARCHIVE_FILE_SUFFIX
# File containing canonical drive properties
DRIVE_PROPERTIES_FILE = home_dir + "/drive_properties.json"
# File ending used for drive properties files
PROPERTIES_SUFFIX = "properties"
REMOVABLE_DEVICE_TYPES = ("SCCD", "SCRM", "SCMO")

View File

@ -24,7 +24,18 @@
<div class="content">
<div class="header">
{% block header %}{% endblock %}
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">Service Running</span>
<table width="100%">
<tbody>
<tr style="background-color: black;">
<td style="background-color: black;">
<a href="http://github.com/akuker/RASCSI">
<h1>RaSCSI - 68kmla Edition</h1>
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="flash">
{% for category, message in get_flashed_messages(with_categories=true) %}
@ -39,6 +50,9 @@
{% block content %}{% endblock %}
</div>
<div class="footer">
{% block footer %}{% endblock %}
<center><tt>RaSCSI version: <strong>{{server_info["version"]}}</strong></tt></center>
<center><tt>RaSCSI git revision: <strong><a href="https://github.com/akuker/RASCSI/commit/{{running_env["git"]}}">{{running_env["git"][:7]}}</a></strong></tt></center>
<center><tt>Server log level: <strong>{{server_info["current_log_level"]}}</strong></tt></center>
<center><tt>Pi environment: {{running_env["env"]}}</tt></center>
</div>
</div>
</div>

View File

@ -0,0 +1,139 @@
{% extends "base.html" %}
{% block content %}
<p><a href="javascript:history.back()">Cancel</a></p>
<h2>Disclaimer</h2>
<p>These device profiles are provided as-is with no guarantee to work on the systems mentioned. You may need appropirate device drivers and/or configuration parameters. If you have improvement suggestions or success stories to share we would love to hear from you, so please connect with us at <a href="https://github.com/akuker/RASCSI">GitHub</a> or <a href="https://discord.gg/PyS58u6">Discord</a>!</p>
<h2>Hard Drives</h2>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
</tr>
{% for hd in hd_conf %}
<tr>
<td style="text-align:center">{{hd.name}}</td>
<td style="text-align:center">{{hd.size_mb}}</td>
<td style="text-align:left">{{hd.description}}</td>
<td style="text-align:left">
{% if hd.url != "" %}
<a href="{{hd.url}}">Link</a>
{% else %}
-
{% endif %}
</td>
<td style="text-align:left">
<form action="/drive/create" method="post">
<input type="hidden" name="vendor" value="{{hd.vendor}}">
<input type="hidden" name="product" value="{{hd.product}}">
<input type="hidden" name="revision" value="{{hd.revision}}">
<input type="hidden" name="blocks" value="{{hd.blocks}}">
<input type="hidden" name="block_size" value="{{hd.block_size}}">
<input type="hidden" name="size" value="{{hd.size}}">
<input type="hidden" name="file_type" value="{{hd.file_type}}">
<label for="file_name">Save as:</label>
<input type="text" name="file_name" value="{{hd.name}}" />.{{hd.file_type}}
<input type="submit" value="Create" />
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<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>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
</tr>
{% for cd in cd_conf %}
<tr>
<td style="text-align:center">{{cd.name}}</td>
<td style="text-align:center">{{cd.size_mb}}</td>
<td style="text-align:left">{{cd.description}}</td>
<td style="text-align:left">
{% if cd.url != "" %}
<a href="{{cd.url}}">Link</a>
{% else %}
-
{% endif %}
</td>
<td style="text-align:left">
<form action="/drive/cdrom" method="post">
<input type="hidden" name="vendor" value="{{cd.vendor}}">
<input type="hidden" name="product" value="{{cd.product}}">
<input type="hidden" name="revision" value="{{cd.revision}}">
<input type="hidden" name="block_size" value="{{cd.block_size}}">
<label for="file_name">Create for:</label>
<select type="select" name="file_name">
{% for f in files %}
{% if f["name"].lower().endswith(cdrom_file_suffix) %}
<option value="{{f["name"]}}">{{f["name"].replace(base_dir, '')}}</option>
{% endif %}
{% endfor %}
</select>
<input type="submit" value="Create" />
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<hr/>
<h2>Removable Drives</h2>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
</tr>
{% for rm in rm_conf %}
<tr>
<td style="text-align:center">{{rm.name}}</td>
<td style="text-align:center">{{rm.size_mb}}</td>
<td style="text-align:left">{{rm.description}}</td>
<td style="text-align:left">
{% if rm.url != "" %}
<a href="{{rm.url}}">Link</a>
{% else %}
-
{% endif %}
</td>
<td style="text-align:left">
<form action="/drive/create" method="post">
<input type="hidden" name="vendor" value="{{rm.vendor}}">
<input type="hidden" name="product" value="{{rm.product}}">
<input type="hidden" name="revision" value="{{rm.revision}}">
<input type="hidden" name="blocks" value="{{rm.blocks}}">
<input type="hidden" name="block_size" value="{{rm.block_size}}">
<input type="hidden" name="size" value="{{rm.size}}">
<input type="hidden" name="file_type" value="{{rm.file_type}}">
<label for="file_name">Save as:</label>
<input type="text" name="file_name" value="{{rm.name}}" />.{{rm.file_type}}
<input type="submit" value="Create" />
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -1,24 +1,5 @@
{% extends "base.html" %}
{% block header %}
{% if server_info["status"] == True %}
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">Service Running</span>
{% else %}
<span style="display: inline-block; width: 100%; color: white; background-color: red; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">Service Stopped</span>
{% endif %}
<table width="100%">
<tbody>
<tr style="background-color: black;">
<td style="background-color: black;">
<a href="http://github.com/akuker/RASCSI">
<h1>RaSCSI - 68kmla Edition</h1>
</a>
</td>
</tr>
</tbody>
</table>
{% endblock %}
{% block content %}
<h2>Current RaSCSI Configuration</h2>
<p>
@ -55,7 +36,7 @@
</tr>
{% for device in devices %}
<tr>
{% if device["id"]|string() not in reserved_scsi_ids %}
{% if device["id"] not in reserved_scsi_ids %}
<td style="text-align:center">{{device.id}}</td>
<td style="text-align:center">{{device.device_type}}</td>
<td style="text-align:center">{{device.status}}</td>
@ -75,8 +56,6 @@
<input type="hidden" name="scsi_id" value="{{device.id}}">
<input type="submit" value="Info" />
</form>
{% elif device.device_type in ["-"] %}
<div>-</div>
{% else %}
<form action="/scsi/detach" method="post" onsubmit="return confirm('Detach Disk?')">
<input type="hidden" name="scsi_id" value="{{device.id}}">
@ -112,33 +91,33 @@
</tr>
{% for file in files %}
<tr>
<td>{{file[0].replace(base_dir, '')}}</td>
<td>{{file["name"].replace(base_dir, '')}}</td>
<td style="text-align:center">
<form action="/files/download" method="post">
<input type="hidden" name="image" value="{{file[0].replace(base_dir, '')}}">
<input type="submit" value="{{file[1]}} MB &#8595;" />
<input type="hidden" name="image" value="{{file["name"].replace(base_dir, '')}}">
<input type="submit" value="{{file["size_mb"]}} MB &#8595;" />
</form>
</td>
<td>
<form action="/scsi/attach" method="post">
<input type="hidden" name="file_name" value="{{file[0]}}">
<input type="hidden" name="file_size" value="{{file[2]}}">
<input type="hidden" name="file_name" value="{{file["name"]}}">
<input type="hidden" name="file_size" value="{{file["size"]}}">
<select name="scsi_id">
{% for id in scsi_ids %}
<option value="{{id}}">{{id}}</option>
{% endfor %}
</select>
{% if not file[0].lower().endswith(archive_file_suffix) %}
{% if not file["name"].lower().endswith(archive_file_suffix) %}
<input type="submit" value="Attach" />
{% endif %}
</form>
<form action="/files/delete" method="post" onsubmit="return confirm('Delete file?')">
<input type="hidden" name="image" value="{{file[0].replace(base_dir, '')}}">
<input type="hidden" name="image" value="{{file["name"].replace(base_dir, '')}}">
<input type="submit" value="Delete" />
</form>
{% if file[0].lower().endswith(archive_file_suffix) %}
{% if file["name"].lower().endswith(archive_file_suffix) %}
<form action="/files/unzip" method="post">
<input type="hidden" name="image" value="{{file[0].replace(base_dir, '')}}">
<input type="hidden" name="image" value="{{file["name"].replace(base_dir, '')}}">
<input type="submit" value="Unzip" />
</form>
{% endif %}
@ -151,11 +130,22 @@
<hr/>
<h2>Attach Ethernet Adapter</h2>
<p>Emulates a SCSI DaynaPORT Ethernet Adapter. <a href="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link">Host drivers required.</a></p>
<p>Emulates a SCSI DaynaPORT Ethernet Adapter. <a href="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#-macintosh-setup-instructions">Host drivers required.</a></p>
<p>Leave Interface and Static IP blank for default settings, and input only Interface for DHCP setups.</p>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/daynaport/attach" method="post">
<label for="if">Interface:</label>
<select name = "if">
{% for if in netinfo["ifs"] %}
<option value="{{if}}">{{if}}</option>
{% endfor %}
</select>
<label for="ip">Static IP (optional):</label>
<input type="text" name="ip" size="15" placeholder="10.10.20.1" />/
<input type="text" name="mask" size="2" placeholder="24">
<label for="scsi_id">ID:</label>
<select name="scsi_id">
{% for id in scsi_ids %}
<option value="{{id}}">{{id}}</option>
@ -236,6 +226,7 @@
</table>
<hr/>
<h2>Create Empty Disk Image File</h2>
<table style="border: none">
<tr style="border: none">
@ -245,14 +236,13 @@
<input type="text" placeholder="File name" name="file_name"/>
<label for="type">Type:</label>
<select name="type">
<option value="hda">SCSI Hard Disk image (APPLE GENUINE) [.hda]</option>
<option value="hds">SCSI Hard Disk image (Generic - recommended for most systems) [.hds]</option>
<option value="hdn">SCSI Hard Disk image (NEC GENUINE) [.hdn]</option>
<!-- Disabling due to https://github.com/akuker/RASCSI/issues/232
<option value="hdi">SCSI Hard Disk image (Anex86 HD image) [.hdi]</option>
<option value="nhd">SCSI Hard Disk image (T98Next HD image) [.nhd]</option> -->
<option value="hds">SCSI Hard Disk image (Generic - recommended for Atari computers) [.hds]</option>
<option value="hdr">SCSI Removable Media Disk image (Generic) [.hdr]</option>
<option value="hdf">SASI Hard Disk image (XM6 SASI HD image - typically only used with X68000) [.hdf]</option>
<option value="hdf">SASI Hard Disk image (legacy format) [.hdf]</option>
</select>
<label for="size">Size(MB):</label>
<input type="number" placeholder="Size(MB)" name="size"/>
@ -264,6 +254,11 @@
<hr/>
<h2>Create Named Drive</h2>
<p><a href="/drive/list">Ceate a named disk image with a companion properties file that mimics real-life drives</a></p>
<hr/>
<h2>Logging</h2>
<table style="border: none">
<tr style="border: none">
@ -327,9 +322,3 @@
</table>
{% endblock %}
{% block footer %}
<center><tt>RaSCSI version: <strong>{{server_info["version"]}}</strong></tt></center>
<center><tt>Server log level: <strong>{{server_info["current_log_level"]}}</strong></tt></center>
<center><tt>{{version}}</tt></center>
{% endblock %}

View File

@ -10,12 +10,13 @@ from file_cmds import (
download_image,
write_config,
read_config,
read_device_config,
write_drive_properties,
read_drive_properties,
)
from pi_cmds import (
shutdown_pi,
reboot_pi,
running_version,
shutdown_pi,
reboot_pi,
running_env,
rascsi_service,
is_bridge_setup,
)
@ -26,10 +27,10 @@ from ractl_cmds import (
detach_by_id,
eject_by_id,
get_valid_scsi_ids,
attach_daynaport,
detach_all,
reserve_scsi_ids,
get_server_info,
get_network_info,
validate_scsi_id,
set_log_level,
)
@ -40,22 +41,30 @@ app = Flask(__name__)
@app.route("/")
def index():
reserved_scsi_ids = app.config.get("RESERVED_SCSI_IDS")
unsorted_devices, occupied_ids = list_devices()
devices = sort_and_format_devices(unsorted_devices, occupied_ids)
scsi_ids = get_valid_scsi_ids(devices, list(reserved_scsi_ids), occupied_ids)
server_info = get_server_info()
devices = list_devices()
files=list_files()
config_files=list_config_files()
sorted_image_files = sorted(files["files"], key = lambda x: x["name"].lower())
sorted_config_files = sorted(config_files, key = lambda x: x.lower())
reserved_scsi_ids = server_info["reserved_ids"]
formatted_devices = sort_and_format_devices(devices["device_list"])
scsi_ids = get_valid_scsi_ids(devices["device_list"], reserved_scsi_ids)
return render_template(
"index.html",
bridge_configured=is_bridge_setup(),
devices=devices,
files=list_files(),
config_files=list_config_files(),
devices=formatted_devices,
files=sorted_image_files,
config_files=sorted_config_files,
base_dir=base_dir,
scsi_ids=scsi_ids,
reserved_scsi_ids=[reserved_scsi_ids],
reserved_scsi_ids=reserved_scsi_ids,
max_file_size=MAX_FILE_SIZE,
version=running_version(),
server_info=get_server_info(),
running_env=running_env(),
server_info=server_info,
netinfo=get_network_info(),
valid_file_suffix=VALID_FILE_SUFFIX,
removable_device_types=REMOVABLE_DEVICE_TYPES,
harddrive_file_suffix=HARDDRIVE_FILE_SUFFIX,
@ -64,44 +73,165 @@ def index():
archive_file_suffix=ARCHIVE_FILE_SUFFIX,
)
@app.route("/drive/list", methods=["GET"])
def drive_list():
"""
Sets up the data structures and kicks off the rendering of the drive list page
"""
server_info = get_server_info()
# Reads the canonical drive properties into a dict
# The file resides in the current dir of the web ui process
from pathlib import Path
drive_properties = Path(DRIVE_PROPERTIES_FILE)
if drive_properties.is_file():
process = read_drive_properties(str(drive_properties))
if process["status"] == False:
flash(process["msg"], "error")
return redirect(url_for("index"))
conf = process["conf"]
else:
flash("Could not read drive properties from " + str(drive_properties), "error")
return redirect(url_for("index"))
hd_conf = []
cd_conf = []
rm_conf = []
for d in conf:
if d["device_type"] == "SCHD":
d["size"] = d["block_size"] * d["blocks"]
d["size_mb"] = "{:,.2f}".format(d["size"] / 1024 / 1024)
hd_conf.append(d)
elif d["device_type"] == "SCCD":
d["size_mb"] = "N/A"
cd_conf.append(d)
elif d["device_type"] == "SCRM":
d["size"] = d["block_size"] * d["blocks"]
d["size_mb"] = "{:,.2f}".format(d["size"] / 1024 / 1024)
rm_conf.append(d)
files=list_files()
sorted_image_files = sorted(files["files"], key = lambda x: x["name"].lower())
hd_conf = sorted(hd_conf, key = lambda x: x["name"].lower())
cd_conf = sorted(cd_conf, key = lambda x: x["name"].lower())
rm_conf = sorted(rm_conf, key = lambda x: x["name"].lower())
return render_template(
"drives.html",
files=sorted_image_files,
base_dir=base_dir,
hd_conf=hd_conf,
cd_conf=cd_conf,
rm_conf=rm_conf,
running_env=running_env(),
server_info=server_info,
cdrom_file_suffix=CDROM_FILE_SUFFIX,
)
@app.route('/pwa/<path:path>')
def send_pwa_files(path):
return send_from_directory('pwa', path)
@app.route("/drive/create", methods=["POST"])
def drive_create():
vendor = request.form.get("vendor")
product = request.form.get("product")
revision = request.form.get("revision")
blocks = request.form.get("blocks")
block_size = request.form.get("block_size")
size = request.form.get("size")
file_type = request.form.get("file_type")
file_name = request.form.get("file_name")
# Creating the image file
process = create_new_image(file_name, file_type, size)
if process["status"] == True:
flash(f"Drive image file {file_name}.{file_type} created")
flash(process["msg"])
else:
flash(f"Failed to create file {file_name}.{file_type}", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
# Creating the drive properties file
from pathlib import Path
file_name = str(Path(file_name).stem) + "." + PROPERTIES_SUFFIX
properties = {"vendor": vendor, "product": product, "revision": revision, \
"blocks": blocks, "block_size": block_size}
process = write_drive_properties(file_name, properties)
if process["status"] == True:
flash(f"Drive properties file {file_name} created")
return redirect(url_for("index"))
else:
flash(f"Failed to create drive properties file {file_name}", "error")
return redirect(url_for("index"))
@app.route("/drive/cdrom", methods=["POST"])
def drive_cdrom():
vendor = request.form.get("vendor")
product = request.form.get("product")
revision = request.form.get("revision")
block_size = request.form.get("block_size")
file_name = request.form.get("file_name")
# Creating the drive properties file
from pathlib import Path
file_name = str(Path(file_name).stem) + "." + PROPERTIES_SUFFIX
properties = {"vendor": vendor, "product": product, "revision": revision, "block_size": block_size}
process = write_drive_properties(file_name, properties)
if process["status"] == True:
flash(f"Drive properties file {file_name} created")
return redirect(url_for("index"))
else:
flash(f"Failed to create drive properties file {file_name}", "error")
return redirect(url_for("index"))
@app.route("/config/save", methods=["POST"])
def config_save():
file_name = request.form.get("name") or "default"
file_name = f"{base_dir}{file_name}.json"
file_name = f"{file_name}.json"
process = write_config(file_name)
if process["status"] == True:
flash(f"Saved config to {file_name}!")
flash(f"Saved config to {file_name}!")
flash(process["msg"])
return redirect(url_for("index"))
else:
flash(f"Failed to saved config to {file_name}!", "error")
flash(f"{process['msg']}", "error")
return redirect(url_for("index"))
flash(f"Failed to saved config to {file_name}!", "error")
flash(process['msg'], "error")
return redirect(url_for("index"))
@app.route("/config/load", methods=["POST"])
def config_load():
file_name = request.form.get("name")
file_name = f"{base_dir}{file_name}"
if "load" in request.form:
process = read_config(file_name)
if process["status"] == True:
flash(f"Loaded config from {file_name}!")
flash(f"Loaded config from {file_name}!")
flash(process["msg"])
return redirect(url_for("index"))
else:
flash(f"Failed to load {file_name}!", "error")
flash(f"{process['msg']}", "error")
flash(f"Failed to load {file_name}!", "error")
flash(process['msg'], "error")
return redirect(url_for("index"))
elif "delete" in request.form:
if delete_file(file_name):
flash(f"Deleted config {file_name}!")
process = delete_file(file_name)
if process["status"] == True:
flash(f"Deleted config {file_name}!")
flash(process["msg"])
return redirect(url_for("index"))
else:
flash(f"Failed to delete {file_name}!", "error")
return redirect(url_for("index"))
flash(f"Failed to delete {file_name}!", "error")
flash(process['msg'], "error")
return redirect(url_for("index"))
@app.route("/logs/show", methods=["POST"])
@ -142,13 +272,23 @@ def log_level():
@app.route("/daynaport/attach", methods=["POST"])
def daynaport_attach():
scsi_id = request.form.get("scsi_id")
interface = request.form.get("if")
ip = request.form.get("ip")
mask = request.form.get("mask")
kwargs = {"device_type": "SCDP"}
if interface != "":
arg = interface
if "" not in (ip, mask):
arg += (":" + ip + "/" + mask)
kwargs["interfaces"] = arg
validate = validate_scsi_id(scsi_id)
if validate["status"] == False:
flash(validate["msg"], "error")
return redirect(url_for("index"))
process = attach_daynaport(scsi_id)
process = attach_image(scsi_id, **kwargs)
if process["status"] == True:
flash(f"Attached DaynaPORT to SCSI id {scsi_id}!")
return redirect(url_for("index"))
@ -171,34 +311,38 @@ def attach():
kwargs = {"image": file_name}
# Attempt to load the device config sidecar file:
# same base path but .rascsi instead of the original suffix.
from pathlib import Path
device_config = Path(base_dir + str(Path(file_name).stem) + ".rascsi")
if device_config.is_file():
process = read_device_config(device_config)
if process["status"] == False:
flash(process["msg"], "error")
return redirect(url_for("index"))
conf = process["conf"]
conf_file_size = conf["blocks"] * conf["block_size"]
if conf_file_size != 0 and conf_file_size > int(file_size):
flash(f"Failed to attach {file_name} to SCSI id {scsi_id}!", "error")
flash(f"The file size {file_size} bytes needs to be at least {conf_file_size} bytes.", "error")
return redirect(url_for("index"))
kwargs["device_type"] = conf["device_type"]
kwargs["vendor"] = conf["vendor"]
kwargs["product"] = conf["product"]
kwargs["revision"] = conf["revision"]
kwargs["block_size"] = conf["block_size"]
# Validate image type by file name suffix as fallback
elif file_name.lower().endswith(CDROM_FILE_SUFFIX):
# Validate image type by file name suffix
if file_name.lower().endswith(CDROM_FILE_SUFFIX):
kwargs["device_type"] = "SCCD"
elif file_name.lower().endswith(REMOVABLE_FILE_SUFFIX):
kwargs["device_type"] = "SCRM"
elif file_name.lower().endswith(HARDDRIVE_FILE_SUFFIX):
kwargs["device_type"] = "SCHD"
# Attempt to load the device properties file:
# same base path but PROPERTIES_SUFFIX instead of the original suffix.
from pathlib import Path
file_name_base = str(Path(file_name).stem)
drive_properties = Path(base_dir + file_name_base + "." + PROPERTIES_SUFFIX)
if drive_properties.is_file():
process = read_drive_properties(str(drive_properties))
if process["status"] == False:
flash(f"Failed to load the device properties file {file_name_base}.{PROPERTIES_SUFFIX}", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
conf = process["conf"]
# CD-ROM drives have no inherent size, so bypass the size check
if kwargs["device_type"] != "SCCD":
conf_file_size = int(conf["blocks"]) * int(conf["block_size"])
if conf_file_size != 0 and conf_file_size > int(file_size):
flash(f"Failed to attach {file_name} to SCSI id {scsi_id}!", "error")
flash(f"The file size {file_size} bytes needs to be at least {conf_file_size} bytes.", "error")
return redirect(url_for("index"))
kwargs["vendor"] = conf["vendor"]
kwargs["product"] = conf["product"]
kwargs["revision"] = conf["revision"]
kwargs["block_size"] = conf["block_size"]
process = attach_image(scsi_id, **kwargs)
if process["status"] == True:
flash(f"Attached {file_name} to SCSI id {scsi_id}!")
@ -249,8 +393,16 @@ def eject():
@app.route("/scsi/info", methods=["POST"])
def device_info():
scsi_id = request.form.get("scsi_id")
# Extracting the 0th dictionary in list index 0
device = list_devices(scsi_id)[0][0]
devices = list_devices(scsi_id)
# First check if any device at all was returned
if devices["status"] == False:
flash(f"No device attached to SCSI id {scsi_id}!", "error")
return redirect(url_for("index"))
# Looking at the first dict in list to get
# the one and only device that should have been returned
device = devices["device_list"][0]
if str(device["id"]) == scsi_id:
flash("=== DEVICE INFO ===")
flash(f"SCSI ID: {device['id']}")
@ -270,31 +422,36 @@ def device_info():
@app.route("/pi/reboot", methods=["POST"])
def restart():
flash("Restarting the Pi momentarily...")
reboot_pi()
flash("Restarting...")
return redirect(url_for("index"))
@app.route("/rascsi/restart", methods=["POST"])
def rascsi_restart():
server_info = get_server_info()
rascsi_service("restart")
flash("Restarting RaSCSI Service...")
reserved_scsi_ids = app.config.get("RESERVED_SCSI_IDS")
if reserved_scsi_ids != "":
reserve_scsi_ids(reserved_scsi_ids)
# Need to turn this into a list of strings from a list of ints
reserve_scsi_ids([str(e) for e in server_info["reserved_ids"]])
return redirect(url_for("index"))
@app.route("/pi/shutdown", methods=["POST"])
def shutdown():
flash("Shutting down the Pi momentarily...")
shutdown_pi()
flash("Shutting down...")
return redirect(url_for("index"))
@app.route("/files/download_to_iso", methods=["POST"])
def download_file():
scsi_id = request.form.get("scsi_id")
validate = validate_scsi_id(scsi_id)
if validate["status"] == False:
flash(validate["msg"], "error")
return redirect(url_for("index"))
url = request.form.get("url")
process = download_file_to_iso(scsi_id, url)
if process["status"] == True:
@ -348,17 +505,17 @@ def upload_file(filename):
@app.route("/files/create", methods=["POST"])
def create_file():
file_name = request.form.get("file_name")
size = request.form.get("size")
size = (int(request.form.get("size")) * 1024 * 1024)
file_type = request.form.get("type")
process = create_new_image(file_name, file_type, size)
if process.returncode == 0:
flash("Drive created")
if process["status"] == True:
flash(f"Drive image created as {file_name}.{file_type}")
flash(process["msg"])
return redirect(url_for("index"))
else:
flash("Failed to create file", "error")
flash(process.stdout, "stdout")
flash(process.stderr, "stderr")
flash(f"Failed to create file {file_name}.{file_type}", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -370,14 +527,35 @@ def download():
@app.route("/files/delete", methods=["POST"])
def delete():
image = request.form.get("image")
if delete_file(base_dir + image):
flash("File " + image + " deleted")
return redirect(url_for("index"))
file_name = request.form.get("image")
process = delete_file(file_name)
if process["status"] == True:
flash(f"File {file_name} deleted!")
flash(process["msg"])
else:
flash("Failed to Delete " + image, "error")
flash(f"Failed to delete file {file_name}!", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
# Delete the drive properties file, if it exists
from pathlib import Path
file_name = str(Path(file_name).stem) + "." + PROPERTIES_SUFFIX
file_path = Path(base_dir + file_name)
if file_path.is_file():
process = delete_file(file_name)
if process["status"] == True:
flash(f"File {file_name} deleted!")
flash(process["msg"])
return redirect(url_for("index"))
else:
flash(f"Failed to delete file {file_name}!", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
return redirect(url_for("index"))
@app.route("/files/unzip", methods=["POST"])
def unzip():
@ -402,18 +580,15 @@ if __name__ == "__main__":
from sys import argv
if len(argv) >= 2:
# Reserved ids format is a string of digits such as '017'
app.config["RESERVED_SCSI_IDS"] = str(argv[1])
# Reserve SCSI IDs on the backend side to prevent use
reserve_scsi_ids(app.config.get("RESERVED_SCSI_IDS"))
else:
app.config["RESERVED_SCSI_IDS"] = ""
# Expecting argv as a string of digits such as '017'
reserve_scsi_ids(list(argv[1]))
# Load the default configuration file, if found
from pathlib import Path
default_config = Path(DEFAULT_CONFIG)
if default_config.is_file():
read_config(default_config)
default_config_path = Path(base_dir + DEFAULT_CONFIG)
if default_config_path.is_file():
read_config(DEFAULT_CONFIG)
import bjoern
print("Serving rascsi-web...")