Skip to content

Commit

Permalink
Merge pull request #69 from Stella-IT/staging
Browse files Browse the repository at this point in the history
v1.3.10
  • Loading branch information
Alex4386 authored Nov 12, 2022
2 parents d2f3b33 + 88ea159 commit 7d0b7c0
Show file tree
Hide file tree
Showing 15 changed files with 309 additions and 24 deletions.
10 changes: 8 additions & 2 deletions API/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from fastapi.security import HTTPBasic, HTTPBasicCredentials

from API.v1 import v1_router as _v1_router
from app.security import Security
from app.services.info import Info
from app.settings import Settings

router = APIRouter()
router = APIRouter(
dependencies=[
*Security.get_authentication_dependencies(),
]
)

_v1_prefix = "/v1"
router.include_router(_v1_router, prefix=_v1_prefix)
Expand Down
3 changes: 1 addition & 2 deletions API/v1/VDI/sr.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ async def vdi_get_sr(cluster_id: str, vdi_uuid: str, url_after: str = ""):

try:
sr = vdi.get_SR()
except Exception as e:
except Exception:
session.xenapi.session.logout()
print(e)
raise HTTPException(
status_code=404, detail=f"VDI {vdi_uuid} does not have proper SR"
)
Expand Down
2 changes: 2 additions & 0 deletions API/v1/VM/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from API.v1.VM.specs import router as _vm_specs
from API.v1.VM.vbd import router as _vm_vbd
from API.v1.VM.vif import router as _vm_vif
from API.v1.VM.xenstore import router as _vm_xenstore
from app.settings import Settings


Expand Down Expand Up @@ -72,4 +73,5 @@ async def verify_vm_uuid(cluster_id: str, vm_uuid: Optional[str] = None):
vm_router.include_router(_vm_specs, tags=["vm"])
vm_router.include_router(_vm_vbd, tags=["vm"])
vm_router.include_router(_vm_vif, tags=["vm"])
vm_router.include_router(_vm_xenstore, tags=["vm"])
vm_router.include_router(_vm_first_vif, tags=["vm"])
29 changes: 29 additions & 0 deletions API/v1/VM/bios.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,35 @@ async def instance_get_bios(cluster_id: str, vm_uuid: str):
raise HTTPException(status_code=500, detail=rd_error.strerror)


@router.patch("/{cluster_id}/vm/{vm_uuid}/bios")
@router.patch("/{cluster_id}/template/{vm_uuid}/bios")
async def instance_add_bios_property(request: Request, cluster_id: str, vm_uuid: str):
"""Add Instance (VM/Template) BIOS Property by Name"""
try:
body = ujson.decode(await request.body())

session = create_session(
_id=cluster_id, get_xen_clusters=Settings.get_xen_clusters()
)

vm: VM = VM.get_by_uuid(session=session, uuid=vm_uuid)
ret = dict(success=vm.add_bios_strings(body))

session.xenapi.session.logout()
return ret
except Failure as xenapi_error:
raise HTTPException(
status_code=500, detail=xenapi_failure_jsonify(xenapi_error)
)
except Fault as xml_rpc_error:
raise HTTPException(
status_code=int(xml_rpc_error.faultCode),
detail=xml_rpc_error.faultString,
)
except RemoteDisconnected as rd_error:
raise HTTPException(status_code=500, detail=rd_error.strerror)


@router.put("/{cluster_id}/vm/{vm_uuid}/bios")
@router.put("/{cluster_id}/template/{vm_uuid}/bios")
async def instance_set_bios_property(request: Request, cluster_id: str, vm_uuid: str):
Expand Down
98 changes: 98 additions & 0 deletions API/v1/VM/xenstore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from http.client import RemoteDisconnected
from xmlrpc.client import Fault

import ujson
from fastapi import APIRouter, HTTPException, Request
from XenAPI.XenAPI import Failure
from XenGarden.session import create_session
from XenGarden.VM import VM

from API.v1.Common import xenapi_failure_jsonify
from app.settings import Settings

router = APIRouter()


@router.get("/{cluster_id}/vm/{vm_uuid}/xenstore")
@router.get("/{cluster_id}/template/{vm_uuid}/xenstore")
async def instance_get_xenstore(cluster_id: str, vm_uuid: str):
"""Get Instance (VM/Template) XenStore"""
try:
session = create_session(
_id=cluster_id, get_xen_clusters=Settings.get_xen_clusters()
)

vm: VM = VM.get_by_uuid(session=session, uuid=vm_uuid)
xenstore = vm.get_xenstore()

session.xenapi.session.logout()
return dict(success=True, data=xenstore)
except Failure as xenapi_error:
raise HTTPException(
status_code=500, detail=xenapi_failure_jsonify(xenapi_error)
)
except Fault as xml_rpc_error:
raise HTTPException(
status_code=int(xml_rpc_error.faultCode),
detail=xml_rpc_error.faultString,
)
except RemoteDisconnected as rd_error:
raise HTTPException(status_code=500, detail=rd_error.strerror)


@router.patch("/{cluster_id}/vm/{vm_uuid}/xenstore")
@router.patch("/{cluster_id}/template/{vm_uuid}/xenstore")
async def instance_add_xen_store(request: Request, cluster_id: str, vm_uuid: str):
"""Add Instance (VM/Template) XenStore by Name"""
try:
body = ujson.decode(await request.body())

session = create_session(
_id=cluster_id, get_xen_clusters=Settings.get_xen_clusters()
)

vm: VM = VM.get_by_uuid(session=session, uuid=vm_uuid)
ret = dict(success=vm.add_xenstore(body))

session.xenapi.session.logout()
return ret
except Failure as xenapi_error:
raise HTTPException(
status_code=500, detail=xenapi_failure_jsonify(xenapi_error)
)
except Fault as xml_rpc_error:
raise HTTPException(
status_code=int(xml_rpc_error.faultCode),
detail=xml_rpc_error.faultString,
)
except RemoteDisconnected as rd_error:
raise HTTPException(status_code=500, detail=rd_error.strerror)


@router.put("/{cluster_id}/vm/{vm_uuid}/xenstore")
@router.put("/{cluster_id}/template/{vm_uuid}/xenstore")
async def instance_set_bios_property(request: Request, cluster_id: str, vm_uuid: str):
"""Set Instance (VM/Template) XenStore by Name"""
try:
body = ujson.decode(await request.body())

session = create_session(
_id=cluster_id, get_xen_clusters=Settings.get_xen_clusters()
)

vm: VM = VM.get_by_uuid(session=session, uuid=vm_uuid)
ret = dict(success=vm.set_xenstore(body))

session.xenapi.session.logout()
return ret
except Failure as xenapi_error:
raise HTTPException(
status_code=500, detail=xenapi_failure_jsonify(xenapi_error)
)
except Fault as xml_rpc_error:
raise HTTPException(
status_code=int(xml_rpc_error.faultCode),
detail=xml_rpc_error.faultString,
)
except RemoteDisconnected as rd_error:
raise HTTPException(status_code=500, detail=rd_error.strerror)
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020 Stella IT Co, Ltd.
Copyright (c) 2020-2022 Stella IT Inc. and XenXenXenSe Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__all__ = ["services", "controller", "settings", "extension"]
__all__ = ["services", "controller", "settings", "security", "extension"]
2 changes: 1 addition & 1 deletion app/controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,5 @@ def start(self) -> None:

asyncio.ensure_future(self.core.make_process(), loop=self.loop)
self.loop.run_forever()
except TypeError:
except TypeError as e:
pass
72 changes: 72 additions & 0 deletions app/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import secrets

from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

from .settings import Settings


class Security:
fastapi_basic = HTTPBasic()

@classmethod
def load_authentication_config(cls):
return Settings.get_authentication_config()

@classmethod
def load_authentication_type(cls):
config = cls.load_authentication_config()

if config is None:
return None

return str(config["type"]).lower()

@classmethod
def load_accounts(cls):
config = cls.load_authentication_config()

accounts = []
if config is not None:
accounts = config["accounts"]

return accounts

@classmethod
def get_authentication_dependencies(cls):
dependencies = []

if cls.load_authentication_type() == "basic":
dependencies.append(Depends(cls.authenticate_basic))

return dependencies

@classmethod
def authenticate_basic(
cls, credentials: HTTPBasicCredentials = Depends(fastapi_basic)
):
if cls.load_authentication_type() == "basic":
match_found = False

username_bytes = credentials.username.encode("utf8")
password_bytes = credentials.password.encode("utf8")

for account in cls.load_accounts():
account_username_bytes = account["username"].encode("utf8")
account_password_bytes = account["password"].encode("utf8")

username_match = secrets.compare_digest(
username_bytes, account_username_bytes
)
password_match = secrets.compare_digest(
password_bytes, account_password_bytes
)
if username_match and password_match:
match_found = True

if not match_found:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"type": "unauthorized"},
headers={"WWW-Authenticate": "Basic"},
)
3 changes: 2 additions & 1 deletion app/services/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ def make_process(self):

self.server.run(
app=self,
debug=self._asgi_debug,
# https://github.com/encode/uvicorn/pull/1640
# debug=self._asgi_debug,
log_config=self._log_config,
**_connect_option,
)
93 changes: 81 additions & 12 deletions app/settings.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import os
from typing import Dict
import time
from typing import Dict, Union

import ujson


class Settings:
config_filename = "config.json"

config_cache = None
config_last_modified = 0
config_last_fetched = 0
config_last_modified_fetched_at = 0
config_last_modified_fetched_at_age = 10

@classmethod
def is_docker(cls) -> bool:
return "DOCKER_XXXS_CONFIG" in os.environ
Expand All @@ -15,17 +24,77 @@ def get_docker_config(cls) -> dict:
return load_config

@classmethod
def get_xen_clusters(cls) -> Dict:
"""set xen credentials"""
xen_clusters = {}
if os.path.isfile("config.json"):
with open("config.json", "r") as config_file:
xen_clusters = ujson.load(config_file)["xen_clusters"]
config_file.close()

# Docker check
def _fetch_config_from_file(cls) -> dict:
try:
if os.path.isfile(cls.config_filename):
result = None
with open(cls.config_filename, "r") as config_file:
result = ujson.load(config_file)
config_file.close()

cls.config_cache = result
cls.config_last_fetched = time.time()
return result
except Exception:
print("Failed")

@classmethod
def _is_config_modified(cls) -> bool:
if cls.config_cache is None:
return True

if cls._is_config_last_modified_cache_expired():
cls._fetch_last_modified_cache()

return cls.config_last_fetched < cls.config_last_modified

@classmethod
def _fetch_last_modified_cache(cls) -> bool:
cls.config_last_modified_fetched_at = time.time()
cls.config_last_modified = os.path.getmtime(cls.config_filename)
return cls.config_last_modified

@classmethod
def _is_config_last_modified_cache_expired(cls) -> bool:
return (
cls.config_last_modified_fetched_at
< time.time() - cls.config_last_modified_fetched_at_age
)

@classmethod
def get_config_json(cls) -> dict:
if cls.is_docker():
if "xen_clusters" in cls.get_docker_config():
xen_clusters = cls.get_docker_config()["xen_clusters"]
return cls.get_docker_config()
else:
if not cls._is_config_modified():
return cls.config_cache

if os.path.isfile(cls.config_filename):
return cls._fetch_config_from_file()
else:
print("Error: XenXenXenSe has failed to initialize!")
print()
print(
"both config.json file and docker config environment variable (DOCKER_XXXS_CONFIG) are missing!"
)
print("Please configure XenXenXenSe!")
exit(1)

@classmethod
def get_xen_clusters(cls) -> Dict:
"""get xen credentials"""
xen_clusters = cls.get_config_json()["xen_clusters"]
return xen_clusters

@classmethod
def get_authentication_config(cls) -> Union[Dict, None]:
"""get authentication settings"""
config = cls.get_config_json()
if "authentication" in config:
return config["authentication"]

return None


class MissingConfigFile(IOError):
"Missing local settings file"
Loading

0 comments on commit 7d0b7c0

Please sign in to comment.