39 Commits

Author SHA1 Message Date
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
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
44 changed files with 1105 additions and 1472 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-jose, python3-sqlalchemy, python3-cryptography, 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

@@ -21,3 +21,7 @@ DATABASE=sqlite:////etc/fastapi-dls/db.sqlite
#SITE_KEY_XID="00000000-0000-0000-0000-000000000000" #SITE_KEY_XID="00000000-0000-0000-0000-000000000000"
#INSTANCE_REF="10000000-0000-0000-0000-000000000001" #INSTANCE_REF="10000000-0000-0000-0000-000000000001"
#ALLOTMENT_REF="20000000-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

@@ -3,6 +3,14 @@
WORKING_DIR=/usr/share/fastapi-dls WORKING_DIR=/usr/share/fastapi-dls
CONFIG_DIR=/etc/fastapi-dls CONFIG_DIR=/etc/fastapi-dls
if [ ! -f $CONFIG_DIR/instance.private.pem ]; then
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
while true; do while true; do
[ -f $CONFIG_DIR/webserver.key ] && default_answer="N" || default_answer="Y" [ -f $CONFIG_DIR/webserver.key ] && default_answer="N" || default_answer="Y"
[ $default_answer == "Y" ] && V="Y/n" || V="y/N" [ $default_answer == "Y" ] && V="Y/n" || V="y/N"

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-cryptography' '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')
@@ -17,7 +17,7 @@ source=("git+file://${CI_PROJECT_DIR}"
"$pkgname.service" "$pkgname.service"
"$pkgname.tmpfiles") "$pkgname.tmpfiles")
sha256sums=('SKIP' sha256sums=('SKIP'
'a4776a0ae4671751065bf3e98aa707030b8b5ffe42dde942c51050dab5028c54' 'fbd015449a30c0ae82733289a56eb98151dcfab66c91b37fe8e202e39f7a5edb'
'2719338541104c537453a65261c012dda58e1dbee99154cf4f33b526ee6ca22e' '2719338541104c537453a65261c012dda58e1dbee99154cf4f33b526ee6ca22e'
'3dc60140c08122a8ec0e7fa7f0937eb8c1288058890ba09478420fc30ce9e30c') '3dc60140c08122a8ec0e7fa7f0937eb8c1288058890ba09478420fc30ce9e30c')
@@ -30,22 +30,59 @@ pkgver() {
check() { check() {
cd "$srcdir/$pkgname/test" cd "$srcdir/$pkgname/test"
mkdir "$srcdir/$pkgname/app/cert" mkdir "$srcdir/$pkgname/app/cert"
openssl genrsa -out "$srcdir/$pkgname/app/cert/instance.private.pem" 2048
openssl rsa -in "$srcdir/$pkgname/app/cert/instance.private.pem" -outform PEM -pubout -out "$srcdir/$pkgname/app/cert/instance.public.pem"
python "$srcdir/$pkgname/test/main.py" python "$srcdir/$pkgname/test/main.py"
rm -rf "$srcdir/$pkgname/app/cert" rm -rf "$srcdir/$pkgname/app/cert"
} }
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"
# copy docs & static files
#cp -r "$srcdir/$pkgname/doc"/* "$pkgdir/usr/share/doc/$pkgname/" #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"

View File

@@ -19,6 +19,10 @@ DATABASE="sqlite:////var/lib/fastapi-dls/db.sqlite"
SITE_KEY_XID="<<sitekey>>" SITE_KEY_XID="<<sitekey>>"
INSTANCE_REF="<<instanceref>>" INSTANCE_REF="<<instanceref>>"
# Site-wide signing keys
INSTANCE_KEY_RSA="/var/lib/fastapi-dls/instance.private.pem"
INSTANCE_KEY_PUB="/var/lib/fastapi-dls/instance.public.pem"
# TLS certificate # TLS certificate
INSTANCE_SSL_CERT="/var/lib/fastapi-dls/cert/webserver.crt" INSTANCE_SSL_CERT="/var/lib/fastapi-dls/cert/webserver.crt"
INSTANCE_SSL_KEY="/var/lib/fastapi-dls/cert/webserver.key" INSTANCE_SSL_KEY="/var/lib/fastapi-dls/cert/webserver.key"

View File

@@ -7,4 +7,8 @@ post_install() {
echo echo
echo 'A valid HTTPS certificate needs to be installed to /var/lib/fastapi-dls/cert/webserver.{crt,key}' echo 'A valid HTTPS certificate needs to be installed to /var/lib/fastapi-dls/cert/webserver.{crt,key}'
echo 'A self-signed certificate can be generated with: openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /var/lib/fastapi-dls/cert/webserver.key -out /var/lib/fastapi-dls/cert/webserver.crt' echo 'A self-signed certificate can be generated with: openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /var/lib/fastapi-dls/cert/webserver.key -out /var/lib/fastapi-dls/cert/webserver.crt'
echo
echo 'The signing keys for your instance need to be generated as well. Generate them with these commands:'
echo 'openssl genrsa -out /var/lib/fastapi-dls/instance.private.pem 2048'
echo 'openssl rsa -in /var/lib/fastapi-dls/instance.private.pem -outform PEM -pubout -out /var/lib/fastapi-dls/instance.public.pem'
} }

View File

@@ -18,6 +18,9 @@ Make sure you create these certificates before starting the container for the fi
WORKING_DIR=/mnt/user/appdata/fastapi-dls/cert&#xD; WORKING_DIR=/mnt/user/appdata/fastapi-dls/cert&#xD;
mkdir -p $WORKING_DIR&#xD; mkdir -p $WORKING_DIR&#xD;
cd $WORKING_DIR&#xD; cd $WORKING_DIR&#xD;
# create instance private and public key for singing JWT's&#xD;
openssl genrsa -out $WORKING_DIR/instance.private.pem 2048 &#xD;
openssl rsa -in $WORKING_DIR/instance.private.pem -outform PEM -pubout -out $WORKING_DIR/instance.public.pem&#xD;
# create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl&#xD; # create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl&#xD;
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt&#xD; openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt&#xD;
```&#xD; ```&#xD;

View File

@@ -16,12 +16,12 @@ build:docker:
interruptible: true interruptible: true
stage: build stage: build
rules: rules:
# deployment is in "deploy:docker:" - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
changes: changes:
- app/**/* - app/**/*
- Dockerfile - Dockerfile
- requirements.txt - requirements.txt
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
tags: [ docker ] tags: [ docker ]
before_script: before_script:
- docker buildx inspect - docker buildx inspect
@@ -44,13 +44,16 @@ build:apt:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
variables: variables:
VERSION: $CI_COMMIT_REF_NAME VERSION: $CI_COMMIT_REF_NAME
- if: ($CI_PIPELINE_SOURCE == 'merge_request_event') || ($CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH) - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
changes: changes:
- app/**/* - app/**/*
- .DEBIAN/**/* - .DEBIAN/**/*
- .gitlab-ci.yml - .gitlab-ci.yml
variables: variables:
VERSION: "0.0.1" VERSION: "0.0.1"
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
variables:
VERSION: "0.0.1"
before_script: before_script:
- echo -e "VERSION=$VERSION\nCOMMIT=$CI_COMMIT_SHA" > version.env - echo -e "VERSION=$VERSION\nCOMMIT=$CI_COMMIT_SHA" > version.env
# install build dependencies # install build dependencies
@@ -91,13 +94,16 @@ build:pacman:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
variables: variables:
VERSION: $CI_COMMIT_REF_NAME VERSION: $CI_COMMIT_REF_NAME
- if: ($CI_PIPELINE_SOURCE == 'merge_request_event') || ($CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH) - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
changes: changes:
- app/**/* - app/**/*
- .PKGBUILD/**/* - .PKGBUILD/**/*
- .gitlab-ci.yml - .gitlab-ci.yml
variables: variables:
VERSION: "0.0.1" VERSION: "0.0.1"
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
variables:
VERSION: "0.0.1"
before_script: before_script:
#- echo -e "VERSION=$VERSION\nCOMMIT=$CI_COMMIT_SHA" > version.env #- echo -e "VERSION=$VERSION\nCOMMIT=$CI_COMMIT_SHA" > version.env
# install build dependencies # install build dependencies
@@ -120,12 +126,13 @@ build:pacman:
paths: paths:
- "*.pkg.tar.zst" - "*.pkg.tar.zst"
test:python: test:
image: $IMAGE image: python:3.12-slim-bookworm
stage: test stage: test
interruptible: true interruptible: true
rules: rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
- if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
changes: changes:
@@ -135,49 +142,37 @@ test:python:
DATABASE: sqlite:///../app/db.sqlite DATABASE: sqlite:///../app/db.sqlite
parallel: parallel:
matrix: matrix:
- IMAGE: - REQUIREMENTS:
# https://devguide.python.org/versions/#supported-versions - 'requirements.txt'
# - python:3.14-rc-alpine # EOL 2030-10 => uvicorn does not support 3.14 yet # - '.DEBIAN/requirements-bookworm-12.txt'
- python:3.13-alpine # EOL 2029-10 # - '.DEBIAN/requirements-ubuntu-24.04.txt'
- python:3.12-alpine # EOL 2028-10 # - '.DEBIAN/requirements-ubuntu-24.10.txt'
- python:3.11-alpine # EOL 2027-10
# - python:3.10-alpine # EOL 2026-10 => ImportError: cannot import name 'UTC' from 'datetime'
# - python:3.9-alpine # EOL 2025-10 => ImportError: cannot import name 'UTC' from 'datetime'
before_script: before_script:
- apk --no-cache add openssl - apt-get update && apt-get install -y python3-dev python3-pip python3-venv gcc
- python3 -m venv venv - python3 -m venv venv
- source venv/bin/activate - source venv/bin/activate
- pip install --upgrade pip - pip install --upgrade pip
- pip install -r requirements.txt - pip install -r $REQUIREMENTS
- pip install pytest pytest-cov pytest-custom_exit_code 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 rsa -in app/cert/instance.private.pem -outform PEM -pubout -out app/cert/instance.public.pem
- cd test - cd test
script: script:
- python -m pytest main.py --junitxml=report.xml - python -m pytest main.py --junitxml=report.xml
artifacts: artifacts:
reports: reports:
dotenv: version.env
junit: ['**/report.xml'] junit: ['**/report.xml']
test:apt: .test:apt:
image: $IMAGE
stage: test stage: test
rules: rules:
- if: ($CI_PIPELINE_SOURCE == 'merge_request_event') || ($CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH) - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
changes: changes:
- app/**/* - app/**/*
- .DEBIAN/**/* - .DEBIAN/**/*
- .gitlab-ci.yml - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
variables:
VERSION: "0.0.1"
parallel:
matrix:
- IMAGE:
# - debian:trixie-slim # EOL: t.b.a.; "python3-jose" not available, but "python3-josepy"
- debian:bookworm-slim # EOL: June 06, 2026
- debian:bookworm-slim # EOL: June 06, 2026
- ubuntu:24.04 # EOL: April 2036
# - ubuntu:24.10 # EOL: t.b.a.; "python3-jose" not available, but "python3-josepy"
# - ubuntu:25.04 # EOL: t.b.a.; "python3-jose" not available, but "python3-josepy"
needs: needs:
- job: build:apt - job: build:apt
artifacts: true artifacts: true
@@ -209,14 +204,24 @@ test:apt:
- apt-get purge -qq -y fastapi-dls - apt-get purge -qq -y fastapi-dls
- apt-get autoremove -qq -y && apt-get clean -qq - apt-get autoremove -qq -y && apt-get clean -qq
test:apt:
extends: .test:apt
image: $IMAGE
parallel:
matrix:
- IMAGE:
- debian:bookworm-slim # EOL: June 06, 2026
- ubuntu:24.04 # EOL: April 2036
- ubuntu:24.10
test:pacman:archlinux: test:pacman:archlinux:
image: archlinux:base image: archlinux:base
rules: rules:
- if: ($CI_PIPELINE_SOURCE == 'merge_request_event') || ($CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH) - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
changes: changes:
- app/**/* - app/**/*
- .PKGBUILD/**/* - .PKGBUILD/**/*
- .gitlab-ci.yml - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
needs: needs:
- job: build:pacman - job: build:pacman
artifacts: true artifacts: true
@@ -262,6 +267,8 @@ test_coverage:
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install pytest pytest-cov pytest-custom_exit_code 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 rsa -in app/cert/instance.private.pem -outform PEM -pubout -out app/cert/instance.public.pem
- cd test - cd test
script: script:
- coverage run -m pytest main.py --junitxml=report.xml --suppress-no-test-exit-code - coverage run -m pytest main.py --junitxml=report.xml --suppress-no-test-exit-code
@@ -289,12 +296,15 @@ gemnasium-python-dependency_scanning:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
.deploy:
rules:
- if: $CI_COMMIT_TAG
deploy:docker: deploy:docker:
extends: .deploy
image: docker:dind image: docker:dind
stage: deploy stage: deploy
tags: [ docker ] tags: [ docker ]
rules:
- if: $CI_COMMIT_TAG
before_script: before_script:
- echo "Building docker image for commit $CI_COMMIT_SHA with version $CI_COMMIT_REF_NAME" - echo "Building docker image for commit $CI_COMMIT_SHA with version $CI_COMMIT_REF_NAME"
- docker buildx inspect - docker buildx inspect
@@ -313,10 +323,9 @@ deploy:docker:
deploy:apt: deploy:apt:
# doc: https://git.collinwebdesigns.de/help/user/packages/debian_repository/index.md#install-a-package # doc: https://git.collinwebdesigns.de/help/user/packages/debian_repository/index.md#install-a-package
extends: .deploy
image: debian:bookworm-slim image: debian:bookworm-slim
stage: deploy stage: deploy
rules:
- if: $CI_COMMIT_TAG
needs: needs:
- job: build:apt - job: build:apt
artifacts: true artifacts: true
@@ -353,10 +362,9 @@ deploy:apt:
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file ${EXPORT_NAME} "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${PACKAGE_NAME}/${PACKAGE_VERSION}/${EXPORT_NAME}"' - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file ${EXPORT_NAME} "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${PACKAGE_NAME}/${PACKAGE_VERSION}/${EXPORT_NAME}"'
deploy:pacman: deploy:pacman:
extends: .deploy
image: archlinux:base-devel image: archlinux:base-devel
stage: deploy stage: deploy
rules:
- if: $CI_COMMIT_TAG
needs: needs:
- job: build:pacman - job: build:pacman
artifacts: true artifacts: true
@@ -377,7 +385,7 @@ deploy:pacman:
release: release:
image: registry.gitlab.com/gitlab-org/release-cli:latest image: registry.gitlab.com/gitlab-org/release-cli:latest
stage: .post stage: .post
needs: [ deploy:docker, deploy:apt, deploy:pacman ] needs: [ test ]
rules: rules:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
script: script:
@@ -392,4 +400,4 @@ release:
- name: 'Package Registry' - name: 'Package Registry'
url: 'https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/packages' url: 'https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/packages'
- name: 'Container Registry' - name: 'Container Registry'
url: 'https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/container_registry/70' url: 'https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/container_registry/40'

116
README.md
View File

@@ -2,16 +2,13 @@
Minimal Delegated License Service (DLS). Minimal Delegated License Service (DLS).
> [!warning] Branch support > [!note]
> FastAPI-DLS Version 1.x supports up to **`17.x`** releases. \
> FastAPI-DLS Version 2.x is backwards compatible to `17.x` and supports **`18.x`** releases in combination
> with [gridd-unlock-patcher](https://git.collinwebdesigns.de/oscar.krause/gridd-unlock-patcher).
> Other combinations of FastAPI-DLS and Driver-Branches may work but are not tested.
> [!note] Compatibility
> 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).
> [!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.
@@ -69,6 +66,9 @@ The images include database drivers for `postgres`, `mariadb` and `sqlite`.
WORKING_DIR=/opt/docker/fastapi-dls/cert WORKING_DIR=/opt/docker/fastapi-dls/cert
mkdir -p $WORKING_DIR mkdir -p $WORKING_DIR
cd $WORKING_DIR cd $WORKING_DIR
# create instance private and public key for singing JWT's
openssl genrsa -out $WORKING_DIR/instance.private.pem 2048
openssl rsa -in $WORKING_DIR/instance.private.pem -outform PEM -pubout -out $WORKING_DIR/instance.public.pem
# create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl # create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt
``` ```
@@ -153,6 +153,9 @@ chown -R www-data:www-data $WORKING_DIR
WORKING_DIR=/opt/fastapi-dls/app/cert WORKING_DIR=/opt/fastapi-dls/app/cert
mkdir -p $WORKING_DIR mkdir -p $WORKING_DIR
cd $WORKING_DIR cd $WORKING_DIR
# create instance private and public key for singing JWT's
openssl genrsa -out $WORKING_DIR/instance.private.pem 2048
openssl rsa -in $WORKING_DIR/instance.private.pem -outform PEM -pubout -out $WORKING_DIR/instance.public.pem
# create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl # create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt
chown -R www-data:www-data $WORKING_DIR chown -R www-data:www-data $WORKING_DIR
@@ -252,6 +255,9 @@ CERT_DIR=${BASE_DIR}/app/cert
SERVICE_USER=dls SERVICE_USER=dls
mkdir ${CERT_DIR} mkdir ${CERT_DIR}
cd ${CERT_DIR} cd ${CERT_DIR}
# create instance private and public key for singing JWT's
openssl genrsa -out ${CERT_DIR}/instance.private.pem 2048
openssl rsa -in ${CERT_DIR}/instance.private.pem -outform PEM -pubout -out ${CERT_DIR}/instance.public.pem
# create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl # create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout ${CERT_DIR}/webserver.key -out ${CERT_DIR}/webserver.crt openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout ${CERT_DIR}/webserver.key -out ${CERT_DIR}/webserver.crt
chown -R ${SERVICE_USER} ${CERT_DIR} chown -R ${SERVICE_USER} ${CERT_DIR}
@@ -334,13 +340,12 @@ Successful tested with (**LTS Version**):
- *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: Apr 2029) - **Ubuntu 24.04 (Noble Numbat)** (EOL: Apr 2029)
- *Ubuntu 24.10 (Oracular Oriole)* (EOL: Jul 2025)
Not working with: Not working with:
- Debian 11 (Bullseye) and lower (missing `python-jose` dependency) - Debian 11 (Bullseye) and lower (missing `python-jose` dependency)
- Debian 13 (Trixie) (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)) - 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))
- Ubuntu 24.10 (Oracular Oriole) (missing `python-jose` dependency)
**Run this on your server instance** **Run this on your server instance**
@@ -397,9 +402,6 @@ Continue [here](#unraid-guest) for docker guest setup.
Tanks to [@mrzenc](https://github.com/mrzenc) for [fastapi-dls-nixos](https://github.com/mrzenc/fastapi-dls-nixos). Tanks to [@mrzenc](https://github.com/mrzenc) for [fastapi-dls-nixos](https://github.com/mrzenc/fastapi-dls-nixos).
> [!note] Native NixOS-Package
> There is a [pull request](https://github.com/NixOS/nixpkgs/pull/358647) which adds fastapi-dls into nixpkgs.
## Let's Encrypt Certificate (optional) ## Let's Encrypt Certificate (optional)
If you're using installation via docker, you can use `traefik`. Please refer to their documentation. If you're using installation via docker, you can use `traefik`. Please refer to their documentation.
@@ -418,20 +420,21 @@ After first success you have to replace `--issue` with `--renew`.
# Configuration # Configuration
| Variable | Default | Usage | | Variable | Default | Usage |
|------------------------|----------------------------------------|------------------------------------------------------------------------------------------------------| |--------------------------|----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| `DEBUG` | `false` | Toggles `fastapi` debug mode | | `DEBUG` | `false` | Toggles `fastapi` debug mode |
| `DLS_URL` | `localhost` | Used in client-token to tell guest driver where dls instance is reachable | | `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 | | `DLS_PORT` | `443` | Used in client-token to tell guest driver where dls instance is reachable |
| `CERT_PATH` | `None` | Path to a Directory where generated Certificates are stored. Defaults to `/<app-dir>/cert`. | | `TOKEN_EXPIRE_DAYS` | `1` | Client auth-token validity (used for authenticate client against api, **not `.tok` file!**) |
| `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_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 |
| `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) |
| `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 |
| `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 |
| `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid | | `INSTANCE_REF` | `10000000-0000-0000-0000-000000000001` | Instance identification uuid |
| `INSTANCE_REF` | `10000000-0000-0000-0000-000000000001` | Instance identification uuid | | `ALLOTMENT_REF` | `20000000-0000-0000-0000-000000000001` | Allotment 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 |
\*1 For example, if the lease period is one day and the renewal period is 20%, the client attempts to renew its license \*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 every 4.8 hours. If network connectivity is lost, the loss of connectivity is detected during license renewal and the
@@ -439,6 +442,8 @@ client has 19.2 hours in which to re-establish connectivity before its license e
\*2 Always use `https`, since guest-drivers only support secure connections! \*2 Always use `https`, since guest-drivers only support secure connections!
\*3 If you recreate your instance keys you need to **recreate client-token for each guest**!
# Setup (Client) # Setup (Client)
**The token file has to be copied! It's not enough to C&P file contents, because there can be special characters.** **The token file has to be copied! It's not enough to C&P file contents, because there can be special characters.**
@@ -537,10 +542,6 @@ Status endpoint, used for *healthcheck*.
Shows current runtime environment variables and their values. Shows current runtime environment variables and their values.
**`GET /-/config/root-certificate`**
Returns the Root-Certificate Certificate which is used. This is required for patching `nvidia-gridd` on 18.x releases.
**`GET /-/readme`** **`GET /-/readme`**
HTML rendered README.md. HTML rendered README.md.
@@ -613,7 +614,7 @@ Please download a new client-token. The guest have to register within an hour af
### `jose.exceptions.JWTError: Signature verification failed.` ### `jose.exceptions.JWTError: Signature verification failed.`
- Did you recreate any certificate or keypair? - Did you recreate `instance.public.pem` / `instance.private.pem`?
Then you have to download a **new** client-token on each of your guests. Then you have to download a **new** client-token on each of your guests.
@@ -749,25 +750,33 @@ The error message can safely be ignored (since we have no license limitation :P)
# vGPU Software Compatibility Matrix # vGPU Software Compatibility Matrix
**18.x Drivers are not supported on FastAPI-DLS Versions < 1.6.0**
<details> <details>
<summary>Show Table</summary> <summary>Show Table</summary>
Successfully tested with this package versions. Successfully tested with this package versions.
| FastAPI-DLS Version | vGPU Suftware | Driver Branch | Linux vGPU Manager | Linux Driver | Windows Driver | Release Date | EOL Date | | vGPU Suftware | Driver Branch | Linux vGPU Manager | Linux Driver | Windows Driver | Release Date | EOL Date |
|---------------------|:-------------:|:-------------:|--------------------|--------------|----------------|--------------:|--------------:| |:-------------:|:-------------:|--------------------|--------------|----------------|--------------:|--------------:|
| `2.x` | `18.1` | **R570** | `570.133.08` | `570.133.07` | `572.83` | April 2025 | March 2026 | | `17.5` | R550 | `550.144.02` | `550.144.03` | `553.62` | January 2025 | June 2025 |
| | `18.0` | **R570** | `570.124.03` | `570.124.06` | `572.60` | March 2025 | March 2026 | | `17.4` | R550 | `550.127.06` | `550.127.05` | `553.24` | October 2024 | |
| `1.x` & `2.x` | `17.6` | **R550** | `550.163.02` | `550.63.01` | `553.74` | April 2025 | June 2025 | | `17.3` | R550 | `550.90.05` | `550.90.07` | `552.74` | July 2024 | |
| | `17.5` | | `550.144.02` | `550.144.03` | `553.62` | January 2025 | | | `17.2` | R550 | `550.90.05` | `550.90.07` | `552.55` | June 2024 | |
| | `17.4` | | `550.127.06` | `550.127.05` | `553.24` | October 2024 | | | `17.1` | R550 | `550.54.16` | `550.54.15` | `551.78` | March 2024 | |
| | `17.3` | | `550.90.05` | `550.90.07` | `552.74` | July 2024 | | | `17.0` | R550 | `550.54.10` | `550.54.14` | `551.61` | February 2024 | |
| | `17.2` | | `550.90.05` | `550.90.07` | `552.55` | June 2024 | | | `16.9` | R535 | `535.230.02` | `535.216.01` | `539.19` | October 2024 | July 2026 |
| | `17.1` | | `550.54.16` | `550.54.15` | `551.78` | March 2024 | | | `16.8` | R535 | `535.216.01` | `535.216.01` | `538.95` | October 2024 | |
| | `17.0` | **R550** | `550.54.10` | `550.54.14` | `551.61` | February 2024 | | | `16.7` | R535 | `535.183.04` | `535.183.06` | `538.78` | July 2024 | |
| `1.x` | `16.10` | **R535** | `535.247.02` | `535.247.01` | `539.28` | April 2025 | July 2026 | | `16.6` | R535 | `535.183.04` | `535.183.01` | `538.67` | June 2024 | |
| `1.x` | `15.4` | **R525** | `525.147.01` | `525.147.05` | `529.19` | June 2023 | December 2023 | | `16.5` | R535 | `535.161.05` | `535.161.08` | `538.46` | February 2024 | |
| `1.x` | `14.4` | **R510** | `510.108.03` | `510.108.03` | `514.08` | December 2022 | February 2023 | | `16.4` | R535 | `535.161.05` | `535.161.07` | `538.33` | February 2024 | |
| `16.3` | R535 | `535.154.02` | `535.154.05` | `538.15` | January 2024 | |
| `16.2` | R535 | `535.129.03` | `535.129.03` | `537.70` | October 2023 | |
| `16.1` | R535 | `535.104.06` | `535.104.05` | `537.13` | August 2023 | |
| `16.0` | R535 | `535.54.06` | `535.54.03` | `536.22` | July 2023 | |
| `15.4` | R525 | `525.147.01` | `525.147.05` | `529.19` | June 2023 | December 2023 |
| `14.4` | R510 | `510.108.03` | `510.108.03` | `514.08` | December 2022 | February 2023 |
</details> </details>
@@ -783,14 +792,13 @@ Thanks to vGPU community and all who uses this project and report bugs.
Special thanks to: Special thanks to:
- `samicrusader` who created build file for **ArchLinux** - @samicrusader who created build file for **ArchLinux**
- `cyrus` who wrote the section for **openSUSE** - @cyrus who wrote the section for **openSUSE**
- `midi` who wrote the section for **unRAID** - @midi who wrote the section for **unRAID**
- `polloloco` who wrote the *[NVIDIA vGPU Guide](https://gitlab.com/polloloco/vgpu-proxmox)* - @polloloco who wrote the *[NVIDIA vGPU Guide](https://gitlab.com/polloloco/vgpu-proxmox)*
- `DualCoder` who creates the `vgpu_unlock` functionality [vgpu_unlock](https://github.com/DualCoder/vgpu_unlock) - @DualCoder who creates the `vgpu_unlock` functionality [vgpu_unlock](https://github.com/DualCoder/vgpu_unlock)
- `Krutav Shah` who wrote the [vGPU_Unlock Wiki](https://docs.google.com/document/d/1pzrWJ9h-zANCtyqRgS7Vzla0Y8Ea2-5z2HEi4X75d2Q/) - Krutav Shah who wrote the [vGPU_Unlock Wiki](https://docs.google.com/document/d/1pzrWJ9h-zANCtyqRgS7Vzla0Y8Ea2-5z2HEi4X75d2Q/)
- `Wim van 't Hoog` for the [Proxmox All-In-One Installer Script](https://wvthoog.nl/proxmox-vgpu-v3/) - Wim van 't Hoog for the [Proxmox All-In-One Installer Script](https://wvthoog.nl/proxmox-vgpu-v3/)
- `mrzenc` who wrote [fastapi-dls-nixos](https://github.com/mrzenc/fastapi-dls-nixos) - @mrzenc who wrote [fastapi-dls-nixos](https://github.com/mrzenc/fastapi-dls-nixos)
- `electricsheep49` who wrote [gridd-unlock-patcher](https://git.collinwebdesigns.de/oscar.krause/gridd-unlock-patcher)
And thanks to all people who contributed to all these libraries! And thanks to all people who contributed to all these libraries!

View File

@@ -4,25 +4,26 @@ from calendar import timegm
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime, timedelta, UTC from datetime import datetime, timedelta, UTC
from hashlib import sha256 from hashlib import sha256
from json import loads as json_loads, dumps as json_dumps from json import loads as json_loads
from os import getenv as env from os import getenv as env
from os.path import join, dirname from os.path import join, dirname
from textwrap import wrap
from uuid import uuid4 from uuid import uuid4
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from dotenv import load_dotenv from dotenv import load_dotenv
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.requests import Request from fastapi.requests import Request
from fastapi.responses import Response, RedirectResponse, StreamingResponse
from jose import jws, jwk, jwt, JWTError from jose import jws, jwk, jwt, JWTError
from jose.constants import ALGORITHMS 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, 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 CASetup, PrivateKey, Cert, ProductMapping, load_file from util import PrivateKey, PublicKey, load_file
# Load variables # Load variables
load_dotenv('../version.env') load_dotenv('../version.env')
@@ -40,31 +41,20 @@ db_init(db), migrate(db)
# Load DLS variables (all prefixed with "INSTANCE_*" is used as "SERVICE_INSTANCE_*" or "SI_*" in official dls service) # Load DLS variables (all prefixed with "INSTANCE_*" is used as "SERVICE_INSTANCE_*" or "SI_*" in official dls service)
DLS_URL = str(env('DLS_URL', 'localhost')) DLS_URL = str(env('DLS_URL', 'localhost'))
DLS_PORT = int(env('DLS_PORT', '443')) DLS_PORT = int(env('DLS_PORT', '443'))
CERT_PATH = str(env('CERT_PATH', None))
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 = PrivateKey.from_file(str(env('INSTANCE_KEY_RSA', join(dirname(__file__), 'cert/instance.private.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))
LEASE_RENEWAL_DELTA = timedelta(days=int(env('LEASE_EXPIRE_DAYS', 90)), hours=int(env('LEASE_EXPIRE_HOURS', 0))) LEASE_RENEWAL_DELTA = timedelta(days=int(env('LEASE_EXPIRE_DAYS', 90)), hours=int(env('LEASE_EXPIRE_HOURS', 0)))
CLIENT_TOKEN_EXPIRE_DELTA = relativedelta(years=12) 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}']
DT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
PRODUCT_MAPPING = ProductMapping(filename=join(dirname(__file__), 'static/product_mapping.json'))
# Create certificate chain and signing keys jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.pem(), algorithm=ALGORITHMS.RS256)
ca_setup = CASetup(service_instance_ref=INSTANCE_REF, cert_path=CERT_PATH) jwt_decode_key = jwk.construct(INSTANCE_KEY_PUB.pem(), algorithm=ALGORITHMS.RS256)
my_root_private_key = PrivateKey.from_file(ca_setup.root_private_key_filename)
my_root_public_key = my_root_private_key.public_key()
my_root_certificate = Cert.from_file(ca_setup.root_certificate_filename)
my_ca_certificate = Cert.from_file(ca_setup.ca_certificate_filename)
my_si_certificate = Cert.from_file(ca_setup.si_certificate_filename)
my_si_private_key = PrivateKey.from_file(ca_setup.si_private_key_filename)
my_si_public_key = my_si_private_key.public_key()
jwt_encode_key = jwk.construct(my_si_private_key.pem(), algorithm=ALGORITHMS.RS256)
jwt_decode_key = jwk.construct(my_si_private_key.public_key().pem(), algorithm=ALGORITHMS.RS256)
# Logging # Logging
LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO
@@ -99,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(
@@ -117,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 Response(content=json_dumps({'status': 'up'}), media_type='application/json', status_code=200)
@app.get('/-/config', summary='* Config', description='returns environment variables.')
async def _config():
response = {
'VERSION': str(VERSION), 'VERSION': str(VERSION),
'COMMIT': str(COMMIT), 'COMMIT': str(COMMIT),
'DEBUG': str(DEBUG), 'DEBUG': str(DEBUG),
@@ -150,60 +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),
} }
return Response(content=json_dumps(response), media_type='application/json', status_code=200)
# Endpoints
@app.get('/', summary='* Index')
async def index():
return RedirectResponse('/-/')
@app.get('/-/config/root-certificate', summary='* Root Certificate', description='returns Root--Certificate needed for patching nvidia-gridd') @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(): async def _config():
return Response(content=my_root_certificate.pem().decode('utf-8').strip(), media_type='text/plain') 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')
response = 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'])
return Response(response, media_type='text/html', status_code=200) 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) {
var xhr = new XMLHttpRequest(); @app.get('/-/dashboard', summary='* Dashboard')
xhr.open("DELETE", '/-/origins', true); async def _dashboard(request: Request):
xhr.send(); context = {'request': request, 'VERSION': VERSION, 'CONFIG': __json_config()}
} return templates.TemplateResponse(name='views/dashboard.html', context=context)
}
function deleteLease(lease_ref) {
if(lease_ref === undefined) @app.get('/-/dashboard/origins', summary='* Dashboard - Origins')
lease_ref = window.prompt("Please enter 'lease_ref' which should be deleted"); async def _dashboard_origins(request: Request):
if(lease_ref === null || lease_ref === "") context = {'request': request, 'VERSION': VERSION}
return return templates.TemplateResponse(name='views/dashboard_origins.html', context=context)
var xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/lease/${lease_ref}`, true);
xhr.send(); @app.get('/-/dashboard/leases', summary='* Dashboard - Leases')
} async def _dashboard_origins(request: Request):
</script> context = {'request': request, 'VERSION': VERSION}
</body> return templates.TemplateResponse(name='views/dashboard_leases.html', context=context)
</html>
'''
return Response(response, media_type='text/html', status_code=200)
@app.get('/-/origins', summary='* Origins') @app.get('/-/origins', summary='* Origins')
@@ -217,7 +197,7 @@ async def _origins(request: Request, leases: bool = False):
x['leases'] = list(map(lambda _: _.serialize(**serialize), Lease.find_by_origin_ref(db, origin.origin_ref))) x['leases'] = list(map(lambda _: _.serialize(**serialize), Lease.find_by_origin_ref(db, origin.origin_ref)))
response.append(x) response.append(x)
session.close() session.close()
return Response(content=json_dumps(response), media_type='application/json', status_code=200) return JSONr(response)
@app.delete('/-/origins', summary='* Origins') @app.delete('/-/origins', summary='* Origins')
@@ -226,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)()
@@ -239,21 +232,20 @@ async def _leases(request: Request, origin: bool = False):
x['origin'] = lease_origin.serialize() x['origin'] = lease_origin.serialize()
response.append(x) response.append(x)
session.close() session.close()
return Response(content=json_dumps(response), media_type='application/json', status_code=200) 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)
response = {'status': 404, 'detail': 'lease not found'} return JSONr(status_code=404, content={'status': 404, 'detail': 'lease not found'})
return Response(content=json_dumps(response), media_type='application/json', status_code=404)
# venv/lib/python3.9/site-packages/nls_core_service_instance/service_instance_token_manager.py # venv/lib/python3.9/site-packages/nls_core_service_instance/service_instance_token_manager.py
@@ -269,7 +261,6 @@ async def _client_token():
"iat": timegm(cur_time.timetuple()), "iat": timegm(cur_time.timetuple()),
"nbf": timegm(cur_time.timetuple()), "nbf": timegm(cur_time.timetuple()),
"exp": timegm(exp_time.timetuple()), "exp": timegm(exp_time.timetuple()),
"protocol_version": "2.0",
"update_mode": "ABSOLUTE", "update_mode": "ABSOLUTE",
"scope_ref_list": [ALLOTMENT_REF], "scope_ref_list": [ALLOTMENT_REF],
"fulfillment_class_ref_list": [], "fulfillment_class_ref_list": [],
@@ -279,7 +270,6 @@ async def _client_token():
{ {
"idx": 0, "idx": 0,
"d_name": "DLS", "d_name": "DLS",
# todo: {"service": "quick_release", "port": 80} - see "shutdown for windows"
"svc_port_map": [{"service": "auth", "port": DLS_PORT}, {"service": "lease", "port": DLS_PORT}] "svc_port_map": [{"service": "auth", "port": DLS_PORT}, {"service": "lease", "port": DLS_PORT}]
} }
], ],
@@ -287,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": my_si_public_key.mod(), "mod": hex(INSTANCE_KEY_PUB.raw().public_numbers().n)[2:],
"exp": my_si_public_key.exp(), "exp": int(INSTANCE_KEY_PUB.raw().public_numbers().e),
}, },
"service_instance_public_key_pem": my_si_public_key.pem().decode('utf-8').strip(), "service_instance_public_key_pem": INSTANCE_KEY_PUB.pem().decode('utf-8'),
"key_retention_mode": "LATEST_ONLY" "key_retention_mode": "LATEST_ONLY"
}, },
} }
@@ -321,22 +311,17 @@ async def auth_v1_origin(request: Request):
Origin.create_or_update(db, data) Origin.create_or_update(db, data)
environment = {
'raw_env': j.get('environment')
}
environment.update(j.get('environment'))
response = { response = {
"origin_ref": origin_ref, "origin_ref": origin_ref,
"environment": environment, "environment": j.get('environment'),
"svc_port_set_list": None, "svc_port_set_list": None,
"node_url_list": None, "node_url_list": None,
"node_query_order": None, "node_query_order": None,
"prompts": None, "prompts": None,
"sync_timestamp": cur_time.strftime(DT_FORMAT) "sync_timestamp": cur_time.isoformat()
} }
return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200) return JSONr(response)
# venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py # venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py
@@ -359,10 +344,10 @@ async def auth_v1_origin_update(request: Request):
response = { response = {
"environment": j.get('environment'), "environment": j.get('environment'),
"prompts": None, "prompts": None,
"sync_timestamp": cur_time.strftime(DT_FORMAT) "sync_timestamp": cur_time.isoformat()
} }
return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200) return JSONr(response)
# venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py # venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py
@@ -390,11 +375,11 @@ async def auth_v1_code(request: Request):
response = { response = {
"auth_code": auth_code, "auth_code": auth_code,
"prompts": None, "sync_timestamp": cur_time.isoformat(),
"sync_timestamp": cur_time.strftime(DT_FORMAT), "prompts": None
} }
return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200) return JSONr(response)
# venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py # venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py
@@ -406,8 +391,7 @@ async def auth_v1_token(request: Request):
try: try:
payload = jwt.decode(token=j.get('auth_code'), key=jwt_decode_key, algorithms=ALGORITHMS.RS256) payload = jwt.decode(token=j.get('auth_code'), key=jwt_decode_key, algorithms=ALGORITHMS.RS256)
except JWTError as e: except JWTError as e:
response = {'status': 400, 'title': 'invalid token', 'detail': str(e)} return JSONr(status_code=400, content={'status': 400, 'title': 'invalid token', 'detail': str(e)})
return Response(content=json_dumps(response), media_type='application/json', status_code=400)
origin_ref = payload.get('origin_ref') origin_ref = payload.get('origin_ref')
logger.info(f'> [ auth ]: {origin_ref}: {j}') logger.info(f'> [ auth ]: {origin_ref}: {j}')
@@ -415,8 +399,7 @@ async def auth_v1_token(request: Request):
# validate the code challenge # validate the code challenge
challenge = b64enc(sha256(j.get('code_verifier').encode('utf-8')).digest()).rstrip(b'=').decode('utf-8') challenge = b64enc(sha256(j.get('code_verifier').encode('utf-8')).digest()).rstrip(b'=').decode('utf-8')
if payload.get('challenge') != challenge: if payload.get('challenge') != challenge:
response = {'status': 401, 'detail': 'expected challenge did not match verifier'} return JSONr(status_code=401, content={'status': 401, 'detail': 'expected challenge did not match verifier'})
return Response(content=json_dumps(response), media_type='application/json', status_code=401)
access_expires_on = cur_time + TOKEN_EXPIRE_DELTA access_expires_on = cur_time + TOKEN_EXPIRE_DELTA
@@ -426,90 +409,20 @@ async def auth_v1_token(request: Request):
'iss': 'https://cls.nvidia.org', 'iss': 'https://cls.nvidia.org',
'aud': 'https://cls.nvidia.org', 'aud': 'https://cls.nvidia.org',
'exp': timegm(access_expires_on.timetuple()), 'exp': timegm(access_expires_on.timetuple()),
'origin_ref': origin_ref,
'key_ref': SITE_KEY_XID, 'key_ref': SITE_KEY_XID,
'kid': SITE_KEY_XID, 'kid': SITE_KEY_XID,
'origin_ref': origin_ref,
} }
auth_token = jwt.encode(new_payload, key=jwt_encode_key, headers={'kid': payload.get('kid')}, algorithm=ALGORITHMS.RS256) auth_token = jwt.encode(new_payload, key=jwt_encode_key, headers={'kid': payload.get('kid')}, algorithm=ALGORITHMS.RS256)
response = { response = {
"expires": access_expires_on.isoformat(),
"auth_token": auth_token, "auth_token": auth_token,
"expires": access_expires_on.strftime(DT_FORMAT), "sync_timestamp": cur_time.isoformat(),
"prompts": None,
"sync_timestamp": cur_time.strftime(DT_FORMAT),
} }
return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200) return JSONr(response)
# NLS 3.4.0 - venv/lib/python3.12/site-packages/nls_services_lease/test/test_lease_single_controller.py
@app.post('/leasing/v1/config-token', description='request to get config token for lease operations')
async def leasing_v1_config_token(request: Request):
j, cur_time = json_loads((await request.body()).decode('utf-8')), datetime.now(UTC)
cur_time = datetime.now(UTC)
exp_time = cur_time + CLIENT_TOKEN_EXPIRE_DELTA
payload = {
"iss": "NLS Service Instance",
"aud": "NLS Licensed Client",
"iat": timegm(cur_time.timetuple()),
"nbf": timegm(cur_time.timetuple()),
"exp": timegm(exp_time.timetuple()),
"protocol_version": "2.0",
"d_name": "DLS",
"service_instance_ref": j.get('service_instance_ref'),
"service_instance_public_key_configuration": {
"service_instance_public_key_me": {
"mod": my_si_public_key.mod(),
"exp": my_si_public_key.exp(),
},
"service_instance_public_key_pem": my_si_public_key.pem().decode('utf-8').strip(),
"key_retention_mode": "LATEST_ONLY"
},
}
my_jwt_encode_key = jwk.construct(my_si_private_key.pem().decode('utf-8'), algorithm=ALGORITHMS.RS256)
config_token = jws.sign(payload, key=my_jwt_encode_key, headers=None, algorithm=ALGORITHMS.RS256)
response_ca_chain = my_ca_certificate.pem().decode('utf-8').strip()
# 76 chars per line on original response with "\r\n"
"""
response_ca_chain = my_ca_certificate.pem().decode('utf-8').strip()
response_ca_chain = response_ca_chain.replace('-----BEGIN CERTIFICATE-----', '')
response_ca_chain = response_ca_chain.replace('-----END CERTIFICATE-----', '')
response_ca_chain = response_ca_chain.replace('\n', '')
response_ca_chain = wrap(response_ca_chain, 76)
response_ca_chain = '\r\n'.join(response_ca_chain)
response_ca_chain = f'-----BEGIN CERTIFICATE-----\r\n{response_ca_chain}\r\n-----END CERTIFICATE-----'
"""
response_si_certificate = my_si_certificate.pem().decode('utf-8').strip()
# 76 chars per line on original response with "\r\n"
"""
response_si_certificate = my_si_certificate.pem().decode('utf-8').strip()
response_si_certificate = response_si_certificate.replace('-----BEGIN CERTIFICATE-----', '')
response_si_certificate = response_si_certificate.replace('-----END CERTIFICATE-----', '')
response_si_certificate = response_si_certificate.replace('\n', '')
response_si_certificate = wrap(response_si_certificate, 76)
response_si_certificate = '\r\n'.join(response_si_certificate)
"""
response = {
"certificateConfiguration": {
"caChain": [response_ca_chain],
"publicCert": response_si_certificate,
"publicKey": {
"exp": my_si_certificate.public_key().exp(),
"mod": [my_si_certificate.public_key().mod()],
},
},
"configToken": config_token,
}
return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200)
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py # venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py
@@ -520,67 +433,43 @@ async def leasing_v1_lessor(request: Request):
try: try:
token = __get_token(request) token = __get_token(request)
except JWTError: except JWTError:
response = {'status': 401, 'detail': 'token is not valid'} return JSONr(status_code=401, content={'status': 401, 'detail': 'token is not valid'})
return Response(content=json_dumps(response), media_type='application/json', status_code=401)
origin_ref = token.get('origin_ref') origin_ref = token.get('origin_ref')
scope_ref_list = j.get('scope_ref_list') scope_ref_list = j.get('scope_ref_list')
lease_proposal_list = j.get('lease_proposal_list')
logger.info(f'> [ create ]: {origin_ref}: create leases for scope_ref_list {scope_ref_list}') logger.info(f'> [ create ]: {origin_ref}: create leases for scope_ref_list {scope_ref_list}')
lease_result_list = []
for scope_ref in scope_ref_list: for scope_ref in scope_ref_list:
# if scope_ref not in [ALLOTMENT_REF]: # if scope_ref not in [ALLOTMENT_REF]:
# response = {'status': 400, 'detail': f'service instances not found for scopes: ["{scope_ref}"]')} # return JSONr(status_code=500, detail=f'no service instances found for scopes: ["{scope_ref}"]')
# return Response(content=json_dumps(response), media_type='application/json', status_code=400)
pass
lease_result_list = []
for lease_proposal in lease_proposal_list:
lease_ref = str(uuid4()) lease_ref = str(uuid4())
expires = cur_time + LEASE_EXPIRE_DELTA expires = cur_time + LEASE_EXPIRE_DELTA
product_name = lease_proposal.get('product').get('name')
feature_name = PRODUCT_MAPPING.get_feature_name(product_name=product_name)
lease_result_list.append({ lease_result_list.append({
"error": None, "ordinal": 0,
# https://docs.nvidia.com/license-system/latest/nvidia-license-system-user-guide/index.html
"lease": { "lease": {
"created": cur_time.strftime(DT_FORMAT),
"expires": expires.strftime(DT_FORMAT), # todo: lease_proposal.get('duration') => "P0Y0M0DT12H0M0S
"feature_name": feature_name,
"lease_intent_id": None,
"license_type": "CONCURRENT_COUNTED_SINGLE",
"metadata": None,
"offline_lease": False, # todo
"product_name": product_name,
"recommended_lease_renewal": LEASE_RENEWAL_PERIOD,
"ref": lease_ref, "ref": lease_ref,
}, "created": cur_time.isoformat(),
"ordinal": None, "expires": expires.isoformat(),
"recommended_lease_renewal": LEASE_RENEWAL_PERIOD,
"offline_lease": "true",
"license_type": "CONCURRENT_COUNTED_SINGLE"
}
}) })
data = Lease(origin_ref=origin_ref, lease_ref=lease_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) Lease.create_or_update(db, data)
response = { response = {
"client_challenge": j.get('client_challenge'),
"lease_result_list": lease_result_list, "lease_result_list": lease_result_list,
"prompts": None, "result_code": "SUCCESS",
"result_code": None, "sync_timestamp": cur_time.isoformat(),
"sync_timestamp": cur_time.strftime(DT_FORMAT), "prompts": None
} }
content = json_dumps(response, separators=(',', ':')) return JSONr(response)
content = f'{content}\n'.encode('ascii')
signature = my_si_private_key.generate_signature(content)
headers = {
'Content-Type': 'application/json',
'access-control-expose-headers': 'X-NLS-Signature',
'X-NLS-Signature': f'{signature.hex().encode()}'
}
return Response(content=content, media_type='application/json', headers=headers)
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py # venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py
@@ -596,54 +485,39 @@ async def leasing_v1_lessor_lease(request: Request):
response = { response = {
"active_lease_list": active_lease_list, "active_lease_list": active_lease_list,
"prompts": None, "sync_timestamp": cur_time.isoformat(),
"sync_timestamp": cur_time.strftime(DT_FORMAT), "prompts": None
} }
return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200) 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_services_lease/test/test_lease_single_controller.py
# venv/lib/python3.9/site-packages/nls_core_lease/lease_single.py # venv/lib/python3.9/site-packages/nls_core_lease/lease_single.py
@app.put('/leasing/v1/lease/{lease_ref}', description='renew a lease') @app.put('/leasing/v1/lease/{lease_ref}', description='renew a lease')
async def leasing_v1_lease_renew(request: Request, lease_ref: str): async def leasing_v1_lease_renew(request: Request, lease_ref: str):
j, token, cur_time = json_loads((await request.body()).decode('utf-8')), __get_token(request), datetime.now(UTC) token, cur_time = __get_token(request), datetime.now(UTC)
origin_ref = token.get('origin_ref') origin_ref = token.get('origin_ref')
logger.info(f'> [ renew ]: {origin_ref}: renew {lease_ref}') logger.info(f'> [ renew ]: {origin_ref}: renew {lease_ref}')
entity = Lease.find_by_origin_ref_and_lease_ref(db, origin_ref, lease_ref) entity = Lease.find_by_origin_ref_and_lease_ref(db, origin_ref, lease_ref)
if entity is None: if entity is None:
response = {'status': 404, 'detail': 'requested lease not available'} return JSONr(status_code=404, content={'status': 404, 'detail': 'requested lease not available'})
return Response(content=json_dumps(response), media_type='application/json', status_code=404)
expires = cur_time + LEASE_EXPIRE_DELTA expires = cur_time + LEASE_EXPIRE_DELTA
response = { response = {
"client_challenge": j.get('client_challenge'),
"expires": expires.strftime('%Y-%m-%dT%H:%M:%S.%f'), # DT_FORMAT => "trailing 'Z' missing in this response
"feature_expired": False,
"lease_ref": lease_ref, "lease_ref": lease_ref,
"metadata": None, "expires": expires.isoformat(),
"offline_lease": False, # todo
"prompts": None,
"recommended_lease_renewal": LEASE_RENEWAL_PERIOD, "recommended_lease_renewal": LEASE_RENEWAL_PERIOD,
"sync_timestamp": cur_time.strftime(DT_FORMAT), "offline_lease": True,
"prompts": None,
"sync_timestamp": cur_time.isoformat(),
} }
Lease.renew(db, entity, expires, cur_time) Lease.renew(db, entity, expires, cur_time)
content = json_dumps(response, separators=(',', ':')) return JSONr(response)
content = f'{content}\n'.encode('ascii')
signature = my_si_private_key.generate_signature(content)
headers = {
'Content-Type': 'application/json',
'access-control-expose-headers': 'X-NLS-Signature',
'X-NLS-Signature': f'{signature.hex().encode()}'
}
return Response(content=content, media_type='application/json', headers=headers)
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_single_controller.py # venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_single_controller.py
@@ -656,24 +530,20 @@ async def leasing_v1_lease_delete(request: Request, lease_ref: str):
entity = Lease.find_by_lease_ref(db, lease_ref) entity = Lease.find_by_lease_ref(db, lease_ref)
if entity.origin_ref != origin_ref: if entity.origin_ref != origin_ref:
response = {'status': 403, 'detail': 'access or operation forbidden'} return JSONr(status_code=403, content={'status': 403, 'detail': 'access or operation forbidden'})
return Response(content=json_dumps(response), media_type='application/json', status_code=403)
if entity is None: if entity is None:
response = {'status': 404, 'detail': 'requested lease not available'} return JSONr(status_code=404, content={'status': 404, 'detail': 'requested lease not available'})
return Response(content=json_dumps(response), media_type='application/json', status_code=404)
if Lease.delete(db, lease_ref) == 0: if Lease.delete(db, lease_ref) == 0:
response = {'status': 404, 'detail': 'lease not found'} return JSONr(status_code=404, content={'status': 404, 'detail': 'lease not found'})
return Response(content=json_dumps(response), media_type='application/json', status_code=404)
response = { response = {
"client_challenge": None,
"lease_ref": lease_ref, "lease_ref": lease_ref,
"prompts": None, "prompts": None,
"sync_timestamp": cur_time.strftime(DT_FORMAT), "sync_timestamp": cur_time.isoformat(),
} }
return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200) return JSONr(response)
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py # venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py
@@ -690,11 +560,11 @@ async def leasing_v1_lessor_lease_remove(request: Request):
response = { response = {
"released_lease_list": released_lease_list, "released_lease_list": released_lease_list,
"release_failure_list": None, "release_failure_list": None,
"prompts": None, "sync_timestamp": cur_time.isoformat(),
"sync_timestamp": cur_time.strftime(DT_FORMAT), "prompts": None
} }
return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200) return JSONr(response)
@app.post('/leasing/v1/lessor/shutdown', description='shutdown all leases') @app.post('/leasing/v1/lessor/shutdown', description='shutdown all leases')
@@ -712,11 +582,11 @@ async def leasing_v1_lessor_shutdown(request: Request):
response = { response = {
"released_lease_list": released_lease_list, "released_lease_list": released_lease_list,
"release_failure_list": None, "release_failure_list": None,
"prompts": None, "sync_timestamp": cur_time.isoformat(),
"sync_timestamp": cur_time.strftime(DT_FORMAT), "prompts": None
} }
return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200) return JSONr(response)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -5,7 +5,7 @@ from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, update, and_
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.orm import sessionmaker, declarative_base from sqlalchemy.orm import sessionmaker, declarative_base
from util import DriverMatrix from util import NV
Base = declarative_base() Base = declarative_base()
@@ -25,7 +25,7 @@ class Origin(Base):
return f'Origin(origin_ref={self.origin_ref}, hostname={self.hostname})' return f'Origin(origin_ref={self.origin_ref}, hostname={self.hostname})'
def serialize(self) -> dict: def serialize(self) -> dict:
_ = DriverMatrix().find(self.guest_driver_version) _ = NV().find(self.guest_driver_version)
return { return {
'origin_ref': self.origin_ref, 'origin_ref': self.origin_ref,

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

@@ -1,643 +0,0 @@
{
"product": [
{
"xid": "c0ce7114-d8a5-40d4-b8b0-df204f4ff631",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "NVIDIA-vComputeServer-9.0",
"name": "NVIDIA-vComputeServer-9.0",
"description": null
},
{
"xid": "2a99638e-493f-424b-bc3a-629935307490",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "vGaming_Flexera_License-0.1",
"name": "vGaming_Flexera_License-0.1",
"description": null
},
{
"xid": "a013d60c-3cd6-4e61-ae51-018b5e342178",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "GRID-Virtual-Apps-3.0",
"name": "GRID-Virtual-Apps-3.0",
"description": null
},
{
"xid": "bb99c6a3-81ce-4439-aef5-9648e75dd878",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "GRID-vGaming-NLS-Metered-8.0",
"name": "GRID-vGaming-NLS-Metered-8.0",
"description": null
},
{
"xid": "c653e131-695c-4477-b77c-42ade3dcb02c",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "GRID-Virtual-WS-Ext-2.0",
"name": "GRID-Virtual-WS-Ext-2.0",
"description": null
},
{
"xid": "6fc224ef-e0b5-467b-9bbb-d31c9eb7c6fc",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "GRID-vGaming-8.0",
"name": "GRID-vGaming-8.0",
"description": null
},
{
"xid": "3c88888d-ebf3-4df7-9e86-c97d5b29b997",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "GRID-Virtual-PC-2.0",
"name": "GRID-Virtual-PC-2.0",
"description": null
},
{
"xid": "66744b41-1fff-49be-a5a6-4cbd71b1117e",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "NVAIE_Licensing-1.0",
"name": "NVAIE_Licensing-1.0",
"description": null
},
{
"xid": "1d4e9ebc-a78c-41f4-a11a-de38a467b2ba",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "NVIDIA-vComputeServer NLS Metered-9.0",
"name": "NVIDIA-vComputeServer NLS Metered-9.0",
"description": null
},
{
"xid": "2152f8aa-d17b-46f5-8f5f-6f8c0760ce9c",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "vGaming_FB_License-0.1",
"name": "vGaming_FB_License-0.1",
"description": null
},
{
"xid": "54cbe0e8-7b35-4068-b058-e11f5b367c66",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "Quadro-Virtual-DWS-5.0",
"name": "Quadro-Virtual-DWS-5.0",
"description": null
},
{
"xid": "07a1d2b5-c147-48bc-bf44-9390339ca388",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "GRID-Virtual-WS-2.0",
"name": "GRID-Virtual-WS-2.0",
"description": null
},
{
"xid": "82d7a5f0-0c26-11ef-b3b6-371045c70906",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "vGaming_Flexera_License-0.1",
"name": "vGaming_Flexera_License-0.1",
"description": null
},
{
"xid": "bdfbde00-2cdb-11ec-9838-061a22468b59",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "NVIDIA Virtual Applications",
"name": "NVIDIA Virtual Applications",
"description": null
},
{
"xid": "bdfbe16d-2cdb-11ec-9838-061a22468b59",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "NVIDIA Virtual PC",
"name": "NVIDIA Virtual PC",
"description": null
},
{
"xid": "bdfbe308-2cdb-11ec-9838-061a22468b59",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "NVIDIA RTX Virtual Workstation",
"name": "NVIDIA RTX Virtual Workstation",
"description": null
},
{
"xid": "bdfbe405-2cdb-11ec-9838-061a22468b59",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "NVIDIA vGaming",
"name": "NVIDIA vGaming",
"description": null
},
{
"xid": "bdfbe509-2cdb-11ec-9838-061a22468b59",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "GRID Virtual Applications",
"name": "GRID Virtual Applications",
"description": null
},
{
"xid": "bdfbe5c6-2cdb-11ec-9838-061a22468b59",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "GRID Virtual PC",
"name": "GRID Virtual PC",
"description": null
},
{
"xid": "bdfbe6e8-2cdb-11ec-9838-061a22468b59",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "Quadro Virtual Data Center Workstation",
"name": "Quadro Virtual Data Center Workstation",
"description": null
},
{
"xid": "bdfbe7c8-2cdb-11ec-9838-061a22468b59",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "GRID vGaming",
"name": "GRID vGaming",
"description": null
},
{
"xid": "bdfbe884-2cdb-11ec-9838-061a22468b59",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "NVIDIA Virtual Compute Server",
"name": "NVIDIA Virtual Compute Server",
"description": null
},
{
"xid": "f09b5c33-5c07-11ed-9fa6-061a22468b59",
"product_family_xid": "bda4d909-2cdb-11ec-9838-061a22468b59",
"identifier": "NVIDIA OVE Licensing",
"name": "NVIDIA Omniverse Nucleus",
"description": null
}
],
"product_fulfillment": [
{
"xid": "cf0a5330-b583-4d9f-84bb-cfc8ce0917bb",
"product_xid": "07a1d2b5-c147-48bc-bf44-9390339ca388",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "90d0f05f-9431-4a15-86e7-740a4f08d457",
"product_xid": "1d4e9ebc-a78c-41f4-a11a-de38a467b2ba",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "327385dd-4ba8-4b3c-bc56-30bcf58ae9a3",
"product_xid": "2152f8aa-d17b-46f5-8f5f-6f8c0760ce9c",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "6733f2cc-0736-47ee-bcc8-20c4c624ce37",
"product_xid": "2a99638e-493f-424b-bc3a-629935307490",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "f35396a9-24f8-44b6-aa6a-493b335f4d56",
"product_xid": "3c88888d-ebf3-4df7-9e86-c97d5b29b997",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "6c7981d3-7192-4bfd-b7ec-ea2ad0b466dc",
"product_xid": "54cbe0e8-7b35-4068-b058-e11f5b367c66",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "9bd09610-6190-4684-9be6-3d9503833e80",
"product_xid": "66744b41-1fff-49be-a5a6-4cbd71b1117e",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "a4282e5b-ea08-4e0a-b724-7f4059ba99de",
"product_xid": "6fc224ef-e0b5-467b-9bbb-d31c9eb7c6fc",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "5cf793fc-1fb3-45c0-a711-d3112c775cbe",
"product_xid": "a013d60c-3cd6-4e61-ae51-018b5e342178",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "eb2d39a4-6370-4464-8a6a-ec3f42c69cb5",
"product_xid": "bb99c6a3-81ce-4439-aef5-9648e75dd878",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "e9df1c70-7fac-4c84-b54c-66e922b9791a",
"product_xid": "c0ce7114-d8a5-40d4-b8b0-df204f4ff631",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "6a4d5bcd-7b81-4e22-a289-ce3673e5cabf",
"product_xid": "c653e131-695c-4477-b77c-42ade3dcb02c",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "9e162d3c-0c26-11ef-b3b6-371045c70906",
"product_xid": "82d7a5f0-0c26-11ef-b3b6-371045c70906",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "be2769b9-2cdb-11ec-9838-061a22468b59",
"product_xid": "bdfbde00-2cdb-11ec-9838-061a22468b59",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "be276d7b-2cdb-11ec-9838-061a22468b59",
"product_xid": "bdfbe16d-2cdb-11ec-9838-061a22468b59",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "be276efe-2cdb-11ec-9838-061a22468b59",
"product_xid": "bdfbe308-2cdb-11ec-9838-061a22468b59",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "be276ff0-2cdb-11ec-9838-061a22468b59",
"product_xid": "bdfbe405-2cdb-11ec-9838-061a22468b59",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "be2770af-2cdb-11ec-9838-061a22468b59",
"product_xid": "bdfbe509-2cdb-11ec-9838-061a22468b59",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "be277164-2cdb-11ec-9838-061a22468b59",
"product_xid": "bdfbe5c6-2cdb-11ec-9838-061a22468b59",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "be277214-2cdb-11ec-9838-061a22468b59",
"product_xid": "bdfbe6e8-2cdb-11ec-9838-061a22468b59",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "be2772c8-2cdb-11ec-9838-061a22468b59",
"product_xid": "bdfbe7c8-2cdb-11ec-9838-061a22468b59",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "be277379-2cdb-11ec-9838-061a22468b59",
"product_xid": "bdfbe884-2cdb-11ec-9838-061a22468b59",
"qualifier_specification": null,
"evaluation_order_index": 0
},
{
"xid": "c4284597-5c09-11ed-9fa6-061a22468b59",
"product_xid": "f09b5c33-5c07-11ed-9fa6-061a22468b59",
"qualifier_specification": null,
"evaluation_order_index": 0
}
],
"product_fulfillment_feature": [
{
"xid": "9ca32d2b-736e-4e4f-8f5a-895a755b4c41",
"product_fulfillment_xid": "5cf793fc-1fb3-45c0-a711-d3112c775cbe",
"feature_identifier": "GRID-Virtual-Apps",
"feature_version": "3.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "d8b25329-f47f-43dc-a278-f2d38f9e939b",
"product_fulfillment_xid": "f35396a9-24f8-44b6-aa6a-493b335f4d56",
"feature_identifier": "GRID-Virtual-PC",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "e7102df8-d88a-4bd0-aa79-9a53d8b77888",
"product_fulfillment_xid": "cf0a5330-b583-4d9f-84bb-cfc8ce0917bb",
"feature_identifier": "GRID-Virtual-WS",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "30761db3-0afe-454d-b284-efba6d9b13a3",
"product_fulfillment_xid": "6a4d5bcd-7b81-4e22-a289-ce3673e5cabf",
"feature_identifier": "GRID-Virtual-WS-Ext",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "10fd7701-83ae-4caf-a27f-75880fab23f6",
"product_fulfillment_xid": "a4282e5b-ea08-4e0a-b724-7f4059ba99de",
"feature_identifier": "GRID-vGaming",
"feature_version": "8.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "cbd61276-fb1e-42e1-b844-43e94465da8f",
"product_fulfillment_xid": "9bd09610-6190-4684-9be6-3d9503833e80",
"feature_identifier": "NVAIE_Licensing",
"feature_version": "1.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "6b1c74b5-1511-46ee-9f12-8bc6d5636fef",
"product_fulfillment_xid": "90d0f05f-9431-4a15-86e7-740a4f08d457",
"feature_identifier": "NVIDIA-vComputeServer NLS Metered",
"feature_version": "9.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "db53af09-7295-48b7-b927-24b23690c959",
"product_fulfillment_xid": "e9df1c70-7fac-4c84-b54c-66e922b9791a",
"feature_identifier": "NVIDIA-vComputeServer",
"feature_version": "9.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "1f62be61-a887-4e54-a34e-61cfa7b2db30",
"product_fulfillment_xid": "6c7981d3-7192-4bfd-b7ec-ea2ad0b466dc",
"feature_identifier": "Quadro-Virtual-DWS",
"feature_version": "5.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "8a4b5e98-f1ca-4c18-b0d4-8f4f9f0462e2",
"product_fulfillment_xid": "327385dd-4ba8-4b3c-bc56-30bcf58ae9a3",
"feature_identifier": "vGaming_FB_License",
"feature_version": "0.1",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "be531e98-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be2769b9-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-Apps",
"feature_version": "3.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "be53219e-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be276d7b-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-PC",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "be5322f0-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be276d7b-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "Quadro-Virtual-DWS",
"feature_version": "5.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 1
},
{
"xid": "be5323d8-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be276d7b-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-WS",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 2
},
{
"xid": "be5324a6-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be276d7b-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-WS-Ext",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 3
},
{
"xid": "be532568-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be276efe-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "Quadro-Virtual-DWS",
"feature_version": "5.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "be532630-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be276efe-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-WS",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 1
},
{
"xid": "be5326e7-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be276efe-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-WS-Ext",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 2
},
{
"xid": "be5327a7-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be276ff0-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-vGaming",
"feature_version": "8.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "be532923-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be2770af-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-Apps",
"feature_version": "3.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "be5329e0-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be277164-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-PC",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "be532aa0-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be277164-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "Quadro-Virtual-DWS",
"feature_version": "5.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 1
},
{
"xid": "be532b5c-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be277164-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-WS",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 2
},
{
"xid": "be532c19-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be277164-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-WS-Ext",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 3
},
{
"xid": "be532ccb-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be277214-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "Quadro-Virtual-DWS",
"feature_version": "5.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "be532d92-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be277214-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-WS",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 1
},
{
"xid": "be532e45-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be277214-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-Virtual-WS-Ext",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 2
},
{
"xid": "be532efa-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be2772c8-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-vGaming",
"feature_version": "8.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "be53306d-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be277379-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "NVIDIA-vComputeServer",
"feature_version": "9.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "be533228-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be277379-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "NVIDIA-vComputeServer NLS Metered",
"feature_version": "9.0",
"license_type_identifier": "CONCURRENT_UNCOUNTED_SINGLE",
"evaluation_order_index": 2
},
{
"xid": "be5332f6-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be277379-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "NVAIE_Licensing",
"feature_version": "1.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 3
},
{
"xid": "15ff4f16-57a8-4593-93ec-58352a256f12",
"product_fulfillment_xid": "eb2d39a4-6370-4464-8a6a-ec3f42c69cb5",
"feature_identifier": "GRID-vGaming-NLS-Metered",
"feature_version": "8.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 3
},
{
"xid": "0c1552ca-3ef8-11ed-9fa6-061a22468b59",
"product_fulfillment_xid": "be276ff0-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "vGaming_Flexera_License",
"feature_version": "0.1",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 1
},
{
"xid": "31c3be8c-5c0a-11ed-9fa6-061a22468b59",
"product_fulfillment_xid": "c4284597-5c09-11ed-9fa6-061a22468b59",
"feature_identifier": "OVE_Licensing",
"feature_version": "1.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
},
{
"xid": "6caeb4cf-360f-11ee-b67d-02f279bf2bff",
"product_fulfillment_xid": "be277379-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "NVAIE_Licensing",
"feature_version": "2.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 4
},
{
"xid": "7fb1d01d-3f0e-11ed-9fa6-061a22468b59",
"product_fulfillment_xid": "be276ff0-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "vGaming_FB_License",
"feature_version": "0.1",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 2
},
{
"xid": "8eabcb08-3f0e-11ed-9fa6-061a22468b59",
"product_fulfillment_xid": "be2772c8-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "vGaming_FB_License",
"feature_version": "0.1",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 2
},
{
"xid": "a1dfe741-3e49-11ed-9fa6-061a22468b59",
"product_fulfillment_xid": "be2772c8-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "vGaming_Flexera_License",
"feature_version": "0.1",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 1
},
{
"xid": "be53286a-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be276ff0-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-vGaming-NLS-Metered",
"feature_version": "8.0",
"license_type_identifier": "CONCURRENT_UNCOUNTED_SINGLE",
"evaluation_order_index": 3
},
{
"xid": "be532fb2-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be2772c8-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "GRID-vGaming-NLS-Metered",
"feature_version": "8.0",
"license_type_identifier": "CONCURRENT_UNCOUNTED_SINGLE",
"evaluation_order_index": 3
},
{
"xid": "be533144-2cdb-11ec-9838-061a22468b59",
"product_fulfillment_xid": "be277379-2cdb-11ec-9838-061a22468b59",
"feature_identifier": "Quadro-Virtual-DWS",
"feature_version": "0.0",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 1
},
{
"xid": "bf105e18-0c26-11ef-b3b6-371045c70906",
"product_fulfillment_xid": "9e162d3c-0c26-11ef-b3b6-371045c70906",
"feature_identifier": "vGaming_Flexera_License",
"feature_version": "0.1",
"license_type_identifier": "CONCURRENT_COUNTED_SINGLE",
"evaluation_order_index": 0
}
]
}

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,232 +1,12 @@
import logging import logging
from datetime import datetime, UTC, timedelta
from json import loads as json_loads
from os.path import join, dirname, isfile, isdir
from cryptography import x509 from cryptography.hazmat.primitives import serialization
from cryptography.hazmat._oid import NameOID
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey, generate_private_key from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey, generate_private_key
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
from cryptography.x509 import load_pem_x509_certificate, Certificate
logging.basicConfig() logging.basicConfig()
def load_file(filename: str) -> bytes:
log = logging.getLogger(f'{__name__}')
log.debug(f'Loading contents of file "{filename}')
with open(filename, 'rb') as file:
content = file.read()
return content
class CASetup:
###
#
# https://git.collinwebdesigns.de/nvidia/nls/-/blob/main/src/test/test_config_token.py
#
###
ROOT_PRIVATE_KEY_FILENAME = 'root_private_key.pem'
ROOT_CERTIFICATE_FILENAME = 'root_certificate.pem'
CA_PRIVATE_KEY_FILENAME = 'ca_private_key.pem'
CA_CERTIFICATE_FILENAME = 'ca_certificate.pem'
SI_PRIVATE_KEY_FILENAME = 'si_private_key.pem'
SI_CERTIFICATE_FILENAME = 'si_certificate.pem'
def __init__(self, service_instance_ref: str, cert_path: str = None):
cert_path_prefix = join(dirname(__file__), 'cert')
if cert_path is not None and len(cert_path) > 0 and isdir(cert_path):
cert_path_prefix = cert_path
self.service_instance_ref = service_instance_ref
self.root_private_key_filename = join(cert_path_prefix, CASetup.ROOT_PRIVATE_KEY_FILENAME)
self.root_certificate_filename = join(dirname(__file__), 'cert', CASetup.ROOT_CERTIFICATE_FILENAME)
self.ca_private_key_filename = join(dirname(__file__), 'cert', CASetup.CA_PRIVATE_KEY_FILENAME)
self.ca_certificate_filename = join(dirname(__file__), 'cert', CASetup.CA_CERTIFICATE_FILENAME)
self.si_private_key_filename = join(dirname(__file__), 'cert', CASetup.SI_PRIVATE_KEY_FILENAME)
self.si_certificate_filename = join(dirname(__file__), 'cert', CASetup.SI_CERTIFICATE_FILENAME)
if not (isfile(self.root_private_key_filename)
and isfile(self.root_certificate_filename)
and isfile(self.ca_private_key_filename)
and isfile(self.ca_certificate_filename)
and isfile(self.si_private_key_filename)
and isfile(self.si_certificate_filename)):
self.init_config_token_demo()
def init_config_token_demo(self):
""" Create Root Key and Certificate """
# create root keypair
my_root_private_key = generate_private_key(public_exponent=65537, key_size=4096)
my_root_public_key = my_root_private_key.public_key()
# create root-certificate subject
my_root_subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u'US'),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u'California'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'Nvidia'),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u'Nvidia Licensing Service (NLS)'),
x509.NameAttribute(NameOID.COMMON_NAME, u'NLS Root CA'),
])
# create self-signed root-certificate
my_root_certificate = (
x509.CertificateBuilder()
.subject_name(my_root_subject)
.issuer_name(my_root_subject)
.public_key(my_root_public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(tz=UTC) - timedelta(days=1))
.not_valid_after(datetime.now(tz=UTC) + timedelta(days=365 * 10))
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
.add_extension(x509.KeyUsage(
digital_signature=False,
key_encipherment=False,
key_cert_sign=True,
key_agreement=False,
content_commitment=False,
data_encipherment=False,
crl_sign=True,
encipher_only=False,
decipher_only=False),
critical=True
)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(my_root_public_key), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(my_root_public_key), critical=False)
.sign(my_root_private_key, hashes.SHA256()))
my_root_private_key_as_pem = my_root_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
with open(self.root_private_key_filename, 'wb') as f:
f.write(my_root_private_key_as_pem)
with open(self.root_certificate_filename, 'wb') as f:
f.write(my_root_certificate.public_bytes(encoding=Encoding.PEM))
""" Create CA (Intermediate) Key and Certificate """
# create ca keypair
my_ca_private_key = generate_private_key(public_exponent=65537, key_size=4096)
my_ca_public_key = my_ca_private_key.public_key()
# create ca-certificate subject
my_ca_subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u'US'),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u'California'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'Nvidia'),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u'Nvidia Licensing Service (NLS)'),
x509.NameAttribute(NameOID.COMMON_NAME, u'NLS Intermediate CA'),
])
# create self-signed ca-certificate
my_ca_certificate = (
x509.CertificateBuilder()
.subject_name(my_ca_subject)
.issuer_name(my_root_subject)
.public_key(my_ca_public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(tz=UTC) - timedelta(days=1))
.not_valid_after(datetime.now(tz=UTC) + timedelta(days=365 * 10))
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
.add_extension(x509.KeyUsage(
digital_signature=False,
key_encipherment=False,
key_cert_sign=True,
key_agreement=False,
content_commitment=False,
data_encipherment=False,
crl_sign=True,
encipher_only=False,
decipher_only=False),
critical=True
)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(my_ca_public_key), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
my_root_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
), critical=False)
.sign(my_root_private_key, hashes.SHA256()))
my_ca_private_key_as_pem = my_ca_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
with open(self.ca_private_key_filename, 'wb') as f:
f.write(my_ca_private_key_as_pem)
with open(self.ca_certificate_filename, 'wb') as f:
f.write(my_ca_certificate.public_bytes(encoding=Encoding.PEM))
""" Create Service-Instance Key and Certificate """
# create si keypair
my_si_private_key = generate_private_key(public_exponent=65537, key_size=2048)
my_si_public_key = my_si_private_key.public_key()
my_si_private_key_as_pem = my_si_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
my_si_public_key_as_pem = my_si_public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
with open(self.si_private_key_filename, 'wb') as f:
f.write(my_si_private_key_as_pem)
# with open(self.si_public_key_filename, 'wb') as f:
# f.write(my_si_public_key_as_pem)
# create si-certificate subject
my_si_subject = x509.Name([
# x509.NameAttribute(NameOID.COMMON_NAME, INSTANCE_REF),
x509.NameAttribute(NameOID.COMMON_NAME, self.service_instance_ref),
])
# create self-signed si-certificate
my_si_certificate = (
x509.CertificateBuilder()
.subject_name(my_si_subject)
.issuer_name(my_ca_subject)
.public_key(my_si_public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(tz=UTC) - timedelta(days=1))
.not_valid_after(datetime.now(tz=UTC) + timedelta(days=365 * 10))
.add_extension(x509.KeyUsage(digital_signature=True, key_encipherment=True, key_cert_sign=False,
key_agreement=True, content_commitment=False, data_encipherment=False,
crl_sign=False, encipher_only=False, decipher_only=False), critical=True)
.add_extension(x509.ExtendedKeyUsage([
x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH]
), critical=False)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(my_si_public_key), critical=False)
# .add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(my_ca_public_key), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
my_ca_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
), critical=False)
.add_extension(x509.SubjectAlternativeName([
# x509.DNSName(INSTANCE_REF)
x509.DNSName(self.service_instance_ref)
]), critical=False)
.sign(my_ca_private_key, hashes.SHA256()))
with open(self.si_certificate_filename, 'wb') as f:
f.write(my_si_certificate.public_bytes(encoding=Encoding.PEM))
class PrivateKey: class PrivateKey:
def __init__(self, data: bytes): def __init__(self, data: bytes):
@@ -259,9 +39,6 @@ class PrivateKey:
) )
return PublicKey(data=data) return PublicKey(data=data)
def generate_signature(self, data: bytes) -> bytes:
return self.__key.sign(data=data, padding=PKCS1v15(), algorithm=SHA256())
@staticmethod @staticmethod
def generate(public_exponent: int = 65537, key_size: int = 2048) -> "PrivateKey": def generate(public_exponent: int = 65537, key_size: int = 2048) -> "PrivateKey":
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -299,78 +76,38 @@ class PublicKey:
format=serialization.PublicFormat.SubjectPublicKeyInfo format=serialization.PublicFormat.SubjectPublicKeyInfo
) )
def mod(self) -> str: def load_file(filename: str) -> bytes:
return hex(self.__key.public_numbers().n)[2:] log = logging.getLogger(f'{__name__}')
log.debug(f'Loading contents of file "{filename}')
def exp(self): with open(filename, 'rb') as file:
return int(self.__key.public_numbers().e) content = file.read()
return content
def verify_signature(self, signature: bytes, data: bytes) -> None:
self.__key.verify(signature=signature, data=data, padding=PKCS1v15(), algorithm=SHA256())
class Cert: class NV:
def __init__(self, data: bytes):
self.__cert = load_pem_x509_certificate(data)
@staticmethod
def from_file(filename: str) -> "Cert":
log = logging.getLogger(__name__)
log.debug(f'Importing Certificate from "{filename}"')
with open(filename, 'rb') as f:
data = f.read()
return Cert(data=data.strip())
def raw(self) -> Certificate:
return self.__cert
def pem(self) -> bytes:
return self.__cert.public_bytes(encoding=serialization.Encoding.PEM)
def public_key(self) -> "PublicKey":
data = self.__cert.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return PublicKey(data=data)
def signature(self) -> bytes:
return self.__cert.signature
def subject_key_identifier(self):
return self.__cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value.key_identifier
def authority_key_identifier(self):
return self.__cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value.key_identifier
class DriverMatrix:
__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"
def __init__(self): def __init__(self):
self.log = logging.getLogger(self.__class__.__name__) self.log = logging.getLogger(self.__class__.__name__)
if DriverMatrix.__DRIVER_MATRIX is None: if NV.__DRIVER_MATRIX is None:
self.__load() from os.path import join, dirname
from json import load as json_load
def __load(self): try:
try: file = open(join(dirname(__file__), NV.__DRIVER_MATRIX_FILENAME))
with open(DriverMatrix.__DRIVER_MATRIX_FILENAME, 'r') as f: NV.__DRIVER_MATRIX = json_load(file)
DriverMatrix.__DRIVER_MATRIX = json_loads(f.read()) file.close()
self.log.debug(f'Successfully loaded "{DriverMatrix.__DRIVER_MATRIX_FILENAME}".') self.log.debug(f'Successfully loaded "{NV.__DRIVER_MATRIX_FILENAME}".')
except Exception as e: except Exception as e:
DriverMatrix.__DRIVER_MATRIX = {} # init empty dict to not try open file everytime, just when restarting app NV.__DRIVER_MATRIX = {} # init empty dict to not try open file everytime, just when restarting app
# self.log.warning(f'Failed to load "{NV.__DRIVER_MATRIX_FILENAME}": {e}') # self.log.warning(f'Failed to load "{NV.__DRIVER_MATRIX_FILENAME}": {e}')
@staticmethod @staticmethod
def find(version: str) -> dict | None: def find(version: str) -> dict | None:
if DriverMatrix.__DRIVER_MATRIX is None: if NV.__DRIVER_MATRIX is None:
return None return None
for idx, (key, branch) in enumerate(DriverMatrix.__DRIVER_MATRIX.items()): for idx, (key, branch) in enumerate(NV.__DRIVER_MATRIX.items()):
for release in branch.get('$releases'): for release in branch.get('$releases'):
linux_driver = release.get('Linux Driver') linux_driver = release.get('Linux Driver')
windows_driver = release.get('Windows Driver') windows_driver = release.get('Windows Driver')
@@ -390,34 +127,3 @@ class DriverMatrix:
'is_latest': is_latest, 'is_latest': is_latest,
} }
return None return None
class ProductMapping:
def __init__(self, filename: str):
with open(filename, 'r') as file:
self.data = json_loads(file.read())
def get_feature_name(self, product_name: str) -> (str, str):
product = self.__get_product(product_name)
product_fulfillment = self.__get_product_fulfillment(product.get('xid'))
feature = self.__get_product_fulfillment_feature(product_fulfillment.get('xid'))
return feature.get('feature_identifier')
def __get_product(self, product_name: str):
product_list = self.data.get('product')
return next(filter(lambda _: _.get('identifier') == product_name, product_list))
def __get_product_fulfillment(self, product_xid: str):
product_fulfillment_list = self.data.get('product_fulfillment')
return next(filter(lambda _: _.get('product_xid') == product_xid, product_fulfillment_list))
def __get_product_fulfillment_feature(self, product_fulfillment_xid: str):
feature_list = self.data.get('product_fulfillment_feature')
features = list(filter(lambda _: _.get('product_fulfillment_xid') == product_fulfillment_xid, feature_list))
features.sort(key=lambda _: _.get('evaluation_order_index'))
return features[0]

View File

@@ -15,7 +15,7 @@ services:
<<: *dls-variables <<: *dls-variables
volumes: volumes:
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /opt/docker/fastapi-dls/cert:/app/cert - /opt/docker/fastapi-dls/cert:/app/cert # instance.private.pem, instance.public.pem
- db:/app/database - db:/app/database
entrypoint: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app", "--proxy-headers"] entrypoint: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app", "--proxy-headers"]
healthcheck: healthcheck:

View File

@@ -1,8 +1,9 @@
fastapi==0.115.12 fastapi==0.115.12
uvicorn[standard]==0.34.2 uvicorn[standard]==0.34.0
python-jose[cryptography]==3.4.0 python-jose[cryptography]==3.4.0
cryptography==44.0.3 cryptography==44.0.2
python-dateutil==2.9.0 python-dateutil==2.9.0
sqlalchemy==2.0.40 sqlalchemy==2.0.40
markdown==3.8 markdown==3.7
python-dotenv==1.1.0 python-dotenv==1.1.0
jinja2==3.1.3

View File

@@ -6,7 +6,7 @@ logger.setLevel(logging.INFO)
URL = 'https://docs.nvidia.com/vgpu/index.html' URL = 'https://docs.nvidia.com/vgpu/index.html'
BRANCH_STATUS_KEY = 'vGPU Branch Status' BRANCH_STATUS_KEY, SOFTWARE_BRANCH_KEY, = 'vGPU Branch Status', 'vGPU Software Branch'
VGPU_KEY, GRID_KEY, DRIVER_BRANCH_KEY = 'vGPU Software', 'vGPU Software', 'Driver Branch' VGPU_KEY, GRID_KEY, DRIVER_BRANCH_KEY = 'vGPU Software', 'vGPU Software', 'Driver Branch'
LINUX_VGPU_MANAGER_KEY, LINUX_DRIVER_KEY = 'Linux vGPU Manager', 'Linux Driver' LINUX_VGPU_MANAGER_KEY, LINUX_DRIVER_KEY = 'Linux vGPU Manager', 'Linux Driver'
WINDOWS_VGPU_MANAGER_KEY, WINDOWS_DRIVER_KEY = 'Windows vGPU Manager', 'Windows Driver' WINDOWS_VGPU_MANAGER_KEY, WINDOWS_DRIVER_KEY = 'Windows vGPU Manager', 'Windows Driver'
@@ -26,15 +26,12 @@ def __driver_versions(html: 'BeautifulSoup'):
# find wrapper for "DriverVersions" and find tables # find wrapper for "DriverVersions" and find tables
data = html.find('div', {'id': 'driver-versions'}) data = html.find('div', {'id': 'driver-versions'})
items = data.find_all('bsp-accordion', {'class': 'Accordion-items-item'}) items = data.findAll('bsp-accordion', {'class': 'Accordion-items-item'})
for item in items: for item in items:
software_branch = item.find('div', {'class': 'Accordion-items-item-title'}).text.strip() software_branch = item.find('div', {'class': 'Accordion-items-item-title'}).text.strip()
software_branch = software_branch.replace(' Releases', '') software_branch = software_branch.replace(' Releases', '')
matrix_key = software_branch.lower() matrix_key = software_branch.lower()
branch_status = item.find('a', href=True, string='Branch status')
branch_status = branch_status.next_sibling.replace(':', '').strip()
# driver version info from table-heads (ths) and table-rows (trs) # driver version info from table-heads (ths) and table-rows (trs)
table = item.find('table') table = item.find('table')
ths, trs = table.find_all('th'), table.find_all('tr') ths, trs = table.find_all('th'), table.find_all('tr')
@@ -45,20 +42,48 @@ def __driver_versions(html: 'BeautifulSoup'):
continue continue
# create dict with table-heads as key and cell content as value # create dict with table-heads as key and cell content as value
x = {headers[i]: __strip(cell.text) for i, cell in enumerate(tds)} x = {headers[i]: __strip(cell.text) for i, cell in enumerate(tds)}
x.setdefault(BRANCH_STATUS_KEY, branch_status)
releases.append(x) releases.append(x)
# add to matrix # add to matrix
MATRIX.update({matrix_key: {JSON_RELEASES_KEY: releases}}) MATRIX.update({matrix_key: {JSON_RELEASES_KEY: releases}})
def __release_branches(html: 'BeautifulSoup'):
# find wrapper for "AllReleaseBranches" and find table
data = html.find('div', {'id': 'all-release-branches'})
table = data.find('table')
# branch releases info from table-heads (ths) and table-rows (trs)
ths, trs = table.find_all('th'), table.find_all('tr')
headers = [header.text.strip() for header in ths]
for trs in trs:
tds = trs.find_all('td')
if len(tds) == 0: # skip empty
continue
# create dict with table-heads as key and cell content as value
x = {headers[i]: cell.text.strip() for i, cell in enumerate(tds)}
# get matrix_key
software_branch = x.get(SOFTWARE_BRANCH_KEY)
matrix_key = software_branch.lower()
# add to matrix
MATRIX.update({matrix_key: MATRIX.get(matrix_key) | x})
def __debug(): def __debug():
# print table head # print table head
s = f'{VGPU_KEY:^13} | {LINUX_VGPU_MANAGER_KEY:^21} | {LINUX_DRIVER_KEY:^21} | {WINDOWS_VGPU_MANAGER_KEY:^21} | {WINDOWS_DRIVER_KEY:^21} | {RELEASE_DATE_KEY:>21} | {BRANCH_STATUS_KEY:^21}' s = f'{SOFTWARE_BRANCH_KEY:^21} | {BRANCH_STATUS_KEY:^21} | {VGPU_KEY:^13} | {LINUX_VGPU_MANAGER_KEY:^21} | {LINUX_DRIVER_KEY:^21} | {WINDOWS_VGPU_MANAGER_KEY:^21} | {WINDOWS_DRIVER_KEY:^21} | {RELEASE_DATE_KEY:>21} | {EOL_KEY:>21}'
print(s) print(s)
# iterate over dict & format some variables to not overload table # iterate over dict & format some variables to not overload table
for idx, (key, branch) in enumerate(MATRIX.items()): for idx, (key, branch) in enumerate(MATRIX.items()):
branch_status = branch.get(BRANCH_STATUS_KEY)
branch_status = branch_status.replace('Branch ', '')
branch_status = branch_status.replace('Long-Term Support', 'LTS')
branch_status = branch_status.replace('Production', 'Prod.')
software_branch = branch.get(SOFTWARE_BRANCH_KEY).replace('NVIDIA ', '')
for release in branch.get(JSON_RELEASES_KEY): for release in branch.get(JSON_RELEASES_KEY):
version = release.get(VGPU_KEY, release.get(GRID_KEY, '')) version = release.get(VGPU_KEY, release.get(GRID_KEY, ''))
linux_manager = release.get(LINUX_VGPU_MANAGER_KEY, release.get(ALT_VGPU_MANAGER_KEY, '')) linux_manager = release.get(LINUX_VGPU_MANAGER_KEY, release.get(ALT_VGPU_MANAGER_KEY, ''))
@@ -67,25 +92,13 @@ def __debug():
windows_driver = release.get(WINDOWS_DRIVER_KEY) windows_driver = release.get(WINDOWS_DRIVER_KEY)
release_date = release.get(RELEASE_DATE_KEY) release_date = release.get(RELEASE_DATE_KEY)
is_latest = release.get(VGPU_KEY) == branch.get(LATEST_KEY) is_latest = release.get(VGPU_KEY) == branch.get(LATEST_KEY)
branch_status = __parse_branch_status(release.get(BRANCH_STATUS_KEY, ''))
version = f'{version} *' if is_latest else version version = f'{version} *' if is_latest else version
s = f'{version:<13} | {linux_manager:<21} | {linux_driver:<21} | {windows_manager:<21} | {windows_driver:<21} | {release_date:>21} | {branch_status:^21}' eol = branch.get(EOL_KEY) if is_latest else ''
s = f'{software_branch:^21} | {branch_status:^21} | {version:<13} | {linux_manager:<21} | {linux_driver:<21} | {windows_manager:<21} | {windows_driver:<21} | {release_date:>21} | {eol:>21}'
print(s) print(s)
def __parse_branch_status(string: str) -> str:
string = string.replace('Production Branch', 'Prod. -')
string = string.replace('Long-Term Support Branch', 'LTS -')
string = string.replace('supported until', '')
string = string.replace('EOL since', 'EOL - ')
string = string.replace('EOL from', 'EOL -')
return string
def __dump(filename: str): def __dump(filename: str):
import json import json
@@ -115,6 +128,7 @@ if __name__ == '__main__':
# build matrix # build matrix
__driver_versions(soup) __driver_versions(soup)
__release_branches(soup)
# debug output # debug output
__debug() __debug()

View File

@@ -1,15 +1,13 @@
import json
import sys import sys
from base64 import b64encode as b64enc from base64 import b64encode as b64enc
from calendar import timegm from calendar import timegm
from datetime import datetime, UTC from datetime import datetime, UTC
from hashlib import sha256 from hashlib import sha256
from os.path import dirname, join
from uuid import uuid4, UUID from uuid import uuid4, UUID
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.hashes import SHA256
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from jose import jwt, jwk, jws from jose import jwt, jwk
from jose.constants import ALGORITHMS from jose.constants import ALGORITHMS
from starlette.testclient import TestClient from starlette.testclient import TestClient
@@ -18,28 +16,20 @@ sys.path.append('../')
sys.path.append('../app') sys.path.append('../app')
from app import main from app import main
from util import CASetup, PrivateKey, PublicKey, Cert from util import PrivateKey, PublicKey
client = TestClient(main.app) client = TestClient(main.app)
# Instance
INSTANCE_REF = '10000000-0000-0000-0000-000000000001'
ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-000000000001', 'HelloWorld' ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-000000000001', 'HelloWorld'
# CA & Signing # INSTANCE_KEY_RSA = generate_key()
ca_setup = CASetup(service_instance_ref=INSTANCE_REF) # INSTANCE_KEY_PUB = INSTANCE_KEY_RSA.public_key()
my_root_private_key = PrivateKey.from_file(ca_setup.root_private_key_filename)
my_root_certificate = Cert.from_file(ca_setup.root_certificate_filename)
my_ca_certificate = Cert.from_file(ca_setup.ca_certificate_filename)
my_ca_private_key = PrivateKey.from_file(ca_setup.ca_private_key_filename)
my_si_private_key = PrivateKey.from_file(ca_setup.si_private_key_filename)
my_si_private_key_as_pem = my_si_private_key.pem()
my_si_public_key = my_si_private_key.public_key()
my_si_public_key_as_pem = my_si_private_key.public_key().pem()
my_si_certificate = Cert.from_file(ca_setup.si_certificate_filename)
jwt_encode_key = jwk.construct(my_si_private_key_as_pem, algorithm=ALGORITHMS.RS256) INSTANCE_KEY_RSA = PrivateKey.from_file(str(join(dirname(__file__), '../app/cert/instance.private.pem')))
jwt_decode_key = jwk.construct(my_si_public_key_as_pem, algorithm=ALGORITHMS.RS256) INSTANCE_KEY_PUB = PublicKey.from_file(str(join(dirname(__file__), '../app/cert/instance.public.pem')))
jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.pem(), 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:
@@ -48,48 +38,6 @@ def __bearer_token(origin_ref: str) -> str:
return token return token
def test_signing():
signature_set_header = my_si_private_key.generate_signature(b'Hello')
# test plain
my_si_public_key.verify_signature(signature_set_header, b'Hello')
# test "X-NLS-Signature: b'....'
x_nls_signature_header_value = f'{signature_set_header.hex().encode()}'
assert f'{x_nls_signature_header_value}'.startswith('b\'')
assert f'{x_nls_signature_header_value}'.endswith('\'')
# test eval
signature_get_header = eval(x_nls_signature_header_value)
signature_get_header = bytes.fromhex(signature_get_header.decode('ascii'))
my_si_public_key.verify_signature(signature_get_header, b'Hello')
def test_keypair_and_certificates():
assert my_root_certificate.public_key().mod() == my_root_private_key.public_key().mod()
assert my_ca_certificate.public_key().mod() == my_ca_private_key.public_key().mod()
assert my_si_certificate.public_key().mod() == my_si_public_key.mod()
assert len(my_root_certificate.public_key().mod()) == 1024
assert len(my_ca_certificate.public_key().mod()) == 1024
assert len(my_si_certificate.public_key().mod()) == 512
#assert my_si_certificate.public_key().mod() != my_si_public_key.mod()
my_root_certificate.public_key().raw().verify(
my_ca_certificate.raw().signature,
my_ca_certificate.raw().tbs_certificate_bytes,
PKCS1v15(),
SHA256(),
)
my_ca_certificate.public_key().raw().verify(
my_si_certificate.raw().signature,
my_si_certificate.raw().tbs_certificate_bytes,
PKCS1v15(),
SHA256(),
)
def test_index(): def test_index():
response = client.get('/') response = client.get('/')
assert response.status_code == 200 assert response.status_code == 200
@@ -106,19 +54,13 @@ def test_config():
assert response.status_code == 200 assert response.status_code == 200
def test_config_root_ca():
response = client.get('/-/config/root-certificate')
assert response.status_code == 200
assert response.content.decode('utf-8').strip() == my_root_certificate.pem().decode('utf-8').strip()
def test_readme(): def test_readme():
response = client.get('/-/readme') response = client.get('/-/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
@@ -127,41 +69,6 @@ def test_client_token():
assert response.status_code == 200 assert response.status_code == 200
def test_config_token():
# https://git.collinwebdesigns.de/nvidia/nls/-/blob/main/src/test/test_config_token.py
response = client.post('/leasing/v1/config-token', json={"service_instance_ref": INSTANCE_REF})
assert response.status_code == 200
nv_response_certificate_configuration = response.json().get('certificateConfiguration')
nv_ca_chain = nv_response_certificate_configuration.get('caChain')[0].encode('utf-8')
nv_ca_chain = Cert(nv_ca_chain)
nv_response_public_cert = nv_response_certificate_configuration.get('publicCert').encode('utf-8')
nv_response_public_key = nv_response_certificate_configuration.get('publicKey')
nv_si_certificate = Cert(nv_response_public_cert)
assert nv_si_certificate.public_key().mod() == nv_response_public_key.get('mod')[0]
assert nv_si_certificate.authority_key_identifier() == nv_ca_chain.subject_key_identifier()
nv_jwt_decode_key = jwk.construct(nv_response_public_cert, algorithm=ALGORITHMS.RS256)
nv_response_config_token = response.json().get('configToken')
payload = jws.verify(nv_response_config_token, key=nv_jwt_decode_key, algorithms=ALGORITHMS.RS256)
payload = json.loads(payload)
assert payload.get('iss') == 'NLS Service Instance'
assert payload.get('aud') == 'NLS Licensed Client'
assert payload.get('service_instance_ref') == INSTANCE_REF
nv_si_public_key_configuration = payload.get('service_instance_public_key_configuration')
nv_si_public_key_me = nv_si_public_key_configuration.get('service_instance_public_key_me')
assert len(nv_si_public_key_me.get('mod')) == 512 # nv_si_public_key_mod
assert nv_si_public_key_me.get('exp') == 65537 # nv_si_public_key_exp
def test_origins(): def test_origins():
pass pass
@@ -261,13 +168,12 @@ def test_auth_v1_token():
def test_leasing_v1_lessor(): def test_leasing_v1_lessor():
payload = { payload = {
'client_challenge': 'my_unique_string',
'fulfillment_context': { 'fulfillment_context': {
'fulfillment_class_ref_list': [] 'fulfillment_class_ref_list': []
}, },
'lease_proposal_list': [{ 'lease_proposal_list': [{
'license_type_qualifiers': {'count': 1}, 'license_type_qualifiers': {'count': 1},
'product': {'name': 'NVIDIA Virtual Applications'} 'product': {'name': 'NVIDIA RTX Virtual Workstation'}
}], }],
'proposal_evaluation_mode': 'ALL_OF', 'proposal_evaluation_mode': 'ALL_OF',
'scope_ref_list': [ALLOTMENT_REF] 'scope_ref_list': [ALLOTMENT_REF]
@@ -276,21 +182,10 @@ def test_leasing_v1_lessor():
response = client.post('/leasing/v1/lessor', json=payload, headers={'authorization': __bearer_token(ORIGIN_REF)}) response = client.post('/leasing/v1/lessor', json=payload, headers={'authorization': __bearer_token(ORIGIN_REF)})
assert response.status_code == 200 assert response.status_code == 200
client_challenge = response.json().get('client_challenge')
assert client_challenge == payload.get('client_challenge')
signature = eval(response.headers.get('X-NLS-Signature'))
assert len(signature) == 512
signature = bytes.fromhex(signature.decode('ascii'))
assert len(signature) == 256
my_si_public_key.verify_signature(signature, response.content)
lease_result_list = response.json().get('lease_result_list') lease_result_list = response.json().get('lease_result_list')
assert len(lease_result_list) == 1 assert len(lease_result_list) == 1
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']
assert lease_result_list[0]['lease']['product_name'] == 'NVIDIA Virtual Applications'
assert lease_result_list[0]['lease']['feature_name'] == 'GRID-Virtual-Apps'
def test_leasing_v1_lessor_lease(): def test_leasing_v1_lessor_lease():
@@ -310,18 +205,9 @@ def test_leasing_v1_lease_renew():
### ###
payload = {'client_challenge': 'my_unique_string'} response = client.put(f'/leasing/v1/lease/{active_lease_ref}', headers={'authorization': __bearer_token(ORIGIN_REF)})
response = client.put(f'/leasing/v1/lease/{active_lease_ref}', json=payload, headers={'authorization': __bearer_token(ORIGIN_REF)})
assert response.status_code == 200 assert response.status_code == 200
client_challenge = response.json().get('client_challenge')
assert client_challenge == payload.get('client_challenge')
signature = eval(response.headers.get('X-NLS-Signature'))
assert len(signature) == 512
signature = bytes.fromhex(signature.decode('ascii'))
assert len(signature) == 256
my_si_public_key.verify_signature(signature, response.content)
lease_ref = response.json().get('lease_ref') lease_ref = response.json().get('lease_ref')
assert len(lease_ref) == 36 assert len(lease_ref) == 36
assert lease_ref == active_lease_ref assert lease_ref == active_lease_ref
@@ -350,7 +236,7 @@ def test_leasing_v1_lessor_lease_remove():
}, },
'lease_proposal_list': [{ 'lease_proposal_list': [{
'license_type_qualifiers': {'count': 1}, 'license_type_qualifiers': {'count': 1},
'product': {'name': 'NVIDIA Virtual Applications'} 'product': {'name': 'NVIDIA RTX Virtual Workstation'}
}], }],
'proposal_evaluation_mode': 'ALL_OF', 'proposal_evaluation_mode': 'ALL_OF',
'scope_ref_list': [ALLOTMENT_REF] 'scope_ref_list': [ALLOTMENT_REF]