mirror of
https://github.com/SeriousBug/gandi-live-dns-rust
synced 2024-12-26 15:19:59 -06:00
Use die-exit to clean up exits
This commit is contained in:
parent
c5ed8fe6ab
commit
3a7620b6d5
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -101,6 +101,11 @@ version = "0.8.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "die-exit"
|
||||
version = "0.3.3"
|
||||
source = "git+https://github.com/SeriousBug/die.git#76f91559d6b5c1c2f6a889b9cf9780c663918ac5"
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "4.0.1"
|
||||
|
@ -255,6 +260,7 @@ name = "gandi-rust-dns-updater"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"die-exit",
|
||||
"directories",
|
||||
"futures",
|
||||
"json",
|
||||
|
|
|
@ -15,4 +15,5 @@ directories = "4.0.1"
|
|||
structopt = "0.3.25"
|
||||
tokio = { version = "1.14.0", features = ["full"] }
|
||||
futures = "0.3.17"
|
||||
anyhow = "1.0"
|
||||
anyhow = "1.0"
|
||||
die-exit = { git = "https://github.com/SeriousBug/die.git" }
|
11
Readme.md
Normal file
11
Readme.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
## gandi-live-dns-rust
|
||||
|
||||
A program that can set the IP addresses for configured DNS entries in [Gandi](https://gandi.net)'s domain configuration.
|
||||
Thanks to Gandi's [LiveDNS API](https://api.gandi.net/docs/livedns/), this creates a dynamic DNS system.
|
||||
|
||||
Inspired by [cavebeat's similar tool](https://github.com/cavebeat/gandi-live-dns),
|
||||
which seems to be unmaintained at the time I'm writing this. I decided to rewrite it in Rust as a learning project.
|
||||
|
||||
## Usage
|
||||
|
||||
- Copy `example.toml` to
|
|
@ -1,9 +1,9 @@
|
|||
use crate::opts;
|
||||
use anyhow;
|
||||
use directories::ProjectDirs;
|
||||
use serde::Deserialize;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use crate::opts;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Entry {
|
||||
|
@ -19,7 +19,7 @@ pub struct Config {
|
|||
pub entry: Vec<Entry>,
|
||||
}
|
||||
|
||||
const DEFAULT_TYPES: Vec<&str> = vec!["A"];
|
||||
const DEFAULT_TYPES: &'static [&'static str] = &["A"];
|
||||
|
||||
impl Config {
|
||||
pub fn fqdn<'c>(entry: &'c Entry, config: &'c Config) -> &'c str {
|
||||
|
@ -27,39 +27,50 @@ impl Config {
|
|||
}
|
||||
|
||||
pub fn types<'e>(entry: &'e Entry) -> Vec<&'e str> {
|
||||
return entry.types.as_ref().and_then(
|
||||
|ts| Some(ts.iter().map(|t| t.as_str()).collect())
|
||||
).unwrap_or(DEFAULT_TYPES);
|
||||
return entry
|
||||
.types
|
||||
.as_ref()
|
||||
.and_then(|ts| Some(ts.iter().map(|t| t.as_str()).collect()))
|
||||
.unwrap_or_else(|| DEFAULT_TYPES.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_config(file: PathBuf) -> Result<Config, Box<dyn Error>> {
|
||||
let output = fs::read_to_string(file)?;
|
||||
let contents = output.as_str();
|
||||
|
||||
let config = toml::from_str(contents)?;
|
||||
return Ok(config);
|
||||
fn load_config_from<P: std::convert::AsRef<std::path::Path>>(path: P) -> anyhow::Result<Config> {
|
||||
let contents = fs::read_to_string(path)?;
|
||||
Ok(toml::from_str(&contents)?)
|
||||
}
|
||||
|
||||
pub fn validate_config(config: &Config) -> Result<(), String> {
|
||||
pub fn load_config(opts: &opts::Opts) -> anyhow::Result<Config> {
|
||||
match &opts.config {
|
||||
Some(config_path) => load_config_from(&config_path),
|
||||
None => {
|
||||
let confpath = ProjectDirs::from("me", "kaangenc", "gandi-dynamic-dns")
|
||||
.and_then(|dir| Some(PathBuf::from(dir.config_dir()).join("config.toml")));
|
||||
confpath.as_ref()
|
||||
.and_then(|path| {
|
||||
Some(load_config_from(path))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let path = PathBuf::from(".").join("gandi.toml");
|
||||
load_config_from(path)
|
||||
})
|
||||
.or_else(|_| {
|
||||
match &confpath {
|
||||
Some(path) => anyhow::bail!("Can't find the config file! Please create {}, or gandi.toml in the current directory.", path.to_string_lossy()),
|
||||
None => anyhow::bail!("Can't find the config file! Please create gandi.toml in the current directory."),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_config(config: &Config) -> anyhow::Result<()> {
|
||||
for entry in &config.entry {
|
||||
for entry_type in Config::types(&entry) {
|
||||
if entry_type != "A" && entry_type != "AAA" {
|
||||
return Err(format!("Entry {} has invalid type {}", entry.name, entry_type));
|
||||
anyhow::bail!("Entry {} has invalid type {}", entry.name, entry_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub fn config_path(opts: &opts::Opts) -> PathBuf {
|
||||
return opts
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|conf| Some(PathBuf::from(conf)))
|
||||
.unwrap_or(
|
||||
ProjectDirs::from("me", "kaangenc", "gandi-dynamic-dns")
|
||||
.and_then(|dir| Some(PathBuf::from(dir.config_dir())))
|
||||
.unwrap_or(PathBuf::from(".")),
|
||||
);
|
||||
}
|
||||
|
|
61
src/main.rs
61
src/main.rs
|
@ -2,15 +2,12 @@ use crate::config::Config;
|
|||
use anyhow;
|
||||
use futures;
|
||||
use reqwest::{header, Client, ClientBuilder, StatusCode};
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, process::exit};
|
||||
use structopt::StructOpt;
|
||||
use tokio::{self, task::JoinHandle};
|
||||
mod config;
|
||||
mod opts;
|
||||
|
||||
fn gandi_api_get(fqdn: &str) -> String {
|
||||
return format!("https://api.gandi.net/v5/livedns/domains/{}/records", fqdn);
|
||||
}
|
||||
use die_exit::*;
|
||||
|
||||
fn gandi_api_url(fqdn: &str, rrset_name: &str, rrset_type: &str) -> String {
|
||||
return format!(
|
||||
|
@ -33,29 +30,47 @@ fn api_client(api_key: &str) -> anyhow::Result<Client> {
|
|||
return Ok(client);
|
||||
}
|
||||
|
||||
async fn get_ip(api_url: &str) -> anyhow::Result<String> {
|
||||
let response = reqwest::get(api_url).await?;
|
||||
let text = response.text().await?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let opts = opts::Opts::from_args();
|
||||
let conf_path = config::config_path(&opts);
|
||||
println!("Loading config from {:#?}", conf_path);
|
||||
let conf = config::load_config(conf_path)?;
|
||||
config::validate_config(&conf)?;
|
||||
let conf = config::load_config(&opts)
|
||||
.die_with(|error| format!("Failed to read config file: {}", error));
|
||||
config::validate_config(&conf).die_with(|error| format!("Invalid config: {}", error));
|
||||
println!("Finding out the API address...");
|
||||
let ipv4 = get_ip("https://api.ipify.org").await;
|
||||
let ipv6 = get_ip("https://api6.ipify.org").await;
|
||||
println!("Found these:");
|
||||
println!("\tIPv4: {}", ipv4.unwrap_or_else(|error| error.to_string()));
|
||||
println!("\tIPv6: {}", ipv6.unwrap_or_else(|error| error.to_string()));
|
||||
|
||||
let client = api_client(&conf.api_key)?;
|
||||
|
||||
let ipv4 = String::from("173.89.215.91");
|
||||
let ipv6 = String::from("2603:6011:be07:302:79f4:50dd:6abe:be38");
|
||||
|
||||
let mut results: Vec<JoinHandle<(StatusCode, String)>> = Vec::new();
|
||||
let mut tasks: Vec<JoinHandle<(StatusCode, String)>> = Vec::new();
|
||||
println!("Attempting to update DNS entries now");
|
||||
|
||||
for entry in &conf.entry {
|
||||
for entry_type in Config::types(entry) {
|
||||
let fqdn = Config::fqdn(&entry, &conf);
|
||||
let url = gandi_api_url(fqdn, entry.name.as_str(), entry_type);
|
||||
let ip = if entry_type.eq("A") {
|
||||
ipv4.as_str()
|
||||
} else {
|
||||
ipv6.as_str()
|
||||
let ip = match entry_type {
|
||||
"A" => ipv4.unwrap_or_else(|error| {
|
||||
panic!(
|
||||
"Need IPv4 address for {} but failed to get it: {}",
|
||||
fqdn, error
|
||||
)
|
||||
}),
|
||||
"AAA" => ipv6.unwrap_or_else(|error| {
|
||||
panic!(
|
||||
"Need IPv6 address for {} but failed to get it: {}",
|
||||
fqdn, error
|
||||
)
|
||||
}),
|
||||
&_ => panic!("Unexpected entry type {}", entry_type),
|
||||
};
|
||||
let mut map = HashMap::new();
|
||||
map.insert("rrset_values", ip);
|
||||
|
@ -69,17 +84,15 @@ async fn main() -> anyhow::Result<()> {
|
|||
.await
|
||||
.unwrap_or_else(|error| error.to_string()),
|
||||
),
|
||||
Err(error) => (
|
||||
StatusCode::IM_A_TEAPOT, error.to_string()
|
||||
),
|
||||
Err(error) => (StatusCode::IM_A_TEAPOT, error.to_string()),
|
||||
}
|
||||
});
|
||||
results.push(task);
|
||||
tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
let results = futures::future::try_join_all(results).await?;
|
||||
|
||||
let results = futures::future::try_join_all(tasks).await?;
|
||||
println!("Updates done for {} entries", results.len());
|
||||
for (status, body) in results {
|
||||
println!("{} - {}", status, body);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue