Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f5a0b2d
feat: README
lg-epitech Nov 11, 2025
2429a5b
feat(api): endpoint DELETE vms/:id , stops and erases vm, deletes its…
skl1017 Nov 14, 2025
c3e51f6
Merge branch 'feat/api-integration' of github.com:PoCInnovation/Distr…
skl1017 Nov 14, 2025
d66196f
feat(api): endpoint DELETE /vms/:id/password , resets the password to…
skl1017 Nov 14, 2025
9419884
fix: changed vm creation script for distribox image, now using virt-c…
skl1017 Nov 18, 2025
859d60c
Merge branch 'test/vm_creation' of github.com:PoCInnovation/Distribox…
skl1017 Nov 18, 2025
22181bc
fix: changed virtual image size to 5G instead of 10
skl1017 Nov 19, 2025
7bda9b2
feat: create-distribox-image script now takes 2 arguments, a download…
skl1017 Nov 20, 2025
3aeb148
Merge branch 'feat/api-integration' into test/vm_creation
skl1017 Nov 20, 2025
d292ea7
feat: uncommentend xml_builder to use iso cloud init
skl1017 Nov 20, 2025
6b7c760
feat: installation scripts for ubuntu, debian and almalinux
skl1017 Dec 1, 2025
5faa339
fix: uncommentend forgotten commentend lines for ISO device
skl1017 Dec 1, 2025
20d2cad
feat: added GET /images to see all available distribox images
skl1017 Dec 15, 2025
04e0762
feat: added disk extension feature to POST /vms
skl1017 Dec 15, 2025
d4e5f8f
Merge branch 'test/vm_creation' into feat/api-integration
skl1017 Dec 16, 2025
0984fb3
feat: added GET /host/info to gather infos such as cpu, ram, and disk…
skl1017 Dec 24, 2025
ca8b2c9
GET /host/info : added percent_used_per_vm, percent_used_total_vms
skl1017 Dec 26, 2025
ed73f52
feat: added response models to routes to complete fastAPI generated d…
skl1017 Dec 30, 2025
fdb39a0
merged main to feat/api-integration
skl1017 Jan 5, 2026
0b17e31
Merge branch 'feat/api-integration' of github.com:PoCInnovation/Distr…
skl1017 Jan 5, 2026
66edaf0
style: reformat code with autopep8
skl1017 Jan 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
/backend/venv

__pycache__/

.vscode/
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,13 @@ Developers
| [<img src=".github/assets/loan.jpeg" width=85><br><sub>Loan Riyanto</sub>](https://github.com/skl1017)
| :---: |

<<<<<<< HEAD
Manager
| [<img src=".github/assets/laurent.jpg" width=85><br><sub>Laurent Gonzalez</sub>](https://github.com/lg-epitech)
=======
### Manager
| [<img src="https://avatars.githubusercontent.com/lg-epitech" width=85><br><sub>Laurent Gonzalez</sub>](https://github.com/lg-epitech) |
>>>>>>> main
| :---: |

<h2 align=center>
Expand Down
14 changes: 10 additions & 4 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import libvirt
from app.orm.vm import VmORM
from dotenv import load_dotenv
from os import getenv
from sqlmodel import create_engine, SQLModel
from app.telemetry.monitor import SystemMonitor
from app.orm.vm import VmORM

load_dotenv()

Expand All @@ -11,7 +12,8 @@
db_pass = getenv("POSTGRES_PASSWORD", "distribox_password")
db_port = getenv("POSTGRES_PORT", "5432")

database_url = f"postgresql+psycopg2://{db_user}:{db_pass}@localhost:{db_port}/{db_name}"
database_url = f"postgresql+psycopg2://{db_user}:{
db_pass}@localhost:{db_port}/{db_name}"
engine = create_engine(database_url, echo=True)
SQLModel.metadata.create_all(engine)

Expand All @@ -23,7 +25,11 @@ class QEMUConfig:
def get_connection(cls):
if cls.qemu_conn is None or cls.qemu_conn.isAlive() == 0:
try:
qemu_conn = libvirt.open("qemu:///system")
cls.qemu_conn = libvirt.open("qemu:///system")
except libvirt.libvirtError:
raise
return qemu_conn
return cls.qemu_conn


system_monitor = SystemMonitor(interval=3,
get_connection=QEMUConfig.get_connection)
13 changes: 8 additions & 5 deletions backend/app/core/xml_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@ def build_xml(vm_read: VmRead):
etree.SubElement(channel, "target", type="virtio",
name="org.qemu.guest_agent.0")

# disk_seed = etree.SubElement(devices, "disk", type="file", device="cdrom")
# etree.SubElement(disk_seed, "driver", name="qemu", type="raw")
# etree.SubElement(disk_seed, "source", file="/var/lib/distribox/images/seed.iso")
# etree.SubElement(disk_seed, "target", dev="hdb", bus="ide")
# etree.SubElement(disk_seed, "readonly")
disk_seed = etree.SubElement(devices, "disk", type="file", device="cdrom")
etree.SubElement(disk_seed, "driver", name="qemu", type="raw")
etree.SubElement(
disk_seed,
"source",
file="/var/lib/distribox/images/seed.iso")
etree.SubElement(disk_seed, "target", dev="hdb", bus="ide")
etree.SubElement(disk_seed, "readonly")

iface = etree.SubElement(devices, "interface", type="network")
etree.SubElement(iface, "source", network="default")
Expand Down
4 changes: 3 additions & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from app.routes import vm
from app.routes import vm, image, host
app = FastAPI()


Expand All @@ -19,3 +19,5 @@ async def global_exception_handler(_, exc: Exception):
content={"detail": str(exc)}
)
app.include_router(vm.router, prefix="/vms")
app.include_router(image.router, prefix="/images")
app.include_router(host.router, prefix="/host")
20 changes: 20 additions & 0 deletions backend/app/models/host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pydantic import BaseModel
from app.models.resources import MemoryInfoBase, DiskInfoBase, CPUInfoBase


class DiskInfoHost(DiskInfoBase):
distribox_used: float


class MemoryInfoHost(MemoryInfoBase):
pass


class CPUInfoHost(CPUInfoBase):
pass


class HostInfoBase(BaseModel):
disk: DiskInfoHost
mem: MemoryInfoHost
cpu: CPUInfoHost
11 changes: 11 additions & 0 deletions backend/app/models/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pydantic import BaseModel


class ImageBase(BaseModel):
name: str
virtual_size: float
actual_size: float


class ImageRead(ImageBase):
pass
24 changes: 24 additions & 0 deletions backend/app/models/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from pydantic import BaseModel


class ResourceStatsBase(BaseModel):
total: float
used: float
available: float
percent_used: float


class DiskInfoBase(ResourceStatsBase):
pass


class MemoryInfoBase(ResourceStatsBase):
pass


class CPUInfoBase(BaseModel):
percent_used_total: float
percent_used_per_cpu: list[float]
percent_used_per_vm: list
percent_used_total_vms: float
cpu_count: int
8 changes: 6 additions & 2 deletions backend/app/models/vm.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pydantic import BaseModel
from typing import Optional
from uuid import UUID


class VmBase(BaseModel):
Expand All @@ -10,9 +10,13 @@ class VmBase(BaseModel):


class VmRead(VmBase):
id: str
id: UUID
state: str


class VmCreate(VmBase):
pass


class PasswordCreated(BaseModel):
password: str
12 changes: 12 additions & 0 deletions backend/app/routes/host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from fastapi import APIRouter, status
from app.services.host_service import HostService
from app.models.host import HostInfoBase


router = APIRouter()


@router.get("/info", status_code=status.HTTP_200_OK,
response_model=HostInfoBase)
def get_host_info():
return HostService.get_host_info()
15 changes: 15 additions & 0 deletions backend/app/routes/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from fastapi import APIRouter, status
from app.services.image_service import ImageService
from app.models.image import ImageRead


router = APIRouter()


@router.get("/", status_code=status.HTTP_200_OK,
response_model=list[ImageRead])
def get_distribox_image_list():
try:
return ImageService.get_distribox_image_list()
except Exception:
raise
36 changes: 23 additions & 13 deletions backend/app/routes/vm.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,56 @@
from fastapi import status, APIRouter
from app.models.vm import VmCreate
from app.models.vm import VmCreate, VmRead, PasswordCreated
from app.services.vm_service import VmService

router = APIRouter()


@router.get('/', status_code=status.HTTP_200_OK)
@router.get('/', status_code=status.HTTP_200_OK, response_model=list[VmRead])
def get_vm_list():
vm_list = VmService.get_vm_list()
return vm_list


@router.get("/{vm_id}", status_code=status.HTTP_200_OK)
@router.get("/{vm_id}", status_code=status.HTTP_200_OK, response_model=VmRead)
def get_vm(vm_id: str):
vm = VmService.get_vm(vm_id)
return vm


@router.get("/{vm_id}/state", status_code=status.HTTP_200_OK)
def get_vm(vm_id: str):
state = VmService.get_state(vm_id)
return state


@router.post("/{vm_id}/start", status_code=status.HTTP_200_OK)
@router.post("/{vm_id}/start",
status_code=status.HTTP_200_OK,
response_model=VmRead)
def start_vm(vm_id: str):
vm = VmService.start_vm(vm_id)
return vm


@router.post("/{vm_id}/stop", status_code=status.HTTP_200_OK)
@router.post("/{vm_id}/stop",
status_code=status.HTTP_200_OK,
response_model=VmRead)
def stop_vm(vm_id: str):
vm = VmService.stop_vm(vm_id)
return vm


@router.post("/", status_code=status.HTTP_201_CREATED)
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=VmRead)
def create_vm(vm: VmCreate):
created_vm = VmService.create_vm(vm)
return created_vm


@router.put("/{vm_id}/password", status_code=status.HTTP_200_OK)
@router.put("/{vm_id}/password",
status_code=status.HTTP_200_OK,
response_model=PasswordCreated)
def set_vm_password(vm_id):
return VmService.set_vm_password(vm_id)


@router.delete("/{vm_id}", status_code=status.HTTP_204_NO_CONTENT)
def remove_vm(vm_id: str):
VmService.remove_vm(vm_id)


@router.delete("/{vm_id}/password", status_code=status.HTTP_204_NO_CONTENT)
def remove_vm_password(vm_id: str):
VmService.remove_vm_password(vm_id)
98 changes: 98 additions & 0 deletions backend/app/services/host_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import shutil
import psutil
from app.core.config import QEMUConfig
from threading import Thread
from app.models.host import HostInfoBase
from app.services.vm_service import VmService
import libvirt
from time import sleep
from collections import Counter
from app.core.config import system_monitor

# cpu_total_usage = 0
# usage_per_cpu = []

# def get_cpu_usage_percent(cpu_idle_time_t2, cpu_idle_time_t1, cpu_total_time_t2, cpu_total_time_t1):
# return (1 - (cpu_idle_time_t2 - cpu_idle_time_t1)/
# (sum(cpu_total_time_t2) - sum(cpu_total_time_t1))) * 100

# def get_cpu_counters():
# per_cpus = psutil.cpu_times(percpu=True)
# total = Counter()
# for cpu in per_cpus:
# total.update(cpu._asdict())
# cpu_total = psutil.cpu_times()
# return {
# "per_cpus": per_cpus,
# "cpu_total": cpu_total
# }

# def get_cpu_usage():
# global cpu_total_usage, usage_per_cpu
# while True:
# cpu_usage_t1 = get_cpu_counters()
# sleep(3)
# cpu_usage_t2 = get_cpu_counters()
# cpu_total_usage = get_cpu_usage_percent(cpu_usage_t2["cpu_total"].idle, cpu_usage_t1["cpu_total"].idle, cpu_usage_t2["cpu_total"], cpu_usage_t1["cpu_total"])
# usage_per_cpu = []
# for i in range(len(cpu_usage_t1["per_cpus"])):
# usage_per_cpu.append(round(get_cpu_usage_percent(cpu_usage_t2["per_cpus"][i].idle, cpu_usage_t1["per_cpus"][i].idle, cpu_usage_t2["per_cpus"][i], cpu_usage_t1["per_cpus"][i]), 2))


# Thread(target=get_cpu_usage, daemon=True).start()

conn = QEMUConfig.get_connection()
stats = conn.getAllDomainStats(
stats=libvirt.VIR_DOMAIN_STATS_CPU_TOTAL | libvirt.VIR_DOMAIN_STATS_INTERFACE,
flags=libvirt.VIR_CONNECT_GET_ALL_DOMAINS_STATS_RUNNING)

for s in stats:
print('lol')
print(s[0].name(), s[1])

# cpu_overall_time_t1 = psutil.cpu_times(percpu=True)
# cpu_total_time_t1 = psutil.cpu_times()
# print(sum(cpu_total_time_t1))
# cpu_idle_time_t1 = cpu_total_time_t1.idle


# sleep(3)
# cpu_total_time_t2 = psutil.cpu_times()
# cpu_idle_time_t2 = cpu_total_time_t2.idle

# percent_used = (1 - (cpu_idle_time_t2 - cpu_idle_time_t1)/ (sum(cpu_total_time_t2) - sum(cpu_total_time_t1))) * 100
# print(percent_used)
# print(psutil.cpu_percent(interval=1))
# print(sum(psutil.cpu_times(percpu=True)))


# print(psutil.cpu_times_percent(interval=2))


# for dom_stats in stats:
# data = dom_stats[1]
# print(data.get('cpu.system'))

class HostService:

@staticmethod
def get_host_info():
disk_usage = shutil.disk_usage("/")
mem_usage = psutil.virtual_memory()

disk = {
"total": round(disk_usage.total / 2**30, 2),
"used": round(disk_usage.used / 2**30, 2),
"available": round(disk_usage.free / 2**30, 2),
"percent_used": round((disk_usage.used / disk_usage.total) * 100, 2),
"distribox_used": sum([vm.disk_size for vm in VmService.get_vm_list()])
}
mem = {
"total": round(mem_usage.total / 2**30, 2),
"used": round(mem_usage.used / 2**30, 2),
"available": round(mem_usage.available / 2**30, 2),
"percent_used": mem_usage.percent
}
cpu = system_monitor.cpu
host_info = HostInfoBase(disk=disk, mem=mem, cpu=cpu)
return host_info
26 changes: 26 additions & 0 deletions backend/app/services/image_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import subprocess
import json
from app.models.image import ImageRead
from pathlib import Path


class ImageService():
@staticmethod
def get_distribox_image_list():
image_list = []
images_folder = Path("/var/lib/distribox/images")
for file in images_folder.iterdir():
image_info = subprocess.run(["qemu-img",
"info",
"--output=json",
file],
capture_output=True,
text=True,
check=True)
image_info_json = json.loads(image_info.stdout)
image_list.append(ImageRead(
name=image_info_json["filename"].split("/")[-1],
virtual_size=round(image_info_json["virtual-size"] / (1024 ** 3), 2),
actual_size=round(image_info_json["actual-size"] / (1024 ** 3), 2)
))
return image_list
Loading
Loading