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