diff --git a/.DEBIAN/conffiles b/.DEBIAN/conffiles index cd8fdd7..008d731 100644 --- a/.DEBIAN/conffiles +++ b/.DEBIAN/conffiles @@ -1,2 +1 @@ /etc/fastapi-dls/env -/etc/systemd/system/fastapi-dls.service diff --git a/.DEBIAN/postinst b/.DEBIAN/postinst index fbf9b82..8213736 100644 --- a/.DEBIAN/postinst +++ b/.DEBIAN/postinst @@ -3,7 +3,7 @@ WORKING_DIR=/usr/share/fastapi-dls CONFIG_DIR=/etc/fastapi-dls -if [[ ! -f $CONFIG_DIR/instance.private.pem ]]; then +if [ ! -f $CONFIG_DIR/instance.private.pem ]; then echo "> Create dls-instance keypair ..." openssl genrsa -out $CONFIG_DIR/instance.private.pem 2048 openssl rsa -in $CONFIG_DIR/instance.private.pem -outform PEM -pubout -out $CONFIG_DIR/instance.public.pem @@ -12,8 +12,8 @@ else fi while true; do - [[ -f $CONFIG_DIR/webserver.key ]] && default_answer="N" || default_answer="Y" - [[ $default_answer == "Y" ]] && V="Y/n" || V="y/N" + [ -f $CONFIG_DIR/webserver.key ] && default_answer="N" || default_answer="Y" + [ $default_answer == "Y" ] && V="Y/n" || V="y/N" read -p "> Do you wish to create self-signed webserver certificate? [${V}]" yn yn=${yn:-$default_answer} # ${parameter:-word} If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted. case $yn in @@ -27,7 +27,7 @@ while true; do esac done -if [[ -f $CONFIG_DIR/webserver.key ]]; then +if [ -f $CONFIG_DIR/webserver.key ]; then echo "> Starting service ..." systemctl start fastapi-dls.service diff --git a/.DEBIAN/postrm b/.DEBIAN/postrm index b99d0fa..9c715dd 100755 --- a/.DEBIAN/postrm +++ b/.DEBIAN/postrm @@ -1,8 +1,9 @@ #!/bin/bash -if [[ -f /etc/systemd/system/fastapi-dls.service ]]; then - echo "> Removing service file." - rm /etc/systemd/system/fastapi-dls.service -fi +# is removed automatically +#if [ "$1" = purge ] && [ -d /usr/share/fastapi-dls ]; then +# echo "> Removing app." +# rm -r /usr/share/fastapi-dls +#fi -# todo +echo -e "> Done." diff --git a/.DEBIAN/prerm b/.DEBIAN/prerm index 296c995..3678725 100755 --- a/.DEBIAN/prerm +++ b/.DEBIAN/prerm @@ -1,5 +1,3 @@ #!/bin/bash echo -e "> Starting uninstallation of 'fastapi-dls'!" - -# todo diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cb1cdea..360fe9a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -98,7 +98,7 @@ build:pacman: - "*.pkg.tar.zst" test: - image: python:3.10-slim-bullseye + image: python:3.11-slim-bullseye stage: test rules: - if: $CI_COMMIT_BRANCH @@ -114,6 +114,9 @@ test: - cd test script: - pytest main.py + artifacts: + reports: + dotenv: version.env .test:linux: stage: test @@ -272,24 +275,11 @@ deploy:pacman: - 'echo "EXPORT_NAME: ${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}"' -release:prepare: - stage: .pre - rules: - - if: $CI_COMMIT_TAG - when: never - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - script: - - source version.env - - echo $VERSION - artifacts: - reports: - dotenv: version.env - release: image: registry.gitlab.com/gitlab-org/release-cli:latest stage: .post needs: - - job: release:prepare + - job: test artifacts: true rules: - if: $CI_COMMIT_TAG @@ -298,7 +288,7 @@ release: script: - echo "Running release-job for $VERSION" release: - name: $CI_PROJECT_TITLE $version + name: $CI_PROJECT_TITLE $VERSION description: Release of $CI_PROJECT_TITLE version $VERSION tag_name: $VERSION ref: $CI_COMMIT_SHA diff --git a/Dockerfile b/Dockerfile index 6ffaee6..e92f5dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-alpine +FROM python:3.11-alpine COPY requirements.txt /tmp/requirements.txt diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 0000000..4ddd379 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,17 @@ +# FAQ + +## `Failed to acquire license from (Info: - Error: The allowed time to process response has expired)` + +- Did your timezone settings are correct on fastapi-dls **and your guest**? + +- Did you download the client-token more than an hour ago? + +Please download a new client-token. The guest have to register within an hour after client-token was created. + + +## `jose.exceptions.JWTError: Signature verification failed.` + +- Did you recreated `instance.public.pem` / `instance.private.pem`? + +Then you have to download a **new** client-token on each of your guests. + diff --git a/README.md b/README.md index c07deb3..28a7bdc 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ volumes: dls-db: ``` -## Debian/Ubuntu (manual method using `git clone`) +## Debian/Ubuntu (manual method using `git clone` and python virtual environment) Tested on `Debian 11 (bullseye)`, Ubuntu may also work. @@ -175,6 +175,11 @@ Successful tested with: - Debian 12 (Bookworm) (works but not recommended because it is currently in *testing* state) - Ubuntu 22.10 (Kinetic Kudu) +Not working with: + +- Debian 11 (Bullseye) and lower (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)) + **Run this on your server instance** First go to [GitLab-Registry](https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/packages) and select your @@ -201,13 +206,17 @@ Packages are available here: ```shell pacman -Sy FILENAME=/opt/fastapi-dls.pkg.tar.zst -url -o $FILENAME + +curl -o $FILENAME +# or +wget -O $FILENAME + pacman -U --noconfirm fastapi-dls.pkg.tar.zst ``` Start with `systemctl start fastapi-dls.service` and enable autostart with `systemctl enable fastapi-dls.service`. -## Let's Encrypt Certificate +## Let's Encrypt Certificate (optional) If you're using installation via docker, you can use `traefik`. Please refer to their documentation. @@ -261,26 +270,67 @@ Successfully tested with this package versions: ## Linux +Download *client-token* and place it into `/etc/nvidia/ClientConfigToken`: + +```shell +curl --insecure -L -X GET https:///-/client-token -o /etc/nvidia/ClientConfigToken/client_configuration_token_$(date '+%d-%m-%Y-%H-%M-%S').tok +# or +wget --no-check-certificate -O /etc/nvidia/ClientConfigToken/client_configuration_token_$(date '+%d-%m-%Y-%H-%M-%S').tok https:///-/client-token +``` + +Restart `nvidia-gridd` service: + ```shell -curl --insecure -L -X GET https:///client-token -o /etc/nvidia/ClientConfigToken/client_configuration_token_$(date '+%d-%m-%Y-%H-%M-%S').tok service nvidia-gridd restart +``` + +Check licensing status: + +```shell nvidia-smi -q | grep "License" ``` -## Windows +Output should be something like: -Download file and place it into `C:\Program Files\NVIDIA Corporation\vGPU Licensing\ClientConfigToken`. -Now restart `NvContainerLocalSystem` service. - -**Power-Shell** - -```Shell -curl.exe --insecure -L -X GET https:///client-token -o "C:\Program Files\NVIDIA Corporation\vGPU Licensing\ClientConfigToken\client_configuration_token_$($(Get-Date).tostring('dd-MM-yy-hh-mm-ss')).tok" -Restart-Service NVDisplay.ContainerLocalSystem -'C:\Program Files\NVIDIA Corporation\NVSMI\nvidia-smi.exe' -q | Select-String "License" +```text +vGPU Software Licensed Product + License Status : Licensed (Expiry: YYYY-M-DD hh:mm:ss GMT) ``` -## Endpoints +Done. For more information check [troubleshoot section](#troubleshoot). + +## Windows + +**Power-Shell** (run as administrator!) + +Download *client-token* and place it into `C:\Program Files\NVIDIA Corporation\vGPU Licensing\ClientConfigToken`: + +```shell +curl.exe --insecure -L -X GET https:///-/client-token -o "C:\Program Files\NVIDIA Corporation\vGPU Licensing\ClientConfigToken\client_configuration_token_$($(Get-Date).tostring('dd-MM-yy-hh-mm-ss')).tok" +``` + +Restart `NvContainerLocalSystem` service: + +```Shell +Restart-Service NVDisplay.ContainerLocalSystem +``` + +Check licensing status: + +```shell +& 'C:\Program Files\NVIDIA Corporation\NVSMI\nvidia-smi.exe' -q | Select-String "License" +``` + +Output should be something like: + +```text +vGPU Software Licensed Product + License Status : Licensed (Expiry: YYYY-M-DD hh:mm:ss GMT) +``` + +Done. For more information check [troubleshoot section](#troubleshoot). + +# Endpoints ### `GET /` diff --git a/app/main.py b/app/main.py index b7989e7..fbc69cc 100644 --- a/app/main.py +++ b/app/main.py @@ -9,7 +9,7 @@ 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 +from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta from calendar import timegm from jose import jws, jwk, jwt, JWTError @@ -50,6 +50,7 @@ INSTANCE_KEY_PUB = load_key(str(env('INSTANCE_KEY_PUB', join(dirname(__file__), TOKEN_EXPIRE_DELTA = relativedelta(days=int(env('TOKEN_EXPIRE_DAYS', 1)), hours=int(env('TOKEN_EXPIRE_HOURS', 0))) LEASE_EXPIRE_DELTA = relativedelta(days=int(env('LEASE_EXPIRE_DAYS', 90)), hours=int(env('LEASE_EXPIRE_HOURS', 0))) LEASE_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))) CORS_ORIGINS = str(env('CORS_ORIGINS', '')).split(',') if (env('CORS_ORIGINS')) else [f'https://{DLS_URL}'] jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256) @@ -143,7 +144,8 @@ async def _origins(request: Request, leases: bool = False): for origin in session.query(Origin).all(): x = origin.serialize() if leases: - x['leases'] = list(map(lambda _: _.serialize(), Lease.find_by_origin_ref(db, origin.origin_ref))) + serialize = dict(renewal_period=LEASE_RENEWAL_PERIOD, renewal_delta=LEASE_RENEWAL_DELTA) + x['leases'] = list(map(lambda _: _.serialize(**serialize), Lease.find_by_origin_ref(db, origin.origin_ref))) response.append(x) session.close() return JSONr(response) @@ -167,10 +169,12 @@ async def _leases(request: Request, origin: bool = False): session = sessionmaker(bind=db)() response = [] for lease in session.query(Lease).all(): - x = lease.serialize() + serialize = dict(renewal_period=LEASE_RENEWAL_PERIOD, renewal_delta=LEASE_RENEWAL_DELTA) + x = lease.serialize(**serialize) if origin: - # assume that each lease has a valid origin record - x['origin'] = session.query(Origin).filter(Origin.origin_ref == lease.origin_ref).first().serialize() + lease_origin = session.query(Origin).filter(Origin.origin_ref == lease.origin_ref).first() + if lease_origin is not None: + x['origin'] = lease_origin.serialize() response.append(x) session.close() return JSONr(response) diff --git a/app/orm.py b/app/orm.py index 0f1fdfe..ee4e084 100644 --- a/app/orm.py +++ b/app/orm.py @@ -1,4 +1,5 @@ -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone +from dateutil.relativedelta import relativedelta from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, update, and_, inspect from sqlalchemy.ext.declarative import declarative_base @@ -56,12 +57,12 @@ class Origin(Base): session.close() @staticmethod - def delete(engine: Engine, origin_ref: str = None) -> int: + def delete(engine: Engine, origins: ["Origin"] = None) -> int: session = sessionmaker(bind=engine)() - if origin_ref is None: + if origins is None: deletions = session.query(Origin).delete() else: - deletions = session.query(Origin).filter(Origin.origin_ref == origin_ref).delete() + deletions = session.query(Origin).filter(Origin.origin_ref in origins).delete() session.commit() session.close() return deletions @@ -81,7 +82,10 @@ class Lease(Base): def __repr__(self): return f'Lease(origin_ref={self.origin_ref}, lease_ref={self.lease_ref}, expires={self.lease_expires})' - def serialize(self) -> dict: + def serialize(self, renewal_period: float, renewal_delta: timedelta) -> dict: + lease_renewal = int(Lease.calculate_renewal(renewal_period, renewal_delta).total_seconds()) + lease_renewal = self.lease_updated + relativedelta(seconds=lease_renewal) + return { 'lease_ref': self.lease_ref, 'origin_ref': self.origin_ref, @@ -89,6 +93,7 @@ class Lease(Base): '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(), } @staticmethod @@ -156,6 +161,20 @@ class Lease(Base): session.close() return deletions + @staticmethod + def calculate_renewal(renewal_period: float, delta: timedelta) -> timedelta: + """ + import datetime + LEASE_RENEWAL_PERIOD=0.2 # 20% + delta = datetime.timedelta(days=1) + renew = delta.total_seconds() * LEASE_RENEWAL_PERIOD + renew = datetime.timedelta(seconds=renew) + expires = delta - renew # 19.2 + """ + renew = delta.total_seconds() * renewal_period + renew = timedelta(seconds=renew) + return renew + def init(engine: Engine): tables = [Origin, Lease] diff --git a/requirements.txt b/requirements.txt index b7f24ed..086a68a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.88.0 +fastapi==0.89.1 uvicorn[standard]==0.20.0 python-jose==3.3.0 pycryptodome==3.16.0 diff --git a/version.env b/version.env index 5be527b..93176fc 100644 --- a/version.env +++ b/version.env @@ -1 +1 @@ -VERSION=1.3 +VERSION=1.3.3