purple_ssh/providers/
mod.rs1pub mod config;
2mod digitalocean;
3mod hetzner;
4mod linode;
5mod proxmox;
6pub mod sync;
7mod upcloud;
8mod vultr;
9
10use std::sync::atomic::AtomicBool;
11
12use thiserror::Error;
13
14#[derive(Debug, Clone)]
16#[allow(dead_code)]
17pub struct ProviderHost {
18 pub server_id: String,
20 pub name: String,
22 pub ip: String,
24 pub tags: Vec<String>,
26}
27
28#[derive(Debug, Error)]
30pub enum ProviderError {
31 #[error("HTTP error: {0}")]
32 Http(String),
33 #[error("Failed to parse response: {0}")]
34 Parse(String),
35 #[error("Authentication failed. Check your API token.")]
36 AuthFailed,
37 #[error("Rate limited. Try again in a moment.")]
38 RateLimited,
39 #[error("Cancelled.")]
40 Cancelled,
41 #[error("Partial result: {failures} of {total} failed")]
44 PartialResult {
45 hosts: Vec<ProviderHost>,
46 failures: usize,
47 total: usize,
48 },
49}
50
51pub trait Provider {
53 fn name(&self) -> &str;
55 fn short_label(&self) -> &str;
57 fn fetch_hosts_cancellable(
59 &self,
60 token: &str,
61 cancel: &AtomicBool,
62 ) -> Result<Vec<ProviderHost>, ProviderError>;
63 #[allow(dead_code)]
65 fn fetch_hosts(&self, token: &str) -> Result<Vec<ProviderHost>, ProviderError> {
66 self.fetch_hosts_cancellable(token, &AtomicBool::new(false))
67 }
68 fn fetch_hosts_with_progress(
70 &self,
71 token: &str,
72 cancel: &AtomicBool,
73 _progress: &dyn Fn(&str),
74 ) -> Result<Vec<ProviderHost>, ProviderError> {
75 self.fetch_hosts_cancellable(token, cancel)
76 }
77}
78
79pub const PROVIDER_NAMES: &[&str] = &["digitalocean", "vultr", "linode", "hetzner", "upcloud", "proxmox"];
81
82pub fn get_provider(name: &str) -> Option<Box<dyn Provider>> {
84 match name {
85 "digitalocean" => Some(Box::new(digitalocean::DigitalOcean)),
86 "vultr" => Some(Box::new(vultr::Vultr)),
87 "linode" => Some(Box::new(linode::Linode)),
88 "hetzner" => Some(Box::new(hetzner::Hetzner)),
89 "upcloud" => Some(Box::new(upcloud::UpCloud)),
90 "proxmox" => Some(Box::new(proxmox::Proxmox {
91 base_url: String::new(),
92 verify_tls: true,
93 })),
94 _ => None,
95 }
96}
97
98pub fn get_provider_with_config(name: &str, section: &config::ProviderSection) -> Option<Box<dyn Provider>> {
102 match name {
103 "proxmox" => Some(Box::new(proxmox::Proxmox {
104 base_url: section.url.clone(),
105 verify_tls: section.verify_tls,
106 })),
107 _ => get_provider(name),
108 }
109}
110
111pub fn provider_display_name(name: &str) -> &str {
113 match name {
114 "digitalocean" => "DigitalOcean",
115 "vultr" => "Vultr",
116 "linode" => "Linode",
117 "hetzner" => "Hetzner",
118 "upcloud" => "UpCloud",
119 "proxmox" => "Proxmox VE",
120 other => other,
121 }
122}
123
124pub(crate) fn http_agent() -> ureq::Agent {
126 ureq::AgentBuilder::new()
127 .timeout(std::time::Duration::from_secs(30))
128 .redirects(0)
129 .build()
130}
131
132pub(crate) fn http_agent_insecure() -> Result<ureq::Agent, ProviderError> {
134 let tls = ureq::native_tls::TlsConnector::builder()
135 .danger_accept_invalid_certs(true)
136 .danger_accept_invalid_hostnames(true)
137 .build()
138 .map_err(|e| ProviderError::Http(format!("TLS setup failed: {}", e)))?;
139 Ok(ureq::AgentBuilder::new()
140 .timeout(std::time::Duration::from_secs(30))
141 .redirects(0)
142 .tls_connector(std::sync::Arc::new(tls))
143 .build())
144}
145
146pub(crate) fn strip_cidr(ip: &str) -> &str {
150 if let Some(pos) = ip.rfind('/') {
152 if ip[pos + 1..].bytes().all(|b| b.is_ascii_digit()) && pos + 1 < ip.len() {
153 return &ip[..pos];
154 }
155 }
156 ip
157}
158
159fn map_ureq_error(err: ureq::Error) -> ProviderError {
161 match err {
162 ureq::Error::Status(401, _) | ureq::Error::Status(403, _) => ProviderError::AuthFailed,
163 ureq::Error::Status(429, _) => ProviderError::RateLimited,
164 ureq::Error::Status(code, _) => ProviderError::Http(format!("HTTP {}", code)),
165 ureq::Error::Transport(t) => ProviderError::Http(t.to_string()),
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_strip_cidr_ipv6_with_prefix() {
175 assert_eq!(strip_cidr("2600:3c00::1/128"), "2600:3c00::1");
176 assert_eq!(strip_cidr("2a01:4f8::1/64"), "2a01:4f8::1");
177 }
178
179 #[test]
180 fn test_strip_cidr_bare_ipv6() {
181 assert_eq!(strip_cidr("2600:3c00::1"), "2600:3c00::1");
182 }
183
184 #[test]
185 fn test_strip_cidr_ipv4_passthrough() {
186 assert_eq!(strip_cidr("1.2.3.4"), "1.2.3.4");
187 assert_eq!(strip_cidr("10.0.0.1/24"), "10.0.0.1");
188 }
189
190 #[test]
191 fn test_strip_cidr_empty() {
192 assert_eq!(strip_cidr(""), "");
193 }
194}