Compare commits
	
		
			No commits in common. "85d7baac6cdc45fd10cbb057b0e78515b3acaefa" and "32ffc3ef61db74d8c7528cf824d410df9a6968f7" have entirely different histories.
		
	
	
		
			85d7baac6c
			...
			32ffc3ef61
		
	
		
| @ -1,32 +0,0 @@ | ||||
| { | ||||
|   "db_name": "SQLite", | ||||
|   "query": "SELECT id, version_id, data as \"data: Json<PhysicalHost>\" FROM physical_hosts WHERE id = ? ORDER BY version_id DESC LIMIT 1", | ||||
|   "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": "Null" | ||||
|       } | ||||
|     ], | ||||
|     "parameters": { | ||||
|       "Right": 1 | ||||
|     }, | ||||
|     "nullable": [ | ||||
|       false, | ||||
|       false, | ||||
|       false | ||||
|     ] | ||||
|   }, | ||||
|   "hash": "934035c7ca6e064815393e4e049a7934b0a7fac04a4fe4b2a354f0443d630990" | ||||
| } | ||||
| @ -1,12 +0,0 @@ | ||||
| { | ||||
|   "db_name": "SQLite", | ||||
|   "query": "INSERT INTO physical_hosts (id, version_id, data) VALUES (?, ?, ?)", | ||||
|   "describe": { | ||||
|     "columns": [], | ||||
|     "parameters": { | ||||
|       "Right": 3 | ||||
|     }, | ||||
|     "nullable": [] | ||||
|   }, | ||||
|   "hash": "f10f615ee42129ffa293e46f2f893d65a237d31d24b74a29c6a8d8420d255ab8" | ||||
| } | ||||
							
								
								
									
										1075
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1075
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -14,7 +14,7 @@ members = [ | ||||
|   "harmony_composer", | ||||
|   "harmony_inventory_agent", | ||||
|   "harmony_secret_derive", | ||||
|   "harmony_secret", "adr/agent_discovery/mdns", | ||||
|   "harmony_secret", | ||||
| ] | ||||
| 
 | ||||
| [workspace.package] | ||||
| @ -36,7 +36,7 @@ tokio = { version = "1.40", features = [ | ||||
| cidr = { features = ["serde"], version = "0.2" } | ||||
| russh = "0.45" | ||||
| russh-keys = "0.45" | ||||
| rand = "0.9" | ||||
| rand = "0.8" | ||||
| url = "2.5" | ||||
| kube = { version = "1.1.0", features = [ | ||||
|   "config", | ||||
| @ -65,6 +65,3 @@ directories = "6.0.0" | ||||
| thiserror = "2.0.14" | ||||
| serde = { version = "1.0.209", features = ["derive", "rc"] } | ||||
| serde_json = "1.0.127" | ||||
| askama = "0.14" | ||||
| sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite" ] } | ||||
| reqwest = { version = "0.12", features = ["blocking", "stream", "rustls-tls", "http2", "json"], default-features = false } | ||||
|  | ||||
| @ -1,17 +0,0 @@ | ||||
| [package] | ||||
| name = "mdns" | ||||
| edition = "2024" | ||||
| version.workspace = true | ||||
| readme.workspace = true | ||||
| license.workspace = true | ||||
| 
 | ||||
| [dependencies] | ||||
| mdns-sd = "0.14" | ||||
| tokio = { version = "1", features = ["full"] } | ||||
| futures = "0.3" | ||||
| dmidecode = "0.2" # For getting the motherboard ID on the agent | ||||
| log.workspace=true | ||||
| env_logger.workspace=true | ||||
| clap = { version = "4.5.46", features = ["derive"] } | ||||
| get_if_addrs = "0.5.3" | ||||
| local-ip-address = "0.6.5" | ||||
| @ -1,60 +0,0 @@ | ||||
| // harmony-agent/src/main.rs
 | ||||
| 
 | ||||
| use log::info; | ||||
| use mdns_sd::{ServiceDaemon, ServiceInfo}; | ||||
| use std::collections::HashMap; | ||||
| 
 | ||||
| use crate::SERVICE_TYPE; | ||||
| 
 | ||||
| // The service we are advertising.
 | ||||
| const SERVICE_PORT: u16 = 43210; // A port for the service. It needs one, even if unused.
 | ||||
| 
 | ||||
| pub async fn advertise() { | ||||
|     info!("Starting Harmony Agent..."); | ||||
| 
 | ||||
|     // Get a unique ID for this machine.
 | ||||
|     let motherboard_id = "some motherboard id"; | ||||
|     let instance_name = format!("harmony-agent-{}", motherboard_id); | ||||
|     info!("This agent's instance name: {}", instance_name); | ||||
|     info!("Advertising with ID: {}", motherboard_id); | ||||
| 
 | ||||
|     // Create a new mDNS daemon.
 | ||||
|     let mdns = ServiceDaemon::new().expect("Failed to create mDNS daemon"); | ||||
| 
 | ||||
|     // Create a TXT record HashMap to hold our metadata.
 | ||||
|     let mut properties = HashMap::new(); | ||||
|     properties.insert("id".to_string(), motherboard_id.to_string()); | ||||
|     properties.insert("version".to_string(), "1.0".to_string()); | ||||
| 
 | ||||
|     // Create the service information.
 | ||||
|     // The instance name should be unique on the network.
 | ||||
|     let local_ip = local_ip_address::local_ip().unwrap(); | ||||
|     let service_info = ServiceInfo::new( | ||||
|         SERVICE_TYPE, | ||||
|         &instance_name, | ||||
|         "harmony-host.local.", // A hostname for the service
 | ||||
|         local_ip, | ||||
|         // "0.0.0.0",
 | ||||
|         SERVICE_PORT, | ||||
|         Some(properties), | ||||
|     ) | ||||
|     .expect("Failed to create service info"); | ||||
| 
 | ||||
|     // Register our service with the daemon.
 | ||||
|     mdns.register(service_info) | ||||
|         .expect("Failed to register service"); | ||||
| 
 | ||||
|     info!( | ||||
|         "Service '{}' registered and now being advertised.", | ||||
|         instance_name | ||||
|     ); | ||||
|     info!("Agent is running. Press Ctrl+C to exit."); | ||||
| 
 | ||||
|     for iface in get_if_addrs::get_if_addrs().unwrap() { | ||||
|         println!("{:#?}", iface); | ||||
|     } | ||||
| 
 | ||||
|     // Keep the agent running indefinitely.
 | ||||
|     tokio::signal::ctrl_c().await.unwrap(); | ||||
|     info!("Shutting down agent."); | ||||
| } | ||||
| @ -1,110 +0,0 @@ | ||||
| use log::debug; | ||||
| use mdns_sd::{ServiceDaemon, ServiceEvent}; | ||||
| 
 | ||||
| use crate::SERVICE_TYPE; | ||||
| 
 | ||||
| pub async fn discover() { | ||||
|     println!("Starting Harmony Master and browsing for agents..."); | ||||
| 
 | ||||
|     // Create a new mDNS daemon.
 | ||||
|     let mdns = ServiceDaemon::new().expect("Failed to create mDNS daemon"); | ||||
| 
 | ||||
|     // Start browsing for the service type.
 | ||||
|     // The receiver will be a stream of events.
 | ||||
|     let receiver = mdns.browse(SERVICE_TYPE).expect("Failed to browse"); | ||||
| 
 | ||||
|     println!( | ||||
|         "Listening for mDNS events for '{}'. Press Ctrl+C to exit.", | ||||
|         SERVICE_TYPE | ||||
|     ); | ||||
| 
 | ||||
|     std::thread::spawn(move || { | ||||
|         while let Ok(event) = receiver.recv() { | ||||
|             match event { | ||||
|                 ServiceEvent::ServiceData(resolved) => { | ||||
|                     println!("Resolved a new service: {}", resolved.fullname); | ||||
|                 } | ||||
|                 other_event => { | ||||
|                     println!("Received other event: {:?}", &other_event); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // Gracefully shutdown the daemon.
 | ||||
|     std::thread::sleep(std::time::Duration::from_secs(1000000)); | ||||
|     mdns.shutdown().unwrap(); | ||||
| 
 | ||||
|     // Process events as they come in.
 | ||||
|     // while let Ok(event) = receiver.recv_async().await {
 | ||||
|     //     debug!("Received event {event:?}");
 | ||||
|     //     // match event {
 | ||||
|     //     //     ServiceEvent::ServiceFound(svc_type, fullname) => {
 | ||||
|     //     //         println!("\n--- Agent Discovered ---");
 | ||||
|     //     //         println!("  Service Name: {}", fullname());
 | ||||
|     //     //         // You can now resolve this service to get its IP, port, and TXT records
 | ||||
|     //     //         // The resolve operation is a separate network call.
 | ||||
|     //     //         let receiver = mdns.browse(info.get_fullname()).unwrap();
 | ||||
|     //     //         if let Ok(resolve_event) = receiver.recv_timeout(Duration::from_secs(2)) {
 | ||||
|     //     //              if let ServiceEvent::ServiceResolved(info) = resolve_event {
 | ||||
|     //     //                 let ip = info.get_addresses().iter().next().unwrap();
 | ||||
|     //     //                 let port = info.get_port();
 | ||||
|     //     //                 let motherboard_id = info.get_property("id").map_or("N/A", |v| v.val_str());
 | ||||
|     //     //
 | ||||
|     //     //                 println!("  IP: {}:{}", ip, port);
 | ||||
|     //     //                 println!("  Motherboard ID: {}", motherboard_id);
 | ||||
|     //     //                 println!("------------------------");
 | ||||
|     //     //
 | ||||
|     //     //                 // TODO: Add this agent to your central list of discovered hosts.
 | ||||
|     //     //              }
 | ||||
|     //     //         } else {
 | ||||
|     //     //             println!("Could not resolve service '{}' in time.", info.get_fullname());
 | ||||
|     //     //         }
 | ||||
|     //     //     }
 | ||||
|     //     //     ServiceEvent::ServiceRemoved(info) => {
 | ||||
|     //     //         println!("\n--- Agent Removed ---");
 | ||||
|     //     //         println!("  Service Name: {}", info.get_fullname());
 | ||||
|     //     //         println!("---------------------");
 | ||||
|     //     //         // TODO: Remove this agent from your list.
 | ||||
|     //     //     }
 | ||||
|     //     //     _ => {
 | ||||
|     //     //         // We don't care about other event types for this example
 | ||||
|     //     //     }
 | ||||
|     //     // }
 | ||||
|     // }
 | ||||
| } | ||||
| 
 | ||||
| async fn discover_example() { | ||||
|     use mdns_sd::{ServiceDaemon, ServiceEvent}; | ||||
| 
 | ||||
|     // Create a daemon
 | ||||
|     let mdns = ServiceDaemon::new().expect("Failed to create daemon"); | ||||
| 
 | ||||
|     // Use recently added `ServiceEvent::ServiceData`.
 | ||||
|     mdns.use_service_data(true) | ||||
|         .expect("Failed to use ServiceData"); | ||||
| 
 | ||||
|     // Browse for a service type.
 | ||||
|     let service_type = "_mdns-sd-my-test._udp.local."; | ||||
|     let receiver = mdns.browse(service_type).expect("Failed to browse"); | ||||
| 
 | ||||
|     // Receive the browse events in sync or async. Here is
 | ||||
|     // an example of using a thread. Users can call `receiver.recv_async().await`
 | ||||
|     // if running in async environment.
 | ||||
|     std::thread::spawn(move || { | ||||
|         while let Ok(event) = receiver.recv() { | ||||
|             match event { | ||||
|                 ServiceEvent::ServiceData(resolved) => { | ||||
|                     println!("Resolved a new service: {}", resolved.fullname); | ||||
|                 } | ||||
|                 other_event => { | ||||
|                     println!("Received other event: {:?}", &other_event); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // Gracefully shutdown the daemon.
 | ||||
|     std::thread::sleep(std::time::Duration::from_secs(1)); | ||||
|     mdns.shutdown().unwrap(); | ||||
| } | ||||
| @ -1,31 +0,0 @@ | ||||
| use clap::{Parser, ValueEnum}; | ||||
| 
 | ||||
| mod advertise; | ||||
| mod discover; | ||||
| 
 | ||||
| #[derive(Parser, Debug)] | ||||
| #[command(version, about, long_about = None)] | ||||
| struct Args { | ||||
|     #[arg(value_enum)] | ||||
|     profile: Profiles, | ||||
| } | ||||
| 
 | ||||
| #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] | ||||
| enum Profiles { | ||||
|     Advertise, | ||||
|     Discover, | ||||
| } | ||||
| 
 | ||||
| // The service type we are looking for.
 | ||||
| const SERVICE_TYPE: &str = "_harmony._tcp.local."; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     env_logger::init(); | ||||
|     let args = Args::parse(); | ||||
| 
 | ||||
|     match args.profile { | ||||
|         Profiles::Advertise => advertise::advertise().await, | ||||
|         Profiles::Discover => discover::discover().await, | ||||
|     } | ||||
| } | ||||
							
								
								
									
										1
									
								
								check.sh
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								check.sh
									
									
									
									
									
								
							| @ -1,7 +1,6 @@ | ||||
| #!/bin/sh | ||||
| set -e | ||||
| 
 | ||||
| rustc --version | ||||
| cargo check --all-targets --all-features --keep-going | ||||
| cargo fmt --check | ||||
| cargo clippy | ||||
|  | ||||
| @ -1,8 +0,0 @@ | ||||
| Here lies all the data files required for an OKD cluster PXE boot setup. | ||||
| 
 | ||||
| This inclues ISO files, binary boot files, ipxe, etc. | ||||
| 
 | ||||
| TODO as of august 2025 : | ||||
| 
 | ||||
| - `harmony_inventory_agent` should be downloaded from official releases, this embedded version is practical for now though | ||||
| - The cluster ssh key should be generated and handled by harmony with the private key saved in a secret store | ||||
							
								
								
									
										9
									
								
								data/pxe/okd/http_files/.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								data/pxe/okd/http_files/.gitattributes
									
									
									
									
										vendored
									
									
								
							| @ -1,9 +0,0 @@ | ||||
| harmony_inventory_agent filter=lfs diff=lfs merge=lfs -text | ||||
| os filter=lfs diff=lfs merge=lfs -text | ||||
| os/centos-stream-9 filter=lfs diff=lfs merge=lfs -text | ||||
| os/centos-stream-9/images filter=lfs diff=lfs merge=lfs -text | ||||
| os/centos-stream-9/initrd.img filter=lfs diff=lfs merge=lfs -text | ||||
| os/centos-stream-9/vmlinuz filter=lfs diff=lfs merge=lfs -text | ||||
| os/centos-stream-9/images/efiboot.img filter=lfs diff=lfs merge=lfs -text | ||||
| os/centos-stream-9/images/install.img filter=lfs diff=lfs merge=lfs -text | ||||
| os/centos-stream-9/images/pxeboot filter=lfs diff=lfs merge=lfs -text | ||||
| @ -1 +0,0 @@ | ||||
| ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBx6bDylvC68cVpjKfEFtLQJ/dOFi6PVS2vsIOqPDJIc jeangab@liliane2 | ||||
							
								
								
									
										
											BIN
										
									
								
								data/pxe/okd/http_files/harmony_inventory_agent
									 (Stored with Git LFS)
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								data/pxe/okd/http_files/harmony_inventory_agent
									 (Stored with Git LFS)
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								data/pxe/okd/http_files/os/centos-stream-9/images/efiboot.img
									 (Stored with Git LFS)
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								data/pxe/okd/http_files/os/centos-stream-9/images/efiboot.img
									 (Stored with Git LFS)
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								data/pxe/okd/http_files/os/centos-stream-9/images/install.img
									 (Stored with Git LFS)
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								data/pxe/okd/http_files/os/centos-stream-9/images/install.img
									 (Stored with Git LFS)
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								data/pxe/okd/http_files/os/centos-stream-9/initrd.img
									 (Stored with Git LFS)
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								data/pxe/okd/http_files/os/centos-stream-9/initrd.img
									 (Stored with Git LFS)
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								data/pxe/okd/http_files/os/centos-stream-9/vmlinuz
									 (Stored with Git LFS)
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								data/pxe/okd/http_files/os/centos-stream-9/vmlinuz
									 (Stored with Git LFS)
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @ -1,108 +0,0 @@ | ||||
| # OPNsense PXE Lab Environment | ||||
| 
 | ||||
| This project contains a script to automatically set up a virtual lab environment for testing PXE boot services managed by an OPNsense firewall. | ||||
| 
 | ||||
| ## Overview | ||||
| 
 | ||||
| The `pxe_vm_lab_setup.sh` script will create the following resources using libvirt/KVM: | ||||
| 
 | ||||
| 1.  **A Virtual Network**: An isolated network named `harmonylan` (`virbr1`) for the lab. | ||||
| 2.  **Two Virtual Machines**: | ||||
|     *   `opnsense-pxe`: A firewall VM that will act as the gateway and PXE server. | ||||
|     *   `pxe-node-1`: A client VM configured to boot from the network. | ||||
| 
 | ||||
| ## Prerequisites | ||||
| 
 | ||||
| Ensure you have the following software installed on your Arch Linux host: | ||||
| 
 | ||||
| *   `libvirt` | ||||
| *   `qemu` | ||||
| *   `virt-install` (from the `virt-install` package) | ||||
| *   `curl` | ||||
| *   `bzip2` | ||||
| 
 | ||||
| ## Usage | ||||
| 
 | ||||
| ### 1. Create the Environment | ||||
| 
 | ||||
| Run the `up` command to download the necessary images and create the network and VMs. | ||||
| 
 | ||||
| ```bash | ||||
| sudo ./pxe_vm_lab_setup.sh up | ||||
| ``` | ||||
| 
 | ||||
| ### 2. Install and Configure OPNsense | ||||
| 
 | ||||
| The OPNsense VM is created but the OS needs to be installed manually via the console. | ||||
| 
 | ||||
| 1.  **Connect to the VM console**: | ||||
|     ```bash | ||||
|     sudo virsh console opnsense-pxe | ||||
|     ``` | ||||
| 
 | ||||
| 2.  **Log in as the installer**: | ||||
|     *   Username: `installer` | ||||
|     *   Password: `opnsense` | ||||
| 
 | ||||
| 3.  **Follow the on-screen installation wizard**. When prompted to assign network interfaces (`WAN` and `LAN`): | ||||
|     *   Find the MAC address for the `harmonylan` interface by running this command in another terminal: | ||||
|         ```bash | ||||
|         virsh domiflist opnsense-pxe | ||||
|         # Example output: | ||||
|         # Interface   Type      Source       Model    MAC | ||||
|         # --------------------------------------------------------- | ||||
|         # vnet18      network   default      virtio   52:54:00:b5:c4:6d | ||||
|         # vnet19      network   harmonylan   virtio   52:54:00:21:f9:ba | ||||
|         ``` | ||||
|     *   Assign the interface connected to `harmonylan` (e.g., `vtnet1` with MAC `52:54:00:21:f9:ba`) as your **LAN**. | ||||
|     *   Assign the other interface as your **WAN**. | ||||
| 
 | ||||
| 4.  After the installation is complete, **shut down** the VM from the console menu. | ||||
| 
 | ||||
| 5.  **Detach the installation media** by editing the VM's configuration: | ||||
|     ```bash | ||||
|     sudo virsh edit opnsense-pxe | ||||
|     ``` | ||||
|     Find and **delete** the entire `<disk>` block corresponding to the `.img` file (the one with `<target ... bus='usb'/>`). | ||||
| 
 | ||||
| 6.  **Start the VM** to boot into the newly installed system: | ||||
|     ```bash | ||||
|     sudo virsh start opnsense-pxe | ||||
|     ``` | ||||
| 
 | ||||
| ### 3. Connect to OPNsense from Your Host | ||||
| 
 | ||||
| To configure OPNsense, you need to connect your host to the `harmonylan` network. | ||||
| 
 | ||||
| 1.  By default, OPNsense configures its LAN interface with the IP `192.168.1.1`. | ||||
| 2.  Assign a compatible IP address to your host's `virbr1` bridge interface: | ||||
|     ```bash | ||||
|     sudo ip addr add 192.168.1.5/24 dev virbr1 | ||||
|     ``` | ||||
| 3.  You can now access the OPNsense VM from your host: | ||||
|     *   **SSH**: `ssh root@192.168.1.1` (password: `opnsense`) | ||||
|     *   **Web UI**: `https://192.168.1.1` | ||||
| 
 | ||||
| ### 4. Configure PXE Services with Harmony | ||||
| 
 | ||||
| With connectivity established, you can now use Harmony to configure the OPNsense firewall for PXE booting. Point your Harmony OPNsense scores to the firewall using these details: | ||||
| 
 | ||||
| *   **Hostname/IP**: `192.168.1.1` | ||||
| *   **Credentials**: `root` / `opnsense` | ||||
| 
 | ||||
| ### 5. Boot the PXE Client | ||||
| 
 | ||||
| Once your Harmony configuration has been applied and OPNsense is serving DHCP/TFTP, start the client VM. It will automatically attempt to boot from the network. | ||||
| 
 | ||||
| ```bash | ||||
| sudo virsh start pxe-node-1 | ||||
| sudo virsh console pxe-node-1 | ||||
| ``` | ||||
| 
 | ||||
| ## Cleanup | ||||
| 
 | ||||
| To destroy all VMs and networks created by the script, run the `clean` command: | ||||
| 
 | ||||
| ```bash | ||||
| sudo ./pxe_vm_lab_setup.sh clean | ||||
| ``` | ||||
| @ -1,191 +0,0 @@ | ||||
| #!/usr/bin/env bash | ||||
| set -euo pipefail | ||||
| 
 | ||||
| # --- Configuration --- | ||||
| LAB_DIR="/var/lib/harmony_pxe_test" | ||||
| IMG_DIR="${LAB_DIR}/images" | ||||
| STATE_DIR="${LAB_DIR}/state" | ||||
| VM_OPN="opnsense-pxe" | ||||
| VM_PXE="pxe-node-1" | ||||
| NET_HARMONYLAN="harmonylan" | ||||
| 
 | ||||
| # Network settings for the isolated LAN | ||||
| VLAN_CIDR="192.168.150.0/24" | ||||
| VLAN_GW="192.168.150.1" | ||||
| VLAN_MASK="255.255.255.0" | ||||
| 
 | ||||
| # VM Specifications | ||||
| RAM_OPN="2048" | ||||
| VCPUS_OPN="2" | ||||
| DISK_OPN_GB="10" | ||||
| OS_VARIANT_OPN="freebsd14.0" # Updated to a more recent FreeBSD variant | ||||
| 
 | ||||
| RAM_PXE="4096" | ||||
| VCPUS_PXE="2" | ||||
| DISK_PXE_GB="40" | ||||
| OS_VARIANT_LINUX="centos-stream9" | ||||
| 
 | ||||
| OPN_IMG_URL="https://mirror.ams1.nl.leaseweb.net/opnsense/releases/25.7/OPNsense-25.7-serial-amd64.img.bz2" | ||||
| OPN_IMG_PATH="${IMG_DIR}/OPNsense-25.7-serial-amd64.img" | ||||
| CENTOS_ISO_URL="https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/images/boot.iso" | ||||
| CENTOS_ISO_PATH="${IMG_DIR}/CentOS-Stream-9-latest-boot.iso" | ||||
| 
 | ||||
| CONNECT_URI="qemu:///system" | ||||
| 
 | ||||
| download_if_missing() { | ||||
|   local url="$1" | ||||
|   local dest="$2" | ||||
|   if [[ ! -f "$dest" ]]; then | ||||
|     echo "Downloading $url to $dest" | ||||
|     mkdir -p "$(dirname "$dest")" | ||||
|     local tmp | ||||
|     tmp="$(mktemp)" | ||||
|     curl -L --progress-bar "$url" -o "$tmp" | ||||
|     case "$url" in | ||||
|       *.bz2) bunzip2 -c "$tmp" > "$dest" && rm -f "$tmp" ;; | ||||
|       *) mv "$tmp" "$dest" ;; | ||||
|     esac | ||||
|   else | ||||
|     echo "Already present: $dest" | ||||
|   fi | ||||
| } | ||||
| 
 | ||||
| # Ensures a libvirt network is defined and active | ||||
| ensure_network() { | ||||
|   local net_name="$1" | ||||
|   local net_xml_path="$2" | ||||
|   if virsh --connect "${CONNECT_URI}" net-info "${net_name}" >/dev/null 2>&1; then | ||||
|     echo "Network ${net_name} already exists." | ||||
|   else | ||||
|     echo "Defining network ${net_name} from ${net_xml_path}" | ||||
|     virsh --connect "${CONNECT_URI}" net-define "${net_xml_path}" | ||||
|   fi | ||||
| 
 | ||||
|   if ! virsh --connect "${CONNECT_URI}" net-info "${net_name}" | grep "Active: *yes"; then | ||||
|     echo "Starting network ${net_name}..." | ||||
|     virsh --connect "${CONNECT_URI}" net-start "${net_name}" | ||||
|     virsh --connect "${CONNECT_URI}" net-autostart "${net_name}" | ||||
|   fi | ||||
| } | ||||
| 
 | ||||
| # Destroys a VM completely | ||||
| destroy_vm() { | ||||
|   local vm_name="$1" | ||||
|   if virsh --connect "${CONNECT_URI}" dominfo "$vm_name" >/dev/null 2>&1; then | ||||
|     echo "Destroying and undefining VM: ${vm_name}" | ||||
|     virsh --connect "${CONNECT_URI}" destroy "$vm_name" || true | ||||
|     virsh --connect "${CONNECT_URI}" undefine "$vm_name" --nvram | ||||
|   fi | ||||
| } | ||||
| 
 | ||||
| # Destroys a libvirt network | ||||
| destroy_network() { | ||||
|   local net_name="$1" | ||||
|   if virsh --connect "${CONNECT_URI}" net-info "$net_name" >/dev/null 2>&1; then | ||||
|     echo "Destroying and undefining network: ${net_name}" | ||||
|     virsh --connect "${CONNECT_URI}" net-destroy "$net_name" || true | ||||
|     virsh --connect "${CONNECT_URI}" net-undefine "$net_name" | ||||
|   fi | ||||
| } | ||||
| 
 | ||||
| # --- Main Logic --- | ||||
| create_lab_environment() { | ||||
|   # Create network definition files | ||||
|   cat > "${STATE_DIR}/default.xml" <<EOF | ||||
| <network> | ||||
|   <name>default</name> | ||||
|   <forward mode='nat'/> | ||||
|   <bridge name='virbr0' stp='on' delay='0'/> | ||||
|   <ip address='192.168.122.1' netmask='255.255.255.0'> | ||||
|     <dhcp> | ||||
|       <range start='192.168.122.100' end='192.168.122.200'/> | ||||
|     </dhcp> | ||||
|   </ip> | ||||
| </network> | ||||
| EOF | ||||
| 
 | ||||
|   cat > "${STATE_DIR}/${NET_HARMONYLAN}.xml" <<EOF | ||||
| <network> | ||||
|   <name>${NET_HARMONYLAN}</name> | ||||
|   <bridge name='virbr1' stp='on' delay='0'/> | ||||
| </network> | ||||
| EOF | ||||
| 
 | ||||
|   # Ensure both networks exist and are active | ||||
|   ensure_network "default" "${STATE_DIR}/default.xml" | ||||
|   ensure_network "${NET_HARMONYLAN}" "${STATE_DIR}/${NET_HARMONYLAN}.xml" | ||||
| 
 | ||||
|   # --- Create OPNsense VM (MODIFIED SECTION) --- | ||||
|   local disk_opn="${IMG_DIR}/${VM_OPN}.qcow2" | ||||
|   if [[ ! -f "$disk_opn" ]]; then | ||||
|     qemu-img create -f qcow2 "$disk_opn" "${DISK_OPN_GB}G" | ||||
|   fi | ||||
| 
 | ||||
|   echo "Creating OPNsense VM using serial image..." | ||||
|   virt-install \ | ||||
|     --connect "${CONNECT_URI}" \ | ||||
|     --name "${VM_OPN}" \ | ||||
|     --ram "${RAM_OPN}" \ | ||||
|     --vcpus "${VCPUS_OPN}" \ | ||||
|     --cpu host-passthrough \ | ||||
|     --os-variant "${OS_VARIANT_OPN}" \ | ||||
|     --graphics none \ | ||||
|     --noautoconsole \ | ||||
|     --disk path="${disk_opn}",device=disk,bus=virtio,boot.order=1 \ | ||||
|     --disk path="${OPN_IMG_PATH}",device=disk,bus=usb,readonly=on,boot.order=2 \ | ||||
|     --network network=default,model=virtio \ | ||||
|     --network network="${NET_HARMONYLAN}",model=virtio \ | ||||
|     --boot uefi,menu=on | ||||
| 
 | ||||
|   echo "OPNsense VM created. Connect with: sudo virsh console ${VM_OPN}" | ||||
|   echo "The VM will boot from the serial installation image." | ||||
|   echo "Login with user 'installer' and password 'opnsense' to start the installation." | ||||
|   echo "Install onto the VirtIO disk (vtbd0)." | ||||
|   echo "After installation, shutdown the VM, then run 'sudo virsh edit ${VM_OPN}' and remove the USB disk block to boot from the installed system." | ||||
| 
 | ||||
|   # --- Create PXE Client VM --- | ||||
|   local disk_pxe="${IMG_DIR}/${VM_PXE}.qcow2" | ||||
|   if [[ ! -f "$disk_pxe" ]]; then | ||||
|     qemu-img create -f qcow2 "$disk_pxe" "${DISK_PXE_GB}G" | ||||
|   fi | ||||
| 
 | ||||
|   echo "Creating PXE client VM..." | ||||
|   virt-install \ | ||||
|     --connect "${CONNECT_URI}" \ | ||||
|     --name "${VM_PXE}" \ | ||||
|     --ram "${RAM_PXE}" \ | ||||
|     --vcpus "${VCPUS_PXE}" \ | ||||
|     --cpu host-passthrough \ | ||||
|     --os-variant "${OS_VARIANT_LINUX}" \ | ||||
|     --graphics none \ | ||||
|     --noautoconsole \ | ||||
|     --disk path="${disk_pxe}",format=qcow2,bus=virtio \ | ||||
|     --network network="${NET_HARMONYLAN}",model=virtio \ | ||||
|     --pxe \ | ||||
|     --boot uefi,menu=on | ||||
| 
 | ||||
|   echo "PXE VM created. It will attempt to netboot on ${NET_HARMONYLAN}." | ||||
| } | ||||
| 
 | ||||
| # --- Script Entrypoint --- | ||||
| case "${1:-}" in | ||||
|   up) | ||||
|     mkdir -p "${IMG_DIR}" "${STATE_DIR}" | ||||
|     download_if_missing "$OPN_IMG_URL" "$OPN_IMG_PATH" | ||||
|     download_if_missing "$CENTOS_ISO_URL" "$CENTOS_ISO_PATH" | ||||
|     create_lab_environment | ||||
|     echo "Lab setup complete. Use 'sudo virsh list --all' to see VMs." | ||||
|     ;; | ||||
|   clean) | ||||
|     destroy_vm "${VM_PXE}" | ||||
|     destroy_vm "${VM_OPN}" | ||||
|     destroy_network "${NET_HARMONYLAN}" | ||||
|     # Optionally destroy the default network if you want a full reset | ||||
|     # destroy_network "default" | ||||
|     echo "Cleanup complete." | ||||
|     ;; | ||||
|   *) | ||||
|     echo "Usage: sudo $0 {up|clean}" | ||||
|     exit 1 | ||||
|     ;; | ||||
| esac | ||||
| @ -7,9 +7,8 @@ license.workspace = true | ||||
| 
 | ||||
| [dependencies] | ||||
| env_logger.workspace = true | ||||
| harmony = { path = "../../harmony" } | ||||
| harmony_cli = { path = "../../harmony_cli" } | ||||
| harmony_types = { path = "../../harmony_types" } | ||||
| harmony = { version = "0.1.0", path = "../../harmony" } | ||||
| harmony_cli = { version = "0.1.0", path = "../../harmony_cli" } | ||||
| logging = "0.1.0" | ||||
| tokio.workspace = true | ||||
| url.workspace = true | ||||
|  | ||||
| @ -1,16 +1,15 @@ | ||||
| use std::{path::PathBuf, str::FromStr, sync::Arc}; | ||||
| 
 | ||||
| use harmony::{ | ||||
|     data::Id, | ||||
|     inventory::Inventory, | ||||
|     modules::{ | ||||
|         application::{ApplicationScore, RustWebFramework, RustWebapp, features::Monitoring}, | ||||
|         monitoring::alert_channel::webhook_receiver::WebhookReceiver, | ||||
|         tenant::TenantScore, | ||||
|     }, | ||||
|     topology::{K8sAnywhereTopology, tenant::TenantConfig}, | ||||
|     topology::{K8sAnywhereTopology, Url, tenant::TenantConfig}, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| use harmony_types::net::Url; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|  | ||||
| @ -1,9 +1,6 @@ | ||||
| use harmony::{ | ||||
|     inventory::Inventory, | ||||
|     modules::{ | ||||
|         dummy::{ErrorScore, PanicScore, SuccessScore}, | ||||
|         inventory::DiscoverInventoryAgentScore, | ||||
|     }, | ||||
|     modules::dummy::{ErrorScore, PanicScore, SuccessScore}, | ||||
|     topology::LocalhostTopology, | ||||
| }; | ||||
| 
 | ||||
| @ -16,9 +13,6 @@ async fn main() { | ||||
|             Box::new(SuccessScore {}), | ||||
|             Box::new(ErrorScore {}), | ||||
|             Box::new(PanicScore {}), | ||||
|             Box::new(DiscoverInventoryAgentScore { | ||||
|                 discovery_timeout: Some(10), | ||||
|             }), | ||||
|         ], | ||||
|         None, | ||||
|     ) | ||||
|  | ||||
| @ -2,9 +2,8 @@ use harmony::{ | ||||
|     data::Version, | ||||
|     inventory::Inventory, | ||||
|     modules::lamp::{LAMPConfig, LAMPScore}, | ||||
|     topology::K8sAnywhereTopology, | ||||
|     topology::{K8sAnywhereTopology, Url}, | ||||
| }; | ||||
| use harmony_types::net::Url; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|  | ||||
| @ -6,9 +6,8 @@ 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" } | ||||
| harmony = { version = "0.1.0", path = "../../harmony" } | ||||
| harmony_cli = { version = "0.1.0", path = "../../harmony_cli" } | ||||
| harmony_macros = { version = "0.1.0", path = "../../harmony_macros" } | ||||
| tokio.workspace = true | ||||
| url.workspace = true | ||||
|  | ||||
| @ -22,9 +22,8 @@ use harmony::{ | ||||
|             k8s::pvc::high_pvc_fill_rate_over_two_days, | ||||
|         }, | ||||
|     }, | ||||
|     topology::K8sAnywhereTopology, | ||||
|     topology::{K8sAnywhereTopology, Url}, | ||||
| }; | ||||
| use harmony_types::net::Url; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|  | ||||
| @ -7,8 +7,7 @@ license.workspace = true | ||||
| 
 | ||||
| [dependencies] | ||||
| cidr.workspace = true | ||||
| harmony = { path = "../../harmony" } | ||||
| harmony_cli = { path = "../../harmony_cli" } | ||||
| harmony_types = { path = "../../harmony_types" } | ||||
| harmony = { version = "0.1.0", path = "../../harmony" } | ||||
| harmony_cli = { version = "0.1.0", path = "../../harmony_cli" } | ||||
| tokio.workspace = true | ||||
| url.workspace = true | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| use std::{collections::HashMap, str::FromStr}; | ||||
| 
 | ||||
| use harmony::{ | ||||
|     data::Id, | ||||
|     inventory::Inventory, | ||||
|     modules::{ | ||||
|         monitoring::{ | ||||
| @ -18,12 +19,10 @@ use harmony::{ | ||||
|         tenant::TenantScore, | ||||
|     }, | ||||
|     topology::{ | ||||
|         K8sAnywhereTopology, | ||||
|         K8sAnywhereTopology, Url, | ||||
|         tenant::{ResourceLimits, TenantConfig, TenantNetworkPolicy}, | ||||
|     }, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| use harmony_types::net::Url; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|  | ||||
| @ -18,10 +18,9 @@ use harmony::{ | ||||
|         }, | ||||
|         tftp::TftpScore, | ||||
|     }, | ||||
|     topology::{LogicalHost, UnmanagedRouter}, | ||||
|     topology::{LogicalHost, UnmanagedRouter, Url}, | ||||
| }; | ||||
| use harmony_macros::{ip, mac_address}; | ||||
| use harmony_types::net::Url; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
| @ -87,7 +86,8 @@ async fn main() { | ||||
|     let inventory = Inventory { | ||||
|         location: Location::new("I am mobile".to_string(), "earth".to_string()), | ||||
|         switch: SwitchGroup::from([]), | ||||
|         firewall_mgmt: Box::new(OPNSenseManagementInterface::new()), | ||||
|         firewall: FirewallGroup::from([PhysicalHost::empty(HostCategory::Firewall) | ||||
|             .management(Arc::new(OPNSenseManagementInterface::new()))]), | ||||
|         storage_host: vec![], | ||||
|         worker_host: vec![ | ||||
|             PhysicalHost::empty(HostCategory::Server) | ||||
| @ -125,12 +125,9 @@ async fn main() { | ||||
|         harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology); | ||||
| 
 | ||||
|     let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); | ||||
|     let http_score = StaticFilesHttpScore { | ||||
|         folder_to_serve: Some(Url::LocalFolder( | ||||
|             "./data/watchguard/pxe-http-files".to_string(), | ||||
|         )), | ||||
|         files: vec![], | ||||
|     }; | ||||
|     let http_score = StaticFilesHttpScore::new(Url::LocalFolder( | ||||
|         "./data/watchguard/pxe-http-files".to_string(), | ||||
|     )); | ||||
|     let ipxe_score = IpxeScore::new(); | ||||
| 
 | ||||
|     harmony_tui::run( | ||||
|  | ||||
| @ -1,21 +0,0 @@ | ||||
| [package] | ||||
| name = "example-pxe" | ||||
| 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 | ||||
| @ -1,24 +0,0 @@ | ||||
| mod topology; | ||||
| 
 | ||||
| use crate::topology::{get_inventory, get_topology}; | ||||
| use harmony::modules::okd::ipxe::OkdIpxeScore; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     let inventory = get_inventory(); | ||||
|     let topology = get_topology().await; | ||||
| 
 | ||||
|     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 ipxe_score = OkdIpxeScore { | ||||
|         kickstart_filename, | ||||
|         harmony_inventory_agent, | ||||
|         cluster_pubkey_filename, | ||||
|     }; | ||||
| 
 | ||||
|     harmony_cli::run(inventory, topology, vec![Box::new(ipxe_score)], None) | ||||
|         .await | ||||
|         .unwrap(); | ||||
| } | ||||
| @ -1,77 +0,0 @@ | ||||
| use cidr::Ipv4Cidr; | ||||
| use harmony::{ | ||||
|     hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup}, | ||||
|     infra::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}; | ||||
| 
 | ||||
| #[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 config = SecretManager::get::<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 { | ||||
|         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!("10.100.8.20"), | ||||
|             name: "cp0".to_string(), | ||||
|         }], | ||||
|         bootstrap_host: LogicalHost { | ||||
|             ip: ip!("10.100.8.20"), | ||||
|             name: "cp0".to_string(), | ||||
|         }, | ||||
|         workers: vec![], | ||||
|         switch: vec![], | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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![], | ||||
|     } | ||||
| } | ||||
| @ -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 | ||||
| @ -15,10 +15,9 @@ use harmony::{ | ||||
|         opnsense::OPNsenseShellCommandScore, | ||||
|         tftp::TftpScore, | ||||
|     }, | ||||
|     topology::{LogicalHost, UnmanagedRouter}, | ||||
|     topology::{LogicalHost, UnmanagedRouter, Url}, | ||||
| }; | ||||
| use harmony_macros::{ip, mac_address}; | ||||
| use harmony_types::net::Url; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
| @ -63,7 +62,8 @@ async fn main() { | ||||
|             "wk".to_string(), | ||||
|         ), | ||||
|         switch: SwitchGroup::from([]), | ||||
|         firewall_mgmt: Box::new(OPNSenseManagementInterface::new()), | ||||
|         firewall: FirewallGroup::from([PhysicalHost::empty(HostCategory::Firewall) | ||||
|             .management(Arc::new(OPNSenseManagementInterface::new()))]), | ||||
|         storage_host: vec![], | ||||
|         worker_host: vec![], | ||||
|         control_plane_host: vec![ | ||||
| @ -80,12 +80,9 @@ async fn main() { | ||||
|     let load_balancer_score = OKDLoadBalancerScore::new(&topology); | ||||
| 
 | ||||
|     let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); | ||||
|     let http_score = StaticFilesHttpScore { | ||||
|         folder_to_serve: Some(Url::LocalFolder( | ||||
|             "./data/watchguard/pxe-http-files".to_string(), | ||||
|         )), | ||||
|         files: vec![], | ||||
|     }; | ||||
|     let http_score = StaticFilesHttpScore::new(Url::LocalFolder( | ||||
|         "./data/watchguard/pxe-http-files".to_string(), | ||||
|     )); | ||||
| 
 | ||||
|     harmony_tui::run( | ||||
|         inventory, | ||||
|  | ||||
| @ -11,9 +11,8 @@ use harmony::{ | ||||
|             discord_alert_channel::DiscordWebhook, webhook_receiver::WebhookReceiver, | ||||
|         }, | ||||
|     }, | ||||
|     topology::K8sAnywhereTopology, | ||||
|     topology::{K8sAnywhereTopology, Url}, | ||||
| }; | ||||
| use harmony_types::net::Url; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| use std::str::FromStr; | ||||
| 
 | ||||
| use harmony::{ | ||||
|     data::Id, | ||||
|     inventory::Inventory, | ||||
|     modules::tenant::TenantScore, | ||||
|     topology::{K8sAnywhereTopology, tenant::TenantConfig}, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|  | ||||
| @ -9,8 +9,10 @@ license.workspace = true | ||||
| testing = [] | ||||
| 
 | ||||
| [dependencies] | ||||
| rand = "0.9" | ||||
| hex = "0.4" | ||||
| reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features = false } | ||||
| libredfish = "0.1.1" | ||||
| reqwest = { version = "0.11", features = ["blocking", "json"] } | ||||
| russh = "0.45.0" | ||||
| rust-ipmi = "0.1.1" | ||||
| semver = "1.0.23" | ||||
| @ -64,14 +66,9 @@ kube-derive = "1.1.0" | ||||
| bollard.workspace = true | ||||
| tar.workspace = true | ||||
| base64.workspace = true | ||||
| thiserror.workspace = true | ||||
| once_cell = "1.21.3" | ||||
| harmony-secret-derive = { version = "0.1.0", path = "../harmony_secret_derive" } | ||||
| walkdir = "2.5.0" | ||||
| harmony_inventory_agent = { path = "../harmony_inventory_agent" } | ||||
| harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" } | ||||
| askama.workspace = true | ||||
| sqlx.workspace = true | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| pretty_assertions.workspace = true | ||||
|  | ||||
| @ -12,12 +12,4 @@ lazy_static! { | ||||
|         std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string()); | ||||
|     pub static ref DRY_RUN: bool = | ||||
|         std::env::var("HARMONY_DRY_RUN").is_ok_and(|value| value.parse().unwrap_or(false)); | ||||
|     pub static ref DEFAULT_DATABASE_URL: String = "sqlite://harmony.sqlite".to_string(); | ||||
|     pub static ref DATABASE_URL: String = std::env::var("HARMONY_DATABASE_URL") | ||||
|         .map(|value| if value.is_empty() { | ||||
|             (*DEFAULT_DATABASE_URL).clone() | ||||
|         } else { | ||||
|             value | ||||
|         }) | ||||
|         .unwrap_or((*DEFAULT_DATABASE_URL).clone()); | ||||
| } | ||||
|  | ||||
| @ -1,22 +0,0 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct FileContent { | ||||
|     pub path: FilePath, | ||||
|     pub content: String, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub enum FilePath { | ||||
|     Relative(String), | ||||
|     Absolute(String), | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for FilePath { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         match self { | ||||
|             FilePath::Relative(path) => f.write_fmt(format_args!("./{path}")), | ||||
|             FilePath::Absolute(path) => f.write_fmt(format_args!("/{path}")), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -24,14 +24,6 @@ pub struct Id { | ||||
|     value: String, | ||||
| } | ||||
| 
 | ||||
| impl Id { | ||||
|     pub fn empty() -> Self { | ||||
|         Id { | ||||
|             value: String::new(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl FromStr for Id { | ||||
|     type Err = (); | ||||
| 
 | ||||
| @ -42,12 +34,6 @@ impl FromStr for Id { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<String> for Id { | ||||
|     fn from(value: String) -> Self { | ||||
|         Self { value } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for Id { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         f.write_str(&self.value) | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| mod file; | ||||
| mod id; | ||||
| mod version; | ||||
| pub use file::*; | ||||
| pub use id::*; | ||||
| pub use version::*; | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| use std::fmt; | ||||
| 
 | ||||
| use async_trait::async_trait; | ||||
| use harmony_types::net::IpAddress; | ||||
| 
 | ||||
| use super::topology::IpAddress; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum ExecutorError { | ||||
|  | ||||
| @ -1,156 +1,38 @@ | ||||
| use std::sync::Arc; | ||||
| 
 | ||||
| use derive_new::new; | ||||
| use harmony_inventory_agent::hwinfo::{CPU, MemoryModule, NetworkInterface, StorageDrive}; | ||||
| use harmony_types::net::MacAddress; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serde::{Serialize, Serializer, ser::SerializeStruct}; | ||||
| use serde_value::Value; | ||||
| 
 | ||||
| pub type HostGroup = Vec<PhysicalHost>; | ||||
| pub type SwitchGroup = Vec<Switch>; | ||||
| pub type FirewallGroup = Vec<PhysicalHost>; | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct PhysicalHost { | ||||
|     pub id: Id, | ||||
|     pub category: HostCategory, | ||||
|     pub network: Vec<NetworkInterface>, | ||||
|     pub storage: Vec<StorageDrive>, | ||||
|     pub management: Arc<dyn ManagementInterface>, | ||||
|     pub storage: Vec<Storage>, | ||||
|     pub labels: Vec<Label>, | ||||
|     pub memory_modules: Vec<MemoryModule>, | ||||
|     pub cpus: Vec<CPU>, | ||||
|     pub memory_size: Option<u64>, | ||||
|     pub cpu_count: Option<u64>, | ||||
| } | ||||
| 
 | ||||
| impl PhysicalHost { | ||||
|     pub fn empty(category: HostCategory) -> Self { | ||||
|         Self { | ||||
|             id: Id::empty(), | ||||
|             category, | ||||
|             network: vec![], | ||||
|             storage: vec![], | ||||
|             labels: vec![], | ||||
|             memory_modules: vec![], | ||||
|             cpus: vec![], | ||||
|             management: Arc::new(ManualManagementInterface {}), | ||||
|             memory_size: None, | ||||
|             cpu_count: None, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn summary(&self) -> String { | ||||
|         let mut parts = Vec::new(); | ||||
| 
 | ||||
|         // Part 1: System Model (from labels) or Category as a fallback
 | ||||
|         let model = self | ||||
|             .labels | ||||
|             .iter() | ||||
|             .find(|l| l.name == "system-product-name" || l.name == "model") | ||||
|             .map(|l| l.value.clone()) | ||||
|             .unwrap_or_else(|| self.category.to_string()); | ||||
|         parts.push(model); | ||||
| 
 | ||||
|         // Part 2: CPU Information
 | ||||
|         if !self.cpus.is_empty() { | ||||
|             let cpu_count = self.cpus.len(); | ||||
|             let total_cores = self.cpus.iter().map(|c| c.cores).sum::<u32>(); | ||||
|             let total_threads = self.cpus.iter().map(|c| c.threads).sum::<u32>(); | ||||
|             let model_name = &self.cpus[0].model; | ||||
| 
 | ||||
|             let cpu_summary = if cpu_count > 1 { | ||||
|                 format!( | ||||
|                     "{}x {} ({}c/{}t)", | ||||
|                     cpu_count, model_name, total_cores, total_threads | ||||
|                 ) | ||||
|             } else { | ||||
|                 format!("{} ({}c/{}t)", model_name, total_cores, total_threads) | ||||
|             }; | ||||
|             parts.push(cpu_summary); | ||||
|         } | ||||
| 
 | ||||
|         // Part 3: Memory Information
 | ||||
|         if !self.memory_modules.is_empty() { | ||||
|             let total_mem_bytes = self | ||||
|                 .memory_modules | ||||
|                 .iter() | ||||
|                 .map(|m| m.size_bytes) | ||||
|                 .sum::<u64>(); | ||||
|             let total_mem_gb = (total_mem_bytes as f64 / (1024.0 * 1024.0 * 1024.0)).round() as u64; | ||||
| 
 | ||||
|             // Find the most common speed among modules
 | ||||
|             let mut speeds = std::collections::HashMap::new(); | ||||
|             for module in &self.memory_modules { | ||||
|                 if let Some(speed) = module.speed_mhz { | ||||
|                     *speeds.entry(speed).or_insert(0) += 1; | ||||
|                 } | ||||
|             } | ||||
|             let common_speed = speeds | ||||
|                 .into_iter() | ||||
|                 .max_by_key(|&(_, count)| count) | ||||
|                 .map(|(speed, _)| speed); | ||||
| 
 | ||||
|             if let Some(speed) = common_speed { | ||||
|                 parts.push(format!("{} GB RAM @ {}MHz", total_mem_gb, speed)); | ||||
|             } else { | ||||
|                 parts.push(format!("{} GB RAM", total_mem_gb)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Part 4: Storage Information
 | ||||
|         if !self.storage.is_empty() { | ||||
|             let total_storage_bytes = self.storage.iter().map(|d| d.size_bytes).sum::<u64>(); | ||||
|             let drive_count = self.storage.len(); | ||||
|             let first_drive_model = &self.storage[0].model; | ||||
| 
 | ||||
|             // Helper to format bytes into TB or GB
 | ||||
|             let format_storage = |bytes: u64| { | ||||
|                 let tb = bytes as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0); | ||||
|                 if tb >= 1.0 { | ||||
|                     format!("{:.2} TB", tb) | ||||
|                 } else { | ||||
|                     let gb = bytes as f64 / (1024.0 * 1024.0 * 1024.0); | ||||
|                     format!("{:.0} GB", gb) | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             let storage_summary = if drive_count > 1 { | ||||
|                 format!( | ||||
|                     "{} Storage ({}x {})", | ||||
|                     format_storage(total_storage_bytes), | ||||
|                     drive_count, | ||||
|                     first_drive_model | ||||
|                 ) | ||||
|             } else { | ||||
|                 format!( | ||||
|                     "{} Storage ({})", | ||||
|                     format_storage(total_storage_bytes), | ||||
|                     first_drive_model | ||||
|                 ) | ||||
|             }; | ||||
|             parts.push(storage_summary); | ||||
|         } | ||||
| 
 | ||||
|         // Part 5: Network Information
 | ||||
|         // Prioritize an "up" interface with an IPv4 address
 | ||||
|         let best_nic = self | ||||
|             .network | ||||
|             .iter() | ||||
|             .find(|n| n.is_up && !n.ipv4_addresses.is_empty()) | ||||
|             .or_else(|| self.network.first()); | ||||
| 
 | ||||
|         if let Some(nic) = best_nic { | ||||
|             let speed = nic | ||||
|                 .speed_mbps | ||||
|                 .map(|s| format!("{}Gbps", s / 1000)) | ||||
|                 .unwrap_or_else(|| "N/A".to_string()); | ||||
|             let mac = nic.mac_address.to_string(); | ||||
|             let nic_summary = if let Some(ip) = nic.ipv4_addresses.first() { | ||||
|                 format!("NIC: {} ({}, {})", speed, ip, mac) | ||||
|             } else { | ||||
|                 format!("NIC: {} ({})", speed, mac) | ||||
|             }; | ||||
|             parts.push(nic_summary); | ||||
|         } | ||||
| 
 | ||||
|         parts.join(" | ") | ||||
|     } | ||||
| 
 | ||||
|     pub fn cluster_mac(&self) -> MacAddress { | ||||
|         self.network | ||||
|             .first() | ||||
| @ -158,17 +40,37 @@ impl PhysicalHost { | ||||
|             .mac_address | ||||
|     } | ||||
| 
 | ||||
|     pub fn cpu(mut self, cpu_count: Option<u64>) -> Self { | ||||
|         self.cpu_count = cpu_count; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn memory_size(mut self, memory_size: Option<u64>) -> Self { | ||||
|         self.memory_size = memory_size; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn storage( | ||||
|         mut self, | ||||
|         connection: StorageConnectionType, | ||||
|         kind: StorageKind, | ||||
|         size: u64, | ||||
|         serial: String, | ||||
|     ) -> Self { | ||||
|         self.storage.push(Storage { | ||||
|             connection, | ||||
|             kind, | ||||
|             size, | ||||
|             serial, | ||||
|         }); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn mac_address(mut self, mac_address: MacAddress) -> Self { | ||||
|         self.network.push(NetworkInterface { | ||||
|             name: String::new(), | ||||
|             name: None, | ||||
|             mac_address, | ||||
|             speed_mbps: None, | ||||
|             is_up: false, | ||||
|             mtu: 0, | ||||
|             ipv4_addresses: vec![], | ||||
|             ipv6_addresses: vec![], | ||||
|             driver: String::new(), | ||||
|             firmware_version: None, | ||||
|             speed: None, | ||||
|         }); | ||||
|         self | ||||
|     } | ||||
| @ -177,56 +79,52 @@ impl PhysicalHost { | ||||
|         self.labels.push(Label { name, value }); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn management(mut self, management: Arc<dyn ManagementInterface>) -> Self { | ||||
|         self.management = management; | ||||
|         self | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Custom Serialize implementation for PhysicalHost
 | ||||
| // impl Serialize for PhysicalHost {
 | ||||
| //     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
 | ||||
| //     where
 | ||||
| //         S: Serializer,
 | ||||
| //     {
 | ||||
| //         // Determine the number of fields
 | ||||
| //         let mut num_fields = 5; // category, network, storage, labels, management
 | ||||
| //         if self.memory_modules.is_some() {
 | ||||
| //             num_fields += 1;
 | ||||
| //         }
 | ||||
| //         if self.cpus.is_some() {
 | ||||
| //             num_fields += 1;
 | ||||
| //         }
 | ||||
| //
 | ||||
| //         // Create a serialization structure
 | ||||
| //         let mut state = serializer.serialize_struct("PhysicalHost", num_fields)?;
 | ||||
| //
 | ||||
| //         // Serialize the standard fields
 | ||||
| //         state.serialize_field("category", &self.category)?;
 | ||||
| //         state.serialize_field("network", &self.network)?;
 | ||||
| //         state.serialize_field("storage", &self.storage)?;
 | ||||
| //         state.serialize_field("labels", &self.labels)?;
 | ||||
| //
 | ||||
| //         // Serialize optional fields
 | ||||
| //         if let Some(memory) = self.memory_modules {
 | ||||
| //             state.serialize_field("memory_size", &memory)?;
 | ||||
| //         }
 | ||||
| //         if let Some(cpu) = self.cpus {
 | ||||
| //             state.serialize_field("cpu_count", &cpu)?;
 | ||||
| //         }
 | ||||
| //
 | ||||
| //         let mgmt_data = self.management.serialize_management();
 | ||||
| //         // pub management: Arc<dyn ManagementInterface>,
 | ||||
| //
 | ||||
| //         // Handle management interface - either as a field or flattened
 | ||||
| //         state.serialize_field("management", &mgmt_data)?;
 | ||||
| //
 | ||||
| //         state.end()
 | ||||
| //     }
 | ||||
| // }
 | ||||
| 
 | ||||
| impl<'de> Deserialize<'de> for PhysicalHost { | ||||
|     fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error> | ||||
| impl Serialize for PhysicalHost { | ||||
|     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||||
|     where | ||||
|         D: serde::Deserializer<'de>, | ||||
|         S: Serializer, | ||||
|     { | ||||
|         todo!() | ||||
|         // Determine the number of fields
 | ||||
|         let mut num_fields = 5; // category, network, storage, labels, management
 | ||||
|         if self.memory_size.is_some() { | ||||
|             num_fields += 1; | ||||
|         } | ||||
|         if self.cpu_count.is_some() { | ||||
|             num_fields += 1; | ||||
|         } | ||||
| 
 | ||||
|         // Create a serialization structure
 | ||||
|         let mut state = serializer.serialize_struct("PhysicalHost", num_fields)?; | ||||
| 
 | ||||
|         // Serialize the standard fields
 | ||||
|         state.serialize_field("category", &self.category)?; | ||||
|         state.serialize_field("network", &self.network)?; | ||||
|         state.serialize_field("storage", &self.storage)?; | ||||
|         state.serialize_field("labels", &self.labels)?; | ||||
| 
 | ||||
|         // Serialize optional fields
 | ||||
|         if let Some(memory) = self.memory_size { | ||||
|             state.serialize_field("memory_size", &memory)?; | ||||
|         } | ||||
|         if let Some(cpu) = self.cpu_count { | ||||
|             state.serialize_field("cpu_count", &cpu)?; | ||||
|         } | ||||
| 
 | ||||
|         let mgmt_data = self.management.serialize_management(); | ||||
|         // pub management: Arc<dyn ManagementInterface>,
 | ||||
| 
 | ||||
|         // Handle management interface - either as a field or flattened
 | ||||
|         state.serialize_field("management", &mgmt_data)?; | ||||
| 
 | ||||
|         state.end() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -280,10 +178,59 @@ pub enum HostCategory { | ||||
|     Switch, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, new, Clone, Serialize)] | ||||
| pub struct NetworkInterface { | ||||
|     pub name: Option<String>, | ||||
|     pub mac_address: MacAddress, | ||||
|     pub speed: Option<u64>, | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| use harmony_macros::mac_address; | ||||
| #[cfg(test)] | ||||
| impl NetworkInterface { | ||||
|     pub fn dummy() -> Self { | ||||
|         Self { | ||||
|             name: Some(String::new()), | ||||
|             mac_address: mac_address!("00:00:00:00:00:00"), | ||||
|             speed: Some(0), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| use harmony_types::id::Id; | ||||
| #[derive(Debug, new, Clone, Serialize)] | ||||
| pub enum StorageConnectionType { | ||||
|     Sata3g, | ||||
|     Sata6g, | ||||
|     Sas6g, | ||||
|     Sas12g, | ||||
|     PCIE, | ||||
| } | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| pub enum StorageKind { | ||||
|     SSD, | ||||
|     NVME, | ||||
|     HDD, | ||||
| } | ||||
| #[derive(Debug, new, Clone, Serialize)] | ||||
| pub struct Storage { | ||||
|     pub connection: StorageConnectionType, | ||||
|     pub kind: StorageKind, | ||||
|     pub size: u64, | ||||
|     pub serial: String, | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| impl Storage { | ||||
|     pub fn dummy() -> Self { | ||||
|         Self { | ||||
|             connection: StorageConnectionType::Sata3g, | ||||
|             kind: StorageKind::SSD, | ||||
|             size: 0, | ||||
|             serial: String::new(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| pub struct Switch { | ||||
| @ -314,65 +261,146 @@ impl Location { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for HostCategory { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         match self { | ||||
|             HostCategory::Server => write!(f, "Server"), | ||||
|             HostCategory::Firewall => write!(f, "Firewall"), | ||||
|             HostCategory::Switch => write!(f, "Switch"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for Label { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "{}: {}", self.name, self.value) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for Location { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "Address: {}, Name: {}", self.address, self.name) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for PhysicalHost { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "{}", self.summary()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for Switch { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "Switch with {} interfaces", self._interface.len()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|     use serde::{Deserialize, Serialize}; | ||||
|     use std::sync::Arc; | ||||
| 
 | ||||
|     // Mock implementation of ManagementInterface
 | ||||
|     #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
|     struct MockHPIlo { | ||||
|         ip: String, | ||||
|         username: String, | ||||
|         password: String, | ||||
|         firmware_version: String, | ||||
|     } | ||||
| 
 | ||||
|     impl ManagementInterface for MockHPIlo { | ||||
|         fn boot_to_pxe(&self) {} | ||||
| 
 | ||||
|         fn get_supported_protocol_names(&self) -> String { | ||||
|             String::new() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // Another mock implementation
 | ||||
|     #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
|     struct MockDellIdrac { | ||||
|         hostname: String, | ||||
|         port: u16, | ||||
|         api_token: String, | ||||
|     } | ||||
| 
 | ||||
|     impl ManagementInterface for MockDellIdrac { | ||||
|         fn boot_to_pxe(&self) {} | ||||
| 
 | ||||
|         fn get_supported_protocol_names(&self) -> String { | ||||
|             String::new() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_serialize_physical_host_with_hp_ilo() { | ||||
|         // Create a PhysicalHost with HP iLO management
 | ||||
|         let host = PhysicalHost { | ||||
|             category: HostCategory::Server, | ||||
|             network: vec![NetworkInterface::dummy()], | ||||
|             management: Arc::new(MockHPIlo { | ||||
|                 ip: "192.168.1.100".to_string(), | ||||
|                 username: "admin".to_string(), | ||||
|                 password: "password123".to_string(), | ||||
|                 firmware_version: "2.5.0".to_string(), | ||||
|             }), | ||||
|             storage: vec![Storage::dummy()], | ||||
|             labels: vec![Label::new("datacenter".to_string(), "us-east".to_string())], | ||||
|             memory_size: Some(64_000_000), | ||||
|             cpu_count: Some(16), | ||||
|         }; | ||||
| 
 | ||||
|         // Serialize to JSON
 | ||||
|         let json = serde_json::to_string(&host).expect("Failed to serialize host"); | ||||
| 
 | ||||
|         // Check that the serialized JSON contains the HP iLO details
 | ||||
|         assert!(json.contains("192.168.1.100")); | ||||
|         assert!(json.contains("admin")); | ||||
|         assert!(json.contains("password123")); | ||||
|         assert!(json.contains("firmware_version")); | ||||
|         assert!(json.contains("2.5.0")); | ||||
| 
 | ||||
|         // Parse back to verify structure (not the exact management interface)
 | ||||
|         let parsed: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON"); | ||||
| 
 | ||||
|         // Verify basic structure
 | ||||
|         assert_eq!(parsed["cpu_count"], 16); | ||||
|         assert_eq!(parsed["memory_size"], 64_000_000); | ||||
|         assert_eq!(parsed["network"][0]["name"], ""); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_serialize_physical_host_with_dell_idrac() { | ||||
|         // Create a PhysicalHost with Dell iDRAC management
 | ||||
|         let host = PhysicalHost { | ||||
|             category: HostCategory::Server, | ||||
|             network: vec![NetworkInterface::dummy()], | ||||
|             management: Arc::new(MockDellIdrac { | ||||
|                 hostname: "idrac-server01".to_string(), | ||||
|                 port: 443, | ||||
|                 api_token: "abcdef123456".to_string(), | ||||
|             }), | ||||
|             storage: vec![Storage::dummy()], | ||||
|             labels: vec![Label::new("env".to_string(), "production".to_string())], | ||||
|             memory_size: Some(128_000_000), | ||||
|             cpu_count: Some(32), | ||||
|         }; | ||||
| 
 | ||||
|         // Serialize to JSON
 | ||||
|         let json = serde_json::to_string(&host).expect("Failed to serialize host"); | ||||
| 
 | ||||
|         // Check that the serialized JSON contains the Dell iDRAC details
 | ||||
|         assert!(json.contains("idrac-server01")); | ||||
|         assert!(json.contains("443")); | ||||
|         assert!(json.contains("abcdef123456")); | ||||
| 
 | ||||
|         // Parse back to verify structure
 | ||||
|         let parsed: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON"); | ||||
| 
 | ||||
|         // Verify basic structure
 | ||||
|         assert_eq!(parsed["cpu_count"], 32); | ||||
|         assert_eq!(parsed["memory_size"], 128_000_000); | ||||
|         assert_eq!(parsed["storage"][0]["path"], serde_json::Value::Null); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_different_management_implementations_produce_valid_json() { | ||||
|         // Create hosts with different management implementations
 | ||||
|         let host1 = PhysicalHost { | ||||
|             id: Id::empty(), | ||||
|             category: HostCategory::Server, | ||||
|             network: vec![], | ||||
|             management: Arc::new(MockHPIlo { | ||||
|                 ip: "10.0.0.1".to_string(), | ||||
|                 username: "root".to_string(), | ||||
|                 password: "secret".to_string(), | ||||
|                 firmware_version: "3.0.0".to_string(), | ||||
|             }), | ||||
|             storage: vec![], | ||||
|             labels: vec![], | ||||
|             memory_modules: vec![], | ||||
|             cpus: vec![], | ||||
|             memory_size: None, | ||||
|             cpu_count: None, | ||||
|         }; | ||||
| 
 | ||||
|         let host2 = PhysicalHost { | ||||
|             id: Id::empty(), | ||||
|             category: HostCategory::Server, | ||||
|             network: vec![], | ||||
|             management: Arc::new(MockDellIdrac { | ||||
|                 hostname: "server02-idrac".to_string(), | ||||
|                 port: 8443, | ||||
|                 api_token: "token123".to_string(), | ||||
|             }), | ||||
|             storage: vec![], | ||||
|             labels: vec![], | ||||
|             memory_modules: vec![], | ||||
|             cpus: vec![], | ||||
|             memory_size: None, | ||||
|             cpu_count: None, | ||||
|         }; | ||||
| 
 | ||||
|         // Both should serialize successfully
 | ||||
| @ -382,5 +410,8 @@ mod tests { | ||||
|         // Both JSONs should be valid and parseable
 | ||||
|         let _: serde_json::Value = serde_json::from_str(&json1).expect("Invalid JSON for host1"); | ||||
|         let _: serde_json::Value = serde_json::from_str(&json2).expect("Invalid JSON for host2"); | ||||
| 
 | ||||
|         // The JSONs should be different because they contain different management interfaces
 | ||||
|         assert_ne!(json1, json2); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| use log::debug; | ||||
| use once_cell::sync::Lazy; | ||||
| use std::{collections::HashMap, sync::Mutex}; | ||||
| use tokio::sync::broadcast; | ||||
| 
 | ||||
| use crate::modules::application::ApplicationFeatureStatus; | ||||
| 
 | ||||
| @ -39,46 +40,43 @@ pub enum HarmonyEvent { | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| type Subscriber = Box<dyn Fn(&HarmonyEvent) + Send + Sync>; | ||||
| static HARMONY_EVENT_BUS: Lazy<broadcast::Sender<HarmonyEvent>> = Lazy::new(|| { | ||||
|     // TODO: Adjust channel capacity
 | ||||
|     let (tx, _rx) = broadcast::channel(100); | ||||
|     tx | ||||
| }); | ||||
| 
 | ||||
| static SUBSCRIBERS: Lazy<Mutex<HashMap<String, Subscriber>>> = | ||||
|     Lazy::new(|| Mutex::new(HashMap::new())); | ||||
| 
 | ||||
| /// Subscribes a listener to all instrumentation events.
 | ||||
| ///
 | ||||
| /// Simply provide a unique name and a closure to run when an event happens.
 | ||||
| ///
 | ||||
| /// # Example
 | ||||
| /// ```
 | ||||
| /// use harmony::instrumentation;
 | ||||
| /// instrumentation::subscribe("my_logger", |event| {
 | ||||
| ///   println!("Event occurred: {:?}", event);
 | ||||
| /// });
 | ||||
| /// ```
 | ||||
| pub fn subscribe<F>(name: &str, callback: F) | ||||
| where | ||||
|     F: Fn(&HarmonyEvent) + Send + Sync + 'static, | ||||
| { | ||||
|     let mut subs = SUBSCRIBERS.lock().unwrap(); | ||||
|     subs.insert(name.to_string(), Box::new(callback)); | ||||
| } | ||||
| 
 | ||||
| /// Instruments an event, notifying all subscribers.
 | ||||
| ///
 | ||||
| /// This will call every closure that was registered with `subscribe`.
 | ||||
| ///
 | ||||
| /// # Example
 | ||||
| /// ```
 | ||||
| /// use harmony::instrumentation;
 | ||||
| /// use harmony::instrumentation::HarmonyEvent;
 | ||||
| /// instrumentation::instrument(HarmonyEvent::HarmonyStarted);
 | ||||
| /// ```
 | ||||
| pub fn instrument(event: HarmonyEvent) -> Result<(), &'static str> { | ||||
|     let subs = SUBSCRIBERS.lock().unwrap(); | ||||
| 
 | ||||
|     for callback in subs.values() { | ||||
|         callback(&event); | ||||
|     if cfg!(any(test, feature = "testing")) { | ||||
|         let _ = event; // Suppress the "unused variable" warning for `event`
 | ||||
|         Ok(()) | ||||
|     } else { | ||||
|         match HARMONY_EVENT_BUS.send(event) { | ||||
|             Ok(_) => Ok(()), | ||||
|             Err(_) => Err("send error: no subscribers"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub async fn subscribe<F, Fut>(name: &str, mut handler: F) | ||||
| where | ||||
|     F: FnMut(HarmonyEvent) -> Fut + Send + 'static, | ||||
|     Fut: Future<Output = bool> + Send, | ||||
| { | ||||
|     let mut rx = HARMONY_EVENT_BUS.subscribe(); | ||||
|     debug!("[{name}] Service started. Listening for events..."); | ||||
|     loop { | ||||
|         match rx.recv().await { | ||||
|             Ok(event) => { | ||||
|                 if !handler(event).await { | ||||
|                     debug!("[{name}] Handler requested exit."); | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             Err(broadcast::error::RecvError::Lagged(n)) => { | ||||
|                 debug!("[{name}] Lagged behind by {n} messages."); | ||||
|             } | ||||
|             Err(_) => break, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| @ -1,11 +1,13 @@ | ||||
| use harmony_types::id::Id; | ||||
| use std::error::Error; | ||||
| 
 | ||||
| use async_trait::async_trait; | ||||
| use derive_new::new; | ||||
| 
 | ||||
| use super::{ | ||||
|     data::Version, executors::ExecutorError, inventory::Inventory, topology::PreparationError, | ||||
|     data::{Id, Version}, | ||||
|     executors::ExecutorError, | ||||
|     inventory::Inventory, | ||||
|     topology::PreparationError, | ||||
| }; | ||||
| 
 | ||||
| pub enum InterpretName { | ||||
| @ -30,7 +32,6 @@ pub enum InterpretName { | ||||
|     Lamp, | ||||
|     ApplicationMonitoring, | ||||
|     K8sPrometheusCrdAlerting, | ||||
|     DiscoverInventoryAgent, | ||||
|     CephClusterHealth, | ||||
| } | ||||
| 
 | ||||
| @ -58,7 +59,6 @@ impl std::fmt::Display for InterpretName { | ||||
|             InterpretName::Lamp => f.write_str("LAMP"), | ||||
|             InterpretName::ApplicationMonitoring => f.write_str("ApplicationMonitoring"), | ||||
|             InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"), | ||||
|             InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"), | ||||
|             InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"), | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -1,6 +1,3 @@ | ||||
| mod repository; | ||||
| pub use repository::*; | ||||
| 
 | ||||
| #[derive(Debug, new, Clone)] | ||||
| pub struct InventoryFilter { | ||||
|     target: Vec<Filter>, | ||||
| @ -18,8 +15,6 @@ impl InventoryFilter { | ||||
| use derive_new::new; | ||||
| use log::info; | ||||
| 
 | ||||
| use crate::hardware::{ManagementInterface, ManualManagementInterface}; | ||||
| 
 | ||||
| use super::{ | ||||
|     filter::Filter, | ||||
|     hardware::{FirewallGroup, HostGroup, Location, SwitchGroup}, | ||||
| @ -32,7 +27,7 @@ pub struct Inventory { | ||||
|     // Firewall is really just a host but with somewhat specialized hardware
 | ||||
|     // I'm not entirely sure it belongs to its own category but it helps make things easier and
 | ||||
|     // clearer for now so let's try it this way.
 | ||||
|     pub firewall_mgmt: Box<dyn ManagementInterface>, | ||||
|     pub firewall: FirewallGroup, | ||||
|     pub worker_host: HostGroup, | ||||
|     pub storage_host: HostGroup, | ||||
|     pub control_plane_host: HostGroup, | ||||
| @ -43,7 +38,7 @@ impl Inventory { | ||||
|         Self { | ||||
|             location: Location::new("Empty".to_string(), "location".to_string()), | ||||
|             switch: vec![], | ||||
|             firewall_mgmt: Box::new(ManualManagementInterface {}), | ||||
|             firewall: vec![], | ||||
|             worker_host: vec![], | ||||
|             storage_host: vec![], | ||||
|             control_plane_host: vec![], | ||||
| @ -54,7 +49,7 @@ impl Inventory { | ||||
|         Self { | ||||
|             location: Location::test_building(), | ||||
|             switch: SwitchGroup::new(), | ||||
|             firewall_mgmt: Box::new(ManualManagementInterface {}), | ||||
|             firewall: FirewallGroup::new(), | ||||
|             worker_host: HostGroup::new(), | ||||
|             storage_host: HostGroup::new(), | ||||
|             control_plane_host: HostGroup::new(), | ||||
|  | ||||
| @ -1,25 +0,0 @@ | ||||
| use async_trait::async_trait; | ||||
| 
 | ||||
| use crate::hardware::PhysicalHost; | ||||
| 
 | ||||
| /// Errors that can occur within the repository layer.
 | ||||
| #[derive(thiserror::Error, Debug)] | ||||
| pub enum RepoError { | ||||
|     #[error("Database query failed: {0}")] | ||||
|     QueryFailed(String), | ||||
|     #[error("Data serialization failed: {0}")] | ||||
|     Serialization(String), | ||||
|     #[error("Data deserialization failed: {0}")] | ||||
|     Deserialization(String), | ||||
|     #[error("Could not connect to the database: {0}")] | ||||
|     ConnectionFailed(String), | ||||
| } | ||||
| 
 | ||||
| // --- Trait and Implementation ---
 | ||||
| 
 | ||||
| /// Defines the contract for inventory persistence.
 | ||||
| #[async_trait] | ||||
| pub trait InventoryRepository: Send + Sync + 'static { | ||||
|     async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError>; | ||||
|     async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError>; | ||||
| } | ||||
| @ -74,7 +74,6 @@ impl<T: Topology> Maestro<T> { | ||||
| 
 | ||||
|     fn is_topology_initialized(&self) -> bool { | ||||
|         self.topology_state.status == TopologyStatus::Success | ||||
|             || self.topology_state.status == TopologyStatus::Noop | ||||
|     } | ||||
| 
 | ||||
|     pub async fn interpret(&self, score: Box<dyn Score<T>>) -> Result<Outcome, InterpretError> { | ||||
|  | ||||
| @ -1,4 +1,3 @@ | ||||
| use harmony_types::id::Id; | ||||
| use std::collections::BTreeMap; | ||||
| 
 | ||||
| use async_trait::async_trait; | ||||
| @ -6,6 +5,7 @@ use serde::Serialize; | ||||
| use serde_value::Value; | ||||
| 
 | ||||
| use super::{ | ||||
|     data::Id, | ||||
|     instrumentation::{self, HarmonyEvent}, | ||||
|     interpret::{Interpret, InterpretError, Outcome}, | ||||
|     inventory::Inventory, | ||||
|  | ||||
| @ -1,13 +1,9 @@ | ||||
| use async_trait::async_trait; | ||||
| use harmony_macros::ip; | ||||
| use harmony_types::net::MacAddress; | ||||
| use harmony_types::net::Url; | ||||
| use log::debug; | ||||
| use log::info; | ||||
| 
 | ||||
| use crate::data::FileContent; | ||||
| use crate::executors::ExecutorError; | ||||
| use crate::topology::PxeOptions; | ||||
| 
 | ||||
| use super::DHCPStaticEntry; | ||||
| use super::DhcpServer; | ||||
| @ -27,6 +23,7 @@ use super::Router; | ||||
| use super::TftpServer; | ||||
| 
 | ||||
| use super::Topology; | ||||
| use super::Url; | ||||
| use super::k8s::K8sClient; | ||||
| use std::sync::Arc; | ||||
| 
 | ||||
| @ -52,10 +49,9 @@ impl Topology for HAClusterTopology { | ||||
|         "HAClusterTopology" | ||||
|     } | ||||
|     async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> { | ||||
|         debug!( | ||||
|         todo!( | ||||
|             "ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready." | ||||
|         ); | ||||
|         Ok(PreparationOutcome::Noop) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -157,10 +153,12 @@ impl DhcpServer for HAClusterTopology { | ||||
|     async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> { | ||||
|         self.dhcp_server.list_static_mappings().await | ||||
|     } | ||||
|     async fn set_pxe_options(&self, options: PxeOptions) -> Result<(), ExecutorError> { | ||||
|         self.dhcp_server.set_pxe_options(options).await | ||||
|     async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> { | ||||
|         self.dhcp_server.set_next_server(ip).await | ||||
|     } | ||||
|     async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> { | ||||
|         self.dhcp_server.set_boot_filename(boot_filename).await | ||||
|     } | ||||
| 
 | ||||
|     fn get_ip(&self) -> IpAddress { | ||||
|         self.dhcp_server.get_ip() | ||||
|     } | ||||
| @ -170,6 +168,16 @@ impl DhcpServer for HAClusterTopology { | ||||
|     async fn commit_config(&self) -> Result<(), ExecutorError> { | ||||
|         self.dhcp_server.commit_config().await | ||||
|     } | ||||
| 
 | ||||
|     async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> { | ||||
|         self.dhcp_server.set_filename(filename).await | ||||
|     } | ||||
|     async fn set_filename64(&self, filename64: &str) -> Result<(), ExecutorError> { | ||||
|         self.dhcp_server.set_filename64(filename64).await | ||||
|     } | ||||
|     async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError> { | ||||
|         self.dhcp_server.set_filenameipxe(filenameipxe).await | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| @ -213,21 +221,17 @@ impl HttpServer for HAClusterTopology { | ||||
|         self.http_server.serve_files(url).await | ||||
|     } | ||||
| 
 | ||||
|     async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> { | ||||
|         self.http_server.serve_file_content(file).await | ||||
|     } | ||||
| 
 | ||||
|     fn get_ip(&self) -> IpAddress { | ||||
|         self.http_server.get_ip() | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|     async fn ensure_initialized(&self) -> Result<(), ExecutorError> { | ||||
|         self.http_server.ensure_initialized().await | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|     async fn commit_config(&self) -> Result<(), ExecutorError> { | ||||
|         self.http_server.commit_config().await | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|     async fn reload_restart(&self) -> Result<(), ExecutorError> { | ||||
|         self.http_server.reload_restart().await | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -295,7 +299,19 @@ impl DhcpServer for DummyInfra { | ||||
|     async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> { | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|     async fn set_pxe_options(&self, _options: PxeOptions) -> Result<(), ExecutorError> { | ||||
|     async fn set_next_server(&self, _ip: IpAddress) -> Result<(), ExecutorError> { | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|     async fn set_boot_filename(&self, _boot_filename: &str) -> Result<(), ExecutorError> { | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|     async fn set_filename(&self, _filename: &str) -> Result<(), ExecutorError> { | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|     async fn set_filename64(&self, _filename: &str) -> Result<(), ExecutorError> { | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|     async fn set_filenameipxe(&self, _filenameipxe: &str) -> Result<(), ExecutorError> { | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|     fn get_ip(&self) -> IpAddress { | ||||
| @ -365,9 +381,6 @@ impl HttpServer for DummyInfra { | ||||
|     async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> { | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|     async fn serve_file_content(&self, _file: &FileContent) -> Result<(), ExecutorError> { | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|     fn get_ip(&self) -> IpAddress { | ||||
|         unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) | ||||
|     } | ||||
|  | ||||
| @ -1,12 +1,11 @@ | ||||
| use crate::{data::FileContent, executors::ExecutorError}; | ||||
| use crate::executors::ExecutorError; | ||||
| use async_trait::async_trait; | ||||
| 
 | ||||
| use harmony_types::net::IpAddress; | ||||
| use harmony_types::net::Url; | ||||
| use super::{IpAddress, Url}; | ||||
| 
 | ||||
| #[async_trait] | ||||
| pub trait HttpServer: Send + Sync { | ||||
|     async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>; | ||||
|     async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError>; | ||||
|     fn get_ip(&self) -> IpAddress; | ||||
| 
 | ||||
|     // async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError>;
 | ||||
|  | ||||
| @ -185,10 +185,7 @@ impl K8sClient { | ||||
|                 if let Some(s) = status.status { | ||||
|                     let mut stdout_buf = String::new(); | ||||
|                     if let Some(mut stdout) = process.stdout().take() { | ||||
|                         stdout | ||||
|                             .read_to_string(&mut stdout_buf) | ||||
|                             .await | ||||
|                             .map_err(|e| format!("Failed to get status stdout {e}"))?; | ||||
|                         stdout.read_to_string(&mut stdout_buf).await; | ||||
|                     } | ||||
|                     debug!("Status: {} - {:?}", s, status.details); | ||||
|                     if s == "Success" { | ||||
|  | ||||
| @ -4,9 +4,8 @@ use async_trait::async_trait; | ||||
| use log::debug; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| use super::LogicalHost; | ||||
| use super::{IpAddress, LogicalHost}; | ||||
| use crate::executors::ExecutorError; | ||||
| use harmony_types::net::IpAddress; | ||||
| 
 | ||||
| impl std::fmt::Debug for dyn LoadBalancer { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| mod ha_cluster; | ||||
| use harmony_types::net::IpAddress; | ||||
| mod host_binding; | ||||
| mod http; | ||||
| pub mod installable; | ||||
| @ -33,6 +32,7 @@ use super::{ | ||||
|     instrumentation::{self, HarmonyEvent}, | ||||
| }; | ||||
| use std::error::Error; | ||||
| use std::net::IpAddr; | ||||
| 
 | ||||
| /// Represents a logical view of an infrastructure environment providing specific capabilities.
 | ||||
| ///
 | ||||
| @ -196,6 +196,35 @@ pub trait MultiTargetTopology: Topology { | ||||
|     fn current_target(&self) -> DeploymentTarget; | ||||
| } | ||||
| 
 | ||||
| pub type IpAddress = IpAddr; | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub enum Url { | ||||
|     LocalFolder(String), | ||||
|     Url(url::Url), | ||||
| } | ||||
| 
 | ||||
| impl Serialize for Url { | ||||
|     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||||
|     where | ||||
|         S: serde::Serializer, | ||||
|     { | ||||
|         match self { | ||||
|             Url::LocalFolder(path) => serializer.serialize_str(path), | ||||
|             Url::Url(url) => serializer.serialize_str(url.as_str()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for Url { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         match self { | ||||
|             Url::LocalFolder(path) => write!(f, "{}", path), | ||||
|             Url::Url(url) => write!(f, "{}", url), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Represents a logical member of a cluster that provides one or more services.
 | ||||
| ///
 | ||||
| /// A LogicalHost can represent various roles within the infrastructure, such as:
 | ||||
| @ -234,8 +263,7 @@ impl LogicalHost { | ||||
|     ///
 | ||||
|     /// ```
 | ||||
|     /// use std::str::FromStr;
 | ||||
|     /// use harmony::topology::{LogicalHost};
 | ||||
|     /// use harmony_types::net::IpAddress;
 | ||||
|     /// use harmony::topology::{IpAddress, LogicalHost};
 | ||||
|     ///
 | ||||
|     /// let start_ip = IpAddress::from_str("192.168.0.20").unwrap();
 | ||||
|     /// let hosts = LogicalHost::create_hosts(3, start_ip, "worker");
 | ||||
| @ -291,7 +319,7 @@ fn increment_ip(ip: IpAddress, increment: u32) -> Option<IpAddress> { | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use harmony_types::net::Url; | ||||
|     use super::*; | ||||
|     use serde_json; | ||||
| 
 | ||||
|     #[test] | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| use std::{net::Ipv4Addr, str::FromStr, sync::Arc}; | ||||
| 
 | ||||
| use async_trait::async_trait; | ||||
| use harmony_types::net::{IpAddress, MacAddress}; | ||||
| use harmony_types::net::MacAddress; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| use crate::executors::ExecutorError; | ||||
| 
 | ||||
| use super::{LogicalHost, k8s::K8sClient}; | ||||
| use super::{IpAddress, LogicalHost, k8s::K8sClient}; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct DHCPStaticEntry { | ||||
| @ -46,19 +46,16 @@ pub trait K8sclient: Send + Sync { | ||||
|     async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>; | ||||
| } | ||||
| 
 | ||||
| pub struct PxeOptions { | ||||
|     pub ipxe_filename: String, | ||||
|     pub bios_filename: String, | ||||
|     pub efi_filename: String, | ||||
|     pub tftp_ip: Option<IpAddress>, | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| pub trait DhcpServer: Send + Sync + std::fmt::Debug { | ||||
|     async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>; | ||||
|     async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>; | ||||
|     async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>; | ||||
|     async fn set_pxe_options(&self, pxe_options: PxeOptions) -> Result<(), ExecutorError>; | ||||
|     async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError>; | ||||
|     async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError>; | ||||
|     async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError>; | ||||
|     async fn set_filename64(&self, filename64: &str) -> Result<(), ExecutorError>; | ||||
|     async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError>; | ||||
|     fn get_ip(&self) -> IpAddress; | ||||
|     fn get_host(&self) -> LogicalHost; | ||||
|     async fn commit_config(&self) -> Result<(), ExecutorError>; | ||||
|  | ||||
| @ -4,12 +4,11 @@ use async_trait::async_trait; | ||||
| use log::debug; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     topology::{Topology, installable::Installable}, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[async_trait] | ||||
| pub trait AlertSender: Send + Sync + std::fmt::Debug { | ||||
|  | ||||
| @ -2,7 +2,7 @@ pub mod k8s; | ||||
| mod manager; | ||||
| pub mod network_policy; | ||||
| 
 | ||||
| use harmony_types::id::Id; | ||||
| use crate::data::Id; | ||||
| pub use manager::*; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::str::FromStr; | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| use crate::executors::ExecutorError; | ||||
| use async_trait::async_trait; | ||||
| 
 | ||||
| use harmony_types::net::{IpAddress, Url}; | ||||
| use super::{IpAddress, Url}; | ||||
| 
 | ||||
| #[async_trait] | ||||
| pub trait TftpServer: Send + Sync { | ||||
|  | ||||
| @ -3,9 +3,11 @@ use std::sync::Arc; | ||||
| 
 | ||||
| use russh::{client, keys::key}; | ||||
| 
 | ||||
| use crate::domain::executors::{ExecutorError, SshClient}; | ||||
| use crate::{ | ||||
|     domain::executors::{ExecutorError, SshClient}, | ||||
|     topology::IpAddress, | ||||
| }; | ||||
| 
 | ||||
| use harmony_types::net::IpAddress; | ||||
| pub struct RusshClient; | ||||
| 
 | ||||
| #[async_trait] | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| use crate::hardware::ManagementInterface; | ||||
| use crate::topology::IpAddress; | ||||
| use derive_new::new; | ||||
| use harmony_types::net::IpAddress; | ||||
| use harmony_types::net::MacAddress; | ||||
| use log::info; | ||||
| use serde::Serialize; | ||||
|  | ||||
| @ -1,17 +0,0 @@ | ||||
| use crate::{ | ||||
|     config::DATABASE_URL, | ||||
|     infra::inventory::sqlite::SqliteInventoryRepository, | ||||
|     inventory::{InventoryRepository, RepoError}, | ||||
| }; | ||||
| 
 | ||||
| pub mod sqlite; | ||||
| 
 | ||||
| pub struct InventoryRepositoryFactory; | ||||
| 
 | ||||
| impl InventoryRepositoryFactory { | ||||
|     pub async fn build() -> Result<Box<dyn InventoryRepository>, RepoError> { | ||||
|         Ok(Box::new( | ||||
|             SqliteInventoryRepository::new(&(*DATABASE_URL)).await?, | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| @ -1,65 +0,0 @@ | ||||
| use crate::{ | ||||
|     hardware::PhysicalHost, | ||||
|     inventory::{InventoryRepository, RepoError}, | ||||
| }; | ||||
| use async_trait::async_trait; | ||||
| use harmony_types::id::Id; | ||||
| use log::info; | ||||
| use sqlx::{Pool, Sqlite, SqlitePool}; | ||||
| 
 | ||||
| /// A thread-safe, connection-pooled repository using SQLite.
 | ||||
| #[derive(Debug)] | ||||
| pub struct SqliteInventoryRepository { | ||||
|     pool: Pool<Sqlite>, | ||||
| } | ||||
| 
 | ||||
| impl SqliteInventoryRepository { | ||||
|     pub async fn new(database_url: &str) -> Result<Self, RepoError> { | ||||
|         let pool = SqlitePool::connect(database_url) | ||||
|             .await | ||||
|             .map_err(|e| RepoError::ConnectionFailed(e.to_string()))?; | ||||
| 
 | ||||
|         info!("SQLite inventory repository initialized at '{database_url}'"); | ||||
|         Ok(Self { pool }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl InventoryRepository for SqliteInventoryRepository { | ||||
|     async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError> { | ||||
|         let data = serde_json::to_vec(host).map_err(|e| RepoError::Serialization(e.to_string()))?; | ||||
| 
 | ||||
|         let id = Id::default().to_string(); | ||||
|         let host_id = host.id.to_string(); | ||||
| 
 | ||||
|         sqlx::query!( | ||||
|             "INSERT INTO physical_hosts (id, version_id, data) VALUES (?, ?, ?)", | ||||
|             host_id, | ||||
|             id, | ||||
|             data, | ||||
|         ) | ||||
|         .execute(&self.pool) | ||||
|         .await?; | ||||
| 
 | ||||
|         info!("Saved new inventory version for host '{}'", host.id); | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError> { | ||||
|         let _row = sqlx::query_as!( | ||||
|             DbHost, | ||||
|             r#"SELECT id, version_id, data as "data: Json<PhysicalHost>" FROM physical_hosts WHERE id = ? ORDER BY version_id DESC LIMIT 1"#, | ||||
|             host_id | ||||
|         ) | ||||
|         .fetch_optional(&self.pool) | ||||
|         .await?; | ||||
|         todo!() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| use sqlx::types::Json; | ||||
| struct DbHost { | ||||
|     data: Json<PhysicalHost>, | ||||
|     id: Id, | ||||
|     version_id: Id, | ||||
| } | ||||
| @ -1,6 +1,4 @@ | ||||
| pub mod executors; | ||||
| pub mod hp_ilo; | ||||
| pub mod intel_amt; | ||||
| pub mod inventory; | ||||
| pub mod opnsense; | ||||
| mod sqlx; | ||||
|  | ||||
| @ -1,14 +1,13 @@ | ||||
| use async_trait::async_trait; | ||||
| use harmony_types::net::MacAddress; | ||||
| use log::info; | ||||
| use log::debug; | ||||
| 
 | ||||
| use crate::{ | ||||
|     executors::ExecutorError, | ||||
|     topology::{DHCPStaticEntry, DhcpServer, LogicalHost, PxeOptions}, | ||||
|     topology::{DHCPStaticEntry, DhcpServer, IpAddress, LogicalHost}, | ||||
| }; | ||||
| 
 | ||||
| use super::OPNSenseFirewall; | ||||
| use harmony_types::net::IpAddress; | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl DhcpServer for OPNSenseFirewall { | ||||
| @ -27,7 +26,7 @@ impl DhcpServer for OPNSenseFirewall { | ||||
|                 .unwrap(); | ||||
|         } | ||||
| 
 | ||||
|         info!("Registered {:?}", entry); | ||||
|         debug!("Registered {:?}", entry); | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
| @ -47,25 +46,57 @@ impl DhcpServer for OPNSenseFirewall { | ||||
|         self.host.clone() | ||||
|     } | ||||
| 
 | ||||
|     async fn set_pxe_options(&self, options: PxeOptions) -> Result<(), ExecutorError> { | ||||
|         let mut writable_opnsense = self.opnsense_config.write().await; | ||||
|         let PxeOptions { | ||||
|             ipxe_filename, | ||||
|             bios_filename, | ||||
|             efi_filename, | ||||
|             tftp_ip, | ||||
|         } = options; | ||||
|         writable_opnsense | ||||
|             .dhcp() | ||||
|             .set_pxe_options( | ||||
|                 tftp_ip.map(|i| i.to_string()), | ||||
|                 bios_filename, | ||||
|                 efi_filename, | ||||
|                 ipxe_filename, | ||||
|             ) | ||||
|             .await | ||||
|             .map_err(|dhcp_error| { | ||||
|                 ExecutorError::UnexpectedError(format!("Failed to set_pxe_options : {dhcp_error}")) | ||||
|             }) | ||||
|     async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> { | ||||
|         let ipv4 = match ip { | ||||
|             std::net::IpAddr::V4(ipv4_addr) => ipv4_addr, | ||||
|             std::net::IpAddr::V6(_) => todo!("ipv6 not supported yet"), | ||||
|         }; | ||||
|         { | ||||
|             let mut writable_opnsense = self.opnsense_config.write().await; | ||||
|             writable_opnsense.dhcp().set_next_server(ipv4); | ||||
|             debug!("OPNsense dhcp server set next server {ipv4}"); | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> { | ||||
|         { | ||||
|             let mut writable_opnsense = self.opnsense_config.write().await; | ||||
|             writable_opnsense.dhcp().set_boot_filename(boot_filename); | ||||
|             debug!("OPNsense dhcp server set boot filename {boot_filename}"); | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> { | ||||
|         { | ||||
|             let mut writable_opnsense = self.opnsense_config.write().await; | ||||
|             writable_opnsense.dhcp().set_filename(filename); | ||||
|             debug!("OPNsense dhcp server set filename {filename}"); | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn set_filename64(&self, filename: &str) -> Result<(), ExecutorError> { | ||||
|         { | ||||
|             let mut writable_opnsense = self.opnsense_config.write().await; | ||||
|             writable_opnsense.dhcp().set_filename64(filename); | ||||
|             debug!("OPNsense dhcp server set filename {filename}"); | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError> { | ||||
|         { | ||||
|             let mut writable_opnsense = self.opnsense_config.write().await; | ||||
|             writable_opnsense.dhcp().set_filenameipxe(filenameipxe); | ||||
|             debug!("OPNsense dhcp server set filenameipxe {filenameipxe}"); | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| use crate::infra::opnsense::Host; | ||||
| use crate::infra::opnsense::IpAddress; | ||||
| use crate::infra::opnsense::LogicalHost; | ||||
| use crate::{ | ||||
|     executors::ExecutorError, | ||||
|     topology::{DnsRecord, DnsServer}, | ||||
| }; | ||||
| use async_trait::async_trait; | ||||
| use harmony_types::net::IpAddress; | ||||
| 
 | ||||
| use super::OPNSenseFirewall; | ||||
| 
 | ||||
|  | ||||
| @ -1,10 +1,9 @@ | ||||
| use crate::{ | ||||
|     executors::ExecutorError, | ||||
|     topology::{Firewall, FirewallRule, LogicalHost}, | ||||
|     topology::{Firewall, FirewallRule, IpAddress, LogicalHost}, | ||||
| }; | ||||
| 
 | ||||
| use super::OPNSenseFirewall; | ||||
| use harmony_types::net::IpAddress; | ||||
| 
 | ||||
| impl Firewall for OPNSenseFirewall { | ||||
|     fn add_rule(&mut self, _rule: FirewallRule) -> Result<(), ExecutorError> { | ||||
|  | ||||
| @ -1,22 +1,24 @@ | ||||
| use async_trait::async_trait; | ||||
| use log::info; | ||||
| 
 | ||||
| use crate::{data::FileContent, executors::ExecutorError, topology::HttpServer}; | ||||
| use crate::{ | ||||
|     executors::ExecutorError, | ||||
|     topology::{HttpServer, IpAddress, Url}, | ||||
| }; | ||||
| 
 | ||||
| use super::OPNSenseFirewall; | ||||
| use harmony_types::net::IpAddress; | ||||
| use harmony_types::net::Url; | ||||
| const OPNSENSE_HTTP_ROOT_PATH: &str = "/usr/local/http"; | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl HttpServer for OPNSenseFirewall { | ||||
|     async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> { | ||||
|         let http_root_path = "/usr/local/http"; | ||||
| 
 | ||||
|         let config = self.opnsense_config.read().await; | ||||
|         info!("Uploading files from url {url} to {OPNSENSE_HTTP_ROOT_PATH}"); | ||||
|         info!("Uploading files from url {url} to {http_root_path}"); | ||||
|         match url { | ||||
|             Url::LocalFolder(path) => { | ||||
|                 config | ||||
|                     .upload_files(path, OPNSENSE_HTTP_ROOT_PATH) | ||||
|                     .upload_files(path, http_root_path) | ||||
|                     .await | ||||
|                     .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; | ||||
|             } | ||||
| @ -25,29 +27,8 @@ impl HttpServer for OPNSenseFirewall { | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> { | ||||
|         let path = match &file.path { | ||||
|             crate::data::FilePath::Relative(path) => { | ||||
|                 format!("{OPNSENSE_HTTP_ROOT_PATH}/{}", path.to_string()) | ||||
|             } | ||||
|             crate::data::FilePath::Absolute(path) => { | ||||
|                 return Err(ExecutorError::ConfigurationError(format!( | ||||
|                     "Cannot serve file from http server with absolute path : {path}" | ||||
|                 ))); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         let config = self.opnsense_config.read().await; | ||||
|         info!("Uploading file content to {}", path); | ||||
|         config | ||||
|             .upload_file_content(&path, &file.content) | ||||
|             .await | ||||
|             .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn get_ip(&self) -> IpAddress { | ||||
|         OPNSenseFirewall::get_ip(self) | ||||
|         todo!(); | ||||
|     } | ||||
| 
 | ||||
|     async fn commit_config(&self) -> Result<(), ExecutorError> { | ||||
|  | ||||
| @ -6,11 +6,10 @@ use uuid::Uuid; | ||||
| use crate::{ | ||||
|     executors::ExecutorError, | ||||
|     topology::{ | ||||
|         BackendServer, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService, | ||||
|         LogicalHost, | ||||
|         BackendServer, HealthCheck, HttpMethod, HttpStatusCode, IpAddress, LoadBalancer, | ||||
|         LoadBalancerService, LogicalHost, | ||||
|     }, | ||||
| }; | ||||
| use harmony_types::net::IpAddress; | ||||
| 
 | ||||
| use super::OPNSenseFirewall; | ||||
| 
 | ||||
|  | ||||
| @ -11,8 +11,10 @@ pub use management::*; | ||||
| use opnsense_config_xml::Host; | ||||
| use tokio::sync::RwLock; | ||||
| 
 | ||||
| use crate::{executors::ExecutorError, topology::LogicalHost}; | ||||
| use harmony_types::net::IpAddress; | ||||
| use crate::{ | ||||
|     executors::ExecutorError, | ||||
|     topology::{IpAddress, LogicalHost}, | ||||
| }; | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct OPNSenseFirewall { | ||||
|  | ||||
| @ -1,9 +1,10 @@ | ||||
| use async_trait::async_trait; | ||||
| use log::info; | ||||
| 
 | ||||
| use crate::{executors::ExecutorError, topology::TftpServer}; | ||||
| use harmony_types::net::IpAddress; | ||||
| use harmony_types::net::Url; | ||||
| use crate::{ | ||||
|     executors::ExecutorError, | ||||
|     topology::{IpAddress, TftpServer, Url}, | ||||
| }; | ||||
| 
 | ||||
| use super::OPNSenseFirewall; | ||||
| 
 | ||||
| @ -27,7 +28,7 @@ impl TftpServer for OPNSenseFirewall { | ||||
|     } | ||||
| 
 | ||||
|     fn get_ip(&self) -> IpAddress { | ||||
|         OPNSenseFirewall::get_ip(self) | ||||
|         todo!() | ||||
|     } | ||||
| 
 | ||||
|     async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError> { | ||||
|  | ||||
| @ -1,36 +0,0 @@ | ||||
| use crate::inventory::RepoError; | ||||
| 
 | ||||
| impl From<sqlx::Error> for RepoError { | ||||
|     fn from(value: sqlx::Error) -> Self { | ||||
|         match value { | ||||
|             sqlx::Error::Configuration(_) | ||||
|             | sqlx::Error::Io(_) | ||||
|             | sqlx::Error::Tls(_) | ||||
|             | sqlx::Error::Protocol(_) | ||||
|             | sqlx::Error::PoolTimedOut | ||||
|             | sqlx::Error::PoolClosed | ||||
|             | sqlx::Error::WorkerCrashed => RepoError::ConnectionFailed(value.to_string()), | ||||
|             sqlx::Error::InvalidArgument(_) | ||||
|             | sqlx::Error::Database(_) | ||||
|             | sqlx::Error::RowNotFound | ||||
|             | sqlx::Error::TypeNotFound { .. } | ||||
|             | sqlx::Error::ColumnIndexOutOfBounds { .. } | ||||
|             | sqlx::Error::ColumnNotFound(_) | ||||
|             | sqlx::Error::AnyDriverError(_) | ||||
|             | sqlx::Error::Migrate(_) | ||||
|             | sqlx::Error::InvalidSavePointStatement | ||||
|             | sqlx::Error::BeginFailed => RepoError::QueryFailed(value.to_string()), | ||||
|             sqlx::Error::Encode(_) => RepoError::Serialization(value.to_string()), | ||||
|             sqlx::Error::Decode(_) | sqlx::Error::ColumnDecode { .. } => { | ||||
|                 RepoError::Deserialization(value.to_string()) | ||||
|             } | ||||
|             _ => RepoError::QueryFailed(value.to_string()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<serde_json::Error> for RepoError { | ||||
|     fn from(value: serde_json::Error) -> Self { | ||||
|         RepoError::Serialization(value.to_string()) | ||||
|     } | ||||
| } | ||||
| @ -4,14 +4,13 @@ use serde::Serialize; | ||||
| use std::str::FromStr; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     modules::helm::chart::{HelmChartScore, HelmRepository}, | ||||
|     score::Score, | ||||
|     topology::{HelmCommand, K8sclient, Topology}, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| use super::ArgoApplication; | ||||
| 
 | ||||
|  | ||||
| @ -11,7 +11,7 @@ use crate::{ | ||||
|         alert_channel::webhook_receiver::WebhookReceiver, ntfy::ntfy::NtfyScore, | ||||
|     }, | ||||
|     score::Score, | ||||
|     topology::{HelmCommand, K8sclient, Topology, tenant::TenantManager}, | ||||
|     topology::{HelmCommand, K8sclient, Topology, Url, tenant::TenantManager}, | ||||
| }; | ||||
| use crate::{ | ||||
|     modules::prometheus::prometheus::PrometheusApplicationMonitoring, | ||||
| @ -19,7 +19,6 @@ use crate::{ | ||||
| }; | ||||
| use async_trait::async_trait; | ||||
| use base64::{Engine as _, engine::general_purpose}; | ||||
| use harmony_types::net::Url; | ||||
| use log::{debug, info}; | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
|  | ||||
| @ -13,13 +13,12 @@ use async_trait::async_trait; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     instrumentation::{self, HarmonyEvent}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     topology::Topology, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[derive(Clone, Debug)] | ||||
| pub enum ApplicationFeatureStatus { | ||||
|  | ||||
| @ -17,8 +17,10 @@ use tar::{Archive, Builder, Header}; | ||||
| use walkdir::WalkDir; | ||||
| 
 | ||||
| use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; | ||||
| use crate::{score::Score, topology::Topology}; | ||||
| use harmony_types::net::Url; | ||||
| use crate::{ | ||||
|     score::Score, | ||||
|     topology::{Topology, Url}, | ||||
| }; | ||||
| 
 | ||||
| use super::{Application, ApplicationFeature, ApplicationInterpret, HelmPackage, OCICompliant}; | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| use async_trait::async_trait; | ||||
| use derive_new::new; | ||||
| use harmony_types::id::Id; | ||||
| use log::info; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| @ -8,11 +7,10 @@ use crate::{ | ||||
|     domain::{data::Version, interpret::InterpretStatus}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     topology::{DHCPStaticEntry, DhcpServer, HostBinding, PxeOptions, Topology}, | ||||
|     topology::{DHCPStaticEntry, DhcpServer, HostBinding, IpAddress, Topology}, | ||||
| }; | ||||
| 
 | ||||
| use crate::domain::score::Score; | ||||
| use harmony_types::net::IpAddress; | ||||
| 
 | ||||
| #[derive(Debug, new, Clone, Serialize)] | ||||
| pub struct DhcpScore { | ||||
| @ -100,14 +98,69 @@ impl DhcpInterpret { | ||||
|         _inventory: &Inventory, | ||||
|         dhcp_server: &D, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         let pxe_options = PxeOptions { | ||||
|             ipxe_filename: self.score.filenameipxe.clone().unwrap_or_default(), | ||||
|             bios_filename: self.score.filename.clone().unwrap_or_default(), | ||||
|             efi_filename: self.score.filename64.clone().unwrap_or_default(), | ||||
|             tftp_ip: self.score.next_server, | ||||
|         let next_server_outcome = match self.score.next_server { | ||||
|             Some(next_server) => { | ||||
|                 dhcp_server.set_next_server(next_server).await?; | ||||
|                 Outcome::new( | ||||
|                     InterpretStatus::SUCCESS, | ||||
|                     format!("Dhcp Interpret Set next boot to {next_server}"), | ||||
|                 ) | ||||
|             } | ||||
|             None => Outcome::noop(), | ||||
|         }; | ||||
| 
 | ||||
|         dhcp_server.set_pxe_options(pxe_options).await?; | ||||
|         let boot_filename_outcome = match &self.score.boot_filename { | ||||
|             Some(boot_filename) => { | ||||
|                 dhcp_server.set_boot_filename(boot_filename).await?; | ||||
|                 Outcome::new( | ||||
|                     InterpretStatus::SUCCESS, | ||||
|                     format!("Dhcp Interpret Set boot filename to {boot_filename}"), | ||||
|                 ) | ||||
|             } | ||||
|             None => Outcome::noop(), | ||||
|         }; | ||||
| 
 | ||||
|         let filename_outcome = match &self.score.filename { | ||||
|             Some(filename) => { | ||||
|                 dhcp_server.set_filename(filename).await?; | ||||
|                 Outcome::new( | ||||
|                     InterpretStatus::SUCCESS, | ||||
|                     format!("Dhcp Interpret Set filename to {filename}"), | ||||
|                 ) | ||||
|             } | ||||
|             None => Outcome::noop(), | ||||
|         }; | ||||
| 
 | ||||
|         let filename64_outcome = match &self.score.filename64 { | ||||
|             Some(filename64) => { | ||||
|                 dhcp_server.set_filename64(filename64).await?; | ||||
|                 Outcome::new( | ||||
|                     InterpretStatus::SUCCESS, | ||||
|                     format!("Dhcp Interpret Set filename64 to {filename64}"), | ||||
|                 ) | ||||
|             } | ||||
|             None => Outcome::noop(), | ||||
|         }; | ||||
| 
 | ||||
|         let filenameipxe_outcome = match &self.score.filenameipxe { | ||||
|             Some(filenameipxe) => { | ||||
|                 dhcp_server.set_filenameipxe(filenameipxe).await?; | ||||
|                 Outcome::new( | ||||
|                     InterpretStatus::SUCCESS, | ||||
|                     format!("Dhcp Interpret Set filenameipxe to {filenameipxe}"), | ||||
|                 ) | ||||
|             } | ||||
|             None => Outcome::noop(), | ||||
|         }; | ||||
| 
 | ||||
|         if next_server_outcome.status == InterpretStatus::NOOP | ||||
|             && boot_filename_outcome.status == InterpretStatus::NOOP | ||||
|             && filename_outcome.status == InterpretStatus::NOOP | ||||
|             && filename64_outcome.status == InterpretStatus::NOOP | ||||
|             && filenameipxe_outcome.status == InterpretStatus::NOOP | ||||
|         { | ||||
|             return Ok(Outcome::noop()); | ||||
|         } | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
| @ -137,7 +190,7 @@ impl<T: DhcpServer> Interpret<T> for DhcpInterpret { | ||||
|         self.status.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<Id> { | ||||
|     fn get_children(&self) -> Vec<crate::domain::data::Id> { | ||||
|         todo!() | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| use async_trait::async_trait; | ||||
| use derive_new::new; | ||||
| use harmony_types::id::Id; | ||||
| use log::info; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| @ -92,7 +91,7 @@ impl<T: Topology + DnsServer> Interpret<T> for DnsInterpret { | ||||
|         self.status.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<Id> { | ||||
|     fn get_children(&self) -> Vec<crate::domain::data::Id> { | ||||
|         todo!() | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| use async_trait::async_trait; | ||||
| use harmony_types::id::Id; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| use crate::{ | ||||
| @ -68,7 +67,7 @@ impl<T: Topology> Interpret<T> for DummyInterpret { | ||||
|         self.status.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<Id> { | ||||
|     fn get_children(&self) -> Vec<crate::domain::data::Id> { | ||||
|         todo!() | ||||
|     } | ||||
| 
 | ||||
| @ -114,7 +113,7 @@ impl<T: Topology> Interpret<T> for PanicInterpret { | ||||
|         InterpretStatus::QUEUED | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<Id> { | ||||
|     fn get_children(&self) -> Vec<crate::domain::data::Id> { | ||||
|         todo!() | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,10 +1,9 @@ | ||||
| use crate::data::Version; | ||||
| use crate::data::{Id, Version}; | ||||
| use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}; | ||||
| use crate::inventory::Inventory; | ||||
| use crate::score::Score; | ||||
| use crate::topology::{HelmCommand, Topology}; | ||||
| use async_trait::async_trait; | ||||
| use harmony_types::id::Id; | ||||
| use helm_wrapper_rs; | ||||
| use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor}; | ||||
| use log::{debug, info, warn}; | ||||
|  | ||||
| @ -8,12 +8,11 @@ use std::process::{Command, Output}; | ||||
| use temp_dir::{self, TempDir}; | ||||
| use temp_file::TempFile; | ||||
| 
 | ||||
| use crate::data::Version; | ||||
| use crate::data::{Id, Version}; | ||||
| use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}; | ||||
| use crate::inventory::Inventory; | ||||
| use crate::score::Score; | ||||
| use crate::topology::{HelmCommand, K8sclient, Topology}; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct HelmCommandExecutor { | ||||
|  | ||||
| @ -3,14 +3,12 @@ use derive_new::new; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::{FileContent, Version}, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
|     topology::{HttpServer, Topology}, | ||||
|     topology::{HttpServer, Topology, Url}, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| use harmony_types::net::Url; | ||||
| 
 | ||||
| /// Configure an HTTP server that is provided by the Topology
 | ||||
| ///
 | ||||
| @ -25,8 +23,7 @@ use harmony_types::net::Url; | ||||
| /// ```
 | ||||
| #[derive(Debug, new, Clone, Serialize)] | ||||
| pub struct StaticFilesHttpScore { | ||||
|     pub folder_to_serve: Option<Url>, | ||||
|     pub files: Vec<FileContent>, | ||||
|     files_to_serve: Url, | ||||
| } | ||||
| 
 | ||||
| impl<T: Topology + HttpServer> Score<T> for StaticFilesHttpScore { | ||||
| @ -53,25 +50,12 @@ impl<T: Topology + HttpServer> Interpret<T> for StaticFilesHttpInterpret { | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         http_server.ensure_initialized().await?; | ||||
|         // http_server.set_ip(topology.router.get_gateway()).await?;
 | ||||
|         if let Some(folder) = self.score.folder_to_serve.as_ref() { | ||||
|             http_server.serve_files(folder).await?; | ||||
|         } | ||||
| 
 | ||||
|         for f in self.score.files.iter() { | ||||
|             http_server.serve_file_content(&f).await? | ||||
|         } | ||||
| 
 | ||||
|         http_server.serve_files(&self.score.files_to_serve).await?; | ||||
|         http_server.commit_config().await?; | ||||
|         http_server.reload_restart().await?; | ||||
|         Ok(Outcome::success(format!( | ||||
|             "Http Server running and serving files from folder {:?} and content for {}", | ||||
|             self.score.folder_to_serve, | ||||
|             self.score | ||||
|                 .files | ||||
|                 .iter() | ||||
|                 .map(|f| f.path.to_string()) | ||||
|                 .collect::<Vec<String>>() | ||||
|                 .join(",") | ||||
|             "Http Server running and serving files from {}", | ||||
|             self.score.files_to_serve | ||||
|         ))) | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,146 +0,0 @@ | ||||
| use async_trait::async_trait; | ||||
| use harmony_inventory_agent::local_presence::DiscoveryEvent; | ||||
| use log::{debug, info, trace}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     hardware::{HostCategory, Label, PhysicalHost}, | ||||
|     infra::inventory::InventoryRepositoryFactory, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
|     topology::Topology, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| /// This launches an harmony_inventory_agent discovery process
 | ||||
| /// This will allow us to register/update hosts running harmony_inventory_agent
 | ||||
| /// from LAN in the Harmony inventory
 | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct DiscoverInventoryAgentScore { | ||||
|     pub discovery_timeout: Option<u64>, | ||||
| } | ||||
| 
 | ||||
| impl<T: Topology> Score<T> for DiscoverInventoryAgentScore { | ||||
|     fn name(&self) -> String { | ||||
|         "DiscoverInventoryAgentScore".to_string() | ||||
|     } | ||||
| 
 | ||||
|     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||
|         Box::new(DiscoverInventoryAgentInterpret { | ||||
|             score: self.clone(), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| struct DiscoverInventoryAgentInterpret { | ||||
|     score: DiscoverInventoryAgentScore, | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret { | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         _inventory: &Inventory, | ||||
|         _topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         harmony_inventory_agent::local_presence::discover_agents( | ||||
|             self.score.discovery_timeout, | ||||
|             |event: DiscoveryEvent| -> Result<(), String> { | ||||
|                 debug!("Discovery event {event:?}"); | ||||
|                 match event { | ||||
|                     DiscoveryEvent::ServiceResolved(service) => { | ||||
|                         let service_name = service.fullname.clone(); | ||||
|                         info!("Found service {service_name}"); | ||||
| 
 | ||||
|                         let address = match service.get_addresses().iter().next() { | ||||
|                             Some(address) => address, | ||||
|                             None => { | ||||
|                                 return Err(format!( | ||||
|                                     "Could not find address for service {service_name}" | ||||
|                                 )); | ||||
|                             } | ||||
|                         }; | ||||
| 
 | ||||
|                         let address = address.to_string(); | ||||
|                         let port = service.get_port(); | ||||
| 
 | ||||
|                         tokio::task::spawn(async move { | ||||
|                             info!("Getting inventory for host {address} at port {port}"); | ||||
|                             let host = | ||||
|                                 harmony_inventory_agent::client::get_host_inventory(&address, port) | ||||
|                                     .await | ||||
|                                     .unwrap(); | ||||
| 
 | ||||
|                             trace!("Found host information {host:?}"); | ||||
|                             // TODO its useless to have two distinct host types but requires a bit much
 | ||||
|                             // refactoring to do it now
 | ||||
|                             let harmony_inventory_agent::hwinfo::PhysicalHost { | ||||
|                                 storage_drives, | ||||
|                                 storage_controller, | ||||
|                                 memory_modules, | ||||
|                                 cpus, | ||||
|                                 chipset, | ||||
|                                 network_interfaces, | ||||
|                                 management_interface, | ||||
|                                 host_uuid, | ||||
|                             } = host; | ||||
| 
 | ||||
|                             let host = PhysicalHost { | ||||
|                                 id: Id::from(host_uuid), | ||||
|                                 category: HostCategory::Server, | ||||
|                                 network: network_interfaces, | ||||
|                                 storage: storage_drives, | ||||
|                                 labels: vec![Label { | ||||
|                                     name: "discovered-by".to_string(), | ||||
|                                     value: "harmony-inventory-agent".to_string(), | ||||
|                                 }], | ||||
|                                 memory_modules, | ||||
|                                 cpus, | ||||
|                             }; | ||||
| 
 | ||||
|                             let repo = InventoryRepositoryFactory::build() | ||||
|                                 .await | ||||
|                                 .map_err(|e| format!("Could not build repository : {e}")) | ||||
|                                 .unwrap(); | ||||
|                             repo.save(&host) | ||||
|                                 .await | ||||
|                                 .map_err(|e| format!("Could not save host : {e}")) | ||||
|                                 .unwrap(); | ||||
|                             info!( | ||||
|                                 "Saved new host id {}, summary : {}", | ||||
|                                 host.id, | ||||
|                                 host.summary() | ||||
|                             ); | ||||
|                         }); | ||||
|                     } | ||||
|                     _ => debug!("Unhandled event {event:?}"), | ||||
|                 }; | ||||
|                 Ok(()) | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|         Ok(Outcome { | ||||
|             status: InterpretStatus::SUCCESS, | ||||
|             message: "Discovery process completed successfully".to_string(), | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fn get_name(&self) -> InterpretName { | ||||
|         InterpretName::DiscoverInventoryAgent | ||||
|     } | ||||
| 
 | ||||
|     fn get_version(&self) -> Version { | ||||
|         todo!() | ||||
|     } | ||||
| 
 | ||||
|     fn get_status(&self) -> InterpretStatus { | ||||
|         todo!() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<Id> { | ||||
|         todo!() | ||||
|     } | ||||
| } | ||||
| @ -3,13 +3,12 @@ use derive_new::new; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
|     topology::Topology, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[derive(Debug, new, Clone, Serialize)] | ||||
| pub struct IpxeScore { | ||||
|  | ||||
| @ -6,13 +6,12 @@ use serde::Serialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|     config::HARMONY_DATA_DIR, | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
|     topology::Topology, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| pub struct K3DInstallationScore { | ||||
|  | ||||
| @ -5,13 +5,12 @@ use log::info; | ||||
| use serde::{Serialize, de::DeserializeOwned}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
|     topology::{K8sclient, Topology}, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| pub struct K8sResourceScore<K: Resource + std::fmt::Debug> { | ||||
|  | ||||
| @ -3,7 +3,6 @@ use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR | ||||
| use dockerfile_builder::{Dockerfile, instruction_builder::EnvBuilder}; | ||||
| use fqdn::fqdn; | ||||
| use harmony_macros::ingress_path; | ||||
| use harmony_types::net::Url; | ||||
| use non_blank_string_rs::NonBlankString; | ||||
| use serde_json::json; | ||||
| use std::collections::HashMap; | ||||
| @ -19,14 +18,13 @@ use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; | ||||
| use crate::modules::k8s::ingress::K8sIngressScore; | ||||
| use crate::topology::HelmCommand; | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     modules::k8s::deployment::K8sDeploymentScore, | ||||
|     score::Score, | ||||
|     topology::{K8sclient, Topology}, | ||||
|     topology::{K8sclient, Topology, Url}, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| use super::helm::chart::HelmChartScore; | ||||
| 
 | ||||
|  | ||||
| @ -3,13 +3,12 @@ use log::info; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
|     topology::{LoadBalancer, LoadBalancerService, Topology}, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| pub struct LoadBalancerScore { | ||||
|  | ||||
| @ -5,7 +5,6 @@ pub mod dns; | ||||
| pub mod dummy; | ||||
| pub mod helm; | ||||
| pub mod http; | ||||
| pub mod inventory; | ||||
| pub mod ipxe; | ||||
| pub mod k3d; | ||||
| pub mod k8s; | ||||
|  | ||||
| @ -20,9 +20,8 @@ use crate::{ | ||||
|         }, | ||||
|         prometheus::prometheus::{Prometheus, PrometheusReceiver}, | ||||
|     }, | ||||
|     topology::oberservability::monitoring::AlertReceiver, | ||||
|     topology::{Url, oberservability::monitoring::AlertReceiver}, | ||||
| }; | ||||
| use harmony_types::net::Url; | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| pub struct DiscordWebhook { | ||||
|  | ||||
| @ -19,9 +19,8 @@ use crate::{ | ||||
|         }, | ||||
|         prometheus::prometheus::{Prometheus, PrometheusReceiver}, | ||||
|     }, | ||||
|     topology::oberservability::monitoring::AlertReceiver, | ||||
|     topology::{Url, oberservability::monitoring::AlertReceiver}, | ||||
| }; | ||||
| use harmony_types::net::Url; | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| pub struct WebhookReceiver { | ||||
|  | ||||
| @ -4,7 +4,7 @@ use async_trait::async_trait; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     modules::{ | ||||
| @ -15,7 +15,6 @@ use crate::{ | ||||
|     score::Score, | ||||
|     topology::{PreparationOutcome, Topology, oberservability::monitoring::AlertReceiver}, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| pub struct ApplicationMonitoringScore { | ||||
|  | ||||
| @ -6,14 +6,13 @@ use serde::Serialize; | ||||
| use strum::{Display, EnumString}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     modules::monitoring::ntfy::helm::ntfy_helm_chart::ntfy_helm_chart_score, | ||||
|     score::Score, | ||||
|     topology::{HelmCommand, K8sclient, MultiTargetTopology, Topology, k8s::K8sClient}, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| pub struct NtfyScore { | ||||
| @ -38,6 +37,18 @@ pub struct NtfyInterpret { | ||||
|     pub score: NtfyScore, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, EnumString, Display)] | ||||
| enum NtfyAccessMode { | ||||
|     #[strum(serialize = "read-write", serialize = "rw")] | ||||
|     ReadWrite, | ||||
|     #[strum(serialize = "read-only", serialize = "ro", serialize = "read")] | ||||
|     ReadOnly, | ||||
|     #[strum(serialize = "write-only", serialize = "wo", serialize = "write")] | ||||
|     WriteOnly, | ||||
|     #[strum(serialize = "deny", serialize = "none")] | ||||
|     Deny, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, EnumString, Display)] | ||||
| enum NtfyRole { | ||||
|     #[strum(serialize = "user")] | ||||
|  | ||||
| @ -1,150 +0,0 @@ | ||||
| use askama::Template; | ||||
| use async_trait::async_trait; | ||||
| use derive_new::new; | ||||
| use harmony_types::net::Url; | ||||
| use serde::Serialize; | ||||
| use std::net::IpAddr; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::{FileContent, FilePath, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     modules::{dhcp::DhcpScore, http::StaticFilesHttpScore, tftp::TftpScore}, | ||||
|     score::Score, | ||||
|     topology::{DhcpServer, HttpServer, Router, TftpServer, Topology}, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[derive(Debug, new, Clone, Serialize)] | ||||
| pub struct OkdIpxeScore { | ||||
|     pub kickstart_filename: String, | ||||
|     pub harmony_inventory_agent: String, | ||||
|     pub cluster_pubkey_filename: String, | ||||
| } | ||||
| 
 | ||||
| impl<T: Topology + DhcpServer + TftpServer + HttpServer + Router> Score<T> for OkdIpxeScore { | ||||
|     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||
|         Box::new(IpxeInterpret::new(self.clone())) | ||||
|     } | ||||
| 
 | ||||
|     fn name(&self) -> String { | ||||
|         "OkdIpxeScore".to_string() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, new, Clone)] | ||||
| pub struct IpxeInterpret { | ||||
|     score: OkdIpxeScore, | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology + DhcpServer + TftpServer + HttpServer + Router> Interpret<T> for IpxeInterpret { | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         let gateway_ip = topology.get_gateway(); | ||||
| 
 | ||||
|         let scores: Vec<Box<dyn Score<T>>> = vec![ | ||||
|             Box::new(DhcpScore { | ||||
|                 host_binding: vec![], | ||||
|                 next_server: Some(topology.get_gateway()), | ||||
|                 boot_filename: None, | ||||
|                 filename: Some("undionly.kpxe".to_string()), | ||||
|                 filename64: Some("ipxe.efi".to_string()), | ||||
|                 filenameipxe: Some(format!("http://{gateway_ip}:8080/boot.ipxe").to_string()), | ||||
|             }), | ||||
|             Box::new(TftpScore { | ||||
|                 files_to_serve: Url::LocalFolder("./data/pxe/okd/tftpboot/".to_string()), | ||||
|             }), | ||||
|             Box::new(StaticFilesHttpScore { | ||||
|                 // TODO The current russh based copy is way too slow, check for a lib update or use scp
 | ||||
|                 // when available
 | ||||
|                 //
 | ||||
|                 // For now just run :
 | ||||
|                 // scp -r data/pxe/okd/http_files/* root@192.168.1.1:/usr/local/http/
 | ||||
|                 //
 | ||||
|                 folder_to_serve: None, | ||||
|                 // folder_to_serve: Some(Url::LocalFolder("./data/pxe/okd/http_files/".to_string())),
 | ||||
|                 files: vec![ | ||||
|                     FileContent { | ||||
|                         path: FilePath::Relative("boot.ipxe".to_string()), | ||||
|                         content: BootIpxeTpl { | ||||
|                             gateway_ip: &gateway_ip, | ||||
|                         } | ||||
|                         .to_string(), | ||||
|                     }, | ||||
|                     FileContent { | ||||
|                         path: FilePath::Relative(self.score.kickstart_filename.clone()), | ||||
|                         content: InventoryKickstartTpl { | ||||
|                             gateway_ip: &gateway_ip, | ||||
|                             harmony_inventory_agent: &self.score.harmony_inventory_agent, | ||||
|                             cluster_pubkey_filename: &self.score.cluster_pubkey_filename, | ||||
|                         } | ||||
|                         .to_string(), | ||||
|                     }, | ||||
|                     FileContent { | ||||
|                         path: FilePath::Relative("fallback.ipxe".to_string()), | ||||
|                         content: FallbackIpxeTpl { | ||||
|                             gateway_ip: &gateway_ip, | ||||
|                             kickstart_filename: &self.score.kickstart_filename, | ||||
|                         } | ||||
|                         .to_string(), | ||||
|                     }, | ||||
|                 ], | ||||
|             }), | ||||
|         ]; | ||||
| 
 | ||||
|         for score in scores { | ||||
|             let result = score.interpret(inventory, topology).await; | ||||
|             match result { | ||||
|                 Ok(outcome) => match outcome.status { | ||||
|                     InterpretStatus::SUCCESS => continue, | ||||
|                     InterpretStatus::NOOP => continue, | ||||
|                     _ => return Err(InterpretError::new(outcome.message)), | ||||
|                 }, | ||||
|                 Err(e) => return Err(e), | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         Ok(Outcome::success("Ipxe installed".to_string())) | ||||
|     } | ||||
| 
 | ||||
|     fn get_name(&self) -> InterpretName { | ||||
|         InterpretName::Ipxe | ||||
|     } | ||||
| 
 | ||||
|     fn get_version(&self) -> Version { | ||||
|         todo!() | ||||
|     } | ||||
| 
 | ||||
|     fn get_status(&self) -> InterpretStatus { | ||||
|         todo!() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<Id> { | ||||
|         todo!() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Template)] | ||||
| #[template(path = "boot.ipxe.j2")] | ||||
| struct BootIpxeTpl<'a> { | ||||
|     gateway_ip: &'a IpAddr, | ||||
| } | ||||
| 
 | ||||
| #[derive(Template)] | ||||
| #[template(path = "fallback.ipxe.j2")] | ||||
| struct FallbackIpxeTpl<'a> { | ||||
|     gateway_ip: &'a IpAddr, | ||||
|     kickstart_filename: &'a str, | ||||
| } | ||||
| 
 | ||||
| #[derive(Template)] | ||||
| #[template(path = "inventory.kickstart.j2")] | ||||
| struct InventoryKickstartTpl<'a> { | ||||
|     gateway_ip: &'a IpAddr, | ||||
|     cluster_pubkey_filename: &'a str, | ||||
|     harmony_inventory_agent: &'a str, | ||||
| } | ||||
| @ -2,6 +2,5 @@ pub mod bootstrap_dhcp; | ||||
| pub mod bootstrap_load_balancer; | ||||
| pub mod dhcp; | ||||
| pub mod dns; | ||||
| pub mod ipxe; | ||||
| pub mod load_balancer; | ||||
| pub mod upgrade; | ||||
|  | ||||
| @ -5,13 +5,12 @@ use serde::Serialize; | ||||
| use tokio::sync::RwLock; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
|     topology::Topology, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct OPNsenseShellCommandScore { | ||||
|  | ||||
| @ -24,7 +24,7 @@ use crate::modules::monitoring::kube_prometheus::crd::service_monitor::{ | ||||
| use crate::topology::oberservability::monitoring::AlertReceiver; | ||||
| use crate::topology::{K8sclient, Topology, k8s::K8sClient}; | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     data::{Id, Version}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     modules::monitoring::kube_prometheus::crd::{ | ||||
| @ -37,7 +37,6 @@ use crate::{ | ||||
|     }, | ||||
|     score::Score, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| 
 | ||||
| use super::prometheus::PrometheusApplicationMonitoring; | ||||
| 
 | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user