Compare commits
2 Commits
de33c7d521
...
6a7af7e6c4
Author | SHA1 | Date | |
---|---|---|---|
6a7af7e6c4 | |||
a8171375a8 |
@ -111,3 +111,6 @@ pub const API_TOKEN_RIGHT_PATH_MAX_LENGTH: usize = 255;
|
|||||||
|
|
||||||
/// Qemu image program path
|
/// Qemu image program path
|
||||||
pub const QEMU_IMAGE_PROGRAM: &str = "/usr/bin/qemu-img";
|
pub const QEMU_IMAGE_PROGRAM: &str = "/usr/bin/qemu-img";
|
||||||
|
|
||||||
|
/// IP program path
|
||||||
|
pub const IP_PROGRAM: &str = "/usr/sbin/ip";
|
||||||
|
@ -188,3 +188,7 @@ pub async fn number_vcpus() -> HttpResult {
|
|||||||
pub async fn networks_list() -> HttpResult {
|
pub async fn networks_list() -> HttpResult {
|
||||||
Ok(HttpResponse::Ok().json(net_utils::net_list()))
|
Ok(HttpResponse::Ok().json(net_utils::net_list()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn bridges_list() -> HttpResult {
|
||||||
|
Ok(HttpResponse::Ok().json(net_utils::bridges_list()?))
|
||||||
|
}
|
||||||
|
@ -48,6 +48,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
constants::QEMU_IMAGE_PROGRAM,
|
constants::QEMU_IMAGE_PROGRAM,
|
||||||
"QEMU disk image utility is required to manipulate QCow2 files!",
|
"QEMU disk image utility is required to manipulate QCow2 files!",
|
||||||
);
|
);
|
||||||
|
exec_utils::check_program(
|
||||||
|
constants::IP_PROGRAM,
|
||||||
|
"ip is required to access bridges information!",
|
||||||
|
);
|
||||||
|
|
||||||
log::debug!("Create required directory, if missing");
|
log::debug!("Create required directory, if missing");
|
||||||
files_utils::create_directory_if_missing(AppConfig::get().iso_storage_path()).unwrap();
|
files_utils::create_directory_if_missing(AppConfig::get().iso_storage_path()).unwrap();
|
||||||
@ -137,6 +141,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/server/networks",
|
"/api/server/networks",
|
||||||
web::get().to(server_controller::networks_list),
|
web::get().to(server_controller::networks_list),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/server/bridges",
|
||||||
|
web::get().to(server_controller::bridges_list),
|
||||||
|
)
|
||||||
// Auth controller
|
// Auth controller
|
||||||
.route(
|
.route(
|
||||||
"/api/auth/local",
|
"/api/auth/local",
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
use crate::constants;
|
||||||
use nix::sys::socket::{AddressFamily, SockaddrLike};
|
use nix::sys::socket::{AddressFamily, SockaddrLike};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||||
|
use std::process::Command;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use sysinfo::Networks;
|
use sysinfo::Networks;
|
||||||
|
|
||||||
@ -68,7 +70,7 @@ pub fn net_list() -> Vec<String> {
|
|||||||
|
|
||||||
/// Get the list of available network interfaces associated with their IP address
|
/// Get the list of available network interfaces associated with their IP address
|
||||||
pub fn net_list_and_ips() -> anyhow::Result<HashMap<String, Vec<IpAddr>>> {
|
pub fn net_list_and_ips() -> anyhow::Result<HashMap<String, Vec<IpAddr>>> {
|
||||||
let addrs = nix::ifaddrs::getifaddrs().unwrap();
|
let addrs = nix::ifaddrs::getifaddrs()?;
|
||||||
|
|
||||||
let mut res = HashMap::new();
|
let mut res = HashMap::new();
|
||||||
|
|
||||||
@ -136,6 +138,31 @@ pub fn net_list_and_ips() -> anyhow::Result<HashMap<String, Vec<IpAddr>>> {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct IPBridgeInfo {
|
||||||
|
ifname: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the list of bridge interfaces
|
||||||
|
pub fn bridges_list() -> anyhow::Result<Vec<String>> {
|
||||||
|
let mut cmd = Command::new(constants::IP_PROGRAM);
|
||||||
|
cmd.args(["-json", "link", "show", "type", "bridge"]);
|
||||||
|
let output = cmd.output()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"{} failed, status: {}, stderr: {}",
|
||||||
|
constants::IP_PROGRAM,
|
||||||
|
output.status,
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON result
|
||||||
|
let res: Vec<IPBridgeInfo> = serde_json::from_str(&String::from_utf8_lossy(&output.stdout))?;
|
||||||
|
|
||||||
|
Ok(res.iter().map(|ip| ip.ifname.clone()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::utils::net_utils::{
|
use crate::utils::net_utils::{
|
||||||
|
@ -38,6 +38,9 @@ sudo netplan apply
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo brctl show
|
sudo brctl show
|
||||||
|
|
||||||
|
# Or
|
||||||
|
ip link show type bridge
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
@ -217,4 +217,16 @@ export class ServerApi {
|
|||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get host networks bridges list
|
||||||
|
*/
|
||||||
|
static async GetNetworksBridgesList(): Promise<string[]> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri: "/server/bridges",
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,11 @@ export interface VMNetInterfaceFilter {
|
|||||||
parameters: VMNetInterfaceFilterParams[];
|
parameters: VMNetInterfaceFilterParams[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VMNetInterface = (VMNetUserspaceSLIRPStack | VMNetDefinedNetwork) &
|
export type VMNetInterface = (
|
||||||
|
| VMNetUserspaceSLIRPStack
|
||||||
|
| VMNetDefinedNetwork
|
||||||
|
| VMNetBridge
|
||||||
|
) &
|
||||||
VMNetInterfaceBase;
|
VMNetInterfaceBase;
|
||||||
|
|
||||||
export interface VMNetInterfaceBase {
|
export interface VMNetInterfaceBase {
|
||||||
@ -67,6 +71,11 @@ export interface VMNetDefinedNetwork {
|
|||||||
network: string;
|
network: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VMNetBridge {
|
||||||
|
type: "Bridge";
|
||||||
|
bridge: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface VMInfoInterface {
|
interface VMInfoInterface {
|
||||||
name: string;
|
name: string;
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
|
@ -29,6 +29,7 @@ export function VMNetworksList(p: {
|
|||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
networksList: NetworkInfo[];
|
networksList: NetworkInfo[];
|
||||||
|
bridgesList: string[];
|
||||||
networkFiltersList: NWFilter[];
|
networkFiltersList: NWFilter[];
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const addNew = () => {
|
const addNew = () => {
|
||||||
@ -72,6 +73,7 @@ function NetworkInfoWidget(p: {
|
|||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
removeFromList: () => void;
|
removeFromList: () => void;
|
||||||
networksList: NetworkInfo[];
|
networksList: NetworkInfo[];
|
||||||
|
bridgesList: string[];
|
||||||
networkFiltersList: NWFilter[];
|
networkFiltersList: NWFilter[];
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
@ -130,6 +132,11 @@ function NetworkInfoWidget(p: {
|
|||||||
value: "DefinedNetwork",
|
value: "DefinedNetwork",
|
||||||
description: "Attach to a defined network",
|
description: "Attach to a defined network",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Host bridge",
|
||||||
|
value: "Bridge",
|
||||||
|
description: "Attach to an host's bridge",
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -149,31 +156,53 @@ function NetworkInfoWidget(p: {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Defined network selection */}
|
||||||
{p.network.type === "DefinedNetwork" && (
|
{p.network.type === "DefinedNetwork" && (
|
||||||
|
<SelectInput
|
||||||
|
editable={p.editable}
|
||||||
|
label="Defined network"
|
||||||
|
options={p.networksList.map((n) => {
|
||||||
|
const chars = [n.forward_mode.toString()];
|
||||||
|
if (n.ip_v4) chars.push("IPv4");
|
||||||
|
if (n.ip_v6) chars.push("IPv6");
|
||||||
|
if (n.description) chars.push(n.description);
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: n.name,
|
||||||
|
value: n.name,
|
||||||
|
description: chars.join(" - "),
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
value={p.network.network}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (p.network.type === "DefinedNetwork")
|
||||||
|
p.network.network = v as any;
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bridge selection */}
|
||||||
|
{p.network.type === "Bridge" && (
|
||||||
|
<SelectInput
|
||||||
|
editable={p.editable}
|
||||||
|
label="Host bridge"
|
||||||
|
options={p.bridgesList.map((n) => {
|
||||||
|
return {
|
||||||
|
label: n,
|
||||||
|
value: n,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
value={p.network.bridge}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (p.network.type === "Bridge") p.network.bridge = v as any;
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{p.network.type !== "UserspaceSLIRPStack" && (
|
||||||
<>
|
<>
|
||||||
<SelectInput
|
|
||||||
editable={p.editable}
|
|
||||||
label="Defined network"
|
|
||||||
options={p.networksList.map((n) => {
|
|
||||||
const chars = [n.forward_mode.toString()];
|
|
||||||
if (n.ip_v4) chars.push("IPv4");
|
|
||||||
if (n.ip_v6) chars.push("IPv6");
|
|
||||||
if (n.description) chars.push(n.description);
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: n.name,
|
|
||||||
value: n.name,
|
|
||||||
description: chars.join(" - "),
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
value={p.network.network}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
if (p.network.type === "DefinedNetwork")
|
|
||||||
p.network.network = v as any;
|
|
||||||
p.onChange?.();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Network Filter */}
|
{/* Network Filter */}
|
||||||
<NWFilterSelectInput
|
<NWFilterSelectInput
|
||||||
editable={p.editable}
|
editable={p.editable}
|
||||||
|
@ -725,6 +725,11 @@ export function TokenRightsEditor(p: {
|
|||||||
right={{ verb: "GET", path: "/api/server/networks" }}
|
right={{ verb: "GET", path: "/api/server/networks" }}
|
||||||
label="Get list of network cards"
|
label="Get list of network cards"
|
||||||
/>
|
/>
|
||||||
|
<RouteRight
|
||||||
|
{...p}
|
||||||
|
right={{ verb: "GET", path: "/api/server/bridges" }}
|
||||||
|
label="Get list of network bridges"
|
||||||
|
/>
|
||||||
</RightsSection>
|
</RightsSection>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -38,6 +38,7 @@ interface DetailsProps {
|
|||||||
export function VMDetails(p: DetailsProps): React.ReactElement {
|
export function VMDetails(p: DetailsProps): React.ReactElement {
|
||||||
const [groupsList, setGroupsList] = React.useState<string[] | undefined>();
|
const [groupsList, setGroupsList] = React.useState<string[] | undefined>();
|
||||||
const [isoList, setIsoList] = React.useState<IsoFile[] | undefined>();
|
const [isoList, setIsoList] = React.useState<IsoFile[] | undefined>();
|
||||||
|
const [bridgesList, setBridgesList] = React.useState<string[] | undefined>();
|
||||||
const [vcpuCombinations, setVCPUCombinations] = React.useState<
|
const [vcpuCombinations, setVCPUCombinations] = React.useState<
|
||||||
number[] | undefined
|
number[] | undefined
|
||||||
>();
|
>();
|
||||||
@ -51,6 +52,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
|
|||||||
const load = async () => {
|
const load = async () => {
|
||||||
setGroupsList(await GroupApi.GetList());
|
setGroupsList(await GroupApi.GetList());
|
||||||
setIsoList(await IsoFilesApi.GetList());
|
setIsoList(await IsoFilesApi.GetList());
|
||||||
|
setBridgesList(await ServerApi.GetNetworksBridgesList());
|
||||||
setVCPUCombinations(await ServerApi.NumberVCPUs());
|
setVCPUCombinations(await ServerApi.NumberVCPUs());
|
||||||
setNetworksList(await NetworkApi.GetList());
|
setNetworksList(await NetworkApi.GetList());
|
||||||
setNetworkFiltersList(await NWFilterApi.GetList());
|
setNetworkFiltersList(await NWFilterApi.GetList());
|
||||||
@ -65,6 +67,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
|
|||||||
<VMDetailsInner
|
<VMDetailsInner
|
||||||
groupsList={groupsList!}
|
groupsList={groupsList!}
|
||||||
isoList={isoList!}
|
isoList={isoList!}
|
||||||
|
bridgesList={bridgesList!}
|
||||||
vcpuCombinations={vcpuCombinations!}
|
vcpuCombinations={vcpuCombinations!}
|
||||||
networksList={networksList!}
|
networksList={networksList!}
|
||||||
networkFiltersList={networkFiltersList!}
|
networkFiltersList={networkFiltersList!}
|
||||||
@ -87,6 +90,7 @@ enum VMTab {
|
|||||||
type DetailsInnerProps = DetailsProps & {
|
type DetailsInnerProps = DetailsProps & {
|
||||||
groupsList: string[];
|
groupsList: string[];
|
||||||
isoList: IsoFile[];
|
isoList: IsoFile[];
|
||||||
|
bridgesList: string[];
|
||||||
vcpuCombinations: number[];
|
vcpuCombinations: number[];
|
||||||
networksList: NetworkInfo[];
|
networksList: NetworkInfo[];
|
||||||
networkFiltersList: NWFilter[];
|
networkFiltersList: NWFilter[];
|
||||||
|
Reference in New Issue
Block a user