Compare commits

...

63 Commits

Author SHA1 Message Date
Oscar Krause
84e51a5826 Merge branch 'ui' into 'main'
UI

See merge request oscar.krause/fastapi-dls!37
2025-04-08 10:43:14 +02:00
Oscar Krause
b6a0158ee2 Merge branch 'main' into ui
# Conflicts:
#	.DEBIAN/control
#	.PKGBUILD/PKGBUILD
#	requirements.txt
2025-04-08 10:41:55 +02:00
Oscar Krause
4522425bcc Merge branch 'dev' into 'main'
dev

See merge request oscar.krause/fastapi-dls!48
2025-04-08 10:10:31 +02:00
Oscar Krause
4568881d1e fixes 2025-04-08 09:56:51 +02:00
Oscar Krause
d4d49956fe fixes 2025-04-08 09:38:49 +02:00
Oscar Krause
44a63efc4f requirements.txt updated 2025-04-08 09:22:03 +02:00
Oscar Krause
ca0eedc1f2 .gitignore 2025-04-08 09:18:50 +02:00
Oscar Krause
6d5b389f2a .gitlab-ci.yml 2025-04-08 09:17:44 +02:00
Oscar Krause
c3dbc043b3 updated EOLs 2025-03-21 08:47:28 +01:00
Oscar Krause
666a07507e typos 2025-03-21 08:39:32 +01:00
Oscar Krause
76272af36f added some glfm 2025-03-21 07:47:41 +01:00
Oscar Krause
cc2a11d07b moved the only two FAQ entries to Known-Issues on README 2025-03-21 07:47:26 +01:00
Oscar Krause
b26704646d fixes 2025-03-20 20:24:43 +01:00
Oscar Krause
e0c9bb46ee requirements.txt updated 2025-03-20 20:09:20 +01:00
Oscar Krause
490720f2d6 code styling 2025-03-20 20:09:13 +01:00
Oscar Krause
9ed178098b removed doc directory 2025-03-20 07:11:01 +01:00
Oscar Krause
951fc35203 moved Reverse Engineering Notes to separate project
ref.: https://git.collinwebdesigns.de/nvidia/nls
2025-03-19 21:03:54 +01:00
Oscar Krause
26248a4ea5 fixed tests 2025-03-19 21:01:58 +01:00
Oscar Krause
85623d1a65 code styling 2025-03-18 14:47:33 +01:00
Oscar Krause
e6a2de40c9 fixed test deprecations 2025-03-18 10:33:52 +01:00
Oscar Krause
fd46eecfb3 created PrivateKey / PublicKey wrapper classes 2025-03-18 09:43:44 +01:00
Oscar Krause
958f23f79d fixed "cryptography" dependency 2025-03-17 14:09:55 +01:00
Oscar Krause
0bdd3a6ac2 migrated from "pycryptodome" to "cryptography" 2025-03-17 14:05:26 +01:00
Oscar Krause
8a269b0393 Merge branch 'main' into 'dev'
# Conflicts:
#   doc/Reverse Engineering Notes.md
2025-03-13 20:33:01 +01:00
Oscar Krause
6607756c08 updated Reverse Engineering Notes.md 2025-03-13 20:29:16 +01:00
Oscar Krause
0d8d546545 typos 2025-01-21 07:31:18 +01:00
Oscar Krause
b1c7d4f4b4 Merge branch 'main' into ui
# Conflicts:
#	.DEBIAN/control
#	.PKGBUILD/PKGBUILD
2024-12-16 15:38:41 +01:00
Oscar Krause
f95ba1347f Merge branch 'main' into ui
# Conflicts:
#	.PKGBUILD/PKGBUILD
2024-11-21 11:14:32 +01:00
Oscar Krause
9926f8248e fixes 2024-11-15 06:33:45 +01:00
Oscar Krause
36094276ea fixes 2024-11-14 07:23:12 +01:00
Oscar Krause
04bbca1a3a added "branchVersion" to origin table (if matrix is present) 2024-11-14 07:13:30 +01:00
Oscar Krause
486ebdb4fb implemented origins & leases auto refresh button 2024-11-14 07:12:33 +01:00
Oscar Krause
ef78fec58a fixed setTimeout to setInterval 2024-11-14 07:12:33 +01:00
Oscar Krause
9283129a08 updated bootstrap icons to 1.11.4 2024-11-14 07:12:33 +01:00
Oscar Krause
1bf8d27dbe updated bootstrap to 5.3.3 2024-11-14 07:12:33 +01:00
Oscar Krause
5fafda1123 implemented origins & leases counter for sidebar 2024-11-14 07:12:33 +01:00
Oscar Krause
227ee8cfe1 code styling 2024-11-14 07:12:33 +01:00
Oscar Krause
8f4ed2d9ff improved dashboard config view 2024-11-14 07:12:32 +01:00
Oscar Krause
87a79815dd implemented endpoint for expired origins (origins without active leases) 2024-11-14 07:10:01 +01:00
Oscar Krause
d25921dbb0 requirements.txt updated 2024-11-14 07:10:01 +01:00
Oscar Krause
161b48be8e removed "url_for" to prevent proxy http/https missmatch 2024-11-14 07:10:01 +01:00
Oscar Krause
90c1ce979d added meta tag for "Content-Security-Policy" 2024-11-14 07:10:01 +01:00
Oscar Krause
5d12226e9c fixed layouts path 2024-11-14 07:10:01 +01:00
Oscar Krause
0e39f1849b implemented button for delete all expired leases 2024-11-14 07:08:50 +01:00
Oscar Krause
77dd0922d8 fixed Origin.delete() 2024-11-14 07:08:50 +01:00
Oscar Krause
64327c3a64 fixes 2024-11-14 07:08:50 +01:00
Oscar Krause
0769a7fcaa helper.js - added "lease_renewal" to table 2024-11-14 07:08:50 +01:00
Oscar Krause
f9ac3786ab fixed origin null pointer exception on leases table 2024-11-14 07:08:19 +01:00
Oscar Krause
956f92eca6 PKGBUILD - fixed jinja dependency 2024-11-14 07:08:19 +01:00
Oscar Krause
a61ce2d478 dashboard_origins.html - implemented confirmation before deleting all origins 2024-11-14 07:08:19 +01:00
Oscar Krause
3a3401fb90 implemented refresh button 2024-11-14 07:08:19 +01:00
Oscar Krause
e04b4e09d9 display hostname as title on leases table 2024-11-14 07:08:19 +01:00
Oscar Krause
0c86f3275d display hostname as title on leases table 2024-11-14 07:08:19 +01:00
Oscar Krause
fc00dab5c9 fixed refreshing data after deleting some origin or lease 2024-11-14 07:07:50 +01:00
Oscar Krause
dcbcba88cb implemented delete origin endpoint for frontend 2024-11-14 07:07:50 +01:00
Oscar Krause
c8df32375a fixed timezone handling 2024-11-14 07:07:48 +01:00
Oscar Krause
2c9e8a8b0b sort leases by expires 2024-11-14 07:03:54 +01:00
Oscar Krause
02b2d5cbe8 sort origins by hostname 2024-11-14 07:03:54 +01:00
Oscar Krause
a77d3aa623 fixed origin table 2024-11-14 07:03:54 +01:00
Oscar Krause
783d95ab0d added jinja2 dependency 2024-11-14 07:03:51 +01:00
Oscar Krause
a6b82f0466 fixed imports and tests 2024-11-14 07:03:25 +01:00
Oscar Krause
46822bca80 added config values to dashboard 2024-11-14 07:00:50 +01:00
Oscar Krause
a30b8d039b implemented basic ui 2024-11-14 07:00:46 +01:00
42 changed files with 983 additions and 361 deletions

View File

@ -2,7 +2,7 @@ Package: fastapi-dls
Version: 0.0 Version: 0.0
Architecture: all Architecture: all
Maintainer: Oscar Krause oscar.krause@collinwebdesigns.de Maintainer: Oscar Krause oscar.krause@collinwebdesigns.de
Depends: python3, python3-fastapi, python3-uvicorn, python3-dotenv, python3-dateutil, python3-josepy, python3-sqlalchemy, python3-pycryptodome, python3-markdown, uvicorn, openssl Depends: python3, python3-fastapi, python3-uvicorn, python3-dotenv, python3-dateutil, python3-josepy, python3-sqlalchemy, python3-cryptography, python3-markdown, python3-jinja2, uvicorn, openssl
Recommends: curl Recommends: curl
Installed-Size: 10240 Installed-Size: 10240
Homepage: https://git.collinwebdesigns.de/oscar.krause/fastapi-dls Homepage: https://git.collinwebdesigns.de/oscar.krause/fastapi-dls

View File

@ -1,8 +1,8 @@
# https://packages.debian.org/hu/ # https://packages.debian.org/hu/
fastapi==0.92.0 fastapi==0.92.0
uvicorn[standard]==0.17.6 uvicorn[standard]==0.17.6
python-jose[pycryptodome]==3.3.0 python-jose[cryptography]==3.3.0
pycryptodome==3.11.0 cryptography==38.0.4
python-dateutil==2.8.2 python-dateutil==2.8.2
sqlalchemy==1.4.46 sqlalchemy==1.4.46
markdown==3.4.1 markdown==3.4.1

View File

@ -1,8 +1,8 @@
# https://packages.ubuntu.com # https://packages.ubuntu.com
fastapi==0.101.0 fastapi==0.101.0
uvicorn[standard]==0.27.1 uvicorn[standard]==0.27.1
python-jose[pycryptodome]==3.3.0 python-jose[cryptography]==3.3.0
pycryptodome==3.20.0 cryptography==41.0.7
python-dateutil==2.8.2 python-dateutil==2.8.2
sqlalchemy==1.4.50 sqlalchemy==1.4.50
markdown==3.5.2 markdown==3.5.2

View File

@ -1,8 +1,8 @@
# https://packages.ubuntu.com # https://packages.ubuntu.com
fastapi==0.110.3 fastapi==0.110.3
uvicorn[standard]==0.30.3 uvicorn[standard]==0.30.3
python-jose[pycryptodome]==3.3.0 python-jose[cryptography]==3.3.0
pycryptodome==3.20.0 cryptography==42.0.5
python-dateutil==2.9.0 python-dateutil==2.9.0
sqlalchemy==2.0.32 sqlalchemy==2.0.32
markdown==3.6 markdown==3.6

View File

@ -8,7 +8,7 @@ pkgdesc='NVIDIA DLS server implementation with FastAPI'
arch=('any') arch=('any')
url='https://git.collinwebdesigns.de/oscar.krause/fastapi-dls' url='https://git.collinwebdesigns.de/oscar.krause/fastapi-dls'
license=('MIT') 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-cryptography' 'python-jinja' 'uvicorn' 'python-markdown' 'openssl')
provider=("$pkgname") provider=("$pkgname")
install="$pkgname.install" install="$pkgname.install"
backup=('etc/default/fastapi-dls') backup=('etc/default/fastapi-dls')
@ -37,17 +37,52 @@ check() {
} }
package() { package() {
# create directories
install -d "$pkgdir/usr/share/doc/$pkgname" install -d "$pkgdir/usr/share/doc/$pkgname"
install -d "$pkgdir/var/lib/$pkgname/cert" install -d "$pkgdir/var/lib/$pkgname/cert"
cp -r "$srcdir/$pkgname/doc"/* "$pkgdir/usr/share/doc/$pkgname/"
# copy docs & static files
#cp -r "$srcdir/$pkgname/doc"/* "$pkgdir/usr/share/doc/$pkgname/"
install -Dm644 "$srcdir/$pkgname/README.md" "$pkgdir/usr/share/doc/$pkgname/README.md" install -Dm644 "$srcdir/$pkgname/README.md" "$pkgdir/usr/share/doc/$pkgname/README.md"
install -Dm644 "$srcdir/$pkgname/version.env" "$pkgdir/usr/share/doc/$pkgname/version.env" install -Dm644 "$srcdir/$pkgname/version.env" "$pkgdir/usr/share/doc/$pkgname/version.env"
sed -i "s/README.md/\/usr\/share\/doc\/$pkgname\/README.md/g" "$srcdir/$pkgname/app/main.py" sed -i "s/README.md/\/usr\/share\/doc\/$pkgname\/README.md/g" "$srcdir/$pkgname/app/main.py"
# copy main app python files
sed -i "s/join(dirname(__file__), 'cert\//join('\/var\/lib\/$pkgname', 'cert\//g" "$srcdir/$pkgname/app/main.py" sed -i "s/join(dirname(__file__), 'cert\//join('\/var\/lib\/$pkgname', 'cert\//g" "$srcdir/$pkgname/app/main.py"
install -Dm755 "$srcdir/$pkgname/app/main.py" "$pkgdir/opt/$pkgname/main.py" install -Dm755 "$srcdir/$pkgname/app/main.py" "$pkgdir/opt/$pkgname/main.py"
install -Dm755 "$srcdir/$pkgname/app/orm.py" "$pkgdir/opt/$pkgname/orm.py" install -Dm755 "$srcdir/$pkgname/app/orm.py" "$pkgdir/opt/$pkgname/orm.py"
install -Dm755 "$srcdir/$pkgname/app/util.py" "$pkgdir/opt/$pkgname/util.py" install -Dm755 "$srcdir/$pkgname/app/util.py" "$pkgdir/opt/$pkgname/util.py"
# copy static asset files
install -Dm755 "$srcdir/$pkgname/app/static/assets/css/bootstrap.min.css" "$pkgdir/opt/$pkgname/static/assets/css/bootstrap.min.css"
install -Dm755 "$srcdir/$pkgname/app/static/assets/css/bootstrap-icons.min.css" "$pkgdir/opt/$pkgname/static/assets/css/bootstrap-icons.min.css"
install -Dm755 "$srcdir/$pkgname/app/static/assets/css/custom.css" "$pkgdir/opt/$pkgname/static/assets/css/custom.css"
install -Dm755 "$srcdir/$pkgname/app/static/assets/css/dashboard.css" "$pkgdir/opt/$pkgname/static/assets/css/dashboard.css"
install -Dm755 "$srcdir/$pkgname/app/static/assets/fonts/bootstrap-icons.woff" "$pkgdir/opt/$pkgname/static/assets/fonts/bootstrap-icons.woff"
install -Dm755 "$srcdir/$pkgname/app/static/assets/fonts/bootstrap-icons.woff2" "$pkgdir/opt/$pkgname/static/assets/fonts/bootstrap-icons.woff2"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/android-chrome-192x192.png" "$pkgdir/opt/$pkgname/static/assets/img/favicons/android-chrome-192x192.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/android-chrome-512x512.png" "$pkgdir/opt/$pkgname/static/assets/img/favicons/android-chrome-512x512.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/apple-touch-icon.png" "$pkgdir/opt/$pkgname/static/assets/img/favicons/apple-touch-icon.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/favicon.ico" "$pkgdir/opt/$pkgname/static/assets/img/favicons/favicon.ico"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/favicon-16x16.png" "$pkgdir/opt/$pkgname/static/assets/img/favicons/favicon-16x16.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/favicon-32x32.png" "$pkgdir/opt/$pkgname/static/assets/img/favicons/favicon-32x32.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/manifest.json" "$pkgdir/opt/$pkgname/static/assets/img/favicons/manifest.json"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/logo.png" "$pkgdir/opt/$pkgname/static/assets/img/logo.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/js/bootstrap.min.js" "$pkgdir/opt/$pkgname/static/assets/js/bootstrap.min.js"
install -Dm755 "$srcdir/$pkgname/app/static/assets/js/helper.js" "$pkgdir/opt/$pkgname/static/assets/js/helper.js"
install -Dm755 "$srcdir/$pkgname/app/static/assets/js/popper.min.js" "$pkgdir/opt/$pkgname/static/assets/js/popper.min.js"
install -Dm755 "$srcdir/$pkgname/app/templates/components/navbar.html" "$pkgdir/opt/$pkgname/templates/components/navbar.html"
install -Dm755 "$srcdir/$pkgname/app/templates/components/sidebar.html" "$pkgdir/opt/$pkgname/templates/components/sidebar.html"
install -Dm755 "$srcdir/$pkgname/app/templates/layouts/base.html" "$pkgdir/opt/$pkgname/templates/layouts/base.html"
install -Dm755 "$srcdir/$pkgname/app/templates/layouts/bootstrap.html" "$pkgdir/opt/$pkgname/templates/layouts/bootstrap.html"
install -Dm755 "$srcdir/$pkgname/app/templates/layouts/bootstrap-dashboard.html" "$pkgdir/opt/$pkgname/templates/layouts/bootstrap-dashboard.html"
install -Dm755 "$srcdir/$pkgname/app/templates/views/dashboard.html" "$pkgdir/opt/$pkgname/templates/views/dashboard.html"
install -Dm755 "$srcdir/$pkgname/app/templates/views/dashboard_leases.html" "$pkgdir/opt/$pkgname/templates/views/dashboard_leases.html"
install -Dm755 "$srcdir/$pkgname/app/templates/views/dashboard_origins.html" "$pkgdir/opt/$pkgname/templates/views/dashboard_origins.html"
install -Dm755 "$srcdir/$pkgname/app/templates/views/dashboard_readme.html" "$pkgdir/opt/$pkgname/templates/views/dashboard_readme.html"
install -Dm755 "$srcdir/$pkgname/app/templates/views/index.html" "$pkgdir/opt/$pkgname/templates/views/index.html"
# copy service files
install -Dm644 "$srcdir/$pkgname.default" "$pkgdir/etc/default/$pkgname" install -Dm644 "$srcdir/$pkgname.default" "$pkgdir/etc/default/$pkgname"
install -Dm644 "$srcdir/$pkgname.service" "$pkgdir/usr/lib/systemd/system/$pkgname.service" install -Dm644 "$srcdir/$pkgname.service" "$pkgdir/usr/lib/systemd/system/$pkgname.service"
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf" install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
.DS_Store .DS_Store
venv/ venv/
.idea/ .idea/
app/*.sqlite* *.sqlite
app/cert/*.* app/cert/*.*
.pytest_cache .pytest_cache

View File

@ -153,7 +153,7 @@ test:
- source venv/bin/activate - source venv/bin/activate
- pip install --upgrade pip - pip install --upgrade pip
- pip install -r $REQUIREMENTS - pip install -r $REQUIREMENTS
- pip install pytest httpx - pip install pytest pytest-cov pytest-custom_exit_code httpx
- mkdir -p app/cert - mkdir -p app/cert
- openssl genrsa -out app/cert/instance.private.pem 2048 - openssl genrsa -out app/cert/instance.private.pem 2048
- openssl rsa -in app/cert/instance.private.pem -outform PEM -pubout -out app/cert/instance.public.pem - openssl rsa -in app/cert/instance.private.pem -outform PEM -pubout -out app/cert/instance.public.pem
@ -265,19 +265,19 @@ test_coverage:
before_script: before_script:
- apt-get update && apt-get install -y python3-dev gcc - apt-get update && apt-get install -y python3-dev gcc
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install pytest httpx - pip install pytest pytest-cov pytest-custom_exit_code httpx
- mkdir -p app/cert - mkdir -p app/cert
- openssl genrsa -out app/cert/instance.private.pem 2048 - openssl genrsa -out app/cert/instance.private.pem 2048
- openssl rsa -in app/cert/instance.private.pem -outform PEM -pubout -out app/cert/instance.public.pem - openssl rsa -in app/cert/instance.private.pem -outform PEM -pubout -out app/cert/instance.public.pem
- cd test - cd test
script: script:
- pip install pytest pytest-cov - coverage run -m pytest main.py --junitxml=report.xml --suppress-no-test-exit-code
- coverage run -m pytest main.py
- coverage report - coverage report
- coverage xml - coverage xml
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts: artifacts:
reports: reports:
junit: [ '**/report.xml' ]
coverage_report: coverage_report:
coverage_format: cobertura coverage_format: cobertura
path: '**/coverage.xml' path: '**/coverage.xml'

17
FAQ.md
View File

@ -1,17 +0,0 @@
# 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.

View File

@ -2,9 +2,12 @@
Minimal Delegated License Service (DLS). Minimal Delegated License Service (DLS).
> [!note]
> Compatibility tested with official NLS 2.0.1, 2.1.0, 3.1.0, 3.3.1, 3.4.0. For Driver compatibility > Compatibility tested with official NLS 2.0.1, 2.1.0, 3.1.0, 3.3.1, 3.4.0. For Driver compatibility
see [compatibility matrix](#vgpu-software-compatibility-matrix). > see [compatibility matrix](#vgpu-software-compatibility-matrix).
Drivers are only supported until **17.x releases**.
> [!warning] 18.x Drivers are not yet supported!
> Drivers are only supported until **17.x releases**.
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.
@ -83,7 +86,7 @@ docker run -e DLS_URL=`hostname -i` -e DLS_PORT=443 -p 443:443 -v $WORKING_DIR:/
See [`examples`](examples) directory for more advanced examples (with reverse proxy usage). See [`examples`](examples) directory for more advanced examples (with reverse proxy usage).
> Adjust *REQUIRED* variables as needed > Adjust `REQUIRED` variables as needed
```yaml ```yaml
version: '3.9' version: '3.9'
@ -330,14 +333,14 @@ Packages are available here:
- [GitLab-Registry](https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/packages) - [GitLab-Registry](https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/packages)
Successful tested with: Successful tested with (**LTS Version**):
- **Debian 12 (Bookworm)** (EOL: June 06, 2026) - **Debian 12 (Bookworm)** (EOL: June 06, 2026)
- *Ubuntu 22.10 (Kinetic Kudu)* (EOL: July 20, 2023) - *Ubuntu 22.10 (Kinetic Kudu)* (EOL: July 20, 2023)
- *Ubuntu 23.04 (Lunar Lobster)* (EOL: January 2024) - *Ubuntu 23.04 (Lunar Lobster)* (EOL: January 2024)
- *Ubuntu 23.10 (Mantic Minotaur)* (EOL: July 2024) - *Ubuntu 23.10 (Mantic Minotaur)* (EOL: July 2024)
- **Ubuntu 24.04 (Noble Numbat)** (EOL: April 2036) - **Ubuntu 24.04 (Noble Numbat)** (EOL: Apr 2029)
- *Ubuntu 24.10 (Oracular Oriole)* (EOL: tba.) - *Ubuntu 24.10 (Oracular Oriole)* (EOL: Jul 2025)
Not working with: Not working with:
@ -600,6 +603,21 @@ Logs are available in `C:\Users\Public\Documents\Nvidia\LoggingLog.NVDisplay.Con
# Known Issues # Known Issues
## Generic
### `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 recreate `instance.public.pem` / `instance.private.pem`?
Then you have to download a **new** client-token on each of your guests.
## Linux ## Linux
### Invalid HTTP request ### Invalid HTTP request

View File

@ -18,10 +18,12 @@ from jose.constants import ALGORITHMS
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from starlette.responses import StreamingResponse, JSONResponse as JSONr, HTMLResponse as HTMLr, Response, RedirectResponse from starlette.responses import StreamingResponse, JSONResponse as JSONr, Response, RedirectResponse
from starlette.staticfiles import StaticFiles
from starlette.templating import Jinja2Templates
from orm import Origin, Lease, init as db_init, migrate from orm import Origin, Lease, init as db_init, migrate
from util import load_key, load_file from util import PrivateKey, PublicKey, load_file
# Load variables # Load variables
load_dotenv('../version.env') load_dotenv('../version.env')
@ -42,8 +44,8 @@ DLS_PORT = int(env('DLS_PORT', '443'))
SITE_KEY_XID = str(env('SITE_KEY_XID', '00000000-0000-0000-0000-000000000000')) SITE_KEY_XID = str(env('SITE_KEY_XID', '00000000-0000-0000-0000-000000000000'))
INSTANCE_REF = str(env('INSTANCE_REF', '10000000-0000-0000-0000-000000000001')) INSTANCE_REF = str(env('INSTANCE_REF', '10000000-0000-0000-0000-000000000001'))
ALLOTMENT_REF = str(env('ALLOTMENT_REF', '20000000-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_RSA = PrivateKey.from_file(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')))) INSTANCE_KEY_PUB = PublicKey.from_file(str(env('INSTANCE_KEY_PUB', join(dirname(__file__), 'cert/instance.public.pem'))))
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))
@ -51,8 +53,8 @@ LEASE_RENEWAL_DELTA = timedelta(days=int(env('LEASE_EXPIRE_DAYS', 90)), hours=in
CLIENT_TOKEN_EXPIRE_DELTA = relativedelta(years=12) 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.pem(), algorithm=ALGORITHMS.RS256)
jwt_decode_key = jwk.construct(INSTANCE_KEY_PUB.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256) jwt_decode_key = jwk.construct(INSTANCE_KEY_PUB.pem(), algorithm=ALGORITHMS.RS256)
# Logging # Logging
LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO
@ -87,6 +89,8 @@ async def lifespan(_: FastAPI):
config = dict(openapi_url=None, docs_url=None, redoc_url=None) # 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, lifespan=lifespan, **config) app = FastAPI(title='FastAPI-DLS', description='Minimal Delegated License Service (DLS).', version=VERSION, lifespan=lifespan, **config)
app.mount('/static', StaticFiles(directory=join(dirname(__file__), 'static'), html=True), name='static')
templates = Jinja2Templates(directory=join(dirname(__file__), 'templates'))
app.debug = DEBUG app.debug = DEBUG
app.add_middleware( app.add_middleware(
@ -105,26 +109,8 @@ def __get_token(request: Request) -> dict:
return jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False}) return jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False})
# Endpoints def __json_config() -> dict:
return {
@app.get('/', summary='Index')
async def index():
return RedirectResponse('/-/readme')
@app.get('/-/', summary='* Index')
async def _index():
return RedirectResponse('/-/readme')
@app.get('/-/health', summary='* Health')
async def _health():
return JSONr({'status': 'up'})
@app.get('/-/config', summary='* Config', description='returns environment variables.')
async def _config():
return JSONr({
'VERSION': str(VERSION), 'VERSION': str(VERSION),
'COMMIT': str(COMMIT), 'COMMIT': str(COMMIT),
'DEBUG': str(DEBUG), 'DEBUG': str(DEBUG),
@ -138,52 +124,66 @@ async def _config():
'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), 'TZ': str(TZ),
}) # static / calculated
'LEASE_RENEWAL_DELTA': str(LEASE_RENEWAL_DELTA),
'LEASE_CALCULATED_RENEWAL': str(Lease.calculate_renewal(LEASE_RENEWAL_PERIOD, LEASE_RENEWAL_DELTA)),
'CLIENT_TOKEN_EXPIRE_DELTA': str(CLIENT_TOKEN_EXPIRE_DELTA),
}
# Endpoints
@app.get('/', summary='* Index')
async def index():
return RedirectResponse('/-/')
@app.get('/-/', summary='* Index')
async def _index(request: Request):
return templates.TemplateResponse(name='views/index.html', context={'request': request, 'VERSION': VERSION})
@app.get('/-/health', summary='* Health')
async def _health():
return JSONr({'status': 'up'})
@app.get('/-/config', summary='* Config', description='returns environment variables.')
async def _config():
return JSONr(__json_config())
@app.get('/-/readme', summary='* Readme') @app.get('/-/readme', summary='* Readme')
async def _readme(): async def _readme(request: Request):
from markdown import markdown from markdown import markdown
content = load_file(join(dirname(__file__), '../README.md')).decode('utf-8') content = load_file(join(dirname(__file__), '../README.md')).decode('utf-8')
return HTMLr(markdown(text=content, extensions=['tables', 'fenced_code', 'md_in_html', 'nl2br', 'toc'])) markdown = markdown(text=content, extensions=['tables', 'fenced_code', 'md_in_html', 'nl2br', 'toc'])
context = {'request': request, 'VERSION': VERSION, 'markdown': markdown }
return templates.TemplateResponse(name='views/dashboard_readme.html', context=context)
@app.get('/-/manage', summary='* Management UI') @app.get('/-/manage', summary='* Management UI')
async def _manage(request: Request): async def _manage(request: Request):
response = ''' context = {'request': request, 'VERSION': VERSION}
<!DOCTYPE html> return templates.TemplateResponse(name='views/manage.html', context=context)
<html>
<head>
<title>FastAPI-DLS Management</title>
</head>
<body>
<button onclick="deleteOrigins()">delete ALL origins and their leases</button>
<button onclick="deleteLease()">delete specific lease</button>
<script>
function deleteOrigins() {
const response = confirm('Are you sure you want to delete all origins and their leases?');
if (response) { @app.get('/-/dashboard', summary='* Dashboard')
var xhr = new XMLHttpRequest(); async def _dashboard(request: Request):
xhr.open("DELETE", '/-/origins', true); context = {'request': request, 'VERSION': VERSION, 'CONFIG': __json_config()}
xhr.send(); return templates.TemplateResponse(name='views/dashboard.html', context=context)
}
}
function deleteLease(lease_ref) { @app.get('/-/dashboard/origins', summary='* Dashboard - Origins')
if(lease_ref === undefined) async def _dashboard_origins(request: Request):
lease_ref = window.prompt("Please enter 'lease_ref' which should be deleted"); context = {'request': request, 'VERSION': VERSION}
if(lease_ref === null || lease_ref === "") return templates.TemplateResponse(name='views/dashboard_origins.html', context=context)
return
var xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/lease/${lease_ref}`, true); @app.get('/-/dashboard/leases', summary='* Dashboard - Leases')
xhr.send(); async def _dashboard_origins(request: Request):
} context = {'request': request, 'VERSION': VERSION}
</script> return templates.TemplateResponse(name='views/dashboard_leases.html', context=context)
</body>
</html>
'''
return HTMLr(response)
@app.get('/-/origins', summary='* Origins') @app.get('/-/origins', summary='* Origins')
@ -206,6 +206,19 @@ async def _origins_delete(request: Request):
return Response(status_code=201) return Response(status_code=201)
@app.delete('/-/origins/expired', summary='* Delete all Origins without active Lease')
async def _origins_delete_expired(request: Request):
Origin.delete_expired(db)
return Response(status_code=201)
@app.delete('/-/origins/{origin_ref}', summary='* Delete specific Origin')
async def _origins_delete_origin_ref(request: Request, origin_ref: str):
if Origin.delete(db, [origin_ref]) == 1:
return Response(status_code=201)
return JSONr(status_code=404, content={'status': 404, 'detail': 'lease not found'})
@app.get('/-/leases', summary='* Leases') @app.get('/-/leases', summary='* Leases')
async def _leases(request: Request, origin: bool = False): async def _leases(request: Request, origin: bool = False):
session = sessionmaker(bind=db)() session = sessionmaker(bind=db)()
@ -222,13 +235,13 @@ async def _leases(request: Request, origin: bool = False):
return JSONr(response) return JSONr(response)
@app.delete('/-/leases/expired', summary='* Leases') @app.delete('/-/leases/expired', summary='* Delete all expired Leases')
async def _lease_delete_expired(request: Request): async def _lease_delete_expired(request: Request):
Lease.delete_expired(db) Lease.delete_expired(db)
return Response(status_code=201) return Response(status_code=201)
@app.delete('/-/lease/{lease_ref}', summary='* Lease') @app.delete('/-/lease/{lease_ref}', summary='* Delete specific Lease')
async def _lease_delete(request: Request, lease_ref: str): async def _lease_delete(request: Request, lease_ref: str):
if Lease.delete(db, lease_ref) == 1: if Lease.delete(db, lease_ref) == 1:
return Response(status_code=201) return Response(status_code=201)
@ -264,10 +277,10 @@ async def _client_token():
}, },
"service_instance_public_key_configuration": { "service_instance_public_key_configuration": {
"service_instance_public_key_me": { "service_instance_public_key_me": {
"mod": hex(INSTANCE_KEY_PUB.public_key().n)[2:], "mod": hex(INSTANCE_KEY_PUB.raw().public_numbers().n)[2:],
"exp": int(INSTANCE_KEY_PUB.public_key().e), "exp": int(INSTANCE_KEY_PUB.raw().public_numbers().e),
}, },
"service_instance_public_key_pem": INSTANCE_KEY_PUB.export_key().decode('utf-8'), "service_instance_public_key_pem": INSTANCE_KEY_PUB.pem().decode('utf-8'),
"key_retention_mode": "LATEST_ONLY" "key_retention_mode": "LATEST_ONLY"
}, },
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,56 @@
/*
Original: #76b900
Darken 1: #5DA000 (10%)
Darken 2: #438600 (20%)
Darken 3: #2A6D00 (30%)
Darken 4: #105300 (40%)
Darken 5: #003A00 (50%)
*/
.text-primary {
color: #76b900 !important;
}
.lead {
color: #105300 !important;
}
.navbar-green {
background-color: #76b900 !important;
}
.navbar-brand {
background-color: transparent;
color: #ffffff;
}
.navbar-brand:focus, .navbar-brand:hover {
color: #fcfcfc;
}
.btn-primary {
background-color: #76b900 !important;
border-color: #76b900 !important;
}
.btn-primary:focus, .btn-primary:hover {
background-color: #5DA000 !important;
border-color: #5DA000 !important;
}
code {
color: #105300 !important;
}
.sidebar .nav-link.active {
color: #76b900 !important;
}
.sidebar .nav-link:focus, .sidebar .nav-link:hover {
color: #105300 !important;
}
.navbar-nav .nav-item .nav-link {
color: white !important;
}

View File

@ -0,0 +1,101 @@
body {
font-size: .875rem;
}
.feather {
width: 16px;
height: 16px;
vertical-align: text-bottom;
}
/*
* Sidebar
*/
.sidebar {
position: fixed;
top: 0;
/* rtl:raw:
right: 0;
*/
bottom: 0;
/* rtl:remove */
left: 0;
z-index: 100; /* Behind the navbar */
padding: 48px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
@media (max-width: 767.98px) {
.sidebar {
top: 0;
}
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 48px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link .feather {
margin-right: 4px;
color: #727272;
}
.sidebar .nav-link.active {
color: #2470dc;
}
.sidebar .nav-link:hover .feather,
.sidebar .nav-link.active .feather {
color: inherit;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
/*
* Navbar
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
font-size: 1rem;
background-color: rgba(0, 0, 0, .25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}
.navbar .navbar-toggler {
top: .25rem;
right: 1rem;
}
.navbar .form-control {
padding: .75rem 1rem;
border-width: 0;
border-radius: 0;
}
.form-control-dark {
color: #fff;
background-color: rgba(255, 255, 255, .1);
border-color: rgba(255, 255, 255, .1);
}
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

7
app/static/assets/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,140 @@
async function fetchConfig(element) {
let xhr = new XMLHttpRequest();
xhr.open("GET", '/-/config', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
element.innerHTML = JSON.stringify(JSON.parse(xhr.response),null,2);
}
};
xhr.send();
}
async function fetchOriginsWithLeases(element) {
let xhr = new XMLHttpRequest();
xhr.open("GET", '/-/origins?leases=true', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
const x = JSON.parse(xhr.response)
console.debug(x)
element.innerHTML = ''
let table = document.createElement('table')
table.classList.add('table', 'mt-4');
let thead = document.createElement('thead');
thead.innerHTML = `
<tr>
<th scope="col">origin</th>
<th scope="col">hostname</th>
<th scope="col">OS</th>
<th scope="col">driver version</th>
<th scope="col">leases</th>
</tr>`
table.appendChild(thead)
let tbody = document.createElement('thead');
x.sort((a, b) => a.hostname.localeCompare(b.hostname)).forEach((o) => {
let row = document.createElement('tr');
const branchVersion= o.$driver ? `(<code>${o.$driver.branch_version}</code>) ` : ''
row.innerHTML = `
<td><code>${o.origin_ref}</code></td>
<td>${o.hostname}</td>
<td>${o.os_platform} (${o.os_version})</td>
<td>${branchVersion}<code>${o.guest_driver_version}</code></td>
<td>${o.leases.map(x => `<code title="expires: ${x.lease_expires}">${x.lease_ref}</code>`).join(', ')}</td>`
tbody.appendChild(row);
})
table.appendChild(tbody)
element.appendChild(table)
}
};
xhr.send();
}
async function fetchLeases(element) {
// datetime config
const dtc = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short"
}
let xhr = new XMLHttpRequest();
xhr.open("GET", '/-/leases?origin=true', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
const x = JSON.parse(xhr.response)
console.debug(x)
element.innerHTML = ''
let table = document.createElement('table')
table.classList.add('table', 'mt-4');
let thead = document.createElement('thead');
thead.innerHTML = `
<tr>
<th scope="col">lease</th>
<th scope="col">created</th>
<th scope="col">updated</th>
<th scope="col">next renew</th>
<th scope="col">expires</th>
<th scope="col">origin</th>
</tr>`
table.appendChild(thead)
let tbody = document.createElement('thead');
x.sort((a, b) => new Date(a.lease_expires) - new Date(b.lease_expires)).forEach((o) => {
let row = document.createElement('tr');
row.innerHTML = `
<td><code>${o.lease_ref}</code></td>
<td>${new Date(o.lease_created).toLocaleDateString('system', dtc)}</td>
<td>${new Date(o.lease_updated).toLocaleDateString('system', dtc)}</td>
<td>${new Date(o.lease_renewal).toLocaleDateString('system', dtc)}</td>
<td>${new Date(o.lease_expires).toLocaleDateString('system', dtc)}</td>
<td><code title="hostname: ${o.origin?.hostname}">${o.origin_ref}</code></td>`
tbody.appendChild(row);
})
table.appendChild(tbody)
element.appendChild(table)
}
};
xhr.send();
}
async function deleteOrigins() {
let xhr = new XMLHttpRequest();
xhr.open("DELETE", '/-/origins', true);
xhr.send();
}
async function deleteOrigin(origin_ref) {
if (origin_ref === undefined)
origin_ref = window.prompt("Please enter 'origin_ref' which should be deleted");
if (origin_ref === null || origin_ref === "")
return
let xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/origins/${origin_ref}`, true);
xhr.send();
}
async function deleteExpiredOrigins() {
let xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/origins/expired`, true);
xhr.send();
}
async function deleteLease(lease_ref) {
if (lease_ref === undefined)
lease_ref = window.prompt("Please enter 'lease_ref' which should be deleted");
if (lease_ref === null || lease_ref === "")
return
let xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/lease/${lease_ref}`, true);
xhr.send();
}
async function deleteExpiredLeases() {
let xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/leases/expired`, true);
xhr.send();
}

5
app/static/assets/js/popper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
<header class="navbar navbar-expand-md navbar-green sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="/-/">FastAPI-DLS {{ VERSION }}</a>
<button class="navbar-toggler position-absolute d-lg-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</header>

View File

@ -0,0 +1,93 @@
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {{ 'active' if request.url.path == '/-/dashboard' }}" aria-current="page" href="/-/dashboard">
<i class="bi-house-door"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.url.path == '/-/dashboard/origins' }}" aria-current="page" href="/-/dashboard/origins">
<i class="bi-pc-display-horizontal"></i> Origins <span id="origin-cnt" class="badge text-bg-secondary"></span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.url.path == '/-/dashboard/leases' }}" aria-current="page" href="/-/dashboard/leases">
<i class="bi-layers"></i> Leases <span id="lease-cnt" class="badge text-bg-secondary"></span>
</a>
</li>
</ul>
<script type="application/javascript">
function loadLOriginCnt() {
let xhr = new XMLHttpRequest();
xhr.open("GET", '/-/origins?leases=false', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
const x = JSON.parse(xhr.response)
document.getElementById('origin-cnt').innerHTML = x.length
}
};
xhr.send();
}
function loadLeaseCnt() {
let xhr = new XMLHttpRequest();
xhr.open("GET", '/-/leases?origin=false', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
const x = JSON.parse(xhr.response)
document.getElementById('lease-cnt').innerHTML = x.length
}
};
xhr.send();
}
// load initial
loadLOriginCnt()
loadLeaseCnt()
// refresh every 5 seconds
setInterval(() => {
loadLOriginCnt()
loadLeaseCnt()
}, 5000);
</script>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
<span>Help</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {{ 'active' if request.url.path == '/-/readme' }}" aria-current="page" href="/-/readme">
<i class="bi-question-circle"></i> Readme
</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="https://git.collinwebdesigns.de/oscar.krause/fastapi-dls" target="_blank">
<i class="bi-git"></i> Git Repo
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
<span>Integrations</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/-/doc" target="_blank">
<i class="bi-file-text"></i> Swagger UI
</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/-/redoc" target="_blank">
<i class="bi-file-text"></i> Redoc
</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/-/openapi.json" target="_blank">
<i class="bi bi-filetype-json"></i> OpenAPI JSON
</a>
</li>
</ul>
</div>
</nav>

View File

@ -0,0 +1,33 @@
<!doctype html>
<html lang="en" class="h-100">
<head>
{% block title %}
<title>FastAPI-DLS</title>
{% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
<link rel="icon" href="/static/assets/img/favicons/favicon-32x32.png" sizes="32x32" type="image/png">
<link rel="icon" href="/static/assets/img/favicons/favicon-16x16.png" sizes="16x16" type="image/png">
<link rel="manifest" href="/static/assets/img/favicons/manifest.json">
<link rel="icon" href="/static/assets/img/favicons/favicon.ico">
<link rel="apple-touch-icon" href="/static/assets/img/favicons/apple-touch-icon.png" sizes="180x180">
{% block styles %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="/static/assets/css/custom.css">
</head>
<body class="d-flex flex-column {% block body_class %}{% endblock %}">
{% block body %}
{% endblock %}
<script src="/static/assets/js/helper.js"></script>
{% block scripts %}
{% endblock %}
</body>
</html>

View File

@ -0,0 +1,16 @@
{% extends 'layouts/bootstrap.html' %}
{% block body %}
{% include 'components/navbar.html' %}
<div class="container-fluid">
<div class="row">
{% include 'components/sidebar.html' %}
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
{% block content %}
{% endblock %}
</main>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends 'layouts/base.html' %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" type="text/css" href="/static/assets/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/static/assets/css/bootstrap-icons.min.css">
<link rel="stylesheet" type="text/css" href="/static/assets/css/dashboard.css">
<script src="/static/assets/js/popper.min.js"></script>
<script src="/static/assets/js/bootstrap.min.js"></script>
{% endblock %}

View File

@ -0,0 +1,69 @@
{% extends 'layouts/bootstrap-dashboard.html' %}
{% block title %}
<title>Dashboard</title>
{% endblock %}
{% block content %}
<div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Dashboard</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="downloadClientToken()">
<i class="bi bi-download"></i>
Client Token
</button>
</div>
</div>
</div>
<div class="p-5 mb-4 bg-light rounded-3">
<div class="container-fluid py-5">
<h1 class="display-5 fw-bold">FastAPI-DLS</h1>
<p class="col-md-8 fs-4">Minimal Delegated License Service (DLS).</p>
<a href="https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/releases" class="btn btn-primary btn-lg" target="_blank">
Releases &raquo;
</a>
</div>
</div>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Configuration</h5>
<h6 class="card-subtitle mb-2 text-body-secondary">
Using timezone: {{ CONFIG.TZ }}. Make sure this is correct and match your clients!
</h6>
<p class="card-text">
Your clients renew their license every {{ CONFIG.LEASE_CALCULATED_RENEWAL }}.<br/>
If the renewal fails, the license is {{ CONFIG.LEASE_RENEWAL_DELTA }} valid.<br/>
<br/>
Your client-token file (.tok) is valid for {{ CONFIG.CLIENT_TOKEN_EXPIRE_DELTA }}.
</p>
</div>
</div>
<div class="card">
<div class="card-body">
<pre id="config"></pre>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script type="application/javascript">
function downloadClientToken() {
window.open('/-/client-token', "_blank")
}
function load() {
const config = document.getElementById('config')
fetchConfig(config)
}
load()
</script>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends 'layouts/bootstrap-dashboard.html' %}
{% block title %}
<title>Origins</title>
{% endblock %}
{% block content %}
<div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Leases <small>with origin</small></h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteLease().finally(() => load())">
delete lease
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteExpiredLeases().finally(() => load())">
delete all expired leases
</button>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="load()" title="refresh">
<i class="bi bi-arrow-clockwise"></i>
</button>
<button id="btn-auto-refresh" type="button" class="btn btn-sm active">auto-refresh</button>
</div>
</div>
<div id="leases" class="mt-3"></div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script type="application/javascript">
let autoRefresh = true
function load() {
const leases = document.getElementById('leases')
fetchLeases(leases)
}
load()
setInterval(() => {
if(autoRefresh)
load()
}, 5000);
const btnAutoRefresh = document.getElementById('btn-auto-refresh')
btnAutoRefresh.addEventListener("click", () => {
if(btnAutoRefresh.classList.contains('active')) {
autoRefresh = false
btnAutoRefresh.classList.remove('active')
} else {
autoRefresh = false
btnAutoRefresh.classList.add('active')
}
}, true);
</script>
{% endblock %}

View File

@ -0,0 +1,70 @@
{% extends 'layouts/bootstrap-dashboard.html' %}
{% block title %}
<title>Origins</title>
{% endblock %}
{% block content %}
<div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Origins <small>with leases</small></h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteOrigin().finally(() => load())">
delete origin
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteExpiredOrigins().finally(() => load())">
delete all expired origins
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteOriginsWrapper()">
delete all
</button>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="load()" title="refresh">
<i class="bi bi-arrow-clockwise"></i>
</button>
<button id="btn-auto-refresh" type="button" class="btn btn-sm active">auto-refresh</button>
</div>
</div>
<div id="origins" class="mt-3"></div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script type="application/javascript">
let autoRefresh = true
function load() {
const origins = document.getElementById('origins')
fetchOriginsWithLeases(origins)
}
load()
function deleteOriginsWrapper() {
const response = confirm('Are you sure you want to delete all origins and their leases?');
if (response)
deleteOrigins().finally(() => load())
}
setInterval(() => {
if(autoRefresh)
load()
}, 5000);
const btnAutoRefresh = document.getElementById('btn-auto-refresh')
btnAutoRefresh.addEventListener("click", () => {
if(btnAutoRefresh.classList.contains('active')) {
autoRefresh = false
btnAutoRefresh.classList.remove('active')
} else {
autoRefresh = false
btnAutoRefresh.classList.add('active')
}
}, true);
</script>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends 'layouts/bootstrap-dashboard.html' %}
{% block title %}
<title>Origins</title>
{% endblock %}
{% block content %}
<div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<div class="overflow-hidden">
{{ markdown|safe }}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends 'layouts/bootstrap.html' %}
{% block title %}
<title>Index</title>
{% endblock %}
{% block body_class %}h-100{% endblock %}
{% block body %}
<main class="flex-shrink-0">
<div class="container">
<h1 class="mt-5 text-primary">FastAPI-DLS</h1>
<p class="lead">Minimal Delegated License Service (DLS).</p>
<p>
<a href="/-/dashboard">Dashboard</a>,
<a href="/-/readme">Readme</a>
</p>
</div>
</main>
<footer class="footer mt-auto py-3 bg-light">
<div class="container">
<span class="text-muted">FastAPI-DLS Version {{ VERSION }}</span>
</div>
</footer>
{% endblock %}

View File

@ -1,8 +1,81 @@
import logging import logging
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey, generate_private_key
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
logging.basicConfig() logging.basicConfig()
class PrivateKey:
def __init__(self, data: bytes):
self.__key = load_pem_private_key(data, password=None)
@staticmethod
def from_file(filename: str) -> "PrivateKey":
log = logging.getLogger(__name__)
log.debug(f'Importing RSA-Private-Key from "{filename}"')
with open(filename, 'rb') as f:
data = f.read()
return PrivateKey(data=data.strip())
def raw(self) -> RSAPrivateKey:
return self.__key
def pem(self) -> bytes:
return self.__key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
def public_key(self) -> "PublicKey":
data = self.__key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return PublicKey(data=data)
@staticmethod
def generate(public_exponent: int = 65537, key_size: int = 2048) -> "PrivateKey":
log = logging.getLogger(__name__)
log.debug(f'Generating RSA-Key')
key = generate_private_key(public_exponent=public_exponent, key_size=key_size)
data = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
return PrivateKey(data=data)
class PublicKey:
def __init__(self, data: bytes):
self.__key = load_pem_public_key(data)
@staticmethod
def from_file(filename: str) -> "PublicKey":
log = logging.getLogger(__name__)
log.debug(f'Importing RSA-Public-Key from "{filename}"')
with open(filename, 'rb') as f:
data = f.read()
return PublicKey(data=data.strip())
def raw(self) -> RSAPublicKey:
return self.__key
def pem(self) -> bytes:
return self.__key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
def load_file(filename: str) -> bytes: def load_file(filename: str) -> bytes:
log = logging.getLogger(f'{__name__}') log = logging.getLogger(f'{__name__}')
log.debug(f'Loading contents of file "{filename}') log.debug(f'Loading contents of file "{filename}')
@ -11,33 +84,6 @@ def load_file(filename: str) -> bytes:
return content return content
def load_key(filename: str) -> "RsaKey":
try:
# Crypto | Cryptodome on Debian
from Crypto.PublicKey import RSA
from Crypto.PublicKey.RSA import RsaKey
except ModuleNotFoundError:
from Cryptodome.PublicKey import RSA
from Cryptodome.PublicKey.RSA import RsaKey
log = logging.getLogger(__name__)
log.debug(f'Importing RSA-Key from "{filename}"')
return RSA.import_key(extern_key=load_file(filename), passphrase=None)
def generate_key() -> "RsaKey":
try:
# Crypto | Cryptodome on Debian
from Crypto.PublicKey import RSA
from Crypto.PublicKey.RSA import RsaKey
except ModuleNotFoundError:
from Cryptodome.PublicKey import RSA
from Cryptodome.PublicKey.RSA import RsaKey
log = logging.getLogger(__name__)
log.debug(f'Generating RSA-Key')
return RSA.generate(bits=2048)
class NV: class NV:
__DRIVER_MATRIX_FILENAME = 'static/driver_matrix.json' __DRIVER_MATRIX_FILENAME = 'static/driver_matrix.json'
__DRIVER_MATRIX: None | dict = None # https://docs.nvidia.com/grid/ => "Driver Versions" __DRIVER_MATRIX: None | dict = None # https://docs.nvidia.com/grid/ => "Driver Versions"
@ -46,9 +92,10 @@ class NV:
self.log = logging.getLogger(self.__class__.__name__) self.log = logging.getLogger(self.__class__.__name__)
if NV.__DRIVER_MATRIX is None: if NV.__DRIVER_MATRIX is None:
from os.path import join, dirname
from json import load as json_load from json import load as json_load
try: try:
file = open(NV.__DRIVER_MATRIX_FILENAME) file = open(join(dirname(__file__), NV.__DRIVER_MATRIX_FILENAME))
NV.__DRIVER_MATRIX = json_load(file) NV.__DRIVER_MATRIX = json_load(file)
file.close() file.close()
self.log.debug(f'Successfully loaded "{NV.__DRIVER_MATRIX_FILENAME}".') self.log.debug(f'Successfully loaded "{NV.__DRIVER_MATRIX_FILENAME}".')

View File

@ -1,26 +0,0 @@
# 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

@ -1,186 +0,0 @@
# Reverse Engineering Notes
[[_TOC_]]
# Usefully commands
## Check licensing status
- `nvidia-smi -q | grep "License"`
**Output**
```
vGPU Software Licensed Product
License Status : Licensed (Expiry: 2023-1-14 12:59:52 GMT)
```
## Track licensing progress
- NVIDIA Grid Log: `journalctl -u nvidia-gridd -f`
```
systemd[1]: Started NVIDIA Grid Daemon.
nvidia-gridd[2986]: Configuration parameter ( ServerAddress ) not set
nvidia-gridd[2986]: vGPU Software package (0)
nvidia-gridd[2986]: Ignore service provider and node-locked licensing
nvidia-gridd[2986]: NLS initialized
nvidia-gridd[2986]: Acquiring license. (Info: license.nvidia.space; NVIDIA RTX Virtual Workstation)
nvidia-gridd[2986]: License acquired successfully. (Info: license.nvidia.space, NVIDIA RTX Virtual Workstation; Expiry: 2023-1-29 22:3:0 GMT)
```
# Docker DLS-Container File-System
- More about Docker Images https://git.collinwebdesigns.de/nvidia/nls
## Configuration data
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/...`)
Config-Variables are in `etc/dls/config/service_env.conf`.
## Site Key Uri - `/etc/dls/config/site_key_uri.bin`
```
base64-content...
```
## DB Password - `/etc/dls/config/dls_db_password.bin`
```
# docker cp -a <container-id>:/etc/dls/config/dls_db_password.bin /tmp/dls_db_password.bin
base64-content...
```
**Decrypt database password**
```
cat dls_db_password.bin | base64 -d > dls_db_password.bin.raw
openssl rsautl -decrypt -inkey /tmp/private-key.pem -in dls_db_password.bin.raw
```
# Docker Postgres-Container
- It's enough to manipulate database licenses. There must not be changed any line of code to bypass licensing
validations.
## Inspect
Valid users are `dls_writer` and `postgres`.
```shell
docker exec -it <dls:pgsql> psql -h localhost -U postgres
```
## External Access
Or you can modify `docker-compose.yaml` to forward Postgres port. To create a superuser for external access, use `docker exec` from above and rund the following:
```sql
CREATE USER admin WITH LOGIN SUPERUSER PASSWORD 'admin';
```
# Dive / Docker image inspector
- `dive dls:appliance`
The source code is stored in `/venv/lib/python3.9/site-packages/nls_*`.
Image-Reference:
```
Tags: (unavailable)
Id: d1c7976a5d2b3681ff6c5a30f8187e4015187a83f3f285ba4a37a45458bd6b98
Digest: sha256:311223c5af7a298ec1104f5dc8c3019bfb0e1f77256dc3d995244ffb295a97
1f
Command:
#(nop) ADD file:c1900d3e3a29c29a743a8da86c437006ec5d2aa873fb24e48033b6bf492bb37b in /
```
# Logging / Stack Trace
- https://docs.nvidia.com/license-system/latest/nvidia-license-system-user-guide/index.html#troubleshooting-dls-instance
**Failed licensing log**
```
{
"activity": 100,
"context": {
"SERVICE_INSTANCE_ID": "b43d6e46-d6d0-4943-8b8d-c66a5f6e0d38",
"SERVICE_INSTANCE_NAME": "DEFAULT_2022-12-14_12:48:30",
"description": "borrow failed: NotFoundError(no pool features found for: NVIDIA RTX Virtual Workstation)",
"event_type": null,
"function_name": "_evt",
"lineno": 54,
"module_name": "nls_dal_lease_dls.event",
"operation_id": "e72a8ca7-34cc-4e11-b80c-273592085a24",
"origin_ref": "3f7f5a50-a26b-425b-8d5e-157f63e72b1c",
"service_name": "nls_services_lease"
},
"detail": {
"oc": {
"license_allotment_xid": "10c4317f-7c4c-11ed-a524-0e4252a7e5f1",
"origin_ref": "3f7f5a50-a26b-425b-8d5e-157f63e72b1c",
"service_instance_xid": "b43d6e46-d6d0-4943-8b8d-c66a5f6e0d38"
},
"operation_id": "e72a8ca7-34cc-4e11-b80c-273592085a24"
},
"id": "0cc9e092-3b92-4652-8d9e-7622ef85dc79",
"metadata": {},
"ts": "2022-12-15T10:25:36.827661Z"
}
{
"activity": 400,
"context": {
"SERVICE_INSTANCE_ID": "b43d6e46-d6d0-4943-8b8d-c66a5f6e0d38",
"SERVICE_INSTANCE_NAME": "DEFAULT_2022-12-14_12:48:30",
"description": "lease_multi_create failed: no pool features found for: NVIDIA RTX Virtual Workstation",
"event_by": "system",
"function_name": "lease_multi_create",
"level": "warning",
"lineno": 157,
"module_name": "nls_services_lease.controllers.lease_multi_controller",
"operation_id": "e72a8ca7-34cc-4e11-b80c-273592085a24",
"service_name": "nls_services_lease"
},
"detail": {
"_msg": "lease_multi_create failed: no pool features found for: NVIDIA RTX Virtual Workstation",
"exec_info": ["NotFoundError", "NotFoundError(no pool features found for: NVIDIA RTX Virtual Workstation)", " File \"/venv/lib/python3.9/site-packages/nls_services_lease/controllers/lease_multi_controller.py\", line 127, in lease_multi_create\n data = _leaseMulti.lease_multi_create(event_args)\n File \"/venv/lib/python3.9/site-packages/nls_core_lease/lease_multi.py\", line 208, in lease_multi_create\n raise e\n File \"/venv/lib/python3.9/site-packages/nls_core_lease/lease_multi.py\", line 184, in lease_multi_create\n self._try_proposals(oc, mlr, results, detail)\n File \"/venv/lib/python3.9/site-packages/nls_core_lease/lease_multi.py\", line 219, in _try_proposals\n lease = self._leases.create(creator)\n File \"/venv/lib/python3.9/site-packages/nls_dal_lease_dls/leases.py\", line 230, in create\n features = self._get_features(creator)\n File \"/venv/lib/python3.9/site-packages/nls_dal_lease_dls/leases.py\", line 148, in _get_features\n self._explain_not_available(cur, creator)\n File \"/venv/lib/python3.9/site-packages/nls_dal_lease_dls/leases.py\", line 299, in _explain_not_available\n raise NotFoundError(f'no pool features found for: {lcc.product_name}')\n"],
"operation_id": "e72a8ca7-34cc-4e11-b80c-273592085a24"
},
"id": "282801b9-d612-40a5-9145-b56d8e420dac",
"metadata": {},
"ts": "2022-12-15T10:25:36.831673Z"
}
```
**Stack Trace**
```
"NotFoundError", "NotFoundError(no pool features found for: NVIDIA RTX Virtual Workstation)", " File \"/venv/lib/python3.9/site-packages/nls_services_lease/controllers/lease_multi_controller.py\", line 127, in lease_multi_create
data = _leaseMulti.lease_multi_create(event_args)
File \"/venv/lib/python3.9/site-packages/nls_core_lease/lease_multi.py\", line 208, in lease_multi_create
raise e
File \"/venv/lib/python3.9/site-packages/nls_core_lease/lease_multi.py\", line 184, in lease_multi_create
self._try_proposals(oc, mlr, results, detail)
File \"/venv/lib/python3.9/site-packages/nls_core_lease/lease_multi.py\", line 219, in _try_proposals
lease = self._leases.create(creator)
File \"/venv/lib/python3.9/site-packages/nls_dal_lease_dls/leases.py\", line 230, in create
features = self._get_features(creator)
File \"/venv/lib/python3.9/site-packages/nls_dal_lease_dls/leases.py\", line 148, in _get_features
self._explain_not_available(cur, creator)
File \"/venv/lib/python3.9/site-packages/nls_dal_lease_dls/leases.py\", line 299, in _explain_not_available
raise NotFoundError(f'no pool features found for: {lcc.product_name}')
"
```
# Nginx
- NGINX uses `/opt/certs/cert.pem` and `/opt/certs/key.pem`

View File

@ -1,8 +1,9 @@
fastapi==0.115.8 fastapi==0.115.12
uvicorn[standard]==0.34.0 uvicorn[standard]==0.34.0
python-jose==3.4.0 python-jose[cryptography]==3.4.0
pycryptodome==3.21.0 cryptography==44.0.2
python-dateutil==2.8.2 python-dateutil==2.9.0
sqlalchemy==2.0.38 sqlalchemy==2.0.40
markdown==3.7 markdown==3.7
python-dotenv==1.0.1 python-dotenv==1.1.0
jinja2==3.1.3

View File

@ -16,7 +16,7 @@ sys.path.append('../')
sys.path.append('../app') sys.path.append('../app')
from app import main from app import main
from app.util import load_key from util import PrivateKey, PublicKey
client = TestClient(main.app) client = TestClient(main.app)
@ -25,11 +25,11 @@ ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-00000
# INSTANCE_KEY_RSA = generate_key() # INSTANCE_KEY_RSA = generate_key()
# INSTANCE_KEY_PUB = INSTANCE_KEY_RSA.public_key() # INSTANCE_KEY_PUB = INSTANCE_KEY_RSA.public_key()
INSTANCE_KEY_RSA = load_key(str(join(dirname(__file__), '../app/cert/instance.private.pem'))) INSTANCE_KEY_RSA = PrivateKey.from_file(str(join(dirname(__file__), '../app/cert/instance.private.pem')))
INSTANCE_KEY_PUB = load_key(str(join(dirname(__file__), '../app/cert/instance.public.pem'))) INSTANCE_KEY_PUB = PublicKey.from_file(str(join(dirname(__file__), '../app/cert/instance.public.pem')))
jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256) jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.pem(), algorithm=ALGORITHMS.RS256)
jwt_decode_key = jwk.construct(INSTANCE_KEY_PUB.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256) jwt_decode_key = jwk.construct(INSTANCE_KEY_PUB.pem(), algorithm=ALGORITHMS.RS256)
def __bearer_token(origin_ref: str) -> str: def __bearer_token(origin_ref: str) -> str:
@ -59,8 +59,8 @@ def test_readme():
assert response.status_code == 200 assert response.status_code == 200
def test_manage(): def test_dashboard():
response = client.get('/-/manage') response = client.get('/-/dashboard')
assert response.status_code == 200 assert response.status_code == 200
@ -187,8 +187,6 @@ def test_leasing_v1_lessor():
assert len(lease_result_list[0]['lease']['ref']) == 36 assert len(lease_result_list[0]['lease']['ref']) == 36
assert str(UUID(lease_result_list[0]['lease']['ref'])) == lease_result_list[0]['lease']['ref'] 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(): def test_leasing_v1_lessor_lease():
response = client.get('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)}) response = client.get('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)})
@ -231,7 +229,23 @@ def test_leasing_v1_lease_delete():
def test_leasing_v1_lessor_lease_remove(): def test_leasing_v1_lessor_lease_remove():
lease_ref = test_leasing_v1_lessor() # see "test_leasing_v1_lessor()"
payload = {
'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': [ALLOTMENT_REF]
}
response = client.post('/leasing/v1/lessor', json=payload, headers={'authorization': __bearer_token(ORIGIN_REF)})
lease_result_list = response.json().get('lease_result_list')
lease_ref = lease_result_list[0]['lease']['ref']
#
response = client.delete('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)}) response = client.delete('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)})
assert response.status_code == 200 assert response.status_code == 200