1use crate::api_objects::{
3 Error, MetaData, Node, Resources, Result, SclName, SclObject, SeparationContext,
4 TransitionError, Url, Volume,
5};
6use crate::LogError;
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9
10#[cfg(feature = "openapi")]
11use aide::OperationIo;
12#[cfg(feature = "openapi")]
13use schemars::JsonSchema;
14
15#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
16#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
17#[cfg_attr(feature = "openapi", aide(output))]
18#[serde(rename_all = "camelCase")]
19pub enum TargetVmStatus {
20 #[default]
21 Running,
22 Paused,
23 Stopped,
24}
25
26impl FromStr for TargetVmStatus {
27 type Err = String;
28
29 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
30 use TargetVmStatus::*;
31 match s {
32 "running" => Ok(Running),
33 "paused" => Ok(Paused),
34 "stopped" => Ok(Stopped),
35 _ => Err("Valid variants are: running, paused, stopped.".to_string()),
36 }
37 }
38}
39
40#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
42#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
43#[cfg_attr(feature = "openapi", aide(output))]
44#[serde(rename_all = "camelCase")]
45pub struct VirtualMachine {
46 #[serde(flatten)]
47 pub metadata: MetaData,
48 pub separation_context: SclName,
49 pub spec: VmSpec,
50 #[serde(default)]
53 pub status: VmStatus,
54}
55
56use std::cmp::Ordering;
57
58impl Ord for VirtualMachine {
59 fn cmp(&self, other: &Self) -> Ordering {
60 (&self.separation_context, &self.metadata.name)
61 .cmp(&(&other.separation_context, &other.metadata.name))
62 }
63}
64
65impl PartialOrd for VirtualMachine {
66 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
67 Some(self.cmp(other))
68 }
69}
70
71impl SclObject for VirtualMachine {
72 const PREFIX: &'static str = "/vms";
74
75 fn name(&self) -> &SclName {
76 &self.metadata.name
77 }
78
79 fn separation_context(&self) -> Option<&SclName> {
80 Some(&self.separation_context)
81 }
82
83 fn metadata(&self) -> &MetaData {
84 &self.metadata
85 }
86
87 fn metadata_mut(&mut self) -> &mut MetaData {
88 &mut self.metadata
89 }
90
91 fn referenced_db_keys(&self) -> Vec<String> {
92 use VmStatus::*;
93
94 let mut references = vec![SeparationContext::db_key(
95 self.separation_context.as_str(),
96 None,
97 )];
98
99 if let BootVolume::Volume(ref vol) = self.spec.boot_volume {
100 references.push(Volume::db_key(
101 self.separation_context.as_str(),
102 vol.as_str(),
103 ))
104 }
105
106 match &self.status {
107 NotAssigned(_) => references,
108 Prepared(c) | Scheduled(c) | Running(c) | Paused(c) | Stopped(c) | Error(c) => {
109 let node_key = Node::db_key(None, c.assigned_node.as_str());
110 references.push(node_key);
111 references
112 }
113 }
114 }
115
116 fn validate_fields_before_create(&self) -> Result<()> {
117 self.metadata.validate_fields_before_create()?;
118
119 if self.status != VmStatus::default() {
120 return Err(Error::IllegalInitialValue(
121 "Invalid initial VM status!".to_string(),
122 ));
123 }
124
125 if self.spec.target_state != TargetVmStatus::default() {
126 return Err(Error::IllegalInitialValue(
127 "Invalid initial VM target state!".to_string(),
128 ));
129 }
130
131 if !self.spec.resources.all_resources_are_greater_than_zero() {
132 return Err(Error::IllegalInitialValue(
133 "All resources must be greater than 0!".to_string(),
134 ));
135 }
136
137 Ok(())
138 }
139
140 fn validate_fields_before_update(
141 current_db_state: &Self,
142 proposed_new_state: &Self,
143 ) -> Result<()> {
144 MetaData::validate_fields_before_regular_update(
145 ¤t_db_state.metadata,
146 &proposed_new_state.metadata,
147 )?;
148
149 if current_db_state.separation_context != proposed_new_state.separation_context
150 || current_db_state.spec.resources != proposed_new_state.spec.resources
151 || current_db_state.spec.boot_volume != proposed_new_state.spec.boot_volume
152 || current_db_state.spec.network_device_name
153 != proposed_new_state.spec.network_device_name
154 || current_db_state.spec.cloud_init_config != proposed_new_state.spec.cloud_init_config
155 {
156 return Err(TransitionError::Other(
157 "Only vm.status and vm.spec.target_state fields may be updated!".to_string(),
158 )
159 .into());
160 }
161
162 if let (VmStatus::NotAssigned(_), VmStatus::Scheduled(cond)) =
163 (¤t_db_state.status, &proposed_new_state.status)
164 {
165 if current_db_state.spec.resources != cond.reserved_resources {
166 return Err(TransitionError::Other(
167 "Reserved resources must match specified resources".to_string(),
168 )
169 .into());
170 }
171
172 if !cond
173 .reserved_resources
174 .all_resources_are_greater_than_zero()
175 {
176 return Err(Error::Application);
179 }
180 }
181
182 validate_status_transition(¤t_db_state.status, &proposed_new_state.status).log_err()
183 }
184}
185
186#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
187#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
188#[cfg_attr(feature = "openapi", aide(output))]
189#[serde(rename_all = "camelCase")]
190pub enum VmStatus {
191 NotAssigned(NotAssignedStatus),
192 Prepared(VmCondition),
193 Scheduled(VmCondition),
194 Running(VmCondition),
195 Paused(VmCondition),
196 Stopped(VmCondition),
197 Error(VmCondition),
198}
199
200impl VmStatus {
201 pub fn condition(&self) -> Option<&VmCondition> {
202 use VmStatus::*;
203 match self {
204 Scheduled(c) | Prepared(c) | Running(c) | Paused(c) | Stopped(c) | Error(c) => Some(c),
205 NotAssigned(_) => None,
206 }
207 }
208
209 pub fn condition_mut(&mut self) -> Option<&mut VmCondition> {
210 use VmStatus::*;
211 match self {
212 Scheduled(c) | Prepared(c) | Running(c) | Paused(c) | Stopped(c) | Error(c) => Some(c),
213 NotAssigned(_) => None,
214 }
215 }
216}
217
218#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
219#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
220#[cfg_attr(feature = "openapi", aide(output))]
221#[serde(rename_all = "camelCase")]
222pub enum NotAssignedStatus {
223 Pending,
224 InsufficientResources,
225}
226
227#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
228#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
229#[cfg_attr(feature = "openapi", aide(output))]
230#[serde(rename_all = "camelCase")]
231pub struct VmCondition {
232 pub reserved_resources: Resources,
233 pub assigned_node: SclName,
234 pub transition_info: Option<TransitionInfo>,
235}
236
237#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
238#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
239#[cfg_attr(feature = "openapi", aide(output))]
240#[serde(rename_all = "camelCase")]
241pub struct TransitionInfo {
242 pub transition_time: u64, pub previous_vm_state: String, pub error_description: String, }
246
247impl Default for VmStatus {
248 fn default() -> Self {
249 VmStatus::NotAssigned(NotAssignedStatus::Pending)
250 }
251}
252
253#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
254#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
255#[cfg_attr(feature = "openapi", aide(output))]
256#[serde(rename_all = "camelCase")]
257pub struct LocalStorage {
258 #[cfg_attr(feature = "openapi", validate(range(min = 1)))]
260 #[serde(rename = "sizeMiB")]
261 pub size_mib: u64,
262 pub image: Url,
265}
266
267#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
268#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
269#[cfg_attr(feature = "openapi", aide(output))]
270#[serde(rename_all = "camelCase")]
271pub enum BootVolume {
272 Volume(SclName),
273 Local(LocalStorage),
274}
275
276#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
277#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
278#[cfg_attr(feature = "openapi", aide(output))]
279#[serde(rename_all = "camelCase")]
280pub struct VmSpec {
281 pub resources: Resources,
282 #[serde(default)]
283 pub target_state: TargetVmStatus,
284 pub boot_volume: BootVolume,
285 pub network_device_name: String,
286 pub cloud_init_config: Option<serde_json::Value>,
287}
288
289fn validate_status_transition(old_state: &VmStatus, new_state: &VmStatus) -> Result<()> {
292 use NotAssignedStatus::*;
293 use VmStatus::*;
294 match (old_state, new_state) {
295 (NotAssigned(_), Scheduled(_))
296 | (NotAssigned(Pending), NotAssigned(InsufficientResources))
297 | (Scheduled(_), NotAssigned(Pending))
298 | (Stopped(_), NotAssigned(Pending)) => Ok(()),
299
300 (Scheduled(a), Prepared(b))
302 | (Scheduled(a), Scheduled(b))
303 | (Prepared(a), Running(b))
304 | (Prepared(a), Prepared(b))
305 | (Running(a), Paused(b))
306 | (Running(a), Stopped(b))
307 | (Running(a), Running(b))
308 | (Paused(a), Stopped(b))
309 | (Paused(a), Running(b))
310 | (Paused(a), Paused(b))
311 | (Stopped(a), Running(b))
312 | (Stopped(a), Stopped(b))
313 if a.assigned_node == b.assigned_node
314 && a.reserved_resources == b.reserved_resources =>
315 {
316 Ok(())
317 }
318 (_, Error(_)) => Ok(()),
319 (_, _) => Err(TransitionError::Other(format!(
320 "Invalid transition: Old({:?}) / New({:?})",
321 old_state, new_state
322 ))
323 .into()),
324 }
325}
326
327#[cfg(test)]
328mod test {
329 use super::{TargetVmStatus, VirtualMachine, VmSpec};
330 use crate::api_objects::test::{
331 detect_invalid_create_mutations, detect_invalid_metadata_create_mutations,
332 detect_invalid_metadata_update_mutations, detect_invalid_update_mutations, parse_json_dir,
333 };
334 use crate::api_objects::virtual_machine::validate_status_transition;
335 use crate::api_objects::{
336 BootVolume, MetaData, Node, NotAssignedStatus, Resources, SclName, SclObject,
337 SeparationContext, VmCondition, VmStatus, Volume,
338 };
339 use std::path::Path;
340
341 #[test]
342 fn parse_sample_json() {
343 parse_json_dir::<VirtualMachine>(Path::new("../test/sample_json/vms"));
344 }
345
346 fn example_vm() -> VirtualMachine {
347 VirtualMachine {
348 metadata: MetaData::new(SclName::try_from("example").unwrap()),
349 separation_context: SclName::try_from("sc-123").unwrap(),
350 spec: VmSpec {
351 resources: Resources {
352 vcpu: 10,
353 ram_mib: 5000,
354 },
355 boot_volume: BootVolume::Volume(SclName::try_from("vol-333").unwrap()),
356 target_state: TargetVmStatus::default(),
357 network_device_name: "tapvm".to_string(),
358 cloud_init_config: None,
359 },
360 status: Default::default(),
361 }
362 }
363
364 fn example_condition() -> VmCondition {
365 VmCondition {
366 reserved_resources: Resources {
367 vcpu: 1,
368 ram_mib: 1,
369 },
370 assigned_node: SclName::try_from("some-node").unwrap(),
371 transition_info: None,
372 }
373 }
374
375 #[test]
376 fn metadata_references() {
377 use std::convert::TryFrom;
378 let mut vm = example_vm();
379 let mut expected_refs = vec![
380 SeparationContext::db_key(None, "sc-123"),
381 Volume::db_key("sc-123", "vol-333"),
382 ];
383 assert_eq!(vm.referenced_db_keys(), expected_refs);
384
385 vm.status = VmStatus::Scheduled(VmCondition {
386 reserved_resources: Default::default(),
387 assigned_node: SclName::try_from("node-456").unwrap(),
388 transition_info: None,
389 });
390
391 expected_refs.push(Node::db_key(None, "node-456"));
392 assert_eq!(vm.referenced_db_keys(), expected_refs);
393 }
394
395 #[test]
397 fn test_validate_status_transition() {
398 use NotAssignedStatus::{InsufficientResources as IR, Pending as P}; use VmStatus::*;
400
401 let cond = example_condition();
402 let inputs: Vec<(VmStatus, VmStatus, bool)> = vec![
403 (NotAssigned(P), NotAssigned(P), false),
404 (NotAssigned(P), NotAssigned(IR), true),
405 (NotAssigned(P), Scheduled(cond.clone()), true),
406 (NotAssigned(P), Running(cond.clone()), false),
407 (NotAssigned(P), Paused(cond.clone()), false),
408 (NotAssigned(P), Stopped(cond.clone()), false),
409 (NotAssigned(IR), NotAssigned(P), false),
410 (NotAssigned(IR), NotAssigned(IR), false),
411 (NotAssigned(IR), Scheduled(cond.clone()), true),
412 (NotAssigned(IR), Running(cond.clone()), false),
413 (NotAssigned(IR), Paused(cond.clone()), false),
414 (NotAssigned(IR), Stopped(cond.clone()), false),
415 (Scheduled(cond.clone()), NotAssigned(P), true),
416 (Scheduled(cond.clone()), NotAssigned(IR), false),
417 (Scheduled(cond.clone()), Scheduled(cond.clone()), true),
418 (Scheduled(cond.clone()), Running(cond.clone()), false),
419 (Scheduled(cond.clone()), Paused(cond.clone()), false),
420 (Scheduled(cond.clone()), Stopped(cond.clone()), false),
421 (Scheduled(cond.clone()), Prepared(cond.clone()), true),
422 (Running(cond.clone()), NotAssigned(P), false),
423 (Running(cond.clone()), NotAssigned(IR), false),
424 (Running(cond.clone()), Scheduled(cond.clone()), false),
425 (Running(cond.clone()), Running(cond.clone()), true),
426 (Running(cond.clone()), Paused(cond.clone()), true),
427 (Running(cond.clone()), Stopped(cond.clone()), true),
428 (Running(cond.clone()), Prepared(cond.clone()), false),
429 (Paused(cond.clone()), NotAssigned(P), false),
430 (Paused(cond.clone()), NotAssigned(IR), false),
431 (Paused(cond.clone()), Scheduled(cond.clone()), false),
432 (Paused(cond.clone()), Running(cond.clone()), true),
433 (Paused(cond.clone()), Paused(cond.clone()), true),
434 (Paused(cond.clone()), Stopped(cond.clone()), true),
435 (Paused(cond.clone()), Prepared(cond.clone()), false),
436 (Stopped(cond.clone()), NotAssigned(P), true),
437 (Stopped(cond.clone()), NotAssigned(IR), false),
438 (Stopped(cond.clone()), Scheduled(cond.clone()), false),
439 (Stopped(cond.clone()), Running(cond.clone()), true),
440 (Stopped(cond.clone()), Paused(cond.clone()), false),
441 (Stopped(cond.clone()), Stopped(cond.clone()), true),
442 (Stopped(cond.clone()), Prepared(cond), false),
443 ];
444
445 for (a, b, is_ok) in inputs {
446 assert_eq!(validate_status_transition(&a, &b).is_ok(), is_ok);
447 }
448 }
449
450 #[test]
451 fn test_validate_status_transition_not_assigned_scheduled() {
452 let mut a = example_vm();
454 assert!(matches!(
455 &a.status,
456 VmStatus::NotAssigned(NotAssignedStatus::Pending)
457 ));
458 let mut b = example_vm();
459 b.status = VmStatus::Scheduled(example_condition());
460 assert_ne!(a.spec.resources, example_condition().reserved_resources);
461 assert!(VirtualMachine::validate_fields_before_update(&a, &b).is_err());
462
463 a.spec.resources = example_condition().reserved_resources;
464 b.spec.resources = example_condition().reserved_resources;
465 assert!(VirtualMachine::validate_fields_before_update(&a, &b).is_ok());
466
467 a.status = VmStatus::NotAssigned(NotAssignedStatus::InsufficientResources);
469 assert!(VirtualMachine::validate_fields_before_update(&b, &a).is_err());
470
471 a.spec.resources = Resources {
475 vcpu: 0,
476 ram_mib: 0,
477 };
478
479 b.spec.resources = a.spec.resources.clone();
480 b.status = VmStatus::Scheduled(VmCondition {
481 reserved_resources: Resources {
482 vcpu: 0,
483 ram_mib: 0,
484 },
485 assigned_node: SclName::try_from("hello").unwrap(),
486 transition_info: None,
487 });
488 assert!(VirtualMachine::validate_fields_before_update(&a, &b).is_err());
489 }
490
491 macro_rules! test_invalid_vm_condition_changes {
494 ($($name:ident: $value:expr,)*) => {
495 $(
496 #[test]
497 fn $name() {
498 let cond = example_condition();
499 let scheduled: VmStatus = $value.0(cond.clone());
500
501 let mut v1 = cond.clone();
503 v1.reserved_resources.vcpu = 0;
504 let c1: VmStatus = $value.1(v1);
505 assert!(validate_status_transition(&scheduled, &c1).is_err());
506
507 let mut v2 = cond.clone();
509 v2.reserved_resources.ram_mib = 0;
510 let c2: VmStatus = $value.1(v2);
511 assert!(validate_status_transition(&scheduled, &c2).is_err());
512
513 let mut v3 = cond.clone();
515 v3.assigned_node = SclName::try_from("changed").unwrap();
516 let c3: VmStatus = $value.1(v3);
517 assert!(validate_status_transition(&scheduled, &c3).is_err());
518 }
519 )*
520 }
521 }
522
523 test_invalid_vm_condition_changes! {
524 test_vm_condition_changes_sc_sc: (VmStatus::Scheduled, VmStatus::Scheduled),
527 test_vm_condition_changes_sc_ru: (VmStatus::Scheduled, VmStatus::Running),
528 test_vm_condition_changes_sc_pa: (VmStatus::Scheduled, VmStatus::Paused),
529 test_vm_condition_changes_sc_st: (VmStatus::Scheduled, VmStatus::Stopped),
530 test_vm_condition_changes_ru_sc: (VmStatus::Running, VmStatus::Scheduled),
531 test_vm_condition_changes_ru_ru: (VmStatus::Running, VmStatus::Running),
532 test_vm_condition_changes_ru_pa: (VmStatus::Running, VmStatus::Paused),
533 test_vm_condition_changes_ru_st: (VmStatus::Running, VmStatus::Stopped),
534 test_vm_condition_changes_pa_sc: (VmStatus::Paused, VmStatus::Scheduled),
535 test_vm_condition_changes_pa_ru: (VmStatus::Paused, VmStatus::Running),
536 test_vm_condition_changes_pa_pa: (VmStatus::Paused, VmStatus::Paused),
537 test_vm_condition_changes_pa_st: (VmStatus::Paused, VmStatus::Stopped),
538 test_vm_condition_changes_st_sc: (VmStatus::Stopped, VmStatus::Scheduled),
539 test_vm_condition_changes_st_ru: (VmStatus::Stopped, VmStatus::Running),
540 test_vm_condition_changes_st_pa: (VmStatus::Stopped, VmStatus::Paused),
541 test_vm_condition_changes_st_st: (VmStatus::Stopped, VmStatus::Stopped),
542 }
543
544 #[test]
545 fn validate_fields_before_create() {
546 use VmStatus::*;
547
548 let vm = example_vm();
549 assert!(VirtualMachine::validate_fields_before_create(&vm).is_ok());
550
551 #[allow(clippy::type_complexity)]
552 let invalid_mutations: Vec<Box<dyn Fn(&mut VirtualMachine)>> = vec![
553 Box::new(|t| t.spec.resources.vcpu = 0),
554 Box::new(|t| t.spec.resources.ram_mib = 0),
555 Box::new(|t| t.spec.target_state = TargetVmStatus::Paused),
556 Box::new(|t| t.spec.target_state = TargetVmStatus::Stopped),
557 Box::new(|t| t.status = NotAssigned(NotAssignedStatus::InsufficientResources)),
558 Box::new(|t| t.status = Scheduled(example_condition())),
559 Box::new(|t| t.status = Running(example_condition())),
560 Box::new(|t| t.status = Paused(example_condition())),
561 Box::new(|t| t.status = Stopped(example_condition())),
562 ];
563
564 detect_invalid_create_mutations(&vm, invalid_mutations);
565 }
566
567 #[test]
568 fn validate_fields_before_update() {
569 let current = example_vm();
570
571 #[allow(clippy::type_complexity)]
572 let invalid_mutations: Vec<Box<dyn Fn(&mut VirtualMachine)>> = vec![
573 Box::new(|t| t.separation_context = SclName::try_from("changed").unwrap()),
574 Box::new(|t| t.spec.resources.vcpu = 4),
575 Box::new(|t| t.spec.resources.vcpu = 0),
576 Box::new(|t| t.spec.resources.ram_mib = 4),
577 Box::new(|t| t.spec.resources.ram_mib = 0),
578 Box::new(|t| {
579 t.spec.boot_volume = BootVolume::Volume(SclName::try_from("changed").unwrap())
580 }),
581 Box::new(|t| t.spec.network_device_name = String::from("changed")),
582 Box::new(|t| t.spec.cloud_init_config = Some(serde_json::from_str("{}").unwrap())),
583 ];
584
585 detect_invalid_update_mutations(¤t, ¤t, invalid_mutations);
586 }
587
588 #[test]
589 fn detect_invalid_metadata_mutations() {
590 let t = example_vm();
591 detect_invalid_metadata_create_mutations(&t);
592 detect_invalid_metadata_update_mutations(&t);
593 }
594}