Merge tag 'v0.1.0' into develop
First working version with installer
This commit is contained in:
commit
092f336870
13 changed files with 697 additions and 82 deletions
31
Dockerfile
31
Dockerfile
|
@ -17,12 +17,31 @@ RUN yarn install --production false
|
|||
COPY ui ./
|
||||
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 #
|
||||
###########
|
||||
|
||||
ARG PYTHON_VERSION
|
||||
FROM tiangolo/uvicorn-gunicorn:python${PYTHON_VERSION} AS production
|
||||
FROM uvicorn-gunicorn AS production
|
||||
|
||||
# env setup
|
||||
WORKDIR /usr/local/src/ovdashboard_api
|
||||
|
@ -38,13 +57,13 @@ RUN set -ex; \
|
|||
export DEBIAN_FRONTEND=noninteractive; \
|
||||
apt-get update; apt-get install --yes --no-install-recommends \
|
||||
libmagic1 \
|
||||
# need to build hiredis
|
||||
gcc \
|
||||
libc-dev \
|
||||
; rm -rf /var/lib/apt/lists/*; \
|
||||
\
|
||||
# remove example app
|
||||
rm -rf /app; \
|
||||
\
|
||||
# install ovdashboard_api
|
||||
python -m pip --no-cache-dir install ./
|
||||
python3 -m pip --no-cache-dir install ./
|
||||
|
||||
# add prepared ovdashboard_ui
|
||||
COPY --from=build-ui /tmp/ovdashboard_ui /usr/local/share/ovdashboard_ui
|
||||
|
|
106
README.md
106
README.md
|
@ -12,7 +12,7 @@ All that matters, at one glance! <br />
|
|||
*Date and Time – Upcoming Events – Public Announcements – News – Pictures*
|
||||
|
||||
- **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 />
|
||||
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
|
||||
|
||||
### Prerequisites
|
||||
|
||||
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 />
|
||||
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:
|
||||
1. Have a Raspberry Pi (3 or later) connected to a HDMI screen
|
||||
1. Boot up "Raspberry Pi OS" (64 bit) and connect the Pi to your local network
|
||||
1. Download (and review) the [OVDashboard installer](TODO), then run it from a terminal
|
||||
> 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
|
||||
Your target device should be a Raspberry Pi Model 3 or later[^2]. You will need some accessories:
|
||||
- microSD card, class 10 or UHS (min. 8 GB)
|
||||
- network connectivity (bring WiFi credentials if applicable)
|
||||
- connection to a HDMI screen
|
||||
|
||||
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
|
||||
|
||||
### "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
|
||||
|
||||
|
@ -60,31 +116,9 @@ Refer to the specific README files for [the API](./api/README.md) and [the UI](.
|
|||
|
||||
## 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.
|
||||
|
||||
The **service** will run continuously and makes sure your OVDashboard is up to date.
|
||||
|
||||
|
||||
### 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
|
||||
- install Chromium-Browser, Docker and Docker Compose
|
||||
- create the OVDashboard project at `/opt/ovdashboard`
|
||||
- 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
|
||||
|
|
|
@ -18,12 +18,11 @@ class DAVSettings(BaseModel):
|
|||
Connection to a DAV server.
|
||||
"""
|
||||
|
||||
protocol: str | None = None
|
||||
host: str | None = None
|
||||
path: str | None = None
|
||||
protocol: str = "https"
|
||||
host: str = "example.com"
|
||||
|
||||
username: str | None = None
|
||||
password: str | None = None
|
||||
username: str = "ovd_user"
|
||||
password: str = "password"
|
||||
|
||||
cache_ttl: int = 60 * 10
|
||||
|
||||
|
@ -33,21 +32,34 @@ class DAVSettings(BaseModel):
|
|||
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.
|
||||
"""
|
||||
|
||||
protocol: str = "https"
|
||||
host: str = "example.com"
|
||||
path: str = "/remote.php/dav"
|
||||
prefix: str = "/ovdashboard"
|
||||
|
||||
username: str = "ovd_user"
|
||||
password: str = "password"
|
||||
path: str = "/remote.php/webdav"
|
||||
|
||||
config_filename: str = "config.txt"
|
||||
|
||||
|
@ -62,7 +74,10 @@ class WebDAVSettings(DAVSettings):
|
|||
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):
|
||||
|
@ -132,7 +147,7 @@ class Settings(BaseSettings):
|
|||
# caldav settings
|
||||
#####
|
||||
|
||||
caldav: DAVSettings = DAVSettings()
|
||||
caldav: CalDAVSettings = CalDAVSettings()
|
||||
|
||||
#####
|
||||
# redis settings
|
||||
|
@ -141,37 +156,19 @@ class Settings(BaseSettings):
|
|||
redis: RedisSettings = RedisSettings()
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_dav_settings(cls, data) -> dict[str, Any]:
|
||||
assert isinstance(data, dict)
|
||||
|
||||
# ensure both settings dicts are created
|
||||
for key in ("webdav", "caldav"):
|
||||
if key not in data:
|
||||
data[key] = {}
|
||||
data[key] = data.get(key, {})
|
||||
|
||||
default_dav = DAVSettings(
|
||||
protocol="https",
|
||||
host="example.com",
|
||||
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]
|
||||
for key in _DAV_DEFAULT:
|
||||
# if "webdav" value is not specified, use default value
|
||||
data["webdav"][key] = data["webdav"].get(key, _WEBDAV_DEFAULT[key])
|
||||
|
||||
# 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["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"
|
||||
data["caldav"][key] = data["caldav"].get(key, data["webdav"][key])
|
||||
|
||||
return data
|
||||
|
||||
|
|
|
@ -63,13 +63,23 @@ async def find_images_by_prefix(
|
|||
response_class=StreamingResponse,
|
||||
)
|
||||
async def get_image_by_prefix(
|
||||
cfg: Config = Depends(get_config),
|
||||
remote_path: str = Depends(RP_IMAGE),
|
||||
name: str = Depends(LM_IMAGE.getter.func),
|
||||
) -> StreamingResponse:
|
||||
cfg = await get_config()
|
||||
img = Image.open(BytesIO(await WebDAV.read_bytes(f"{remote_path}/{name}")))
|
||||
|
||||
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_buffer.seek(0)
|
||||
|
||||
|
|
46
api/poetry.lock
generated
46
api/poetry.lock
generated
|
@ -522,6 +522,17 @@ files = [
|
|||
{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]]
|
||||
name = "isort"
|
||||
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)"]
|
||||
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]]
|
||||
name = "pycodestyle"
|
||||
version = "2.11.1"
|
||||
|
@ -961,6 +987,26 @@ files = [
|
|||
{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]]
|
||||
name = "python-dateutil"
|
||||
version = "2.8.2"
|
||||
|
|
|
@ -26,6 +26,7 @@ black = "^23.10.1"
|
|||
flake8 = "^6.1.0"
|
||||
flake8-isort = "^6.1.0"
|
||||
types-cachetools = "^5.3.0.6"
|
||||
pytest = "^7.4.3"
|
||||
|
||||
[build-system]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
|
224
api/test/test_settings.py
Normal file
224
api/test/test_settings.py
Normal 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
44
deploy/chores/check_version
Executable 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
17
deploy/chores/docker_buildx
Executable 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
38
deploy/docker-compose.yml
Normal 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
98
deploy/install.sh
Normal 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."
|
67
deploy/mini-tiangolo/gunicorn_conf.py
Normal file
67
deploy/mini-tiangolo/gunicorn_conf.py
Normal 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))
|
20
deploy/mini-tiangolo/start.sh
Normal file
20
deploy/mini-tiangolo/start.sh
Normal 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}"
|
Loading…
Reference in a new issue