phala_tee_deploy_rs/client.rs
1use reqwest::Client;
2use serde_json::json;
3use std::collections::HashMap;
4use std::time::Duration;
5
6use crate::{
7 config::DeploymentConfig,
8 crypto::Encryptor,
9 error::Error,
10 types::{
11 AttestationResponse, ComposeResponse, CvmInfo, CvmStateResponse, DeploymentResponse,
12 NetworkInfoResponse, SystemStatsResponse, VmConfig,
13 },
14 PubkeyResponse, TeePodDiscoveryResponse,
15};
16
17/// Client for interacting with the Phala TEE Cloud API.
18///
19/// `TeeClient` provides low-level access to the Phala Cloud API for deploying
20/// and managing containerized applications in a Trusted Execution Environment (TEE).
21/// This client handles authentication, API requests, and encryption of sensitive data.
22///
23/// For most use cases, consider using the higher-level `TeeDeployer` API instead,
24/// which provides a more ergonomic interface built on top of this client.
25///
26/// # Features
27///
28/// * Direct API access to the Phala TEE Cloud
29/// * Secure environment variable encryption
30/// * TEEPod discovery and selection
31/// * Application deployment and management
32pub struct TeeClient {
33 client: Client,
34 config: DeploymentConfig,
35}
36
37impl TeeClient {
38 /// Creates a new `TeeClient` with the given configuration.
39 ///
40 /// # Parameters
41 ///
42 /// * `config` - The deployment configuration including API credentials and default settings
43 ///
44 /// # Returns
45 ///
46 /// A new `TeeClient` instance if successful
47 ///
48 /// # Errors
49 ///
50 /// Returns an error if the HTTP client cannot be created
51 pub fn new(config: DeploymentConfig) -> Result<Self, Error> {
52 let client = Client::builder()
53 .timeout(Duration::from_secs(30))
54 .build()
55 .map_err(Error::HttpClient)?;
56
57 Ok(Self { client, config })
58 }
59
60 /// Deploys a container to the TEE environment using the client's configuration.
61 ///
62 /// This method uses the configuration set during client creation to deploy
63 /// an application. It handles VM configuration, encryption, and API communication.
64 ///
65 /// # Returns
66 ///
67 /// A `DeploymentResponse` containing the deployment details if successful
68 ///
69 /// # Errors
70 ///
71 /// Returns an error if:
72 /// * The API request fails
73 /// * Environment variables cannot be encrypted
74 /// * The API returns an error response
75 pub async fn deploy(&self) -> Result<DeploymentResponse, Error> {
76 // Get or create VM configuration
77 let vm_config = self.config.vm_config.clone().unwrap_or_else(|| VmConfig {
78 name: format!("tee-deploy-{}", uuid::Uuid::new_v4()),
79 compose_manifest: crate::types::ComposeManifest {
80 name: "tee-deployment".to_string(),
81 features: vec!["kms".to_string(), "tproxy-net".to_string()],
82 docker_compose_file: self.config.docker_compose.clone(),
83 },
84 vcpu: 2,
85 memory: 8192,
86 disk_size: 40,
87 teepod_id: self.config.teepod_id,
88 image: self.config.image.clone(),
89 advanced_features: crate::types::AdvancedFeatures {
90 tproxy: true,
91 kms: true,
92 public_sys_info: true,
93 public_logs: true,
94 docker_config: crate::types::DockerConfig {
95 username: String::new(),
96 password: String::new(),
97 registry: None,
98 },
99 listed: false,
100 },
101 });
102
103 // Get encryption public key
104 let pubkey_response = self.get_pubkey(&vm_config).await?;
105
106 // Encrypt environment variables
107 let env_vars: Vec<_> = self
108 .config
109 .env_vars
110 .iter()
111 .map(|(k, v)| (k.clone(), v.clone()))
112 .collect();
113
114 let encrypted_env =
115 Encryptor::encrypt_env_vars(&env_vars, &pubkey_response.app_env_encrypt_pubkey)?;
116
117 // Create a mutable request body from vm_config
118 let mut request_body = serde_json::to_value(&vm_config)
119 .unwrap()
120 .as_object()
121 .cloned()
122 .unwrap_or_default();
123
124 // Add the additional fields
125 request_body.insert(
126 "encrypted_env".to_string(),
127 serde_json::Value::String(encrypted_env),
128 );
129 request_body.insert(
130 "app_env_encrypt_pubkey".to_string(),
131 serde_json::Value::String(pubkey_response.app_env_encrypt_pubkey.clone()),
132 );
133
134 // Create deployment
135 let response = self
136 .client
137 .post(format!(
138 "{}/cvms/from_cvm_configuration",
139 self.config.api_url
140 ))
141 .header("Content-Type", "application/json")
142 .header("x-api-key", &self.config.api_key)
143 .json(&request_body)
144 .send()
145 .await?;
146
147 if !response.status().is_success() {
148 return Err(Error::Api {
149 status_code: response.status().as_u16(),
150 message: response.text().await?,
151 });
152 }
153
154 response
155 .json::<DeploymentResponse>()
156 .await
157 .map_err(Error::HttpClient)
158 }
159
160 /// Retrieves the encryption public key for a given VM configuration.
161 ///
162 /// This is a helper method used internally to get the public key needed
163 /// for securely encrypting environment variables.
164 ///
165 /// # Parameters
166 ///
167 /// * `vm_config` - The VM configuration to get a public key for
168 ///
169 /// # Returns
170 ///
171 /// A JSON value containing the public key and salt if successful
172 ///
173 /// # Errors
174 ///
175 /// Returns an error if the API request fails or returns an error
176 async fn get_pubkey(&self, vm_config: &VmConfig) -> Result<PubkeyResponse, Error> {
177 let response = self
178 .client
179 .post(format!(
180 "{}/cvms/pubkey/from_cvm_configuration",
181 self.config.api_url
182 ))
183 .header("Content-Type", "application/json")
184 .header("x-api-key", &self.config.api_key)
185 .json(&vm_config)
186 .send()
187 .await?;
188
189 if !response.status().is_success() {
190 return Err(Error::Api {
191 status_code: response.status().as_u16(),
192 message: response.text().await?,
193 });
194 }
195
196 response
197 .json::<PubkeyResponse>()
198 .await
199 .map_err(Error::HttpClient)
200 }
201
202 /// Retrieves the current Docker Compose configuration for an application.
203 ///
204 /// # Parameters
205 ///
206 /// * `app_id` - The ID of the application to get the configuration for
207 ///
208 /// # Returns
209 ///
210 /// A `ComposeResponse` containing the compose file and encryption public key
211 ///
212 /// # Errors
213 ///
214 /// Returns an error if the API request fails or the application is not found
215 pub async fn get_compose(&self, app_id: &str) -> Result<ComposeResponse, Error> {
216 let response = self
217 .client
218 .get(format!("{}/cvms/{}/compose", self.config.api_url, app_id))
219 .header("Content-Type", "application/json")
220 .header("x-api-key", &self.config.api_key)
221 .send()
222 .await?;
223
224 if !response.status().is_success() {
225 return Err(Error::Api {
226 status_code: response.status().as_u16(),
227 message: response.text().await?,
228 });
229 }
230
231 response
232 .json::<ComposeResponse>()
233 .await
234 .map_err(Error::HttpClient)
235 }
236
237 /// Updates the Docker Compose configuration for an existing application.
238 ///
239 /// This method can update both the application configuration and its
240 /// environment variables.
241 ///
242 /// # Parameters
243 ///
244 /// * `app_id` - The ID of the application to update
245 /// * `compose_file` - The new Docker Compose configuration
246 /// * `env_vars` - Optional new environment variables
247 /// * `env_pubkey` - The public key for encrypting environment variables
248 ///
249 /// # Returns
250 ///
251 /// A JSON value containing the update operation result
252 ///
253 /// # Errors
254 ///
255 /// Returns an error if:
256 /// * The API request fails
257 /// * The application is not found
258 /// * Environment variables cannot be encrypted
259 pub async fn update_compose(
260 &self,
261 app_id: &str,
262 compose_file: serde_json::Value,
263 env_vars: Option<HashMap<String, String>>,
264 env_pubkey: String,
265 ) -> Result<serde_json::Value, Error> {
266 let mut body = json!({
267 "compose_manifest": compose_file
268 });
269
270 // Encrypt environment variables if provided
271 if let Some(vars) = env_vars {
272 let env_vars: Vec<_> = vars.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
273 let encrypted_env = Encryptor::encrypt_env_vars(&env_vars, &env_pubkey)?;
274 body["encrypted_env"] = json!(encrypted_env);
275 }
276
277 let response = self
278 .client
279 .put(format!("{}/cvms/{}/compose", self.config.api_url, app_id))
280 .header("Content-Type", "application/json")
281 .header("x-api-key", &self.config.api_key)
282 .json(&body)
283 .send()
284 .await?;
285
286 if !response.status().is_success() {
287 return Err(Error::Api {
288 status_code: response.status().as_u16(),
289 message: response.text().await?,
290 });
291 }
292
293 response.json().await.map_err(Error::HttpClient)
294 }
295
296 /// Retrieves a list of available TEEPods from the Phala Cloud API.
297 ///
298 /// This method queries the API for TEEPods that are available for deployment,
299 /// providing detailed diagnostics for any connection issues.
300 ///
301 /// # Returns
302 ///
303 /// A JSON value containing the list of available TEEPods if successful
304 ///
305 /// # Errors
306 ///
307 /// Returns an error if:
308 /// * The network request fails (timeout, connection issues, etc.)
309 /// * The API returns an error response
310 /// * The response cannot be parsed as valid JSON
311 pub async fn get_available_teepods(&self) -> Result<TeePodDiscoveryResponse, Error> {
312 let response = self
313 .client
314 .get(format!("{}/teepods/available", self.config.api_url))
315 .header("Content-Type", "application/json")
316 .header("x-api-key", &self.config.api_key)
317 .timeout(std::time::Duration::from_secs(15))
318 .send()
319 .await?;
320
321 if !response.status().is_success() {
322 return Err(Error::Api {
323 status_code: response.status().as_u16(),
324 message: response.text().await?,
325 });
326 }
327
328 response.json().await.map_err(Error::HttpClient)
329 }
330
331 /// Retrieves the encryption public key for a custom VM configuration.
332 ///
333 /// # Parameters
334 ///
335 /// * `vm_config` - The VM configuration as a JSON value
336 ///
337 /// # Returns
338 ///
339 /// A JSON value containing the public key and salt for encryption
340 ///
341 /// # Errors
342 ///
343 /// Returns an error if the API request fails or returns an error
344 pub async fn get_pubkey_for_config(
345 &self,
346 vm_config: &serde_json::Value,
347 ) -> Result<PubkeyResponse, Error> {
348 let response = self
349 .client
350 .post(format!(
351 "{}/cvms/pubkey/from_cvm_configuration",
352 self.config.api_url
353 ))
354 .header("Content-Type", "application/json")
355 .header("x-api-key", &self.config.api_key)
356 .json(&vm_config)
357 .send()
358 .await?;
359
360 if !response.status().is_success() {
361 return Err(Error::Api {
362 status_code: response.status().as_u16(),
363 message: response.text().await?,
364 });
365 }
366
367 response.json().await.map_err(Error::HttpClient)
368 }
369
370 /// Deploys a container with a custom VM configuration and encrypts environment variables.
371 ///
372 /// This method handles the encryption of environment variables and then calls
373 /// `deploy_with_config_encrypted_env` to perform the actual deployment.
374 ///
375 /// # Parameters
376 ///
377 /// * `vm_config` - The VM configuration as a JSON value
378 /// * `env_vars` - Environment variables to encrypt and include in the deployment
379 /// * `app_env_encrypt_pubkey` - The public key for encrypting environment variables
380 /// * `app_id_salt` - The salt value for encryption
381 ///
382 /// # Returns
383 ///
384 /// A `DeploymentResponse` containing the deployment details if successful
385 ///
386 /// # Errors
387 ///
388 /// Returns an error if:
389 /// * Environment variable encryption fails
390 /// * The API request fails
391 /// * The API returns an error response
392 pub async fn deploy_with_config_do_encrypt(
393 &self,
394 vm_config: serde_json::Value,
395 env_vars: &[(String, String)],
396 app_env_encrypt_pubkey: &str,
397 app_id_salt: &str,
398 ) -> Result<DeploymentResponse, Error> {
399 // Encrypt environment variables
400 let encrypted_env = Encryptor::encrypt_env_vars(env_vars, app_env_encrypt_pubkey)?;
401
402 self.deploy_with_config_encrypted_env(
403 vm_config,
404 encrypted_env,
405 app_env_encrypt_pubkey,
406 app_id_salt,
407 )
408 .await
409 }
410
411 /// Deploys a container with a custom VM configuration and pre-encrypted environment variables.
412 ///
413 /// This method is the final step in the deployment process, sending the VM configuration
414 /// and encrypted environment variables to the API.
415 ///
416 /// # Parameters
417 ///
418 /// * `vm_config` - The VM configuration as a JSON value
419 /// * `encrypted_env` - Pre-encrypted environment variables as a string
420 /// * `app_env_encrypt_pubkey` - The public key used for encryption
421 /// * `app_id_salt` - The salt value used for encryption
422 ///
423 /// # Returns
424 ///
425 /// A `DeploymentResponse` containing the deployment details if successful
426 ///
427 /// # Errors
428 ///
429 /// Returns an error if the API request fails or returns an error
430 pub async fn deploy_with_config_encrypted_env(
431 &self,
432 vm_config: serde_json::Value,
433 encrypted_env: String,
434 app_env_encrypt_pubkey: &str,
435 app_id_salt: &str,
436 ) -> Result<DeploymentResponse, Error> {
437 // Create a mutable request body
438 let mut request_body = vm_config.as_object().cloned().unwrap_or_default();
439
440 // Add the additional fields
441 request_body.insert(
442 "encrypted_env".to_string(),
443 serde_json::Value::String(encrypted_env),
444 );
445 request_body.insert(
446 "app_env_encrypt_pubkey".to_string(),
447 serde_json::Value::String(app_env_encrypt_pubkey.to_string()),
448 );
449 request_body.insert(
450 "app_id_salt".to_string(),
451 serde_json::Value::String(app_id_salt.to_string()),
452 );
453
454 // Create deployment
455 let response = self
456 .client
457 .post(format!(
458 "{}/cvms/from_cvm_configuration",
459 self.config.api_url
460 ))
461 .header("Content-Type", "application/json")
462 .header("x-api-key", &self.config.api_key)
463 .json(&request_body)
464 .send()
465 .await?;
466
467 if !response.status().is_success() {
468 return Err(Error::Api {
469 status_code: response.status().as_u16(),
470 message: response.text().await?,
471 });
472 }
473
474 response
475 .json::<DeploymentResponse>()
476 .await
477 .map_err(Error::HttpClient)
478 }
479
480 /// Provisions a new ELIZA chatbot deployment.
481 ///
482 /// This method initiates the ELIZA deployment process by requesting an app_id
483 /// and encryption key from the API. This is the first step in the two-step
484 /// deployment process.
485 ///
486 /// # Parameters
487 ///
488 /// * `name` - Name for the ELIZA deployment
489 /// * `character_file` - Character configuration file content
490 /// * `env_keys` - List of environment variable keys to include
491 /// * `image` - Docker image to use for the deployment
492 ///
493 /// # Returns
494 ///
495 /// A tuple containing:
496 /// * `app_id` - The ID of the provisioned application
497 /// * `app_env_encrypt_pubkey` - The public key for encrypting environment variables
498 ///
499 /// # Errors
500 ///
501 /// Returns an error if:
502 /// * The API request fails
503 /// * Invalid configuration is provided
504 /// * The response cannot be parsed
505 pub async fn provision_eliza(
506 &self,
507 name: String,
508 character_file: String,
509 env_keys: Vec<String>,
510 image: String,
511 ) -> Result<(String, String), Error> {
512 let request_body = serde_json::json!({
513 "name": name,
514 "characterfile": character_file,
515 "env_keys": env_keys,
516 "teepod_id": self.config.teepod_id,
517 "image": image
518 });
519
520 let response = self
521 .client
522 .post(format!("{}/cvms/provision/eliza", self.config.api_url))
523 .header("Content-Type", "application/json")
524 .header("x-api-key", &self.config.api_key)
525 .json(&request_body)
526 .send()
527 .await?;
528
529 if !response.status().is_success() {
530 return Err(Error::Api {
531 status_code: response.status().as_u16(),
532 message: response.text().await?,
533 });
534 }
535
536 // Get the response as JSON Value to extract necessary fields
537 let provision_response = response.json::<serde_json::Value>().await?;
538
539 // Extract required values
540 let app_id = match provision_response.get("app_id") {
541 Some(id) => id.as_str().ok_or_else(|| {
542 Error::Configuration("Missing app_id in provision response".to_string())
543 })?,
544 None => {
545 return Err(Error::Configuration(
546 "Missing app_id in provision response".to_string(),
547 ))
548 }
549 };
550
551 let app_env_encrypt_pubkey = match provision_response.get("app_env_encrypt_pubkey") {
552 Some(key) => key.as_str().ok_or_else(|| {
553 Error::Configuration("Missing encryption key in provision response".to_string())
554 })?,
555 None => {
556 return Err(Error::Configuration(
557 "Missing encryption key in provision response".to_string(),
558 ))
559 }
560 };
561
562 Ok((app_id.to_string(), app_env_encrypt_pubkey.to_string()))
563 }
564
565 /// Creates a VM for an ELIZA deployment with encrypted environment variables.
566 ///
567 /// This method is the second step in the ELIZA deployment process, creating
568 /// the actual VM with the provided encrypted environment variables.
569 ///
570 /// # Parameters
571 ///
572 /// * `app_id` - The ID of the provisioned application
573 /// * `encrypted_env` - Pre-encrypted environment variables
574 ///
575 /// # Returns
576 ///
577 /// A `DeploymentResponse` containing the deployment details and status
578 ///
579 /// # Errors
580 ///
581 /// Returns an error if:
582 /// * The API request fails
583 /// * The deployment cannot be created
584 /// * The response cannot be parsed
585 pub async fn create_eliza_vm(
586 &self,
587 app_id: &str,
588 encrypted_env: &str,
589 ) -> Result<DeploymentResponse, Error> {
590 // Create the VM
591 let create_body = serde_json::json!({
592 "app_id": app_id,
593 "encrypted_env": encrypted_env
594 });
595
596 let create_response = self
597 .client
598 .post(format!("{}/cvms", self.config.api_url))
599 .header("Content-Type", "application/json")
600 .header("x-api-key", &self.config.api_key)
601 .json(&create_body)
602 .send()
603 .await?;
604
605 if !create_response.status().is_success() {
606 return Err(Error::Api {
607 status_code: create_response.status().as_u16(),
608 message: create_response.text().await?,
609 });
610 }
611
612 // Parse final response into DeploymentResponse
613 let response_text = create_response.text().await?;
614
615 match serde_json::from_str::<DeploymentResponse>(&response_text) {
616 Ok(deployment_response) => Ok(deployment_response),
617 Err(e) => {
618 // Try to extract ID from app_id
619 let mut response_json: serde_json::Value = serde_json::from_str(&response_text)
620 .map_err(|_| {
621 Error::Configuration(format!("Failed to parse response: {}", e))
622 })?;
623
624 // If response already has app_id, use it to build DeploymentResponse
625 if response_json.get("app_id").is_some() {
626 // Remove app_ prefix if necessary
627 let id_str = app_id.trim_start_matches("app_");
628
629 // Try to parse as number
630 let id = id_str.parse::<u64>().unwrap_or(0);
631
632 // Create a details map with all available information
633 let mut details = HashMap::new();
634 if let Some(obj) = response_json.as_object_mut() {
635 for (k, v) in obj {
636 details.insert(k.clone(), v.clone());
637 }
638 }
639
640 Ok(DeploymentResponse {
641 id,
642 status: "pending".to_string(),
643 details: Some(details),
644 })
645 } else {
646 Err(Error::Configuration(format!(
647 "Failed to parse deployment response: {}",
648 e
649 )))
650 }
651 }
652 }
653 }
654
655 /// Retrieves network information for a deployed application.
656 ///
657 /// This method fetches network connectivity details, status, and public URLs
658 /// for accessing the deployed application.
659 ///
660 /// # Parameters
661 ///
662 /// * `app_id` - The ID of the application to get network information for
663 ///
664 /// # Returns
665 ///
666 /// A `NetworkInfoResponse` containing network details including status and URLs
667 ///
668 /// # Errors
669 ///
670 /// Returns an error if:
671 /// * The API request fails
672 /// * The application is not found
673 /// * The network information cannot be retrieved
674 pub async fn get_network_info(&self, app_id: &str) -> Result<NetworkInfoResponse, Error> {
675 let response = self
676 .client
677 .get(format!("{}/cvms/{}/network", self.config.api_url, app_id))
678 .header("Content-Type", "application/json")
679 .header("x-api-key", &self.config.api_key)
680 .send()
681 .await?;
682
683 if !response.status().is_success() {
684 return Err(Error::Api {
685 status_code: response.status().as_u16(),
686 message: response.text().await?,
687 });
688 }
689
690 response
691 .json::<NetworkInfoResponse>()
692 .await
693 .map_err(Error::HttpClient)
694 }
695
696 /// Retrieves system statistics for a deployed application.
697 ///
698 /// This method fetches detailed system information including OS details,
699 /// CPU, memory, disk usage, and load averages for a deployed containerized application.
700 ///
701 /// # Parameters
702 ///
703 /// * `app_id` - The ID of the application to get system statistics for
704 ///
705 /// # Returns
706 ///
707 /// A `SystemStatsResponse` containing system information if successful
708 ///
709 /// # Errors
710 ///
711 /// Returns an error if:
712 /// * The API request fails
713 /// * The application is not found
714 /// * The system statistics cannot be retrieved
715 pub async fn get_system_stats(&self, app_id: &str) -> Result<SystemStatsResponse, Error> {
716 let response = self
717 .client
718 .get(format!("{}/cvms/{}/stats", self.config.api_url, app_id))
719 .header("Content-Type", "application/json")
720 .header("x-api-key", &self.config.api_key)
721 .send()
722 .await?;
723
724 if !response.status().is_success() {
725 return Err(Error::Api {
726 status_code: response.status().as_u16(),
727 message: response.text().await?,
728 });
729 }
730
731 response
732 .json::<SystemStatsResponse>()
733 .await
734 .map_err(Error::HttpClient)
735 }
736
737 // ─────────────────────────────────────────────────────────────────────
738 // CVM lifecycle
739 // ─────────────────────────────────────────────────────────────────────
740
741 /// Get CVM details including status.
742 /// `GET /api/v1/cvms/{cvm_id}`
743 pub async fn get_cvm(&self, cvm_id: &str) -> Result<CvmInfo, Error> {
744 let response = self
745 .client
746 .get(format!("{}/cvms/{}", self.config.api_url, cvm_id))
747 .header("Content-Type", "application/json")
748 .header("x-api-key", &self.config.api_key)
749 .send()
750 .await?;
751
752 if !response.status().is_success() {
753 return Err(Error::Api {
754 status_code: response.status().as_u16(),
755 message: response.text().await?,
756 });
757 }
758
759 response.json::<CvmInfo>().await.map_err(Error::HttpClient)
760 }
761
762 /// Get CVM state (running, stopped, etc.).
763 /// `GET /api/v1/cvms/{cvm_id}/state`
764 pub async fn get_state(&self, cvm_id: &str) -> Result<CvmStateResponse, Error> {
765 let response = self
766 .client
767 .get(format!("{}/cvms/{}/state", self.config.api_url, cvm_id))
768 .header("Content-Type", "application/json")
769 .header("x-api-key", &self.config.api_key)
770 .send()
771 .await?;
772
773 if !response.status().is_success() {
774 return Err(Error::Api {
775 status_code: response.status().as_u16(),
776 message: response.text().await?,
777 });
778 }
779
780 response
781 .json::<CvmStateResponse>()
782 .await
783 .map_err(Error::HttpClient)
784 }
785
786 /// Start a stopped CVM.
787 /// `POST /api/v1/cvms/{cvm_id}/start`
788 pub async fn start_cvm(&self, cvm_id: &str) -> Result<CvmInfo, Error> {
789 let response = self
790 .client
791 .post(format!("{}/cvms/{}/start", self.config.api_url, cvm_id))
792 .header("Content-Type", "application/json")
793 .header("x-api-key", &self.config.api_key)
794 .send()
795 .await?;
796
797 if !response.status().is_success() {
798 return Err(Error::Api {
799 status_code: response.status().as_u16(),
800 message: response.text().await?,
801 });
802 }
803
804 response.json::<CvmInfo>().await.map_err(Error::HttpClient)
805 }
806
807 /// Graceful shutdown (SIGTERM, then SIGKILL after timeout).
808 /// `POST /api/v1/cvms/{cvm_id}/shutdown`
809 pub async fn shutdown_cvm(&self, cvm_id: &str) -> Result<CvmInfo, Error> {
810 let response = self
811 .client
812 .post(format!(
813 "{}/cvms/{}/shutdown",
814 self.config.api_url, cvm_id
815 ))
816 .header("Content-Type", "application/json")
817 .header("x-api-key", &self.config.api_key)
818 .send()
819 .await?;
820
821 if !response.status().is_success() {
822 return Err(Error::Api {
823 status_code: response.status().as_u16(),
824 message: response.text().await?,
825 });
826 }
827
828 response.json::<CvmInfo>().await.map_err(Error::HttpClient)
829 }
830
831 /// Force stop (immediate, like power loss).
832 /// `POST /api/v1/cvms/{cvm_id}/stop`
833 pub async fn stop_cvm(&self, cvm_id: &str) -> Result<CvmInfo, Error> {
834 let response = self
835 .client
836 .post(format!("{}/cvms/{}/stop", self.config.api_url, cvm_id))
837 .header("Content-Type", "application/json")
838 .header("x-api-key", &self.config.api_key)
839 .send()
840 .await?;
841
842 if !response.status().is_success() {
843 return Err(Error::Api {
844 status_code: response.status().as_u16(),
845 message: response.text().await?,
846 });
847 }
848
849 response.json::<CvmInfo>().await.map_err(Error::HttpClient)
850 }
851
852 /// Permanently delete a stopped CVM (irreversible).
853 /// `DELETE /api/v1/cvms/{cvm_id}`
854 pub async fn delete_cvm(&self, cvm_id: &str) -> Result<(), Error> {
855 let response = self
856 .client
857 .delete(format!("{}/cvms/{}", self.config.api_url, cvm_id))
858 .header("Content-Type", "application/json")
859 .header("x-api-key", &self.config.api_key)
860 .send()
861 .await?;
862
863 if !response.status().is_success() {
864 return Err(Error::Api {
865 status_code: response.status().as_u16(),
866 message: response.text().await?,
867 });
868 }
869
870 Ok(())
871 }
872
873 /// Get TEE attestation data.
874 /// `GET /api/v1/cvms/{cvm_id}/attestation`
875 pub async fn get_attestation(&self, cvm_id: &str) -> Result<AttestationResponse, Error> {
876 let response = self
877 .client
878 .get(format!(
879 "{}/cvms/{}/attestation",
880 self.config.api_url, cvm_id
881 ))
882 .header("Content-Type", "application/json")
883 .header("x-api-key", &self.config.api_key)
884 .send()
885 .await?;
886
887 if !response.status().is_success() {
888 return Err(Error::Api {
889 status_code: response.status().as_u16(),
890 message: response.text().await?,
891 });
892 }
893
894 response
895 .json::<AttestationResponse>()
896 .await
897 .map_err(Error::HttpClient)
898 }
899}