From c79455b84d200f9515e976ac8cc385323a8bbb30 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Fri, 21 Jun 2024 19:01:33 +0200 Subject: [PATCH] added way to include driver version in api use `create_driver_matrix_json.py` to generate file in `static/driver_matrix.json`. Logging currently is disabled to not confuse users when file is missing. This is optional! --- app/main.py | 1 + app/orm.py | 5 ++ app/util.py | 44 ++++++++++ test/create_driver_matrix_json.py | 137 ++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+) create mode 100644 test/create_driver_matrix_json.py diff --git a/app/main.py b/app/main.py index 81c1c37..60a66b2 100644 --- a/app/main.py +++ b/app/main.py @@ -95,6 +95,7 @@ logging.basicConfig(format='[{levelname:^7}] [{module:^15}] {message}', style='{ logger = logging.getLogger(__name__) logger.setLevel(LOG_LEVEL) logging.getLogger('util').setLevel(LOG_LEVEL) +logging.getLogger('NV').setLevel(LOG_LEVEL) # Helper diff --git a/app/orm.py b/app/orm.py index dacf2b0..73828fd 100644 --- a/app/orm.py +++ b/app/orm.py @@ -5,6 +5,8 @@ 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 + Base = declarative_base() @@ -23,6 +25,8 @@ 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, @@ -30,6 +34,7 @@ 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 diff --git a/app/util.py b/app/util.py index 530e5c0..b5b1ff1 100644 --- a/app/util.py +++ b/app/util.py @@ -36,3 +36,47 @@ def generate_key() -> "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 diff --git a/test/create_driver_matrix_json.py b/test/create_driver_matrix_json.py new file mode 100644 index 0000000..1f31884 --- /dev/null +++ b/test/create_driver_matrix_json.py @@ -0,0 +1,137 @@ +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')