add azaion loader

This commit is contained in:
Alex Bezdieniezhnykh
2025-06-01 19:16:49 +03:00
parent 2584b4f125
commit 500db31142
54 changed files with 629 additions and 291 deletions
-20
View File
@@ -1,20 +0,0 @@
from user cimport User
from credentials cimport Credentials
from cdn_manager cimport CDNManager
cdef class ApiClient:
cdef Credentials credentials
cdef CDNManager cdn_manager
cdef str token, folder, api_url
cdef User user
cdef set_credentials(self, Credentials credentials)
cdef login(self)
cdef set_token(self, str token)
cdef get_user(self)
cdef load_bytes(self, str filename, str folder)
cdef upload_file(self, str filename, bytes resource, str folder)
cdef load_big_small_resource(self, str resource_name, str folder, str key)
cdef upload_big_small_resource(self, bytes resource, str resource_name, str folder, str key)
-177
View File
@@ -1,177 +0,0 @@
import json
import os
from http import HTTPStatus
from os import path
from uuid import UUID
import jwt
import requests
cimport constants
import yaml
from requests import HTTPError
from cdn_manager cimport CDNManager, CDNCredentials
from hardware_service cimport HardwareService
from security cimport Security
from user cimport User, RoleEnum
cdef class ApiClient:
"""Handles API authentication and downloading of the AI model."""
def __init__(self, str api_url):
self.credentials = None
self.user = None
self.token = None
self.cdn_manager = None
self.api_url = api_url
cdef set_credentials(self, Credentials credentials):
self.credentials = credentials
if self.cdn_manager is not None:
return
yaml_bytes = self.load_bytes(constants.CDN_CONFIG, <str>'')
yaml_config = yaml.safe_load(yaml_bytes)
creds = CDNCredentials(yaml_config["host"],
yaml_config["downloader_access_key"],
yaml_config["downloader_access_secret"],
yaml_config["uploader_access_key"],
yaml_config["uploader_access_secret"])
self.cdn_manager = CDNManager(creds)
cdef login(self):
response = None
try:
response = requests.post(f"{self.api_url}/login",
json={"email": self.credentials.email, "password": self.credentials.password})
response.raise_for_status()
token = response.json()["token"]
self.set_token(token)
except HTTPError as e:
print(response.json())
if response.status_code == HTTPStatus.CONFLICT:
res = response.json()
raise Exception(res['Message'])
cdef set_token(self, str token):
self.token = token
claims = jwt.decode(token, options={"verify_signature": False})
try:
id = str(UUID(claims.get("nameid", "")))
except ValueError:
raise ValueError("Invalid GUID format in claims")
email = claims.get("unique_name", "")
role_str = claims.get("role", "")
if role_str == "ApiAdmin":
role = RoleEnum.ApiAdmin
elif role_str == "Admin":
role = RoleEnum.Admin
elif role_str == "ResourceUploader":
role = RoleEnum.ResourceUploader
elif role_str == "Validator":
role = RoleEnum.Validator
elif role_str == "Operator":
role = RoleEnum.Operator
else:
role = RoleEnum.NONE
self.user = User(id, email, role)
cdef get_user(self):
if self.user is None:
self.login()
return self.user
cdef upload_file(self, str filename, bytes resource, str folder):
if self.token is None:
self.login()
url = f"{self.api_url}/resources/{folder}"
headers = { "Authorization": f"Bearer {self.token}" }
files = {'data': (filename, resource)}
try:
r = requests.post(url, headers=headers, files=files, allow_redirects=True)
r.raise_for_status()
constants.log(f"Uploaded {filename} to {self.api_url}/{folder} successfully: {r.status_code}.")
except Exception as e:
constants.log(f"Upload fail: {e}")
cdef load_bytes(self, str filename, str folder):
hardware_service = HardwareService()
cdef str hardware = hardware_service.get_hardware_info()
if self.token is None:
self.login()
url = f"{self.api_url}/resources/get/{folder}"
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
payload = json.dumps(
{
"password": self.credentials.password,
"hardware": hardware,
"fileName": filename
}, indent=4)
response = requests.post(url, data=payload, headers=headers, stream=True)
if response.status_code == HTTPStatus.UNAUTHORIZED or response.status_code == HTTPStatus.FORBIDDEN:
self.login()
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
response = requests.post(url, data=payload, headers=headers, stream=True)
if response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR:
print('500!')
hw_hash = Security.get_hw_hash(hardware)
key = Security.get_api_encryption_key(self.credentials, hw_hash)
resp_bytes = response.raw.read()
data = Security.decrypt_to(resp_bytes, key)
constants.log(<str>f'Downloaded file: {filename}, {len(data)} bytes')
return data
cdef load_big_file_cdn(self, str folder, str big_part):
print(f'downloading file {folder}\\{big_part} from cdn...')
if self.cdn_manager.download(folder, big_part):
with open(path.join(<str> folder, big_part), 'rb') as binary_file:
encrypted_bytes_big = binary_file.read()
else:
return None
cdef load_big_small_resource(self, str resource_name, str folder, str key):
cdef str big_part = f'{resource_name}.big'
cdef str small_part = f'{resource_name}.small'
encrypted_bytes_small = self.load_bytes(small_part, folder)
print(f'checking on existence for {folder}\\{big_part}')
if os.path.exists(os.path.join(<str> folder, big_part)):
with open(path.join(<str> folder, big_part), 'rb') as binary_file:
local_bytes_big = binary_file.read()
print(f'local file {folder}\\{big_part} is found!')
try:
return Security.decrypt_to(encrypted_bytes_small + local_bytes_big, key)
except Exception as ex:
print('Local file doesnt match with api file, old version')
remote_bytes_big = self.load_big_file_cdn(folder, big_part)
return Security.decrypt_to(encrypted_bytes_small + remote_bytes_big)
cdef upload_big_small_resource(self, bytes resource, str resource_name, str folder, str key):
cdef str big_part_name = f'{resource_name}.big'
cdef str small_part_name = f'{resource_name}.small'
resource_encrypted = Security.encrypt_to(<bytes>resource, key)
part_small_size = min(constants.SMALL_SIZE_KB * 1024, int(0.3 * len(resource_encrypted)))
part_small = resource_encrypted[:part_small_size] # slice bytes for part1
part_big = resource_encrypted[part_small_size:]
self.cdn_manager.upload(folder, <str>big_part_name, part_big)
with open(path.join(<str>folder, <str>big_part_name), 'wb') as f:
f.write(part_big)
self.upload_file(small_part_name, part_small, folder)
-2
View File
@@ -26,8 +26,6 @@ tmp_ret = collect_all('pycuda')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('pynvml')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('boto3')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('jwt')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
-1
View File
@@ -33,7 +33,6 @@ venv\Scripts\pyinstaller --name=azaion-inference ^
--collect-all tensorrt ^
--collect-all pycuda ^
--collect-all pynvml ^
--collect-all boto3 ^
--collect-all jwt ^
--hidden-import constants ^
--hidden-import annotation ^
-14
View File
@@ -1,14 +0,0 @@
cdef class CDNCredentials:
cdef str host
cdef str downloader_access_key
cdef str downloader_access_secret
cdef str uploader_access_key
cdef str uploader_access_secret
cdef class CDNManager:
cdef CDNCredentials creds
cdef object download_client
cdef object upload_client
cdef upload(self, str bucket, str filename, bytes file_bytes)
cdef download(self, str bucket, str filename)
-41
View File
@@ -1,41 +0,0 @@
import io
import boto3
cdef class CDNCredentials:
def __init__(self, host, downloader_access_key, downloader_access_secret, uploader_access_key, uploader_access_secret):
self.host = host
self.downloader_access_key = downloader_access_key
self.downloader_access_secret = downloader_access_secret
self.uploader_access_key = uploader_access_key
self.uploader_access_secret = uploader_access_secret
cdef class CDNManager:
def __init__(self, CDNCredentials credentials):
self.creds = credentials
self.download_client = boto3.client('s3', endpoint_url=self.creds.host,
aws_access_key_id=self.creds.downloader_access_key,
aws_secret_access_key=self.creds.downloader_access_secret)
self.upload_client = boto3.client('s3', endpoint_url=self.creds.host,
aws_access_key_id=self.creds.uploader_access_key,
aws_secret_access_key=self.creds.uploader_access_secret)
cdef upload(self, str bucket, str filename, bytes file_bytes):
try:
self.upload_client.upload_fileobj(io.BytesIO(file_bytes), bucket, filename)
print(f'uploaded {filename} ({len(file_bytes)} bytes) to the {bucket}')
return True
except Exception as e:
print(e)
return False
cdef download(self, str folder, str filename):
try:
self.download_client.download_file(folder, filename, f'{folder}\\{filename}')
print(f'downloaded {filename} from the {folder} to current folder')
return True
except Exception as e:
print(e)
return False
-5
View File
@@ -2,12 +2,7 @@ import time
cdef str CONFIG_FILE = "config.yaml" # Port for the zmq
cdef int QUEUE_MAXSIZE = 1000 # Maximum size of the command queue
cdef str COMMANDS_QUEUE = "azaion-commands"
cdef str ANNOTATIONS_QUEUE = "azaion-annotations"
cdef str QUEUE_CONFIG_FILENAME = "secured-config.json"
cdef str AI_ONNX_MODEL_FILE = "azaion.onnx"
cdef str CDN_CONFIG = "cdn.yaml"
-6
View File
@@ -1,6 +0,0 @@
cdef class Credentials:
cdef public str email
cdef public str password
@staticmethod
cdef from_msgpack(bytes data)
-15
View File
@@ -1,15 +0,0 @@
from msgpack import unpackb
cdef class Credentials:
def __init__(self, str email, str password):
self.email = email
self.password = password
@staticmethod
cdef from_msgpack(bytes data):
unpacked = unpackb(data, strict_map_key=False)
return Credentials(
unpacked.get("Email"),
unpacked.get("Password"))
-6
View File
@@ -1,6 +0,0 @@
cdef class FileData:
cdef public str folder
cdef public str filename
@staticmethod
cdef from_msgpack(bytes data)
-14
View File
@@ -1,14 +0,0 @@
from msgpack import unpackb
cdef class FileData:
def __init__(self, str folder, str filename):
self.folder = folder
self.filename = filename
@staticmethod
cdef from_msgpack(bytes data):
unpacked = unpackb(data, strict_map_key=False)
return FileData(
unpacked.get("Folder"),
unpacked.get("Filename"))
-5
View File
@@ -1,5 +0,0 @@
cdef class HardwareService:
@staticmethod
cdef has_nvidia_gpu()
cdef str get_hardware_info(self)
-61
View File
@@ -1,61 +0,0 @@
import os
import subprocess
import pynvml
cdef class HardwareService:
@staticmethod
cdef has_nvidia_gpu():
try:
pynvml.nvmlInit()
device_count = pynvml.nvmlDeviceGetCount()
if device_count > 0:
print(f"Found NVIDIA GPU(s).")
return True
else:
print("No NVIDIA GPUs found by NVML.")
return False
except pynvml.NVMLError as error:
print(f"Failed to find NVIDIA GPU")
return False
finally:
try:
pynvml.nvmlShutdown()
except:
print('Failed to shutdown pynvml cause probably no NVidia GPU')
pass
cdef str get_hardware_info(self):
if os.name == 'nt': # windows
os_command = (
"powershell -Command \""
"Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; "
"Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; "
"Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output; "
"(Get-Disk | Where-Object {$_.IsSystem -eq $true}).SerialNumber"
"\""
)
else:
os_command = (
"lscpu | grep 'Model name:' | cut -d':' -f2 && "
"lspci | grep VGA | cut -d':' -f3 && "
"free -k | awk '/^Mem:/ {print $2}' && "
"cat /sys/block/sda/device/vpd_pg80 2>/dev/null || cat /sys/block/sda/device/serial 2>/dev/null"
)
result = subprocess.check_output(os_command, shell=True).decode('utf-8', errors='ignore')
lines = [line.replace(" ", " ").replace("Name=", "").strip('\x00\x14 \t\n\r\v\f') for line in result.splitlines() if line.strip()]
cdef str cpu = lines[0]
cdef str gpu = lines[1]
# could be multiple gpus
len_lines = len(lines)
cdef str memory = lines[len_lines-2].replace("TotalVisibleMemorySize=", "").replace(" ", " ")
cdef str drive_serial = lines[len_lines-1]
cdef str res = f'CPU: {cpu}. GPU: {gpu}. Memory: {memory}. DriveSerial: {drive_serial}'
return res
+2 -5
View File
@@ -6,27 +6,24 @@ from threading import Thread
import yaml
from api_client cimport ApiClient
from annotation cimport Annotation
from inference cimport Inference
from remote_command cimport RemoteCommand, CommandType
from remote_command_handler cimport RemoteCommandHandler
from credentials cimport Credentials
from file_data cimport FileData
cdef class CommandProcessor:
cdef ApiClient api_client
cdef RemoteCommandHandler remote_handler
cdef object inference_queue
cdef bint running
cdef Inference inference
def __init__(self, int zmq_port, str api_url):
self.api_client = ApiClient(api_url)
self.remote_handler = RemoteCommandHandler(zmq_port, self.on_command)
self.inference_queue = Queue(maxsize=constants.QUEUE_MAXSIZE)
self.remote_handler.start()
self.running = True
#TODO: replace api_client to azaion_loader.exe call
self.inference = Inference(self.api_client, self.on_annotation)
def start(self):
-20
View File
@@ -1,20 +0,0 @@
from credentials cimport Credentials
cdef class Security:
@staticmethod
cdef encrypt_to(input_stream, key)
@staticmethod
cdef decrypt_to(input_bytes, key)
@staticmethod
cdef get_hw_hash(str hardware)
@staticmethod
cdef get_api_encryption_key(Credentials credentials, str hardware_hash)
@staticmethod
cdef get_model_encryption_key()
@staticmethod
cdef calc_hash(str key)
-68
View File
@@ -1,68 +0,0 @@
import base64
import hashlib
import os
from hashlib import sha384
from credentials cimport Credentials
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
BUFFER_SIZE = 64 * 1024 # 64 KB
cdef class Security:
@staticmethod
cdef encrypt_to(input_bytes, key):
cdef bytes aes_key = hashlib.sha256(key.encode('utf-8')).digest()
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(<bytes> aes_key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
padder = padding.PKCS7(128).padder()
padded_plaintext = padder.update(input_bytes) + padder.finalize()
ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize()
return iv + ciphertext
@staticmethod
cdef decrypt_to(ciphertext_with_iv_bytes, key):
cdef bytes aes_key = hashlib.sha256(key.encode('utf-8')).digest()
iv = ciphertext_with_iv_bytes[:16]
ciphertext_bytes = ciphertext_with_iv_bytes[16:]
cipher = Cipher(algorithms.AES(<bytes>aes_key), modes.CBC(<bytes>iv), backend=default_backend())
decryptor = cipher.decryptor()
decrypted_padded_bytes = decryptor.update(ciphertext_bytes) + decryptor.finalize()
# Manual PKCS7 unpadding check and removal
padding_value = decrypted_padded_bytes[-1] # Get the last byte, which indicates padding length
if 1 <= padding_value <= 16: # Valid PKCS7 padding value range for AES-128
padding_length = padding_value
plaintext_bytes = decrypted_padded_bytes[:-padding_length] # Remove padding bytes
else:
plaintext_bytes = decrypted_padded_bytes
return bytes(plaintext_bytes)
@staticmethod
cdef get_hw_hash(str hardware):
cdef str key = f'Azaion_{hardware}_%$$$)0_'
return Security.calc_hash(key)
@staticmethod
cdef get_api_encryption_key(Credentials creds, str hardware_hash):
cdef str key = f'{creds.email}-{creds.password}-{hardware_hash}-#%@AzaionKey@%#---'
return Security.calc_hash(key)
@staticmethod
cdef get_model_encryption_key():
cdef str key = '-#%@AzaionKey@%#---234sdfklgvhjbnn'
return Security.calc_hash(key)
@staticmethod
cdef calc_hash(str key):
str_bytes = key.encode('utf-8')
hash_bytes = sha384(str_bytes).digest()
cdef str h = base64.b64encode(hash_bytes).decode('utf-8')
return h
-15
View File
@@ -1,15 +0,0 @@
cdef enum RoleEnum:
NONE = 0
Operator = 10
Validator = 20
CompanionPC = 30
Admin = 40
ResourceUploader = 50
ApiAdmin = 1000
cdef class User:
cdef public str id
cdef public str email
cdef public RoleEnum role
cdef bytes serialize(self)
-15
View File
@@ -1,15 +0,0 @@
import msgpack
cdef class User:
def __init__(self, str id, str email, RoleEnum role):
self.id = id
self.email = email
self.role = role
cdef bytes serialize(self):
return msgpack.packb({
"i": self.id,
"e": self.email,
"r": self.role
})