44 Commits
1.3.1 ... 1.3.5

Author SHA1 Message Date
Oscar Krause
e1bbd42b50 Merge branch 'dev' into 'main'
1.3.5

See merge request oscar.krause/fastapi-dls!23
2023-02-13 17:30:11 +01:00
Oscar Krause
c1d541f7c6 bump version to 1.3.5 2023-02-13 08:09:37 +01:00
Oscar Krause
4b58fe6e20 added openSUSE Leap 15.4 support 2023-02-13 08:09:21 +01:00
Oscar Krause
b36b49df11 fixed missing mkdir for config file on manual installation method 2023-02-01 07:55:12 +01:00
Oscar Krause
a42b1c8cfb added note to be logged in as root using manual install method (git) 2023-01-30 12:34:46 +01:00
Oscar Krause
59152f95e6 fixed - The `declarative_base()` function is now available as sqlalchemy.orm.declarative_base() 2023-01-30 10:23:09 +01:00
Oscar Krause
62d347510d fixed - sqlalchemy.exc.ArgumentError: Textual SQL expression '\nCREATE TABLE origin (\n\to...' should be explicitly declared as text('\nCREATE TABLE origin (\n\to...') 2023-01-30 10:22:18 +01:00
Oscar Krause
f540c4b25b requirements.txt updated 2023-01-30 09:19:03 +01:00
Oscar Krause
70212e0edd improved docker-compose examples 2023-01-30 09:18:57 +01:00
Oscar Krause
616e8fba5e README - improvements 2023-01-30 08:37:34 +01:00
Oscar Krause
b905ab9dd9 Merge branch 'dev' into 'main'
1.3.4

See merge request oscar.krause/fastapi-dls!22
2023-01-26 07:56:34 +01:00
Oscar Krause
9edc93653e bump version to 1.3.4 2023-01-26 07:38:48 +01:00
Oscar Krause
f30e9237a5 requirements.txt updated 2023-01-26 07:18:01 +01:00
Oscar Krause
f12dc28c42 fixed overriding config file on update / reinstall 2023-01-26 07:17:16 +01:00
Oscar Krause
02276d5440 disabled openapi endpoints 2023-01-23 07:33:54 +01:00
Oscar Krause
9ebff8d6ca typos 2023-01-23 07:29:13 +01:00
Oscar Krause
48eb6d6c64 typos 2023-01-23 07:23:42 +01:00
Oscar Krause
f7ef8d76b6 fixed Origin.delete() 2023-01-23 07:12:02 +01:00
Oscar Krause
bed24b56ce styling 2023-01-19 08:26:35 +01:00
Oscar Krause
95427d430e added startup script 2023-01-19 07:26:22 +01:00
Oscar Krause
c3ea0aa48c added variable for client-token-expire-delta 2023-01-19 07:26:07 +01:00
Oscar Krause
91be7b226c added some comments for default values 2023-01-19 07:25:44 +01:00
Oscar Krause
7045692958 added official links 2023-01-19 07:25:24 +01:00
Oscar Krause
38177fa259 styling 2023-01-18 14:29:48 +01:00
Oscar Krause
9411759f6d added system requirements and preparements 2023-01-18 14:23:34 +01:00
Oscar Krause
48c37987b2 fixed logging and added current timezone info 2023-01-18 14:23:25 +01:00
Oscar Krause
e3745d7fa8 Merge branch 'dev' into 'main'
1.3.3

See merge request oscar.krause/fastapi-dls!21
2023-01-18 08:13:42 +01:00
Oscar Krause
5bb8f17679 improvements 2023-01-18 08:07:55 +01:00
Oscar Krause
de17b0f1b5 fixes 2023-01-18 08:03:02 +01:00
Oscar Krause
0ab5969d3a fixes 2023-01-18 06:56:16 +01:00
Oscar Krause
059a51fe74 refactored commands 2023-01-17 17:25:48 +01:00
Oscar Krause
bf858b38f4 fixes 2023-01-17 17:09:13 +01:00
Oscar Krause
f60f08d543 run powershell as administrator 2023-01-17 16:57:15 +01:00
Oscar Krause
b2e6fab294 fixes 2023-01-17 16:49:15 +01:00
Oscar Krause
b09bb091a5 bump version to 1.3.3 2023-01-17 16:29:32 +01:00
Oscar Krause
651af4cc82 fixed client-token url and added wget als alternative to curl 2023-01-17 16:29:21 +01:00
Oscar Krause
70f7d3f483 mark Let's Encrypt section as optional 2023-01-17 15:36:38 +01:00
Oscar Krause
1e4070a1ba added remove "/usr/share/fastapi-dls" to "postrm" 2023-01-17 14:57:54 +01:00
Oscar Krause
d69d833923 migrated "[[ ]]" if statements to "[ ]" 2023-01-17 14:57:39 +01:00
Oscar Krause
7ef071f92b removed fastapi-dls.service from conffiles 2023-01-17 14:57:09 +01:00
Oscar Krause
3c19fc9d5b implemented "lease_renewal" attribute as calculated value within what period of time the license must be renewed 2023-01-17 11:49:56 +01:00
Oscar Krause
164b5ebc44 Merge branch 'dev' into 'main'
1.3.2

See merge request oscar.krause/fastapi-dls!20
2023-01-17 11:36:23 +01:00
Oscar Krause
742fa07ed4 bump version to 1.3.2 2023-01-17 11:18:25 +01:00
Oscar Krause
a758d93970 main.py - fixed empty lease origin response 2023-01-17 11:18:07 +01:00
11 changed files with 280 additions and 58 deletions

View File

@@ -1,2 +1 @@
/etc/fastapi-dls/env /etc/fastapi-dls/env
/etc/systemd/system/fastapi-dls.service

View File

@@ -3,7 +3,7 @@
WORKING_DIR=/usr/share/fastapi-dls WORKING_DIR=/usr/share/fastapi-dls
CONFIG_DIR=/etc/fastapi-dls CONFIG_DIR=/etc/fastapi-dls
if [[ ! -f $CONFIG_DIR/instance.private.pem ]]; then if [ ! -f $CONFIG_DIR/instance.private.pem ]; then
echo "> Create dls-instance keypair ..." echo "> Create dls-instance keypair ..."
openssl genrsa -out $CONFIG_DIR/instance.private.pem 2048 openssl genrsa -out $CONFIG_DIR/instance.private.pem 2048
openssl rsa -in $CONFIG_DIR/instance.private.pem -outform PEM -pubout -out $CONFIG_DIR/instance.public.pem openssl rsa -in $CONFIG_DIR/instance.private.pem -outform PEM -pubout -out $CONFIG_DIR/instance.public.pem
@@ -12,8 +12,8 @@ else
fi fi
while true; do while true; do
[[ -f $CONFIG_DIR/webserver.key ]] && default_answer="N" || default_answer="Y" [ -f $CONFIG_DIR/webserver.key ] && default_answer="N" || default_answer="Y"
[[ $default_answer == "Y" ]] && V="Y/n" || V="y/N" [ $default_answer == "Y" ] && V="Y/n" || V="y/N"
read -p "> Do you wish to create self-signed webserver certificate? [${V}]" yn read -p "> Do you wish to create self-signed webserver certificate? [${V}]" yn
yn=${yn:-$default_answer} # ${parameter:-word} If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted. yn=${yn:-$default_answer} # ${parameter:-word} If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.
case $yn in case $yn in
@@ -27,7 +27,7 @@ while true; do
esac esac
done done
if [[ -f $CONFIG_DIR/webserver.key ]]; then if [ -f $CONFIG_DIR/webserver.key ]; then
echo "> Starting service ..." echo "> Starting service ..."
systemctl start fastapi-dls.service systemctl start fastapi-dls.service

View File

@@ -1,8 +1,9 @@
#!/bin/bash #!/bin/bash
if [[ -f /etc/systemd/system/fastapi-dls.service ]]; then # is removed automatically
echo "> Removing service file." #if [ "$1" = purge ] && [ -d /usr/share/fastapi-dls ]; then
rm /etc/systemd/system/fastapi-dls.service # echo "> Removing app."
fi # rm -r /usr/share/fastapi-dls
#fi
# todo echo -e "> Done."

View File

@@ -1,5 +1,3 @@
#!/bin/bash #!/bin/bash
echo -e "> Starting uninstallation of 'fastapi-dls'!" echo -e "> Starting uninstallation of 'fastapi-dls'!"
# todo

View File

@@ -11,6 +11,7 @@ license=('MIT')
depends=('python' 'python-jose' 'python-starlette' 'python-httpx' 'python-fastapi' 'python-dotenv' 'python-dateutil' 'python-sqlalchemy' 'python-pycryptodome' 'uvicorn' 'python-markdown' 'openssl') depends=('python' 'python-jose' 'python-starlette' 'python-httpx' 'python-fastapi' 'python-dotenv' 'python-dateutil' 'python-sqlalchemy' 'python-pycryptodome' 'uvicorn' 'python-markdown' 'openssl')
provider=("$pkgname") provider=("$pkgname")
install="$pkgname.install" install="$pkgname.install"
backup=('etc/default/fastapi-dls')
source=('git+file:///builds/oscar.krause/fastapi-dls' # https://gitea.publichub.eu/oscar.krause/fastapi-dls.git source=('git+file:///builds/oscar.krause/fastapi-dls' # https://gitea.publichub.eu/oscar.krause/fastapi-dls.git
"$pkgname.default" "$pkgname.default"
"$pkgname.service" "$pkgname.service"

222
README.md
View File

@@ -7,11 +7,31 @@ Compatibility tested with official DLS 2.0.1.
This service can be used without internet connection. This service can be used without internet connection.
Only the clients need a connection to this service on configured port. Only the clients need a connection to this service on configured port.
**Official Links**
- https://git.collinwebdesigns.de/oscar.krause/fastapi-dls
- https://gitea.publichub.eu/oscar.krause/fastapi-dls
- Docker Image `collinwebdesigns/fastapi-dls:latest`
*All other repositories are forks! (which is no bad - just for information and bug reports)*
---
[[_TOC_]] [[_TOC_]]
# Setup (Service) # Setup (Service)
**System requirements**
- 256mb ram
- 4gb hdd
Tested with Ubuntu 22.10 (from Proxmox templates), actually its consuming 100mb ram and 750mb hdd.
**Prepare your system**
- Make sure your timezone is set correct on you fastapi-dls server and your client
## Docker ## Docker
Docker-Images are available here: Docker-Images are available here:
@@ -49,10 +69,12 @@ Goto [`docker-compose.yml`](docker-compose.yml) for more advanced example (with
version: '3.9' version: '3.9'
x-dls-variables: &dls-variables x-dls-variables: &dls-variables
TZ: Europe/Berlin # REQUIRED, set your timezone correctly on fastapi-dls AND YOUR CLIENTS !!!
DLS_URL: localhost # REQUIRED, change to your ip or hostname DLS_URL: localhost # REQUIRED, change to your ip or hostname
DLS_PORT: 443 DLS_PORT: 443
LEASE_EXPIRE_DAYS: 90 LEASE_EXPIRE_DAYS: 90 # 90 days is maximum
DATABASE: sqlite:////app/database/db.sqlite DATABASE: sqlite:////app/database/db.sqlite
DEBUG: false
services: services:
dls: dls:
@@ -65,7 +87,12 @@ services:
volumes: volumes:
- /opt/docker/fastapi-dls/cert:/app/cert - /opt/docker/fastapi-dls/cert:/app/cert
- dls-db:/app/database - dls-db:/app/database
logging: # optional, for those who do not need logs
driver: "json-file"
options:
max-file: 5
max-size: 10m
volumes: volumes:
dls-db: dls-db:
``` ```
@@ -74,6 +101,8 @@ volumes:
Tested on `Debian 11 (bullseye)`, Ubuntu may also work. Tested on `Debian 11 (bullseye)`, Ubuntu may also work.
**Make sure you are logged in as root.**
**Install requirements** **Install requirements**
```shell ```shell
@@ -98,7 +127,7 @@ chown -R www-data:www-data $WORKING_DIR
```shell ```shell
WORKING_DIR=/opt/fastapi-dls/app/cert WORKING_DIR=/opt/fastapi-dls/app/cert
mkdir $WORKING_DIR mkdir -p $WORKING_DIR
cd $WORKING_DIR cd $WORKING_DIR
# create instance private and public key for singing JWT's # create instance private and public key for singing JWT's
openssl genrsa -out $WORKING_DIR/instance.private.pem 2048 openssl genrsa -out $WORKING_DIR/instance.private.pem 2048
@@ -115,11 +144,14 @@ This is only to test whether the service starts successfully.
```shell ```shell
cd /opt/fastapi-dls/app cd /opt/fastapi-dls/app
su - www-data -c "/opt/fastapi-dls/venv/bin/uvicorn main:app --app-dir=/opt/fastapi-dls/app" su - www-data -c "/opt/fastapi-dls/venv/bin/uvicorn main:app --app-dir=/opt/fastapi-dls/app"
# or
sudo -u www-data -c "/opt/fastapi-dls/venv/bin/uvicorn main:app --app-dir=/opt/fastapi-dls/app"
``` ```
**Create config file** **Create config file**
```shell ```shell
mkdir /etc/fastapi-dls
cat <<EOF >/etc/fastapi-dls/env cat <<EOF >/etc/fastapi-dls/env
DLS_URL=127.0.0.1 DLS_URL=127.0.0.1
DLS_PORT=443 DLS_PORT=443
@@ -164,6 +196,108 @@ EOF
Now you have to run `systemctl daemon-reload`. After that you can start service Now you have to run `systemctl daemon-reload`. After that you can start service
with `systemctl start fastapi-dls.service` and enable autostart with `systemctl enable fastapi-dls.service`. with `systemctl start fastapi-dls.service` and enable autostart with `systemctl enable fastapi-dls.service`.
## openSUSE Leap (manual method using `git clone` and python virtual environment)
Tested on `openSUSE Leap 15.4`, openSUSE Tumbleweed may also work.
**Install requirements**
```shell
zypper in -y python310 python3-virtualenv python3-pip
```
**Install FastAPI-DLS**
```shell
BASE_DIR=/opt/fastapi-dls
SERVICE_USER=dls
mkdir -p ${BASE_DIR}
cd ${BASE_DIR}
git clone https://git.collinwebdesigns.de/oscar.krause/fastapi-dls .
python3.10 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
deactivate
useradd -r ${SERVICE_USER} -M -d /opt/fastapi-dls
chown -R ${SERVICE_USER} ${BASE_DIR}
```
**Create keypair and webserver certificate**
```shell
CERT_DIR=${BASE_DIR}/app/cert
SERVICE_USER=dls
mkdir ${CERT_DIR}
cd ${CERT_DIR}
# create instance private and public key for singing JWT's
openssl genrsa -out ${CERT_DIR}/instance.private.pem 2048
openssl rsa -in ${CERT_DIR}/instance.private.pem -outform PEM -pubout -out ${CERT_DIR}/instance.public.pem
# create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout ${CERT_DIR}/webserver.key -out ${CERT_DIR}/webserver.crt
chown -R ${SERVICE_USER} ${CERT_DIR}
```
**Test Service**
This is only to test whether the service starts successfully.
```shell
BASE_DIR=/opt/fastapi-dls
SERVICE_USER=dls
cd ${BASE_DIR}
su - ${SERVICE_USER} -c "${BASE_DIR}/venv/bin/uvicorn main:app --app-dir=${BASE_DIR}/app"
```
**Create config file**
```shell
BASE_DIR=/opt/fastapi-dls
cat <<EOF >/etc/fastapi-dls/env
# Adjust DSL_URL as needed (accessing from LAN won't work with 127.0.0.1)
DLS_URL=127.0.0.1
DLS_PORT=443
LEASE_EXPIRE_DAYS=90
DATABASE=sqlite:///${BASE_DIR}/app/db.sqlite
EOF
```
**Create service**
```shell
BASE_DIR=/opt/fastapi-dls
SERVICE_USER=dls
cat <<EOF >/etc/systemd/system/fastapi-dls.service
[Unit]
Description=Service for fastapi-dls vGPU licensing service
After=network.target
[Service]
User=${SERVICE_USER}
AmbientCapabilities=CAP_NET_BIND_SERVICE
WorkingDirectory=${BASE_DIR}/app
EnvironmentFile=/etc/fastapi-dls/env
ExecStart=${BASE_DIR}/venv/bin/uvicorn main:app \\
--env-file /etc/fastapi-dls/env \\
--host \$DLS_URL --port \$DLS_PORT \\
--app-dir ${BASE_DIR}/app \\
--ssl-keyfile ${BASE_DIR}/app/cert/webserver.key \\
--ssl-certfile ${BASE_DIR}/app/cert/webserver.crt \\
--proxy-headers
Restart=always
KillSignal=SIGQUIT
Type=simple
NotifyAccess=all
[Install]
WantedBy=multi-user.target
EOF
```
Now you have to run `systemctl daemon-reload`. After that you can start service
with `systemctl start fastapi-dls.service` and enable autostart with `systemctl enable fastapi-dls.service`.
## Debian/Ubuntu (using `dpkg`) ## Debian/Ubuntu (using `dpkg`)
Packages are available here: Packages are available here:
@@ -206,13 +340,17 @@ Packages are available here:
```shell ```shell
pacman -Sy pacman -Sy
FILENAME=/opt/fastapi-dls.pkg.tar.zst FILENAME=/opt/fastapi-dls.pkg.tar.zst
url -o $FILENAME <download-url>
curl -o $FILENAME <download-url>
# or
wget -O $FILENAME <download-url>
pacman -U --noconfirm fastapi-dls.pkg.tar.zst pacman -U --noconfirm fastapi-dls.pkg.tar.zst
``` ```
Start with `systemctl start fastapi-dls.service` and enable autostart with `systemctl enable fastapi-dls.service`. Start with `systemctl start fastapi-dls.service` and enable autostart with `systemctl enable fastapi-dls.service`.
## Let's Encrypt Certificate ## Let's Encrypt Certificate (optional)
If you're using installation via docker, you can use `traefik`. Please refer to their documentation. If you're using installation via docker, you can use `traefik`. Please refer to their documentation.
@@ -266,26 +404,67 @@ Successfully tested with this package versions:
## Linux ## Linux
Download *client-token* and place it into `/etc/nvidia/ClientConfigToken`:
```shell
curl --insecure -L -X GET https://<dls-hostname-or-ip>/-/client-token -o /etc/nvidia/ClientConfigToken/client_configuration_token_$(date '+%d-%m-%Y-%H-%M-%S').tok
# or
wget --no-check-certificate -O /etc/nvidia/ClientConfigToken/client_configuration_token_$(date '+%d-%m-%Y-%H-%M-%S').tok https://<dls-hostname-or-ip>/-/client-token
```
Restart `nvidia-gridd` service:
```shell ```shell
curl --insecure -L -X GET https://<dls-hostname-or-ip>/client-token -o /etc/nvidia/ClientConfigToken/client_configuration_token_$(date '+%d-%m-%Y-%H-%M-%S').tok
service nvidia-gridd restart service nvidia-gridd restart
```
Check licensing status:
```shell
nvidia-smi -q | grep "License" nvidia-smi -q | grep "License"
``` ```
## Windows Output should be something like:
Download file and place it into `C:\Program Files\NVIDIA Corporation\vGPU Licensing\ClientConfigToken`. ```text
Now restart `NvContainerLocalSystem` service. vGPU Software Licensed Product
License Status : Licensed (Expiry: YYYY-M-DD hh:mm:ss GMT)
**Power-Shell**
```Shell
curl.exe --insecure -L -X GET https://<dls-hostname-or-ip>/client-token -o "C:\Program Files\NVIDIA Corporation\vGPU Licensing\ClientConfigToken\client_configuration_token_$($(Get-Date).tostring('dd-MM-yy-hh-mm-ss')).tok"
Restart-Service NVDisplay.ContainerLocalSystem
'C:\Program Files\NVIDIA Corporation\NVSMI\nvidia-smi.exe' -q | Select-String "License"
``` ```
## Endpoints Done. For more information check [troubleshoot section](#troubleshoot).
## Windows
**Power-Shell** (run as administrator!)
Download *client-token* and place it into `C:\Program Files\NVIDIA Corporation\vGPU Licensing\ClientConfigToken`:
```shell
curl.exe --insecure -L -X GET https://<dls-hostname-or-ip>/-/client-token -o "C:\Program Files\NVIDIA Corporation\vGPU Licensing\ClientConfigToken\client_configuration_token_$($(Get-Date).tostring('dd-MM-yy-hh-mm-ss')).tok"
```
Restart `NvContainerLocalSystem` service:
```Shell
Restart-Service NVDisplay.ContainerLocalSystem
```
Check licensing status:
```shell
& 'C:\Program Files\NVIDIA Corporation\NVSMI\nvidia-smi.exe' -q | Select-String "License"
```
Output should be something like:
```text
vGPU Software Licensed Product
License Status : Licensed (Expiry: YYYY-M-DD hh:mm:ss GMT)
```
Done. For more information check [troubleshoot section](#troubleshoot).
# Endpoints
### `GET /` ### `GET /`
@@ -303,10 +482,6 @@ Shows current runtime environment variables and their values.
HTML rendered README.md. HTML rendered README.md.
### `GET /-/docs`, `GET /-/redoc`
OpenAPI specifications rendered from `GET /-/openapi.json`.
### `GET /-/manage` ### `GET /-/manage`
Shows a very basic UI to delete origins or leases. Shows a very basic UI to delete origins or leases.
@@ -491,5 +666,4 @@ The error message can safely be ignored (since we have no license limitation :P)
Thanks to vGPU community and all who uses this project and report bugs. Thanks to vGPU community and all who uses this project and report bugs.
Special thanks to @samicrusader who created build file for ArchLinux. Special thanks to @samicrusader who created build file for ArchLinux and @cyrus who wrote the section for openSUSE.

View File

@@ -9,7 +9,7 @@ from dotenv import load_dotenv
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.requests import Request from fastapi.requests import Request
from json import loads as json_loads from json import loads as json_loads
from datetime import datetime from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from calendar import timegm from calendar import timegm
from jose import jws, jwk, jwt, JWTError from jose import jws, jwk, jwt, JWTError
@@ -22,12 +22,13 @@ from sqlalchemy.orm import sessionmaker
from util import load_key, load_file from util import load_key, load_file
from orm import Origin, Lease, init as db_init, migrate from orm import Origin, Lease, init as db_init, migrate
logger = logging.getLogger()
load_dotenv('../version.env') load_dotenv('../version.env')
TZ = datetime.now().astimezone().tzinfo
VERSION, COMMIT, DEBUG = env('VERSION', 'unknown'), env('COMMIT', 'unknown'), bool(env('DEBUG', False)) VERSION, COMMIT, DEBUG = env('VERSION', 'unknown'), env('COMMIT', 'unknown'), bool(env('DEBUG', False))
config = dict(openapi_url='/-/openapi.json', docs_url='/-/docs', redoc_url='/-/redoc') config = dict(openapi_url=None, docs_url=None, redoc_url=None) # dict(openapi_url='/-/openapi.json', docs_url='/-/docs', redoc_url='/-/redoc')
app = FastAPI(title='FastAPI-DLS', description='Minimal Delegated License Service (DLS).', version=VERSION, **config) app = FastAPI(title='FastAPI-DLS', description='Minimal Delegated License Service (DLS).', version=VERSION, **config)
db = create_engine(str(env('DATABASE', 'sqlite:///db.sqlite'))) db = create_engine(str(env('DATABASE', 'sqlite:///db.sqlite')))
db_init(db), migrate(db) db_init(db), migrate(db)
@@ -43,6 +44,8 @@ INSTANCE_KEY_PUB = load_key(str(env('INSTANCE_KEY_PUB', join(dirname(__file__),
TOKEN_EXPIRE_DELTA = relativedelta(days=int(env('TOKEN_EXPIRE_DAYS', 1)), hours=int(env('TOKEN_EXPIRE_HOURS', 0))) TOKEN_EXPIRE_DELTA = relativedelta(days=int(env('TOKEN_EXPIRE_DAYS', 1)), hours=int(env('TOKEN_EXPIRE_HOURS', 0)))
LEASE_EXPIRE_DELTA = relativedelta(days=int(env('LEASE_EXPIRE_DAYS', 90)), hours=int(env('LEASE_EXPIRE_HOURS', 0))) LEASE_EXPIRE_DELTA = relativedelta(days=int(env('LEASE_EXPIRE_DAYS', 90)), hours=int(env('LEASE_EXPIRE_HOURS', 0)))
LEASE_RENEWAL_PERIOD = float(env('LEASE_RENEWAL_PERIOD', 0.15)) LEASE_RENEWAL_PERIOD = float(env('LEASE_RENEWAL_PERIOD', 0.15))
LEASE_RENEWAL_DELTA = timedelta(days=int(env('LEASE_EXPIRE_DAYS', 90)), hours=int(env('LEASE_EXPIRE_HOURS', 0)))
CLIENT_TOKEN_EXPIRE_DELTA = relativedelta(years=12)
CORS_ORIGINS = str(env('CORS_ORIGINS', '')).split(',') if (env('CORS_ORIGINS')) else [f'https://{DLS_URL}'] CORS_ORIGINS = str(env('CORS_ORIGINS', '')).split(',') if (env('CORS_ORIGINS')) else [f'https://{DLS_URL}']
jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256) jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256)
@@ -57,6 +60,8 @@ app.add_middleware(
allow_headers=['*'], allow_headers=['*'],
) )
logging.basicConfig()
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO) logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
@@ -96,6 +101,7 @@ async def _config():
'LEASE_EXPIRE_DELTA': str(LEASE_EXPIRE_DELTA), 'LEASE_EXPIRE_DELTA': str(LEASE_EXPIRE_DELTA),
'LEASE_RENEWAL_PERIOD': str(LEASE_RENEWAL_PERIOD), 'LEASE_RENEWAL_PERIOD': str(LEASE_RENEWAL_PERIOD),
'CORS_ORIGINS': str(CORS_ORIGINS), 'CORS_ORIGINS': str(CORS_ORIGINS),
'TZ': str(TZ),
}) })
@@ -151,7 +157,8 @@ async def _origins(request: Request, leases: bool = False):
for origin in session.query(Origin).all(): for origin in session.query(Origin).all():
x = origin.serialize() x = origin.serialize()
if leases: if leases:
x['leases'] = list(map(lambda _: _.serialize(), Lease.find_by_origin_ref(db, origin.origin_ref))) serialize = dict(renewal_period=LEASE_RENEWAL_PERIOD, renewal_delta=LEASE_RENEWAL_DELTA)
x['leases'] = list(map(lambda _: _.serialize(**serialize), Lease.find_by_origin_ref(db, origin.origin_ref)))
response.append(x) response.append(x)
session.close() session.close()
return JSONr(response) return JSONr(response)
@@ -168,10 +175,12 @@ async def _leases(request: Request, origin: bool = False):
session = sessionmaker(bind=db)() session = sessionmaker(bind=db)()
response = [] response = []
for lease in session.query(Lease).all(): for lease in session.query(Lease).all():
x = lease.serialize() serialize = dict(renewal_period=LEASE_RENEWAL_PERIOD, renewal_delta=LEASE_RENEWAL_DELTA)
x = lease.serialize(**serialize)
if origin: if origin:
# assume that each lease has a valid origin record lease_origin = session.query(Origin).filter(Origin.origin_ref == lease.origin_ref).first()
x['origin'] = session.query(Origin).filter(Origin.origin_ref == lease.origin_ref).first().serialize() if lease_origin is not None:
x['origin'] = lease_origin.serialize()
response.append(x) response.append(x)
session.close() session.close()
return JSONr(response) return JSONr(response)
@@ -188,7 +197,7 @@ async def _lease_delete(request: Request, lease_ref: str):
@app.get('/-/client-token', summary='* Client-Token', description='creates a new messenger token for this service instance') @app.get('/-/client-token', summary='* Client-Token', description='creates a new messenger token for this service instance')
async def _client_token(): async def _client_token():
cur_time = datetime.utcnow() cur_time = datetime.utcnow()
exp_time = cur_time + relativedelta(years=12) exp_time = cur_time + CLIENT_TOKEN_EXPIRE_DELTA
payload = { payload = {
"jti": str(uuid4()), "jti": str(uuid4()),
@@ -525,6 +534,18 @@ async def leasing_v1_lessor_shutdown(request: Request):
return JSONr(response) return JSONr(response)
@app.on_event('startup')
async def app_on_startup():
logger.info(f'''
Using timezone: {str(TZ)}. Make sure this is correct and match your clients!
Your clients renew their license every {str(Lease.calculate_renewal(LEASE_RENEWAL_PERIOD, LEASE_RENEWAL_DELTA))}.
If the renewal fails, the license is {str(LEASE_RENEWAL_DELTA)} valid.
Your client-token file (.tok) is valid for {str(CLIENT_TOKEN_EXPIRE_DELTA)}.
''')
if __name__ == '__main__': if __name__ == '__main__':
import uvicorn import uvicorn

View File

@@ -1,9 +1,9 @@
import datetime from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, update, and_, inspect from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, update, and_, inspect, text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker, declarative_base
Base = declarative_base() Base = declarative_base()
@@ -56,12 +56,12 @@ class Origin(Base):
session.close() session.close()
@staticmethod @staticmethod
def delete(engine: Engine, origins: ["Origin"] = None) -> int: def delete(engine: Engine, origin_refs: [str] = None) -> int:
session = sessionmaker(bind=engine)() session = sessionmaker(bind=engine)()
if origins is None: if origin_refs is None:
deletions = session.query(Origin).delete() deletions = session.query(Origin).delete()
else: else:
deletions = session.query(Origin).filter(Origin.origin_ref in origins).delete() deletions = session.query(Origin).filter(Origin.origin_ref in origin_refs).delete()
session.commit() session.commit()
session.close() session.close()
return deletions return deletions
@@ -81,7 +81,10 @@ class Lease(Base):
def __repr__(self): def __repr__(self):
return f'Lease(origin_ref={self.origin_ref}, lease_ref={self.lease_ref}, expires={self.lease_expires})' return f'Lease(origin_ref={self.origin_ref}, lease_ref={self.lease_ref}, expires={self.lease_expires})'
def serialize(self) -> dict: def serialize(self, renewal_period: float, renewal_delta: timedelta) -> dict:
lease_renewal = int(Lease.calculate_renewal(renewal_period, renewal_delta).total_seconds())
lease_renewal = self.lease_updated + relativedelta(seconds=lease_renewal)
return { return {
'lease_ref': self.lease_ref, 'lease_ref': self.lease_ref,
'origin_ref': self.origin_ref, 'origin_ref': self.origin_ref,
@@ -89,6 +92,7 @@ class Lease(Base):
'lease_created': self.lease_created.isoformat(), 'lease_created': self.lease_created.isoformat(),
'lease_expires': self.lease_expires.isoformat(), 'lease_expires': self.lease_expires.isoformat(),
'lease_updated': self.lease_updated.isoformat(), 'lease_updated': self.lease_updated.isoformat(),
'lease_renewal': lease_renewal.isoformat(),
} }
@staticmethod @staticmethod
@@ -133,7 +137,7 @@ class Lease(Base):
return entity return entity
@staticmethod @staticmethod
def renew(engine: Engine, lease: "Lease", lease_expires: datetime.datetime, lease_updated: datetime.datetime): def renew(engine: Engine, lease: "Lease", lease_expires: datetime, lease_updated: datetime):
session = sessionmaker(bind=engine)() session = sessionmaker(bind=engine)()
x = dict(lease_expires=lease_expires, lease_updated=lease_updated) x = dict(lease_expires=lease_expires, lease_updated=lease_updated)
session.execute(update(Lease).where(and_(Lease.origin_ref == lease.origin_ref, Lease.lease_ref == lease.lease_ref)).values(**x)) session.execute(update(Lease).where(and_(Lease.origin_ref == lease.origin_ref, Lease.lease_ref == lease.lease_ref)).values(**x))
@@ -156,6 +160,28 @@ class Lease(Base):
session.close() session.close()
return deletions return deletions
@staticmethod
def calculate_renewal(renewal_period: float, delta: timedelta) -> timedelta:
"""
import datetime
LEASE_RENEWAL_PERIOD=0.2 # 20%
delta = datetime.timedelta(days=1)
renew = delta.total_seconds() * LEASE_RENEWAL_PERIOD
renew = datetime.timedelta(seconds=renew)
expires = delta - renew # 19.2
import datetime
LEASE_RENEWAL_PERIOD=0.15 # 15%
delta = datetime.timedelta(days=90)
renew = delta.total_seconds() * LEASE_RENEWAL_PERIOD
renew = datetime.timedelta(seconds=renew)
expires = delta - renew # 76 days, 12:00:00 hours
"""
renew = delta.total_seconds() * renewal_period
renew = timedelta(seconds=renew)
return renew
def init(engine: Engine): def init(engine: Engine):
tables = [Origin, Lease] tables = [Origin, Lease]
@@ -163,7 +189,7 @@ def init(engine: Engine):
session = sessionmaker(bind=engine)() session = sessionmaker(bind=engine)()
for table in tables: for table in tables:
if not db.dialect.has_table(engine.connect(), table.__tablename__): if not db.dialect.has_table(engine.connect(), table.__tablename__):
session.execute(str(table.create_statement(engine))) session.execute(text(str(table.create_statement(engine))))
session.commit() session.commit()
session.close() session.close()

View File

@@ -14,6 +14,7 @@ services:
environment: environment:
<<: *dls-variables <<: *dls-variables
volumes: volumes:
- /etc/timezone:/etc/timezone:ro
- /opt/docker/fastapi-dls/cert:/app/cert # instance.private.pem, instance.public.pem - /opt/docker/fastapi-dls/cert:/app/cert # instance.private.pem, instance.public.pem
- db:/app/database - db:/app/database
entrypoint: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app", "--proxy-headers"] entrypoint: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app", "--proxy-headers"]
@@ -30,6 +31,7 @@ services:
- "80:80" # for "/leasing/v1/lessor/shutdown" used by windows guests, can't be changed! - "80:80" # for "/leasing/v1/lessor/shutdown" used by windows guests, can't be changed!
- "443:443" # first part must match "DLS_PORT" - "443:443" # first part must match "DLS_PORT"
volumes: volumes:
- /etc/timezone:/etc/timezone:ro
- /opt/docker/fastapi-dls/cert:/opt/cert - /opt/docker/fastapi-dls/cert:/opt/cert
healthcheck: healthcheck:
test: ["CMD", "curl", "--insecure", "--fail", "https://localhost/-/health"] test: ["CMD", "curl", "--insecure", "--fail", "https://localhost/-/health"]

View File

@@ -1,8 +1,8 @@
fastapi==0.89.1 fastapi==0.89.1
uvicorn[standard]==0.20.0 uvicorn[standard]==0.20.0
python-jose==3.3.0 python-jose==3.3.0
pycryptodome==3.16.0 pycryptodome==3.17
python-dateutil==2.8.2 python-dateutil==2.8.2
sqlalchemy==1.4.46 sqlalchemy==2.0.0
markdown==3.4.1 markdown==3.4.1
python-dotenv==0.21.0 python-dotenv==0.21.1

View File

@@ -1 +1 @@
VERSION=1.3.1 VERSION=1.3.5