Skip to content

Commit 912204e

Browse files
author
Mathieu Poussin
committed
v0.1.8: Disable devices on netshot when they are missing from netbox
1 parent 5408172 commit 912204e

File tree

7 files changed

+240
-21
lines changed

7 files changed

+240
-21
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "netbox2netshot"
3-
version = "0.1.7"
3+
version = "0.1.8"
44
authors = ["Mathieu Poussin <[email protected]>"]
55
edition = "2018"
66
description = "Synchronization tool between netbox and netshot"

src/main.rs

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ fn main() -> Result<(), Error> {
121121
log::info!("Getting devices list from Netshot");
122122
let netshot_devices = netshot_client.get_devices()?;
123123

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

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

158158
log::debug!(
159-
"Hashmaps: Netbox({}), Netshot({})",
160-
netbox_hashmap.len(),
161-
netshot_hashmap.len()
159+
"Simplified inventories: Netbox({}), Netshot({})",
160+
netbox_simplified_devices.len(),
161+
netshot_simplified_inventory.len()
162162
);
163163

164-
log::debug!("Comparing HashMaps");
165-
let mut missing_devices: Vec<String> = Vec::new();
166-
for (ip, hostname) in netbox_hashmap {
167-
match netshot_hashmap.get(&ip) {
164+
log::debug!("Comparing inventories");
165+
166+
let mut devices_to_register: Vec<String> = Vec::new();
167+
for (ip, hostname) in &netbox_simplified_devices {
168+
match netshot_simplified_inventory.get(ip) {
168169
Some(x) => log::debug!("{}({}) is present on both", x, ip),
169170
None => {
170171
log::debug!("{}({}) missing from Netshot", hostname, ip);
171-
missing_devices.push(ip);
172+
devices_to_register.push(ip.clone());
172173
}
173174
}
174175
}
175176

176-
log::info!("Found {} devices missing on Netshot", missing_devices.len());
177+
let mut devices_to_disable: Vec<String> = Vec::new();
178+
for (ip, hostname) in &netshot_simplified_inventory {
179+
match netbox_simplified_devices.get(ip) {
180+
Some(x) => log::debug!("{}({}) is present on both", x, ip),
181+
None => {
182+
log::debug!("{}({}) missing from Netbox", hostname, ip);
183+
devices_to_disable.push(ip.clone());
184+
}
185+
}
186+
}
187+
188+
log::info!(
189+
"Found {} devices missing on Netshot, to be added",
190+
devices_to_register.len()
191+
);
192+
log::info!(
193+
"Found {} devices missing on Netbox, to be disabled",
194+
devices_to_disable.len()
195+
);
177196

178197
if !opt.check {
179-
for device in missing_devices {
180-
let registration = netshot_client.register_device(&device, opt.netshot_domain_id);
198+
for device in devices_to_register {
199+
let registration = netshot_client.register_device(device, opt.netshot_domain_id);
181200
if let Err(error) = registration {
182201
log::warn!("Registration failure: {}", error);
183202
}
184203
}
204+
205+
for device in devices_to_disable {
206+
let registration = netshot_client.disable_device(device);
207+
if let Err(error) = registration {
208+
log::warn!("Disable failure: {}", error);
209+
}
210+
}
185211
}
186212
Ok(())
187213
}
@@ -192,7 +218,7 @@ mod tests {
192218

193219
#[ctor::ctor]
194220
fn enable_logging() {
195-
Logger::try_with_str("info")
221+
Logger::try_with_str("debug")
196222
.unwrap()
197223
.adaptive_format_for_stderr(AdaptiveFormat::Detailed);
198224
}

src/rest/helpers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use anyhow::{Error};
1+
use anyhow::Error;
22
use reqwest::Identity;
33
use std::fs::File;
44
use std::io::Read;

src/rest/netshot.rs

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
88
use std::time::Duration;
99

1010
const PATH_DEVICES: &str = "/api/devices";
11+
const PATH_DEVICES_SEARCH: &str = "/api/devices/search";
1112

1213
#[derive(Debug)]
1314
pub struct NetshotClient {
@@ -53,6 +54,27 @@ pub struct NewDeviceCreatedPayload {
5354
pub status: String,
5455
}
5556

57+
#[derive(Debug, Serialize, Deserialize)]
58+
struct UpdateDevicePayload {
59+
enabled: bool,
60+
}
61+
62+
#[derive(Debug, Serialize, Deserialize)]
63+
pub struct DeviceUpdatedPayload {
64+
pub status: String,
65+
}
66+
67+
#[derive(Debug, Serialize, Deserialize)]
68+
struct DeviceSearchQueryPayload {
69+
query: String,
70+
}
71+
72+
#[derive(Debug, Serialize, Deserialize)]
73+
pub struct DeviceSearchResultPayload {
74+
pub query: String,
75+
pub devices: Vec<Device>,
76+
}
77+
5678
impl NetshotClient {
5779
/// Create a client with the given authentication token
5880
pub fn new(
@@ -111,7 +133,7 @@ impl NetshotClient {
111133
/// Register a given IP into Netshot and return the corresponding device
112134
pub fn register_device(
113135
&self,
114-
ip_address: &String,
136+
ip_address: String,
115137
domain_id: u32,
116138
) -> Result<NewDeviceCreatedPayload, Error> {
117139
log::info!("Registering new device with IP {}", ip_address);
@@ -143,6 +165,106 @@ impl NetshotClient {
143165

144166
Ok(device_registration)
145167
}
168+
169+
/// Search for a device
170+
pub fn search_device(&self, query_string: String) -> Result<DeviceSearchResultPayload, Error> {
171+
let url = format!("{}{}", self.url, PATH_DEVICES_SEARCH);
172+
173+
let query = DeviceSearchQueryPayload {
174+
query: query_string.clone(),
175+
};
176+
177+
let response = self.client.post(url).json(&query).send()?;
178+
179+
if !response.status().is_success() {
180+
log::warn!(
181+
"Failed to search for device with query `{}`: {}",
182+
query_string.clone(),
183+
response.status().to_string()
184+
);
185+
return Err(anyhow!(
186+
"Failed to search for device with query: {}",
187+
query_string
188+
));
189+
}
190+
191+
let search_result: DeviceSearchResultPayload = response.json()?;
192+
log::debug!(
193+
"Found {} devices with the given search",
194+
search_result.devices.len(),
195+
);
196+
197+
Ok(search_result)
198+
}
199+
200+
/// Set the given device to a given state (enabled/disabled)
201+
fn set_device_enabled(
202+
&self,
203+
ip_address: String,
204+
enabled: bool,
205+
) -> Result<Option<DeviceUpdatedPayload>, Error> {
206+
log::info!(
207+
"Setting device with IP {} to enabled={}",
208+
ip_address,
209+
enabled
210+
);
211+
212+
let state = UpdateDevicePayload { enabled: enabled };
213+
214+
// Search for the device ID
215+
let response = self.search_device(format!("[IP] IS {}", ip_address))?;
216+
let device = response.devices.first().unwrap();
217+
218+
if !enabled && device.status == "DISABLED" {
219+
log::warn!(
220+
"Device {}({}) is already disabled, skipping",
221+
device.name,
222+
ip_address
223+
);
224+
return Ok(Option::None);
225+
} else if enabled && device.status != "DISABLED" {
226+
log::warn!(
227+
"Device {}({}) is already enabled, skipping",
228+
device.name,
229+
ip_address
230+
);
231+
return Ok(Option::None);
232+
}
233+
234+
let url = format!("{}{}/{}", self.url, PATH_DEVICES, device.id);
235+
let response = self.client.put(url).json(&state).send()?;
236+
237+
if !response.status().is_success() {
238+
log::warn!(
239+
"Failed to update state for device {}, got status {}",
240+
ip_address,
241+
response.status().to_string()
242+
);
243+
return Err(anyhow!(
244+
"Failed to update state for device {}, got status {}",
245+
ip_address,
246+
response.status().to_string()
247+
));
248+
}
249+
250+
let device_update: DeviceUpdatedPayload = response.json()?;
251+
log::debug!("Device state of {} set to enabled={}", ip_address, enabled);
252+
253+
Ok(Option::Some(device_update))
254+
}
255+
256+
/// Disable a given device
257+
pub fn disable_device(
258+
&self,
259+
ip_address: String,
260+
) -> Result<Option<DeviceUpdatedPayload>, Error> {
261+
self.set_device_enabled(ip_address, false)
262+
}
263+
264+
/// Enable a given device
265+
pub fn enable_device(&self, ip_address: String) -> Result<Option<DeviceUpdatedPayload>, Error> {
266+
self.set_device_enabled(ip_address, true)
267+
}
146268
}
147269

148270
#[cfg(test)]
@@ -191,9 +313,50 @@ mod tests {
191313
.create();
192314

193315
let client = NetshotClient::new(url.clone(), String::new(), None, None, None).unwrap();
194-
let registration = client.register_device(&String::from("1.2.3.4"), 2).unwrap();
316+
let registration = client.register_device(String::from("1.2.3.4"), 2).unwrap();
195317

196318
assert_eq!(registration.task_id, 504);
197319
assert_eq!(registration.status, "SCHEDULED");
198320
}
321+
322+
#[test]
323+
fn search_devices() {
324+
let url = mockito::server_url();
325+
326+
let _mock = mockito::mock("POST", PATH_DEVICES_SEARCH)
327+
.match_query(mockito::Matcher::Any)
328+
.match_body(r#"{"query":"[IP] IS 1.2.3.4"}"#)
329+
.with_body_from_file("tests/data/netshot/search.json")
330+
.create();
331+
332+
let client = NetshotClient::new(url.clone(), String::new(), None, None, None).unwrap();
333+
let result = client
334+
.search_device(String::from("[IP] IS 1.2.3.4"))
335+
.unwrap();
336+
337+
assert_eq!(result.devices.len(), 2);
338+
assert_eq!(result.query, "[IP] IS 1.2.3.4");
339+
}
340+
341+
#[test]
342+
fn disable_device() {
343+
let url = mockito::server_url();
344+
345+
let _mock = mockito::mock("PUT", format!("{}/{}", PATH_DEVICES, 2318).as_str())
346+
.match_query(mockito::Matcher::Any)
347+
.match_body(r#"{"enabled":false}"#)
348+
.with_body_from_file("tests/data/netshot/disable_device.json")
349+
.create();
350+
351+
let _mock2 = mockito::mock("POST", PATH_DEVICES_SEARCH)
352+
.match_query(mockito::Matcher::Any)
353+
.match_body(r#"{"query":"[IP] IS 1.2.3.4"}"#)
354+
.with_body_from_file("tests/data/netshot/search.json")
355+
.create();
356+
357+
let client = NetshotClient::new(url.clone(), String::new(), None, None, None).unwrap();
358+
let registration = client.disable_device(String::from("1.2.3.4")).unwrap();
359+
360+
assert_eq!(registration.unwrap().status, "DISABLED");
361+
}
199362
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"status": "DISABLED"
3+
}

tests/data/netshot/search.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"query": "[IP] IS 1.2.3.4",
3+
"devices": [
4+
{
5+
"id": 2318,
6+
"name": "test-device.dc",
7+
"family": "Cisco Catalyst 2900",
8+
"mgmtAddress": {
9+
"prefixLength": 0,
10+
"addressUsage": "PRIMARY",
11+
"ip": "1.2.3.4"
12+
},
13+
"status": "INPRODUCTION"
14+
},
15+
{
16+
"id": 2318,
17+
"name": "test-device.dc",
18+
"family": "Cisco Catalyst 2900",
19+
"mgmtAddress": {
20+
"prefixLength": 0,
21+
"addressUsage": "PRIMARY",
22+
"ip": "1.2.3.4"
23+
},
24+
"status": "INPRODUCTION"
25+
}
26+
]
27+
}

0 commit comments

Comments
 (0)