diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2da6cbe..f2e15e9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -120,13 +120,12 @@ build:pacman: paths: - "*.pkg.tar.zst" -test: - image: python:3.12-slim-bookworm +test:python: + image: $IMAGE stage: test interruptible: true rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - - if: $CI_COMMIT_TAG - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH changes: @@ -136,17 +135,20 @@ test: DATABASE: sqlite:///../app/db.sqlite parallel: matrix: - - REQUIREMENTS: - - 'requirements.txt' -# - '.DEBIAN/requirements-bookworm-12.txt' -# - '.DEBIAN/requirements-ubuntu-24.04.txt' -# - '.DEBIAN/requirements-ubuntu-24.10.txt' + - IMAGE: + # https://devguide.python.org/versions/#supported-versions +# - python:3.14-rc-alpine # EOL 2030-10 => uvicorn does not support 3.14 yet + - python:3.13-alpine # EOL 2029-10 + - 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: - - apt-get update && apt-get install -y python3-dev python3-pip python3-venv gcc + - apk --no-cache add openssl - python3 -m venv venv - source venv/bin/activate - pip install --upgrade pip - - pip install -r $REQUIREMENTS + - pip install -r requirements.txt - pip install pytest pytest-cov pytest-custom_exit_code httpx - mkdir -p app/cert - openssl genrsa -out app/cert/instance.private.pem 2048 @@ -156,10 +158,10 @@ test: - python -m pytest main.py --junitxml=report.xml artifacts: reports: - dotenv: version.env junit: ['**/report.xml'] -.test:apt: +test:apt: + image: $IMAGE stage: test rules: - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH @@ -167,7 +169,15 @@ test: changes: - app/**/* - .DEBIAN/**/* - - .gitlab-ci.yml + - if: $CI_PIPELINE_SOURCE == 'merge_request_event' + parallel: + matrix: + - IMAGE: + - debian:trixie-slim # EOL: t.b.a. + - debian:bookworm-slim # EOL: June 06, 2026 + - debian:bookworm-slim # EOL: June 06, 2026 + - ubuntu:24.04 # EOL: April 2036 + - ubuntu:24.10 needs: - job: build:apt artifacts: true @@ -199,16 +209,6 @@ test: - apt-get purge -qq -y fastapi-dls - 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: image: archlinux:base rules: @@ -292,15 +292,12 @@ gemnasium-python-dependency_scanning: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH -.deploy: - rules: - - if: $CI_COMMIT_TAG - deploy:docker: - extends: .deploy image: docker:dind stage: deploy tags: [ docker ] + rules: + - if: $CI_COMMIT_TAG before_script: - echo "Building docker image for commit $CI_COMMIT_SHA with version $CI_COMMIT_REF_NAME" - docker buildx inspect @@ -319,9 +316,10 @@ deploy:docker: deploy:apt: # doc: https://git.collinwebdesigns.de/help/user/packages/debian_repository/index.md#install-a-package - extends: .deploy image: debian:bookworm-slim stage: deploy + rules: + - if: $CI_COMMIT_TAG needs: - job: build:apt artifacts: true @@ -358,9 +356,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}"' deploy:pacman: - extends: .deploy image: archlinux:base-devel stage: deploy + rules: + - if: $CI_COMMIT_TAG needs: - job: build:pacman artifacts: true @@ -381,7 +380,7 @@ deploy:pacman: release: image: registry.gitlab.com/gitlab-org/release-cli:latest stage: .post - needs: [ test ] + needs: [ build:docker, build:apt, build:pacman ] rules: - if: $CI_COMMIT_TAG script: diff --git a/README.md b/README.md index 25d3588..dd64223 100644 --- a/README.md +++ b/README.md @@ -795,13 +795,13 @@ 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/) -- @mrzenc who wrote [fastapi-dls-nixos](https://github.com/mrzenc/fastapi-dls-nixos) +- `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/) +- `mrzenc` who wrote [fastapi-dls-nixos](https://github.com/mrzenc/fastapi-dls-nixos) And thanks to all people who contributed to all these libraries! diff --git a/app/orm.py b/app/orm.py index e2dd0bc..89235b0 100644 --- a/app/orm.py +++ b/app/orm.py @@ -5,7 +5,7 @@ from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, update, and_ from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker, declarative_base -from util import NV +from util import DriverMatrix Base = declarative_base() @@ -25,7 +25,7 @@ class Origin(Base): return f'Origin(origin_ref={self.origin_ref}, hostname={self.hostname})' def serialize(self) -> dict: - _ = NV().find(self.guest_driver_version) + _ = DriverMatrix().find(self.guest_driver_version) return { 'origin_ref': self.origin_ref, diff --git a/app/util.py b/app/util.py index 1aae17b..c6c93d0 100644 --- a/app/util.py +++ b/app/util.py @@ -1,4 +1,5 @@ import logging +from json import load as json_load from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey, generate_private_key @@ -7,6 +8,14 @@ from cryptography.hazmat.primitives.serialization import load_pem_private_key, l 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 PrivateKey: def __init__(self, data: bytes): @@ -76,37 +85,32 @@ class PublicKey: format=serialization.PublicFormat.SubjectPublicKeyInfo ) -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 NV: +class DriverMatrix: __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}') + if DriverMatrix.__DRIVER_MATRIX is None: + self.__load() + + def __load(self): + try: + file = open(DriverMatrix.__DRIVER_MATRIX_FILENAME) + DriverMatrix.__DRIVER_MATRIX = json_load(file) + file.close() + self.log.debug(f'Successfully loaded "{DriverMatrix.__DRIVER_MATRIX_FILENAME}".') + except Exception as e: + 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}') @staticmethod def find(version: str) -> dict | None: - if NV.__DRIVER_MATRIX is None: + if DriverMatrix.__DRIVER_MATRIX is 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'): linux_driver = release.get('Linux Driver') windows_driver = release.get('Windows Driver') diff --git a/requirements.txt b/requirements.txt index 6819866..734ff42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ fastapi==0.115.12 -uvicorn[standard]==0.34.0 +uvicorn[standard]==0.34.1 python-jose[cryptography]==3.4.0 cryptography==44.0.2 python-dateutil==2.9.0 sqlalchemy==2.0.40 -markdown==3.7 +markdown==3.8 python-dotenv==1.1.0 diff --git a/test/create_driver_matrix_json.py b/test/create_driver_matrix_json.py index 2047d2e..a2afd9f 100644 --- a/test/create_driver_matrix_json.py +++ b/test/create_driver_matrix_json.py @@ -6,7 +6,7 @@ logger.setLevel(logging.INFO) 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' LINUX_VGPU_MANAGER_KEY, LINUX_DRIVER_KEY = 'Linux vGPU Manager', 'Linux 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 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: software_branch = item.find('div', {'class': 'Accordion-items-item-title'}).text.strip() software_branch = software_branch.replace(' Releases', '') 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) table = item.find('table') ths, trs = table.find_all('th'), table.find_all('tr') @@ -42,48 +45,20 @@ def __driver_versions(html: 'BeautifulSoup'): 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)} + x.setdefault(BRANCH_STATUS_KEY, branch_status) 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': '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(): # 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) # 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, '')) @@ -92,13 +67,25 @@ def __debug(): windows_driver = release.get(WINDOWS_DRIVER_KEY) release_date = release.get(RELEASE_DATE_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 - 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}' + s = f'{version:<13} | {linux_manager:<21} | {linux_driver:<21} | {windows_manager:<21} | {windows_driver:<21} | {release_date:>21} | {branch_status:^21}' 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): import json @@ -128,7 +115,6 @@ if __name__ == '__main__': # build matrix __driver_versions(soup) - __release_branches(soup) # debug output __debug()