xnode_deployer/hivelocity/
mod.rs

1use std::{fmt::Display, time::Duration};
2
3use reqwest::Client;
4use serde_json::json;
5use tokio::time::sleep;
6
7use crate::{
8    DeployInput, DeployOutput, Error, XnodeDeployer, XnodeDeployerError,
9    utils::XnodeDeployerErrorInner,
10};
11
12#[derive(Debug)]
13pub enum HivelocityError {
14    ResponseNotObject {
15        response: serde_json::Value,
16    },
17    ResponseMissingDeviceId {
18        map: serde_json::Map<String, serde_json::Value>,
19    },
20    ResponseInvalidDeviceId {
21        device_id: serde_json::Value,
22    },
23}
24
25impl Display for HivelocityError {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.write_str(
28            match self {
29                HivelocityError::ResponseNotObject { response } => {
30                    format!("Hivelocity response not object: {response}")
31                }
32                HivelocityError::ResponseMissingDeviceId { map } => {
33                    format!("Hivelocity response missing device id: {map:?}")
34                }
35                HivelocityError::ResponseInvalidDeviceId { device_id } => {
36                    format!("Hivelocity response invalid device id: {device_id}")
37                }
38            }
39            .as_str(),
40        )
41    }
42}
43
44pub struct HivelocityDeployer {
45    client: Client,
46    api_key: String,
47    hardware: HivelocityHardware,
48}
49
50impl HivelocityDeployer {
51    pub fn new(api_key: String, hardware: HivelocityHardware) -> Self {
52        Self {
53            client: Client::new(),
54            api_key,
55            hardware,
56        }
57    }
58}
59
60impl XnodeDeployer for HivelocityDeployer {
61    type ProviderOutput = HivelocityOutput;
62
63    async fn deploy(
64        &self,
65        input: DeployInput,
66    ) -> Result<DeployOutput<Self::ProviderOutput>, Error> {
67        log::info!(
68            "Hivelocity deployment of {input:?} on {hardware:?} started",
69            hardware = self.hardware
70        );
71        let mut response = match &self.hardware {
72            HivelocityHardware::BareMetal {
73                location_name,
74                period,
75                tags,
76                product_id,
77                hostname,
78            } => self
79                .client
80                .post("https://core.hivelocity.net/api/v2/bare-metal-devices/")
81                .json(&json!({
82                    "locationName": location_name,
83                    "period": period,
84                    "tags": tags,
85                    "script": input.cloud_init(),
86                    "productId": product_id,
87                    "osName": "Ubuntu 24.04",
88                    "hostname": hostname
89                })),
90            HivelocityHardware::Compute {
91                location_name,
92                period,
93                tags,
94                product_id,
95                hostname,
96            } => self
97                .client
98                .post("https://core.hivelocity.net/api/v2/compute/")
99                .json(&json!({
100                    "locationName": location_name,
101                    "period": period,
102                    "tags": tags,
103                    "script": input.cloud_init(),
104                    "productId": product_id,
105                    "osName": "Ubuntu 24.04 (VPS)",
106                    "hostname": hostname
107                })),
108        }
109        .header("X-API-KEY", self.api_key.clone())
110        .send()
111        .await
112        .and_then(|response| response.error_for_status())
113        .map_err(Error::ReqwestError)?
114        .json::<serde_json::Value>()
115        .await
116        .map_err(Error::ReqwestError)?;
117
118        let device_id = match &response {
119            serde_json::Value::Object(map) => map
120                .get("deviceId")
121                .ok_or(Error::XnodeDeployerError(XnodeDeployerError::new(
122                    XnodeDeployerErrorInner::HivelocityError(
123                        HivelocityError::ResponseMissingDeviceId { map: map.clone() },
124                    ),
125                )))
126                .and_then(|device_id| {
127                    match device_id {
128                        serde_json::Value::Number(number) => number.as_u64(),
129                        _ => None,
130                    }
131                    .ok_or(Error::XnodeDeployerError(XnodeDeployerError::new(
132                        XnodeDeployerErrorInner::HivelocityError(
133                            HivelocityError::ResponseInvalidDeviceId {
134                                device_id: device_id.clone(),
135                            },
136                        ),
137                    )))
138                }),
139            _ => Err(Error::XnodeDeployerError(XnodeDeployerError::new(
140                XnodeDeployerErrorInner::HivelocityError(HivelocityError::ResponseNotObject {
141                    response: response.clone(),
142                }),
143            ))),
144        };
145        let device_id = match device_id {
146            Ok(device_id) => device_id,
147            Err(e) => return Err(e),
148        };
149
150        let mut ip = "0.0.0.0".to_string();
151        while ip == "0.0.0.0" {
152            log::info!("Getting ip address of hivelocity device {device_id}",);
153            if let serde_json::Value::Object(map) = &response {
154                if let Some(serde_json::Value::String(primary_ip)) = map.get("primaryIp") {
155                    ip = primary_ip.clone();
156                }
157            };
158
159            sleep(Duration::from_secs(1)).await;
160            let scope = match self.hardware {
161                HivelocityHardware::BareMetal { .. } => "bare-metal-devices",
162                HivelocityHardware::Compute { .. } => "compute",
163            };
164            response = self
165                .client
166                .get(format!(
167                    "https://core.hivelocity.net/api/v2/{scope}/{device_id}"
168                ))
169                .header("X-API-KEY", self.api_key.clone())
170                .send()
171                .await
172                .map_err(Error::ReqwestError)?
173                .json::<serde_json::Value>()
174                .await
175                .map_err(Error::ReqwestError)?;
176        }
177
178        let output = DeployOutput::<Self::ProviderOutput> {
179            ip,
180            provider: HivelocityOutput { device_id },
181        };
182        log::info!("Hivelocity deployment succeeded: {output:?}");
183        Ok(output)
184    }
185
186    async fn undeploy(&self, xnode: DeployOutput<Self::ProviderOutput>) -> Option<Error> {
187        let device_id = xnode.provider.device_id;
188        log::info!("Undeploying hivelocity device {device_id} started",);
189        let scope = match self.hardware {
190            HivelocityHardware::BareMetal { .. } => "bare-metal-devices",
191            HivelocityHardware::Compute { .. } => "compute",
192        };
193        if let Err(e) = self
194            .client
195            .delete(format!(
196                "https://core.hivelocity.net/api/v2/{scope}/{device_id}"
197            ))
198            .header("X-API-KEY", self.api_key.clone())
199            .send()
200            .await
201            .and_then(|response| response.error_for_status())
202        {
203            return Some(Error::ReqwestError(e));
204        }
205
206        log::info!("Undeploying hivelocity device {device_id} succeeded");
207        None
208    }
209}
210
211#[derive(Debug)]
212pub struct HivelocityOutput {
213    pub device_id: u64,
214}
215
216#[derive(Debug)]
217pub enum HivelocityHardware {
218    // https://developers.hivelocity.net/reference/post_bare_metal_device_resource
219    BareMetal {
220        location_name: String,
221        period: String,
222        tags: Option<Vec<String>>,
223        product_id: u64,
224        hostname: String,
225    },
226    // https://developers.hivelocity.net/reference/post_compute_resource
227    Compute {
228        location_name: String,
229        period: String,
230        tags: Option<Vec<String>>,
231        product_id: u64,
232        hostname: String,
233    },
234}
235
236#[derive(Debug)]
237pub enum HivelocityUndeployInput {
238    BareMetal { device_id: u64 },
239    Compute { device_id: u64 },
240}