scl_lib/api_objects/
virtual_machine.rs

1// SPDX-License-Identifier: EUPL-1.2
2use 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// Important: Make sure to update `deny_illegal_field_changes` when new fields are added.
41#[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    /// Latest status of the virtual machine. Does not need to be specified by the user when
51    /// creating (POST), as it is initialized with the default values.
52    #[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    /// Prefix for all [VirtualMachine]s (`"/vms"`).
73    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            &current_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            (&current_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                // Given that the specified resources and reserved resources are equal,
177                // it was possible to insert vcpu / ram_mib values with 0, which must not happen.
178                return Err(Error::Application);
179            }
180        }
181
182        validate_status_transition(&current_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,      // TODO do we need to change this?
243    pub previous_vm_state: String, // Don't use `VmStatus` here in order to prevent infinite recursion.
244    pub error_description: String, // Like "VM won't boot". TODO use proper error type instead.
245}
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    /// Size in mebibyte reserved for the volume.
259    #[cfg_attr(feature = "openapi", validate(range(min = 1)))]
260    #[serde(rename = "sizeMiB")]
261    pub size_mib: u64,
262    // URL pointing to initial data that should be copied into the new volume.
263    /// Make sure that the `size_mib` is at least as large as the required disk space.
264    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
289/// Does **not** consider additional context (e.g, specified resources vs. reserved resources
290/// in the [VmCondition]).
291fn 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        // Assigned node and reserved resources must stay the same.
301        (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    /// Does not consider any [VmCondition] details.
396    #[test]
397    fn test_validate_status_transition() {
398        use NotAssignedStatus::{InsufficientResources as IR, Pending as P}; // Better formatting.
399        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        // Specified and reserved *resources* must match.
453        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        // Example: status validation is performed.
468        a.status = VmStatus::NotAssigned(NotAssignedStatus::InsufficientResources);
469        assert!(VirtualMachine::validate_fields_before_update(&b, &a).is_err());
470
471        // Reserved resources must be greater than 0. (*specified* resources are validated during
472        // creation and immutable later; *reserved* resources are specified during an update
473        // from NotAssigned to Scheduled and otherwise (in theory) immutable).
474        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    /// Checks for every [VmStatus] pair with a nested [VmCondition] that changes of its `assigned_node`,
492    /// `reserved_resources.vcpu`, and `reserved_resources.ram_mib` fields are denied.
493    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                    // Changed vcpu value must be denied.
502                    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                    // Changed ram_mib value must be denied.
508                    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                    // Changed assigned host must be denied.
514                    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        // The over-coverage (resulting from testing on a type level and therefore including
525        // *currently* invalid deemed transitions) reduces the future need for test maintenance.
526        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(&current, &current, 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}