Implements VM groups API (#206)
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #206
This commit is contained in:
Pierre HUBERT 2024-11-28 18:06:20 +00:00
parent 821021e66f
commit ee77bd11c9
9 changed files with 420 additions and 31 deletions

View File

@ -1,5 +1,8 @@
use crate::controllers::{HttpResult, LibVirtReq};
use crate::extractors::group_vm_id_extractor::GroupVmIdExtractor;
use crate::libvirt_rest_structures::vm::VMInfo;
use actix_web::HttpResponse;
use std::collections::HashMap;
/// Get the list of groups
pub async fn list(client: LibVirtReq) -> HttpResult {
@ -14,3 +17,132 @@ pub async fn list(client: LibVirtReq) -> HttpResult {
Ok(HttpResponse::Ok().json(groups))
}
/// Get information about the VMs of a group
pub async fn vm_info(vms_xml: GroupVmIdExtractor) -> HttpResult {
let mut vms = Vec::new();
for vm in vms_xml.0 {
vms.push(VMInfo::from_domain(vm)?)
}
Ok(HttpResponse::Ok().json(vms))
}
#[derive(Default, serde::Serialize)]
pub struct TreatmentResult {
ok: usize,
failed: usize,
}
/// Start the VMs of a group
pub async fn vm_start(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult {
let mut res = TreatmentResult::default();
for vm in vms.0 {
if let Some(uuid) = vm.uuid {
match client.start_domain(uuid).await {
Ok(_) => res.ok += 1,
Err(_) => res.failed += 1,
}
}
}
Ok(HttpResponse::Ok().json(res))
}
/// Shutdown the VMs of a group
pub async fn vm_shutdown(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult {
let mut res = TreatmentResult::default();
for vm in vms.0 {
if let Some(uuid) = vm.uuid {
match client.shutdown_domain(uuid).await {
Ok(_) => res.ok += 1,
Err(_) => res.failed += 1,
}
}
}
Ok(HttpResponse::Ok().json(res))
}
/// Suspend the VMs of a group
pub async fn vm_suspend(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult {
let mut res = TreatmentResult::default();
for vm in vms.0 {
if let Some(uuid) = vm.uuid {
match client.suspend_domain(uuid).await {
Ok(_) => res.ok += 1,
Err(_) => res.failed += 1,
}
}
}
Ok(HttpResponse::Ok().json(res))
}
/// Resume the VMs of a group
pub async fn vm_resume(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult {
let mut res = TreatmentResult::default();
for vm in vms.0 {
if let Some(uuid) = vm.uuid {
match client.resume_domain(uuid).await {
Ok(_) => res.ok += 1,
Err(_) => res.failed += 1,
}
}
}
Ok(HttpResponse::Ok().json(res))
}
/// Kill the VMs of a group
pub async fn vm_kill(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult {
let mut res = TreatmentResult::default();
for vm in vms.0 {
if let Some(uuid) = vm.uuid {
match client.kill_domain(uuid).await {
Ok(_) => res.ok += 1,
Err(_) => res.failed += 1,
}
}
}
Ok(HttpResponse::Ok().json(res))
}
/// Reset the VMs of a group
pub async fn vm_reset(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult {
let mut res = TreatmentResult::default();
for vm in vms.0 {
if let Some(uuid) = vm.uuid {
match client.reset_domain(uuid).await {
Ok(_) => res.ok += 1,
Err(_) => res.failed += 1,
}
}
}
Ok(HttpResponse::Ok().json(res))
}
/// Get the screenshot of the VMs of a group
pub async fn vm_screenshot(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult {
if vms.0.is_empty() {
return Ok(HttpResponse::NoContent().finish());
}
let image = if vms.0.len() == 1 {
client.screenshot_domain(vms.0[0].uuid.unwrap()).await?
} else {
return Ok(
HttpResponse::UnprocessableEntity().json("Cannot return multiple VM screenshots!!")
);
};
Ok(HttpResponse::Ok().content_type("image/png").body(image))
}
/// Get the state of the VMs
pub async fn vm_state(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult {
let mut states = HashMap::new();
for vm in vms.0 {
if let Some(uuid) = vm.uuid {
states.insert(uuid, client.get_domain_state(uuid).await?);
}
}
Ok(HttpResponse::Ok().json(states))
}

View File

@ -0,0 +1,66 @@
use crate::controllers::LibVirtReq;
use crate::libvirt_lib_structures::domain::DomainXML;
use crate::libvirt_lib_structures::XMLUuid;
use crate::libvirt_rest_structures::vm::VMGroupId;
use actix_http::Payload;
use actix_web::error::ErrorBadRequest;
use actix_web::web::Query;
use actix_web::{web, Error, FromRequest, HttpRequest};
use std::future::Future;
use std::pin::Pin;
pub struct GroupVmIdExtractor(pub Vec<DomainXML>);
#[derive(serde::Deserialize)]
struct GroupIDInPath {
gid: VMGroupId,
}
#[derive(serde::Deserialize)]
struct FilterVM {
vm_id: Option<XMLUuid>,
}
impl FromRequest for GroupVmIdExtractor {
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let req = req.clone();
Box::pin(async move {
let Ok(group_id) =
web::Path::<GroupIDInPath>::from_request(&req, &mut Payload::None).await
else {
return Err(ErrorBadRequest("Group ID not specified in path!"));
};
let group_id = group_id.into_inner().gid;
let filter_vm = match Query::<FilterVM>::from_request(&req, &mut Payload::None).await {
Ok(v) => v,
Err(e) => {
log::error!("Failed to extract VM id from request! {e}");
return Err(ErrorBadRequest("Failed to extract VM id from request!"));
}
};
let Ok(client) = LibVirtReq::from_request(&req, &mut Payload::None).await else {
return Err(ErrorBadRequest("Failed to extract client handle!"));
};
let vms = match client.get_full_group_vm_list(&group_id).await {
Ok(vms) => vms,
Err(e) => {
log::error!("Failed to get the VMs of the group {group_id:?}: {e}");
return Err(ErrorBadRequest("Failed to get the VMs of the group!"));
}
};
// Filter (if requested by the user)
Ok(GroupVmIdExtractor(match filter_vm.vm_id {
None => vms,
Some(id) => vms.into_iter().filter(|vms| vms.uuid == Some(id)).collect(),
}))
})
}
}

View File

@ -1,3 +1,4 @@
pub mod api_auth_extractor;
pub mod auth_extractor;
pub mod group_vm_id_extractor;
pub mod local_auth_extractor;

View File

@ -122,6 +122,21 @@ impl LibVirtClient {
Ok(out)
}
/// Get the full list of VMs of a given group
pub async fn get_full_group_vm_list(
&self,
group: &VMGroupId,
) -> anyhow::Result<Vec<DomainXML>> {
let vms = self.get_full_domains_list().await?;
let mut out = Vec::new();
for vm in vms {
if VMInfo::from_domain(vm.clone())?.group == Some(group.clone()) {
out.push(vm);
}
}
Ok(out)
}
/// Update a network configuration
pub async fn update_network(
&self,

View File

@ -19,7 +19,7 @@ pub struct DomainMetadataXML {
}
/// OS information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "os")]
pub struct OSXML {
#[serde(rename = "@firmware", default)]
@ -29,7 +29,7 @@ pub struct OSXML {
}
/// OS Type information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "os")]
pub struct OSTypeXML {
#[serde(rename = "@arch")]
@ -41,7 +41,7 @@ pub struct OSTypeXML {
}
/// OS Loader information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "loader")]
pub struct OSLoaderXML {
#[serde(rename = "@secure")]
@ -49,39 +49,39 @@ pub struct OSLoaderXML {
}
/// Hypervisor features
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
#[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)]
#[serde(rename = "features")]
pub struct FeaturesXML {
pub acpi: ACPIXML,
}
/// ACPI feature
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
#[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)]
#[serde(rename = "acpi")]
pub struct ACPIXML {}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "mac")]
pub struct NetMacAddress {
#[serde(rename = "@address")]
pub address: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "source")]
pub struct NetIntSourceXML {
#[serde(rename = "@network")]
pub network: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "model")]
pub struct NetIntModelXML {
#[serde(rename = "@type")]
pub r#type: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "filterref")]
pub struct NetIntFilterParameterXML {
#[serde(rename = "@name")]
@ -90,7 +90,7 @@ pub struct NetIntFilterParameterXML {
pub value: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "filterref")]
pub struct NetIntfilterRefXML {
#[serde(rename = "@filter")]
@ -99,7 +99,7 @@ pub struct NetIntfilterRefXML {
pub parameters: Vec<NetIntFilterParameterXML>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "interface")]
pub struct DomainNetInterfaceXML {
#[serde(rename = "@type")]
@ -113,14 +113,14 @@ pub struct DomainNetInterfaceXML {
pub filterref: Option<NetIntfilterRefXML>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "input")]
pub struct DomainInputXML {
#[serde(rename = "@type")]
pub r#type: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "backend")]
pub struct TPMBackendXML {
#[serde(rename = "@type")]
@ -130,7 +130,7 @@ pub struct TPMBackendXML {
pub r#version: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "tpm")]
pub struct TPMDeviceXML {
#[serde(rename = "@model")]
@ -139,7 +139,7 @@ pub struct TPMDeviceXML {
}
/// Devices information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "devices")]
pub struct DevicesXML {
/// Graphics (used for VNC)
@ -168,7 +168,7 @@ pub struct DevicesXML {
}
/// Graphics information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "graphics")]
pub struct GraphicsXML {
#[serde(rename = "@type")]
@ -178,14 +178,14 @@ pub struct GraphicsXML {
}
/// Video device information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "video")]
pub struct VideoXML {
pub model: VideoModelXML,
}
/// Video model device information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "model")]
pub struct VideoModelXML {
#[serde(rename = "@type")]
@ -193,7 +193,7 @@ pub struct VideoModelXML {
}
/// Disk information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "disk")]
pub struct DiskXML {
#[serde(rename = "@type")]
@ -211,7 +211,7 @@ pub struct DiskXML {
pub address: Option<DiskAddressXML>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "driver")]
pub struct DiskDriverXML {
#[serde(rename = "@name")]
@ -222,14 +222,14 @@ pub struct DiskDriverXML {
pub r#cache: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "source")]
pub struct DiskSourceXML {
#[serde(rename = "@file")]
pub file: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "target")]
pub struct DiskTargetXML {
#[serde(rename = "@dev")]
@ -238,18 +238,18 @@ pub struct DiskTargetXML {
pub bus: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "readonly")]
pub struct DiskReadOnlyXML {}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "boot")]
pub struct DiskBootXML {
#[serde(rename = "@order")]
pub order: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "address")]
pub struct DiskAddressXML {
#[serde(rename = "@type")]
@ -269,7 +269,7 @@ pub struct DiskAddressXML {
}
/// Domain RAM information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "memory")]
pub struct DomainMemoryXML {
#[serde(rename = "@unit")]
@ -279,7 +279,7 @@ pub struct DomainMemoryXML {
pub memory: usize,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "topology")]
pub struct DomainCPUTopology {
#[serde(rename = "@sockets")]
@ -290,14 +290,14 @@ pub struct DomainCPUTopology {
pub threads: usize,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "cpu")]
pub struct DomainVCPUXML {
#[serde(rename = "$value")]
pub body: usize,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "cpu")]
pub struct DomainCPUXML {
#[serde(rename = "@mode")]
@ -306,7 +306,7 @@ pub struct DomainCPUXML {
}
/// Domain information, see https://libvirt.org/formatdomain.html
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "domain")]
pub struct DomainXML {
/// Domain type (kvm)

View File

@ -1,4 +1,4 @@
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct XMLUuid(pub uuid::Uuid);
impl XMLUuid {

View File

@ -212,6 +212,42 @@ async fn main() -> std::io::Result<()> {
.route("/api/vnc", web::get().to(vm_controller::vnc))
// Groups controller
.route("/api/group/list", web::get().to(groups_controller::list))
.route(
"/api/group/{gid}/vm/info",
web::get().to(groups_controller::vm_info),
)
.route(
"/api/group/{gid}/vm/start",
web::get().to(groups_controller::vm_start),
)
.route(
"/api/group/{gid}/vm/shutdown",
web::get().to(groups_controller::vm_shutdown),
)
.route(
"/api/group/{gid}/vm/suspend",
web::get().to(groups_controller::vm_suspend),
)
.route(
"/api/group/{gid}/vm/resume",
web::get().to(groups_controller::vm_resume),
)
.route(
"/api/group/{gid}/vm/kill",
web::get().to(groups_controller::vm_kill),
)
.route(
"/api/group/{gid}/vm/reset",
web::get().to(groups_controller::vm_reset),
)
.route(
"/api/group/{gid}/vm/screenshot",
web::get().to(groups_controller::vm_screenshot),
)
.route(
"/api/group/{gid}/vm/state",
web::get().to(groups_controller::vm_state),
)
// Network controller
.route(
"/api/network/create",

View File

@ -2,6 +2,7 @@ import { Button } from "@mui/material";
import Grid from "@mui/material/Grid2";
import React from "react";
import { useNavigate } from "react-router-dom";
import { GroupApi } from "../../api/GroupApi";
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
import { ServerApi } from "../../api/ServerApi";
@ -35,12 +36,14 @@ interface DetailsProps {
export function APITokenDetails(p: DetailsProps): React.ReactElement {
const [vms, setVMs] = React.useState<VMInfo[]>();
const [groups, setGroups] = React.useState<string[]>();
const [networks, setNetworks] = React.useState<NetworkInfo[]>();
const [nwFilters, setNetworkFilters] = React.useState<NWFilter[]>();
const [tokens, setTokens] = React.useState<APIToken[]>();
const load = async () => {
setVMs(await VMApi.GetList());
setGroups(await GroupApi.GetList());
setNetworks(await NetworkApi.GetList());
setNetworkFilters(await NWFilterApi.GetList());
setTokens(await TokensApi.GetList());
@ -54,6 +57,7 @@ export function APITokenDetails(p: DetailsProps): React.ReactElement {
build={() => (
<APITokenDetailsInner
vms={vms!}
groups={groups!}
networks={networks!}
nwFilters={nwFilters!}
tokens={tokens!}
@ -73,6 +77,7 @@ enum TokenTab {
type DetailsInnerProps = DetailsProps & {
vms: VMInfo[];
groups: string[];
networks: NetworkInfo[];
nwFilters: NWFilter[];
tokens: APIToken[];

View File

@ -22,6 +22,7 @@ export function TokenRightsEditor(p: {
editable: boolean;
onChange?: () => void;
vms: VMInfo[];
groups: string[];
networks: NetworkInfo[];
nwFilters: NWFilter[];
tokens: APIToken[];
@ -238,6 +239,139 @@ export function TokenRightsEditor(p: {
</Table>
</RightsSection>
<RightsSection label="VM groups">
<RouteRight
{...p}
right={{ verb: "POST", path: "/api/group/list" }}
label="Get the list of groups"
/>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Group name</TableCell>
<TableCell align="center">Get VM info</TableCell>
<TableCell align="center">Start VM</TableCell>
<TableCell align="center">Shutdown VM</TableCell>
<TableCell align="center">Suspend VM</TableCell>
<TableCell align="center">Resume VM</TableCell>
<TableCell align="center">Kill VM</TableCell>
<TableCell align="center">Reset VM</TableCell>
<TableCell align="center">Screenshot VM</TableCell>
<TableCell align="center">Get VM State</TableCell>
</TableRow>
</TableHead>
<TableBody>
{/* All Group operations */}
<TableRow hover>
<TableCell>
<i>All</i>
</TableCell>
<CellRight
{...p}
right={{ verb: "GET", path: "/api/group/*/vm/info" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: "/api/group/*/vm/start" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: "/api/group/*/vm/shutdown" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: "/api/group/*/vm/suspend" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: "/api/group/*/vm/resume" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: "/api/group/*/vm/kill" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: "/api/group/*/vm/reset" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: "/api/group/*/vm/screenshot" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: "/api/group/*/vm/state" }}
/>
</TableRow>
{/* Per VM operations */}
{p.groups.map((v, n) => (
<TableRow hover key={n}>
<TableCell>{v}</TableCell>
<CellRight
{...p}
right={{ verb: "GET", path: `/api/group/${v}/vm/info` }}
parent={{ verb: "GET", path: "/api/group/*/vm/info" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: `/api/group/${v}/vm/start` }}
parent={{ verb: "GET", path: "/api/group/*/vm/start" }}
/>
<CellRight
{...p}
right={{
verb: "GET",
path: `/api/group/${v}/vm/shutdown`,
}}
parent={{ verb: "GET", path: "/api/group/*/vm/shutdown" }}
/>
<CellRight
{...p}
right={{
verb: "GET",
path: `/api/group/${v}/vm/suspend`,
}}
parent={{ verb: "GET", path: "/api/group/*/vm/suspend" }}
/>
<CellRight
{...p}
right={{
verb: "GET",
path: `/api/group/${v}/vm/resume`,
}}
parent={{ verb: "GET", path: "/api/group/*/vm/resume" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: `/api/group/${v}/vm/kill` }}
parent={{ verb: "GET", path: "/api/group/*/vm/kill" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: `/api/group/${v}/vm/reset` }}
parent={{ verb: "GET", path: "/api/group/*/vm/reset" }}
/>
<CellRight
{...p}
right={{
verb: "GET",
path: `/api/group/${v}/vm/screenshot`,
}}
parent={{ verb: "GET", path: "/api/group/*/vm/screenshot" }}
/>
<CellRight
{...p}
right={{ verb: "GET", path: `/api/group/${v}/vm/state` }}
parent={{ verb: "GET", path: "/api/group/*/vm/state" }}
/>
</TableRow>
))}
</TableBody>
</Table>
</RightsSection>
{/* Networks */}
<RightsSection label="Networks">
<RouteRight