1 Commits

Author SHA1 Message Date
Oscar Krause
ef542ec821 Merge branch 'dev' into 'main'
Dev

See merge request oscar.krause/fastapi-dls!31
2024-04-09 10:28:57 +02:00
38 changed files with 117 additions and 1148 deletions

View File

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

View File

@@ -1,10 +0,0 @@
# https://packages.ubuntu.com
fastapi==0.101.0
uvicorn[standard]==0.27.1
python-jose[pycryptodome]==3.3.0
pycryptodome==3.20.0
python-dateutil==2.8.2
sqlalchemy==1.4.50
markdown==3.5.2
python-dotenv==1.0.1
jinja2==3.1.2

View File

@@ -8,7 +8,7 @@ pkgdesc='NVIDIA DLS server implementation with FastAPI'
arch=('any')
url='https://git.collinwebdesigns.de/oscar.krause/fastapi-dls'
license=('MIT')
depends=('python' 'python-jose' 'python-starlette' 'python-httpx' 'python-fastapi' 'python-dotenv' 'python-dateutil' 'python-sqlalchemy' 'python-pycryptodome' 'python-jinja' 'uvicorn' 'python-markdown' 'openssl')
depends=('python' 'python-jose' 'python-starlette' 'python-httpx' 'python-fastapi' 'python-dotenv' 'python-dateutil' 'python-sqlalchemy' 'python-pycryptodome' 'uvicorn' 'python-markdown' 'openssl')
provider=("$pkgname")
install="$pkgname.install"
backup=('etc/default/fastapi-dls')
@@ -37,52 +37,17 @@ check() {
}
package() {
# create directories
install -d "$pkgdir/usr/share/doc/$pkgname"
install -d "$pkgdir/var/lib/$pkgname/cert"
# copy docs & static files
cp -r "$srcdir/$pkgname/doc"/* "$pkgdir/usr/share/doc/$pkgname/"
install -Dm644 "$srcdir/$pkgname/README.md" "$pkgdir/usr/share/doc/$pkgname/README.md"
install -Dm644 "$srcdir/$pkgname/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"
# copy main app python files
sed -i "s/README.md/\/usr\/share\/doc\/$pkgname\/README.md/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/orm.py" "$pkgdir/opt/$pkgname/orm.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.service" "$pkgdir/usr/lib/systemd/system/$pkgname.service"
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"

View File

@@ -20,7 +20,6 @@ build:docker:
changes:
- app/**/*
- Dockerfile
- requirements.txt
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
tags: [ docker ]
before_script:
@@ -127,7 +126,7 @@ build:pacman:
- "*.pkg.tar.zst"
test:
image: $IMAGE
image: python:3.11-slim-bookworm
stage: test
interruptible: true
rules:
@@ -142,12 +141,10 @@ test:
DATABASE: sqlite:///../app/db.sqlite
parallel:
matrix:
- IMAGE: [ 'python:3.11-slim-bookworm', 'python:3.12-slim-bullseye' ]
REQUIREMENTS:
- REQUIREMENTS:
- requirements.txt
- .DEBIAN/requirements-bookworm-12.txt
- .DEBIAN/requirements-ubuntu-23.10.txt
- .DEBIAN/requirements-ubuntu-24.04.txt
before_script:
- apt-get update && apt-get install -y python3-dev gcc
- pip install -r $REQUIREMENTS
@@ -208,7 +205,7 @@ test:debian:
test:ubuntu:
extends: .test:linux
image: ubuntu:24.04
image: ubuntu:23.10
test:archlinux:
image: archlinux:base

View File

@@ -9,24 +9,17 @@ Only the clients need a connection to this service on configured port.
**Official Links**
* 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://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`)
*All other repositories are forks! (which is no bad - just for information and bug reports)*
[Releases & Release Notes](https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/releases)
**Further Reading**
* [NVIDIA vGPU Guide](https://gitlab.com/polloloco/vgpu-proxmox) - This document serves as a guide to install NVIDIA vGPU host drivers on the latest Proxmox VE version
* [vgpu_unlock](https://github.com/DualCoder/vgpu_unlock) - Unlock vGPU functionality for consumer-grade Nvidia GPUs.
* [vGPU_Unlock Wiki](https://docs.google.com/document/d/1pzrWJ9h-zANCtyqRgS7Vzla0Y8Ea2-5z2HEi4X75d2Q) - Guide for `vgpu_unlock`
* [Proxmox All-In-One Installer Script](https://wvthoog.nl/proxmox-vgpu-v3/) - Also known as `proxmox-installer.sh`
---
[TOC]
[[_TOC_]]
# Setup (Service)
@@ -111,7 +104,7 @@ volumes:
dls-db:
```
## Debian / Ubuntu / macOS (manual method using `git clone` and python virtual environment)
## Debian/Ubuntu/macOS (manual method using `git clone` and python virtual environment)
Tested on `Debian 11 (bullseye)`, `Debian 12 (bookworm)` and `macOS Ventura (13.6)`, Ubuntu may also work.
**Please note that setup on macOS differs from Debian based systems.**
@@ -318,7 +311,7 @@ EOF
Now you have to run `systemctl daemon-reload`. After that you can start service
with `systemctl start fastapi-dls.service` and enable autostart with `systemctl enable fastapi-dls.service`.
## Debian / Ubuntu (using `dpkg` / `apt`)
## Debian/Ubuntu (using `dpkg`)
Packages are available here:
@@ -326,11 +319,10 @@ Packages are available here:
Successful tested with:
- Debian 12 (Bookworm) (EOL: tba.)
- Debian 12 (Bookworm)
- Ubuntu 22.10 (Kinetic Kudu) (EOL: July 20, 2023)
- Ubuntu 23.04 (Lunar Lobster) (EOL: January 2024)
- Ubuntu 23.10 (Mantic Minotaur) (EOL: July 2024)
- Ubuntu 24.04 (Noble Numbat) (EOL: April 2036)
Not working with:
@@ -438,11 +430,9 @@ Successfully tested with this package versions:
| vGPU Suftware | Driver Branch | Linux vGPU Manager | Linux Driver | Windows Driver | Release Date | EOL Date |
|:-------------:|:-------------:|--------------------|--------------|----------------|--------------:|--------------:|
| `17.2` | R550 | `550.90.05` | `550.90.07` | `552.55` | June 2024 | February 2025 |
| `17.1` | R550 | `550.54.16` | `550.54.15` | `551.78` | March 2024 | |
| `17.1` | R550 | `550.54.16` | `550.54.15` | `551.78` | March 2024 | February 2025 |
| `17.0` | R550 | `550.54.10` | `550.54.14` | `551.61` | February 2024 | |
| `16.6` | R535 | `535.183.04` | `535.183.01` | `538.67` | June 2024 | July 2026 |
| `16.5` | R535 | `535.161.05` | `535.161.08` | `538.46` | February 2024 | |
| `16.5` | R535 | `535.161.05` | `535.161.08` | `538.46` | February 2024 | July 2026 |
| `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 | |
@@ -457,7 +447,6 @@ Successfully tested with this package versions:
| `14.3` | R510 | `510.108.03` | `510.108.03` | `513.91` | November 2022 | |
- https://docs.nvidia.com/grid/index.html
- https://docs.nvidia.com/grid/gpus-supported-by-vgpu.html
*To get the latest drivers, visit Nvidia or search in Discord-Channel `GPU Unlocking` (Server-ID: `829786927829745685`) on channel `licensing` `biggerthanshit`
@@ -594,7 +583,7 @@ Generate client token, (see [installation](#installation)).
There are many other internal api endpoints for handling authentication and lease process.
</details>
# Troubleshoot / Debug
# Troubleshoot
**Please make sure that fastapi-dls and your guests are on the same timezone!**
@@ -750,12 +739,6 @@ Thanks to vGPU community and all who uses this project and report bugs.
Special thanks to
- @samicrusader who created build file for **ArchLinux**
- @cyrus who wrote the section for **openSUSE**
- @midi who wrote the section for **unRAID**
- @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)
- 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/)
And thanks to all people who contributed to all these libraries!
- @samicrusader who created build file for ArchLinux
- @cyrus who wrote the section for openSUSE
- @midi who wrote the section for unRAID

View File

@@ -1,44 +1,39 @@
import logging
from base64 import b64encode as b64enc
from calendar import timegm
from contextlib import asynccontextmanager
from datetime import datetime, timedelta
from hashlib import sha256
from json import loads as json_loads
from os import getenv as env
from os.path import join, dirname
from uuid import uuid4
from os.path import join, dirname
from os import getenv as env
from dateutil.relativedelta import relativedelta
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.requests import Request
from json import loads as json_loads
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from calendar import timegm
from jose import jws, jwk, jwt, JWTError
from jose.constants import ALGORITHMS
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import StreamingResponse, JSONResponse as JSONr, HTMLResponse as HTMLr, Response, RedirectResponse
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
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 util import load_key, load_file
from orm import Origin, Lease, init as db_init, migrate
# Load variables
load_dotenv('../version.env')
# Get current timezone
TZ = datetime.now().astimezone().tzinfo
# Load basic variables
VERSION, COMMIT, DEBUG = env('VERSION', 'unknown'), env('COMMIT', 'unknown'), bool(env('DEBUG', False))
# Database connection
config = dict(openapi_url=None, docs_url=None, redoc_url=None) # dict(openapi_url='/-/openapi.json', docs_url='/-/docs', redoc_url='/-/redoc')
app = FastAPI(title='FastAPI-DLS', description='Minimal Delegated License Service (DLS).', version=VERSION, **config)
db = create_engine(str(env('DATABASE', 'sqlite:///db.sqlite')))
db_init(db), migrate(db)
# Load DLS variables (all prefixed with "INSTANCE_*" is used as "SERVICE_INSTANCE_*" or "SI_*" in official dls service)
# everything prefixed with "INSTANCE_*" is used as "SERVICE_INSTANCE_*" or "SI_*" in official dls service
DLS_URL = str(env('DLS_URL', 'localhost'))
DLS_PORT = int(env('DLS_PORT', '443'))
SITE_KEY_XID = str(env('SITE_KEY_XID', '00000000-0000-0000-0000-000000000000'))
@@ -56,35 +51,6 @@ CORS_ORIGINS = str(env('CORS_ORIGINS', '')).split(',') if (env('CORS_ORIGINS'))
jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256)
jwt_decode_key = jwk.construct(INSTANCE_KEY_PUB.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256)
# FastAPI
@asynccontextmanager
async def lifespan(_: FastAPI):
# on startup
logger.info(f'''
Using timezone: {str(TZ)}. Make sure this is correct and match your clients!
Your clients renew their license every {str(Lease.calculate_renewal(LEASE_RENEWAL_PERIOD, LEASE_RENEWAL_DELTA))}.
If the renewal fails, the license is {str(LEASE_RENEWAL_DELTA)} valid.
Your client-token file (.tok) is valid for {str(CLIENT_TOKEN_EXPIRE_DELTA)}.
''')
logger.info(f'Debug is {"enabled" if DEBUG else "disabled"}.')
yield
# on shutdown
logger.info(f'Shutting down ...')
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.mount('/static', StaticFiles(directory=join(dirname(__file__), 'static'), html=True), name='static'),
templates = Jinja2Templates(directory=join(dirname(__file__), 'templates'))
app.debug = DEBUG
app.add_middleware(
CORSMiddleware,
@@ -94,23 +60,35 @@ app.add_middleware(
allow_headers=['*'],
)
# Logging
LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO
logging.basicConfig(format='[{levelname:^7}] [{module:^15}] {message}', style='{')
logging.basicConfig()
logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
logging.getLogger('util').setLevel(LOG_LEVEL)
logging.getLogger('NV').setLevel(LOG_LEVEL)
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
# Helper
def __get_token(request: Request) -> dict:
authorization_header = request.headers.get('authorization')
token = authorization_header.split(' ')[1]
return jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False})
def __json_config() -> dict:
return {
@app.get('/', summary='Index')
async def index():
return RedirectResponse('/-/readme')
@app.get('/-/', summary='* Index')
async def _index():
return RedirectResponse('/-/readme')
@app.get('/-/health', summary='* Health')
async def _health():
return JSONr({'status': 'up'})
@app.get('/-/config', summary='* Config', description='returns environment variables.')
async def _config():
return JSONr({
'VERSION': str(VERSION),
'COMMIT': str(COMMIT),
'DEBUG': str(DEBUG),
@@ -124,66 +102,52 @@ def __json_config() -> dict:
'LEASE_RENEWAL_PERIOD': str(LEASE_RENEWAL_PERIOD),
'CORS_ORIGINS': str(CORS_ORIGINS),
'TZ': str(TZ),
# static / calculated
'LEASE_RENEWAL_DELTA': str(LEASE_RENEWAL_DELTA),
'LEASE_CALCULATED_RENEWAL': str(Lease.calculate_renewal(LEASE_RENEWAL_PERIOD, LEASE_RENEWAL_DELTA)),
'CLIENT_TOKEN_EXPIRE_DELTA': str(CLIENT_TOKEN_EXPIRE_DELTA),
}
# Endpoints
@app.get('/', summary='* Index')
async def index():
return RedirectResponse('/-/')
@app.get('/-/', summary='* Index')
async def _index(request: Request):
return templates.TemplateResponse(name='views/index.html', context={'request': request, 'VERSION': VERSION})
@app.get('/-/health', summary='* Health')
async def _health():
return JSONr({'status': 'up'})
@app.get('/-/config', summary='* Config', description='returns environment variables.')
async def _config():
return JSONr(__json_config())
})
@app.get('/-/readme', summary='* Readme')
async def _readme(request: Request):
async def _readme():
from markdown import markdown
content = load_file(join(dirname(__file__), '../README.md')).decode('utf-8')
markdown = markdown(text=content, extensions=['tables', 'fenced_code', 'md_in_html', 'nl2br', 'toc'])
context = {'request': request, 'VERSION': VERSION, 'markdown': markdown }
return templates.TemplateResponse(name='views/dashboard_readme.html', context=context)
content = load_file('../README.md').decode('utf-8')
return HTMLr(markdown(text=content, extensions=['tables', 'fenced_code', 'md_in_html', 'nl2br', 'toc']))
@app.get('/-/manage', summary='* Management UI')
async def _manage(request: Request):
context = {'request': request, 'VERSION': VERSION}
return templates.TemplateResponse(name='views/manage.html', context=context)
response = '''
<!DOCTYPE html>
<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?');
@app.get('/-/dashboard', summary='* Dashboard')
async def _dashboard(request: Request):
context = {'request': request, 'VERSION': VERSION, 'CONFIG': __json_config()}
return templates.TemplateResponse(name='views/dashboard.html', context=context)
@app.get('/-/dashboard/origins', summary='* Dashboard - Origins')
async def _dashboard_origins(request: Request):
context = {'request': request, 'VERSION': VERSION}
return templates.TemplateResponse(name='views/dashboard_origins.html', context=context)
@app.get('/-/dashboard/leases', summary='* Dashboard - Leases')
async def _dashboard_origins(request: Request):
context = {'request': request, 'VERSION': VERSION}
return templates.TemplateResponse(name='views/dashboard_leases.html', context=context)
if (response) {
var xhr = new XMLHttpRequest();
xhr.open("DELETE", '/-/origins', true);
xhr.send();
}
}
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
var xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/lease/${lease_ref}`, true);
xhr.send();
}
</script>
</body>
</html>
'''
return HTMLr(response)
@app.get('/-/origins', summary='* Origins')
@@ -206,19 +170,6 @@ async def _origins_delete(request: Request):
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')
async def _leases(request: Request, origin: bool = False):
session = sessionmaker(bind=db)()
@@ -235,13 +186,13 @@ async def _leases(request: Request, origin: bool = False):
return JSONr(response)
@app.delete('/-/leases/expired', summary='* Delete all expired Leases')
@app.delete('/-/leases/expired', summary='* Leases')
async def _lease_delete_expired(request: Request):
Lease.delete_expired(db)
return Response(status_code=201)
@app.delete('/-/lease/{lease_ref}', summary='* Delete specific Lease')
@app.delete('/-/lease/{lease_ref}', summary='* Lease')
async def _lease_delete(request: Request, lease_ref: str):
if Lease.delete(db, lease_ref) == 1:
return Response(status_code=201)
@@ -589,6 +540,18 @@ async def leasing_v1_lessor_shutdown(request: Request):
return JSONr(response)
@app.on_event('startup')
async def app_on_startup():
logger.info(f'''
Using timezone: {str(TZ)}. Make sure this is correct and match your clients!
Your clients renew their license every {str(Lease.calculate_renewal(LEASE_RENEWAL_PERIOD, LEASE_RENEWAL_DELTA))}.
If the renewal fails, the license is {str(LEASE_RENEWAL_DELTA)} valid.
Your client-token file (.tok) is valid for {str(CLIENT_TOKEN_EXPIRE_DELTA)}.
''')
if __name__ == '__main__':
import uvicorn

View File

@@ -1,12 +1,10 @@
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, update, and_, inspect, text
from sqlalchemy.engine import Engine
from sqlalchemy.orm import sessionmaker, declarative_base
from util import NV
Base = declarative_base()
@@ -25,8 +23,6 @@ class Origin(Base):
return f'Origin(origin_ref={self.origin_ref}, hostname={self.hostname})'
def serialize(self) -> dict:
_ = NV().find(self.guest_driver_version)
return {
'origin_ref': self.origin_ref,
# 'service_instance_xid': self.service_instance_xid,
@@ -34,7 +30,6 @@ class Origin(Base):
'guest_driver_version': self.guest_driver_version,
'os_platform': self.os_platform,
'os_version': self.os_version,
'$driver': _ if _ is not None else None,
}
@staticmethod
@@ -66,17 +61,7 @@ class Origin(Base):
if origin_refs is None:
deletions = session.query(Origin).delete()
else:
deletions = session.query(Origin).filter(Origin.origin_ref.in_(origin_refs)).delete()
session.commit()
session.close()
return deletions
@staticmethod
def delete_expired(engine: Engine) -> int:
session = sessionmaker(bind=engine)()
origins = session.query(Origin).join(Lease, Origin.origin_ref == Lease.origin_ref, isouter=True).filter(Lease.lease_ref.is_(None)).all()
origin_refs = [origin.origin_ref for origin in origins]
deletions = session.query(Origin).filter(Origin.origin_ref.in_(origin_refs)).delete()
deletions = session.query(Origin).filter(Origin.origin_ref in origin_refs).delete()
session.commit()
session.close()
return deletions
@@ -104,10 +89,10 @@ class Lease(Base):
'lease_ref': self.lease_ref,
'origin_ref': self.origin_ref,
# 'scope_ref': self.scope_ref,
'lease_created': self.lease_created.replace(tzinfo=timezone.utc).isoformat(),
'lease_expires': self.lease_expires.replace(tzinfo=timezone.utc).isoformat(),
'lease_updated': self.lease_updated.replace(tzinfo=timezone.utc).isoformat(),
'lease_renewal': lease_renewal.replace(tzinfo=timezone.utc).isoformat(),
'lease_created': self.lease_created.isoformat(),
'lease_expires': self.lease_expires.isoformat(),
'lease_updated': self.lease_updated.isoformat(),
'lease_renewal': lease_renewal.isoformat(),
}
@staticmethod

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,56 +0,0 @@
/*
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

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

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1 +0,0 @@
{"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.

Before

Width:  |  Height:  |  Size: 73 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,139 +0,0 @@
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');
row.innerHTML = `
<td><code>${o.origin_ref}</code></td>
<td>${o.hostname}</td>
<td>${o.os_platform} (${o.os_version})</td>
<td>${o.guest_driver_version}</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();
}

File diff suppressed because one or more lines are too long

View File

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

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

@@ -1,33 +0,0 @@
<!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

@@ -1,16 +0,0 @@
{% 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

@@ -1,14 +0,0 @@
{% 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

@@ -1,69 +0,0 @@
{% 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

@@ -1,60 +0,0 @@
{% 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

@@ -1,70 +0,0 @@
{% 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

@@ -1,15 +0,0 @@
{% 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

@@ -1,26 +0,0 @@
{% 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,17 +1,10 @@
import logging
logging.basicConfig()
def load_file(filename: str) -> bytes:
log = logging.getLogger(f'{__name__}')
log.debug(f'Loading contents of file "{filename}')
def load_file(filename) -> bytes:
with open(filename, 'rb') as file:
content = file.read()
return content
def load_key(filename: str) -> "RsaKey":
def load_key(filename) -> "RsaKey":
try:
# Crypto | Cryptodome on Debian
from Crypto.PublicKey import RSA
@@ -20,8 +13,6 @@ def load_key(filename: str) -> "RsaKey":
from Cryptodome.PublicKey import RSA
from Cryptodome.PublicKey.RSA import RsaKey
log = logging.getLogger(__name__)
log.debug(f'Importing RSA-Key from "{filename}"')
return RSA.import_key(extern_key=load_file(filename), passphrase=None)
@@ -33,50 +24,5 @@ def generate_key() -> "RsaKey":
except ModuleNotFoundError:
from Cryptodome.PublicKey import RSA
from Cryptodome.PublicKey.RSA import RsaKey
log = logging.getLogger(__name__)
log.debug(f'Generating RSA-Key')
return RSA.generate(bits=2048)
class NV:
__DRIVER_MATRIX_FILENAME = 'static/driver_matrix.json'
__DRIVER_MATRIX: None | dict = None # https://docs.nvidia.com/grid/ => "Driver Versions"
def __init__(self):
self.log = logging.getLogger(self.__class__.__name__)
if NV.__DRIVER_MATRIX is None:
from json import load as json_load
try:
file = open(NV.__DRIVER_MATRIX_FILENAME)
NV.__DRIVER_MATRIX = json_load(file)
file.close()
self.log.debug(f'Successfully loaded "{NV.__DRIVER_MATRIX_FILENAME}".')
except Exception as e:
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}')
@staticmethod
def find(version: str) -> dict | None:
if NV.__DRIVER_MATRIX is None:
return None
for idx, (key, branch) in enumerate(NV.__DRIVER_MATRIX.items()):
for release in branch.get('$releases'):
linux_driver = release.get('Linux Driver')
windows_driver = release.get('Windows Driver')
if version == linux_driver or version == windows_driver:
tmp = branch.copy()
tmp.pop('$releases')
is_latest = release.get('vGPU Software') == branch.get('Latest Release in Branch')
return {
'software_branch': branch.get('vGPU Software Branch'),
'branch_version': release.get('vGPU Software'),
'driver_branch': branch.get('Driver Branch'),
'branch_status': branch.get('vGPU Branch Status'),
'release_date': release.get('Release Date'),
'eol': branch.get('EOL Date') if is_latest else None,
'is_latest': is_latest,
}
return None

View File

@@ -1,9 +1,8 @@
fastapi==0.111.0
fastapi==0.110.1
uvicorn[standard]==0.29.0
python-jose==3.3.0
pycryptodome==3.20.0
python-dateutil==2.8.2
sqlalchemy==2.0.30
sqlalchemy==2.0.29
markdown==3.6
python-dotenv==1.0.1
jinja2==3.1.3

View File

@@ -1,137 +0,0 @@
import logging
logging.basicConfig()
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
URL = 'https://docs.nvidia.com/grid/'
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'
LINUX_VGPU_MANAGER_KEY, LINUX_DRIVER_KEY = 'Linux vGPU Manager', 'Linux Driver'
WINDOWS_VGPU_MANAGER_KEY, WINDOWS_DRIVER_KEY = 'Windows vGPU Manager', 'Windows Driver'
ALT_VGPU_MANAGER_KEY = 'vGPU Manager'
RELEASE_DATE_KEY, LATEST_KEY, EOL_KEY = 'Release Date', 'Latest Release in Branch', 'EOL Date'
JSON_RELEASES_KEY = '$releases'
def __driver_versions(html: 'BeautifulSoup'):
def __strip(_: str) -> str:
# removes content after linebreak (e.g. "Hello\n World" to "Hello")
_ = _.strip()
tmp = _.split('\n')
if len(tmp) > 0:
return tmp[0]
return _
# find wrapper for "DriverVersions" and find tables
data = html.find('div', {'id': 'DriverVersions'})
tables = data.findAll('table')
for table in tables:
# parse software-branch (e.g. "vGPU software 17 Releases" and remove " Releases" for "matrix_key")
software_branch = table.parent.find_previous_sibling('button', {'class': 'accordion'}).text.strip()
software_branch = software_branch.replace(' Releases', '')
matrix_key = software_branch.lower()
# driver version info from table-heads (ths) and table-rows (trs)
ths, trs = table.find_all('th'), table.find_all('tr')
headers, releases = [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]: __strip(cell.text) for i, cell in enumerate(tds)}
releases.append(x)
# add to matrix
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': 'AllReleaseBranches'})
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():
# 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}'
print(s)
# iterate over dict & format some variables to not overload table
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):
version = release.get(VGPU_KEY, release.get(GRID_KEY, ''))
linux_manager = release.get(LINUX_VGPU_MANAGER_KEY, release.get(ALT_VGPU_MANAGER_KEY, ''))
linux_driver = release.get(LINUX_DRIVER_KEY)
windows_manager = release.get(WINDOWS_VGPU_MANAGER_KEY, release.get(ALT_VGPU_MANAGER_KEY, ''))
windows_driver = release.get(WINDOWS_DRIVER_KEY)
release_date = release.get(RELEASE_DATE_KEY)
is_latest = release.get(VGPU_KEY) == branch.get(LATEST_KEY)
version = f'{version} *' if is_latest else version
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)
def __dump(filename: str):
import json
file = open(filename, 'w')
json.dump(MATRIX, file)
file.close()
if __name__ == '__main__':
MATRIX = {}
try:
import httpx
from bs4 import BeautifulSoup
except Exception as e:
logger.error(f'Failed to import module: {e}')
logger.info('Run "pip install beautifulsoup4 httpx"')
exit(1)
r = httpx.get(URL)
if r.status_code != 200:
logger.error(f'Error loading "{URL}" with status code {r.status_code}.')
exit(2)
# parse html
soup = BeautifulSoup(r.text, features='html.parser')
# build matrix
__driver_versions(soup)
__release_branches(soup)
# debug output
__debug()
# dump data to file
__dump('../app/static/driver_matrix.json')

View File

@@ -59,8 +59,8 @@ def test_readme():
assert response.status_code == 200
def test_dashboard():
response = client.get('/-/dashboard')
def test_manage():
response = client.get('/-/manage')
assert response.status_code == 200