clean squashed restructuring branch. #455
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
../.pylintrc
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 574 B |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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...")
|
||||
@@ -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."
|
||||
)
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
../.pylintrc
|
||||
@@ -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.
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
[python: **.py]
|
||||
[jinja2: **/templates/**.html]
|
||||
extensions=jinja2.ext.autoescape,jinja2.ext.with_
|
||||
@@ -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
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Mock responses to rascsi-web
|
||||
case $1 in
|
||||
-n)
|
||||
echo "logs $*"
|
||||
;;
|
||||
|
||||
**)
|
||||
echo "default"
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Mock responses to rascsi-web
|
||||
case $1 in
|
||||
is-active)
|
||||
echo "is-active"
|
||||
;;
|
||||
|
||||
**)
|
||||
echo "default"
|
||||
;;
|
||||
esac
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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": ""
|
||||
}
|
||||
]
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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": ""}
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -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>
|
||||
|
After Width: | Height: | Size: 904 B |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
@@ -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}
|
||||
@@ -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"]
|
||||
@@ -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."
|
||||
)
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) }} – <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") }} – {{ _("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>
|
||||
@@ -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 %}
|
||||
@@ -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") }} ↓">
|
||||
</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") }} ↓">
|
||||
</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 %}
|
||||
@@ -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}
|
||||