2023-06-12 10:42:13 +00:00
import logging
2023-01-17 15:37:45 +00:00
from datetime import datetime , timedelta
2022-12-22 11:57:06 +00:00
2023-01-17 15:37:45 +00:00
from dateutil . relativedelta import relativedelta
2023-01-30 09:22:18 +00:00
from sqlalchemy import Column , VARCHAR , CHAR , ForeignKey , DATETIME , update , and_ , inspect , text
2022-12-28 10:05:41 +00:00
from sqlalchemy . engine import Engine
2023-06-12 13:13:29 +00:00
from sqlalchemy . orm import sessionmaker , declarative_base , Session , relationship
2023-06-12 10:42:13 +00:00
2024-06-21 17:01:33 +00:00
from util import NV
2023-06-12 10:42:13 +00:00
logging . basicConfig ( )
logger = logging . getLogger ( __name__ )
logger . setLevel ( logging . INFO )
2022-12-22 11:57:06 +00:00
Base = declarative_base ( )
2023-06-12 10:42:13 +00:00
class Site ( Base ) :
__tablename__ = " site "
INITIAL_SITE_KEY_XID = ' 00000000-0000-0000-0000-000000000000 '
INITIAL_SITE_NAME = ' default '
site_key = Column ( CHAR ( length = 36 ) , primary_key = True , unique = True , index = True ) # uuid4, SITE_KEY_XID
name = Column ( VARCHAR ( length = 256 ) , nullable = False )
def __str__ ( self ) :
return f ' SITE_KEY_XID: { self . site_key } '
@staticmethod
def create_statement ( engine : Engine ) :
from sqlalchemy . schema import CreateTable
return CreateTable ( Site . __table__ ) . compile ( engine )
@staticmethod
def get_default_site ( engine : Engine ) - > " Site " :
session = sessionmaker ( bind = engine ) ( )
entity = session . query ( Site ) . filter ( Site . site_key == Site . INITIAL_SITE_KEY_XID ) . first ( )
session . close ( )
return entity
class Instance ( Base ) :
__tablename__ = " instance "
DEFAULT_INSTANCE_REF = ' 10000000-0000-0000-0000-000000000001 '
DEFAULT_TOKEN_EXPIRE_DELTA = 86_400 # 1 day
DEFAULT_LEASE_EXPIRE_DELTA = 7_776_000 # 90 days
DEFAULT_LEASE_RENEWAL_PERIOD = 0.15
DEFAULT_CLIENT_TOKEN_EXPIRE_DELTA = 378_432_000 # 12 years
# 1 day = 86400 (min. in production setup, max 90 days), 1 hour = 3600
instance_ref = Column ( CHAR ( length = 36 ) , primary_key = True , unique = True , index = True ) # uuid4, INSTANCE_REF
site_key = Column ( CHAR ( length = 36 ) , ForeignKey ( Site . site_key , ondelete = ' CASCADE ' ) , nullable = False , index = True ) # uuid4
private_key = Column ( BLOB ( length = 2048 ) , nullable = False )
public_key = Column ( BLOB ( length = 512 ) , nullable = False )
token_expire_delta = Column ( INT ( ) , nullable = False , default = DEFAULT_TOKEN_EXPIRE_DELTA , comment = ' in seconds ' )
lease_expire_delta = Column ( INT ( ) , nullable = False , default = DEFAULT_LEASE_EXPIRE_DELTA , comment = ' in seconds ' )
lease_renewal_period = Column ( FLOAT ( precision = 2 ) , nullable = False , default = DEFAULT_LEASE_RENEWAL_PERIOD )
client_token_expire_delta = Column ( INT ( ) , nullable = False , default = DEFAULT_CLIENT_TOKEN_EXPIRE_DELTA , comment = ' in seconds ' )
2023-06-12 13:13:29 +00:00
__origin = relationship ( Site , foreign_keys = [ site_key ] )
2023-06-12 10:42:13 +00:00
def __str__ ( self ) :
return f ' INSTANCE_REF: { self . instance_ref } (SITE_KEY_XID: { self . site_key } ) '
@staticmethod
def create_statement ( engine : Engine ) :
from sqlalchemy . schema import CreateTable
return CreateTable ( Instance . __table__ ) . compile ( engine )
@staticmethod
def create_or_update ( engine : Engine , instance : " Instance " ) :
session = sessionmaker ( bind = engine ) ( )
entity = session . query ( Instance ) . filter ( Instance . instance_ref == instance . instance_ref ) . first ( )
if entity is None :
session . add ( instance )
else :
x = dict (
site_key = instance . site_key ,
private_key = instance . private_key ,
public_key = instance . public_key ,
token_expire_delta = instance . token_expire_delta ,
lease_expire_delta = instance . lease_expire_delta ,
lease_renewal_period = instance . lease_renewal_period ,
client_token_expire_delta = instance . client_token_expire_delta ,
)
session . execute ( update ( Instance ) . where ( Instance . instance_ref == instance . instance_ref ) . values ( * * x ) )
session . commit ( )
session . flush ( )
session . close ( )
# todo: validate on startup that "lease_expire_delta" is between 1 day and 90 days
@staticmethod
def get_default_instance ( engine : Engine ) - > " Instance " :
session = sessionmaker ( bind = engine ) ( )
site = Site . get_default_site ( engine )
entity = session . query ( Instance ) . filter ( Instance . site_key == site . site_key ) . first ( )
session . close ( )
return entity
def get_token_expire_delta ( self ) - > " dateutil.relativedelta.relativedelta " :
return relativedelta ( seconds = self . token_expire_delta )
def get_lease_expire_delta ( self ) - > " dateutil.relativedelta.relativedelta " :
return relativedelta ( seconds = self . lease_expire_delta )
def get_lease_renewal_delta ( self ) - > " datetime.timedelta " :
return timedelta ( seconds = self . lease_expire_delta )
def get_client_token_expire_delta ( self ) - > " dateutil.relativedelta.relativedelta " :
return relativedelta ( seconds = self . client_token_expire_delta )
2023-06-12 13:14:12 +00:00
def __get_private_key ( self ) - > " RsaKey " :
return parse_key ( self . private_key )
2023-06-12 10:42:13 +00:00
def get_public_key ( self ) - > " RsaKey " :
return parse_key ( self . public_key )
def get_jwt_encode_key ( self ) - > " jose.jkw " :
from jose import jwk
from jose . constants import ALGORITHMS
2023-06-12 13:14:12 +00:00
return jwk . construct ( self . __get_private_key ( ) . export_key ( ) . decode ( ' utf-8 ' ) , algorithm = ALGORITHMS . RS256 )
2023-06-12 10:42:13 +00:00
def get_jwt_decode_key ( self ) - > " jose.jwt " :
from jose import jwk
from jose . constants import ALGORITHMS
2023-06-12 13:14:12 +00:00
return jwk . construct ( self . get_public_key ( ) . export_key ( ) . decode ( ' utf-8 ' ) , algorithm = ALGORITHMS . RS256 )
2023-06-12 10:42:13 +00:00
2023-06-12 13:14:12 +00:00
def get_private_key_str ( self , encoding : str = ' utf-8 ' ) - > str :
2023-06-12 10:42:13 +00:00
return self . private_key . decode ( encoding )
2023-06-12 13:14:12 +00:00
def get_public_key_str ( self , encoding : str = ' utf-8 ' ) - > str :
2023-06-12 10:42:13 +00:00
return self . private_key . decode ( encoding )
2022-12-22 11:57:06 +00:00
class Origin ( Base ) :
__tablename__ = " origin "
origin_ref = Column ( CHAR ( length = 36 ) , primary_key = True , unique = True , index = True ) # uuid4
2023-01-03 13:20:13 +00:00
# service_instance_xid = Column(CHAR(length=36), nullable=False, index=True) # uuid4 # not necessary, we only support one service_instance_xid ('INSTANCE_REF')
2022-12-22 11:57:06 +00:00
hostname = Column ( VARCHAR ( length = 256 ) , nullable = True )
guest_driver_version = Column ( VARCHAR ( length = 10 ) , nullable = True )
os_platform = Column ( VARCHAR ( length = 256 ) , nullable = True )
os_version = Column ( VARCHAR ( length = 256 ) , nullable = True )
def __repr__ ( self ) :
return f ' Origin(origin_ref= { self . origin_ref } , hostname= { self . hostname } ) '
2022-12-29 08:00:52 +00:00
def serialize ( self ) - > dict :
2024-06-21 17:01:33 +00:00
_ = NV ( ) . find ( self . guest_driver_version )
2022-12-29 08:00:52 +00:00
return {
' origin_ref ' : self . origin_ref ,
2023-01-03 13:20:13 +00:00
# 'service_instance_xid': self.service_instance_xid,
2022-12-29 08:00:52 +00:00
' hostname ' : self . hostname ,
' guest_driver_version ' : self . guest_driver_version ,
' os_platform ' : self . os_platform ,
' os_version ' : self . os_version ,
2024-06-21 17:01:33 +00:00
' $driver ' : _ if _ is not None else None ,
2022-12-29 08:00:52 +00:00
}
2022-12-22 11:57:06 +00:00
@staticmethod
def create_statement ( engine : Engine ) :
from sqlalchemy . schema import CreateTable
return CreateTable ( Origin . __table__ ) . compile ( engine )
@staticmethod
def create_or_update ( engine : Engine , origin : " Origin " ) :
2022-12-29 06:09:39 +00:00
session = sessionmaker ( bind = engine ) ( )
2022-12-22 11:57:06 +00:00
entity = session . query ( Origin ) . filter ( Origin . origin_ref == origin . origin_ref ) . first ( )
if entity is None :
session . add ( origin )
else :
2022-12-29 08:40:36 +00:00
x = dict (
2022-12-23 07:16:58 +00:00
hostname = origin . hostname ,
guest_driver_version = origin . guest_driver_version ,
os_platform = origin . os_platform ,
2022-12-29 08:40:36 +00:00
os_version = origin . os_version
2022-12-23 07:16:58 +00:00
)
2022-12-29 08:40:36 +00:00
session . execute ( update ( Origin ) . where ( Origin . origin_ref == origin . origin_ref ) . values ( * * x ) )
2022-12-29 06:09:39 +00:00
session . commit ( )
2022-12-22 11:57:06 +00:00
session . flush ( )
session . close ( )
2022-12-29 08:57:37 +00:00
@staticmethod
2023-01-23 06:12:02 +00:00
def delete ( engine : Engine , origin_refs : [ str ] = None ) - > int :
2022-12-29 08:57:37 +00:00
session = sessionmaker ( bind = engine ) ( )
2023-01-23 06:12:02 +00:00
if origin_refs is None :
2022-12-29 08:57:37 +00:00
deletions = session . query ( Origin ) . delete ( )
else :
2023-01-23 06:12:02 +00:00
deletions = session . query ( Origin ) . filter ( Origin . origin_ref in origin_refs ) . delete ( )
2022-12-29 08:57:37 +00:00
session . commit ( )
session . close ( )
return deletions
2022-12-22 11:57:06 +00:00
class Lease ( Base ) :
__tablename__ = " lease "
2023-06-12 13:14:12 +00:00
instance_ref = Column ( CHAR ( length = 36 ) , ForeignKey ( Instance . instance_ref , ondelete = ' CASCADE ' ) , nullable = False , index = True ) # uuid4
2022-12-23 07:22:21 +00:00
lease_ref = Column ( CHAR ( length = 36 ) , primary_key = True , nullable = False , index = True ) # uuid4
2022-12-29 08:57:37 +00:00
origin_ref = Column ( CHAR ( length = 36 ) , ForeignKey ( Origin . origin_ref , ondelete = ' CASCADE ' ) , nullable = False , index = True ) # uuid4
2023-01-03 13:20:13 +00:00
# scope_ref = Column(CHAR(length=36), nullable=False, index=True) # uuid4 # not necessary, we only support one scope_ref ('ALLOTMENT_REF')
2022-12-22 11:57:06 +00:00
lease_created = Column ( DATETIME ( ) , nullable = False )
lease_expires = Column ( DATETIME ( ) , nullable = False )
lease_updated = Column ( DATETIME ( ) , nullable = False )
2023-06-12 13:13:29 +00:00
__instance = relationship ( Instance , foreign_keys = [ instance_ref ] )
__origin = relationship ( Origin , foreign_keys = [ origin_ref ] )
2022-12-22 11:57:06 +00:00
def __repr__ ( self ) :
2023-01-03 13:52:31 +00:00
return f ' Lease(origin_ref= { self . origin_ref } , lease_ref= { self . lease_ref } , expires= { self . lease_expires } ) '
2022-12-22 11:57:06 +00:00
2023-06-12 13:14:12 +00:00
def serialize ( self ) - > dict :
renewal_period = self . __instance . lease_renewal_period
renewal_delta = self . __instance . get_lease_renewal_delta
2023-01-17 15:37:45 +00:00
lease_renewal = int ( Lease . calculate_renewal ( renewal_period , renewal_delta ) . total_seconds ( ) )
2023-01-17 16:27:26 +00:00
lease_renewal = self . lease_updated + relativedelta ( seconds = lease_renewal )
2023-01-17 15:37:45 +00:00
2022-12-29 08:00:52 +00:00
return {
' lease_ref ' : self . lease_ref ,
' origin_ref ' : self . origin_ref ,
2023-01-03 13:09:19 +00:00
# 'scope_ref': self.scope_ref,
2022-12-29 08:00:52 +00:00
' lease_created ' : self . lease_created . isoformat ( ) ,
' lease_expires ' : self . lease_expires . isoformat ( ) ,
' lease_updated ' : self . lease_updated . isoformat ( ) ,
2023-01-17 15:37:45 +00:00
' lease_renewal ' : lease_renewal . isoformat ( ) ,
2022-12-29 08:00:52 +00:00
}
2022-12-22 11:57:06 +00:00
@staticmethod
def create_statement ( engine : Engine ) :
from sqlalchemy . schema import CreateTable
return CreateTable ( Lease . __table__ ) . compile ( engine )
@staticmethod
def create_or_update ( engine : Engine , lease : " Lease " ) :
2022-12-29 06:09:39 +00:00
session = sessionmaker ( bind = engine ) ( )
2022-12-29 08:40:36 +00:00
entity = session . query ( Lease ) . filter ( Lease . lease_ref == lease . lease_ref ) . first ( )
2022-12-22 11:57:06 +00:00
if entity is None :
2022-12-27 18:57:58 +00:00
if lease . lease_updated is None :
lease . lease_updated = lease . lease_created
2022-12-22 11:57:06 +00:00
session . add ( lease )
else :
2022-12-29 08:40:36 +00:00
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 ) )
2022-12-29 06:09:39 +00:00
session . commit ( )
2022-12-22 11:57:06 +00:00
session . flush ( )
session . close ( )
@staticmethod
def find_by_origin_ref ( engine : Engine , origin_ref : str ) - > [ " Lease " ] :
2022-12-29 06:09:39 +00:00
session = sessionmaker ( bind = engine ) ( )
2022-12-22 11:57:06 +00:00
entities = session . query ( Lease ) . filter ( Lease . origin_ref == origin_ref ) . all ( )
session . close ( )
return entities
2022-12-29 18:03:09 +00:00
@staticmethod
def find_by_lease_ref ( engine : Engine , lease_ref : str ) - > " Lease " :
session = sessionmaker ( bind = engine ) ( )
entity = session . query ( Lease ) . filter ( Lease . lease_ref == lease_ref ) . first ( )
session . close ( )
return entity
2022-12-22 11:57:06 +00:00
@staticmethod
def find_by_origin_ref_and_lease_ref ( engine : Engine , origin_ref : str , lease_ref : str ) - > " Lease " :
2022-12-29 06:09:39 +00:00
session = sessionmaker ( bind = engine ) ( )
2022-12-22 11:57:06 +00:00
entity = session . query ( Lease ) . filter ( and_ ( Lease . origin_ref == origin_ref , Lease . lease_ref == lease_ref ) ) . first ( )
session . close ( )
return entity
@staticmethod
2023-01-17 15:37:45 +00:00
def renew ( engine : Engine , lease : " Lease " , lease_expires : datetime , lease_updated : datetime ) :
2022-12-29 06:09:39 +00:00
session = sessionmaker ( bind = engine ) ( )
2022-12-29 18:00:14 +00:00
x = dict ( lease_expires = lease_expires , lease_updated = lease_updated )
2022-12-29 08:40:36 +00:00
session . execute ( update ( Lease ) . where ( and_ ( Lease . origin_ref == lease . origin_ref , Lease . lease_ref == lease . lease_ref ) ) . values ( * * x ) )
2022-12-29 06:09:39 +00:00
session . commit ( )
2022-12-22 11:57:06 +00:00
session . close ( )
@staticmethod
def cleanup ( engine : Engine , origin_ref : str ) - > int :
2022-12-29 06:09:39 +00:00
session = sessionmaker ( bind = engine ) ( )
2022-12-27 19:28:09 +00:00
deletions = session . query ( Lease ) . filter ( Lease . origin_ref == origin_ref ) . delete ( )
2022-12-29 06:09:39 +00:00
session . commit ( )
2022-12-22 11:57:06 +00:00
session . close ( )
return deletions
2022-12-23 12:17:19 +00:00
2022-12-29 08:57:37 +00:00
@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
2023-06-12 08:48:00 +00:00
@staticmethod
def delete_expired ( engine : Engine ) - > int :
session = sessionmaker ( bind = engine ) ( )
deletions = session . query ( Lease ) . filter ( Lease . lease_expires < = datetime . utcnow ( ) ) . delete ( )
session . commit ( )
session . close ( )
return deletions
2023-01-17 10:49:56 +00:00
@staticmethod
2023-01-17 15:37:45 +00:00
def calculate_renewal ( renewal_period : float , delta : timedelta ) - > timedelta :
2023-01-17 10:49:56 +00:00
"""
2023-01-17 15:37:45 +00:00
import datetime
2023-01-17 10:49:56 +00:00
LEASE_RENEWAL_PERIOD = 0.2 # 20%
delta = datetime . timedelta ( days = 1 )
renew = delta . total_seconds ( ) * LEASE_RENEWAL_PERIOD
2023-01-17 15:37:45 +00:00
renew = datetime . timedelta ( seconds = renew )
2023-01-17 10:49:56 +00:00
expires = delta - renew # 19.2
2023-01-19 06:25:44 +00:00
import datetime
LEASE_RENEWAL_PERIOD = 0.15 # 15%
delta = datetime . timedelta ( days = 90 )
renew = delta . total_seconds ( ) * LEASE_RENEWAL_PERIOD
renew = datetime . timedelta ( seconds = renew )
expires = delta - renew # 76 days, 12:00:00 hours
2023-01-17 10:49:56 +00:00
"""
renew = delta . total_seconds ( ) * renewal_period
2023-01-17 15:37:45 +00:00
renew = timedelta ( seconds = renew )
2023-01-17 16:27:26 +00:00
return renew
2023-01-17 10:49:56 +00:00
2022-12-23 12:17:19 +00:00
2023-06-12 10:42:13 +00:00
def init_default_site ( session : Session ) :
from uuid import uuid4
from app . util import generate_key
private_key = generate_key ( )
public_key = private_key . public_key ( )
site = Site (
site_key = Site . INITIAL_SITE_KEY_XID ,
name = Site . INITIAL_SITE_NAME
)
session . add ( site )
session . commit ( )
instance = Instance (
2023-06-12 13:14:12 +00:00
instance_ref = Instance . DEFAULT_INSTANCE_REF ,
2023-06-12 10:42:13 +00:00
site_key = site . site_key ,
private_key = private_key . export_key ( ) ,
public_key = public_key . export_key ( ) ,
)
session . add ( instance )
session . commit ( )
2022-12-23 12:17:19 +00:00
def init ( engine : Engine ) :
2023-06-12 10:42:13 +00:00
tables = [ Site , Instance , Origin , Lease ]
2022-12-23 12:17:19 +00:00
db = inspect ( engine )
session = sessionmaker ( bind = engine ) ( )
for table in tables :
2023-06-12 10:42:13 +00:00
exists = db . dialect . has_table ( engine . connect ( ) , table . __tablename__ )
logger . info ( f ' > Table " { table . __tablename__ : <16 } " exists: { exists } ' )
if not exists :
2023-01-30 09:22:18 +00:00
session . execute ( text ( str ( table . create_statement ( engine ) ) ) )
2022-12-29 06:09:39 +00:00
session . commit ( )
2023-06-12 10:42:13 +00:00
# create default site
cnt = session . query ( Site ) . count ( )
if cnt == 0 :
init_default_site ( session )
session . flush ( )
2022-12-23 12:17:19 +00:00
session . close ( )
2022-12-29 08:40:36 +00:00
def migrate ( engine : Engine ) :
2023-06-12 10:42:13 +00:00
from os import getenv as env
from os . path import join , dirname , isfile
from util import load_key
2022-12-29 08:40:36 +00:00
db = inspect ( engine )
2023-06-12 10:42:13 +00:00
# todo: add update guide to use 1.LATEST to 2.0
def upgrade_1_x_to_2_0 ( ) :
site = Site . get_default_site ( engine )
logger . info ( site )
instance = Instance . get_default_instance ( engine )
logger . info ( instance )
# SITE_KEY_XID
if site_key := env ( ' SITE_KEY_XID ' , None ) is not None :
site . site_key = str ( site_key )
# INSTANCE_REF
if instance_ref := env ( ' INSTANCE_REF ' , None ) is not None :
instance . instance_ref = str ( instance_ref )
# ALLOTMENT_REF
if allotment_ref := env ( ' ALLOTMENT_REF ' , None ) is not None :
pass # todo
# INSTANCE_KEY_RSA, INSTANCE_KEY_PUB
default_instance_private_key_path = str ( join ( dirname ( __file__ ) , ' cert/instance.private.pem ' ) )
if instance_private_key := env ( ' INSTANCE_KEY_RSA ' , None ) is not None :
instance . private_key = load_key ( str ( instance_private_key ) )
elif isfile ( default_instance_private_key_path ) :
instance . private_key = load_key ( default_instance_private_key_path )
default_instance_public_key_path = str ( join ( dirname ( __file__ ) , ' cert/instance.public.pem ' ) )
if instance_public_key := env ( ' INSTANCE_KEY_PUB ' , None ) is not None :
instance . public_key = load_key ( str ( instance_public_key ) )
elif isfile ( default_instance_public_key_path ) :
instance . public_key = load_key ( default_instance_public_key_path )
# TOKEN_EXPIRE_DELTA
if token_expire_delta := env ( ' TOKEN_EXPIRE_DAYS ' , None ) not in ( None , 0 ) :
instance . token_expire_delta = token_expire_delta * 86_400
if token_expire_delta := env ( ' TOKEN_EXPIRE_HOURS ' , None ) not in ( None , 0 ) :
instance . token_expire_delta = token_expire_delta * 3_600
# LEASE_EXPIRE_DELTA, LEASE_RENEWAL_DELTA
if lease_expire_delta := env ( ' LEASE_EXPIRE_DAYS ' , None ) not in ( None , 0 ) :
instance . lease_expire_delta = lease_expire_delta * 86_400
if lease_expire_delta := env ( ' LEASE_EXPIRE_HOURS ' , None ) not in ( None , 0 ) :
instance . lease_expire_delta = lease_expire_delta * 3_600
# LEASE_RENEWAL_PERIOD
if lease_renewal_period := env ( ' LEASE_RENEWAL_PERIOD ' , None ) is not None :
instance . lease_renewal_period = lease_renewal_period
# todo: update site, instance
upgrade_1_x_to_2_0 ( )