Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxmox provider #1

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/rpm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:
test-rpm-build:
name: "Build (Fedora)"
runs-on: ubuntu-latest
permissions:
contents: write
container:
image: registry.fedoraproject.org/fedora:latest
options: --privileged
Expand All @@ -45,3 +47,12 @@ jobs:
with:
name: rpms
path: rpms/

- name: Format version name
run: echo "VERSION=v$(date +'%Y%m%d.%H%M')" >> $GITHUB_ENV

- name: Create release
uses: ncipollo/release-action@v1
with:
tag: ${{ env.VERSION }}
artifacts: "rpms/*.rpm"
5 changes: 5 additions & 0 deletions docs/platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ The following platforms are supported, with a different set of features availabl
* powervs
- Attributes
- SSH keys
* proxmoxve
- Attributes
- Hostname
- SSH keys
- Network configuration
* scaleway
- Attributes
- Boot check-in
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Major changes:
- Add support for Hetzner Cloud
- Add support for Scaleway
- Add Netplan guestinfo support on VMware
- Add support for Proxmox VE

Minor changes:

Expand Down
5 changes: 5 additions & 0 deletions docs/usage/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ Cloud providers with supported metadata endpoints and their respective attribute
* powervs
- AFTERBURN_POWERVS_INSTANCE_ID
- AFTERBURN_POWERVS_LOCAL_HOSTNAME
* proxmoxve
- AFTERBURN_PROXMOXVE_HOSTNAME
- AFTERBURN_PROXMOXVE_INSTANCE_ID
- AFTERBURN_PROXMOXVE_IPV4
- AFTERBURN_PROXMOXVE_IPV6
* scaleway
- AFTERBURN_SCALEWAY_HOSTNAME
- AFTERBURN_SCALEWAY_INSTANCE_ID
Expand Down
2 changes: 2 additions & 0 deletions src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use crate::providers::openstack;
use crate::providers::openstack::network::OpenstackProviderNetwork;
use crate::providers::packet::PacketProvider;
use crate::providers::powervs::PowerVSProvider;
use crate::providers::proxmoxve::ProxmoxVEConfigDrive;
use crate::providers::scaleway::ScalewayProvider;
use crate::providers::vmware::VmwareProvider;
use crate::providers::vultr::VultrProvider;
Expand Down Expand Up @@ -68,6 +69,7 @@ pub fn fetch_metadata(provider: &str) -> Result<Box<dyn providers::MetadataProvi
"openstack-metadata" => box_result!(OpenstackProviderNetwork::try_new()?),
"packet" => box_result!(PacketProvider::try_new()?),
"powervs" => box_result!(PowerVSProvider::try_new()?),
"proxmoxve" => box_result!(ProxmoxVEConfigDrive::try_new()?),
"scaleway" => box_result!(ScalewayProvider::try_new()?),
"vmware" => box_result!(VmwareProvider::try_new()?),
"vultr" => box_result!(VultrProvider::try_new()?),
Expand Down
1 change: 1 addition & 0 deletions src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub mod microsoft;
pub mod openstack;
pub mod packet;
pub mod powervs;
pub mod proxmoxve;
pub mod scaleway;
pub mod vmware;
pub mod vultr;
Expand Down
253 changes: 253 additions & 0 deletions src/providers/proxmoxve/cloudconfig.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
use crate::{
network::{self, NetworkRoute},
providers::MetadataProvider,
};
use anyhow::Result;
use ipnetwork::IpNetwork;
use openssh_keys::PublicKey;
use pnet_base::MacAddr;
use serde::Deserialize;
use slog_scope::warn;
use std::{
collections::HashMap,
fs::File,
net::{AddrParseError, IpAddr},
path::Path,
str::FromStr,
};

#[derive(Debug)]
pub struct ProxmoxVECloudConfig {
pub meta_data: ProxmoxVECloudMetaData,
pub user_data: ProxmoxVECloudUserData,
pub vendor_data: ProxmoxVECloudVendorData,
pub network_config: ProxmoxVECloudNetworkConfig,
}

#[derive(Debug, Deserialize)]
pub struct ProxmoxVECloudMetaData {
#[serde(rename = "instance-id")]
pub instance_id: String,
}

#[derive(Debug, Deserialize)]
pub struct ProxmoxVECloudUserData {
pub hostname: String,
pub manage_etc_hosts: bool,
pub fqdn: String,
pub chpasswd: ProxmoxVECloudChpasswdConfig,
pub users: Vec<String>,
pub package_upgrade: bool,
#[serde(default)]
pub ssh_authorized_keys: Vec<String>,
}

#[derive(Debug, Deserialize)]
pub struct ProxmoxVECloudChpasswdConfig {
pub expire: bool,
}

#[derive(Debug, Deserialize)]
pub struct ProxmoxVECloudVendorData {}

#[derive(Debug, Deserialize)]
pub struct ProxmoxVECloudNetworkConfig {
pub version: u32,
pub config: Vec<ProxmoxVECloudNetworkConfigEntry>,
}

#[derive(Debug, Deserialize)]
pub struct ProxmoxVECloudNetworkConfigEntry {
#[serde(rename = "type")]
pub network_type: String,
pub name: Option<String>,
pub mac_address: Option<String>,
#[serde(default)]
pub address: Vec<String>,
#[serde(default)]
pub search: Vec<String>,
#[serde(default)]
pub subnets: Vec<ProxmoxVECloudNetworkConfigSubnet>,
}

#[derive(Debug, Deserialize)]
pub struct ProxmoxVECloudNetworkConfigSubnet {
#[serde(rename = "type")]
pub subnet_type: String,
pub address: Option<String>,
pub netmask: Option<String>,
pub gateway: Option<String>,
}

impl ProxmoxVECloudConfig {
pub fn try_new(path: &Path) -> Result<Self> {
Ok(Self {
meta_data: serde_yaml::from_reader(File::open(path.join("meta-data"))?)?,
user_data: serde_yaml::from_reader(File::open(path.join("user-data"))?)?,
vendor_data: serde_yaml::from_reader(File::open(path.join("vendor-data"))?)?,
network_config: serde_yaml::from_reader(File::open(path.join("network-config"))?)?,
})
}
}

impl MetadataProvider for ProxmoxVECloudConfig {
fn attributes(&self) -> Result<HashMap<String, String>> {
let mut out = HashMap::new();

out.insert(
"AFTERBURN_PROXMOXVE_HOSTNAME".to_owned(),
self.hostname()?.unwrap_or_default(),
);

out.insert(
"AFTERBURN_PROXMOXVE_INSTANCE_ID".to_owned(),
self.meta_data.instance_id.clone(),
);

if let Some(first_interface) = self.networks()?.first() {
first_interface.ip_addresses.iter().for_each(|ip| match ip {
IpNetwork::V4(network) => {
out.insert(
"AFTERBURN_PROXMOXVE_IPV4".to_owned(),
network.ip().to_string(),
);
}
IpNetwork::V6(network) => {
out.insert(
"AFTERBURN_PROXMOXVE_IPV6".to_owned(),
network.ip().to_string(),
);
}
});
}

Ok(out)
}

fn hostname(&self) -> Result<Option<String>> {
Ok(Some(self.user_data.hostname.clone()))
}

fn ssh_keys(&self) -> Result<Vec<PublicKey>> {
Ok(self
.user_data
.ssh_authorized_keys
.iter()
.map(|key| PublicKey::from_str(key))
.collect::<Result<Vec<_>, _>>()?)
}

fn networks(&self) -> Result<Vec<network::Interface>> {
let nameservers = self
.network_config
.config
.iter()
.filter(|config| config.network_type == "nameserver")
.collect::<Vec<_>>();

if nameservers.len() > 1 {
return Err(anyhow::anyhow!("too many nameservers, only one supported"));
}

let mut interfaces = self
.network_config
.config
.iter()
.filter(|config| config.network_type == "physical")
.map(|entry| entry.to_interface())
.collect::<Result<Vec<_>, _>>()?;

if let Some(iface) = interfaces.first_mut() {
if let Some(nameserver) = nameservers.first() {
iface.nameservers = nameserver
.address
.iter()
.map(|ip| IpAddr::from_str(ip))
.collect::<Result<Vec<IpAddr>, AddrParseError>>()?;
}
}

Ok(interfaces)
}
}

impl ProxmoxVECloudNetworkConfigEntry {
pub fn to_interface(&self) -> Result<network::Interface> {
if self.network_type != "physical" {
return Err(anyhow::anyhow!(
"cannot convert config to interface: unsupported config type \"{}\"",
self.network_type
));
}

let mut iface = network::Interface {
name: self.name.clone(),

// filled later
nameservers: vec![],
// filled below
ip_addresses: vec![],
// filled below
routes: vec![],
// filled below because Option::try_map doesn't exist yet
mac_address: None,

// unsupported by proxmox ve
bond: None,

// default values
path: None,
priority: 20,
unmanaged: false,
required_for_online: None,
};

for subnet in &self.subnets {
if subnet.subnet_type.contains("static") {
if subnet.address.is_none() {
return Err(anyhow::anyhow!(
"cannot convert static subnet to interface: missing address"
));
}

if let Some(netmask) = &subnet.netmask {
iface.ip_addresses.push(IpNetwork::with_netmask(
IpAddr::from_str(subnet.address.as_ref().unwrap())?,
IpAddr::from_str(netmask)?,
)?);
} else {
iface
.ip_addresses
.push(IpNetwork::from_str(subnet.address.as_ref().unwrap())?);
}

if let Some(gateway) = &subnet.gateway {
let gateway = IpAddr::from_str(gateway)?;

let destination = if gateway.is_ipv6() {
IpNetwork::from_str("::/0")?
} else {
IpNetwork::from_str("0.0.0.0/0")?
};

iface.routes.push(NetworkRoute {
destination,
gateway,
});
} else {
warn!("found subnet type \"static\" without gateway");
}
}

if subnet.subnet_type == "ipv6_slaac" {
warn!("subnet type \"ipv6_slaac\" not supported, ignoring");
}
}

if let Some(mac) = &self.mac_address {
iface.mac_address = Some(MacAddr::from_str(mac)?);
}

Ok(iface)
}
}
59 changes: 59 additions & 0 deletions src/providers/proxmoxve/configdrive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use super::ProxmoxVECloudConfig;
use crate::{network, providers::MetadataProvider};
use anyhow::{Context, Result};
use openssh_keys::PublicKey;
use slog_scope::error;
use std::path::{Path, PathBuf};

#[derive(Debug)]
pub struct ProxmoxVEConfigDrive {
mount_path: PathBuf,
config: ProxmoxVECloudConfig,
}

impl ProxmoxVEConfigDrive {
pub fn try_new() -> Result<Self> {
const CONFIG_DRIVE_LABEL: &str = "cidata";
const TARGET_FS: &str = "iso9660";

let target = tempfile::Builder::new()
.prefix("afterburn-")
.tempdir()
.context("failed to create temporary directory")?;

crate::util::mount_ro(
&Path::new("/dev/disk/by-label/").join(CONFIG_DRIVE_LABEL),
target.path(),
TARGET_FS,
3,
)?;

let mount_path = target.path().to_owned();
Ok(Self {
config: ProxmoxVECloudConfig::try_new(&mount_path)?,
mount_path,
})
}
}

impl MetadataProvider for ProxmoxVEConfigDrive {
fn hostname(&self) -> Result<Option<String>> {
self.config.hostname()
}

fn ssh_keys(&self) -> Result<Vec<PublicKey>> {
self.config.ssh_keys()
}

fn networks(&self) -> Result<Vec<network::Interface>> {
self.config.networks()
}
}

impl Drop for ProxmoxVEConfigDrive {
fn drop(&mut self) {
if let Err(e) = crate::util::unmount(&self.mount_path, 3) {
error!("failed to cleanup Proxmox VE config-drive: {:?}", e);
};
}
}
Loading