scl_lib/api_objects/
node.rs

1// SPDX-License-Identifier: EUPL-1.2
2use crate::api_objects::{Error, MetaData, Result, SclName, SclObject, TransitionError, Url};
3use serde::{Deserialize, Serialize};
4
5#[cfg(feature = "openapi")]
6use schemars::JsonSchema;
7
8#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
9#[cfg_attr(feature = "openapi", derive(JsonSchema))]
10#[serde(rename_all = "camelCase")]
11pub struct Node {
12    #[serde(flatten)]
13    pub metadata: MetaData,
14    /// Total amount of computational resources offered by the Node
15    pub resources: Resources,
16    /// Endpoint of the Node API
17    pub node_api: Endpoint,
18    /// Endpoint of the SmartNIC API
19    pub nic_api: Endpoint,
20    /// Latest status of the node. Does not need to be specified by the user when
21    /// creating (POST), as it is initialized with the default values.
22    #[serde(default)]
23    pub status: NodeStatus,
24}
25
26impl SclObject for Node {
27    /// Prefix for all [Node]s (`"/nodes"`).
28    const PREFIX: &'static str = "/nodes";
29
30    fn name(&self) -> &SclName {
31        &self.metadata.name
32    }
33
34    /// Overrides the default implementation of the [SclObject] trait to ignore the `sc` argument
35    /// when constructing API endpoints.
36    ///
37    /// # Examples
38    ///
39    /// ```
40    /// use scl_lib::api_objects::{Node, SclObject};
41    ///
42    /// assert_eq!(Node::api_endpoint(None, "node-01"), "/nodes/node-01");
43    /// assert_eq!(Node::api_endpoint(None, "node-01"), Node::api_endpoint("node-01", "node-01"));
44    /// assert_eq!(Node::api_endpoint("node-01", None), Node::api_endpoint(None, None));
45    /// assert_eq!(Node::api_endpoint(None, None), Node::PREFIX);
46    /// ```
47    fn api_endpoint<'a>(
48        _sc: impl Into<Option<&'a str>>,
49        name: impl Into<Option<&'a str>>,
50    ) -> String {
51        if let Some(name) = name.into() {
52            format!("{}/{}", Self::PREFIX, name)
53        } else {
54            Self::PREFIX.into()
55        }
56    }
57
58    /// Overrides the default implementation of the [SclObject] trait to use either the `sc` or the
59    /// `name` argument for DB key construction.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use scl_lib::api_objects::{Node, SclObject};
65    ///
66    /// assert_eq!(Node::db_key(None, "node-01"), "/nodes/node-01");
67    /// assert_eq!(Node::db_key(None, "node-01"), Node::db_key("node-01", "node-01"));
68    /// assert_eq!(Node::db_key("node-01", None), Node::db_key(None, None));
69    /// assert_eq!(Node::db_key(None, None), Node::PREFIX);
70    /// ```
71    fn db_key<'a>(_sc: impl Into<Option<&'a str>>, name: impl Into<Option<&'a str>>) -> String {
72        if let Some(name) = name.into() {
73            format!("{}/{}", Self::PREFIX, name)
74        } else {
75            Self::PREFIX.into()
76        }
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        vec![]
89    }
90
91    fn validate_fields_before_create(&self) -> Result<()> {
92        self.metadata.validate_fields_before_create()?;
93
94        if !self.resources.all_resources_are_greater_than_zero() {
95            return Err(Error::IllegalInitialValue(
96                "All resources must be greater than 0!".to_string(),
97            ));
98        }
99
100        Ok(())
101    }
102
103    fn validate_fields_before_update(
104        current_db_state: &Self,
105        proposed_new_state: &Self,
106    ) -> Result<()> {
107        MetaData::validate_fields_before_regular_update(
108            &current_db_state.metadata,
109            &proposed_new_state.metadata,
110        )?;
111
112        if current_db_state.nic_api != proposed_new_state.nic_api
113            || current_db_state.node_api != proposed_new_state.node_api
114            || current_db_state.resources != proposed_new_state.resources
115        {
116            Err(TransitionError::Other("Only status field may be updated!".to_string()).into())
117        } else {
118            Ok(())
119        }
120    }
121}
122
123#[derive(Clone, Default, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
124#[cfg_attr(feature = "openapi", derive(JsonSchema))]
125#[serde(rename_all = "camelCase")]
126pub struct Resources {
127    /// Number of vCPUs.
128    #[cfg_attr(feature = "openapi", validate(range(min = 1)))]
129    pub vcpu: u16,
130    /// Amount of RAM in mebibyte.
131    #[cfg_attr(feature = "openapi", validate(range(min = 1)))]
132    #[serde(rename = "ramMiB")]
133    pub ram_mib: u32,
134}
135
136impl Resources {
137    pub fn all_resources_are_greater_than_zero(&self) -> bool {
138        self.vcpu > 0 && self.ram_mib > 0
139    }
140}
141
142#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
143#[cfg_attr(feature = "openapi", derive(JsonSchema))]
144#[serde(rename_all = "camelCase")]
145pub struct Endpoint {
146    /// Address of the endpoint.
147    pub url: Url,
148
149    /// API version supported by the endpoint
150    pub version: u8, // TODO use enum instead. Or Vec of enums?
151}
152
153#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
154#[cfg_attr(feature = "openapi", derive(JsonSchema))]
155#[serde(rename_all = "camelCase")]
156pub struct NodeStatus {
157    pub last_heartbeat: String, // TODO u64?
158    pub healthy: bool,
159}
160
161#[cfg(test)]
162mod test {
163    use super::Node;
164    use crate::api_objects::test::{
165        detect_invalid_metadata_create_mutations, detect_invalid_metadata_update_mutations,
166        detect_invalid_update_mutations, parse_json_dir,
167    };
168    use crate::api_objects::{Endpoint, MetaData, Resources, SclName, SclObject, Url};
169    use std::path::Path;
170
171    #[test]
172    fn parse_sample_json() {
173        parse_json_dir::<Node>(Path::new("../test/sample_json/nodes"));
174    }
175
176    fn example_node() -> Node {
177        let endpoint = Endpoint {
178            url: Url::try_from("ftp://example.com").unwrap(),
179            version: 1,
180        };
181
182        Node {
183            metadata: MetaData::new(SclName::try_from("example").unwrap()),
184            resources: Resources {
185                vcpu: 1,
186                ram_mib: 1,
187            },
188            node_api: endpoint.clone(),
189            nic_api: endpoint,
190            status: Default::default(),
191        }
192    }
193
194    #[test]
195    fn validate_fields_before_create() {
196        let mut node = example_node();
197        assert!(node.validate_fields_before_create().is_ok());
198        node.resources.vcpu = 0;
199        assert!(node.validate_fields_before_create().is_err());
200        node.resources.vcpu = 1;
201        node.resources.ram_mib = 0;
202        assert!(node.validate_fields_before_create().is_err());
203    }
204
205    #[test]
206    fn validate_fields_before_update() {
207        let current = example_node();
208        assert!(Node::validate_fields_before_update(&current, &current).is_ok());
209
210        #[allow(clippy::type_complexity)]
211        let invalid_mutations: Vec<Box<dyn Fn(&mut Node)>> = vec![
212            Box::new(|t| t.resources.ram_mib = 999),
213            Box::new(|t| t.resources.vcpu = 999),
214            Box::new(|t| t.nic_api.url = Url::try_from("ftp://other.com").unwrap()),
215            Box::new(|t| t.nic_api.version = 123),
216            Box::new(|t| t.node_api.url = Url::try_from("ftp://other.com").unwrap()),
217            Box::new(|t| t.node_api.version = 123),
218        ];
219
220        detect_invalid_update_mutations(&current, &current, invalid_mutations);
221    }
222
223    #[test]
224    fn detect_invalid_metadata_mutations() {
225        let t = example_node();
226        detect_invalid_metadata_create_mutations(&t);
227        detect_invalid_metadata_update_mutations(&t);
228    }
229
230    #[test]
231    fn test_all_resources_are_greater_than_zero() {
232        let mut r = Resources {
233            vcpu: 0,
234            ram_mib: 0,
235        };
236        assert!(!r.all_resources_are_greater_than_zero());
237        r.vcpu = 1;
238        assert!(!r.all_resources_are_greater_than_zero());
239        r.ram_mib = 1;
240        assert!(r.all_resources_are_greater_than_zero());
241        r.vcpu = 0;
242        assert!(!r.all_resources_are_greater_than_zero());
243    }
244}