mirror of
				https://gitea.publichub.eu/oscar.krause/fastapi-dls.git
				synced 2025-11-04 12:36:09 +00:00 
			
		
		
		
	Merge branch 'dev' into 'main'
1.1 See merge request oscar.krause/fastapi-dls!14
This commit is contained in:
		@@ -1,5 +1,5 @@
 | 
			
		||||
Package: fastapi-dls
 | 
			
		||||
Version: 1.0.0
 | 
			
		||||
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, uvicorn, openssl
 | 
			
		||||
@@ -41,10 +41,29 @@ if [[ ! -f $CONFIG_DIR/env ]]; then
 | 
			
		||||
  echo "> Writing initial config ..."
 | 
			
		||||
  touch $CONFIG_DIR/env
 | 
			
		||||
  cat <<EOF >$CONFIG_DIR/env
 | 
			
		||||
# Toggle debug mode
 | 
			
		||||
#DEBUG=false
 | 
			
		||||
 | 
			
		||||
# Where the client can find the DLS server
 | 
			
		||||
DLS_URL=127.0.0.1
 | 
			
		||||
DLS_PORT=443
 | 
			
		||||
 | 
			
		||||
# CORS configuration
 | 
			
		||||
## comma separated list without spaces
 | 
			
		||||
#CORS_ORIGINS="https://$DLS_URL:$DLS_PORT"
 | 
			
		||||
 | 
			
		||||
# Lease expiration in days
 | 
			
		||||
LEASE_EXPIRE_DAYS=90
 | 
			
		||||
 | 
			
		||||
# Database location
 | 
			
		||||
## https://docs.sqlalchemy.org/en/14/core/engines.html
 | 
			
		||||
DATABASE=sqlite:///$CONFIG_DIR/db.sqlite
 | 
			
		||||
 | 
			
		||||
# UUIDs for identifying the instance
 | 
			
		||||
#SITE_KEY_XID="00000000-0000-0000-0000-000000000000"
 | 
			
		||||
#INSTANCE_REF="00000000-0000-0000-0000-000000000000"
 | 
			
		||||
 | 
			
		||||
# Site-wide signing keys
 | 
			
		||||
INSTANCE_KEY_RSA=$CONFIG_DIR/instance.private.pem
 | 
			
		||||
INSTANCE_KEY_PUB=$CONFIG_DIR/instance.public.pem
 | 
			
		||||
 | 
			
		||||
@@ -75,7 +94,7 @@ if [[ -f $CONFIG_DIR/webserver.key ]]; then
 | 
			
		||||
  if [ -x "$(command -v curl)" ]; then
 | 
			
		||||
    echo "> Testing API ..."
 | 
			
		||||
    source $CONFIG_DIR/env
 | 
			
		||||
    curl --insecure -X GET https://$DLS_URL:$DLS_PORT/status
 | 
			
		||||
    curl --insecure -X GET https://$DLS_URL:$DLS_PORT/-/health
 | 
			
		||||
  else
 | 
			
		||||
    echo "> Testing API failed, curl not available. Please test manually!"
 | 
			
		||||
  fi
 | 
			
		||||
							
								
								
									
										49
									
								
								.PKGBUILD/PKGBUILD
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								.PKGBUILD/PKGBUILD
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
# Maintainer: samicrusader <hi@samicrusader.me>
 | 
			
		||||
# Maintainer: Oscar Krause <oscar.krause@collinwebdesigns.de>
 | 
			
		||||
 | 
			
		||||
pkgname=fastapi-dls
 | 
			
		||||
pkgver=0.0
 | 
			
		||||
pkgrel=1
 | 
			
		||||
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' 'uvicorn' 'python-markdown' 'openssl')
 | 
			
		||||
provider=("$pkgname")
 | 
			
		||||
install="$pkgname.install"
 | 
			
		||||
source=('git+file:///builds/oscar.krause/fastapi-dls' # https://gitea.publichub.eu/oscar.krause/fastapi-dls.git
 | 
			
		||||
        "$pkgname.default"
 | 
			
		||||
        "$pkgname.service")
 | 
			
		||||
sha256sums=('SKIP'
 | 
			
		||||
            '4c07e9b627853bd4f3a398371912fc72302dac33f43e4cb7e9b79746cc9c9136'
 | 
			
		||||
            '10cb98d64f8bf37b11a60510793c187cc664e63c895d1205781c21fa2e703f32')
 | 
			
		||||
 | 
			
		||||
pkgver() {
 | 
			
		||||
  source $srcdir/$pkgname/version.env
 | 
			
		||||
  echo ${VERSION}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
check() {
 | 
			
		||||
    cd "$srcdir/$pkgname/test"
 | 
			
		||||
    mkdir "$srcdir/$pkgname/app/cert"
 | 
			
		||||
    openssl genrsa -out "$srcdir/$pkgname/app/cert/instance.private.pem" 2048
 | 
			
		||||
    openssl rsa -in "$srcdir/$pkgname/app/cert/instance.private.pem" -outform PEM -pubout -out "$srcdir/$pkgname/app/cert/instance.public.pem"
 | 
			
		||||
    python "$srcdir/$pkgname/test/main.py"
 | 
			
		||||
    rm -rf "$srcdir/$pkgname/app/cert"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
package() {
 | 
			
		||||
    install -d "$pkgdir/usr/share/doc/$pkgname"
 | 
			
		||||
    install -d "$pkgdir/var/lib/$pkgname/cert"
 | 
			
		||||
    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"
 | 
			
		||||
    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"
 | 
			
		||||
    install -Dm644 "$srcdir/$pkgname.default" "$pkgdir/etc/default/$pkgname"
 | 
			
		||||
    install -Dm644 "$srcdir/$pkgname.service" "$pkgdir/usr/lib/systemd/system/$pkgname.service"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								.PKGBUILD/fastapi-dls.default
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.PKGBUILD/fastapi-dls.default
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Toggle FastAPI debug mode
 | 
			
		||||
DEBUG=false
 | 
			
		||||
 | 
			
		||||
# Where the client can find the DLS server
 | 
			
		||||
## DLS_URL should be a hostname
 | 
			
		||||
DLS_URL="localhost.localdomain"
 | 
			
		||||
DLS_PORT=8443
 | 
			
		||||
CORS_ORIGINS="https://$DLS_URL:$DLS_PORT"
 | 
			
		||||
 | 
			
		||||
# Lease expiration in days
 | 
			
		||||
LEASE_EXPIRE_DAYS=90
 | 
			
		||||
 | 
			
		||||
# Database location
 | 
			
		||||
## https://docs.sqlalchemy.org/en/14/core/engines.html
 | 
			
		||||
DATABASE="sqlite:////var/lib/fastapi-dls/db.sqlite"
 | 
			
		||||
 | 
			
		||||
# UUIDs for identifying the instance
 | 
			
		||||
SITE_KEY_XID="<<sitekey>>"
 | 
			
		||||
INSTANCE_REF="<<instanceref>>"
 | 
			
		||||
 | 
			
		||||
# Site-wide signing keys
 | 
			
		||||
INSTANCE_KEY_RSA="/var/lib/fastapi-dls/instance.private.pem"
 | 
			
		||||
INSTANCE_KEY_PUB="/var/lib/fastapi-dls/instance.public.pem"
 | 
			
		||||
							
								
								
									
										14
									
								
								.PKGBUILD/fastapi-dls.install
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.PKGBUILD/fastapi-dls.install
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
post_install() {
 | 
			
		||||
    sed -i "s/<<sitekey>>/$(uuidgen)/" /etc/default/fastapi-dls
 | 
			
		||||
    sed -i "s/<<instanceref>>/$(uuidgen)/" /etc/default/fastapi-dls
 | 
			
		||||
 | 
			
		||||
    echo 'The environment variables for this server can be edited at: /etc/default/fastapi-dls'
 | 
			
		||||
    echo 'The server can be started with: systemctl start fastapi-dls.service'
 | 
			
		||||
    echo
 | 
			
		||||
    echo 'A valid HTTPS certificate needs to be installed to /var/lib/fastapi-dls/cert/webserver.{crt,key}'
 | 
			
		||||
    echo 'A self-signed certificate can be generated with: openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /var/lib/fastapi-dls/cert/webserver.key -out /var/lib/fastapi-dls/cert/webserver.crt'
 | 
			
		||||
    echo
 | 
			
		||||
    echo 'The signing keys for your instance need to be generated as well. Generate them with these commands:'
 | 
			
		||||
    echo 'openssl genrsa -out /var/lib/fastapi-dls/instance.private.pem 2048'
 | 
			
		||||
    echo 'openssl rsa -in /var/lib/fastapi-dls/instance.private.pem -outform PEM -pubout -out /var/lib/fastapi-dls/instance.public.pem'
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								.PKGBUILD/fastapi-dls.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.PKGBUILD/fastapi-dls.service
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=FastAPI-DLS
 | 
			
		||||
Documentation=https://git.collinwebdesigns.de/oscar.krause/fastapi-dls
 | 
			
		||||
After=network.target
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=forking
 | 
			
		||||
EnvironmentFile=/etc/default/fastapi-dls
 | 
			
		||||
ExecStart=/usr/bin/python /opt/fastapi-dls/main.py
 | 
			
		||||
WorkingDir=/opt/fastapi-dls
 | 
			
		||||
Restart=on-abort
 | 
			
		||||
User=root
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
							
								
								
									
										191
									
								
								.gitlab-ci.yml
									
									
									
									
									
								
							
							
						
						
									
										191
									
								
								.gitlab-ci.yml
									
									
									
									
									
								
							@@ -1,17 +1,45 @@
 | 
			
		||||
cache:
 | 
			
		||||
  key: one-key-to-rule-them-all
 | 
			
		||||
 | 
			
		||||
build:debian:
 | 
			
		||||
  # debian:bullseye-slim
 | 
			
		||||
  image: debian:bookworm-slim  # just to get "python3-jose" working
 | 
			
		||||
build:docker:
 | 
			
		||||
  image: docker:dind
 | 
			
		||||
  interruptible: true
 | 
			
		||||
  stage: build
 | 
			
		||||
  rules:
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
 | 
			
		||||
      changes:
 | 
			
		||||
        - app/**/*
 | 
			
		||||
        - Dockerfile
 | 
			
		||||
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
 | 
			
		||||
  tags: [ docker ]
 | 
			
		||||
  before_script:
 | 
			
		||||
    - echo "COMMIT=${CI_COMMIT_SHA}" >> version.env  # COMMIT=`git rev-parse HEAD`
 | 
			
		||||
  script:
 | 
			
		||||
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
 | 
			
		||||
    - docker build . --tag ${CI_REGISTRY}/${CI_PROJECT_PATH}/${CI_BUILD_REF_NAME}:${CI_BUILD_REF}
 | 
			
		||||
    - docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}/${CI_BUILD_REF_NAME}:${CI_BUILD_REF}
 | 
			
		||||
 | 
			
		||||
build:apt:
 | 
			
		||||
  image: debian:bookworm-slim
 | 
			
		||||
  interruptible: true
 | 
			
		||||
  stage: build
 | 
			
		||||
  rules:
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
 | 
			
		||||
      changes:
 | 
			
		||||
        - app/**/*
 | 
			
		||||
        - .DEBIAN/**/*
 | 
			
		||||
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
 | 
			
		||||
  before_script:
 | 
			
		||||
    - echo "COMMIT=${CI_COMMIT_SHA}" >> version.env
 | 
			
		||||
    - source version.env
 | 
			
		||||
    # install build dependencies
 | 
			
		||||
    - apt-get update -qq && apt-get install -qq -y build-essential
 | 
			
		||||
    - chmod 0755 -R .
 | 
			
		||||
    # create build directory for .deb sources
 | 
			
		||||
    - mkdir build
 | 
			
		||||
    # copy install instructions
 | 
			
		||||
    - cp -r DEBIAN build/
 | 
			
		||||
    - cp -r .DEBIAN build/DEBIAN
 | 
			
		||||
    - chmod -R 0775 build/DEBIAN
 | 
			
		||||
    # copy app into "/usr/share/fastapi-dls" as "/usr/share/fastapi-dls/app" & copy README.md and version.env
 | 
			
		||||
    - mkdir -p build/usr/share/fastapi-dls
 | 
			
		||||
    - cp -r app build/usr/share/fastapi-dls
 | 
			
		||||
@@ -22,29 +50,55 @@ build:debian:
 | 
			
		||||
    # cd into "build/"
 | 
			
		||||
    - cd build/
 | 
			
		||||
  script:
 | 
			
		||||
    # set version based on value in "$VERSION" (which is set above from version.env)
 | 
			
		||||
    - sed -i -E 's/(Version\:\s)0.0/\1'"$VERSION"'/g' DEBIAN/control
 | 
			
		||||
    # build
 | 
			
		||||
    - dpkg -b . build.deb
 | 
			
		||||
    - dpkg -I build.deb
 | 
			
		||||
  artifacts:
 | 
			
		||||
    expire_in: 1 week
 | 
			
		||||
    paths:
 | 
			
		||||
      - build/build.deb
 | 
			
		||||
 | 
			
		||||
build:docker:
 | 
			
		||||
  image: docker:dind
 | 
			
		||||
build:pacman:
 | 
			
		||||
  image: archlinux:base-devel
 | 
			
		||||
  interruptible: true
 | 
			
		||||
  stage: build
 | 
			
		||||
  rules:
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
 | 
			
		||||
  tags: [ docker ]
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
 | 
			
		||||
      changes:
 | 
			
		||||
        - app/**/*
 | 
			
		||||
        - .PKGBUILD/**/*
 | 
			
		||||
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
 | 
			
		||||
  before_script:
 | 
			
		||||
    - echo "COMMIT=${CI_COMMIT_SHA}" >> version.env  # COMMIT=`git rev-parse HEAD`
 | 
			
		||||
    - echo "COMMIT=${CI_COMMIT_SHA}" >> version.env
 | 
			
		||||
    # install build dependencies
 | 
			
		||||
    - pacman -Syu --noconfirm git
 | 
			
		||||
    # create a build-user because "makepkg" don't like root user
 | 
			
		||||
    - useradd --no-create-home --shell=/bin/false build && usermod -L build
 | 
			
		||||
    - 'echo "build ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers'
 | 
			
		||||
    - 'echo "root ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers'
 | 
			
		||||
    - chown -R build:build .
 | 
			
		||||
    # move .PKGBUILD contents to root directory
 | 
			
		||||
    - mv .PKGBUILD/* .
 | 
			
		||||
  script:
 | 
			
		||||
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
 | 
			
		||||
    - docker build . --tag ${CI_REGISTRY}/${CI_PROJECT_PATH}/${CI_BUILD_REF_NAME}:${CI_BUILD_REF}
 | 
			
		||||
    - docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}/${CI_BUILD_REF_NAME}:${CI_BUILD_REF}
 | 
			
		||||
    - pwd
 | 
			
		||||
    # download dependencies
 | 
			
		||||
    - source PKGBUILD && pacman -Syu --noconfirm --needed --asdeps "${makedepends[@]}" "${depends[@]}"
 | 
			
		||||
    # build
 | 
			
		||||
    - sudo -u build makepkg -s
 | 
			
		||||
  artifacts:
 | 
			
		||||
    expire_in: 1 week
 | 
			
		||||
    paths:
 | 
			
		||||
      - "*.pkg.tar.zst"
 | 
			
		||||
 | 
			
		||||
test:
 | 
			
		||||
  image: python:3.10-slim-bullseye
 | 
			
		||||
  stage: test
 | 
			
		||||
  rules:
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH
 | 
			
		||||
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
 | 
			
		||||
  variables:
 | 
			
		||||
    DATABASE: sqlite:///../app/db.sqlite
 | 
			
		||||
  before_script:
 | 
			
		||||
@@ -57,37 +111,75 @@ test:
 | 
			
		||||
  script:
 | 
			
		||||
    - pytest main.py
 | 
			
		||||
 | 
			
		||||
test:debian:
 | 
			
		||||
  image: debian:bookworm-slim
 | 
			
		||||
.test:linux:
 | 
			
		||||
  stage: test
 | 
			
		||||
  rules:
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
 | 
			
		||||
      changes:
 | 
			
		||||
        - app/**/*
 | 
			
		||||
        - .DEBIAN/**/*
 | 
			
		||||
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
 | 
			
		||||
  needs:
 | 
			
		||||
    - job: build:apt
 | 
			
		||||
      artifacts: true
 | 
			
		||||
  variables:
 | 
			
		||||
    DEBIAN_FRONTEND: noninteractive
 | 
			
		||||
  needs:
 | 
			
		||||
    - job: build:debian
 | 
			
		||||
      artifacts: true
 | 
			
		||||
  before_script:
 | 
			
		||||
    - apt-get update -qq && apt-get install -qq -y jq
 | 
			
		||||
    - apt-get update -qq && apt-get install -qq -y jq curl
 | 
			
		||||
  script:
 | 
			
		||||
    # test installation
 | 
			
		||||
    - apt-get install -q -y ./build/build.deb --fix-missing
 | 
			
		||||
    - openssl req -x509 -newkey rsa:2048 -nodes -out /etc/fastapi-dls/webserver.crt -keyout /etc/fastapi-dls/webserver.key -days 7 -subj "/C=DE/O=GitLab-CI/OU=Test/CN=localhost"
 | 
			
		||||
    # copy example config from GitLab-CI-Variables
 | 
			
		||||
    #- cat ${EXAMPLE_CONFIG} > /etc/fastapi-dls/env
 | 
			
		||||
    # start service in background
 | 
			
		||||
    - uvicorn --host 127.0.0.1 --port 443
 | 
			
		||||
    - cd /usr/share/fastapi-dls/app
 | 
			
		||||
    - uvicorn main:app
 | 
			
		||||
      --host 127.0.0.1 --port 443
 | 
			
		||||
      --app-dir /usr/share/fastapi-dls/app
 | 
			
		||||
      --ssl-keyfile /etc/fastapi-dls/webserver.key
 | 
			
		||||
      --ssl-certfile /opt/fastapi-dls/webserver.crt
 | 
			
		||||
      --ssl-certfile /etc/fastapi-dls/webserver.crt
 | 
			
		||||
      --proxy-headers &
 | 
			
		||||
    - FASTAPI_DLS_PID=$!
 | 
			
		||||
    - echo "Started service with pid $FASTAPI_DLS_PID"
 | 
			
		||||
    # testing service
 | 
			
		||||
    - if [ "`curl --insecure -s https://127.0.0.1/status | jq .status`" != "up" ]; then echo "Success"; else "Error"; fi
 | 
			
		||||
    - if [ "`curl --insecure -s https://127.0.0.1/-/health | jq .status`" != "up" ]; then echo "Success"; else "Error"; fi
 | 
			
		||||
    # cleanup
 | 
			
		||||
    - kill $FASTAPI_DLS_PID
 | 
			
		||||
    - apt-get purge -qq -y fastapi-dls
 | 
			
		||||
    - apt-get autoremove -qq -y && apt-get clean -qq
 | 
			
		||||
 | 
			
		||||
test:debian:
 | 
			
		||||
  extends: .test:linux
 | 
			
		||||
  image: debian:bookworm-slim
 | 
			
		||||
 | 
			
		||||
test:ubuntu:
 | 
			
		||||
  extends: .test:linux
 | 
			
		||||
  image: ubuntu:22.10
 | 
			
		||||
 | 
			
		||||
test:archlinux:
 | 
			
		||||
  image: archlinux:base
 | 
			
		||||
  rules:
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
 | 
			
		||||
      changes:
 | 
			
		||||
        - app/**/*
 | 
			
		||||
        - .PKGBUILD/**/*
 | 
			
		||||
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
 | 
			
		||||
  needs:
 | 
			
		||||
    - job: build:pacman
 | 
			
		||||
      artifacts: true
 | 
			
		||||
  script:
 | 
			
		||||
    - pacman -Sy
 | 
			
		||||
    - pacman -U --noconfirm *.pkg.tar.zst
 | 
			
		||||
 | 
			
		||||
.deploy:
 | 
			
		||||
  rules:
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
 | 
			
		||||
    - if: $CI_COMMIT_TAG
 | 
			
		||||
      when: never
 | 
			
		||||
 | 
			
		||||
deploy:docker:
 | 
			
		||||
  extends: .deploy
 | 
			
		||||
  stage: deploy
 | 
			
		||||
  rules:
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
 | 
			
		||||
@@ -109,14 +201,15 @@ deploy:docker:
 | 
			
		||||
    - docker push $PUBLIC_REGISTRY_USER/${CI_PROJECT_NAME}:${VERSION}
 | 
			
		||||
    - docker push $PUBLIC_REGISTRY_USER/${CI_PROJECT_NAME}:latest
 | 
			
		||||
 | 
			
		||||
deploy:debian:
 | 
			
		||||
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_BRANCH == $CI_DEFAULT_BRANCH
 | 
			
		||||
  rules:
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
 | 
			
		||||
  needs:
 | 
			
		||||
    - job: build:debian
 | 
			
		||||
    - job: build:apt
 | 
			
		||||
      artifacts: true
 | 
			
		||||
  before_script:
 | 
			
		||||
    - apt-get update -qq && apt-get install -qq -y curl lsb-release
 | 
			
		||||
@@ -149,3 +242,49 @@ deploy:debian:
 | 
			
		||||
    # using generic-package-registry until debian-registry is GA
 | 
			
		||||
    # https://docs.gitlab.com/ee/user/packages/generic_packages/index.html#publish-a-generic-package-by-using-cicd
 | 
			
		||||
    - '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_BRANCH == $CI_DEFAULT_BRANCH
 | 
			
		||||
  needs:
 | 
			
		||||
    - job: build:pacman
 | 
			
		||||
      artifacts: true
 | 
			
		||||
  script:
 | 
			
		||||
    - source .PKGBUILD/PKGBUILD
 | 
			
		||||
    # fastapi-dls-1.0-1-any.pkg.tar.zst
 | 
			
		||||
    - BUILD_NAME=${pkgname}-${pkgver}-${pkgrel}-any.pkg.tar.zst
 | 
			
		||||
    - PACKAGE_NAME=${pkgname}
 | 
			
		||||
    - PACKAGE_VERSION=${pkgver}
 | 
			
		||||
    - PACKAGE_ARCH=any
 | 
			
		||||
    - EXPORT_NAME=${BUILD_NAME}
 | 
			
		||||
    - 'echo "PACKAGE_NAME:    ${PACKAGE_NAME}"'
 | 
			
		||||
    - 'echo "PACKAGE_VERSION: ${PACKAGE_VERSION}"'
 | 
			
		||||
    - 'echo "PACKAGE_ARCH:    ${PACKAGE_ARCH}"'
 | 
			
		||||
    - '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:
 | 
			
		||||
  image: registry.gitlab.com/gitlab-org/release-cli:latest
 | 
			
		||||
  stage: .post
 | 
			
		||||
  rules:
 | 
			
		||||
    - if: $CI_COMMIT_TAG
 | 
			
		||||
      when: never
 | 
			
		||||
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
 | 
			
		||||
  before_script:
 | 
			
		||||
    - source version.env
 | 
			
		||||
  script:
 | 
			
		||||
    - echo "Running release-job for $VERSION"
 | 
			
		||||
  release:
 | 
			
		||||
    name: $CI_PROJECT_TITLE $version
 | 
			
		||||
    description: Release of $CI_PROJECT_TITLE version $VERSION
 | 
			
		||||
    tag_name: $VERSION
 | 
			
		||||
    ref: $CI_COMMIT_SHA
 | 
			
		||||
    assets:
 | 
			
		||||
      links:
 | 
			
		||||
        - name: 'Package Registry'
 | 
			
		||||
          url: 'https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/packages'
 | 
			
		||||
        - name: 'Container Registry'
 | 
			
		||||
          url: 'https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/container_registry/40'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								CODEOWNERS
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								CODEOWNERS
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
* @oscar.krause
 | 
			
		||||
.PKGBUILD/ @samicrusader
 | 
			
		||||
@@ -14,5 +14,5 @@ COPY app /app
 | 
			
		||||
COPY version.env /version.env
 | 
			
		||||
COPY README.md /README.md
 | 
			
		||||
 | 
			
		||||
HEALTHCHECK --start-period=30s --interval=10s --timeout=5s --retries=3 CMD curl --insecure --fail https://localhost/status || exit 1
 | 
			
		||||
HEALTHCHECK --start-period=30s --interval=10s --timeout=5s --retries=3 CMD curl --insecure --fail https://localhost/-/health || exit 1
 | 
			
		||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "443", "--app-dir", "/app", "--proxy-headers", "--ssl-keyfile", "/app/cert/webserver.key", "--ssl-certfile", "/app/cert/webserver.crt"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										93
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										93
									
								
								README.md
									
									
									
									
									
								
							@@ -9,31 +9,58 @@ Only the clients need a connection to this service on configured port.
 | 
			
		||||
 | 
			
		||||
## ToDo's
 | 
			
		||||
 | 
			
		||||
- migrate from `fastapi` to `flask`
 | 
			
		||||
- Support http mode for using external https proxy (disable uvicorn ssl for using behind proxy)
 | 
			
		||||
 | 
			
		||||
## Endpoints
 | 
			
		||||
 | 
			
		||||
### `GET /`
 | 
			
		||||
### [`GET /`](/)
 | 
			
		||||
 | 
			
		||||
HTML rendered README.md.
 | 
			
		||||
Redirect to `/-/readme`.
 | 
			
		||||
 | 
			
		||||
### `GET /status`
 | 
			
		||||
### [`GET /status`](/status) (deprecated: use `/-/health`)
 | 
			
		||||
 | 
			
		||||
Status endpoint, used for *healthcheck*. Shows also current version and commit hash.
 | 
			
		||||
 | 
			
		||||
### `GET /docs`
 | 
			
		||||
### [`GET /-/health`](/-/health)
 | 
			
		||||
 | 
			
		||||
OpenAPI specifications rendered from `GET /openapi.json`.
 | 
			
		||||
Status endpoint, used for *healthcheck*. Shows also current version and commit hash.
 | 
			
		||||
 | 
			
		||||
### `GET /-/origins`
 | 
			
		||||
### [`GET /-/readme`](/-/readme)
 | 
			
		||||
 | 
			
		||||
HTML rendered README.md.
 | 
			
		||||
 | 
			
		||||
### [`GET /-/docs`](/-/docs), [`GET /-/redocs`](/-/redocs)
 | 
			
		||||
 | 
			
		||||
OpenAPI specifications rendered from `GET /-/openapi.json`.
 | 
			
		||||
 | 
			
		||||
### [`GET /-/manage`](/-/manage)
 | 
			
		||||
 | 
			
		||||
Shows a very basic UI to delete origins or leases.
 | 
			
		||||
 | 
			
		||||
### `GET /-/origins?leases=false`
 | 
			
		||||
 | 
			
		||||
List registered origins.
 | 
			
		||||
 | 
			
		||||
### `GET /-/leases`
 | 
			
		||||
| Query Parameter | Default | Usage                                |
 | 
			
		||||
|-----------------|---------|--------------------------------------|
 | 
			
		||||
| `leases`        | `false` | Include referenced leases per origin |
 | 
			
		||||
 | 
			
		||||
### `DELETE /-/origins`
 | 
			
		||||
 | 
			
		||||
Deletes all origins and their leases.
 | 
			
		||||
 | 
			
		||||
### `GET /-/leases?origin=false`
 | 
			
		||||
 | 
			
		||||
List current leases.
 | 
			
		||||
 | 
			
		||||
| Query Parameter | Default | Usage                               |
 | 
			
		||||
|-----------------|---------|-------------------------------------|
 | 
			
		||||
| `origin`        | `false` | Include referenced origin per lease |
 | 
			
		||||
 | 
			
		||||
### `DELETE /-/lease/{lease_ref}`
 | 
			
		||||
 | 
			
		||||
Deletes an lease.
 | 
			
		||||
 | 
			
		||||
### `GET /client-token`
 | 
			
		||||
 | 
			
		||||
Generate client token, (see [installation](#installation)).
 | 
			
		||||
@@ -200,7 +227,7 @@ Packages are available here:
 | 
			
		||||
 | 
			
		||||
Successful tested with:
 | 
			
		||||
 | 
			
		||||
- Debian 12 (Bookworm)
 | 
			
		||||
- Debian 12 (Bookworm) (works but not recommended because it is currently in *testing* state)
 | 
			
		||||
- Ubuntu 22.10 (Kinetic Kudu)
 | 
			
		||||
 | 
			
		||||
**Run this on your server instance**
 | 
			
		||||
@@ -218,6 +245,23 @@ apt-get install -f --fix-missing
 | 
			
		||||
 | 
			
		||||
Start with `systemctl start fastapi-dls.service` and enable autostart with `systemctl enable fastapi-dls.service`.
 | 
			
		||||
 | 
			
		||||
## ArchLinux (using `pacman`)
 | 
			
		||||
 | 
			
		||||
**Shout out to `samicrusader` who created build file for ArchLinux!**
 | 
			
		||||
 | 
			
		||||
Packages are available here:
 | 
			
		||||
 | 
			
		||||
- [GitLab-Registry](https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/packages)
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
pacman -Sy
 | 
			
		||||
FILENAME=/opt/fastapi-dls.pkg.tar.zst
 | 
			
		||||
url -o $FILENAME <download-url>
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
If you're using installation via docker, you can use `traefik`. Please refer to their documentation.
 | 
			
		||||
@@ -236,18 +280,18 @@ After first success you have to replace `--issue` with `--renew`.
 | 
			
		||||
 | 
			
		||||
# Configuration
 | 
			
		||||
 | 
			
		||||
| Variable            | Default                                | Usage                                                                                 |
 | 
			
		||||
|---------------------|----------------------------------------|---------------------------------------------------------------------------------------|
 | 
			
		||||
| `DEBUG`             | `false`                                | Toggles `fastapi` debug mode                                                          |
 | 
			
		||||
| `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             |
 | 
			
		||||
| `LEASE_EXPIRE_DAYS` | `90`                                   | Lease time in days                                                                    |
 | 
			
		||||
| `DATABASE`          | `sqlite:///db.sqlite`                  | See [official dataset docs](https://dataset.readthedocs.io/en/latest/quickstart.html) |
 | 
			
		||||
| `CORS_ORIGINS`      | `https://{DLS_URL}`                    | Sets `Access-Control-Allow-Origin` header (comma separated string)                    |
 | 
			
		||||
| `SITE_KEY_XID`      | `00000000-0000-0000-0000-000000000000` | Site identification uuid                                                              |
 | 
			
		||||
| `INSTANCE_REF`      | `00000000-0000-0000-0000-000000000000` | Instance identification uuid                                                          |
 | 
			
		||||
| `INSTANCE_KEY_RSA`  | `<app-dir>/cert/instance.private.pem`  | Site-wide private RSA key for singing JWTs                                            |
 | 
			
		||||
| `INSTANCE_KEY_PUB`  | `<app-dir>/cert/instance.public.pem`   | Site-wide public key                                                                  |
 | 
			
		||||
| Variable            | Default                                | Usage                                                                               |
 | 
			
		||||
|---------------------|----------------------------------------|-------------------------------------------------------------------------------------|
 | 
			
		||||
| `DEBUG`             | `false`                                | Toggles `fastapi` debug mode                                                        |
 | 
			
		||||
| `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           |
 | 
			
		||||
| `LEASE_EXPIRE_DAYS` | `90`                                   | Lease time in days                                                                  |
 | 
			
		||||
| `DATABASE`          | `sqlite:///db.sqlite`                  | See [official SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html) |
 | 
			
		||||
| `CORS_ORIGINS`      | `https://{DLS_URL}`                    | Sets `Access-Control-Allow-Origin` header (comma separated string)                  |
 | 
			
		||||
| `SITE_KEY_XID`      | `00000000-0000-0000-0000-000000000000` | Site identification uuid                                                            |
 | 
			
		||||
| `INSTANCE_REF`      | `00000000-0000-0000-0000-000000000000` | Instance identification uuid                                                        |
 | 
			
		||||
| `INSTANCE_KEY_RSA`  | `<app-dir>/cert/instance.private.pem`  | Site-wide private RSA key for singing JWTs                                          |
 | 
			
		||||
| `INSTANCE_KEY_PUB`  | `<app-dir>/cert/instance.public.pem`   | Site-wide public key                                                                |
 | 
			
		||||
 | 
			
		||||
# Setup (Client)
 | 
			
		||||
 | 
			
		||||
@@ -376,3 +420,10 @@ Dec 20 17:53:34 ubuntu-grid-server nvidia-gridd[10354]: License acquired success
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
</details>
 | 
			
		||||
 | 
			
		||||
# Credits
 | 
			
		||||
 | 
			
		||||
Thanks to vGPU community and all who uses this project and report bugs.
 | 
			
		||||
 | 
			
		||||
Special thanks to @samicrusader who created build file for ArchLinux.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										150
									
								
								app/main.py
									
									
									
									
									
								
							
							
						
						
									
										150
									
								
								app/main.py
									
									
									
									
									
								
							@@ -3,12 +3,11 @@ from base64 import b64encode as b64enc
 | 
			
		||||
from hashlib import sha256
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
from os.path import join, dirname
 | 
			
		||||
from os import getenv
 | 
			
		||||
from os import getenv as env
 | 
			
		||||
 | 
			
		||||
from dotenv import load_dotenv
 | 
			
		||||
from fastapi import FastAPI, HTTPException
 | 
			
		||||
from fastapi.requests import Request
 | 
			
		||||
from fastapi.encoders import jsonable_encoder
 | 
			
		||||
import json
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from dateutil.relativedelta import relativedelta
 | 
			
		||||
@@ -16,56 +15,33 @@ from calendar import timegm
 | 
			
		||||
from jose import jws, jwk, jwt
 | 
			
		||||
from jose.constants import ALGORITHMS
 | 
			
		||||
from starlette.middleware.cors import CORSMiddleware
 | 
			
		||||
from starlette.responses import StreamingResponse, JSONResponse, HTMLResponse
 | 
			
		||||
from starlette.responses import StreamingResponse, JSONResponse, HTMLResponse, Response, RedirectResponse
 | 
			
		||||
from sqlalchemy import create_engine
 | 
			
		||||
from sqlalchemy.orm import sessionmaker
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    # Crypto | Cryptodome on Debian
 | 
			
		||||
    from Crypto.PublicKey import RSA
 | 
			
		||||
    from Crypto.PublicKey.RSA import RsaKey
 | 
			
		||||
except ModuleNotFoundError:
 | 
			
		||||
    from Cryptodome.PublicKey import RSA
 | 
			
		||||
    from Cryptodome.PublicKey.RSA import RsaKey
 | 
			
		||||
from orm import Origin, Lease, init as db_init
 | 
			
		||||
from util import load_key, load_file
 | 
			
		||||
from orm import Origin, Lease, init as db_init, migrate
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger()
 | 
			
		||||
load_dotenv('../version.env')
 | 
			
		||||
 | 
			
		||||
VERSION, COMMIT, DEBUG = getenv('VERSION', 'unknown'), getenv('COMMIT', 'unknown'), bool(getenv('DEBUG', False))
 | 
			
		||||
VERSION, COMMIT, DEBUG = env('VERSION', 'unknown'), env('COMMIT', 'unknown'), bool(env('DEBUG', False))
 | 
			
		||||
 | 
			
		||||
config = 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)
 | 
			
		||||
 | 
			
		||||
def load_file(filename) -> bytes:
 | 
			
		||||
    with open(filename, 'rb') as file:
 | 
			
		||||
        content = file.read()
 | 
			
		||||
    return content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def load_key(filename) -> RsaKey:
 | 
			
		||||
    return RSA.import_key(extern_key=load_file(filename), passphrase=None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# todo: initialize certificate (or should be done by user, and passed through "volumes"?)
 | 
			
		||||
 | 
			
		||||
__details = dict(
 | 
			
		||||
    title='FastAPI-DLS',
 | 
			
		||||
    description='Minimal Delegated License Service (DLS).',
 | 
			
		||||
    version=VERSION,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
app, db = FastAPI(**__details), create_engine(str(getenv('DATABASE', 'sqlite:///db.sqlite')))
 | 
			
		||||
db_init(db)
 | 
			
		||||
 | 
			
		||||
DLS_URL = str(getenv('DLS_URL', 'localhost'))
 | 
			
		||||
DLS_PORT = int(getenv('DLS_PORT', '443'))
 | 
			
		||||
SITE_KEY_XID = str(getenv('SITE_KEY_XID', '00000000-0000-0000-0000-000000000000'))
 | 
			
		||||
INSTANCE_REF = str(getenv('INSTANCE_REF', '00000000-0000-0000-0000-000000000000'))
 | 
			
		||||
INSTANCE_KEY_RSA = load_key(str(getenv('INSTANCE_KEY_RSA', join(dirname(__file__), 'cert/instance.private.pem'))))
 | 
			
		||||
INSTANCE_KEY_PUB = load_key(str(getenv('INSTANCE_KEY_PUB', join(dirname(__file__), 'cert/instance.public.pem'))))
 | 
			
		||||
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'))
 | 
			
		||||
INSTANCE_REF = str(env('INSTANCE_REF', '00000000-0000-0000-0000-000000000000'))
 | 
			
		||||
INSTANCE_KEY_RSA = load_key(str(env('INSTANCE_KEY_RSA', join(dirname(__file__), 'cert/instance.private.pem'))))
 | 
			
		||||
INSTANCE_KEY_PUB = load_key(str(env('INSTANCE_KEY_PUB', join(dirname(__file__), 'cert/instance.public.pem'))))
 | 
			
		||||
TOKEN_EXPIRE_DELTA = relativedelta(hours=1)  # days=1
 | 
			
		||||
LEASE_EXPIRE_DELTA = relativedelta(days=int(getenv('LEASE_EXPIRE_DAYS', 90)))
 | 
			
		||||
LEASE_EXPIRE_DELTA = relativedelta(days=int(env('LEASE_EXPIRE_DAYS', 90)))
 | 
			
		||||
 | 
			
		||||
CORS_ORIGINS = getenv('CORS_ORIGINS').split(',') if (getenv('CORS_ORIGINS')) else f'https://{DLS_URL}'  # todo: prevent static https
 | 
			
		||||
CORS_ORIGINS = env('CORS_ORIGINS').split(',') if (env('CORS_ORIGINS')) else f'https://{DLS_URL}'  # todo: prevent static https
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
@@ -88,36 +64,104 @@ def get_token(request: Request) -> dict:
 | 
			
		||||
    return jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.get('/')
 | 
			
		||||
@app.get('/', summary='* Index')
 | 
			
		||||
async def index():
 | 
			
		||||
    return RedirectResponse('/-/readme')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.get('/status', summary='* Status', description='Returns current service status, version (incl. git-commit) and some variables.', deprecated=True)
 | 
			
		||||
async def status(request: Request):
 | 
			
		||||
    return JSONResponse({'status': 'up', 'version': VERSION, 'commit': COMMIT, 'debug': DEBUG})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.get('/-/health', summary='* Health')
 | 
			
		||||
async def _health(request: Request):
 | 
			
		||||
    return JSONResponse({'status': 'up', 'version': VERSION, 'commit': COMMIT, 'debug': DEBUG})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.get('/-/readme', summary='* Readme')
 | 
			
		||||
async def _readme():
 | 
			
		||||
    from markdown import markdown
 | 
			
		||||
    content = load_file('../README.md').decode('utf-8')
 | 
			
		||||
    return HTMLResponse(markdown(text=content, extensions=['tables', 'fenced_code', 'md_in_html', 'nl2br', 'toc']))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.get('/status')
 | 
			
		||||
async def status(request: Request):
 | 
			
		||||
    return JSONResponse({'status': 'up', 'version': VERSION, 'commit': COMMIT, 'debug': DEBUG})
 | 
			
		||||
@app.get('/-/manage', summary='* Management UI')
 | 
			
		||||
async def _manage(request: Request):
 | 
			
		||||
    response = '''
 | 
			
		||||
    <!DOCTYPE html>
 | 
			
		||||
    <html>
 | 
			
		||||
        <head>
 | 
			
		||||
            <title>FastAPI-DLS Management</title>
 | 
			
		||||
        </head>
 | 
			
		||||
        <body>
 | 
			
		||||
            <button onclick="deleteOrigins()">delete origins and their leases</button>
 | 
			
		||||
            <button onclick="deleteLease()">delete specific lease</button>
 | 
			
		||||
            
 | 
			
		||||
            <script>
 | 
			
		||||
                function deleteOrigins() {
 | 
			
		||||
                    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 HTMLResponse(response)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.get('/-/origins')
 | 
			
		||||
async def _origins(request: Request):
 | 
			
		||||
@app.get('/-/origins', summary='* Origins')
 | 
			
		||||
async def _origins(request: Request, leases: bool = False):
 | 
			
		||||
    session = sessionmaker(bind=db)()
 | 
			
		||||
    response = list(map(lambda x: jsonable_encoder(x), session.query(Origin).all()))
 | 
			
		||||
    response = []
 | 
			
		||||
    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)))
 | 
			
		||||
        response.append(x)
 | 
			
		||||
    session.close()
 | 
			
		||||
    return JSONResponse(response)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.get('/-/leases')
 | 
			
		||||
async def _leases(request: Request):
 | 
			
		||||
@app.delete('/-/origins', summary='* Origins')
 | 
			
		||||
async def _origins_delete(request: Request):
 | 
			
		||||
    Origin.delete(db)
 | 
			
		||||
    return Response(status_code=201)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.get('/-/leases', summary='* Leases')
 | 
			
		||||
async def _leases(request: Request, origin: bool = False):
 | 
			
		||||
    session = sessionmaker(bind=db)()
 | 
			
		||||
    response = list(map(lambda x: jsonable_encoder(x), session.query(Lease).all()))
 | 
			
		||||
    response = []
 | 
			
		||||
    for lease in session.query(Lease).all():
 | 
			
		||||
        x = lease.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()
 | 
			
		||||
        response.append(x)
 | 
			
		||||
    session.close()
 | 
			
		||||
    return JSONResponse(response)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@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)
 | 
			
		||||
    raise HTTPException(status_code=404, detail='lease not found')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# venv/lib/python3.9/site-packages/nls_core_service_instance/service_instance_token_manager.py
 | 
			
		||||
@app.get('/client-token')
 | 
			
		||||
@app.get('/client-token', summary='* Client-Token')
 | 
			
		||||
async def client_token():
 | 
			
		||||
    cur_time = datetime.utcnow()
 | 
			
		||||
    exp_time = cur_time + relativedelta(years=12)
 | 
			
		||||
@@ -130,7 +174,7 @@ async def client_token():
 | 
			
		||||
        "nbf": timegm(cur_time.timetuple()),
 | 
			
		||||
        "exp": timegm(exp_time.timetuple()),
 | 
			
		||||
        "update_mode": "ABSOLUTE",
 | 
			
		||||
        "scope_ref_list": [str(uuid4())],
 | 
			
		||||
        "scope_ref_list": [str(uuid4())],  # this is our LEASE_REF
 | 
			
		||||
        "fulfillment_class_ref_list": [],
 | 
			
		||||
        "service_instance_configuration": {
 | 
			
		||||
            "nls_service_instance_ref": INSTANCE_REF,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										90
									
								
								app/orm.py
									
									
									
									
									
								
							
							
						
						
									
										90
									
								
								app/orm.py
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, UniqueConstraint, update, and_, delete, inspect
 | 
			
		||||
from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, update, and_, inspect
 | 
			
		||||
from sqlalchemy.ext.declarative import declarative_base
 | 
			
		||||
from sqlalchemy.engine import Engine
 | 
			
		||||
from sqlalchemy.orm import sessionmaker
 | 
			
		||||
@@ -21,6 +21,15 @@ class Origin(Base):
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return f'Origin(origin_ref={self.origin_ref}, hostname={self.hostname})'
 | 
			
		||||
 | 
			
		||||
    def serialize(self) -> dict:
 | 
			
		||||
        return {
 | 
			
		||||
            'origin_ref': self.origin_ref,
 | 
			
		||||
            'hostname': self.hostname,
 | 
			
		||||
            'guest_driver_version': self.guest_driver_version,
 | 
			
		||||
            'os_platform': self.os_platform,
 | 
			
		||||
            'os_version': self.os_version,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def create_statement(engine: Engine):
 | 
			
		||||
        from sqlalchemy.schema import CreateTable
 | 
			
		||||
@@ -28,29 +37,41 @@ class Origin(Base):
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def create_or_update(engine: Engine, origin: "Origin"):
 | 
			
		||||
        session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
 | 
			
		||||
        session = sessionmaker(bind=engine)()
 | 
			
		||||
        entity = session.query(Origin).filter(Origin.origin_ref == origin.origin_ref).first()
 | 
			
		||||
        print(entity)
 | 
			
		||||
        if entity is None:
 | 
			
		||||
            session.add(origin)
 | 
			
		||||
        else:
 | 
			
		||||
            values = dict(
 | 
			
		||||
            x = dict(
 | 
			
		||||
                hostname=origin.hostname,
 | 
			
		||||
                guest_driver_version=origin.guest_driver_version,
 | 
			
		||||
                os_platform=origin.os_platform,
 | 
			
		||||
                os_version=origin.os_version,
 | 
			
		||||
                os_version=origin.os_version
 | 
			
		||||
            )
 | 
			
		||||
            session.execute(update(Origin).where(Origin.origin_ref == origin.origin_ref).values(**values))
 | 
			
		||||
            session.execute(update(Origin).where(Origin.origin_ref == origin.origin_ref).values(**x))
 | 
			
		||||
        session.commit()
 | 
			
		||||
        session.flush()
 | 
			
		||||
        session.close()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def delete(engine: Engine, origins: ["Origin"] = None) -> int:
 | 
			
		||||
        session = sessionmaker(bind=engine)()
 | 
			
		||||
        if origins is None:
 | 
			
		||||
            deletions = session.query(Origin).delete()
 | 
			
		||||
        else:
 | 
			
		||||
            deletions = session.query(Origin).filter(Origin.origin_ref in origins).delete()
 | 
			
		||||
        session.commit()
 | 
			
		||||
        session.close()
 | 
			
		||||
        return deletions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Lease(Base):
 | 
			
		||||
    __tablename__ = "lease"
 | 
			
		||||
 | 
			
		||||
    origin_ref = Column(CHAR(length=36), ForeignKey(Origin.origin_ref), primary_key=True, nullable=False, index=True)  # uuid4
 | 
			
		||||
    lease_ref = Column(CHAR(length=36), primary_key=True, nullable=False, index=True)  # uuid4
 | 
			
		||||
 | 
			
		||||
    origin_ref = Column(CHAR(length=36), ForeignKey(Origin.origin_ref, ondelete='CASCADE'), nullable=False, index=True)  # uuid4
 | 
			
		||||
    lease_created = Column(DATETIME(), nullable=False)
 | 
			
		||||
    lease_expires = Column(DATETIME(), nullable=False)
 | 
			
		||||
    lease_updated = Column(DATETIME(), nullable=False)
 | 
			
		||||
@@ -58,6 +79,15 @@ 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:
 | 
			
		||||
        return {
 | 
			
		||||
            'lease_ref': self.lease_ref,
 | 
			
		||||
            'origin_ref': self.origin_ref,
 | 
			
		||||
            'lease_created': self.lease_created.isoformat(),
 | 
			
		||||
            'lease_expires': self.lease_expires.isoformat(),
 | 
			
		||||
            'lease_updated': self.lease_updated.isoformat(),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def create_statement(engine: Engine):
 | 
			
		||||
        from sqlalchemy.schema import CreateTable
 | 
			
		||||
@@ -65,43 +95,54 @@ class Lease(Base):
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def create_or_update(engine: Engine, lease: "Lease"):
 | 
			
		||||
        session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
 | 
			
		||||
        entity = session.query(Lease).filter(and_(Lease.origin_ref == lease.origin_ref, Lease.lease_ref == lease.lease_ref)).first()
 | 
			
		||||
        session = sessionmaker(bind=engine)()
 | 
			
		||||
        entity = session.query(Lease).filter(Lease.lease_ref == lease.lease_ref).first()
 | 
			
		||||
        if entity is None:
 | 
			
		||||
            if lease.lease_updated is None:
 | 
			
		||||
                lease.lease_updated = lease.lease_created
 | 
			
		||||
            session.add(lease)
 | 
			
		||||
        else:
 | 
			
		||||
            values = dict(lease_expires=lease.lease_expires, lease_updated=lease.lease_updated)
 | 
			
		||||
            session.execute(update(Lease).where(and_(Lease.origin_ref == lease.origin_ref, Lease.lease_ref == lease.lease_ref)).values(**values))
 | 
			
		||||
            x = dict(origin_ref=lease.origin_ref, lease_expires=lease.lease_expires, lease_updated=lease.lease_updated)
 | 
			
		||||
            session.execute(update(Lease).where(Lease.lease_ref == lease.lease_ref).values(**x))
 | 
			
		||||
        session.commit()
 | 
			
		||||
        session.flush()
 | 
			
		||||
        session.close()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def find_by_origin_ref(engine: Engine, origin_ref: str) -> ["Lease"]:
 | 
			
		||||
        session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
 | 
			
		||||
        session = sessionmaker(bind=engine)()
 | 
			
		||||
        entities = session.query(Lease).filter(Lease.origin_ref == origin_ref).all()
 | 
			
		||||
        session.close()
 | 
			
		||||
        return entities
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def find_by_origin_ref_and_lease_ref(engine: Engine, origin_ref: str, lease_ref: str) -> "Lease":
 | 
			
		||||
        session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
 | 
			
		||||
        session = sessionmaker(bind=engine)()
 | 
			
		||||
        entity = session.query(Lease).filter(and_(Lease.origin_ref == origin_ref, Lease.lease_ref == lease_ref)).first()
 | 
			
		||||
        session.close()
 | 
			
		||||
        return entity
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def renew(engine: Engine, lease: "Lease", lease_expires: datetime.datetime, lease_updated: datetime.datetime):
 | 
			
		||||
        session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
 | 
			
		||||
        values = dict(lease_expires=lease.lease_expires, lease_updated=lease.lease_updated)
 | 
			
		||||
        session.execute(update(Lease).where(and_(Lease.origin_ref == lease.origin_ref, Lease.lease_ref == lease.lease_ref)).values(**values))
 | 
			
		||||
        session = sessionmaker(bind=engine)()
 | 
			
		||||
        x = dict(lease_expires=lease.lease_expires, lease_updated=lease.lease_updated)
 | 
			
		||||
        session.execute(update(Lease).where(and_(Lease.origin_ref == lease.origin_ref, Lease.lease_ref == lease.lease_ref)).values(**x))
 | 
			
		||||
        session.commit()
 | 
			
		||||
        session.close()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def cleanup(engine: Engine, origin_ref: str) -> int:
 | 
			
		||||
        session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
 | 
			
		||||
        session = sessionmaker(bind=engine)()
 | 
			
		||||
        deletions = session.query(Lease).filter(Lease.origin_ref == origin_ref).delete()
 | 
			
		||||
        session.commit()
 | 
			
		||||
        session.close()
 | 
			
		||||
        return deletions
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def delete(engine: Engine, lease_ref: str) -> int:
 | 
			
		||||
        session = sessionmaker(bind=engine)()
 | 
			
		||||
        deletions = session.query(Lease).filter(Lease.lease_ref == lease_ref).delete()
 | 
			
		||||
        session.commit()
 | 
			
		||||
        session.close()
 | 
			
		||||
        return deletions
 | 
			
		||||
 | 
			
		||||
@@ -113,4 +154,21 @@ def init(engine: Engine):
 | 
			
		||||
    for table in tables:
 | 
			
		||||
        if not db.dialect.has_table(engine.connect(), table.__tablename__):
 | 
			
		||||
            session.execute(str(table.create_statement(engine)))
 | 
			
		||||
            session.commit()
 | 
			
		||||
    session.close()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def migrate(engine: Engine):
 | 
			
		||||
    db = inspect(engine)
 | 
			
		||||
 | 
			
		||||
    def upgrade_1_0_to_1_1():
 | 
			
		||||
        x = db.dialect.get_columns(engine.connect(), Lease.__tablename__)
 | 
			
		||||
        x = next(_ for _ in x if _['name'] == 'origin_ref')
 | 
			
		||||
        if x['primary_key'] > 0:
 | 
			
		||||
            print('Found old database schema with "origin_ref" as primary-key in "lease" table. Dropping table!')
 | 
			
		||||
            print('  Your leases are recreated on next renewal!')
 | 
			
		||||
            print('  If an error message appears on the client, you can ignore it.')
 | 
			
		||||
            Lease.__table__.drop(bind=engine)
 | 
			
		||||
            init(engine)
 | 
			
		||||
 | 
			
		||||
    upgrade_1_0_to_1_1()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								app/util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/util.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
try:
 | 
			
		||||
    # Crypto | Cryptodome on Debian
 | 
			
		||||
    from Crypto.PublicKey import RSA
 | 
			
		||||
    from Crypto.PublicKey.RSA import RsaKey
 | 
			
		||||
except ModuleNotFoundError:
 | 
			
		||||
    from Cryptodome.PublicKey import RSA
 | 
			
		||||
    from Cryptodome.PublicKey.RSA import RsaKey
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def load_file(filename) -> bytes:
 | 
			
		||||
    with open(filename, 'rb') as file:
 | 
			
		||||
        content = file.read()
 | 
			
		||||
    return content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def load_key(filename) -> RsaKey:
 | 
			
		||||
    return RSA.import_key(extern_key=load_file(filename), passphrase=None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_key() -> RsaKey:
 | 
			
		||||
    return RSA.generate(bits=2048)
 | 
			
		||||
							
								
								
									
										147
									
								
								test/main.py
									
									
									
									
									
								
							
							
						
						
									
										147
									
								
								test/main.py
									
									
									
									
									
								
							@@ -1,6 +1,13 @@
 | 
			
		||||
from base64 import b64encode as b64enc
 | 
			
		||||
from hashlib import sha256
 | 
			
		||||
from calendar import timegm
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from os.path import dirname, join
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from jose import jwt
 | 
			
		||||
from dateutil.relativedelta import relativedelta
 | 
			
		||||
from jose import jwt, jwk
 | 
			
		||||
from jose.constants import ALGORITHMS
 | 
			
		||||
from starlette.testclient import TestClient
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
@@ -9,10 +16,21 @@ sys.path.append('../')
 | 
			
		||||
sys.path.append('../app')
 | 
			
		||||
 | 
			
		||||
from app import main
 | 
			
		||||
from app.util import generate_key, load_key
 | 
			
		||||
 | 
			
		||||
client = TestClient(main.app)
 | 
			
		||||
 | 
			
		||||
ORIGIN_REF = str(uuid4())
 | 
			
		||||
ORIGIN_REF, LEASE_REF = str(uuid4()), str(uuid4())
 | 
			
		||||
SECRET = "HelloWorld"
 | 
			
		||||
 | 
			
		||||
# INSTANCE_KEY_RSA = generate_key()
 | 
			
		||||
# INSTANCE_KEY_PUB = INSTANCE_KEY_RSA.public_key()
 | 
			
		||||
 | 
			
		||||
INSTANCE_KEY_RSA = load_key(str(join(dirname(__file__), '../app/cert/instance.private.pem')))
 | 
			
		||||
INSTANCE_KEY_PUB = load_key(str(join(dirname(__file__), '../app/cert/instance.public.pem')))
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_index():
 | 
			
		||||
@@ -26,11 +44,43 @@ def test_status():
 | 
			
		||||
    assert response.json()['status'] == 'up'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_health():
 | 
			
		||||
    response = client.get('/-/health')
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
    assert response.json()['status'] == 'up'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_readme():
 | 
			
		||||
    response = client.get('/-/readme')
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_manage():
 | 
			
		||||
    response = client.get('/-/manage')
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_client_token():
 | 
			
		||||
    response = client.get('/client-token')
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_origins():
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_origins_delete():
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_leases():
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_lease_delete():
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_auth_v1_origin():
 | 
			
		||||
    payload = {
 | 
			
		||||
        "registration_pending": False,
 | 
			
		||||
@@ -52,9 +102,30 @@ def test_auth_v1_origin():
 | 
			
		||||
    assert response.json()['origin_ref'] == ORIGIN_REF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def auth_v1_origin_update():
 | 
			
		||||
    payload = {
 | 
			
		||||
        "registration_pending": False,
 | 
			
		||||
        "environment": {
 | 
			
		||||
            "guest_driver_version": "guest_driver_version",
 | 
			
		||||
            "hostname": "myhost",
 | 
			
		||||
            "ip_address_list": ["192.168.1.123"],
 | 
			
		||||
            "os_version": "os_version",
 | 
			
		||||
            "os_platform": "os_platform",
 | 
			
		||||
            "fingerprint": {"mac_address_list": ["ff:ff:ff:ff:ff:ff"]},
 | 
			
		||||
            "host_driver_version": "host_driver_version"
 | 
			
		||||
        },
 | 
			
		||||
        "update_pending": False,
 | 
			
		||||
        "candidate_origin_ref": ORIGIN_REF,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    response = client.post('/auth/v1/origin/update', json=payload)
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
    assert response.json()['origin_ref'] == ORIGIN_REF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_auth_v1_code():
 | 
			
		||||
    payload = {
 | 
			
		||||
        "code_challenge": "0wmaiAMAlTIDyz4Fgt2/j0tXnGv72TYbbLs4ISRCZlY",
 | 
			
		||||
        "code_challenge": b64enc(sha256(SECRET.encode('utf-8')).digest()).rstrip(b'=').decode('utf-8'),
 | 
			
		||||
        "origin_ref": ORIGIN_REF,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -66,20 +137,80 @@ def test_auth_v1_code():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_auth_v1_token():
 | 
			
		||||
    pass
 | 
			
		||||
    cur_time = datetime.utcnow()
 | 
			
		||||
    access_expires_on = cur_time + relativedelta(hours=1)
 | 
			
		||||
 | 
			
		||||
    payload = {
 | 
			
		||||
        "iat": timegm(cur_time.timetuple()),
 | 
			
		||||
        "exp": timegm(access_expires_on.timetuple()),
 | 
			
		||||
        "challenge": b64enc(sha256(SECRET.encode('utf-8')).digest()).rstrip(b'=').decode('utf-8'),
 | 
			
		||||
        "origin_ref": ORIGIN_REF,
 | 
			
		||||
        "key_ref": "00000000-0000-0000-0000-000000000000",
 | 
			
		||||
        "kid": "00000000-0000-0000-0000-000000000000"
 | 
			
		||||
    }
 | 
			
		||||
    payload = {
 | 
			
		||||
        "auth_code": jwt.encode(payload, key=jwt_encode_key, headers={'kid': payload.get('kid')},
 | 
			
		||||
                                algorithm=ALGORITHMS.RS256),
 | 
			
		||||
        "code_verifier": SECRET,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    response = client.post('/auth/v1/token', json=payload)
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
    token = response.json()['auth_token']
 | 
			
		||||
    payload = jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False})
 | 
			
		||||
    assert payload['origin_ref'] == ORIGIN_REF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_leasing_v1_lessor():
 | 
			
		||||
    pass
 | 
			
		||||
    payload = {
 | 
			
		||||
        'fulfillment_context': {
 | 
			
		||||
            'fulfillment_class_ref_list': []
 | 
			
		||||
        },
 | 
			
		||||
        'lease_proposal_list': [{
 | 
			
		||||
            'license_type_qualifiers': {'count': 1},
 | 
			
		||||
            'product': {'name': 'NVIDIA RTX Virtual Workstation'}
 | 
			
		||||
        }],
 | 
			
		||||
        'proposal_evaluation_mode': 'ALL_OF',
 | 
			
		||||
        'scope_ref_list': [LEASE_REF]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bearer_token = jwt.encode({"origin_ref": ORIGIN_REF}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256)
 | 
			
		||||
    bearer_token = f'Bearer {bearer_token}'
 | 
			
		||||
    response = client.post('/leasing/v1/lessor', json=payload, headers={'authorization': bearer_token})
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
    lease_result_list = response.json()['lease_result_list']
 | 
			
		||||
    assert len(lease_result_list) == 1
 | 
			
		||||
    assert lease_result_list[0]['lease']['ref'] == LEASE_REF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_leasing_v1_lessor_lease():
 | 
			
		||||
    pass
 | 
			
		||||
    bearer_token = jwt.encode({"origin_ref": ORIGIN_REF}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256)
 | 
			
		||||
    bearer_token = f'Bearer {bearer_token}'
 | 
			
		||||
    response = client.get('/leasing/v1/lessor/leases', headers={'authorization': bearer_token})
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
    active_lease_list = response.json()['active_lease_list']
 | 
			
		||||
    assert len(active_lease_list) == 1
 | 
			
		||||
    assert active_lease_list[0] == LEASE_REF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_leasing_v1_lease_renew():
 | 
			
		||||
    pass
 | 
			
		||||
    bearer_token = jwt.encode({"origin_ref": ORIGIN_REF}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256)
 | 
			
		||||
    bearer_token = f'Bearer {bearer_token}'
 | 
			
		||||
    response = client.put(f'/leasing/v1/lease/{LEASE_REF}', headers={'authorization': bearer_token})
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
    assert response.json()['lease_ref'] == LEASE_REF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_leasing_v1_lessor_lease_remove():
 | 
			
		||||
    pass
 | 
			
		||||
    bearer_token = jwt.encode({"origin_ref": ORIGIN_REF}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256)
 | 
			
		||||
    bearer_token = f'Bearer {bearer_token}'
 | 
			
		||||
    response = client.delete('/leasing/v1/lessor/leases', headers={'authorization': bearer_token})
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
    released_lease_list = response.json()['released_lease_list']
 | 
			
		||||
    assert len(released_lease_list) == 1
 | 
			
		||||
    assert released_lease_list[0] == LEASE_REF
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
VERSION=1.0.0
 | 
			
		||||
VERSION=1.1
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user