feat: 添加虚拟机列表获取和选择功能

This commit is contained in:
xiaji
2026-04-14 07:30:02 +08:00
parent 0e08df76a4
commit 43e5746892
2 changed files with 140 additions and 11 deletions

View File

@@ -66,18 +66,31 @@ impl ProxmoxClient {
})?;
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
println!("[API] 响应状态: {}", status);
println!("[API] 响应内容: {}", body);
if !status.is_success() {
let body = resp.text().await.unwrap_or_default();
println!("[API] HTTP错误 {}: {}", status, body);
return Err(format!("HTTP {}: {}", status, body));
}
#[derive(Deserialize)]
struct NodeResponse {
data: Option<Vec<NodeInfo>>,
struct NodeItem {
#[serde(rename = "node")]
node: String,
}
let response: NodeResponse = resp.json().await.map_err(|e| e.to_string())?;
#[derive(Deserialize)]
struct NodeResponse {
data: Option<Vec<NodeItem>>,
}
let response: NodeResponse = serde_json::from_str(&body).map_err(|e| {
let err = format!("JSON解析失败: {}", e);
println!("[API] {}", err);
err
})?;
let nodes = response.data
.unwrap_or_default()
.into_iter()
@@ -87,6 +100,58 @@ impl ProxmoxClient {
Ok(nodes)
}
pub async fn get_vms(&self, node: &str) -> Result<Vec<(u32, String)>, String> {
let url = format!("{}/nodes/{}/qemu", self.base_url, node);
println!("[API] 获取虚拟机列表: GET {}", url);
let resp = self.client
.get(&url)
.header("Authorization", self.auth_header())
.send()
.await
.map_err(|e| {
let err = format!("网络错误: {}", e);
println!("[API] {}", err);
err
})?;
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
println!("[API] 响应状态: {}", status);
println!("[API] 响应内容: {}", body);
if !status.is_success() {
return Err(format!("HTTP {}: {}", status, body));
}
#[derive(Deserialize)]
struct VmItem {
#[serde(rename = "vmid")]
vmid: u32,
#[serde(rename = "name")]
name: Option<String>,
}
#[derive(Deserialize)]
struct VmResponse {
data: Option<Vec<VmItem>>,
}
let response: VmResponse = serde_json::from_str(&body).map_err(|e| {
let err = format!("JSON解析失败: {}", e);
println!("[API] {}", err);
err
})?;
let vms: Vec<(u32, String)> = response.data
.unwrap_or_default()
.into_iter()
.map(|v| (v.vmid, v.name.unwrap_or_else(|| format!("VM{}", v.vmid))))
.collect();
println!("[API] 虚拟机列表: {:?}", vms);
Ok(vms)
}
pub async fn get_cluster_resources(&self) -> Result<Vec<ClusterResources>, String> {
let url = format!("{}/cluster/resources", self.base_url);
println!("[API] 获取集群资源: GET {}", url);

View File

@@ -135,6 +135,12 @@ pub struct NodesData {
pub selected_index: usize,
}
#[derive(Default)]
pub struct VmListData {
pub vms: Vec<(u32, String)>,
pub selected_index: usize,
}
pub fn gui_run() {
let options = NativeOptions {
viewport: egui::ViewportBuilder::default()
@@ -169,6 +175,7 @@ struct App {
show_settings: bool,
settings: SettingsData,
nodes: NodesData,
vms: VmListData,
}
impl App {
@@ -186,6 +193,7 @@ impl App {
show_settings: false,
settings,
nodes: NodesData::default(),
vms: VmListData::default(),
}
}
}
@@ -220,11 +228,6 @@ impl eframe::App for App {
ui.colored_label(status_color, status_text);
});
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("VM ID:");
ui.add(egui::DragValue::new(&mut st.vm_id).range(100..=999).speed(1));
});
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("节点:");
if self.nodes.nodes.is_empty() {
@@ -238,9 +241,10 @@ impl eframe::App for App {
}
});
}
if ui.button("🔄").clicked() {
if ui.button("🔄节点").clicked() {
let client = st.client.clone();
let state_clone = state.clone();
let nodes_clone = Arc::new(RwLock::new(Vec::<String>::new()));
st.add_log("正在获取节点列表...");
ctx.request_repaint();
thread::spawn(move || {
@@ -250,6 +254,7 @@ impl eframe::App for App {
if let Some(c) = client.as_ref() {
match c.get_nodes().await {
Ok(nodes) => {
*nodes_clone.write().unwrap() = nodes.clone();
if !nodes.is_empty() {
state_clone.write().unwrap().add_log(&format!("找到节点: {:?}", nodes));
} else {
@@ -268,6 +273,65 @@ impl eframe::App for App {
}
});
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("虚拟机:");
if self.vms.vms.is_empty() {
ui.label("无虚拟机");
} else {
let vm_names: Vec<String> = self.vms.vms.iter().map(|(id, name)| format!("{} ({})", name, id)).collect();
egui::ComboBox::from_id_salt("vm_combo")
.selected_text(&vm_names[self.vms.selected_index.min(vm_names.len().saturating_sub(1))])
.show_ui(ui, |ui| {
for (i, (_, name)) in self.vms.vms.iter().enumerate() {
if ui.selectable_value(&mut self.vms.selected_index, i, format!("{} ({})", name, self.vms.vms[i].0)).clicked() {
st.vm_id = self.vms.vms[i].0;
}
}
});
}
if ui.button("🔄VM").clicked() {
if self.nodes.nodes.is_empty() {
st.add_log("请先获取节点列表");
} else {
let node = self.nodes.nodes[self.nodes.selected_index.min(self.nodes.nodes.len().saturating_sub(1))].clone();
let client = st.client.clone();
let state_clone = state.clone();
st.add_log(&format!("正在获取 {} 的虚拟机列表...", node));
ctx.request_repaint();
thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let client = client.lock().unwrap();
if let Some(c) = client.as_ref() {
match c.get_vms(&node).await {
Ok(vms) => {
if !vms.is_empty() {
state_clone.write().unwrap().add_log(&format!("找到 {} 台虚拟机", vms.len()));
for (id, name) in &vms {
state_clone.write().unwrap().add_log(&format!(" - {} ({})", name, id));
}
} else {
state_clone.write().unwrap().add_log("未找到虚拟机");
}
}
Err(e) => {
state_clone.write().unwrap().add_log(&format!("获取虚拟机失败: {}", e));
}
}
} else {
state_clone.write().unwrap().add_log("未连接到服务器");
}
});
});
}
}
});
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("VM ID:");
ui.add(egui::DragValue::new(&mut st.vm_id).range(100..=999).speed(1));
});
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("VM 状态:");
ui.label(&st.vm_status);