ffs_cli/providers/hetzner/
mod.rs

1use 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        // Authenticate using a private key
139        sess.userauth_pubkey_file("root", None, Path::new(&config.ssh_key_path), None)?;
140
141        // Open a channel
142        let mut channel = sess.channel_session()?;
143
144        // Execute command to read log file
145        channel.exec(&format!("cat {}", &filename))?;
146
147        // Read the output
148        let mut s = String::new();
149        channel.read_to_string(&mut s)?;
150
151        // Print the logs
152        println!("{s}");
153
154        // Close the channel
155        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        // Read and print output in real-time
183        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}