Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API] Implement device creation #65

Merged
merged 14 commits into from
Apr 22, 2024
Merged
Prev Previous commit
Next Next commit
Add non-collectible fields to config and translator
split off device creation request
ByteOtter committed Apr 22, 2024
commit dc509ba2b295a23fd46e2a91cee65c8f17f7732b
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ toml = "0.7.6"
thanix_client = "1.0.0"
# Uncomment this line if you are using a custom thanix client implementation.
# Change the path to be relative to this Cargo.toml file.
# thanix_client = { path = "../thanix_client/" }
# thanix_client = { path = "path/to/your/client/crate" }

[dev-dependencies]
mockall = "0.11.4"
9 changes: 5 additions & 4 deletions src/collectors/dmi_collector.rs
Original file line number Diff line number Diff line change
@@ -427,7 +427,10 @@ fn dmidecode_cpu<T: DmiDecodeTable>(_param: T) -> CpuInformation {
cpu_information.arch = match get_architecture() {
Ok(arch) => Some(arch),
Err(e) => {
eprintln!("\x1b[31m[error]\x1b[0m Failure to get cpu information via `uname`!\n{}", e);
eprintln!(
"\x1b[31m[error]\x1b[0m Failure to get cpu information via `uname`!\n{}",
e
);
None
}
};
@@ -445,9 +448,7 @@ fn dmidecode_cpu<T: DmiDecodeTable>(_param: T) -> CpuInformation {
fn get_architecture() -> Result<String, UnableToCollectDataError> {
println!("Running uname to collect CPU architecture...");
let output = match Command::new("uname").arg("-p").output() {
Ok(output) => {
output
},
Ok(output) => output,
Err(e) => {
let exc: UnableToCollectDataError = UnableToCollectDataError {
message: String::from(format!("\x1b[31m[error]\x1b[0m An error occured while attempting to execute `uname -p`! {}", e))
31 changes: 30 additions & 1 deletion src/configuration/config_parser.rs
Original file line number Diff line number Diff line change
@@ -42,6 +42,11 @@ use std::{fs, path::PathBuf};

use super::config_exceptions::{self, *};

/// Configuration State set by the configuration file.
///
/// # Members
/// - netbox: `NetBoxConfig` - Configuration parameters for the NetBox connection.
/// - system: `SystemConfig` - Parameters abouth the system.
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigData {
pub netbox: NetboxConfig,
@@ -54,6 +59,29 @@ pub struct NetboxConfig {
pub netbox_uri: String,
}

/// Additional information about the system.
///
/// # Members
///
/// * name: `String` - Name of the device. *Required for virtual machines! Must be unique!*
/// * site_id: `i64` - ID of the site the device is located in.
/// * description: `String` - Short description of the device.
/// * comments: `String` - Comment field.
/// * device_type: `i64` - ID of the device type.
/// * device_role: `i64` - ID of the device role.
/// * face: `String` - Tag of the orientation of the device.
/// * status: `String` - Status of the device. (Default: `active`)
/// * airflow `String` - Airflow orientation of the device.
/// * primary_network_interface: `Option<String>` - Name of the network interface you want to set as
/// primary.
/// * custom_fields: `Option<HashMap<String, Value, RandomState>>` - Unsorted, unfiltered list of
/// information that will be handed to NetBox as-is.
/// * platform_name: `Option<String>` - Name of the processor architecture used as a fallback if collection by `uname`
/// fails. *Must be present in your NetBox instance!*
/// * tenant_group: `Option<i64>` - ID of the tenant group this device belongs to. (e.g: department)
/// * tenant: `Option<i64>` - ID of tenant this device belongs to. (e.g: team or individual)
/// * rack: `Option<i64>` - ID of the rack this device is located in.
/// * position: `Option<i64>` - Position of the device within a rack if any.
#[derive(Debug, Serialize, Deserialize)]
pub struct SystemConfig {
pub name: String,
@@ -71,6 +99,7 @@ pub struct SystemConfig {
// optional System information
pub tenant_group: Option<i64>,
pub tenant: Option<i64>,
pub location: Option<i64>,
pub rack: Option<i64>,
pub position: Option<i64>,
}
@@ -318,7 +347,7 @@ impl ConfigData {
);
}

return Ok(());
Ok(())
}

/// Opens and reads the config file and writes the set parameters into a [`ConfigData`](struct.ConfigData) Object
2 changes: 1 addition & 1 deletion src/configuration/config_template.toml
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ device_type = 0 # ID of the type of the Device Type in NetBox
device_role = 0 # ID of the device role in NetBox
# Name of the network interface to set. (e.g eth0, etc)
# If not set, the first active interface will be selected.
primary_network_interface = ""
primary_network_interface = "eth0"
face = "" # Direction this device may face in (e.g front or rear)
status = "active" # status of the device. default: active
airflow = "Front to rear" # Direction of airflow
58 changes: 25 additions & 33 deletions src/publisher/api_client.rs
Original file line number Diff line number Diff line change
@@ -8,42 +8,10 @@
extern crate thanix_client;

use reqwest::{blocking::Client, Error as ReqwestError};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json;
use thanix_client::util::ThanixClient;

use crate::collectors::{dmi_collector::DmiInformation, network_collector::NetworkInformation};
use thanix_client::{paths::{dcim_devices_create, DcimDevicesCreateResponse}, types::WritableDeviceWithConfigContextRequest, util::ThanixClient};

use super::{publisher, publisher_exceptions};

/// Represents the combined system information required to create a new machine or update an existing one.
///
/// # Members
///
/// * `dmi_information: DmiInformation` - DMI (Desktop Management Interface) details of the system such as model and manufacturer.
/// * `network_information: NetworkInformation` - A list of network interface details.
/// * `system_location: String` A string representing the location or office the system is located at. Read from the config file.
#[derive(Serialize)]
pub struct SystemData {
pub dmi_information: DmiInformation,
pub network_information: Vec<NetworkInformation>,
pub system_name: String,
pub system_location: String,
pub device_role: String,
}

/// Encapsulates the payload required to create a new machine in NetBox.
///
/// This struct is serialized into JSON format and sent as the body of the HTTP request to create a new machine.
///
/// # Members
///
/// * `system_information: SystemData` - System Information to include in the request.
#[derive(Serialize)]
pub struct CreateMachinePayload {
pub system_information: SystemData,
}

/// Tests connection to the NetBox API.
///
/// This method attempts to retrieve the API root from the NetBox API to affirm that it is reachable.
@@ -77,3 +45,27 @@ pub fn test_connection(client: &ThanixClient) -> Result<(), publisher_exceptions
Err(e) => Err(publisher_exceptions::NetBoxApiError::Reqwest(e)),
}
}

pub fn create_device(client: &ThanixClient, payload: &WritableDeviceWithConfigContextRequest) {
println!("Creating Device in NetBox...");

match dcim_devices_create(client, payload.clone()) {
Ok(response) => {
match response {
DcimDevicesCreateResponse::Http201(created_device) => {
// TODO
println!(
"[success] Device creation successful!\nYour machine can be found under the ID {}.", created_device.id
);
}
DcimDevicesCreateResponse::Other(other_response) => {
// TODO
todo!("Other Response codes from creation not yet implemented! {}", other_response.text().unwrap());
}
}
}
Err(err) => {
panic!("{}", err); // Handle this better
}
}
}
28 changes: 3 additions & 25 deletions src/publisher/publisher.rs
Original file line number Diff line number Diff line change
@@ -5,9 +5,6 @@
//! machine or update an existing one.
//!
//! The actual request logic will be provided by the `thanix_client` crate.
//!
//! The `api_client` module will provide the actual client and request logic.
//!
use std::process;

/// TODO: 1. Implement Creation/update logic 2. Denest by splitting query logic off 3. Do not panic upon request fail
@@ -25,7 +22,7 @@ use thanix_client::{

use crate::{
configuration::config_parser::ConfigData,
publisher::{api_client::test_connection, translator},
publisher::{api_client::{create_device, test_connection}, translator},
Machine,
};

@@ -81,33 +78,14 @@ pub fn register_machine(
todo!("Virtual machine creation not yet implemented!") // TODO: VM Creation / Update
} else {
let payload: WritableDeviceWithConfigContextRequest =
translator::information_to_device(&machine, config_data);
translator::information_to_device(&client, &machine, config_data);

match search_for_matches(&machine, &nb_devices) {
Some(device_id) => {
todo!("Device update not yet implemented.") // TODO Implement machine update
}
None => {
match paths::dcim_devices_create(&client, payload) {
Ok(response) => {
// Check response code 201, handle other. (Should not happen)
match response {
DcimDevicesCreateResponse::Http201(created_device) => {
// TODO
todo!("Device creation not yet implemented!")
}
DcimDevicesCreateResponse::Other(other_response) => {
// TODO
todo!(
"Unexpected response code on device creation not handled yet!"
)
}
}
}
Err(err) => {
panic!("{}", err) // Handle failure correctly
}
}
create_device(client, &payload);
}
}
}
111 changes: 99 additions & 12 deletions src/publisher/translator.rs
Original file line number Diff line number Diff line change
@@ -4,15 +4,24 @@
//!
//! TODO:
//! - Identify primary IPv4 or IPv6 using the primary_network_interface field from `ConfigData`.
use std::process;
use thanix_client::paths::{self, DcimPlatformsListQuery};
use thanix_client::types::{
WritableDeviceWithConfigContextRequest, WritableVirtualMachineWithConfigContextRequest,
Platform, WritableDeviceWithConfigContextRequest,
WritableVirtualMachineWithConfigContextRequest,
};
use thanix_client::util::ThanixClient;

use crate::{configuration::config_parser::ConfigData, Machine};

use super::publisher_exceptions::NetBoxApiError;

/// Translate the machine information to a `WritableDeviceWithConfigContextRequest` required by
/// NetBox's API.
///
/// *Certain information provided in the config file, like the CPU platform, will be overwritten
/// if another one is detected by the collector!*
///
/// # Parameters
///
/// - `machine: &Machine` - Collected information about the device
@@ -23,23 +32,101 @@ use crate::{configuration::config_parser::ConfigData, Machine};
///
/// - `device: WritableDeviceWithConfigContextRequest` - Payload for machine creation request
pub fn information_to_device(
state: &ThanixClient,
machine: &Machine,
config_data: ConfigData,
) -> WritableDeviceWithConfigContextRequest {
// let device: WritableDeviceWithConfigContextRequest = WritableDeviceWithConfigContextRequest {
// name: Some(config_data.system.name),
// device_type: config_data.system.device_type,
// role: config_data.system.device_role,
// tenant: config_data.system.tenant,
// };
// device
todo!()
let wanted_platform: Option<String> = if let Some(arch) =
machine.dmi_information.cpu_information.arch.as_ref()
{
println!("[info] CPU architecture was collected. Used by default, overriding possible config options...");
Some(arch.clone())
} else if let Some(config_value) = config_data.system.platform_name.as_ref() {
println!("[info] Architecture was not collected. Using config specifications...");
Some(config_value.clone())
} else {
println!("[warning] No cpu architecture specified. Proceeding with 'none'...");
None
};

let device: WritableDeviceWithConfigContextRequest = WritableDeviceWithConfigContextRequest {
name: Some(config_data.system.name),
device_type: config_data.system.device_type,
role: config_data.system.device_role,
tenant: config_data.system.tenant,
platform: match wanted_platform {
Some(platform_name) => get_platform_id(&state, platform_name),
None => None,
},
serial: machine.dmi_information.system_information.serial,
asset_tag: todo!(),
site: todo!("Implement search logic for site id"),
location: todo!(),
rack: todo!(),
face: todo!(),

};
device
}

fn get_platform_id(machine: &Machine) -> Option<i64> {
todo!("Get platform by name an return platform id!")
/// Returns the ID of the platform this machine uses.
///
/// # Parameters
///
/// * state: `&ThanixClient` - The client required for searching for the platform.
fn get_platform_id(state: &ThanixClient, platform_name: String) -> Option<i64> {
let platform_list: Vec<Platform>;

match paths::dcim_platforms_list(&state, DcimPlatformsListQuery::default()) {
Ok(response) => {
println!("List received. Analyzing...");

platform_list = match response {
paths::DcimPlatformsListResponse::Http200(platforms) => platforms.results,
_ => {
todo!("Handling of non 200 Response code when getting platforms not implemented yet.")
}
};
}
Err(e) => {
eprintln!(
"\x1b[31m[error]\x1b[0m Failure while receiving list of platforms.\n{}",
e
);
process::exit(1);
}
};

for platform in platform_list {
if platform.name == platform_name {
return Some(platform.id);
}
}

None
}

pub fn information_to_vm(machine: &Machine) -> WritableVirtualMachineWithConfigContextRequest {
/// Translate gathered information about the virtual machine into a usable Payload.
pub fn information_to_vm(
state: &ThanixClient,
machine: &Machine,
config_data: &ConfigData,
) -> WritableVirtualMachineWithConfigContextRequest {
todo!()
}

/// Validate the `Device` Payload.
///
/// # Parameters
/// * payload: `WritableDeviceWithConfigContextRequest` - The struct to validate.
///
/// # Returns
///
/// - Ok(())
fn validate_device_payload(payload: WritableDeviceWithConfigContextRequest) -> bool {
println!("Validating device payload...");

todo!("Device payload validation not implemented yet!");

false
}