Skip to main content

zlayer_types/api/
nodes.rs

1//! Node management API DTOs.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7/// Summary of a GPU on a node, stored in Raft cluster state and reported
8/// via cluster join requests.
9///
10/// Lifted from `zlayer-scheduler::raft` so that wire-types consumers
11/// (zlayer-api DTOs, the CLI, the manager UI) can describe GPU inventory
12/// without depending on the heavier scheduler crate. `zlayer-scheduler`
13/// re-exports this type for source compatibility.
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
15pub struct GpuInfoSummary {
16    /// Vendor: "nvidia", "amd", "intel", or "unknown"
17    pub vendor: String,
18    /// Model name (e.g., "NVIDIA A100-SXM4-80GB")
19    pub model: String,
20    /// VRAM in MB
21    pub memory_mb: u64,
22}
23
24/// Node summary for list operations
25#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
26pub struct NodeSummary {
27    /// Node identifier
28    pub id: u64,
29    /// Node network address
30    pub address: String,
31    /// Current node status (e.g., "ready", "notready", "disconnected")
32    pub status: String,
33    /// Node role (e.g., "leader", "worker")
34    pub role: String,
35    /// Node labels for scheduling
36    pub labels: HashMap<String, String>,
37    /// Last seen timestamp (Unix timestamp)
38    pub last_seen: u64,
39}
40
41/// Node resource information
42#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
43pub struct NodeResourceInfo {
44    /// Total CPU cores
45    pub cpu_total: f64,
46    /// Used CPU cores
47    pub cpu_used: f64,
48    /// CPU usage percentage
49    pub cpu_percent: f64,
50    /// Total memory in bytes
51    pub memory_total: u64,
52    /// Used memory in bytes
53    pub memory_used: u64,
54    /// Memory usage percentage
55    pub memory_percent: f64,
56}
57
58/// Detailed node information
59#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
60pub struct NodeDetails {
61    /// Node identifier
62    pub id: u64,
63    /// Node network address
64    pub address: String,
65    /// Current node status
66    pub status: String,
67    /// Node role
68    pub role: String,
69    /// Node labels for scheduling
70    pub labels: HashMap<String, String>,
71    /// Last seen timestamp (Unix timestamp)
72    pub last_seen: u64,
73    /// Node resource information
74    pub resources: NodeResourceInfo,
75    /// Services running on this node
76    pub services: Vec<String>,
77    /// When the node was registered (Unix timestamp)
78    pub registered_at: u64,
79    /// Last heartbeat timestamp (Unix timestamp)
80    pub last_heartbeat: u64,
81}
82
83/// Request to update node labels
84#[derive(Debug, Deserialize, utoipa::ToSchema)]
85pub struct UpdateLabelsRequest {
86    /// Labels to add or update
87    pub labels: HashMap<String, String>,
88    /// Label keys to remove
89    pub remove: Vec<String>,
90}
91
92/// Response after updating labels
93#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
94pub struct UpdateLabelsResponse {
95    /// Current labels after the update
96    pub labels: HashMap<String, String>,
97}
98
99/// Join token response for cluster joining
100#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
101pub struct JoinTokenResponse {
102    /// Join token for authenticating the new node
103    pub token: String,
104    /// Leader endpoint to connect to
105    pub leader_endpoint: String,
106    /// Leader's public key for secure communication
107    pub leader_public_key: String,
108    /// Overlay network CIDR
109    pub overlay_cidr: String,
110    /// Allocated IP address for the joining node
111    pub allocated_ip: String,
112    /// Token expiration timestamp (Unix timestamp)
113    pub expires_at: u64,
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_node_summary_serialize() {
122        let summary = NodeSummary {
123            id: 1,
124            address: "192.168.1.10:9090".to_string(),
125            status: "ready".to_string(),
126            role: "worker".to_string(),
127            labels: HashMap::from([("zone".to_string(), "us-east-1".to_string())]),
128            last_seen: 1_706_745_600,
129        };
130        let json = serde_json::to_string(&summary).unwrap();
131        assert!(json.contains("192.168.1.10"));
132        assert!(json.contains("ready"));
133        assert!(json.contains("worker"));
134    }
135
136    #[test]
137    fn test_node_details_serialize() {
138        let details = NodeDetails {
139            id: 1,
140            address: "192.168.1.10:9090".to_string(),
141            status: "ready".to_string(),
142            role: "worker".to_string(),
143            labels: HashMap::new(),
144            last_seen: 1_706_745_600,
145            resources: NodeResourceInfo {
146                cpu_total: 4.0,
147                cpu_used: 1.5,
148                cpu_percent: 37.5,
149                memory_total: 8_589_934_592,
150                memory_used: 4_294_967_296,
151                memory_percent: 50.0,
152            },
153            services: vec!["api".to_string(), "worker".to_string()],
154            registered_at: 1_706_659_200,
155            last_heartbeat: 1_706_745_600,
156        };
157        let json = serde_json::to_string(&details).unwrap();
158        assert!(json.contains("resources"));
159        assert!(json.contains("services"));
160        assert!(json.contains("cpu_total"));
161    }
162
163    #[test]
164    fn test_update_labels_request_deserialize() {
165        let json = r#"{"labels": {"zone": "us-west-2"}, "remove": ["old-label"]}"#;
166        let request: UpdateLabelsRequest = serde_json::from_str(json).unwrap();
167        assert_eq!(request.labels.get("zone"), Some(&"us-west-2".to_string()));
168        assert_eq!(request.remove, vec!["old-label".to_string()]);
169    }
170
171    #[test]
172    fn test_join_token_response_serialize() {
173        let response = JoinTokenResponse {
174            token: "abc123".to_string(),
175            leader_endpoint: "192.168.1.1:9090".to_string(),
176            leader_public_key: "pubkey".to_string(),
177            overlay_cidr: "10.0.0.0/16".to_string(),
178            allocated_ip: "10.0.1.5".to_string(),
179            expires_at: 1_706_832_000,
180        };
181        let json = serde_json::to_string(&response).unwrap();
182        assert!(json.contains("abc123"));
183        assert!(json.contains("overlay_cidr"));
184    }
185}