Compare commits
1 Commits
feat/okd_d
...
16cd63118a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16cd63118a |
2
.gitattributes
vendored
@@ -2,5 +2,3 @@ bootx64.efi filter=lfs diff=lfs merge=lfs -text
|
|||||||
grubx64.efi filter=lfs diff=lfs merge=lfs -text
|
grubx64.efi filter=lfs diff=lfs merge=lfs -text
|
||||||
initrd filter=lfs diff=lfs merge=lfs -text
|
initrd filter=lfs diff=lfs merge=lfs -text
|
||||||
linux filter=lfs diff=lfs merge=lfs -text
|
linux filter=lfs diff=lfs merge=lfs -text
|
||||||
data/okd/bin/* filter=lfs diff=lfs merge=lfs -text
|
|
||||||
data/okd/installer_image/* filter=lfs diff=lfs merge=lfs -text
|
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -3,7 +3,6 @@ private_repos/
|
|||||||
|
|
||||||
### Harmony ###
|
### Harmony ###
|
||||||
harmony.log
|
harmony.log
|
||||||
data/okd/installation_files*
|
|
||||||
|
|
||||||
### Helm ###
|
### Helm ###
|
||||||
# Chart dependencies
|
# Chart dependencies
|
||||||
|
|||||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "examples/try_rust_webapp/tryrust.org"]
|
|
||||||
path = examples/try_rust_webapp/tryrust.org
|
|
||||||
url = https://github.com/rust-dd/tryrust.org.git
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "SELECT host_id FROM host_role_mapping WHERE role = ?",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "host_id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "2ea29df2326f7c84bd4100ad510a3fd4878dc2e217dc83f9bf45a402dfd62a91"
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n SELECT\n p1.id,\n p1.version_id,\n p1.data as \"data: Json<PhysicalHost>\"\n FROM\n physical_hosts p1\n INNER JOIN (\n SELECT\n id,\n MAX(version_id) AS max_version\n FROM\n physical_hosts\n GROUP BY\n id\n ) p2 ON p1.id = p2.id AND p1.version_id = p2.max_version\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version_id",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data: Json<PhysicalHost>",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Blob"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 0
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "8d247918eca10a88b784ee353db090c94a222115c543231f2140cba27bd0f067"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n INSERT INTO host_role_mapping (host_id, role)\n VALUES (?, ?)\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 2
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "df7a7c9cfdd0972e2e0ce7ea444ba8bc9d708a4fb89d5593a0be2bbebde62aff"
|
|
||||||
}
|
|
||||||
748
Cargo.lock
generated
69
README.md
@@ -36,59 +36,48 @@ These principles surface as simple, ergonomic Rust APIs that let teams focus on
|
|||||||
|
|
||||||
## 2 · Quick Start
|
## 2 · Quick Start
|
||||||
|
|
||||||
The snippet below spins up a complete **production-grade Rust + Leptos Webapp** with monitoring. Swap it for your own scores to deploy anything from microservices to machine-learning pipelines.
|
The snippet below spins up a complete **production-grade LAMP stack** with monitoring. Swap it for your own scores to deploy anything from microservices to machine-learning pipelines.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use harmony::{
|
use harmony::{
|
||||||
|
data::Version,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
|
maestro::Maestro,
|
||||||
modules::{
|
modules::{
|
||||||
application::{
|
lamp::{LAMPConfig, LAMPScore},
|
||||||
ApplicationScore, RustWebFramework, RustWebapp,
|
monitoring::monitoring_alerting::MonitoringAlertingStackScore,
|
||||||
features::{PackagingDeployment, rhob_monitoring::Monitoring},
|
|
||||||
},
|
|
||||||
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
|
||||||
},
|
},
|
||||||
topology::K8sAnywhereTopology,
|
topology::{K8sAnywhereTopology, Url},
|
||||||
};
|
};
|
||||||
use harmony_macros::hurl;
|
|
||||||
use std::{path::PathBuf, sync::Arc};
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let application = Arc::new(RustWebapp {
|
// 1. Describe what you want
|
||||||
name: "harmony-example-leptos".to_string(),
|
let lamp_stack = LAMPScore {
|
||||||
project_root: PathBuf::from(".."), // <== Your project root, usually .. if you use the standard `/harmony` folder
|
name: "harmony-lamp-demo".into(),
|
||||||
framework: Some(RustWebFramework::Leptos),
|
domain: Url::Url(url::Url::parse("https://lampdemo.example.com").unwrap()),
|
||||||
service_port: 8080,
|
php_version: Version::from("8.3.0").unwrap(),
|
||||||
});
|
config: LAMPConfig {
|
||||||
|
project_root: "./php".into(),
|
||||||
// Define your Application deployment and the features you want
|
database_size: "4Gi".into(),
|
||||||
let app = ApplicationScore {
|
..Default::default()
|
||||||
features: vec![
|
},
|
||||||
Box::new(PackagingDeployment {
|
|
||||||
application: application.clone(),
|
|
||||||
}),
|
|
||||||
Box::new(Monitoring {
|
|
||||||
application: application.clone(),
|
|
||||||
alert_receiver: vec![
|
|
||||||
Box::new(DiscordWebhook {
|
|
||||||
name: "test-discord".to_string(),
|
|
||||||
url: hurl!("https://discord.doesnt.exist.com"), // <== Get your discord webhook url
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
application,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 2. Enhance with extra scores (monitoring, CI/CD, …)
|
||||||
|
let mut monitoring = MonitoringAlertingStackScore::new();
|
||||||
|
monitoring.namespace = Some(lamp_stack.config.namespace.clone());
|
||||||
|
|
||||||
|
// 3. Run your scores on the desired topology & inventory
|
||||||
harmony_cli::run(
|
harmony_cli::run(
|
||||||
Inventory::autoload(),
|
Inventory::autoload(), // auto-detect hardware / kube-config
|
||||||
K8sAnywhereTopology::from_env(), // <== Deploy to local automatically provisioned local k3d by default or connect to any kubernetes cluster
|
K8sAnywhereTopology::from_env(), // local k3d, CI, staging, prod…
|
||||||
vec![Box::new(app)],
|
vec![
|
||||||
None,
|
Box::new(lamp_stack),
|
||||||
)
|
Box::new(monitoring)
|
||||||
.await
|
],
|
||||||
.unwrap();
|
None
|
||||||
|
).await.unwrap();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use log::debug;
|
||||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||||
|
|
||||||
use crate::SERVICE_TYPE;
|
use crate::SERVICE_TYPE;
|
||||||
@@ -73,7 +74,7 @@ pub async fn discover() {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _discover_example() {
|
async fn discover_example() {
|
||||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||||
|
|
||||||
// Create a daemon
|
// Create a daemon
|
||||||
|
|||||||
BIN
data/okd/bin/kubectl
(Stored with Git LFS)
BIN
data/okd/bin/oc
(Stored with Git LFS)
BIN
data/okd/bin/oc_README.md
(Stored with Git LFS)
BIN
data/okd/bin/openshift-install
(Stored with Git LFS)
BIN
data/okd/bin/openshift-install_README.md
(Stored with Git LFS)
BIN
data/okd/installer_image/scos-9.0.20250510-0-live-initramfs.x86_64.img
(Stored with Git LFS)
BIN
data/okd/installer_image/scos-9.0.20250510-0-live-kernel.x86_64
(Stored with Git LFS)
BIN
data/okd/installer_image/scos-9.0.20250510-0-live-rootfs.x86_64.img
(Stored with Git LFS)
@@ -1 +0,0 @@
|
|||||||
scos-9.0.20250510-0-live-initramfs.x86_64.img
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
scos-9.0.20250510-0-live-kernel.x86_64
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
scos-9.0.20250510-0-live-rootfs.x86_64.img
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.terraform
|
|
||||||
*.tfstate
|
|
||||||
venv
|
|
||||||
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 100 KiB |
@@ -1,5 +0,0 @@
|
|||||||
To build :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx @marp-team/marp-cli@latest -w slides.md
|
|
||||||
```
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,9 +0,0 @@
|
|||||||
To run this :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
virtualenv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
pip install ansible ansible-dev-tools
|
|
||||||
ansible-lint download.yml
|
|
||||||
ansible-playbook -i localhost download.yml
|
|
||||||
```
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
- name: Test Ansible URL Validation
|
|
||||||
hosts: localhost
|
|
||||||
tasks:
|
|
||||||
- name: Download a file
|
|
||||||
ansible.builtin.get_url:
|
|
||||||
url: "http:/wikipedia.org/"
|
|
||||||
dest: "/tmp/ansible-test/wikipedia.html"
|
|
||||||
mode: '0900'
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
@@ -1,241 +0,0 @@
|
|||||||
---
|
|
||||||
theme: uncover
|
|
||||||
---
|
|
||||||
|
|
||||||
# Voici l'histoire de Petit Poisson
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer.jpg" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./happy_landscape_swimmer.jpg" width="1000"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer.jpg" width="200"/>
|
|
||||||
|
|
||||||
<img src="./tryrust.org.png" width="600"/>
|
|
||||||
|
|
||||||
[https://tryrust.org](https://tryrust.org)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_deploy_prod_1.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_deploy_prod_2.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_deploy_prod_3.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_deploy_prod_4.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Demo time
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_sunglasses.jpg" width="1000"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_download_wikipedia.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./ansible.jpg" width="200"/>
|
|
||||||
|
|
||||||
## Ansible❓
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer.jpg" width="200"/>
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Download wikipedia
|
|
||||||
hosts: localhost
|
|
||||||
tasks:
|
|
||||||
- name: Download a file
|
|
||||||
ansible.builtin.get_url:
|
|
||||||
url: "https:/wikipedia.org/"
|
|
||||||
dest: "/tmp/ansible-test/wikipedia.html"
|
|
||||||
mode: '0900'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer.jpg" width="200"/>
|
|
||||||
|
|
||||||
```
|
|
||||||
ansible-lint download.yml
|
|
||||||
|
|
||||||
Passed: 0 failure(s), 0 warning(s) on 1 files. Last profile that met the validation criteria was 'production'.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```
|
|
||||||
git push
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./75_years_later.jpg" width="1100"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./texto_download_wikipedia_fail.png" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_reversed.jpg" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./ansible_output_fail.jpg" width="1100"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_reversed_1hit.jpg" width="600"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./ansible_crossed_out.jpg" width="400"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
<img src="./terraform.jpg" width="400"/>
|
|
||||||
|
|
||||||
## Terraform❓❗
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_reversed_1hit.jpg" width="200"/>
|
|
||||||
<img src="./terraform.jpg" width="200"/>
|
|
||||||
|
|
||||||
```tf
|
|
||||||
provider "docker" {}
|
|
||||||
|
|
||||||
resource "docker_network" "invalid_network" {
|
|
||||||
name = "my-invalid-network"
|
|
||||||
|
|
||||||
ipam_config {
|
|
||||||
subnet = "172.17.0.0/33"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_reversed_1hit.jpg" width="100"/>
|
|
||||||
<img src="./terraform.jpg" width="200"/>
|
|
||||||
|
|
||||||
```
|
|
||||||
terraform plan
|
|
||||||
|
|
||||||
Terraform used the selected providers to generate the following execution plan.
|
|
||||||
Resource actions are indicated with the following symbols:
|
|
||||||
+ create
|
|
||||||
|
|
||||||
Terraform will perform the following actions:
|
|
||||||
|
|
||||||
# docker_network.invalid_network will be created
|
|
||||||
+ resource "docker_network" "invalid_network" {
|
|
||||||
+ driver = (known after apply)
|
|
||||||
+ id = (known after apply)
|
|
||||||
+ internal = (known after apply)
|
|
||||||
+ ipam_driver = "default"
|
|
||||||
+ name = "my-invalid-network"
|
|
||||||
+ options = (known after apply)
|
|
||||||
+ scope = (known after apply)
|
|
||||||
|
|
||||||
+ ipam_config {
|
|
||||||
+ subnet = "172.17.0.0/33"
|
|
||||||
# (2 unchanged attributes hidden)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```
|
|
||||||
terraform apply
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```
|
|
||||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
|
||||||
|
|
||||||
Do you want to perform these actions?
|
|
||||||
Terraform will perform the actions described above.
|
|
||||||
Only 'yes' will be accepted to approve.
|
|
||||||
|
|
||||||
Enter a value: yes
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```
|
|
||||||
docker_network.invalid_network: Creating...
|
|
||||||
╷
|
|
||||||
│ Error: Unable to create network: Error response from daemon: invalid network config:
|
|
||||||
│ invalid subnet 172.17.0.0/33: invalid CIDR block notation
|
|
||||||
│
|
|
||||||
│ with docker_network.invalid_network,
|
|
||||||
│ on main.tf line 11, in resource "docker_network" "invalid_network":
|
|
||||||
│ 11: resource "docker_network" "invalid_network" {
|
|
||||||
│
|
|
||||||
╵
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer_reversed_fullhit.jpg" width="1100"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./ansible_crossed_out.jpg" width="300"/>
|
|
||||||
<img src="./terraform_crossed_out.jpg" width="400"/>
|
|
||||||
<img src="./Happy_swimmer_reversed_fullhit.jpg" width="300"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Harmony❓❗
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Demo time
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img src="./Happy_swimmer.jpg" width="300"/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🎼
|
|
||||||
|
|
||||||
Harmony : [https://git.nationtech.io/nationtech/harmony](https://git.nationtech.io/nationtech/harmony)
|
|
||||||
|
|
||||||
|
|
||||||
<img src="./qrcode_gitea_nationtech.png" width="120"/>
|
|
||||||
|
|
||||||
|
|
||||||
LinkedIn : [https://www.linkedin.com/in/jean-gabriel-gill-couture/](https://www.linkedin.com/in/jean-gabriel-gill-couture/)
|
|
||||||
|
|
||||||
Courriel : [jg@nationtech.io](mailto:jg@nationtech.io)
|
|
||||||
@@ -25,11 +25,11 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### **Slide 3: The Real Cost of Infrastructure**
|
#### **Slide 3: The Real Cost: Cognitive Fatigue**
|
||||||
|
|
||||||
- **Visual:** "The Jenga Tower of Tools". A tall, precarious Jenga tower where each block is the logo of a different tool (Terraform, K8s, Helm, Ansible, Prometheus, ArgoCD, etc.). One block near the bottom is being nervously pulled out.
|
- **Visual:** "The Jenga Tower of Tools". A tall, precarious Jenga tower where each block is the logo of a different tool (Terraform, K8s, Helm, Ansible, Prometheus, ArgoCD, etc.). One block near the bottom is being nervously pulled out.
|
||||||
- **Narration:**
|
- **Narration:**
|
||||||
"The real cost isn't just complexity; it's the constant need to choose, learn, integrate, and operate a dozen different tools, each with its own syntax and failure modes. It's the nagging fear that a tiny typo in a config file could bring everything down. Click-ops isn't the answer, but the current state of IaC feels like we've traded one problem for another."
|
"The real cost isn't just complexity; it's _cognitive fatigue_. The constant need to choose, learn, integrate, and operate a dozen different tools, each with its own syntax and failure modes. It's the nagging fear that a tiny typo in a config file could bring everything down. Click-ops isn't the answer, but the current state of IaC feels like we've traded one problem for another."
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
- **Visual:** The clean, simple Harmony Rust DSL code from Slide 6. A summary of what was just accomplished is listed next to it: `✓ GitHub to Prod in minutes`, `✓ Type-Safe Validation`, `✓ Built-in Monitoring`, `✓ Automated Multi-Site Failover`.
|
- **Visual:** The clean, simple Harmony Rust DSL code from Slide 6. A summary of what was just accomplished is listed next to it: `✓ GitHub to Prod in minutes`, `✓ Type-Safe Validation`, `✓ Built-in Monitoring`, `✓ Automated Multi-Site Failover`.
|
||||||
- **Narration:**
|
- **Narration:**
|
||||||
"So, in just a few minutes, we went from a simple web app to a multi-site, monitored, and chaos-proof production deployment. We did it with a small amount of code that is easy to read, easy to verify, and completely portable. This is our vision: to offload the complexity, and make infrastructure simple, predictable, and even fun again."
|
"So, in just a few minutes, we went from a simple web app to a multi-site, monitored, and chaos-proof production deployment. We did it with a small amount of code that is easy to read, easy to verify, and completely portable. This is our vision: to offload the complexity, eliminate cognitive fatigue, and make infrastructure simple, predictable, and even fun again."
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,40 +0,0 @@
|
|||||||
# This file is maintained automatically by "terraform init".
|
|
||||||
# Manual edits may be lost in future updates.
|
|
||||||
|
|
||||||
provider "registry.terraform.io/hashicorp/http" {
|
|
||||||
version = "3.5.0"
|
|
||||||
hashes = [
|
|
||||||
"h1:8bUoPwS4hahOvzCBj6b04ObLVFXCEmEN8T/5eOHmWOM=",
|
|
||||||
"zh:047c5b4920751b13425efe0d011b3a23a3be97d02d9c0e3c60985521c9c456b7",
|
|
||||||
"zh:157866f700470207561f6d032d344916b82268ecd0cf8174fb11c0674c8d0736",
|
|
||||||
"zh:1973eb9383b0d83dd4fd5e662f0f16de837d072b64a6b7cd703410d730499476",
|
|
||||||
"zh:212f833a4e6d020840672f6f88273d62a564f44acb0c857b5961cdb3bbc14c90",
|
|
||||||
"zh:2c8034bc039fffaa1d4965ca02a8c6d57301e5fa9fff4773e684b46e3f78e76a",
|
|
||||||
"zh:5df353fc5b2dd31577def9cc1a4ebf0c9a9c2699d223c6b02087a3089c74a1c6",
|
|
||||||
"zh:672083810d4185076c81b16ad13d1224b9e6ea7f4850951d2ab8d30fa6e41f08",
|
|
||||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
||||||
"zh:7b4200f18abdbe39904b03537e1a78f21ebafe60f1c861a44387d314fda69da6",
|
|
||||||
"zh:843feacacd86baed820f81a6c9f7bd32cf302db3d7a0f39e87976ebc7a7cc2ee",
|
|
||||||
"zh:a9ea5096ab91aab260b22e4251c05f08dad2ed77e43e5e4fadcdfd87f2c78926",
|
|
||||||
"zh:d02b288922811739059e90184c7f76d45d07d3a77cc48d0b15fd3db14e928623",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
provider "registry.terraform.io/hashicorp/local" {
|
|
||||||
version = "2.5.3"
|
|
||||||
hashes = [
|
|
||||||
"h1:1Nkh16jQJMp0EuDmvP/96f5Unnir0z12WyDuoR6HjMo=",
|
|
||||||
"zh:284d4b5b572eacd456e605e94372f740f6de27b71b4e1fd49b63745d8ecd4927",
|
|
||||||
"zh:40d9dfc9c549e406b5aab73c023aa485633c1b6b730c933d7bcc2fa67fd1ae6e",
|
|
||||||
"zh:6243509bb208656eb9dc17d3c525c89acdd27f08def427a0dce22d5db90a4c8b",
|
|
||||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
||||||
"zh:885d85869f927853b6fe330e235cd03c337ac3b933b0d9ae827ec32fa1fdcdbf",
|
|
||||||
"zh:bab66af51039bdfcccf85b25fe562cbba2f54f6b3812202f4873ade834ec201d",
|
|
||||||
"zh:c505ff1bf9442a889ac7dca3ac05a8ee6f852e0118dd9a61796a2f6ff4837f09",
|
|
||||||
"zh:d36c0b5770841ddb6eaf0499ba3de48e5d4fc99f4829b6ab66b0fab59b1aaf4f",
|
|
||||||
"zh:ddb6a407c7f3ec63efb4dad5f948b54f7f4434ee1a2607a49680d494b1776fe1",
|
|
||||||
"zh:e0dafdd4500bec23d3ff221e3a9b60621c5273e5df867bc59ef6b7e41f5c91f6",
|
|
||||||
"zh:ece8742fd2882a8fc9d6efd20e2590010d43db386b920b2a9c220cfecc18de47",
|
|
||||||
"zh:f4c6b3eb8f39105004cf720e202f04f57e3578441cfb76ca27611139bc116a82",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
provider "http" {}
|
|
||||||
|
|
||||||
data "http" "remote_file" {
|
|
||||||
url = "http:/example.com/file.txt"
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "local_file" "downloaded_file" {
|
|
||||||
content = data.http.remote_file.body
|
|
||||||
filename = "${path.module}/downloaded_file.txt"
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# This file is maintained automatically by "terraform init".
|
|
||||||
# Manual edits may be lost in future updates.
|
|
||||||
|
|
||||||
provider "registry.terraform.io/kreuzwerker/docker" {
|
|
||||||
version = "3.0.2"
|
|
||||||
constraints = "~> 3.0.1"
|
|
||||||
hashes = [
|
|
||||||
"h1:cT2ccWOtlfKYBUE60/v2/4Q6Stk1KYTNnhxSck+VPlU=",
|
|
||||||
"zh:15b0a2b2b563d8d40f62f83057d91acb02cd0096f207488d8b4298a59203d64f",
|
|
||||||
"zh:23d919de139f7cd5ebfd2ff1b94e6d9913f0977fcfc2ca02e1573be53e269f95",
|
|
||||||
"zh:38081b3fe317c7e9555b2aaad325ad3fa516a886d2dfa8605ae6a809c1072138",
|
|
||||||
"zh:4a9c5065b178082f79ad8160243369c185214d874ff5048556d48d3edd03c4da",
|
|
||||||
"zh:5438ef6afe057945f28bce43d76c4401254073de01a774760169ac1058830ac2",
|
|
||||||
"zh:60b7fadc287166e5c9873dfe53a7976d98244979e0ab66428ea0dea1ebf33e06",
|
|
||||||
"zh:61c5ec1cb94e4c4a4fb1e4a24576d5f39a955f09afb17dab982de62b70a9bdd1",
|
|
||||||
"zh:a38fe9016ace5f911ab00c88e64b156ebbbbfb72a51a44da3c13d442cd214710",
|
|
||||||
"zh:c2c4d2b1fd9ebb291c57f524b3bf9d0994ff3e815c0cd9c9bcb87166dc687005",
|
|
||||||
"zh:d567bb8ce483ab2cf0602e07eae57027a1a53994aba470fa76095912a505533d",
|
|
||||||
"zh:e83bf05ab6a19dd8c43547ce9a8a511f8c331a124d11ac64687c764ab9d5a792",
|
|
||||||
"zh:e90c934b5cd65516fbcc454c89a150bfa726e7cf1fe749790c7480bbeb19d387",
|
|
||||||
"zh:f05f167d2eaf913045d8e7b88c13757e3cf595dd5cd333057fdafc7c4b7fed62",
|
|
||||||
"zh:fcc9c1cea5ce85e8bcb593862e699a881bd36dffd29e2e367f82d15368659c3d",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
docker = {
|
|
||||||
source = "kreuzwerker/docker"
|
|
||||||
version = "~> 3.0.1" # Adjust version as needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
provider "docker" {}
|
|
||||||
|
|
||||||
resource "docker_network" "invalid_network" {
|
|
||||||
name = "my-invalid-network"
|
|
||||||
|
|
||||||
ipam_config {
|
|
||||||
subnet = "172.17.0.0/33"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 325 KiB |
@@ -1,8 +0,0 @@
|
|||||||
## Bios settings
|
|
||||||
|
|
||||||
1. CSM : Disabled (compatibility support to boot gpt formatted drives)
|
|
||||||
2. Secure boot : disabled
|
|
||||||
3. Boot order :
|
|
||||||
1. Local Hard drive
|
|
||||||
2. PXE IPv4
|
|
||||||
4. System clock, make sure it is adjusted, otherwise you will get invalid certificates error
|
|
||||||
@@ -27,9 +27,9 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
let application = Arc::new(RustWebapp {
|
let application = Arc::new(RustWebapp {
|
||||||
name: "example-monitoring".to_string(),
|
name: "example-monitoring".to_string(),
|
||||||
|
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
|
||||||
project_root: PathBuf::from("./examples/rust/webapp"),
|
project_root: PathBuf::from("./examples/rust/webapp"),
|
||||||
framework: Some(RustWebFramework::Leptos),
|
framework: Some(RustWebFramework::Leptos),
|
||||||
service_port: 3000,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let webhook_receiver = WebhookReceiver {
|
let webhook_receiver = WebhookReceiver {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use harmony::{
|
|||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::{
|
modules::{
|
||||||
dummy::{ErrorScore, PanicScore, SuccessScore},
|
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
inventory::LaunchDiscoverInventoryAgentScore,
|
inventory::DiscoverInventoryAgentScore,
|
||||||
},
|
},
|
||||||
topology::LocalhostTopology,
|
topology::LocalhostTopology,
|
||||||
};
|
};
|
||||||
@@ -16,7 +16,7 @@ async fn main() {
|
|||||||
Box::new(SuccessScore {}),
|
Box::new(SuccessScore {}),
|
||||||
Box::new(ErrorScore {}),
|
Box::new(ErrorScore {}),
|
||||||
Box::new(PanicScore {}),
|
Box::new(PanicScore {}),
|
||||||
Box::new(LaunchDiscoverInventoryAgentScore {
|
Box::new(DiscoverInventoryAgentScore {
|
||||||
discovery_timeout: Some(10),
|
discovery_timeout: Some(10),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ harmony_types = { path = "../../harmony_types" }
|
|||||||
cidr = { workspace = true }
|
cidr = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
harmony_macros = { path = "../../harmony_macros" }
|
||||||
harmony_secret = { path = "../../harmony_secret" }
|
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
env_logger = { workspace = true }
|
env_logger = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
|||||||
@@ -5,24 +5,22 @@ use std::{
|
|||||||
|
|
||||||
use cidr::Ipv4Cidr;
|
use cidr::Ipv4Cidr;
|
||||||
use harmony::{
|
use harmony::{
|
||||||
config::secret::SshKeyPair,
|
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||||
data::{FileContent, FilePath},
|
|
||||||
hardware::{HostCategory, Location, PhysicalHost, SwitchGroup},
|
|
||||||
infra::opnsense::OPNSenseManagementInterface,
|
infra::opnsense::OPNSenseManagementInterface,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::{
|
modules::{
|
||||||
http::StaticFilesHttpScore,
|
http::StaticFilesHttpScore,
|
||||||
|
ipxe::IpxeScore,
|
||||||
okd::{
|
okd::{
|
||||||
bootstrap_dhcp::OKDBootstrapDhcpScore,
|
bootstrap_dhcp::OKDBootstrapDhcpScore,
|
||||||
bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, dhcp::OKDDhcpScore,
|
bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, dhcp::OKDDhcpScore,
|
||||||
dns::OKDDnsScore, ipxe::OKDIpxeScore,
|
dns::OKDDnsScore,
|
||||||
},
|
},
|
||||||
tftp::TftpScore,
|
tftp::TftpScore,
|
||||||
},
|
},
|
||||||
topology::{LogicalHost, UnmanagedRouter},
|
topology::{LogicalHost, UnmanagedRouter},
|
||||||
};
|
};
|
||||||
use harmony_macros::{ip, mac_address};
|
use harmony_macros::{ip, mac_address};
|
||||||
use harmony_secret::SecretManager;
|
|
||||||
use harmony_types::net::Url;
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -126,28 +124,14 @@ async fn main() {
|
|||||||
let load_balancer_score =
|
let load_balancer_score =
|
||||||
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
|
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
|
||||||
|
|
||||||
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap();
|
|
||||||
|
|
||||||
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
|
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
|
||||||
let http_score = StaticFilesHttpScore {
|
let http_score = StaticFilesHttpScore {
|
||||||
folder_to_serve: Some(Url::LocalFolder(
|
folder_to_serve: Some(Url::LocalFolder(
|
||||||
"./data/watchguard/pxe-http-files".to_string(),
|
"./data/watchguard/pxe-http-files".to_string(),
|
||||||
)),
|
)),
|
||||||
files: vec![],
|
files: vec![],
|
||||||
remote_path: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let kickstart_filename = "inventory.kickstart".to_string();
|
|
||||||
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
|
||||||
|
|
||||||
let ipxe_score = OKDIpxeScore {
|
|
||||||
kickstart_filename,
|
|
||||||
harmony_inventory_agent,
|
|
||||||
cluster_pubkey: FileContent {
|
|
||||||
path: FilePath::Relative("cluster_ssh_key.pub".to_string()),
|
|
||||||
content: ssh_key.public,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
let ipxe_score = IpxeScore::new();
|
||||||
|
|
||||||
harmony_tui::run(
|
harmony_tui::run(
|
||||||
inventory,
|
inventory,
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "example-okd-install"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { path = "../../harmony" }
|
|
||||||
harmony_cli = { path = "../../harmony_cli" }
|
|
||||||
harmony_types = { path = "../../harmony_types" }
|
|
||||||
harmony_secret = { path = "../../harmony_secret" }
|
|
||||||
harmony_secret_derive = { path = "../../harmony_secret_derive" }
|
|
||||||
cidr = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
|
||||||
log = { workspace = true }
|
|
||||||
env_logger = { workspace = true }
|
|
||||||
url = { workspace = true }
|
|
||||||
serde.workspace = true
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export HARMONY_SECRET_NAMESPACE=example-vms
|
|
||||||
export HARMONY_SECRET_STORE=file
|
|
||||||
export HARMONY_DATABASE_URL=sqlite://harmony_vms.sqlite RUST_LOG=info
|
|
||||||
export RUST_LOG=info
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
mod topology;
|
|
||||||
|
|
||||||
use crate::topology::{get_inventory, get_topology};
|
|
||||||
use harmony::{
|
|
||||||
config::secret::SshKeyPair,
|
|
||||||
data::{FileContent, FilePath},
|
|
||||||
modules::okd::{installation::OKDInstallationPipeline, ipxe::OKDIpxeScore},
|
|
||||||
score::Score,
|
|
||||||
topology::HAClusterTopology,
|
|
||||||
};
|
|
||||||
use harmony_secret::SecretManager;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let inventory = get_inventory();
|
|
||||||
let topology = get_topology().await;
|
|
||||||
|
|
||||||
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap();
|
|
||||||
|
|
||||||
let mut scores: Vec<Box<dyn Score<HAClusterTopology>>> = vec![Box::new(OKDIpxeScore {
|
|
||||||
kickstart_filename: "inventory.kickstart".to_string(),
|
|
||||||
harmony_inventory_agent: "harmony_inventory_agent".to_string(),
|
|
||||||
cluster_pubkey: FileContent {
|
|
||||||
path: FilePath::Relative("cluster_ssh_key.pub".to_string()),
|
|
||||||
content: ssh_key.public,
|
|
||||||
},
|
|
||||||
})];
|
|
||||||
|
|
||||||
scores.append(&mut OKDInstallationPipeline::get_all_scores().await);
|
|
||||||
|
|
||||||
harmony_cli::run(inventory, topology, scores, None)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -1,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_or_prompt::<OPNSenseFirewallConfig>().await;
|
|
||||||
let config = config.unwrap();
|
|
||||||
|
|
||||||
let opnsense = Arc::new(
|
|
||||||
harmony::infra::opnsense::OPNSenseFirewall::new(
|
|
||||||
firewall,
|
|
||||||
None,
|
|
||||||
&config.username,
|
|
||||||
&config.password,
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
);
|
|
||||||
let lan_subnet = ipv4!("192.168.1.0");
|
|
||||||
let gateway_ipv4 = ipv4!("192.168.1.1");
|
|
||||||
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
|
||||||
harmony::topology::HAClusterTopology {
|
|
||||||
domain_name: "demo.harmony.mcd".to_string(),
|
|
||||||
router: Arc::new(UnmanagedRouter::new(
|
|
||||||
gateway_ip,
|
|
||||||
Ipv4Cidr::new(lan_subnet, 24).unwrap(),
|
|
||||||
)),
|
|
||||||
load_balancer: opnsense.clone(),
|
|
||||||
firewall: opnsense.clone(),
|
|
||||||
tftp_server: opnsense.clone(),
|
|
||||||
http_server: opnsense.clone(),
|
|
||||||
dhcp_server: opnsense.clone(),
|
|
||||||
dns_server: opnsense.clone(),
|
|
||||||
control_plane: vec![LogicalHost {
|
|
||||||
ip: ip!("192.168.1.20"),
|
|
||||||
name: "master".to_string(),
|
|
||||||
}],
|
|
||||||
bootstrap_host: LogicalHost {
|
|
||||||
ip: ip!("192.168.1.10"),
|
|
||||||
name: "bootstrap".to_string(),
|
|
||||||
},
|
|
||||||
workers: vec![],
|
|
||||||
switch: 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
|
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
mod topology;
|
mod topology;
|
||||||
|
|
||||||
use crate::topology::{get_inventory, get_topology};
|
use crate::topology::{get_inventory, get_topology};
|
||||||
use harmony::{
|
use harmony::modules::okd::ipxe::OkdIpxeScore;
|
||||||
config::secret::SshKeyPair,
|
|
||||||
data::{FileContent, FilePath},
|
|
||||||
modules::okd::ipxe::OKDIpxeScore,
|
|
||||||
};
|
|
||||||
use harmony_secret::SecretManager;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -14,16 +9,13 @@ async fn main() {
|
|||||||
let topology = get_topology().await;
|
let topology = get_topology().await;
|
||||||
|
|
||||||
let kickstart_filename = "inventory.kickstart".to_string();
|
let kickstart_filename = "inventory.kickstart".to_string();
|
||||||
|
let cluster_pubkey_filename = "cluster_ssh_key.pub".to_string();
|
||||||
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
||||||
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap();
|
|
||||||
|
|
||||||
let ipxe_score = OKDIpxeScore {
|
let ipxe_score = OkdIpxeScore {
|
||||||
kickstart_filename,
|
kickstart_filename,
|
||||||
harmony_inventory_agent,
|
harmony_inventory_agent,
|
||||||
cluster_pubkey: FileContent {
|
cluster_pubkey_filename,
|
||||||
path: FilePath::Relative("cluster_ssh_key.pub".to_string()),
|
|
||||||
content: ssh_key.public,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
harmony_cli::run(inventory, topology, vec![Box::new(ipxe_score)], None)
|
harmony_cli::run(inventory, topology, vec![Box::new(ipxe_score)], None)
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
use cidr::Ipv4Cidr;
|
use cidr::Ipv4Cidr;
|
||||||
use harmony::{
|
use harmony::{
|
||||||
config::secret::OPNSenseFirewallCredentials,
|
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||||
hardware::{Location, SwitchGroup},
|
|
||||||
infra::opnsense::OPNSenseManagementInterface,
|
infra::opnsense::OPNSenseManagementInterface,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
|
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
|
||||||
};
|
};
|
||||||
use harmony_macros::{ip, ipv4};
|
use harmony_macros::{ip, ipv4};
|
||||||
use harmony_secret::SecretManager;
|
use harmony_secret::{Secret, SecretManager};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{net::IpAddr, sync::Arc};
|
use std::{net::IpAddr, sync::Arc};
|
||||||
|
|
||||||
|
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
struct OPNSenseFirewallConfig {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_topology() -> HAClusterTopology {
|
pub async fn get_topology() -> HAClusterTopology {
|
||||||
let firewall = harmony::topology::LogicalHost {
|
let firewall = harmony::topology::LogicalHost {
|
||||||
ip: ip!("192.168.1.1"),
|
ip: ip!("192.168.1.1"),
|
||||||
name: String::from("opnsense-1"),
|
name: String::from("opnsense-1"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let config = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>().await;
|
let config = SecretManager::get::<OPNSenseFirewallConfig>().await;
|
||||||
let config = config.unwrap();
|
let config = config.unwrap();
|
||||||
|
|
||||||
let opnsense = Arc::new(
|
let opnsense = Arc::new(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::{
|
|||||||
|
|
||||||
use cidr::Ipv4Cidr;
|
use cidr::Ipv4Cidr;
|
||||||
use harmony::{
|
use harmony::{
|
||||||
hardware::{HostCategory, Location, PhysicalHost, SwitchGroup},
|
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||||
infra::opnsense::OPNSenseManagementInterface,
|
infra::opnsense::OPNSenseManagementInterface,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::{
|
modules::{
|
||||||
@@ -85,7 +85,6 @@ async fn main() {
|
|||||||
"./data/watchguard/pxe-http-files".to_string(),
|
"./data/watchguard/pxe-http-files".to_string(),
|
||||||
)),
|
)),
|
||||||
files: vec![],
|
files: vec![],
|
||||||
remote_path: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
harmony_tui::run(
|
harmony_tui::run(
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rhob-application-monitoring"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { path = "../../harmony" }
|
|
||||||
harmony_cli = { path = "../../harmony_cli" }
|
|
||||||
harmony_types = { path = "../../harmony_types" }
|
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
log = { workspace = true }
|
|
||||||
env_logger = { workspace = true }
|
|
||||||
url = { workspace = true }
|
|
||||||
base64.workspace = true
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
use std::{path::PathBuf, sync::Arc};
|
|
||||||
|
|
||||||
use harmony::{
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::{
|
|
||||||
application::{
|
|
||||||
ApplicationScore, RustWebFramework, RustWebapp, features::rhob_monitoring::Monitoring,
|
|
||||||
},
|
|
||||||
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
|
||||||
},
|
|
||||||
topology::K8sAnywhereTopology,
|
|
||||||
};
|
|
||||||
use harmony_types::net::Url;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let application = Arc::new(RustWebapp {
|
|
||||||
name: "test-rhob-monitoring".to_string(),
|
|
||||||
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
|
|
||||||
framework: Some(RustWebFramework::Leptos),
|
|
||||||
service_port: 3000,
|
|
||||||
});
|
|
||||||
|
|
||||||
let discord_receiver = DiscordWebhook {
|
|
||||||
name: "test-discord".to_string(),
|
|
||||||
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = ApplicationScore {
|
|
||||||
features: vec![
|
|
||||||
Box::new(Monitoring {
|
|
||||||
application: application.clone(),
|
|
||||||
alert_receiver: vec![Box::new(discord_receiver)],
|
|
||||||
}),
|
|
||||||
// TODO add backups, multisite ha, etc
|
|
||||||
],
|
|
||||||
application,
|
|
||||||
};
|
|
||||||
|
|
||||||
harmony_cli::run(
|
|
||||||
Inventory::autoload(),
|
|
||||||
K8sAnywhereTopology::from_env(),
|
|
||||||
vec![Box::new(app)],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ use harmony::{
|
|||||||
modules::{
|
modules::{
|
||||||
application::{
|
application::{
|
||||||
ApplicationScore, RustWebFramework, RustWebapp,
|
ApplicationScore, RustWebFramework, RustWebapp,
|
||||||
features::{Monitoring, PackagingDeployment},
|
features::{ContinuousDelivery, Monitoring},
|
||||||
},
|
},
|
||||||
monitoring::alert_channel::{
|
monitoring::alert_channel::{
|
||||||
discord_alert_channel::DiscordWebhook, webhook_receiver::WebhookReceiver,
|
discord_alert_channel::DiscordWebhook, webhook_receiver::WebhookReceiver,
|
||||||
@@ -13,30 +13,30 @@ use harmony::{
|
|||||||
},
|
},
|
||||||
topology::K8sAnywhereTopology,
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
use harmony_macros::hurl;
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let application = Arc::new(RustWebapp {
|
let application = Arc::new(RustWebapp {
|
||||||
name: "harmony-example-rust-webapp".to_string(),
|
name: "harmony-example-rust-webapp".to_string(),
|
||||||
project_root: PathBuf::from("./webapp"),
|
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
|
||||||
|
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
|
||||||
framework: Some(RustWebFramework::Leptos),
|
framework: Some(RustWebFramework::Leptos),
|
||||||
service_port: 3000,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: "test-discord".to_string(),
|
name: "test-discord".to_string(),
|
||||||
url: hurl!("https://discord.doesnt.exist.com"),
|
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let webhook_receiver = WebhookReceiver {
|
let webhook_receiver = WebhookReceiver {
|
||||||
name: "sample-webhook-receiver".to_string(),
|
name: "sample-webhook-receiver".to_string(),
|
||||||
url: hurl!("https://webhook-doesnt-exist.com"),
|
url: Url::Url(url::Url::parse("https://webhook-doesnt-exist.com").unwrap()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = ApplicationScore {
|
let app = ApplicationScore {
|
||||||
features: vec![
|
features: vec![
|
||||||
Box::new(PackagingDeployment {
|
Box::new(ContinuousDelivery {
|
||||||
application: application.clone(),
|
application: application.clone(),
|
||||||
}),
|
}),
|
||||||
Box::new(Monitoring {
|
Box::new(Monitoring {
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "example-try-rust-webapp"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { path = "../../harmony" }
|
|
||||||
harmony_cli = { path = "../../harmony_cli" }
|
|
||||||
harmony_types = { path = "../../harmony_types" }
|
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
log = { workspace = true }
|
|
||||||
env_logger = { workspace = true }
|
|
||||||
url = { workspace = true }
|
|
||||||
base64.workspace = true
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
harmony
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "harmony-tryrust"
|
|
||||||
edition = "2024"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { path = "../../../nationtech/harmony/harmony" }
|
|
||||||
harmony_cli = { path = "../../../nationtech/harmony/harmony_cli" }
|
|
||||||
harmony_types = { path = "../../../nationtech/harmony/harmony_types" }
|
|
||||||
harmony_macros = { path = "../../../nationtech/harmony/harmony_macros" }
|
|
||||||
tokio = { version = "1.40", features = [
|
|
||||||
"io-std",
|
|
||||||
"fs",
|
|
||||||
"macros",
|
|
||||||
"rt-multi-thread",
|
|
||||||
] }
|
|
||||||
log = { version = "0.4", features = ["kv"] }
|
|
||||||
env_logger = "0.11"
|
|
||||||
url = "2.5"
|
|
||||||
base64 = "0.22.1"
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use harmony::{
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::{
|
|
||||||
application::{
|
|
||||||
ApplicationScore, RustWebFramework, RustWebapp,
|
|
||||||
features::{PackagingDeployment, rhob_monitoring::Monitoring},
|
|
||||||
},
|
|
||||||
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
|
||||||
},
|
|
||||||
topology::K8sAnywhereTopology,
|
|
||||||
};
|
|
||||||
use harmony_macros::hurl;
|
|
||||||
use std::{path::PathBuf, sync::Arc};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let application = Arc::new(RustWebapp {
|
|
||||||
name: "tryrust".to_string(),
|
|
||||||
project_root: PathBuf::from(".."),
|
|
||||||
framework: Some(RustWebFramework::Leptos),
|
|
||||||
service_port: 8080,
|
|
||||||
});
|
|
||||||
|
|
||||||
let discord_webhook = DiscordWebhook {
|
|
||||||
name: "harmony_demo".to_string(),
|
|
||||||
url: hurl!("http://not_a_url.com"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = ApplicationScore {
|
|
||||||
features: vec![
|
|
||||||
Box::new(PackagingDeployment {
|
|
||||||
application: application.clone(),
|
|
||||||
}),
|
|
||||||
Box::new(Monitoring {
|
|
||||||
application: application.clone(),
|
|
||||||
alert_receiver: vec![Box::new(discord_webhook)],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
application,
|
|
||||||
};
|
|
||||||
|
|
||||||
harmony_cli::run(
|
|
||||||
Inventory::autoload(),
|
|
||||||
K8sAnywhereTopology::from_env(),
|
|
||||||
vec![Box::new(app)],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use harmony::{
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::{
|
|
||||||
application::{
|
|
||||||
ApplicationScore, RustWebFramework, RustWebapp,
|
|
||||||
features::{PackagingDeployment, rhob_monitoring::Monitoring},
|
|
||||||
},
|
|
||||||
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
|
||||||
},
|
|
||||||
topology::K8sAnywhereTopology,
|
|
||||||
};
|
|
||||||
use harmony_macros::hurl;
|
|
||||||
use std::{path::PathBuf, sync::Arc};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let application = Arc::new(RustWebapp {
|
|
||||||
name: "harmony-example-tryrust".to_string(),
|
|
||||||
project_root: PathBuf::from("./tryrust.org"), // <== Project root, in this case it is a
|
|
||||||
// submodule
|
|
||||||
framework: Some(RustWebFramework::Leptos),
|
|
||||||
service_port: 8080,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define your Application deployment and the features you want
|
|
||||||
let app = ApplicationScore {
|
|
||||||
features: vec![
|
|
||||||
Box::new(PackagingDeployment {
|
|
||||||
application: application.clone(),
|
|
||||||
}),
|
|
||||||
Box::new(Monitoring {
|
|
||||||
application: application.clone(),
|
|
||||||
alert_receiver: vec![Box::new(DiscordWebhook {
|
|
||||||
name: "test-discord".to_string(),
|
|
||||||
url: hurl!("https://discord.doesnt.exist.com"),
|
|
||||||
})],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
application,
|
|
||||||
};
|
|
||||||
|
|
||||||
harmony_cli::run(
|
|
||||||
Inventory::autoload(),
|
|
||||||
K8sAnywhereTopology::from_env(), // <== Deploy to local automatically provisioned k3d by default or connect to any kubernetes cluster
|
|
||||||
vec![Box::new(app)],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ use harmony::{
|
|||||||
},
|
},
|
||||||
topology::{
|
topology::{
|
||||||
BackendServer, DummyInfra, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancerService,
|
BackendServer, DummyInfra, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancerService,
|
||||||
SSL,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use harmony_macros::ipv4;
|
use harmony_macros::ipv4;
|
||||||
@@ -48,7 +47,6 @@ fn build_large_score() -> LoadBalancerScore {
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
HttpMethod::GET,
|
HttpMethod::GET,
|
||||||
HttpStatusCode::Success2xx,
|
HttpStatusCode::Success2xx,
|
||||||
SSL::Disabled,
|
|
||||||
)),
|
)),
|
||||||
};
|
};
|
||||||
LoadBalancerScore {
|
LoadBalancerScore {
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ testing = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
reqwest = { version = "0.11", features = [
|
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features = false }
|
||||||
"blocking",
|
|
||||||
"json",
|
|
||||||
"rustls-tls",
|
|
||||||
], default-features = false }
|
|
||||||
russh = "0.45.0"
|
russh = "0.45.0"
|
||||||
rust-ipmi = "0.1.1"
|
rust-ipmi = "0.1.1"
|
||||||
semver = "1.0.23"
|
semver = "1.0.23"
|
||||||
@@ -70,13 +66,10 @@ tar.workspace = true
|
|||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
walkdir = "2.5.0"
|
|
||||||
harmony_inventory_agent = { path = "../harmony_inventory_agent" }
|
harmony_inventory_agent = { path = "../harmony_inventory_agent" }
|
||||||
harmony_secret_derive = { path = "../harmony_secret_derive" }
|
harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" }
|
||||||
harmony_secret = { path = "../harmony_secret" }
|
|
||||||
askama.workspace = true
|
askama.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
inquire.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
pub mod secret;
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
use harmony_secret_derive::Secret;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
pub struct OPNSenseFirewallCredentials {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO we need a better way to handle multiple "instances" of the same secret structure.
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
pub struct SshKeyPair {
|
|
||||||
pub private: String,
|
|
||||||
pub public: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
pub struct RedhatSecret {
|
|
||||||
pub pull_secret: String,
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use harmony_inventory_agent::hwinfo::{CPU, MemoryModule, NetworkInterface, StorageDrive};
|
use harmony_inventory_agent::hwinfo::{CPU, MemoryModule, NetworkInterface, StorageDrive};
|
||||||
use harmony_types::net::MacAddress;
|
use harmony_types::net::MacAddress;
|
||||||
@@ -8,7 +10,7 @@ pub type HostGroup = Vec<PhysicalHost>;
|
|||||||
pub type SwitchGroup = Vec<Switch>;
|
pub type SwitchGroup = Vec<Switch>;
|
||||||
pub type FirewallGroup = Vec<PhysicalHost>;
|
pub type FirewallGroup = Vec<PhysicalHost>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct PhysicalHost {
|
pub struct PhysicalHost {
|
||||||
pub id: Id,
|
pub id: Id,
|
||||||
pub category: HostCategory,
|
pub category: HostCategory,
|
||||||
@@ -149,98 +151,6 @@ impl PhysicalHost {
|
|||||||
parts.join(" | ")
|
parts.join(" | ")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parts_list(&self) -> String {
|
|
||||||
let PhysicalHost {
|
|
||||||
id,
|
|
||||||
category,
|
|
||||||
network,
|
|
||||||
storage,
|
|
||||||
labels,
|
|
||||||
memory_modules,
|
|
||||||
cpus,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let mut parts_list = String::new();
|
|
||||||
parts_list.push_str("\n\n=====================");
|
|
||||||
parts_list.push_str(&format!("\nHost ID {id}"));
|
|
||||||
parts_list.push_str("\n=====================");
|
|
||||||
parts_list.push_str("\n\n=====================");
|
|
||||||
parts_list.push_str(&format!("\nCPU count {}", cpus.len()));
|
|
||||||
parts_list.push_str("\n=====================");
|
|
||||||
cpus.iter().for_each(|c| {
|
|
||||||
let CPU {
|
|
||||||
model,
|
|
||||||
vendor,
|
|
||||||
cores,
|
|
||||||
threads,
|
|
||||||
frequency_mhz,
|
|
||||||
} = c;
|
|
||||||
parts_list.push_str(&format!(
|
|
||||||
"\n{vendor} {model}, {cores}/{threads} {}Ghz",
|
|
||||||
*frequency_mhz as f64 / 1000.0
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
parts_list.push_str("\n\n=====================");
|
|
||||||
parts_list.push_str(&format!("\nNetwork Interfaces count {}", network.len()));
|
|
||||||
parts_list.push_str("\n=====================");
|
|
||||||
network.iter().for_each(|nic| {
|
|
||||||
parts_list.push_str(&format!(
|
|
||||||
"\nNic({} {}Gbps mac({}) ipv4({}), ipv6({})",
|
|
||||||
nic.name,
|
|
||||||
nic.speed_mbps.unwrap_or(0) / 1000,
|
|
||||||
nic.mac_address,
|
|
||||||
nic.ipv4_addresses.join(","),
|
|
||||||
nic.ipv6_addresses.join(",")
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
parts_list.push_str("\n\n=====================");
|
|
||||||
parts_list.push_str(&format!("\nStorage drives count {}", storage.len()));
|
|
||||||
parts_list.push_str("\n=====================");
|
|
||||||
storage.iter().for_each(|drive| {
|
|
||||||
let StorageDrive {
|
|
||||||
name,
|
|
||||||
model,
|
|
||||||
serial,
|
|
||||||
size_bytes,
|
|
||||||
logical_block_size: _,
|
|
||||||
physical_block_size: _,
|
|
||||||
rotational: _,
|
|
||||||
wwn: _,
|
|
||||||
interface_type,
|
|
||||||
smart_status,
|
|
||||||
} = drive;
|
|
||||||
parts_list.push_str(&format!(
|
|
||||||
"\n{name} {}Gb {model} {interface_type} smart({smart_status:?}) {serial}",
|
|
||||||
size_bytes / 1000 / 1000 / 1000
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
parts_list.push_str("\n\n=====================");
|
|
||||||
parts_list.push_str(&format!("\nMemory modules count {}", memory_modules.len()));
|
|
||||||
parts_list.push_str("\n=====================");
|
|
||||||
memory_modules.iter().for_each(|mem| {
|
|
||||||
let MemoryModule {
|
|
||||||
size_bytes,
|
|
||||||
speed_mhz,
|
|
||||||
manufacturer,
|
|
||||||
part_number,
|
|
||||||
serial_number,
|
|
||||||
rank,
|
|
||||||
} = mem;
|
|
||||||
parts_list.push_str(&format!(
|
|
||||||
"\n{}Gb, {}Mhz, Manufacturer ({}), Part Number ({})",
|
|
||||||
size_bytes / 1000 / 1000 / 1000,
|
|
||||||
speed_mhz.unwrap_or(0),
|
|
||||||
manufacturer.as_ref().unwrap_or(&String::new()),
|
|
||||||
part_number.as_ref().unwrap_or(&String::new()),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
parts_list
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cluster_mac(&self) -> MacAddress {
|
pub fn cluster_mac(&self) -> MacAddress {
|
||||||
self.network
|
self.network
|
||||||
.first()
|
.first()
|
||||||
@@ -263,10 +173,6 @@ impl PhysicalHost {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mac_address(&self) -> Vec<MacAddress> {
|
|
||||||
self.network.iter().map(|nic| nic.mac_address).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn label(mut self, name: String, value: String) -> Self {
|
pub fn label(mut self, name: String, value: String) -> Self {
|
||||||
self.labels.push(Label { name, value });
|
self.labels.push(Label { name, value });
|
||||||
self
|
self
|
||||||
@@ -315,6 +221,15 @@ impl PhysicalHost {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for PhysicalHost {
|
||||||
|
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(new, Serialize)]
|
#[derive(new, Serialize)]
|
||||||
pub struct ManualManagementInterface;
|
pub struct ManualManagementInterface;
|
||||||
|
|
||||||
@@ -358,13 +273,16 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub enum HostCategory {
|
pub enum HostCategory {
|
||||||
Server,
|
Server,
|
||||||
Firewall,
|
Firewall,
|
||||||
Switch,
|
Switch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use harmony_macros::mac_address;
|
||||||
|
|
||||||
use harmony_types::id::Id;
|
use harmony_types::id::Id;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -373,7 +291,7 @@ pub struct Switch {
|
|||||||
_management_interface: NetworkInterface,
|
_management_interface: NetworkInterface,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, new, Clone, Serialize, Deserialize)]
|
#[derive(Debug, new, Clone, Serialize)]
|
||||||
pub struct Label {
|
pub struct Label {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
|||||||
@@ -32,9 +32,6 @@ pub enum InterpretName {
|
|||||||
K8sPrometheusCrdAlerting,
|
K8sPrometheusCrdAlerting,
|
||||||
DiscoverInventoryAgent,
|
DiscoverInventoryAgent,
|
||||||
CephClusterHealth,
|
CephClusterHealth,
|
||||||
Custom(&'static str),
|
|
||||||
RHOBAlerting,
|
|
||||||
K8sIngress,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for InterpretName {
|
impl std::fmt::Display for InterpretName {
|
||||||
@@ -63,9 +60,6 @@ impl std::fmt::Display for InterpretName {
|
|||||||
InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"),
|
InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"),
|
||||||
InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"),
|
InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"),
|
||||||
InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"),
|
InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"),
|
||||||
InterpretName::Custom(name) => f.write_str(name),
|
|
||||||
InterpretName::RHOBAlerting => f.write_str("RHOBAlerting"),
|
|
||||||
InterpretName::K8sIngress => f.write_str("K8sIngress"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,15 +78,13 @@ pub trait Interpret<T>: std::fmt::Debug + Send {
|
|||||||
pub struct Outcome {
|
pub struct Outcome {
|
||||||
pub status: InterpretStatus,
|
pub status: InterpretStatus,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub details: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Outcome {
|
impl Outcome {
|
||||||
pub fn noop(message: String) -> Self {
|
pub fn noop() -> Self {
|
||||||
Self {
|
Self {
|
||||||
status: InterpretStatus::NOOP,
|
status: InterpretStatus::NOOP,
|
||||||
message,
|
message: String::new(),
|
||||||
details: vec![],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,23 +92,6 @@ impl Outcome {
|
|||||||
Self {
|
Self {
|
||||||
status: InterpretStatus::SUCCESS,
|
status: InterpretStatus::SUCCESS,
|
||||||
message,
|
message,
|
||||||
details: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn success_with_details(message: String, details: Vec<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
status: InterpretStatus::SUCCESS,
|
|
||||||
message,
|
|
||||||
details,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn running(message: String) -> Self {
|
|
||||||
Self {
|
|
||||||
status: InterpretStatus::RUNNING,
|
|
||||||
message,
|
|
||||||
details: vec![],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,12 +140,6 @@ impl From<PreparationError> for InterpretError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<harmony_secret::SecretStoreError> for InterpretError {
|
|
||||||
fn from(value: harmony_secret::SecretStoreError) -> Self {
|
|
||||||
InterpretError::new(format!("Interpret error : {value}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ExecutorError> for InterpretError {
|
impl From<ExecutorError> for InterpretError {
|
||||||
fn from(value: ExecutorError) -> Self {
|
fn from(value: ExecutorError) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -17,14 +17,12 @@ impl InventoryFilter {
|
|||||||
|
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use log::info;
|
use log::info;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use strum::EnumIter;
|
|
||||||
|
|
||||||
use crate::hardware::{ManagementInterface, ManualManagementInterface};
|
use crate::hardware::{ManagementInterface, ManualManagementInterface};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
filter::Filter,
|
filter::Filter,
|
||||||
hardware::{HostGroup, Location, SwitchGroup},
|
hardware::{FirewallGroup, HostGroup, Location, SwitchGroup},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -63,11 +61,3 @@ impl Inventory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, EnumIter)]
|
|
||||||
pub enum HostRole {
|
|
||||||
Bootstrap,
|
|
||||||
ControlPlane,
|
|
||||||
Worker,
|
|
||||||
Storage,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use crate::{hardware::PhysicalHost, interpret::InterpretError, inventory::HostRole};
|
use crate::hardware::PhysicalHost;
|
||||||
|
|
||||||
/// Errors that can occur within the repository layer.
|
/// Errors that can occur within the repository layer.
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
@@ -15,12 +15,6 @@ pub enum RepoError {
|
|||||||
ConnectionFailed(String),
|
ConnectionFailed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RepoError> for InterpretError {
|
|
||||||
fn from(value: RepoError) -> Self {
|
|
||||||
InterpretError::new(format!("Interpret error : {value}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Trait and Implementation ---
|
// --- Trait and Implementation ---
|
||||||
|
|
||||||
/// Defines the contract for inventory persistence.
|
/// Defines the contract for inventory persistence.
|
||||||
@@ -28,11 +22,4 @@ impl From<RepoError> for InterpretError {
|
|||||||
pub trait InventoryRepository: Send + Sync + 'static {
|
pub trait InventoryRepository: Send + Sync + 'static {
|
||||||
async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError>;
|
async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError>;
|
||||||
async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError>;
|
async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError>;
|
||||||
async fn get_all_hosts(&self) -> Result<Vec<PhysicalHost>, RepoError>;
|
|
||||||
async fn get_host_for_role(&self, role: &HostRole) -> Result<Vec<PhysicalHost>, RepoError>;
|
|
||||||
async fn save_role_mapping(
|
|
||||||
&self,
|
|
||||||
role: &HostRole,
|
|
||||||
host: &PhysicalHost,
|
|
||||||
) -> Result<(), RepoError>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,26 +69,6 @@ impl K8sclient for HAClusterTopology {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HAClusterTopology {
|
impl HAClusterTopology {
|
||||||
// TODO this is a hack to avoid refactoring
|
|
||||||
pub fn get_cluster_name(&self) -> String {
|
|
||||||
self.domain_name
|
|
||||||
.split(".")
|
|
||||||
.next()
|
|
||||||
.expect("Cluster domain name must not be empty")
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cluster_base_domain(&self) -> String {
|
|
||||||
let base_domain = self
|
|
||||||
.domain_name
|
|
||||||
.strip_prefix(&self.get_cluster_name())
|
|
||||||
.expect("cluster domain must start with cluster name");
|
|
||||||
base_domain
|
|
||||||
.strip_prefix(".")
|
|
||||||
.unwrap_or(base_domain)
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn autoload() -> Self {
|
pub fn autoload() -> Self {
|
||||||
let dummy_infra = Arc::new(DummyInfra {});
|
let dummy_infra = Arc::new(DummyInfra {});
|
||||||
let dummy_host = LogicalHost {
|
let dummy_host = LogicalHost {
|
||||||
@@ -181,14 +161,6 @@ impl DhcpServer for HAClusterTopology {
|
|||||||
self.dhcp_server.set_pxe_options(options).await
|
self.dhcp_server.set_pxe_options(options).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_dhcp_range(
|
|
||||||
&self,
|
|
||||||
start: &IpAddress,
|
|
||||||
end: &IpAddress,
|
|
||||||
) -> Result<(), ExecutorError> {
|
|
||||||
self.dhcp_server.set_dhcp_range(start, end).await
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_ip(&self) -> IpAddress {
|
fn get_ip(&self) -> IpAddress {
|
||||||
self.dhcp_server.get_ip()
|
self.dhcp_server.get_ip()
|
||||||
}
|
}
|
||||||
@@ -237,12 +209,8 @@ impl Router for HAClusterTopology {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl HttpServer for HAClusterTopology {
|
impl HttpServer for HAClusterTopology {
|
||||||
async fn serve_files(
|
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> {
|
||||||
&self,
|
self.http_server.serve_files(url).await
|
||||||
url: &Url,
|
|
||||||
remote_path: &Option<String>,
|
|
||||||
) -> Result<(), ExecutorError> {
|
|
||||||
self.http_server.serve_files(url, remote_path).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> {
|
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> {
|
||||||
@@ -330,13 +298,6 @@ impl DhcpServer for DummyInfra {
|
|||||||
async fn set_pxe_options(&self, _options: PxeOptions) -> Result<(), ExecutorError> {
|
async fn set_pxe_options(&self, _options: PxeOptions) -> Result<(), ExecutorError> {
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||||
}
|
}
|
||||||
async fn set_dhcp_range(
|
|
||||||
&self,
|
|
||||||
start: &IpAddress,
|
|
||||||
end: &IpAddress,
|
|
||||||
) -> Result<(), ExecutorError> {
|
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
|
||||||
}
|
|
||||||
fn get_ip(&self) -> IpAddress {
|
fn get_ip(&self) -> IpAddress {
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||||
}
|
}
|
||||||
@@ -401,11 +362,7 @@ impl TftpServer for DummyInfra {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl HttpServer for DummyInfra {
|
impl HttpServer for DummyInfra {
|
||||||
async fn serve_files(
|
async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> {
|
||||||
&self,
|
|
||||||
_url: &Url,
|
|
||||||
_remote_path: &Option<String>,
|
|
||||||
) -> Result<(), ExecutorError> {
|
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||||
}
|
}
|
||||||
async fn serve_file_content(&self, _file: &FileContent) -> Result<(), ExecutorError> {
|
async fn serve_file_content(&self, _file: &FileContent) -> Result<(), ExecutorError> {
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ use harmony_types::net::IpAddress;
|
|||||||
use harmony_types::net::Url;
|
use harmony_types::net::Url;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait HttpServer: Send + Sync {
|
pub trait HttpServer: Send + Sync {
|
||||||
async fn serve_files(
|
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>;
|
||||||
&self,
|
|
||||||
url: &Url,
|
|
||||||
remote_path: &Option<String>,
|
|
||||||
) -> Result<(), ExecutorError>;
|
|
||||||
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError>;
|
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError>;
|
||||||
fn get_ip(&self) -> IpAddress;
|
fn get_ip(&self) -> IpAddress;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
use crate::topology::PreparationError;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Ingress {
|
|
||||||
async fn get_domain(&self, service: &str) -> Result<String, PreparationError>;
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ use kube::{
|
|||||||
api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt},
|
api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt},
|
||||||
config::{KubeConfigOptions, Kubeconfig},
|
config::{KubeConfigOptions, Kubeconfig},
|
||||||
core::ErrorResponse,
|
core::ErrorResponse,
|
||||||
error::DiscoveryError,
|
|
||||||
runtime::reflector::Lookup,
|
runtime::reflector::Lookup,
|
||||||
};
|
};
|
||||||
use kube::{api::DynamicObject, runtime::conditions};
|
use kube::{api::DynamicObject, runtime::conditions};
|
||||||
@@ -18,12 +17,10 @@ use kube::{
|
|||||||
};
|
};
|
||||||
use log::{debug, error, trace};
|
use log::{debug, error, trace};
|
||||||
use serde::{Serialize, de::DeserializeOwned};
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
use serde_json::{Value, json};
|
use serde_json::json;
|
||||||
use similar::TextDiff;
|
use similar::TextDiff;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use crate::interpret::Outcome;
|
|
||||||
|
|
||||||
#[derive(new, Clone)]
|
#[derive(new, Clone)]
|
||||||
pub struct K8sClient {
|
pub struct K8sClient {
|
||||||
client: Client,
|
client: Client,
|
||||||
@@ -56,72 +53,6 @@ impl K8sClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ensure_deployment(
|
|
||||||
&self,
|
|
||||||
resource_name: &str,
|
|
||||||
resource_namespace: &str,
|
|
||||||
) -> Result<Outcome, Error> {
|
|
||||||
match self
|
|
||||||
.get_deployment(resource_name, Some(&resource_namespace))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Some(deployment)) => {
|
|
||||||
if let Some(status) = deployment.status {
|
|
||||||
let ready_count = status.ready_replicas.unwrap_or(0);
|
|
||||||
if ready_count >= 1 {
|
|
||||||
Ok(Outcome::success(format!(
|
|
||||||
"'{}' is ready with {} replica(s).",
|
|
||||||
resource_name, ready_count
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Err(Error::Discovery(DiscoveryError::MissingResource(format!(
|
|
||||||
"Deployment '{}' in namespace '{}' has 0 ready replicas",
|
|
||||||
resource_name, resource_namespace
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(Error::Api(ErrorResponse {
|
|
||||||
status: "Failure".to_string(),
|
|
||||||
message: format!(
|
|
||||||
"No status found for deployment '{}' in namespace '{}'",
|
|
||||||
resource_name, resource_namespace
|
|
||||||
),
|
|
||||||
reason: "MissingStatus".to_string(),
|
|
||||||
code: 404,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None) => Err(Error::Discovery(DiscoveryError::MissingResource(format!(
|
|
||||||
"Deployment '{}' not found in namespace '{}'",
|
|
||||||
resource_name, resource_namespace
|
|
||||||
)))),
|
|
||||||
Err(e) => Err(Error::Api(ErrorResponse {
|
|
||||||
status: "Failure".to_string(),
|
|
||||||
message: format!(
|
|
||||||
"Failed to fetch deployment '{}' in namespace '{}': {}",
|
|
||||||
resource_name, resource_namespace, e
|
|
||||||
),
|
|
||||||
reason: "ApiError".to_string(),
|
|
||||||
code: 500,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_resource_json_value(
|
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
namespace: Option<&str>,
|
|
||||||
gvk: &GroupVersionKind,
|
|
||||||
) -> Result<DynamicObject, Error> {
|
|
||||||
let gvk = ApiResource::from_gvk(gvk);
|
|
||||||
let resource: Api<DynamicObject> = if let Some(ns) = namespace {
|
|
||||||
Api::namespaced_with(self.client.clone(), ns, &gvk)
|
|
||||||
} else {
|
|
||||||
Api::default_namespaced_with(self.client.clone(), &gvk)
|
|
||||||
};
|
|
||||||
Ok(resource.get(name).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_deployment(
|
pub async fn get_deployment(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
@@ -144,25 +75,6 @@ impl K8sClient {
|
|||||||
Ok(pods.get_opt(name).await?)
|
Ok(pods.get_opt(name).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn patch_resource_by_merge(
|
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
namespace: Option<&str>,
|
|
||||||
gvk: &GroupVersionKind,
|
|
||||||
patch: Value,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let gvk = ApiResource::from_gvk(gvk);
|
|
||||||
let resource: Api<DynamicObject> = if let Some(ns) = namespace {
|
|
||||||
Api::namespaced_with(self.client.clone(), ns, &gvk)
|
|
||||||
} else {
|
|
||||||
Api::default_namespaced_with(self.client.clone(), &gvk)
|
|
||||||
};
|
|
||||||
let pp = PatchParams::default();
|
|
||||||
let merge = Patch::Merge(&patch);
|
|
||||||
resource.patch(name, &pp, &merge).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn scale_deployment(
|
pub async fn scale_deployment(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::{process::Command, sync::Arc};
|
use std::{process::Command, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use kube::api::GroupVersionKind;
|
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
@@ -15,15 +14,13 @@ use crate::{
|
|||||||
monitoring::kube_prometheus::crd::{
|
monitoring::kube_prometheus::crd::{
|
||||||
crd_alertmanager_config::CRDPrometheus,
|
crd_alertmanager_config::CRDPrometheus,
|
||||||
prometheus_operator::prometheus_operator_helm_chart_score,
|
prometheus_operator::prometheus_operator_helm_chart_score,
|
||||||
rhob_alertmanager_config::RHOBObservability,
|
|
||||||
},
|
},
|
||||||
prometheus::{
|
prometheus::{
|
||||||
k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore,
|
k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore,
|
||||||
prometheus::PrometheusApplicationMonitoring, rhob_alerting_score::RHOBAlertingScore,
|
prometheus::PrometheusApplicationMonitoring,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::ingress::Ingress,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -111,43 +108,6 @@ impl PrometheusApplicationMonitoring<CRDPrometheus> for K8sAnywhereTopology {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl PrometheusApplicationMonitoring<RHOBObservability> for K8sAnywhereTopology {
|
|
||||||
async fn install_prometheus(
|
|
||||||
&self,
|
|
||||||
sender: &RHOBObservability,
|
|
||||||
inventory: &Inventory,
|
|
||||||
receivers: Option<Vec<Box<dyn AlertReceiver<RHOBObservability>>>>,
|
|
||||||
) -> Result<PreparationOutcome, PreparationError> {
|
|
||||||
let po_result = self.ensure_cluster_observability_operator(sender).await?;
|
|
||||||
|
|
||||||
if po_result == PreparationOutcome::Noop {
|
|
||||||
debug!("Skipping Prometheus CR installation due to missing operator.");
|
|
||||||
return Ok(po_result);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = self
|
|
||||||
.get_cluster_observability_operator_prometheus_application_score(
|
|
||||||
sender.clone(),
|
|
||||||
receivers,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.interpret(inventory, self)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(outcome) => match outcome.status {
|
|
||||||
InterpretStatus::SUCCESS => Ok(PreparationOutcome::Success {
|
|
||||||
details: outcome.message,
|
|
||||||
}),
|
|
||||||
InterpretStatus::NOOP => Ok(PreparationOutcome::Noop),
|
|
||||||
_ => Err(PreparationError::new(outcome.message)),
|
|
||||||
},
|
|
||||||
Err(err) => Err(PreparationError::new(err.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for K8sAnywhereTopology {
|
impl Serialize for K8sAnywhereTopology {
|
||||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
@@ -174,19 +134,6 @@ impl K8sAnywhereTopology {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_cluster_observability_operator_prometheus_application_score(
|
|
||||||
&self,
|
|
||||||
sender: RHOBObservability,
|
|
||||||
receivers: Option<Vec<Box<dyn AlertReceiver<RHOBObservability>>>>,
|
|
||||||
) -> RHOBAlertingScore {
|
|
||||||
RHOBAlertingScore {
|
|
||||||
sender,
|
|
||||||
receivers: receivers.unwrap_or_default(),
|
|
||||||
service_monitors: vec![],
|
|
||||||
prometheus_rules: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_k8s_prometheus_application_score(
|
async fn get_k8s_prometheus_application_score(
|
||||||
&self,
|
&self,
|
||||||
sender: CRDPrometheus,
|
sender: CRDPrometheus,
|
||||||
@@ -200,26 +147,6 @@ impl K8sAnywhereTopology {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn openshift_ingress_operator_available(&self) -> Result<(), PreparationError> {
|
|
||||||
let client = self.k8s_client().await?;
|
|
||||||
let gvk = GroupVersionKind {
|
|
||||||
group: "operator.openshift.io".into(),
|
|
||||||
version: "v1".into(),
|
|
||||||
kind: "IngressController".into(),
|
|
||||||
};
|
|
||||||
let ic = client
|
|
||||||
.get_resource_json_value("default", Some("openshift-ingress-operator"), &gvk)
|
|
||||||
.await?;
|
|
||||||
let ready_replicas = ic.data["status"]["availableReplicas"].as_i64().unwrap_or(0);
|
|
||||||
if ready_replicas >= 1 {
|
|
||||||
return Ok(());
|
|
||||||
} else {
|
|
||||||
return Err(PreparationError::new(
|
|
||||||
"openshift-ingress-operator not available".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_helm_available(&self) -> Result<(), String> {
|
fn is_helm_available(&self) -> Result<(), String> {
|
||||||
let version_result = Command::new("helm")
|
let version_result = Command::new("helm")
|
||||||
.arg("version")
|
.arg("version")
|
||||||
@@ -359,64 +286,6 @@ impl K8sAnywhereTopology {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_cluster_observability_operator(
|
|
||||||
&self,
|
|
||||||
sender: &RHOBObservability,
|
|
||||||
) -> Result<PreparationOutcome, PreparationError> {
|
|
||||||
let status = Command::new("sh")
|
|
||||||
.args(["-c", "kubectl get crd -A | grep -i rhobs"])
|
|
||||||
.status()
|
|
||||||
.map_err(|e| PreparationError::new(format!("could not connect to cluster: {}", e)))?;
|
|
||||||
|
|
||||||
if !status.success() {
|
|
||||||
if let Some(Some(k8s_state)) = self.k8s_state.get() {
|
|
||||||
match k8s_state.source {
|
|
||||||
K8sSource::LocalK3d => {
|
|
||||||
warn!(
|
|
||||||
"Installing observability operator is not supported on LocalK3d source"
|
|
||||||
);
|
|
||||||
return Ok(PreparationOutcome::Noop);
|
|
||||||
debug!("installing cluster observability operator");
|
|
||||||
todo!();
|
|
||||||
let op_score =
|
|
||||||
prometheus_operator_helm_chart_score(sender.namespace.clone());
|
|
||||||
let result = op_score.interpret(&Inventory::empty(), self).await;
|
|
||||||
|
|
||||||
return match result {
|
|
||||||
Ok(outcome) => match outcome.status {
|
|
||||||
InterpretStatus::SUCCESS => Ok(PreparationOutcome::Success {
|
|
||||||
details: "installed cluster observability operator".into(),
|
|
||||||
}),
|
|
||||||
InterpretStatus::NOOP => Ok(PreparationOutcome::Noop),
|
|
||||||
_ => Err(PreparationError::new(
|
|
||||||
"failed to install cluster observability operator (unknown error)".into(),
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
Err(err) => Err(PreparationError::new(err.to_string())),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
K8sSource::Kubeconfig => {
|
|
||||||
debug!(
|
|
||||||
"unable to install cluster observability operator, contact cluster admin"
|
|
||||||
);
|
|
||||||
return Ok(PreparationOutcome::Noop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"Unable to detect k8s_state. Skipping Cluster Observability Operator install."
|
|
||||||
);
|
|
||||||
return Ok(PreparationOutcome::Noop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Cluster Observability Operator is already present, skipping install");
|
|
||||||
|
|
||||||
Ok(PreparationOutcome::Success {
|
|
||||||
details: "cluster observability operator present in cluster".into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ensure_prometheus_operator(
|
async fn ensure_prometheus_operator(
|
||||||
&self,
|
&self,
|
||||||
sender: &CRDPrometheus,
|
sender: &CRDPrometheus,
|
||||||
@@ -554,7 +423,7 @@ impl MultiTargetTopology for K8sAnywhereTopology {
|
|||||||
match self.config.harmony_profile.to_lowercase().as_str() {
|
match self.config.harmony_profile.to_lowercase().as_str() {
|
||||||
"staging" => DeploymentTarget::Staging,
|
"staging" => DeploymentTarget::Staging,
|
||||||
"production" => DeploymentTarget::Production,
|
"production" => DeploymentTarget::Production,
|
||||||
_ => todo!("HARMONY_PROFILE must be set when use_local_k3d is false"),
|
_ => todo!("HARMONY_PROFILE must be set when use_local_k3d is not set"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -576,45 +445,3 @@ impl TenantManager for K8sAnywhereTopology {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Ingress for K8sAnywhereTopology {
|
|
||||||
//TODO this is specifically for openshift/okd which violates the k8sanywhere idea
|
|
||||||
async fn get_domain(&self, service: &str) -> Result<String, PreparationError> {
|
|
||||||
let client = self.k8s_client().await?;
|
|
||||||
|
|
||||||
if let Some(Some(k8s_state)) = self.k8s_state.get() {
|
|
||||||
match k8s_state.source {
|
|
||||||
K8sSource::LocalK3d => Ok(format!("{service}.local.k3d")),
|
|
||||||
K8sSource::Kubeconfig => {
|
|
||||||
self.openshift_ingress_operator_available().await?;
|
|
||||||
|
|
||||||
let gvk = GroupVersionKind {
|
|
||||||
group: "operator.openshift.io".into(),
|
|
||||||
version: "v1".into(),
|
|
||||||
kind: "IngressController".into(),
|
|
||||||
};
|
|
||||||
let ic = client
|
|
||||||
.get_resource_json_value(
|
|
||||||
"default",
|
|
||||||
Some("openshift-ingress-operator"),
|
|
||||||
&gvk,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| {
|
|
||||||
PreparationError::new("Failed to fetch IngressController".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match ic.data["status"]["domain"].as_str() {
|
|
||||||
Some(domain) => Ok(format!("{service}.{domain}")),
|
|
||||||
None => Err(PreparationError::new("Could not find domain".to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(PreparationError::new(
|
|
||||||
"Cannot get domain: unable to detect K8s state".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -102,17 +102,8 @@ pub enum HttpStatusCode {
|
|||||||
ServerError5xx,
|
ServerError5xx,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
|
||||||
pub enum SSL {
|
|
||||||
SSL,
|
|
||||||
Disabled,
|
|
||||||
Default,
|
|
||||||
SNI,
|
|
||||||
Other(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
pub enum HealthCheck {
|
pub enum HealthCheck {
|
||||||
HTTP(String, HttpMethod, HttpStatusCode, SSL),
|
HTTP(String, HttpMethod, HttpStatusCode),
|
||||||
TCP(Option<u16>),
|
TCP(Option<u16>),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::{HelmCommand, PreparationError, PreparationOutcome, Topology};
|
use super::{HelmCommand, PreparationError, PreparationOutcome, Topology};
|
||||||
|
|
||||||
#[derive(new, Clone, Debug, Serialize, Deserialize)]
|
#[derive(new)]
|
||||||
pub struct LocalhostTopology;
|
pub struct LocalhostTopology;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
mod ha_cluster;
|
mod ha_cluster;
|
||||||
pub mod ingress;
|
|
||||||
use harmony_types::net::IpAddress;
|
use harmony_types::net::IpAddress;
|
||||||
mod host_binding;
|
mod host_binding;
|
||||||
mod http;
|
mod http;
|
||||||
|
|||||||
@@ -11,21 +11,15 @@ use super::{LogicalHost, k8s::K8sClient};
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DHCPStaticEntry {
|
pub struct DHCPStaticEntry {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub mac: Vec<MacAddress>,
|
pub mac: MacAddress,
|
||||||
pub ip: Ipv4Addr,
|
pub ip: Ipv4Addr,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for DHCPStaticEntry {
|
impl std::fmt::Display for DHCPStaticEntry {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let mac = self
|
|
||||||
.mac
|
|
||||||
.iter()
|
|
||||||
.map(|m| m.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(",");
|
|
||||||
f.write_fmt(format_args!(
|
f.write_fmt(format_args!(
|
||||||
"DHCPStaticEntry : name {}, mac {}, ip {}",
|
"DHCPStaticEntry : name {}, mac {}, ip {}",
|
||||||
self.name, mac, self.ip
|
self.name, self.mac, self.ip
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,7 +41,6 @@ impl std::fmt::Debug for dyn Firewall {
|
|||||||
pub struct NetworkDomain {
|
pub struct NetworkDomain {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait K8sclient: Send + Sync {
|
pub trait K8sclient: Send + Sync {
|
||||||
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>;
|
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>;
|
||||||
@@ -66,8 +59,6 @@ pub trait DhcpServer: Send + Sync + std::fmt::Debug {
|
|||||||
async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>;
|
async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>;
|
||||||
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>;
|
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>;
|
||||||
async fn set_pxe_options(&self, pxe_options: PxeOptions) -> Result<(), ExecutorError>;
|
async fn set_pxe_options(&self, pxe_options: PxeOptions) -> Result<(), ExecutorError>;
|
||||||
async fn set_dhcp_range(&self, start: &IpAddress, end: &IpAddress)
|
|
||||||
-> Result<(), ExecutorError>;
|
|
||||||
fn get_ip(&self) -> IpAddress;
|
fn get_ip(&self) -> IpAddress;
|
||||||
fn get_host(&self) -> LogicalHost;
|
fn get_host(&self) -> LogicalHost;
|
||||||
async fn commit_config(&self) -> Result<(), ExecutorError>;
|
async fn commit_config(&self) -> Result<(), ExecutorError>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
hardware::PhysicalHost,
|
hardware::PhysicalHost,
|
||||||
inventory::{HostRole, InventoryRepository, RepoError},
|
inventory::{InventoryRepository, RepoError},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_types::id::Id;
|
use harmony_types::id::Id;
|
||||||
@@ -46,104 +46,20 @@ impl InventoryRepository for SqliteInventoryRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError> {
|
async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError> {
|
||||||
let row = sqlx::query_as!(
|
let _row = sqlx::query_as!(
|
||||||
DbHost,
|
DbHost,
|
||||||
r#"SELECT id, version_id, data as "data: Json<PhysicalHost>" FROM physical_hosts WHERE id = ? ORDER BY version_id DESC LIMIT 1"#,
|
r#"SELECT id, version_id, data as "data: Json<PhysicalHost>" FROM physical_hosts WHERE id = ? ORDER BY version_id DESC LIMIT 1"#,
|
||||||
host_id
|
host_id
|
||||||
)
|
)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
todo!()
|
||||||
Ok(row.map(|r| r.data.0))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_all_hosts(&self) -> Result<Vec<PhysicalHost>, RepoError> {
|
|
||||||
let db_hosts = sqlx::query_as!(
|
|
||||||
DbHost,
|
|
||||||
r#"
|
|
||||||
SELECT
|
|
||||||
p1.id,
|
|
||||||
p1.version_id,
|
|
||||||
p1.data as "data: Json<PhysicalHost>"
|
|
||||||
FROM
|
|
||||||
physical_hosts p1
|
|
||||||
INNER JOIN (
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
MAX(version_id) AS max_version
|
|
||||||
FROM
|
|
||||||
physical_hosts
|
|
||||||
GROUP BY
|
|
||||||
id
|
|
||||||
) p2 ON p1.id = p2.id AND p1.version_id = p2.max_version
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let hosts = db_hosts.into_iter().map(|row| row.data.0).collect();
|
|
||||||
|
|
||||||
Ok(hosts)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn save_role_mapping(
|
|
||||||
&self,
|
|
||||||
role: &HostRole,
|
|
||||||
host: &PhysicalHost,
|
|
||||||
) -> Result<(), RepoError> {
|
|
||||||
let host_id = host.id.to_string();
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO host_role_mapping (host_id, role)
|
|
||||||
VALUES (?, ?)
|
|
||||||
"#,
|
|
||||||
host_id,
|
|
||||||
role
|
|
||||||
)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("Saved role mapping for host '{}' as '{:?}'", host.id, role);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_host_for_role(&self, role: &HostRole) -> Result<Vec<PhysicalHost>, RepoError> {
|
|
||||||
struct HostIdRow {
|
|
||||||
host_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let role_str = format!("{:?}", role);
|
|
||||||
|
|
||||||
let host_id_rows = sqlx::query_as!(
|
|
||||||
HostIdRow,
|
|
||||||
"SELECT host_id FROM host_role_mapping WHERE role = ?",
|
|
||||||
role_str
|
|
||||||
)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut hosts = Vec::with_capacity(host_id_rows.len());
|
|
||||||
for row in host_id_rows {
|
|
||||||
match self.get_latest_by_id(&row.host_id).await? {
|
|
||||||
Some(host) => hosts.push(host),
|
|
||||||
None => {
|
|
||||||
log::warn!(
|
|
||||||
"Found a role mapping for host_id '{}', but the host does not exist in the physical_hosts table. This may indicate a data integrity issue.",
|
|
||||||
row.host_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(hosts)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use sqlx::types::Json;
|
use sqlx::types::Json;
|
||||||
struct DbHost {
|
struct DbHost {
|
||||||
data: Json<PhysicalHost>,
|
data: Json<PhysicalHost>,
|
||||||
id: String,
|
id: Id,
|
||||||
version_id: String,
|
version_id: Id,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ impl DhcpServer for OPNSenseFirewall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> {
|
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> {
|
||||||
let mac: Vec<String> = entry.mac.iter().map(MacAddress::to_string).collect();
|
let mac: String = String::from(&entry.mac);
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
let mut writable_opnsense = self.opnsense_config.write().await;
|
||||||
writable_opnsense
|
writable_opnsense
|
||||||
.dhcp()
|
.dhcp()
|
||||||
.add_static_mapping(&mac, &entry.ip, &entry.name)
|
.add_static_mapping(&mac, entry.ip, &entry.name)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,19 +68,4 @@ impl DhcpServer for OPNSenseFirewall {
|
|||||||
ExecutorError::UnexpectedError(format!("Failed to set_pxe_options : {dhcp_error}"))
|
ExecutorError::UnexpectedError(format!("Failed to set_pxe_options : {dhcp_error}"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_dhcp_range(
|
|
||||||
&self,
|
|
||||||
start: &IpAddress,
|
|
||||||
end: &IpAddress,
|
|
||||||
) -> Result<(), ExecutorError> {
|
|
||||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
|
||||||
writable_opnsense
|
|
||||||
.dhcp()
|
|
||||||
.set_dhcp_range(&start.to_string(), &end.to_string())
|
|
||||||
.await
|
|
||||||
.map_err(|dhcp_error| {
|
|
||||||
ExecutorError::UnexpectedError(format!("Failed to set_dhcp_range : {dhcp_error}"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::infra::opnsense::Host;
|
||||||
use crate::infra::opnsense::LogicalHost;
|
use crate::infra::opnsense::LogicalHost;
|
||||||
use crate::{
|
use crate::{
|
||||||
executors::ExecutorError,
|
executors::ExecutorError,
|
||||||
@@ -11,22 +12,21 @@ use super::OPNSenseFirewall;
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl DnsServer for OPNSenseFirewall {
|
impl DnsServer for OPNSenseFirewall {
|
||||||
async fn register_hosts(&self, hosts: Vec<DnsRecord>) -> Result<(), ExecutorError> {
|
async fn register_hosts(&self, hosts: Vec<DnsRecord>) -> Result<(), ExecutorError> {
|
||||||
todo!("Refactor this to use dnsmasq")
|
let mut writable_opnsense = self.opnsense_config.write().await;
|
||||||
// let mut writable_opnsense = self.opnsense_config.write().await;
|
let mut dns = writable_opnsense.dns();
|
||||||
// let mut dns = writable_opnsense.dns();
|
let hosts = hosts
|
||||||
// let hosts = hosts
|
.iter()
|
||||||
// .iter()
|
.map(|h| {
|
||||||
// .map(|h| {
|
Host::new(
|
||||||
// Host::new(
|
h.host.clone(),
|
||||||
// h.host.clone(),
|
h.domain.clone(),
|
||||||
// h.domain.clone(),
|
h.record_type.to_string(),
|
||||||
// h.record_type.to_string(),
|
h.value.to_string(),
|
||||||
// h.value.to_string(),
|
)
|
||||||
// )
|
})
|
||||||
// })
|
.collect();
|
||||||
// .collect();
|
dns.register_hosts(hosts);
|
||||||
// dns.add_static_mapping(hosts);
|
Ok(())
|
||||||
// Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_record(
|
fn remove_record(
|
||||||
@@ -38,26 +38,25 @@ impl DnsServer for OPNSenseFirewall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn list_records(&self) -> Vec<crate::topology::DnsRecord> {
|
async fn list_records(&self) -> Vec<crate::topology::DnsRecord> {
|
||||||
todo!("Refactor this to use dnsmasq")
|
self.opnsense_config
|
||||||
// self.opnsense_config
|
.write()
|
||||||
// .write()
|
.await
|
||||||
// .await
|
.dns()
|
||||||
// .dns()
|
.get_hosts()
|
||||||
// .get_hosts()
|
.iter()
|
||||||
// .iter()
|
.map(|h| DnsRecord {
|
||||||
// .map(|h| DnsRecord {
|
host: h.hostname.clone(),
|
||||||
// host: h.hostname.clone(),
|
domain: h.domain.clone(),
|
||||||
// domain: h.domain.clone(),
|
record_type: h
|
||||||
// record_type: h
|
.rr
|
||||||
// .rr
|
.parse()
|
||||||
// .parse()
|
.expect("received invalid record type {h.rr} from opnsense"),
|
||||||
// .expect("received invalid record type {h.rr} from opnsense"),
|
value: h
|
||||||
// value: h
|
.server
|
||||||
// .server
|
.parse()
|
||||||
// .parse()
|
.expect("received invalid ipv4 record from opnsense {h.server}"),
|
||||||
// .expect("received invalid ipv4 record from opnsense {h.server}"),
|
})
|
||||||
// })
|
.collect()
|
||||||
// .collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ip(&self) -> IpAddress {
|
fn get_ip(&self) -> IpAddress {
|
||||||
@@ -69,12 +68,11 @@ impl DnsServer for OPNSenseFirewall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError> {
|
async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError> {
|
||||||
todo!("Refactor this to use dnsmasq")
|
let mut writable_opnsense = self.opnsense_config.write().await;
|
||||||
// let mut writable_opnsense = self.opnsense_config.write().await;
|
let mut dns = writable_opnsense.dns();
|
||||||
// let mut dns = writable_opnsense.dns();
|
dns.register_dhcp_leases(register);
|
||||||
// dns.register_dhcp_leases(register);
|
|
||||||
//
|
Ok(())
|
||||||
// Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn commit_config(&self) -> Result<(), ExecutorError> {
|
async fn commit_config(&self) -> Result<(), ExecutorError> {
|
||||||
|
|||||||
@@ -10,21 +10,13 @@ const OPNSENSE_HTTP_ROOT_PATH: &str = "/usr/local/http";
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl HttpServer for OPNSenseFirewall {
|
impl HttpServer for OPNSenseFirewall {
|
||||||
async fn serve_files(
|
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> {
|
||||||
&self,
|
|
||||||
url: &Url,
|
|
||||||
remote_path: &Option<String>,
|
|
||||||
) -> Result<(), ExecutorError> {
|
|
||||||
let config = self.opnsense_config.read().await;
|
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 {OPNSENSE_HTTP_ROOT_PATH}");
|
||||||
let remote_upload_path = remote_path
|
|
||||||
.clone()
|
|
||||||
.map(|r| format!("{OPNSENSE_HTTP_ROOT_PATH}/{r}"))
|
|
||||||
.unwrap_or(OPNSENSE_HTTP_ROOT_PATH.to_string());
|
|
||||||
match url {
|
match url {
|
||||||
Url::LocalFolder(path) => {
|
Url::LocalFolder(path) => {
|
||||||
config
|
config
|
||||||
.upload_files(path, &remote_upload_path)
|
.upload_files(path, OPNSENSE_HTTP_ROOT_PATH)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
|
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, info, warn};
|
||||||
use opnsense_config_xml::{
|
use opnsense_config_xml::{Frontend, HAProxy, HAProxyBackend, HAProxyHealthCheck, HAProxyServer};
|
||||||
Frontend, HAProxy, HAProxyBackend, HAProxyHealthCheck, HAProxyServer, MaybeString,
|
|
||||||
};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
executors::ExecutorError,
|
executors::ExecutorError,
|
||||||
topology::{
|
topology::{
|
||||||
BackendServer, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService,
|
BackendServer, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService,
|
||||||
LogicalHost, SSL,
|
LogicalHost,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use harmony_types::net::IpAddress;
|
use harmony_types::net::IpAddress;
|
||||||
@@ -208,22 +206,7 @@ pub(crate) fn get_health_check_for_backend(
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into();
|
.into();
|
||||||
let status_code: HttpStatusCode = HttpStatusCode::Success2xx;
|
let status_code: HttpStatusCode = HttpStatusCode::Success2xx;
|
||||||
let ssl = match haproxy_health_check
|
Some(HealthCheck::HTTP(path, method, status_code))
|
||||||
.ssl
|
|
||||||
.content_string()
|
|
||||||
.to_uppercase()
|
|
||||||
.as_str()
|
|
||||||
{
|
|
||||||
"SSL" => SSL::SSL,
|
|
||||||
"SSLNI" => SSL::SNI,
|
|
||||||
"NOSSL" => SSL::Disabled,
|
|
||||||
"" => SSL::Default,
|
|
||||||
other => {
|
|
||||||
error!("Unknown haproxy health check ssl config {other}");
|
|
||||||
SSL::Other(other.to_string())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Some(HealthCheck::HTTP(path, method, status_code, ssl))
|
|
||||||
}
|
}
|
||||||
_ => panic!("Received unsupported health check type {}", uppercase),
|
_ => panic!("Received unsupported health check type {}", uppercase),
|
||||||
}
|
}
|
||||||
@@ -258,14 +241,7 @@ pub(crate) fn harmony_load_balancer_service_to_haproxy_xml(
|
|||||||
// frontend points to backend
|
// frontend points to backend
|
||||||
let healthcheck = if let Some(health_check) = &service.health_check {
|
let healthcheck = if let Some(health_check) = &service.health_check {
|
||||||
match health_check {
|
match health_check {
|
||||||
HealthCheck::HTTP(path, http_method, _http_status_code, ssl) => {
|
HealthCheck::HTTP(path, http_method, _http_status_code) => {
|
||||||
let ssl: MaybeString = match ssl {
|
|
||||||
SSL::SSL => "ssl".into(),
|
|
||||||
SSL::SNI => "sslni".into(),
|
|
||||||
SSL::Disabled => "nossl".into(),
|
|
||||||
SSL::Default => "".into(),
|
|
||||||
SSL::Other(other) => other.as_str().into(),
|
|
||||||
};
|
|
||||||
let haproxy_check = HAProxyHealthCheck {
|
let haproxy_check = HAProxyHealthCheck {
|
||||||
name: format!("HTTP_{http_method}_{path}"),
|
name: format!("HTTP_{http_method}_{path}"),
|
||||||
uuid: Uuid::new_v4().to_string(),
|
uuid: Uuid::new_v4().to_string(),
|
||||||
@@ -273,7 +249,6 @@ pub(crate) fn harmony_load_balancer_service_to_haproxy_xml(
|
|||||||
health_check_type: "http".to_string(),
|
health_check_type: "http".to_string(),
|
||||||
http_uri: path.clone().into(),
|
http_uri: path.clone().into(),
|
||||||
interval: "2s".to_string(),
|
interval: "2s".to_string(),
|
||||||
ssl,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||