diff --git a/.gitignore b/.gitignore index 0086a8a..3063256 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ /target -tarpaulin-report.* \ No newline at end of file +tarpaulin-report.* +*.log \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a26803a..bc10b7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "0.7.15" @@ -25,6 +40,9 @@ name = "anyhow" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" +dependencies = [ + "backtrace", +] [[package]] name = "assert-json-diff" @@ -53,6 +71,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ed203b9ba68b242c62b3fb7480f589dd49829be1edb3fe8fc8b4ffda2dcb8d" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -231,6 +263,12 @@ version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "098cd1c6dda6ca01650f1a37a794245eb73181d0d4d4e955e2f3c37db7af1815" +[[package]] +name = "futures-io" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" + [[package]] name = "futures-sink" version = "0.3.14" @@ -250,9 +288,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c144ad54d60f23927f0a6b6d816e4271278b64f005ad65e4e35291d2de9c025" dependencies = [ "futures-core", + "futures-io", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -266,6 +307,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + [[package]] name = "glob" version = "0.3.0" @@ -407,15 +454,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "instant" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" -dependencies = [ - "cfg-if", -] - [[package]] name = "ipnet" version = "2.3.0" @@ -449,15 +487,6 @@ version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" -[[package]] -name = "lock_api" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.14" @@ -485,6 +514,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" version = "0.7.11" @@ -545,7 +584,7 @@ dependencies = [ [[package]] name = "netbox2netshot" -version = "0.1.4" +version = "0.1.5" dependencies = [ "anyhow", "ctor", @@ -555,7 +594,6 @@ dependencies = [ "reqwest", "serde", "structopt", - "tokio", ] [[package]] @@ -596,6 +634,12 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" + [[package]] name = "once_cell" version = "1.7.2" @@ -635,31 +679,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "parking_lot" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", -] - [[package]] name = "percent-encoding" version = "2.1.0" @@ -862,6 +881,12 @@ dependencies = [ "winreg", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "ryu" version = "1.0.5" @@ -878,12 +903,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - [[package]] name = "security-framework" version = "2.2.0" @@ -950,27 +969,12 @@ dependencies = [ "serde", ] -[[package]] -name = "signal-hook-registry" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" -dependencies = [ - "libc", -] - [[package]] name = "slab" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" -[[package]] -name = "smallvec" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" - [[package]] name = "socket2" version = "0.4.0" @@ -1103,23 +1107,7 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", - "parking_lot", "pin-project-lite", - "signal-hook-registry", - "tokio-macros", - "winapi", -] - -[[package]] -name = "tokio-macros" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" -dependencies = [ - "proc-macro2", - "quote", - "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 582bfdd..b453267 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "netbox2netshot" -version = "0.1.4" +version = "0.1.5" authors = ["Mathieu Poussin "] edition = "2018" description = "Synchronization tool between netbox and netshot" @@ -15,9 +15,8 @@ serde = { version = "1.0.125", features = ["derive"]} structopt = "0.3" log = "0.4" flexi_logger = "0.17" -reqwest = { version = "0.11", features = ["json"]} -tokio = { version = "1", features = ["full"] } -anyhow = "1.0" +reqwest = { version = "0.11", features = ["json", "native-tls", "blocking"]} +anyhow = { version = "1.0", features = ["backtrace"]} [dev-dependencies] mockito = "0.30" diff --git a/README.md b/README.md index ccdc4f3..66fd072 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,18 @@ OPTIONS: --netbox-proxy HTTP(s) proxy to use to connect to Netbox [env: NETBOX_PROXY=] - --netbox-token The Netbox token [env: NETBOX_TOKEN] [default: ] - --netbox-url The Netbox API URL [env: NETBOX_URL=] + --netbox-tls-client-certificate + The TLS certificate to use to authenticate to Netbox (PKCS12 format) [env: NETBOX_TLS_CLIENT_CERTIFICATE=] + + --netbox-tls-client-certificate-password + The optional password for the netbox PKCS12 file [env: NETBOX_TLS_CLIENT_CERTIFICATE_PASSWORD=] + + --netbox-token + The Netbox token [env: NETBOX_TOKEN] + + --netbox-url + The Netbox API URL [env: NETBOX_URL=] + --netbox-vms-filter The querystring to use to select the VM from netbox [env: NETBOX_VMS_FILTER=] @@ -47,9 +57,17 @@ OPTIONS: --netshot-proxy HTTP(s) proxy to use to connect to Netshot [env: NETSHOT_PROXY=] - --netshot-token The Netshot token [env: NETSHOT_TOKEN] - --netshot-url The Netshot API URL [env: NETSHOT_URL=] -``` + --netshot-tls-client-certificate + The TLS certificate to use to authenticate to Netshot (PKCS12 format) [env: NETSHOT_TLS_CLIENT_CERTIFICATE=] + + --netshot-tls-client-certificate-password + The optional password for the netshot PKCS12 file [env: NETSHOT_TLS_CLIENT_CERTIFICATE_PASSWORD=] + + --netshot-token + The Netshot token [env: NETSHOT_TOKEN] + + --netshot-url + The Netshot API URL [env: NETSHOT_URL=]``` The query-string format need to be like this (url query string without the `?`): @@ -57,3 +75,7 @@ The query-string format need to be like this (url query string without the `?`): status=active&platform=cisco-ios&platform=cisco-ios-xe&platform=cisco-ios-xr&platform=cisco-nx-os&platform=juniper-junos&has_primary_ip=true&tenant_group=network ``` +If you plan to use TLS authentication, please provide a PKCS12 formatted identity file (.pfx or .p12), they can be created from .pem/.key/.crt using the following command: +```bash +openssl pkcs12 -export -out my.pfx -inkey my.key -in my.crt +``` diff --git a/src/main.rs b/src/main.rs index 3e4951e..91ff6db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,16 @@ struct Opt { #[structopt(long, help = "The Netshot API URL", env)] netshot_url: String, + #[structopt( + long, + help = "The TLS certificate to use to authenticate to Netshot (PKCS12 format)", + env + )] + netshot_tls_client_certificate: Option, + + #[structopt(long, help = "The optional password for the netshot PKCS12 file", env)] + netshot_tls_client_certificate_password: Option, + #[structopt(long, help = "The Netshot token", env, hide_env_values = true)] netshot_token: String, @@ -35,12 +45,16 @@ struct Opt { #[structopt( long, - help = "The Netbox token", - env, - hide_env_values = true, - default_value = "" + help = "The TLS certificate to use to authenticate to Netbox (PKCS12 format)", + env )] - netbox_token: String, + netbox_tls_client_certificate: Option, + + #[structopt(long, help = "The optional password for the netbox PKCS12 file", env)] + netbox_tls_client_certificate_password: Option, + + #[structopt(long, help = "The Netbox token", env, hide_env_values = true)] + netbox_token: Option, #[structopt( long, @@ -65,8 +79,7 @@ struct Opt { } /// Main application entrypoint -#[tokio::main] -async fn main() -> Result<(), Error> { +fn main() -> Result<(), Error> { let opt = Opt::from_args(); let mut logging_level = "info"; let mut duplicate_level = Duplicate::Info; @@ -78,21 +91,32 @@ async fn main() -> Result<(), Error> { Logger::with_str(logging_level) .log_target(LogTarget::File) .duplicate_to_stdout(duplicate_level) - .start()?; + .start() + .unwrap(); log::info!("Logger initialized with level {}", logging_level); log::debug!("CLI Parameters : {:#?}", opt); - let netbox_client = - netbox::NetboxClient::new(opt.netbox_url, opt.netbox_token, opt.netbox_proxy)?; - netbox_client.ping().await?; - - let netshot_client = - netshot::NetshotClient::new(opt.netshot_url, opt.netshot_token, opt.netshot_proxy)?; - netshot_client.ping().await?; + let netbox_client = netbox::NetboxClient::new( + opt.netbox_url, + opt.netbox_token, + opt.netbox_proxy, + opt.netbox_tls_client_certificate, + opt.netbox_tls_client_certificate_password, + )?; + netbox_client.ping()?; + + let netshot_client = netshot::NetshotClient::new( + opt.netshot_url, + opt.netshot_token, + opt.netshot_proxy, + opt.netshot_tls_client_certificate, + opt.netshot_tls_client_certificate_password, + )?; + netshot_client.ping()?; log::info!("Getting devices list from Netshot"); - let netshot_devices = netshot_client.get_devices().await?; + let netshot_devices = netshot_client.get_devices()?; log::debug!("Building netshot devices hashmap"); let netshot_hashmap: HashMap<_, _> = netshot_devices @@ -101,15 +125,11 @@ async fn main() -> Result<(), Error> { .collect(); log::info!("Getting devices list from Netbox"); - let mut netbox_devices = netbox_client - .get_devices(&opt.netbox_devices_filter) - .await?; + let mut netbox_devices = netbox_client.get_devices(&opt.netbox_devices_filter)?; if opt.netbox_vms_filter.is_some() { log::info!("Getting VMS list rom Netbox"); - let mut vms = netbox_client - .get_vms(&opt.netbox_vms_filter.unwrap()) - .await?; + let mut vms = netbox_client.get_vms(&opt.netbox_vms_filter.unwrap())?; log::debug!("Merging VMs and Devices lists"); netbox_devices.append(&mut vms); } @@ -154,21 +174,18 @@ async fn main() -> Result<(), Error> { if !opt.check { for device in missing_devices { - let registration = netshot_client - .register_device(&device, opt.netshot_domain_id) - .await; + let registration = netshot_client.register_device(&device, opt.netshot_domain_id); if let Err(error) = registration { log::warn!("Registration failure: {}", error); } } } - Ok(()) } #[cfg(test)] mod tests { - use flexi_logger::{Duplicate, LogTarget, Logger}; + use flexi_logger::{LogTarget, Logger}; #[ctor::ctor] fn enable_logging() { diff --git a/src/rest/helpers.rs b/src/rest/helpers.rs new file mode 100644 index 0000000..aea7dfb --- /dev/null +++ b/src/rest/helpers.rs @@ -0,0 +1,21 @@ +use anyhow::{anyhow, Error}; +use reqwest::Identity; +use std::fs::File; +use std::io::Read; + +/// Create an identity from a private key and certificate registered in a PKCS12 file (with or without password) +pub fn build_identity_from_file( + filename: String, + password: Option, +) -> Result { + let mut buf = Vec::new(); + File::open(filename.as_str())?.read_to_end(&mut buf)?; + + log::info!("Building identity from {} PFX/P12 file", filename); + let identity = match password { + Some(p) => Identity::from_pkcs12_der(&buf, p.as_str())?, + None => Identity::from_pkcs12_der(&buf, "")?, + }; + + Ok(identity) +} diff --git a/src/rest/mod.rs b/src/rest/mod.rs index 766dfdc..a93d88c 100644 --- a/src/rest/mod.rs +++ b/src/rest/mod.rs @@ -1,2 +1,3 @@ +pub mod helpers; pub mod netbox; pub mod netshot; diff --git a/src/rest/netbox.rs b/src/rest/netbox.rs index ed35049..3bac355 100644 --- a/src/rest/netbox.rs +++ b/src/rest/netbox.rs @@ -1,4 +1,5 @@ use crate::common::APP_USER_AGENT; +use crate::rest::helpers::build_identity_from_file; use anyhow::{anyhow, Error, Result}; use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::Proxy; @@ -15,7 +16,7 @@ const PATH_VIRT_VM: &str = "/api/virtualization/virtual-machines/"; pub struct NetboxClient { pub url: String, pub token: String, - pub client: reqwest::Client, + pub client: reqwest::blocking::Client, } /// Represent the primary_ip field from the DCIM device API call @@ -63,45 +64,63 @@ impl Device { impl NetboxClient { /// Create a client without authentication pub fn new_anonymous(url: String, proxy: Option) -> Result { - NetboxClient::new(url, String::from(""), proxy) + NetboxClient::new(url, None, proxy, None, None) } /// Create a client with the given authentication token - pub fn new(url: String, token: String, proxy: Option) -> Result { + pub fn new( + url: String, + token: Option, + proxy: Option, + tls_client_certificate: Option, + tls_client_certificate_password: Option, + ) -> Result { log::debug!("Creating new Netbox client to {}", url); - let mut http_headers = HeaderMap::new(); - if token != "" { - let header_value = HeaderValue::from_str(format!("Token {}", token).as_str())?; - http_headers.insert("Authorization", header_value); - } - let mut http_client = reqwest::Client::builder() + let mut http_client = reqwest::blocking::Client::builder() .user_agent(APP_USER_AGENT) - .timeout(Duration::from_secs(5)) - .default_headers(http_headers); + .timeout(Duration::from_secs(5)); + + http_client = match token { + Some(ref t) => { + let mut http_headers = HeaderMap::new(); + let header_value = HeaderValue::from_str(format!("Token {}", t).as_str())?; + http_headers.insert("Authorization", header_value); + http_client.default_headers(http_headers) + } + None => http_client, + }; http_client = match proxy { Some(p) => http_client.proxy(Proxy::all(p)?), None => http_client, }; + http_client = match tls_client_certificate { + Some(c) => http_client.identity(build_identity_from_file( + c, + tls_client_certificate_password, + )?), + None => http_client, + }; + Ok(Self { url, - token, + token: token.unwrap_or("".to_string()), client: http_client.build()?, }) } /// Ping the service to make sure it is reachable and pass the authentication (if there is any) - pub async fn ping(&self) -> Result { + pub fn ping(&self) -> Result { let url = format!("{}{}", self.url, PATH_PING); log::debug!("Pinging {}", url); - let response = self.client.get(url).send().await?; + let response = self.client.get(url).send()?; log::debug!("Ping response: {}", response.status()); Ok(response.status().is_success()) } /// Get a single device page - pub async fn get_devices_page( + pub fn get_devices_page( &self, path: &str, query_string: &String, @@ -112,19 +131,18 @@ impl NetboxClient { "{}{}?limit={}&offset={}&{}", self.url, path, limit, offset, query_string ); - let page: NetboxDCIMDeviceList = self.client.get(url).send().await?.json().await?; + let page: NetboxDCIMDeviceList = self.client.get(url).send()?.json()?; Ok(page) } /// Get the devices using the given filter - pub async fn get_devices(&self, query_string: &String) -> Result, Error> { + pub fn get_devices(&self, query_string: &String) -> Result, Error> { let mut devices: Vec = Vec::new(); let mut offset = 0; loop { - let mut response = self - .get_devices_page(PATH_DCIM_DEVICES, &query_string, API_LIMIT, offset) - .await?; + let mut response = + self.get_devices_page(PATH_DCIM_DEVICES, &query_string, API_LIMIT, offset)?; devices.append(&mut response.results); @@ -150,14 +168,13 @@ impl NetboxClient { } /// Get the VMs as device using the given filter - pub async fn get_vms(&self, query_string: &String) -> Result, Error> { + pub fn get_vms(&self, query_string: &String) -> Result, Error> { let mut devices: Vec = Vec::new(); let mut offset = 0; loop { - let mut response = self - .get_devices_page(PATH_VIRT_VM, &query_string, API_LIMIT, offset) - .await?; + let mut response = + self.get_devices_page(PATH_VIRT_VM, &query_string, API_LIMIT, offset)?; devices.append(&mut response.results); @@ -200,13 +217,13 @@ mod tests { fn authenticated_initialization() { let url = mockito::server_url(); let token = String::from("hello"); - let client = NetboxClient::new(url.clone(), token.clone(), None).unwrap(); + let client = NetboxClient::new(url.clone(), Some(token.clone()), None, None, None).unwrap(); assert_eq!(client.token, token); assert_eq!(client.url, url); } - #[tokio::test] - async fn failed_ping() { + #[test] + fn failed_ping() { let url = mockito::server_url(); let _mock = mockito::mock("GET", mockito::Matcher::Any) @@ -214,12 +231,12 @@ mod tests { .create(); let client = NetboxClient::new_anonymous(url.clone(), None).unwrap(); - let ping = client.ping().await.unwrap(); + let ping = client.ping().unwrap(); assert_eq!(ping, false); } - #[tokio::test] - async fn successful_ping() { + #[test] + fn successful_ping() { let url = mockito::server_url(); let _mock = mockito::mock("GET", PATH_PING) @@ -227,12 +244,12 @@ mod tests { .create(); let client = NetboxClient::new_anonymous(url.clone(), None).unwrap(); - let ping = client.ping().await.unwrap(); + let ping = client.ping().unwrap(); assert_eq!(ping, true); } - #[tokio::test] - async fn single_good_device() { + #[test] + fn single_good_device() { let url = mockito::server_url(); let _mock = mockito::mock("GET", PATH_DCIM_DEVICES) @@ -241,7 +258,7 @@ mod tests { .create(); let client = NetboxClient::new_anonymous(url.clone(), None).unwrap(); - let devices = client.get_devices(&String::from("")).await.unwrap(); + let devices = client.get_devices(&String::from("")).unwrap(); assert_eq!(devices.len(), 1); @@ -253,8 +270,8 @@ mod tests { assert_eq!(device.is_valid(), true); } - #[tokio::test] - async fn single_device_without_primary_ip() { + #[test] + fn single_device_without_primary_ip() { let url = mockito::server_url(); let _mock = mockito::mock("GET", PATH_DCIM_DEVICES) @@ -263,7 +280,7 @@ mod tests { .create(); let client = NetboxClient::new_anonymous(url.clone(), None).unwrap(); - let devices = client.get_devices(&String::from("")).await.unwrap(); + let devices = client.get_devices(&String::from("")).unwrap(); assert_eq!(devices.len(), 1); @@ -272,8 +289,8 @@ mod tests { assert_eq!(device.is_valid(), false); } - #[tokio::test] - async fn single_device_without_name() { + #[test] + fn single_device_without_name() { let url = mockito::server_url(); let _mock = mockito::mock("GET", PATH_DCIM_DEVICES) @@ -282,7 +299,7 @@ mod tests { .create(); let client = NetboxClient::new_anonymous(url.clone(), None).unwrap(); - let devices = client.get_devices(&String::from("")).await.unwrap(); + let devices = client.get_devices(&String::from("")).unwrap(); assert_eq!(devices.len(), 1); diff --git a/src/rest/netshot.rs b/src/rest/netshot.rs index 5ea6bc8..381d0fa 100644 --- a/src/rest/netshot.rs +++ b/src/rest/netshot.rs @@ -1,4 +1,5 @@ use crate::common::APP_USER_AGENT; +use crate::rest::helpers::build_identity_from_file; use anyhow::{anyhow, Error, Result}; use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::Proxy; @@ -12,7 +13,7 @@ const PATH_DEVICES: &str = "/api/devices"; pub struct NetshotClient { pub url: String, pub token: String, - pub client: reqwest::Client, + pub client: reqwest::blocking::Client, } #[derive(Debug, Serialize, Deserialize)] @@ -54,13 +55,19 @@ pub struct NewDeviceCreatedPayload { impl NetshotClient { /// Create a client with the given authentication token - pub fn new(url: String, token: String, proxy: Option) -> Result { + pub fn new( + url: String, + token: String, + proxy: Option, + tls_client_certificate: Option, + tls_client_certificate_password: Option, + ) -> Result { log::debug!("Creating new Netshot client to {}", url); let mut http_headers = HeaderMap::new(); let header_value = HeaderValue::from_str(token.as_str())?; http_headers.insert("X-Netshot-API-Token", header_value); http_headers.insert("Accept", HeaderValue::from_str("application/json")?); - let mut http_client = reqwest::Client::builder() + let mut http_client = reqwest::blocking::Client::builder() .user_agent(APP_USER_AGENT) .timeout(Duration::from_secs(5)) .default_headers(http_headers); @@ -70,6 +77,14 @@ impl NetshotClient { None => http_client, }; + http_client = match tls_client_certificate { + Some(c) => http_client.identity(build_identity_from_file( + c, + tls_client_certificate_password, + )?), + None => http_client, + }; + Ok(Self { url, token, @@ -78,15 +93,15 @@ impl NetshotClient { } /// To be implemented server side, always return true for now - pub async fn ping(&self) -> Result { + pub fn ping(&self) -> Result { log::warn!("Not health check implemented on Netshot, ping will always succeed"); Ok(true) } /// Get devices registered in Netshot - pub async fn get_devices(&self) -> Result, Error> { + pub fn get_devices(&self) -> Result, Error> { let url = format!("{}{}", self.url, PATH_DEVICES); - let devices: Vec = self.client.get(url).send().await?.json().await?; + let devices: Vec = self.client.get(url).send()?.json()?; log::debug!("Got {} devices from Netshot", devices.len()); @@ -94,7 +109,7 @@ impl NetshotClient { } /// Register a given IP into Netshot and return the corresponding device - pub async fn register_device( + pub fn register_device( &self, ip_address: &String, domain_id: u32, @@ -108,7 +123,7 @@ impl NetshotClient { }; let url = format!("{}{}", self.url, PATH_DEVICES); - let response = self.client.post(url).json(&new_device).send().await?; + let response = self.client.post(url).json(&new_device).send()?; if !response.status().is_success() { log::warn!( @@ -119,7 +134,7 @@ impl NetshotClient { return Err(anyhow!("Failed to register new device {}", ip_address)); } - let device_registration: NewDeviceCreatedPayload = response.json().await?; + let device_registration: NewDeviceCreatedPayload = response.json()?; log::debug!( "Device registration for device {} requested with task ID {}", ip_address, @@ -139,13 +154,13 @@ mod tests { fn authenticated_initialization() { let url = mockito::server_url(); let token = String::from("hello"); - let client = NetshotClient::new(url.clone(), token.clone(), None).unwrap(); + let client = NetshotClient::new(url.clone(), token.clone(), None, None, None).unwrap(); assert_eq!(client.token, token); assert_eq!(client.url, url); } - #[tokio::test] - async fn single_good_device() { + #[test] + fn single_good_device() { let url = mockito::server_url(); let _mock = mockito::mock("GET", PATH_DEVICES) @@ -153,8 +168,8 @@ mod tests { .with_body_from_file("tests/data/netshot/single_good_device.json") .create(); - let client = NetshotClient::new(url.clone(), String::new(), None).unwrap(); - let devices = client.get_devices().await.unwrap(); + let client = NetshotClient::new(url.clone(), String::new(), None, None, None).unwrap(); + let devices = client.get_devices().unwrap(); assert_eq!(devices.len(), 1); @@ -165,8 +180,8 @@ mod tests { assert_eq!(device.management_address.ip, "1.2.3.4"); } - #[tokio::test] - async fn good_device_registration() { + #[test] + fn good_device_registration() { let url = mockito::server_url(); let _mock = mockito::mock("POST", PATH_DEVICES) @@ -175,11 +190,8 @@ mod tests { .with_body_from_file("tests/data/netshot/good_device_registration.json") .create(); - let client = NetshotClient::new(url.clone(), String::new(), None).unwrap(); - let registration = client - .register_device(&String::from("1.2.3.4"), 2) - .await - .unwrap(); + let client = NetshotClient::new(url.clone(), String::new(), None, None, None).unwrap(); + let registration = client.register_device(&String::from("1.2.3.4"), 2).unwrap(); assert_eq!(registration.task_id, 504); assert_eq!(registration.status, "SCHEDULED");