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            "Deploying Xnode with configuration {input:?} on {hardware:?}",
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        .map_err(Error::ReqwestError)?
113        .json::<serde_json::Value>()
114        .await
115        .map_err(Error::ReqwestError)?;
116
117        let device_id = match &response {
118            serde_json::Value::Object(map) => map
119                .get("deviceId")
120                .ok_or(Error::XnodeDeployerError(XnodeDeployerError::new(
121                    XnodeDeployerErrorInner::HivelocityError(
122                        HivelocityError::ResponseMissingDeviceId { map: map.clone() },
123                    ),
124                )))
125                .and_then(|device_id| {
126                    match device_id {
127                        serde_json::Value::Number(number) => number.as_u64(),
128                        _ => None,
129                    }
130                    .ok_or(Error::XnodeDeployerError(XnodeDeployerError::new(
131                        XnodeDeployerErrorInner::HivelocityError(
132                            HivelocityError::ResponseInvalidDeviceId {
133                                device_id: device_id.clone(),
134                            },
135                        ),
136                    )))
137                }),
138            _ => Err(Error::XnodeDeployerError(XnodeDeployerError::new(
139                XnodeDeployerErrorInner::HivelocityError(HivelocityError::ResponseNotObject {
140                    response: response.clone(),
141                }),
142            ))),
143        };
144        let device_id = match device_id {
145            Ok(device_id) => device_id,
146            Err(e) => return Err(e),
147        };
148
149        let mut ip = "0.0.0.0".to_string();
150        while ip == "0.0.0.0" {
151            log::info!("Getting ip address of hivelocity device {device_id}",);
152            if let serde_json::Value::Object(map) = &response {
153                if let Some(serde_json::Value::String(primary_ip)) = map.get("primaryIp") {
154                    ip = primary_ip.clone();
155                }
156            };
157
158            sleep(Duration::from_secs(1)).await;
159            let scope = match self.hardware {
160                HivelocityHardware::BareMetal { .. } => "bare-metal-devices",
161                HivelocityHardware::Compute { .. } => "compute",
162            };
163            response = self
164                .client
165                .get(format!(
166                    "https://core.hivelocity.net/api/v2/{scope}/{device_id}"
167                ))
168                .header("X-API-KEY", self.api_key.clone())
169                .send()
170                .await
171                .map_err(Error::ReqwestError)?
172                .json::<serde_json::Value>()
173                .await
174                .map_err(Error::ReqwestError)?;
175        }
176
177        let output = DeployOutput::<Self::ProviderOutput> {
178            ip,
179            provider: HivelocityOutput { device_id },
180        };
181        log::info!("Hivelocity deployment succeeded: {output:?}");
182        Ok(output)
183    }
184}
185
186#[derive(Debug)]
187pub struct HivelocityOutput {
188    pub device_id: u64,
189}
190
191#[derive(Debug)]
192pub enum HivelocityHardware {
193    // https://developers.hivelocity.net/reference/post_bare_metal_device_resource
194    BareMetal {
195        location_name: String,
196        period: String,
197        tags: Option<Vec<String>>,
198        product_id: u64,
199        hostname: String,
200    },
201    // https://developers.hivelocity.net/reference/post_compute_resource
202    Compute {
203        location_name: String,
204        period: String,
205        tags: Option<Vec<String>>,
206        product_id: u64,
207        hostname: String,
208    },
209}