Compare commits
1 Commits
feat/broca
...
16cd63118a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16cd63118a |
2
.gitattributes
vendored
@@ -2,5 +2,3 @@ bootx64.efi filter=lfs diff=lfs merge=lfs -text
|
|||||||
grubx64.efi filter=lfs diff=lfs merge=lfs -text
|
grubx64.efi filter=lfs diff=lfs merge=lfs -text
|
||||||
initrd filter=lfs diff=lfs merge=lfs -text
|
initrd filter=lfs diff=lfs merge=lfs -text
|
||||||
linux filter=lfs diff=lfs merge=lfs -text
|
linux filter=lfs diff=lfs merge=lfs -text
|
||||||
data/okd/bin/* filter=lfs diff=lfs merge=lfs -text
|
|
||||||
data/okd/installer_image/* filter=lfs diff=lfs merge=lfs -text
|
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -3,7 +3,6 @@ private_repos/
|
|||||||
|
|
||||||
### Harmony ###
|
### Harmony ###
|
||||||
harmony.log
|
harmony.log
|
||||||
data/okd/installation_files*
|
|
||||||
|
|
||||||
### Helm ###
|
### Helm ###
|
||||||
# Chart dependencies
|
# Chart dependencies
|
||||||
|
|||||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "examples/try_rust_webapp/tryrust.org"]
|
|
||||||
path = examples/try_rust_webapp/tryrust.org
|
|
||||||
url = https://github.com/rust-dd/tryrust.org.git
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "SELECT host_id FROM host_role_mapping WHERE role = ?",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "host_id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "2ea29df2326f7c84bd4100ad510a3fd4878dc2e217dc83f9bf45a402dfd62a91"
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n SELECT\n p1.id,\n p1.version_id,\n p1.data as \"data: Json<PhysicalHost>\"\n FROM\n physical_hosts p1\n INNER JOIN (\n SELECT\n id,\n MAX(version_id) AS max_version\n FROM\n physical_hosts\n GROUP BY\n id\n ) p2 ON p1.id = p2.id AND p1.version_id = p2.max_version\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version_id",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data: Json<PhysicalHost>",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Blob"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 0
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "8d247918eca10a88b784ee353db090c94a222115c543231f2140cba27bd0f067"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n INSERT INTO host_role_mapping (host_id, role)\n VALUES (?, ?)\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 2
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "df7a7c9cfdd0972e2e0ce7ea444ba8bc9d708a4fb89d5593a0be2bbebde62aff"
|
|
||||||
}
|
|
||||||
805
Cargo.lock
generated
15
Cargo.toml
@@ -14,9 +14,7 @@ members = [
|
|||||||
"harmony_composer",
|
"harmony_composer",
|
||||||
"harmony_inventory_agent",
|
"harmony_inventory_agent",
|
||||||
"harmony_secret_derive",
|
"harmony_secret_derive",
|
||||||
"harmony_secret",
|
"harmony_secret", "adr/agent_discovery/mdns",
|
||||||
"adr/agent_discovery/mdns",
|
|
||||||
"brocade",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
@@ -68,12 +66,5 @@ thiserror = "2.0.14"
|
|||||||
serde = { version = "1.0.209", features = ["derive", "rc"] }
|
serde = { version = "1.0.209", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0.127"
|
serde_json = "1.0.127"
|
||||||
askama = "0.14"
|
askama = "0.14"
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite" ] }
|
||||||
reqwest = { version = "0.12", features = [
|
reqwest = { version = "0.12", features = ["blocking", "stream", "rustls-tls", "http2", "json"], default-features = false }
|
||||||
"blocking",
|
|
||||||
"stream",
|
|
||||||
"rustls-tls",
|
|
||||||
"http2",
|
|
||||||
"json",
|
|
||||||
], default-features = false }
|
|
||||||
assertor = "0.0.4"
|
|
||||||
|
|||||||
69
README.md
@@ -36,59 +36,48 @@ These principles surface as simple, ergonomic Rust APIs that let teams focus on
|
|||||||
|
|
||||||
## 2 · Quick Start
|
## 2 · Quick Start
|
||||||
|
|
||||||
The snippet below spins up a complete **production-grade Rust + Leptos Webapp** with monitoring. Swap it for your own scores to deploy anything from microservices to machine-learning pipelines.
|
The snippet below spins up a complete **production-grade LAMP stack** with monitoring. Swap it for your own scores to deploy anything from microservices to machine-learning pipelines.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use harmony::{
|
use harmony::{
|
||||||
|
data::Version,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
|
maestro::Maestro,
|
||||||
modules::{
|
modules::{
|
||||||
application::{
|
lamp::{LAMPConfig, LAMPScore},
|
||||||
ApplicationScore, RustWebFramework, RustWebapp,
|
monitoring::monitoring_alerting::MonitoringAlertingStackScore,
|
||||||
features::{PackagingDeployment, rhob_monitoring::Monitoring},
|
|
||||||
},
|
|
||||||
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
|
||||||
},
|
},
|
||||||
topology::K8sAnywhereTopology,
|
topology::{K8sAnywhereTopology, Url},
|
||||||
};
|
};
|
||||||
use harmony_macros::hurl;
|
|
||||||
use std::{path::PathBuf, sync::Arc};
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let application = Arc::new(RustWebapp {
|
// 1. Describe what you want
|
||||||
name: "harmony-example-leptos".to_string(),
|
let lamp_stack = LAMPScore {
|
||||||
project_root: PathBuf::from(".."), // <== Your project root, usually .. if you use the standard `/harmony` folder
|
name: "harmony-lamp-demo".into(),
|
||||||
framework: Some(RustWebFramework::Leptos),
|
domain: Url::Url(url::Url::parse("https://lampdemo.example.com").unwrap()),
|
||||||
service_port: 8080,
|
php_version: Version::from("8.3.0").unwrap(),
|
||||||
});
|
config: LAMPConfig {
|
||||||
|
project_root: "./php".into(),
|
||||||
// Define your Application deployment and the features you want
|
database_size: "4Gi".into(),
|
||||||
let app = ApplicationScore {
|
..Default::default()
|
||||||
features: vec![
|
},
|
||||||
Box::new(PackagingDeployment {
|
|
||||||
application: application.clone(),
|
|
||||||
}),
|
|
||||||
Box::new(Monitoring {
|
|
||||||
application: application.clone(),
|
|
||||||
alert_receiver: vec![
|
|
||||||
Box::new(DiscordWebhook {
|
|
||||||
name: "test-discord".to_string(),
|
|
||||||
url: hurl!("https://discord.doesnt.exist.com"), // <== Get your discord webhook url
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
application,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 2. Enhance with extra scores (monitoring, CI/CD, …)
|
||||||
|
let mut monitoring = MonitoringAlertingStackScore::new();
|
||||||
|
monitoring.namespace = Some(lamp_stack.config.namespace.clone());
|
||||||
|
|
||||||
|
// 3. Run your scores on the desired topology & inventory
|
||||||
harmony_cli::run(
|
harmony_cli::run(
|
||||||
Inventory::autoload(),
|
Inventory::autoload(), // auto-detect hardware / kube-config
|
||||||
K8sAnywhereTopology::from_env(), // <== Deploy to local automatically provisioned local k3d by default or connect to any kubernetes cluster
|
K8sAnywhereTopology::from_env(), // local k3d, CI, staging, prod…
|
||||||
vec![Box::new(app)],
|
vec![
|
||||||
None,
|
Box::new(lamp_stack),
|
||||||
)
|
Box::new(monitoring)
|
||||||
.await
|
],
|
||||||
.unwrap();
|
None
|
||||||
|
).await.unwrap();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use log::debug;
|
||||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||||
|
|
||||||
use crate::SERVICE_TYPE;
|
use crate::SERVICE_TYPE;
|
||||||
@@ -73,7 +74,7 @@ pub async fn discover() {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _discover_example() {
|
async fn discover_example() {
|
||||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||||
|
|
||||||
// Create a daemon
|
// Create a daemon
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "brocade"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
async-trait.workspace = true
|
|
||||||
harmony_types = { path = "../harmony_types" }
|
|
||||||
russh.workspace = true
|
|
||||||
russh-keys.workspace = true
|
|
||||||
tokio.workspace = true
|
|
||||||
log.workspace = true
|
|
||||||
env_logger.workspace = true
|
|
||||||
regex = "1.11.3"
|
|
||||||
harmony_secret = { path = "../harmony_secret" }
|
|
||||||
serde.workspace = true
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
use std::net::{IpAddr, Ipv4Addr};
|
|
||||||
|
|
||||||
use brocade::BrocadeOptions;
|
|
||||||
use harmony_secret::{Secret, SecretManager};
|
|
||||||
use harmony_types::switch::PortLocation;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Secret, Clone, Debug, Serialize, Deserialize)]
|
|
||||||
struct BrocadeSwitchAuth {
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
|
||||||
|
|
||||||
// let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 250)); // old brocade @ ianlet
|
|
||||||
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 55, 101)); // brocade @ sto1
|
|
||||||
// let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 4, 11)); // brocade @ st
|
|
||||||
let switch_addresses = vec![ip];
|
|
||||||
|
|
||||||
let config = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let brocade = brocade::init(
|
|
||||||
&switch_addresses,
|
|
||||||
22,
|
|
||||||
&config.username,
|
|
||||||
&config.password,
|
|
||||||
Some(BrocadeOptions {
|
|
||||||
dry_run: true,
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Brocade client failed to connect");
|
|
||||||
|
|
||||||
let entries = brocade.get_stack_topology().await.unwrap();
|
|
||||||
println!("Stack topology: {entries:#?}");
|
|
||||||
|
|
||||||
let entries = brocade.get_interfaces().await.unwrap();
|
|
||||||
println!("Interfaces: {entries:#?}");
|
|
||||||
|
|
||||||
let version = brocade.version().await.unwrap();
|
|
||||||
println!("Version: {version:?}");
|
|
||||||
|
|
||||||
println!("--------------");
|
|
||||||
let mac_adddresses = brocade.get_mac_address_table().await.unwrap();
|
|
||||||
println!("VLAN\tMAC\t\t\tPORT");
|
|
||||||
for mac in mac_adddresses {
|
|
||||||
println!("{}\t{}\t{}", mac.vlan, mac.mac_address, mac.port);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("--------------");
|
|
||||||
let channel_name = "1";
|
|
||||||
brocade.clear_port_channel(channel_name).await.unwrap();
|
|
||||||
|
|
||||||
println!("--------------");
|
|
||||||
let channel_id = brocade.find_available_channel_id().await.unwrap();
|
|
||||||
|
|
||||||
println!("--------------");
|
|
||||||
let channel_name = "HARMONY_LAG";
|
|
||||||
let ports = [PortLocation(2, 0, 35)];
|
|
||||||
brocade
|
|
||||||
.create_port_channel(channel_id, channel_name, &ports)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
use super::BrocadeClient;
|
|
||||||
use crate::{
|
|
||||||
BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo, MacAddressEntry,
|
|
||||||
PortChannelId, PortOperatingMode, SecurityLevel, parse_brocade_mac_address,
|
|
||||||
shell::BrocadeShell,
|
|
||||||
};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use harmony_types::switch::{PortDeclaration, PortLocation};
|
|
||||||
use log::{debug, info};
|
|
||||||
use regex::Regex;
|
|
||||||
use std::{collections::HashSet, str::FromStr};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FastIronClient {
|
|
||||||
shell: BrocadeShell,
|
|
||||||
version: BrocadeInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FastIronClient {
|
|
||||||
pub fn init(mut shell: BrocadeShell, version_info: BrocadeInfo) -> Self {
|
|
||||||
shell.before_all(vec!["skip-page-display".into()]);
|
|
||||||
shell.after_all(vec!["page".into()]);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
shell,
|
|
||||||
version: version_info,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_mac_entry(&self, line: &str) -> Option<Result<MacAddressEntry, Error>> {
|
|
||||||
debug!("[Brocade] Parsing mac address entry: {line}");
|
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
||||||
if parts.len() < 3 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (vlan, mac_address, port) = match parts.len() {
|
|
||||||
3 => (
|
|
||||||
u16::from_str(parts[0]).ok()?,
|
|
||||||
parse_brocade_mac_address(parts[1]).ok()?,
|
|
||||||
parts[2].to_string(),
|
|
||||||
),
|
|
||||||
_ => (
|
|
||||||
1,
|
|
||||||
parse_brocade_mac_address(parts[0]).ok()?,
|
|
||||||
parts[1].to_string(),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let port =
|
|
||||||
PortDeclaration::parse(&port).map_err(|e| Error::UnexpectedError(format!("{e}")));
|
|
||||||
|
|
||||||
match port {
|
|
||||||
Ok(p) => Some(Ok(MacAddressEntry {
|
|
||||||
vlan,
|
|
||||||
mac_address,
|
|
||||||
port: p,
|
|
||||||
})),
|
|
||||||
Err(e) => Some(Err(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_stack_port_entry(&self, line: &str) -> Option<Result<InterSwitchLink, Error>> {
|
|
||||||
debug!("[Brocade] Parsing stack port entry: {line}");
|
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
||||||
if parts.len() < 10 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let local_port = PortLocation::from_str(parts[0]).ok()?;
|
|
||||||
|
|
||||||
Some(Ok(InterSwitchLink {
|
|
||||||
local_port,
|
|
||||||
remote_port: None,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_port_channel_commands(
|
|
||||||
&self,
|
|
||||||
channel_id: PortChannelId,
|
|
||||||
channel_name: &str,
|
|
||||||
ports: &[PortLocation],
|
|
||||||
) -> Vec<String> {
|
|
||||||
let mut commands = vec![
|
|
||||||
"configure terminal".to_string(),
|
|
||||||
format!("lag {channel_name} static id {channel_id}"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for port in ports {
|
|
||||||
commands.push(format!("ports ethernet {port}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
commands.push(format!("primary-port {}", ports[0]));
|
|
||||||
commands.push("deploy".into());
|
|
||||||
commands.push("exit".into());
|
|
||||||
commands.push("write memory".into());
|
|
||||||
commands.push("exit".into());
|
|
||||||
|
|
||||||
commands
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl BrocadeClient for FastIronClient {
|
|
||||||
async fn version(&self) -> Result<BrocadeInfo, Error> {
|
|
||||||
Ok(self.version.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> {
|
|
||||||
info!("[Brocade] Showing MAC address table...");
|
|
||||||
|
|
||||||
let output = self
|
|
||||||
.shell
|
|
||||||
.run_command("show mac-address", ExecutionMode::Regular)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
output
|
|
||||||
.lines()
|
|
||||||
.skip(2)
|
|
||||||
.filter_map(|line| self.parse_mac_entry(line))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_stack_topology(&self) -> Result<Vec<InterSwitchLink>, Error> {
|
|
||||||
let output = self
|
|
||||||
.shell
|
|
||||||
.run_command("show interface stack-ports", crate::ExecutionMode::Regular)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
output
|
|
||||||
.lines()
|
|
||||||
.skip(1)
|
|
||||||
.filter_map(|line| self.parse_stack_port_entry(line))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_interfaces(&self) -> Result<Vec<InterfaceInfo>, Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn configure_interfaces(
|
|
||||||
&self,
|
|
||||||
_interfaces: Vec<(String, PortOperatingMode)>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_available_channel_id(&self) -> Result<PortChannelId, Error> {
|
|
||||||
info!("[Brocade] Finding next available channel id...");
|
|
||||||
|
|
||||||
let output = self
|
|
||||||
.shell
|
|
||||||
.run_command("show lag", ExecutionMode::Regular)
|
|
||||||
.await?;
|
|
||||||
let re = Regex::new(r"=== LAG .* ID\s+(\d+)").expect("Invalid regex");
|
|
||||||
|
|
||||||
let used_ids: HashSet<u8> = output
|
|
||||||
.lines()
|
|
||||||
.filter_map(|line| {
|
|
||||||
re.captures(line)
|
|
||||||
.and_then(|c| c.get(1))
|
|
||||||
.and_then(|id_match| id_match.as_str().parse().ok())
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut next_id: u8 = 1;
|
|
||||||
loop {
|
|
||||||
if !used_ids.contains(&next_id) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
next_id += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("[Brocade] Found channel id: {next_id}");
|
|
||||||
Ok(next_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_port_channel(
|
|
||||||
&self,
|
|
||||||
channel_id: PortChannelId,
|
|
||||||
channel_name: &str,
|
|
||||||
ports: &[PortLocation],
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
info!(
|
|
||||||
"[Brocade] Configuring port-channel '{channel_name} {channel_id}' with ports: {ports:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let commands = self.build_port_channel_commands(channel_id, channel_name, ports);
|
|
||||||
self.shell
|
|
||||||
.run_commands(commands, ExecutionMode::Privileged)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("[Brocade] Port-channel '{channel_name}' configured.");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error> {
|
|
||||||
info!("[Brocade] Clearing port-channel: {channel_name}");
|
|
||||||
|
|
||||||
let commands = vec![
|
|
||||||
"configure terminal".to_string(),
|
|
||||||
format!("no lag {channel_name}"),
|
|
||||||
"write memory".to_string(),
|
|
||||||
];
|
|
||||||
self.shell
|
|
||||||
.run_commands(commands, ExecutionMode::Privileged)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("[Brocade] Port-channel '{channel_name}' cleared.");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn enable_snmp(&self, user_name: &str, auth: &str, des: &str) -> Result<(), Error> {
|
|
||||||
let commands = vec![
|
|
||||||
"configure terminal".into(),
|
|
||||||
"snmp-server view ALL 1 included".into(),
|
|
||||||
"snmp-server group public v3 priv read ALL".into(),
|
|
||||||
format!(
|
|
||||||
"snmp-server user {user_name} groupname public auth md5 auth-password {auth} priv des priv-password {des}"
|
|
||||||
),
|
|
||||||
"exit".into(),
|
|
||||||
];
|
|
||||||
self.shell
|
|
||||||
.run_commands(commands, ExecutionMode::Regular)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
use std::net::IpAddr;
|
|
||||||
use std::{
|
|
||||||
fmt::{self, Display},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::network_operating_system::NetworkOperatingSystemClient;
|
|
||||||
use crate::{
|
|
||||||
fast_iron::FastIronClient,
|
|
||||||
shell::{BrocadeSession, BrocadeShell},
|
|
||||||
};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use harmony_types::net::MacAddress;
|
|
||||||
use harmony_types::switch::{PortDeclaration, PortLocation};
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
mod fast_iron;
|
|
||||||
mod network_operating_system;
|
|
||||||
mod shell;
|
|
||||||
mod ssh;
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
|
||||||
pub struct BrocadeOptions {
|
|
||||||
pub dry_run: bool,
|
|
||||||
pub ssh: ssh::SshOptions,
|
|
||||||
pub timeouts: TimeoutConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct TimeoutConfig {
|
|
||||||
pub shell_ready: Duration,
|
|
||||||
pub command_execution: Duration,
|
|
||||||
pub command_output: Duration,
|
|
||||||
pub cleanup: Duration,
|
|
||||||
pub message_wait: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TimeoutConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
shell_ready: Duration::from_secs(10),
|
|
||||||
command_execution: Duration::from_secs(60), // Commands like `deploy` (for a LAG) can take a while
|
|
||||||
command_output: Duration::from_secs(5), // Delay to start logging "waiting for command output"
|
|
||||||
cleanup: Duration::from_secs(10),
|
|
||||||
message_wait: Duration::from_millis(500),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ExecutionMode {
|
|
||||||
Regular,
|
|
||||||
Privileged,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct BrocadeInfo {
|
|
||||||
os: BrocadeOs,
|
|
||||||
version: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum BrocadeOs {
|
|
||||||
NetworkOperatingSystem,
|
|
||||||
FastIron,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
|
||||||
pub struct MacAddressEntry {
|
|
||||||
pub vlan: u16,
|
|
||||||
pub mac_address: MacAddress,
|
|
||||||
pub port: PortDeclaration,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type PortChannelId = u8;
|
|
||||||
|
|
||||||
/// Represents a single physical or logical link connecting two switches within a stack or fabric.
|
|
||||||
///
|
|
||||||
/// This structure provides a standardized view of the topology regardless of the
|
|
||||||
/// underlying Brocade OS configuration (stacking vs. fabric).
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct InterSwitchLink {
|
|
||||||
/// The local port on the switch where the topology command was run.
|
|
||||||
pub local_port: PortLocation,
|
|
||||||
/// The port on the directly connected neighboring switch.
|
|
||||||
pub remote_port: Option<PortLocation>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the key running configuration status of a single switch interface.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct InterfaceInfo {
|
|
||||||
/// The full configuration name (e.g., "TenGigabitEthernet 1/0/1", "FortyGigabitEthernet 2/0/2").
|
|
||||||
pub name: String,
|
|
||||||
/// The physical location of the interface.
|
|
||||||
pub port_location: PortLocation,
|
|
||||||
/// The parsed type and name prefix of the interface.
|
|
||||||
pub interface_type: InterfaceType,
|
|
||||||
/// The primary configuration mode defining the interface's behavior (L2, L3, Fabric).
|
|
||||||
pub operating_mode: Option<PortOperatingMode>,
|
|
||||||
/// Indicates the current state of the interface.
|
|
||||||
pub status: InterfaceStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Categorizes the functional type of a switch interface.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum InterfaceType {
|
|
||||||
/// Physical or virtual Ethernet interface (e.g., TenGigabitEthernet, FortyGigabitEthernet).
|
|
||||||
Ethernet(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for InterfaceType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
InterfaceType::Ethernet(name) => write!(f, "{name}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Defines the primary configuration mode of a switch interface, representing mutually exclusive roles.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum PortOperatingMode {
|
|
||||||
/// The interface is explicitly configured for Brocade fabric roles (ISL or Trunk enabled).
|
|
||||||
Fabric,
|
|
||||||
/// The interface is configured for standard Layer 2 switching as Trunk port (`switchport mode trunk`).
|
|
||||||
Trunk,
|
|
||||||
/// The interface is configured for standard Layer 2 switching as Access port (`switchport` without trunk mode).
|
|
||||||
Access,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Defines the possible status of an interface.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum InterfaceStatus {
|
|
||||||
/// The interface is connected.
|
|
||||||
Connected,
|
|
||||||
/// The interface is not connected and is not expected to be.
|
|
||||||
NotConnected,
|
|
||||||
/// The interface is not connected but is expected to be (configured with `no shutdown`).
|
|
||||||
SfpAbsent,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init(
|
|
||||||
ip_addresses: &[IpAddr],
|
|
||||||
port: u16,
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
options: Option<BrocadeOptions>,
|
|
||||||
) -> Result<Box<dyn BrocadeClient + Send + Sync>, Error> {
|
|
||||||
let shell = BrocadeShell::init(ip_addresses, port, username, password, options).await?;
|
|
||||||
|
|
||||||
let version_info = shell
|
|
||||||
.with_session(ExecutionMode::Regular, |session| {
|
|
||||||
Box::pin(get_brocade_info(session))
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(match version_info.os {
|
|
||||||
BrocadeOs::FastIron => Box::new(FastIronClient::init(shell, version_info)),
|
|
||||||
BrocadeOs::NetworkOperatingSystem => {
|
|
||||||
Box::new(NetworkOperatingSystemClient::init(shell, version_info))
|
|
||||||
}
|
|
||||||
BrocadeOs::Unknown => todo!(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait BrocadeClient: std::fmt::Debug {
|
|
||||||
/// Retrieves the operating system and version details from the connected Brocade switch.
|
|
||||||
///
|
|
||||||
/// This is typically the first call made after establishing a connection to determine
|
|
||||||
/// the switch OS family (e.g., FastIron, NOS) for feature compatibility.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A `BrocadeInfo` structure containing parsed OS type and version string.
|
|
||||||
async fn version(&self) -> Result<BrocadeInfo, Error>;
|
|
||||||
|
|
||||||
/// Retrieves the dynamically learned MAC address table from the switch.
|
|
||||||
///
|
|
||||||
/// This is crucial for discovering where specific network endpoints (MAC addresses)
|
|
||||||
/// are currently located on the physical ports.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A vector of `MacAddressEntry`, where each entry typically contains VLAN, MAC address,
|
|
||||||
/// and the associated port name/index.
|
|
||||||
async fn get_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error>;
|
|
||||||
|
|
||||||
/// Derives the physical connections used to link multiple switches together
|
|
||||||
/// to form a single logical entity (stack, fabric, etc.).
|
|
||||||
///
|
|
||||||
/// This abstracts the underlying configuration (e.g., stack ports, fabric ports)
|
|
||||||
/// to return a standardized view of the topology.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A vector of `InterSwitchLink` structs detailing which ports are used for stacking/fabric.
|
|
||||||
/// If the switch is not stacked, returns an empty vector.
|
|
||||||
async fn get_stack_topology(&self) -> Result<Vec<InterSwitchLink>, Error>;
|
|
||||||
|
|
||||||
/// Retrieves the status for all interfaces
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A vector of `InterfaceInfo` structures.
|
|
||||||
async fn get_interfaces(&self) -> Result<Vec<InterfaceInfo>, Error>;
|
|
||||||
|
|
||||||
/// Configures a set of interfaces to be operated with a specified mode (access ports, ISL, etc.).
|
|
||||||
async fn configure_interfaces(
|
|
||||||
&self,
|
|
||||||
interfaces: Vec<(String, PortOperatingMode)>,
|
|
||||||
) -> Result<(), Error>;
|
|
||||||
|
|
||||||
/// Scans the existing configuration to find the next available (unused)
|
|
||||||
/// Port-Channel ID (`lag` or `trunk`) for assignment.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// The smallest, unassigned `PortChannelId` within the supported range.
|
|
||||||
async fn find_available_channel_id(&self) -> Result<PortChannelId, Error>;
|
|
||||||
|
|
||||||
/// Creates and configures a new Port-Channel (Link Aggregation Group or LAG)
|
|
||||||
/// using the specified channel ID and ports.
|
|
||||||
///
|
|
||||||
/// The resulting configuration must be persistent (saved to startup-config).
|
|
||||||
/// Assumes a static LAG configuration mode unless specified otherwise by the implementation.
|
|
||||||
///
|
|
||||||
/// # Parameters
|
|
||||||
///
|
|
||||||
/// * `channel_id`: The ID (e.g., 1-128) for the logical port channel.
|
|
||||||
/// * `channel_name`: A descriptive name for the LAG (used in configuration context).
|
|
||||||
/// * `ports`: A slice of `PortLocation` structs defining the physical member ports.
|
|
||||||
async fn create_port_channel(
|
|
||||||
&self,
|
|
||||||
channel_id: PortChannelId,
|
|
||||||
channel_name: &str,
|
|
||||||
ports: &[PortLocation],
|
|
||||||
) -> Result<(), Error>;
|
|
||||||
|
|
||||||
/// Enables Simple Network Management Protocol (SNMP) server for switch
|
|
||||||
///
|
|
||||||
/// # Parameters
|
|
||||||
///
|
|
||||||
/// * `user_name`: The user name for the snmp server
|
|
||||||
/// * `auth`: The password for authentication process for verifying the identity of a device
|
|
||||||
/// * `des`: The Data Encryption Standard algorithm key
|
|
||||||
async fn enable_snmp(&self, user_name: &str, auth: &str, des: &str) -> Result<(), Error>;
|
|
||||||
|
|
||||||
/// Removes all configuration associated with the specified Port-Channel name.
|
|
||||||
///
|
|
||||||
/// This operation should be idempotent; attempting to clear a non-existent
|
|
||||||
/// channel should succeed (or return a benign error).
|
|
||||||
///
|
|
||||||
/// # Parameters
|
|
||||||
///
|
|
||||||
/// * `channel_name`: The name of the Port-Channel (LAG) to delete.
|
|
||||||
///
|
|
||||||
async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_brocade_info(session: &mut BrocadeSession) -> Result<BrocadeInfo, Error> {
|
|
||||||
let output = session.run_command("show version").await?;
|
|
||||||
|
|
||||||
if output.contains("Network Operating System") {
|
|
||||||
let re = Regex::new(r"Network Operating System Version:\s*(?P<version>[a-zA-Z0-9.\-]+)")
|
|
||||||
.expect("Invalid regex");
|
|
||||||
let version = re
|
|
||||||
.captures(&output)
|
|
||||||
.and_then(|cap| cap.name("version"))
|
|
||||||
.map(|m| m.as_str().to_string())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
return Ok(BrocadeInfo {
|
|
||||||
os: BrocadeOs::NetworkOperatingSystem,
|
|
||||||
version,
|
|
||||||
});
|
|
||||||
} else if output.contains("ICX") {
|
|
||||||
let re = Regex::new(r"(?m)^\s*SW: Version\s*(?P<version>[a-zA-Z0-9.\-]+)")
|
|
||||||
.expect("Invalid regex");
|
|
||||||
let version = re
|
|
||||||
.captures(&output)
|
|
||||||
.and_then(|cap| cap.name("version"))
|
|
||||||
.map(|m| m.as_str().to_string())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
return Ok(BrocadeInfo {
|
|
||||||
os: BrocadeOs::FastIron,
|
|
||||||
version,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(Error::UnexpectedError("Unknown Brocade OS version".into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_brocade_mac_address(value: &str) -> Result<MacAddress, String> {
|
|
||||||
let cleaned_mac = value.replace('.', "");
|
|
||||||
|
|
||||||
if cleaned_mac.len() != 12 {
|
|
||||||
return Err(format!("Invalid MAC address: {value}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut bytes = [0u8; 6];
|
|
||||||
for (i, pair) in cleaned_mac.as_bytes().chunks(2).enumerate() {
|
|
||||||
let byte_str = std::str::from_utf8(pair).map_err(|_| "Invalid UTF-8")?;
|
|
||||||
bytes[i] =
|
|
||||||
u8::from_str_radix(byte_str, 16).map_err(|_| format!("Invalid hex in MAC: {value}"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(MacAddress(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum SecurityLevel {
|
|
||||||
AuthPriv(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
NetworkError(String),
|
|
||||||
AuthenticationError(String),
|
|
||||||
ConfigurationError(String),
|
|
||||||
TimeoutError(String),
|
|
||||||
UnexpectedError(String),
|
|
||||||
CommandError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Error::NetworkError(msg) => write!(f, "Network error: {msg}"),
|
|
||||||
Error::AuthenticationError(msg) => write!(f, "Authentication error: {msg}"),
|
|
||||||
Error::ConfigurationError(msg) => write!(f, "Configuration error: {msg}"),
|
|
||||||
Error::TimeoutError(msg) => write!(f, "Timeout error: {msg}"),
|
|
||||||
Error::UnexpectedError(msg) => write!(f, "Unexpected error: {msg}"),
|
|
||||||
Error::CommandError(msg) => write!(f, "{msg}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Error> for String {
|
|
||||||
fn from(val: Error) -> Self {
|
|
||||||
format!("{val}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
|
|
||||||
impl From<russh::Error> for Error {
|
|
||||||
fn from(value: russh::Error) -> Self {
|
|
||||||
Error::NetworkError(format!("Russh client error: {value}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use harmony_types::switch::{PortDeclaration, PortLocation};
|
|
||||||
use log::{debug, info};
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo,
|
|
||||||
InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, PortOperatingMode,
|
|
||||||
SecurityLevel, parse_brocade_mac_address, shell::BrocadeShell,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct NetworkOperatingSystemClient {
|
|
||||||
shell: BrocadeShell,
|
|
||||||
version: BrocadeInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetworkOperatingSystemClient {
|
|
||||||
pub fn init(mut shell: BrocadeShell, version_info: BrocadeInfo) -> Self {
|
|
||||||
shell.before_all(vec!["terminal length 0".into()]);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
shell,
|
|
||||||
version: version_info,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_mac_entry(&self, line: &str) -> Option<Result<MacAddressEntry, Error>> {
|
|
||||||
debug!("[Brocade] Parsing mac address entry: {line}");
|
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
||||||
if parts.len() < 5 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (vlan, mac_address, port) = match parts.len() {
|
|
||||||
5 => (
|
|
||||||
u16::from_str(parts[0]).ok()?,
|
|
||||||
parse_brocade_mac_address(parts[1]).ok()?,
|
|
||||||
parts[4].to_string(),
|
|
||||||
),
|
|
||||||
_ => (
|
|
||||||
u16::from_str(parts[0]).ok()?,
|
|
||||||
parse_brocade_mac_address(parts[1]).ok()?,
|
|
||||||
parts[5].to_string(),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let port =
|
|
||||||
PortDeclaration::parse(&port).map_err(|e| Error::UnexpectedError(format!("{e}")));
|
|
||||||
|
|
||||||
match port {
|
|
||||||
Ok(p) => Some(Ok(MacAddressEntry {
|
|
||||||
vlan,
|
|
||||||
mac_address,
|
|
||||||
port: p,
|
|
||||||
})),
|
|
||||||
Err(e) => Some(Err(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_inter_switch_link_entry(&self, line: &str) -> Option<Result<InterSwitchLink, Error>> {
|
|
||||||
debug!("[Brocade] Parsing inter switch link entry: {line}");
|
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
||||||
if parts.len() < 10 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let local_port = PortLocation::from_str(parts[2]).ok()?;
|
|
||||||
let remote_port = PortLocation::from_str(parts[5]).ok()?;
|
|
||||||
|
|
||||||
Some(Ok(InterSwitchLink {
|
|
||||||
local_port,
|
|
||||||
remote_port: Some(remote_port),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_interface_status_entry(&self, line: &str) -> Option<Result<InterfaceInfo, Error>> {
|
|
||||||
debug!("[Brocade] Parsing interface status entry: {line}");
|
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
||||||
if parts.len() < 6 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let interface_type = match parts[0] {
|
|
||||||
"Fo" => InterfaceType::Ethernet("FortyGigabitEthernet".to_string()),
|
|
||||||
"Te" => InterfaceType::Ethernet("TenGigabitEthernet".to_string()),
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
let port_location = PortLocation::from_str(parts[1]).ok()?;
|
|
||||||
let status = match parts[2] {
|
|
||||||
"connected" => InterfaceStatus::Connected,
|
|
||||||
"notconnected" => InterfaceStatus::NotConnected,
|
|
||||||
"sfpAbsent" => InterfaceStatus::SfpAbsent,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
let operating_mode = match parts[3] {
|
|
||||||
"ISL" => Some(PortOperatingMode::Fabric),
|
|
||||||
"Trunk" => Some(PortOperatingMode::Trunk),
|
|
||||||
"Access" => Some(PortOperatingMode::Access),
|
|
||||||
"--" => None,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(Ok(InterfaceInfo {
|
|
||||||
name: format!("{interface_type} {port_location}"),
|
|
||||||
port_location,
|
|
||||||
interface_type,
|
|
||||||
operating_mode,
|
|
||||||
status,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_configure_interfaces_error(&self, err: Error) -> Error {
|
|
||||||
debug!("[Brocade] {err}");
|
|
||||||
|
|
||||||
if let Error::CommandError(message) = &err {
|
|
||||||
if message.contains("switchport")
|
|
||||||
&& message.contains("Cannot configure aggregator member")
|
|
||||||
{
|
|
||||||
let re = Regex::new(r"\(conf-if-([a-zA-Z]+)-([\d/]+)\)#").unwrap();
|
|
||||||
|
|
||||||
if let Some(caps) = re.captures(message) {
|
|
||||||
let interface_type = &caps[1];
|
|
||||||
let port_location = &caps[2];
|
|
||||||
let interface = format!("{interface_type} {port_location}");
|
|
||||||
|
|
||||||
return Error::CommandError(format!(
|
|
||||||
"Cannot configure interface '{interface}', it is a member of a port-channel (LAG)"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl BrocadeClient for NetworkOperatingSystemClient {
|
|
||||||
async fn version(&self) -> Result<BrocadeInfo, Error> {
|
|
||||||
Ok(self.version.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> {
|
|
||||||
let output = self
|
|
||||||
.shell
|
|
||||||
.run_command("show mac-address-table", ExecutionMode::Regular)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
output
|
|
||||||
.lines()
|
|
||||||
.skip(1)
|
|
||||||
.filter_map(|line| self.parse_mac_entry(line))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_stack_topology(&self) -> Result<Vec<InterSwitchLink>, Error> {
|
|
||||||
let output = self
|
|
||||||
.shell
|
|
||||||
.run_command("show fabric isl", ExecutionMode::Regular)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
output
|
|
||||||
.lines()
|
|
||||||
.skip(6)
|
|
||||||
.filter_map(|line| self.parse_inter_switch_link_entry(line))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_interfaces(&self) -> Result<Vec<InterfaceInfo>, Error> {
|
|
||||||
let output = self
|
|
||||||
.shell
|
|
||||||
.run_command(
|
|
||||||
"show interface status rbridge-id all",
|
|
||||||
ExecutionMode::Regular,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
output
|
|
||||||
.lines()
|
|
||||||
.skip(2)
|
|
||||||
.filter_map(|line| self.parse_interface_status_entry(line))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn configure_interfaces(
|
|
||||||
&self,
|
|
||||||
interfaces: Vec<(String, PortOperatingMode)>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
info!("[Brocade] Configuring {} interface(s)...", interfaces.len());
|
|
||||||
|
|
||||||
let mut commands = vec!["configure terminal".to_string()];
|
|
||||||
|
|
||||||
for interface in interfaces {
|
|
||||||
commands.push(format!("interface {}", interface.0));
|
|
||||||
|
|
||||||
match interface.1 {
|
|
||||||
PortOperatingMode::Fabric => {
|
|
||||||
commands.push("fabric isl enable".into());
|
|
||||||
commands.push("fabric trunk enable".into());
|
|
||||||
}
|
|
||||||
PortOperatingMode::Trunk => {
|
|
||||||
commands.push("switchport".into());
|
|
||||||
commands.push("switchport mode trunk".into());
|
|
||||||
commands.push("no spanning-tree shutdown".into());
|
|
||||||
commands.push("no fabric isl enable".into());
|
|
||||||
commands.push("no fabric trunk enable".into());
|
|
||||||
}
|
|
||||||
PortOperatingMode::Access => {
|
|
||||||
commands.push("switchport".into());
|
|
||||||
commands.push("switchport mode access".into());
|
|
||||||
commands.push("switchport access vlan 1".into());
|
|
||||||
commands.push("no spanning-tree shutdown".into());
|
|
||||||
commands.push("no fabric isl enable".into());
|
|
||||||
commands.push("no fabric trunk enable".into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commands.push("no shutdown".into());
|
|
||||||
commands.push("exit".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.shell
|
|
||||||
.run_commands(commands, ExecutionMode::Regular)
|
|
||||||
.await
|
|
||||||
.map_err(|err| self.map_configure_interfaces_error(err))?;
|
|
||||||
|
|
||||||
info!("[Brocade] Interfaces configured.");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_available_channel_id(&self) -> Result<PortChannelId, Error> {
|
|
||||||
info!("[Brocade] Finding next available channel id...");
|
|
||||||
|
|
||||||
let output = self
|
|
||||||
.shell
|
|
||||||
.run_command("show port-channel summary", ExecutionMode::Regular)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let used_ids: Vec<u8> = output
|
|
||||||
.lines()
|
|
||||||
.skip(6)
|
|
||||||
.filter_map(|line| {
|
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
||||||
if parts.len() < 8 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
u8::from_str(parts[0]).ok()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut next_id: u8 = 1;
|
|
||||||
loop {
|
|
||||||
if !used_ids.contains(&next_id) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
next_id += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("[Brocade] Found channel id: {next_id}");
|
|
||||||
Ok(next_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_port_channel(
|
|
||||||
&self,
|
|
||||||
channel_id: PortChannelId,
|
|
||||||
channel_name: &str,
|
|
||||||
ports: &[PortLocation],
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
info!(
|
|
||||||
"[Brocade] Configuring port-channel '{channel_id} {channel_name}' with ports: {}",
|
|
||||||
ports
|
|
||||||
.iter()
|
|
||||||
.map(|p| format!("{p}"))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ")
|
|
||||||
);
|
|
||||||
|
|
||||||
let interfaces = self.get_interfaces().await?;
|
|
||||||
|
|
||||||
let mut commands = vec![
|
|
||||||
"configure terminal".into(),
|
|
||||||
format!("interface port-channel {}", channel_id),
|
|
||||||
"no shutdown".into(),
|
|
||||||
"exit".into(),
|
|
||||||
];
|
|
||||||
|
|
||||||
for port in ports {
|
|
||||||
let interface = interfaces.iter().find(|i| i.port_location == *port);
|
|
||||||
let Some(interface) = interface else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
commands.push(format!("interface {}", interface.name));
|
|
||||||
commands.push("no switchport".into());
|
|
||||||
commands.push("no ip address".into());
|
|
||||||
commands.push("no fabric isl enable".into());
|
|
||||||
commands.push("no fabric trunk enable".into());
|
|
||||||
commands.push(format!("channel-group {channel_id} mode active"));
|
|
||||||
commands.push("no shutdown".into());
|
|
||||||
commands.push("exit".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.shell
|
|
||||||
.run_commands(commands, ExecutionMode::Regular)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("[Brocade] Port-channel '{channel_name}' configured.");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error> {
|
|
||||||
info!("[Brocade] Clearing port-channel: {channel_name}");
|
|
||||||
|
|
||||||
let commands = vec![
|
|
||||||
"configure terminal".into(),
|
|
||||||
format!("no interface port-channel {}", channel_name),
|
|
||||||
"exit".into(),
|
|
||||||
];
|
|
||||||
|
|
||||||
self.shell
|
|
||||||
.run_commands(commands, ExecutionMode::Regular)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("[Brocade] Port-channel '{channel_name}' cleared.");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn enable_snmp(&self, user_name: &str, auth: &str, des: &str) -> Result<(), Error> {
|
|
||||||
let commands = vec![
|
|
||||||
"configure terminal".into(),
|
|
||||||
"snmp-server view ALL 1 included".into(),
|
|
||||||
"snmp-server group public v3 priv read ALL".into(),
|
|
||||||
format!(
|
|
||||||
"snmp-server user {user_name} groupname public auth md5 auth-password {auth} priv des priv-password {des}"
|
|
||||||
),
|
|
||||||
"exit".into(),
|
|
||||||
];
|
|
||||||
self.shell
|
|
||||||
.run_commands(commands, ExecutionMode::Regular)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,370 +0,0 @@
|
|||||||
use std::net::IpAddr;
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use crate::BrocadeOptions;
|
|
||||||
use crate::Error;
|
|
||||||
use crate::ExecutionMode;
|
|
||||||
use crate::TimeoutConfig;
|
|
||||||
use crate::ssh;
|
|
||||||
|
|
||||||
use log::debug;
|
|
||||||
use log::info;
|
|
||||||
use russh::ChannelMsg;
|
|
||||||
use tokio::time::timeout;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct BrocadeShell {
|
|
||||||
ip: IpAddr,
|
|
||||||
port: u16,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
options: BrocadeOptions,
|
|
||||||
before_all_commands: Vec<String>,
|
|
||||||
after_all_commands: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BrocadeShell {
|
|
||||||
pub async fn init(
|
|
||||||
ip_addresses: &[IpAddr],
|
|
||||||
port: u16,
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
options: Option<BrocadeOptions>,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
let ip = ip_addresses
|
|
||||||
.first()
|
|
||||||
.ok_or_else(|| Error::ConfigurationError("No IP addresses provided".to_string()))?;
|
|
||||||
|
|
||||||
let base_options = options.unwrap_or_default();
|
|
||||||
let options = ssh::try_init_client(username, password, ip, base_options).await?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
ip: *ip,
|
|
||||||
port,
|
|
||||||
username: username.to_string(),
|
|
||||||
password: password.to_string(),
|
|
||||||
before_all_commands: vec![],
|
|
||||||
after_all_commands: vec![],
|
|
||||||
options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn open_session(&self, mode: ExecutionMode) -> Result<BrocadeSession, Error> {
|
|
||||||
BrocadeSession::open(
|
|
||||||
self.ip,
|
|
||||||
self.port,
|
|
||||||
&self.username,
|
|
||||||
&self.password,
|
|
||||||
self.options.clone(),
|
|
||||||
mode,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn with_session<F, R>(&self, mode: ExecutionMode, callback: F) -> Result<R, Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
&mut BrocadeSession,
|
|
||||||
) -> std::pin::Pin<
|
|
||||||
Box<dyn std::future::Future<Output = Result<R, Error>> + Send + '_>,
|
|
||||||
>,
|
|
||||||
{
|
|
||||||
let mut session = self.open_session(mode).await?;
|
|
||||||
|
|
||||||
let _ = session.run_commands(self.before_all_commands.clone()).await;
|
|
||||||
let result = callback(&mut session).await;
|
|
||||||
let _ = session.run_commands(self.after_all_commands.clone()).await;
|
|
||||||
|
|
||||||
session.close().await?;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_command(&self, command: &str, mode: ExecutionMode) -> Result<String, Error> {
|
|
||||||
let mut session = self.open_session(mode).await?;
|
|
||||||
|
|
||||||
let _ = session.run_commands(self.before_all_commands.clone()).await;
|
|
||||||
let result = session.run_command(command).await;
|
|
||||||
let _ = session.run_commands(self.after_all_commands.clone()).await;
|
|
||||||
|
|
||||||
session.close().await?;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_commands(
|
|
||||||
&self,
|
|
||||||
commands: Vec<String>,
|
|
||||||
mode: ExecutionMode,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let mut session = self.open_session(mode).await?;
|
|
||||||
|
|
||||||
let _ = session.run_commands(self.before_all_commands.clone()).await;
|
|
||||||
let result = session.run_commands(commands).await;
|
|
||||||
let _ = session.run_commands(self.after_all_commands.clone()).await;
|
|
||||||
|
|
||||||
session.close().await?;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn before_all(&mut self, commands: Vec<String>) {
|
|
||||||
self.before_all_commands = commands;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn after_all(&mut self, commands: Vec<String>) {
|
|
||||||
self.after_all_commands = commands;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct BrocadeSession {
|
|
||||||
pub channel: russh::Channel<russh::client::Msg>,
|
|
||||||
pub mode: ExecutionMode,
|
|
||||||
pub options: BrocadeOptions,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BrocadeSession {
|
|
||||||
pub async fn open(
|
|
||||||
ip: IpAddr,
|
|
||||||
port: u16,
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
options: BrocadeOptions,
|
|
||||||
mode: ExecutionMode,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
let client = ssh::create_client(ip, port, username, password, &options).await?;
|
|
||||||
let mut channel = client.channel_open_session().await?;
|
|
||||||
|
|
||||||
channel
|
|
||||||
.request_pty(false, "vt100", 80, 24, 0, 0, &[])
|
|
||||||
.await?;
|
|
||||||
channel.request_shell(false).await?;
|
|
||||||
|
|
||||||
wait_for_shell_ready(&mut channel, &options.timeouts).await?;
|
|
||||||
|
|
||||||
if let ExecutionMode::Privileged = mode {
|
|
||||||
try_elevate_session(&mut channel, username, password, &options.timeouts).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
channel,
|
|
||||||
mode,
|
|
||||||
options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn close(&mut self) -> Result<(), Error> {
|
|
||||||
debug!("[Brocade] Closing session...");
|
|
||||||
|
|
||||||
self.channel.data(&b"exit\n"[..]).await?;
|
|
||||||
if let ExecutionMode::Privileged = self.mode {
|
|
||||||
self.channel.data(&b"exit\n"[..]).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
while start.elapsed() < self.options.timeouts.cleanup {
|
|
||||||
match timeout(self.options.timeouts.message_wait, self.channel.wait()).await {
|
|
||||||
Ok(Some(ChannelMsg::Close)) => break,
|
|
||||||
Ok(Some(_)) => continue,
|
|
||||||
Ok(None) | Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("[Brocade] Session closed.");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_command(&mut self, command: &str) -> Result<String, Error> {
|
|
||||||
if self.should_skip_command(command) {
|
|
||||||
return Ok(String::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("[Brocade] Running command: '{command}'...");
|
|
||||||
|
|
||||||
self.channel
|
|
||||||
.data(format!("{}\n", command).as_bytes())
|
|
||||||
.await?;
|
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
||||||
|
|
||||||
let output = self.collect_command_output().await?;
|
|
||||||
let output = String::from_utf8(output)
|
|
||||||
.map_err(|_| Error::UnexpectedError("Invalid UTF-8 in command output".to_string()))?;
|
|
||||||
|
|
||||||
self.check_for_command_errors(&output, command)?;
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_commands(&mut self, commands: Vec<String>) -> Result<(), Error> {
|
|
||||||
for command in commands {
|
|
||||||
self.run_command(&command).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_skip_command(&self, command: &str) -> bool {
|
|
||||||
if (command.starts_with("write") || command.starts_with("deploy")) && self.options.dry_run {
|
|
||||||
info!("[Brocade] Dry-run mode enabled, skipping command: {command}");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn collect_command_output(&mut self) -> Result<Vec<u8>, Error> {
|
|
||||||
let mut output = Vec::new();
|
|
||||||
let start = Instant::now();
|
|
||||||
let read_timeout = Duration::from_millis(500);
|
|
||||||
let log_interval = Duration::from_secs(5);
|
|
||||||
let mut last_log = Instant::now();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if start.elapsed() > self.options.timeouts.command_execution {
|
|
||||||
return Err(Error::TimeoutError(
|
|
||||||
"Timeout waiting for command completion.".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if start.elapsed() > self.options.timeouts.command_output
|
|
||||||
&& last_log.elapsed() > log_interval
|
|
||||||
{
|
|
||||||
info!("[Brocade] Waiting for command output...");
|
|
||||||
last_log = Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
match timeout(read_timeout, self.channel.wait()).await {
|
|
||||||
Ok(Some(ChannelMsg::Data { data } | ChannelMsg::ExtendedData { data, .. })) => {
|
|
||||||
output.extend_from_slice(&data);
|
|
||||||
let current_output = String::from_utf8_lossy(&output);
|
|
||||||
if current_output.contains('>') || current_output.contains('#') {
|
|
||||||
return Ok(output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Some(ChannelMsg::Eof | ChannelMsg::Close)) => return Ok(output),
|
|
||||||
Ok(Some(ChannelMsg::ExitStatus { exit_status })) => {
|
|
||||||
debug!("[Brocade] Command exit status: {exit_status}");
|
|
||||||
}
|
|
||||||
Ok(Some(_)) => continue,
|
|
||||||
Ok(None) | Err(_) => {
|
|
||||||
if output.is_empty() {
|
|
||||||
if let Ok(None) = timeout(read_timeout, self.channel.wait()).await {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
||||||
let current_output = String::from_utf8_lossy(&output);
|
|
||||||
if current_output.contains('>') || current_output.contains('#') {
|
|
||||||
return Ok(output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_for_command_errors(&self, output: &str, command: &str) -> Result<(), Error> {
|
|
||||||
const ERROR_PATTERNS: &[&str] = &[
|
|
||||||
"invalid input",
|
|
||||||
"syntax error",
|
|
||||||
"command not found",
|
|
||||||
"unknown command",
|
|
||||||
"permission denied",
|
|
||||||
"access denied",
|
|
||||||
"authentication failed",
|
|
||||||
"configuration error",
|
|
||||||
"failed to",
|
|
||||||
"error:",
|
|
||||||
];
|
|
||||||
|
|
||||||
let output_lower = output.to_lowercase();
|
|
||||||
if ERROR_PATTERNS.iter().any(|&p| output_lower.contains(p)) {
|
|
||||||
return Err(Error::CommandError(format!(
|
|
||||||
"Command error: {}",
|
|
||||||
output.trim()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !command.starts_with("show") && output.trim().is_empty() {
|
|
||||||
return Err(Error::CommandError(format!(
|
|
||||||
"Command '{command}' produced no output"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wait_for_shell_ready(
|
|
||||||
channel: &mut russh::Channel<russh::client::Msg>,
|
|
||||||
timeouts: &TimeoutConfig,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
let start = Instant::now();
|
|
||||||
|
|
||||||
while start.elapsed() < timeouts.shell_ready {
|
|
||||||
match timeout(timeouts.message_wait, channel.wait()).await {
|
|
||||||
Ok(Some(ChannelMsg::Data { data })) => {
|
|
||||||
buffer.extend_from_slice(&data);
|
|
||||||
let output = String::from_utf8_lossy(&buffer);
|
|
||||||
let output = output.trim();
|
|
||||||
if output.ends_with('>') || output.ends_with('#') {
|
|
||||||
debug!("[Brocade] Shell ready");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Some(_)) => continue,
|
|
||||||
Ok(None) => break,
|
|
||||||
Err(_) => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_elevate_session(
|
|
||||||
channel: &mut russh::Channel<russh::client::Msg>,
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
timeouts: &TimeoutConfig,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
channel.data(&b"enable\n"[..]).await?;
|
|
||||||
let start = Instant::now();
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
|
|
||||||
while start.elapsed() < timeouts.shell_ready {
|
|
||||||
match timeout(timeouts.message_wait, channel.wait()).await {
|
|
||||||
Ok(Some(ChannelMsg::Data { data })) => {
|
|
||||||
buffer.extend_from_slice(&data);
|
|
||||||
let output = String::from_utf8_lossy(&buffer);
|
|
||||||
|
|
||||||
if output.ends_with('#') {
|
|
||||||
debug!("[Brocade] Privileged mode established");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if output.contains("User Name:") {
|
|
||||||
channel.data(format!("{}\n", username).as_bytes()).await?;
|
|
||||||
buffer.clear();
|
|
||||||
} else if output.contains("Password:") {
|
|
||||||
channel.data(format!("{}\n", password).as_bytes()).await?;
|
|
||||||
buffer.clear();
|
|
||||||
} else if output.contains('>') {
|
|
||||||
return Err(Error::AuthenticationError(
|
|
||||||
"Enable authentication failed".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Some(_)) => continue,
|
|
||||||
Ok(None) => break,
|
|
||||||
Err(_) => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = String::from_utf8_lossy(&buffer);
|
|
||||||
if output.ends_with('#') {
|
|
||||||
debug!("[Brocade] Privileged mode established");
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Error::AuthenticationError(format!(
|
|
||||||
"Enable failed. Output:\n{output}"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use russh::client::Handler;
|
|
||||||
use russh::kex::DH_G1_SHA1;
|
|
||||||
use russh::kex::ECDH_SHA2_NISTP256;
|
|
||||||
use russh_keys::key::SSH_RSA;
|
|
||||||
|
|
||||||
use super::BrocadeOptions;
|
|
||||||
use super::Error;
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
|
||||||
pub struct SshOptions {
|
|
||||||
pub preferred_algorithms: russh::Preferred,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SshOptions {
|
|
||||||
fn ecdhsa_sha2_nistp256() -> Self {
|
|
||||||
Self {
|
|
||||||
preferred_algorithms: russh::Preferred {
|
|
||||||
kex: Cow::Borrowed(&[ECDH_SHA2_NISTP256]),
|
|
||||||
key: Cow::Borrowed(&[SSH_RSA]),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn legacy() -> Self {
|
|
||||||
Self {
|
|
||||||
preferred_algorithms: russh::Preferred {
|
|
||||||
kex: Cow::Borrowed(&[DH_G1_SHA1]),
|
|
||||||
key: Cow::Borrowed(&[SSH_RSA]),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Client;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Handler for Client {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
async fn check_server_key(
|
|
||||||
&mut self,
|
|
||||||
_server_public_key: &russh_keys::key::PublicKey,
|
|
||||||
) -> Result<bool, Self::Error> {
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn try_init_client(
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
ip: &std::net::IpAddr,
|
|
||||||
base_options: BrocadeOptions,
|
|
||||||
) -> Result<BrocadeOptions, Error> {
|
|
||||||
let ssh_options = vec![
|
|
||||||
SshOptions::default(),
|
|
||||||
SshOptions::ecdhsa_sha2_nistp256(),
|
|
||||||
SshOptions::legacy(),
|
|
||||||
];
|
|
||||||
|
|
||||||
for ssh in ssh_options {
|
|
||||||
let opts = BrocadeOptions {
|
|
||||||
ssh,
|
|
||||||
..base_options.clone()
|
|
||||||
};
|
|
||||||
let client = create_client(*ip, 22, username, password, &opts).await;
|
|
||||||
|
|
||||||
match client {
|
|
||||||
Ok(_) => {
|
|
||||||
return Ok(opts);
|
|
||||||
}
|
|
||||||
Err(e) => match e {
|
|
||||||
Error::NetworkError(e) => {
|
|
||||||
if e.contains("No common key exchange algorithm") {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
return Err(Error::NetworkError(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => return Err(e),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(Error::NetworkError(
|
|
||||||
"Could not establish ssh connection: wrong key exchange algorithm)".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_client(
|
|
||||||
ip: std::net::IpAddr,
|
|
||||||
port: u16,
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
options: &BrocadeOptions,
|
|
||||||
) -> Result<russh::client::Handle<Client>, Error> {
|
|
||||||
let config = russh::client::Config {
|
|
||||||
preferred: options.ssh.preferred_algorithms.clone(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let mut client = russh::client::connect(Arc::new(config), (ip, port), Client {}).await?;
|
|
||||||
if !client.authenticate_password(username, password).await? {
|
|
||||||
return Err(Error::AuthenticationError(
|
|
||||||
"ssh authentication failed".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(client)
|
|
||||||
}
|
|
||||||
BIN
data/okd/bin/kubectl
(Stored with Git LFS)
BIN
data/okd/bin/oc
(Stored with Git LFS)
BIN
data/okd/bin/oc_README.md
(Stored with Git LFS)
BIN
data/okd/bin/openshift-install
(Stored with Git LFS)
BIN
data/okd/bin/openshift-install_README.md
(Stored with Git LFS)
BIN
data/okd/installer_image/scos-9.0.20250510-0-live-initramfs.x86_64.img
(Stored with Git LFS)
BIN
data/okd/installer_image/scos-9.0.20250510-0-live-kernel.x86_64
(Stored with Git LFS)
BIN
data/okd/installer_image/scos-9.0.20250510-0-live-rootfs.x86_64.img
(Stored with Git LFS)
@@ -1 +0,0 @@
|
|||||||
scos-9.0.20250510-0-live-initramfs.x86_64.img
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
scos-9.0.20250510-0-live-kernel.x86_64
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
scos-9.0.20250510-0-live-rootfs.x86_64.img
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.terraform
|
|
||||||
*.tfstate
|
|
||||||
venv
|
|
||||||
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 100 KiB |
@@ -1,5 +0,0 @@
|
|||||||
To build :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx @marp-team/marp-cli@latest -w slides.md
|
|
||||||
```
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,9 +0,0 @@
|
|||||||
To run this :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
virtualenv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
pip install ansible ansible-dev-tools
|
|
||||||
ansible-lint download.yml
|
|
||||||
ansible-playbook -i localhost download.yml
|
|
||||||
```
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
- name: Test Ansible URL Validation
|
|
||||||
hosts: localhost
|
|
||||||
tasks:
|
|
||||||
- name: Download a file
|
|
||||||
ansible.builtin.get_url:
|
|
||||||
url: "http:/wikipedia.org/"
|
|
||||||
dest: "/tmp/ansible-test/wikipedia.html"
|
|
||||||
mode: '0900'
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
@@ -1,241 +0,0 @@
|
|||||||
---
|
|
||||||
theme: uncover
|
|
||||||
---
|
|
||||||
|
|
||||||
# Voici l'histoire de Petit Poisson
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer.jpg" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./happy_landscape_swimmer.jpg" width="1000"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer.jpg" width="200"/>
|
|
||||||
|
|
||||||
<img src="./tryrust.org.png" width="600"/>
|
|
||||||
|
|
||||||
[https://tryrust.org](https://tryrust.org)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_deploy_prod_1.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_deploy_prod_2.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_deploy_prod_3.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_deploy_prod_4.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Demo time
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_sunglasses.jpg" width="1000"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_download_wikipedia.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./ansible.jpg" width="200"/>
|
|
||||||
|
|
||||||
## Ansible❓
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer.jpg" width="200"/>
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Download wikipedia
|
|
||||||
hosts: localhost
|
|
||||||
tasks:
|
|
||||||
- name: Download a file
|
|
||||||
ansible.builtin.get_url:
|
|
||||||
url: "https:/wikipedia.org/"
|
|
||||||
dest: "/tmp/ansible-test/wikipedia.html"
|
|
||||||
mode: '0900'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer.jpg" width="200"/>
|
|
||||||
|
|
||||||
```
|
|
||||||
ansible-lint download.yml
|
|
||||||
|
|
||||||
Passed: 0 failure(s), 0 warning(s) on 1 files. Last profile that met the validation criteria was 'production'.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```
|
|
||||||
git push
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./75_years_later.jpg" width="1100"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_download_wikipedia_fail.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_reversed.jpg" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./ansible_output_fail.jpg" width="1100"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_reversed_1hit.jpg" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./ansible_crossed_out.jpg" width="400"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
<img src="./terraform.jpg" width="400"/>
|
|
||||||
|
|
||||||
## Terraform❓❗
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_reversed_1hit.jpg" width="200"/>
|
|
||||||
<img src="./terraform.jpg" width="200"/>
|
|
||||||
|
|
||||||
```tf
|
|
||||||
provider "docker" {}
|
|
||||||
|
|
||||||
resource "docker_network" "invalid_network" {
|
|
||||||
name = "my-invalid-network"
|
|
||||||
|
|
||||||
ipam_config {
|
|
||||||
subnet = "172.17.0.0/33"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_reversed_1hit.jpg" width="100"/>
|
|
||||||
<img src="./terraform.jpg" width="200"/>
|
|
||||||
|
|
||||||
```
|
|
||||||
terraform plan
|
|
||||||
|
|
||||||
Terraform used the selected providers to generate the following execution plan.
|
|
||||||
Resource actions are indicated with the following symbols:
|
|
||||||
+ create
|
|
||||||
|
|
||||||
Terraform will perform the following actions:
|
|
||||||
|
|
||||||
# docker_network.invalid_network will be created
|
|
||||||
+ resource "docker_network" "invalid_network" {
|
|
||||||
+ driver = (known after apply)
|
|
||||||
+ id = (known after apply)
|
|
||||||
+ internal = (known after apply)
|
|
||||||
+ ipam_driver = "default"
|
|
||||||
+ name = "my-invalid-network"
|
|
||||||
+ options = (known after apply)
|
|
||||||
+ scope = (known after apply)
|
|
||||||
|
|
||||||
+ ipam_config {
|
|
||||||
+ subnet = "172.17.0.0/33"
|
|
||||||
# (2 unchanged attributes hidden)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```
|
|
||||||
terraform apply
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```
|
|
||||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
|
||||||
|
|
||||||
Do you want to perform these actions?
|
|
||||||
Terraform will perform the actions described above.
|
|
||||||
Only 'yes' will be accepted to approve.
|
|
||||||
|
|
||||||
Enter a value: yes
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```
|
|
||||||
docker_network.invalid_network: Creating...
|
|
||||||
╷
|
|
||||||
│ Error: Unable to create network: Error response from daemon: invalid network config:
|
|
||||||
│ invalid subnet 172.17.0.0/33: invalid CIDR block notation
|
|
||||||
│
|
|
||||||
│ with docker_network.invalid_network,
|
|
||||||
│ on main.tf line 11, in resource "docker_network" "invalid_network":
|
|
||||||
│ 11: resource "docker_network" "invalid_network" {
|
|
||||||
│
|
|
||||||
╵
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_reversed_fullhit.jpg" width="1100"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./ansible_crossed_out.jpg" width="300"/>
|
|
||||||
<img src="./terraform_crossed_out.jpg" width="400"/>
|
|
||||||
<img src="./Happy_swimmer_reversed_fullhit.jpg" width="300"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Harmony❓❗
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Demo time
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer.jpg" width="300"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🎼
|
|
||||||
|
|
||||||
Harmony : [https://git.nationtech.io/nationtech/harmony](https://git.nationtech.io/nationtech/harmony)
|
|
||||||
|
|
||||||
|
|
||||||
<img src="./qrcode_gitea_nationtech.png" width="120"/>
|
|
||||||
|
|
||||||
|
|
||||||
LinkedIn : [https://www.linkedin.com/in/jean-gabriel-gill-couture/](https://www.linkedin.com/in/jean-gabriel-gill-couture/)
|
|
||||||
|
|
||||||
Courriel : [jg@nationtech.io](mailto:jg@nationtech.io)
|
|
||||||
@@ -25,11 +25,11 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### **Slide 3: The Real Cost of Infrastructure**
|
#### **Slide 3: The Real Cost: Cognitive Fatigue**
|
||||||
|
|
||||||
- **Visual:** "The Jenga Tower of Tools". A tall, precarious Jenga tower where each block is the logo of a different tool (Terraform, K8s, Helm, Ansible, Prometheus, ArgoCD, etc.). One block near the bottom is being nervously pulled out.
|
- **Visual:** "The Jenga Tower of Tools". A tall, precarious Jenga tower where each block is the logo of a different tool (Terraform, K8s, Helm, Ansible, Prometheus, ArgoCD, etc.). One block near the bottom is being nervously pulled out.
|
||||||
- **Narration:**
|
- **Narration:**
|
||||||
"The real cost isn't just complexity; it's the constant need to choose, learn, integrate, and operate a dozen different tools, each with its own syntax and failure modes. It's the nagging fear that a tiny typo in a config file could bring everything down. Click-ops isn't the answer, but the current state of IaC feels like we've traded one problem for another."
|
"The real cost isn't just complexity; it's _cognitive fatigue_. The constant need to choose, learn, integrate, and operate a dozen different tools, each with its own syntax and failure modes. It's the nagging fear that a tiny typo in a config file could bring everything down. Click-ops isn't the answer, but the current state of IaC feels like we've traded one problem for another."
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
- **Visual:** The clean, simple Harmony Rust DSL code from Slide 6. A summary of what was just accomplished is listed next to it: `✓ GitHub to Prod in minutes`, `✓ Type-Safe Validation`, `✓ Built-in Monitoring`, `✓ Automated Multi-Site Failover`.
|
- **Visual:** The clean, simple Harmony Rust DSL code from Slide 6. A summary of what was just accomplished is listed next to it: `✓ GitHub to Prod in minutes`, `✓ Type-Safe Validation`, `✓ Built-in Monitoring`, `✓ Automated Multi-Site Failover`.
|
||||||
- **Narration:**
|
- **Narration:**
|
||||||
"So, in just a few minutes, we went from a simple web app to a multi-site, monitored, and chaos-proof production deployment. We did it with a small amount of code that is easy to read, easy to verify, and completely portable. This is our vision: to offload the complexity, and make infrastructure simple, predictable, and even fun again."
|
"So, in just a few minutes, we went from a simple web app to a multi-site, monitored, and chaos-proof production deployment. We did it with a small amount of code that is easy to read, easy to verify, and completely portable. This is our vision: to offload the complexity, eliminate cognitive fatigue, and make infrastructure simple, predictable, and even fun again."
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,40 +0,0 @@
|
|||||||
# This file is maintained automatically by "terraform init".
|
|
||||||
# Manual edits may be lost in future updates.
|
|
||||||
|
|
||||||
provider "registry.terraform.io/hashicorp/http" {
|
|
||||||
version = "3.5.0"
|
|
||||||
hashes = [
|
|
||||||
"h1:8bUoPwS4hahOvzCBj6b04ObLVFXCEmEN8T/5eOHmWOM=",
|
|
||||||
"zh:047c5b4920751b13425efe0d011b3a23a3be97d02d9c0e3c60985521c9c456b7",
|
|
||||||
"zh:157866f700470207561f6d032d344916b82268ecd0cf8174fb11c0674c8d0736",
|
|
||||||
"zh:1973eb9383b0d83dd4fd5e662f0f16de837d072b64a6b7cd703410d730499476",
|
|
||||||
"zh:212f833a4e6d020840672f6f88273d62a564f44acb0c857b5961cdb3bbc14c90",
|
|
||||||
"zh:2c8034bc039fffaa1d4965ca02a8c6d57301e5fa9fff4773e684b46e3f78e76a",
|
|
||||||
"zh:5df353fc5b2dd31577def9cc1a4ebf0c9a9c2699d223c6b02087a3089c74a1c6",
|
|
||||||
"zh:672083810d4185076c81b16ad13d1224b9e6ea7f4850951d2ab8d30fa6e41f08",
|
|
||||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
||||||
"zh:7b4200f18abdbe39904b03537e1a78f21ebafe60f1c861a44387d314fda69da6",
|
|
||||||
"zh:843feacacd86baed820f81a6c9f7bd32cf302db3d7a0f39e87976ebc7a7cc2ee",
|
|
||||||
"zh:a9ea5096ab91aab260b22e4251c05f08dad2ed77e43e5e4fadcdfd87f2c78926",
|
|
||||||
"zh:d02b288922811739059e90184c7f76d45d07d3a77cc48d0b15fd3db14e928623",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
provider "registry.terraform.io/hashicorp/local" {
|
|
||||||
version = "2.5.3"
|
|
||||||
hashes = [
|
|
||||||
"h1:1Nkh16jQJMp0EuDmvP/96f5Unnir0z12WyDuoR6HjMo=",
|
|
||||||
"zh:284d4b5b572eacd456e605e94372f740f6de27b71b4e1fd49b63745d8ecd4927",
|
|
||||||
"zh:40d9dfc9c549e406b5aab73c023aa485633c1b6b730c933d7bcc2fa67fd1ae6e",
|
|
||||||
"zh:6243509bb208656eb9dc17d3c525c89acdd27f08def427a0dce22d5db90a4c8b",
|
|
||||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
||||||
"zh:885d85869f927853b6fe330e235cd03c337ac3b933b0d9ae827ec32fa1fdcdbf",
|
|
||||||
"zh:bab66af51039bdfcccf85b25fe562cbba2f54f6b3812202f4873ade834ec201d",
|
|
||||||
"zh:c505ff1bf9442a889ac7dca3ac05a8ee6f852e0118dd9a61796a2f6ff4837f09",
|
|
||||||
"zh:d36c0b5770841ddb6eaf0499ba3de48e5d4fc99f4829b6ab66b0fab59b1aaf4f",
|
|
||||||
"zh:ddb6a407c7f3ec63efb4dad5f948b54f7f4434ee1a2607a49680d494b1776fe1",
|
|
||||||
"zh:e0dafdd4500bec23d3ff221e3a9b60621c5273e5df867bc59ef6b7e41f5c91f6",
|
|
||||||
"zh:ece8742fd2882a8fc9d6efd20e2590010d43db386b920b2a9c220cfecc18de47",
|
|
||||||
"zh:f4c6b3eb8f39105004cf720e202f04f57e3578441cfb76ca27611139bc116a82",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
provider "http" {}
|
|
||||||
|
|
||||||
data "http" "remote_file" {
|
|
||||||
url = "http:/example.com/file.txt"
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "local_file" "downloaded_file" {
|
|
||||||
content = data.http.remote_file.body
|
|
||||||
filename = "${path.module}/downloaded_file.txt"
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# This file is maintained automatically by "terraform init".
|
|
||||||
# Manual edits may be lost in future updates.
|
|
||||||
|
|
||||||
provider "registry.terraform.io/kreuzwerker/docker" {
|
|
||||||
version = "3.0.2"
|
|
||||||
constraints = "~> 3.0.1"
|
|
||||||
hashes = [
|
|
||||||
"h1:cT2ccWOtlfKYBUE60/v2/4Q6Stk1KYTNnhxSck+VPlU=",
|
|
||||||
"zh:15b0a2b2b563d8d40f62f83057d91acb02cd0096f207488d8b4298a59203d64f",
|
|
||||||
"zh:23d919de139f7cd5ebfd2ff1b94e6d9913f0977fcfc2ca02e1573be53e269f95",
|
|
||||||
"zh:38081b3fe317c7e9555b2aaad325ad3fa516a886d2dfa8605ae6a809c1072138",
|
|
||||||
"zh:4a9c5065b178082f79ad8160243369c185214d874ff5048556d48d3edd03c4da",
|
|
||||||
"zh:5438ef6afe057945f28bce43d76c4401254073de01a774760169ac1058830ac2",
|
|
||||||
"zh:60b7fadc287166e5c9873dfe53a7976d98244979e0ab66428ea0dea1ebf33e06",
|
|
||||||
"zh:61c5ec1cb94e4c4a4fb1e4a24576d5f39a955f09afb17dab982de62b70a9bdd1",
|
|
||||||
"zh:a38fe9016ace5f911ab00c88e64b156ebbbbfb72a51a44da3c13d442cd214710",
|
|
||||||
"zh:c2c4d2b1fd9ebb291c57f524b3bf9d0994ff3e815c0cd9c9bcb87166dc687005",
|
|
||||||
"zh:d567bb8ce483ab2cf0602e07eae57027a1a53994aba470fa76095912a505533d",
|
|
||||||
"zh:e83bf05ab6a19dd8c43547ce9a8a511f8c331a124d11ac64687c764ab9d5a792",
|
|
||||||
"zh:e90c934b5cd65516fbcc454c89a150bfa726e7cf1fe749790c7480bbeb19d387",
|
|
||||||
"zh:f05f167d2eaf913045d8e7b88c13757e3cf595dd5cd333057fdafc7c4b7fed62",
|
|
||||||
"zh:fcc9c1cea5ce85e8bcb593862e699a881bd36dffd29e2e367f82d15368659c3d",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
docker = {
|
|
||||||
source = "kreuzwerker/docker"
|
|
||||||
version = "~> 3.0.1" # Adjust version as needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
provider "docker" {}
|
|
||||||
|
|
||||||
resource "docker_network" "invalid_network" {
|
|
||||||
name = "my-invalid-network"
|
|
||||||
|
|
||||||
ipam_config {
|
|
||||||
subnet = "172.17.0.0/33"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 325 KiB |
@@ -1,8 +0,0 @@
|
|||||||
## Bios settings
|
|
||||||
|
|
||||||
1. CSM : Disabled (compatibility support to boot gpt formatted drives)
|
|
||||||
2. Secure boot : disabled
|
|
||||||
3. Boot order :
|
|
||||||
1. Local Hard drive
|
|
||||||
2. PXE IPv4
|
|
||||||
4. System clock, make sure it is adjusted, otherwise you will get invalid certificates error
|
|
||||||
@@ -27,9 +27,9 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
let application = Arc::new(RustWebapp {
|
let application = Arc::new(RustWebapp {
|
||||||
name: "example-monitoring".to_string(),
|
name: "example-monitoring".to_string(),
|
||||||
|
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
|
||||||
project_root: PathBuf::from("./examples/rust/webapp"),
|
project_root: PathBuf::from("./examples/rust/webapp"),
|
||||||
framework: Some(RustWebFramework::Leptos),
|
framework: Some(RustWebFramework::Leptos),
|
||||||
service_port: 3000,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let webhook_receiver = WebhookReceiver {
|
let webhook_receiver = WebhookReceiver {
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "brocade-snmp-server"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { path = "../../harmony" }
|
|
||||||
brocade = { path = "../../brocade" }
|
|
||||||
harmony_secret = { path = "../../harmony_secret" }
|
|
||||||
harmony_cli = { path = "../../harmony_cli" }
|
|
||||||
harmony_types = { path = "../../harmony_types" }
|
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
log = { workspace = true }
|
|
||||||
env_logger = { workspace = true }
|
|
||||||
url = { workspace = true }
|
|
||||||
base64.workspace = true
|
|
||||||
serde.workspace = true
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
use std::net::{IpAddr, Ipv4Addr};
|
|
||||||
|
|
||||||
use harmony::{
|
|
||||||
inventory::Inventory, modules::brocade::BrocadeEnableSnmpScore, topology::K8sAnywhereTopology,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let brocade_snmp_server = BrocadeEnableSnmpScore {
|
|
||||||
server_ips: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 111))],
|
|
||||||
dry_run: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
harmony_cli::run(
|
|
||||||
Inventory::autoload(),
|
|
||||||
K8sAnywhereTopology::from_env(),
|
|
||||||
vec![Box::new(brocade_snmp_server)],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ use harmony::{
|
|||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::{
|
modules::{
|
||||||
dummy::{ErrorScore, PanicScore, SuccessScore},
|
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
inventory::LaunchDiscoverInventoryAgentScore,
|
inventory::DiscoverInventoryAgentScore,
|
||||||
},
|
},
|
||||||
topology::LocalhostTopology,
|
topology::LocalhostTopology,
|
||||||
};
|
};
|
||||||
@@ -16,7 +16,7 @@ async fn main() {
|
|||||||
Box::new(SuccessScore {}),
|
Box::new(SuccessScore {}),
|
||||||
Box::new(ErrorScore {}),
|
Box::new(ErrorScore {}),
|
||||||
Box::new(PanicScore {}),
|
Box::new(PanicScore {}),
|
||||||
Box::new(LaunchDiscoverInventoryAgentScore {
|
Box::new(DiscoverInventoryAgentScore {
|
||||||
discovery_timeout: Some(10),
|
discovery_timeout: Some(10),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ harmony_types = { path = "../../harmony_types" }
|
|||||||
cidr = { workspace = true }
|
cidr = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
harmony_macros = { path = "../../harmony_macros" }
|
||||||
harmony_secret = { path = "../../harmony_secret" }
|
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
env_logger = { workspace = true }
|
env_logger = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
serde = { workspace = true }
|
|
||||||
brocade = { path = "../../brocade" }
|
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
use std::{
|
use std::{
|
||||||
net::{IpAddr, Ipv4Addr},
|
net::{IpAddr, Ipv4Addr},
|
||||||
sync::{Arc, OnceLock},
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use brocade::BrocadeOptions;
|
|
||||||
use cidr::Ipv4Cidr;
|
use cidr::Ipv4Cidr;
|
||||||
use harmony::{
|
use harmony::{
|
||||||
config::secret::SshKeyPair,
|
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||||
data::{FileContent, FilePath},
|
infra::opnsense::OPNSenseManagementInterface,
|
||||||
hardware::{HostCategory, Location, PhysicalHost, SwitchGroup},
|
|
||||||
infra::{brocade::BrocadeSwitchClient, opnsense::OPNSenseManagementInterface},
|
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::{
|
modules::{
|
||||||
http::StaticFilesHttpScore,
|
http::StaticFilesHttpScore,
|
||||||
|
ipxe::IpxeScore,
|
||||||
okd::{
|
okd::{
|
||||||
bootstrap_dhcp::OKDBootstrapDhcpScore,
|
bootstrap_dhcp::OKDBootstrapDhcpScore,
|
||||||
bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, dhcp::OKDDhcpScore,
|
bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, dhcp::OKDDhcpScore,
|
||||||
dns::OKDDnsScore, ipxe::OKDIpxeScore,
|
dns::OKDDnsScore,
|
||||||
},
|
},
|
||||||
tftp::TftpScore,
|
tftp::TftpScore,
|
||||||
},
|
},
|
||||||
topology::{LogicalHost, UnmanagedRouter},
|
topology::{LogicalHost, UnmanagedRouter},
|
||||||
};
|
};
|
||||||
use harmony_macros::{ip, mac_address};
|
use harmony_macros::{ip, mac_address};
|
||||||
use harmony_secret::{Secret, SecretManager};
|
|
||||||
use harmony_types::net::Url;
|
use harmony_types::net::Url;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -34,26 +30,6 @@ async fn main() {
|
|||||||
name: String::from("fw0"),
|
name: String::from("fw0"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let switch_auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
|
||||||
.await
|
|
||||||
.expect("Failed to get credentials");
|
|
||||||
|
|
||||||
let switches: Vec<IpAddr> = vec![ip!("192.168.33.101")];
|
|
||||||
let brocade_options = Some(BrocadeOptions {
|
|
||||||
dry_run: *harmony::config::DRY_RUN,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let switch_client = BrocadeSwitchClient::init(
|
|
||||||
&switches,
|
|
||||||
&switch_auth.username,
|
|
||||||
&switch_auth.password,
|
|
||||||
brocade_options,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to connect to switch");
|
|
||||||
|
|
||||||
let switch_client = Arc::new(switch_client);
|
|
||||||
|
|
||||||
let opnsense = Arc::new(
|
let opnsense = Arc::new(
|
||||||
harmony::infra::opnsense::OPNSenseFirewall::new(firewall, None, "root", "opnsense").await,
|
harmony::infra::opnsense::OPNSenseFirewall::new(firewall, None, "root", "opnsense").await,
|
||||||
);
|
);
|
||||||
@@ -61,7 +37,6 @@ async fn main() {
|
|||||||
let gateway_ipv4 = Ipv4Addr::new(192, 168, 33, 1);
|
let gateway_ipv4 = Ipv4Addr::new(192, 168, 33, 1);
|
||||||
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
||||||
let topology = harmony::topology::HAClusterTopology {
|
let topology = harmony::topology::HAClusterTopology {
|
||||||
kubeconfig: None,
|
|
||||||
domain_name: "ncd0.harmony.mcd".to_string(), // TODO this must be set manually correctly
|
domain_name: "ncd0.harmony.mcd".to_string(), // TODO this must be set manually correctly
|
||||||
// when setting up the opnsense firewall
|
// when setting up the opnsense firewall
|
||||||
router: Arc::new(UnmanagedRouter::new(
|
router: Arc::new(UnmanagedRouter::new(
|
||||||
@@ -106,8 +81,7 @@ async fn main() {
|
|||||||
name: "wk2".to_string(),
|
name: "wk2".to_string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
switch_client: switch_client.clone(),
|
switch: vec![],
|
||||||
network_manager: OnceLock::new(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inventory = Inventory {
|
let inventory = Inventory {
|
||||||
@@ -150,28 +124,14 @@ async fn main() {
|
|||||||
let load_balancer_score =
|
let load_balancer_score =
|
||||||
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
|
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
|
||||||
|
|
||||||
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap();
|
|
||||||
|
|
||||||
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
|
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
|
||||||
let http_score = StaticFilesHttpScore {
|
let http_score = StaticFilesHttpScore {
|
||||||
folder_to_serve: Some(Url::LocalFolder(
|
folder_to_serve: Some(Url::LocalFolder(
|
||||||
"./data/watchguard/pxe-http-files".to_string(),
|
"./data/watchguard/pxe-http-files".to_string(),
|
||||||
)),
|
)),
|
||||||
files: vec![],
|
files: vec![],
|
||||||
remote_path: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let kickstart_filename = "inventory.kickstart".to_string();
|
|
||||||
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
|
||||||
|
|
||||||
let ipxe_score = OKDIpxeScore {
|
|
||||||
kickstart_filename,
|
|
||||||
harmony_inventory_agent,
|
|
||||||
cluster_pubkey: FileContent {
|
|
||||||
path: FilePath::Relative("cluster_ssh_key.pub".to_string()),
|
|
||||||
content: ssh_key.public,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
let ipxe_score = IpxeScore::new();
|
||||||
|
|
||||||
harmony_tui::run(
|
harmony_tui::run(
|
||||||
inventory,
|
inventory,
|
||||||
@@ -190,9 +150,3 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct BrocadeSwitchAuth {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "example-okd-install"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { path = "../../harmony" }
|
|
||||||
harmony_cli = { path = "../../harmony_cli" }
|
|
||||||
harmony_types = { path = "../../harmony_types" }
|
|
||||||
harmony_secret = { path = "../../harmony_secret" }
|
|
||||||
harmony_secret_derive = { path = "../../harmony_secret_derive" }
|
|
||||||
cidr = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
|
||||||
log = { workspace = true }
|
|
||||||
env_logger = { workspace = true }
|
|
||||||
url = { workspace = true }
|
|
||||||
serde.workspace = true
|
|
||||||
brocade = { path = "../../brocade" }
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export HARMONY_SECRET_NAMESPACE=example-vms
|
|
||||||
export HARMONY_SECRET_STORE=file
|
|
||||||
export HARMONY_DATABASE_URL=sqlite://harmony_vms.sqlite RUST_LOG=info
|
|
||||||
export RUST_LOG=info
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
mod topology;
|
|
||||||
|
|
||||||
use crate::topology::{get_inventory, get_topology};
|
|
||||||
use harmony::{
|
|
||||||
config::secret::SshKeyPair,
|
|
||||||
data::{FileContent, FilePath},
|
|
||||||
modules::okd::{installation::OKDInstallationPipeline, ipxe::OKDIpxeScore},
|
|
||||||
score::Score,
|
|
||||||
topology::HAClusterTopology,
|
|
||||||
};
|
|
||||||
use harmony_secret::SecretManager;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let inventory = get_inventory();
|
|
||||||
let topology = get_topology().await;
|
|
||||||
|
|
||||||
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap();
|
|
||||||
|
|
||||||
let mut scores: Vec<Box<dyn Score<HAClusterTopology>>> = vec![Box::new(OKDIpxeScore {
|
|
||||||
kickstart_filename: "inventory.kickstart".to_string(),
|
|
||||||
harmony_inventory_agent: "harmony_inventory_agent".to_string(),
|
|
||||||
cluster_pubkey: FileContent {
|
|
||||||
path: FilePath::Relative("cluster_ssh_key.pub".to_string()),
|
|
||||||
content: ssh_key.public,
|
|
||||||
},
|
|
||||||
})];
|
|
||||||
|
|
||||||
scores.append(&mut OKDInstallationPipeline::get_all_scores().await);
|
|
||||||
|
|
||||||
harmony_cli::run(inventory, topology, scores, None)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
use brocade::BrocadeOptions;
|
|
||||||
use cidr::Ipv4Cidr;
|
|
||||||
use harmony::{
|
|
||||||
hardware::{Location, SwitchGroup},
|
|
||||||
infra::{brocade::BrocadeSwitchClient, opnsense::OPNSenseManagementInterface},
|
|
||||||
inventory::Inventory,
|
|
||||||
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
|
|
||||||
};
|
|
||||||
use harmony_macros::{ip, ipv4};
|
|
||||||
use harmony_secret::{Secret, SecretManager};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{
|
|
||||||
net::IpAddr,
|
|
||||||
sync::{Arc, OnceLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
struct OPNSenseFirewallConfig {
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_topology() -> HAClusterTopology {
|
|
||||||
let firewall = harmony::topology::LogicalHost {
|
|
||||||
ip: ip!("192.168.1.1"),
|
|
||||||
name: String::from("opnsense-1"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let switch_auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
|
||||||
.await
|
|
||||||
.expect("Failed to get credentials");
|
|
||||||
|
|
||||||
let switches: Vec<IpAddr> = vec![ip!("192.168.1.101")]; // TODO: Adjust me
|
|
||||||
let brocade_options = Some(BrocadeOptions {
|
|
||||||
dry_run: *harmony::config::DRY_RUN,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let switch_client = BrocadeSwitchClient::init(
|
|
||||||
&switches,
|
|
||||||
&switch_auth.username,
|
|
||||||
&switch_auth.password,
|
|
||||||
brocade_options,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to connect to switch");
|
|
||||||
|
|
||||||
let switch_client = Arc::new(switch_client);
|
|
||||||
|
|
||||||
let config = SecretManager::get_or_prompt::<OPNSenseFirewallConfig>().await;
|
|
||||||
let config = config.unwrap();
|
|
||||||
|
|
||||||
let opnsense = Arc::new(
|
|
||||||
harmony::infra::opnsense::OPNSenseFirewall::new(
|
|
||||||
firewall,
|
|
||||||
None,
|
|
||||||
&config.username,
|
|
||||||
&config.password,
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
);
|
|
||||||
let lan_subnet = ipv4!("192.168.1.0");
|
|
||||||
let gateway_ipv4 = ipv4!("192.168.1.1");
|
|
||||||
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
|
||||||
harmony::topology::HAClusterTopology {
|
|
||||||
kubeconfig: None,
|
|
||||||
domain_name: "demo.harmony.mcd".to_string(),
|
|
||||||
router: Arc::new(UnmanagedRouter::new(
|
|
||||||
gateway_ip,
|
|
||||||
Ipv4Cidr::new(lan_subnet, 24).unwrap(),
|
|
||||||
)),
|
|
||||||
load_balancer: opnsense.clone(),
|
|
||||||
firewall: opnsense.clone(),
|
|
||||||
tftp_server: opnsense.clone(),
|
|
||||||
http_server: opnsense.clone(),
|
|
||||||
dhcp_server: opnsense.clone(),
|
|
||||||
dns_server: opnsense.clone(),
|
|
||||||
control_plane: vec![LogicalHost {
|
|
||||||
ip: ip!("192.168.1.20"),
|
|
||||||
name: "master".to_string(),
|
|
||||||
}],
|
|
||||||
bootstrap_host: LogicalHost {
|
|
||||||
ip: ip!("192.168.1.10"),
|
|
||||||
name: "bootstrap".to_string(),
|
|
||||||
},
|
|
||||||
workers: vec![],
|
|
||||||
switch_client: switch_client.clone(),
|
|
||||||
network_manager: OnceLock::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_inventory() -> Inventory {
|
|
||||||
Inventory {
|
|
||||||
location: Location::new(
|
|
||||||
"Some virtual machine or maybe a physical machine if you're cool".to_string(),
|
|
||||||
"testopnsense".to_string(),
|
|
||||||
),
|
|
||||||
switch: SwitchGroup::from([]),
|
|
||||||
firewall_mgmt: Box::new(OPNSenseManagementInterface::new()),
|
|
||||||
storage_host: vec![],
|
|
||||||
worker_host: vec![],
|
|
||||||
control_plane_host: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct BrocadeSwitchAuth {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
|
||||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
|
||||||
QyNTUxOQAAACAcemw8pbwuvHFaYynxBbS0Cf3ThYuj1Utr7CDqjwySHAAAAJikacCNpGnA
|
|
||||||
jQAAAAtzc2gtZWQyNTUxOQAAACAcemw8pbwuvHFaYynxBbS0Cf3ThYuj1Utr7CDqjwySHA
|
|
||||||
AAAECiiKk4V6Q5cVs6axDM4sjAzZn/QCZLQekmYQXS9XbEYxx6bDylvC68cVpjKfEFtLQJ
|
|
||||||
/dOFi6PVS2vsIOqPDJIcAAAAEGplYW5nYWJAbGlsaWFuZTIBAgMEBQ==
|
|
||||||
-----END OPENSSH PRIVATE KEY-----
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBx6bDylvC68cVpjKfEFtLQJ/dOFi6PVS2vsIOqPDJIc jeangab@liliane2
|
|
||||||
@@ -19,4 +19,3 @@ log = { workspace = true }
|
|||||||
env_logger = { workspace = true }
|
env_logger = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
brocade = { path = "../../brocade" }
|
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
mod topology;
|
mod topology;
|
||||||
|
|
||||||
use crate::topology::{get_inventory, get_topology};
|
use crate::topology::{get_inventory, get_topology};
|
||||||
use harmony::{
|
use harmony::modules::okd::ipxe::OkdIpxeScore;
|
||||||
config::secret::SshKeyPair,
|
|
||||||
data::{FileContent, FilePath},
|
|
||||||
modules::okd::ipxe::OKDIpxeScore,
|
|
||||||
};
|
|
||||||
use harmony_secret::SecretManager;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -14,16 +9,13 @@ async fn main() {
|
|||||||
let topology = get_topology().await;
|
let topology = get_topology().await;
|
||||||
|
|
||||||
let kickstart_filename = "inventory.kickstart".to_string();
|
let kickstart_filename = "inventory.kickstart".to_string();
|
||||||
|
let cluster_pubkey_filename = "cluster_ssh_key.pub".to_string();
|
||||||
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
||||||
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap();
|
|
||||||
|
|
||||||
let ipxe_score = OKDIpxeScore {
|
let ipxe_score = OkdIpxeScore {
|
||||||
kickstart_filename,
|
kickstart_filename,
|
||||||
harmony_inventory_agent,
|
harmony_inventory_agent,
|
||||||
cluster_pubkey: FileContent {
|
cluster_pubkey_filename,
|
||||||
path: FilePath::Relative("cluster_ssh_key.pub".to_string()),
|
|
||||||
content: ssh_key.public,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
harmony_cli::run(inventory, topology, vec![Box::new(ipxe_score)], None)
|
harmony_cli::run(inventory, topology, vec![Box::new(ipxe_score)], None)
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
use brocade::BrocadeOptions;
|
|
||||||
use cidr::Ipv4Cidr;
|
use cidr::Ipv4Cidr;
|
||||||
use harmony::{
|
use harmony::{
|
||||||
config::secret::OPNSenseFirewallCredentials,
|
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||||
hardware::{Location, SwitchGroup},
|
infra::opnsense::OPNSenseManagementInterface,
|
||||||
infra::{brocade::BrocadeSwitchClient, opnsense::OPNSenseManagementInterface},
|
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
|
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
|
||||||
};
|
};
|
||||||
use harmony_macros::{ip, ipv4};
|
use harmony_macros::{ip, ipv4};
|
||||||
use harmony_secret::{Secret, SecretManager};
|
use harmony_secret::{Secret, SecretManager};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{net::IpAddr, sync::Arc};
|
||||||
net::IpAddr,
|
|
||||||
sync::{Arc, OnceLock},
|
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
||||||
};
|
struct OPNSenseFirewallConfig {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_topology() -> HAClusterTopology {
|
pub async fn get_topology() -> HAClusterTopology {
|
||||||
let firewall = harmony::topology::LogicalHost {
|
let firewall = harmony::topology::LogicalHost {
|
||||||
@@ -21,27 +22,7 @@ pub async fn get_topology() -> HAClusterTopology {
|
|||||||
name: String::from("opnsense-1"),
|
name: String::from("opnsense-1"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let switch_auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
let config = SecretManager::get::<OPNSenseFirewallConfig>().await;
|
||||||
.await
|
|
||||||
.expect("Failed to get credentials");
|
|
||||||
|
|
||||||
let switches: Vec<IpAddr> = vec![ip!("192.168.1.101")]; // TODO: Adjust me
|
|
||||||
let brocade_options = Some(BrocadeOptions {
|
|
||||||
dry_run: *harmony::config::DRY_RUN,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let switch_client = BrocadeSwitchClient::init(
|
|
||||||
&switches,
|
|
||||||
&switch_auth.username,
|
|
||||||
&switch_auth.password,
|
|
||||||
brocade_options,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to connect to switch");
|
|
||||||
|
|
||||||
let switch_client = Arc::new(switch_client);
|
|
||||||
|
|
||||||
let config = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>().await;
|
|
||||||
let config = config.unwrap();
|
let config = config.unwrap();
|
||||||
|
|
||||||
let opnsense = Arc::new(
|
let opnsense = Arc::new(
|
||||||
@@ -57,7 +38,6 @@ pub async fn get_topology() -> HAClusterTopology {
|
|||||||
let gateway_ipv4 = ipv4!("192.168.1.1");
|
let gateway_ipv4 = ipv4!("192.168.1.1");
|
||||||
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
||||||
harmony::topology::HAClusterTopology {
|
harmony::topology::HAClusterTopology {
|
||||||
kubeconfig: None,
|
|
||||||
domain_name: "demo.harmony.mcd".to_string(),
|
domain_name: "demo.harmony.mcd".to_string(),
|
||||||
router: Arc::new(UnmanagedRouter::new(
|
router: Arc::new(UnmanagedRouter::new(
|
||||||
gateway_ip,
|
gateway_ip,
|
||||||
@@ -78,8 +58,7 @@ pub async fn get_topology() -> HAClusterTopology {
|
|||||||
name: "cp0".to_string(),
|
name: "cp0".to_string(),
|
||||||
},
|
},
|
||||||
workers: vec![],
|
workers: vec![],
|
||||||
switch_client: switch_client.clone(),
|
switch: vec![],
|
||||||
network_manager: OnceLock::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,9 +75,3 @@ pub fn get_inventory() -> Inventory {
|
|||||||
control_plane_host: vec![],
|
control_plane_host: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct BrocadeSwitchAuth {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "example-openbao"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { path = "../../harmony" }
|
|
||||||
harmony_cli = { path = "../../harmony_cli" }
|
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
|
||||||
harmony_types = { path = "../../harmony_types" }
|
|
||||||
tokio.workspace = true
|
|
||||||
url.workspace = true
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
To install an openbao instance with harmony simply `cargo run -p example-openbao` .
|
|
||||||
|
|
||||||
Depending on your environement configuration, it will either install a k3d cluster locally and deploy on it, or install to a remote cluster.
|
|
||||||
|
|
||||||
Then follow the openbao documentation to initialize and unseal, this will make openbao usable.
|
|
||||||
|
|
||||||
https://openbao.org/docs/platform/k8s/helm/run/
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
use std::{collections::HashMap, str::FromStr};
|
|
||||||
|
|
||||||
use harmony::{
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::helm::chart::{HelmChartScore, HelmRepository, NonBlankString},
|
|
||||||
topology::K8sAnywhereTopology,
|
|
||||||
};
|
|
||||||
use harmony_macros::hurl;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let values_yaml = Some(
|
|
||||||
r#"server:
|
|
||||||
standalone:
|
|
||||||
enabled: true
|
|
||||||
config: |
|
|
||||||
listener "tcp" {
|
|
||||||
tls_disable = true
|
|
||||||
address = "[::]:8200"
|
|
||||||
cluster_address = "[::]:8201"
|
|
||||||
}
|
|
||||||
|
|
||||||
storage "file" {
|
|
||||||
path = "/openbao/data"
|
|
||||||
}
|
|
||||||
|
|
||||||
service:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
dataStorage:
|
|
||||||
enabled: true
|
|
||||||
size: 10Gi
|
|
||||||
storageClass: null
|
|
||||||
accessMode: ReadWriteOnce
|
|
||||||
|
|
||||||
auditStorage:
|
|
||||||
enabled: true
|
|
||||||
size: 10Gi
|
|
||||||
storageClass: null
|
|
||||||
accessMode: ReadWriteOnce"#
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
let openbao = HelmChartScore {
|
|
||||||
namespace: Some(NonBlankString::from_str("openbao").unwrap()),
|
|
||||||
release_name: NonBlankString::from_str("openbao").unwrap(),
|
|
||||||
chart_name: NonBlankString::from_str("openbao/openbao").unwrap(),
|
|
||||||
chart_version: None,
|
|
||||||
values_overrides: None,
|
|
||||||
values_yaml,
|
|
||||||
create_namespace: true,
|
|
||||||
install_only: true,
|
|
||||||
repository: Some(HelmRepository::new(
|
|
||||||
"openbao".to_string(),
|
|
||||||
hurl!("https://openbao.github.io/openbao-helm"),
|
|
||||||
true,
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
|
|
||||||
harmony_cli::run(
|
|
||||||
Inventory::autoload(),
|
|
||||||
K8sAnywhereTopology::from_env(),
|
|
||||||
vec![Box::new(openbao)],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,3 @@ harmony_macros = { path = "../../harmony_macros" }
|
|||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
env_logger = { workspace = true }
|
env_logger = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
harmony_secret = { path = "../../harmony_secret" }
|
|
||||||
brocade = { path = "../../brocade" }
|
|
||||||
serde = { workspace = true }
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
use std::{
|
use std::{
|
||||||
net::{IpAddr, Ipv4Addr},
|
net::{IpAddr, Ipv4Addr},
|
||||||
sync::{Arc, OnceLock},
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use brocade::BrocadeOptions;
|
|
||||||
use cidr::Ipv4Cidr;
|
use cidr::Ipv4Cidr;
|
||||||
use harmony::{
|
use harmony::{
|
||||||
hardware::{HostCategory, Location, PhysicalHost, SwitchGroup},
|
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||||
infra::{brocade::BrocadeSwitchClient, opnsense::OPNSenseManagementInterface},
|
infra::opnsense::OPNSenseManagementInterface,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::{
|
modules::{
|
||||||
dummy::{ErrorScore, PanicScore, SuccessScore},
|
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
@@ -19,9 +18,7 @@ use harmony::{
|
|||||||
topology::{LogicalHost, UnmanagedRouter},
|
topology::{LogicalHost, UnmanagedRouter},
|
||||||
};
|
};
|
||||||
use harmony_macros::{ip, mac_address};
|
use harmony_macros::{ip, mac_address};
|
||||||
use harmony_secret::{Secret, SecretManager};
|
|
||||||
use harmony_types::net::Url;
|
use harmony_types::net::Url;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -30,26 +27,6 @@ async fn main() {
|
|||||||
name: String::from("opnsense-1"),
|
name: String::from("opnsense-1"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let switch_auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
|
||||||
.await
|
|
||||||
.expect("Failed to get credentials");
|
|
||||||
|
|
||||||
let switches: Vec<IpAddr> = vec![ip!("192.168.5.101")]; // TODO: Adjust me
|
|
||||||
let brocade_options = Some(BrocadeOptions {
|
|
||||||
dry_run: *harmony::config::DRY_RUN,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let switch_client = BrocadeSwitchClient::init(
|
|
||||||
&switches,
|
|
||||||
&switch_auth.username,
|
|
||||||
&switch_auth.password,
|
|
||||||
brocade_options,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to connect to switch");
|
|
||||||
|
|
||||||
let switch_client = Arc::new(switch_client);
|
|
||||||
|
|
||||||
let opnsense = Arc::new(
|
let opnsense = Arc::new(
|
||||||
harmony::infra::opnsense::OPNSenseFirewall::new(firewall, None, "root", "opnsense").await,
|
harmony::infra::opnsense::OPNSenseFirewall::new(firewall, None, "root", "opnsense").await,
|
||||||
);
|
);
|
||||||
@@ -57,7 +34,6 @@ async fn main() {
|
|||||||
let gateway_ipv4 = Ipv4Addr::new(10, 100, 8, 1);
|
let gateway_ipv4 = Ipv4Addr::new(10, 100, 8, 1);
|
||||||
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
||||||
let topology = harmony::topology::HAClusterTopology {
|
let topology = harmony::topology::HAClusterTopology {
|
||||||
kubeconfig: None,
|
|
||||||
domain_name: "demo.harmony.mcd".to_string(),
|
domain_name: "demo.harmony.mcd".to_string(),
|
||||||
router: Arc::new(UnmanagedRouter::new(
|
router: Arc::new(UnmanagedRouter::new(
|
||||||
gateway_ip,
|
gateway_ip,
|
||||||
@@ -78,8 +54,7 @@ async fn main() {
|
|||||||
name: "cp0".to_string(),
|
name: "cp0".to_string(),
|
||||||
},
|
},
|
||||||
workers: vec![],
|
workers: vec![],
|
||||||
switch_client: switch_client.clone(),
|
switch: vec![],
|
||||||
network_manager: OnceLock::new(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inventory = Inventory {
|
let inventory = Inventory {
|
||||||
@@ -110,7 +85,6 @@ async fn main() {
|
|||||||
"./data/watchguard/pxe-http-files".to_string(),
|
"./data/watchguard/pxe-http-files".to_string(),
|
||||||
)),
|
)),
|
||||||
files: vec![],
|
files: vec![],
|
||||||
remote_path: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
harmony_tui::run(
|
harmony_tui::run(
|
||||||
@@ -134,9 +108,3 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct BrocadeSwitchAuth {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "example-remove-rook-osd"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { version = "0.1.0", path = "../../harmony" }
|
|
||||||
harmony_cli = { version = "0.1.0", path = "../../harmony_cli" }
|
|
||||||
tokio.workspace = true
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
use harmony::{
|
|
||||||
inventory::Inventory, modules::storage::ceph::ceph_remove_osd_score::CephRemoveOsd,
|
|
||||||
topology::K8sAnywhereTopology,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let ceph_score = CephRemoveOsd {
|
|
||||||
osd_deployment_name: "rook-ceph-osd-2".to_string(),
|
|
||||||
rook_ceph_namespace: "rook-ceph".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let topology = K8sAnywhereTopology::from_env();
|
|
||||||
let inventory = Inventory::autoload();
|
|
||||||
harmony_cli::run(inventory, topology, vec![Box::new(ceph_score)], None)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rhob-application-monitoring"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { path = "../../harmony" }
|
|
||||||
harmony_cli = { path = "../../harmony_cli" }
|
|
||||||
harmony_types = { path = "../../harmony_types" }
|
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
log = { workspace = true }
|
|
||||||
env_logger = { workspace = true }
|
|
||||||
url = { workspace = true }
|
|
||||||
base64.workspace = true
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
use std::{path::PathBuf, sync::Arc};
|
|
||||||
|
|
||||||
use harmony::{
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::{
|
|
||||||
application::{
|
|
||||||
ApplicationScore, RustWebFramework, RustWebapp, features::rhob_monitoring::Monitoring,
|
|
||||||
},
|
|
||||||
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
|
||||||
},
|
|
||||||
topology::K8sAnywhereTopology,
|
|
||||||
};
|
|
||||||
use harmony_types::net::Url;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let application = Arc::new(RustWebapp {
|
|
||||||
name: "test-rhob-monitoring".to_string(),
|
|
||||||
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
|
|
||||||
framework: Some(RustWebFramework::Leptos),
|
|
||||||
service_port: 3000,
|
|
||||||
});
|
|
||||||
|
|
||||||
let discord_receiver = DiscordWebhook {
|
|
||||||
name: "test-discord".to_string(),
|
|
||||||
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = ApplicationScore {
|
|
||||||
features: vec![
|
|
||||||
Box::new(Monitoring {
|
|
||||||
application: application.clone(),
|
|
||||||
alert_receiver: vec![Box::new(discord_receiver)],
|
|
||||||
}),
|
|
||||||
// TODO add backups, multisite ha, etc
|
|
||||||
],
|
|
||||||
application,
|
|
||||||
};
|
|
||||||
|
|
||||||
harmony_cli::run(
|
|
||||||
Inventory::autoload(),
|
|
||||||
K8sAnywhereTopology::from_env(),
|
|
||||||
vec![Box::new(app)],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ use harmony::{
|
|||||||
modules::{
|
modules::{
|
||||||
application::{
|
application::{
|
||||||
ApplicationScore, RustWebFramework, RustWebapp,
|
ApplicationScore, RustWebFramework, RustWebapp,
|
||||||
features::{Monitoring, PackagingDeployment},
|
features::{ContinuousDelivery, Monitoring},
|
||||||
},
|
},
|
||||||
monitoring::alert_channel::{
|
monitoring::alert_channel::{
|
||||||
discord_alert_channel::DiscordWebhook, webhook_receiver::WebhookReceiver,
|
discord_alert_channel::DiscordWebhook, webhook_receiver::WebhookReceiver,
|
||||||
@@ -13,30 +13,30 @@ use harmony::{
|
|||||||
},
|
},
|
||||||
topology::K8sAnywhereTopology,
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
use harmony_macros::hurl;
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let application = Arc::new(RustWebapp {
|
let application = Arc::new(RustWebapp {
|
||||||
name: "harmony-example-rust-webapp".to_string(),
|
name: "harmony-example-rust-webapp".to_string(),
|
||||||
project_root: PathBuf::from("./webapp"),
|
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
|
||||||
|
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
|
||||||
framework: Some(RustWebFramework::Leptos),
|
framework: Some(RustWebFramework::Leptos),
|
||||||
service_port: 3000,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: "test-discord".to_string(),
|
name: "test-discord".to_string(),
|
||||||
url: hurl!("https://discord.doesnt.exist.com"),
|
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let webhook_receiver = WebhookReceiver {
|
let webhook_receiver = WebhookReceiver {
|
||||||
name: "sample-webhook-receiver".to_string(),
|
name: "sample-webhook-receiver".to_string(),
|
||||||
url: hurl!("https://webhook-doesnt-exist.com"),
|
url: Url::Url(url::Url::parse("https://webhook-doesnt-exist.com").unwrap()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = ApplicationScore {
|
let app = ApplicationScore {
|
||||||
features: vec![
|
features: vec![
|
||||||
Box::new(PackagingDeployment {
|
Box::new(ContinuousDelivery {
|
||||||
application: application.clone(),
|
application: application.clone(),
|
||||||
}),
|
}),
|
||||||
Box::new(Monitoring {
|
Box::new(Monitoring {
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "example-try-rust-webapp"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { path = "../../harmony" }
|
|
||||||
harmony_cli = { path = "../../harmony_cli" }
|
|
||||||
harmony_types = { path = "../../harmony_types" }
|
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
log = { workspace = true }
|
|
||||||
env_logger = { workspace = true }
|
|
||||||
url = { workspace = true }
|
|
||||||
base64.workspace = true
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
harmony
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "harmony-tryrust"
|
|
||||||
edition = "2024"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { path = "../../../nationtech/harmony/harmony" }
|
|
||||||
harmony_cli = { path = "../../../nationtech/harmony/harmony_cli" }
|
|
||||||
harmony_types = { path = "../../../nationtech/harmony/harmony_types" }
|
|
||||||
harmony_macros = { path = "../../../nationtech/harmony/harmony_macros" }
|
|
||||||
tokio = { version = "1.40", features = [
|
|
||||||
"io-std",
|
|
||||||
"fs",
|
|
||||||
"macros",
|
|
||||||
"rt-multi-thread",
|
|
||||||
] }
|
|
||||||
log = { version = "0.4", features = ["kv"] }
|
|
||||||
env_logger = "0.11"
|
|
||||||
url = "2.5"
|
|
||||||
base64 = "0.22.1"
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use harmony::{
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::{
|
|
||||||
application::{
|
|
||||||
ApplicationScore, RustWebFramework, RustWebapp,
|
|
||||||
features::{PackagingDeployment, rhob_monitoring::Monitoring},
|
|
||||||
},
|
|
||||||
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
|
||||||
},
|
|
||||||
topology::K8sAnywhereTopology,
|
|
||||||
};
|
|
||||||
use harmony_macros::hurl;
|
|
||||||
use std::{path::PathBuf, sync::Arc};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let application = Arc::new(RustWebapp {
|
|
||||||
name: "tryrust".to_string(),
|
|
||||||
project_root: PathBuf::from(".."),
|
|
||||||
framework: Some(RustWebFramework::Leptos),
|
|
||||||
service_port: 8080,
|
|
||||||
});
|
|
||||||
|
|
||||||
let discord_webhook = DiscordWebhook {
|
|
||||||
name: "harmony_demo".to_string(),
|
|
||||||
url: hurl!("http://not_a_url.com"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = ApplicationScore {
|
|
||||||
features: vec![
|
|
||||||
Box::new(PackagingDeployment {
|
|
||||||
application: application.clone(),
|
|
||||||
}),
|
|
||||||
Box::new(Monitoring {
|
|
||||||
application: application.clone(),
|
|
||||||
alert_receiver: vec![Box::new(discord_webhook)],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
application,
|
|
||||||
};
|
|
||||||
|
|
||||||
harmony_cli::run(
|
|
||||||
Inventory::autoload(),
|
|
||||||
K8sAnywhereTopology::from_env(),
|
|
||||||
vec![Box::new(app)],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use harmony::{
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::{
|
|
||||||
application::{
|
|
||||||
ApplicationScore, RustWebFramework, RustWebapp,
|
|
||||||
features::{Monitoring, PackagingDeployment},
|
|
||||||
},
|
|
||||||
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
|
||||||
},
|
|
||||||
topology::K8sAnywhereTopology,
|
|
||||||
};
|
|
||||||
use harmony_macros::hurl;
|
|
||||||
use std::{path::PathBuf, sync::Arc};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let application = Arc::new(RustWebapp {
|
|
||||||
name: "harmony-example-tryrust".to_string(),
|
|
||||||
project_root: PathBuf::from("./tryrust.org"), // <== Project root, in this case it is a
|
|
||||||
// submodule
|
|
||||||
framework: Some(RustWebFramework::Leptos),
|
|
||||||
service_port: 8080,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define your Application deployment and the features you want
|
|
||||||
let app = ApplicationScore {
|
|
||||||
features: vec![
|
|
||||||
Box::new(PackagingDeployment {
|
|
||||||
application: application.clone(),
|
|
||||||
}),
|
|
||||||
Box::new(Monitoring {
|
|
||||||
application: application.clone(),
|
|
||||||
alert_receiver: vec![Box::new(DiscordWebhook {
|
|
||||||
name: "test-discord".to_string(),
|
|
||||||
url: hurl!("https://discord.doesnt.exist.com"),
|
|
||||||
})],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
application,
|
|
||||||
};
|
|
||||||
|
|
||||||
harmony_cli::run(
|
|
||||||
Inventory::autoload(),
|
|
||||||
K8sAnywhereTopology::from_env(), // <== Deploy to local automatically provisioned k3d by default or connect to any kubernetes cluster
|
|
||||||
vec![Box::new(app)],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ use harmony::{
|
|||||||
},
|
},
|
||||||
topology::{
|
topology::{
|
||||||
BackendServer, DummyInfra, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancerService,
|
BackendServer, DummyInfra, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancerService,
|
||||||
SSL,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use harmony_macros::ipv4;
|
use harmony_macros::ipv4;
|
||||||
@@ -48,7 +47,6 @@ fn build_large_score() -> LoadBalancerScore {
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
HttpMethod::GET,
|
HttpMethod::GET,
|
||||||
HttpStatusCode::Success2xx,
|
HttpStatusCode::Success2xx,
|
||||||
SSL::Disabled,
|
|
||||||
)),
|
)),
|
||||||
};
|
};
|
||||||
LoadBalancerScore {
|
LoadBalancerScore {
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ testing = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
reqwest = { version = "0.11", features = [
|
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features = false }
|
||||||
"blocking",
|
|
||||||
"json",
|
|
||||||
"rustls-tls",
|
|
||||||
], default-features = false }
|
|
||||||
russh = "0.45.0"
|
russh = "0.45.0"
|
||||||
rust-ipmi = "0.1.1"
|
rust-ipmi = "0.1.1"
|
||||||
semver = "1.0.23"
|
semver = "1.0.23"
|
||||||
@@ -70,16 +66,10 @@ tar.workspace = true
|
|||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
walkdir = "2.5.0"
|
|
||||||
harmony_inventory_agent = { path = "../harmony_inventory_agent" }
|
harmony_inventory_agent = { path = "../harmony_inventory_agent" }
|
||||||
harmony_secret_derive = { path = "../harmony_secret_derive" }
|
harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" }
|
||||||
harmony_secret = { path = "../harmony_secret" }
|
|
||||||
askama.workspace = true
|
askama.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
inquire.workspace = true
|
|
||||||
brocade = { path = "../brocade" }
|
|
||||||
option-ext = "0.2.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
assertor.workspace = true
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
pub mod secret;
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
use harmony_secret_derive::Secret;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
pub struct OPNSenseFirewallCredentials {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO we need a better way to handle multiple "instances" of the same secret structure.
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
pub struct SshKeyPair {
|
|
||||||
pub private: String,
|
|
||||||
pub public: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
pub struct RedhatSecret {
|
|
||||||
pub pull_secret: String,
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use harmony_inventory_agent::hwinfo::{CPU, MemoryModule, NetworkInterface, StorageDrive};
|
use harmony_inventory_agent::hwinfo::{CPU, MemoryModule, NetworkInterface, StorageDrive};
|
||||||
use harmony_types::net::MacAddress;
|
use harmony_types::net::MacAddress;
|
||||||
@@ -8,7 +10,7 @@ pub type HostGroup = Vec<PhysicalHost>;
|
|||||||
pub type SwitchGroup = Vec<Switch>;
|
pub type SwitchGroup = Vec<Switch>;
|
||||||
pub type FirewallGroup = Vec<PhysicalHost>;
|
pub type FirewallGroup = Vec<PhysicalHost>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct PhysicalHost {
|
pub struct PhysicalHost {
|
||||||
pub id: Id,
|
pub id: Id,
|
||||||
pub category: HostCategory,
|
pub category: HostCategory,
|
||||||
@@ -149,98 +151,6 @@ impl PhysicalHost {
|
|||||||
parts.join(" | ")
|
parts.join(" | ")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parts_list(&self) -> String {
|
|
||||||
let PhysicalHost {
|
|
||||||
id,
|
|
||||||
category,
|
|
||||||
network,
|
|
||||||
storage,
|
|
||||||
labels,
|
|
||||||
memory_modules,
|
|
||||||
cpus,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let mut parts_list = String::new();
|
|
||||||
parts_list.push_str("\n\n=====================");
|
|
||||||
parts_list.push_str(&format!("\nHost ID {id}"));
|
|
||||||
parts_list.push_str("\n=====================");
|
|
||||||
parts_list.push_str("\n\n=====================");
|
|
||||||
parts_list.push_str(&format!("\nCPU count {}", cpus.len()));
|
|
||||||
parts_list.push_str("\n=====================");
|
|
||||||
cpus.iter().for_each(|c| {
|
|
||||||
let CPU {
|
|
||||||
model,
|
|
||||||
vendor,
|
|
||||||
cores,
|
|
||||||
threads,
|
|
||||||
frequency_mhz,
|
|
||||||
} = c;
|
|
||||||
parts_list.push_str(&format!(
|
|
||||||
"\n{vendor} {model}, {cores}/{threads} {}Ghz",
|
|
||||||
*frequency_mhz as f64 / 1000.0
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
parts_list.push_str("\n\n=====================");
|
|
||||||
parts_list.push_str(&format!("\nNetwork Interfaces count {}", network.len()));
|
|
||||||
parts_list.push_str("\n=====================");
|
|
||||||
network.iter().for_each(|nic| {
|
|
||||||
parts_list.push_str(&format!(
|
|
||||||
"\nNic({} {}Gbps mac({}) ipv4({}), ipv6({})",
|
|
||||||
nic.name,
|
|
||||||
nic.speed_mbps.unwrap_or(0) / 1000,
|
|
||||||
nic.mac_address,
|
|
||||||
nic.ipv4_addresses.join(","),
|
|
||||||
nic.ipv6_addresses.join(",")
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
parts_list.push_str("\n\n=====================");
|
|
||||||
parts_list.push_str(&format!("\nStorage drives count {}", storage.len()));
|
|
||||||
parts_list.push_str("\n=====================");
|
|
||||||
storage.iter().for_each(|drive| {
|
|
||||||
let StorageDrive {
|
|
||||||
name,
|
|
||||||
model,
|
|
||||||
serial,
|
|
||||||
size_bytes,
|
|
||||||
logical_block_size: _,
|
|
||||||
physical_block_size: _,
|
|
||||||
rotational: _,
|
|
||||||
wwn: _,
|
|
||||||
interface_type,
|
|
||||||
smart_status,
|
|
||||||
} = drive;
|
|
||||||
parts_list.push_str(&format!(
|
|
||||||
"\n{name} {}Gb {model} {interface_type} smart({smart_status:?}) {serial}",
|
|
||||||
size_bytes / 1000 / 1000 / 1000
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
parts_list.push_str("\n\n=====================");
|
|
||||||
parts_list.push_str(&format!("\nMemory modules count {}", memory_modules.len()));
|
|
||||||
parts_list.push_str("\n=====================");
|
|
||||||
memory_modules.iter().for_each(|mem| {
|
|
||||||
let MemoryModule {
|
|
||||||
size_bytes,
|
|
||||||
speed_mhz,
|
|
||||||
manufacturer,
|
|
||||||
part_number,
|
|
||||||
serial_number,
|
|
||||||
rank,
|
|
||||||
} = mem;
|
|
||||||
parts_list.push_str(&format!(
|
|
||||||
"\n{}Gb, {}Mhz, Manufacturer ({}), Part Number ({})",
|
|
||||||
size_bytes / 1000 / 1000 / 1000,
|
|
||||||
speed_mhz.unwrap_or(0),
|
|
||||||
manufacturer.as_ref().unwrap_or(&String::new()),
|
|
||||||
part_number.as_ref().unwrap_or(&String::new()),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
parts_list
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cluster_mac(&self) -> MacAddress {
|
pub fn cluster_mac(&self) -> MacAddress {
|
||||||
self.network
|
self.network
|
||||||
.first()
|
.first()
|
||||||
@@ -263,10 +173,6 @@ impl PhysicalHost {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mac_address(&self) -> Vec<MacAddress> {
|
|
||||||
self.network.iter().map(|nic| nic.mac_address).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn label(mut self, name: String, value: String) -> Self {
|
pub fn label(mut self, name: String, value: String) -> Self {
|
||||||
self.labels.push(Label { name, value });
|
self.labels.push(Label { name, value });
|
||||||
self
|
self
|
||||||
@@ -315,6 +221,15 @@ impl PhysicalHost {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for PhysicalHost {
|
||||||
|
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(new, Serialize)]
|
#[derive(new, Serialize)]
|
||||||
pub struct ManualManagementInterface;
|
pub struct ManualManagementInterface;
|
||||||
|
|
||||||
@@ -358,13 +273,16 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub enum HostCategory {
|
pub enum HostCategory {
|
||||||
Server,
|
Server,
|
||||||
Firewall,
|
Firewall,
|
||||||
Switch,
|
Switch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use harmony_macros::mac_address;
|
||||||
|
|
||||||
use harmony_types::id::Id;
|
use harmony_types::id::Id;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -373,7 +291,7 @@ pub struct Switch {
|
|||||||
_management_interface: NetworkInterface,
|
_management_interface: NetworkInterface,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, new, Clone, Serialize, Deserialize)]
|
#[derive(Debug, new, Clone, Serialize)]
|
||||||
pub struct Label {
|
pub struct Label {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
|||||||