From ab996bb030e7c7130ca73d8d1103e3b860392171 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 2 Jan 2023 18:04:14 +0100 Subject: [PATCH 01/16] code styling --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 63df4f4..883709d 100644 --- a/app/main.py +++ b/app/main.py @@ -69,7 +69,7 @@ async def index(): @app.get('/status', summary='* Status', description='returns current service status, version (incl. git-commit) and some variables.', deprecated=True) -async def status(request: Request): +async def status(): return JSONResponse({'status': 'up', 'version': VERSION, 'commit': COMMIT, 'debug': DEBUG}) From a3e089a3d5b6b862c67dcba2b865b3a7b31d32bc Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 2 Jan 2023 18:10:11 +0100 Subject: [PATCH 02/16] added some references --- app/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/main.py b/app/main.py index 883709d..ad90266 100644 --- a/app/main.py +++ b/app/main.py @@ -345,6 +345,7 @@ async def auth_v1_token(request: Request): return JSONResponse(response) +# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py # {'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': ['00112233-4455-6677-8899-aabbccddeeff']} @app.post('/leasing/v1/lessor', description='request multiple leases (borrow) for current origin') async def leasing_v1_lessor(request: Request): @@ -404,6 +405,7 @@ async def leasing_v1_lessor_lease(request: Request): return JSONResponse(response) +# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_single_controller.py # venv/lib/python3.9/site-packages/nls_core_lease/lease_single.py @app.put('/leasing/v1/lease/{lease_ref}', description='renew a lease') async def leasing_v1_lease_renew(request: Request, lease_ref: str): @@ -431,6 +433,7 @@ async def leasing_v1_lease_renew(request: Request, lease_ref: str): return JSONResponse(response) +# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_single_controller.py @app.delete('/leasing/v1/lease/{lease_ref}', description='release (return) a lease') async def leasing_v1_lease_delete(request: Request, lease_ref: str): token, cur_time = __get_token(request), datetime.utcnow() @@ -456,6 +459,7 @@ async def leasing_v1_lease_delete(request: Request, lease_ref: str): return JSONResponse(response) +# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py @app.delete('/leasing/v1/lessor/leases', description='release all leases') async def leasing_v1_lessor_lease_remove(request: Request): token, cur_time = __get_token(request), datetime.utcnow() From 34662e6612be51a0bdef4862a5f8fa46f2724782 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 2 Jan 2023 18:57:41 +0100 Subject: [PATCH 03/16] implemented 'LEASE_RENEWAL_PERIOD' variable --- README.md | 31 ++++++++++++++++++------------- app/main.py | 4 ++-- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6597c19..6ea0572 100644 --- a/README.md +++ b/README.md @@ -282,20 +282,25 @@ 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 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` | `/cert/instance.private.pem` | Site-wide private RSA key for singing JWTs | -| `INSTANCE_KEY_PUB` | `/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 | +| `LEASE_RENEWAL_PERIOD` | `0.15` | The percentage of the lease period that must elapse before a licensed client can renew a license \*1 | +| `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) \*2 | +| `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid | +| `INSTANCE_REF` | `00000000-0000-0000-0000-000000000000` | Instance identification uuid | +| `INSTANCE_KEY_RSA` | `/cert/instance.private.pem` | Site-wide private RSA key for singing JWTs | +| `INSTANCE_KEY_PUB` | `/cert/instance.public.pem` | Site-wide public key | -\* Always use `https`, since guest-drivers only support secure connections! +\*1 For example, if the lease period is one day and the renewal period is 20%, the client attempts to renew its license +every 4.8 hours. If network connectivity is lost, the loss of connectivity is detected during license renewal and the +client has 19.2 hours in which to re-establish connectivity before its license expires. + +\*2 Always use `https`, since guest-drivers only support secure connections! # Setup (Client) diff --git a/app/main.py b/app/main.py index ad90266..8d0f5a5 100644 --- a/app/main.py +++ b/app/main.py @@ -40,6 +40,7 @@ INSTANCE_KEY_RSA = load_key(str(env('INSTANCE_KEY_RSA', join(dirname(__file__), 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(env('LEASE_EXPIRE_DAYS', 90))) +LEASE_RENEWAL_PERIOD = float(env('LEASE_RENEWAL_PERIOD', 0.15)) 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) @@ -365,8 +366,7 @@ async def leasing_v1_lessor(request: Request): "ref": scope_ref, "created": cur_time.isoformat(), "expires": expires.isoformat(), - # The percentage of the lease period that must elapse before a licensed client can renew a license - "recommended_lease_renewal": 0.15, + "recommended_lease_renewal": LEASE_RENEWAL_PERIOD, "offline_lease": "true", "license_type": "CONCURRENT_COUNTED_SINGLE" } From 2e950ca6f4543901f9919ea94ae4ec55df7a3437 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 2 Jan 2023 19:14:25 +0100 Subject: [PATCH 04/16] implemented '/-/config' endpoint to list runtime environment variables --- README.md | 6 +++++- app/main.py | 19 ++++++++++++++++++- test/main.py | 5 +++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6ea0572..52caac7 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,11 @@ Status endpoint, used for *healthcheck*. Shows also current version and commit h ### `GET /-/health` -Status endpoint, used for *healthcheck*. Shows also current version and commit hash. +Status endpoint, used for *healthcheck*. + +### `GET /-/config` + +Shows current runtime environment variables and their values. ### `GET /-/readme` diff --git a/app/main.py b/app/main.py index 8d0f5a5..27dbfbf 100644 --- a/app/main.py +++ b/app/main.py @@ -81,7 +81,24 @@ async def _index(): @app.get('/-/health', summary='* Health') async def _health(request: Request): - return JSONResponse({'status': 'up', 'version': VERSION, 'commit': COMMIT, 'debug': DEBUG}) + return JSONResponse({'status': 'up'}) + + +@app.get('/-/config', summary='* Config', description='returns environment variables.') +async def _config(): + return JSONResponse({ + 'VERSION': VERSION, + 'COMMIT': COMMIT, + 'DEBUG': DEBUG, + 'DLS_URL': DLS_URL, + 'DLS_PORT': DLS_PORT, + 'SITE_KEY_XID': SITE_KEY_XID, + 'INSTANCE_REF': INSTANCE_REF, + 'TOKEN_EXPIRE_DELTA': TOKEN_EXPIRE_DELTA, + 'LEASE_EXPIRE_DELTA': LEASE_EXPIRE_DELTA, + 'LEASE_RENEWAL_PERIOD': LEASE_RENEWAL_PERIOD, + 'CORS_ORIGINS': CORS_ORIGINS, + }) @app.get('/-/readme', summary='* Readme') diff --git a/test/main.py b/test/main.py index 9ef4457..8514e78 100644 --- a/test/main.py +++ b/test/main.py @@ -56,6 +56,11 @@ def test_health(): assert response.json()['status'] == 'up' +def test_config(): + response = client.get('/-/') + assert response.status_code == 200 + + def test_readme(): response = client.get('/-/readme') assert response.status_code == 200 From 4fb624333026865fcc80636f3facd3a298eda6ae Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 2 Jan 2023 19:15:48 +0100 Subject: [PATCH 05/16] removed deprecated endpoints - '/client-token' moved to '/-/client-token' - '/status' moved to '/-/health' and '/-/config' see README.md for more information --- README.md | 6 +----- app/main.py | 10 ---------- test/main.py | 10 ---------- 3 files changed, 1 insertion(+), 25 deletions(-) diff --git a/README.md b/README.md index 52caac7..3a09c6a 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,6 @@ Only the clients need a connection to this service on configured port. Redirect to `/-/readme`. -### `GET /status` (deprecated: use `/-/health`) - -Status endpoint, used for *healthcheck*. Shows also current version and commit hash. - ### `GET /-/health` Status endpoint, used for *healthcheck*. @@ -67,7 +63,7 @@ List current leases. Deletes an lease. -### `GET /client-token` (deprecated: use `/-/client-token`) +### `GET /-/client-token` Generate client token, (see [installation](#installation)). diff --git a/app/main.py b/app/main.py index 27dbfbf..3708ea5 100644 --- a/app/main.py +++ b/app/main.py @@ -69,11 +69,6 @@ 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(): - return JSONResponse({'status': 'up', 'version': VERSION, 'commit': COMMIT, 'debug': DEBUG}) - - @app.get('/-/', summary='* Index') async def _index(): return RedirectResponse('/-/readme') @@ -228,11 +223,6 @@ async def _client_token(): return response -@app.get('/client-token', summary='* Client-Token', description='creates a new messenger token for this service instance', deprecated=True) -async def client_token(): - return RedirectResponse('/-/client-token') - - # venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py # {"candidate_origin_ref":"00112233-4455-6677-8899-aabbccddeeff","environment":{"fingerprint":{"mac_address_list":["ff:ff:ff:ff:ff:ff"]},"hostname":"my-hostname","ip_address_list":["192.168.178.123","fe80::","fe80::1%enp6s18"],"guest_driver_version":"510.85.02","os_platform":"Debian GNU/Linux 11 (bullseye) 11","os_version":"11 (bullseye)"},"registration_pending":false,"update_pending":false} @app.post('/auth/v1/origin', description='find or create an origin') diff --git a/test/main.py b/test/main.py index 8514e78..980fe9e 100644 --- a/test/main.py +++ b/test/main.py @@ -44,11 +44,6 @@ def test_index(): assert response.status_code == 200 -def test_status(): - response = client.get('/status') - assert response.status_code == 200 - assert response.json()['status'] == 'up' - def test_health(): response = client.get('/-/health') @@ -76,11 +71,6 @@ def test_client_token(): assert response.status_code == 200 -def test_client_token_deprecated(): - response = client.get('/client-token') - assert response.status_code == 200 - - def test_origins(): pass From 8b934dfeef9751280637d7b9af4592e07740f512 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 2 Jan 2023 19:23:23 +0100 Subject: [PATCH 06/16] fixed '/-/config' endpoint serialisation --- app/main.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/main.py b/app/main.py index 3708ea5..d4da9f4 100644 --- a/app/main.py +++ b/app/main.py @@ -82,17 +82,17 @@ async def _health(request: Request): @app.get('/-/config', summary='* Config', description='returns environment variables.') async def _config(): return JSONResponse({ - 'VERSION': VERSION, - 'COMMIT': COMMIT, - 'DEBUG': DEBUG, - 'DLS_URL': DLS_URL, - 'DLS_PORT': DLS_PORT, - 'SITE_KEY_XID': SITE_KEY_XID, - 'INSTANCE_REF': INSTANCE_REF, - 'TOKEN_EXPIRE_DELTA': TOKEN_EXPIRE_DELTA, - 'LEASE_EXPIRE_DELTA': LEASE_EXPIRE_DELTA, - 'LEASE_RENEWAL_PERIOD': LEASE_RENEWAL_PERIOD, - 'CORS_ORIGINS': CORS_ORIGINS, + 'VERSION': str(VERSION), + 'COMMIT': str(COMMIT), + 'DEBUG': str(DEBUG), + 'DLS_URL': str(DLS_URL), + 'DLS_PORT': str(DLS_PORT), + 'SITE_KEY_XID': str(SITE_KEY_XID), + 'INSTANCE_REF': str(INSTANCE_REF), + 'TOKEN_EXPIRE_DELTA': str(TOKEN_EXPIRE_DELTA), + 'LEASE_EXPIRE_DELTA': str(LEASE_EXPIRE_DELTA), + 'LEASE_RENEWAL_PERIOD': str(LEASE_RENEWAL_PERIOD), + 'CORS_ORIGINS': str(CORS_ORIGINS), }) From 50e0dc8d1f7dd1c600f2945de356f8e817160c9d Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 2 Jan 2023 19:42:23 +0100 Subject: [PATCH 07/16] implemented '/leasing/v1/lessor/shutdown' for windows guests --- app/main.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/main.py b/app/main.py index d4da9f4..02fe569 100644 --- a/app/main.py +++ b/app/main.py @@ -487,6 +487,28 @@ async def leasing_v1_lessor_lease_remove(request: Request): return JSONResponse(response) +@app.post('/leasing/v1/lessor/shutdown', description='shutdown all leases') +async def leasing_v1_lessor_shutdown(request: Request): + j, cur_time = json.loads((await request.body()).decode('utf-8')) + + token = j['token'] + token = jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False}) + origin_ref = token.get('origin_ref') + + released_lease_list = list(map(lambda x: x.lease_ref, Lease.find_by_origin_ref(db, origin_ref))) + deletions = Lease.cleanup(db, origin_ref) + logging.info(f'> [ shutdown ]: {origin_ref}: removed {deletions} leases') + + response = { + "released_lease_list": released_lease_list, + "release_failure_list": None, + "sync_timestamp": cur_time.isoformat(), + "prompts": None + } + + return JSONResponse(response) + + if __name__ == '__main__': import uvicorn From 2b8c4682708d20e1695b563baf35dca85cb3d838 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 3 Jan 2023 07:25:09 +0100 Subject: [PATCH 08/16] main.py - fixed missing 'LEASE_RENEWAL_PERIOD' on '/auth/v1/origin' --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 02fe569..e63fc5e 100644 --- a/app/main.py +++ b/app/main.py @@ -429,7 +429,7 @@ async def leasing_v1_lease_renew(request: Request, lease_ref: str): response = { "lease_ref": lease_ref, "expires": expires.isoformat(), - "recommended_lease_renewal": 0.16, + "recommended_lease_renewal": LEASE_RENEWAL_PERIOD, "offline_lease": True, "prompts": None, "sync_timestamp": cur_time.isoformat(), From 8f9d95056fa4f0bae4e616ae14e9e8d2fb764c27 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 3 Jan 2023 09:20:18 +0100 Subject: [PATCH 09/16] code styling - migrated direct dict access to '.get()' --- app/main.py | 36 ++++++++++++++++++------------------ test/main.py | 19 +++++++++---------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/app/main.py b/app/main.py index e63fc5e..b4ba3fd 100644 --- a/app/main.py +++ b/app/main.py @@ -229,21 +229,21 @@ async def _client_token(): async def auth_v1_origin(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() - origin_ref = j['candidate_origin_ref'] + origin_ref = j.get('candidate_origin_ref') logging.info(f'> [ origin ]: {origin_ref}: {j}') data = Origin( origin_ref=origin_ref, - hostname=j['environment']['hostname'], - guest_driver_version=j['environment']['guest_driver_version'], - os_platform=j['environment']['os_platform'], os_version=j['environment']['os_version'], + hostname=j.get('environment').get('hostname'), + guest_driver_version=j.get('environment').get('guest_driver_version'), + os_platform=j.get('environment').get('os_platform'), os_version=j.get('environment').get('os_version'), ) Origin.create_or_update(db, data) response = { "origin_ref": origin_ref, - "environment": j['environment'], + "environment": j.get('environment'), "svc_port_set_list": None, "node_url_list": None, "node_query_order": None, @@ -260,20 +260,20 @@ async def auth_v1_origin(request: Request): async def auth_v1_origin_update(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() - origin_ref = j['origin_ref'] + origin_ref = j.get('origin_ref') logging.info(f'> [ update ]: {origin_ref}: {j}') data = Origin( origin_ref=origin_ref, - hostname=j['environment']['hostname'], - guest_driver_version=j['environment']['guest_driver_version'], - os_platform=j['environment']['os_platform'], os_version=j['environment']['os_version'], + hostname=j.get('environment').get('hostname'), + guest_driver_version=j.get('environment').get('guest_driver_version'), + os_platform=j.get('environment').get('os_platform'), os_version=j.get('environment').get('os_version'), ) Origin.create_or_update(db, data) response = { - "environment": j['environment'], + "environment": j.get('environment'), "prompts": None, "sync_timestamp": cur_time.isoformat() } @@ -288,7 +288,7 @@ async def auth_v1_origin_update(request: Request): async def auth_v1_code(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() - origin_ref = j['origin_ref'] + origin_ref = j.get('origin_ref') logging.info(f'> [ code ]: {origin_ref}: {j}') delta = relativedelta(minutes=15) @@ -297,8 +297,8 @@ async def auth_v1_code(request: Request): payload = { 'iat': timegm(cur_time.timetuple()), 'exp': timegm(expires.timetuple()), - 'challenge': j['code_challenge'], - 'origin_ref': j['origin_ref'], + 'challenge': j.get('code_challenge'), + 'origin_ref': j.get('origin_ref'), 'key_ref': SITE_KEY_XID, 'kid': SITE_KEY_XID } @@ -320,13 +320,13 @@ async def auth_v1_code(request: Request): @app.post('/auth/v1/token', description='exchange auth code and verifier for token') async def auth_v1_token(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() - payload = jwt.decode(token=j['auth_code'], key=jwt_decode_key) + payload = jwt.decode(token=j.get('auth_code'), key=jwt_decode_key) - origin_ref = payload['origin_ref'] + origin_ref = payload.get('origin_ref') logging.info(f'> [ auth ]: {origin_ref}: {j}') # validate the code challenge - if payload['challenge'] != b64enc(sha256(j['code_verifier'].encode('utf-8')).digest()).rstrip(b'=').decode('utf-8'): + if payload.get('challenge') != b64enc(sha256(j.get('code_verifier').encode('utf-8')).digest()).rstrip(b'=').decode('utf-8'): raise HTTPException(status_code=401, detail='expected challenge did not match verifier') access_expires_on = cur_time + TOKEN_EXPIRE_DELTA @@ -360,7 +360,7 @@ async def leasing_v1_lessor(request: Request): j, token, cur_time = json.loads((await request.body()).decode('utf-8')), __get_token(request), datetime.utcnow() origin_ref = token.get('origin_ref') - scope_ref_list = j['scope_ref_list'] + scope_ref_list = j.get('scope_ref_list') logging.info(f'> [ create ]: {origin_ref}: create leases for scope_ref_list {scope_ref_list}') lease_result_list = [] @@ -491,7 +491,7 @@ async def leasing_v1_lessor_lease_remove(request: Request): async def leasing_v1_lessor_shutdown(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')) - token = j['token'] + token = j.get('token') token = jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False}) origin_ref = token.get('origin_ref') diff --git a/test/main.py b/test/main.py index 980fe9e..a1e25d3 100644 --- a/test/main.py +++ b/test/main.py @@ -44,11 +44,10 @@ def test_index(): assert response.status_code == 200 - def test_health(): response = client.get('/-/health') assert response.status_code == 200 - assert response.json()['status'] == 'up' + assert response.json().get('status') == 'up' def test_config(): @@ -105,7 +104,7 @@ def test_auth_v1_origin(): response = client.post('/auth/v1/origin', json=payload) assert response.status_code == 200 - assert response.json()['origin_ref'] == ORIGIN_REF + assert response.json().get('origin_ref') == ORIGIN_REF def auth_v1_origin_update(): @@ -126,7 +125,7 @@ def auth_v1_origin_update(): response = client.post('/auth/v1/origin/update', json=payload) assert response.status_code == 200 - assert response.json()['origin_ref'] == ORIGIN_REF + assert response.json().get('origin_ref') == ORIGIN_REF def test_auth_v1_code(): @@ -138,8 +137,8 @@ def test_auth_v1_code(): response = client.post('/auth/v1/code', json=payload) assert response.status_code == 200 - payload = jwt.get_unverified_claims(token=response.json()['auth_code']) - assert payload['origin_ref'] == ORIGIN_REF + payload = jwt.get_unverified_claims(token=response.json().get('auth_code')) + assert payload.get('origin_ref') == ORIGIN_REF def test_auth_v1_token(): @@ -163,9 +162,9 @@ def test_auth_v1_token(): response = client.post('/auth/v1/token', json=payload) assert response.status_code == 200 - token = response.json()['auth_token'] + token = response.json().get('auth_token') payload = jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False}) - assert payload['origin_ref'] == ORIGIN_REF + assert payload.get('origin_ref') == ORIGIN_REF def test_leasing_v1_lessor(): @@ -193,7 +192,7 @@ def test_leasing_v1_lessor_lease(): response = client.get('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)}) assert response.status_code == 200 - active_lease_list = response.json()['active_lease_list'] + active_lease_list = response.json().get('active_lease_list') assert len(active_lease_list) == 1 assert active_lease_list[0] == LEASE_REF @@ -218,6 +217,6 @@ def test_leasing_v1_lessor_lease_remove(): response = client.delete('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)}) assert response.status_code == 200 - released_lease_list = response.json()['released_lease_list'] + released_lease_list = response.json().get('released_lease_list') assert len(released_lease_list) == 1 assert released_lease_list[0] == LEASE_REF From bd5625af42a47ed1771d3b773b05c2fa961a640b Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 3 Jan 2023 13:02:37 +0100 Subject: [PATCH 10/16] main.py - removed example responses --- app/main.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/main.py b/app/main.py index b4ba3fd..0fe9c2a 100644 --- a/app/main.py +++ b/app/main.py @@ -32,6 +32,7 @@ app = FastAPI(title='FastAPI-DLS', description='Minimal Delegated License Servic db = create_engine(str(env('DATABASE', 'sqlite:///db.sqlite'))) db_init(db), migrate(db) +# everything prefixed with "INSTANCE_*" is used as "SERVICE_INSTANCE_*" or "SI_*" in official dls service DLS_URL = str(env('DLS_URL', 'localhost')) DLS_PORT = int(env('DLS_PORT', '443')) SITE_KEY_XID = str(env('SITE_KEY_XID', '00000000-0000-0000-0000-000000000000')) @@ -224,7 +225,6 @@ async def _client_token(): # venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py -# {"candidate_origin_ref":"00112233-4455-6677-8899-aabbccddeeff","environment":{"fingerprint":{"mac_address_list":["ff:ff:ff:ff:ff:ff"]},"hostname":"my-hostname","ip_address_list":["192.168.178.123","fe80::","fe80::1%enp6s18"],"guest_driver_version":"510.85.02","os_platform":"Debian GNU/Linux 11 (bullseye) 11","os_version":"11 (bullseye)"},"registration_pending":false,"update_pending":false} @app.post('/auth/v1/origin', description='find or create an origin') async def auth_v1_origin(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() @@ -255,7 +255,6 @@ async def auth_v1_origin(request: Request): # venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py -# { "environment" : { "guest_driver_version" : "guest_driver_version", "hostname" : "myhost", "ip_address_list" : [ "192.168.1.129" ], "os_version" : "os_version", "os_platform" : "os_platform", "fingerprint" : { "mac_address_list" : [ "e4:b9:7a:e5:7b:ff" ] }, "host_driver_version" : "host_driver_version" }, "origin_ref" : "00112233-4455-6677-8899-aabbccddeeff" } @app.post('/auth/v1/origin/update', description='update an origin evidence') async def auth_v1_origin_update(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() @@ -283,7 +282,6 @@ async def auth_v1_origin_update(request: Request): # venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py # venv/lib/python3.9/site-packages/nls_core_auth/auth.py - CodeResponse -# {"code_challenge":"...","origin_ref":"00112233-4455-6677-8899-aabbccddeeff"} @app.post('/auth/v1/code', description='get an authorization code') async def auth_v1_code(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() @@ -316,7 +314,6 @@ async def auth_v1_code(request: Request): # venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py # venv/lib/python3.9/site-packages/nls_core_auth/auth.py - TokenResponse -# {"auth_code":"...","code_verifier":"..."} @app.post('/auth/v1/token', description='exchange auth code and verifier for token') async def auth_v1_token(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() @@ -354,7 +351,6 @@ async def auth_v1_token(request: Request): # venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py -# {'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': ['00112233-4455-6677-8899-aabbccddeeff']} @app.post('/leasing/v1/lessor', description='request multiple leases (borrow) for current origin') async def leasing_v1_lessor(request: Request): j, token, cur_time = json.loads((await request.body()).decode('utf-8')), __get_token(request), datetime.utcnow() From 0e3e7cbd3aeee489cfe7d2b3d4f0b49ddc086f6a Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 3 Jan 2023 13:05:05 +0100 Subject: [PATCH 11/16] main.py - corrected leasing behaviour (migrated from 'LEASE_REF' to 'ALLOTMENT_REF') --- README.md | 3 ++- app/main.py | 14 ++++++++++---- app/orm.py | 14 +++++++++++++- doc/Database.md | 26 ++++++++++++++++++++++++++ test/main.py | 38 +++++++++++++++++++++++++------------- 5 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 doc/Database.md diff --git a/README.md b/README.md index 3a09c6a..e64effa 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,8 @@ After first success you have to replace `--issue` with `--renew`. | `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) \*2 | | `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid | -| `INSTANCE_REF` | `00000000-0000-0000-0000-000000000000` | Instance identification uuid | +| `INSTANCE_REF` | `10000000-0000-0000-0000-000000000001` | Instance identification uuid | +| `ALLOTMENT_REF` | `20000000-0000-0000-0000-000000000001` | Allotment identification uuid | | `INSTANCE_KEY_RSA` | `/cert/instance.private.pem` | Site-wide private RSA key for singing JWTs | | `INSTANCE_KEY_PUB` | `/cert/instance.public.pem` | Site-wide public key | diff --git a/app/main.py b/app/main.py index 0fe9c2a..9259843 100644 --- a/app/main.py +++ b/app/main.py @@ -36,7 +36,8 @@ db_init(db), migrate(db) 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_REF = str(env('INSTANCE_REF', '10000000-0000-0000-0000-000000000001')) +ALLOTMENT_REF = str(env('ALLOTMENT_REF', '20000000-0000-0000-0000-000000000001')) 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 @@ -90,6 +91,7 @@ async def _config(): 'DLS_PORT': str(DLS_PORT), 'SITE_KEY_XID': str(SITE_KEY_XID), 'INSTANCE_REF': str(INSTANCE_REF), + 'ALLOTMENT_REF': [ALLOTMENT_REF], 'TOKEN_EXPIRE_DELTA': str(TOKEN_EXPIRE_DELTA), 'LEASE_EXPIRE_DELTA': str(LEASE_EXPIRE_DELTA), 'LEASE_RENEWAL_PERIOD': str(LEASE_RENEWAL_PERIOD), @@ -192,7 +194,7 @@ async def _client_token(): "nbf": timegm(cur_time.timetuple()), "exp": timegm(exp_time.timetuple()), "update_mode": "ABSOLUTE", - "scope_ref_list": [str(uuid4())], # this is our LEASE_REF + "scope_ref_list": [ALLOTMENT_REF], "fulfillment_class_ref_list": [], "service_instance_configuration": { "nls_service_instance_ref": INSTANCE_REF, @@ -361,12 +363,16 @@ async def leasing_v1_lessor(request: Request): lease_result_list = [] for scope_ref in scope_ref_list: + if scope_ref not in [ALLOTMENT_REF]: + raise HTTPException(status_code=500, detail=f'no service instances found for scopes: ["{scope_ref}"]') + + lease_ref = str(uuid4()) expires = cur_time + LEASE_EXPIRE_DELTA lease_result_list.append({ "ordinal": 0, # https://docs.nvidia.com/license-system/latest/nvidia-license-system-user-guide/index.html "lease": { - "ref": scope_ref, + "ref": lease_ref, "created": cur_time.isoformat(), "expires": expires.isoformat(), "recommended_lease_renewal": LEASE_RENEWAL_PERIOD, @@ -375,7 +381,7 @@ async def leasing_v1_lessor(request: Request): } }) - data = Lease(origin_ref=origin_ref, lease_ref=scope_ref, lease_created=cur_time, lease_expires=expires) + data = Lease(origin_ref=origin_ref, scope_ref=scope_ref, lease_ref=lease_ref, lease_created=cur_time, lease_expires=expires) Lease.create_or_update(db, data) response = { diff --git a/app/orm.py b/app/orm.py index 0f5d386..8128804 100644 --- a/app/orm.py +++ b/app/orm.py @@ -72,17 +72,19 @@ class Lease(Base): 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 + scope_ref = Column(CHAR(length=36), nullable=False, index=True) # uuid4 lease_created = Column(DATETIME(), nullable=False) lease_expires = Column(DATETIME(), nullable=False) lease_updated = Column(DATETIME(), nullable=False) def __repr__(self): - return f'Lease(origin_ref={self.origin_ref}, lease_ref={self.lease_ref}, expires={self.lease_expires})' + return f'Lease(origin_ref={self.origin_ref}, scope_ref={self.scope_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, + 'scope_ref': self.scope_ref, 'lease_created': self.lease_created.isoformat(), 'lease_expires': self.lease_expires.isoformat(), 'lease_updated': self.lease_updated.isoformat(), @@ -178,4 +180,14 @@ def migrate(engine: Engine): Lease.__table__.drop(bind=engine) init(engine) + def upgrade_1_2_to_1_3(): + x = db.dialect.get_columns(engine.connect(), Lease.__tablename__) + x = next((_ for _ in x if _['name'] == 'scope_ref'), None) + if x is None: + Lease.scope_ref.compile() + column_name = Lease.scope_ref.name + column_type = Lease.scope_ref.type.compile(engine.dialect) + engine.execute(f'ALTER TABLE "{Lease.__tablename__}" ADD COLUMN "{column_name}" {column_type}') + upgrade_1_0_to_1_1() + upgrade_1_2_to_1_3() diff --git a/doc/Database.md b/doc/Database.md new file mode 100644 index 0000000..5a838a3 --- /dev/null +++ b/doc/Database.md @@ -0,0 +1,26 @@ +# Database structure + +## `request_routing.service_instance` + +| xid | org_name | +|----------------------------------------|--------------------------| +| `10000000-0000-0000-0000-000000000000` | `lic-000000000000000000` | + +- `xid` is used as `SERVICE_INSTANCE_XID` + +## `request_routing.license_allotment_service_instance` + +| xid | service_instance_xid | license_allotment_xid | +|----------------------------------------|----------------------------------------|----------------------------------------| +| `90000000-0000-0000-0000-000000000001` | `10000000-0000-0000-0000-000000000000` | `80000000-0000-0000-0000-000000000001` | + +- `xid` is only a primary-key and never used as foreign-key or reference +- `license_allotment_xid` must be used to fetch `xid`'s from `request_routing.license_allotment_reference` + +## `request_routing.license_allotment_reference` + +| xid | license_allotment_xid | +|----------------------------------------|----------------------------------------| +| `20000000-0000-0000-0000-000000000001` | `80000000-0000-0000-0000-000000000001` | + +- `xid` is used as `scope_ref_list` on token request diff --git a/test/main.py b/test/main.py index a1e25d3..f04de99 100644 --- a/test/main.py +++ b/test/main.py @@ -3,7 +3,7 @@ from hashlib import sha256 from calendar import timegm from datetime import datetime from os.path import dirname, join -from uuid import uuid4 +from uuid import uuid4, UUID from dateutil.relativedelta import relativedelta from jose import jwt, jwk @@ -20,8 +20,7 @@ from app.util import load_key client = TestClient(main.app) -ORIGIN_REF, LEASE_REF = str(uuid4()), str(uuid4()) -SECRET = "HelloWorld" +ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-000000000001', 'HelloWorld' # INSTANCE_KEY_RSA = generate_key() # INSTANCE_KEY_PUB = INSTANCE_KEY_RSA.public_key() @@ -177,15 +176,16 @@ def test_leasing_v1_lessor(): 'product': {'name': 'NVIDIA RTX Virtual Workstation'} }], 'proposal_evaluation_mode': 'ALL_OF', - 'scope_ref_list': [LEASE_REF] + 'scope_ref_list': [ALLOTMENT_REF] } response = client.post('/leasing/v1/lessor', json=payload, headers={'authorization': __bearer_token(ORIGIN_REF)}) assert response.status_code == 200 - lease_result_list = response.json()['lease_result_list'] + lease_result_list = response.json().get('lease_result_list') assert len(lease_result_list) == 1 - assert lease_result_list[0]['lease']['ref'] == LEASE_REF + assert str(UUID(lease_result_list[0]['lease']['ref'])) == lease_result_list[0]['lease']['ref'] + return lease_result_list[0]['lease']['ref'] def test_leasing_v1_lessor_lease(): @@ -194,29 +194,41 @@ def test_leasing_v1_lessor_lease(): active_lease_list = response.json().get('active_lease_list') assert len(active_lease_list) == 1 - assert active_lease_list[0] == LEASE_REF + assert str(UUID(active_lease_list[0])) == active_lease_list[0] def test_leasing_v1_lease_renew(): - response = client.put(f'/leasing/v1/lease/{LEASE_REF}', headers={'authorization': __bearer_token(ORIGIN_REF)}) + response = client.get('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)}) + active_lease_list = response.json().get('active_lease_list') + lease_ref = active_lease_list[0] + + ### + + response = client.put(f'/leasing/v1/lease/{lease_ref}', headers={'authorization': __bearer_token(ORIGIN_REF)}) assert response.status_code == 200 - assert response.json()['lease_ref'] == LEASE_REF + assert response.json().get('lease_ref') == lease_ref def test_leasing_v1_lease_delete(): - response = client.delete(f'/leasing/v1/lease/{LEASE_REF}', headers={'authorization': __bearer_token(ORIGIN_REF)}) + response = client.get('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)}) + active_lease_list = response.json().get('active_lease_list') + lease_ref = active_lease_list[0] + + ### + + response = client.delete(f'/leasing/v1/lease/{lease_ref}', headers={'authorization': __bearer_token(ORIGIN_REF)}) assert response.status_code == 200 - assert response.json()['lease_ref'] == LEASE_REF + assert response.json().get('lease_ref') == lease_ref def test_leasing_v1_lessor_lease_remove(): - test_leasing_v1_lessor() + lease_ref = test_leasing_v1_lessor() response = client.delete('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)}) assert response.status_code == 200 released_lease_list = response.json().get('released_lease_list') assert len(released_lease_list) == 1 - assert released_lease_list[0] == LEASE_REF + assert released_lease_list[0] == lease_ref From 5a5ad0e6542b5b8605efbe6fe2f48c20a4df5732 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 3 Jan 2023 14:09:19 +0100 Subject: [PATCH 12/16] removed 'scope_ref' from code checks because we only support one 'ALLOTMENT_REF', so we need no checks --- app/main.py | 6 +++--- app/orm.py | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/main.py b/app/main.py index 9259843..303f81d 100644 --- a/app/main.py +++ b/app/main.py @@ -363,8 +363,8 @@ async def leasing_v1_lessor(request: Request): lease_result_list = [] for scope_ref in scope_ref_list: - if scope_ref not in [ALLOTMENT_REF]: - raise HTTPException(status_code=500, detail=f'no service instances found for scopes: ["{scope_ref}"]') + # if scope_ref not in [ALLOTMENT_REF]: + # raise HTTPException(status_code=500, detail=f'no service instances found for scopes: ["{scope_ref}"]') lease_ref = str(uuid4()) expires = cur_time + LEASE_EXPIRE_DELTA @@ -381,7 +381,7 @@ async def leasing_v1_lessor(request: Request): } }) - data = Lease(origin_ref=origin_ref, scope_ref=scope_ref, lease_ref=lease_ref, lease_created=cur_time, lease_expires=expires) + data = Lease(origin_ref=origin_ref, lease_ref=lease_ref, lease_created=cur_time, lease_expires=expires) Lease.create_or_update(db, data) response = { diff --git a/app/orm.py b/app/orm.py index 8128804..1dc5de8 100644 --- a/app/orm.py +++ b/app/orm.py @@ -72,7 +72,7 @@ class Lease(Base): 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 - scope_ref = Column(CHAR(length=36), nullable=False, index=True) # uuid4 + # scope_ref = Column(CHAR(length=36), nullable=False, index=True) # uuid4 # not necessary, we only support one scope_ref lease_created = Column(DATETIME(), nullable=False) lease_expires = Column(DATETIME(), nullable=False) lease_updated = Column(DATETIME(), nullable=False) @@ -84,7 +84,7 @@ class Lease(Base): return { 'lease_ref': self.lease_ref, 'origin_ref': self.origin_ref, - 'scope_ref': self.scope_ref, + # 'scope_ref': self.scope_ref, 'lease_created': self.lease_created.isoformat(), 'lease_expires': self.lease_expires.isoformat(), 'lease_updated': self.lease_updated.isoformat(), @@ -180,14 +180,14 @@ def migrate(engine: Engine): Lease.__table__.drop(bind=engine) init(engine) - def upgrade_1_2_to_1_3(): - x = db.dialect.get_columns(engine.connect(), Lease.__tablename__) - x = next((_ for _ in x if _['name'] == 'scope_ref'), None) - if x is None: - Lease.scope_ref.compile() - column_name = Lease.scope_ref.name - column_type = Lease.scope_ref.type.compile(engine.dialect) - engine.execute(f'ALTER TABLE "{Lease.__tablename__}" ADD COLUMN "{column_name}" {column_type}') + # def upgrade_1_2_to_1_3(): + # x = db.dialect.get_columns(engine.connect(), Lease.__tablename__) + # x = next((_ for _ in x if _['name'] == 'scope_ref'), None) + # if x is None: + # Lease.scope_ref.compile() + # column_name = Lease.scope_ref.name + # column_type = Lease.scope_ref.type.compile(engine.dialect) + # engine.execute(f'ALTER TABLE "{Lease.__tablename__}" ADD COLUMN "{column_name}" {column_type}') upgrade_1_0_to_1_1() - upgrade_1_2_to_1_3() + # upgrade_1_2_to_1_3() From 146ae8b82438cff8051f26e971551c635753809e Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 3 Jan 2023 14:09:35 +0100 Subject: [PATCH 13/16] updated docs --- doc/Reverse Engineering Notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/Reverse Engineering Notes.md b/doc/Reverse Engineering Notes.md index 6746cf7..920a2c3 100644 --- a/doc/Reverse Engineering Notes.md +++ b/doc/Reverse Engineering Notes.md @@ -33,6 +33,8 @@ nvidia-gridd[2986]: License acquired successfully. (Info: license.nvidia.space, Most variables and configs are stored in `/var/lib/docker/volumes/configurations/_data`. +Files can be modified with `docker cp :/venv/... /opt/localfile/...` and back. + ## Dive / Docker image inspector - `dive dls:appliance` From ef1730f4fe1a0fee9661b32721c4c8a5fb68517d Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 3 Jan 2023 14:20:13 +0100 Subject: [PATCH 14/16] orm.py - added some docs --- app/orm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/orm.py b/app/orm.py index 1dc5de8..1232460 100644 --- a/app/orm.py +++ b/app/orm.py @@ -13,6 +13,7 @@ class Origin(Base): origin_ref = Column(CHAR(length=36), primary_key=True, unique=True, index=True) # uuid4 + # service_instance_xid = Column(CHAR(length=36), nullable=False, index=True) # uuid4 # not necessary, we only support one service_instance_xid ('INSTANCE_REF') hostname = Column(VARCHAR(length=256), nullable=True) guest_driver_version = Column(VARCHAR(length=10), nullable=True) os_platform = Column(VARCHAR(length=256), nullable=True) @@ -24,6 +25,7 @@ class Origin(Base): def serialize(self) -> dict: return { 'origin_ref': self.origin_ref, + # 'service_instance_xid': self.service_instance_xid, 'hostname': self.hostname, 'guest_driver_version': self.guest_driver_version, 'os_platform': self.os_platform, @@ -72,7 +74,7 @@ class Lease(Base): 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 - # scope_ref = Column(CHAR(length=36), nullable=False, index=True) # uuid4 # not necessary, we only support one scope_ref + # scope_ref = Column(CHAR(length=36), nullable=False, index=True) # uuid4 # not necessary, we only support one scope_ref ('ALLOTMENT_REF') lease_created = Column(DATETIME(), nullable=False) lease_expires = Column(DATETIME(), nullable=False) lease_updated = Column(DATETIME(), nullable=False) From b64c5318985d6ad010bb45ca47116011ef4858b8 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 3 Jan 2023 14:50:52 +0100 Subject: [PATCH 15/16] bump version to 1.3 --- version.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.env b/version.env index 955cc3f..5be527b 100644 --- a/version.env +++ b/version.env @@ -1 +1 @@ -VERSION=1.2 +VERSION=1.3 From 18e9ab2ebffbf140c38da36ee93959138b859f3a Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 3 Jan 2023 14:52:31 +0100 Subject: [PATCH 16/16] fixes --- app/orm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/orm.py b/app/orm.py index 1232460..efc5853 100644 --- a/app/orm.py +++ b/app/orm.py @@ -80,7 +80,7 @@ class Lease(Base): lease_updated = Column(DATETIME(), nullable=False) def __repr__(self): - return f'Lease(origin_ref={self.origin_ref}, scope_ref={self.scope_ref}, lease_ref={self.lease_ref}, expires={self.lease_expires})' + return f'Lease(origin_ref={self.origin_ref}, lease_ref={self.lease_ref}, expires={self.lease_expires})' def serialize(self) -> dict: return {