From ddac328ae56fec94cdbbdd4718de4ba43fb182c5 Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Mon, 31 Jan 2022 06:10:45 +0000 Subject: [PATCH] client: make more commands automation-friendly Fixes #190 --- client/src/main.rs | 64 +++++++++++++++++-------- shared/src/prompts.rs | 109 +++++++++++++++++++++++++++++------------- shared/src/types.rs | 6 ++- 3 files changed, 123 insertions(+), 56 deletions(-) diff --git a/client/src/main.rs b/client/src/main.rs index 9334e28..fd6fef9 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -9,8 +9,8 @@ use shared::{ interface_config::InterfaceConfig, prompts, wg::{DeviceExt, PeerInfoExt}, - AddAssociationOpts, AddCidrOpts, AddPeerOpts, Association, AssociationContents, Cidr, CidrTree, - DeleteCidrOpts, Endpoint, EndpointContents, InstallOpts, Interface, IoErrorContext, + AddCidrOpts, AddDeleteAssociationOpts, AddPeerOpts, Association, AssociationContents, Cidr, + CidrTree, DeleteCidrOpts, Endpoint, EndpointContents, InstallOpts, Interface, IoErrorContext, ListenPortOpts, NatOpts, NetworkOpts, OverrideEndpointOpts, Peer, RedeemContents, RenamePeerOpts, State, WrappedIoError, REDEEM_TRANSITION_WAIT, }; @@ -149,7 +149,13 @@ enum Command { }, /// Uninstall an innernet network. - Uninstall { interface: Interface }, + Uninstall { + interface: Interface, + + /// Bypass confirmation + #[clap(long)] + yes: bool, + }, /// Bring down the interface (equivalent to 'wg-quick down ') Down { interface: Interface }, @@ -216,11 +222,16 @@ enum Command { interface: Interface, #[clap(flatten)] - sub_opts: AddAssociationOpts, + sub_opts: AddDeleteAssociationOpts, }, /// Delete an association between CIDRs - DeleteAssociation { interface: Interface }, + DeleteAssociation { + interface: Interface, + + #[clap(flatten)] + sub_opts: AddDeleteAssociationOpts, + }, /// List existing assocations between CIDRs ListAssociations { interface: Interface }, @@ -628,7 +639,7 @@ fn fetch( Ok(()) } -fn uninstall(interface: &InterfaceName, opts: &Opts) -> Result<(), Error> { +fn uninstall(interface: &InterfaceName, opts: &Opts, yes: bool) -> Result<(), Error> { let config = InterfaceConfig::get_path(&opts.config_dir, interface); let data = DataStore::get_path(&opts.data_dir, interface); @@ -639,14 +650,15 @@ fn uninstall(interface: &InterfaceName, opts: &Opts) -> Result<(), Error> { ); } - if Confirm::with_theme(&*prompts::THEME) - .with_prompt(&format!( - "Permanently delete network \"{}\"?", - interface.as_str_lossy().yellow() - )) - .default(false) - .wait_for_newline(true) - .interact()? + if yes + || Confirm::with_theme(&*prompts::THEME) + .with_prompt(&format!( + "Permanently delete network \"{}\"?", + interface.as_str_lossy().yellow() + )) + .default(false) + .wait_for_newline(true) + .interact()? { log::info!("bringing down interface (if up)."); wg::down(interface, opts.network.backend).ok(); @@ -821,7 +833,7 @@ fn enable_or_disable_peer( fn add_association( interface: &InterfaceName, opts: &Opts, - sub_opts: AddAssociationOpts, + sub_opts: AddDeleteAssociationOpts, ) -> Result<(), Error> { let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(&opts.config_dir, interface)?; @@ -830,7 +842,8 @@ fn add_association( log::info!("Fetching CIDRs"); let cidrs: Vec = api.http("GET", "/admin/cidrs")?; - let association = if let (Some(ref cidr1), Some(ref cidr2)) = (sub_opts.cidr1, sub_opts.cidr2) { + let association = if let (Some(ref cidr1), Some(ref cidr2)) = (&sub_opts.cidr1, &sub_opts.cidr2) + { let cidr1 = cidrs .iter() .find(|c| &c.name == cidr1) @@ -840,7 +853,7 @@ fn add_association( .find(|c| &c.name == cidr2) .ok_or_else(|| anyhow!("can't find cidr '{}'", cidr2))?; (cidr1, cidr2) - } else if let Some((cidr1, cidr2)) = prompts::add_association(&cidrs[..])? { + } else if let Some((cidr1, cidr2)) = prompts::add_association(&cidrs[..], &sub_opts)? { (cidr1, cidr2) } else { log::info!("exiting without adding association."); @@ -859,7 +872,11 @@ fn add_association( Ok(()) } -fn delete_association(interface: &InterfaceName, opts: &Opts) -> Result<(), Error> { +fn delete_association( + interface: &InterfaceName, + opts: &Opts, + sub_opts: AddDeleteAssociationOpts, +) -> Result<(), Error> { let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(&opts.config_dir, interface)?; let api = Api::new(&server); @@ -869,7 +886,9 @@ fn delete_association(interface: &InterfaceName, opts: &Opts) -> Result<(), Erro log::info!("Fetching associations"); let associations: Vec = api.http("GET", "/admin/associations")?; - if let Some(association) = prompts::delete_association(&associations[..], &cidrs[..])? { + if let Some(association) = + prompts::delete_association(&associations[..], &cidrs[..], &sub_opts)? + { api.http("DELETE", &format!("/admin/associations/{}", association.id))?; } else { log::info!("exiting without adding association."); @@ -1211,7 +1230,7 @@ fn run(opts: &Opts) -> Result<(), Error> { &nat, )?, Command::Down { interface } => wg::down(&interface, opts.network.backend)?, - Command::Uninstall { interface } => uninstall(&interface, opts)?, + Command::Uninstall { interface, yes } => uninstall(&interface, opts, yes)?, Command::AddPeer { interface, sub_opts, @@ -1235,7 +1254,10 @@ fn run(opts: &Opts) -> Result<(), Error> { interface, sub_opts, } => add_association(&interface, opts, sub_opts)?, - Command::DeleteAssociation { interface } => delete_association(&interface, opts)?, + Command::DeleteAssociation { + interface, + sub_opts, + } => delete_association(&interface, opts, sub_opts)?, Command::ListAssociations { interface } => list_associations(&interface, opts)?, Command::SetListenPort { interface, diff --git a/shared/src/prompts.rs b/shared/src/prompts.rs index 334bd19..d617935 100644 --- a/shared/src/prompts.rs +++ b/shared/src/prompts.rs @@ -1,8 +1,8 @@ use crate::{ interface_config::{InterfaceConfig, InterfaceInfo, ServerInfo}, - AddCidrOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, DeleteCidrOpts, Endpoint, - Error, Hostname, ListenPortOpts, OverrideEndpointOpts, Peer, PeerContents, RenamePeerOpts, - PERSISTENT_KEEPALIVE_INTERVAL_SECS, + AddCidrOpts, AddDeleteAssociationOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, + DeleteCidrOpts, Endpoint, Error, Hostname, ListenPortOpts, OverrideEndpointOpts, Peer, + PeerContents, RenamePeerOpts, PERSISTENT_KEEPALIVE_INTERVAL_SECS, }; use anyhow::anyhow; use colored::*; @@ -151,41 +151,81 @@ pub fn choose_cidr<'a>(cidrs: &'a [Cidr], text: &'static str) -> Result<&'a Cidr pub fn choose_association<'a>( associations: &'a [Association], cidrs: &'a [Cidr], + args: &AddDeleteAssociationOpts, ) -> Result<&'a Association, Error> { - let names: Vec<_> = associations - .iter() - .map(|association| { - format!( - "{}: {} <=> {}", - association.id, - &cidrs - .iter() - .find(|c| c.id == association.cidr_id_1) - .unwrap() - .name, - &cidrs - .iter() - .find(|c| c.id == association.cidr_id_2) - .unwrap() - .name - ) - }) - .collect(); - let (index, _) = select("Association", &names)?; + match (&args.cidr1, &args.cidr2) { + (Some(cidr1_name), Some(cidr2_name)) => { + let cidr1 = find_cidr(cidrs, cidr1_name)?; + let cidr2 = find_cidr(cidrs, cidr2_name)?; + associations + .iter() + .find(|association| { + (association.cidr_id_1 == cidr1.id && association.cidr_id_2 == cidr2.id) + || (association.cidr_id_1 == cidr2.id && association.cidr_id_2 == cidr1.id) + }) + .ok_or_else(|| anyhow!("CIDR association does not exist")) + }, + _ => { + let names: Vec<_> = associations + .iter() + .map(|association| { + format!( + "{}: {} <=> {}", + association.id, + &cidrs + .iter() + .find(|c| c.id == association.cidr_id_1) + .unwrap() + .name, + &cidrs + .iter() + .find(|c| c.id == association.cidr_id_2) + .unwrap() + .name + ) + }) + .collect(); + let (index, _) = select("Association", &names)?; - Ok(&associations[index]) + Ok(&associations[index]) + }, + } } -pub fn add_association(cidrs: &[Cidr]) -> Result, Error> { - let cidr1 = choose_cidr(cidrs, "First CIDR")?; - let cidr2 = choose_cidr(cidrs, "Second CIDR")?; +fn find_cidr<'a>(cidrs: &'a [Cidr], name: &str) -> Result<&'a Cidr, Error> { + cidrs + .iter() + .find(|c| c.name == name) + .ok_or_else(|| anyhow!("can't find cidr '{}'", name)) +} + +fn find_or_prompt_cidr<'a>( + cidrs: &'a [Cidr], + sub_opt: &Option, + prompt: &'static str, +) -> Result<&'a Cidr, Error> { + if let Some(name) = sub_opt { + find_cidr(cidrs, name) + } else { + choose_cidr(cidrs, prompt) + } +} + +pub fn add_association<'a>( + cidrs: &'a [Cidr], + args: &AddDeleteAssociationOpts, +) -> Result, Error> { + let cidr1 = find_or_prompt_cidr(cidrs, &args.cidr1, "First CIDR")?; + let cidr2 = find_or_prompt_cidr(cidrs, &args.cidr2, "Second CIDR")?; Ok( - if confirm(&format!( - "Add association: {} <=> {}?", - cidr1.name.yellow().bold(), - cidr2.name.yellow().bold() - ))? { + if args.yes + || confirm(&format!( + "Add association: {} <=> {}?", + cidr1.name.yellow().bold(), + cidr2.name.yellow().bold() + ))? + { Some((cidr1, cidr2)) } else { None @@ -196,11 +236,12 @@ pub fn add_association(cidrs: &[Cidr]) -> Result, Error> pub fn delete_association<'a>( associations: &'a [Association], cidrs: &'a [Cidr], + args: &AddDeleteAssociationOpts, ) -> Result, Error> { - let association = choose_association(associations, cidrs)?; + let association = choose_association(associations, cidrs, args)?; Ok( - if confirm(&format!("Delete association #{}?", association.id))? { + if args.yes || confirm(&format!("Delete association #{}?", association.id))? { Some(association) } else { None diff --git a/shared/src/types.rs b/shared/src/types.rs index 0466d67..e797608 100644 --- a/shared/src/types.rs +++ b/shared/src/types.rs @@ -379,12 +379,16 @@ pub struct DeleteCidrOpts { } #[derive(Debug, Clone, PartialEq, Args)] -pub struct AddAssociationOpts { +pub struct AddDeleteAssociationOpts { /// The first cidr to associate pub cidr1: Option, /// The second cidr to associate pub cidr2: Option, + + /// Bypass confirmation + #[clap(long)] + pub yes: bool, } #[derive(Debug, Clone, PartialEq, Args)]