Use die-exit to clean up exits

This commit is contained in:
Kaan Genc 2022-01-23 16:12:27 -05:00
parent c5ed8fe6ab
commit 3a7620b6d5
5 changed files with 93 additions and 51 deletions

6
Cargo.lock generated
View file

@ -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",

View file

@ -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
View 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

View file

@ -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(".")),
);
}

View file

@ -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);
}