Merge tag 'v0.1.0' into develop

First working version with installer
This commit is contained in:
Jörn-Michael Miehe 2023-11-16 16:07:47 +01:00
commit 092f336870
13 changed files with 697 additions and 82 deletions

View file

@ -17,12 +17,31 @@ RUN yarn install --production false
COPY ui ./ COPY ui ./
RUN yarn build --dest /tmp/ovdashboard_ui/html RUN yarn build --dest /tmp/ovdashboard_ui/html
######################
# python preparation #
######################
ARG PYTHON_VERSION
FROM python:${PYTHON_VERSION} as uvicorn-gunicorn
# where credit is due ...
LABEL maintainer="Sebastian Ramirez <tiangolo@gmail.com>"
WORKDIR /usr/local/share/uvicorn-gunicorn
# install uvicorn-gunicorn
COPY "./deploy/mini-tiangolo/" "."
RUN set -ex; \
chmod +x start.sh; \
python3 -m pip --no-cache-dir install gunicorn;
CMD ["/usr/local/share/uvicorn-gunicorn/start.sh"]
########### ###########
# web app # # web app #
########### ###########
ARG PYTHON_VERSION FROM uvicorn-gunicorn AS production
FROM tiangolo/uvicorn-gunicorn:python${PYTHON_VERSION} AS production
# env setup # env setup
WORKDIR /usr/local/src/ovdashboard_api WORKDIR /usr/local/src/ovdashboard_api
@ -38,13 +57,13 @@ RUN set -ex; \
export DEBIAN_FRONTEND=noninteractive; \ export DEBIAN_FRONTEND=noninteractive; \
apt-get update; apt-get install --yes --no-install-recommends \ apt-get update; apt-get install --yes --no-install-recommends \
libmagic1 \ libmagic1 \
# need to build hiredis
gcc \
libc-dev \
; rm -rf /var/lib/apt/lists/*; \ ; rm -rf /var/lib/apt/lists/*; \
\ \
# remove example app
rm -rf /app; \
\
# install ovdashboard_api # install ovdashboard_api
python -m pip --no-cache-dir install ./ python3 -m pip --no-cache-dir install ./
# add prepared ovdashboard_ui # add prepared ovdashboard_ui
COPY --from=build-ui /tmp/ovdashboard_ui /usr/local/share/ovdashboard_ui COPY --from=build-ui /tmp/ovdashboard_ui /usr/local/share/ovdashboard_ui

106
README.md
View file

@ -12,7 +12,7 @@ All that matters, at one glance! <br />
*Date and Time &ndash; Upcoming Events &ndash; Public Announcements &ndash; News &ndash; Pictures* *Date and Time &ndash; Upcoming Events &ndash; Public Announcements &ndash; News &ndash; Pictures*
- **Easy Install** <br /> - **Easy Install** <br />
Set up a RaspberryPi, run the [installer script](TODO), done! Set up a RaspberryPi, run the [installer script](./deploy/install.sh), done!
- **DAV Server Interface** <br /> - **DAV Server Interface** <br />
Update your content anytime, from anywhere! Update your content anytime, from anywhere!
@ -36,22 +36,78 @@ The Dashboard UI is created using [Vue](https://vuejs.org/) and the [Vuetify](ht
## Quick Start ## Quick Start
### Prerequisites
Make sure you have a WebDAV and CalDAV account available. Make sure you have a WebDAV and CalDAV account available.
For an all-in-one solution, consider setting up an account on a [Nextcloud](https://nextcloud.com/) instance! <br /> For an all-in-one solution, consider setting up an account on a [Nextcloud](https://nextcloud.com/) instance! <br />
On your WebvDAV account, create a resource (directory) named `ovdashboard`. On your WebDAV account, create a resource (directory) named `ovdashboard`[^1].
The intended installation method is as follows: Your target device should be a Raspberry Pi Model 3 or later[^2]. You will need some accessories:
1. Have a Raspberry Pi (3 or later) connected to a HDMI screen - microSD card, class 10 or UHS (min. 8 GB)
1. Boot up "Raspberry Pi OS" (64 bit) and connect the Pi to your local network - network connectivity (bring WiFi credentials if applicable)
1. Download (and review) the [OVDashboard installer](TODO), then run it from a terminal - connection to a HDMI screen
> If you feel adventurous and want to skip the script review, run `curl --proto '=https' --tlsv1.2 -sSf 'TODO' | sh` instead.
1. Reboot the Raspberry Pi
For a better understanding of your newly created OVDashboard, refer to the [about section](TODO). It is also heavily advisable that you log into your device using SSH, so you should get another device (PC, tablet or smartphone) onto the same network as your OVDashboard.
[^1]: if named differently, you will need to adjust your compose file later on
[^2]: other devices will also work, but might require extra installation steps
### Install Base System
OVDashboard is designed to run on a `DietPi` installation. Full installation documentation is available [at dietpi.com](https://dietpi.com/docs/deploy/). To quickly get up and running:
1. Download Image from [dietpi.com/#download](https://dietpi.com/#download)
1. Uncompress Image and flash onto SD card you might need "7zip", "balenaEtcher" and/or other tools
1. Check the SD card, open "dietpi.txt" and change some options (full documentation [here](https://dietpi.com/docs/usage/#options-within-the-file)):
- For WiFi, use `AUTO_SETUP_NET_WIFI_ENABLED=1` and `AUTO_SETUP_NET_WIFI_COUNTRY_CODE=DE`, and also edit "dietpi-wifi.txt" like `aWIFI_SSID[0]='OV WLAN'` and `aWIFI_KEY[0]='Strong_pa55w0rd'`
- System options, e.g. `AUTO_SETUP_AUTOMATED=1`, `AUTO_SETUP_NET_HOSTNAME=OVDashboard`, `AUTO_SETUP_GLOBAL_PASSWORD=dietpi`, `AUTO_SETUP_LOCALE=de_DE.UTF-8`, `AUTO_SETUP_KEYBOARD_LAYOUT=de`, `AUTO_SETUP_TIMEZONE=Europe/Berlin`, `CONFIG_SERIAL_CONSOLE_ENABLE=0`
1. Be sure to at least change the password (and remember it 🙂️), then put the SD card into your device and boot it. Let the first time setup finish, it will take a bit. It will let you know when it is done!
1. Log into your device using SSH. By default, that's user name `root` and password `dietpi`
### Install OVDashboard
Download (and review) the [OVDashboard install script](//code.yavook.de/OEKZident.de/ovdashboard/raw/branch/master/deploy/install.sh), then run it from a terminal.
This can all be done after logging into your prepared device:
- The safe way:
1. download: `wget 'https://code.yavook.de/OEKZident.de/ovdashboard/raw/branch/master/deploy/install.sh'`
1. read/edit: `nano install.sh`
1. execute: `sh install.sh`
- If you feel adventurous and do not want to review the script, just run `sh <( curl --proto '=https' --tlsv1.2 -sSf 'https://code.yavook.de/OEKZident.de/ovdashboard/raw/branch/master/deploy/install.sh' )`
> There will be some prompts during installation.
>
> - DietPi might ask: "Would you like DietPi to apply the recommended GPU memory split?". Choose **Yes**.
> - DietPi will ask: "Would you like to configure the DietPi-AutoStart option?". **Cancel** that, this choice does not matter as it is changed by the installer.
> - DietPi will ask: "Would you like to join DietPi-Survey?". It's up to you, but I'd suggest to **opt OUT**.
> - The installer will ask for the connected screen's resolution. The default values should be fine, you can likely just hit **Return** here.
> - The installer will ask for "display languages". This will affect some details on the connected screen. <br />
For German, enter `de-DE,de,en-US,en`.
> - The installer will ask you to "review the Docker Compose file" before starting the services. You will want to edit `WEBDAV__HOST`, `WEBDAV__USERNAME` and `WEBDAV__PASSWORD` at least. <br />
Refer to [the "Settings"](TODO) for the full list of options.
Afterwards, reboot your device (`reboot` in the terminal).
If the install was successful, your OVDashboard should be showing up on the connected screen after rebooting.
> You will also be able to view your OVDashboard on any webbrowser in your network using `http://<device-ip>`. <br />
> The device IP is displayed in the lower right region of the OVDashboard.
For a better understanding of your newly created OVDashboard, refer to the [about section](#about-the-default-ovdashboard-deployment).
## Configuration ## Configuration
### "Config" in your WebDAV share: `config.txt`
<!-- TODO -->
### "Settings" on your Device: `/opt/ovdashboard/docker-compose.yml`
<!-- TODO -->
## Updating your Device
<!-- TODO `/opt/ovdashboard` -->
## Setup for development and contribution ## Setup for development and contribution
@ -60,31 +116,9 @@ Refer to the specific README files for [the API](./api/README.md) and [the UI](.
## About the "default" OVDashboard deployment ## About the "default" OVDashboard deployment
The default deployment is made up of three parts: The **installer** which runs once, prepares your device for OVDashboard usage and deploys the **client** and the **service**. Running the installer script carries out the following actions:
The **client** will connect to and display your OVDashboard. - install Chromium-Browser, Docker and Docker Compose
- create the OVDashboard project at `/opt/ovdashboard`
The **service** will run continuously and makes sure your OVDashboard is up to date. - start the OVDashboard project, this deploys the `ovdashboard` service from [`code.yavook.de`](https://code.yavook.de/OEKZident.de/-/packages/container/ovdashboard) and a [`redis` instance](https://redis.io/) to your device
- set up your device to auto-boot into Chromium "kiosk" mode to display the local OVDashboard
### The OVDashboard installer will:
- install some general-purpose packages
- make you set a new password, if you haven't yet
- install and set up Docker Engine and Mozilla Firefox
- (optionally) enable SSH and/or VNC access to your device
- create the OVDashboard configuration directory `/usr/local/etc/ovdashboard` on your device
- set up the OVDashboard service
- (optionally) set up your device to auto-run the OVDashboard client
### The OVDashboard service will:
- regularly pull the latest `docker` image from [`TODO`](TODO), containing both the OVDashboard API and UI
- ensure a container of that image is always running, using your OVDashboard configuration directory
### The OVDashboard client will:
- open Mozilla Firefox in full-screen mode and browse to the local OVDashboard UI
- enable auto-hiding your mouse cursor

View file

@ -18,12 +18,11 @@ class DAVSettings(BaseModel):
Connection to a DAV server. Connection to a DAV server.
""" """
protocol: str | None = None protocol: str = "https"
host: str | None = None host: str = "example.com"
path: str | None = None
username: str | None = None username: str = "ovd_user"
password: str | None = None password: str = "password"
cache_ttl: int = 60 * 10 cache_ttl: int = 60 * 10
@ -33,21 +32,34 @@ class DAVSettings(BaseModel):
Combined DAV URL. Combined DAV URL.
""" """
return f"{self.protocol}://{self.host}{self.path}" return f"{self.protocol}://{self.host}"
class WebDAVSettings(DAVSettings): _DAV_DEFAULT = DAVSettings().model_dump()
class CalDAVSettings(DAVSettings):
"""
Connection to a CalDAV server.
"""
path: str = "/remote.php/dav"
@property
def url(self) -> str:
"""
Combined DAV URL.
"""
return f"{super().url}{self.path}"
class WebDAVSettings(CalDAVSettings):
""" """
Connection to a WebDAV server. Connection to a WebDAV server.
""" """
protocol: str = "https" path: str = "/remote.php/webdav"
host: str = "example.com"
path: str = "/remote.php/dav"
prefix: str = "/ovdashboard"
username: str = "ovd_user"
password: str = "password"
config_filename: str = "config.txt" config_filename: str = "config.txt"
@ -62,7 +74,10 @@ class WebDAVSettings(DAVSettings):
Combined DAV URL. Combined DAV URL.
""" """
return f"{self.protocol}://{self.host}{self.path}{self.prefix}" return f"{super().url}{self.prefix}"
_WEBDAV_DEFAULT = WebDAVSettings().model_dump()
class RedisSettings(BaseModel): class RedisSettings(BaseModel):
@ -132,7 +147,7 @@ class Settings(BaseSettings):
# caldav settings # caldav settings
##### #####
caldav: DAVSettings = DAVSettings() caldav: CalDAVSettings = CalDAVSettings()
##### #####
# redis settings # redis settings
@ -141,37 +156,19 @@ class Settings(BaseSettings):
redis: RedisSettings = RedisSettings() redis: RedisSettings = RedisSettings()
@model_validator(mode="before") @model_validator(mode="before")
@classmethod
def validate_dav_settings(cls, data) -> dict[str, Any]: def validate_dav_settings(cls, data) -> dict[str, Any]:
assert isinstance(data, dict) assert isinstance(data, dict)
# ensure both settings dicts are created # ensure both settings dicts are created
for key in ("webdav", "caldav"): for key in ("webdav", "caldav"):
if key not in data: data[key] = data.get(key, {})
data[key] = {}
default_dav = DAVSettings( for key in _DAV_DEFAULT:
protocol="https", # if "webdav" value is not specified, use default value
host="example.com", data["webdav"][key] = data["webdav"].get(key, _WEBDAV_DEFAULT[key])
username="ovdashboard",
password="secret",
).model_dump()
for key in default_dav:
# if "webdav" value is not specified, use default
if key not in data["webdav"] or data["webdav"][key] is None:
data["webdav"][key] = default_dav[key]
# if "caldav" value is not specified, use "webdav" value # if "caldav" value is not specified, use "webdav" value
if key not in data["caldav"] or data["caldav"][key] is None: data["caldav"][key] = data["caldav"].get(key, data["webdav"][key])
data["caldav"][key] = data["webdav"][key]
# add default "path"s if None
if data["webdav"]["path"] is None:
data["webdav"]["path"] = "/remote.php/webdav"
if data["caldav"]["path"] is None:
data["caldav"]["path"] = "/remote.php/dav"
return data return data

View file

@ -63,13 +63,23 @@ async def find_images_by_prefix(
response_class=StreamingResponse, response_class=StreamingResponse,
) )
async def get_image_by_prefix( async def get_image_by_prefix(
cfg: Config = Depends(get_config),
remote_path: str = Depends(RP_IMAGE), remote_path: str = Depends(RP_IMAGE),
name: str = Depends(LM_IMAGE.getter.func), name: str = Depends(LM_IMAGE.getter.func),
) -> StreamingResponse: ) -> StreamingResponse:
cfg = await get_config()
img = Image.open(BytesIO(await WebDAV.read_bytes(f"{remote_path}/{name}"))) img = Image.open(BytesIO(await WebDAV.read_bytes(f"{remote_path}/{name}")))
img_buffer = BytesIO() img_buffer = BytesIO()
width, height = img.size
target_height = cfg.image.height
target_width = int(width * target_height / height)
img = img.resize(
size=(target_width, target_height),
resample=Image.LANCZOS,
)
img.save(img_buffer, **cfg.image.save_params) img.save(img_buffer, **cfg.image.save_params)
img_buffer.seek(0) img_buffer.seek(0)

46
api/poetry.lock generated
View file

@ -522,6 +522,17 @@ files = [
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
] ]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]] [[package]]
name = "isort" name = "isort"
version = "5.12.0" version = "5.12.0"
@ -787,6 +798,21 @@ files = [
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
[[package]]
name = "pluggy"
version = "1.3.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
{file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "pycodestyle" name = "pycodestyle"
version = "2.11.1" version = "2.11.1"
@ -961,6 +987,26 @@ files = [
{file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"},
] ]
[[package]]
name = "pytest"
version = "7.4.3"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
{file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
[package.extras]
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.8.2" version = "2.8.2"

View file

@ -26,6 +26,7 @@ black = "^23.10.1"
flake8 = "^6.1.0" flake8 = "^6.1.0"
flake8-isort = "^6.1.0" flake8-isort = "^6.1.0"
types-cachetools = "^5.3.0.6" types-cachetools = "^5.3.0.6"
pytest = "^7.4.3"
[build-system] [build-system]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"

224
api/test/test_settings.py Normal file
View file

@ -0,0 +1,224 @@
import os
import pytest
from ovdashboard_api.core.settings import Settings
@pytest.fixture(autouse=True, scope="function")
def patch_settings_env_file(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(os, "environ", {})
monkeypatch.delitem(Settings.model_config, "env_file")
def test_empty():
s = Settings.model_validate({})
assert s.log_level == "INFO"
assert s.production_mode is False
assert s.ui_directory == "/usr/local/share/ovdashboard_ui/html"
assert s.ping_host == "1.0.0.0"
assert s.ping_port == 1
assert s.openapi_url == "/api/openapi.json"
assert s.docs_url == "/api/docs"
assert s.redoc_url == "/api/redoc"
# webdav
assert s.webdav.protocol == "https"
assert s.webdav.host == "example.com"
assert s.webdav.username == "ovd_user"
assert s.webdav.password == "password"
assert s.webdav.cache_ttl == 600
assert s.webdav.path == "/remote.php/webdav"
assert s.webdav.config_filename == "config.txt"
assert s.webdav.disable_check is False
assert s.webdav.retries == 20
assert s.webdav.retry_delay == 30
assert s.webdav.prefix == "/ovdashboard"
# caldav
assert s.caldav.protocol == "https"
assert s.caldav.host == "example.com"
assert s.caldav.username == "ovd_user"
assert s.caldav.password == "password"
assert s.caldav.cache_ttl == 600
assert s.caldav.path == "/remote.php/dav"
def test_prod():
s = Settings.model_validate({"production_mode": True})
assert s.log_level == "INFO"
assert s.production_mode is True
assert s.openapi_url is None
assert s.docs_url is None
assert s.redoc_url is None
def test_set_caldav():
s = Settings.model_validate(
{
"caldav": {
"protocol": "cd_protocol",
"host": "cd_host",
"username": "cd_username",
"password": "cd_password",
"cache_ttl": "0",
"path": "cd_path",
}
}
)
# webdav
assert s.webdav.protocol == "https"
assert s.webdav.host == "example.com"
assert s.webdav.username == "ovd_user"
assert s.webdav.password == "password"
assert s.webdav.cache_ttl == 600
assert s.webdav.path == "/remote.php/webdav"
assert s.webdav.config_filename == "config.txt"
assert s.webdav.disable_check is False
assert s.webdav.retries == 20
assert s.webdav.retry_delay == 30
assert s.webdav.prefix == "/ovdashboard"
# caldav
assert s.caldav.protocol == "cd_protocol"
assert s.caldav.host == "cd_host"
assert s.caldav.username == "cd_username"
assert s.caldav.password == "cd_password"
assert s.caldav.cache_ttl == 0
assert s.caldav.path == "cd_path"
def test_set_webdav():
s = Settings.model_validate(
{
"webdav": {
"protocol": "wd_protocol",
"host": "wd_host",
"username": "wd_username",
"password": "wd_password",
"cache_ttl": "99",
"path": "wd_path",
"config_filename": "wd_config_filename",
"disable_check": "true",
"retries": "99",
"retry_delay": "99",
"prefix": "wd_prefix",
}
}
)
# webdav
assert s.webdav.protocol == "wd_protocol"
assert s.webdav.host == "wd_host"
assert s.webdav.username == "wd_username"
assert s.webdav.password == "wd_password"
assert s.webdav.cache_ttl == 99
assert s.webdav.path == "wd_path"
assert s.webdav.config_filename == "wd_config_filename"
assert s.webdav.disable_check is True
assert s.webdav.retries == 99
assert s.webdav.retry_delay == 99
assert s.webdav.prefix == "wd_prefix"
# caldav
assert s.caldav.protocol == "wd_protocol"
assert s.caldav.host == "wd_host"
assert s.caldav.username == "wd_username"
assert s.caldav.password == "wd_password"
assert s.caldav.cache_ttl == 99
assert s.caldav.path == "/remote.php/dav"
def test_set_caldav_webdav():
s = Settings.model_validate(
{
"webdav": {
"protocol": "wd_protocol",
"host": "wd_host",
"username": "wd_username",
"password": "wd_password",
"cache_ttl": "99",
"path": "wd_path",
"config_filename": "wd_config_filename",
"disable_check": "true",
"retries": "99",
"retry_delay": "99",
"prefix": "wd_prefix",
},
"caldav": {
"protocol": "cd_protocol",
"host": "cd_host",
"username": "cd_username",
"password": "cd_password",
"cache_ttl": "0",
"path": "cd_path",
},
}
)
# webdav
assert s.webdav.protocol == "wd_protocol"
assert s.webdav.host == "wd_host"
assert s.webdav.username == "wd_username"
assert s.webdav.password == "wd_password"
assert s.webdav.cache_ttl == 99
assert s.webdav.path == "wd_path"
assert s.webdav.config_filename == "wd_config_filename"
assert s.webdav.disable_check is True
assert s.webdav.retries == 99
assert s.webdav.retry_delay == 99
assert s.webdav.prefix == "wd_prefix"
# caldav
assert s.caldav.protocol == "cd_protocol"
assert s.caldav.host == "cd_host"
assert s.caldav.username == "cd_username"
assert s.caldav.password == "cd_password"
assert s.caldav.cache_ttl == 0
assert s.caldav.path == "cd_path"

44
deploy/chores/check_version Executable file
View file

@ -0,0 +1,44 @@
#!/bin/sh
script="$( readlink -f "${0}" )"
script_dir="$( dirname "${script}" )"
git_version="$( \
git rev-parse --abbrev-ref HEAD \
| cut -d '/' -f 2
)"
api_version="$( \
grep '^version' "${script_dir}/../../api/pyproject.toml" \
| sed -E 's/^version[^0-9]*((0|[1-9][0-9]*)[0-9\.]*[0-9]).*$/\1/'
)"
ui_version="$( \
python3 -c 'import sys, json; print(json.load(sys.stdin)["version"])' \
< "${script_dir}/../../ui/package.json" \
)"
install_version="$( \
grep '^ovd_version' "${script_dir}/../install.sh" \
| sed -E 's/^ovd_version[^0-9]*((0|[1-9][0-9]*)[0-9\.]*[0-9]).*$/\1/'
)"
compose_version="$( \
grep 'image: code\.yavook\.de/oekzident\.de/ovdashboard' "${script_dir}/../docker-compose.yml" \
| sed -E 's/.*code\.yavook\.de\/oekzident\.de\/ovdashboard[^0-9]*((0|[1-9][0-9]*)[0-9\.]*[0-9]).*$/\1/'
)"
if [ "${git_version}" = "${api_version}" ] \
&& [ "${git_version}" = "${ui_version}" ] \
&& [ "${git_version}" = "${install_version}" ] \
&& [ "${git_version}" = "${compose_version}" ]; then
mark="✅️"
else
mark="❌️"
fi
echo "git: ${git_version}, api: ${api_version}, ui: ${ui_version}"
echo "installer: ${install_version}, compose: ${compose_version}"
echo ">>>>> RESULT: ${mark} <<<<<"
[ "${mark}" = "✅️" ] || exit 1

17
deploy/chores/docker_buildx Executable file
View file

@ -0,0 +1,17 @@
#!/bin/sh
script="$( readlink -f "${0}" )"
script_dir="$( dirname "${script}" )"
# shellcheck disable=SC1091
. "${script_dir}/check_version"
# defined in `check_version` script
# shellcheck disable=SC2154
echo "${git_version}" >/dev/null
docker buildx build \
--pull --push \
--tag "code.yavook.de/oekzident.de/ovdashboard:${git_version}" \
--platform "linux/amd64,linux/arm64" \
"${script_dir}/../.."

38
deploy/docker-compose.yml Normal file
View file

@ -0,0 +1,38 @@
services:
# shared cache
redis:
image: redis:7-alpine
restart: always
pull_policy: always
# necessary for "host" network deployment,
# but only listen on localhost
ports:
- "127.0.0.1:6379:6379"
app:
image: code.yavook.de/oekzident.de/ovdashboard:0.1.0
restart: always
pull_policy: always
depends_on:
- redis
# "app" container needs host ip
network_mode: host
user: root
environment:
# necessary for "host" network deployment
PORT: "80"
REDIS__HOST: "localhost"
# >>>>> USER VARIABLES <<<<<
# you will want to adjust these!
TZ: "Europe/Berlin"
WEBDAV__HOST: "example.com"
WEBDAV__PATH: "/remote.php/webdav"
WEBDAV__PREFIX: "/ovdashboard"
WEBDAV__USERNAME: "ovd_user"
WEBDAV__PASSWORD: "password"

98
deploy/install.sh Normal file
View file

@ -0,0 +1,98 @@
#!/bin/sh
#########
# start #
#########
# env setup
ovd_version="0.1.0"
export DEBIAN_FRONTEND="noninteractive"
set -e
# banner
echo "Installer for OVDashboard ${ovd_version}"
echo "Waiting 10 seconds, press Ctrl+C to cancel installation ..."
sleep 10
#################
# prerequisites #
#################
# 134: docker with compose
# 113: chromium browser
/boot/dietpi/dietpi-software install 134 113
# htpdate (timesync in restricted networks)
# unclutter (hides mouse cursor)
apt-get update && apt-get install --yes --no-install-recommends \
htpdate unclutter
# activate unclutter
echo '/usr/bin/unclutter -idle 0.1 &' > /etc/chromium.d/dietpi-unclutter
# chromium window size
echo "Please enter your screen resolution!"
screen_x="$( cut -d ',' -f 1 '/sys/class/graphics/fb0/virtual_size' )"
printf "Width [default: %d]: " "${screen_x}"
read -r screen_x_in
screen_x="${screen_x_in:-$screen_x}"
sed -ri "s/^(SOFTWARE_CHROMIUM_RES_X=)[0-9]+$/\1${screen_x}/" '/boot/dietpi.txt'
screen_y="$( cut -d ',' -f 2 '/sys/class/graphics/fb0/virtual_size' )"
printf "Height [default: %d]: " "${screen_y}"
read -r screen_y_in
screen_y="${screen_y_in:-$screen_y}"
sed -ri "s/^(SOFTWARE_CHROMIUM_RES_Y=)[0-9]+$/\1${screen_y}/" '/boot/dietpi.txt'
# chromium autostart
sed -ri "s/^(AUTO_SETUP_AUTOSTART_LOGIN_USER=).+$/\1dietpi/" '/boot/dietpi.txt' # run as "dietpi"
sed -ri "s/^(SOFTWARE_CHROMIUM_AUTOSTART_URL=).+$/\1http:\/\/localhost\//" '/boot/dietpi.txt' # open "localhost"
/boot/dietpi/dietpi-autostart 11 # 11: magic number for chromium autostart
# chromium language
display_lang="en-US,en"
printf "Enter display language(s) [default: %s]: " "${display_lang}"
read -r display_lang_in
display_lang="${display_lang_in:-${display_lang}}"
sudo -u dietpi mkdir -p '/home/dietpi/.config/chromium/Default'
echo '{"intl":{"selected_languages":"'"${display_lang}"'"}}' \
| sudo -u dietpi tee '/home/dietpi/.config/chromium/Default/Preferences' \
> /dev/null
#######
# app #
#######
mkdir -p /opt/ovdashboard
# prepare compose project
curl \
--proto "=https" --tlsv1.2 -sSf \
--output "/opt/ovdashboard/docker-compose.yml" \
"https://code.yavook.de/OEKZident.de/ovdashboard/raw/tag/v${ovd_version}/deploy/docker-compose.yml"
docker compose \
--project-directory "/opt/ovdashboard" \
pull
# review compose file
echo "Please review the Docker Compose file before continuing! [hit Return]"
read -r _RETURN
nano "/opt/ovdashboard/docker-compose.yml"
# start server
docker compose \
--project-directory "/opt/ovdashboard" \
up --detach
############
# finalize #
############
echo ""
echo "#########################"
echo "# OVDashboard Installed #"
echo "#########################"
echo ""
echo "You can now reboot your device."

View file

@ -0,0 +1,67 @@
import json
import multiprocessing
import os
workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1")
max_workers_str = os.getenv("MAX_WORKERS")
use_max_workers = None
if max_workers_str:
use_max_workers = int(max_workers_str)
web_concurrency_str = os.getenv("WEB_CONCURRENCY", None)
host = os.getenv("HOST", "0.0.0.0")
port = os.getenv("PORT", "80")
bind_env = os.getenv("BIND", None)
use_loglevel = os.getenv("LOG_LEVEL", "info")
if bind_env:
use_bind = bind_env
else:
use_bind = f"{host}:{port}"
cores = multiprocessing.cpu_count()
workers_per_core = float(workers_per_core_str)
default_web_concurrency = workers_per_core * cores
if web_concurrency_str:
web_concurrency = int(web_concurrency_str)
assert web_concurrency > 0
else:
web_concurrency = max(int(default_web_concurrency), 2)
if use_max_workers:
web_concurrency = min(web_concurrency, use_max_workers)
accesslog_var = os.getenv("ACCESS_LOG", "-")
use_accesslog = accesslog_var or None
errorlog_var = os.getenv("ERROR_LOG", "-")
use_errorlog = errorlog_var or None
graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120")
timeout_str = os.getenv("TIMEOUT", "120")
keepalive_str = os.getenv("KEEP_ALIVE", "5")
# Gunicorn config variables
loglevel = use_loglevel
workers = web_concurrency
bind = use_bind
errorlog = use_errorlog
worker_tmp_dir = "/dev/shm"
accesslog = use_accesslog
graceful_timeout = int(graceful_timeout_str)
timeout = int(timeout_str)
keepalive = int(keepalive_str)
# For debugging and testing
log_data = {
"loglevel": loglevel,
"workers": workers,
"bind": bind,
"graceful_timeout": graceful_timeout,
"timeout": timeout,
"keepalive": keepalive,
"errorlog": errorlog,
"accesslog": accesslog,
# Additional, non-gunicorn variables
"workers_per_core": workers_per_core,
"use_max_workers": use_max_workers,
"host": host,
"port": port,
}
print(json.dumps(log_data))

View file

@ -0,0 +1,20 @@
#!/bin/sh
set -e
MODULE_NAME=${MODULE_NAME:-"app.main"}
VARIABLE_NAME=${VARIABLE_NAME:-"app"}
export APP_MODULE="${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"}"
export GUNICORN_CONF="${GUNICORN_CONF:-"/usr/local/share/uvicorn-gunicorn/gunicorn_conf.py"}"
export WORKER_CLASS="${WORKER_CLASS:-"uvicorn.workers.UvicornWorker"}"
if [ -f "${PRE_START_PATH}" ] ; then
echo "Running script ${PRE_START_PATH}"
# shellcheck disable=SC1090
. "${PRE_START_PATH}"
fi
# Start Gunicorn
exec gunicorn \
-k "${WORKER_CLASS}" \
-c "${GUNICORN_CONF}" \
"${APP_MODULE}"