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 "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 BareMetal {
195 location_name: String,
196 period: String,
197 tags: Option<Vec<String>>,
198 product_id: u64,
199 hostname: String,
200 },
201 Compute {
203 location_name: String,
204 period: String,
205 tags: Option<Vec<String>>,
206 product_id: u64,
207 hostname: String,
208 },
209}