Skip to content

Commit

Permalink
v0.1.8: Disable devices on netshot when they are missing from netbox
Browse files Browse the repository at this point in the history
  • Loading branch information
Mathieu Poussin committed Nov 17, 2021
1 parent 5408172 commit 912204e
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 21 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "netbox2netshot"
version = "0.1.7"
version = "0.1.8"
authors = ["Mathieu Poussin <[email protected]>"]
edition = "2018"
description = "Synchronization tool between netbox and netshot"
Expand Down
58 changes: 42 additions & 16 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ fn main() -> Result<(), Error> {
log::info!("Getting devices list from Netshot");
let netshot_devices = netshot_client.get_devices()?;

log::debug!("Building netshot devices hashmap");
let netshot_hashmap: HashMap<_, _> = netshot_devices
log::debug!("Building netshot devices simplified inventory");
let netshot_simplified_inventory: HashMap<_, _> = netshot_devices
.into_iter()
.map(|dev| (dev.management_address.ip, dev.name))
.collect();
Expand All @@ -137,8 +137,8 @@ fn main() -> Result<(), Error> {
netbox_devices.append(&mut vms);
}

log::debug!("Building netbox devices hashmap");
let netbox_hashmap: HashMap<_, _> = netbox_devices
log::debug!("Building netbox devices simplified inventory");
let netbox_simplified_devices: HashMap<_, _> = netbox_devices
.into_iter()
.filter_map(|device| match device.primary_ip4 {
Some(x) => Some((
Expand All @@ -156,32 +156,58 @@ fn main() -> Result<(), Error> {
.collect();

log::debug!(
"Hashmaps: Netbox({}), Netshot({})",
netbox_hashmap.len(),
netshot_hashmap.len()
"Simplified inventories: Netbox({}), Netshot({})",
netbox_simplified_devices.len(),
netshot_simplified_inventory.len()
);

log::debug!("Comparing HashMaps");
let mut missing_devices: Vec<String> = Vec::new();
for (ip, hostname) in netbox_hashmap {
match netshot_hashmap.get(&ip) {
log::debug!("Comparing inventories");

let mut devices_to_register: Vec<String> = Vec::new();
for (ip, hostname) in &netbox_simplified_devices {
match netshot_simplified_inventory.get(ip) {
Some(x) => log::debug!("{}({}) is present on both", x, ip),
None => {
log::debug!("{}({}) missing from Netshot", hostname, ip);
missing_devices.push(ip);
devices_to_register.push(ip.clone());
}
}
}

log::info!("Found {} devices missing on Netshot", missing_devices.len());
let mut devices_to_disable: Vec<String> = Vec::new();
for (ip, hostname) in &netshot_simplified_inventory {
match netbox_simplified_devices.get(ip) {
Some(x) => log::debug!("{}({}) is present on both", x, ip),
None => {
log::debug!("{}({}) missing from Netbox", hostname, ip);
devices_to_disable.push(ip.clone());
}
}
}

log::info!(
"Found {} devices missing on Netshot, to be added",
devices_to_register.len()
);
log::info!(
"Found {} devices missing on Netbox, to be disabled",
devices_to_disable.len()
);

if !opt.check {
for device in missing_devices {
let registration = netshot_client.register_device(&device, opt.netshot_domain_id);
for device in devices_to_register {
let registration = netshot_client.register_device(device, opt.netshot_domain_id);
if let Err(error) = registration {
log::warn!("Registration failure: {}", error);
}
}

for device in devices_to_disable {
let registration = netshot_client.disable_device(device);
if let Err(error) = registration {
log::warn!("Disable failure: {}", error);
}
}
}
Ok(())
}
Expand All @@ -192,7 +218,7 @@ mod tests {

#[ctor::ctor]
fn enable_logging() {
Logger::try_with_str("info")
Logger::try_with_str("debug")
.unwrap()
.adaptive_format_for_stderr(AdaptiveFormat::Detailed);
}
Expand Down
2 changes: 1 addition & 1 deletion src/rest/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{Error};
use anyhow::Error;
use reqwest::Identity;
use std::fs::File;
use std::io::Read;
Expand Down
167 changes: 165 additions & 2 deletions src/rest/netshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use std::time::Duration;

const PATH_DEVICES: &str = "/api/devices";
const PATH_DEVICES_SEARCH: &str = "/api/devices/search";

#[derive(Debug)]
pub struct NetshotClient {
Expand Down Expand Up @@ -53,6 +54,27 @@ pub struct NewDeviceCreatedPayload {
pub status: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct UpdateDevicePayload {
enabled: bool,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct DeviceUpdatedPayload {
pub status: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct DeviceSearchQueryPayload {
query: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct DeviceSearchResultPayload {
pub query: String,
pub devices: Vec<Device>,
}

impl NetshotClient {
/// Create a client with the given authentication token
pub fn new(
Expand Down Expand Up @@ -111,7 +133,7 @@ impl NetshotClient {
/// Register a given IP into Netshot and return the corresponding device
pub fn register_device(
&self,
ip_address: &String,
ip_address: String,
domain_id: u32,
) -> Result<NewDeviceCreatedPayload, Error> {
log::info!("Registering new device with IP {}", ip_address);
Expand Down Expand Up @@ -143,6 +165,106 @@ impl NetshotClient {

Ok(device_registration)
}

/// Search for a device
pub fn search_device(&self, query_string: String) -> Result<DeviceSearchResultPayload, Error> {
let url = format!("{}{}", self.url, PATH_DEVICES_SEARCH);

let query = DeviceSearchQueryPayload {
query: query_string.clone(),
};

let response = self.client.post(url).json(&query).send()?;

if !response.status().is_success() {
log::warn!(
"Failed to search for device with query `{}`: {}",
query_string.clone(),
response.status().to_string()
);
return Err(anyhow!(
"Failed to search for device with query: {}",
query_string
));
}

let search_result: DeviceSearchResultPayload = response.json()?;
log::debug!(
"Found {} devices with the given search",
search_result.devices.len(),
);

Ok(search_result)
}

/// Set the given device to a given state (enabled/disabled)
fn set_device_enabled(
&self,
ip_address: String,
enabled: bool,
) -> Result<Option<DeviceUpdatedPayload>, Error> {
log::info!(
"Setting device with IP {} to enabled={}",
ip_address,
enabled
);

let state = UpdateDevicePayload { enabled: enabled };

// Search for the device ID
let response = self.search_device(format!("[IP] IS {}", ip_address))?;
let device = response.devices.first().unwrap();

if !enabled && device.status == "DISABLED" {
log::warn!(
"Device {}({}) is already disabled, skipping",
device.name,
ip_address
);
return Ok(Option::None);
} else if enabled && device.status != "DISABLED" {
log::warn!(
"Device {}({}) is already enabled, skipping",
device.name,
ip_address
);
return Ok(Option::None);
}

let url = format!("{}{}/{}", self.url, PATH_DEVICES, device.id);
let response = self.client.put(url).json(&state).send()?;

if !response.status().is_success() {
log::warn!(
"Failed to update state for device {}, got status {}",
ip_address,
response.status().to_string()
);
return Err(anyhow!(
"Failed to update state for device {}, got status {}",
ip_address,
response.status().to_string()
));
}

let device_update: DeviceUpdatedPayload = response.json()?;
log::debug!("Device state of {} set to enabled={}", ip_address, enabled);

Ok(Option::Some(device_update))
}

/// Disable a given device
pub fn disable_device(
&self,
ip_address: String,
) -> Result<Option<DeviceUpdatedPayload>, Error> {
self.set_device_enabled(ip_address, false)
}

/// Enable a given device
pub fn enable_device(&self, ip_address: String) -> Result<Option<DeviceUpdatedPayload>, Error> {
self.set_device_enabled(ip_address, true)
}
}

#[cfg(test)]
Expand Down Expand Up @@ -191,9 +313,50 @@ mod tests {
.create();

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();
let registration = client.register_device(String::from("1.2.3.4"), 2).unwrap();

assert_eq!(registration.task_id, 504);
assert_eq!(registration.status, "SCHEDULED");
}

#[test]
fn search_devices() {
let url = mockito::server_url();

let _mock = mockito::mock("POST", PATH_DEVICES_SEARCH)
.match_query(mockito::Matcher::Any)
.match_body(r#"{"query":"[IP] IS 1.2.3.4"}"#)
.with_body_from_file("tests/data/netshot/search.json")
.create();

let client = NetshotClient::new(url.clone(), String::new(), None, None, None).unwrap();
let result = client
.search_device(String::from("[IP] IS 1.2.3.4"))
.unwrap();

assert_eq!(result.devices.len(), 2);
assert_eq!(result.query, "[IP] IS 1.2.3.4");
}

#[test]
fn disable_device() {
let url = mockito::server_url();

let _mock = mockito::mock("PUT", format!("{}/{}", PATH_DEVICES, 2318).as_str())
.match_query(mockito::Matcher::Any)
.match_body(r#"{"enabled":false}"#)
.with_body_from_file("tests/data/netshot/disable_device.json")
.create();

let _mock2 = mockito::mock("POST", PATH_DEVICES_SEARCH)
.match_query(mockito::Matcher::Any)
.match_body(r#"{"query":"[IP] IS 1.2.3.4"}"#)
.with_body_from_file("tests/data/netshot/search.json")
.create();

let client = NetshotClient::new(url.clone(), String::new(), None, None, None).unwrap();
let registration = client.disable_device(String::from("1.2.3.4")).unwrap();

assert_eq!(registration.unwrap().status, "DISABLED");
}
}
3 changes: 3 additions & 0 deletions tests/data/netshot/disable_device.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"status": "DISABLED"
}
27 changes: 27 additions & 0 deletions tests/data/netshot/search.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"query": "[IP] IS 1.2.3.4",
"devices": [
{
"id": 2318,
"name": "test-device.dc",
"family": "Cisco Catalyst 2900",
"mgmtAddress": {
"prefixLength": 0,
"addressUsage": "PRIMARY",
"ip": "1.2.3.4"
},
"status": "INPRODUCTION"
},
{
"id": 2318,
"name": "test-device.dc",
"family": "Cisco Catalyst 2900",
"mgmtAddress": {
"prefixLength": 0,
"addressUsage": "PRIMARY",
"ip": "1.2.3.4"
},
"status": "INPRODUCTION"
}
]
}

0 comments on commit 912204e

Please sign in to comment.