mirror of
https://github.com/akuker/RASCSI.git
synced 2024-11-22 01:31:25 +00:00
Run web API test suite in GitHub Actions (#1009)
- Fixed ignore patterns in .dockerignore - Added healthchecks to backend and web containers - Reduced Docker image sizes - Removed RaSCSI references in various areas (e.g. rascsi -> backend) - Added compilation-only step to easyinstall.sh - Moved apt package lists to variables - Revert to triggering GitHub Actions runs on push - Updated web/frontend_checks workflow to run black and flake8 against all Python sources - Capture log files from backend/web containers - Fix None to float conversion bug when user agent is absent or unrecognised
This commit is contained in:
parent
eca8145311
commit
88ff542aeb
@ -2,9 +2,8 @@
|
|||||||
/*
|
/*
|
||||||
|
|
||||||
# Paths to include
|
# Paths to include
|
||||||
!/docker/rascsi/rascsi_wrapper.sh
|
!/docker/backend/rascsi_wrapper.sh
|
||||||
!/docker/rascsi/cfilesystem.patch
|
!/docker/web/start.sh
|
||||||
!/docker/rascsi-web/start.sh
|
|
||||||
!/doc
|
!/doc
|
||||||
!/python
|
!/python
|
||||||
!/cpp
|
!/cpp
|
||||||
@ -13,19 +12,27 @@
|
|||||||
!/LICENCE
|
!/LICENCE
|
||||||
!/README.md
|
!/README.md
|
||||||
|
|
||||||
# From .gitignore
|
# Dev artifacts to exclude
|
||||||
venv
|
**/.git
|
||||||
*.pyc
|
|
||||||
core
|
/cpp/bin
|
||||||
|
/cpp/obj
|
||||||
|
|
||||||
|
**/venv*
|
||||||
|
**/*.pyc
|
||||||
|
**/__pycache__
|
||||||
|
**/.pytest_cache
|
||||||
|
**/rascsi_interface_pb2.py
|
||||||
|
**/report.xml
|
||||||
|
|
||||||
**/.idea
|
**/.idea
|
||||||
.DS_Store
|
**/.vscode
|
||||||
*.swp
|
**/.DS_Store
|
||||||
__pycache__
|
|
||||||
current
|
**/core
|
||||||
rascsi_interface_pb2.py
|
**/*.swp
|
||||||
src/raspberrypi/hfdisk/
|
**/current
|
||||||
*~
|
|
||||||
messages.pot
|
**/node_modules
|
||||||
messages.mo
|
**/messages.pot
|
||||||
s.sh
|
**/messages.mo
|
||||||
*-backups
|
|
||||||
|
88
.github/workflows/web.yml
vendored
88
.github/workflows/web.yml
vendored
@ -2,22 +2,18 @@ name: Web Tests/Analysis
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
push:
|
||||||
types: [opened, synchronize]
|
|
||||||
paths:
|
paths:
|
||||||
- 'python/web/**'
|
- 'python/web/**'
|
||||||
- 'python/common/**'
|
- 'python/common/**'
|
||||||
- '.github/workflows/web.yml'
|
- '.github/workflows/web.yml'
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backend_checks:
|
backend_checks:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: python/web
|
working-directory: python
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
@ -26,14 +22,88 @@ jobs:
|
|||||||
python-version: 3.7.15
|
python-version: 3.7.15
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
|
||||||
- run: pip install -r requirements-dev.txt
|
- run: pip install -r web/requirements-dev.txt
|
||||||
id: pip
|
id: pip
|
||||||
|
|
||||||
- run: black --check tests
|
- run: black --check .
|
||||||
|
|
||||||
- run: flake8 tests
|
- run: flake8 .
|
||||||
if: success() || failure() && steps.pip.outcome == 'success'
|
if: success() || failure() && steps.pip.outcome == 'success'
|
||||||
|
|
||||||
|
backend_tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: docker
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Check DockerHub for existing backend image
|
||||||
|
run: |
|
||||||
|
export DOCKER_BACKEND_IMAGE="piscsi/backend-standalone:`git ls-files -s python .github/workflows/web.yml | git hash-object --stdin`"
|
||||||
|
echo "DOCKER_BACKEND_IMAGE=${DOCKER_BACKEND_IMAGE}" >> $GITHUB_ENV
|
||||||
|
docker pull --quiet ${DOCKER_BACKEND_IMAGE} || echo "DOCKER_BACKEND_NEEDS_PUSH=1" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build and launch containers
|
||||||
|
run: docker compose -f docker-compose.ci.yml up -d
|
||||||
|
|
||||||
|
- name: Run test suite
|
||||||
|
run: docker compose -f docker-compose.ci.yml run pytest -v
|
||||||
|
|
||||||
|
- name: Check if DockerHub secrets defined
|
||||||
|
run: if [[ $DOCKERHUB_USERNAME && $DOCKERHUB_TOKEN ]]; then echo "DOCKERHUB_SECRETS_DEFINED=1" >> $GITHUB_ENV; fi
|
||||||
|
env:
|
||||||
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
if: env.DOCKERHUB_SECRETS_DEFINED && env.DOCKER_BACKEND_NEEDS_PUSH
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Push backend image to DockerHub
|
||||||
|
if: (success() || failure()) && env.DOCKERHUB_SECRETS_DEFINED && env.DOCKER_BACKEND_NEEDS_PUSH
|
||||||
|
run: docker compose -f docker-compose.ci.yml push backend
|
||||||
|
|
||||||
|
- name: Upload test artifacts
|
||||||
|
if: success() || failure()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: pytest-output.zip
|
||||||
|
path: |
|
||||||
|
docker/volumes/pytest/report.xml
|
||||||
|
docker/volumes/pytest/pytest.log
|
||||||
|
|
||||||
|
- name: Output container logs
|
||||||
|
if: success() || failure()
|
||||||
|
run: |
|
||||||
|
docker compose -f docker-compose.ci.yml logs backend > backend.log
|
||||||
|
docker compose -f docker-compose.ci.yml logs web > web.log
|
||||||
|
docker compose -f docker-compose.ci.yml logs -t | sort -u -k 3 > combined.log
|
||||||
|
|
||||||
|
- name: Upload backend log
|
||||||
|
if: success() || failure()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: backend.log
|
||||||
|
path: docker/backend.log
|
||||||
|
|
||||||
|
- name: Upload web log
|
||||||
|
if: success() || failure()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: web.log
|
||||||
|
path: docker/web.log
|
||||||
|
|
||||||
|
- name: Upload combined log
|
||||||
|
if: success() || failure()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: combined.log
|
||||||
|
path: docker/combined.log
|
||||||
|
|
||||||
frontend_checks:
|
frontend_checks:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,13 +1,15 @@
|
|||||||
venv
|
venv
|
||||||
*.pyc
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
*.log
|
||||||
|
*~
|
||||||
core
|
core
|
||||||
.idea/
|
.idea/
|
||||||
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
|
||||||
__pycache__
|
__pycache__
|
||||||
current
|
current
|
||||||
rascsi_interface_pb2.py
|
rascsi_interface_pb2.py
|
||||||
*~
|
|
||||||
messages.pot
|
messages.pot
|
||||||
messages.mo
|
messages.mo
|
||||||
report.xml
|
report.xml
|
||||||
|
@ -35,14 +35,12 @@ from another terminal.
|
|||||||
The following environment variables are available when using Docker Compose:
|
The following environment variables are available when using Docker Compose:
|
||||||
|
|
||||||
| Environment Variable | Default |
|
| Environment Variable | Default |
|
||||||
| -------------------- | -------- |
|
| -------------------- |----------|
|
||||||
| `OS_DISTRO` | debian |
|
|
||||||
| `OS_VERSION` | buster |
|
| `OS_VERSION` | buster |
|
||||||
| `OS_ARCH` | amd64 |
|
|
||||||
| `WEB_HTTP_PORT` | 8080 |
|
| `WEB_HTTP_PORT` | 8080 |
|
||||||
| `WEB_HTTPS_PORT` | 8443 |
|
| `WEB_HTTPS_PORT` | 8443 |
|
||||||
| `WEB_LOG_LEVEL` | info |
|
| `WEB_LOG_LEVEL` | info |
|
||||||
| `RASCSI_HOST` | rascsi |
|
| `RASCSI_HOST` | backend |
|
||||||
| `RASCSI_PORT` | 6868 |
|
| `RASCSI_PORT` | 6868 |
|
||||||
| `RASCSI_PASSWORD` | *[None]* |
|
| `RASCSI_PASSWORD` | *[None]* |
|
||||||
| `RASCSI_LOG_LEVEL` | debug |
|
| `RASCSI_LOG_LEVEL` | debug |
|
||||||
@ -83,7 +81,7 @@ docker compose up --build
|
|||||||
|
|
||||||
### Open a Shell on a Running Container
|
### Open a Shell on a Running Container
|
||||||
|
|
||||||
Run the following command, replacing `[CONTAINER]` with `rascsi` or `rascsi_web`.
|
Run the following command, replacing `[CONTAINER]` with `backend` or `web`.
|
||||||
|
|
||||||
```
|
```
|
||||||
docker compose exec [CONTAINER] bash
|
docker compose exec [CONTAINER] bash
|
||||||
@ -92,7 +90,7 @@ docker compose exec [CONTAINER] bash
|
|||||||
### Setup Live Editing for the Web UI
|
### Setup Live Editing for the Web UI
|
||||||
|
|
||||||
Use a `docker-compose.override.yml` to mount the local `python` directory to
|
Use a `docker-compose.override.yml` to mount the local `python` directory to
|
||||||
`/home/pi/RASCSI/python/` in the `rascsi_web` container.
|
`/home/pi/RASCSI/python/` in the `web` container.
|
||||||
|
|
||||||
Any changes to *.py files on the host computer (i.e. in your IDE) will trigger
|
Any changes to *.py files on the host computer (i.e. in your IDE) will trigger
|
||||||
the web UI process to be restarted in the container.
|
the web UI process to be restarted in the container.
|
||||||
@ -100,7 +98,7 @@ the web UI process to be restarted in the container.
|
|||||||
**Example:**
|
**Example:**
|
||||||
```
|
```
|
||||||
services:
|
services:
|
||||||
rascsi_web:
|
web:
|
||||||
volumes:
|
volumes:
|
||||||
- ../python:/home/pi/RASCSI/python:delegated
|
- ../python:/home/pi/RASCSI/python:delegated
|
||||||
```
|
```
|
||||||
|
36
docker/backend/Dockerfile
Normal file
36
docker/backend/Dockerfile
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
FROM debian:bullseye AS build
|
||||||
|
RUN apt-get update && apt-get install --assume-yes --no-install-recommends sudo
|
||||||
|
RUN groupadd pi \
|
||||||
|
&& useradd --create-home --shell /bin/bash -g pi pi \
|
||||||
|
&& echo "pi ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||||
|
|
||||||
|
USER pi
|
||||||
|
WORKDIR /home/pi/RASCSI
|
||||||
|
|
||||||
|
COPY --chown=pi:pi easyinstall.sh .
|
||||||
|
COPY --chown=pi:pi cpp cpp
|
||||||
|
COPY --chown=pi:pi doc doc
|
||||||
|
RUN ./easyinstall.sh --run_choice=15 --cores=`nproc`
|
||||||
|
|
||||||
|
FROM debian:bullseye-slim AS runner
|
||||||
|
USER root
|
||||||
|
WORKDIR /home/pi
|
||||||
|
|
||||||
|
COPY --from=build /home/pi/RASCSI/cpp/bin/fullspec/* /usr/local/bin/
|
||||||
|
COPY docker/backend/rascsi_wrapper.sh /usr/local/bin/rascsi_wrapper.sh
|
||||||
|
RUN chmod +x /usr/local/bin/*
|
||||||
|
RUN mkdir -p /home/pi/images
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install --no-install-recommends --assume-yes libpcap-dev libprotobuf-dev \
|
||||||
|
&& apt autoremove -y \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
EXPOSE 6868
|
||||||
|
ENTRYPOINT ["/usr/local/bin/rascsi_wrapper.sh", "-r", "7", "-F", "/home/pi/images"]
|
||||||
|
CMD ["-L", "trace"]
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=5m --timeout=1s CMD rasctl -v
|
42
docker/docker-compose.ci.yml
Normal file
42
docker/docker-compose.ci.yml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
image: ${DOCKER_BACKEND_IMAGE}
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/backend/Dockerfile
|
||||||
|
init: true
|
||||||
|
volumes:
|
||||||
|
- ./volumes/images:/home/pi/images:delegated
|
||||||
|
healthcheck:
|
||||||
|
interval: 5s
|
||||||
|
start_period: 5s
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/web/Dockerfile
|
||||||
|
args:
|
||||||
|
- OS_VERSION=buster
|
||||||
|
volumes:
|
||||||
|
- ./volumes/images:/home/pi/images:delegated
|
||||||
|
init: true
|
||||||
|
command: ["--rascsi-host=backend", "--log-level=debug"]
|
||||||
|
healthcheck:
|
||||||
|
interval: 5s
|
||||||
|
start_period: 5s
|
||||||
|
|
||||||
|
pytest:
|
||||||
|
depends_on:
|
||||||
|
web:
|
||||||
|
condition: service_healthy
|
||||||
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
profiles:
|
||||||
|
- webui-tests
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/pytest/Dockerfile
|
||||||
|
working_dir: /src
|
||||||
|
volumes:
|
||||||
|
- ./volumes/pytest:/src/tests/output:delegated
|
||||||
|
command: ["-vv"]
|
@ -1,4 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
rascsi_web:
|
web:
|
||||||
volumes:
|
volumes:
|
||||||
- ../python:/home/pi/RASCSI/python:delegated
|
- ../python:/home/pi/RASCSI/python:delegated
|
||||||
|
pytest:
|
||||||
|
volumes:
|
||||||
|
- ../python/web:/src:delegated
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
rascsi:
|
backend:
|
||||||
container_name: rascsi
|
container_name: rascsi_backend
|
||||||
image: rascsi:develop-${OS_DISTRO:-debian}-${OS_VERSION:-buster}-${OS_ARCH:-amd64}-standalone
|
image: rascsi-backend
|
||||||
pull_policy: never
|
pull_policy: never
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: docker/rascsi/Dockerfile
|
dockerfile: docker/backend/Dockerfile
|
||||||
args:
|
|
||||||
- OS_DISTRO=${OS_DISTRO:-debian}
|
|
||||||
- OS_VERSION=${OS_VERSION:-buster}
|
|
||||||
- OS_ARCH=${OS_ARCH:-amd64}
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/images:/home/pi/images:delegated
|
- ./volumes/images:/home/pi/images:delegated
|
||||||
- ./volumes/config:/home/pi/.config/rascsi:delegated
|
- ./volumes/config:/home/pi/.config/rascsi:delegated
|
||||||
@ -19,26 +15,19 @@ services:
|
|||||||
- RASCSI_PASSWORD=${RASCSI_PASSWORD:-}
|
- RASCSI_PASSWORD=${RASCSI_PASSWORD:-}
|
||||||
init: true
|
init: true
|
||||||
command: [
|
command: [
|
||||||
"/usr/local/bin/rascsi_wrapper.sh",
|
|
||||||
"-L",
|
"-L",
|
||||||
"${RASCSI_LOG_LEVEL:-trace}",
|
"${RASCSI_LOG_LEVEL:-trace}",
|
||||||
"-r",
|
|
||||||
"7",
|
|
||||||
"-F",
|
|
||||||
"/home/pi/images"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
rascsi_web:
|
web:
|
||||||
container_name: rascsi_web
|
container_name: rascsi_web
|
||||||
image: rascsi:develop-${OS_DISTRO:-debian}-${OS_VERSION:-buster}-${OS_ARCH:-amd64}-web
|
image: rascsi-web:${OS_VERSION:-buster}
|
||||||
pull_policy: never
|
pull_policy: never
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: docker/rascsi-web/Dockerfile
|
dockerfile: docker/web/Dockerfile
|
||||||
args:
|
args:
|
||||||
- OS_DISTRO=${OS_DISTRO:-debian}
|
|
||||||
- OS_VERSION=${OS_VERSION:-buster}
|
- OS_VERSION=${OS_VERSION:-buster}
|
||||||
- OS_ARCH=${OS_ARCH:-amd64}
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/images:/home/pi/images:delegated
|
- ./volumes/images:/home/pi/images:delegated
|
||||||
- ./volumes/config:/home/pi/.config/rascsi:delegated
|
- ./volumes/config:/home/pi/.config/rascsi:delegated
|
||||||
@ -49,23 +38,20 @@ services:
|
|||||||
- RASCSI_PASSWORD=${RASCSI_PASSWORD:-}
|
- RASCSI_PASSWORD=${RASCSI_PASSWORD:-}
|
||||||
init: true
|
init: true
|
||||||
command: [
|
command: [
|
||||||
"start.sh",
|
"--rascsi-host=${RASCSI_HOST:-backend}",
|
||||||
"--rascsi-host=${RASCSI_HOST:-rascsi}",
|
|
||||||
"--rascsi-port=${RASCSI_PORT:-6868}",
|
"--rascsi-port=${RASCSI_PORT:-6868}",
|
||||||
"--log-level=${WEB_LOG_LEVEL:-info}",
|
"--log-level=${WEB_LOG_LEVEL:-debug}",
|
||||||
"--dev-mode"
|
"--dev-mode"
|
||||||
]
|
]
|
||||||
|
|
||||||
pytest:
|
pytest:
|
||||||
container_name: pytest
|
container_name: rascsi_pytest
|
||||||
image: rascsi:pytest
|
image: rascsi-pytest
|
||||||
pull_policy: never
|
pull_policy: never
|
||||||
profiles:
|
profiles:
|
||||||
- webui-tests
|
- webui-tests
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: docker/pytest/Dockerfile
|
dockerfile: docker/pytest/Dockerfile
|
||||||
volumes:
|
|
||||||
- ../python/web:/src:delegated
|
|
||||||
working_dir: /src
|
working_dir: /src
|
||||||
entrypoint: "pytest"
|
command: ["-vv"]
|
@ -1,5 +1,12 @@
|
|||||||
FROM python:3.7-bullseye
|
FROM python:3.7-slim
|
||||||
ENV DOCKER=1
|
ENV DOCKER=1
|
||||||
|
|
||||||
COPY python/web/requirements-dev.txt /requirements-dev.txt
|
WORKDIR /src
|
||||||
RUN pip install -r /requirements-dev.txt
|
|
||||||
|
COPY python/web/requirements-dev.txt /src/requirements-dev.txt
|
||||||
|
COPY python/web/pyproject.toml /src/pyproject.toml
|
||||||
|
COPY python/web/tests /src/tests
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r /src/requirements-dev.txt
|
||||||
|
|
||||||
|
ENTRYPOINT ["pytest"]
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
ARG OS_DISTRO=debian
|
|
||||||
ARG OS_VERSION=buster
|
|
||||||
ARG OS_ARCH=amd64
|
|
||||||
FROM "${OS_ARCH}/${OS_DISTRO}:${OS_VERSION}"
|
|
||||||
|
|
||||||
EXPOSE 80 443
|
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends \
|
|
||||||
sudo \
|
|
||||||
systemd \
|
|
||||||
rsyslog \
|
|
||||||
procps \
|
|
||||||
man-db \
|
|
||||||
wget \
|
|
||||||
git
|
|
||||||
|
|
||||||
RUN groupadd pi
|
|
||||||
RUN useradd --create-home --shell /bin/bash -g pi pi
|
|
||||||
RUN echo "pi ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
|
||||||
RUN echo "pi:rascsi" | chpasswd
|
|
||||||
|
|
||||||
# Allows custom PATH for mock commands to work when executing with sudo
|
|
||||||
RUN sed -i 's/^Defaults\tsecure_path/#Defaults\tsecure_path./' /etc/sudoers
|
|
||||||
|
|
||||||
RUN mkdir /home/pi/shared_files
|
|
||||||
RUN touch /etc/dhcpcd.conf
|
|
||||||
RUN mkdir -p /etc/network/interfaces.d/
|
|
||||||
|
|
||||||
USER pi
|
|
||||||
WORKDIR /home/pi/RASCSI
|
|
||||||
COPY --chown=pi:pi . .
|
|
||||||
|
|
||||||
# Install standalone RaSCSI web UI
|
|
||||||
RUN ./easyinstall.sh --run_choice=11
|
|
||||||
|
|
||||||
# Enable web UI authentication
|
|
||||||
RUN ./easyinstall.sh --run_choice=13
|
|
||||||
|
|
||||||
# Setup wired network bridge
|
|
||||||
RUN ./easyinstall.sh --run_choice=5 --headless
|
|
||||||
|
|
||||||
USER root
|
|
||||||
WORKDIR /home/pi
|
|
||||||
RUN pip3 install watchdog
|
|
||||||
COPY docker/rascsi-web/start.sh /usr/local/bin/start.sh
|
|
||||||
RUN chmod +x /usr/local/bin/start.sh
|
|
||||||
CMD ["/usr/local/bin/start.sh"]
|
|
@ -1,26 +0,0 @@
|
|||||||
ARG OS_DISTRO=debian
|
|
||||||
ARG OS_VERSION=buster
|
|
||||||
ARG OS_ARCH=amd64
|
|
||||||
FROM "${OS_ARCH}/${OS_DISTRO}:${OS_VERSION}"
|
|
||||||
|
|
||||||
EXPOSE 6868
|
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends sudo systemd rsyslog patch wget
|
|
||||||
|
|
||||||
RUN groupadd pi
|
|
||||||
RUN useradd --create-home --shell /bin/bash -g pi pi
|
|
||||||
RUN echo "pi ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
|
||||||
|
|
||||||
USER pi
|
|
||||||
WORKDIR /home/pi/RASCSI
|
|
||||||
COPY --chown=pi:pi . .
|
|
||||||
|
|
||||||
# Install RaSCSI standalone
|
|
||||||
RUN ./easyinstall.sh --run_choice=10 --cores=`nproc`
|
|
||||||
|
|
||||||
USER root
|
|
||||||
WORKDIR /home/pi
|
|
||||||
COPY docker/rascsi/rascsi_wrapper.sh /usr/local/bin/rascsi_wrapper.sh
|
|
||||||
RUN chmod +x /usr/local/bin/rascsi_wrapper.sh
|
|
||||||
CMD ["/usr/local/bin/rascsi_wrapper.sh", "-L", "trace", "-r", "7", "-F", "/home/pi/images"]
|
|
57
docker/web/Dockerfile
Normal file
57
docker/web/Dockerfile
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ARG OS_VERSION=buster
|
||||||
|
|
||||||
|
FROM "debian:${OS_VERSION}-slim"
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends sudo systemd rsyslog procps man-db wget git \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN groupadd pi \
|
||||||
|
&& useradd --create-home --shell /bin/bash -g pi pi \
|
||||||
|
&& echo "pi ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \
|
||||||
|
&& echo "pi:rascsi" | chpasswd
|
||||||
|
|
||||||
|
# Allows custom PATH for mock commands to work when executing with sudo
|
||||||
|
RUN sed -i 's/^Defaults\tsecure_path/#Defaults\tsecure_path./' /etc/sudoers
|
||||||
|
|
||||||
|
RUN mkdir -p /home/pi/shared_files \
|
||||||
|
&& mkdir /home/pi/images \
|
||||||
|
&& mkdir -p /etc/network/interfaces.d \
|
||||||
|
&& touch /etc/dhcpcd.conf
|
||||||
|
|
||||||
|
USER pi
|
||||||
|
WORKDIR /home/pi/RASCSI
|
||||||
|
|
||||||
|
RUN mkdir /home/pi/RASCSI/{python,cpp}
|
||||||
|
COPY --chown=pi:pi easyinstall.sh .
|
||||||
|
COPY --chown=pi:pi cpp/os_integration cpp/os_integration
|
||||||
|
COPY --chown=pi:pi cpp/rascsi_interface.proto cpp/rascsi_interface.proto
|
||||||
|
COPY --chown=pi:pi python/web python/web
|
||||||
|
COPY --chown=pi:pi python/common python/common
|
||||||
|
|
||||||
|
# Install standalone RaSCSI web UI
|
||||||
|
RUN ./easyinstall.sh --run_choice=11 \
|
||||||
|
&& sudo apt-get remove build-essential --yes \
|
||||||
|
&& sudo apt autoremove -y \
|
||||||
|
&& sudo apt-get clean \
|
||||||
|
&& sudo rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Enable web UI authentication
|
||||||
|
RUN ./easyinstall.sh --run_choice=13
|
||||||
|
|
||||||
|
# Setup wired network bridge
|
||||||
|
RUN ./easyinstall.sh --run_choice=5 --headless
|
||||||
|
|
||||||
|
USER root
|
||||||
|
WORKDIR /home/pi
|
||||||
|
RUN pip3 install --no-cache-dir PyYAML watchdog
|
||||||
|
COPY docker/web/start.sh /usr/local/bin/start.sh
|
||||||
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|
||||||
|
EXPOSE 80 443
|
||||||
|
ENTRYPOINT ["/usr/local/bin/start.sh"]
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=5m --timeout=3s \
|
||||||
|
CMD wget --quiet --server-response http://localhost/healthcheck 2>&1 | grep "200 OK"
|
@ -3,7 +3,7 @@
|
|||||||
if ! [[ -f "/home/pi/RASCSI/python/common/src/rascsi_interface_pb2.py" ]]; then
|
if ! [[ -f "/home/pi/RASCSI/python/common/src/rascsi_interface_pb2.py" ]]; then
|
||||||
# Build rascsi_interface_pb2.py with the protobuf compiler
|
# Build rascsi_interface_pb2.py with the protobuf compiler
|
||||||
protoc \
|
protoc \
|
||||||
-I=/home/pi/RASCSI/src/raspberrypi \
|
-I=/home/pi/RASCSI/cpp \
|
||||||
--python_out=/home/pi/RASCSI/python/common/src \
|
--python_out=/home/pi/RASCSI/python/common/src \
|
||||||
rascsi_interface.proto
|
rascsi_interface.proto
|
||||||
fi
|
fi
|
@ -69,6 +69,11 @@ SECRET_FILE="$HOME/.config/rascsi/rascsi_secret"
|
|||||||
FILE_SHARE_PATH="$HOME/shared_files"
|
FILE_SHARE_PATH="$HOME/shared_files"
|
||||||
FILE_SHARE_NAME="Pi File Server"
|
FILE_SHARE_NAME="Pi File Server"
|
||||||
|
|
||||||
|
APT_PACKAGES_COMMON="build-essential git protobuf-compiler bridge-utils"
|
||||||
|
APT_PACKAGES_BACKEND="libspdlog-dev libpcap-dev libprotobuf-dev protobuf-compiler libgmock-dev clang-11"
|
||||||
|
APT_PACKAGES_PYTHON="python3 python3-dev python3-pip python3-venv python3-setuptools python3-wheel libev-dev libevdev2"
|
||||||
|
APT_PACKAGES_WEB="nginx-light genisoimage man2html hfsutils dosfstools kpartx unzip unar disktype"
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# checks to run before entering the script main menu
|
# checks to run before entering the script main menu
|
||||||
@ -96,51 +101,36 @@ function installPackages() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y -qq \
|
sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y -qq \
|
||||||
build-essential \
|
$APT_PACKAGES_COMMON \
|
||||||
git \
|
$APT_PACKAGES_BACKEND \
|
||||||
libspdlog-dev \
|
$APT_PACKAGES_PYTHON \
|
||||||
libpcap-dev \
|
$APT_PACKAGES_WEB
|
||||||
libprotobuf-dev \
|
|
||||||
genisoimage \
|
|
||||||
python3 \
|
|
||||||
python3-dev \
|
|
||||||
python3-pip \
|
|
||||||
python3-venv \
|
|
||||||
python3-setuptools \
|
|
||||||
python3-wheel \
|
|
||||||
nginx-light \
|
|
||||||
protobuf-compiler \
|
|
||||||
bridge-utils \
|
|
||||||
libev-dev \
|
|
||||||
libevdev2 \
|
|
||||||
unzip \
|
|
||||||
unar \
|
|
||||||
disktype \
|
|
||||||
libgmock-dev \
|
|
||||||
man2html \
|
|
||||||
hfsutils \
|
|
||||||
dosfstools \
|
|
||||||
kpartx \
|
|
||||||
clang-11
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# install Debian packges for RaSCSI standalone
|
# install Debian packages for RaSCSI standalone
|
||||||
function installPackagesStandalone() {
|
function installPackagesStandalone() {
|
||||||
if [[ $SKIP_PACKAGES ]]; then
|
if [[ $SKIP_PACKAGES ]]; then
|
||||||
echo "Skipping package installation"
|
echo "Skipping package installation"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y -qq \
|
sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y -qq \
|
||||||
build-essential \
|
$APT_PACKAGES_COMMON \
|
||||||
git \
|
$APT_PACKAGES_BACKEND
|
||||||
libspdlog-dev \
|
|
||||||
libpcap-dev \
|
|
||||||
libprotobuf-dev \
|
|
||||||
protobuf-compiler \
|
|
||||||
libgmock-dev \
|
|
||||||
clang-11
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# install Debian packages for RaSCSI web UI standalone
|
||||||
|
function installPackagesWeb() {
|
||||||
|
if [[ $SKIP_PACKAGES ]]; then
|
||||||
|
echo "Skipping package installation"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y -qq \
|
||||||
|
$APT_PACKAGES_COMMON \
|
||||||
|
$APT_PACKAGES_PYTHON \
|
||||||
|
$APT_PACKAGES_WEB
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# cache the pip packages
|
# cache the pip packages
|
||||||
function cachePipPackages(){
|
function cachePipPackages(){
|
||||||
pushd $WEB_INSTALL_PATH
|
pushd $WEB_INSTALL_PATH
|
||||||
@ -1363,7 +1353,7 @@ function runChoice() {
|
|||||||
sudoCheck
|
sudoCheck
|
||||||
createCfgDir
|
createCfgDir
|
||||||
updateRaScsiGit
|
updateRaScsiGit
|
||||||
installPackages
|
installPackagesWeb
|
||||||
installHfdisk
|
installHfdisk
|
||||||
fetchHardDiskDrivers
|
fetchHardDiskDrivers
|
||||||
preparePythonCommon
|
preparePythonCommon
|
||||||
@ -1394,6 +1384,10 @@ function runChoice() {
|
|||||||
shareImagesWithNetatalk
|
shareImagesWithNetatalk
|
||||||
echo "Configuring AppleShare File Server - Complete!"
|
echo "Configuring AppleShare File Server - Complete!"
|
||||||
;;
|
;;
|
||||||
|
15)
|
||||||
|
installPackagesStandalone
|
||||||
|
compileRaScsi
|
||||||
|
;;
|
||||||
-h|--help|h|help)
|
-h|--help|h|help)
|
||||||
showMenu
|
showMenu
|
||||||
;;
|
;;
|
||||||
@ -1439,6 +1433,7 @@ function showMenu() {
|
|||||||
echo " 13) Enable or disable RaSCSI Web Interface authentication"
|
echo " 13) Enable or disable RaSCSI Web Interface authentication"
|
||||||
echo "EXPERIMENTAL FEATURES"
|
echo "EXPERIMENTAL FEATURES"
|
||||||
echo " 14) Share the images dir over AppleShare (requires Netatalk)"
|
echo " 14) Share the images dir over AppleShare (requires Netatalk)"
|
||||||
|
echo " 15) Compile RaSCSI binaries"
|
||||||
}
|
}
|
||||||
|
|
||||||
# parse arguments passed to the script
|
# parse arguments passed to the script
|
||||||
@ -1454,7 +1449,7 @@ while [ "$1" != "" ]; do
|
|||||||
CONNECT_TYPE=$VALUE
|
CONNECT_TYPE=$VALUE
|
||||||
;;
|
;;
|
||||||
-r | --run_choice)
|
-r | --run_choice)
|
||||||
if ! [[ $VALUE =~ ^[1-9][0-9]?$ && $VALUE -ge 1 && $VALUE -le 14 ]]; then
|
if ! [[ $VALUE =~ ^[1-9][0-9]?$ && $VALUE -ge 1 && $VALUE -le 15 ]]; then
|
||||||
echo "ERROR: The run choice parameter must have a numeric value between 1 and 14"
|
echo "ERROR: The run choice parameter must have a numeric value between 1 and 14"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
@ -47,6 +47,13 @@ class FileCmds:
|
|||||||
self.token = token
|
self.token = token
|
||||||
self.locale = locale
|
self.locale = locale
|
||||||
|
|
||||||
|
def send_pb_command(self, command):
|
||||||
|
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
||||||
|
# TODO: Uncouple/move to common dependency
|
||||||
|
logging.debug(self.ractl.format_pb_command(command))
|
||||||
|
|
||||||
|
return self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||||
|
|
||||||
# noinspection PyMethodMayBeStatic
|
# noinspection PyMethodMayBeStatic
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
def list_files(self, file_types, dir_path):
|
def list_files(self, file_types, dir_path):
|
||||||
@ -89,7 +96,7 @@ class FileCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.locale
|
command.params["locale"] = self.locale
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
|
|
||||||
@ -168,7 +175,7 @@ class FileCmds:
|
|||||||
command.params["size"] = str(size)
|
command.params["size"] = str(size)
|
||||||
command.params["read_only"] = "false"
|
command.params["read_only"] = "false"
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
@ -186,7 +193,7 @@ class FileCmds:
|
|||||||
|
|
||||||
command.params["file"] = file_name
|
command.params["file"] = file_name
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
@ -205,7 +212,7 @@ class FileCmds:
|
|||||||
command.params["from"] = file_name
|
command.params["from"] = file_name
|
||||||
command.params["to"] = new_file_name
|
command.params["to"] = new_file_name
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
@ -224,7 +231,7 @@ class FileCmds:
|
|||||||
command.params["from"] = file_name
|
command.params["from"] = file_name
|
||||||
command.params["to"] = new_file_name
|
command.params["to"] = new_file_name
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
|
@ -5,6 +5,7 @@ Module for commands sent to the RaSCSI backend service.
|
|||||||
import rascsi_interface_pb2 as proto
|
import rascsi_interface_pb2 as proto
|
||||||
from rascsi.return_codes import ReturnCodes
|
from rascsi.return_codes import ReturnCodes
|
||||||
from rascsi.socket_cmds import SocketCmds
|
from rascsi.socket_cmds import SocketCmds
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class RaCtlCmds:
|
class RaCtlCmds:
|
||||||
@ -17,6 +18,12 @@ class RaCtlCmds:
|
|||||||
self.token = token
|
self.token = token
|
||||||
self.locale = locale
|
self.locale = locale
|
||||||
|
|
||||||
|
def send_pb_command(self, command):
|
||||||
|
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
||||||
|
logging.debug(self.format_pb_command(command))
|
||||||
|
|
||||||
|
return self.sock_cmd.send_pb_command(command.SerializeToString())
|
||||||
|
|
||||||
def get_server_info(self):
|
def get_server_info(self):
|
||||||
"""
|
"""
|
||||||
Sends a SERVER_INFO command to the server.
|
Sends a SERVER_INFO command to the server.
|
||||||
@ -35,7 +42,7 @@ class RaCtlCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.locale
|
command.params["locale"] = self.locale
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
version = (
|
version = (
|
||||||
@ -93,7 +100,7 @@ class RaCtlCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.locale
|
command.params["locale"] = self.locale
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
scsi_ids = []
|
scsi_ids = []
|
||||||
@ -114,7 +121,7 @@ class RaCtlCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.locale
|
command.params["locale"] = self.locale
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
ifs = result.network_interfaces_info.name
|
ifs = result.network_interfaces_info.name
|
||||||
@ -133,7 +140,7 @@ class RaCtlCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.locale
|
command.params["locale"] = self.locale
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
device_types = {}
|
device_types = {}
|
||||||
@ -199,7 +206,7 @@ class RaCtlCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.token
|
command.params["locale"] = self.token
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
images_dir = result.image_files_info.default_image_folder
|
images_dir = result.image_files_info.default_image_folder
|
||||||
@ -273,7 +280,7 @@ class RaCtlCmds:
|
|||||||
|
|
||||||
command.devices.append(devices)
|
command.devices.append(devices)
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
@ -295,7 +302,7 @@ class RaCtlCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.locale
|
command.params["locale"] = self.locale
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
@ -310,7 +317,7 @@ class RaCtlCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.locale
|
command.params["locale"] = self.locale
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
@ -332,7 +339,7 @@ class RaCtlCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.locale
|
command.params["locale"] = self.locale
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
@ -360,7 +367,7 @@ class RaCtlCmds:
|
|||||||
device.unit = int(unit)
|
device.unit = int(unit)
|
||||||
command.devices.append(device)
|
command.devices.append(device)
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
|
|
||||||
@ -430,7 +437,7 @@ class RaCtlCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.locale
|
command.params["locale"] = self.locale
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
@ -447,7 +454,7 @@ class RaCtlCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.locale
|
command.params["locale"] = self.locale
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
@ -466,7 +473,7 @@ class RaCtlCmds:
|
|||||||
command.params["token"] = self.token
|
command.params["token"] = self.token
|
||||||
command.params["locale"] = self.locale
|
command.params["locale"] = self.locale
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
@ -480,7 +487,37 @@ class RaCtlCmds:
|
|||||||
command = proto.PbCommand()
|
command = proto.PbCommand()
|
||||||
command.operation = proto.PbOperation.CHECK_AUTHENTICATION
|
command.operation = proto.PbOperation.CHECK_AUTHENTICATION
|
||||||
|
|
||||||
data = self.sock_cmd.send_pb_command(command.SerializeToString())
|
data = self.send_pb_command(command)
|
||||||
result = proto.PbResult()
|
result = proto.PbResult()
|
||||||
result.ParseFromString(data)
|
result.ParseFromString(data)
|
||||||
return {"status": result.status, "msg": result.msg}
|
return {"status": result.status, "msg": result.msg}
|
||||||
|
|
||||||
|
def format_pb_command(self, command):
|
||||||
|
"""
|
||||||
|
Formats the Protobuf command for output
|
||||||
|
"""
|
||||||
|
message = f"Sending: {proto.PbOperation.Name(command.operation)}"
|
||||||
|
|
||||||
|
params = {
|
||||||
|
name: "***" if name == "token" else value
|
||||||
|
for (name, value) in sorted(command.params.items())
|
||||||
|
}
|
||||||
|
message += f", params: {params}"
|
||||||
|
|
||||||
|
for device in command.devices:
|
||||||
|
formatted_device = {
|
||||||
|
key: value
|
||||||
|
for (key, value) in {
|
||||||
|
"id": device.id,
|
||||||
|
"unit": device.unit,
|
||||||
|
"type": proto.PbDeviceType.Name(device.type) if device.type else None,
|
||||||
|
"params": device.params,
|
||||||
|
"vendor": device.vendor,
|
||||||
|
"product": device.product,
|
||||||
|
"revision": device.revision,
|
||||||
|
}.items()
|
||||||
|
if key == "id" or value
|
||||||
|
}
|
||||||
|
message += f", device: {formatted_device}"
|
||||||
|
|
||||||
|
return message
|
||||||
|
0
python/web/mock/bin/brctl
Normal file → Executable file
0
python/web/mock/bin/brctl
Normal file → Executable file
2
python/web/mock/bin/git
Executable file
2
python/web/mock/bin/git
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 0
|
0
python/web/mock/bin/journalctl
Normal file → Executable file
0
python/web/mock/bin/journalctl
Normal file → Executable file
0
python/web/mock/bin/systemctl
Normal file → Executable file
0
python/web/mock/bin/systemctl
Normal file → Executable file
@ -1,4 +1,4 @@
|
|||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "--junitxml=report.xml"
|
addopts = "--junitxml=tests/output/report.xml --log-file=tests/output/pytest.log"
|
||||||
log_cli = true
|
log_cli = true
|
||||||
log_cli_level = "warn"
|
log_cli_level = "warn"
|
@ -709,7 +709,11 @@
|
|||||||
<label for="locale">{{ _("Language:") }}</label>
|
<label for="locale">{{ _("Language:") }}</label>
|
||||||
<select name="locale" id="locale">
|
<select name="locale" id="locale">
|
||||||
{% for locale in locales %}
|
{% for locale in locales %}
|
||||||
<option value="{{ locale.language }}">
|
{% if locale.language == env['locale'] %}
|
||||||
|
<option value="{{ locale.language }}" selected="selected">
|
||||||
|
{% else %}
|
||||||
|
<option value="{{ locale.language }}">
|
||||||
|
{% endif %}
|
||||||
{{ locale.language }} - {{ locale.display_name }}
|
{{ locale.language }} - {{ locale.display_name }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
Module for the Flask app rendering and endpoints
|
Module for the Flask app rendering and endpoints
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import logging
|
import logging
|
||||||
|
import logging.config
|
||||||
import argparse
|
import argparse
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@ -168,16 +168,16 @@ def get_locale():
|
|||||||
"""
|
"""
|
||||||
Uses the session language, or tries to detect based on accept-languages header
|
Uses the session language, or tries to detect based on accept-languages header
|
||||||
"""
|
"""
|
||||||
try:
|
session_locale = session.get("language")
|
||||||
language = session["language"]
|
if session_locale:
|
||||||
except KeyError:
|
return session_locale
|
||||||
language = ""
|
|
||||||
logging.info("The default locale could not be detected. Falling back to English.")
|
client_locale = request.accept_languages.best_match(LANGUAGES)
|
||||||
if language:
|
if client_locale:
|
||||||
return language
|
return client_locale
|
||||||
# Hardcoded fallback to "en" when the user agent does not send an accept-language header
|
|
||||||
language = request.accept_languages.best_match(LANGUAGES) or "en"
|
logging.info("The default locale could not be detected. Falling back to English.")
|
||||||
return language
|
return "en"
|
||||||
|
|
||||||
|
|
||||||
def get_supported_locales():
|
def get_supported_locales():
|
||||||
@ -994,7 +994,7 @@ def create_file():
|
|||||||
|
|
||||||
message_postfix = ""
|
message_postfix = ""
|
||||||
|
|
||||||
# Formatting and injecting driver, if one is choosen
|
# Formatting and injecting driver, if one is chosen
|
||||||
if drive_format:
|
if drive_format:
|
||||||
volume_name = f"HD {size / 1024 / 1024:0.0f}M"
|
volume_name = f"HD {size / 1024 / 1024:0.0f}M"
|
||||||
known_formats = [
|
known_formats = [
|
||||||
@ -1285,6 +1285,11 @@ def change_theme():
|
|||||||
return response(message=_("Theme changed to '%(theme)s'.", theme=theme))
|
return response(message=_("Theme changed to '%(theme)s'.", theme=theme))
|
||||||
|
|
||||||
|
|
||||||
|
@APP.route("/healthcheck", methods=["GET"])
|
||||||
|
def healthcheck():
|
||||||
|
return "", 200
|
||||||
|
|
||||||
|
|
||||||
@APP.before_first_request
|
@APP.before_first_request
|
||||||
def detect_locale():
|
def detect_locale():
|
||||||
"""
|
"""
|
||||||
@ -1296,6 +1301,22 @@ def detect_locale():
|
|||||||
file_cmd.locale = session["language"]
|
file_cmd.locale = session["language"]
|
||||||
|
|
||||||
|
|
||||||
|
@APP.before_request
|
||||||
|
def log_http_request():
|
||||||
|
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
||||||
|
message = f"HTTP request: {request.method} {request.path}"
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if request.path == "/login":
|
||||||
|
message += " (<hidden>)"
|
||||||
|
elif len(request.get_data()) > 100:
|
||||||
|
message += f" (payload: {request.get_data()[:100]} <truncated>)"
|
||||||
|
else:
|
||||||
|
message += f" (payload: {request.get_data()})"
|
||||||
|
|
||||||
|
logging.debug(message)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
APP.secret_key = "rascsi_is_awesome_insecure_secret_key"
|
APP.secret_key = "rascsi_is_awesome_insecure_secret_key"
|
||||||
APP.config["SESSION_TYPE"] = "filesystem"
|
APP.config["SESSION_TYPE"] = "filesystem"
|
||||||
@ -1347,6 +1368,27 @@ if __name__ == "__main__":
|
|||||||
arguments = parser.parse_args()
|
arguments = parser.parse_args()
|
||||||
APP.config["RASCSI_TOKEN"] = arguments.password
|
APP.config["RASCSI_TOKEN"] = arguments.password
|
||||||
|
|
||||||
|
logging.config.dictConfig(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"formatters": {
|
||||||
|
"default": {
|
||||||
|
"format": "[%(asctime)s] [%(levelname)s] %(filename)s:%(lineno)s %(message)s",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"wsgi": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "default",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"level": arguments.log_level.upper(),
|
||||||
|
"handlers": ["wsgi"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
sock_cmd = SocketCmdsFlask(host=arguments.rascsi_host, port=arguments.rascsi_port)
|
sock_cmd = SocketCmdsFlask(host=arguments.rascsi_host, port=arguments.rascsi_port)
|
||||||
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=APP.config["RASCSI_TOKEN"])
|
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=APP.config["RASCSI_TOKEN"])
|
||||||
file_cmd = FileCmds(sock_cmd=sock_cmd, ractl=ractl_cmd, token=APP.config["RASCSI_TOKEN"])
|
file_cmd = FileCmds(sock_cmd=sock_cmd, ractl=ractl_cmd, token=APP.config["RASCSI_TOKEN"])
|
||||||
@ -1365,14 +1407,9 @@ if __name__ == "__main__":
|
|||||||
APP.config["RASCSI_DRIVE_PROPERTIES"] = []
|
APP.config["RASCSI_DRIVE_PROPERTIES"] = []
|
||||||
logging.warning("Could not read drive properties from %s", DRIVE_PROPERTIES_FILE)
|
logging.warning("Could not read drive properties from %s", DRIVE_PROPERTIES_FILE)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.info("Starting WSGI server...")
|
||||||
stream=sys.stdout,
|
|
||||||
format="%(asctime)s %(levelname)s %(filename)s:%(lineno)s %(message)s",
|
|
||||||
level=arguments.log_level.upper(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if arguments.dev_mode:
|
if arguments.dev_mode:
|
||||||
print("Running rascsi-web in development mode ...")
|
logging.info("Dev mode enabled")
|
||||||
APP.debug = True
|
APP.debug = True
|
||||||
from werkzeug.debug import DebuggedApplication
|
from werkzeug.debug import DebuggedApplication
|
||||||
|
|
||||||
@ -1381,5 +1418,4 @@ if __name__ == "__main__":
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
print("Serving rascsi-web...")
|
|
||||||
bjoern.run(APP, "0.0.0.0", arguments.port)
|
bjoern.run(APP, "0.0.0.0", arguments.port)
|
||||||
|
@ -324,10 +324,17 @@ def browser_supports_modern_themes():
|
|||||||
]
|
]
|
||||||
|
|
||||||
current_ua_family = user_agent["user_agent"]["family"]
|
current_ua_family = user_agent["user_agent"]["family"]
|
||||||
current_ua_version = float(user_agent["user_agent"]["major"])
|
current_ua_version = user_agent["user_agent"]["major"]
|
||||||
logging.info(f"Identified browser as family={current_ua_family}, version={current_ua_version}")
|
logging.info(f"Identified browser as family={current_ua_family}, version={current_ua_version}")
|
||||||
|
|
||||||
|
# Supported browsers cannot be identified without a version
|
||||||
|
if not current_ua_version:
|
||||||
|
return False
|
||||||
|
|
||||||
for supported_browser, supported_version in supported_browsers:
|
for supported_browser, supported_version in supported_browsers:
|
||||||
if current_ua_family == supported_browser and current_ua_version >= supported_version:
|
if (
|
||||||
|
current_ua_family == supported_browser
|
||||||
|
and float(current_ua_version) >= supported_version
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
@ -121,13 +121,14 @@ while [ "$1" != "" ]; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
PYTHON_COMMON_PATH=$(dirname $PWD)/common/src
|
PYTHON_COMMON_PATH=$(dirname $PWD)/common/src
|
||||||
echo "Starting web server for RaSCSI Web Interface..."
|
|
||||||
export PYTHONPATH=$PWD/src:${PYTHON_COMMON_PATH}
|
export PYTHONPATH=$PWD/src:${PYTHON_COMMON_PATH}
|
||||||
cd src
|
cd src
|
||||||
|
|
||||||
if [[ $ARG_DEV_MODE ]]; then
|
if [[ $ARG_DEV_MODE ]]; then
|
||||||
|
echo "Starting web UI (dev mode) ..."
|
||||||
watchmedo auto-restart --directory=../../ --pattern=*.py --recursive -- \
|
watchmedo auto-restart --directory=../../ --pattern=*.py --recursive -- \
|
||||||
python3 web.py ${ARG_PORT} ${ARG_PASSWORD} ${ARG_RASCSI_HOST} ${ARG_RASCSI_PORT} ${ARG_LOG_LEVEL} ${ARG_DEV_MODE}
|
python3 web.py ${ARG_PORT} ${ARG_PASSWORD} ${ARG_RASCSI_HOST} ${ARG_RASCSI_PORT} ${ARG_LOG_LEVEL} ${ARG_DEV_MODE}
|
||||||
else
|
else
|
||||||
|
echo "Starting web UI ..."
|
||||||
python3 web.py ${ARG_PORT} ${ARG_PASSWORD} ${ARG_RASCSI_HOST} ${ARG_RASCSI_PORT} ${ARG_LOG_LEVEL} ${ARG_DEV_MODE}
|
python3 web.py ${ARG_PORT} ${ARG_PASSWORD} ${ARG_RASCSI_HOST} ${ARG_RASCSI_PORT} ${ARG_LOG_LEVEL} ${ARG_DEV_MODE}
|
||||||
fi
|
fi
|
@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
|
import datetime
|
||||||
|
|
||||||
SCSI_ID = 6
|
SCSI_ID = 6
|
||||||
FILE_SIZE_1_MIB = 1048576
|
FILE_SIZE_1_MIB = 1048576
|
||||||
@ -13,7 +14,7 @@ def create_test_image(request, http_client):
|
|||||||
images = []
|
images = []
|
||||||
|
|
||||||
def create(image_type="hds", size=1, auto_delete=True):
|
def create(image_type="hds", size=1, auto_delete=True):
|
||||||
file_prefix = str(uuid.uuid4())
|
file_prefix = f"{request.function.__name__}___{uuid.uuid4()}"
|
||||||
file_name = f"{file_prefix}.{image_type}"
|
file_name = f"{file_prefix}.{image_type}"
|
||||||
|
|
||||||
response = http_client.post(
|
response = http_client.post(
|
||||||
@ -29,13 +30,19 @@ def create_test_image(request, http_client):
|
|||||||
raise Exception("Failed to create temporary image")
|
raise Exception("Failed to create temporary image")
|
||||||
|
|
||||||
if auto_delete:
|
if auto_delete:
|
||||||
images.append(file_name)
|
images.append(
|
||||||
|
{
|
||||||
|
"file_name": file_name,
|
||||||
|
"function": request.function,
|
||||||
|
"created": str(datetime.datetime.now()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return file_name
|
return file_name
|
||||||
|
|
||||||
def delete():
|
def delete():
|
||||||
for image in images:
|
for image in images:
|
||||||
response = http_client.post("/files/delete", data={"file_name": image})
|
response = http_client.post("/files/delete", data={"file_name": image["file_name"]})
|
||||||
if response.status_code != 200 or response.json()["status"] != STATUS_SUCCESS:
|
if response.status_code != 200 or response.json()["status"] != STATUS_SUCCESS:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
f"Failed to auto-delete file created with create_test_image fixture: {image}"
|
f"Failed to auto-delete file created with create_test_image fixture: {image}"
|
||||||
|
@ -112,3 +112,9 @@ def test_show_manpage(http_client):
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "rascsi" in response_data["data"]["manpage"]
|
assert "rascsi" in response_data["data"]["manpage"]
|
||||||
|
|
||||||
|
|
||||||
|
# route("/healthcheck", methods=["GET"])
|
||||||
|
def test_healthcheck(http_client):
|
||||||
|
response = http_client.get("/healthcheck")
|
||||||
|
assert response.status_code == 200
|
||||||
|
@ -5,7 +5,7 @@ import os
|
|||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
default_base_url = "http://rascsi_web" if os.getenv("DOCKER") else "http://localhost:8080"
|
default_base_url = "http://web" if os.getenv("DOCKER") else "http://localhost:8080"
|
||||||
|
|
||||||
parser.addoption("--home_dir", action="store", default="/home/pi")
|
parser.addoption("--home_dir", action="store", default="/home/pi")
|
||||||
parser.addoption("--base_url", action="store", default=default_base_url)
|
parser.addoption("--base_url", action="store", default=default_base_url)
|
||||||
|
0
python/web/tests/output/.gitkeep
Normal file
0
python/web/tests/output/.gitkeep
Normal file
Loading…
Reference in New Issue
Block a user