Skip to main content

product_os_command_control/
registry.rs

1//! Node registry module
2//!
3//! Manages the registry of Product OS nodes in the cluster, including
4//! node information, capabilities, services, and features.
5
6use core::str::FromStr;
7use std::prelude::v1::*;
8
9use std::collections::BTreeMap;
10use std::sync::Arc;
11use serde::{ Deserialize, Serialize };
12
13use product_os_capabilities::{Features, ServiceError, Services, What};
14use product_os_security::{AsByteVector, DHKeyStore, RandomGenerator, certificates::Certificates, RandomGeneratorTemplate, RNG, StdRng, SeedableRng};
15use product_os_store::ProductOSKeyValueStore;
16
17use chrono::{DateTime, Utc };
18use parking_lot::Mutex;
19use product_os_request::Uri;
20
21/// Represents a single server instance (node) in the Product OS cluster.
22///
23/// Each node has a unique identifier, machine ID, URI, and tracks its own
24/// capabilities, services, features, and failure count.
25#[derive(Debug, Deserialize, Serialize)]
26#[serde(rename_all = "camelCase")]
27pub struct Node {
28    id: uuid::Uuid,
29    machine_id: String,
30
31    uri: String,
32    process_id: u32,
33
34    certificate: Vec<u8>,
35
36    capabilities: Vec<String>,
37    services: Services,
38    features: Features,
39
40    failures: u8,
41    created_at: DateTime<Utc>,
42    updated_at: DateTime<Utc>
43}
44
45impl AsByteVector for &Node {
46    fn as_byte_vector(&self) -> Vec<u8> {
47        let mut bytes = vec!();
48
49        bytes.extend_from_slice(self.id.as_bytes());
50        bytes.extend_from_slice(self.machine_id.as_bytes());
51        bytes.extend_from_slice(self.uri.as_bytes());
52        bytes.extend_from_slice(&self.certificate);
53        bytes.extend_from_slice(&[self.failures]);
54        bytes.extend_from_slice(self.created_at.to_string().as_bytes());
55        bytes.extend_from_slice(self.updated_at.to_string().as_bytes());
56
57        bytes
58    }
59}
60
61
62impl Node {
63    /// Creates a new `Node` instance with the given configuration and certificates.
64    ///
65    /// Generates a unique UUID, retrieves the machine ID, and initialises
66    /// the node with the configured URL address.
67    ///
68    /// # Panics
69    ///
70    /// Panics if the machine UID cannot be retrieved or if the configured
71    /// URL address is not a valid URI.
72    pub fn new(config: &crate::config::CommandControl, certificates: Certificates) -> Self {
73        let machine_uid = match machine_uid::get() {
74            Ok(uid) => uid,
75            Err(e) => panic!("Unable to generate machine id: {}", e)
76        };
77
78        Self {
79            id: uuid::Uuid::new_v4(),
80            uri: Uri::from_str(config.url_address.as_str()).unwrap().to_string(),
81            process_id: std::process::id(),
82            machine_id: product_os_security::create_string_hash(machine_uid.as_str()),
83            certificate: certificates.certificates.first().unwrap().to_owned(),
84            capabilities: Vec::new(),
85            services: Services::new(),
86            features: Features::new(),
87            failures: 0,
88            created_at: Utc::now(),
89            updated_at: Utc::now()
90        }
91    }
92
93    /// Creates a new `Node` instance with the given configuration and certificates.
94    #[deprecated(since = "0.0.28", note = "Renamed to Node::new() to follow Rust conventions")]
95    pub fn default(config: &crate::config::CommandControl, certificates: Certificates) -> Self {
96        Self::new(config, certificates)
97    }
98
99    /// Returns the unique identifier for this node as a string.
100    pub fn get_identifier(&self) -> String {
101        self.id.to_string()
102    }
103
104    /// Returns the URI protocol scheme (e.g., "https") for this node.
105    ///
106    /// Returns an empty string if the URI has no scheme.
107    #[deprecated(since = "0.0.28", note = "Use try_get_protocol() to avoid potential panics")]
108    pub fn get_protocol(&self) -> String {
109        let uri = Uri::from_str(self.uri.as_str()).unwrap();
110        match uri.scheme() {
111            None => String::new(),
112            Some(s) => s.to_string()
113        }
114    }
115
116    /// Returns the URI protocol scheme (e.g., "https") for this node, or `None` if the URI is invalid.
117    pub fn try_get_protocol(&self) -> Option<String> {
118        Uri::from_str(self.uri.as_str())
119            .ok()
120            .and_then(|uri| uri.scheme().map(|s| s.to_string()))
121    }
122
123    /// Returns the parsed URI address for this node.
124    ///
125    /// # Panics
126    ///
127    /// Panics if the stored URI string is not valid. This should not happen
128    /// if the node was constructed via `Node::new()`.
129    #[deprecated(since = "0.0.28", note = "Use try_get_address() to avoid potential panics")]
130    pub fn get_address(&self) -> Uri {
131        Uri::from_str(self.uri.as_str()).unwrap()
132    }
133
134    /// Returns the parsed URI address for this node, or `None` if the URI is invalid.
135    pub fn try_get_address(&self) -> Option<Uri> {
136        Uri::from_str(self.uri.as_str()).ok()
137    }
138
139    /// Returns the OS process ID of this node.
140    pub fn get_process_id(&self) -> u32 {
141        self.process_id
142    }
143
144    /// Returns a copy of the certificate bytes for this node.
145    pub fn get_certificate(&self) -> Vec<u8> {
146        self.certificate.to_owned()
147    }
148
149    /// Returns the current failure count for this node.
150    pub fn get_failures(&self) -> u8 {
151        self.failures
152    }
153
154    /// Returns a reference to this node's registered features.
155    pub fn get_features(&self) -> &Features {
156        &self.features
157    }
158
159    /// Returns a reference to this node's registered services.
160    pub fn get_services(&self) -> &Services {
161        &self.services
162    }
163
164    /// Tests whether this node matches a single selector/value pair.
165    ///
166    /// Supported selectors:
167    /// - `"feature"` - checks if the node has the named feature
168    /// - `"capability"` - checks if the node has the named capability (empty capabilities match nothing)
169    /// - `"service.kind"` - checks if the node has a service of the given kind
170    /// - `"service.enabled"` - checks if all services match the enabled state
171    /// - `"service.active"` - checks if all services match the active state
172    pub fn match_node(&self, selector: &str, search_value: &str) -> bool {
173        match selector {
174            "feature" => {
175                self.features.get(search_value).is_some()
176            },
177            "capability" => {
178                self.capabilities.contains(&search_value.to_string())
179            },
180            "service.kind" => {
181                self.services.find(search_value).is_some()
182            },
183            "service.enabled" => {
184                self.services.list().all(|(_, service)| service.enabled.to_string() == search_value)
185            },
186            "service.active" => {
187                self.services.list().all(|(_, service)| service.active.to_string() == search_value)
188            },
189            _ => true
190        }
191    }
192
193    /// Tests whether this node matches all selector/value pairs in the query.
194    ///
195    /// Returns `true` only if every entry in the query matches.
196    /// An empty query always matches.
197    pub fn match_node_query(&self, query: &BTreeMap<&str, &str>) -> bool {
198        query.iter().all(|(selector, value)| self.match_node(selector, value))
199    }
200
201    /// Returns the timestamp when this node was created.
202    pub fn get_created_at(&self) -> DateTime<Utc> {
203        self.created_at
204    }
205
206    /// Returns the timestamp when this node was last updated.
207    pub fn get_last_updated_at(&self) -> DateTime<Utc> {
208        self.updated_at
209    }
210}
211
212
213
214/// Manages the collection of known nodes in the Product OS cluster.
215///
216/// The registry tracks the local node ("me"), remote nodes discovered
217/// from the key-value store, and cryptographic key sessions for
218/// authenticated communication.
219pub struct Registry {
220    me: Node,
221    nodes: BTreeMap<String, Node>,
222    key_store: DHKeyStore,
223
224    store: Arc<ProductOSKeyValueStore>,
225
226    max_failures: u8
227}
228
229impl Registry {
230    /// Creates a new registry with the given configuration and certificates.
231    ///
232    /// Initialises the local node and an empty set of remote nodes.
233    pub fn new(config: &crate::config::CommandControl, key_value_store: Arc<ProductOSKeyValueStore>, certificates: Certificates) -> Self {
234        let me = Node::new(config, certificates);
235
236        Registry {
237            me,
238            nodes: BTreeMap::new(),
239            key_store: DHKeyStore::new(),
240            store: key_value_store,
241            max_failures: config.max_failures,
242        }
243    }
244
245    /// Returns the maximum number of failures before a node is removed.
246    pub fn get_max_failures(&self) -> u8 {
247        self.max_failures
248    }
249
250    async fn upsert_me_remote(&mut self) {
251        self.store.group_set(self.me.id.to_string().as_str(), serde_json::to_string(&self.me).unwrap().as_str()).unwrap_or_default()
252    }
253
254    async fn upsert_node_remote(&mut self, node: &Node) {
255        tracing::info!("Upserting node: {:?}", node);
256        self.store.group_set(node.id.to_string().as_str(), serde_json::to_string(node).unwrap().as_str()).unwrap_or_default();
257    }
258
259    async fn remove_node_remote(&mut self, identifier: &str) {
260        self.store.group_remove(identifier).unwrap_or_default()
261    }
262
263    async fn get_node_remote(&mut self, id: &str) -> Option<Node> {
264        match self.store.group_get(id) {
265            Ok(v) => {
266                let mut node: Node = serde_json::from_str(v.as_str()).unwrap();
267                let _ = node.features.setup_router();
268                Some(node)
269            },
270            Err(_) => None
271        }
272    }
273
274    /// Checks if the local node still exists in the remote store.
275    ///
276    /// Returns `Some` reference to self if the remote node matches,
277    /// `None` if the node was lost or has a mismatched ID.
278    pub async fn check_me_remote(&mut self) -> Option<&Node> {
279        let id = self.me.id.to_string();
280
281        match self.get_node_remote(id.as_str()).await {
282            Some(node) => {
283                if node.id != self.me.id {
284                    None
285                }
286                else {
287                    Some(&self.me)
288                }
289            },
290            None => { None }
291        }
292    }
293
294    /// Returns a reference to the local node.
295    pub fn get_me(&self) -> &Node {
296        &self.me
297    }
298
299    /// Persists the local node's current state to the remote store.
300    pub async fn update_me(&mut self) {
301        self.upsert_me_remote().await;
302    }
303
304    /// Updates the local node's failure status and returns whether the node is still alive.
305    ///
306    /// If `success` is `true`, resets the failure counter. If `false`, increments it.
307    /// Returns `false` (dead) when the failure count reaches `max_failures`.
308    pub fn update_me_status(&mut self, success: bool) -> bool {
309        let failures = if success { 0 } else { self.me.failures + 1 };
310
311        if failures < self.max_failures {
312            self.me.failures = failures;
313            self.me.updated_at = Utc::now();
314
315            true
316        }
317        else {
318            false
319        }
320    }
321
322    /// Updates a remote node's pulse status and returns whether the node is still alive.
323    ///
324    /// Fetches the node from the remote store, updates its failure count,
325    /// and removes it if the failure threshold is exceeded.
326    pub async fn update_pulse_status(&mut self, id: &str, success: bool) -> bool {
327        match self.get_node_remote(id).await {
328            Some(mut node) => {
329                let failures = if success { 0 } else { node.failures + 1 };
330
331                if failures < self.max_failures {
332                    node.failures = failures;
333                    node.updated_at = Utc::now();
334
335                    self.upsert_node_remote(&node).await;
336                    self.nodes.insert(node.id.to_string(), node);
337
338                    true
339                }
340                else {
341                    tracing::info!("Removing node due to failures count {}: {:?}", failures, node);
342                    self.remove_node(node.id.to_string().as_str()).await;
343
344                    false
345                }
346            },
347            None => false
348        }
349    }
350
351    /// Inserts or updates a node in the local registry (not persisted to remote store).
352    pub fn upsert_node_local(&mut self, identifier: String, mut node: Node) {
353        let _ = node.features.setup_router();
354        self.nodes.insert(identifier, node);
355    }
356
357    /// Finds all nodes matching the given query, optionally excluding the local node.
358    pub fn find_nodes(&self, query: BTreeMap<&str, &str>, exclude_me: bool) -> BTreeMap<String, &Node> {
359        let mut result = BTreeMap::new();
360        let me = self.me.id.to_string();
361
362        for (id, node) in &self.nodes {
363            if (!exclude_me || !me.eq(id)) && node.match_node_query(&query) {
364                result.insert(id.to_string(), node);
365            }
366        }
367
368        result
369    }
370
371    /// Returns a reference to the node with the given ID, if present in the local registry.
372    pub fn get_node(&self, id: &str) -> Option<&Node> {
373        self.nodes.get(id)
374    }
375
376    /// Returns a map of nodes, optionally skipping the first `skip` entries and excluding the local node.
377    pub fn get_nodes(&self, skip: u8, exclude_me: bool) -> BTreeMap<String, &Node> {
378        let me = self.me.id.to_string();
379
380        self.nodes.iter()
381            .filter(|(id, _)| !exclude_me || !me.eq(*id))
382            .skip(skip as usize)
383            .map(|(id, node)| (id.to_string(), node))
384            .collect()
385    }
386
387    /// Returns certificate bytes for nodes, optionally skipping entries and excluding the local node.
388    #[deprecated(since = "0.0.28", note = "Use get_nodes_raw_certificates instead, which has identical behavior")]
389    pub fn get_nodes_certificates(&self, skip: u8, exclude_me: bool) -> Vec<Vec<u8>> {
390        self.get_nodes_raw_certificates(skip, exclude_me)
391    }
392
393    /// Returns raw certificate bytes for nodes, optionally skipping entries and excluding the local node.
394    pub fn get_nodes_raw_certificates(&self, skip: u8, exclude_me: bool) -> Vec<Vec<u8>> {
395        let me = self.me.id.to_string();
396
397        self.nodes.iter()
398            .filter(|(id, _)| !exclude_me || !me.eq(*id))
399            .skip(skip as usize)
400            .map(|(_, node)| node.get_certificate())
401            .collect()
402    }
403
404    /// Returns a map of node endpoints (URI + optional key), skipping entries and optionally excluding the local node.
405    pub fn get_nodes_endpoints(&self, skip: u8, exclude_me: bool) -> BTreeMap<String, (Uri, Option<Vec<u8>>)> {
406        let me = self.me.id.to_string();
407
408        #[allow(deprecated)]
409        self.nodes.iter()
410            .filter(|(id, _)| !exclude_me || !me.eq(*id))
411            .skip(skip as usize)
412            .map(|(id, node)| {
413                let address = node.get_address();
414                let key = self.get_key(id);
415                (id.to_string(), (address, key))
416            })
417            .collect()
418    }
419
420    /// Removes a node from both the local registry and the remote store.
421    pub async fn remove_node(&mut self, identifier: &str) -> Option<Node> {
422        match self.nodes.remove(identifier) {
423            Some(node) => {
424                self.remove_node_remote(node.id.to_string().as_str()).await;
425                Some(node)
426            },
427            None => None
428        }
429    }
430
431    /// Picks a random node from those matching the query.
432    pub fn pick_node(&self, query: BTreeMap<&str, &str>) -> Option<&Node> {
433        let eligible_nodes: Vec<&Node> = self.find_nodes(query, false).values().copied().collect();
434        let select = RandomGenerator::new(Some(RNG::Std(StdRng::from_entropy()))).get_random_usize(0, eligible_nodes.len());
435
436        eligible_nodes.get(select).copied()
437    }
438
439    /// Picks a random node that has the specified capability.
440    pub fn pick_node_for_capability(&self, capability: &str) -> Option<&Node> {
441        let mut query = BTreeMap::new();
442        query.insert("capability", capability);
443        self.pick_node(query)
444    }
445
446    /// Registers a feature on the local node and updates the remote store.
447    pub async fn add_feature(&mut self, feature: Arc<dyn product_os_capabilities::Feature>, base_path: String, router: &mut product_os_router::ProductOSRouter) {
448        let _ = self.me.features.add(feature, base_path, router).await;
449        self.update_me().await;
450    }
451
452    /// Registers a mutable feature on the local node and updates the remote store.
453    pub async fn add_feature_mut(&mut self, feature: Arc<Mutex<dyn product_os_capabilities::Feature>>, base_path: String, router: &mut product_os_router::ProductOSRouter) {
454        let _ = self.me.features.add_mut(feature, base_path, router).await;
455        self.update_me().await;
456    }
457
458    /// Picks a random node that has the specified feature.
459    pub fn pick_node_for_feature(&self, feature: &str) -> Option<&Node> {
460        let mut query = BTreeMap::new();
461        query.insert("feature", feature);
462        self.pick_node(query)
463    }
464
465    /// Removes a feature from the local node and updates the remote store.
466    pub async fn remove_feature(&mut self, identifier: &str) {
467        let _ = self.me.features.remove(identifier);
468        self.update_me().await;
469    }
470
471    /// Registers a service on the local node and updates the remote store.
472    pub async fn add_service(&mut self, service: Arc<dyn product_os_capabilities::Service>) {
473        self.me.services.add(service).await;
474        self.update_me().await;
475    }
476
477    /// Registers a mutable service on the local node and updates the remote store.
478    pub async fn add_service_mut(&mut self, service: Arc<Mutex<dyn product_os_capabilities::Service>>) {
479        let _ = self.me.services.add_mut(service).await;
480        self.update_me().await;
481    }
482
483    /// Sets the active status of a service on the local node and updates the remote store.
484    pub async fn set_service_active(&mut self, identifier: String, status: bool) {
485        let id = identifier.as_str();
486        match self.me.services.get_mut(id) {
487            None => (),
488            Some(s) => {
489                s.active = status;
490                self.update_me().await;
491            }
492        }
493    }
494
495    /// Removes a service from the local node and updates the remote store.
496    pub async fn remove_service(&mut self, identifier: &str) {
497        self.me.services.remove(identifier);
498        self.update_me().await;
499    }
500
501    /// Removes all inactive services matching the query and updates the remote store.
502    pub async fn remove_inactive_services(&mut self, query: BTreeMap<&str, &str>) {
503        let mut matches = Vec::new();
504
505        for (identifier, _) in self.find_nodes(query, true) {
506            matches.push(identifier.to_owned());
507        }
508
509        for identifier in &matches {
510            self.remove_service(identifier).await;
511        }
512
513        if !matches.is_empty() { self.update_me().await };
514    }
515
516    /// Starts all registered services on the local node.
517    pub async fn start_services(&mut self) -> Result<(), ()> {
518        for (_, service) in self.me.services.list_mut() {
519            match service.start().await {
520                Ok(_) => {}
521                Err(_) => return Err(())
522            }
523        }
524        
525        Ok(())
526    }
527
528    /// Starts the service with the given identifier on the local node.
529    pub async fn start_service(&mut self, identifier: &str) -> Result<(), ()> {
530        match self.me.services.get_mut(identifier) {
531            None => Err(()),
532            Some(s) => s.start().await
533        }
534    }
535    
536    /// Stops the service with the given identifier on the local node.
537    pub async fn stop_service(&mut self, identifier: &str) -> Result<(), ()> {
538        match self.me.services.get_mut(identifier) {
539            None => Err(()),
540            Some(s) => s.stop().await
541        }
542    }
543
544    /// Restarts the service with the given identifier on the local node.
545    pub async fn restart_service(&mut self, identifier: &str) -> Result<(), ()> {
546        match self.me.services.get_mut(identifier) {
547            None => Err(()),
548            Some(s) => s.restart().await
549        }
550    }
551
552    /// Calls a service action with the given input on the local node.
553    pub async fn call_service(&mut self, identifier: &str, action: &What, input: &Option<serde_json::Value>) -> Result<Option<serde_json::Value>, ServiceError> {
554        match self.me.services.get_mut(identifier) {
555            None => Err(ServiceError::GenericError(format!("Service {} not found", identifier))),
556            Some(s) => s.call(action, input).await
557        }
558    }
559
560    /// Discovers nodes from the remote key-value store and imports them into the local registry.
561    pub async fn discover_nodes(&mut self) {
562        let mut nodes = BTreeMap::new();
563
564        match self.store.group_find(None) {
565            Ok(ns) => {
566                nodes = ns;
567            },
568            Err(_) => {
569                tracing::error!("Error getting nodes from store");
570            }
571        }
572
573        for (id, node) in nodes {
574            match serde_json::from_str(node.as_str()) {
575                Ok(n) => {
576                    let mut node: Node = n;
577                    let _ = node.features.setup_router();
578                    tracing::trace!("Importing remote node: {:?}", node.id);
579                    self.upsert_node_local(node.id.to_string(), node);
580                },
581                Err(e) => {
582                    tracing::error!("Error importing remote node {} - purging: {:?}", id, e);
583                    self.remove_node_remote(id.as_str()).await;
584                }
585            }
586        }
587    }
588
589    /// Returns the cryptographic key associated with the given node identifier.
590    pub fn get_key(&self, identifier: &str) -> Option<Vec<u8>> {
591        self.key_store.get_key(identifier).map(|k| k.to_vec())
592    }
593
594    /// Creates a new Diffie-Hellman key session and returns the session ID and public key.
595    pub fn create_key_session(&mut self) -> (String, [u8; 32]) {
596        self.key_store.create_session()
597    }
598
599    /// Generates a shared key from a Diffie-Hellman exchange session.
600    pub fn generate_key(&mut self, session_identifier: &str, remote_public_key: &[u8], association: String, remote_session_identifier: Option<String>) {
601        self.key_store.generate_key(session_identifier, remote_public_key, association, remote_session_identifier);
602    }
603}