Compare commits

..

1 Commits

Author SHA1 Message Date
6272df6b8b Update dependency react-vnc to v2
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-12 00:14:01 +00:00
29 changed files with 7466 additions and 6761 deletions

View File

@ -5,7 +5,7 @@ name: default
steps:
- name: web_build
image: node:23
image: node:22
volumes:
- name: web_app
path: /tmp/web_build

File diff suppressed because it is too large Load Diff

View File

@ -8,41 +8,41 @@ edition = "2021"
[dependencies]
log = "0.4.21"
env_logger = "0.11.3"
clap = { version = "4.5.20", features = ["derive", "env"] }
clap = { version = "4.5.4", features = ["derive", "env"] }
light-openid = { version = "1.0.2", features = ["crypto-wrapper"] }
lazy_static = "1.5.0"
lazy_static = "1.4.0"
actix = "0.13.3"
actix-web = "4.9.0"
actix-web = "4.5.1"
actix-remote-ip = "0.1.0"
actix-session = { version = "0.10.0", features = ["cookie-session"] }
actix-identity = "0.8.0"
actix-session = { version = "0.9.0", features = ["cookie-session"] }
actix-identity = "0.7.1"
actix-cors = "0.7.0"
actix-files = "0.6.5"
actix-web-actors = "4.3.0"
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"] }
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"] }
url = "2.5.0"
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"
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"
rand = "0.8.5"
bytes = "1.8.0"
tokio = "1.41.0"
futures = "0.3.31"
bytes = "1.6.0"
tokio = "1.37.0"
futures = "0.3.30"
ipnetwork = "0.20.0"
num = "0.4.2"
rust-embed = { version = "8.5.0" }
rust-embed = { version = "8.3.0" }
mime_guess = "2.0.4"
dotenvy = "0.15.7"
nix = { version = "0.29.0", features = ["net"] }
nix = { version = "0.28.0", features = ["net"] }
basic-jwt = "0.2.0"

View File

@ -31,7 +31,7 @@ impl LibVirtActor {
"Will connect to hypvervisor at address '{}'",
hypervisor_uri
);
let conn = Connect::open(Some(hypervisor_uri))?;
let conn = Connect::open(hypervisor_uri)?;
Ok(Self { m: conn })
}

View File

@ -1,16 +0,0 @@
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))
}

View File

@ -8,7 +8,6 @@ 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;

View File

@ -40,7 +40,6 @@ 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,
@ -73,7 +72,6 @@ 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,
@ -173,7 +171,7 @@ pub async fn network_hook_status() -> HttpResult {
pub async fn number_vcpus() -> HttpResult {
let mut system = System::new();
system.refresh_cpu_all();
system.refresh_cpu();
let number_cpus = system.cpus().len();
assert_ne!(number_cpus, 0, "Got invlid number of CPU!");

View File

@ -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_domain() {
let domain = match req.0.as_tomain() {
Ok(d) => d,
Err(e) => {
log::error!("Failed to extract domain info! {e}");
@ -83,8 +83,6 @@ 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 {
@ -114,7 +112,7 @@ pub async fn update(
id: web::Path<SingleVMUUidReq>,
req: web::Json<VMInfo>,
) -> HttpResult {
let mut domain = match req.0.as_domain() {
let mut domain = match req.0.as_tomain() {
Ok(d) => d,
Err(e) => {
log::error!("Failed to extract domain info! {e}");

View File

@ -7,9 +7,8 @@ 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::{VMGroupId, VMInfo};
use crate::libvirt_rest_structures::vm::VMInfo;
use actix::Addr;
use std::collections::HashSet;
#[derive(Clone)]
pub struct LibVirtClient(pub Addr<LibVirtActor>);
@ -108,20 +107,6 @@ 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,

View File

@ -1,25 +1,7 @@
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(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "os")]
pub struct OSXML {
#[serde(rename = "@firmware", default)]
@ -29,7 +11,7 @@ pub struct OSXML {
}
/// OS Type information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "os")]
pub struct OSTypeXML {
#[serde(rename = "@arch")]
@ -41,7 +23,7 @@ pub struct OSTypeXML {
}
/// OS Loader information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "loader")]
pub struct OSLoaderXML {
#[serde(rename = "@secure")]
@ -49,39 +31,39 @@ pub struct OSLoaderXML {
}
/// Hypervisor features
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
#[derive(serde::Serialize, serde::Deserialize, Default)]
#[serde(rename = "features")]
pub struct FeaturesXML {
pub acpi: ACPIXML,
}
/// ACPI feature
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
#[derive(serde::Serialize, serde::Deserialize, Default)]
#[serde(rename = "acpi")]
pub struct ACPIXML {}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "mac")]
pub struct NetMacAddress {
#[serde(rename = "@address")]
pub address: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "source")]
pub struct NetIntSourceXML {
#[serde(rename = "@network")]
pub network: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "model")]
pub struct NetIntModelXML {
#[serde(rename = "@type")]
pub r#type: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "filterref")]
pub struct NetIntFilterParameterXML {
#[serde(rename = "@name")]
@ -90,7 +72,7 @@ pub struct NetIntFilterParameterXML {
pub value: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "filterref")]
pub struct NetIntfilterRefXML {
#[serde(rename = "@filter")]
@ -99,7 +81,7 @@ pub struct NetIntfilterRefXML {
pub parameters: Vec<NetIntFilterParameterXML>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "interface")]
pub struct DomainNetInterfaceXML {
#[serde(rename = "@type")]
@ -113,14 +95,14 @@ pub struct DomainNetInterfaceXML {
pub filterref: Option<NetIntfilterRefXML>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "input")]
pub struct DomainInputXML {
#[serde(rename = "@type")]
pub r#type: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "backend")]
pub struct TPMBackendXML {
#[serde(rename = "@type")]
@ -130,7 +112,7 @@ pub struct TPMBackendXML {
pub r#version: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "tpm")]
pub struct TPMDeviceXML {
#[serde(rename = "@model")]
@ -139,7 +121,7 @@ pub struct TPMDeviceXML {
}
/// Devices information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "devices")]
pub struct DevicesXML {
/// Graphics (used for VNC)
@ -168,7 +150,7 @@ pub struct DevicesXML {
}
/// Graphics information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "graphics")]
pub struct GraphicsXML {
#[serde(rename = "@type")]
@ -178,14 +160,14 @@ pub struct GraphicsXML {
}
/// Video device information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "video")]
pub struct VideoXML {
pub model: VideoModelXML,
}
/// Video model device information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "model")]
pub struct VideoModelXML {
#[serde(rename = "@type")]
@ -193,7 +175,7 @@ pub struct VideoModelXML {
}
/// Disk information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "disk")]
pub struct DiskXML {
#[serde(rename = "@type")]
@ -211,7 +193,7 @@ pub struct DiskXML {
pub address: Option<DiskAddressXML>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "driver")]
pub struct DiskDriverXML {
#[serde(rename = "@name")]
@ -222,14 +204,14 @@ pub struct DiskDriverXML {
pub r#cache: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "source")]
pub struct DiskSourceXML {
#[serde(rename = "@file")]
pub file: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "target")]
pub struct DiskTargetXML {
#[serde(rename = "@dev")]
@ -238,18 +220,18 @@ pub struct DiskTargetXML {
pub bus: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "readonly")]
pub struct DiskReadOnlyXML {}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "boot")]
pub struct DiskBootXML {
#[serde(rename = "@order")]
pub order: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "address")]
pub struct DiskAddressXML {
#[serde(rename = "@type")]
@ -269,7 +251,7 @@ pub struct DiskAddressXML {
}
/// Domain RAM information
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "memory")]
pub struct DomainMemoryXML {
#[serde(rename = "@unit")]
@ -279,7 +261,7 @@ pub struct DomainMemoryXML {
pub memory: usize,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "topology")]
pub struct DomainCPUTopology {
#[serde(rename = "@sockets")]
@ -290,14 +272,14 @@ pub struct DomainCPUTopology {
pub threads: usize,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "cpu")]
pub struct DomainVCPUXML {
#[serde(rename = "$value")]
pub body: usize,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "cpu")]
pub struct DomainCPUXML {
#[serde(rename = "@mode")]
@ -306,7 +288,7 @@ pub struct DomainCPUXML {
}
/// Domain information, see https://libvirt.org/formatdomain.html
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "domain")]
pub struct DomainXML {
/// Domain type (kvm)
@ -318,9 +300,6 @@ 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,
@ -340,32 +319,10 @@ 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> {
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)
Ok(quick_xml::de::from_str(xml)?)
}
/// Turn this domain into its XML definition

View File

@ -10,11 +10,6 @@ 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,
@ -64,9 +59,6 @@ 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
@ -87,7 +79,7 @@ pub struct VMInfo {
impl VMInfo {
/// Turn this VM into a domain
pub fn as_domain(&self) -> anyhow::Result<DomainXML> {
pub fn as_tomain(&self) -> anyhow::Result<DomainXML> {
if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) {
return Err(StructureExtraction("VM name is invalid!").into());
}
@ -113,12 +105,6 @@ 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());
}
@ -296,12 +282,6 @@ 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 {
@ -389,13 +369,6 @@ 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() {

View File

@ -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, groups_controller, iso_controller, network_controller,
api_tokens_controller, auth_controller, iso_controller, network_controller,
nwfilter_controller, server_controller, static_controller, vm_controller,
};
use virtweb_backend::libvirt_client::LibVirtClient;
@ -210,8 +210,6 @@ 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",

View File

@ -9,7 +9,7 @@ make
The release file will be available in `virtweb_backend/target/release/virtweb_backend`.
This is the only artifact that must be copied to the server. It is recommended to copy it to the `/usr/local/bin` directory.
This is the only artifcat 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`:

File diff suppressed because it is too large Load Diff

View File

@ -6,36 +6,36 @@
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.1.0",
"@fontsource/roboto": "^5.0.13",
"@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.1",
"@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",
"@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",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/humanize-duration": "^3.27.1",
"@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",
"@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",
"humanize-duration": "^3.29.0",
"mui-file-input": "^6.0.0",
"mui-file-input": "^4.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.23.0",
"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",
"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",
"web-vitals": "^3.5.2",
"xml-formatter": "^3.6.0"
},

View File

@ -1,15 +0,0 @@
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;
}
}

View File

@ -16,7 +16,6 @@ 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;
@ -74,7 +73,7 @@ interface SystemInfo {
secs: number;
nanos: number;
};
global_cpu_usage: number;
global_cpu_info: GlobalCPUInfo;
cpus: CpuCore[];
physical_core_count: number;
total_memory: number;
@ -95,6 +94,14 @@ 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;

View File

@ -63,7 +63,6 @@ interface VMInfoInterface {
genid?: string;
title?: string;
description?: string;
group?: string;
boot_type: "UEFI" | "UEFISecureBoot";
architecture: "i686" | "x86_64";
memory: number;
@ -81,7 +80,6 @@ export class VMInfo implements VMInfoInterface {
genid?: string;
title?: string;
description?: string;
group?: string;
boot_type: "UEFI" | "UEFISecureBoot";
architecture: "i686" | "x86_64";
number_vcpu: number;
@ -98,7 +96,6 @@ 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;

View File

@ -8,6 +8,7 @@ import {
import Icon from "@mdi/react";
import {
Box,
Grid,
LinearProgress,
Table,
TableBody,
@ -16,10 +17,7 @@ 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,
@ -30,6 +28,8 @@ 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 size={{ xs: 4 }}>
<Grid xs={4}>
<Box flexGrow={1}>
<Typography style={{ textAlign: "center" }}>Memory</Typography>
<PieChart
@ -97,7 +97,7 @@ export function SysInfoRouteInner(p: {
</Grid>
{/* Disk usage */}
<Grid size={{ xs: 4 }}>
<Grid 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 size={{ xs: 4 }}>
<Grid 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_usage,
value: 100 - p.info.system.global_cpu_info.cpu_usage,
label: "Free",
},
{
id: 2,
value: p.info.system.global_cpu_usage,
value: p.info.system.global_cpu_info.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.cpus[0].brand },
{ label: "Brand", value: p.info.system.global_cpu_info.brand },
{
label: "Vendor ID",
value: p.info.system.cpus[0].vendor_id,
value: p.info.system.global_cpu_info.vendor_id,
},
{
label: "CPU usage",
value: p.info.system.cpus[0].cpu_usage,
value: p.info.system.global_cpu_info.cpu_usage,
},
{
label: "Name",
value: p.info.system.cpus[0].name,
value: p.info.system.global_cpu_info.name,
},
{
label: "CPU model",

View File

@ -1,5 +1,3 @@
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import VisibilityIcon from "@mui/icons-material/Visibility";
import {
Button,
@ -9,7 +7,6 @@ import {
TableBody,
TableCell,
TableContainer,
TableFooter,
TableHead,
TableRow,
Tooltip,
@ -17,27 +14,19 @@ import {
import { filesize } from "filesize";
import React from "react";
import { useNavigate } from "react-router-dom";
import { GroupApi } from "../api/GroupApi";
import { VMApi, VMInfo, VMState } from "../api/VMApi";
import { VMApi, VMInfo } 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 () => {
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);
setList(await VMApi.GetList());
};
const reload = () => {
@ -62,7 +51,7 @@ export function VMListRoute(): React.ReactElement {
</>
}
>
<VMListWidget list={list!} groups={groups!} onReload={reload} />
<VMListWidget list={list!} onReload={reload} />
</VirtWebRouteContainer>
)}
/>
@ -70,37 +59,11 @@ 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>
@ -109,100 +72,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.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 />
)}
{p.list.map((row) => (
<TableRow
hover
key={row.name}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
onDoubleClick={() => navigate(row.ViewURL)}
>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell>{row.description ?? ""}</TableCell>
<TableCell>{filesize(row.memory * 1000 * 1000)}</TableCell>
<TableCell>
<VMStatusWidget vm={row} />
</TableCell>
<TableCell>
<Tooltip title="View this VM">
<RouterLink to={row.ViewURL}>
<IconButton>
<VisibilityIcon />
</IconButton>
{g ?? "default"}
</TableCell>
</TableRow>
)}
{!hiddenGroups.has(g) &&
p.list
.filter((row) => row.group === g)
.map((row) => (
<TableRow
hover
key={row.name}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
onDoubleClick={() => navigate(row.ViewURL)}
>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell>{row.description ?? ""}</TableCell>
<TableCell>{vmMemoryToHuman(row.memory)}</TableCell>
<TableCell>{row.number_vcpu}</TableCell>
<TableCell>
<VMStatusWidget
vm={row}
onChange={(s) => updateVMState(row, s)}
/>
</TableCell>
<TableCell>
<Tooltip title="View this VM">
<RouterLink to={row.ViewURL}>
<IconButton>
<VisibilityIcon />
</IconButton>
</RouterLink>
</Tooltip>
</TableCell>
</TableRow>
))}
</React.Fragment>
</RouterLink>
</Tooltip>
</TableCell>
</TableRow>
))}
</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);
}

View File

@ -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/Grid2";
import Grid from "@mui/material/Grid";
import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography";
import { Link, Outlet } from "react-router-dom";
@ -38,7 +38,10 @@ export function BaseLoginPage() {
<Grid container component="main" sx={{ height: "100vh" }}>
<CssBaseline />
<Grid
size={{ xs: false, sm: 4, md: 7 }}
item
xs={false}
sm={4}
md={7}
sx={{
backgroundImage: "url(/login_splash.jpg)",
backgroundRepeat: "no-repeat",
@ -50,12 +53,7 @@ export function BaseLoginPage() {
backgroundPosition: "center",
}}
/>
<Grid
size={{ xs: 12, sm: 8, md: 5 }}
component={Paper}
elevation={6}
square
>
<Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
<Box
sx={{
my: 8,

View File

@ -1,6 +1,5 @@
import { Paper, Typography } from "@mui/material";
import { Grid, Paper, Typography } from "@mui/material";
import React, { PropsWithChildren } from "react";
import Grid from "@mui/material/Grid2";
export function EditSection(
p: {
@ -10,7 +9,7 @@ export function EditSection(
} & PropsWithChildren
): React.ReactElement {
return (
<Grid size={{ sm: 12, md: p.fullWidth ? 12 : 6 }}>
<Grid item sm={12} md={p.fullWidth ? 12 : 6}>
<Paper style={{ margin: "10px", padding: "10px" }}>
{(p.title || p.actions) && (
<span

View File

@ -4,6 +4,7 @@ import DeleteIcon from "@mui/icons-material/Delete";
import {
Avatar,
Button,
Grid,
IconButton,
ListItem,
ListItemAvatar,
@ -18,7 +19,6 @@ 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} size={{ sm: 12, md: 6 }} style={{ padding: "10px" }}>
<Grid key={num} sm={12} md={6} item style={{ padding: "10px" }}>
<HostReservationWidget
key={num}
{...p}

View File

@ -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 size={{ sm: 12, md: 6 }} style={{ padding: "20px" }}>
<Grid item sm={12} md={6} style={{ padding: "20px" }}>
{p.label && (
<Typography variant="h6" style={{ marginBottom: "10px" }}>
{p.label}

View File

@ -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";

View File

@ -1,5 +1,4 @@
import { Button, Checkbox } from "@mui/material";
import Grid from "@mui/material/Grid2";
import { Button, Checkbox, Grid } from "@mui/material";
import React from "react";
import { useNavigate } from "react-router-dom";
import { IpConfig, NetworkApi, NetworkInfo } from "../../api/NetworksApi";

View File

@ -1,5 +1,4 @@
import { Button } from "@mui/material";
import Grid from "@mui/material/Grid2";
import { Button, Grid } from "@mui/material";
import React, { ReactElement } from "react";
import { useNavigate } from "react-router-dom";
import {
@ -7,7 +6,6 @@ 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";
@ -15,11 +13,12 @@ import { AsyncWidget } from "../AsyncWidget";
import { TabsWidget } from "../TabsWidget";
import { XMLAsyncWidget } from "../XMLWidget";
import { EditSection } from "../forms/EditSection";
import { NWFSelectReferencedFilters } from "../forms/NWFSelectReferencedFilters";
import { NWFilterPriorityInput } from "../forms/NWFilterPriorityInput";
import { NWFilterRules } from "../forms/NWFilterRules";
import { SelectInput } from "../forms/SelectInput";
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";
interface DetailsProps {
nwfilter: NWFilter;

View File

@ -1,5 +1,4 @@
import { Button } from "@mui/material";
import Grid from "@mui/material/Grid2";
import { Button, Grid } from "@mui/material";
import React from "react";
import { useNavigate } from "react-router-dom";
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";

View File

@ -1,11 +1,7 @@
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 { Button, Grid } from "@mui/material";
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";
@ -16,7 +12,6 @@ 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";
@ -26,6 +21,7 @@ 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;
@ -35,7 +31,6 @@ 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
@ -46,7 +41,6 @@ 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());
@ -60,7 +54,6 @@ 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}
@ -81,7 +74,6 @@ enum VMTab {
}
type DetailsInnerProps = DetailsProps & {
groupsList: string[];
isoList: IsoFile[];
vcpuCombinations: number[];
networksList: NetworkInfo[];
@ -124,8 +116,6 @@ function VMDetailsInner(p: DetailsInnerProps): React.ReactElement {
}
function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
const [addGroup, setAddGroup] = React.useState(false);
return (
<Grid container spacing={2}>
{
@ -184,50 +174,6 @@ 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 */}