ffs_cli/providers/hetzner/
mod.rs1use std::io::prelude::*;
2use std::net::TcpStream;
3use std::path::Path;
4
5use async_trait::async_trait;
6use hcloud::apis::configuration::Configuration;
7use hcloud::apis::servers_api;
8use hcloud::apis::servers_api::{CreateServerParams, DeleteServerParams, ListServersParams};
9use hcloud::models::CreateServerRequest;
10use ssh2::Session;
11
12use super::Provider;
13use crate::config::Config;
14use crate::jobs::Job;
15
16#[async_trait]
17impl Provider for HetznerProvider {
18 async fn start_job(&self, name: &str) -> Result<Job, Box<dyn std::error::Error + Send + Sync>> {
19 let config = Config::new();
20 let mut configuration = Configuration::new();
21 configuration.bearer_access_token = Some(config.hcloud_api_token);
22
23 let params = CreateServerParams {
24 create_server_request: Some(CreateServerRequest {
25 name: name.to_string(),
26 image: config.image,
27 server_type: config.server_type,
28 location: Some(config.location),
29 ssh_keys: Some(vec![config.ssh_key_name]),
30 user_data: Some(config.user_data),
31 ..Default::default()
32 }),
33 };
34 let res = servers_api::create_server(&configuration, params).await?;
35
36 let job = Job {
37 id: res.server.id.to_string(),
38 ipv4: res.server.public_net.ipv4.unwrap().ip,
39 name: Some(name.to_string()),
40 };
41
42 let ip = job.ipv4.clone();
43 let key_path = config.ssh_key_path.clone();
44 tokio::spawn(async move {
45 let _ =
46 tokio::task::spawn_blocking(move || super::install_over_ssh(&ip, &key_path)).await;
47 });
48
49 Ok(job)
50 }
51
52 async fn get_job(
53 &self,
54 id: &str,
55 ) -> Result<Option<Job>, Box<dyn std::error::Error + Send + Sync>> {
56 let config = Config::new();
57 let mut configuration = Configuration::new();
58 configuration.bearer_access_token = Some(config.hcloud_api_token);
59
60 let server = servers_api::get_server(
61 &configuration,
62 hcloud::apis::servers_api::GetServerParams {
63 id: id.parse::<i64>().unwrap(),
64 },
65 )
66 .await?
67 .server;
68
69 server.map_or_else(
70 || Ok(None),
71 |server| {
72 Ok(Some(Job {
73 id: server.id.to_string(),
74 ipv4: server.public_net.ipv4.unwrap().ip,
75 name: Some(server.name),
76 }))
77 },
78 )
79 }
80
81 async fn stop_job(
82 &self,
83 job_id: &str,
84 ) -> Result<Job, Box<dyn std::error::Error + Send + Sync>> {
85 let config = Config::new();
86 let mut configuration = Configuration::new();
87 configuration.bearer_access_token = Some(config.hcloud_api_token);
88 let params = DeleteServerParams {
89 id: job_id.parse::<i64>().unwrap(),
90 };
91 servers_api::delete_server(&configuration, params).await?;
92
93 Ok(Job {
94 id: job_id.to_string(),
95 ipv4: String::new(),
96 name: None,
97 })
98 }
99
100 async fn list_jobs(&self) -> Result<Vec<Job>, Box<dyn std::error::Error + Send + Sync>> {
101 let config = Config::new();
102 let mut configuration = Configuration::new();
103 configuration.bearer_access_token = Some(config.hcloud_api_token);
104
105 let servers = servers_api::list_servers(&configuration, ListServersParams::default())
106 .await?
107 .servers;
108
109 let jobs = servers
110 .into_iter()
111 .map(|server| Job {
112 id: server.id.to_string(),
113 ipv4: server.public_net.ipv4.unwrap().ip,
114 name: Some(server.name),
115 })
116 .collect();
117
118 Ok(jobs)
119 }
120
121 async fn tail(
122 &self,
123 id: &str,
124 filename: &str,
125 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
126 let config = Config::new();
127 let mut configuration = Configuration::new();
128 configuration.bearer_access_token = Some(config.hcloud_api_token);
129
130 let job = self.get_job(id).await?;
131
132 let ipv4 = job.unwrap().ipv4;
133 let tcp = TcpStream::connect((ipv4.as_str(), 22))?;
134 let mut sess = Session::new()?;
135 sess.set_tcp_stream(tcp);
136 sess.handshake()?;
137
138 sess.userauth_pubkey_file("root", None, Path::new(&config.ssh_key_path), None)?;
140
141 let mut channel = sess.channel_session()?;
143
144 channel.exec(&format!("cat {}", &filename))?;
146
147 let mut s = String::new();
149 channel.read_to_string(&mut s)?;
150
151 println!("{s}");
153
154 channel.wait_close()?;
156 println!("{}", channel.exit_status()?);
157
158 Ok(())
159 }
160
161 async fn scp(
162 &self,
163 id: &str,
164 filename: &str,
165 destination: &str,
166 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
167 let config = Config::new();
168 let mut configuration = Configuration::new();
169 configuration.bearer_access_token = Some(config.hcloud_api_token);
170
171 let job = self.get_job(id).await?;
172
173 let ipv4 = job.unwrap().ipv4;
174 let tcp = TcpStream::connect((ipv4.as_str(), 22))?;
175 let mut sess = Session::new()?;
176 sess.set_tcp_stream(tcp);
177 sess.handshake()?;
178
179 let mut channel = sess.channel_session()?;
180 channel.exec(&format!("scp -r root@{ipv4}:{filename} {destination}"))?;
181
182 let mut buffer = [0; 1024];
184 loop {
185 match channel.read(&mut buffer) {
186 Ok(0) => break,
187 Ok(n) => {
188 let output = String::from_utf8_lossy(&buffer[..n]);
189 print!("{output}");
190 }
191 Err(e) => {
192 if e.kind() == std::io::ErrorKind::WouldBlock {
193 continue;
194 }
195 return Err(Box::new(e));
196 }
197 }
198 }
199
200 channel.wait_close()?;
201 println!("{}", channel.exit_status()?);
202
203 Ok(())
204 }
205}
206
207#[derive(Clone)]
208pub struct HetznerProvider {}
209
210impl Default for HetznerProvider {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216impl HetznerProvider {
217 #[must_use]
218 pub const fn new() -> Self {
219 Self {}
220 }
221}