Compare commits
188 Commits
renovate/t
...
master
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
b5cb76cd7d | |||
4f7161ae9e | |||
f3d184e06d | |||
12404cc9a0 | |||
0eabdec559 | |||
8646837035 | |||
a164c6adb5 | |||
7de2c01418 | |||
7f11076f45 | |||
3f32aab8bd | |||
275e706ee5 | |||
7608a7cb18 | |||
e6293e3015 | |||
a44bc0a4fc | |||
a2221b0903 | |||
6ab4111182 | |||
8fb044b61d | |||
06ec35e1e7 | |||
e94b08827c | |||
5d1ab3be67 | |||
383b29ce21 | |||
85c9e0f4c6 | |||
7e3c105d78 | |||
6a3f1f40f9 | |||
b33c660c3e | |||
cd04e04d34 | |||
7dfbed0186 | |||
3dbefc8d84 | |||
077b385c0f | |||
3f203966d4 | |||
0ab8b23de4 | |||
a18787efcb | |||
68465270bf | |||
b88eb08ec2 | |||
8995b5e874 | |||
9fe4c67aa0 | |||
d6e2a10e59 | |||
03c7dbc357 | |||
27f33038a9 | |||
57b0957d3e | |||
2174ececd1 | |||
a61b38b4d3 | |||
ea84ebdda7 | |||
a972ea51aa | |||
f89a4f4481 | |||
0e07ca6bd3 | |||
aaba9f2f80 | |||
0ba70330db | |||
bb55ec4cfe | |||
90f8b46c84 | |||
51b34131d2 |
@ -5,7 +5,7 @@ name: default
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: web_build
|
- name: web_build
|
||||||
image: node:22
|
image: node:23
|
||||||
volumes:
|
volumes:
|
||||||
- name: web_app
|
- name: web_app
|
||||||
path: /tmp/web_build
|
path: /tmp/web_build
|
||||||
|
1578
virtweb_backend/Cargo.lock
generated
1578
virtweb_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -8,41 +8,41 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
env_logger = "0.11.3"
|
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"] }
|
light-openid = { version = "1.0.2", features = ["crypto-wrapper"] }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.5.0"
|
||||||
actix = "0.13.3"
|
actix = "0.13.3"
|
||||||
actix-web = "4.5.1"
|
actix-web = "4.9.0"
|
||||||
actix-remote-ip = "0.1.0"
|
actix-remote-ip = "0.1.0"
|
||||||
actix-session = { version = "0.9.0", features = ["cookie-session"] }
|
actix-session = { version = "0.10.0", features = ["cookie-session"] }
|
||||||
actix-identity = "0.7.1"
|
actix-identity = "0.8.0"
|
||||||
actix-cors = "0.7.0"
|
actix-cors = "0.7.0"
|
||||||
actix-files = "0.6.5"
|
actix-files = "0.6.5"
|
||||||
actix-web-actors = "4.3.0"
|
actix-web-actors = "4.3.0"
|
||||||
actix-http = "3.6.0"
|
actix-http = "3.9.0"
|
||||||
serde = { version = "1.0.199", features = ["derive"] }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
serde_json = "1.0.116"
|
serde_json = "1.0.132"
|
||||||
quick-xml = { version = "0.31.0", features = ["serialize", "overlapped-lists"] }
|
quick-xml = { version = "0.37.0", features = ["serialize", "overlapped-lists"] }
|
||||||
futures-util = "0.3.30"
|
futures-util = "0.3.31"
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0.91"
|
||||||
actix-multipart = "0.6.1"
|
actix-multipart = "0.7.0"
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.13.0"
|
||||||
reqwest = { version = "0.12.4", features = ["stream"] }
|
reqwest = { version = "0.12.9", features = ["stream"] }
|
||||||
url = "2.5.0"
|
url = "2.5.0"
|
||||||
virt = "0.3.1"
|
virt = "0.4.1"
|
||||||
sysinfo = { version = "0.30.11", features = ["serde"] }
|
sysinfo = { version = "0.32.0", features = ["serde"] }
|
||||||
uuid = { version = "1.8.0", features = ["v4", "serde"] }
|
uuid = { version = "1.11.0", features = ["v4", "serde"] }
|
||||||
lazy-regex = "3.1.0"
|
lazy-regex = "3.3.0"
|
||||||
thiserror = "1.0.59"
|
thiserror = "2.0.0"
|
||||||
image = "0.25.1"
|
image = "0.25.4"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
bytes = "1.6.0"
|
bytes = "1.8.0"
|
||||||
tokio = "1.37.0"
|
tokio = "1.41.0"
|
||||||
futures = "0.3.30"
|
futures = "0.3.31"
|
||||||
ipnetwork = "0.20.0"
|
ipnetwork = "0.20.0"
|
||||||
num = "0.4.2"
|
num = "0.4.2"
|
||||||
rust-embed = { version = "8.3.0" }
|
rust-embed = { version = "8.5.0" }
|
||||||
mime_guess = "2.0.4"
|
mime_guess = "2.0.4"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
nix = { version = "0.28.0", features = ["net"] }
|
nix = { version = "0.29.0", features = ["net"] }
|
||||||
basic-jwt = "0.2.0"
|
basic-jwt = "0.2.0"
|
@ -31,7 +31,7 @@ impl LibVirtActor {
|
|||||||
"Will connect to hypvervisor at address '{}'",
|
"Will connect to hypvervisor at address '{}'",
|
||||||
hypervisor_uri
|
hypervisor_uri
|
||||||
);
|
);
|
||||||
let conn = Connect::open(hypervisor_uri)?;
|
let conn = Connect::open(Some(hypervisor_uri))?;
|
||||||
|
|
||||||
Ok(Self { m: conn })
|
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 api_tokens_controller;
|
||||||
pub mod auth_controller;
|
pub mod auth_controller;
|
||||||
|
pub mod groups_controller;
|
||||||
pub mod iso_controller;
|
pub mod iso_controller;
|
||||||
pub mod network_controller;
|
pub mod network_controller;
|
||||||
pub mod nwfilter_controller;
|
pub mod nwfilter_controller;
|
||||||
|
@ -40,6 +40,7 @@ struct ServerConstraints {
|
|||||||
vnc_token_duration: u64,
|
vnc_token_duration: u64,
|
||||||
vm_name_size: LenConstraints,
|
vm_name_size: LenConstraints,
|
||||||
vm_title_size: LenConstraints,
|
vm_title_size: LenConstraints,
|
||||||
|
group_id_size: LenConstraints,
|
||||||
memory_size: LenConstraints,
|
memory_size: LenConstraints,
|
||||||
disk_name_size: LenConstraints,
|
disk_name_size: LenConstraints,
|
||||||
disk_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_name_size: LenConstraints { min: 2, max: 50 },
|
||||||
vm_title_size: LenConstraints { min: 0, max: 50 },
|
vm_title_size: LenConstraints { min: 0, max: 50 },
|
||||||
|
group_id_size: LenConstraints { min: 3, max: 50 },
|
||||||
memory_size: LenConstraints {
|
memory_size: LenConstraints {
|
||||||
min: constants::MIN_VM_MEMORY,
|
min: constants::MIN_VM_MEMORY,
|
||||||
max: constants::MAX_VM_MEMORY,
|
max: constants::MAX_VM_MEMORY,
|
||||||
@ -171,7 +173,7 @@ pub async fn network_hook_status() -> HttpResult {
|
|||||||
|
|
||||||
pub async fn number_vcpus() -> HttpResult {
|
pub async fn number_vcpus() -> HttpResult {
|
||||||
let mut system = System::new();
|
let mut system = System::new();
|
||||||
system.refresh_cpu();
|
system.refresh_cpu_all();
|
||||||
let number_cpus = system.cpus().len();
|
let number_cpus = system.cpus().len();
|
||||||
assert_ne!(number_cpus, 0, "Got invlid number of CPU!");
|
assert_ne!(number_cpus, 0, "Got invlid number of CPU!");
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ struct VMUuid {
|
|||||||
|
|
||||||
/// Create a new VM
|
/// Create a new VM
|
||||||
pub async fn create(client: LibVirtReq, req: web::Json<VMInfo>) -> HttpResult {
|
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,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to extract domain info! {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?;
|
let state = client.get_domain_state(id.uid).await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(VMInfoAndState {
|
Ok(HttpResponse::Ok().json(VMInfoAndState {
|
||||||
@ -112,7 +114,7 @@ pub async fn update(
|
|||||||
id: web::Path<SingleVMUUidReq>,
|
id: web::Path<SingleVMUUidReq>,
|
||||||
req: web::Json<VMInfo>,
|
req: web::Json<VMInfo>,
|
||||||
) -> HttpResult {
|
) -> HttpResult {
|
||||||
let mut domain = match req.0.as_tomain() {
|
let mut domain = match req.0.as_domain() {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to extract domain info! {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::hypervisor::HypervisorInfo;
|
||||||
use crate::libvirt_rest_structures::net::NetworkInfo;
|
use crate::libvirt_rest_structures::net::NetworkInfo;
|
||||||
use crate::libvirt_rest_structures::nw_filter::NetworkFilter;
|
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 actix::Addr;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LibVirtClient(pub Addr<LibVirtActor>);
|
pub struct LibVirtClient(pub Addr<LibVirtActor>);
|
||||||
@ -107,6 +108,20 @@ impl LibVirtClient {
|
|||||||
.await?
|
.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
|
/// Update a network configuration
|
||||||
pub async fn update_network(
|
pub async fn update_network(
|
||||||
&self,
|
&self,
|
||||||
|
@ -1,7 +1,25 @@
|
|||||||
use crate::libvirt_lib_structures::XMLUuid;
|
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
|
/// OS information
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "os")]
|
#[serde(rename = "os")]
|
||||||
pub struct OSXML {
|
pub struct OSXML {
|
||||||
#[serde(rename = "@firmware", default)]
|
#[serde(rename = "@firmware", default)]
|
||||||
@ -11,7 +29,7 @@ pub struct OSXML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// OS Type information
|
/// OS Type information
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "os")]
|
#[serde(rename = "os")]
|
||||||
pub struct OSTypeXML {
|
pub struct OSTypeXML {
|
||||||
#[serde(rename = "@arch")]
|
#[serde(rename = "@arch")]
|
||||||
@ -23,7 +41,7 @@ pub struct OSTypeXML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// OS Loader information
|
/// OS Loader information
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "loader")]
|
#[serde(rename = "loader")]
|
||||||
pub struct OSLoaderXML {
|
pub struct OSLoaderXML {
|
||||||
#[serde(rename = "@secure")]
|
#[serde(rename = "@secure")]
|
||||||
@ -31,39 +49,39 @@ pub struct OSLoaderXML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Hypervisor features
|
/// Hypervisor features
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Default)]
|
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
|
||||||
#[serde(rename = "features")]
|
#[serde(rename = "features")]
|
||||||
pub struct FeaturesXML {
|
pub struct FeaturesXML {
|
||||||
pub acpi: ACPIXML,
|
pub acpi: ACPIXML,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ACPI feature
|
/// ACPI feature
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Default)]
|
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
|
||||||
#[serde(rename = "acpi")]
|
#[serde(rename = "acpi")]
|
||||||
pub struct ACPIXML {}
|
pub struct ACPIXML {}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "mac")]
|
#[serde(rename = "mac")]
|
||||||
pub struct NetMacAddress {
|
pub struct NetMacAddress {
|
||||||
#[serde(rename = "@address")]
|
#[serde(rename = "@address")]
|
||||||
pub address: String,
|
pub address: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "source")]
|
#[serde(rename = "source")]
|
||||||
pub struct NetIntSourceXML {
|
pub struct NetIntSourceXML {
|
||||||
#[serde(rename = "@network")]
|
#[serde(rename = "@network")]
|
||||||
pub network: String,
|
pub network: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "model")]
|
#[serde(rename = "model")]
|
||||||
pub struct NetIntModelXML {
|
pub struct NetIntModelXML {
|
||||||
#[serde(rename = "@type")]
|
#[serde(rename = "@type")]
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "filterref")]
|
#[serde(rename = "filterref")]
|
||||||
pub struct NetIntFilterParameterXML {
|
pub struct NetIntFilterParameterXML {
|
||||||
#[serde(rename = "@name")]
|
#[serde(rename = "@name")]
|
||||||
@ -72,7 +90,7 @@ pub struct NetIntFilterParameterXML {
|
|||||||
pub value: String,
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "filterref")]
|
#[serde(rename = "filterref")]
|
||||||
pub struct NetIntfilterRefXML {
|
pub struct NetIntfilterRefXML {
|
||||||
#[serde(rename = "@filter")]
|
#[serde(rename = "@filter")]
|
||||||
@ -81,7 +99,7 @@ pub struct NetIntfilterRefXML {
|
|||||||
pub parameters: Vec<NetIntFilterParameterXML>,
|
pub parameters: Vec<NetIntFilterParameterXML>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "interface")]
|
#[serde(rename = "interface")]
|
||||||
pub struct DomainNetInterfaceXML {
|
pub struct DomainNetInterfaceXML {
|
||||||
#[serde(rename = "@type")]
|
#[serde(rename = "@type")]
|
||||||
@ -95,14 +113,14 @@ pub struct DomainNetInterfaceXML {
|
|||||||
pub filterref: Option<NetIntfilterRefXML>,
|
pub filterref: Option<NetIntfilterRefXML>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "input")]
|
#[serde(rename = "input")]
|
||||||
pub struct DomainInputXML {
|
pub struct DomainInputXML {
|
||||||
#[serde(rename = "@type")]
|
#[serde(rename = "@type")]
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "backend")]
|
#[serde(rename = "backend")]
|
||||||
pub struct TPMBackendXML {
|
pub struct TPMBackendXML {
|
||||||
#[serde(rename = "@type")]
|
#[serde(rename = "@type")]
|
||||||
@ -112,7 +130,7 @@ pub struct TPMBackendXML {
|
|||||||
pub r#version: String,
|
pub r#version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "tpm")]
|
#[serde(rename = "tpm")]
|
||||||
pub struct TPMDeviceXML {
|
pub struct TPMDeviceXML {
|
||||||
#[serde(rename = "@model")]
|
#[serde(rename = "@model")]
|
||||||
@ -121,7 +139,7 @@ pub struct TPMDeviceXML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Devices information
|
/// Devices information
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "devices")]
|
#[serde(rename = "devices")]
|
||||||
pub struct DevicesXML {
|
pub struct DevicesXML {
|
||||||
/// Graphics (used for VNC)
|
/// Graphics (used for VNC)
|
||||||
@ -150,7 +168,7 @@ pub struct DevicesXML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Graphics information
|
/// Graphics information
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "graphics")]
|
#[serde(rename = "graphics")]
|
||||||
pub struct GraphicsXML {
|
pub struct GraphicsXML {
|
||||||
#[serde(rename = "@type")]
|
#[serde(rename = "@type")]
|
||||||
@ -160,14 +178,14 @@ pub struct GraphicsXML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Video device information
|
/// Video device information
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "video")]
|
#[serde(rename = "video")]
|
||||||
pub struct VideoXML {
|
pub struct VideoXML {
|
||||||
pub model: VideoModelXML,
|
pub model: VideoModelXML,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Video model device information
|
/// Video model device information
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "model")]
|
#[serde(rename = "model")]
|
||||||
pub struct VideoModelXML {
|
pub struct VideoModelXML {
|
||||||
#[serde(rename = "@type")]
|
#[serde(rename = "@type")]
|
||||||
@ -175,7 +193,7 @@ pub struct VideoModelXML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Disk information
|
/// Disk information
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "disk")]
|
#[serde(rename = "disk")]
|
||||||
pub struct DiskXML {
|
pub struct DiskXML {
|
||||||
#[serde(rename = "@type")]
|
#[serde(rename = "@type")]
|
||||||
@ -193,7 +211,7 @@ pub struct DiskXML {
|
|||||||
pub address: Option<DiskAddressXML>,
|
pub address: Option<DiskAddressXML>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "driver")]
|
#[serde(rename = "driver")]
|
||||||
pub struct DiskDriverXML {
|
pub struct DiskDriverXML {
|
||||||
#[serde(rename = "@name")]
|
#[serde(rename = "@name")]
|
||||||
@ -204,14 +222,14 @@ pub struct DiskDriverXML {
|
|||||||
pub r#cache: String,
|
pub r#cache: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "source")]
|
#[serde(rename = "source")]
|
||||||
pub struct DiskSourceXML {
|
pub struct DiskSourceXML {
|
||||||
#[serde(rename = "@file")]
|
#[serde(rename = "@file")]
|
||||||
pub file: String,
|
pub file: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "target")]
|
#[serde(rename = "target")]
|
||||||
pub struct DiskTargetXML {
|
pub struct DiskTargetXML {
|
||||||
#[serde(rename = "@dev")]
|
#[serde(rename = "@dev")]
|
||||||
@ -220,18 +238,18 @@ pub struct DiskTargetXML {
|
|||||||
pub bus: String,
|
pub bus: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "readonly")]
|
#[serde(rename = "readonly")]
|
||||||
pub struct DiskReadOnlyXML {}
|
pub struct DiskReadOnlyXML {}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "boot")]
|
#[serde(rename = "boot")]
|
||||||
pub struct DiskBootXML {
|
pub struct DiskBootXML {
|
||||||
#[serde(rename = "@order")]
|
#[serde(rename = "@order")]
|
||||||
pub order: String,
|
pub order: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "address")]
|
#[serde(rename = "address")]
|
||||||
pub struct DiskAddressXML {
|
pub struct DiskAddressXML {
|
||||||
#[serde(rename = "@type")]
|
#[serde(rename = "@type")]
|
||||||
@ -251,7 +269,7 @@ pub struct DiskAddressXML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Domain RAM information
|
/// Domain RAM information
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "memory")]
|
#[serde(rename = "memory")]
|
||||||
pub struct DomainMemoryXML {
|
pub struct DomainMemoryXML {
|
||||||
#[serde(rename = "@unit")]
|
#[serde(rename = "@unit")]
|
||||||
@ -261,7 +279,7 @@ pub struct DomainMemoryXML {
|
|||||||
pub memory: usize,
|
pub memory: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "topology")]
|
#[serde(rename = "topology")]
|
||||||
pub struct DomainCPUTopology {
|
pub struct DomainCPUTopology {
|
||||||
#[serde(rename = "@sockets")]
|
#[serde(rename = "@sockets")]
|
||||||
@ -272,14 +290,14 @@ pub struct DomainCPUTopology {
|
|||||||
pub threads: usize,
|
pub threads: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "cpu")]
|
#[serde(rename = "cpu")]
|
||||||
pub struct DomainVCPUXML {
|
pub struct DomainVCPUXML {
|
||||||
#[serde(rename = "$value")]
|
#[serde(rename = "$value")]
|
||||||
pub body: usize,
|
pub body: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "cpu")]
|
#[serde(rename = "cpu")]
|
||||||
pub struct DomainCPUXML {
|
pub struct DomainCPUXML {
|
||||||
#[serde(rename = "@mode")]
|
#[serde(rename = "@mode")]
|
||||||
@ -288,7 +306,7 @@ pub struct DomainCPUXML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Domain information, see https://libvirt.org/formatdomain.html
|
/// Domain information, see https://libvirt.org/formatdomain.html
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename = "domain")]
|
#[serde(rename = "domain")]
|
||||||
pub struct DomainXML {
|
pub struct DomainXML {
|
||||||
/// Domain type (kvm)
|
/// Domain type (kvm)
|
||||||
@ -300,6 +318,9 @@ pub struct DomainXML {
|
|||||||
pub genid: Option<uuid::Uuid>,
|
pub genid: Option<uuid::Uuid>,
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub metadata: Option<DomainMetadataXML>,
|
||||||
|
|
||||||
pub os: OSXML,
|
pub os: OSXML,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub features: FeaturesXML,
|
pub features: FeaturesXML,
|
||||||
@ -319,10 +340,32 @@ pub struct DomainXML {
|
|||||||
pub on_crash: String,
|
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 {
|
impl DomainXML {
|
||||||
/// Decode Domain structure from XML definition
|
/// Decode Domain structure from XML definition
|
||||||
pub fn parse_xml(xml: &str) -> anyhow::Result<Self> {
|
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
|
/// 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 lazy_regex::regex;
|
||||||
use num::Integer;
|
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)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
pub enum BootType {
|
pub enum BootType {
|
||||||
UEFI,
|
UEFI,
|
||||||
@ -59,6 +64,9 @@ pub struct VMInfo {
|
|||||||
pub genid: Option<XMLUuid>,
|
pub genid: Option<XMLUuid>,
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub description: 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 boot_type: BootType,
|
||||||
pub architecture: VMArchitecture,
|
pub architecture: VMArchitecture,
|
||||||
/// VM allocated memory, in megabytes
|
/// VM allocated memory, in megabytes
|
||||||
@ -79,7 +87,7 @@ pub struct VMInfo {
|
|||||||
|
|
||||||
impl VMInfo {
|
impl VMInfo {
|
||||||
/// Turn this VM into a domain
|
/// 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) {
|
if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) {
|
||||||
return Err(StructureExtraction("VM name is invalid!").into());
|
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 {
|
if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY {
|
||||||
return Err(StructureExtraction("VM memory is invalid!").into());
|
return Err(StructureExtraction("VM memory is invalid!").into());
|
||||||
}
|
}
|
||||||
@ -282,6 +296,12 @@ impl VMInfo {
|
|||||||
title: self.title.clone(),
|
title: self.title.clone(),
|
||||||
description: self.description.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 {
|
os: OSXML {
|
||||||
r#type: OSTypeXML {
|
r#type: OSTypeXML {
|
||||||
arch: match self.architecture {
|
arch: match self.architecture {
|
||||||
@ -369,6 +389,13 @@ impl VMInfo {
|
|||||||
genid: domain.genid.map(XMLUuid),
|
genid: domain.genid.map(XMLUuid),
|
||||||
title: domain.title,
|
title: domain.title,
|
||||||
description: domain.description,
|
description: domain.description,
|
||||||
|
group: domain
|
||||||
|
.metadata
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.virtweb
|
||||||
|
.group
|
||||||
|
.map(VMGroupId),
|
||||||
boot_type: match domain.os.loader {
|
boot_type: match domain.os.loader {
|
||||||
None => BootType::UEFI,
|
None => BootType::UEFI,
|
||||||
Some(l) => match l.secure.as_str() {
|
Some(l) => match l.secure.as_str() {
|
||||||
|
@ -22,7 +22,7 @@ use virtweb_backend::constants::{
|
|||||||
MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
|
MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
|
||||||
};
|
};
|
||||||
use virtweb_backend::controllers::{
|
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,
|
nwfilter_controller, server_controller, static_controller, vm_controller,
|
||||||
};
|
};
|
||||||
use virtweb_backend::libvirt_client::LibVirtClient;
|
use virtweb_backend::libvirt_client::LibVirtClient;
|
||||||
@ -210,6 +210,8 @@ async fn main() -> std::io::Result<()> {
|
|||||||
web::get().to(vm_controller::vnc_token),
|
web::get().to(vm_controller::vnc_token),
|
||||||
)
|
)
|
||||||
.route("/api/vnc", web::get().to(vm_controller::vnc))
|
.route("/api/vnc", web::get().to(vm_controller::vnc))
|
||||||
|
// Groups controller
|
||||||
|
.route("/api/group/list", web::get().to(groups_controller::list))
|
||||||
// Network controller
|
// Network controller
|
||||||
.route(
|
.route(
|
||||||
"/api/network/create",
|
"/api/network/create",
|
||||||
|
@ -9,7 +9,7 @@ make
|
|||||||
|
|
||||||
The release file will be available in `virtweb_backend/target/release/virtweb_backend`.
|
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
|
## Install requirements
|
||||||
In order to work properly, VirtWeb relies on `libvirt`, `qemu` and `kvm`:
|
In order to work properly, VirtWeb relies on `libvirt`, `qemu` and `kvm`:
|
||||||
|
12452
virtweb_frontend/package-lock.json
generated
12452
virtweb_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,36 +6,36 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@fontsource/roboto": "^5.0.13",
|
"@fontsource/roboto": "^5.1.0",
|
||||||
"@mdi/js": "^7.2.96",
|
"@mdi/js": "^7.2.96",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mui/icons-material": "^5.14.7",
|
"@mui/icons-material": "^6.1.6",
|
||||||
"@mui/material": "^5.14.7",
|
"@mui/material": "^6.1.6",
|
||||||
"@mui/x-charts": "^7.3.0",
|
"@mui/x-charts": "^7.22.1",
|
||||||
"@mui/x-data-grid": "^7.3.0",
|
"@mui/x-data-grid": "^7.22.1",
|
||||||
"@testing-library/jest-dom": "^6.4.2",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^15.0.4",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/humanize-duration": "^3.27.1",
|
"@types/humanize-duration": "^3.27.1",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/react": "^18.2.79",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.2.25",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/react-syntax-highlighter": "^15.5.11",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@types/uuid": "^9.0.5",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
"date-and-time": "^3.1.1",
|
"date-and-time": "^3.6.0",
|
||||||
"filesize": "^10.0.12",
|
"filesize": "^10.1.6",
|
||||||
"humanize-duration": "^3.29.0",
|
"humanize-duration": "^3.29.0",
|
||||||
"mui-file-input": "^4.0.4",
|
"mui-file-input": "^6.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.23.0",
|
"react-router-dom": "^6.23.0",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"react-vnc": "^1.0.0",
|
"react-vnc": "^2.0.2",
|
||||||
"typescript": "^4.0.0",
|
"typescript": "^4.9.5",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^11.0.2",
|
||||||
"vite": "^5.2.10",
|
"vite": "^5.4.10",
|
||||||
"vite-tsconfig-paths": "^4.2.2",
|
"vite-tsconfig-paths": "^5.0.1",
|
||||||
"web-vitals": "^3.5.2",
|
"web-vitals": "^3.5.2",
|
||||||
"xml-formatter": "^3.6.0"
|
"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;
|
vnc_token_duration: number;
|
||||||
vm_name_size: LenConstraint;
|
vm_name_size: LenConstraint;
|
||||||
vm_title_size: LenConstraint;
|
vm_title_size: LenConstraint;
|
||||||
|
group_id_size: LenConstraint;
|
||||||
memory_size: LenConstraint;
|
memory_size: LenConstraint;
|
||||||
disk_name_size: LenConstraint;
|
disk_name_size: LenConstraint;
|
||||||
disk_size: LenConstraint;
|
disk_size: LenConstraint;
|
||||||
@ -73,7 +74,7 @@ interface SystemInfo {
|
|||||||
secs: number;
|
secs: number;
|
||||||
nanos: number;
|
nanos: number;
|
||||||
};
|
};
|
||||||
global_cpu_info: GlobalCPUInfo;
|
global_cpu_usage: number;
|
||||||
cpus: CpuCore[];
|
cpus: CpuCore[];
|
||||||
physical_core_count: number;
|
physical_core_count: number;
|
||||||
total_memory: number;
|
total_memory: number;
|
||||||
@ -94,14 +95,6 @@ interface SystemInfo {
|
|||||||
host_name: string;
|
host_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GlobalCPUInfo {
|
|
||||||
cpu_usage: number;
|
|
||||||
name: string;
|
|
||||||
vendor_id: string;
|
|
||||||
brand: string;
|
|
||||||
frequency: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CpuCore {
|
interface CpuCore {
|
||||||
cpu_usage: number;
|
cpu_usage: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -63,6 +63,7 @@ interface VMInfoInterface {
|
|||||||
genid?: string;
|
genid?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
group?: string;
|
||||||
boot_type: "UEFI" | "UEFISecureBoot";
|
boot_type: "UEFI" | "UEFISecureBoot";
|
||||||
architecture: "i686" | "x86_64";
|
architecture: "i686" | "x86_64";
|
||||||
memory: number;
|
memory: number;
|
||||||
@ -80,6 +81,7 @@ export class VMInfo implements VMInfoInterface {
|
|||||||
genid?: string;
|
genid?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
group?: string;
|
||||||
boot_type: "UEFI" | "UEFISecureBoot";
|
boot_type: "UEFI" | "UEFISecureBoot";
|
||||||
architecture: "i686" | "x86_64";
|
architecture: "i686" | "x86_64";
|
||||||
number_vcpu: number;
|
number_vcpu: number;
|
||||||
@ -96,6 +98,7 @@ export class VMInfo implements VMInfoInterface {
|
|||||||
this.genid = int.genid;
|
this.genid = int.genid;
|
||||||
this.title = int.title;
|
this.title = int.title;
|
||||||
this.description = int.description;
|
this.description = int.description;
|
||||||
|
this.group = int.group;
|
||||||
this.boot_type = int.boot_type;
|
this.boot_type = int.boot_type;
|
||||||
this.architecture = int.architecture;
|
this.architecture = int.architecture;
|
||||||
this.number_vcpu = int.number_vcpu;
|
this.number_vcpu = int.number_vcpu;
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Grid,
|
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -17,7 +16,10 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
import { PieChart } from "@mui/x-charts";
|
import { PieChart } from "@mui/x-charts";
|
||||||
|
import { filesize } from "filesize";
|
||||||
|
import humanizeDuration from "humanize-duration";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
DiskInfo,
|
DiskInfo,
|
||||||
@ -28,8 +30,6 @@ import {
|
|||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
import { VirtWebPaper } from "../widgets/VirtWebPaper";
|
import { VirtWebPaper } from "../widgets/VirtWebPaper";
|
||||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
import humanizeDuration from "humanize-duration";
|
|
||||||
import { filesize } from "filesize";
|
|
||||||
|
|
||||||
export function SysInfoRoute(): React.ReactElement {
|
export function SysInfoRoute(): React.ReactElement {
|
||||||
const [info, setInfo] = React.useState<ServerSystemInfo>();
|
const [info, setInfo] = React.useState<ServerSystemInfo>();
|
||||||
@ -65,7 +65,7 @@ export function SysInfoRouteInner(p: {
|
|||||||
<VirtWebRouteContainer label="Sysinfo">
|
<VirtWebRouteContainer label="Sysinfo">
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{/* Memory */}
|
{/* Memory */}
|
||||||
<Grid xs={4}>
|
<Grid size={{ xs: 4 }}>
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
<Typography style={{ textAlign: "center" }}>Memory</Typography>
|
<Typography style={{ textAlign: "center" }}>Memory</Typography>
|
||||||
<PieChart
|
<PieChart
|
||||||
@ -97,7 +97,7 @@ export function SysInfoRouteInner(p: {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Disk usage */}
|
{/* Disk usage */}
|
||||||
<Grid xs={4}>
|
<Grid size={{ xs: 4 }}>
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
<Typography style={{ textAlign: "center" }}>Disk usage</Typography>
|
<Typography style={{ textAlign: "center" }}>Disk usage</Typography>
|
||||||
<PieChart
|
<PieChart
|
||||||
@ -125,7 +125,7 @@ export function SysInfoRouteInner(p: {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* CPU usage */}
|
{/* CPU usage */}
|
||||||
<Grid xs={4}>
|
<Grid size={{ xs: 4 }}>
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
<Typography style={{ textAlign: "center" }}>CPU usage</Typography>
|
<Typography style={{ textAlign: "center" }}>CPU usage</Typography>
|
||||||
<PieChart
|
<PieChart
|
||||||
@ -134,13 +134,13 @@ export function SysInfoRouteInner(p: {
|
|||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
value: 100 - p.info.system.global_cpu_info.cpu_usage,
|
value: 100 - p.info.system.global_cpu_usage,
|
||||||
label: "Free",
|
label: "Free",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
value: p.info.system.global_cpu_info.cpu_usage,
|
value: p.info.system.global_cpu_usage,
|
||||||
label: "Used",
|
label: "Used",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -180,18 +180,18 @@ export function SysInfoRouteInner(p: {
|
|||||||
label="CPU info"
|
label="CPU info"
|
||||||
icon={<Icon size={"1rem"} path={mdiMemory} />}
|
icon={<Icon size={"1rem"} path={mdiMemory} />}
|
||||||
entries={[
|
entries={[
|
||||||
{ label: "Brand", value: p.info.system.global_cpu_info.brand },
|
{ label: "Brand", value: p.info.system.cpus[0].brand },
|
||||||
{
|
{
|
||||||
label: "Vendor ID",
|
label: "Vendor ID",
|
||||||
value: p.info.system.global_cpu_info.vendor_id,
|
value: p.info.system.cpus[0].vendor_id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "CPU usage",
|
label: "CPU usage",
|
||||||
value: p.info.system.global_cpu_info.cpu_usage,
|
value: p.info.system.cpus[0].cpu_usage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Name",
|
label: "Name",
|
||||||
value: p.info.system.global_cpu_info.name,
|
value: p.info.system.cpus[0].name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "CPU model",
|
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 VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -7,6 +9,7 @@ import {
|
|||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@ -14,19 +17,27 @@ import {
|
|||||||
import { filesize } from "filesize";
|
import { filesize } from "filesize";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
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 { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
import { RouterLink } from "../widgets/RouterLink";
|
import { RouterLink } from "../widgets/RouterLink";
|
||||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
import { VMStatusWidget } from "../widgets/vms/VMStatusWidget";
|
import { VMStatusWidget } from "../widgets/vms/VMStatusWidget";
|
||||||
|
|
||||||
export function VMListRoute(): React.ReactElement {
|
export function VMListRoute(): React.ReactElement {
|
||||||
|
const [groups, setGroups] = React.useState<Array<string | undefined>>();
|
||||||
const [list, setList] = React.useState<VMInfo[] | undefined>();
|
const [list, setList] = React.useState<VMInfo[] | undefined>();
|
||||||
|
|
||||||
const loadKey = React.useRef(1);
|
const loadKey = React.useRef(1);
|
||||||
|
|
||||||
const load = async () => {
|
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 = () => {
|
const reload = () => {
|
||||||
@ -51,7 +62,7 @@ export function VMListRoute(): React.ReactElement {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<VMListWidget list={list!} onReload={reload} />
|
<VMListWidget list={list!} groups={groups!} onReload={reload} />
|
||||||
</VirtWebRouteContainer>
|
</VirtWebRouteContainer>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -59,11 +70,37 @@ export function VMListRoute(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function VMListWidget(p: {
|
function VMListWidget(p: {
|
||||||
|
groups: Array<string | undefined>;
|
||||||
list: VMInfo[];
|
list: VMInfo[];
|
||||||
onReload: () => void;
|
onReload: () => void;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const navigate = useNavigate();
|
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 (
|
return (
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table>
|
<Table>
|
||||||
@ -72,12 +109,39 @@ function VMListWidget(p: {
|
|||||||
<TableCell>Name</TableCell>
|
<TableCell>Name</TableCell>
|
||||||
<TableCell>Description</TableCell>
|
<TableCell>Description</TableCell>
|
||||||
<TableCell>Memory</TableCell>
|
<TableCell>Memory</TableCell>
|
||||||
|
<TableCell>vCPU</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
<TableCell>Actions</TableCell>
|
<TableCell>Actions</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<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
|
<TableRow
|
||||||
hover
|
hover
|
||||||
key={row.name}
|
key={row.name}
|
||||||
@ -88,9 +152,13 @@ function VMListWidget(p: {
|
|||||||
{row.name}
|
{row.name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.description ?? ""}</TableCell>
|
<TableCell>{row.description ?? ""}</TableCell>
|
||||||
<TableCell>{filesize(row.memory * 1000 * 1000)}</TableCell>
|
<TableCell>{vmMemoryToHuman(row.memory)}</TableCell>
|
||||||
|
<TableCell>{row.number_vcpu}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<VMStatusWidget vm={row} />
|
<VMStatusWidget
|
||||||
|
vm={row}
|
||||||
|
onChange={(s) => updateVMState(row, s)}
|
||||||
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Tooltip title="View this VM">
|
<Tooltip title="View this VM">
|
||||||
@ -103,8 +171,38 @@ function VMListWidget(p: {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</TableBody>
|
</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>
|
</Table>
|
||||||
</TableContainer>
|
</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 Avatar from "@mui/material/Avatar";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
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 Paper from "@mui/material/Paper";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { Link, Outlet } from "react-router-dom";
|
import { Link, Outlet } from "react-router-dom";
|
||||||
@ -38,10 +38,7 @@ export function BaseLoginPage() {
|
|||||||
<Grid container component="main" sx={{ height: "100vh" }}>
|
<Grid container component="main" sx={{ height: "100vh" }}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Grid
|
<Grid
|
||||||
item
|
size={{ xs: false, sm: 4, md: 7 }}
|
||||||
xs={false}
|
|
||||||
sm={4}
|
|
||||||
md={7}
|
|
||||||
sx={{
|
sx={{
|
||||||
backgroundImage: "url(/login_splash.jpg)",
|
backgroundImage: "url(/login_splash.jpg)",
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundRepeat: "no-repeat",
|
||||||
@ -53,7 +50,12 @@ export function BaseLoginPage() {
|
|||||||
backgroundPosition: "center",
|
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
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
my: 8,
|
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 React, { PropsWithChildren } from "react";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
|
||||||
export function EditSection(
|
export function EditSection(
|
||||||
p: {
|
p: {
|
||||||
@ -9,7 +10,7 @@ export function EditSection(
|
|||||||
} & PropsWithChildren
|
} & PropsWithChildren
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
return (
|
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" }}>
|
<Paper style={{ margin: "10px", padding: "10px" }}>
|
||||||
{(p.title || p.actions) && (
|
{(p.title || p.actions) && (
|
||||||
<span
|
<span
|
||||||
|
@ -4,7 +4,6 @@ import DeleteIcon from "@mui/icons-material/Delete";
|
|||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Grid,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
@ -19,6 +18,7 @@ import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
|||||||
import { IPInput } from "./IPInput";
|
import { IPInput } from "./IPInput";
|
||||||
import { MACInput } from "./MACInput";
|
import { MACInput } from "./MACInput";
|
||||||
import { TextInput } from "./TextInput";
|
import { TextInput } from "./TextInput";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
|
||||||
export function NetDHCPHostReservations(p: {
|
export function NetDHCPHostReservations(p: {
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
@ -39,7 +39,7 @@ export function NetDHCPHostReservations(p: {
|
|||||||
<>
|
<>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
{p.dhcp.hosts.map((h, num) => (
|
{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
|
<HostReservationWidget
|
||||||
key={num}
|
key={num}
|
||||||
{...p}
|
{...p}
|
||||||
|
@ -5,11 +5,11 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
CardActions,
|
CardActions,
|
||||||
CardContent,
|
CardContent,
|
||||||
Grid,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
import React, { PropsWithChildren } from "react";
|
import React, { PropsWithChildren } from "react";
|
||||||
import { NatEntry } from "../../api/NetworksApi";
|
import { NatEntry } from "../../api/NetworksApi";
|
||||||
import { ServerApi } from "../../api/ServerApi";
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
@ -295,7 +295,7 @@ function NATEntryProp(
|
|||||||
p: PropsWithChildren<{ label?: string }>
|
p: PropsWithChildren<{ label?: string }>
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<Grid item sm={12} md={6} style={{ padding: "20px" }}>
|
<Grid size={{ sm: 12, md: 6 }} style={{ padding: "20px" }}>
|
||||||
{p.label && (
|
{p.label && (
|
||||||
<Typography variant="h6" style={{ marginBottom: "10px" }}>
|
<Typography variant="h6" style={{ marginBottom: "10px" }}>
|
||||||
{p.label}
|
{p.label}
|
||||||
|
@ -4,13 +4,13 @@ import DeleteIcon from "@mui/icons-material/Delete";
|
|||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Grid,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
import { NWFilter } from "../../api/NWFilterApi";
|
import { NWFilter } from "../../api/NWFilterApi";
|
||||||
import { NetworkInfo } from "../../api/NetworksApi";
|
import { NetworkInfo } from "../../api/NetworksApi";
|
||||||
import { ServerApi } from "../../api/ServerApi";
|
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 React from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { IpConfig, NetworkApi, NetworkInfo } from "../../api/NetworksApi";
|
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 React, { ReactElement } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
@ -6,6 +7,7 @@ import {
|
|||||||
NWFilterApi,
|
NWFilterApi,
|
||||||
NWFilterIsBuiltin,
|
NWFilterIsBuiltin,
|
||||||
} from "../../api/NWFilterApi";
|
} from "../../api/NWFilterApi";
|
||||||
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
|
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
|
||||||
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
||||||
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
||||||
@ -13,12 +15,11 @@ import { AsyncWidget } from "../AsyncWidget";
|
|||||||
import { TabsWidget } from "../TabsWidget";
|
import { TabsWidget } from "../TabsWidget";
|
||||||
import { XMLAsyncWidget } from "../XMLWidget";
|
import { XMLAsyncWidget } from "../XMLWidget";
|
||||||
import { EditSection } from "../forms/EditSection";
|
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 { NWFSelectReferencedFilters } from "../forms/NWFSelectReferencedFilters";
|
||||||
import { NWFilterRules } from "../forms/NWFilterRules";
|
|
||||||
import { NWFilterPriorityInput } from "../forms/NWFilterPriorityInput";
|
import { NWFilterPriorityInput } from "../forms/NWFilterPriorityInput";
|
||||||
|
import { NWFilterRules } from "../forms/NWFilterRules";
|
||||||
|
import { SelectInput } from "../forms/SelectInput";
|
||||||
|
import { TextInput } from "../forms/TextInput";
|
||||||
|
|
||||||
interface DetailsProps {
|
interface DetailsProps {
|
||||||
nwfilter: NWFilter;
|
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 React from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
|
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 React from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { validate as validateUUID } from "uuid";
|
import { validate as validateUUID } from "uuid";
|
||||||
|
import { GroupApi } from "../../api/GroupApi";
|
||||||
import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi";
|
import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi";
|
||||||
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
|
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
|
||||||
import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
|
import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
|
||||||
@ -12,6 +16,7 @@ import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
|||||||
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
||||||
import { AsyncWidget } from "../AsyncWidget";
|
import { AsyncWidget } from "../AsyncWidget";
|
||||||
import { TabsWidget } from "../TabsWidget";
|
import { TabsWidget } from "../TabsWidget";
|
||||||
|
import { XMLAsyncWidget } from "../XMLWidget";
|
||||||
import { CheckboxInput } from "../forms/CheckboxInput";
|
import { CheckboxInput } from "../forms/CheckboxInput";
|
||||||
import { EditSection } from "../forms/EditSection";
|
import { EditSection } from "../forms/EditSection";
|
||||||
import { ResAutostartInput } from "../forms/ResAutostartInput";
|
import { ResAutostartInput } from "../forms/ResAutostartInput";
|
||||||
@ -21,7 +26,6 @@ import { VMDisksList } from "../forms/VMDisksList";
|
|||||||
import { VMNetworksList } from "../forms/VMNetworksList";
|
import { VMNetworksList } from "../forms/VMNetworksList";
|
||||||
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
|
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
|
||||||
import { VMScreenshot } from "./VMScreenshot";
|
import { VMScreenshot } from "./VMScreenshot";
|
||||||
import { XMLAsyncWidget } from "../XMLWidget";
|
|
||||||
|
|
||||||
interface DetailsProps {
|
interface DetailsProps {
|
||||||
vm: VMInfo;
|
vm: VMInfo;
|
||||||
@ -31,6 +35,7 @@ interface DetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function VMDetails(p: DetailsProps): React.ReactElement {
|
export function VMDetails(p: DetailsProps): React.ReactElement {
|
||||||
|
const [groupsList, setGroupsList] = React.useState<string[] | any>();
|
||||||
const [isoList, setIsoList] = React.useState<IsoFile[] | any>();
|
const [isoList, setIsoList] = React.useState<IsoFile[] | any>();
|
||||||
const [vcpuCombinations, setVCPUCombinations] = React.useState<
|
const [vcpuCombinations, setVCPUCombinations] = React.useState<
|
||||||
number[] | any
|
number[] | any
|
||||||
@ -41,6 +46,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
|
|||||||
>();
|
>();
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
|
setGroupsList(await GroupApi.GetList());
|
||||||
setIsoList(await IsoFilesApi.GetList());
|
setIsoList(await IsoFilesApi.GetList());
|
||||||
setVCPUCombinations(await ServerApi.NumberVCPUs());
|
setVCPUCombinations(await ServerApi.NumberVCPUs());
|
||||||
setNetworksList(await NetworkApi.GetList());
|
setNetworksList(await NetworkApi.GetList());
|
||||||
@ -54,6 +60,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
|
|||||||
errMsg="Failed to load the list of ISO files"
|
errMsg="Failed to load the list of ISO files"
|
||||||
build={() => (
|
build={() => (
|
||||||
<VMDetailsInner
|
<VMDetailsInner
|
||||||
|
groupsList={groupsList}
|
||||||
isoList={isoList}
|
isoList={isoList}
|
||||||
vcpuCombinations={vcpuCombinations}
|
vcpuCombinations={vcpuCombinations}
|
||||||
networksList={networksList}
|
networksList={networksList}
|
||||||
@ -74,6 +81,7 @@ enum VMTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DetailsInnerProps = DetailsProps & {
|
type DetailsInnerProps = DetailsProps & {
|
||||||
|
groupsList: string[];
|
||||||
isoList: IsoFile[];
|
isoList: IsoFile[];
|
||||||
vcpuCombinations: number[];
|
vcpuCombinations: number[];
|
||||||
networksList: NetworkInfo[];
|
networksList: NetworkInfo[];
|
||||||
@ -116,6 +124,8 @@ function VMDetailsInner(p: DetailsInnerProps): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
||||||
|
const [addGroup, setAddGroup] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{
|
{
|
||||||
@ -174,6 +184,50 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
|||||||
}}
|
}}
|
||||||
multiline={true}
|
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>
|
</EditSection>
|
||||||
|
|
||||||
{/* General section */}
|
{/* General section */}
|
||||||
|
Loading…
Reference in New Issue
Block a user