use axum::{Json, extract::Path, http::StatusCode};
use axvm::{AxVMRef, AxVmError, VmStatus, VmVcpuState};
use axvmconfig::GuestConfig;
use serde_json::{Value, json};
use crate::http::auth::ApiToken;
use crate::manager::AxvmManager;
pub async fn list_vms() -> Json<Vec<Value>> {
let items: Vec<Value> = AxvmManager::vm_list().iter().map(vm_json_summary).collect();
Json(items)
}
pub async fn vm_detail(Path(id_str): Path<String>) -> Result<Json<Value>, StatusCode> {
let Ok(id) = id_str.parse::<usize>() else {
return Err(StatusCode::NOT_FOUND);
};
match AxvmManager::vm_by_id(id) {
Some(vm) => Ok(Json(vm_json(&vm, true))),
None => Err(StatusCode::NOT_FOUND),
}
}
pub async fn vm_create(
_token: ApiToken,
Json(payload): Json<Value>,
) -> Result<Json<Value>, StatusCode> {
let toml = payload
.get("toml")
.and_then(Value::as_str)
.ok_or(StatusCode::BAD_REQUEST)?;
let config = GuestConfig::from_toml(toml).map_err(|_| StatusCode::BAD_REQUEST)?;
let id = config.base.id;
if AxvmManager::vm_by_id(id).is_some() {
return Err(StatusCode::CONFLICT);
}
match AxvmManager::create_vm_from_toml(toml) {
Ok(id) => {
info!("HTTP: VM[{id}] created via control API");
Ok(Json(json!({ "id": id })))
}
Err(error) => {
error!("HTTP: create VM[{id}] failed: {error:#}");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
pub async fn vm_delete(
_token: ApiToken,
Path(id_str): Path<String>,
) -> Result<StatusCode, StatusCode> {
let Ok(id) = id_str.parse::<usize>() else {
return Err(StatusCode::NOT_FOUND);
};
let vm = AxvmManager::vm_by_id(id).ok_or(StatusCode::NOT_FOUND)?;
vm.destroy()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
AxvmManager::remove_vm(id).ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
info!("HTTP: VM[{id}] removed via control API");
Ok(StatusCode::NO_CONTENT)
}
pub async fn vm_start(
_token: ApiToken,
Path(id_str): Path<String>,
) -> Result<Json<Value>, StatusCode> {
vm_action(&id_str, VmAction::Start)
}
pub async fn vm_stop(
_token: ApiToken,
Path(id_str): Path<String>,
) -> Result<Json<Value>, StatusCode> {
vm_action(&id_str, VmAction::Stop)
}
enum VmAction {
Start,
Stop,
}
fn vm_action(id_str: &str, action: VmAction) -> Result<Json<Value>, StatusCode> {
let Ok(id) = id_str.parse::<usize>() else {
return Err(StatusCode::NOT_FOUND);
};
if matches!(action, VmAction::Start)
&& AxvmManager::vm_by_id(id).is_some_and(|vm| vm.status() == VmStatus::Stopped)
{
return Err(StatusCode::CONFLICT);
}
let result = match action {
VmAction::Start => AxvmManager::start_vm(id),
VmAction::Stop => AxvmManager::stop_vm(id),
};
match result {
Ok(()) => Ok(Json(vm_action_json(id, action))),
Err(error) => Err(map_axvm_error(error)),
}
}
fn vm_action_json(id: usize, action: VmAction) -> Value {
let status = AxvmManager::vm_by_id(id)
.map(|vm| vm.status().as_str())
.unwrap_or("unknown");
json!({
"ok": true,
"status": status,
"async": matches!(action, VmAction::Stop),
})
}
fn map_axvm_error(error: anyhow::Error) -> StatusCode {
let cause = error.root_cause();
match cause.downcast_ref::<AxVmError>() {
Some(AxVmError::InvalidTransition { .. } | AxVmError::InvalidState { .. }) => {
StatusCode::CONFLICT
}
Some(AxVmError::OutOfMemory { .. } | AxVmError::ResourceUnavailable { .. }) => {
StatusCode::SERVICE_UNAVAILABLE
}
Some(AxVmError::VmNotFound { .. }) => StatusCode::NOT_FOUND,
_ => {
error!("management HTTP action failed: {error:#}");
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
fn vm_json_summary(vm: &AxVMRef) -> Value {
vm_json(vm, false)
}
fn vm_json(vm: &AxVMRef, with_vcpus: bool) -> Value {
let memory_mb = vm
.memory_regions()
.iter()
.fold(0usize, |acc, region| acc.saturating_add(region.size()))
/ (1024 * 1024);
let mut json = json!({
"id": vm.id(),
"name": vm.name(),
"status": vm.status().as_str(),
"cpu_num": vm.vcpu_num(),
"memory_mb": memory_mb,
});
if with_vcpus {
let vcpus: Vec<Value> = vm
.vcpu_snapshots()
.iter()
.map(|vcpu| {
json!({
"id": vcpu.id,
"state": vcpu_state_str(vcpu.state),
"phys_cpu_set": vcpu.phys_cpu_set,
})
})
.collect();
json["vcpu_states"] = json!(vcpus);
}
json
}
fn vcpu_state_str(state: VmVcpuState) -> &'static str {
match state {
VmVcpuState::Invalid => "invalid",
VmVcpuState::Created => "created",
VmVcpuState::Free => "free",
VmVcpuState::Ready => "ready",
VmVcpuState::Running => "running",
VmVcpuState::Blocked => "blocked",
VmVcpuState::Starting => "starting",
}
}