Skip to content

Commit 5989e51

Browse files
MalteJclaude
andcommitted
Add UI-compatible REST API with Projects, Volumes, Templates
Implement REST endpoints compatible with mvirt-ui mock-server: - Add Project, Volume, Template, ImportJob types and commands - Create UI DTOs with camelCase serialization (ui_types.rs) - Add UI-compatible handlers (ui_handlers.rs) for all resources - Implement VM start/stop/kill with simulated state transitions - Add data locality support (volumes/templates bound to nodes) - Update tests to use camelCase JSON format - Add comprehensive unit and integration tests (97 total) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0fc9997 commit 5989e51

15 files changed

Lines changed: 3538 additions & 235 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mvirt-api/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ utoipa = { version = "5", features = ["axum_extras"] }
2222
utoipa-swagger-ui = { version = "9", features = ["axum"] }
2323

2424
# Async
25-
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "sync", "fs"] }
25+
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "sync", "fs", "time"] }
26+
futures = "0.3"
27+
async-stream = "0.3"
2628

2729
# Serialization
2830
serde = { version = "1", features = ["derive"] }

mvirt-api/src/audit.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,89 @@ impl ApiAuditLogger {
153153
vec![vm_id.to_string()],
154154
);
155155
}
156+
157+
pub fn vm_started(&self, vm_id: &str) {
158+
self.log_async(
159+
LogLevel::Audit,
160+
format!("VM started: {}", vm_id),
161+
vec![vm_id.to_string()],
162+
);
163+
}
164+
165+
pub fn vm_stopped(&self, vm_id: &str) {
166+
self.log_async(
167+
LogLevel::Audit,
168+
format!("VM stopped: {}", vm_id),
169+
vec![vm_id.to_string()],
170+
);
171+
}
172+
173+
pub fn vm_killed(&self, vm_id: &str) {
174+
self.log_async(
175+
LogLevel::Audit,
176+
format!("VM killed: {}", vm_id),
177+
vec![vm_id.to_string()],
178+
);
179+
}
180+
181+
// Project events
182+
pub fn project_created(&self, project_id: &str, project_name: &str) {
183+
self.log_async(
184+
LogLevel::Audit,
185+
format!("Project created: {} ({})", project_name, project_id),
186+
vec![project_id.to_string()],
187+
);
188+
}
189+
190+
pub fn project_deleted(&self, project_id: &str) {
191+
self.log_async(
192+
LogLevel::Audit,
193+
format!("Project deleted: {}", project_id),
194+
vec![project_id.to_string()],
195+
);
196+
}
197+
198+
// Volume events
199+
pub fn volume_created(&self, volume_id: &str, volume_name: &str) {
200+
self.log_async(
201+
LogLevel::Audit,
202+
format!("Volume created: {} ({})", volume_name, volume_id),
203+
vec![volume_id.to_string()],
204+
);
205+
}
206+
207+
pub fn volume_deleted(&self, volume_id: &str) {
208+
self.log_async(
209+
LogLevel::Audit,
210+
format!("Volume deleted: {}", volume_id),
211+
vec![volume_id.to_string()],
212+
);
213+
}
214+
215+
pub fn volume_resized(&self, volume_id: &str, new_size: u64) {
216+
self.log_async(
217+
LogLevel::Audit,
218+
format!("Volume resized: {} to {} bytes", volume_id, new_size),
219+
vec![volume_id.to_string()],
220+
);
221+
}
222+
223+
pub fn snapshot_created(&self, volume_id: &str) {
224+
self.log_async(
225+
LogLevel::Audit,
226+
format!("Snapshot created on volume: {}", volume_id),
227+
vec![volume_id.to_string()],
228+
);
229+
}
230+
231+
// Template events
232+
pub fn template_import_started(&self, job_id: &str) {
233+
self.log_async(
234+
LogLevel::Audit,
235+
format!("Template import started: {}", job_id),
236+
vec![job_id.to_string()],
237+
);
238+
}
156239
}
157240

158241
pub fn create_audit_logger(log_endpoint: &str) -> Arc<ApiAuditLogger> {

mvirt-api/src/command.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,77 @@ pub enum Command {
111111
request_id: String,
112112
id: String,
113113
},
114+
115+
// Project operations
116+
CreateProject {
117+
request_id: String,
118+
id: String,
119+
timestamp: String,
120+
name: String,
121+
description: Option<String>,
122+
},
123+
DeleteProject {
124+
request_id: String,
125+
id: String,
126+
},
127+
128+
// Volume operations (node_id for data locality - Shared Nothing architecture)
129+
CreateVolume {
130+
request_id: String,
131+
id: String,
132+
timestamp: String,
133+
project_id: String,
134+
node_id: String,
135+
name: String,
136+
size_bytes: u64,
137+
template_id: Option<String>,
138+
},
139+
DeleteVolume {
140+
request_id: String,
141+
id: String,
142+
},
143+
ResizeVolume {
144+
request_id: String,
145+
id: String,
146+
timestamp: String,
147+
size_bytes: u64,
148+
},
149+
CreateSnapshot {
150+
request_id: String,
151+
id: String,
152+
timestamp: String,
153+
volume_id: String,
154+
name: String,
155+
},
156+
157+
// Template operations (node_id for locality)
158+
CreateTemplate {
159+
request_id: String,
160+
id: String,
161+
timestamp: String,
162+
node_id: String,
163+
name: String,
164+
size_bytes: u64,
165+
},
166+
167+
// Import job operations
168+
CreateImportJob {
169+
request_id: String,
170+
id: String,
171+
timestamp: String,
172+
node_id: String,
173+
template_name: String,
174+
url: String,
175+
total_bytes: u64,
176+
},
177+
UpdateImportJob {
178+
request_id: String,
179+
id: String,
180+
timestamp: String,
181+
bytes_written: u64,
182+
state: ImportJobState,
183+
error: Option<String>,
184+
},
114185
}
115186

116187
impl Command {
@@ -129,6 +200,15 @@ impl Command {
129200
Command::UpdateVmSpec { request_id, .. } => request_id,
130201
Command::UpdateVmStatus { request_id, .. } => request_id,
131202
Command::DeleteVm { request_id, .. } => request_id,
203+
Command::CreateProject { request_id, .. } => request_id,
204+
Command::DeleteProject { request_id, .. } => request_id,
205+
Command::CreateVolume { request_id, .. } => request_id,
206+
Command::DeleteVolume { request_id, .. } => request_id,
207+
Command::ResizeVolume { request_id, .. } => request_id,
208+
Command::CreateSnapshot { request_id, .. } => request_id,
209+
Command::CreateTemplate { request_id, .. } => request_id,
210+
Command::CreateImportJob { request_id, .. } => request_id,
211+
Command::UpdateImportJob { request_id, .. } => request_id,
132212
}
133213
}
134214
}
@@ -237,16 +317,25 @@ pub struct VmData {
237317
#[derive(Debug, Clone, Serialize, Deserialize)]
238318
pub struct VmSpec {
239319
pub name: String,
320+
pub project_id: Option<String>, // Project this VM belongs to
240321
pub node_selector: Option<String>, // Optional: require specific node
241322
pub cpu_cores: u32,
242323
pub memory_mb: u64,
243324
pub disk_gb: u64,
244325
pub network_id: String,
245326
pub nic_id: Option<String>, // Will be auto-created if not provided
246327
pub image: String, // Boot image reference
328+
pub disks: Vec<DiskConfig>, // Additional disk volumes
247329
pub desired_state: VmDesiredState,
248330
}
249331

332+
/// Disk configuration for a VM
333+
#[derive(Debug, Clone, Serialize, Deserialize)]
334+
pub struct DiskConfig {
335+
pub volume_id: String,
336+
pub readonly: bool,
337+
}
338+
250339
/// Desired power state for a VM
251340
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
252341
pub enum VmDesiredState {
@@ -306,13 +395,100 @@ pub enum ResourcePhase {
306395
Failed,
307396
}
308397

398+
// =============================================================================
399+
// Project Types
400+
// =============================================================================
401+
402+
/// Project data stored in the state machine
403+
#[derive(Debug, Clone, Serialize, Deserialize)]
404+
pub struct ProjectData {
405+
pub id: String,
406+
pub name: String,
407+
pub description: Option<String>,
408+
pub created_at: String,
409+
}
410+
411+
// =============================================================================
412+
// Storage Types (Volumes, Templates, Import Jobs)
413+
// =============================================================================
414+
415+
/// Volume data stored in the state machine (bound to a specific node)
416+
#[derive(Debug, Clone, Serialize, Deserialize)]
417+
pub struct VolumeData {
418+
pub id: String,
419+
pub project_id: String,
420+
pub node_id: String, // Node where the volume is stored (Shared Nothing)
421+
pub name: String,
422+
pub path: String, // ZFS path e.g., /dev/zvol/pool/vol-xxx
423+
pub size_bytes: u64,
424+
pub used_bytes: u64,
425+
pub compression_ratio: f64,
426+
pub snapshots: Vec<SnapshotData>,
427+
pub template_id: Option<String>,
428+
pub created_at: String,
429+
pub updated_at: String,
430+
}
431+
432+
/// Snapshot data stored inline in VolumeData
433+
#[derive(Debug, Clone, Serialize, Deserialize)]
434+
pub struct SnapshotData {
435+
pub id: String,
436+
pub name: String,
437+
pub created_at: String,
438+
pub used_bytes: u64,
439+
}
440+
441+
/// Template data stored in the state machine (bound to a specific node)
442+
#[derive(Debug, Clone, Serialize, Deserialize)]
443+
pub struct TemplateData {
444+
pub id: String,
445+
pub node_id: String, // Node where the template is stored
446+
pub name: String,
447+
pub size_bytes: u64,
448+
pub clone_count: u32,
449+
pub created_at: String,
450+
}
451+
452+
/// Import job data stored in the state machine
453+
#[derive(Debug, Clone, Serialize, Deserialize)]
454+
pub struct ImportJobData {
455+
pub id: String,
456+
pub node_id: String,
457+
pub template_name: String,
458+
pub url: String,
459+
pub state: ImportJobState,
460+
pub bytes_written: u64,
461+
pub total_bytes: u64,
462+
pub error: Option<String>,
463+
pub created_at: String,
464+
pub updated_at: String,
465+
}
466+
467+
/// Import job state
468+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
469+
pub enum ImportJobState {
470+
#[default]
471+
Pending,
472+
Running,
473+
Completed,
474+
Failed,
475+
}
476+
477+
// =============================================================================
478+
// Response Types
479+
// =============================================================================
480+
309481
/// Response from applying a command
310482
#[derive(Debug, Clone, Serialize, Deserialize)]
311483
pub enum Response {
312484
Node(NodeData),
313485
Network(NetworkData),
314486
Nic(NicData),
315487
Vm(VmData),
488+
Project(ProjectData),
489+
Volume(VolumeData),
490+
Template(TemplateData),
491+
ImportJob(ImportJobData),
316492
Deleted { id: String },
317493
DeletedWithCount { id: String, nics_deleted: u32 },
318494
Error { code: u32, message: String },

mvirt-api/src/rest/handlers.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Allow dead code for legacy handlers during transition to UI-compatible API
2+
#![allow(dead_code)]
3+
14
use axum::{
25
Json,
36
extract::{Path, Query, State},
@@ -1174,13 +1177,15 @@ pub async fn create_vm(
11741177

11751178
let spec = VmSpec {
11761179
name: req.name.clone(),
1180+
project_id: None,
11771181
node_selector: req.node_selector,
11781182
cpu_cores: req.cpu_cores,
11791183
memory_mb: req.memory_mb,
11801184
disk_gb: req.disk_gb,
11811185
network_id: req.network_id,
11821186
nic_id: req.nic_id,
11831187
image: req.image,
1188+
disks: vec![],
11841189
desired_state,
11851190
};
11861191

mvirt-api/src/rest/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mod handlers;
22
mod routes;
3+
pub mod ui_handlers;
4+
pub mod ui_types;
35

46
pub use handlers::AppState;
57
pub use routes::create_router;

0 commit comments

Comments
 (0)