xnode_deployer/hivelocity/
mod.rs1use 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 BareMetal {
220 location_name: String,
221 period: String,
222 tags: Option<Vec<String>>,
223 product_id: u64,
224 hostname: String,
225 },
226 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}