clean squashed restructuring branch. #455

This commit is contained in:
Benjamin Zeiss
2022-01-09 15:26:28 +01:00
parent b52abbfdc7
commit 17497cf1fe
77 changed files with 211 additions and 132 deletions
+557
View File
@@ -0,0 +1,557 @@
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
init-hook=
# venv hook for pylint
# Requires pylint-venv package:
# $ pip install pylint-venv
try: import pylint_venv
except ImportError: pass
else: pylint_venv.inithook()
# Use multiple processes to speed up Pylint.
jobs=1
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Specify a configuration file.
#rcfile=
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=print-statement,
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
backtick,
long-suffix,
old-ne-operator,
old-octal-literal,
import-star-module-level,
non-ascii-bytes-literal,
invalid-unicode-literal,
raw-checker-failed,
bad-inline-option,
locally-disabled,
locally-enabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
apply-builtin,
basestring-builtin,
buffer-builtin,
cmp-builtin,
coerce-builtin,
execfile-builtin,
file-builtin,
long-builtin,
raw_input-builtin,
reduce-builtin,
standarderror-builtin,
unicode-builtin,
xrange-builtin,
coerce-method,
delslice-method,
getslice-method,
setslice-method,
no-absolute-import,
old-division,
dict-iter-method,
dict-view-method,
next-method-called,
metaclass-assignment,
indexing-exception,
raising-string,
reload-builtin,
oct-method,
hex-method,
nonzero-method,
cmp-method,
input-builtin,
round-builtin,
intern-builtin,
unichr-builtin,
map-builtin-not-iterating,
zip-builtin-not-iterating,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
eq-without-hash,
div-method,
idiv-method,
rdiv-method,
exception-message-attribute,
invalid-str-codec,
sys-max-int,
bad-python3-import,
deprecated-string-function,
deprecated-str-translate-call,
deprecated-itertools-function,
deprecated-types-field,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating,
deprecated-operator-function,
deprecated-urllib-function,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape,
import-outside-toplevel
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[REPORTS]
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio).You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=optparse.Values,sys.exit
[BASIC]
# Naming style matching correct argument names
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style
#argument-rgx=
# Naming style matching correct attribute names
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Naming style matching correct class attribute names
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style
#class-attribute-rgx=
# Naming style matching correct class names
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-style
#class-rgx=
# Naming style matching correct constant names
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style
#function-rgx=
# Good variable names which should always be accepted, separated by a comma
good-names=i,
j,
k,
ex,
Run,
_
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Naming style matching correct inline iteration names
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style
#inlinevar-rgx=
# Naming style matching correct method names
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style
#method-rgx=
# Naming style matching correct module names
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Naming style matching correct variable names
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style
#variable-rgx=
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module
max-module-lines=1000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,
dict-separator
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=rascsi_interface_pb2
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,io,builtins
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of statements in function / method body
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[IMPORTS]
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,
TERMIOS,
Bastion,
rexec
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception
View File
View File
+261
View File
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
# BSD 3-Clause License
#
# Copyright (c) 2021, akuker
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import RPi.GPIO as gpio
import time
pin_settle_delay = 0.01
err_count = 0
# Define constants for each of the SCSI signals, based upon their
# raspberry pi pin number (since we're using BOARD mode of RPi.GPIO)
scsi_d0_gpio = 19
scsi_d1_gpio = 23
scsi_d2_gpio = 32
scsi_d3_gpio = 33
scsi_d4_gpio = 8
scsi_d5_gpio = 10
scsi_d6_gpio = 36
scsi_d7_gpio = 11
scsi_dp_gpio = 12
scsi_atn_gpio = 35
scsi_rst_gpio = 38
scsi_ack_gpio = 40
scsi_req_gpio = 15
scsi_msg_gpio = 16
scsi_cd_gpio = 18
scsi_io_gpio = 22
scsi_bsy_gpio = 37
scsi_sel_gpio = 13
# Pin numbers of the direction controllers of the RaSCSI board
rascsi_ind_gpio = 31
rascsi_tad_gpio = 26
rascsi_dtd_gpio = 24
rascsi_none = -1
# Matrix showing all of the SCSI signals, along what signal they're looped back to.
# dir_ctrl indicates which direction control pin is associated with that output
gpio_map = [
{ 'gpio_num': scsi_d0_gpio, 'attached_to': scsi_ack_gpio, 'dir_ctrl': rascsi_dtd_gpio},
{ 'gpio_num': scsi_d1_gpio, 'attached_to': scsi_sel_gpio, 'dir_ctrl': rascsi_dtd_gpio},
{ 'gpio_num': scsi_d2_gpio, 'attached_to': scsi_atn_gpio, 'dir_ctrl': rascsi_dtd_gpio},
{ 'gpio_num': scsi_d3_gpio, 'attached_to': scsi_rst_gpio, 'dir_ctrl': rascsi_dtd_gpio},
{ 'gpio_num': scsi_d4_gpio, 'attached_to': scsi_cd_gpio, 'dir_ctrl': rascsi_dtd_gpio},
{ 'gpio_num': scsi_d5_gpio, 'attached_to': scsi_io_gpio, 'dir_ctrl': rascsi_dtd_gpio},
{ 'gpio_num': scsi_d6_gpio, 'attached_to': scsi_msg_gpio, 'dir_ctrl': rascsi_dtd_gpio},
{ 'gpio_num': scsi_d7_gpio, 'attached_to': scsi_req_gpio, 'dir_ctrl': rascsi_dtd_gpio},
{ 'gpio_num': scsi_dp_gpio, 'attached_to': scsi_bsy_gpio, 'dir_ctrl': rascsi_dtd_gpio},
{ 'gpio_num': scsi_atn_gpio, 'attached_to': scsi_d2_gpio, 'dir_ctrl': rascsi_ind_gpio},
{ 'gpio_num': scsi_rst_gpio, 'attached_to': scsi_d3_gpio, 'dir_ctrl': rascsi_ind_gpio},
{ 'gpio_num': scsi_ack_gpio, 'attached_to': scsi_d0_gpio, 'dir_ctrl': rascsi_ind_gpio},
{ 'gpio_num': scsi_req_gpio, 'attached_to': scsi_d7_gpio, 'dir_ctrl': rascsi_tad_gpio},
{ 'gpio_num': scsi_msg_gpio, 'attached_to': scsi_d6_gpio, 'dir_ctrl': rascsi_tad_gpio},
{ 'gpio_num': scsi_cd_gpio, 'attached_to': scsi_d4_gpio, 'dir_ctrl': rascsi_tad_gpio},
{ 'gpio_num': scsi_io_gpio, 'attached_to': scsi_d5_gpio, 'dir_ctrl': rascsi_tad_gpio},
{ 'gpio_num': scsi_bsy_gpio, 'attached_to': scsi_dp_gpio, 'dir_ctrl': rascsi_tad_gpio},
{ 'gpio_num': scsi_sel_gpio, 'attached_to': scsi_d1_gpio, 'dir_ctrl': rascsi_ind_gpio},
]
# List of all of the SCSI signals that is also a dictionary to their human readable name
scsi_signals = {
scsi_d0_gpio: 'D0',
scsi_d1_gpio: 'D1',
scsi_d2_gpio: 'D2',
scsi_d3_gpio: 'D3',
scsi_d4_gpio: 'D4',
scsi_d5_gpio: 'D5',
scsi_d6_gpio: 'D6',
scsi_d7_gpio: 'D7',
scsi_dp_gpio: 'DP',
scsi_atn_gpio: 'ATN',
scsi_rst_gpio: 'RST',
scsi_ack_gpio: 'ACK',
scsi_req_gpio: 'REQ',
scsi_msg_gpio: 'MSG',
scsi_cd_gpio: 'CD',
scsi_io_gpio: 'IO',
scsi_bsy_gpio: 'BSY',
scsi_sel_gpio: 'SEL'
}
# Debug function that just dumps the status of all of the scsi signals to the console
def print_all():
for cur_gpio in gpio_map:
print(cur_gpio['name']+"="+str(gpio.input(cur_gpio['gpio_num'])) + " ", end='', flush=True)
print("")
# Set transceivers IC1 and IC2 to OUTPUT
def set_dtd_out():
gpio.output(rascsi_dtd_gpio,gpio.LOW)
# Set transceivers IC1 and IC2 to INPUT
def set_dtd_in():
gpio.output(rascsi_dtd_gpio,gpio.HIGH)
# Set transceiver IC4 to OUTPUT
def set_ind_out():
gpio.output(rascsi_ind_gpio,gpio.HIGH)
# Set transceiver IC4 to INPUT
def set_ind_in():
gpio.output(rascsi_ind_gpio,gpio.LOW)
# Set transceiver IC3 to OUTPUT
def set_tad_out():
gpio.output(rascsi_tad_gpio,gpio.HIGH)
# Set transceiver IC3 to INPUT
def set_tad_in():
gpio.output(rascsi_tad_gpio,gpio.LOW)
# Set the specified transciever to an OUTPUT. All of the other transceivers
# will be set to inputs. If a non-existent direction gpio is specified, this
# will set all of the transceivers to inputs.
def set_output_channel(out_gpio):
if(out_gpio == rascsi_tad_gpio):
set_tad_out()
else:
set_tad_in()
if(out_gpio == rascsi_dtd_gpio):
set_dtd_out()
else:
set_dtd_in()
if(out_gpio == rascsi_ind_gpio):
set_ind_out()
else:
set_ind_in()
# Main test procedure. This will execute for each of the SCSI pins to make sure its connected
# properly.
def test_gpio_pin(gpio_rec):
global err_count
set_output_channel(gpio_rec['dir_ctrl'])
############################################
# set the test gpio low
gpio.output(gpio_rec['gpio_num'], gpio.LOW)
time.sleep(pin_settle_delay)
# loop through all of the gpios
for cur_gpio in scsi_signals:
# all of the gpios should be high except for the test gpio and the connected gpio
cur_val = gpio.input(cur_gpio)
if( cur_gpio == gpio_rec['gpio_num']):
if(cur_val != gpio.LOW):
print("Error: Test commanded GPIO " + scsi_signals[gpio_rec['gpio_num']] + " to be low, but it did not respond")
err_count = err_count+1
elif (cur_gpio == gpio_rec['attached_to']):
if(cur_val != gpio.LOW):
print("Error: GPIO " + scsi_signals[gpio_rec['gpio_num']] + " should drive " + scsi_signals[gpio_rec['attached_to']] + " low, but did not")
err_count = err_count+1
else:
if(cur_val != gpio.HIGH):
print("Error: GPIO " + scsi_signals[gpio_rec['gpio_num']] + " incorrectly pulled " + scsi_signals[cur_gpio] + " LOW, when it shouldn't have")
err_count = err_count+1
############################################
# set the transceivers to input
set_output_channel(rascsi_none)
time.sleep(pin_settle_delay)
# loop through all of the gpios
for cur_gpio in scsi_signals:
# all of the gpios should be high except for the test gpio
cur_val = gpio.input(cur_gpio)
if( cur_gpio == gpio_rec['gpio_num']):
if(cur_val != gpio.LOW):
print("Error: Test commanded GPIO " + scsi_signals[gpio_rec['gpio_num']] + " to be low, but it did not respond")
err_count = err_count+1
else:
if(cur_val != gpio.HIGH):
print("Error: GPIO " + scsi_signals[gpio_rec['gpio_num']] + " incorrectly pulled " + scsi_signals[cur_gpio] + " LOW, when it shouldn't have")
err_count = err_count+1
# Set the transceiver back to output
set_output_channel(gpio_rec['dir_ctrl'])
#############################################
# set the test gpio high
gpio.output(gpio_rec['gpio_num'], gpio.HIGH)
time.sleep(pin_settle_delay)
# loop through all of the gpios
for cur_gpio in scsi_signals:
# all of the gpios should be high
cur_val = gpio.input(cur_gpio)
if( cur_gpio == gpio_rec['gpio_num']):
if(cur_val != gpio.HIGH):
print("Error: Test commanded GPIO " + scsi_signals[gpio_rec['gpio_num']] + " to be high, but it did not respond")
err_count = err_count+1
else:
if(cur_val != gpio.HIGH):
print("Error: GPIO " + scsi_signals[gpio_rec['gpio_num']] + " incorrectly pulled " + scsi_signals[cur_gpio] + " LOW, when it shouldn't have")
err_count = err_count+1
# Initialize the GPIO library, set all of the gpios associated with SCSI signals to outputs and set
# all of the direction control gpios to outputs
def setup():
gpio.setmode(gpio.BOARD)
gpio.setwarnings(False)
for cur_gpio in gpio_map:
gpio.setup(cur_gpio['gpio_num'], gpio.OUT, initial=gpio.HIGH)
# Setup direction control
gpio.setup(rascsi_ind_gpio, gpio.OUT)
gpio.setup(rascsi_tad_gpio, gpio.OUT)
gpio.setup(rascsi_dtd_gpio, gpio.OUT)
# Main functions for running the actual test.
if __name__ == '__main__':
# setup the GPIOs
setup()
# Test each SCSI signal in the gpio_map
for cur_gpio in gpio_map:
test_gpio_pin(cur_gpio)
# Print the test results
if(err_count == 0):
print("-------- Test PASSED --------")
else:
print("!!!!!!!! Test FAILED !!!!!!!!")
print("Total errors: " + str(err_count))
gpio.cleanup()
+1
View File
@@ -0,0 +1 @@
../.pylintrc
+55
View File
@@ -0,0 +1,55 @@
# RaSCSI OLED Screen
## Run as standalone script for development / troubleshooting
```bash
# Make a virtual env named venv
$ python3 -m venv venv
# Use that virtual env in this shell
$ source venv/bin/activate
# Install requirements
$ pip3 install -r requirements.txt
$ python3 src/rascsi_oled_monitor.py
```
### Parameters
The script takes two positional parameters:
* '0' or '180' which decides the screen rotation
* '32' or '64' which decides the vertical screen resolution in pixels
Ex.
```
$ python3 rascsi_oled_monitor.py 180 64
```
_Note:_ Both parameters must be passed for the script to read them. Ordering is also important.
## Run the start.sh script standalone
The start.sh script can also be run standalone, and will handle the venv creation/updating for you. It takes the same command line parameters in the following format:
```
$ ./start.sh --rotation=180 --height=64
```
## Static analysis with pylint
It is recommended to run pylint against new code to protect against bugs
and keep the code readable and maintainable.
The local pylint configuration lives in .pylintrc (symlink to ../.pylintrc)
```
$ sudo apt install pylint3
$ pylint3 python_source_file.py
```
## Credits
### type_writer.ttf
* _Type Writer_ TrueType font by Mandy Smith
* Source: https://www.dafont.com/type-writer.font
* Distributed under BSD 3-Clause by permission from author (see LICENSE for full text)
### splash_start_\*.bmp, splash_stop_\*.bmp
* Drawn by Daniel Markstedt
* Distributed under BSD 3-Clause by permission from author (see LICENSE for full text)
+16
View File
@@ -0,0 +1,16 @@
Adafruit-Blinka==6.15.0
adafruit-circuitpython-busdevice==5.1.0
adafruit-circuitpython-framebuf==1.4.7
adafruit-circuitpython-ssd1306==2.12.2
Adafruit-PlatformDetect==3.17.2
Adafruit-PureIO==1.1.9
Pillow==8.4.0
pkg-resources==0.0.0
pyftdi==0.53.3
pyserial==3.5
pyusb==1.2.1
rpi-ws281x==4.3.0
RPi.GPIO==0.7.0
sysv-ipc==1.1.0
protobuf==3.19.1
unidecode==1.3.2
Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
@@ -0,0 +1,16 @@
[Unit]
Description=RaSCSI-OLED service
After=network.target rascsi.service
[Service]
Type=simple
Restart=always
RestartSec=2s
ExecStart=/home/pi/RASCSI/python/oled/start.sh
ExecStop=/bin/pkill --signal 2 -f "python3 src/rascsi_oled_monitor.py"
# Sleep 2s as a crude way for the python interrupt handler to take effect and show the shutdown splash
ExecStop=/bin/sleep 2
SyslogIdentifier=RASCSIMON
[Install]
WantedBy=multi-user.target
+38
View File
@@ -0,0 +1,38 @@
"""
Linux interrupt handling module
"""
import signal
class GracefulInterruptHandler():
"""
Class for handling Linux signal interrupts
"""
def __init__(self, signals=(signal.SIGINT, signal.SIGTERM)):
self.signals = signals
self.original_handlers = {}
self.interrupted = False
self.released = False
def __enter__(self):
for sig in self.signals:
self.original_handlers[sig] = signal.getsignal(sig)
signal.signal(sig, self.handler)
return self
def handler(self, signum, frame):
self.release()
self.interrupted = True
def __exit__(self, exception_type, exception_value, traceback):
self.release()
def release(self):
if self.released:
return False
for sig in self.signals:
signal.signal(sig, self.original_handlers[sig])
self.released = True
return True
+20
View File
@@ -0,0 +1,20 @@
"""
Module with methods that interact with the Pi's Linux system
"""
def get_ip_and_host():
"""
Use a mock socket connection to identify the Pi's hostname and IP address
"""
from socket import socket, gethostname, AF_INET, SOCK_DGRAM
host = gethostname()
sock = socket(AF_INET, SOCK_DGRAM)
try:
# mock ip address; doesn't have to be reachable
sock.connect(('10.255.255.255', 1))
ip_addr = sock.getsockname()[0]
except Exception:
ip_addr = False
finally:
sock.close()
return ip_addr, host
+70
View File
@@ -0,0 +1,70 @@
"""
Module for commands sent to the RaSCSI backend service.
"""
from os import path
from unidecode import unidecode
from socket_cmds import send_pb_command
import rascsi_interface_pb2 as proto
def device_list(token):
"""
Sends a DEVICES_INFO command to the server.
Returns a list of dicts with info on all attached devices.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICES_INFO
command.params["token"] = token
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
dlist = []
i = 0
while i < len(result.devices_info.devices):
did = result.devices_info.devices[i].id
dtype = proto.PbDeviceType.Name(result.devices_info.devices[i].type)
dstat = result.devices_info.devices[i].status
dprop = result.devices_info.devices[i].properties
# Building the status string
dstat_msg = []
if dstat.protected and dprop.protectable:
dstat_msg.append("Write-Protected")
if dstat.removed and dprop.removable:
dstat_msg.append("No Media")
if dstat.locked and dprop.lockable:
dstat_msg.append("Locked")
# Transliterate non-ASCII chars in the file name to ASCII
dfile = unidecode(path.basename(result.devices_info.devices[i].file.name))
dven = result.devices_info.devices[i].vendor
dprod = result.devices_info.devices[i].product
dlist.append({
"id": did,
"device_type": dtype,
"status": ", ".join(dstat_msg),
"file": dfile,
"vendor": dven,
"product": dprod,
})
i += 1
return dlist
def is_token_auth(token):
"""
Sends a CHECK_AUTHENTICATION command to the server.
Tells you whether RaSCSI backend is protected by a token password or not.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CHECK_AUTHENTICATION
command.params["token"] = token
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""
RaSCSI Updates:
Updates to output rascsi status to an OLED display
Copyright (C) 2020 Tony Kuker
Author: Tony Kuker
Developed for:
https://www.makerfocus.com/collections/oled/products/2pcs-i2c-oled-display-module-0-91-inch-i2c-ssd1306-oled-display-module-1
All other code:
Copyright (c) 2017 Adafruit Industries
Author: Tony DiCola & James DeVito
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import argparse
import sys
from time import sleep
from collections import deque
from board import I2C
from adafruit_ssd1306 import SSD1306_I2C
from PIL import Image, ImageDraw, ImageFont
from interrupt_handler import GracefulInterruptHandler
from pi_cmds import get_ip_and_host
from ractl_cmds import device_list, is_token_auth
parser = argparse.ArgumentParser(description="RaSCSI OLED Monitor script")
parser.add_argument(
"--rotation",
type=int,
choices=[0, 180],
default=180,
action="store",
help="The rotation of the screen buffer in degrees",
)
parser.add_argument(
"--height",
type=int,
choices=[32, 64],
default=32,
action="store",
help="The pixel height of the screen buffer",
)
parser.add_argument(
"--password",
type=str,
default="",
action="store",
help="Token password string for authenticating with RaSCSI",
)
args = parser.parse_args()
if args.rotation == 0:
ROTATION = 0
elif args.rotation == 180:
ROTATION = 2
if args.height == 64:
HEIGHT = 64
LINES = 8
elif args.height == 32:
HEIGHT = 32
LINES = 4
TOKEN = args.password
WIDTH = 128
BORDER = 5
# How long to delay between each update
DELAY_TIME_MS = 1000
# Define the Reset Pin
OLED_RESET = None
# init i2c
I2C = I2C()
# 128x32 display with hardware I2C:
OLED = SSD1306_I2C(WIDTH, HEIGHT, I2C, addr=0x3C, reset=OLED_RESET)
OLED.rotation = ROTATION
print("Running with the following display:")
print(OLED)
print()
print("Will update the OLED display every " + str(DELAY_TIME_MS) + "ms (approximately)")
# Show a startup splash bitmap image before starting the main loop
# Convert the image to mode '1' for 1-bit color (monochrome)
# Make sure the splash bitmap image is in the same dir as this script
IMAGE = Image.open(f"resources/splash_start_{HEIGHT}.bmp").convert("1")
OLED.image(IMAGE)
OLED.show()
# Keep the pretty splash on screen for a number of seconds
sleep(4)
# Get drawing object to draw on image.
DRAW = ImageDraw.Draw(IMAGE)
# Draw a black filled box to clear the image.
DRAW.rectangle((0, 0, WIDTH, HEIGHT), outline=0, fill=0)
# Draw some shapes.
# First define some constants to allow easy resizing of shapes.
# Depending on the font used, you may want to change the value of PADDING
PADDING = 0
TOP = PADDING
BOTTOM = HEIGHT - PADDING
# Move left to right keeping track of the current x position for drawing shapes.
X_POS = 0
# Font size in pixels. Not all TTF fonts have bitmap representations for all sizes.
FONT_SIZE = 8
# Vertical spacing between each line of text. Adjust in accordance with font size.
# Depending on the design of the font glyphs, this may be larger than FONT_SIZE.
LINE_SPACING = 8
# Load a TTF font for rendering glyphs on the screen.
# Make sure the .ttf font file is in the same directory as the python script!
# When using other fonts, you may need to adjust PADDING, FONT_SIZE,
# LINE_SPACING, and LINES.
# Some other nice fonts to try: http://www.dafont.com/bitmap.php
FONT = ImageFont.truetype('resources/type_writer.ttf', FONT_SIZE)
IP_ADDR, HOSTNAME = get_ip_and_host()
def formatted_output():
"""
Formats the strings to be displayed on the Screen
Returns a (list) of (str) output
"""
rascsi_list = device_list(TOKEN)
output = []
if not TOKEN and not is_token_auth(TOKEN)["status"]:
output.append("Permission denied!")
elif rascsi_list:
for line in rascsi_list:
if line["device_type"] in ("SCCD", "SCRM", "SCMO"):
# Print image file name only when there is an image attached
if line["file"]:
output.append(f"{line['id']} {line['device_type'][2:4]} "
f"{line['file']} {line['status']}")
else:
output.append(f"{line['id']} {line['device_type'][2:4]} {line['status']}")
# Special handling for the DaynaPort device
elif line["device_type"] == "SCDP":
output.append(f"{line['id']} {line['device_type'][2:4]} {line['vendor']} "
f"{line['product']}")
# Special handling for the Host Bridge device
elif line["device_type"] == "SCBR":
output.append(f"{line['id']} {line['device_type'][2:4]} {line['product']}")
# Print only the Vendor/Product info if it's not generic RaSCSI
elif line["vendor"] not in "RaSCSI":
output.append(f"{line['id']} {line['device_type'][2:4]} {line['file']} "
f"{line['vendor']} {line['product']} {line['status']}")
else:
output.append(f"{line['id']} {line['device_type'][2:4]} {line['file']} "
f"{line['status']}")
else:
output.append("No image mounted!")
if IP_ADDR:
output.append(f"IP {IP_ADDR} - {HOSTNAME}")
else:
output.append("RaSCSI has no IP address")
output.append("Check network connection")
return output
with GracefulInterruptHandler() as handler:
while True:
# The reference snapshot of attached devices that will be compared against each cycle
# to identify changes in RaSCSI backend
ref_snapshot = formatted_output()
# The snapshot updated each cycle that will compared with ref_snapshot
snapshot = ref_snapshot
# The active output that will be displayed on the screen
active_output = deque(snapshot)
while snapshot == ref_snapshot:
# Draw a black filled box to clear the image.
DRAW.rectangle((0, 0, WIDTH, HEIGHT), outline=0, fill=0)
Y_POS = TOP
for output_line in active_output:
DRAW.text((X_POS, Y_POS), output_line, font=FONT, fill=255)
Y_POS += LINE_SPACING
# Shift the index of the array by one to get a scrolling effect
if len(active_output) > LINES:
active_output.rotate(-1)
# Display image.
OLED.image(IMAGE)
OLED.show()
sleep(1000/DELAY_TIME_MS)
snapshot = formatted_output()
if handler.interrupted:
# Catch interrupt signals and blank out the screen
DRAW.rectangle((0, 0, WIDTH, HEIGHT), outline=0, fill=0)
OLED.image(IMAGE)
OLED.show()
sys.exit("Shutting down the OLED display...")
+73
View File
@@ -0,0 +1,73 @@
"""
Module for handling socket connections for sending commands
and receiving results from the RaSCSI backend
"""
import socket
from struct import pack, unpack
from time import sleep
def send_pb_command(payload):
"""
Takes a (str) containing a serialized protobuf as argument.
Establishes a socket connection with RaSCSI.
"""
# Host and port number where rascsi is listening for socket connections
host = 'localhost'
port = 6868
counter = 0
tries = 20
error_msg = ""
while counter < tries:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((host, port))
return send_over_socket(sock, payload)
except socket.error as error:
counter += 1
print("The RaSCSI service is not responding - attempt %s/%s",
str(counter), str(tries))
error_msg = str(error)
sleep(0.2)
exit(error_msg)
def send_over_socket(sock, payload):
"""
Takes a socket object and (str) payload with serialized protobuf.
Sends payload to RaSCSI over socket and captures the response.
Tries to extract and interpret the protobuf header to get response size.
Reads data from socket in 2048 bytes chunks until all data is received.
"""
# Sending the magic word "RASCSI" to authenticate with the server
sock.send(b"RASCSI")
# Prepending a little endian 32bit header with the message size
sock.send(pack("<i", len(payload)))
sock.send(payload)
# Receive the first 4 bytes to get the response header
response = sock.recv(4)
if len(response) >= 4:
# Extracting the response header to get the length of the response message
response_length = unpack("<i", response)[0]
# Reading in chunks, to handle a case where the response message is very large
chunks = []
bytes_recvd = 0
while bytes_recvd < response_length:
chunk = sock.recv(min(response_length - bytes_recvd, 2048))
if chunk == b'':
exit("Socket connection has dropped unexpectedly. "
"RaSCSI may have crashed."
)
chunks.append(chunk)
bytes_recvd = bytes_recvd + len(chunk)
response_message = b''.join(chunks)
return response_message
exit("The response from RaSCSI did not contain a protobuf header. "
"RaSCSI may have crashed."
)
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
set -e
# set -x # Uncomment to Debug
cd "$(dirname "$0")"
# verify packages installed
ERROR=0
if ! command -v dpkg -l i2c-tools &> /dev/null ; then
echo "i2c-tools could not be found"
echo "Run 'sudo apt install i2c-tools' to fix."
ERROR=1
fi
if ! command -v python3 &> /dev/null ; then
echo "python3 could not be found"
echo "Run 'sudo apt install python3' to fix."
ERROR=1
fi
if ! python3 -m venv --help &> /dev/null ; then
echo "venv could not be found"
echo "Run 'sudo apt install python3-venv' to fix."
ERROR=1
fi
# Dep to build Pillow
if ! dpkg -l python3-dev &> /dev/null; then
echo "python3-dev could not be found"
echo "Run 'sudo apt install python3-dev' to fix."
ERROR=1
fi
if ! dpkg -l libjpeg-dev &> /dev/null; then
echo "libjpeg-dev could not be found"
echo "Run 'sudo apt install libjpeg-dev' to fix."
ERROR=1
fi
if ! dpkg -l libpng-dev &> /dev/null; then
echo "libpng-dev could not be found"
echo "Run 'sudo apt install libpng-dev' to fix."
ERROR=1
fi
if ! dpkg -l libopenjp2-7-dev &> /dev/null; then
echo "libopenjp2-7-dev could not be found"
echo "Run 'sudo apt install libopenjp2-7-dev' to fix."
ERROR=1
fi
if [ $ERROR = 1 ] ; then
echo
echo "Fix errors and re-run ./start.sh"
exit 1
fi
if pgrep -f "python3 src/rascsi_oled_monitor.py" &> /dev/null; then
echo "Detected active rascsi_oled_monitor.py process"
echo "Terminating before launching a new one."
sudo pkill -f "python3 src/rascsi_oled_monitor.py"
fi
if ! i2cdetect -y 1 &> /dev/null ; then
echo "i2cdetect -y 1 did not find a screen."
exit 2
fi
# Compiler flags needed for gcc v10 and up
if [[ `gcc --version | awk '/gcc/' | awk -F ' ' '{print $3}' | awk -F '.' '{print $1}'` -ge 10 ]]; then
COMPILER_FLAGS="-fcommon"
fi
# Test for two known broken venv states
if test -e venv; then
GOOD_VENV=true
if ! test -e venv/bin/activate; then
GOOD_VENV=false
else
source venv/bin/activate
pip3 list 1> /dev/null
test $? -eq 1 && GOOD_VENV=false
fi
if ! "$GOOD_VENV"; then
echo "Deleting bad python venv"
sudo rm -rf venv
fi
fi
# Create the venv if it doesn't exist
if ! test -e venv; then
echo "Creating python venv for OLED Screen"
python3 -m venv venv
echo "Activating venv"
source venv/bin/activate
echo "Installing requirements.txt"
pip3 install wheel
CFLAGS="$COMPILER_FLAGS" pip3 install -r requirements.txt
set +e
git rev-parse --is-inside-work-tree &> /dev/null
if [[ $? -eq 0 ]]; then
git rev-parse HEAD > current
fi
set -e
fi
source venv/bin/activate
# Detect if someone updates the git repo - we need to re-run pip3 install.
set +e
git rev-parse --is-inside-work-tree &> /dev/null
if [[ $? -eq 0 ]]; then
set -e
if ! test -e current; then
git rev-parse > current
elif [ "$(cat current)" != "$(git rev-parse HEAD)" ]; then
echo "New version detected, updating libraries from requirements.txt"
CFLAGS="$COMPILER_FLAGS" pip3 install -r requirements.txt
git rev-parse HEAD > current
fi
else
echo "Warning: Not running from a valid git repository. Will not be able to update the code."
fi
set -e
# parse arguments
while [ "$1" != "" ]; do
PARAM=$(echo "$1" | awk -F= '{print $1}')
VALUE=$(echo "$1" | awk -F= '{print $2}')
case $PARAM in
-r | --rotation)
ROTATION="--rotation $VALUE"
;;
-h | --height)
HEIGHT="--height $VALUE"
;;
-P | --password)
PASSWORD="--password $VALUE"
;;
*)
echo "ERROR: unknown parameter \"$PARAM\""
exit 1
;;
esac
shift
done
echo "Starting OLED Screen..."
if [ -z ${ROTATION+x} ]; then
echo "No screen rotation parameter given; falling back to the default."
else
echo "Starting with parameter $ROTATION"
fi
if [ -z ${HEIGHT+x} ]; then
echo "No screen height parameter given; falling back to the default."
else
echo "Starting with parameter $HEIGHT"
fi
python3 src/rascsi_oled_monitor.py ${ROTATION} ${HEIGHT} ${PASSWORD}
View File
+1
View File
@@ -0,0 +1 @@
../.pylintrc
+114
View File
@@ -0,0 +1,114 @@
# RaSCSI Web
## Setup local dev env
```bash
# Make a virtual env named venv
$ python3 -m venv venv
# Use that virtual env in this shell
$ source venv/bin/activate
# Install requirements
$ pip install -r requirements.txt
# Use mocks and a temp dir - start the web server
$ BASE_DIR=/tmp/images/ PATH=$PATH:`pwd`/mock/bin/ cd src && python3 web.py
```
### Mocks for local development
You may edit the files under `mock/bin` to simulate Linux command responses.
TODO: rascsi-web uses protobuf commands to send and receive data from rascsi.
A separate mocking solution will be needed for this interface.
### Static analysis with pylint
It is recommended to run pylint against new code to protect against bugs
and keep the code readable and maintainable.
The local pylint configuration lives in .pylintrc
In order for pylint to recognize venv libraries, the pylint-venv package is required.
```
sudo apt install pylint3
sudo pip install pylint-venv
source venv/bin/activate
pylint3 python_source_file.py
```
## Pushing to the Pi via git
Setup a bare repo on the rascsi
```
$ ssh pi@rascsi
$ mkdir /home/pi/dev.git && cd /home/pi/dev.git
$ git --bare init
Initialized empty Git repository in /home/pi/dev.git
```
Locally
```
$ cd ~/source/RASCSI
$ git remote add pi ssh://pi@rascsi/home/pi/dev.git
$ git push pi master
```
## Localizing the Web Interface
We use the Flask-Babel library and Flask/Jinja2 extension for i18n.
It uses the 'pybabel' command line tool for extracting and compiling localizations.
Activate the Python venv in src/web/ to use it:
```
$ cd src/web/
$ source venv/bin/activate
$ pybabel --help
```
To create a new localization, it needs to be added to the LANGAUGES constant in
web/settings.py. To localize messages coming from the RaSCSI backend, update also code in
raspberrypi/localizer.cpp in the RaSCSI C++ code.
Once this is done, follow the steps in the [Flask-Babel documentation](https://flask-babel.tkte.ch/#translating-applications)
to generate the messages.po for the new language.
Updating the strings in an existing messages.po is also covered above.
When you are ready to contribute new or updated localizations, use the same Gitflow Workflow as used for any code contributions to submit PRs against the develop branch.
### Working with PO files
See the [GNU gettext documentation](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) for an introduction to the PO file format.
We make heavy use of __python-format__ for formatting, for instance:
```
#: file_cmds.py:353
#, python-format
msgid "%(file_name)s downloaded to %(save_dir)s"
msgstr "Laddade ner %(file_name)s till %(save_dir)s"
```
There are also a few instances of formatting in JavaScript:
```
#: templates/index.html:381
msgid "Server responded with code: {{statusCode}}"
msgstr "Servern svarade med kod: {{statusCode}}"
```
And with html tags:
```
#: templates/index.html:304
#, python-format
msgid ""
"Emulates a SCSI DaynaPORT Ethernet Adapter. <a href=\"%(url)s\">Host "
"drivers and configuration required</a>."
msgstr ""
"Emulerar en SCSI DaynaPORT ethernet-adapter. <a href=\"%(url)s\">Kräver "
"drivrutiner och inställningar</a>."
```
### (Optional) See translation stats for a localization
Install the gettext package and use msgfmt to see the translation progress.
```
$ sudo apt install gettext
$ cd src/web/
$ msgfmt --statistics translations/sv/LC_MESSAGES/messages.po
215 translated messages, 1 untranslated message.
```
+3
View File
@@ -0,0 +1,3 @@
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Mock responses to rascsi-web
case $1 in
"show")
echo "rascsi_bridge 8000.dca632b05dd1 no eth0"
;;
**)
echo "default"
;;
esac
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Mock responses to rascsi-web
case $1 in
-n)
echo "logs $*"
;;
**)
echo "default"
;;
esac
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Mock responses to rascsi-web
case $1 in
is-active)
echo "is-active"
;;
**)
echo "default"
;;
esac
+10
View File
@@ -0,0 +1,10 @@
bjoern==3.1.0
click==7.1.2
Flask==2.0.1
itsdangerous==2.0.1
Jinja2==3.0.1
MarkupSafe==2.0.1
protobuf==3.17.3
requests==2.26.0
simplepam==0.1.5
flask_babel==2.0.0
+14
View File
@@ -0,0 +1,14 @@
<html>
<head>
<title>RaSCSI-Web is Starting</title>
<meta http-equiv="refresh" content="2">
</head>
</html>
<body>
<center>
<h1>RaSCSI-Web is Starting....</h1>
<h2>This page will automatically refresh.</h2>
<p>First boot and upgrades can take a second while resolving dependencies.</p>
<p>If you're seeing this page for over a minute please check the logs at <tt>sudo journalctl -f</tt></p>
</center>
</body>
@@ -0,0 +1,21 @@
# /etc/nginx/sites-available/default
# Simple proxy_pass for RaSCSI-web
server {
listen [::]:80 default_server;
listen 80 default_server;
location / {
proxy_pass http://127.0.0.1:8080;
}
# Large files
client_max_body_size 0;
proxy_read_timeout 1000;
proxy_connect_timeout 1000;
proxy_send_timeout 1000;
error_page 502 /502.html;
location = /502.html {
root /var/www/html/;
}
}
@@ -0,0 +1,12 @@
[Unit]
Description=RaSCSI-Web service
After=network.target
[Service]
Type=simple
Restart=always
ExecStart=/home/pi/RASCSI/python/web/start.sh
SyslogIdentifier=RASCSIWEB
[Install]
WantedBy=multi-user.target
+49
View File
@@ -0,0 +1,49 @@
"""
Module for RaSCSI device management utility methods
"""
def get_valid_scsi_ids(devices, reserved_ids):
"""
Takes a list of (dict)s devices, and list of (int)s reserved_ids.
Returns:
- (list) of (int)s valid_ids, which are the SCSI ids that are not reserved
- (int) recommended_id, which is the id that the Web UI should default to recommend
"""
occupied_ids = []
for device in devices:
occupied_ids.append(device["id"])
unoccupied_ids = [i for i in list(range(8)) if i not in reserved_ids + occupied_ids]
unoccupied_ids.sort()
valid_ids = [i for i in list(range(8)) if i not in reserved_ids]
valid_ids.sort(reverse=True)
if unoccupied_ids:
recommended_id = unoccupied_ids[-1]
else:
recommended_id = occupied_ids.pop(0)
return valid_ids, recommended_id
def sort_and_format_devices(devices):
"""
Takes a (list) of (dict)s devices and returns a (list) of (dict)s.
Sorts by SCSI ID acending (0 to 7).
For SCSI IDs where no device is attached, inject a (dict) with placeholder text.
"""
occupied_ids = []
for device in devices:
occupied_ids.append(device["id"])
formatted_devices = devices
# Add padding devices and sort the list
for i in range(8):
if i not in occupied_ids:
formatted_devices.append({"id": i, "device_type": "-", \
"status": "-", "file": "-", "product": "-"})
# Sort list of devices by id
formatted_devices.sort(key=lambda dic: str(dic["id"]))
return formatted_devices
+434
View File
@@ -0,0 +1,434 @@
[
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ22 (C) DEC",
"revision": "0A18",
"block_size": 512,
"size": 52445184,
"name": "DEC RZ22",
"file_type": "hds",
"description": "Page/Swap drive for satellite workstations",
"url": "http://lastin.dti.supsi.ch/VET/disks/EK-RZXXD-PS.pdf"
},
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ23 (C) DEC",
"revision": "0A18",
"block_size": 512,
"size": 104890368,
"name": "DEC RZ23",
"file_type": "hds",
"description": "Smallest usable drive for OpenVMS/VAX",
"url": "http://lastin.dti.supsi.ch/VET/disks/EK-RZXXD-PS.pdf"
},
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ24 (C) DEC",
"revision": "1D18",
"block_size": 512,
"size": 209813504,
"name": "DEC RZ24",
"file_type": "hds",
"description": "Smallest usable drive for OpenVMS/VAX + Motif",
"url": "http://lastin.dti.supsi.ch/VET/disks/EK-RZXXD-PS.pdf"
},
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ26L (C) DEC",
"revision": "440C",
"block_size": 512,
"size": 1050040320,
"name": "DEC RZ26L",
"file_type": "hds",
"description": "Largest bootable drive on VAXstation 3100",
"url": "http://lastin.dti.supsi.ch/VET/disks/EK-RZXXD-PS.pdf"
},
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ28M (C) DEC",
"revision": "0568",
"block_size": 512,
"size": 2104565760,
"name": "DEC RZ28M",
"file_type": "hds",
"description": "Typical 2GB data drive",
"url": "http://lastin.dti.supsi.ch/VET/disks/EK-RZXXD-PS.pdf"
},
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ29B (C) DEC",
"revision": "0014",
"block_size": 512,
"size": 4290600960,
"name": "DEC RZ29B",
"file_type": "hds",
"description": "Common Alpha server/workstation drive",
"url": "http://lastin.dti.supsi.ch/VET/disks/EK-RZXXD-PS.pdf"
},
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ55 (C) DEC",
"revision": "",
"block_size": 512,
"size": 332308480,
"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": "RZ56 (C) DEC",
"revision": "0400",
"block_size": 512,
"size": 665177088,
"name": "DEC RZ56",
"file_type": "hds",
"description": "Expansion 5.25 Drive",
"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,
"size": 1037203456,
"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,
"size": 9090874368,
"name": "DEC RZ59",
"file_type": "hds",
"description": "Largest recognized drive on OSF/1 3.x - 5.x",
"url": "https://stason.org/TULARC/pc/hard-drives-hdd/dec/RZ74-3570MB-5-25-FH-SCSI2-FAST.html"
},
{
"device_type": "SCHD",
"vendor": "DEC",
"product": "RZ74 (C) DEC",
"revision": "427H",
"block_size": 512,
"size": 3571904000,
"name": "DEC RZ74",
"file_type": "hds",
"description": "Largest recognized drive on Ultrix 4.4 - 4.5",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "HP",
"product": "C3010",
"revision": "6.0",
"block_size": 512,
"size": 2003032064,
"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": "1528-15MD1066702",
"revision": "DD24",
"block_size": 512,
"size": 1342304256,
"name": "Micropolis 1528",
"file_type": "hds",
"description": "Largest recognized drive on HP-UX 8.0",
"url": "https://parisc.wiki.kernel.org/images-parisc/0/06/980723ng.pdf"
},
{
"device_type": "SCHD",
"vendor": "MICROP",
"product": "1325",
"revision": "",
"block_size": 512,
"size": 70885376,
"name": "Micropolis 1325",
"file_type": "hds",
"description": "Largest predefined on SunOS 2",
"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,
"size": 666324480,
"name": "Micropolis 1588T",
"file_type": "hds",
"description": "Largest predefined on SunOS 3/4 (Sun-3)",
"url": "https://stason.org/TULARC/pc/hard-drives-hdd/micropolis/1588-667MB-5-25-FH-SCSI1-SE.html"
},
{
"device_type": "SCHD",
"vendor": "SEAGATE",
"product": "ST32430N SUN2.1G",
"revision": "0444",
"block_size": 512,
"size": 2149071360,
"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,
"size": 4290969600,
"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,
"size": 9056904192,
"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,
"size": 146789695488,
"name": "Seagate SUN146G",
"file_type": "hds",
"description": "Recommended for Solaris 2.4+",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "QUANTUM",
"product": "FIREBALL540S",
"revision": "",
"block_size": 512,
"size": 545400320,
"name": "Quantum Fireball 540S",
"file_type": "hds",
"description": "Recommended for older Macintosh systems.",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "MINSCRIB",
"product": " M8425 - SCSI",
"revision": "209A",
"block_size": 512,
"size": 20994048,
"name": "Miniscribe M8425",
"file_type": "hds",
"description": "Recognized by unpatched Apple HD SC Setup.",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "QUANTUM",
"product": "FIREBALL ST4.3S",
"revision": "0F0C",
"block_size": 512,
"size": 4337270784,
"name": "Quantum Fireball ST4.3S",
"file_type": "hds",
"description": "Recommended for MacOS 8.1 or later.",
"url": ""
},
{
"device_type": "SCHD",
"vendor": "SEAGATE",
"product": "ST32550N",
"revision": "0019",
"block_size": 512,
"size": 2147357696,
"name": "Seagate Barracuda 2GB",
"file_type": "hds",
"description": "2GB is the largest partition size of HFS on older Macs.",
"url": ""
},
{
"device_type": "SCRM",
"vendor": "DEC",
"product": "RX23 (C) DEC",
"revision": "0054",
"block_size": 512,
"size": 1474560,
"name": "DEC RX23",
"file_type": "hdr",
"description": "SCSI Floppy Drive 1.44MB",
"url": "https://www.netbsd.org/docs/Hardware/Machines/DEC/vax/storage.html#storage:rx23"
},
{
"device_type": "SCRM",
"vendor": "DEC",
"product": "RX26 (C) DEC",
"revision": "0054",
"block_size": 512,
"size": 2949120,
"name": "DEC RX26",
"file_type": "hdr",
"description": "SCSI Floppy Drive 2.88MB",
"url": "https://www.netbsd.org/docs/Hardware/Machines/DEC/vax/storage.html#storage:rx26"
},
{
"device_type": "SCRM",
"vendor": "DEC",
"product": "RX33 (C) DEC",
"revision": "0054",
"block_size": 512,
"size": 1228800,
"name": "DEC RX33",
"file_type": "hdr",
"description": "SCSI Floppy Drive 1.2MB",
"url": "https://www.netbsd.org/docs/Hardware/Machines/DEC/vax/storage.html#storage:rx33"
},
{
"device_type": "SCRM",
"vendor": "IOMEGA",
"product": "ZIP 100",
"revision": "D.13",
"block_size": 512,
"size": 100663296,
"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,
"size": 250640384,
"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,
"size": 2001731584,
"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": "IOMEGA",
"product": "Io20S *F",
"revision": "PP33",
"block_size": 512,
"size": 1474560,
"name": "Iomega Floptical 1.44MB",
"file_type": "hdr",
"description": "Iomega Floptical 1.44MB SCSI drive",
"url": "https://github.com/akuker/RASCSI/wiki/Iomega-Floptical"
},
{
"device_type": "SCRM",
"vendor": "SYQUEST",
"product": "SQ5110C",
"revision": "4AA0",
"block_size": 512,
"size": 88809472,
"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,
"size": 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,
"size": 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,
"size": 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,
"size": null,
"name": "DEC RRD42",
"file_type": null,
"description": "Boots DECstations and VAXstations. Use only with workstations of this vintage.",
"url": ""
},
{
"device_type": "SCCD",
"vendor": "MATSHITA",
"product": "CD-ROM CR-8005 ",
"revision": "1.0k",
"block_size": 2048,
"size": null,
"name": "Apple CD 600e",
"file_type": null,
"description": "Emulates Apple CD ROM drive for use with Macintosh computers.",
"url": ""
}
]
+538
View File
@@ -0,0 +1,538 @@
"""
Module for methods reading from and writing to the file system
"""
import os
import logging
from pathlib import PurePath
from flask import current_app, session
from flask_babel import _
from ractl_cmds import (
get_server_info,
get_reserved_ids,
attach_image,
detach_all,
list_devices,
reserve_scsi_ids,
)
from pi_cmds import run_async
from socket_cmds import send_pb_command
from settings import CFG_DIR, CONFIG_FILE_SUFFIX, PROPERTIES_SUFFIX, RESERVATIONS
import rascsi_interface_pb2 as proto
def list_files(file_types, dir_path):
"""
Takes a (list) or (tuple) of (str) file_types - e.g. ('hda', 'hds')
Returns (list) of (list)s files_list:
index 0 is (str) file name and index 1 is (int) size in bytes
"""
files_list = []
for path, dirs, files in os.walk(dir_path):
# Only list selected file types
files = [f for f in files if f.lower().endswith(file_types)]
files_list.extend(
[
(
file,
os.path.getsize(os.path.join(path, file))
)
for file in files
]
)
return files_list
def list_config_files():
"""
Finds fils with file ending CONFIG_FILE_SUFFIX in CFG_DIR.
Returns a (list) of (str) files_list
"""
files_list = []
for root, dirs, files in os.walk(CFG_DIR):
for file in files:
if file.endswith("." + CONFIG_FILE_SUFFIX):
files_list.append(file)
return files_list
def list_images():
"""
Sends a IMAGE_FILES_INFO command to the server
Returns a (dict) with (bool) status, (str) msg, and (list) of (dict)s files
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
# Get a list of all *.properties files in CFG_DIR
prop_data = list_files(PROPERTIES_SUFFIX, CFG_DIR)
prop_files = [PurePath(x[0]).stem for x in prop_data]
from zipfile import ZipFile, is_zipfile
server_info = get_server_info()
files = []
for file in result.image_files_info.image_files:
# Add properties meta data for the image, if applicable
if file.name in prop_files:
process = read_drive_properties(f"{CFG_DIR}/{file.name}.{PROPERTIES_SUFFIX}")
prop = process["conf"]
else:
prop = False
if file.name.lower().endswith(".zip"):
zip_path = f"{server_info['image_dir']}/{file.name}"
if is_zipfile(zip_path):
zipfile = ZipFile(zip_path)
# Get a list of (str) containing all zipfile members
zip_members = zipfile.namelist()
# Strip out directories from the list
zip_members = [x for x in zip_members if not x.endswith("/")]
else:
logging.warning("%s is an invalid zip file", zip_path)
zip_members = False
else:
zip_members = False
size_mb = "{:,.1f}".format(file.size / 1024 / 1024)
dtype = proto.PbDeviceType.Name(file.type)
files.append({
"name": file.name,
"size": file.size,
"size_mb": size_mb,
"detected_type": dtype,
"prop": prop,
"zip_members": zip_members,
})
return {"status": result.status, "msg": result.msg, "files": files}
def create_new_image(file_name, file_type, size):
"""
Takes (str) file_name, (str) file_type, and (int) size
Sends a CREATE_IMAGE command to the server
Returns (dict) with (bool) status and (str) msg
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CREATE_IMAGE
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
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_image(file_name):
"""
Takes (str) file_name
Sends a DELETE_IMAGE command to the server
Returns (dict) with (bool) status and (str) msg
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DELETE_IMAGE
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
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 rename_image(file_name, new_file_name):
"""
Takes (str) file_name, (str) new_file_name
Sends a RENAME_IMAGE command to the server
Returns (dict) with (bool) status and (str) msg
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.RENAME_IMAGE
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
command.params["from"] = file_name
command.params["to"] = new_file_name
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def delete_file(file_path):
"""
Takes (str) file_path with the full path to the file to delete
Returns (dict) with (bool) status and (str) msg
"""
if os.path.exists(file_path):
os.remove(file_path)
return {
"status": True,
"msg": _(u"File deleted: %(file_path)s", file_path=file_path),
}
return {
"status": False,
"msg": _(u"File to delete not found: %(file_path)s", file_path=file_path),
}
def rename_file(file_path, target_path):
"""
Takes (str) file_path and (str) target_path
Returns (dict) with (bool) status and (str) msg
"""
if os.path.exists(PurePath(target_path).parent):
os.rename(file_path, target_path)
return {
"status": True,
"msg": _(u"File moved to: %(target_path)s", target_path=target_path),
}
return {
"status": False,
"msg": _(u"Unable to move file to: %(target_path)s", target_path=target_path),
}
def unzip_file(file_name, member=False, members=False):
"""
Takes (str) file_name, optional (str) member, optional (list) of (str) members
file_name is the name of the zip file to unzip
member is the full path to the particular file in the zip file to unzip
members contains all of the full paths to each of the zip archive members
Returns (dict) with (boolean) status and (list of str) msg
"""
from asyncio import run
server_info = get_server_info()
prop_flag = False
if not member:
unzip_proc = run(run_async(
f"unzip -d {server_info['image_dir']} -n -j "
f"{server_info['image_dir']}/{file_name}"
))
if members:
for path in members:
if path.endswith(PROPERTIES_SUFFIX):
name = PurePath(path).name
rename_file(f"{server_info['image_dir']}/{name}", f"{CFG_DIR}/{name}")
prop_flag = True
else:
from re import escape
member = escape(member)
unzip_proc = run(run_async(
f"unzip -d {server_info['image_dir']} -n -j "
f"{server_info['image_dir']}/{file_name} {member}"
))
# Attempt to unzip a properties file in the same archive dir
unzip_prop = run(run_async(
f"unzip -d {CFG_DIR} -n -j "
f"{server_info['image_dir']}/{file_name} {member}.{PROPERTIES_SUFFIX}"
))
if unzip_prop["returncode"] == 0:
prop_flag = True
if unzip_proc["returncode"] != 0:
logging.warning("Unzipping failed: %s", unzip_proc["stderr"])
return {"status": False, "msg": unzip_proc["stderr"]}
from re import findall
unzipped = findall(
"(?:inflating|extracting):(.+)\n",
unzip_proc["stdout"]
)
return {"status": True, "msg": unzipped, "prop_flag": prop_flag}
def download_file_to_iso(url, *iso_args):
"""
Takes (str) url and one or more (str) *iso_args
Returns (dict) with (bool) status and (str) msg
"""
from time import time
from subprocess import run, CalledProcessError
import asyncio
server_info = get_server_info()
file_name = PurePath(url).name
tmp_ts = int(time())
tmp_dir = "/tmp/" + str(tmp_ts) + "/"
os.mkdir(tmp_dir)
tmp_full_path = tmp_dir + file_name
iso_filename = f"{server_info['image_dir']}/{file_name}.iso"
req_proc = download_to_dir(url, tmp_dir, file_name)
if not req_proc["status"]:
return {"status": False, "msg": req_proc["msg"]}
from zipfile import is_zipfile, ZipFile
if is_zipfile(tmp_full_path):
if "XtraStuf.mac" in str(ZipFile(tmp_full_path).namelist()):
logging.info("MacZip file format detected. Will not unzip to retain resource fork.")
else:
logging.info(
"%s is a zipfile! Will attempt to unzip and store the resulting files.",
tmp_full_path,
)
unzip_proc = asyncio.run(run_async(
f"unzip -d {tmp_dir} -n {tmp_full_path}"
))
if not unzip_proc["returncode"]:
logging.info(
"%s was successfully unzipped. Deleting the zipfile.",
tmp_full_path,
)
delete_file(tmp_full_path)
try:
run(
[
"genisoimage",
*iso_args,
"-o",
iso_filename,
tmp_dir,
],
capture_output=True,
check=True,
)
except CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
return {"status": False, "msg": error.stderr.decode("utf-8")}
return {
"status": True,
"msg": _(
u"Created CD-ROM ISO image with arguments \"%(value)s\"",
value=" ".join(iso_args),
),
"file_name": iso_filename,
}
def download_to_dir(url, save_dir, file_name):
"""
Takes (str) url, (str) save_dir, (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
import requests
logging.info("Making a request to download %s", url)
try:
with requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"}) as req:
req.raise_for_status()
with open(f"{save_dir}/{file_name}", "wb") as download:
for chunk in req.iter_content(chunk_size=8192):
download.write(chunk)
except requests.exceptions.RequestException as error:
logging.warning("Request failed: %s", str(error))
return {"status": False, "msg": str(error)}
logging.info("Response encoding: %s", req.encoding)
logging.info("Response content-type: %s", req.headers["content-type"])
logging.info("Response status code: %s", req.status_code)
return {
"status": True,
"msg": _(
u"%(file_name)s downloaded to %(save_dir)s",
file_name=file_name,
save_dir=save_dir,
),
}
def write_config(file_name):
"""
Takes (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
from json import dump
file_name = f"{CFG_DIR}/{file_name}"
try:
with open(file_name, "w") as json_file:
version = get_server_info()["version"]
devices = list_devices()["device_list"]
for device in devices:
# Remove keys that we don't want to store in the file
del device["status"]
del device["file"]
# It's cleaner not to store an empty parameter for every device without media
if device["image"] == "":
device["image"] = None
# RaSCSI product names will be generated on the fly by RaSCSI
if device["vendor"] == "RaSCSI":
device["vendor"] = device["product"] = device["revision"] = None
# A block size of 0 is how RaSCSI indicates N/A for block size
if device["block_size"] == 0:
device["block_size"] = None
# Convert to a data type that can be serialized
device["params"] = dict(device["params"])
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": _(u"Saved configuration file to %(file_name)s", file_name=file_name),
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
delete_file(file_name)
return {"status": False, "msg": str(error)}
except:
logging.error("Could not write to file: %s", file_name)
delete_file(file_name)
return {
"status": False,
"msg": _(u"Could not write to file: %(file_name)s", file_name=file_name),
}
def read_config(file_name):
"""
Takes (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
from json import load
file_name = f"{CFG_DIR}/{file_name}"
try:
with open(file_name) as json_file:
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": _(u"Invalid configuration file format")}
return {
"status": True,
"msg": _(u"Loaded configurations from: %(file_name)s", file_name=file_name),
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
return {"status": False, "msg": str(error)}
except:
logging.error("Could not read file: %s", file_name)
return {
"status": False,
"msg": _(u"Could not read configuration file: %(file_name)s", file_name=file_name),
}
def write_drive_properties(file_name, conf):
"""
Writes a drive property configuration file to the config dir.
Takes file name base (str) and (list of dicts) conf as arguments
Returns (dict) with (bool) status and (str) msg
"""
from json import dump
file_path = f"{CFG_DIR}/{file_name}"
try:
with open(file_path, "w") as json_file:
dump(conf, json_file, indent=4)
return {
"status": True,
"msg": _(u"Created properties file: %(file_path)s", file_path=file_path),
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
delete_file(file_path)
return {"status": False, "msg": str(error)}
except:
logging.error("Could not write to file: %s", file_path)
delete_file(file_path)
return {
"status": False,
"msg": _(u"Could not write to properties file: %(file_path)s", file_path=file_path),
}
def read_drive_properties(file_path):
"""
Reads drive properties from json formatted file.
Takes (str) file_path as argument.
Returns (dict) with (bool) status, (str) msg, (dict) conf
"""
from json import load
try:
with open(file_path) as json_file:
conf = load(json_file)
return {
"status": True,
"msg": _(u"Read properties from file: %(file_path)s", file_path=file_path),
"conf": conf,
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
return {"status": False, "msg": str(error)}
except:
logging.error("Could not read file: %s", file_path)
return {
"status": False,
"msg": _(u"Could not read properties from file: %(file_path)s", file_path=file_path),
}
+181
View File
@@ -0,0 +1,181 @@
"""
Module for methods controlling and getting information about the Pi's Linux system
"""
import subprocess
import asyncio
import logging
from flask_babel import _
from settings import AUTH_GROUP
def running_env():
"""
Returns (str) git and (str) env
git contains the git hash of the checked out code
env is the various system information where this app is running
"""
try:
ra_git_version = (
subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
ra_git_version = ""
try:
pi_version = (
subprocess.run(
["uname", "-a"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
pi_version = "Unknown"
return {"git": ra_git_version, "env": pi_version}
def running_proc(daemon):
"""
Takes (str) daemon
Returns (int) proc, which is the number of processes currently running
"""
try:
processes = (
subprocess.run(
["ps", "aux"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
processes = ""
from re import findall
matching_processes = findall(daemon, processes)
return len(matching_processes)
def is_bridge_setup():
"""
Returns (bool) True if the rascsi_bridge network interface exists
"""
try:
bridges = (
subprocess.run(
["brctl", "show"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
bridges = ""
if "rascsi_bridge" in bridges:
return True
return False
def disk_space():
"""
Returns a (dict) with (int) total (int) used (int) free
This is the disk space information of the volume where this app is running
"""
from shutil import disk_usage
total, used, free = disk_usage(__file__)
return {"total": total, "used": used, "free": free}
def get_ip_address():
"""
Use a mock socket connection to identify the Pi's IP address
"""
from socket import socket, AF_INET, SOCK_DGRAM
sock = socket(AF_INET, SOCK_DGRAM)
try:
# mock ip address; doesn't have to be reachable
sock.connect(('10.255.255.255', 1))
ip_addr = sock.getsockname()[0]
except Exception:
ip_addr = '127.0.0.1'
finally:
sock.close()
return ip_addr
def introspect_file(file_path, re_term):
"""
Takes a (str) file_path and (str) re_term in regex format
Will introspect file_path for the existance of re_term
and return True if found, False if not found
"""
from re import match
try:
ifile = open(file_path, "r")
except:
return False
for line in ifile:
if match(re_term, line):
return True
return False
async def run_async(cmd):
"""
Takes (str) cmd with the shell command to execute
Executes shell command and captures output
Returns (dict) with (int) returncode, (str) stdout, (str) stderr
"""
proc = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
stdout, stderr = await proc.communicate()
logging.info("Executed command \"%s\" with status code %d", cmd, proc.returncode)
if stdout:
stdout = stdout.decode()
logging.info("stdout: %s", stdout)
if stderr:
stderr = stderr.decode()
logging.info("stderr: %s", stderr)
return {"returncode": proc.returncode, "stdout": stdout, "stderr": stderr}
def auth_active():
"""
Inspects if the group defined in AUTH_GROUP exists on the system.
If it exists, tell the webapp to enable authentication.
Returns a (dict) with (bool) status and (str) msg
"""
from grp import getgrall
groups = [g.gr_name for g in getgrall()]
if AUTH_GROUP in groups:
return {
"status": True,
"msg": _(u"You must log in to use this function"),
}
return {"status": False, "msg": ""}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/pwa/ms-icon-70x70.png"/><square150x150logo src="/pwa/ms-icon-150x150.png"/><square310x310logo src="/pwa/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+41
View File
@@ -0,0 +1,41 @@
{
"name": "RaSCSI",
"icons": [
{
"src": "\/pwa\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/pwa\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/pwa\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/pwa\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/pwa\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/pwa\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

+465
View File
@@ -0,0 +1,465 @@
"""
Module for commands sent to the RaSCSI backend service.
"""
from flask import current_app, session
from flask_babel import _
import rascsi_interface_pb2 as proto
from settings import REMOVABLE_DEVICE_TYPES
from socket_cmds import send_pb_command
def get_server_info():
"""
Sends a SERVER_INFO command to the server.
Returns a dict with:
- (bool) status
- (str) version (RaSCSI version number)
- (list) of (str) log_levels (the log levels RaSCSI supports)
- (str) current_log_level
- (list) of (int) reserved_ids
- (str) image_dir, path to the default images directory
- (int) scan_depth, the current images directory scan depth
- 5 distinct (list)s of (str)s with file endings recognized by RaSCSI
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.SERVER_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
version = str(result.server_info.version_info.major_version) + "." +\
str(result.server_info.version_info.minor_version) + "." +\
str(result.server_info.version_info.patch_version)
log_levels = result.server_info.log_level_info.log_levels
current_log_level = result.server_info.log_level_info.current_log_level
reserved_ids = list(result.server_info.reserved_ids_info.ids)
image_dir = result.server_info.image_files_info.default_image_folder
scan_depth = result.server_info.image_files_info.depth
# Creates lists of file endings recognized by RaSCSI
mappings = result.server_info.mapping_info.mapping
sahd = []
schd = []
scrm = []
scmo = []
sccd = []
for dtype in mappings:
if mappings[dtype] == proto.PbDeviceType.SAHD:
sahd.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCHD:
schd.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCRM:
scrm.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCMO:
scmo.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCCD:
sccd.append(dtype)
return {
"status": result.status,
"version": version,
"log_levels": log_levels,
"current_log_level": current_log_level,
"reserved_ids": reserved_ids,
"image_dir": image_dir,
"scan_depth": scan_depth,
"sahd": sahd,
"schd": schd,
"scrm": scrm,
"scmo": scmo,
"sccd": sccd,
}
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
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
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.
Returns a dict with:
- (bool) status
- (list) of (str) ifs (network interfaces detected by RaSCSI)
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.NETWORK_INTERFACES_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
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 get_device_types():
"""
Sends a DEVICE_TYPES_INFO command to the server.
Returns a dict with:
- (bool) status
- (list) of (str) device_types (device types that RaSCSI supports, ex. SCHD, SCCD, etc)
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICE_TYPES_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
device_types = []
for prop in result.device_types_info.properties:
device_types.append(proto.PbDeviceType.Name(prop.type))
return {"status": result.status, "device_types": device_types}
def get_image_files_info():
"""
Sends a DEFAULT_IMAGE_FILES_INFO command to the server.
Returns a dict with:
- (bool) status
- (str) images_dir, path to images dir
- (list) of (str) image_files
- (int) scan_depth, the current scan depth
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
images_dir = result.image_files_info.default_image_folder
image_files = result.image_files_info.image_files
scan_depth = result.image_files_info.depth
return {
"status": result.status,
"images_dir": images_dir,
"image_files": image_files,
"scan_depth": scan_depth,
}
def attach_image(scsi_id, **kwargs):
"""
Takes (int) scsi_id and kwargs containing 0 or more device properties
If the current attached device is a removable device wihout media inserted,
this sends a INJECT command to the server.
If there is no currently attached device, this sends the ATTACH command to the server.
Returns (bool) status and (str) msg
"""
command = proto.PbCommand()
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if "device_type" in kwargs.keys():
if kwargs["device_type"] not in [None, ""]:
devices.type = proto.PbDeviceType.Value(str(kwargs["device_type"]))
if "unit" in kwargs.keys():
if kwargs["unit"] not in [None, ""]:
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
device_type = kwargs.get("device_type", None)
currently_attached = list_devices(scsi_id, kwargs.get("unit"))["device_list"]
if currently_attached:
current_type = currently_attached[0]["device_type"]
else:
current_type = None
if device_type in REMOVABLE_DEVICE_TYPES and current_type in REMOVABLE_DEVICE_TYPES:
if current_type != device_type:
return {
"status": False,
"msg": _(
u"Cannot insert an image for %(device_type)s into a "
u"%(current_device_type)s device",
device_type=device_type,
current_device_type=current_type
),
}
command.operation = proto.PbOperation.INSERT
# Handling attaching a new device
else:
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"] is not None:
devices.vendor = kwargs["vendor"]
if "product" in kwargs.keys():
if kwargs["product"] is not None:
devices.product = kwargs["product"]
if "revision" in kwargs.keys():
if kwargs["revision"] is not None:
devices.revision = kwargs["revision"]
if "block_size" in kwargs.keys():
if kwargs["block_size"] not in [None, ""]:
devices.block_size = int(kwargs["block_size"])
command.devices.append(devices)
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, unit=None):
"""
Takes (int) scsi_id and optional (int) unit.
Sends a DETACH command to the server.
Returns (bool) status and (str) msg.
"""
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if unit is not None:
devices.unit = int(unit)
command = proto.PbCommand()
command.operation = proto.PbOperation.DETACH
command.devices.append(devices)
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def detach_all():
"""
Sends a DETACH_ALL command to the server.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DETACH_ALL
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def eject_by_id(scsi_id, unit=None):
"""
Takes (int) scsi_id and optional (int) unit.
Sends an EJECT command to the server.
Returns (bool) status and (str) msg.
"""
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if unit is not None:
devices.unit = int(unit)
command = proto.PbCommand()
command.operation = proto.PbOperation.EJECT
command.devices.append(devices)
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
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, unit=None):
"""
Takes optional (int) scsi_id and optional (int) unit.
Sends a DEVICES_INFO command to the server.
If no scsi_id is provided, returns a (list) of (dict)s of all attached devices.
If scsi_id is is provided, returns a (list) of one (dict) for the given device.
If no attached device is found, returns an empty (list).
Returns (bool) status, (list) of dicts device_list
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICES_INFO
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
# If method is called with scsi_id parameter, return the info on those devices
# Otherwise, return the info on all attached devices
if scsi_id is not None:
device = proto.PbDeviceDefinition()
device.id = int(scsi_id)
if unit is not None:
device.unit = int(unit)
command.devices.append(device)
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
device_list = []
# Return an empty (list) if no devices are attached
if not result.devices_info.devices:
return {"status": False, "device_list": []}
image_files_info = get_image_files_info()
i = 0
while i < len(result.devices_info.devices):
did = result.devices_info.devices[i].id
dunit = result.devices_info.devices[i].unit
dtype = proto.PbDeviceType.Name(result.devices_info.devices[i].type)
dstat = result.devices_info.devices[i].status
dprop = result.devices_info.devices[i].properties
# Building the status string
dstat_msg = []
if dprop.read_only:
dstat_msg.append("Read-Only")
if dstat.protected and dprop.protectable:
dstat_msg.append("Write-Protected")
if dstat.removed and dprop.removable:
dstat_msg.append("No Media")
if dstat.locked and dprop.lockable:
dstat_msg.append("Locked")
dpath = result.devices_info.devices[i].file.name
dfile = dpath.replace(image_files_info["images_dir"] + "/", "")
dparam = result.devices_info.devices[i].params
dven = result.devices_info.devices[i].vendor
dprod = result.devices_info.devices[i].product
drev = result.devices_info.devices[i].revision
dblock = result.devices_info.devices[i].block_size
dsize = int(result.devices_info.devices[i].block_count) * int(dblock)
device_list.append({
"id": did,
"unit": dunit,
"device_type": dtype,
"status": ", ".join(dstat_msg),
"image": dpath,
"file": dfile,
"params": dparam,
"vendor": dven,
"product": dprod,
"revision": drev,
"block_size": dblock,
"size": dsize,
})
i += 1
return {"status": result.status, "msg": result.msg, "device_list": device_list}
def reserve_scsi_ids(reserved_scsi_ids):
"""
Sends the RESERVE_IDS command to the server to reserve SCSI IDs.
Takes a (list) of (str) as argument.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.RESERVE_IDS
command.params["ids"] = ",".join(reserved_scsi_ids)
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
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.
Takes (str) log_level as an argument.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.LOG_LEVEL
command.params["level"] = str(log_level)
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def shutdown_pi(mode):
"""
Sends a SHUT_DOWN command to the server.
Takes (str) mode as an argument.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.SHUT_DOWN
command.params["mode"] = str(mode)
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def is_token_auth():
"""
Sends a CHECK_AUTHENTICATION command to the server.
Tells you whether RaSCSI backend is protected by a token password or not.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CHECK_AUTHENTICATION
command.params["token"] = current_app.config["TOKEN"]
if "language" in session.keys():
command.params["locale"] = session["language"]
data = send_pb_command(command.SerializeToString())
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
+35
View File
@@ -0,0 +1,35 @@
"""
Constant definitions used by other modules
"""
from os import getenv, getcwd
WEB_DIR = getcwd()
# There may be a more elegant way to get the HOME dir of the user that installed RaSCSI
HOME_DIR = "/".join(WEB_DIR.split("/")[0:3])
CFG_DIR = f"{HOME_DIR}/.config/rascsi"
AFP_DIR = f"{HOME_DIR}/afpshare"
MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", str(1024 * 1024 * 1024 * 4)) # 4gb
ARCHIVE_FILE_SUFFIX = "zip"
CONFIG_FILE_SUFFIX = "json"
# File ending used for drive properties files
PROPERTIES_SUFFIX = "properties"
# The file name of the default config file that loads when rascsi-web starts
DEFAULT_CONFIG = f"default.{CONFIG_FILE_SUFFIX}"
# File containing canonical drive properties
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)]
# The user group that is used for webapp authentication
AUTH_GROUP = "rascsi"
# The language locales supported by RaSCSI
LANGUAGES = ["en", "de", "sv", "fr", "es"]
+100
View File
@@ -0,0 +1,100 @@
"""
Module for sending and receiving data over a socket connection with the RaSCSI backend
"""
import logging
from time import sleep
from flask import abort
from flask_babel import _
def send_pb_command(payload):
"""
Takes a (str) containing a serialized protobuf as argument.
Establishes a socket connection with RaSCSI.
"""
# Host and port number where rascsi is listening for socket connections
host = 'localhost'
port = 6868
counter = 0
tries = 20
error_msg = ""
import socket
while counter < tries:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((host, port))
return send_over_socket(sock, payload)
except socket.error as error:
counter += 1
logging.warning("The RaSCSI service is not responding - attempt %s/%s",
str(counter), str(tries))
error_msg = str(error)
sleep(0.2)
logging.error(error_msg)
# After failing all attempts, throw a 404 error
abort(404, _(
u"The RaSCSI Web Interface failed to connect to RaSCSI at %(host)s:%(port)s "
u"with error: %(error_msg)s. The RaSCSI process is not running or may have crashed.",
host=host, port=port, error_msg=error_msg,
)
)
def send_over_socket(sock, payload):
"""
Takes a socket object and (str) payload with serialized protobuf.
Sends payload to RaSCSI over socket and captures the response.
Tries to extract and interpret the protobuf header to get response size.
Reads data from socket in 2048 bytes chunks until all data is received.
"""
from struct import pack, unpack
# Sending the magic word "RASCSI" to authenticate with the server
sock.send(b"RASCSI")
# Prepending a little endian 32bit header with the message size
sock.send(pack("<i", len(payload)))
sock.send(payload)
# Receive the first 4 bytes to get the response header
response = sock.recv(4)
if len(response) >= 4:
# Extracting the response header to get the length of the response message
response_length = unpack("<i", response)[0]
# Reading in chunks, to handle a case where the response message is very large
chunks = []
bytes_recvd = 0
while bytes_recvd < response_length:
chunk = sock.recv(min(response_length - bytes_recvd, 2048))
if chunk == b'':
logging.error(
"Read an empty chunk from the socket. "
"Socket connection has dropped unexpectedly. "
"RaSCSI may have crashed."
)
abort(
503, _(
u"The RaSCSI Web Interface lost connection to RaSCSI. "
u"Please go back and try again. "
u"If the issue persists, please report a bug."
)
)
chunks.append(chunk)
bytes_recvd = bytes_recvd + len(chunk)
response_message = b''.join(chunks)
return response_message
logging.error(
"The response from RaSCSI did not contain a protobuf header. "
"RaSCSI may have crashed."
)
abort(
500, _(
u"The RaSCSI Web Interface did not get a valid response from RaSCSI. "
u"Please go back and try again. "
u"If the issue persists, please report a bug."
)
)
+114
View File
@@ -0,0 +1,114 @@
body {
color: black;
background-color: white;
font-family: Arial, Helvetica, sans-serif;
text-decoration:none;
}
h1 {
color: white;
font-size:20px;
background-color:black;
}
h2 {
color: black;
font-size:16px;
margin: 0px;
}
a {
text-decoration: none;
}
form {
display: inline;
}
table, tr, td {
border: 1px solid black;
border-collapse:collapse;
margin: none;
}
.error {
color: white;
font-size:20px;
background-color:red;
}
.message {
color: white;
font-size:20px;
background-color:green;
}
td.inactive {
text-align:center;
background-color:tan;
}
summary.heading {
color: black;
font-size: large;
font-weight: bold;
margin: 0px;
}
.dropzone, .dropzone * {
box-sizing: border-box;
}
.dropzone {
position: relative;
}
.dropzone .dz-preview {
position: relative;
display: inline-block;
width: 120px;
margin: .5em;
}
.dropzone .dz-preview .dz-progress {
display: block;
height: 15px;
border: 1px solid #aaa;
}
.dropzone .dz-preview .dz-progress .dz-upload {
display: block;
height: 100%;
width: 0;
background: green;
}
.dropzone .dz-preview .dz-error-message {
color: red;
display: none;
}
.dropzone .dz-preview.dz-error .dz-error-message {
display: block;
}
.dropzone .dz-preview.dz-error .dz-error-mark {
display: block;
filter: drop-shadow(0px 0px 2px red);
}
.dropzone .dz-preview.dz-success .dz-success-mark {
display: block;
filter: drop-shadow(0px 0px 2px green);
}
.dropzone .dz-preview .dz-error-mark, .dropzone .dz-preview .dz-success-mark {
position: absolute;
display: none;
left: 30px;
top: 30px;
width: 54px;
height: 58px;
left: 50%;
margin-left: -27px;
}
+91
View File
@@ -0,0 +1,91 @@
<!doctype html>
<html>
<head>
<title>{{ _("RaSCSI Control Page") }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="apple-touch-icon" sizes="57x57" href="/pwa/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/pwa/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/pwa/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/pwa/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/pwa/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/pwa/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/pwa/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/pwa/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/pwa/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/pwa/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/pwa/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/pwa/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/pwa/favicon-16x16.png">
<link rel="manifest" href="/pwa/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/pwa/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script type="application/javascript">
var processNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class='message'>" + Notification + "{{ _(" This process may take a while, and will continue in the background if you navigate away from this page.") }}</div>";
window.scrollTo(0,0);
}
var shutdownNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class='message'>" + Notification + "{{ _(" The Web Interface will become unresponsive momentarily. Reload this page after the Pi has started up again.") }}</div>";
window.scrollTo(0,0);
}
</script>
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
</head>
<body>
<div class="content">
<div class="header">
{% if auth_active %}
{% if username %}
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">{{ _("Logged in as <em>%(username)s</em>", username=username) }} &#8211; <a href="/logout">{{ _("Log Out") }}</a></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;">
<form method="POST" action="/login">
<div>{{ _("Log In to Use Web Interface") }}</div>
<input type="text" name="username" placeholder="{{ _("Username") }}">
<input type="password" name="password" placeholder="{{ _("Password") }}">
<input type="submit" value="Login">
</form>
</span>
{% endif %}
{% else %}
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">{{ _("Web Interface Authentication Disabled") }} &#8211; {{ _("See <a href=\"%(url)s\" target=\"_blank\">Wiki</a> for more information", url="https://github.com/akuker/RASCSI/wiki/Web-Interface#enable-authentication") }}</span>
{% endif %}
<table width="100%">
<tbody>
<tr style="background-color: black;">
<td style="background-color: black;">
<a href="http://github.com/akuker/RASCSI" target="_blank">
<h1>RaSCSI - 68kmla Edition</h1>
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="flash" id="flash">
{% for category, message in get_flashed_messages(with_categories=true) %}
{% if category == "stdout" or category == "stderr" %}
<pre>{{ message }}</pre>
{% else %}
<div class="{{ category }}">{{ message }}</div>
{% endif %}
{% endfor %}
</div>
<div class="content">
{% block content %}{% endblock content %}
</div>
<div class="footer">
<center><tt>{{ _("RaSCSI version: ") }}<strong>{{ version }} <a href="https://github.com/akuker/RASCSI/commit/{{ running_env['git'] }}" target="_blank">{{ running_env["git"][:7] }}</a></strong></tt></center>
<center><tt>{{ _("Pi environment: ") }}{{ running_env["env"] }}</tt></center>
</div>
</div>
</body>
+141
View File
@@ -0,0 +1,141 @@
{% extends "base.html" %}
{% block content %}
<p><a href="/">{{ _("Cancel") }}</a></p>
<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>
<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.secure_name }}" required />.{{ 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.secure_name }}" required />.{{ rm.file_type }}
<input type="submit" value="{{ _("Create") }}" />
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><small>{{ _("%(disk_space)s MB disk space remaining on the Pi", disk_space=free_disk) }}</small></p>
<p><a href="/">{{ _("Cancel") }}</a></p>
{% endblock content %}
+685
View File
@@ -0,0 +1,685 @@
{% extends "base.html" %}
{% block content %}
<details>
<summary class="heading">
{{ _("Current RaSCSI Configuration") }}
</summary>
<ul>
<li>{{ _("Displays the currently attached devices for each available SCSI ID.") }}</li>
<li>{{ _("Save and load device configurations, stored as json files in <tt>%(config_dir)s</tt>", config_dir=CFG_DIR) }}</tt></li>
<li>{{ _("To have a particular device configuration load when RaSCSI starts, save it as <em>default</em>.") }}</li>
</ul>
</details>
<p><form action="/config/load" method="post">
<select name="name" required="" width="14">
{% if config_files %}
{% for config in config_files %}
<option value="{{ config }}">
{{ config.replace(".json", '') }}
</option>
{% endfor %}
{% else %}
<option disabled>
{{ _("No saved configurations") }}
</option>
{% endif %}
</select>
<input name="load" type="submit" value="{{ _("Load") }}" onclick="return confirm('{{ _("Detach all current device and Load configuration?") }}')">
<input name="delete" type="submit" value="{{ _("Delete") }}" onclick="return confirm('{{ _("Delete configuration file?") }}')">
</form></p>
<p><form action="/config/save" method="post">
<input name="name" placeholder="default" size="20">
<input type="submit" value="{{ _("Save") }}">
</form></p>
<table border="black" cellpadding="3">
<tbody>
<tr>
<td><b>{{ _("ID") }}</b></td>
{% if units %}
<td><b>{{ _("LUN") }}</b></td>
{% endif %}
<td><b>{{ _("Type") }}</b></td>
<td><b>{{ _("Status") }}</b></td>
<td><b>{{ _("File") }}</b></td>
<td><b>{{ _("Product") }}</b></td>
<td><b>{{ _("Actions") }}</b></td>
</tr>
{% for device in devices %}
<tr>
{% if device["id"] not in reserved_scsi_ids %}
<td style="text-align:center">{{ device.id }}</td>
{% if units %}
<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>
<td style="text-align:left">
{% if "No Media" in device.status %}
<form action="/scsi/attach" method="post">
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.unit }}">
<input name="type" type="hidden" value="{{ device.device_type }}">
<input name="file_size" type="hidden" value="{{ device.size }}">
<select type="select" name="file_name">
{% for f in files %}
{% if device.device_type == "SCCD" %}
{% if f["name"].lower().endswith(cdrom_file_suffix) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(base_dir, '') }}</option>
{% endif %}
{% elif device.device_type == "SCRM" %}
{% if f["name"].lower().endswith(removable_file_suffix) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(base_dir, '') }}</option>
{% endif %}
{% elif device.device_type == "SCMO" %}
{% if f["name"].lower().endswith(mo_file_suffix) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(base_dir, '') }}</option>
{% endif %}
{% endif %}
{% endfor %}
</select>
<input type="submit" value="{{ _("Attach") }}">
</form>
{% else %}
{{ device.file }}
{% endif %}
</td>
{% if device.vendor == "RaSCSI" %}
<td style="text-align:center">{{ device.product }}</td>
{% else %}
<td style="text-align:center">{{ device.vendor }} {{ device.product }}</td>
{% endif %}
<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? WARNING: On Mac OS, eject the Disk in the Finder instead!") }}')">
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<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.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.unit }}">
<input type="submit" value="{{ _("Info") }}">
</form>
{% else %}
<form action="/scsi/reserve" method="post" onsubmit="var memo = prompt('{{ _("Enter a memo for this reservation") }}'); if (memo === null) event.preventDefault(); 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 %}
<td class="inactive">{{ device.id }}</td>
{% if units %}
<td class="inactive"></td>
{% 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">
<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 %}
</tbody>
</table>
<p><form action="/scsi/detach_all" method="post" onsubmit="return confirm('{{ _("Detach all SCSI Devices?") }}')">
<input type="submit" value="{{ _("Detach All Devices") }}">
</form></p>
<hr/>
<details>
<summary class="heading">
{{ _("Image File Management") }}
</summary>
<ul>
<li>{{ _("Manage image files in the active RaSCSI image directory: <tt>%(directory)s</tt> with a scan depth of %(scan_depth)s.", directory=base_dir, scan_depth=scan_depth) }}</li>
<li>{{ _("Select a valid SCSI ID and <a href=\"%(url)s\">LUN</a> to attach to. Unless you know what you're doing, always use LUN 0.", url="https://en.wikipedia.org/wiki/Logical_unit_number") }}
</li>
<li>{{ _("If RaSCSI was unable to detect the device type associated with the image, you can choose the type from the dropdown.") }}</li>
<li>{{ _("Types: SAHD = SASI HDD | SCHD = SCSI HDD | SCRM = Removable | SCMO = Magneto-Optical | SCCD = CD-ROM | SCBR = Host Bridge | SCDP = DaynaPORT") }}</li>
</ul>
</details>
<table border="black" cellpadding="3">
<tbody>
<tr>
<td><b>{{ _("File") }}</b></td>
<td><b>{{ _("Size") }}</b></td>
<td><b>{{ _("Actions") }}</b></td>
</tr>
{% for file in files %}
<tr>
{% if file["prop"] %}
<td>
<details>
<summary>
{{ file["name"] }}
</summary>
<ul style="list-style: none;">
{% for key in file["prop"] %}
<li>{{ key }}: {{ file['prop'][key] }}</li>
{% endfor %}
<form action="/files/download" method="post">
<input name="file" type="hidden" value="{{ CFG_DIR }}/{{ file['name'].replace(base_dir, '') }}.{{ PROPERTIES_SUFFIX }}">
<input type="submit" value="{{ _("Properties File") }} &#8595;">
</form>
</ul>
</details>
</td>
{% elif file["zip_members"] %}
<td>
<details>
<summary>
{{ file["name"] }}
</summary>
<ul style="list-style: none;">
{% for member in file["zip_members"] %}
{% if not member.lower().endswith(PROPERTIES_SUFFIX) %}
<li>
{% if member + "." + PROPERTIES_SUFFIX in file["zip_members"] %}
<details><summary>{{ member }}
<form action="/files/unzip" method="post">
<input name="zip_file" type="hidden" value="{{ file['name'] }}">
<input name="zip_member" type="hidden" value="{{ member }}">
<input type="submit" value="{{ _("Unzip") }}" onclick="processNotify('{{ _("Unzipping a single file...") }}')">
</form>
</summary>
<ul style="list-style: none;">
<li>
{{ member + "." + PROPERTIES_SUFFIX }}
</li>
</ul>
</details>
{% else %}
<label for="zip_member">{{ member }}</label>
<form action="/files/unzip" method="post">
<input name="zip_file" type="hidden" value="{{ file['name'] }}">
<input name="zip_member" type="hidden" value="{{ member }}">
<input type="submit" value="{{ _("Unzip") }}" onclick="processNotify('{{ _("Unzipping a single file...") }}')">
</form>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
</details>
</td>
{% else %}
<td>{{ file["name"] }}</td>
{% endif %}
<td style="text-align:center">
<form action="/files/download" method="post">
<input name="file" type="hidden" value="{{ base_dir }}/{{ file['name'] }}">
<input type="submit" value="{{ file['size_mb'] }} {{ _("MB") }} &#8595;">
</form>
</td>
<td>
{% if file["name"] in attached_images %}
<center>
{{ _("Attached!") }}
</center>
{% else %}
{% if file["name"].lower().endswith(ARCHIVE_FILE_SUFFIX) %}
<form action="/files/unzip" method="post">
<input name="zip_file" type="hidden" value="{{ file['name'] }}">
<input name="zip_members" type="hidden" value="{{ file['zip_members'] }}">
<input type="submit" value="{{ _("Unzip All") }}" onclick="processNotify('{{ _("Unzipping all files...") }}')">
</form>
{% else %}
<form action="/scsi/attach" method="post">
<input name="file_name" type="hidden" value="{{ file['name'] }}">
<input name="file_size" type="hidden" value="{{ file['size'] }}">
<label for="id">{{ _("ID") }}</label>
<select name="scsi_id">
{% for id in scsi_ids %}
<option name="id" value="{{id}}"{% if id == recommended_id %} selected{% endif %}>
{{ id }}
</option>
{% endfor %}
</select>
<label for="unit">{{ _("LUN") }}</label>
<input name="unit" type="number" size="2" value="0" min="0" max="31">
{% if file["detected_type"] != "UNDEFINED" %}
<input name="type" type="hidden" value="{{ file['detected_type'] }}">
{{ file["detected_type"] }}
{% else %}
<select name="type">
<option selected value="">
{{ _("Type") }}
</option>
{% for d in device_types %}
<option value="{{ d }}">
{{ d }}
</option>
{% endfor %}
{% endif %}
</select>
<input type="submit" value="{{ _("Attach") }}">
{% endif %}
</form>
<form action="/files/rename" method="post" onsubmit="var new_file_name = prompt('{{ _("Enter new file name for: %(file_name)s", file_name=file["name"]) }}', '{{ file['name'] }}'); if (new_file_name === null) event.preventDefault(); document.getElementById('new_file_name_{{ loop.index }}').value = new_file_name;">
<input name="file_name" type="hidden" value="{{ file['name'] }}">
<input name="new_file_name" id="new_file_name_{{ loop.index }}" type="hidden" value="">
<input type="submit" value="{{ _("Rename") }}">
</form>
<form action="/files/delete" method="post" onsubmit="return confirm('{{ _("Delete file: %(file_name)s?", file_name=file["name"]) }}')">
<input name="file_name" type="hidden" value="{{ file['name'] }}">
<input type="submit" value="{{ _("Delete") }}">
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><small>{{ _("%(disk_space)s MB disk space remaining on the Pi", disk_space=free_disk) }}</small></p>
<hr/>
<details>
<summary class="heading">
{{ _("Attach Ethernet Adapter") }}
</summary>
<ul>
<li>{{ _("Emulates a SCSI DaynaPORT Ethernet Adapter. <a href=\"%(url)s\">Host drivers and configuration required</a>.", url="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link") }}
</li>
<li>{{ _("If you have a DHCP setup, choose only the interface you have configured the bridge with. You can ignore the Static IP fields 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") }}
</li>
<li style="list-style: none">{% if bridge_configured %}</li>
<li>{{ _("The <tt>rascsi_bridge</tt> interface is active and ready to be used by DaynaPORT!") }}</li>
<li style="list-style: none">{% endif %}</li>
</ul>
</details>
<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 name="ip" type="text" size="15" placeholder="10.10.20.1" minlength="7" maxlength="15" pattern="^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
<input name="mask" type="number" size="2" placeholder="24" min="16" max="30">
<label for="scsi_id">{{ _("SCSI ID:") }}</label>
<select name="scsi_id">
{% for id in scsi_ids %}
<option value="{{ id }}"{% if id == recommended_id %} selected{% endif %}>
{{ id }}
</option>
{% endfor %}
</select>
<input type="submit" value="{{ _("Attach") }}">
</form>
</td>
</tr>
</table>
{% if macproxy_configured %}
<p><small>{{ _("Macproxy is running at %(ip_addr)s (default port 5000)", ip_addr=ip_addr) }}</small></p>
{% else %}
<p><small>{{ _("Install <a href=\"%(url)s\">Macproxy</a> to browse the Web with any vintage browser. It's not just for Macs!", url="https://github.com/akuker/RASCSI/wiki/Vintage-Web-Proxy#macproxy") }}</small></p>
{% endif %}
<hr/>
<details>
<summary class="heading">
{{ _("Upload File") }}
</summary>
<ul>
<li>{{ _("Uploads file to <tt>%(directory)s</tt>. The largest file size accepted is %(max_file_size)s MB.", directory=base_dir, max_file_size=max_file_size) }}</li>
<li>{{ _("For unrecognized file types, try renaming hard drive images to '.hds', CD-ROM images to '.iso', and removable drive images to '.hdr' before uploading.") }}</li>
<li>{{ _("Recognized file types: %(valid_file_suffix)s", valid_file_suffix=valid_file_suffix) }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form name="dropper" action="/files/upload" method="post" class="dropzone dz-clickable" enctype="multipart/form-data" id="dropper"></form>
</td>
</tr>
</table>
<script type="application/javascript">
Dropzone.options.dropper = {
paramName: 'file',
acceptedFiles: '{{ valid_file_suffix }}',
chunking: true,
forceChunking: true,
url: '/files/upload',
maxFilesize: {{ max_file_size }}, // MB
chunkSize: 1000000, // bytes
dictDefaultMessage: "{{ _("Drop files here to upload") }}",
dictFallbackMessage: "{{ _("Your browser does not support drag'n'drop file uploads.") }}",
dictFallbackText: "{{ _("Please use the fallback form below to upload your files like in the olden days.") }}",
dictFileTooBig: "{{ _("File is too big: {{filesize}}MB. Max filesize: {{maxFilesize}}MB.") }}",
dictInvalidFileType: "{{ _("You can't upload files of this type.") }}",
dictResponseError: "{{ _("Server responded with code: {{statusCode}}") }}",
dictCancelUpload:" {{ _("Cancel upload") }}",
dictUploadCanceled: "{{ _("Upload canceled.") }}",
dictCancelUploadConfirmation: "{{ _("Are you sure you want to cancel this upload?") }}",
dictRemoveFile: "{{ _("Remove file") }}",
dictMaxFilesExceeded: "{{ _("You can not upload any more files.") }}",
dictFileSizeUnits: {
tb: "{{ _("TB") }}",
gb: "{{ _("GB") }}",
mb: "{{ _("MB") }}",
kb: "{{ _("KB") }}",
b: "{{ _("b") }}"
}
}
</script>
<hr/>
<details>
<summary class="heading">
{{ _("Download File to Images") }}
</summary>
<ul>
<li>{{ _("Given a URL, download that file to the <tt>%(directory)s</tt> directory.", directory=base_dir) }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/download_to_images" method="post">
<label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File to Images...") }}')">
</form>
</td>
</tr>
</table>
<hr/>
<details>
<summary class="heading">
{{ _("Download File to AppleShare") }}
</summary>
<ul>
<li>{{ _("Given a URL, download that file to the <tt>%(directory)s</tt> directory and share it over AFP.", directory=AFP_DIR) }}</li>
<li>{{ _("Manage the files you download here through AppleShare on your vintage Mac.") }}</li>
<li>{{ _("Requires <a href=\"%(url)s\">Netatalk</a> to be installed and configured correctly for your network.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}</li>
</ul>
</details>
{% if netatalk_configured %}
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/download_to_afp" method="post">
<label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File to AppleShare...") }}')">
</form>
</td>
</tr>
</table>
{% if netatalk_configured == 1 %}
<p><small>{{ _("The AppleShare server is running. No active connections.") }}</small></p>
{% elif netatalk_configured == 2 %}
<p><small>{{ _("%(value)d active AFP connection", value=(netatalk_configured - 1)) }}</small></p>
{% elif netatalk_configured > 2 %}
<p><small>{{ _("%(value)d active AFP connections", value=(netatalk_configured - 1)) }}</small></p>
{% endif %}
{% else %}
<p>{{ _("Install <a href=\"%(url)s\">Netatalk</a> to use the AppleShare File Server.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}</p>
{% endif %}
<hr/>
<details>
<summary class="heading">
{{ _("Download File and Create CD-ROM image") }}
</summary>
<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>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<label for="scsi_id">{{ _("SCSI ID:") }}</label>
<form action="/files/download_to_iso" method="post">
<select name="scsi_id">
{% for id in scsi_ids %}
<option value="{{ id }}"{% if id == recommended_id %} selected{% endif %}>
{{ id }}
</option>
{% endfor %}
</select>
<label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<label for="type">{{ _("Type:") }}</label>
<select name="type">
<option value="-hfs">
HFS
</option>
<option value="-iso-level 1">
ISO-9660 Level 1
</option>
<option value="-iso-level 2">
ISO-9660 Level 2
</option>
<option value="-iso-level 3">
ISO-9660 Level 3
</option>
<option value="-J">
Joliet
</option>
<option value="-r">
Rock Ridge
</option>
</select>
<input type="submit" value="{{ _("Download and Mount CD-ROM image") }}" onclick="processNotify('{{ _("Downloading File and generating CD-ROM image...") }}')">
</form>
</td>
</tr>
</table>
<hr/>
<details>
<summary class="heading">
{{ _("Create Empty Disk Image File") }}
</summary>
<ul>
<li>{{ _("The Generic image type is recommended for most computer platforms.") }}</li>
<li>{{ _("APPLE GENUINE (.hda) and NEC GENUINE (.hdn) image types will make RaSCSI behave as a particular drive type that are recognized by Mac and PC98 systems, respectively.") }}</li>
<li>{{ _("SASI images should only be used on the original Sharp X68000, or other legacy systems that utilize this pre-SCSI standard.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/create" method="post">
<label for="file_name">{{ _("File Name:") }}</label>
<input name="file_name" placeholder="{{ _("File Name") }}" required="" type="text">
<label for="type">{{ _("Type:") }}</label>
<select name="type">
<option value="hds">
{{ _("SCSI Hard Disk image (Generic) [.hds]") }}
</option>
<option value="hda">
{{ _("SCSI Hard Disk image (APPLE GENUINE) [.hda]") }}
</option>
<option value="hdn">
{{ _("SCSI Hard Disk image (NEC GENUINE) [.hdn]") }}
</option>
<option value="hdr">
{{ _("SCSI Removable Media Disk image (Generic) [.hdr]") }}
</option>
<option value="hdf">
{{ _("SASI Hard Disk image (Legacy) [.hdf]") }}
</option>
</select>
<label for="size">{{ _("Size:") }}</label>
<input name="size" type="number" placeholder="{{ _("MB") }}" min="1" size="6" required>
<input type="submit" value="{{ _("Create") }}">
</form>
</td>
</tr>
</table>
<hr/>
<details>
<summary class="heading">
{{ _("Create Named Drive") }}
</summary>
<ul>
<li>{{ _("Create pairs of images and properties files from a list of real-life drives.") }}</li>
<li>{{ _("This will make RaSCSI use certain vendor strings and block sizes that may improve compatibility with certain systems.") }}</li>
</ul>
</details>
<p><a href="/drive/list">{{ _("Create a named disk image that mimics real-life drives") }}</a></p>
<hr/>
<details>
<summary class="heading">
{{ _("Logging") }}
</summary>
<ul>
<li>{{ _("Fetch a certain number of lines of system logs with the given scope.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/logs/show" method="post">
<label for="lines">{{ _("Log Lines:") }}</label>
<input name="lines" type="number" placeholder="200" min="1" size="4">
<label for="scope">{{ _("Scope:") }}</label>
<select name="scope">
<option value="default">
default
</option>
<option value="rascsi">
rascsi.service
</option>
<option value="rascsi-web">
rascsi-web.service
</option>
</select>
<input type="submit" value="{{ _("Show Logs") }}">
</form>
</td>
</tr>
</table>
<hr/>
<details>
<summary class="heading">
{{ _("Server Log Level") }}
</summary>
<ul>
<li>{{ _("Change the log level of the RaSCSI backend process.") }}</li>
<li>{{ _("The current dropdown selection indicates the active log level.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/logs/level" method="post">
<label for="level">{{ _("Log Level:") }}</label>
<select name="level">
{% for level in log_levels %}
<option value="{{ level }}"{% if level == current_log_level %} selected{% endif %}>
{{ level }}
</option>
{% endfor %}
</select>
<input type="submit" value="{{ _("Set Log Level") }}">
</form>
</td>
</tr>
</table>
<hr/>
<details>
<summary class="heading">
{{ _("Language") }}
</summary>
<ul>
<li>{{ _("Change the Web Interface language.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/language" method="post">
<label for="language">{{ _("Language:") }}</label>
<select name="locale">
{% for locale in locales %}
<option value="{{ locale.language }}">
{{ locale.language }} - {{ locale.display_name }}
</option>
{% endfor %}
</select>
<input type="submit" value="{{ _("Change Language") }}">
</form>
</td>
</tr>
</table>
<hr/>
<details>
<summary class="heading">
{{ _("Raspberry Pi Operations") }}
</summary>
<ul>
<li>{{ _("Reboot or shut down the Raspberry Pi that RaSCSI is running on.") }}</li>
<li>{{ _("IMPORTANT: Always shut down the Pi before turning off the power. Failing to do so may lead to data loss.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/pi/reboot" method="post" onclick="if (confirm('{{ _("Reboot the Raspberry Pi?") }}')) shutdownNotify('{{ _("Rebooting the Raspberry Pi...") }}'); else event.preventDefault();">
<input type="submit" value="{{ _("Reboot Raspberry Pi") }}">
</form>
</td>
<td style="border: none; vertical-align:top;">
<form action="/pi/shutdown" method="post" onclick="if (confirm('{{ _("Shut down the Raspberry Pi?") }}')) shutdownNotify('{{ _("Shutting down the Raspberry Pi...") }}'); else event.preventDefault();">
<input type="submit" value="{{ _("Shut Down Raspberry Pi") }}">
</form>
</td>
</tr>
</table>
{% endblock content %}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
set -e
# set -x # Uncomment to Debug
cd "$(dirname "$0")"
# verify packages installed
ERROR=0
if ! command -v genisoimage &> /dev/null ; then
echo "genisoimage could not be found"
echo "Run 'sudo apt install genisoimage' to fix."
ERROR=1
fi
if ! command -v python3 &> /dev/null ; then
echo "python3 could not be found"
echo "Run 'sudo apt install python3' to fix."
ERROR=1
fi
if ! python3 -m venv --help &> /dev/null ; then
echo "venv could not be found"
echo "Run 'sudo apt install python3-venv' to fix."
ERROR=1
fi
if ! command -v unzip &> /dev/null ; then
echo "unzip could not be found"
echo "Run 'sudo apt install unzip' to fix."
ERROR=1
fi
if [ $ERROR = 1 ] ; then
echo
echo "Fix errors and re-run ./start.sh"
exit 1
fi
# Test for two known broken venv states
if test -e venv; then
GOOD_VENV=true
if ! test -e venv/bin/activate; then
GOOD_VENV=false
else
source venv/bin/activate
pip3 list 1> /dev/null
test $? -eq 1 && GOOD_VENV=false
fi
if ! "$GOOD_VENV"; then
echo "Deleting bad python venv"
sudo rm -rf venv
fi
fi
# Create the venv if it doesn't exist
if ! test -e venv; then
echo "Creating python venv for web server"
python3 -m venv venv
echo "Activating venv"
source venv/bin/activate
echo "Installing requirements.txt"
pip3 install wheel
pip3 install -r requirements.txt
git rev-parse --is-inside-work-tree &> /dev/null
if [[ $? -eq 0 ]]; then
git rev-parse HEAD > current
fi
fi
source venv/bin/activate
# Detect if someone updates the git repo - we need to re-run pip3 install.
set +e
git rev-parse --is-inside-work-tree &> /dev/null
if [[ $? -eq 0 ]]; then
set -e
if ! test -e current; then
git rev-parse > current
elif [ "$(cat current)" != "$(git rev-parse HEAD)" ]; then
echo "New version detected, updating libraries from requirements.txt"
pip3 install -r requirements.txt
git rev-parse HEAD > current
fi
else
echo "Warning: Not running from a valid git repository. Will not be able to update the code."
fi
set -e
pybabel compile -d src/translations
# parse arguments
while [ "$1" != "" ]; do
PARAM=$(echo "$1" | awk -F= '{print $1}')
VALUE=$(echo "$1" | awk -F= '{print $2}')
case $PARAM in
-p | --port)
PORT="--port $VALUE"
;;
-P | --password)
PASSWORD="--password $VALUE"
;;
*)
echo "ERROR: unknown parameter \"$PARAM\""
exit 1
;;
esac
shift
done
echo "Starting web server for RaSCSI Web Interface..."
cd src
python3 web.py ${PORT} ${PASSWORD}
View File