Compare commits
140 Commits
renovate/r
...
master
Author | SHA1 | Date | |
---|---|---|---|
b943691d18 | |||
bc051ee678 | |||
c7a2d1af23 | |||
93fbb31273 | |||
b4eb6f7ea4 | |||
00ff6f0b50 | |||
324042f956 | |||
e466d03ec5 | |||
89ba09f872 | |||
a322c46ca4 | |||
0915a3e2d9 | |||
07eceaf72f | |||
0e1396e177 | |||
e59f21984f | |||
8c508acd32 | |||
26e7af7675 | |||
2fadf53dea | |||
2b58ce4d5e | |||
9755bacc55 | |||
8b16ce0c5d | |||
20e6d7931e | |||
c908d00c62 | |||
55b49699eb | |||
91fe291341 | |||
eec6bbb598 | |||
d2243fa1c2 | |||
6e7dd7c1c4 | |||
e40e15287b | |||
800969b9cc | |||
5917068add | |||
9b14d62830 | |||
25503a688b | |||
868adc6cee | |||
528e30f3dc | |||
cc42d20e67 | |||
d189470539 | |||
6fdd9f91fa | |||
69c2d12fcd | |||
174e4a2c79 | |||
847ab20a63 | |||
09c32a5555 | |||
220c943642 | |||
e5d709c34f | |||
1e359a3b8e | |||
dbff6358db | |||
d5c05a0cdd | |||
1d24d2a84c | |||
d35dac2de8 | |||
01141f77e2 | |||
56f765a15a | |||
639b7f4b38 | |||
babda3acd1 | |||
197b72cad0 | |||
1910c7081b | |||
eda0fc80b0 | |||
f6e5356109 | |||
11da25b4c0 | |||
2599032581 | |||
ed58d60e84 | |||
a126e76eef | |||
c472dfe807 | |||
c883f13bf8 | |||
b320f0b326 | |||
9812120ed6 | |||
9ebd3b0315 | |||
24afa12be2 | |||
310689312c | |||
e7f4bc44e7 | |||
165937f88b | |||
a5d81de62b | |||
ba2b3494cf | |||
1944415371 | |||
4130fdda1c | |||
e4ef4c43bd | |||
afdf639d9b | |||
f2d6b9a5dd | |||
e3b61baf11 | |||
20732860cf | |||
7f14ab8a54 | |||
87d4c5b0fd | |||
0f58f82e52 | |||
16b73a2030 | |||
a32954785d | |||
2789fc299f | |||
0257ecba0b | |||
17fc64b1fe | |||
fbc818b5f3 | |||
a4292795d1 | |||
529e16c0c7 | |||
e1adc1456f | |||
f1f4a88ae3 | |||
8fdbb0f442 | |||
9efb1b29df | |||
2c07f5f121 | |||
953f6fdcf2 | |||
d66e384137 | |||
80bf70502f | |||
7f6cf26617 | |||
c9cf39bb76 | |||
93afb646ca | |||
4b358acbde | |||
b97dbc8149 | |||
e1292ae922 | |||
3e812b5530 | |||
b1e268bf63 | |||
887c4608b4 | |||
49e33cfd57 | |||
842733caa3 | |||
b6b56fdba8 | |||
8163d5e52f | |||
7aca0aee13 | |||
39fc34ef26 | |||
be06339bd7 | |||
00c1047734 | |||
a6c54ada50 | |||
8803c6755b | |||
cdab9df5c1 | |||
75b8c1d9e9 | |||
557fb7d97b | |||
bb85e58008 | |||
b8c1375f4f | |||
a96f6f33df | |||
a55061a2cd | |||
e6d3dd926c | |||
95dc089943 | |||
dafef923f0 | |||
5095a701eb | |||
a157484105 | |||
0e4bf4414c | |||
0a2a9d66e1 | |||
c4ff5d0621 | |||
ff1391694d | |||
368ae4e89d | |||
a539c092f5 | |||
dbf44e6204 | |||
448b029c17 | |||
f06082ce82 | |||
272763bdc3 | |||
1dd2dfc684 | |||
4f7161ae9e |
@ -5,7 +5,7 @@ name: default
|
||||
|
||||
steps:
|
||||
- name: web_build
|
||||
image: node:22
|
||||
image: node:23
|
||||
volumes:
|
||||
- name: web_app
|
||||
path: /tmp/web_build
|
||||
|
1325
virtweb_backend/Cargo.lock
generated
1325
virtweb_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -8,41 +8,41 @@ edition = "2021"
|
||||
[dependencies]
|
||||
log = "0.4.21"
|
||||
env_logger = "0.11.3"
|
||||
clap = { version = "4.5.4", features = ["derive", "env"] }
|
||||
clap = { version = "4.5.20", features = ["derive", "env"] }
|
||||
light-openid = { version = "1.0.2", features = ["crypto-wrapper"] }
|
||||
lazy_static = "1.4.0"
|
||||
lazy_static = "1.5.0"
|
||||
actix = "0.13.3"
|
||||
actix-web = "4.5.1"
|
||||
actix-web = "4.9.0"
|
||||
actix-remote-ip = "0.1.0"
|
||||
actix-session = { version = "0.9.0", features = ["cookie-session"] }
|
||||
actix-identity = "0.7.1"
|
||||
actix-session = { version = "0.10.0", features = ["cookie-session"] }
|
||||
actix-identity = "0.8.0"
|
||||
actix-cors = "0.7.0"
|
||||
actix-files = "0.6.5"
|
||||
actix-web-actors = "4.3.0"
|
||||
actix-http = "3.6.0"
|
||||
serde = { version = "1.0.199", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
quick-xml = { version = "0.31.0", features = ["serialize", "overlapped-lists"] }
|
||||
futures-util = "0.3.30"
|
||||
anyhow = "1.0.82"
|
||||
actix-multipart = "0.6.1"
|
||||
tempfile = "3.10.1"
|
||||
reqwest = { version = "0.12.4", features = ["stream"] }
|
||||
actix-http = "3.9.0"
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
quick-xml = { version = "0.37.0", features = ["serialize", "overlapped-lists"] }
|
||||
futures-util = "0.3.31"
|
||||
anyhow = "1.0.91"
|
||||
actix-multipart = "0.7.0"
|
||||
tempfile = "3.13.0"
|
||||
reqwest = { version = "0.12.9", features = ["stream"] }
|
||||
url = "2.5.0"
|
||||
virt = "0.3.1"
|
||||
sysinfo = { version = "0.30.11", features = ["serde"] }
|
||||
uuid = { version = "1.8.0", features = ["v4", "serde"] }
|
||||
lazy-regex = "3.1.0"
|
||||
thiserror = "1.0.59"
|
||||
image = "0.25.1"
|
||||
virt = "0.4.1"
|
||||
sysinfo = { version = "0.32.0", features = ["serde"] }
|
||||
uuid = { version = "1.11.0", features = ["v4", "serde"] }
|
||||
lazy-regex = "3.3.0"
|
||||
thiserror = "2.0.0"
|
||||
image = "0.25.4"
|
||||
rand = "0.8.5"
|
||||
bytes = "1.6.0"
|
||||
tokio = "1.37.0"
|
||||
futures = "0.3.30"
|
||||
bytes = "1.8.0"
|
||||
tokio = "1.41.0"
|
||||
futures = "0.3.31"
|
||||
ipnetwork = "0.20.0"
|
||||
num = "0.4.2"
|
||||
rust-embed = { version = "8.3.0" }
|
||||
rust-embed = { version = "8.5.0" }
|
||||
mime_guess = "2.0.4"
|
||||
dotenvy = "0.15.7"
|
||||
nix = { version = "0.28.0", features = ["net"] }
|
||||
nix = { version = "0.29.0", features = ["net"] }
|
||||
basic-jwt = "0.2.0"
|
@ -31,7 +31,7 @@ impl LibVirtActor {
|
||||
"Will connect to hypvervisor at address '{}'",
|
||||
hypervisor_uri
|
||||
);
|
||||
let conn = Connect::open(hypervisor_uri)?;
|
||||
let conn = Connect::open(Some(hypervisor_uri))?;
|
||||
|
||||
Ok(Self { m: conn })
|
||||
}
|
||||
|
16
virtweb_backend/src/controllers/groups_controller.rs
Normal file
16
virtweb_backend/src/controllers/groups_controller.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use crate::controllers::{HttpResult, LibVirtReq};
|
||||
use actix_web::HttpResponse;
|
||||
|
||||
/// Get the list of groups
|
||||
pub async fn list(client: LibVirtReq) -> HttpResult {
|
||||
let groups = match client.get_full_groups_list().await {
|
||||
Err(e) => {
|
||||
log::error!("Failed to get the list of groups! {e}");
|
||||
return Ok(HttpResponse::InternalServerError()
|
||||
.json(format!("Failed to get the list of groups! {e}")));
|
||||
}
|
||||
Ok(l) => l,
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok().json(groups))
|
||||
}
|
@ -8,6 +8,7 @@ use std::io::ErrorKind;
|
||||
|
||||
pub mod api_tokens_controller;
|
||||
pub mod auth_controller;
|
||||
pub mod groups_controller;
|
||||
pub mod iso_controller;
|
||||
pub mod network_controller;
|
||||
pub mod nwfilter_controller;
|
||||
|
@ -40,6 +40,7 @@ struct ServerConstraints {
|
||||
vnc_token_duration: u64,
|
||||
vm_name_size: LenConstraints,
|
||||
vm_title_size: LenConstraints,
|
||||
group_id_size: LenConstraints,
|
||||
memory_size: LenConstraints,
|
||||
disk_name_size: LenConstraints,
|
||||
disk_size: LenConstraints,
|
||||
@ -72,6 +73,7 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
||||
|
||||
vm_name_size: LenConstraints { min: 2, max: 50 },
|
||||
vm_title_size: LenConstraints { min: 0, max: 50 },
|
||||
group_id_size: LenConstraints { min: 3, max: 50 },
|
||||
memory_size: LenConstraints {
|
||||
min: constants::MIN_VM_MEMORY,
|
||||
max: constants::MAX_VM_MEMORY,
|
||||
@ -171,7 +173,7 @@ pub async fn network_hook_status() -> HttpResult {
|
||||
|
||||
pub async fn number_vcpus() -> HttpResult {
|
||||
let mut system = System::new();
|
||||
system.refresh_cpu();
|
||||
system.refresh_cpu_all();
|
||||
let number_cpus = system.cpus().len();
|
||||
assert_ne!(number_cpus, 0, "Got invlid number of CPU!");
|
||||
|
||||
|
@ -21,7 +21,7 @@ struct VMUuid {
|
||||
|
||||
/// Create a new VM
|
||||
pub async fn create(client: LibVirtReq, req: web::Json<VMInfo>) -> HttpResult {
|
||||
let domain = match req.0.as_tomain() {
|
||||
let domain = match req.0.as_domain() {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log::error!("Failed to extract domain info! {e}");
|
||||
@ -83,6 +83,8 @@ pub async fn get_single(client: LibVirtReq, id: web::Path<SingleVMUUidReq>) -> H
|
||||
}
|
||||
};
|
||||
|
||||
log::debug!("INFO={info:#?}");
|
||||
|
||||
let state = client.get_domain_state(id.uid).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(VMInfoAndState {
|
||||
@ -112,7 +114,7 @@ pub async fn update(
|
||||
id: web::Path<SingleVMUUidReq>,
|
||||
req: web::Json<VMInfo>,
|
||||
) -> HttpResult {
|
||||
let mut domain = match req.0.as_tomain() {
|
||||
let mut domain = match req.0.as_domain() {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log::error!("Failed to extract domain info! {e}");
|
||||
|
@ -7,8 +7,9 @@ use crate::libvirt_lib_structures::XMLUuid;
|
||||
use crate::libvirt_rest_structures::hypervisor::HypervisorInfo;
|
||||
use crate::libvirt_rest_structures::net::NetworkInfo;
|
||||
use crate::libvirt_rest_structures::nw_filter::NetworkFilter;
|
||||
use crate::libvirt_rest_structures::vm::VMInfo;
|
||||
use crate::libvirt_rest_structures::vm::{VMGroupId, VMInfo};
|
||||
use actix::Addr;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LibVirtClient(pub Addr<LibVirtActor>);
|
||||
@ -107,6 +108,20 @@ impl LibVirtClient {
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Get the full list of groups
|
||||
pub async fn get_full_groups_list(&self) -> anyhow::Result<Vec<VMGroupId>> {
|
||||
let domains = self.get_full_domains_list().await?;
|
||||
let mut out = HashSet::new();
|
||||
for d in domains {
|
||||
if let Some(g) = VMInfo::from_domain(d)?.group {
|
||||
out.insert(g);
|
||||
}
|
||||
}
|
||||
let mut out: Vec<_> = out.into_iter().collect();
|
||||
out.sort();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Update a network configuration
|
||||
pub async fn update_network(
|
||||
&self,
|
||||
|
@ -1,7 +1,25 @@
|
||||
use crate::libvirt_lib_structures::XMLUuid;
|
||||
|
||||
/// VirtWeb specific metadata
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)]
|
||||
#[serde(rename = "virtweb", default)]
|
||||
pub struct DomainMetadataVirtWebXML {
|
||||
#[serde(rename = "@xmlns:virtweb", default)]
|
||||
pub ns: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub group: Option<String>,
|
||||
}
|
||||
|
||||
/// Domain metadata
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)]
|
||||
#[serde(rename = "metadata")]
|
||||
pub struct DomainMetadataXML {
|
||||
#[serde(rename = "virtweb:metadata", default)]
|
||||
pub virtweb: DomainMetadataVirtWebXML,
|
||||
}
|
||||
|
||||
/// OS information
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "os")]
|
||||
pub struct OSXML {
|
||||
#[serde(rename = "@firmware", default)]
|
||||
@ -11,7 +29,7 @@ pub struct OSXML {
|
||||
}
|
||||
|
||||
/// OS Type information
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "os")]
|
||||
pub struct OSTypeXML {
|
||||
#[serde(rename = "@arch")]
|
||||
@ -23,7 +41,7 @@ pub struct OSTypeXML {
|
||||
}
|
||||
|
||||
/// OS Loader information
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "loader")]
|
||||
pub struct OSLoaderXML {
|
||||
#[serde(rename = "@secure")]
|
||||
@ -31,39 +49,39 @@ pub struct OSLoaderXML {
|
||||
}
|
||||
|
||||
/// Hypervisor features
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default)]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
|
||||
#[serde(rename = "features")]
|
||||
pub struct FeaturesXML {
|
||||
pub acpi: ACPIXML,
|
||||
}
|
||||
|
||||
/// ACPI feature
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default)]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
|
||||
#[serde(rename = "acpi")]
|
||||
pub struct ACPIXML {}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "mac")]
|
||||
pub struct NetMacAddress {
|
||||
#[serde(rename = "@address")]
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "source")]
|
||||
pub struct NetIntSourceXML {
|
||||
#[serde(rename = "@network")]
|
||||
pub network: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "model")]
|
||||
pub struct NetIntModelXML {
|
||||
#[serde(rename = "@type")]
|
||||
pub r#type: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "filterref")]
|
||||
pub struct NetIntFilterParameterXML {
|
||||
#[serde(rename = "@name")]
|
||||
@ -72,7 +90,7 @@ pub struct NetIntFilterParameterXML {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "filterref")]
|
||||
pub struct NetIntfilterRefXML {
|
||||
#[serde(rename = "@filter")]
|
||||
@ -81,7 +99,7 @@ pub struct NetIntfilterRefXML {
|
||||
pub parameters: Vec<NetIntFilterParameterXML>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "interface")]
|
||||
pub struct DomainNetInterfaceXML {
|
||||
#[serde(rename = "@type")]
|
||||
@ -95,14 +113,14 @@ pub struct DomainNetInterfaceXML {
|
||||
pub filterref: Option<NetIntfilterRefXML>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "input")]
|
||||
pub struct DomainInputXML {
|
||||
#[serde(rename = "@type")]
|
||||
pub r#type: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "backend")]
|
||||
pub struct TPMBackendXML {
|
||||
#[serde(rename = "@type")]
|
||||
@ -112,7 +130,7 @@ pub struct TPMBackendXML {
|
||||
pub r#version: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "tpm")]
|
||||
pub struct TPMDeviceXML {
|
||||
#[serde(rename = "@model")]
|
||||
@ -121,7 +139,7 @@ pub struct TPMDeviceXML {
|
||||
}
|
||||
|
||||
/// Devices information
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "devices")]
|
||||
pub struct DevicesXML {
|
||||
/// Graphics (used for VNC)
|
||||
@ -150,7 +168,7 @@ pub struct DevicesXML {
|
||||
}
|
||||
|
||||
/// Graphics information
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "graphics")]
|
||||
pub struct GraphicsXML {
|
||||
#[serde(rename = "@type")]
|
||||
@ -160,14 +178,14 @@ pub struct GraphicsXML {
|
||||
}
|
||||
|
||||
/// Video device information
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "video")]
|
||||
pub struct VideoXML {
|
||||
pub model: VideoModelXML,
|
||||
}
|
||||
|
||||
/// Video model device information
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "model")]
|
||||
pub struct VideoModelXML {
|
||||
#[serde(rename = "@type")]
|
||||
@ -175,7 +193,7 @@ pub struct VideoModelXML {
|
||||
}
|
||||
|
||||
/// Disk information
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "disk")]
|
||||
pub struct DiskXML {
|
||||
#[serde(rename = "@type")]
|
||||
@ -193,7 +211,7 @@ pub struct DiskXML {
|
||||
pub address: Option<DiskAddressXML>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "driver")]
|
||||
pub struct DiskDriverXML {
|
||||
#[serde(rename = "@name")]
|
||||
@ -204,14 +222,14 @@ pub struct DiskDriverXML {
|
||||
pub r#cache: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "source")]
|
||||
pub struct DiskSourceXML {
|
||||
#[serde(rename = "@file")]
|
||||
pub file: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "target")]
|
||||
pub struct DiskTargetXML {
|
||||
#[serde(rename = "@dev")]
|
||||
@ -220,18 +238,18 @@ pub struct DiskTargetXML {
|
||||
pub bus: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "readonly")]
|
||||
pub struct DiskReadOnlyXML {}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "boot")]
|
||||
pub struct DiskBootXML {
|
||||
#[serde(rename = "@order")]
|
||||
pub order: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "address")]
|
||||
pub struct DiskAddressXML {
|
||||
#[serde(rename = "@type")]
|
||||
@ -251,7 +269,7 @@ pub struct DiskAddressXML {
|
||||
}
|
||||
|
||||
/// Domain RAM information
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "memory")]
|
||||
pub struct DomainMemoryXML {
|
||||
#[serde(rename = "@unit")]
|
||||
@ -261,7 +279,7 @@ pub struct DomainMemoryXML {
|
||||
pub memory: usize,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "topology")]
|
||||
pub struct DomainCPUTopology {
|
||||
#[serde(rename = "@sockets")]
|
||||
@ -272,14 +290,14 @@ pub struct DomainCPUTopology {
|
||||
pub threads: usize,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "cpu")]
|
||||
pub struct DomainVCPUXML {
|
||||
#[serde(rename = "$value")]
|
||||
pub body: usize,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "cpu")]
|
||||
pub struct DomainCPUXML {
|
||||
#[serde(rename = "@mode")]
|
||||
@ -288,7 +306,7 @@ pub struct DomainCPUXML {
|
||||
}
|
||||
|
||||
/// Domain information, see https://libvirt.org/formatdomain.html
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename = "domain")]
|
||||
pub struct DomainXML {
|
||||
/// Domain type (kvm)
|
||||
@ -300,6 +318,9 @@ pub struct DomainXML {
|
||||
pub genid: Option<uuid::Uuid>,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<DomainMetadataXML>,
|
||||
|
||||
pub os: OSXML,
|
||||
#[serde(default)]
|
||||
pub features: FeaturesXML,
|
||||
@ -319,10 +340,32 @@ pub struct DomainXML {
|
||||
pub on_crash: String,
|
||||
}
|
||||
|
||||
const METADATA_START_MARKER: &str =
|
||||
"<virtweb:metadata xmlns:virtweb=\"https://virtweb.communiquons.org\">";
|
||||
const METADATA_END_MARKER: &str = "</virtweb:metadata>";
|
||||
|
||||
impl DomainXML {
|
||||
/// Decode Domain structure from XML definition
|
||||
pub fn parse_xml(xml: &str) -> anyhow::Result<Self> {
|
||||
Ok(quick_xml::de::from_str(xml)?)
|
||||
let mut res: Self = quick_xml::de::from_str(xml)?;
|
||||
|
||||
// Handle custom metadata parsing issue
|
||||
//
|
||||
// https://github.com/tafia/quick-xml/pull/797
|
||||
if xml.contains(METADATA_START_MARKER) && xml.contains(METADATA_END_MARKER) {
|
||||
let s = xml
|
||||
.split_once(METADATA_START_MARKER)
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once(METADATA_END_MARKER)
|
||||
.unwrap()
|
||||
.0;
|
||||
let s = format!("<virtweb>{s}</virtweb>");
|
||||
let metadata: DomainMetadataVirtWebXML = quick_xml::de::from_str(&s)?;
|
||||
res.metadata = Some(DomainMetadataXML { virtweb: metadata });
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Turn this domain into its XML definition
|
||||
|
@ -10,6 +10,11 @@ use crate::utils::files_utils::convert_size_unit_to_mb;
|
||||
use lazy_regex::regex;
|
||||
use num::Integer;
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, Ord, PartialOrd,
|
||||
)]
|
||||
pub struct VMGroupId(pub String);
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub enum BootType {
|
||||
UEFI,
|
||||
@ -59,6 +64,9 @@ pub struct VMInfo {
|
||||
pub genid: Option<XMLUuid>,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
/// Group associated with the VM (VirtWeb specific field)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub group: Option<VMGroupId>,
|
||||
pub boot_type: BootType,
|
||||
pub architecture: VMArchitecture,
|
||||
/// VM allocated memory, in megabytes
|
||||
@ -79,7 +87,7 @@ pub struct VMInfo {
|
||||
|
||||
impl VMInfo {
|
||||
/// Turn this VM into a domain
|
||||
pub fn as_tomain(&self) -> anyhow::Result<DomainXML> {
|
||||
pub fn as_domain(&self) -> anyhow::Result<DomainXML> {
|
||||
if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) {
|
||||
return Err(StructureExtraction("VM name is invalid!").into());
|
||||
}
|
||||
@ -105,6 +113,12 @@ impl VMInfo {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(group) = &self.group {
|
||||
if !regex!("^[a-zA-Z0-9]+$").is_match(&group.0) {
|
||||
return Err(StructureExtraction("VM group name is invalid!").into());
|
||||
}
|
||||
}
|
||||
|
||||
if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY {
|
||||
return Err(StructureExtraction("VM memory is invalid!").into());
|
||||
}
|
||||
@ -282,6 +296,12 @@ impl VMInfo {
|
||||
title: self.title.clone(),
|
||||
description: self.description.clone(),
|
||||
|
||||
metadata: Some(DomainMetadataXML {
|
||||
virtweb: DomainMetadataVirtWebXML {
|
||||
ns: "https://virtweb.communiquons.org".to_string(),
|
||||
group: self.group.clone().map(|g| g.0),
|
||||
},
|
||||
}),
|
||||
os: OSXML {
|
||||
r#type: OSTypeXML {
|
||||
arch: match self.architecture {
|
||||
@ -369,6 +389,13 @@ impl VMInfo {
|
||||
genid: domain.genid.map(XMLUuid),
|
||||
title: domain.title,
|
||||
description: domain.description,
|
||||
group: domain
|
||||
.metadata
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.virtweb
|
||||
.group
|
||||
.map(VMGroupId),
|
||||
boot_type: match domain.os.loader {
|
||||
None => BootType::UEFI,
|
||||
Some(l) => match l.secure.as_str() {
|
||||
|
@ -22,7 +22,7 @@ use virtweb_backend::constants::{
|
||||
MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
|
||||
};
|
||||
use virtweb_backend::controllers::{
|
||||
api_tokens_controller, auth_controller, iso_controller, network_controller,
|
||||
api_tokens_controller, auth_controller, groups_controller, iso_controller, network_controller,
|
||||
nwfilter_controller, server_controller, static_controller, vm_controller,
|
||||
};
|
||||
use virtweb_backend::libvirt_client::LibVirtClient;
|
||||
@ -210,6 +210,8 @@ async fn main() -> std::io::Result<()> {
|
||||
web::get().to(vm_controller::vnc_token),
|
||||
)
|
||||
.route("/api/vnc", web::get().to(vm_controller::vnc))
|
||||
// Groups controller
|
||||
.route("/api/group/list", web::get().to(groups_controller::list))
|
||||
// Network controller
|
||||
.route(
|
||||
"/api/network/create",
|
||||
|
@ -9,7 +9,7 @@ make
|
||||
|
||||
The release file will be available in `virtweb_backend/target/release/virtweb_backend`.
|
||||
|
||||
This is the only artifcat that must be copied to the server. It is recommended to copy it to the `/usr/local/bin` directory.
|
||||
This is the only artifact that must be copied to the server. It is recommended to copy it to the `/usr/local/bin` directory.
|
||||
|
||||
## Install requirements
|
||||
In order to work properly, VirtWeb relies on `libvirt`, `qemu` and `kvm`:
|
||||
|
12329
virtweb_frontend/package-lock.json
generated
12329
virtweb_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,36 +6,36 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@fontsource/roboto": "^5.1.0",
|
||||
"@mdi/js": "^7.2.96",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@mui/icons-material": "^5.14.7",
|
||||
"@mui/material": "^5.14.7",
|
||||
"@mui/x-charts": "^7.3.0",
|
||||
"@mui/x-data-grid": "^7.3.0",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@mui/icons-material": "^6.1.6",
|
||||
"@mui/material": "^6.1.6",
|
||||
"@mui/x-charts": "^7.22.1",
|
||||
"@mui/x-data-grid": "^7.22.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/humanize-duration": "^3.27.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "^18.2.79",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@types/react-syntax-highlighter": "^15.5.11",
|
||||
"@types/uuid": "^9.0.5",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"date-and-time": "^3.1.1",
|
||||
"filesize": "^10.0.12",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"date-and-time": "^3.6.0",
|
||||
"filesize": "^10.1.6",
|
||||
"humanize-duration": "^3.29.0",
|
||||
"mui-file-input": "^4.0.4",
|
||||
"mui-file-input": "^6.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.23.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-vnc": "^2.0.0",
|
||||
"typescript": "^4.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
"vite": "^5.2.10",
|
||||
"vite-tsconfig-paths": "^4.2.2",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-vnc": "^2.0.2",
|
||||
"typescript": "^4.9.5",
|
||||
"uuid": "^11.0.2",
|
||||
"vite": "^5.4.10",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"web-vitals": "^3.5.2",
|
||||
"xml-formatter": "^3.6.0"
|
||||
},
|
||||
|
15
virtweb_frontend/src/api/GroupApi.ts
Normal file
15
virtweb_frontend/src/api/GroupApi.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { APIClient } from "./ApiClient";
|
||||
|
||||
export class GroupApi {
|
||||
/**
|
||||
* Get the entire list of networks
|
||||
*/
|
||||
static async GetList(): Promise<string[]> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "GET",
|
||||
uri: "/group/list",
|
||||
})
|
||||
).data;
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ export interface ServerConstraints {
|
||||
vnc_token_duration: number;
|
||||
vm_name_size: LenConstraint;
|
||||
vm_title_size: LenConstraint;
|
||||
group_id_size: LenConstraint;
|
||||
memory_size: LenConstraint;
|
||||
disk_name_size: LenConstraint;
|
||||
disk_size: LenConstraint;
|
||||
@ -73,7 +74,7 @@ interface SystemInfo {
|
||||
secs: number;
|
||||
nanos: number;
|
||||
};
|
||||
global_cpu_info: GlobalCPUInfo;
|
||||
global_cpu_usage: number;
|
||||
cpus: CpuCore[];
|
||||
physical_core_count: number;
|
||||
total_memory: number;
|
||||
@ -94,14 +95,6 @@ interface SystemInfo {
|
||||
host_name: string;
|
||||
}
|
||||
|
||||
interface GlobalCPUInfo {
|
||||
cpu_usage: number;
|
||||
name: string;
|
||||
vendor_id: string;
|
||||
brand: string;
|
||||
frequency: number;
|
||||
}
|
||||
|
||||
interface CpuCore {
|
||||
cpu_usage: number;
|
||||
name: string;
|
||||
|
@ -63,6 +63,7 @@ interface VMInfoInterface {
|
||||
genid?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
group?: string;
|
||||
boot_type: "UEFI" | "UEFISecureBoot";
|
||||
architecture: "i686" | "x86_64";
|
||||
memory: number;
|
||||
@ -80,6 +81,7 @@ export class VMInfo implements VMInfoInterface {
|
||||
genid?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
group?: string;
|
||||
boot_type: "UEFI" | "UEFISecureBoot";
|
||||
architecture: "i686" | "x86_64";
|
||||
number_vcpu: number;
|
||||
@ -96,6 +98,7 @@ export class VMInfo implements VMInfoInterface {
|
||||
this.genid = int.genid;
|
||||
this.title = int.title;
|
||||
this.description = int.description;
|
||||
this.group = int.group;
|
||||
this.boot_type = int.boot_type;
|
||||
this.architecture = int.architecture;
|
||||
this.number_vcpu = int.number_vcpu;
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
import Icon from "@mdi/react";
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
LinearProgress,
|
||||
Table,
|
||||
TableBody,
|
||||
@ -17,7 +16,10 @@ import {
|
||||
TableRow,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { PieChart } from "@mui/x-charts";
|
||||
import { filesize } from "filesize";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import React from "react";
|
||||
import {
|
||||
DiskInfo,
|
||||
@ -28,8 +30,6 @@ import {
|
||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||
import { VirtWebPaper } from "../widgets/VirtWebPaper";
|
||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import { filesize } from "filesize";
|
||||
|
||||
export function SysInfoRoute(): React.ReactElement {
|
||||
const [info, setInfo] = React.useState<ServerSystemInfo>();
|
||||
@ -65,7 +65,7 @@ export function SysInfoRouteInner(p: {
|
||||
<VirtWebRouteContainer label="Sysinfo">
|
||||
<Grid container spacing={2}>
|
||||
{/* Memory */}
|
||||
<Grid xs={4}>
|
||||
<Grid size={{ xs: 4 }}>
|
||||
<Box flexGrow={1}>
|
||||
<Typography style={{ textAlign: "center" }}>Memory</Typography>
|
||||
<PieChart
|
||||
@ -97,7 +97,7 @@ export function SysInfoRouteInner(p: {
|
||||
</Grid>
|
||||
|
||||
{/* Disk usage */}
|
||||
<Grid xs={4}>
|
||||
<Grid size={{ xs: 4 }}>
|
||||
<Box flexGrow={1}>
|
||||
<Typography style={{ textAlign: "center" }}>Disk usage</Typography>
|
||||
<PieChart
|
||||
@ -125,7 +125,7 @@ export function SysInfoRouteInner(p: {
|
||||
</Grid>
|
||||
|
||||
{/* CPU usage */}
|
||||
<Grid xs={4}>
|
||||
<Grid size={{ xs: 4 }}>
|
||||
<Box flexGrow={1}>
|
||||
<Typography style={{ textAlign: "center" }}>CPU usage</Typography>
|
||||
<PieChart
|
||||
@ -134,13 +134,13 @@ export function SysInfoRouteInner(p: {
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
value: 100 - p.info.system.global_cpu_info.cpu_usage,
|
||||
value: 100 - p.info.system.global_cpu_usage,
|
||||
label: "Free",
|
||||
},
|
||||
|
||||
{
|
||||
id: 2,
|
||||
value: p.info.system.global_cpu_info.cpu_usage,
|
||||
value: p.info.system.global_cpu_usage,
|
||||
label: "Used",
|
||||
},
|
||||
],
|
||||
@ -180,18 +180,18 @@ export function SysInfoRouteInner(p: {
|
||||
label="CPU info"
|
||||
icon={<Icon size={"1rem"} path={mdiMemory} />}
|
||||
entries={[
|
||||
{ label: "Brand", value: p.info.system.global_cpu_info.brand },
|
||||
{ label: "Brand", value: p.info.system.cpus[0].brand },
|
||||
{
|
||||
label: "Vendor ID",
|
||||
value: p.info.system.global_cpu_info.vendor_id,
|
||||
value: p.info.system.cpus[0].vendor_id,
|
||||
},
|
||||
{
|
||||
label: "CPU usage",
|
||||
value: p.info.system.global_cpu_info.cpu_usage,
|
||||
value: p.info.system.cpus[0].cpu_usage,
|
||||
},
|
||||
{
|
||||
label: "Name",
|
||||
value: p.info.system.global_cpu_info.name,
|
||||
value: p.info.system.cpus[0].name,
|
||||
},
|
||||
{
|
||||
label: "CPU model",
|
||||
|
@ -1,3 +1,5 @@
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||
import {
|
||||
Button,
|
||||
@ -7,6 +9,7 @@ import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
@ -14,19 +17,27 @@ import {
|
||||
import { filesize } from "filesize";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { VMApi, VMInfo } from "../api/VMApi";
|
||||
import { GroupApi } from "../api/GroupApi";
|
||||
import { VMApi, VMInfo, VMState } from "../api/VMApi";
|
||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||
import { RouterLink } from "../widgets/RouterLink";
|
||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||
import { VMStatusWidget } from "../widgets/vms/VMStatusWidget";
|
||||
|
||||
export function VMListRoute(): React.ReactElement {
|
||||
const [groups, setGroups] = React.useState<Array<string | undefined>>();
|
||||
const [list, setList] = React.useState<VMInfo[] | undefined>();
|
||||
|
||||
const loadKey = React.useRef(1);
|
||||
|
||||
const load = async () => {
|
||||
setList(await VMApi.GetList());
|
||||
const groups: Array<string | undefined> = await GroupApi.GetList();
|
||||
const list = await VMApi.GetList();
|
||||
|
||||
if (list.find((v) => !v.group) !== undefined) groups.push(undefined);
|
||||
|
||||
setGroups(groups);
|
||||
setList(list);
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
@ -51,7 +62,7 @@ export function VMListRoute(): React.ReactElement {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<VMListWidget list={list!} onReload={reload} />
|
||||
<VMListWidget list={list!} groups={groups!} onReload={reload} />
|
||||
</VirtWebRouteContainer>
|
||||
)}
|
||||
/>
|
||||
@ -59,11 +70,37 @@ export function VMListRoute(): React.ReactElement {
|
||||
}
|
||||
|
||||
function VMListWidget(p: {
|
||||
groups: Array<string | undefined>;
|
||||
list: VMInfo[];
|
||||
onReload: () => void;
|
||||
}): React.ReactElement {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [hiddenGroups, setHiddenGroups] = React.useState<
|
||||
Set<string | undefined>
|
||||
>(new Set());
|
||||
|
||||
const [runningVMs, setRunningVMs] = React.useState<Set<string>>(new Set());
|
||||
|
||||
const toggleHiddenGroup = (g: string | undefined) => {
|
||||
if (hiddenGroups.has(g)) hiddenGroups.delete(g);
|
||||
else hiddenGroups.add(g);
|
||||
|
||||
setHiddenGroups(new Set([...hiddenGroups]));
|
||||
};
|
||||
|
||||
const updateVMState = (v: VMInfo, s: VMState) => {
|
||||
const running = s !== "Shutoff";
|
||||
if (runningVMs.has(v.name) === running) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (running) runningVMs.add(v.name);
|
||||
else runningVMs.delete(v.name);
|
||||
|
||||
setRunningVMs(new Set([...runningVMs]));
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
@ -72,12 +109,39 @@ function VMListWidget(p: {
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell>Memory</TableCell>
|
||||
<TableCell>vCPU</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{p.list.map((row) => (
|
||||
{p.groups.map((g, num) => (
|
||||
<React.Fragment key={num}>
|
||||
{p.groups.length > 1 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
style={{ paddingBottom: 2, paddingTop: 2 }}
|
||||
colSpan={6}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => toggleHiddenGroup(g)}
|
||||
>
|
||||
{!hiddenGroups?.has(g) ? (
|
||||
<KeyboardArrowUpIcon />
|
||||
) : (
|
||||
<KeyboardArrowDownIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
{g ?? "default"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{!hiddenGroups.has(g) &&
|
||||
p.list
|
||||
.filter((row) => row.group === g)
|
||||
.map((row) => (
|
||||
<TableRow
|
||||
hover
|
||||
key={row.name}
|
||||
@ -88,9 +152,13 @@ function VMListWidget(p: {
|
||||
{row.name}
|
||||
</TableCell>
|
||||
<TableCell>{row.description ?? ""}</TableCell>
|
||||
<TableCell>{filesize(row.memory * 1000 * 1000)}</TableCell>
|
||||
<TableCell>{vmMemoryToHuman(row.memory)}</TableCell>
|
||||
<TableCell>{row.number_vcpu}</TableCell>
|
||||
<TableCell>
|
||||
<VMStatusWidget vm={row} />
|
||||
<VMStatusWidget
|
||||
vm={row}
|
||||
onChange={(s) => updateVMState(row, s)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title="View this VM">
|
||||
@ -103,8 +171,38 @@ function VMListWidget(p: {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell>
|
||||
{vmMemoryToHuman(
|
||||
p.list
|
||||
.filter((v) => runningVMs.has(v.name))
|
||||
.reduce((s, v) => s + v.memory, 0)
|
||||
)}
|
||||
{" / "}
|
||||
{vmMemoryToHuman(p.list.reduce((s, v) => s + v.memory, 0))}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{p.list
|
||||
.filter((v) => runningVMs.has(v.name))
|
||||
.reduce((s, v) => s + v.number_vcpu, 0)}
|
||||
{" / "}
|
||||
{p.list.reduce((s, v) => s + v.number_vcpu, 0)}
|
||||
</TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function vmMemoryToHuman(size: number): string {
|
||||
return filesize(size * 1000 * 1000);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import Icon from "@mdi/react";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import Box from "@mui/material/Box";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Link, Outlet } from "react-router-dom";
|
||||
@ -38,10 +38,7 @@ export function BaseLoginPage() {
|
||||
<Grid container component="main" sx={{ height: "100vh" }}>
|
||||
<CssBaseline />
|
||||
<Grid
|
||||
item
|
||||
xs={false}
|
||||
sm={4}
|
||||
md={7}
|
||||
size={{ xs: false, sm: 4, md: 7 }}
|
||||
sx={{
|
||||
backgroundImage: "url(/login_splash.jpg)",
|
||||
backgroundRepeat: "no-repeat",
|
||||
@ -53,7 +50,12 @@ export function BaseLoginPage() {
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
/>
|
||||
<Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
|
||||
<Grid
|
||||
size={{ xs: 12, sm: 8, md: 5 }}
|
||||
component={Paper}
|
||||
elevation={6}
|
||||
square
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
my: 8,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Grid, Paper, Typography } from "@mui/material";
|
||||
import { Paper, Typography } from "@mui/material";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
|
||||
export function EditSection(
|
||||
p: {
|
||||
@ -9,7 +10,7 @@ export function EditSection(
|
||||
} & PropsWithChildren
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<Grid item sm={12} md={p.fullWidth ? 12 : 6}>
|
||||
<Grid size={{ sm: 12, md: p.fullWidth ? 12 : 6 }}>
|
||||
<Paper style={{ margin: "10px", padding: "10px" }}>
|
||||
{(p.title || p.actions) && (
|
||||
<span
|
||||
|
@ -4,7 +4,6 @@ import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Grid,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
@ -19,6 +18,7 @@ import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
||||
import { IPInput } from "./IPInput";
|
||||
import { MACInput } from "./MACInput";
|
||||
import { TextInput } from "./TextInput";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
|
||||
export function NetDHCPHostReservations(p: {
|
||||
editable: boolean;
|
||||
@ -39,7 +39,7 @@ export function NetDHCPHostReservations(p: {
|
||||
<>
|
||||
<Grid container>
|
||||
{p.dhcp.hosts.map((h, num) => (
|
||||
<Grid key={num} sm={12} md={6} item style={{ padding: "10px" }}>
|
||||
<Grid key={num} size={{ sm: 12, md: 6 }} style={{ padding: "10px" }}>
|
||||
<HostReservationWidget
|
||||
key={num}
|
||||
{...p}
|
||||
|
@ -5,11 +5,11 @@ import {
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Grid,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { NatEntry } from "../../api/NetworksApi";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
@ -295,7 +295,7 @@ function NATEntryProp(
|
||||
p: PropsWithChildren<{ label?: string }>
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<Grid item sm={12} md={6} style={{ padding: "20px" }}>
|
||||
<Grid size={{ sm: 12, md: 6 }} style={{ padding: "20px" }}>
|
||||
{p.label && (
|
||||
<Typography variant="h6" style={{ marginBottom: "10px" }}>
|
||||
{p.label}
|
||||
|
@ -4,13 +4,13 @@ import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Grid,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { NWFilter } from "../../api/NWFilterApi";
|
||||
import { NetworkInfo } from "../../api/NetworksApi";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Button, Checkbox, Grid } from "@mui/material";
|
||||
import { Button, Checkbox } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { IpConfig, NetworkApi, NetworkInfo } from "../../api/NetworksApi";
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Button, Grid } from "@mui/material";
|
||||
import { Button } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import React, { ReactElement } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
@ -6,6 +7,7 @@ import {
|
||||
NWFilterApi,
|
||||
NWFilterIsBuiltin,
|
||||
} from "../../api/NWFilterApi";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
||||
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
||||
@ -13,12 +15,11 @@ import { AsyncWidget } from "../AsyncWidget";
|
||||
import { TabsWidget } from "../TabsWidget";
|
||||
import { XMLAsyncWidget } from "../XMLWidget";
|
||||
import { EditSection } from "../forms/EditSection";
|
||||
import { TextInput } from "../forms/TextInput";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { SelectInput } from "../forms/SelectInput";
|
||||
import { NWFSelectReferencedFilters } from "../forms/NWFSelectReferencedFilters";
|
||||
import { NWFilterRules } from "../forms/NWFilterRules";
|
||||
import { NWFilterPriorityInput } from "../forms/NWFilterPriorityInput";
|
||||
import { NWFilterRules } from "../forms/NWFilterRules";
|
||||
import { SelectInput } from "../forms/SelectInput";
|
||||
import { TextInput } from "../forms/TextInput";
|
||||
|
||||
interface DetailsProps {
|
||||
nwfilter: NWFilter;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Button, Grid } from "@mui/material";
|
||||
import { Button } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { Button, Grid } from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import ListIcon from "@mui/icons-material/List";
|
||||
import { Button, IconButton, Tooltip } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { validate as validateUUID } from "uuid";
|
||||
import { GroupApi } from "../../api/GroupApi";
|
||||
import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi";
|
||||
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
|
||||
import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
|
||||
@ -12,6 +16,7 @@ import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
||||
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
||||
import { AsyncWidget } from "../AsyncWidget";
|
||||
import { TabsWidget } from "../TabsWidget";
|
||||
import { XMLAsyncWidget } from "../XMLWidget";
|
||||
import { CheckboxInput } from "../forms/CheckboxInput";
|
||||
import { EditSection } from "../forms/EditSection";
|
||||
import { ResAutostartInput } from "../forms/ResAutostartInput";
|
||||
@ -21,7 +26,6 @@ import { VMDisksList } from "../forms/VMDisksList";
|
||||
import { VMNetworksList } from "../forms/VMNetworksList";
|
||||
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
|
||||
import { VMScreenshot } from "./VMScreenshot";
|
||||
import { XMLAsyncWidget } from "../XMLWidget";
|
||||
|
||||
interface DetailsProps {
|
||||
vm: VMInfo;
|
||||
@ -31,6 +35,7 @@ interface DetailsProps {
|
||||
}
|
||||
|
||||
export function VMDetails(p: DetailsProps): React.ReactElement {
|
||||
const [groupsList, setGroupsList] = React.useState<string[] | any>();
|
||||
const [isoList, setIsoList] = React.useState<IsoFile[] | any>();
|
||||
const [vcpuCombinations, setVCPUCombinations] = React.useState<
|
||||
number[] | any
|
||||
@ -41,6 +46,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
|
||||
>();
|
||||
|
||||
const load = async () => {
|
||||
setGroupsList(await GroupApi.GetList());
|
||||
setIsoList(await IsoFilesApi.GetList());
|
||||
setVCPUCombinations(await ServerApi.NumberVCPUs());
|
||||
setNetworksList(await NetworkApi.GetList());
|
||||
@ -54,6 +60,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
|
||||
errMsg="Failed to load the list of ISO files"
|
||||
build={() => (
|
||||
<VMDetailsInner
|
||||
groupsList={groupsList}
|
||||
isoList={isoList}
|
||||
vcpuCombinations={vcpuCombinations}
|
||||
networksList={networksList}
|
||||
@ -74,6 +81,7 @@ enum VMTab {
|
||||
}
|
||||
|
||||
type DetailsInnerProps = DetailsProps & {
|
||||
groupsList: string[];
|
||||
isoList: IsoFile[];
|
||||
vcpuCombinations: number[];
|
||||
networksList: NetworkInfo[];
|
||||
@ -116,6 +124,8 @@ function VMDetailsInner(p: DetailsInnerProps): React.ReactElement {
|
||||
}
|
||||
|
||||
function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
||||
const [addGroup, setAddGroup] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
{
|
||||
@ -174,6 +184,50 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
||||
}}
|
||||
multiline={true}
|
||||
/>
|
||||
|
||||
<div style={{ display: "flex" }}>
|
||||
{addGroup ? (
|
||||
<TextInput
|
||||
label="Group"
|
||||
editable={p.editable}
|
||||
value={p.vm.group}
|
||||
onValueChange={(v) => {
|
||||
p.vm.group = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
size={ServerApi.Config.constraints.group_id_size}
|
||||
/>
|
||||
) : (
|
||||
<SelectInput
|
||||
editable={p.editable}
|
||||
label="Group"
|
||||
onValueChange={(v) => {
|
||||
p.vm.group = v! as any;
|
||||
p.onChange?.();
|
||||
}}
|
||||
value={p.vm.group}
|
||||
options={[
|
||||
{ label: "None" },
|
||||
...p.groupsList.map((g) => {
|
||||
return { value: g, label: g };
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{p.editable && (
|
||||
<Tooltip
|
||||
title={
|
||||
addGroup
|
||||
? "Use an existing group"
|
||||
: "Add a new group instead of using existing one"
|
||||
}
|
||||
>
|
||||
<IconButton onClick={() => setAddGroup(!addGroup)}>
|
||||
{addGroup ? <ListIcon /> : <AddIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</EditSection>
|
||||
|
||||
{/* General section */}
|
||||
|
Loading…
Reference in New Issue
Block a user