64 Commits
1.2 ... 1.3.2

Author SHA1 Message Date
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
Oscar Krause
70250f1fca Merge branch 'dev' into 'main'
1.3.1

See merge request oscar.krause/fastapi-dls!19
2023-01-16 13:08:57 +01:00
Oscar Krause
a65687a082 bump version to 1.3.1 2023-01-16 10:34:20 +01:00
Oscar Krause
3e445c80aa fixes 2023-01-16 10:33:52 +01:00
Oscar Krause
20cc984799 FAQ.md 2023-01-16 10:30:55 +01:00
Oscar Krause
3495cc3af5 typos 2023-01-16 10:30:40 +01:00
Oscar Krause
ed13577e82 Dockerfile - updated to python 3.11 2023-01-16 10:30:21 +01:00
Oscar Krause
ca8a9df54c requirements.txt updated 2023-01-16 10:24:08 +01:00
Oscar Krause
5425eec545 .gitlab-ci.yml simplified 2023-01-16 10:23:58 +01:00
Oscar Krause
2f3c7d5433 Merge branch 'main' into dev 2023-01-16 07:00:20 +01:00
Oscar Krause
b551b0e7f9 README.md - added sunsupoorted ubuntu version 2023-01-15 19:47:50 +01:00
Oscar Krause
50dea9ac4e fixes 2023-01-05 14:08:08 +01:00
Oscar Krause
549a48a10b Merge branch 'dev' into 'main'
1.3

See merge request oscar.krause/fastapi-dls!18
2023-01-05 07:27:10 +01:00
Oscar Krause
1f3bc8b4af .gitlab-ci.yml 2023-01-05 07:22:25 +01:00
Oscar Krause
5fc8d4091b Merge branch 'dev' into 'main'
1.3

See merge request oscar.krause/fastapi-dls!17
2023-01-05 07:21:28 +01:00
Oscar Krause
851ec1a5c6 requirements.txt updated 2023-01-05 06:56:56 +01:00
Oscar Krause
9180222169 README.md 2023-01-04 21:46:02 +01:00
Oscar Krause
e71d4c4f4e fixed missing servie file for DEBIAN 2023-01-04 18:27:57 +01:00
Oscar Krause
aecad82914 main.py - added confirmation to deleteOrigins() 2023-01-04 18:12:59 +01:00
Oscar Krause
02fccb3605 README.md 2023-01-04 18:05:07 +01:00
Oscar Krause
24dba89dbe removed todos, currently all done or there is a branch for it 2023-01-04 17:58:23 +01:00
Oscar Krause
f5557a5ccd README.md 2023-01-04 17:46:19 +01:00
Oscar Krause
e8736c94ec docker-compose.yml - disabled internal ssl support 2023-01-04 17:46:02 +01:00
Oscar Krause
4325560ec4 README.md - added some collapses for logs 2023-01-04 17:18:13 +01:00
Oscar Krause
05979490ce README.md - moved "Endpoints" below "Setup" 2023-01-04 17:17:58 +01:00
Oscar Krause
52ffedffc7 code styling 2023-01-04 11:14:26 +01:00
Oscar Krause
5f5569a0c7 improved debian installation 2023-01-04 11:02:54 +01:00
Oscar Krause
32b05808c4 fixed "return" instead of "raise" 2023-01-04 10:14:00 +01:00
Oscar Krause
6c9ea63dc1 added variable for TOKEN_EXPIRE_DELTA 2023-01-04 10:08:17 +01:00
Oscar Krause
b839e6c2b3 code styling
- replaced 'json.loads' with 'json_loads'
- shortened 'JSONResponse' to 'JSONr'
- shortened 'HTMLResponse' to 'HTMLr'
- replaced HTTPException with JsonResponses
- added some error handing for invalid tokens
2023-01-04 10:04:52 +01:00
Oscar Krause
8bd37c0ead added some notes to required variables to change 2023-01-04 07:40:37 +01:00
Oscar Krause
27f47b93b8 docker-compose.yml - added experimental health endpoint 2023-01-03 20:45:16 +01:00
Oscar Krause
5bb8437b1d README.md - added timestamp to linux token filename 2023-01-03 18:59:34 +01:00
Oscar Krause
7e3f2d0345 docker-compose.yml - fixes 2023-01-03 18:44:30 +01:00
Oscar Krause
4198021212 README.md - fixed windows issue with /leasing/v1/lessor/shutdown 2023-01-03 18:10:02 +01:00
Oscar Krause
7e6e523799 improved test (checking uuid are 36 chars long) 2023-01-03 18:05:46 +01:00
Oscar Krause
7b2428ea38 removed some debugging 2023-01-03 18:05:46 +01:00
Oscar Krause
ac811d5df7 added 'LEASE_EXPIRE_HOURS' variable for better debugging 2023-01-03 18:05:46 +01:00
Oscar Krause
5575fee382 fixed config test 2023-01-03 18:05:46 +01:00
Oscar Krause
f1369d5e25 added some docs 2023-01-03 17:38:45 +01:00
Oscar Krause
d6cc6dcbee fixes 2023-01-03 17:38:32 +01:00
Oscar Krause
01fe142850 .gitlab-ci.yml - fixed release job 2023-01-03 15:22:49 +01:00
Oscar Krause
18e9ab2ebf fixes 2023-01-03 14:52:31 +01:00
Oscar Krause
b64c531898 bump version to 1.3 2023-01-03 14:50:52 +01:00
Oscar Krause
ef1730f4fe orm.py - added some docs 2023-01-03 14:20:13 +01:00
Oscar Krause
146ae8b824 updated docs 2023-01-03 14:09:35 +01:00
Oscar Krause
5a5ad0e654 removed 'scope_ref' from code checks because we only support one 'ALLOTMENT_REF', so we need no checks 2023-01-03 14:09:19 +01:00
Oscar Krause
0e3e7cbd3a main.py - corrected leasing behaviour (migrated from 'LEASE_REF' to 'ALLOTMENT_REF') 2023-01-03 13:05:05 +01:00
Oscar Krause
bd5625af42 main.py - removed example responses 2023-01-03 13:02:37 +01:00
Oscar Krause
8f9d95056f code styling - migrated direct dict access to '.get()' 2023-01-03 09:20:18 +01:00
Oscar Krause
2b8c468270 main.py - fixed missing 'LEASE_RENEWAL_PERIOD' on '/auth/v1/origin' 2023-01-03 07:25:09 +01:00
Oscar Krause
50e0dc8d1f implemented '/leasing/v1/lessor/shutdown' for windows guests 2023-01-02 19:42:23 +01:00
Oscar Krause
8b934dfeef fixed '/-/config' endpoint serialisation 2023-01-02 19:23:23 +01:00
Oscar Krause
4fb6243330 removed deprecated endpoints
- '/client-token' moved to '/-/client-token'
- '/status' moved to '/-/health' and '/-/config'

see README.md for more information
2023-01-02 19:18:32 +01:00
Oscar Krause
2e950ca6f4 implemented '/-/config' endpoint to list runtime environment variables 2023-01-02 19:14:25 +01:00
Oscar Krause
34662e6612 implemented 'LEASE_RENEWAL_PERIOD' variable 2023-01-02 18:57:41 +01:00
Oscar Krause
a3e089a3d5 added some references 2023-01-02 18:10:11 +01:00
Oscar Krause
ab996bb030 code styling 2023-01-02 18:04:14 +01:00
Oscar Krause
0853dd64cb README.md - added known issue for error on releasing leases on windows shutdown 2023-01-02 14:12:15 +01:00
Oscar Krause
838956bdb7 README.md - added '-L' parameter to curl commands to follow redirects (from deprecated endpoints) 2023-01-02 11:40:19 +01:00
Oscar Krause
8c515b7f2e README.md - removed links from endpoints 2023-01-02 11:39:37 +01:00
Oscar Krause
de5f07273b README.md - added compatibility to official dls 2023-01-02 11:38:48 +01:00
16 changed files with 568 additions and 276 deletions

View File

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

27
.DEBIAN/env.default Normal file
View File

@@ -0,0 +1,27 @@
# Toggle debug mode
#DEBUG=false
# Where the client can find the DLS server
DLS_URL=127.0.0.1
DLS_PORT=443
# CORS configuration
## comma separated list without spaces
#CORS_ORIGINS="https://$DLS_URL:$DLS_PORT"
# Lease expiration in days
LEASE_EXPIRE_DAYS=90
LEASE_RENEWAL_PERIOD=0.2
# Database location
## https://docs.sqlalchemy.org/en/14/core/engines.html
DATABASE=sqlite:////etc/fastapi-dls/db.sqlite
# UUIDs for identifying the instance
#SITE_KEY_XID="00000000-0000-0000-0000-000000000000"
#INSTANCE_REF="10000000-0000-0000-0000-000000000001"
#ALLOTMENT_REF="20000000-0000-0000-0000-000000000001"
# Site-wide signing keys
INSTANCE_KEY_RSA=/etc/fastapi-dls/instance.private.pem
INSTANCE_KEY_PUB=/etc/fastapi-dls/instance.public.pem

View File

@@ -0,0 +1,25 @@
[Unit]
Description=Service for fastapi-dls
Documentation=https://git.collinwebdesigns.de/oscar.krause/fastapi-dls
After=network.target
[Service]
User=www-data
Group=www-data
AmbientCapabilities=CAP_NET_BIND_SERVICE
WorkingDirectory=/usr/share/fastapi-dls/app
EnvironmentFile=/etc/fastapi-dls/env
ExecStart=uvicorn main:app \
--env-file /etc/fastapi-dls/env \
--host $DLS_URL --port $DLS_PORT \
--app-dir /usr/share/fastapi-dls/app \
--ssl-keyfile /etc/fastapi-dls/webserver.key \
--ssl-certfile /etc/fastapi-dls/webserver.crt \
--proxy-headers
Restart=always
KillSignal=SIGQUIT
Type=simple
NotifyAccess=all
[Install]
WantedBy=multi-user.target

View File

@@ -3,89 +3,26 @@
WORKING_DIR=/usr/share/fastapi-dls
CONFIG_DIR=/etc/fastapi-dls
echo "> Create config directory ..."
mkdir -p $CONFIG_DIR
# normally we would define services in `conffiles` and as separate file, but we like to keep thinks simple.
echo "> Install service ..."
cat <<EOF >/etc/systemd/system/fastapi-dls.service
[Unit]
Description=Service for fastapi-dls
Documentation=https://git.collinwebdesigns.de/oscar.krause/fastapi-dls
After=network.target
[Service]
User=www-data
Group=www-data
AmbientCapabilities=CAP_NET_BIND_SERVICE
WorkingDirectory=$WORKING_DIR/app
EnvironmentFile=$CONFIG_DIR/env
ExecStart=uvicorn main:app \\
--env-file /etc/fastapi-dls/env \\
--host \$DLS_URL --port \$DLS_PORT \\
--app-dir $WORKING_DIR/app \\
--ssl-keyfile /etc/fastapi-dls/webserver.key \\
--ssl-certfile /etc/fastapi-dls/webserver.crt \\
--proxy-headers
Restart=always
KillSignal=SIGQUIT
Type=simple
NotifyAccess=all
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
# normally we would define configfiles in `conffiles` and as separate file, but we like to keep thinks simple.
if [[ ! -f $CONFIG_DIR/env ]]; then
echo "> Writing initial config ..."
touch $CONFIG_DIR/env
cat <<EOF >$CONFIG_DIR/env
# Toggle debug mode
#DEBUG=false
# Where the client can find the DLS server
DLS_URL=127.0.0.1
DLS_PORT=443
# CORS configuration
## comma separated list without spaces
#CORS_ORIGINS="https://$DLS_URL:$DLS_PORT"
# Lease expiration in days
LEASE_EXPIRE_DAYS=90
# Database location
## https://docs.sqlalchemy.org/en/14/core/engines.html
DATABASE=sqlite:///$CONFIG_DIR/db.sqlite
# UUIDs for identifying the instance
#SITE_KEY_XID="00000000-0000-0000-0000-000000000000"
#INSTANCE_REF="00000000-0000-0000-0000-000000000000"
# Site-wide signing keys
INSTANCE_KEY_RSA=$CONFIG_DIR/instance.private.pem
INSTANCE_KEY_PUB=$CONFIG_DIR/instance.public.pem
EOF
if [[ ! -f $CONFIG_DIR/instance.private.pem ]]; then
echo "> Create dls-instance keypair ..."
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
else
echo "> Create dls-instance keypair skipped! (exists)"
fi
echo "> Create dls-instance keypair ..."
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
while true; do
read -p "> Do you wish to create self-signed webserver certificate? [Y/n]" yn
yn=${yn:-y} # ${parameter:-word} If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.
[[ -f $CONFIG_DIR/webserver.key ]] && default_answer="N" || default_answer="Y"
[[ $default_answer == "Y" ]] && V="Y/n" || V="y/N"
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.
case $yn in
[Yy]*)
echo "> Generating keypair ..."
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $CONFIG_DIR/webserver.key -out $CONFIG_DIR/webserver.crt
break
;;
[Nn]*) break ;;
[Nn]*) echo "> Generating keypair skipped! (exists)"; break ;;
*) echo "Please answer [y] or [n]." ;;
esac
done
@@ -115,7 +52,7 @@ cat <<EOF
# Service should be up and running. #
# Webservice is listen to https://localhost #
# #
# Configuration is stored in ${CONFIG_DIR}/env #
# Configuration is stored in /etc/fastapi-dls/env. #
# #
# #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

View File

@@ -46,7 +46,10 @@ build:apt:
- cp README.md version.env build/usr/share/fastapi-dls
# create conf file
- mkdir -p build/etc/fastapi-dls
- touch build/etc/fastapi-dls/env
- cp .DEBIAN/env.default build/etc/fastapi-dls/env
# create service file
- mkdir -p build/etc/systemd/system
- cp .DEBIAN/fastapi-dls.service build/etc/systemd/system/fastapi-dls.service
# cd into "build/"
- cd build/
script:
@@ -94,7 +97,7 @@ build:pacman:
- "*.pkg.tar.zst"
test:
image: python:3.10-slim-bullseye
image: python:3.11-slim-bullseye
stage: test
rules:
- if: $CI_COMMIT_BRANCH
@@ -110,6 +113,9 @@ test:
- cd test
script:
- pytest main.py
artifacts:
reports:
dotenv: version.env
.test:linux:
stage: test
@@ -142,6 +148,7 @@ test:
--proxy-headers &
- FASTAPI_DLS_PID=$!
- echo "Started service with pid $FASTAPI_DLS_PID"
- cat /etc/fastapi-dls/env
# testing service
- if [ "`curl --insecure -s https://127.0.0.1/-/health | jq .status`" != "up" ]; then echo "Success"; else "Error"; fi
# cleanup
@@ -270,19 +277,17 @@ deploy:pacman:
release:
image: registry.gitlab.com/gitlab-org/release-cli:latest
stage: .post
needs:
- job: test
artifacts: true
rules:
- if: $CI_COMMIT_TAG
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
before_script:
- set -a # make variables from "source" command available to release-cli
- source version.env
script:
- echo "Running release-job for $VERSION"
after_script:
- set +a
release:
name: $CI_PROJECT_TITLE $version
name: $CI_PROJECT_TITLE $VERSION
description: Release of $CI_PROJECT_TITLE version $VERSION
tag_name: $VERSION
ref: $CI_COMMIT_SHA

View File

@@ -1,4 +1,4 @@
FROM python:3.10-alpine
FROM python:3.11-alpine
COPY requirements.txt /tmp/requirements.txt

17
FAQ.md Normal file
View File

@@ -0,0 +1,17 @@
# FAQ
## `Failed to acquire license from <ip> (Info: <license> - Error: The allowed time to process response has expired)`
- Did your timezone settings are correct on fastapi-dls **and your guest**?
- Did you download the client-token more than an hour ago?
Please download a new client-token. The guest have to register within an hour after client-token was created.
## `jose.exceptions.JWTError: Signature verification failed.`
- Did you recreated `instance.public.pem` / `instance.private.pem`?
Then you have to download a **new** client-token on each of your guests.

212
README.md
View File

@@ -2,72 +2,13 @@
Minimal Delegated License Service (DLS).
Compatibility tested with official DLS 2.0.1.
This service can be used without internet connection.
Only the clients need a connection to this service on configured port.
[[_TOC_]]
## ToDo's
- Support http mode for using external https proxy (disable uvicorn ssl for using behind proxy)
## Endpoints
### [`GET /`](/)
Redirect to `/-/readme`.
### [`GET /status`](/status) (deprecated: use `/-/health`)
Status endpoint, used for *healthcheck*. Shows also current version and commit hash.
### [`GET /-/health`](/-/health)
Status endpoint, used for *healthcheck*. Shows also current version and commit hash.
### [`GET /-/readme`](/-/readme)
HTML rendered README.md.
### [`GET /-/docs`](/-/docs), [`GET /-/redoc`](/-/redoc)
OpenAPI specifications rendered from `GET /-/openapi.json`.
### [`GET /-/manage`](/-/manage)
Shows a very basic UI to delete origins or leases.
### `GET /-/origins?leases=false`
List registered origins.
| Query Parameter | Default | Usage |
|-----------------|---------|--------------------------------------|
| `leases` | `false` | Include referenced leases per origin |
### `DELETE /-/origins`
Deletes all origins and their leases.
### `GET /-/leases?origin=false`
List current leases.
| Query Parameter | Default | Usage |
|-----------------|---------|-------------------------------------|
| `origin` | `false` | Include referenced origin per lease |
### `DELETE /-/lease/{lease_ref}`
Deletes an lease.
### `GET /client-token`
Generate client token, (see [installation](#installation)).
### Others
There are some more internal api endpoints for handling authentication and lease process.
# Setup (Service)
@@ -93,6 +34,8 @@ openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webse
**Start container**
To test if everything is set up properly you can start container as following:
```shell
docker volume create dls-db
docker run -e DLS_URL=`hostname -i` -e DLS_PORT=443 -p 443:443 -v $WORKING_DIR:/app/cert -v dls-db:/app/database collinwebdesigns/fastapi-dls:latest
@@ -100,11 +43,13 @@ docker run -e DLS_URL=`hostname -i` -e DLS_PORT=443 -p 443:443 -v $WORKING_DIR:/
**Docker-Compose / Deploy stack**
Goto [`docker-compose.yml`](docker-compose.yml) for more advanced example (with reverse proxy usage).
```yaml
version: '3.9'
x-dls-variables: &dls-variables
DLS_URL: localhost # REQUIRED
DLS_URL: localhost # REQUIRED, change to your ip or hostname
DLS_PORT: 443
LEASE_EXPIRE_DAYS: 90
DATABASE: sqlite:////app/database/db.sqlite
@@ -125,7 +70,7 @@ volumes:
dls-db:
```
## Debian/Ubuntu (manual method using `git clone`)
## Debian/Ubuntu (manual method using `git clone` and python virtual environment)
Tested on `Debian 11 (bullseye)`, Ubuntu may also work.
@@ -230,6 +175,11 @@ Successful tested with:
- Debian 12 (Bookworm) (works but not recommended because it is currently in *testing* state)
- Ubuntu 22.10 (Kinetic Kudu)
Not working with:
- Debian 11 (Bullseye) and lower (missing `python-jose` dependency)
- Ubuntu 22.04 (Jammy Jellyfish) (not supported as for 15.01.2023 due to [fastapi - uvicorn version missmatch](https://bugs.launchpad.net/ubuntu/+source/fastapi/+bug/1970557))
**Run this on your server instance**
First go to [GitLab-Registry](https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/packages) and select your
@@ -280,20 +230,29 @@ After first success you have to replace `--issue` with `--renew`.
# Configuration
| Variable | Default | Usage |
|---------------------|----------------------------------------|-------------------------------------------------------------------------------------|
| `DEBUG` | `false` | Toggles `fastapi` debug mode |
| `DLS_URL` | `localhost` | Used in client-token to tell guest driver where dls instance is reachable |
| `DLS_PORT` | `443` | Used in client-token to tell guest driver where dls instance is reachable |
| `LEASE_EXPIRE_DAYS` | `90` | Lease time in days |
| `DATABASE` | `sqlite:///db.sqlite` | See [official SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html) |
| `CORS_ORIGINS` | `https://{DLS_URL}` | Sets `Access-Control-Allow-Origin` header (comma separated string) \* |
| `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid |
| `INSTANCE_REF` | `00000000-0000-0000-0000-000000000000` | Instance identification uuid |
| `INSTANCE_KEY_RSA` | `<app-dir>/cert/instance.private.pem` | Site-wide private RSA key for singing JWTs |
| `INSTANCE_KEY_PUB` | `<app-dir>/cert/instance.public.pem` | Site-wide public key |
| Variable | Default | Usage |
|------------------------|----------------------------------------|------------------------------------------------------------------------------------------------------|
| `DEBUG` | `false` | Toggles `fastapi` debug mode |
| `DLS_URL` | `localhost` | Used in client-token to tell guest driver where dls instance is reachable |
| `DLS_PORT` | `443` | Used in client-token to tell guest driver where dls instance is reachable |
| `TOKEN_EXPIRE_DAYS` | `1` | Client auth-token validity (used for authenticate client against api, **not `.tok` file!**) |
| `LEASE_EXPIRE_DAYS` | `90` | Lease time in days |
| `LEASE_RENEWAL_PERIOD` | `0.15` | The percentage of the lease period that must elapse before a licensed client can renew a license \*1 |
| `DATABASE` | `sqlite:///db.sqlite` | See [official SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html) |
| `CORS_ORIGINS` | `https://{DLS_URL}` | Sets `Access-Control-Allow-Origin` header (comma separated string) \*2 |
| `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid |
| `INSTANCE_REF` | `10000000-0000-0000-0000-000000000001` | Instance identification uuid |
| `ALLOTMENT_REF` | `20000000-0000-0000-0000-000000000001` | Allotment identification uuid |
| `INSTANCE_KEY_RSA` | `<app-dir>/cert/instance.private.pem` | Site-wide private RSA key for singing JWTs \*3 |
| `INSTANCE_KEY_PUB` | `<app-dir>/cert/instance.public.pem` | Site-wide public key \*3 |
\* Always use `https`, since guest-drivers only support secure connections!
\*1 For example, if the lease period is one day and the renewal period is 20%, the client attempts to renew its license
every 4.8 hours. If network connectivity is lost, the loss of connectivity is detected during license renewal and the
client has 19.2 hours in which to re-establish connectivity before its license expires.
\*2 Always use `https`, since guest-drivers only support secure connections!
\*3 If you recreate instance keys you need to **recreate client-token for each guest**!
# Setup (Client)
@@ -308,7 +267,7 @@ Successfully tested with this package versions:
## Linux
```shell
curl --insecure -X GET https://<dls-hostname-or-ip>/client-token -o /etc/nvidia/ClientConfigToken/client_configuration_token.tok
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
nvidia-smi -q | grep "License"
```
@@ -321,13 +280,73 @@ Now restart `NvContainerLocalSystem` service.
**Power-Shell**
```Shell
curl.exe --insecure -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"
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
### `GET /`
Redirect to `/-/readme`.
### `GET /-/health`
Status endpoint, used for *healthcheck*.
### `GET /-/config`
Shows current runtime environment variables and their values.
### `GET /-/readme`
HTML rendered README.md.
### `GET /-/docs`, `GET /-/redoc`
OpenAPI specifications rendered from `GET /-/openapi.json`.
### `GET /-/manage`
Shows a very basic UI to delete origins or leases.
### `GET /-/origins?leases=false`
List registered origins.
| Query Parameter | Default | Usage |
|-----------------|---------|--------------------------------------|
| `leases` | `false` | Include referenced leases per origin |
### `DELETE /-/origins`
Deletes all origins and their leases.
### `GET /-/leases?origin=false`
List current leases.
| Query Parameter | Default | Usage |
|-----------------|---------|-------------------------------------|
| `origin` | `false` | Include referenced origin per lease |
### `DELETE /-/lease/{lease_ref}`
Deletes an lease.
### `GET /-/client-token`
Generate client token, (see [installation](#installation)).
### Others
There are many other internal api endpoints for handling authentication and lease process.
# Troubleshoot
**Please make sure that fastapi-dls and your guests are on the same timezone!**
## Linux
Logs are available with `journalctl -u nvidia-gridd -f`.
@@ -346,6 +365,9 @@ This message can be ignored.
- Ref. https://github.com/encode/uvicorn/issues/441
<details>
<summary>Log example</summary>
```
WARNING:uvicorn.error:Invalid HTTP request received.
Traceback (most recent call last):
@@ -364,6 +386,8 @@ Traceback (most recent call last):
h11._util.RemoteProtocolError: no request line received
```
</details>
## Windows
### Required cipher on Windows Guests (e.g. managed by domain controller with GPO)
@@ -431,6 +455,38 @@ Dec 20 17:53:34 ubuntu-grid-server nvidia-gridd[10354]: License acquired success
</details>
### Error on releasing leases on shutdown (can be ignored and/or fixed with reverse proxy)
The driver wants to release current leases on shutting down windows. This endpoint needs to be a http endpoint.
The error message can safely be ignored (since we have no license limitation :P) and looks like this:
<details>
<summary>Log example</summary>
```
<1>:NLS initialized
<1>:License acquired successfully. (Info: 192.168.178.110, NVIDIA RTX Virtual Workstation; Expiry: 2023-3-30 23:0:22 GMT)
<0>:Failed to return license to 192.168.178.110 (Error: Generic network communication failure)
<0>:End Logging
```
#### log with nginx as reverse proxy (see [docker-compose.yml](docker-compose.yml))
```
<1>:NLS initialized
<2>:NLS initialized
<1>:Valid GRID license not found. GPU features and performance will be fully degraded. To enable full functionality please configure licensing details.
<1>:License acquired successfully. (Info: 192.168.178.33, NVIDIA RTX Virtual Workstation; Expiry: 2023-1-4 16:48:20 GMT)
<2>:Valid GRID license not found. GPU features and performance will be fully degraded. To enable full functionality please configure licensing details.
<2>:License acquired successfully from local trusted store. (Info: 192.168.178.33, NVIDIA RTX Virtual Workstation; Expiry: 2023-1-4 16:48:20 GMT)
<2>:End Logging
<1>:End Logging
<0>:License returned successfully. (Info: 192.168.178.33)
<0>:End Logging
```
</details>
# Credits
Thanks to vGPU community and all who uses this project and report bugs.

View File

@@ -6,16 +6,16 @@ from os.path import join, dirname
from os import getenv as env
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI
from fastapi.requests import Request
import json
from json import loads as json_loads
from datetime import datetime
from dateutil.relativedelta import relativedelta
from calendar import timegm
from jose import jws, jwk, jwt
from jose import jws, jwk, jwt, JWTError
from jose.constants import ALGORITHMS
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import StreamingResponse, JSONResponse, HTMLResponse, Response, RedirectResponse
from starlette.responses import StreamingResponse, JSONResponse as JSONr, HTMLResponse as HTMLr, Response, RedirectResponse
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@@ -32,14 +32,17 @@ app = FastAPI(title='FastAPI-DLS', description='Minimal Delegated License Servic
db = create_engine(str(env('DATABASE', 'sqlite:///db.sqlite')))
db_init(db), migrate(db)
# everything prefixed with "INSTANCE_*" is used as "SERVICE_INSTANCE_*" or "SI_*" in official dls service
DLS_URL = str(env('DLS_URL', 'localhost'))
DLS_PORT = int(env('DLS_PORT', '443'))
SITE_KEY_XID = str(env('SITE_KEY_XID', '00000000-0000-0000-0000-000000000000'))
INSTANCE_REF = str(env('INSTANCE_REF', '00000000-0000-0000-0000-000000000000'))
INSTANCE_REF = str(env('INSTANCE_REF', '10000000-0000-0000-0000-000000000001'))
ALLOTMENT_REF = str(env('ALLOTMENT_REF', '20000000-0000-0000-0000-000000000001'))
INSTANCE_KEY_RSA = load_key(str(env('INSTANCE_KEY_RSA', join(dirname(__file__), 'cert/instance.private.pem'))))
INSTANCE_KEY_PUB = load_key(str(env('INSTANCE_KEY_PUB', join(dirname(__file__), 'cert/instance.public.pem'))))
TOKEN_EXPIRE_DELTA = relativedelta(hours=1) # days=1
LEASE_EXPIRE_DELTA = relativedelta(days=int(env('LEASE_EXPIRE_DAYS', 90)))
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_RENEWAL_PERIOD = float(env('LEASE_RENEWAL_PERIOD', 0.15))
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)
@@ -68,11 +71,6 @@ async def index():
return RedirectResponse('/-/readme')
@app.get('/status', summary='* Status', description='returns current service status, version (incl. git-commit) and some variables.', deprecated=True)
async def status(request: Request):
return JSONResponse({'status': 'up', 'version': VERSION, 'commit': COMMIT, 'debug': DEBUG})
@app.get('/-/', summary='* Index')
async def _index():
return RedirectResponse('/-/readme')
@@ -80,14 +78,32 @@ async def _index():
@app.get('/-/health', summary='* Health')
async def _health(request: Request):
return JSONResponse({'status': 'up', 'version': VERSION, 'commit': COMMIT, 'debug': DEBUG})
return JSONr({'status': 'up'})
@app.get('/-/config', summary='* Config', description='returns environment variables.')
async def _config():
return JSONr({
'VERSION': str(VERSION),
'COMMIT': str(COMMIT),
'DEBUG': str(DEBUG),
'DLS_URL': str(DLS_URL),
'DLS_PORT': str(DLS_PORT),
'SITE_KEY_XID': str(SITE_KEY_XID),
'INSTANCE_REF': str(INSTANCE_REF),
'ALLOTMENT_REF': [str(ALLOTMENT_REF)],
'TOKEN_EXPIRE_DELTA': str(TOKEN_EXPIRE_DELTA),
'LEASE_EXPIRE_DELTA': str(LEASE_EXPIRE_DELTA),
'LEASE_RENEWAL_PERIOD': str(LEASE_RENEWAL_PERIOD),
'CORS_ORIGINS': str(CORS_ORIGINS),
})
@app.get('/-/readme', summary='* Readme')
async def _readme():
from markdown import markdown
content = load_file('../README.md').decode('utf-8')
return HTMLResponse(markdown(text=content, extensions=['tables', 'fenced_code', 'md_in_html', 'nl2br', 'toc']))
return HTMLr(markdown(text=content, extensions=['tables', 'fenced_code', 'md_in_html', 'nl2br', 'toc']))
@app.get('/-/manage', summary='* Management UI')
@@ -99,14 +115,18 @@ async def _manage(request: Request):
<title>FastAPI-DLS Management</title>
</head>
<body>
<button onclick="deleteOrigins()">delete origins and their leases</button>
<button onclick="deleteOrigins()">delete ALL origins and their leases</button>
<button onclick="deleteLease()">delete specific lease</button>
<script>
function deleteOrigins() {
var xhr = new XMLHttpRequest();
xhr.open("DELETE", '/-/origins', true);
xhr.send();
const response = confirm('Are you sure you want to delete all origins and their leases?');
if (response) {
var xhr = new XMLHttpRequest();
xhr.open("DELETE", '/-/origins', true);
xhr.send();
}
}
function deleteLease(lease_ref) {
if(lease_ref === undefined)
@@ -121,7 +141,7 @@ async def _manage(request: Request):
</body>
</html>
'''
return HTMLResponse(response)
return HTMLr(response)
@app.get('/-/origins', summary='* Origins')
@@ -134,7 +154,7 @@ async def _origins(request: Request, leases: bool = False):
x['leases'] = list(map(lambda _: _.serialize(), Lease.find_by_origin_ref(db, origin.origin_ref)))
response.append(x)
session.close()
return JSONResponse(response)
return JSONr(response)
@app.delete('/-/origins', summary='* Origins')
@@ -150,18 +170,19 @@ async def _leases(request: Request, origin: bool = False):
for lease in session.query(Lease).all():
x = lease.serialize()
if origin:
# assume that each lease has a valid origin record
x['origin'] = session.query(Origin).filter(Origin.origin_ref == lease.origin_ref).first().serialize()
lease_origin = session.query(Origin).filter(Origin.origin_ref == lease.origin_ref).first()
if lease_origin is not None:
x['origin'] = lease_origin.serialize()
response.append(x)
session.close()
return JSONResponse(response)
return JSONr(response)
@app.delete('/-/lease/{lease_ref}', summary='* Lease')
async def _lease_delete(request: Request, lease_ref: str):
if Lease.delete(db, lease_ref) == 1:
return Response(status_code=201)
raise HTTPException(status_code=404, detail='lease not found')
return JSONr(status_code=404, content={'status': 404, 'detail': 'lease not found'})
# venv/lib/python3.9/site-packages/nls_core_service_instance/service_instance_token_manager.py
@@ -178,7 +199,7 @@ async def _client_token():
"nbf": timegm(cur_time.timetuple()),
"exp": timegm(exp_time.timetuple()),
"update_mode": "ABSOLUTE",
"scope_ref_list": [str(uuid4())], # this is our LEASE_REF
"scope_ref_list": [ALLOTMENT_REF],
"fulfillment_class_ref_list": [],
"service_instance_configuration": {
"nls_service_instance_ref": INSTANCE_REF,
@@ -210,32 +231,26 @@ async def _client_token():
return response
@app.get('/client-token', summary='* Client-Token', description='creates a new messenger token for this service instance', deprecated=True)
async def client_token():
return RedirectResponse('/-/client-token')
# venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py
# {"candidate_origin_ref":"00112233-4455-6677-8899-aabbccddeeff","environment":{"fingerprint":{"mac_address_list":["ff:ff:ff:ff:ff:ff"]},"hostname":"my-hostname","ip_address_list":["192.168.178.123","fe80::","fe80::1%enp6s18"],"guest_driver_version":"510.85.02","os_platform":"Debian GNU/Linux 11 (bullseye) 11","os_version":"11 (bullseye)"},"registration_pending":false,"update_pending":false}
@app.post('/auth/v1/origin', description='find or create an origin')
async def auth_v1_origin(request: Request):
j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow()
j, cur_time = json_loads((await request.body()).decode('utf-8')), datetime.utcnow()
origin_ref = j['candidate_origin_ref']
origin_ref = j.get('candidate_origin_ref')
logging.info(f'> [ origin ]: {origin_ref}: {j}')
data = Origin(
origin_ref=origin_ref,
hostname=j['environment']['hostname'],
guest_driver_version=j['environment']['guest_driver_version'],
os_platform=j['environment']['os_platform'], os_version=j['environment']['os_version'],
hostname=j.get('environment').get('hostname'),
guest_driver_version=j.get('environment').get('guest_driver_version'),
os_platform=j.get('environment').get('os_platform'), os_version=j.get('environment').get('os_version'),
)
Origin.create_or_update(db, data)
response = {
"origin_ref": origin_ref,
"environment": j['environment'],
"environment": j.get('environment'),
"svc_port_set_list": None,
"node_url_list": None,
"node_query_order": None,
@@ -243,44 +258,42 @@ async def auth_v1_origin(request: Request):
"sync_timestamp": cur_time.isoformat()
}
return JSONResponse(response)
return JSONr(response)
# venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py
# { "environment" : { "guest_driver_version" : "guest_driver_version", "hostname" : "myhost", "ip_address_list" : [ "192.168.1.129" ], "os_version" : "os_version", "os_platform" : "os_platform", "fingerprint" : { "mac_address_list" : [ "e4:b9:7a:e5:7b:ff" ] }, "host_driver_version" : "host_driver_version" }, "origin_ref" : "00112233-4455-6677-8899-aabbccddeeff" }
@app.post('/auth/v1/origin/update', description='update an origin evidence')
async def auth_v1_origin_update(request: Request):
j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow()
j, cur_time = json_loads((await request.body()).decode('utf-8')), datetime.utcnow()
origin_ref = j['origin_ref']
origin_ref = j.get('origin_ref')
logging.info(f'> [ update ]: {origin_ref}: {j}')
data = Origin(
origin_ref=origin_ref,
hostname=j['environment']['hostname'],
guest_driver_version=j['environment']['guest_driver_version'],
os_platform=j['environment']['os_platform'], os_version=j['environment']['os_version'],
hostname=j.get('environment').get('hostname'),
guest_driver_version=j.get('environment').get('guest_driver_version'),
os_platform=j.get('environment').get('os_platform'), os_version=j.get('environment').get('os_version'),
)
Origin.create_or_update(db, data)
response = {
"environment": j['environment'],
"environment": j.get('environment'),
"prompts": None,
"sync_timestamp": cur_time.isoformat()
}
return JSONResponse(response)
return JSONr(response)
# venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py
# venv/lib/python3.9/site-packages/nls_core_auth/auth.py - CodeResponse
# {"code_challenge":"...","origin_ref":"00112233-4455-6677-8899-aabbccddeeff"}
@app.post('/auth/v1/code', description='get an authorization code')
async def auth_v1_code(request: Request):
j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow()
j, cur_time = json_loads((await request.body()).decode('utf-8')), datetime.utcnow()
origin_ref = j['origin_ref']
origin_ref = j.get('origin_ref')
logging.info(f'> [ code ]: {origin_ref}: {j}')
delta = relativedelta(minutes=15)
@@ -289,8 +302,8 @@ async def auth_v1_code(request: Request):
payload = {
'iat': timegm(cur_time.timetuple()),
'exp': timegm(expires.timetuple()),
'challenge': j['code_challenge'],
'origin_ref': j['origin_ref'],
'challenge': j.get('code_challenge'),
'origin_ref': j.get('origin_ref'),
'key_ref': SITE_KEY_XID,
'kid': SITE_KEY_XID
}
@@ -303,23 +316,27 @@ async def auth_v1_code(request: Request):
"prompts": None
}
return JSONResponse(response)
return JSONr(response)
# venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py
# venv/lib/python3.9/site-packages/nls_core_auth/auth.py - TokenResponse
# {"auth_code":"...","code_verifier":"..."}
@app.post('/auth/v1/token', description='exchange auth code and verifier for token')
async def auth_v1_token(request: Request):
j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow()
payload = jwt.decode(token=j['auth_code'], key=jwt_decode_key)
j, cur_time = json_loads((await request.body()).decode('utf-8')), datetime.utcnow()
origin_ref = payload['origin_ref']
try:
payload = jwt.decode(token=j.get('auth_code'), key=jwt_decode_key)
except JWTError as e:
return JSONr(status_code=400, content={'status': 400, 'title': 'invalid token', 'detail': str(e)})
origin_ref = payload.get('origin_ref')
logging.info(f'> [ auth ]: {origin_ref}: {j}')
# validate the code challenge
if payload['challenge'] != b64enc(sha256(j['code_verifier'].encode('utf-8')).digest()).rstrip(b'=').decode('utf-8'):
raise HTTPException(status_code=401, detail='expected challenge did not match verifier')
challenge = b64enc(sha256(j.get('code_verifier').encode('utf-8')).digest()).rstrip(b'=').decode('utf-8')
if payload.get('challenge') != challenge:
return JSONr(status_code=401, content={'status': 401, 'detail': 'expected challenge did not match verifier'})
access_expires_on = cur_time + TOKEN_EXPIRE_DELTA
@@ -342,36 +359,44 @@ async def auth_v1_token(request: Request):
"sync_timestamp": cur_time.isoformat(),
}
return JSONResponse(response)
return JSONr(response)
# {'fulfillment_context': {'fulfillment_class_ref_list': []}, 'lease_proposal_list': [{'license_type_qualifiers': {'count': 1}, 'product': {'name': 'NVIDIA RTX Virtual Workstation'}}], 'proposal_evaluation_mode': 'ALL_OF', 'scope_ref_list': ['00112233-4455-6677-8899-aabbccddeeff']}
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py
@app.post('/leasing/v1/lessor', description='request multiple leases (borrow) for current origin')
async def leasing_v1_lessor(request: Request):
j, token, cur_time = json.loads((await request.body()).decode('utf-8')), __get_token(request), datetime.utcnow()
j, token, cur_time = json_loads((await request.body()).decode('utf-8')), __get_token(request), datetime.utcnow()
try:
token = __get_token(request)
except JWTError:
return JSONr(status_code=401, content={'status': 401, 'detail': 'token is not valid'})
origin_ref = token.get('origin_ref')
scope_ref_list = j['scope_ref_list']
scope_ref_list = j.get('scope_ref_list')
logging.info(f'> [ create ]: {origin_ref}: create leases for scope_ref_list {scope_ref_list}')
lease_result_list = []
for scope_ref in scope_ref_list:
# if scope_ref not in [ALLOTMENT_REF]:
# return JSONr(status_code=500, detail=f'no service instances found for scopes: ["{scope_ref}"]')
lease_ref = str(uuid4())
expires = cur_time + LEASE_EXPIRE_DELTA
lease_result_list.append({
"ordinal": 0,
# https://docs.nvidia.com/license-system/latest/nvidia-license-system-user-guide/index.html
"lease": {
"ref": scope_ref,
"ref": lease_ref,
"created": cur_time.isoformat(),
"expires": expires.isoformat(),
# The percentage of the lease period that must elapse before a licensed client can renew a license
"recommended_lease_renewal": 0.15,
"recommended_lease_renewal": LEASE_RENEWAL_PERIOD,
"offline_lease": "true",
"license_type": "CONCURRENT_COUNTED_SINGLE"
}
})
data = Lease(origin_ref=origin_ref, lease_ref=scope_ref, lease_created=cur_time, lease_expires=expires)
data = Lease(origin_ref=origin_ref, lease_ref=lease_ref, lease_created=cur_time, lease_expires=expires)
Lease.create_or_update(db, data)
response = {
@@ -381,7 +406,7 @@ async def leasing_v1_lessor(request: Request):
"prompts": None
}
return JSONResponse(response)
return JSONr(response)
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py
@@ -401,9 +426,10 @@ async def leasing_v1_lessor_lease(request: Request):
"prompts": None
}
return JSONResponse(response)
return JSONr(response)
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_single_controller.py
# venv/lib/python3.9/site-packages/nls_core_lease/lease_single.py
@app.put('/leasing/v1/lease/{lease_ref}', description='renew a lease')
async def leasing_v1_lease_renew(request: Request, lease_ref: str):
@@ -414,13 +440,13 @@ async def leasing_v1_lease_renew(request: Request, lease_ref: str):
entity = Lease.find_by_origin_ref_and_lease_ref(db, origin_ref, lease_ref)
if entity is None:
raise HTTPException(status_code=404, detail='requested lease not available')
return JSONr(status_code=404, content={'status': 404, 'detail': 'requested lease not available'})
expires = cur_time + LEASE_EXPIRE_DELTA
response = {
"lease_ref": lease_ref,
"expires": expires.isoformat(),
"recommended_lease_renewal": 0.16,
"recommended_lease_renewal": LEASE_RENEWAL_PERIOD,
"offline_lease": True,
"prompts": None,
"sync_timestamp": cur_time.isoformat(),
@@ -428,9 +454,10 @@ async def leasing_v1_lease_renew(request: Request, lease_ref: str):
Lease.renew(db, entity, expires, cur_time)
return JSONResponse(response)
return JSONr(response)
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_single_controller.py
@app.delete('/leasing/v1/lease/{lease_ref}', description='release (return) a lease')
async def leasing_v1_lease_delete(request: Request, lease_ref: str):
token, cur_time = __get_token(request), datetime.utcnow()
@@ -440,12 +467,12 @@ async def leasing_v1_lease_delete(request: Request, lease_ref: str):
entity = Lease.find_by_lease_ref(db, lease_ref)
if entity.origin_ref != origin_ref:
raise HTTPException(status_code=403, detail='access or operation forbidden')
return JSONr(status_code=403, content={'status': 403, 'detail': 'access or operation forbidden'})
if entity is None:
raise HTTPException(status_code=404, detail='requested lease not available')
return JSONr(status_code=404, content={'status': 404, 'detail': 'requested lease not available'})
if Lease.delete(db, lease_ref) == 0:
raise HTTPException(status_code=404, detail='lease not found')
return JSONr(status_code=404, content={'status': 404, 'detail': 'lease not found'})
response = {
"lease_ref": lease_ref,
@@ -453,9 +480,10 @@ async def leasing_v1_lease_delete(request: Request, lease_ref: str):
"sync_timestamp": cur_time.isoformat(),
}
return JSONResponse(response)
return JSONr(response)
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py
@app.delete('/leasing/v1/lessor/leases', description='release all leases')
async def leasing_v1_lessor_lease_remove(request: Request):
token, cur_time = __get_token(request), datetime.utcnow()
@@ -473,7 +501,29 @@ async def leasing_v1_lessor_lease_remove(request: Request):
"prompts": None
}
return JSONResponse(response)
return JSONr(response)
@app.post('/leasing/v1/lessor/shutdown', description='shutdown all leases')
async def leasing_v1_lessor_shutdown(request: Request):
j, cur_time = json_loads((await request.body()).decode('utf-8')), datetime.utcnow()
token = j.get('token')
token = jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False})
origin_ref = token.get('origin_ref')
released_lease_list = list(map(lambda x: x.lease_ref, Lease.find_by_origin_ref(db, origin_ref)))
deletions = Lease.cleanup(db, origin_ref)
logging.info(f'> [ shutdown ]: {origin_ref}: removed {deletions} leases')
response = {
"released_lease_list": released_lease_list,
"release_failure_list": None,
"sync_timestamp": cur_time.isoformat(),
"prompts": None
}
return JSONr(response)
if __name__ == '__main__':

View File

@@ -13,6 +13,7 @@ class Origin(Base):
origin_ref = Column(CHAR(length=36), primary_key=True, unique=True, index=True) # uuid4
# service_instance_xid = Column(CHAR(length=36), nullable=False, index=True) # uuid4 # not necessary, we only support one service_instance_xid ('INSTANCE_REF')
hostname = Column(VARCHAR(length=256), nullable=True)
guest_driver_version = Column(VARCHAR(length=10), nullable=True)
os_platform = Column(VARCHAR(length=256), nullable=True)
@@ -24,6 +25,7 @@ class Origin(Base):
def serialize(self) -> dict:
return {
'origin_ref': self.origin_ref,
# 'service_instance_xid': self.service_instance_xid,
'hostname': self.hostname,
'guest_driver_version': self.guest_driver_version,
'os_platform': self.os_platform,
@@ -39,7 +41,6 @@ class Origin(Base):
def create_or_update(engine: Engine, origin: "Origin"):
session = sessionmaker(bind=engine)()
entity = session.query(Origin).filter(Origin.origin_ref == origin.origin_ref).first()
print(entity)
if entity is None:
session.add(origin)
else:
@@ -72,6 +73,7 @@ class Lease(Base):
lease_ref = Column(CHAR(length=36), primary_key=True, nullable=False, index=True) # uuid4
origin_ref = Column(CHAR(length=36), ForeignKey(Origin.origin_ref, ondelete='CASCADE'), nullable=False, index=True) # uuid4
# scope_ref = Column(CHAR(length=36), nullable=False, index=True) # uuid4 # not necessary, we only support one scope_ref ('ALLOTMENT_REF')
lease_created = Column(DATETIME(), nullable=False)
lease_expires = Column(DATETIME(), nullable=False)
lease_updated = Column(DATETIME(), nullable=False)
@@ -83,6 +85,7 @@ class Lease(Base):
return {
'lease_ref': self.lease_ref,
'origin_ref': self.origin_ref,
# 'scope_ref': self.scope_ref,
'lease_created': self.lease_created.isoformat(),
'lease_expires': self.lease_expires.isoformat(),
'lease_updated': self.lease_updated.isoformat(),
@@ -178,4 +181,14 @@ def migrate(engine: Engine):
Lease.__table__.drop(bind=engine)
init(engine)
# def upgrade_1_2_to_1_3():
# x = db.dialect.get_columns(engine.connect(), Lease.__tablename__)
# x = next((_ for _ in x if _['name'] == 'scope_ref'), None)
# if x is None:
# Lease.scope_ref.compile()
# column_name = Lease.scope_ref.name
# column_type = Lease.scope_ref.type.compile(engine.dialect)
# engine.execute(f'ALTER TABLE "{Lease.__tablename__}" ADD COLUMN "{column_name}" {column_type}')
upgrade_1_0_to_1_1()
# upgrade_1_2_to_1_3()

26
doc/Database.md Normal file
View File

@@ -0,0 +1,26 @@
# Database structure
## `request_routing.service_instance`
| xid | org_name |
|----------------------------------------|--------------------------|
| `10000000-0000-0000-0000-000000000000` | `lic-000000000000000000` |
- `xid` is used as `SERVICE_INSTANCE_XID`
## `request_routing.license_allotment_service_instance`
| xid | service_instance_xid | license_allotment_xid |
|----------------------------------------|----------------------------------------|----------------------------------------|
| `90000000-0000-0000-0000-000000000001` | `10000000-0000-0000-0000-000000000000` | `80000000-0000-0000-0000-000000000001` |
- `xid` is only a primary-key and never used as foreign-key or reference
- `license_allotment_xid` must be used to fetch `xid`'s from `request_routing.license_allotment_reference`
## `request_routing.license_allotment_reference`
| xid | license_allotment_xid |
|----------------------------------------|----------------------------------------|
| `20000000-0000-0000-0000-000000000001` | `80000000-0000-0000-0000-000000000001` |
- `xid` is used as `scope_ref_list` on token request

View File

@@ -33,6 +33,9 @@ nvidia-gridd[2986]: License acquired successfully. (Info: license.nvidia.space,
Most variables and configs are stored in `/var/lib/docker/volumes/configurations/_data`.
Files can be modified with `docker cp <container-id>:/venv/... /opt/localfile/...` and back.
(May you need to fix permissions with `docker exec -u 0 <container-id> chown nonroot:nonroot /venv/...`)
## Dive / Docker image inspector
- `dive dls:appliance`

118
docker-compose.yml Normal file
View File

@@ -0,0 +1,118 @@
version: '3.9'
x-dls-variables: &dls-variables
DLS_URL: localhost # REQUIRED, change to your ip or hostname
DLS_PORT: 443 # must match nginx listen & exposed port
LEASE_EXPIRE_DAYS: 90
DATABASE: sqlite:////app/database/db.sqlite
DEBUG: false
services:
dls:
image: collinwebdesigns/fastapi-dls:latest
restart: always
environment:
<<: *dls-variables
volumes:
- /opt/docker/fastapi-dls/cert:/app/cert # instance.private.pem, instance.public.pem
- db:/app/database
entrypoint: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app", "--proxy-headers"]
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8000/-/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
proxy:
image: nginx
ports:
# thees are ports where nginx (!) is listen to
- "80:80" # for "/leasing/v1/lessor/shutdown" used by windows guests, can't be changed!
- "443:443" # first part must match "DLS_PORT"
volumes:
- /opt/docker/fastapi-dls/cert:/opt/cert
healthcheck:
test: ["CMD", "curl", "--insecure", "--fail", "https://localhost/-/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
command: |
bash -c "bash -s <<\"EOF\"
cat > /etc/nginx/nginx.conf <<\"EON\"
daemon off;
user root;
worker_processes auto;
events {
worker_connections 1024;
}
http {
gzip on;
gzip_disable "msie6";
include /etc/nginx/mime.types;
upstream dls-backend {
server dls:8000; # must match dls listen port
}
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
root /var/www/html;
index index.html;
server_name _;
ssl_certificate "/opt/cert/webserver.crt";
ssl_certificate_key "/opt/cert/webserver.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_protocols TLSv1.3 TLSv1.2;
# ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305";
# ssl_ciphers PROFILE=SYSTEM;
ssl_prefer_server_ciphers on;
location / {
proxy_set_header Host $$http_host;
proxy_set_header X-Real-IP $$remote_addr;
proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $$scheme;
proxy_pass http://dls-backend$$request_uri;
}
location = /-/health {
access_log off;
add_header 'Content-Type' 'application/json';
return 200 '{\"status\":\"up\",\"service\":\"nginx\"}';
}
}
server {
listen 80;
listen [::]:80;
root /var/www/html;
index index.html;
server_name _;
location /leasing/v1/lessor/shutdown {
proxy_set_header Host $$http_host;
proxy_set_header X-Real-IP $$remote_addr;
proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $$scheme;
proxy_pass http://dls-backend/leasing/v1/lessor/shutdown;
}
location / {
return 301 https://$$host$$request_uri;
}
}
}
EON
nginx
EOF"
volumes:
db:

View File

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

View File

@@ -3,7 +3,7 @@ from hashlib import sha256
from calendar import timegm
from datetime import datetime
from os.path import dirname, join
from uuid import uuid4
from uuid import uuid4, UUID
from dateutil.relativedelta import relativedelta
from jose import jwt, jwk
@@ -20,8 +20,7 @@ from app.util import load_key
client = TestClient(main.app)
ORIGIN_REF, LEASE_REF = str(uuid4()), str(uuid4())
SECRET = "HelloWorld"
ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-000000000001', 'HelloWorld'
# INSTANCE_KEY_RSA = generate_key()
# INSTANCE_KEY_PUB = INSTANCE_KEY_RSA.public_key()
@@ -44,16 +43,15 @@ def test_index():
assert response.status_code == 200
def test_status():
response = client.get('/status')
assert response.status_code == 200
assert response.json()['status'] == 'up'
def test_health():
response = client.get('/-/health')
assert response.status_code == 200
assert response.json()['status'] == 'up'
assert response.json().get('status') == 'up'
def test_config():
response = client.get('/-/config')
assert response.status_code == 200
def test_readme():
@@ -71,11 +69,6 @@ def test_client_token():
assert response.status_code == 200
def test_client_token_deprecated():
response = client.get('/client-token')
assert response.status_code == 200
def test_origins():
pass
@@ -110,7 +103,7 @@ def test_auth_v1_origin():
response = client.post('/auth/v1/origin', json=payload)
assert response.status_code == 200
assert response.json()['origin_ref'] == ORIGIN_REF
assert response.json().get('origin_ref') == ORIGIN_REF
def auth_v1_origin_update():
@@ -131,7 +124,7 @@ def auth_v1_origin_update():
response = client.post('/auth/v1/origin/update', json=payload)
assert response.status_code == 200
assert response.json()['origin_ref'] == ORIGIN_REF
assert response.json().get('origin_ref') == ORIGIN_REF
def test_auth_v1_code():
@@ -143,8 +136,8 @@ def test_auth_v1_code():
response = client.post('/auth/v1/code', json=payload)
assert response.status_code == 200
payload = jwt.get_unverified_claims(token=response.json()['auth_code'])
assert payload['origin_ref'] == ORIGIN_REF
payload = jwt.get_unverified_claims(token=response.json().get('auth_code'))
assert payload.get('origin_ref') == ORIGIN_REF
def test_auth_v1_token():
@@ -168,9 +161,9 @@ def test_auth_v1_token():
response = client.post('/auth/v1/token', json=payload)
assert response.status_code == 200
token = response.json()['auth_token']
token = response.json().get('auth_token')
payload = jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False})
assert payload['origin_ref'] == ORIGIN_REF
assert payload.get('origin_ref') == ORIGIN_REF
def test_leasing_v1_lessor():
@@ -183,46 +176,67 @@ def test_leasing_v1_lessor():
'product': {'name': 'NVIDIA RTX Virtual Workstation'}
}],
'proposal_evaluation_mode': 'ALL_OF',
'scope_ref_list': [LEASE_REF]
'scope_ref_list': [ALLOTMENT_REF]
}
response = client.post('/leasing/v1/lessor', json=payload, headers={'authorization': __bearer_token(ORIGIN_REF)})
assert response.status_code == 200
lease_result_list = response.json()['lease_result_list']
lease_result_list = response.json().get('lease_result_list')
assert len(lease_result_list) == 1
assert lease_result_list[0]['lease']['ref'] == LEASE_REF
assert len(lease_result_list[0]['lease']['ref']) == 36
assert str(UUID(lease_result_list[0]['lease']['ref'])) == lease_result_list[0]['lease']['ref']
return lease_result_list[0]['lease']['ref']
def test_leasing_v1_lessor_lease():
response = client.get('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)})
assert response.status_code == 200
active_lease_list = response.json()['active_lease_list']
active_lease_list = response.json().get('active_lease_list')
assert len(active_lease_list) == 1
assert active_lease_list[0] == LEASE_REF
assert len(active_lease_list[0]) == 36
assert str(UUID(active_lease_list[0])) == active_lease_list[0]
def test_leasing_v1_lease_renew():
response = client.put(f'/leasing/v1/lease/{LEASE_REF}', headers={'authorization': __bearer_token(ORIGIN_REF)})
response = client.get('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)})
active_lease_list = response.json().get('active_lease_list')
active_lease_ref = active_lease_list[0]
###
response = client.put(f'/leasing/v1/lease/{active_lease_ref}', headers={'authorization': __bearer_token(ORIGIN_REF)})
assert response.status_code == 200
assert response.json()['lease_ref'] == LEASE_REF
lease_ref = response.json().get('lease_ref')
assert len(lease_ref) == 36
assert lease_ref == active_lease_ref
def test_leasing_v1_lease_delete():
response = client.delete(f'/leasing/v1/lease/{LEASE_REF}', headers={'authorization': __bearer_token(ORIGIN_REF)})
response = client.get('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)})
active_lease_list = response.json().get('active_lease_list')
active_lease_ref = active_lease_list[0]
###
response = client.delete(f'/leasing/v1/lease/{active_lease_ref}', headers={'authorization': __bearer_token(ORIGIN_REF)})
assert response.status_code == 200
assert response.json()['lease_ref'] == LEASE_REF
lease_ref = response.json().get('lease_ref')
assert len(lease_ref) == 36
assert lease_ref == active_lease_ref
def test_leasing_v1_lessor_lease_remove():
test_leasing_v1_lessor()
lease_ref = test_leasing_v1_lessor()
response = client.delete('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)})
assert response.status_code == 200
released_lease_list = response.json()['released_lease_list']
released_lease_list = response.json().get('released_lease_list')
assert len(released_lease_list) == 1
assert released_lease_list[0] == LEASE_REF
assert len(released_lease_list[0]) == 36
assert released_lease_list[0] == lease_ref

View File

@@ -1 +1 @@
VERSION=1.2
VERSION=1.3.2