71 Commits

Author SHA1 Message Date
Oscar Krause
5973e75327 fixed imports 2025-07-07 08:13:17 +02:00
Oscar Krause
42008ff66f Merge branch 'main' into drivers-dir
# Conflicts:
#	app/main.py
2025-07-07 08:07:09 +02:00
Oscar Krause
0c5368b863 styling 2025-07-04 10:12:26 +02:00
Oscar Krause
5c54f5171f styling 2025-07-04 10:12:07 +02:00
Oscar Krause
2afd3a4eb3 requirements.txt updated 2025-07-03 09:03:53 +02:00
Oscar Krause
b5e186a58e added vgpu 18.3 2025-07-03 08:55:00 +02:00
Oscar Krause
3ebf87f01a added vgpu 18.2 2025-06-02 07:03:06 +02:00
Oscar Krause
b8ec5e11e2 updated urls 2025-05-21 10:10:49 +02:00
Oscar Krause
bffa357225 typos 2025-05-21 07:19:50 +02:00
Oscar Krause
656d568cbe removed gitea links 2025-05-21 06:35:53 +02:00
Oscar Krause
82c8e89d8f fixes 2025-05-19 12:05:23 +02:00
Oscar Krause
2ee92b6f81 added "Nginx Reverse Proxy (experimental)" section 2025-05-19 12:05:00 +02:00
Oscar Krause
52a1834817 requirements.txt updated 2025-05-15 07:32:44 +02:00
Oscar Krause
dd0042b850 updated roadmap 2025-05-15 07:32:44 +02:00
Oscar Krause
eded286c89 improved debian installation 2025-05-15 07:32:44 +02:00
Oscar Krause
764e012fb7 fixed cert_path_prefix 2025-05-13 17:53:23 +02:00
Oscar Krause
52e9f2cae9 ci fixes 2025-05-13 09:53:15 +02:00
Oscar Krause
09fe2a605c requirements.txt updated 2025-05-13 09:51:07 +02:00
Oscar Krause
8ec87a8859 updated compatibility 2025-05-13 09:51:02 +02:00
Oscar Krause
18f577b4f6 ci fixes 2025-05-13 09:48:30 +02:00
Oscar Krause
eab3b21d60 ci fixes 2025-04-30 14:24:54 +02:00
Oscar Krause
2157394dfa fixed debian dependency 2025-04-30 14:20:09 +02:00
Oscar Krause
b577edcf67 release registry url updated 2025-04-23 22:19:27 +02:00
Oscar Krause
8e3e49fc36 ci fixes 2025-04-23 22:02:04 +02:00
Oscar Krause
2638f67cd9 typos 2025-04-23 21:55:45 +02:00
Oscar Krause
60a6c83a3d Merge branch 'v18.x-support' into 'main'
v18.x support / NLS 3.4.x compatibility

See merge request oscar.krause/fastapi-dls!46
2025-04-23 21:54:43 +02:00
Oscar Krause
ae03867502 updated compatibility matrix 2025-04-23 10:35:07 +02:00
Oscar Krause
295e3c9482 fixes 2025-04-22 20:42:11 +02:00
Oscar Krause
3961acf231 fixes 2025-04-22 15:07:29 +02:00
Oscar Krause
04914740a4 improved tests 2025-04-22 14:38:17 +02:00
Oscar Krause
6af9cd04c9 added variable for custom cert path 2025-04-22 14:38:05 +02:00
Oscar Krause
29268b1658 code styling and tests 2025-04-22 14:16:30 +02:00
Oscar Krause
938a112b8a fixes 2025-04-22 14:00:56 +02:00
Oscar Krause
16870e9d67 code styling 2025-04-22 12:35:34 +02:00
Oscar Krause
55b7437fe7 fixes 2025-04-22 11:29:31 +02:00
Oscar Krause
e7e007a45f fixes 2025-04-22 11:22:57 +02:00
Oscar Krause
161a1430cf code styling 2025-04-22 11:06:54 +02:00
Oscar Krause
1ccb203b25 code styling 2025-04-22 11:05:03 +02:00
Oscar Krause
6c1a8d42dc fixes 2025-04-22 11:04:32 +02:00
Oscar Krause
d248496f34 fixes 2025-04-22 10:45:41 +02:00
Oscar Krause
fd1babaca5 fixes 2025-04-22 10:10:32 +02:00
Oscar Krause
cd9c655d65 fixes 2025-04-22 09:45:58 +02:00
Oscar Krause
6ed4bdfe6f fixes 2025-04-22 09:05:18 +02:00
Oscar Krause
e1ae757a50 updated tests 2025-04-22 08:53:30 +02:00
Oscar Krause
b0ca5d7ab5 fixes 2025-04-22 08:06:17 +02:00
Oscar Krause
14f8b54752 test with "76 chars per line" 2025-04-22 07:32:30 +02:00
Oscar Krause
dc783e6518 typos 2025-04-22 07:32:05 +02:00
Oscar Krause
3666e22707 fixes 2025-04-21 21:57:40 +02:00
Oscar Krause
6b54d4794b .gitlab-ci.yml bearbeiten 2025-04-16 15:07:28 +02:00
Oscar Krause
f38378bbc8 updated credits 2025-04-16 15:05:45 +02:00
Oscar Krause
cd4c3d379a code styling 2025-04-16 15:01:14 +02:00
Oscar Krause
59645d1daf Merge branch 'main' into v18.x-support
# Conflicts:
#	app/util.py
2025-04-16 15:00:33 +02:00
Oscar Krause
9605ba3eee Merge branch 'dev' into 'main'
Dev

See merge request oscar.krause/fastapi-dls!51
2025-04-16 14:43:58 +02:00
Oscar Krause
cd5c2a6cb1 code styling 2025-04-16 14:18:04 +02:00
Oscar Krause
1d3255188e code styling 2025-04-16 14:12:40 +02:00
Oscar Krause
cea7a01b54 code styling 2025-04-16 14:12:15 +02:00
Oscar Krause
ff2fbaf83f code styling 2025-04-16 14:11:04 +02:00
Oscar Krause
9f417b61a9 fixes 2025-04-16 14:07:18 +02:00
Oscar Krause
4f77200628 code styling 2025-04-16 14:07:18 +02:00
Oscar Krause
20b03446dc Merge branch 'main' into 'dev'
# Conflicts:
#   .gitlab-ci.yml
2025-04-16 13:57:51 +02:00
Oscar Krause
df506e8591 removed unsupported python versions 2025-04-16 13:56:36 +02:00
Oscar Krause
3fe3429986 added some python versions and added EOL 2025-04-16 12:54:24 +02:00
Oscar Krause
a996504c50 test python3.13 and 3.11 2025-04-16 12:44:58 +02:00
Oscar Krause
5d2bff88d8 fixes 2025-04-16 12:35:24 +02:00
Oscar Krause
67f2d18a95 requirements.txt updated 2025-04-16 12:25:37 +02:00
Oscar Krause
52cd34cb5c ci improvements 2025-04-16 12:24:19 +02:00
Oscar Krause
6fb03309a5 ci improvements 2025-04-16 12:23:21 +02:00
Oscar Krause
477e5abbca refactored test pipeline to test different python versions 2025-04-16 12:12:23 +02:00
Oscar Krause
e2c4e45764 Datei .gitlab-ci.yml aktualisieren 2025-04-14 20:45:12 +02:00
Oscar Krause
a8c1cdf095 updated create_driver_matrix_json.py 2025-04-11 14:10:26 +02:00
Oscar Krause
c6ca1cafb9 serve drivers directly via api if configured 2025-04-10 09:02:41 +02:00
12 changed files with 433 additions and 174 deletions

View File

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

View File

@@ -1,6 +1,9 @@
# Toggle debug mode # Toggle debug mode
#DEBUG=false #DEBUG=false
# Cert Path
CERT_PATH="/etc/fastapi-dls/cert"
# Where the client can find the DLS server # Where the client can find the DLS server
DLS_URL=127.0.0.1 DLS_URL=127.0.0.1
DLS_PORT=443 DLS_PORT=443

View File

@@ -3,6 +3,8 @@
WORKING_DIR=/usr/share/fastapi-dls WORKING_DIR=/usr/share/fastapi-dls
CONFIG_DIR=/etc/fastapi-dls CONFIG_DIR=/etc/fastapi-dls
source $CONFIG_DIR/env
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"
@@ -25,27 +27,32 @@ if [ -f $CONFIG_DIR/webserver.key ]; then
if [ -x "$(command -v curl)" ]; then if [ -x "$(command -v curl)" ]; then
echo "> Testing API ..." echo "> Testing API ..."
source $CONFIG_DIR/env
curl --insecure -X GET https://$DLS_URL:$DLS_PORT/-/health curl --insecure -X GET https://$DLS_URL:$DLS_PORT/-/health
else else
echo "> Testing API failed, curl not available. Please test manually!" echo "> Testing API failed, curl not available. Please test manually!"
fi fi
fi fi
echo "> Create Certificate-Chain folder ..."
mkdir -p $CERT_PATH
echo "> Set permissions ..."
chown -R www-data:www-data $CONFIG_DIR chown -R www-data:www-data $CONFIG_DIR
chown -R www-data:www-data $WORKING_DIR chown -R www-data:www-data $WORKING_DIR
echo "> Done."
cat <<EOF cat <<EOF
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # #
# fastapi-dls is now installed. # # fastapi-dls is now installed. #
# # # #
# Service should be up and running. # # Service should be up and running (if you choose to auto-generate #
# Webservice is listen to https://localhost # # self-signed webserver certificate). #
# #
# Configuration is stored in /etc/fastapi-dls/env. #
# # # #
# - Webservice is listen to https://localhost # #
# - Configuration is stored in /etc/fastapi-dls/env #
# # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

View File

@@ -41,11 +41,10 @@ build:apt:
interruptible: true interruptible: true
stage: build stage: build
rules: rules:
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- 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' - if: ($CI_PIPELINE_SOURCE == 'merge_request_event') || ($CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH)
changes: changes:
- app/**/* - app/**/*
- .DEBIAN/**/* - .DEBIAN/**/*
@@ -89,11 +88,10 @@ build:pacman:
interruptible: true interruptible: true
stage: build stage: build
rules: rules:
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- 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' - if: ($CI_PIPELINE_SOURCE == 'merge_request_event') || ($CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH)
changes: changes:
- app/**/* - app/**/*
- .PKGBUILD/**/* - .PKGBUILD/**/*
@@ -122,13 +120,12 @@ build:pacman:
paths: paths:
- "*.pkg.tar.zst" - "*.pkg.tar.zst"
test: test:python:
image: python:3.12-slim-bookworm image: $IMAGE
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:
@@ -138,17 +135,20 @@ test:
DATABASE: sqlite:///../app/db.sqlite DATABASE: sqlite:///../app/db.sqlite
parallel: parallel:
matrix: matrix:
- REQUIREMENTS: - IMAGE:
- 'requirements.txt' # https://devguide.python.org/versions/#supported-versions
# - '.DEBIAN/requirements-bookworm-12.txt' # - python:3.14-rc-alpine # EOL 2030-10 => uvicorn does not support 3.14 yet
# - '.DEBIAN/requirements-ubuntu-24.04.txt' - python:3.13-alpine # EOL 2029-10
# - '.DEBIAN/requirements-ubuntu-24.10.txt' - python:3.12-alpine # EOL 2028-10
- 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:
- apt-get update && apt-get install -y python3-dev python3-pip python3-venv gcc - apk --no-cache add openssl
- 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 - 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
- cd test - cd test
@@ -156,18 +156,28 @@ test:
- 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_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: ($CI_PIPELINE_SOURCE == 'merge_request_event') || ($CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH)
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
changes: changes:
- app/**/* - app/**/*
- .DEBIAN/**/* - .DEBIAN/**/*
- .gitlab-ci.yml - .gitlab-ci.yml
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
@@ -199,21 +209,10 @@ test:
- 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_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: ($CI_PIPELINE_SOURCE == 'merge_request_event') || ($CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH)
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
changes: changes:
- app/**/* - app/**/*
- .PKGBUILD/**/* - .PKGBUILD/**/*
@@ -290,15 +289,12 @@ 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
@@ -317,9 +313,10 @@ 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
@@ -356,9 +353,10 @@ 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
@@ -379,7 +377,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: [ test ] needs: [ deploy:docker, deploy:apt, deploy:pacman ]
rules: rules:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
script: script:
@@ -394,4 +392,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/40' url: 'https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/container_registry/70'

188
README.md
View File

@@ -2,15 +2,15 @@
Minimal Delegated License Service (DLS). Minimal Delegated License Service (DLS).
> [!warning] Branch support \ > [!warning] Branch support
> FastAPI-DLS Version 1.x supports up to **`17.x`** releases. \ > 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 > 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). > with [gridd-unlock-patcher](https://git.collinwebdesigns.de/vgpu/gridd-unlock-patcher).
> Other combinations of FastAPI-DLS and Driver-Branches may work but are not tested. > Other combinations of FastAPI-DLS and Driver-Branches may work but are not tested.
> [!note] Compatibility > [!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)**.
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.
@@ -18,7 +18,6 @@ Only the clients need a connection to this service on configured port.
**Official Links** **Official Links**
* https://git.collinwebdesigns.de/oscar.krause/fastapi-dls (Private Git) * https://git.collinwebdesigns.de/oscar.krause/fastapi-dls (Private Git)
* https://gitea.publichub.eu/oscar.krause/fastapi-dls (Public Git)
* https://hub.docker.com/r/collinwebdesigns/fastapi-dls (Docker-Hub `collinwebdesigns/fastapi-dls:latest`) * https://hub.docker.com/r/collinwebdesigns/fastapi-dls (Docker-Hub `collinwebdesigns/fastapi-dls:latest`)
*All other repositories are forks! (which is no bad - just for information and bug reports)* *All other repositories are forks! (which is no bad - just for information and bug reports)*
@@ -334,12 +333,13 @@ 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**
@@ -415,13 +415,148 @@ acme.sh --issue -d example.com \
After first success you have to replace `--issue` with `--renew`. After first success you have to replace `--issue` with `--renew`.
# Configuration ## Nginx Reverse Proxy (experimental)
- This guide is written for Debian/Ubuntu systems, other may work, but you have to do your setup on your own
- Uvicorn does no longer serve requests directly
- NGINX is used as HTTP & HTTPS entrypoint
- Assumes you already have set up webserver certificate and private-key
**Install Nginx Webserver**
```shell
apt-get install nginx-light
```
**Remove default vhost**
```shell
rm /etc/nginx/sites-enabled/default
```
**Create fastapi-dls vhost**
<details>
<summary>`/etc/nginx/sites-available/fastapi-dls`</summary>
```
upstream dls-backend {
server 127.0.0.1:8000; # must match dls listen port
}
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
root /var/www/html;
index index.html;
server_name _;
ssl_certificate "/etc/fastapi-dls/cert/webserver.crt";
ssl_certificate_key "/etc/fastapi-dls/cert/webserver.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_protocols TLSv1.3 TLSv1.2;
# ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305";
# ssl_ciphers PROFILE=SYSTEM;
ssl_prefer_server_ciphers on;
location / {
# https://www.uvicorn.org/deployment/
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_redirect off;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://dls-backend$request_uri;
}
location = /-/health {
access_log off;
add_header 'Content-Type' 'application/json';
return 200 '{\"status\":\"up\",\"service\":\"nginx\"}';
}
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
root /var/www/html;
index index.html;
server_name _;
location /leasing/v1/lessor/shutdown {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://dls-backend/leasing/v1/lessor/shutdown;
}
location / {
return 301 https://$host$request_uri;
}
}
```
</details>
**Enable and test vhost**
```shell
ln -s /etc/nginx/sites-available/fastapi-dls /etc/nginx/sites-enabled/fastapi-dls
nginx -t
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful
```
**Override default fastapi-dls systemd service**
```shell
mkdir /etc/systemd/system/fastapi-dls.service.d
```
<details>
<summary>`/etc/systemd/system/fastapi-dls.service.d/override.conf`</summary>
```
[Service]
ExecStart=
ExecStart=uvicorn main:app \
--env-file /etc/fastapi-dls/env \
--host 127.0.0.1 --port 8000 \
--app-dir /usr/share/fastapi-dls/app \
--proxy-headers
```
</details>
**Run**
```shell
systemctl daemon-reload
service nginx start
service fastapi-dls start
```
# Configuration (Service)
| 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 |
@@ -429,7 +564,7 @@ After first success you have to replace `--issue` with `--renew`.
| `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 |
\*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
@@ -535,9 +670,9 @@ Status endpoint, used for *healthcheck*.
Shows current runtime environment variables and their values. Shows current runtime environment variables and their values.
**`GET /-/config/root-ca`** **`GET /-/config/root-certificate`**
Returns the Root-CA Certificate which is used. This is required for patching `nvidia-gridd` on 18.x releases. Returns the Root-Certificate Certificate which is used. This is required for patching `nvidia-gridd` on 18.x releases.
**`GET /-/readme`** **`GET /-/readme`**
@@ -748,20 +883,22 @@ The error message can safely be ignored (since we have no license limitation :P)
# vGPU Software Compatibility Matrix # vGPU Software Compatibility Matrix
<details> <details>
<summary>Show Table</summary> <summary>Successfully tested with this package versions: Show Table</summary>
Successfully tested with this package versions.
| FastAPI-DLS Version | vGPU Suftware | Driver Branch | Linux vGPU Manager | Linux Driver | Windows Driver | Release Date | EOL Date | | FastAPI-DLS Version | vGPU Suftware | Driver Branch | Linux vGPU Manager | Linux Driver | Windows Driver | Release Date | EOL Date |
|---------------------|:-------------:|:-------------:|--------------------|--------------|----------------|--------------:|--------------:| |---------------------|:-------------:|:-------------:|--------------------|--------------|----------------|--------------:|--------------:|
| `2.x` | `18.0` | **R570** | `570.124.03` | `570.124.06` | `572.60` | March 2025 | March 2026 | | `2.x` | `18.3` | **R570** | `570.158.02` | `570.158.01` | `573.36` | June 2025 | March 2026 |
| `1.x` & `2.x` | `17.5` | | `550.144.02` | `550.144.03` | `553.62` | January 2025 | June 2025 | | | `18.2` | **R570** | `570.148.06` | `570.148.08` | `573.07` | May 2025 | |
| | `18.1` | **R570** | `570.133.08` | `570.133.07` | `572.83` | April 2025 | |
| | `18.0` | **R570** | `570.124.03` | `570.124.06` | `572.60` | March 2025 | |
| `1.x` & `2.x` | `17.6` | **R550** | `550.163.02` | `550.63.01` | `553.74` | April 2025 | June 2025 |
| | `17.5` | | `550.144.02` | `550.144.03` | `553.62` | January 2025 | |
| | `17.4` | | `550.127.06` | `550.127.05` | `553.24` | October 2024 | | | | `17.4` | | `550.127.06` | `550.127.05` | `553.24` | October 2024 | |
| | `17.3` | | `550.90.05` | `550.90.07` | `552.74` | July 2024 | | | | `17.3` | | `550.90.05` | `550.90.07` | `552.74` | July 2024 | |
| | `17.2` | | `550.90.05` | `550.90.07` | `552.55` | June 2024 | | | | `17.2` | | `550.90.05` | `550.90.07` | `552.55` | June 2024 | |
| | `17.1` | | `550.54.16` | `550.54.15` | `551.78` | March 2024 | | | | `17.1` | | `550.54.16` | `550.54.15` | `551.78` | March 2024 | |
| | `17.0` | **R550** | `550.54.10` | `550.54.14` | `551.61` | February 2024 | | | | `17.0` | **R550** | `550.54.10` | `550.54.14` | `551.61` | February 2024 | |
| `1.x` | `16.9` | **R535** | `535.230.02` | `535.216.01` | `539.19` | October 2024 | July 2026 | | `1.x` | `16.10` | **R535** | `535.247.02` | `535.247.01` | `539.28` | April 2025 | July 2026 |
| `1.x` | `15.4` | **R525** | `525.147.01` | `525.147.05` | `529.19` | June 2023 | December 2023 | | `1.x` | `15.4` | **R525** | `525.147.01` | `525.147.05` | `529.19` | June 2023 | December 2023 |
| `1.x` | `14.4` | **R510** | `510.108.03` | `510.108.03` | `514.08` | December 2022 | February 2023 | | `1.x` | `14.4` | **R510** | `510.108.03` | `510.108.03` | `514.08` | December 2022 | February 2023 |
@@ -779,13 +916,14 @@ 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/vgpu/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

@@ -2,6 +2,17 @@
I am planning to implement the following features in the future. I am planning to implement the following features in the future.
## Patching Endpoint
A (optional) Path-Variable to `gridd-unlock-patcher` which enables an additional endpoint.
Here you can upload your `nvidia-gridd` binary or `nvxdapix.dll` which then will be patched and responded.
## All-In-One Installer Script Endpoint
A new all-in-one installer endpoint
(here a script is returned for linux or windows which then could be called like
curl https://<fastapi-dls>/-/install/deb | sh which then
download and place a client-token in the right directory, patch your girdd / dll and restart nvidia-gridd service)
## HA - High Availability ## HA - High Availability

View File

@@ -6,7 +6,8 @@ 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, dumps as json_dumps
from os import getenv as env from os import getenv as env
from os.path import join, dirname from os.path import join, dirname, exists, isdir, isfile
from textwrap import wrap
from uuid import uuid4 from uuid import uuid4
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@@ -19,6 +20,7 @@ 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.staticfiles import StaticFiles
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 CASetup, PrivateKey, Cert, ProductMapping, load_file
@@ -39,6 +41,7 @@ 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'))
@@ -48,11 +51,14 @@ 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}']
DRIVERS_DIR = env('DRIVERS_DIR', None)
DT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' DT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
PRODUCT_MAPPING = ProductMapping(filename=join(dirname(__file__), 'static/product_mapping.json')) PRODUCT_MAPPING = ProductMapping(filename=join(dirname(__file__), 'static/product_mapping.json'))
# Create certificate chain and signing keys # Create certificate chain and signing keys
ca_setup = CASetup(service_instance_ref=INSTANCE_REF) ca_setup = CASetup(service_instance_ref=INSTANCE_REF, cert_path=CERT_PATH)
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_root_certificate = Cert.from_file(ca_setup.root_certificate_filename)
my_ca_certificate = Cert.from_file(ca_setup.ca_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_certificate = Cert.from_file(ca_setup.si_certificate_filename)
@@ -96,6 +102,9 @@ 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)
if DRIVERS_DIR is not None:
app.mount('/-/static-drivers', StaticFiles(directory=str(DRIVERS_DIR), html=False), name='drivers')
app.debug = DEBUG app.debug = DEBUG
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@@ -151,10 +160,9 @@ async def _config():
return Response(content=json_dumps(response), media_type='application/json', status_code=200) return Response(content=json_dumps(response), media_type='application/json', status_code=200)
@app.get('/-/config/root-certificate', summary='* Root Certificate', description='returns Root--Certificate needed for patching nvidia-gridd')
@app.get('/-/config/root-ca', summary='* Root CA', description='returns Root-CA needed for patching nvidia-gridd')
async def _config(): async def _config():
return Response(content=my_root_certificate.pem().decode('utf-8'), media_type='text/plain') return Response(content=my_root_certificate.pem().decode('utf-8').strip(), media_type='text/plain')
@app.get('/-/readme', summary='* Readme') @app.get('/-/readme', summary='* Readme')
@@ -203,6 +211,25 @@ async def _manage(request: Request):
return Response(response, media_type='text/html', status_code=200) return Response(response, media_type='text/html', status_code=200)
@app.get('/-/drivers/{directory:path}', summary='* List drivers directory')
async def _drivers(request: Request, directory: str | None):
if DRIVERS_DIR is None:
return Response(status_code=404, content=f'Variable "DRIVERS_DIR" not set.')
path = join(DRIVERS_DIR, directory)
if not exists(path) and not isfile(path):
return Response(status_code=404, content=f'Resource "{path}" not found!')
content = [{
"type": "file" if isfile(f'{path}/{_}') else "folder" if isdir(f'{path}/{_}') else "unknown",
"name": _,
"link": f'/-/static-drivers/{directory}{_}',
} for _ in listdir(path)]
return Response(content=json_dumps({"directory": path, "content": content}), media_type='application/json', status_code=200)
@app.get('/-/origins', summary='* Origins') @app.get('/-/origins', summary='* Origins')
async def _origins(request: Request, leases: bool = False): async def _origins(request: Request, leases: bool = False):
session = sessionmaker(bind=db)() session = sessionmaker(bind=db)()
@@ -287,7 +314,7 @@ async def _client_token():
"mod": my_si_public_key.mod(), "mod": my_si_public_key.mod(),
"exp": my_si_public_key.exp(), "exp": my_si_public_key.exp(),
}, },
"service_instance_public_key_pem": my_si_private_key.public_key().pem().decode('utf-8'), "service_instance_public_key_pem": my_si_public_key.pem().decode('utf-8').strip(),
"key_retention_mode": "LATEST_ONLY" "key_retention_mode": "LATEST_ONLY"
}, },
} }
@@ -462,8 +489,7 @@ async def leasing_v1_config_token(request: Request):
"mod": my_si_public_key.mod(), "mod": my_si_public_key.mod(),
"exp": my_si_public_key.exp(), "exp": my_si_public_key.exp(),
}, },
# 64 chars per line (pem default) "service_instance_public_key_pem": my_si_public_key.pem().decode('utf-8').strip(),
"service_instance_public_key_pem": my_si_private_key.public_key().pem().decode('utf-8').strip(),
"key_retention_mode": "LATEST_ONLY" "key_retention_mode": "LATEST_ONLY"
}, },
} }
@@ -471,18 +497,37 @@ async def leasing_v1_config_token(request: Request):
my_jwt_encode_key = jwk.construct(my_si_private_key.pem().decode('utf-8'), algorithm=ALGORITHMS.RS256) 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) 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') response_ca_chain = my_ca_certificate.pem().decode('utf-8').strip()
response_si_certificate = my_si_certificate.pem().decode('utf-8')
# 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 = { response = {
"certificateConfiguration": { "certificateConfiguration": {
# 76 chars per line
"caChain": [response_ca_chain], "caChain": [response_ca_chain],
# 76 chars per line
"publicCert": response_si_certificate, "publicCert": response_si_certificate,
"publicKey": { "publicKey": {
"exp": int(my_si_certificate.raw().public_key().public_numbers().e), "exp": my_si_certificate.public_key().exp(),
"mod": [hex(my_si_certificate.raw().public_key().public_numbers().n)[2:]], "mod": [my_si_certificate.public_key().mod()],
}, },
}, },
"configToken": config_token, "configToken": config_token,

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 NV from util import DriverMatrix
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:
_ = NV().find(self.guest_driver_version) _ = DriverMatrix().find(self.guest_driver_version)
return { return {
'origin_ref': self.origin_ref, 'origin_ref': self.origin_ref,

View File

@@ -1,7 +1,7 @@
import logging import logging
from datetime import datetime, UTC, timedelta from datetime import datetime, UTC, timedelta
from json import loads as json_loads from json import loads as json_loads
from os.path import join, dirname, isfile from os.path import join, dirname, isfile, isdir
from cryptography import x509 from cryptography import x509
from cryptography.hazmat._oid import NameOID from cryptography.hazmat._oid import NameOID
@@ -16,6 +16,14 @@ 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: class CASetup:
### ###
# #
@@ -30,14 +38,18 @@ class CASetup:
SI_PRIVATE_KEY_FILENAME = 'si_private_key.pem' SI_PRIVATE_KEY_FILENAME = 'si_private_key.pem'
SI_CERTIFICATE_FILENAME = 'si_certificate.pem' SI_CERTIFICATE_FILENAME = 'si_certificate.pem'
def __init__(self, service_instance_ref: str): 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.service_instance_ref = service_instance_ref
self.root_private_key_filename = join(dirname(__file__), 'cert', CASetup.ROOT_PRIVATE_KEY_FILENAME) 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.root_certificate_filename = join(cert_path_prefix, CASetup.ROOT_CERTIFICATE_FILENAME)
self.ca_private_key_filename = join(dirname(__file__), 'cert', CASetup.CA_PRIVATE_KEY_FILENAME) self.ca_private_key_filename = join(cert_path_prefix, CASetup.CA_PRIVATE_KEY_FILENAME)
self.ca_certificate_filename = join(dirname(__file__), 'cert', CASetup.CA_CERTIFICATE_FILENAME) self.ca_certificate_filename = join(cert_path_prefix, CASetup.CA_CERTIFICATE_FILENAME)
self.si_private_key_filename = join(dirname(__file__), 'cert', CASetup.SI_PRIVATE_KEY_FILENAME) self.si_private_key_filename = join(cert_path_prefix, CASetup.SI_PRIVATE_KEY_FILENAME)
self.si_certificate_filename = join(dirname(__file__), 'cert', CASetup.SI_CERTIFICATE_FILENAME) self.si_certificate_filename = join(cert_path_prefix, CASetup.SI_CERTIFICATE_FILENAME)
if not (isfile(self.root_private_key_filename) if not (isfile(self.root_private_key_filename)
and isfile(self.root_certificate_filename) and isfile(self.root_certificate_filename)
@@ -73,7 +85,20 @@ class CASetup:
.not_valid_before(datetime.now(tz=UTC) - timedelta(days=1)) .not_valid_before(datetime.now(tz=UTC) - timedelta(days=1))
.not_valid_after(datetime.now(tz=UTC) + timedelta(days=365 * 10)) .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.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.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())) .sign(my_root_private_key, hashes.SHA256()))
my_root_private_key_as_pem = my_root_private_key.private_bytes( my_root_private_key_as_pem = my_root_private_key.private_bytes(
@@ -126,7 +151,6 @@ class CASetup:
critical=True critical=True
) )
.add_extension(x509.SubjectKeyIdentifier.from_public_key(my_ca_public_key), critical=False) .add_extension(x509.SubjectKeyIdentifier.from_public_key(my_ca_public_key), critical=False)
# .add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(my_root_public_key), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( .add_extension(x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
my_root_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value my_root_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
), critical=False) ), critical=False)
@@ -236,7 +260,7 @@ class PrivateKey:
return PublicKey(data=data) return PublicKey(data=data)
def generate_signature(self, data: bytes) -> bytes: def generate_signature(self, data: bytes) -> bytes:
return self.__key.sign(data, padding=PKCS1v15(), algorithm=SHA256()) 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":
@@ -281,8 +305,8 @@ class PublicKey:
def exp(self): def exp(self):
return int(self.__key.public_numbers().e) return int(self.__key.public_numbers().e)
def verify_signature(self, signature: bytes, data: bytes) -> bytes: def verify_signature(self, signature: bytes, data: bytes) -> None:
return self.__key.verify(signature, data, padding=PKCS1v15(), algorithm=SHA256()) self.__key.verify(signature=signature, data=data, padding=PKCS1v15(), algorithm=SHA256())
class Cert: class Cert:
@@ -306,41 +330,47 @@ class Cert:
def pem(self) -> bytes: def pem(self) -> bytes:
return self.__cert.public_bytes(encoding=serialization.Encoding.PEM) 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: def signature(self) -> bytes:
return self.__cert.signature return self.__cert.signature
def subject_key_identifier(self):
return self.__cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value.key_identifier
def load_file(filename: str) -> bytes: def authority_key_identifier(self):
log = logging.getLogger(f'{__name__}') return self.__cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value.key_identifier
log.debug(f'Loading contents of file "{filename}')
with open(filename, 'rb') as file:
content = file.read()
return content
class NV: 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 NV.__DRIVER_MATRIX is None: if DriverMatrix.__DRIVER_MATRIX is None:
from json import load as json_load self.__load()
def __load(self):
try: try:
file = open(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:
NV.__DRIVER_MATRIX = {} # init empty dict to not try open file everytime, just when restarting app DriverMatrix.__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 NV.__DRIVER_MATRIX is None: if DriverMatrix.__DRIVER_MATRIX is None:
return None return None
for idx, (key, branch) in enumerate(NV.__DRIVER_MATRIX.items()): for idx, (key, branch) in enumerate(DriverMatrix.__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')

View File

@@ -1,8 +1,8 @@
fastapi==0.115.12 fastapi==0.115.14
uvicorn[standard]==0.34.0 uvicorn[standard]==0.35.0
python-jose[cryptography]==3.4.0 python-jose[cryptography]==3.5.0
cryptography==44.0.2 cryptography==45.0.5
python-dateutil==2.9.0 python-dateutil==2.9.0
sqlalchemy==2.0.40 sqlalchemy==2.0.41
markdown==3.7 markdown==3.8.2
python-dotenv==1.1.0 python-dotenv==1.1.1

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, SOFTWARE_BRANCH_KEY, = 'vGPU Branch Status', 'vGPU Software Branch' BRANCH_STATUS_KEY = 'vGPU Branch Status'
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,12 +26,15 @@ 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.findAll('bsp-accordion', {'class': 'Accordion-items-item'}) items = data.find_all('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')
@@ -42,48 +45,20 @@ 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'{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}' 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}'
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, ''))
@@ -92,13 +67,25 @@ 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
eol = branch.get(EOL_KEY) if is_latest else '' s = f'{version:<13} | {linux_manager:<21} | {linux_driver:<21} | {windows_manager:<21} | {windows_driver:<21} | {release_date:>21} | {branch_status:^21}'
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
@@ -128,7 +115,6 @@ if __name__ == '__main__':
# build matrix # build matrix
__driver_versions(soup) __driver_versions(soup)
__release_branches(soup)
# debug output # debug output
__debug() __debug()

View File

@@ -6,6 +6,8 @@ from datetime import datetime, UTC
from hashlib import sha256 from hashlib import sha256
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, jws
from jose.constants import ALGORITHMS from jose.constants import ALGORITHMS
@@ -26,11 +28,15 @@ ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-00000
# CA & Signing # CA & Signing
ca_setup = CASetup(service_instance_ref=INSTANCE_REF) ca_setup = CASetup(service_instance_ref=INSTANCE_REF)
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_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 = PrivateKey.from_file(ca_setup.si_private_key_filename)
my_si_private_key_as_pem = my_si_private_key.pem() 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 = my_si_private_key.public_key()
my_si_public_key_as_pem = my_si_private_key.public_key().pem() 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) jwt_encode_key = jwk.construct(my_si_private_key_as_pem, algorithm=ALGORITHMS.RS256)
jwt_decode_key = jwk.construct(my_si_public_key_as_pem, algorithm=ALGORITHMS.RS256) jwt_decode_key = jwk.construct(my_si_public_key_as_pem, algorithm=ALGORITHMS.RS256)
@@ -59,6 +65,31 @@ def test_signing():
my_si_public_key.verify_signature(signature_get_header, b'Hello') 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
@@ -76,9 +107,9 @@ def test_config():
def test_config_root_ca(): def test_config_root_ca():
response = client.get('/-/config/root-ca') response = client.get('/-/config/root-certificate')
assert response.status_code == 200 assert response.status_code == 200
assert response.content.decode('utf-8') == my_root_certificate.pem().decode('utf-8') assert response.content.decode('utf-8').strip() == my_root_certificate.pem().decode('utf-8').strip()
def test_readme(): def test_readme():
@@ -103,7 +134,17 @@ def test_config_token():
assert response.status_code == 200 assert response.status_code == 200
nv_response_certificate_configuration = response.json().get('certificateConfiguration') 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_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_jwt_decode_key = jwk.construct(nv_response_public_cert, algorithm=ALGORITHMS.RS256)
nv_response_config_token = response.json().get('configToken') nv_response_config_token = response.json().get('configToken')
@@ -116,8 +157,8 @@ def test_config_token():
nv_si_public_key_configuration = payload.get('service_instance_public_key_configuration') 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') nv_si_public_key_me = nv_si_public_key_configuration.get('service_instance_public_key_me')
# assert nv_si_public_key_me.get('mod') == 1 #nv_si_public_key_mod
assert len(nv_si_public_key_me.get('mod')) == 512 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 assert nv_si_public_key_me.get('exp') == 65537 # nv_si_public_key_exp