1use crate::certificate::CertificateFileResponse;
2use crate::client::{Client, Response};
3use crate::errors::Error;
4use crate::machine_file::MachineFile;
5use crate::KeygenResponseData;
6use chrono::{DateTime, Utc};
7use futures::future::{BoxFuture, FutureExt};
8use futures::StreamExt;
9use serde::{Deserialize, Serialize};
10use serde_json::{json, Value};
11use std::collections::HashMap;
12use std::sync::mpsc::{Receiver, Sender};
13use std::sync::Arc;
14use std::time::Duration;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub(crate) struct MachineAttributes {
18 pub fingerprint: String,
19 pub name: Option<String>,
20 pub platform: Option<String>,
21 pub hostname: Option<String>,
22 pub ip: Option<String>,
23 pub cores: Option<i32>,
24 pub metadata: Option<HashMap<String, Value>>,
25 #[serde(rename = "requireHeartbeat")]
26 pub require_heartbeat: bool,
27 #[serde(rename = "heartbeatStatus")]
28 pub heartbeat_status: String,
29 #[serde(rename = "heartbeatDuration")]
30 pub heartbeat_duration: Option<i32>,
31 pub created: DateTime<Utc>,
32 pub updated: DateTime<Utc>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub(crate) struct MachineResponse {
37 pub data: KeygenResponseData<MachineAttributes>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub(crate) struct MachinesResponse {
42 pub data: Vec<KeygenResponseData<MachineAttributes>>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Machine {
47 pub id: String,
48 pub fingerprint: String,
49 pub name: Option<String>,
50 pub platform: Option<String>,
51 pub hostname: Option<String>,
52 pub ip: Option<String>,
53 pub cores: Option<i32>,
54 pub metadata: Option<HashMap<String, Value>>,
55 #[serde(rename = "requireHeartbeat")]
56 pub require_heartbeat: bool,
57 #[serde(rename = "heartbeatStatus")]
58 pub heartbeat_status: String,
59 #[serde(rename = "heartbeatDuration")]
60 pub heartbeat_duration: Option<i32>,
61 pub created: DateTime<Utc>,
62 pub updated: DateTime<Utc>,
63 pub account_id: Option<String>,
64 pub environment_id: Option<String>,
65 pub product_id: Option<String>,
66 pub license_id: Option<String>,
67 pub owner_id: Option<String>,
68 pub group_id: Option<String>,
69}
70
71pub struct MachineCheckoutOpts {
72 pub ttl: Option<i64>,
73 pub include: Option<Vec<String>>,
74}
75
76impl MachineCheckoutOpts {
77 pub fn new() -> Self {
78 Self {
79 ttl: None,
80 include: None,
81 }
82 }
83
84 pub fn with_ttl(ttl: i64) -> Self {
85 Self {
86 ttl: Some(ttl),
87 include: None,
88 }
89 }
90
91 pub fn with_include(include: Vec<String>) -> Self {
92 Self {
93 ttl: None,
94 include: Some(include),
95 }
96 }
97}
98
99impl Default for MachineCheckoutOpts {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, Default)]
106pub struct MachineListFilters {
107 pub license: Option<String>,
108 pub user: Option<String>,
109 pub platform: Option<String>,
110 pub name: Option<String>,
111 pub fingerprint: Option<String>,
112 pub ip: Option<String>,
113 pub hostname: Option<String>,
114 pub product: Option<String>,
115 pub owner: Option<String>,
116 pub group: Option<String>,
117 pub metadata: Option<HashMap<String, Value>>,
118 pub page_number: Option<i32>,
119 pub page_size: Option<i32>,
120 pub limit: Option<i32>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct MachineCreateRequest {
125 pub fingerprint: String,
126 pub name: Option<String>,
127 pub platform: Option<String>,
128 pub hostname: Option<String>,
129 pub ip: Option<String>,
130 pub cores: Option<i32>,
131 pub metadata: Option<HashMap<String, Value>>,
132 pub license_id: String,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct MachineUpdateRequest {
137 pub name: Option<String>,
138 pub platform: Option<String>,
139 pub hostname: Option<String>,
140 pub ip: Option<String>,
141 pub cores: Option<i32>,
142 pub metadata: Option<HashMap<String, Value>>,
143}
144
145impl Machine {
146 pub(crate) fn from(data: KeygenResponseData<MachineAttributes>) -> Machine {
147 Machine {
148 id: data.id,
149 fingerprint: data.attributes.fingerprint,
150 name: data.attributes.name,
151 platform: data.attributes.platform,
152 hostname: data.attributes.hostname,
153 ip: data.attributes.ip,
154 cores: data.attributes.cores,
155 metadata: data.attributes.metadata,
156 require_heartbeat: data.attributes.require_heartbeat,
157 heartbeat_status: data.attributes.heartbeat_status,
158 heartbeat_duration: data.attributes.heartbeat_duration,
159 created: data.attributes.created,
160 updated: data.attributes.updated,
161 account_id: data
162 .relationships
163 .account
164 .as_ref()
165 .and_then(|a| a.data.as_ref().map(|d| d.id.clone())),
166 environment_id: data
167 .relationships
168 .environment
169 .as_ref()
170 .and_then(|e| e.data.as_ref().map(|d| d.id.clone())),
171 product_id: data
172 .relationships
173 .product
174 .as_ref()
175 .and_then(|p| p.data.as_ref().map(|d| d.id.clone())),
176 license_id: data
177 .relationships
178 .license
179 .as_ref()
180 .and_then(|l| l.data.as_ref().map(|d| d.id.clone())),
181 owner_id: data
182 .relationships
183 .owner
184 .as_ref()
185 .and_then(|o| o.data.as_ref().map(|d| d.id.clone())),
186 group_id: data
187 .relationships
188 .group
189 .as_ref()
190 .and_then(|g| g.data.as_ref().map(|d| d.id.clone())),
191 }
192 }
193
194 pub async fn deactivate(&self) -> Result<(), Error> {
195 let client = Client::default()?;
196 let _response = client
197 .delete::<(), serde_json::Value>(&format!("machines/{}", self.id), None::<&()>)
198 .await?;
199 Ok(())
200 }
201
202 pub async fn checkout(&self, options: &MachineCheckoutOpts) -> Result<MachineFile, Error> {
203 let client = Client::default()?;
204 let mut query = json!({
205 "encrypt": 1
206 });
207
208 if let Some(ttl) = options.ttl {
209 query["ttl"] = ttl.into();
210 }
211
212 if let Some(ref include) = options.include {
213 query["include"] = json!(include.join(","));
214 } else {
215 query["include"] = "license.entitlements".into();
216 }
217
218 let response = client
219 .post(
220 &format!("machines/{}/actions/check-out", self.id),
221 None::<&()>,
222 Some(&query),
223 )
224 .await?;
225
226 let machine_file_response: CertificateFileResponse = serde_json::from_value(response.body)?;
227 let machine_file = MachineFile::from(machine_file_response.data);
228 Ok(machine_file)
229 }
230
231 pub async fn ping(&self) -> Result<Machine, Error> {
232 let client: Client = Client::default()?;
233 let response: Response<MachineResponse> = client
234 .post(
235 &format!("machines/{}/actions/ping", self.id),
236 None::<&()>,
237 None::<&()>,
238 )
239 .await?;
240 let machine = Machine::from(response.body.data);
241 Ok(machine)
242 }
243
244 pub fn monitor(
245 self: Arc<Self>,
246 heartbeat_interval: Duration,
247 tx: Option<Sender<Result<Machine, Error>>>,
248 cancel_rx: Option<Receiver<()>>,
249 ) -> BoxFuture<'static, ()> {
250 async move {
251 let send = |result: Result<Machine, Error>| {
252 if let Some(tx) = &tx {
253 tx.send(result).unwrap();
254 }
255 };
256
257 let mut interval_stream = futures::stream::unfold((), move |_| {
258 let delay = futures_timer::Delay::new(heartbeat_interval);
259 Box::pin(async move {
260 delay.await;
261 Some(((), ()))
262 })
263 });
264
265 send(self.ping().await);
266 while interval_stream.next().await.is_some() {
267 if let Some(ref rx) = cancel_rx {
268 if rx.try_recv().is_ok() {
269 break;
270 }
271 }
272 send(self.ping().await);
273 }
274 }
275 .boxed()
276 }
277
278 #[cfg(feature = "token")]
280 pub async fn create(request: MachineCreateRequest) -> Result<Machine, Error> {
281 let client = Client::default()?;
282
283 let mut attributes = serde_json::Map::new();
284 attributes.insert("fingerprint".to_string(), json!(request.fingerprint));
285
286 if let Some(name) = request.name {
287 attributes.insert("name".to_string(), json!(name));
288 }
289 if let Some(platform) = request.platform {
290 attributes.insert("platform".to_string(), json!(platform));
291 }
292 if let Some(hostname) = request.hostname {
293 attributes.insert("hostname".to_string(), json!(hostname));
294 }
295 if let Some(ip) = request.ip {
296 attributes.insert("ip".to_string(), json!(ip));
297 }
298 if let Some(cores) = request.cores {
299 attributes.insert("cores".to_string(), json!(cores));
300 }
301 if let Some(metadata) = request.metadata {
302 attributes.insert("metadata".to_string(), json!(metadata));
303 }
304
305 let body = json!({
306 "data": {
307 "type": "machines",
308 "attributes": attributes,
309 "relationships": {
310 "license": {
311 "data": {
312 "type": "licenses",
313 "id": request.license_id
314 }
315 }
316 }
317 }
318 });
319
320 let response = client.post("machines", Some(&body), None::<&()>).await?;
321 let machine_response: MachineResponse = serde_json::from_value(response.body)?;
322 Ok(Machine::from(machine_response.data))
323 }
324
325 #[cfg(feature = "token")]
327 pub async fn list(filters: Option<MachineListFilters>) -> Result<Vec<Machine>, Error> {
328 let client = Client::default()?;
329
330 let mut query_params = Vec::new();
331 if let Some(filters) = filters {
332 if let Some(license) = filters.license {
333 query_params.push(("license".to_string(), license));
334 }
335 if let Some(user) = filters.user {
336 query_params.push(("user".to_string(), user));
337 }
338 if let Some(platform) = filters.platform {
339 query_params.push(("platform".to_string(), platform));
340 }
341 if let Some(name) = filters.name {
342 query_params.push(("name".to_string(), name));
343 }
344 if let Some(fingerprint) = filters.fingerprint {
345 query_params.push(("fingerprint".to_string(), fingerprint));
346 }
347 if let Some(ip) = filters.ip {
348 query_params.push(("ip".to_string(), ip));
349 }
350 if let Some(hostname) = filters.hostname {
351 query_params.push(("hostname".to_string(), hostname));
352 }
353 if let Some(product) = filters.product {
354 query_params.push(("product".to_string(), product));
355 }
356 if let Some(owner) = filters.owner {
357 query_params.push(("owner".to_string(), owner));
358 }
359 if let Some(group) = filters.group {
360 query_params.push(("group".to_string(), group));
361 }
362 if let Some(metadata) = filters.metadata {
363 for (key, value) in metadata {
364 query_params.push((format!("metadata[{key}]"), value.to_string()));
365 }
366 }
367 if let Some(page_number) = filters.page_number {
369 query_params.push(("page[number]".to_string(), page_number.to_string()));
370 }
371 if let Some(page_size) = filters.page_size {
372 query_params.push(("page[size]".to_string(), page_size.to_string()));
373 }
374 if let Some(limit) = filters.limit {
375 query_params.push(("limit".to_string(), limit.to_string()));
376 }
377 }
378
379 let query = if query_params.is_empty() {
380 None
381 } else {
382 Some(
383 query_params
384 .into_iter()
385 .collect::<HashMap<String, String>>(),
386 )
387 };
388
389 let response = client.get("machines", query.as_ref()).await?;
390 let machines_response: MachinesResponse = serde_json::from_value(response.body)?;
391 Ok(machines_response
392 .data
393 .into_iter()
394 .map(Machine::from)
395 .collect())
396 }
397
398 #[cfg(feature = "token")]
400 pub async fn get(id: &str) -> Result<Machine, Error> {
401 let client = Client::default()?;
402 let endpoint = format!("machines/{id}");
403 let response = client.get(&endpoint, None::<&()>).await?;
404 let machine_response: MachineResponse = serde_json::from_value(response.body)?;
405 Ok(Machine::from(machine_response.data))
406 }
407
408 #[cfg(feature = "token")]
410 pub async fn update(&self, request: MachineUpdateRequest) -> Result<Machine, Error> {
411 let client = Client::default()?;
412 let endpoint = format!("machines/{}", self.id);
413
414 let mut attributes = serde_json::Map::new();
415 if let Some(name) = request.name {
416 attributes.insert("name".to_string(), json!(name));
417 }
418 if let Some(platform) = request.platform {
419 attributes.insert("platform".to_string(), json!(platform));
420 }
421 if let Some(hostname) = request.hostname {
422 attributes.insert("hostname".to_string(), json!(hostname));
423 }
424 if let Some(ip) = request.ip {
425 attributes.insert("ip".to_string(), json!(ip));
426 }
427 if let Some(cores) = request.cores {
428 attributes.insert("cores".to_string(), json!(cores));
429 }
430 if let Some(metadata) = request.metadata {
431 attributes.insert("metadata".to_string(), json!(metadata));
432 }
433
434 let body = json!({
435 "data": {
436 "type": "machines",
437 "attributes": attributes
438 }
439 });
440
441 let response = client.patch(&endpoint, Some(&body), None::<&()>).await?;
442 let machine_response: MachineResponse = serde_json::from_value(response.body)?;
443 Ok(Machine::from(machine_response.data))
444 }
445
446 #[cfg(feature = "token")]
448 pub async fn reset(&self) -> Result<Machine, Error> {
449 let client = Client::default()?;
450 let endpoint = format!("machines/{}/actions/reset", self.id);
451 let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
452 let machine_response: MachineResponse = serde_json::from_value(response.body)?;
453 Ok(Machine::from(machine_response.data))
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 use crate::{
461 KeygenRelationship, KeygenRelationshipData, KeygenRelationships, KeygenResponseData,
462 };
463 use chrono::Utc;
464
465 #[test]
466 fn test_machine_relationships() {
467 let machine_data = KeygenResponseData {
469 id: "test-machine-id".to_string(),
470 r#type: "machines".to_string(),
471 attributes: MachineAttributes {
472 fingerprint: "test-fingerprint".to_string(),
473 name: Some("Test Machine".to_string()),
474 platform: Some("linux".to_string()),
475 hostname: Some("test-host".to_string()),
476 ip: Some("192.168.1.1".to_string()),
477 cores: Some(8),
478 metadata: Some(HashMap::new()),
479 require_heartbeat: true,
480 heartbeat_status: "ALIVE".to_string(),
481 heartbeat_duration: Some(3600),
482 created: Utc::now(),
483 updated: Utc::now(),
484 },
485 relationships: KeygenRelationships {
486 policy: None,
487 account: Some(KeygenRelationship {
488 data: Some(KeygenRelationshipData {
489 r#type: "accounts".to_string(),
490 id: "test-account-id".to_string(),
491 }),
492 links: None,
493 }),
494 product: Some(KeygenRelationship {
495 data: Some(KeygenRelationshipData {
496 r#type: "products".to_string(),
497 id: "test-product-id".to_string(),
498 }),
499 links: None,
500 }),
501 group: Some(KeygenRelationship {
502 data: Some(KeygenRelationshipData {
503 r#type: "groups".to_string(),
504 id: "test-group-id".to_string(),
505 }),
506 links: None,
507 }),
508 owner: Some(KeygenRelationship {
509 data: Some(KeygenRelationshipData {
510 r#type: "users".to_string(),
511 id: "test-owner-id".to_string(),
512 }),
513 links: None,
514 }),
515 users: None,
516 machines: None,
517 environment: Some(KeygenRelationship {
518 data: Some(KeygenRelationshipData {
519 r#type: "environments".to_string(),
520 id: "test-environment-id".to_string(),
521 }),
522 links: None,
523 }),
524 license: Some(KeygenRelationship {
525 data: Some(KeygenRelationshipData {
526 r#type: "licenses".to_string(),
527 id: "test-license-id".to_string(),
528 }),
529 links: None,
530 }),
531 other: HashMap::new(),
532 },
533 };
534
535 let machine = Machine::from(machine_data);
536
537 assert_eq!(machine.account_id, Some("test-account-id".to_string()));
538 assert_eq!(
539 machine.environment_id,
540 Some("test-environment-id".to_string())
541 );
542 assert_eq!(machine.product_id, Some("test-product-id".to_string()));
543 assert_eq!(machine.license_id, Some("test-license-id".to_string()));
544 assert_eq!(machine.owner_id, Some("test-owner-id".to_string()));
545 assert_eq!(machine.group_id, Some("test-group-id".to_string()));
546 assert_eq!(machine.id, "test-machine-id");
547 assert_eq!(machine.fingerprint, "test-fingerprint");
548 }
549
550 #[test]
551 fn test_machine_without_relationships() {
552 let machine_data = KeygenResponseData {
554 id: "test-machine-id".to_string(),
555 r#type: "machines".to_string(),
556 attributes: MachineAttributes {
557 fingerprint: "test-fingerprint".to_string(),
558 name: Some("Test Machine".to_string()),
559 platform: Some("linux".to_string()),
560 hostname: Some("test-host".to_string()),
561 ip: Some("192.168.1.1".to_string()),
562 cores: Some(8),
563 metadata: Some(HashMap::new()),
564 require_heartbeat: true,
565 heartbeat_status: "ALIVE".to_string(),
566 heartbeat_duration: Some(3600),
567 created: Utc::now(),
568 updated: Utc::now(),
569 },
570 relationships: KeygenRelationships {
571 policy: None,
572 account: None,
573 product: None,
574 group: None,
575 owner: None,
576 users: None,
577 machines: None,
578 environment: None,
579 license: None,
580 other: HashMap::new(),
581 },
582 };
583
584 let machine = Machine::from(machine_data);
585
586 assert_eq!(machine.account_id, None);
587 assert_eq!(machine.environment_id, None);
588 assert_eq!(machine.product_id, None);
589 assert_eq!(machine.license_id, None);
590 assert_eq!(machine.owner_id, None);
591 assert_eq!(machine.group_id, None);
592 }
593}