use std::sync::atomic::{AtomicBool, Ordering};
use serde::Deserialize;
use super::{Provider, ProviderError, ProviderHost, map_ureq_error};
pub struct UpCloud;
#[derive(Deserialize)]
struct ServerListResponse {
servers: ServerListWrapper,
}
#[derive(Deserialize)]
struct ServerListWrapper {
server: Vec<ServerSummary>,
}
#[derive(Deserialize)]
struct ServerSummary {
uuid: String,
title: String,
hostname: String,
#[serde(default)]
tags: TagWrapper,
#[serde(default)]
labels: LabelWrapper,
#[serde(default)]
zone: String,
#[serde(default)]
plan: String,
#[serde(default)]
state: String,
}
#[derive(Deserialize, Default)]
struct TagWrapper {
#[serde(default)]
tag: Vec<String>,
}
#[derive(Deserialize, Default)]
struct LabelWrapper {
#[serde(default)]
label: Vec<Label>,
}
#[derive(Deserialize)]
struct Label {
key: String,
value: String,
}
#[derive(Deserialize)]
struct ServerDetailResponse {
server: ServerDetail,
}
#[derive(Deserialize)]
struct ServerDetail {
#[serde(default)]
networking: Networking,
#[serde(default)]
storage_devices: Option<StorageDevices>,
}
#[derive(Deserialize)]
struct StorageDevices {
#[serde(default)]
storage_device: Vec<StorageDevice>,
}
#[derive(Deserialize)]
struct StorageDevice {
#[serde(default)]
storage_title: String,
#[serde(default)]
boot_disk: String,
}
#[derive(Deserialize, Default)]
struct Networking {
#[serde(default)]
interfaces: InterfacesWrapper,
}
#[derive(Deserialize, Default)]
struct InterfacesWrapper {
#[serde(default)]
interface: Vec<NetworkInterface>,
}
#[derive(Deserialize)]
struct NetworkInterface {
#[serde(default)]
ip_addresses: IpAddressesWrapper,
#[serde(rename = "type")]
iface_type: String,
}
#[derive(Deserialize, Default)]
struct IpAddressesWrapper {
#[serde(default)]
ip_address: Vec<IpAddress>,
}
#[derive(Deserialize)]
struct IpAddress {
address: String,
family: String,
}
fn collect_ips<'a>(interfaces: &'a [NetworkInterface], iface_type: &str) -> Vec<&'a IpAddress> {
interfaces
.iter()
.filter(|iface| iface.iface_type == iface_type)
.flat_map(|iface| &iface.ip_addresses.ip_address)
.collect()
}
fn select_ip(interfaces: &[NetworkInterface]) -> Option<String> {
let public_ips = collect_ips(interfaces, "public");
if let Some(ip) = public_ips
.iter()
.find(|a| a.family == "IPv4" && a.address != "0.0.0.0")
{
return Some(ip.address.clone());
}
public_ips
.iter()
.find(|a| a.family == "IPv6" && a.address != "::")
.map(|ip| ip.address.clone())
}
impl Provider for UpCloud {
fn name(&self) -> &str {
"upcloud"
}
fn short_label(&self) -> &str {
"uc"
}
fn fetch_hosts_cancellable(
&self,
token: &str,
cancel: &AtomicBool,
) -> Result<Vec<ProviderHost>, ProviderError> {
let mut all_servers: Vec<ServerSummary> = Vec::new();
let limit = 100;
let mut offset = 0u64;
let agent = super::http_agent();
let mut pages = 0u64;
loop {
if cancel.load(Ordering::Relaxed) {
return Err(ProviderError::Cancelled);
}
let url = format!(
"https://api.upcloud.com/1.3/server?limit={}&offset={}",
limit, offset
);
let resp: ServerListResponse = agent
.get(&url)
.header("Authorization", &format!("Bearer {}", token))
.call()
.map_err(map_ureq_error)?
.body_mut()
.read_json()
.map_err(|e| ProviderError::Parse(e.to_string()))?;
let count = resp.servers.server.len();
all_servers.extend(resp.servers.server);
if count < limit {
break;
}
offset += limit as u64;
pages += 1;
if pages >= 500 {
break;
}
}
let mut all_hosts = Vec::new();
let mut fetch_failures = 0usize;
for server in &all_servers {
if cancel.load(Ordering::Relaxed) {
return Err(ProviderError::Cancelled);
}
let url = format!("https://api.upcloud.com/1.3/server/{}", server.uuid);
let detail: ServerDetailResponse = match agent
.get(&url)
.header("Authorization", &format!("Bearer {}", token))
.call()
{
Ok(mut resp) => match resp.body_mut().read_json() {
Ok(d) => d,
Err(_) => {
fetch_failures += 1;
continue;
}
},
Err(ureq::Error::StatusCode(401 | 403)) => {
return Err(ProviderError::AuthFailed);
}
Err(ureq::Error::StatusCode(429)) => {
return Err(ProviderError::RateLimited);
}
Err(_) => {
fetch_failures += 1;
continue;
}
};
let ip = match select_ip(&detail.server.networking.interfaces.interface) {
Some(ip) => super::strip_cidr(&ip).to_string(),
None => continue,
};
let name = if server.title.is_empty() {
server.hostname.clone()
} else {
server.title.clone()
};
let mut tags: Vec<String> = server.tags.tag.iter().map(|t| t.to_lowercase()).collect();
for label in &server.labels.label {
if label.value.is_empty() {
tags.push(label.key.clone());
} else {
tags.push(format!("{}={}", label.key, label.value));
}
}
tags.sort();
let mut metadata = Vec::new();
if !server.zone.is_empty() {
metadata.push(("zone".to_string(), server.zone.clone()));
}
if !server.plan.is_empty() {
metadata.push(("plan".to_string(), server.plan.clone()));
}
if let Some(ref sd) = detail.server.storage_devices {
let boot = sd
.storage_device
.iter()
.find(|d| d.boot_disk == "1")
.or_else(|| sd.storage_device.first());
if let Some(disk) = boot {
if !disk.storage_title.is_empty() {
metadata.push(("image".to_string(), disk.storage_title.clone()));
}
}
}
if !server.state.is_empty() {
metadata.push(("status".to_string(), server.state.clone()));
}
all_hosts.push(ProviderHost {
server_id: server.uuid.clone(),
name,
ip,
tags,
metadata,
});
}
if fetch_failures > 0 {
let total = all_servers.len();
if all_hosts.is_empty() {
return Err(ProviderError::Http(format!(
"Failed to fetch details for all {} servers",
total
)));
}
return Err(ProviderError::PartialResult {
hosts: all_hosts,
failures: fetch_failures,
total,
});
}
Ok(all_hosts)
}
}
#[cfg(test)]
#[path = "upcloud_tests.rs"]
mod tests;