Attempt to implement and test setting static ips from instance metadata
This commit is contained in:
parent
dd1cfa79e7
commit
47dbb7cdd3
16 changed files with 258 additions and 59 deletions
2
rust/Cargo.lock
generated
2
rust/Cargo.lock
generated
|
@ -187,7 +187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hetzner-ipv6"
|
||||
name = "hetzner-static-ip"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
|
|
|
@ -26,7 +26,7 @@ let
|
|||
fileset = lib.fileset.unions [
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
(craneLib.fileset.commonCargoSources ./lib/common)
|
||||
(craneLib.fileset.commonCargoSources ./lib)
|
||||
(craneLib.fileset.commonCargoSources ./program/${crate})
|
||||
];
|
||||
};
|
||||
|
@ -40,6 +40,19 @@ in
|
|||
pname = crateName;
|
||||
cargoExtraArgs = "-p ${crateName}";
|
||||
src = fileSetForCrate crateName;
|
||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||
postFixup = ''
|
||||
wrapProgram $out/bin/${crateName} --set PATH "${
|
||||
lib.makeBinPath [
|
||||
pkgs.curl
|
||||
pkgs.uutils-coreutils-noprefix
|
||||
pkgs.iproute2
|
||||
]
|
||||
}"
|
||||
'';
|
||||
meta = {
|
||||
mainProgram = crateName;
|
||||
};
|
||||
}
|
||||
);
|
||||
checks = {
|
||||
|
|
|
@ -8,3 +8,53 @@ pub fn string(str: &str) -> String {
|
|||
pub fn to_string<T: ?Sized + Serialize>(value: &T) -> anyhow::Result<String> {
|
||||
serde_yml::to_string(value).context("Could not serialize to yaml")
|
||||
}
|
||||
|
||||
pub fn from_str<D: for<'de> serde::Deserialize<'de>>(s: &str) -> anyhow::Result<D> {
|
||||
serde_yml::from_str(s).map_err(|e| anyhow::format_err!("{e}:\n{}", extract_context(&e, s)))
|
||||
}
|
||||
|
||||
fn extract_context(serde_error: &serde_yml::Error, s: &str) -> String {
|
||||
let Some(location) = serde_error.location() else {
|
||||
return String::from("Error provided no location information, could not extract context");
|
||||
};
|
||||
let lines: Vec<_> = s.lines().collect();
|
||||
if lines.len() == 1 {
|
||||
let (col_begin, highlight) = if location.column() > 30 {
|
||||
(location.column() - 30, 30)
|
||||
} else {
|
||||
(1, location.column())
|
||||
};
|
||||
let col_end = if lines[0].len() + 31 < location.column() {
|
||||
lines[0].len() + 1
|
||||
} else {
|
||||
location.column() + 30
|
||||
};
|
||||
let mut line: String = lines[0]
|
||||
.chars()
|
||||
.skip(col_begin - 1)
|
||||
.take(col_end - col_begin)
|
||||
.collect();
|
||||
line.push('\n');
|
||||
line.extend(std::iter::repeat_n(' ', highlight - 1));
|
||||
line.push('^');
|
||||
line
|
||||
} else {
|
||||
let error_line = location.line();
|
||||
let mut result = String::new();
|
||||
if error_line > 1 {
|
||||
result.push_str(&format!("{}: {}\n", error_line - 1, lines[error_line - 2]));
|
||||
}
|
||||
result.push_str(&format!("{}: {}\n", error_line, lines[error_line - 1]));
|
||||
result.push_str(&format!(
|
||||
"{} {}\n",
|
||||
" ".repeat(error_line.to_string().len()),
|
||||
std::iter::repeat_n(' ', location.column() - 1)
|
||||
.chain(['^'].into_iter())
|
||||
.collect::<String>(),
|
||||
));
|
||||
if lines.len() > error_line {
|
||||
result.push_str(&format!("{}: {}\n", error_line + 1, lines[error_line]));
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
fn main() {
|
||||
common::entrypoint(program);
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Configures the ipv6 address using instance metadata and iproute2
|
||||
Configure,
|
||||
}
|
||||
|
||||
fn program() -> anyhow::Result<()> {
|
||||
let args = Args::parse();
|
||||
match args.command {
|
||||
Commands::Configure => configure(),
|
||||
}
|
||||
}
|
||||
|
||||
fn configure() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "hetzner-ipv6"
|
||||
name = "hetzner-static-ip"
|
||||
edition = "2024"
|
||||
version = "1.0.0"
|
||||
metadata.crane.name = "hetzner-ipv6"
|
||||
metadata.crane.name = "hetzner-static-ip"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
71
rust/program/hetzner-static-ip/src/main.rs
Normal file
71
rust/program/hetzner-static-ip/src/main.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use anyhow::Context as _;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::metadata::Instance;
|
||||
|
||||
mod metadata;
|
||||
|
||||
fn main() {
|
||||
common::entrypoint(program);
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Configures the ipv6 address using instance metadata and iproute2
|
||||
Configure,
|
||||
}
|
||||
|
||||
fn program() -> anyhow::Result<()> {
|
||||
let args = Args::parse();
|
||||
match args.command {
|
||||
Commands::Configure => configure(),
|
||||
}
|
||||
}
|
||||
|
||||
fn configure() -> anyhow::Result<()> {
|
||||
let metadata_api = common::env::read_env("INSTANCE_API_URI")
|
||||
.unwrap_or(String::from("http://169.254.169.254/hetzner/v1/metadata"));
|
||||
let metadata = common::curl::read_text_as_string(&metadata_api)?;
|
||||
let metadata: Instance = common::yaml::from_str(&metadata)
|
||||
.context("Could not parse instance metadata into expected format")?;
|
||||
for m in metadata.network_config.config {
|
||||
for subnet in m.subnets {
|
||||
match subnet {
|
||||
metadata::InstanceNetworkConfigConfigSubnet::Static {
|
||||
ipv6,
|
||||
ipv4,
|
||||
address,
|
||||
gateway,
|
||||
} => {
|
||||
let mut cmd = common::proc::Command::new("ip");
|
||||
if ipv6.is_some_and(|v| v) {
|
||||
cmd.arg("-6");
|
||||
}
|
||||
if ipv4.is_some_and(|v| v) {
|
||||
cmd.arg("-4");
|
||||
}
|
||||
cmd.args(["addr", "add", &address, "dev", &m.name]);
|
||||
cmd.try_spawn_to_string()?;
|
||||
let mut cmd = common::proc::Command::new("ip");
|
||||
if ipv6.is_some_and(|v| v) {
|
||||
cmd.arg("-6");
|
||||
}
|
||||
if ipv4.is_some_and(|v| v) {
|
||||
cmd.arg("-4");
|
||||
}
|
||||
cmd.args(["route", "add", "default", "via", &gateway, "dev", &m.name]);
|
||||
cmd.try_spawn_to_string()?;
|
||||
}
|
||||
metadata::InstanceNetworkConfigConfigSubnet::Dhcp {} => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
32
rust/program/hetzner-static-ip/src/metadata.rs
Normal file
32
rust/program/hetzner-static-ip/src/metadata.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Instance {
|
||||
#[serde(rename = "network-config")]
|
||||
pub network_config: InstanceNetworkConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct InstanceNetworkConfig {
|
||||
pub config: Vec<InstanceNetworkConfigConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct InstanceNetworkConfigConfig {
|
||||
pub name: String,
|
||||
pub subnets: Vec<InstanceNetworkConfigConfigSubnet>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum InstanceNetworkConfigConfigSubnet {
|
||||
#[serde(rename = "static")]
|
||||
Static {
|
||||
ipv6: Option<bool>,
|
||||
ipv4: Option<bool>,
|
||||
address: String,
|
||||
gateway: String,
|
||||
},
|
||||
#[serde(rename = "dhcp")]
|
||||
Dhcp {},
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "1.88.0"
|
||||
components = ["rustfmt", "clippy", "cargo"]
|
||||
components = ["rustfmt", "clippy", "cargo", "rust-src"]
|
||||
profile = "minimal"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue