Compare commits
90 Commits
better-cli
...
a353249eec
| Author | SHA1 | Date | |
|---|---|---|---|
| a353249eec | |||
| f6e665f990 | |||
| 241980ebec | |||
| 35a459f63c | |||
| f076d36297 | |||
| 138e414727 | |||
| b6be44202e | |||
| c372e781d8 | |||
| 56c181fc3d | |||
| 55bfe306ad | |||
| 62fa3c2b10 | |||
| ea1380f98a | |||
| 701d8cfab9 | |||
| f9906cb419 | |||
| cb4382fbb5 | |||
| 1eca2cc1a9 | |||
| 269f13ae9b | |||
| ec277bc13d | |||
| a9f8cd16ea | |||
| c542a935e3 | |||
| 0395d11e98 | |||
| 05e7b8075c | |||
| b857412151 | |||
| 7bb3602ab8 | |||
| 78b80c2169 | |||
| 0876f4e4f0 | |||
| 6ac0e095a3 | |||
| ff2efc0a66 | |||
|
|
f180cc4c80 | ||
| 3ca31179d0 | |||
| a9fe4ab267 | |||
| 65cc9befeb | |||
| d456a1f9ee | |||
| 5895f867cf | |||
| 8cc7adf196 | |||
| a1ab5d40fb | |||
| 6c92dd24f7 | |||
| c805d7e018 | |||
| b33615b969 | |||
| 0f59f29ac4 | |||
| 361f240762 | |||
| 57c3b01e66 | |||
| 94ddf027dd | |||
| 06a2be4496 | |||
| e2a09efdee | |||
| d36c574590 | |||
| 2618441de3 | |||
| da6610c625 | |||
| e956772593 | |||
| 27c51e0ec5 | |||
| bfca9cf163 | |||
| 597dcbc848 | |||
| cd3ea6fc10 | |||
| a53e8552e9 | |||
| 89eb88d10e | |||
| 72fb05b5cc | |||
| 6685b05cc5 | |||
| 07116eb8a6 | |||
| 3f34f868eb | |||
| bc6f7336d2 | |||
| 01da8631da | |||
| 67b5c2df07 | |||
| 1eaf63417b | |||
| 5e7803d2ba | |||
| 9a610661c7 | |||
| 70a65ed5d0 | |||
| 26e8e386b9 | |||
| 19cb7f73bc | |||
| 84f38974b1 | |||
| 7d027bcfc4 | |||
| d1a274b705 | |||
| b43ca7c740 | |||
| 2a6a233fb2 | |||
|
|
610ce84280 | ||
|
|
8bb4a9d3f6 | ||
|
|
67f3a23071 | ||
| d86970f81b | |||
| 623a3f019b | |||
| fd8f643a8f | |||
|
|
bd214f8fb8 | ||
| f0ed548755 | |||
| 1de96027a1 | |||
| 0812937a67 | |||
| 29a261575b | |||
| dcf8335240 | |||
|
|
f876b5e67b | ||
| 440c1bce12 | |||
| 024084859e | |||
| 54990cd1a5 | |||
| 06aab1f57f |
@@ -9,7 +9,7 @@ jobs:
|
|||||||
check:
|
check:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: hub.nationtech.io/harmony/harmony_composer:latest@sha256:eb0406fcb95c63df9b7c4b19bc50ad7914dd8232ce98e9c9abef628e07c69386
|
image: hub.nationtech.io/harmony/harmony_composer:latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
package_harmony_composer:
|
package_harmony_composer:
|
||||||
container:
|
container:
|
||||||
image: hub.nationtech.io/harmony/harmony_composer:latest@sha256:eb0406fcb95c63df9b7c4b19bc50ad7914dd8232ce98e9c9abef628e07c69386
|
image: hub.nationtech.io/harmony/harmony_composer:latest
|
||||||
runs-on: dind
|
runs-on: dind
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -45,14 +45,14 @@ jobs:
|
|||||||
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
|
||||||
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/tags/snapshot-latest" \
|
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/tags/snapshot-latest" \
|
||||||
| jq -r '.id // empty')
|
| jq -r '.id // empty')
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
# Delete existing release
|
# Delete existing release
|
||||||
curl -X DELETE \
|
curl -X DELETE \
|
||||||
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
|
||||||
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/$RELEASE_ID"
|
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/$RELEASE_ID"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create new release
|
# Create new release
|
||||||
RESPONSE=$(curl -X POST \
|
RESPONSE=$(curl -X POST \
|
||||||
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
"prerelease": true
|
"prerelease": true
|
||||||
}' \
|
}' \
|
||||||
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases")
|
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases")
|
||||||
|
|
||||||
echo "RELEASE_ID=$(echo $RESPONSE | jq -r '.id')" >> $GITHUB_ENV
|
echo "RELEASE_ID=$(echo $RESPONSE | jq -r '.id')" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload Linux binary
|
- name: Upload Linux binary
|
||||||
|
|||||||
20
.sqlx/query-2ea29df2326f7c84bd4100ad510a3fd4878dc2e217dc83f9bf45a402dfd62a91.json
generated
Normal file
20
.sqlx/query-2ea29df2326f7c84bd4100ad510a3fd4878dc2e217dc83f9bf45a402dfd62a91.json
generated
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
32
.sqlx/query-8d247918eca10a88b784ee353db090c94a222115c543231f2140cba27bd0f067.json
generated
Normal file
32
.sqlx/query-8d247918eca10a88b784ee353db090c94a222115c543231f2140cba27bd0f067.json
generated
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
32
.sqlx/query-934035c7ca6e064815393e4e049a7934b0a7fac04a4fe4b2a354f0443d630990.json
generated
Normal file
32
.sqlx/query-934035c7ca6e064815393e4e049a7934b0a7fac04a4fe4b2a354f0443d630990.json
generated
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT id, version_id, data as \"data: Json<PhysicalHost>\" FROM physical_hosts WHERE id = ? ORDER BY version_id DESC LIMIT 1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "version_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data: Json<PhysicalHost>",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "934035c7ca6e064815393e4e049a7934b0a7fac04a4fe4b2a354f0443d630990"
|
||||||
|
}
|
||||||
12
.sqlx/query-df7a7c9cfdd0972e2e0ce7ea444ba8bc9d708a4fb89d5593a0be2bbebde62aff.json
generated
Normal file
12
.sqlx/query-df7a7c9cfdd0972e2e0ce7ea444ba8bc9d708a4fb89d5593a0be2bbebde62aff.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO host_role_mapping (host_id, role)\n VALUES (?, ?)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "df7a7c9cfdd0972e2e0ce7ea444ba8bc9d708a4fb89d5593a0be2bbebde62aff"
|
||||||
|
}
|
||||||
12
.sqlx/query-f10f615ee42129ffa293e46f2f893d65a237d31d24b74a29c6a8d8420d255ab8.json
generated
Normal file
12
.sqlx/query-f10f615ee42129ffa293e46f2f893d65a237d31d24b74a29c6a8d8420d255ab8.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO physical_hosts (id, version_id, data) VALUES (?, ?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 3
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "f10f615ee42129ffa293e46f2f893d65a237d31d24b74a29c6a8d8420d255ab8"
|
||||||
|
}
|
||||||
1755
Cargo.lock
generated
1755
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
@@ -12,6 +12,9 @@ members = [
|
|||||||
"harmony_cli",
|
"harmony_cli",
|
||||||
"k3d",
|
"k3d",
|
||||||
"harmony_composer",
|
"harmony_composer",
|
||||||
|
"harmony_inventory_agent",
|
||||||
|
"harmony_secret_derive",
|
||||||
|
"harmony_secret", "adr/agent_discovery/mdns",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
@@ -20,7 +23,7 @@ readme = "README.md"
|
|||||||
license = "GNU AGPL v3"
|
license = "GNU AGPL v3"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
log = "0.4"
|
log = { version = "0.4", features = ["kv"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
derive-new = "0.7"
|
derive-new = "0.7"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
@@ -33,7 +36,7 @@ tokio = { version = "1.40", features = [
|
|||||||
cidr = { features = ["serde"], version = "0.2" }
|
cidr = { features = ["serde"], version = "0.2" }
|
||||||
russh = "0.45"
|
russh = "0.45"
|
||||||
russh-keys = "0.45"
|
russh-keys = "0.45"
|
||||||
rand = "0.8"
|
rand = "0.9"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
kube = { version = "1.1.0", features = [
|
kube = { version = "1.1.0", features = [
|
||||||
"config",
|
"config",
|
||||||
@@ -53,6 +56,15 @@ chrono = "0.4"
|
|||||||
similar = "2"
|
similar = "2"
|
||||||
uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
|
tempfile = "3.20.0"
|
||||||
bollard = "0.19.1"
|
bollard = "0.19.1"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
directories = "6.0.0"
|
||||||
|
thiserror = "2.0.14"
|
||||||
|
serde = { version = "1.0.209", features = ["derive", "rc"] }
|
||||||
|
serde_json = "1.0.127"
|
||||||
|
askama = "0.14"
|
||||||
|
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite" ] }
|
||||||
|
reqwest = { version = "0.12", features = ["blocking", "stream", "rustls-tls", "http2", "json"], default-features = false }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM docker.io/rust:1.87.0 AS build
|
FROM docker.io/rust:1.89.0 AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -6,13 +6,14 @@ COPY . .
|
|||||||
|
|
||||||
RUN cargo build --release --bin harmony_composer
|
RUN cargo build --release --bin harmony_composer
|
||||||
|
|
||||||
FROM docker.io/rust:1.87.0
|
FROM docker.io/rust:1.89.0
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN rustup target add x86_64-pc-windows-gnu
|
RUN rustup target add x86_64-pc-windows-gnu
|
||||||
RUN rustup target add x86_64-unknown-linux-gnu
|
RUN rustup target add x86_64-unknown-linux-gnu
|
||||||
RUN rustup component add rustfmt
|
RUN rustup component add rustfmt
|
||||||
|
RUN rustup component add clippy
|
||||||
|
|
||||||
RUN apt update
|
RUN apt update
|
||||||
|
|
||||||
@@ -22,4 +23,4 @@ RUN apt install -y nodejs docker.io mingw-w64
|
|||||||
|
|
||||||
COPY --from=build /app/target/release/harmony_composer .
|
COPY --from=build /app/target/release/harmony_composer .
|
||||||
|
|
||||||
ENTRYPOINT ["/app/harmony_composer"]
|
ENTRYPOINT ["/app/harmony_composer"]
|
||||||
|
|||||||
73
README.md
73
README.md
@@ -1,5 +1,6 @@
|
|||||||
# Harmony : Open-source infrastructure orchestration that treats your platform like first-class code.
|
# Harmony : Open-source infrastructure orchestration that treats your platform like first-class code
|
||||||
*By [NationTech](https://nationtech.io)*
|
|
||||||
|
_By [NationTech](https://nationtech.io)_
|
||||||
|
|
||||||
[](https://git.nationtech.io/nationtech/harmony)
|
[](https://git.nationtech.io/nationtech/harmony)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
@@ -23,11 +24,11 @@ From a **developer laptop** to a **global production cluster**, a single **sourc
|
|||||||
|
|
||||||
Infrastructure is essential, but it shouldn’t be your core business. Harmony is built on three guiding principles that make modern platforms reliable, repeatable, and easy to reason about.
|
Infrastructure is essential, but it shouldn’t be your core business. Harmony is built on three guiding principles that make modern platforms reliable, repeatable, and easy to reason about.
|
||||||
|
|
||||||
| Principle | What it means for you |
|
| Principle | What it means for you |
|
||||||
|-----------|-----------------------|
|
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| **Infrastructure as Resilient Code** | Replace sprawling YAML and bash scripts with type-safe Rust. Test, refactor, and version your platform just like application code. |
|
| **Infrastructure as Resilient Code** | Replace sprawling YAML and bash scripts with type-safe Rust. Test, refactor, and version your platform just like application code. |
|
||||||
| **Prove It Works — Before You Deploy** | Harmony uses the compiler to verify that your application’s needs match the target environment’s capabilities at **compile-time**, eliminating an entire class of runtime outages. |
|
| **Prove It Works — Before You Deploy** | Harmony uses the compiler to verify that your application’s needs match the target environment’s capabilities at **compile-time**, eliminating an entire class of runtime outages. |
|
||||||
| **One Unified Model** | Software and infrastructure are a single system. Harmony models them together, enabling deep automation—from bare-metal servers to Kubernetes workloads—with zero context switching. |
|
| **One Unified Model** | Software and infrastructure are a single system. Harmony models them together, enabling deep automation—from bare-metal servers to Kubernetes workloads—with zero context switching. |
|
||||||
|
|
||||||
These principles surface as simple, ergonomic Rust APIs that let teams focus on their product while trusting the platform underneath.
|
These principles surface as simple, ergonomic Rust APIs that let teams focus on their product while trusting the platform underneath.
|
||||||
|
|
||||||
@@ -63,22 +64,20 @@ async fn main() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Pick where it should run
|
// 2. Enhance with extra scores (monitoring, CI/CD, …)
|
||||||
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
|
||||||
Inventory::autoload(), // auto-detect hardware / kube-config
|
|
||||||
K8sAnywhereTopology::from_env(), // local k3d, CI, staging, prod…
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// 3. Enhance with extra scores (monitoring, CI/CD, …)
|
|
||||||
let mut monitoring = MonitoringAlertingStackScore::new();
|
let mut monitoring = MonitoringAlertingStackScore::new();
|
||||||
monitoring.namespace = Some(lamp_stack.config.namespace.clone());
|
monitoring.namespace = Some(lamp_stack.config.namespace.clone());
|
||||||
|
|
||||||
maestro.register_all(vec![Box::new(lamp_stack), Box::new(monitoring)]);
|
// 3. Run your scores on the desired topology & inventory
|
||||||
|
harmony_cli::run(
|
||||||
// 4. Launch an interactive CLI / TUI
|
Inventory::autoload(), // auto-detect hardware / kube-config
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
K8sAnywhereTopology::from_env(), // local k3d, CI, staging, prod…
|
||||||
|
vec![
|
||||||
|
Box::new(lamp_stack),
|
||||||
|
Box::new(monitoring)
|
||||||
|
],
|
||||||
|
None
|
||||||
|
).await.unwrap();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -94,13 +93,13 @@ Harmony analyses the code, shows an execution plan in a TUI, and applies it once
|
|||||||
|
|
||||||
## 3 · Core Concepts
|
## 3 · Core Concepts
|
||||||
|
|
||||||
| Term | One-liner |
|
| Term | One-liner |
|
||||||
|------|-----------|
|
| ---------------- | ---------------------------------------------------------------------------------------------------- |
|
||||||
| **Score<T>** | Declarative description of the desired state (e.g., `LAMPScore`). |
|
| **Score<T>** | Declarative description of the desired state (e.g., `LAMPScore`). |
|
||||||
| **Interpret<T>** | Imperative logic that realises a `Score` on a specific environment. |
|
| **Interpret<T>** | Imperative logic that realises a `Score` on a specific environment. |
|
||||||
| **Topology** | An environment (local k3d, AWS, bare-metal) exposing verified *Capabilities* (Kubernetes, DNS, …). |
|
| **Topology** | An environment (local k3d, AWS, bare-metal) exposing verified _Capabilities_ (Kubernetes, DNS, …). |
|
||||||
| **Maestro** | Orchestrator that compiles Scores + Topology, ensuring all capabilities line up **at compile-time**. |
|
| **Maestro** | Orchestrator that compiles Scores + Topology, ensuring all capabilities line up **at compile-time**. |
|
||||||
| **Inventory** | Optional catalogue of physical assets for bare-metal and edge deployments. |
|
| **Inventory** | Optional catalogue of physical assets for bare-metal and edge deployments. |
|
||||||
|
|
||||||
A visual overview is in the diagram below.
|
A visual overview is in the diagram below.
|
||||||
|
|
||||||
@@ -112,9 +111,9 @@ A visual overview is in the diagram below.
|
|||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
* Rust
|
- Rust
|
||||||
* Docker (if you deploy locally)
|
- Docker (if you deploy locally)
|
||||||
* `kubectl` / `helm` for Kubernetes-based topologies
|
- `kubectl` / `helm` for Kubernetes-based topologies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.nationtech.io/nationtech/harmony
|
git clone https://git.nationtech.io/nationtech/harmony
|
||||||
@@ -126,15 +125,15 @@ cargo build --release # builds the CLI, TUI and libraries
|
|||||||
|
|
||||||
## 5 · Learning More
|
## 5 · Learning More
|
||||||
|
|
||||||
* **Architectural Decision Records** – dive into the rationale
|
- **Architectural Decision Records** – dive into the rationale
|
||||||
- [ADR-001 · Why Rust](adr/001-rust.md)
|
- [ADR-001 · Why Rust](adr/001-rust.md)
|
||||||
- [ADR-003 · Infrastructure Abstractions](adr/003-infrastructure-abstractions.md)
|
- [ADR-003 · Infrastructure Abstractions](adr/003-infrastructure-abstractions.md)
|
||||||
- [ADR-006 · Secret Management](adr/006-secret-management.md)
|
- [ADR-006 · Secret Management](adr/006-secret-management.md)
|
||||||
- [ADR-011 · Multi-Tenant Cluster](adr/011-multi-tenant-cluster.md)
|
- [ADR-011 · Multi-Tenant Cluster](adr/011-multi-tenant-cluster.md)
|
||||||
|
|
||||||
* **Extending Harmony** – write new Scores / Interprets, add hardware like OPNsense firewalls, or embed Harmony in your own tooling (`/docs`).
|
- **Extending Harmony** – write new Scores / Interprets, add hardware like OPNsense firewalls, or embed Harmony in your own tooling (`/docs`).
|
||||||
|
|
||||||
* **Community** – discussions and roadmap live in [GitLab issues](https://git.nationtech.io/nationtech/harmony/-/issues). PRs, ideas, and feedback are welcome!
|
- **Community** – discussions and roadmap live in [GitLab issues](https://git.nationtech.io/nationtech/harmony/-/issues). PRs, ideas, and feedback are welcome!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -148,4 +147,4 @@ See [LICENSE](LICENSE) for the full text.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Made with ❤️ & 🦀 by the NationTech and the Harmony community*
|
_Made with ❤️ & 🦀 by the NationTech and the Harmony community_
|
||||||
|
|||||||
17
adr/agent_discovery/mdns/Cargo.toml
Normal file
17
adr/agent_discovery/mdns/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "mdns"
|
||||||
|
edition = "2024"
|
||||||
|
version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mdns-sd = "0.14"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
futures = "0.3"
|
||||||
|
dmidecode = "0.2" # For getting the motherboard ID on the agent
|
||||||
|
log.workspace=true
|
||||||
|
env_logger.workspace=true
|
||||||
|
clap = { version = "4.5.46", features = ["derive"] }
|
||||||
|
get_if_addrs = "0.5.3"
|
||||||
|
local-ip-address = "0.6.5"
|
||||||
60
adr/agent_discovery/mdns/src/advertise.rs
Normal file
60
adr/agent_discovery/mdns/src/advertise.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// harmony-agent/src/main.rs
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
use mdns_sd::{ServiceDaemon, ServiceInfo};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::SERVICE_TYPE;
|
||||||
|
|
||||||
|
// The service we are advertising.
|
||||||
|
const SERVICE_PORT: u16 = 43210; // A port for the service. It needs one, even if unused.
|
||||||
|
|
||||||
|
pub async fn advertise() {
|
||||||
|
info!("Starting Harmony Agent...");
|
||||||
|
|
||||||
|
// Get a unique ID for this machine.
|
||||||
|
let motherboard_id = "some motherboard id";
|
||||||
|
let instance_name = format!("harmony-agent-{}", motherboard_id);
|
||||||
|
info!("This agent's instance name: {}", instance_name);
|
||||||
|
info!("Advertising with ID: {}", motherboard_id);
|
||||||
|
|
||||||
|
// Create a new mDNS daemon.
|
||||||
|
let mdns = ServiceDaemon::new().expect("Failed to create mDNS daemon");
|
||||||
|
|
||||||
|
// Create a TXT record HashMap to hold our metadata.
|
||||||
|
let mut properties = HashMap::new();
|
||||||
|
properties.insert("id".to_string(), motherboard_id.to_string());
|
||||||
|
properties.insert("version".to_string(), "1.0".to_string());
|
||||||
|
|
||||||
|
// Create the service information.
|
||||||
|
// The instance name should be unique on the network.
|
||||||
|
let local_ip = local_ip_address::local_ip().unwrap();
|
||||||
|
let service_info = ServiceInfo::new(
|
||||||
|
SERVICE_TYPE,
|
||||||
|
&instance_name,
|
||||||
|
"harmony-host.local.", // A hostname for the service
|
||||||
|
local_ip,
|
||||||
|
// "0.0.0.0",
|
||||||
|
SERVICE_PORT,
|
||||||
|
Some(properties),
|
||||||
|
)
|
||||||
|
.expect("Failed to create service info");
|
||||||
|
|
||||||
|
// Register our service with the daemon.
|
||||||
|
mdns.register(service_info)
|
||||||
|
.expect("Failed to register service");
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Service '{}' registered and now being advertised.",
|
||||||
|
instance_name
|
||||||
|
);
|
||||||
|
info!("Agent is running. Press Ctrl+C to exit.");
|
||||||
|
|
||||||
|
for iface in get_if_addrs::get_if_addrs().unwrap() {
|
||||||
|
println!("{:#?}", iface);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the agent running indefinitely.
|
||||||
|
tokio::signal::ctrl_c().await.unwrap();
|
||||||
|
info!("Shutting down agent.");
|
||||||
|
}
|
||||||
110
adr/agent_discovery/mdns/src/discover.rs
Normal file
110
adr/agent_discovery/mdns/src/discover.rs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
use log::debug;
|
||||||
|
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||||
|
|
||||||
|
use crate::SERVICE_TYPE;
|
||||||
|
|
||||||
|
pub async fn discover() {
|
||||||
|
println!("Starting Harmony Master and browsing for agents...");
|
||||||
|
|
||||||
|
// Create a new mDNS daemon.
|
||||||
|
let mdns = ServiceDaemon::new().expect("Failed to create mDNS daemon");
|
||||||
|
|
||||||
|
// Start browsing for the service type.
|
||||||
|
// The receiver will be a stream of events.
|
||||||
|
let receiver = mdns.browse(SERVICE_TYPE).expect("Failed to browse");
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Listening for mDNS events for '{}'. Press Ctrl+C to exit.",
|
||||||
|
SERVICE_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
while let Ok(event) = receiver.recv() {
|
||||||
|
match event {
|
||||||
|
ServiceEvent::ServiceData(resolved) => {
|
||||||
|
println!("Resolved a new service: {}", resolved.fullname);
|
||||||
|
}
|
||||||
|
other_event => {
|
||||||
|
println!("Received other event: {:?}", &other_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gracefully shutdown the daemon.
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(1000000));
|
||||||
|
mdns.shutdown().unwrap();
|
||||||
|
|
||||||
|
// Process events as they come in.
|
||||||
|
// while let Ok(event) = receiver.recv_async().await {
|
||||||
|
// debug!("Received event {event:?}");
|
||||||
|
// // match event {
|
||||||
|
// // ServiceEvent::ServiceFound(svc_type, fullname) => {
|
||||||
|
// // println!("\n--- Agent Discovered ---");
|
||||||
|
// // println!(" Service Name: {}", fullname());
|
||||||
|
// // // You can now resolve this service to get its IP, port, and TXT records
|
||||||
|
// // // The resolve operation is a separate network call.
|
||||||
|
// // let receiver = mdns.browse(info.get_fullname()).unwrap();
|
||||||
|
// // if let Ok(resolve_event) = receiver.recv_timeout(Duration::from_secs(2)) {
|
||||||
|
// // if let ServiceEvent::ServiceResolved(info) = resolve_event {
|
||||||
|
// // let ip = info.get_addresses().iter().next().unwrap();
|
||||||
|
// // let port = info.get_port();
|
||||||
|
// // let motherboard_id = info.get_property("id").map_or("N/A", |v| v.val_str());
|
||||||
|
// //
|
||||||
|
// // println!(" IP: {}:{}", ip, port);
|
||||||
|
// // println!(" Motherboard ID: {}", motherboard_id);
|
||||||
|
// // println!("------------------------");
|
||||||
|
// //
|
||||||
|
// // // TODO: Add this agent to your central list of discovered hosts.
|
||||||
|
// // }
|
||||||
|
// // } else {
|
||||||
|
// // println!("Could not resolve service '{}' in time.", info.get_fullname());
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// // ServiceEvent::ServiceRemoved(info) => {
|
||||||
|
// // println!("\n--- Agent Removed ---");
|
||||||
|
// // println!(" Service Name: {}", info.get_fullname());
|
||||||
|
// // println!("---------------------");
|
||||||
|
// // // TODO: Remove this agent from your list.
|
||||||
|
// // }
|
||||||
|
// // _ => {
|
||||||
|
// // // We don't care about other event types for this example
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn discover_example() {
|
||||||
|
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||||
|
|
||||||
|
// Create a daemon
|
||||||
|
let mdns = ServiceDaemon::new().expect("Failed to create daemon");
|
||||||
|
|
||||||
|
// Use recently added `ServiceEvent::ServiceData`.
|
||||||
|
mdns.use_service_data(true)
|
||||||
|
.expect("Failed to use ServiceData");
|
||||||
|
|
||||||
|
// Browse for a service type.
|
||||||
|
let service_type = "_mdns-sd-my-test._udp.local.";
|
||||||
|
let receiver = mdns.browse(service_type).expect("Failed to browse");
|
||||||
|
|
||||||
|
// Receive the browse events in sync or async. Here is
|
||||||
|
// an example of using a thread. Users can call `receiver.recv_async().await`
|
||||||
|
// if running in async environment.
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
while let Ok(event) = receiver.recv() {
|
||||||
|
match event {
|
||||||
|
ServiceEvent::ServiceData(resolved) => {
|
||||||
|
println!("Resolved a new service: {}", resolved.fullname);
|
||||||
|
}
|
||||||
|
other_event => {
|
||||||
|
println!("Received other event: {:?}", &other_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gracefully shutdown the daemon.
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
mdns.shutdown().unwrap();
|
||||||
|
}
|
||||||
31
adr/agent_discovery/mdns/src/main.rs
Normal file
31
adr/agent_discovery/mdns/src/main.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use clap::{Parser, ValueEnum};
|
||||||
|
|
||||||
|
mod advertise;
|
||||||
|
mod discover;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
#[arg(value_enum)]
|
||||||
|
profile: Profiles,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum Profiles {
|
||||||
|
Advertise,
|
||||||
|
Discover,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The service type we are looking for.
|
||||||
|
const SERVICE_TYPE: &str = "_harmony._tcp.local.";
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
match args.profile {
|
||||||
|
Profiles::Advertise => advertise::advertise().await,
|
||||||
|
Profiles::Discover => discover::discover().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
3
check.sh
3
check.sh
@@ -1,5 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
rustc --version
|
||||||
cargo check --all-targets --all-features --keep-going
|
cargo check --all-targets --all-features --keep-going
|
||||||
cargo fmt --check
|
cargo fmt --check
|
||||||
|
cargo clippy
|
||||||
cargo test
|
cargo test
|
||||||
|
|||||||
8
data/pxe/okd/README.md
Normal file
8
data/pxe/okd/README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Here lies all the data files required for an OKD cluster PXE boot setup.
|
||||||
|
|
||||||
|
This inclues ISO files, binary boot files, ipxe, etc.
|
||||||
|
|
||||||
|
TODO as of august 2025 :
|
||||||
|
|
||||||
|
- `harmony_inventory_agent` should be downloaded from official releases, this embedded version is practical for now though
|
||||||
|
- The cluster ssh key should be generated and handled by harmony with the private key saved in a secret store
|
||||||
9
data/pxe/okd/http_files/.gitattributes
vendored
Normal file
9
data/pxe/okd/http_files/.gitattributes
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
harmony_inventory_agent filter=lfs diff=lfs merge=lfs -text
|
||||||
|
os filter=lfs diff=lfs merge=lfs -text
|
||||||
|
os/centos-stream-9 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
os/centos-stream-9/images filter=lfs diff=lfs merge=lfs -text
|
||||||
|
os/centos-stream-9/initrd.img filter=lfs diff=lfs merge=lfs -text
|
||||||
|
os/centos-stream-9/vmlinuz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
os/centos-stream-9/images/efiboot.img filter=lfs diff=lfs merge=lfs -text
|
||||||
|
os/centos-stream-9/images/install.img filter=lfs diff=lfs merge=lfs -text
|
||||||
|
os/centos-stream-9/images/pxeboot filter=lfs diff=lfs merge=lfs -text
|
||||||
1
data/pxe/okd/http_files/cluster_ssh_key.pub
Normal file
1
data/pxe/okd/http_files/cluster_ssh_key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBx6bDylvC68cVpjKfEFtLQJ/dOFi6PVS2vsIOqPDJIc jeangab@liliane2
|
||||||
BIN
data/pxe/okd/http_files/harmony_inventory_agent
(Stored with Git LFS)
Executable file
BIN
data/pxe/okd/http_files/harmony_inventory_agent
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
data/pxe/okd/http_files/os/centos-stream-9/images/efiboot.img
(Stored with Git LFS)
Normal file
BIN
data/pxe/okd/http_files/os/centos-stream-9/images/efiboot.img
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
data/pxe/okd/http_files/os/centos-stream-9/images/install.img
(Stored with Git LFS)
Normal file
BIN
data/pxe/okd/http_files/os/centos-stream-9/images/install.img
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
BIN
data/pxe/okd/http_files/os/centos-stream-9/images/pxeboot/vmlinuz
Executable file
BIN
data/pxe/okd/http_files/os/centos-stream-9/images/pxeboot/vmlinuz
Executable file
Binary file not shown.
BIN
data/pxe/okd/http_files/os/centos-stream-9/initrd.img
(Stored with Git LFS)
Normal file
BIN
data/pxe/okd/http_files/os/centos-stream-9/initrd.img
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
data/pxe/okd/http_files/os/centos-stream-9/vmlinuz
(Stored with Git LFS)
Executable file
BIN
data/pxe/okd/http_files/os/centos-stream-9/vmlinuz
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
data/pxe/okd/tftpboot/ipxe.efi
Normal file
BIN
data/pxe/okd/tftpboot/ipxe.efi
Normal file
Binary file not shown.
BIN
data/pxe/okd/tftpboot/undionly.kpxe
Normal file
BIN
data/pxe/okd/tftpboot/undionly.kpxe
Normal file
Binary file not shown.
108
docs/pxe_test/README.md
Normal file
108
docs/pxe_test/README.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# OPNsense PXE Lab Environment
|
||||||
|
|
||||||
|
This project contains a script to automatically set up a virtual lab environment for testing PXE boot services managed by an OPNsense firewall.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `pxe_vm_lab_setup.sh` script will create the following resources using libvirt/KVM:
|
||||||
|
|
||||||
|
1. **A Virtual Network**: An isolated network named `harmonylan` (`virbr1`) for the lab.
|
||||||
|
2. **Two Virtual Machines**:
|
||||||
|
* `opnsense-pxe`: A firewall VM that will act as the gateway and PXE server.
|
||||||
|
* `pxe-node-1`: A client VM configured to boot from the network.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Ensure you have the following software installed on your Arch Linux host:
|
||||||
|
|
||||||
|
* `libvirt`
|
||||||
|
* `qemu`
|
||||||
|
* `virt-install` (from the `virt-install` package)
|
||||||
|
* `curl`
|
||||||
|
* `bzip2`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### 1. Create the Environment
|
||||||
|
|
||||||
|
Run the `up` command to download the necessary images and create the network and VMs.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./pxe_vm_lab_setup.sh up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install and Configure OPNsense
|
||||||
|
|
||||||
|
The OPNsense VM is created but the OS needs to be installed manually via the console.
|
||||||
|
|
||||||
|
1. **Connect to the VM console**:
|
||||||
|
```bash
|
||||||
|
sudo virsh console opnsense-pxe
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Log in as the installer**:
|
||||||
|
* Username: `installer`
|
||||||
|
* Password: `opnsense`
|
||||||
|
|
||||||
|
3. **Follow the on-screen installation wizard**. When prompted to assign network interfaces (`WAN` and `LAN`):
|
||||||
|
* Find the MAC address for the `harmonylan` interface by running this command in another terminal:
|
||||||
|
```bash
|
||||||
|
virsh domiflist opnsense-pxe
|
||||||
|
# Example output:
|
||||||
|
# Interface Type Source Model MAC
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# vnet18 network default virtio 52:54:00:b5:c4:6d
|
||||||
|
# vnet19 network harmonylan virtio 52:54:00:21:f9:ba
|
||||||
|
```
|
||||||
|
* Assign the interface connected to `harmonylan` (e.g., `vtnet1` with MAC `52:54:00:21:f9:ba`) as your **LAN**.
|
||||||
|
* Assign the other interface as your **WAN**.
|
||||||
|
|
||||||
|
4. After the installation is complete, **shut down** the VM from the console menu.
|
||||||
|
|
||||||
|
5. **Detach the installation media** by editing the VM's configuration:
|
||||||
|
```bash
|
||||||
|
sudo virsh edit opnsense-pxe
|
||||||
|
```
|
||||||
|
Find and **delete** the entire `<disk>` block corresponding to the `.img` file (the one with `<target ... bus='usb'/>`).
|
||||||
|
|
||||||
|
6. **Start the VM** to boot into the newly installed system:
|
||||||
|
```bash
|
||||||
|
sudo virsh start opnsense-pxe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Connect to OPNsense from Your Host
|
||||||
|
|
||||||
|
To configure OPNsense, you need to connect your host to the `harmonylan` network.
|
||||||
|
|
||||||
|
1. By default, OPNsense configures its LAN interface with the IP `192.168.1.1`.
|
||||||
|
2. Assign a compatible IP address to your host's `virbr1` bridge interface:
|
||||||
|
```bash
|
||||||
|
sudo ip addr add 192.168.1.5/24 dev virbr1
|
||||||
|
```
|
||||||
|
3. You can now access the OPNsense VM from your host:
|
||||||
|
* **SSH**: `ssh root@192.168.1.1` (password: `opnsense`)
|
||||||
|
* **Web UI**: `https://192.168.1.1`
|
||||||
|
|
||||||
|
### 4. Configure PXE Services with Harmony
|
||||||
|
|
||||||
|
With connectivity established, you can now use Harmony to configure the OPNsense firewall for PXE booting. Point your Harmony OPNsense scores to the firewall using these details:
|
||||||
|
|
||||||
|
* **Hostname/IP**: `192.168.1.1`
|
||||||
|
* **Credentials**: `root` / `opnsense`
|
||||||
|
|
||||||
|
### 5. Boot the PXE Client
|
||||||
|
|
||||||
|
Once your Harmony configuration has been applied and OPNsense is serving DHCP/TFTP, start the client VM. It will automatically attempt to boot from the network.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo virsh start pxe-node-1
|
||||||
|
sudo virsh console pxe-node-1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
To destroy all VMs and networks created by the script, run the `clean` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./pxe_vm_lab_setup.sh clean
|
||||||
|
```
|
||||||
191
docs/pxe_test/pxe_vm_lab_setup.sh
Executable file
191
docs/pxe_test/pxe_vm_lab_setup.sh
Executable file
@@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
LAB_DIR="/var/lib/harmony_pxe_test"
|
||||||
|
IMG_DIR="${LAB_DIR}/images"
|
||||||
|
STATE_DIR="${LAB_DIR}/state"
|
||||||
|
VM_OPN="opnsense-pxe"
|
||||||
|
VM_PXE="pxe-node-1"
|
||||||
|
NET_HARMONYLAN="harmonylan"
|
||||||
|
|
||||||
|
# Network settings for the isolated LAN
|
||||||
|
VLAN_CIDR="192.168.150.0/24"
|
||||||
|
VLAN_GW="192.168.150.1"
|
||||||
|
VLAN_MASK="255.255.255.0"
|
||||||
|
|
||||||
|
# VM Specifications
|
||||||
|
RAM_OPN="2048"
|
||||||
|
VCPUS_OPN="2"
|
||||||
|
DISK_OPN_GB="10"
|
||||||
|
OS_VARIANT_OPN="freebsd14.0" # Updated to a more recent FreeBSD variant
|
||||||
|
|
||||||
|
RAM_PXE="4096"
|
||||||
|
VCPUS_PXE="2"
|
||||||
|
DISK_PXE_GB="40"
|
||||||
|
OS_VARIANT_LINUX="centos-stream9"
|
||||||
|
|
||||||
|
OPN_IMG_URL="https://mirror.ams1.nl.leaseweb.net/opnsense/releases/25.7/OPNsense-25.7-serial-amd64.img.bz2"
|
||||||
|
OPN_IMG_PATH="${IMG_DIR}/OPNsense-25.7-serial-amd64.img"
|
||||||
|
CENTOS_ISO_URL="https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/images/boot.iso"
|
||||||
|
CENTOS_ISO_PATH="${IMG_DIR}/CentOS-Stream-9-latest-boot.iso"
|
||||||
|
|
||||||
|
CONNECT_URI="qemu:///system"
|
||||||
|
|
||||||
|
download_if_missing() {
|
||||||
|
local url="$1"
|
||||||
|
local dest="$2"
|
||||||
|
if [[ ! -f "$dest" ]]; then
|
||||||
|
echo "Downloading $url to $dest"
|
||||||
|
mkdir -p "$(dirname "$dest")"
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
curl -L --progress-bar "$url" -o "$tmp"
|
||||||
|
case "$url" in
|
||||||
|
*.bz2) bunzip2 -c "$tmp" > "$dest" && rm -f "$tmp" ;;
|
||||||
|
*) mv "$tmp" "$dest" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo "Already present: $dest"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensures a libvirt network is defined and active
|
||||||
|
ensure_network() {
|
||||||
|
local net_name="$1"
|
||||||
|
local net_xml_path="$2"
|
||||||
|
if virsh --connect "${CONNECT_URI}" net-info "${net_name}" >/dev/null 2>&1; then
|
||||||
|
echo "Network ${net_name} already exists."
|
||||||
|
else
|
||||||
|
echo "Defining network ${net_name} from ${net_xml_path}"
|
||||||
|
virsh --connect "${CONNECT_URI}" net-define "${net_xml_path}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! virsh --connect "${CONNECT_URI}" net-info "${net_name}" | grep "Active: *yes"; then
|
||||||
|
echo "Starting network ${net_name}..."
|
||||||
|
virsh --connect "${CONNECT_URI}" net-start "${net_name}"
|
||||||
|
virsh --connect "${CONNECT_URI}" net-autostart "${net_name}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Destroys a VM completely
|
||||||
|
destroy_vm() {
|
||||||
|
local vm_name="$1"
|
||||||
|
if virsh --connect "${CONNECT_URI}" dominfo "$vm_name" >/dev/null 2>&1; then
|
||||||
|
echo "Destroying and undefining VM: ${vm_name}"
|
||||||
|
virsh --connect "${CONNECT_URI}" destroy "$vm_name" || true
|
||||||
|
virsh --connect "${CONNECT_URI}" undefine "$vm_name" --nvram
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Destroys a libvirt network
|
||||||
|
destroy_network() {
|
||||||
|
local net_name="$1"
|
||||||
|
if virsh --connect "${CONNECT_URI}" net-info "$net_name" >/dev/null 2>&1; then
|
||||||
|
echo "Destroying and undefining network: ${net_name}"
|
||||||
|
virsh --connect "${CONNECT_URI}" net-destroy "$net_name" || true
|
||||||
|
virsh --connect "${CONNECT_URI}" net-undefine "$net_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Main Logic ---
|
||||||
|
create_lab_environment() {
|
||||||
|
# Create network definition files
|
||||||
|
cat > "${STATE_DIR}/default.xml" <<EOF
|
||||||
|
<network>
|
||||||
|
<name>default</name>
|
||||||
|
<forward mode='nat'/>
|
||||||
|
<bridge name='virbr0' stp='on' delay='0'/>
|
||||||
|
<ip address='192.168.122.1' netmask='255.255.255.0'>
|
||||||
|
<dhcp>
|
||||||
|
<range start='192.168.122.100' end='192.168.122.200'/>
|
||||||
|
</dhcp>
|
||||||
|
</ip>
|
||||||
|
</network>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "${STATE_DIR}/${NET_HARMONYLAN}.xml" <<EOF
|
||||||
|
<network>
|
||||||
|
<name>${NET_HARMONYLAN}</name>
|
||||||
|
<bridge name='virbr1' stp='on' delay='0'/>
|
||||||
|
</network>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Ensure both networks exist and are active
|
||||||
|
ensure_network "default" "${STATE_DIR}/default.xml"
|
||||||
|
ensure_network "${NET_HARMONYLAN}" "${STATE_DIR}/${NET_HARMONYLAN}.xml"
|
||||||
|
|
||||||
|
# --- Create OPNsense VM (MODIFIED SECTION) ---
|
||||||
|
local disk_opn="${IMG_DIR}/${VM_OPN}.qcow2"
|
||||||
|
if [[ ! -f "$disk_opn" ]]; then
|
||||||
|
qemu-img create -f qcow2 "$disk_opn" "${DISK_OPN_GB}G"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating OPNsense VM using serial image..."
|
||||||
|
virt-install \
|
||||||
|
--connect "${CONNECT_URI}" \
|
||||||
|
--name "${VM_OPN}" \
|
||||||
|
--ram "${RAM_OPN}" \
|
||||||
|
--vcpus "${VCPUS_OPN}" \
|
||||||
|
--cpu host-passthrough \
|
||||||
|
--os-variant "${OS_VARIANT_OPN}" \
|
||||||
|
--graphics none \
|
||||||
|
--noautoconsole \
|
||||||
|
--disk path="${disk_opn}",device=disk,bus=virtio,boot.order=1 \
|
||||||
|
--disk path="${OPN_IMG_PATH}",device=disk,bus=usb,readonly=on,boot.order=2 \
|
||||||
|
--network network=default,model=virtio \
|
||||||
|
--network network="${NET_HARMONYLAN}",model=virtio \
|
||||||
|
--boot uefi,menu=on
|
||||||
|
|
||||||
|
echo "OPNsense VM created. Connect with: sudo virsh console ${VM_OPN}"
|
||||||
|
echo "The VM will boot from the serial installation image."
|
||||||
|
echo "Login with user 'installer' and password 'opnsense' to start the installation."
|
||||||
|
echo "Install onto the VirtIO disk (vtbd0)."
|
||||||
|
echo "After installation, shutdown the VM, then run 'sudo virsh edit ${VM_OPN}' and remove the USB disk block to boot from the installed system."
|
||||||
|
|
||||||
|
# --- Create PXE Client VM ---
|
||||||
|
local disk_pxe="${IMG_DIR}/${VM_PXE}.qcow2"
|
||||||
|
if [[ ! -f "$disk_pxe" ]]; then
|
||||||
|
qemu-img create -f qcow2 "$disk_pxe" "${DISK_PXE_GB}G"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating PXE client VM..."
|
||||||
|
virt-install \
|
||||||
|
--connect "${CONNECT_URI}" \
|
||||||
|
--name "${VM_PXE}" \
|
||||||
|
--ram "${RAM_PXE}" \
|
||||||
|
--vcpus "${VCPUS_PXE}" \
|
||||||
|
--cpu host-passthrough \
|
||||||
|
--os-variant "${OS_VARIANT_LINUX}" \
|
||||||
|
--graphics none \
|
||||||
|
--noautoconsole \
|
||||||
|
--disk path="${disk_pxe}",format=qcow2,bus=virtio \
|
||||||
|
--network network="${NET_HARMONYLAN}",model=virtio \
|
||||||
|
--pxe \
|
||||||
|
--boot uefi,menu=on
|
||||||
|
|
||||||
|
echo "PXE VM created. It will attempt to netboot on ${NET_HARMONYLAN}."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Script Entrypoint ---
|
||||||
|
case "${1:-}" in
|
||||||
|
up)
|
||||||
|
mkdir -p "${IMG_DIR}" "${STATE_DIR}"
|
||||||
|
download_if_missing "$OPN_IMG_URL" "$OPN_IMG_PATH"
|
||||||
|
download_if_missing "$CENTOS_ISO_URL" "$CENTOS_ISO_PATH"
|
||||||
|
create_lab_environment
|
||||||
|
echo "Lab setup complete. Use 'sudo virsh list --all' to see VMs."
|
||||||
|
;;
|
||||||
|
clean)
|
||||||
|
destroy_vm "${VM_PXE}"
|
||||||
|
destroy_vm "${VM_OPN}"
|
||||||
|
destroy_network "${NET_HARMONYLAN}"
|
||||||
|
# Optionally destroy the default network if you want a full reset
|
||||||
|
# destroy_network "default"
|
||||||
|
echo "Cleanup complete."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: sudo $0 {up|clean}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
15
examples/application_monitoring_with_tenant/Cargo.toml
Normal file
15
examples/application_monitoring_with_tenant/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "example-application-monitoring-with-tenant"
|
||||||
|
edition = "2024"
|
||||||
|
version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
env_logger.workspace = true
|
||||||
|
harmony = { path = "../../harmony" }
|
||||||
|
harmony_cli = { path = "../../harmony_cli" }
|
||||||
|
harmony_types = { path = "../../harmony_types" }
|
||||||
|
logging = "0.1.0"
|
||||||
|
tokio.workspace = true
|
||||||
|
url.workspace = true
|
||||||
BIN
examples/application_monitoring_with_tenant/harmony
Executable file
BIN
examples/application_monitoring_with_tenant/harmony
Executable file
Binary file not shown.
56
examples/application_monitoring_with_tenant/src/main.rs
Normal file
56
examples/application_monitoring_with_tenant/src/main.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use std::{path::PathBuf, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
use harmony::{
|
||||||
|
inventory::Inventory,
|
||||||
|
modules::{
|
||||||
|
application::{ApplicationScore, RustWebFramework, RustWebapp, features::Monitoring},
|
||||||
|
monitoring::alert_channel::webhook_receiver::WebhookReceiver,
|
||||||
|
tenant::TenantScore,
|
||||||
|
},
|
||||||
|
topology::{K8sAnywhereTopology, tenant::TenantConfig},
|
||||||
|
};
|
||||||
|
use harmony_types::id::Id;
|
||||||
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
//TODO there is a bug where the application is deployed into the namespace matching the
|
||||||
|
//application name and the tenant is created in the namesapce matching the tenant name
|
||||||
|
//in order for the application to be deployed in the tenant namespace the application.name and
|
||||||
|
//the TenantConfig.name must match
|
||||||
|
let tenant = TenantScore {
|
||||||
|
config: TenantConfig {
|
||||||
|
id: Id::from_str("test-tenant-id").unwrap(),
|
||||||
|
name: "example-monitoring".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let application = Arc::new(RustWebapp {
|
||||||
|
name: "example-monitoring".to_string(),
|
||||||
|
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
|
||||||
|
project_root: PathBuf::from("./examples/rust/webapp"),
|
||||||
|
framework: Some(RustWebFramework::Leptos),
|
||||||
|
});
|
||||||
|
|
||||||
|
let webhook_receiver = WebhookReceiver {
|
||||||
|
name: "sample-webhook-receiver".to_string(),
|
||||||
|
url: Url::Url(url::Url::parse("https://webhook-doesnt-exist.com").unwrap()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let app = ApplicationScore {
|
||||||
|
features: vec![Box::new(Monitoring {
|
||||||
|
alert_receiver: vec![Box::new(webhook_receiver)],
|
||||||
|
application: application.clone(),
|
||||||
|
})],
|
||||||
|
application,
|
||||||
|
};
|
||||||
|
|
||||||
|
harmony_cli::run(
|
||||||
|
Inventory::autoload(),
|
||||||
|
K8sAnywhereTopology::from_env(),
|
||||||
|
vec![Box::new(tenant), Box::new(app)],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
@@ -1,20 +1,27 @@
|
|||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
modules::{
|
||||||
modules::dummy::{ErrorScore, PanicScore, SuccessScore},
|
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
|
inventory::LaunchDiscoverInventoryAgentScore,
|
||||||
|
},
|
||||||
topology::LocalhostTopology,
|
topology::LocalhostTopology,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let inventory = Inventory::autoload();
|
harmony_cli::run(
|
||||||
let topology = LocalhostTopology::new();
|
Inventory::autoload(),
|
||||||
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
|
LocalhostTopology::new(),
|
||||||
|
vec![
|
||||||
maestro.register_all(vec![
|
Box::new(SuccessScore {}),
|
||||||
Box::new(SuccessScore {}),
|
Box::new(ErrorScore {}),
|
||||||
Box::new(ErrorScore {}),
|
Box::new(PanicScore {}),
|
||||||
Box::new(PanicScore {}),
|
Box::new(LaunchDiscoverInventoryAgentScore {
|
||||||
]);
|
discovery_timeout: Some(10),
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
}),
|
||||||
|
],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,40 +125,47 @@ spec:
|
|||||||
name: nginx"#,
|
name: nginx"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
return deployment;
|
deployment
|
||||||
}
|
}
|
||||||
fn nginx_deployment_2() -> Deployment {
|
fn nginx_deployment_2() -> Deployment {
|
||||||
let mut pod_template = PodTemplateSpec::default();
|
let pod_template = PodTemplateSpec {
|
||||||
pod_template.metadata = Some(ObjectMeta {
|
metadata: Some(ObjectMeta {
|
||||||
labels: Some(BTreeMap::from([(
|
labels: Some(BTreeMap::from([(
|
||||||
"app".to_string(),
|
"app".to_string(),
|
||||||
"nginx-test".to_string(),
|
"nginx-test".to_string(),
|
||||||
)])),
|
)])),
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
pod_template.spec = Some(PodSpec {
|
|
||||||
containers: vec![Container {
|
|
||||||
name: "nginx".to_string(),
|
|
||||||
image: Some("nginx".to_string()),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}),
|
||||||
..Default::default()
|
spec: Some(PodSpec {
|
||||||
});
|
containers: vec![Container {
|
||||||
let mut spec = DeploymentSpec::default();
|
name: "nginx".to_string(),
|
||||||
spec.template = pod_template;
|
image: Some("nginx".to_string()),
|
||||||
spec.selector = LabelSelector {
|
..Default::default()
|
||||||
match_expressions: None,
|
}],
|
||||||
match_labels: Some(BTreeMap::from([(
|
..Default::default()
|
||||||
"app".to_string(),
|
}),
|
||||||
"nginx-test".to_string(),
|
|
||||||
)])),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut deployment = Deployment::default();
|
let spec = DeploymentSpec {
|
||||||
deployment.spec = Some(spec);
|
template: pod_template,
|
||||||
deployment.metadata.name = Some("nginx-test".to_string());
|
selector: LabelSelector {
|
||||||
|
match_expressions: None,
|
||||||
|
match_labels: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"nginx-test".to_string(),
|
||||||
|
)])),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
deployment
|
Deployment {
|
||||||
|
spec: Some(spec),
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("nginx-test".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nginx_deployment() -> Deployment {
|
fn nginx_deployment() -> Deployment {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use harmony::{
|
use harmony::{
|
||||||
data::Version,
|
data::Version,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
|
||||||
modules::lamp::{LAMPConfig, LAMPScore},
|
modules::lamp::{LAMPConfig, LAMPScore},
|
||||||
topology::{K8sAnywhereTopology, Url},
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -24,7 +24,7 @@ async fn main() {
|
|||||||
// This config can be extended as needed for more complicated configurations
|
// This config can be extended as needed for more complicated configurations
|
||||||
config: LAMPConfig {
|
config: LAMPConfig {
|
||||||
project_root: "./php".into(),
|
project_root: "./php".into(),
|
||||||
database_size: format!("4Gi").into(),
|
database_size: "4Gi".to_string().into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -43,15 +43,13 @@ async fn main() {
|
|||||||
// K8sAnywhereTopology as it is the most automatic one that enables you to easily deploy
|
// K8sAnywhereTopology as it is the most automatic one that enables you to easily deploy
|
||||||
// locally, to development environment from a CI, to staging, and to production with settings
|
// locally, to development environment from a CI, to staging, and to production with settings
|
||||||
// that automatically adapt to each environment grade.
|
// that automatically adapt to each environment grade.
|
||||||
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
harmony_cli::run(
|
||||||
Inventory::autoload(),
|
Inventory::autoload(),
|
||||||
K8sAnywhereTopology::from_env(),
|
K8sAnywhereTopology::from_env(),
|
||||||
|
vec![Box::new(lamp_stack)],
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
maestro.register_all(vec![Box::new(lamp_stack)]);
|
|
||||||
// Here we bootstrap the CLI, this gives some nice features if you need them
|
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
|
||||||
}
|
}
|
||||||
// That's it, end of the infra as code.
|
// That's it, end of the infra as code.
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ readme.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
harmony = { version = "0.1.0", path = "../../harmony" }
|
harmony = { path = "../../harmony" }
|
||||||
harmony_cli = { version = "0.1.0", path = "../../harmony_cli" }
|
harmony_cli = { path = "../../harmony_cli" }
|
||||||
harmony_macros = { version = "0.1.0", path = "../../harmony_macros" }
|
harmony_macros = { path = "../../harmony_macros" }
|
||||||
|
harmony_types = { path = "../../harmony_types" }
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
|
||||||
modules::{
|
modules::{
|
||||||
monitoring::{
|
monitoring::{
|
||||||
alert_channel::discord_alert_channel::DiscordWebhook,
|
alert_channel::discord_alert_channel::DiscordWebhook,
|
||||||
@@ -23,8 +22,9 @@ use harmony::{
|
|||||||
k8s::pvc::high_pvc_fill_rate_over_two_days,
|
k8s::pvc::high_pvc_fill_rate_over_two_days,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
topology::{K8sAnywhereTopology, Url},
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -51,8 +51,8 @@ async fn main() {
|
|||||||
|
|
||||||
let service_monitor_endpoint = ServiceMonitorEndpoint {
|
let service_monitor_endpoint = ServiceMonitorEndpoint {
|
||||||
port: Some("80".to_string()),
|
port: Some("80".to_string()),
|
||||||
path: "/metrics".to_string(),
|
path: Some("/metrics".to_string()),
|
||||||
scheme: HTTPScheme::HTTP,
|
scheme: Some(HTTPScheme::HTTP),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,13 +74,13 @@ async fn main() {
|
|||||||
rules: vec![Box::new(additional_rules), Box::new(additional_rules2)],
|
rules: vec![Box::new(additional_rules), Box::new(additional_rules2)],
|
||||||
service_monitors: vec![service_monitor],
|
service_monitors: vec![service_monitor],
|
||||||
};
|
};
|
||||||
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
|
||||||
|
harmony_cli::run(
|
||||||
Inventory::autoload(),
|
Inventory::autoload(),
|
||||||
K8sAnywhereTopology::from_env(),
|
K8sAnywhereTopology::from_env(),
|
||||||
|
vec![Box::new(alerting_score)],
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
maestro.register_all(vec![Box::new(alerting_score)]);
|
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ license.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cidr.workspace = true
|
cidr.workspace = true
|
||||||
harmony = { version = "0.1.0", path = "../../harmony" }
|
harmony = { path = "../../harmony" }
|
||||||
harmony_cli = { version = "0.1.0", path = "../../harmony_cli" }
|
harmony_cli = { path = "../../harmony_cli" }
|
||||||
|
harmony_types = { path = "../../harmony_types" }
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use harmony::{
|
use harmony::{
|
||||||
data::Id,
|
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
|
||||||
modules::{
|
modules::{
|
||||||
monitoring::{
|
monitoring::{
|
||||||
alert_channel::discord_alert_channel::DiscordWebhook,
|
alert_channel::discord_alert_channel::DiscordWebhook,
|
||||||
@@ -20,16 +18,18 @@ use harmony::{
|
|||||||
tenant::TenantScore,
|
tenant::TenantScore,
|
||||||
},
|
},
|
||||||
topology::{
|
topology::{
|
||||||
K8sAnywhereTopology, Url,
|
K8sAnywhereTopology,
|
||||||
tenant::{ResourceLimits, TenantConfig, TenantNetworkPolicy},
|
tenant::{ResourceLimits, TenantConfig, TenantNetworkPolicy},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use harmony_types::id::Id;
|
||||||
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let tenant = TenantScore {
|
let tenant = TenantScore {
|
||||||
config: TenantConfig {
|
config: TenantConfig {
|
||||||
id: Id::from_string("1234".to_string()),
|
id: Id::from_str("1234").unwrap(),
|
||||||
name: "test-tenant".to_string(),
|
name: "test-tenant".to_string(),
|
||||||
resource_limits: ResourceLimits {
|
resource_limits: ResourceLimits {
|
||||||
cpu_request_cores: 6.0,
|
cpu_request_cores: 6.0,
|
||||||
@@ -54,8 +54,8 @@ async fn main() {
|
|||||||
|
|
||||||
let service_monitor_endpoint = ServiceMonitorEndpoint {
|
let service_monitor_endpoint = ServiceMonitorEndpoint {
|
||||||
port: Some("80".to_string()),
|
port: Some("80".to_string()),
|
||||||
path: "/metrics".to_string(),
|
path: Some("/metrics".to_string()),
|
||||||
scheme: HTTPScheme::HTTP,
|
scheme: Some(HTTPScheme::HTTP),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,13 +78,13 @@ async fn main() {
|
|||||||
rules: vec![Box::new(additional_rules)],
|
rules: vec![Box::new(additional_rules)],
|
||||||
service_monitors: vec![service_monitor],
|
service_monitors: vec![service_monitor],
|
||||||
};
|
};
|
||||||
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
|
||||||
|
harmony_cli::run(
|
||||||
Inventory::autoload(),
|
Inventory::autoload(),
|
||||||
K8sAnywhereTopology::from_env(),
|
K8sAnywhereTopology::from_env(),
|
||||||
|
vec![Box::new(tenant), Box::new(alerting_score)],
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
maestro.register_all(vec![Box::new(tenant), Box::new(alerting_score)]);
|
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,23 +5,22 @@ use std::{
|
|||||||
|
|
||||||
use cidr::Ipv4Cidr;
|
use cidr::Ipv4Cidr;
|
||||||
use harmony::{
|
use harmony::{
|
||||||
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
hardware::{HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||||
infra::opnsense::OPNSenseManagementInterface,
|
infra::opnsense::OPNSenseManagementInterface,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
|
||||||
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,
|
dns::OKDDnsScore, ipxe::OkdIpxeScore,
|
||||||
},
|
},
|
||||||
tftp::TftpScore,
|
tftp::TftpScore,
|
||||||
},
|
},
|
||||||
topology::{LogicalHost, UnmanagedRouter, Url},
|
topology::{LogicalHost, UnmanagedRouter},
|
||||||
};
|
};
|
||||||
use harmony_macros::{ip, mac_address};
|
use harmony_macros::{ip, mac_address};
|
||||||
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -87,8 +86,7 @@ async fn main() {
|
|||||||
let inventory = Inventory {
|
let inventory = Inventory {
|
||||||
location: Location::new("I am mobile".to_string(), "earth".to_string()),
|
location: Location::new("I am mobile".to_string(), "earth".to_string()),
|
||||||
switch: SwitchGroup::from([]),
|
switch: SwitchGroup::from([]),
|
||||||
firewall: FirewallGroup::from([PhysicalHost::empty(HostCategory::Firewall)
|
firewall_mgmt: Box::new(OPNSenseManagementInterface::new()),
|
||||||
.management(Arc::new(OPNSenseManagementInterface::new()))]),
|
|
||||||
storage_host: vec![],
|
storage_host: vec![],
|
||||||
worker_host: vec![
|
worker_host: vec![
|
||||||
PhysicalHost::empty(HostCategory::Server)
|
PhysicalHost::empty(HostCategory::Server)
|
||||||
@@ -126,20 +124,37 @@ async fn main() {
|
|||||||
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
|
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
|
||||||
|
|
||||||
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::new(Url::LocalFolder(
|
let http_score = StaticFilesHttpScore {
|
||||||
"./data/watchguard/pxe-http-files".to_string(),
|
folder_to_serve: Some(Url::LocalFolder(
|
||||||
));
|
"./data/watchguard/pxe-http-files".to_string(),
|
||||||
let ipxe_score = IpxeScore::new();
|
)),
|
||||||
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
|
files: vec![],
|
||||||
maestro.register_all(vec![
|
};
|
||||||
Box::new(dns_score),
|
|
||||||
Box::new(bootstrap_dhcp_score),
|
let kickstart_filename = "inventory.kickstart".to_string();
|
||||||
Box::new(bootstrap_load_balancer_score),
|
let cluster_pubkey_filename = "cluster_ssh_key.pub".to_string();
|
||||||
Box::new(load_balancer_score),
|
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
||||||
Box::new(tftp_score),
|
|
||||||
Box::new(http_score),
|
let ipxe_score = OkdIpxeScore {
|
||||||
Box::new(ipxe_score),
|
kickstart_filename,
|
||||||
Box::new(dhcp_score),
|
harmony_inventory_agent,
|
||||||
]);
|
cluster_pubkey_filename,
|
||||||
harmony_tui::init(maestro).await.unwrap();
|
};
|
||||||
|
|
||||||
|
harmony_tui::run(
|
||||||
|
inventory,
|
||||||
|
topology,
|
||||||
|
vec![
|
||||||
|
Box::new(dns_score),
|
||||||
|
Box::new(bootstrap_dhcp_score),
|
||||||
|
Box::new(bootstrap_load_balancer_score),
|
||||||
|
Box::new(load_balancer_score),
|
||||||
|
Box::new(tftp_score),
|
||||||
|
Box::new(http_score),
|
||||||
|
Box::new(ipxe_score),
|
||||||
|
Box::new(dhcp_score),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory, maestro::Maestro, modules::monitoring::ntfy::ntfy::NtfyScore,
|
inventory::Inventory, modules::monitoring::ntfy::ntfy::NtfyScore, topology::K8sAnywhereTopology,
|
||||||
topology::K8sAnywhereTopology,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
harmony_cli::run(
|
||||||
Inventory::autoload(),
|
Inventory::autoload(),
|
||||||
K8sAnywhereTopology::from_env(),
|
K8sAnywhereTopology::from_env(),
|
||||||
|
vec![Box::new(NtfyScore {
|
||||||
|
namespace: "monitoring".to_string(),
|
||||||
|
host: "localhost".to_string(),
|
||||||
|
})],
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
maestro.register_all(vec![Box::new(NtfyScore {
|
|
||||||
namespace: "monitoring".to_string(),
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
})]);
|
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|||||||
21
examples/okd_installation/Cargo.toml
Normal file
21
examples/okd_installation/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[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
|
||||||
20
examples/okd_installation/src/main.rs
Normal file
20
examples/okd_installation/src/main.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
mod topology;
|
||||||
|
|
||||||
|
use crate::topology::{get_inventory, get_topology};
|
||||||
|
use harmony::modules::okd::{installation::OKDInstallationScore, ipxe::OkdIpxeScore};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let inventory = get_inventory();
|
||||||
|
let topology = get_topology().await;
|
||||||
|
|
||||||
|
let kickstart_filename = "inventory.kickstart".to_string();
|
||||||
|
let cluster_pubkey_filename = "cluster_ssh_key.pub".to_string();
|
||||||
|
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
||||||
|
|
||||||
|
let okd_install = Box::new(OKDInstallationScore {});
|
||||||
|
|
||||||
|
harmony_cli::run(inventory, topology, vec![okd_install], None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
77
examples/okd_installation/src/topology.rs
Normal file
77
examples/okd_installation/src/topology.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use cidr::Ipv4Cidr;
|
||||||
|
use harmony::{
|
||||||
|
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||||
|
infra::opnsense::OPNSenseManagementInterface,
|
||||||
|
inventory::Inventory,
|
||||||
|
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
|
||||||
|
};
|
||||||
|
use harmony_macros::{ip, ipv4};
|
||||||
|
use harmony_secret::{Secret, SecretManager};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{net::IpAddr, sync::Arc};
|
||||||
|
|
||||||
|
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
struct OPNSenseFirewallConfig {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_topology() -> HAClusterTopology {
|
||||||
|
let firewall = harmony::topology::LogicalHost {
|
||||||
|
ip: ip!("192.168.1.1"),
|
||||||
|
name: String::from("opnsense-1"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = SecretManager::get::<OPNSenseFirewallConfig>().await;
|
||||||
|
let config = config.unwrap();
|
||||||
|
|
||||||
|
let opnsense = Arc::new(
|
||||||
|
harmony::infra::opnsense::OPNSenseFirewall::new(
|
||||||
|
firewall,
|
||||||
|
None,
|
||||||
|
&config.username,
|
||||||
|
&config.password,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
let lan_subnet = ipv4!("192.168.1.0");
|
||||||
|
let gateway_ipv4 = ipv4!("192.168.1.1");
|
||||||
|
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
||||||
|
harmony::topology::HAClusterTopology {
|
||||||
|
domain_name: "demo.harmony.mcd".to_string(),
|
||||||
|
router: Arc::new(UnmanagedRouter::new(
|
||||||
|
gateway_ip,
|
||||||
|
Ipv4Cidr::new(lan_subnet, 24).unwrap(),
|
||||||
|
)),
|
||||||
|
load_balancer: opnsense.clone(),
|
||||||
|
firewall: opnsense.clone(),
|
||||||
|
tftp_server: opnsense.clone(),
|
||||||
|
http_server: opnsense.clone(),
|
||||||
|
dhcp_server: opnsense.clone(),
|
||||||
|
dns_server: opnsense.clone(),
|
||||||
|
control_plane: vec![LogicalHost {
|
||||||
|
ip: ip!("192.168.1.20"),
|
||||||
|
name: "cp0".to_string(),
|
||||||
|
}],
|
||||||
|
bootstrap_host: LogicalHost {
|
||||||
|
ip: ip!("192.168.1.20"),
|
||||||
|
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![],
|
||||||
|
}
|
||||||
|
}
|
||||||
7
examples/okd_installation/ssh_example_key
Normal file
7
examples/okd_installation/ssh_example_key
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACAcemw8pbwuvHFaYynxBbS0Cf3ThYuj1Utr7CDqjwySHAAAAJikacCNpGnA
|
||||||
|
jQAAAAtzc2gtZWQyNTUxOQAAACAcemw8pbwuvHFaYynxBbS0Cf3ThYuj1Utr7CDqjwySHA
|
||||||
|
AAAECiiKk4V6Q5cVs6axDM4sjAzZn/QCZLQekmYQXS9XbEYxx6bDylvC68cVpjKfEFtLQJ
|
||||||
|
/dOFi6PVS2vsIOqPDJIcAAAAEGplYW5nYWJAbGlsaWFuZTIBAgMEBQ==
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
1
examples/okd_installation/ssh_example_key.pub
Normal file
1
examples/okd_installation/ssh_example_key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBx6bDylvC68cVpjKfEFtLQJ/dOFi6PVS2vsIOqPDJIc jeangab@liliane2
|
||||||
21
examples/okd_pxe/Cargo.toml
Normal file
21
examples/okd_pxe/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "example-pxe"
|
||||||
|
edition = "2024"
|
||||||
|
version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
harmony = { path = "../../harmony" }
|
||||||
|
harmony_cli = { path = "../../harmony_cli" }
|
||||||
|
harmony_types = { path = "../../harmony_types" }
|
||||||
|
harmony_secret = { path = "../../harmony_secret" }
|
||||||
|
harmony_secret_derive = { path = "../../harmony_secret_derive" }
|
||||||
|
cidr = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
harmony_macros = { path = "../../harmony_macros" }
|
||||||
|
log = { workspace = true }
|
||||||
|
env_logger = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
serde.workspace = true
|
||||||
24
examples/okd_pxe/src/main.rs
Normal file
24
examples/okd_pxe/src/main.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
mod topology;
|
||||||
|
|
||||||
|
use crate::topology::{get_inventory, get_topology};
|
||||||
|
use harmony::modules::okd::ipxe::OkdIpxeScore;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let inventory = get_inventory();
|
||||||
|
let topology = get_topology().await;
|
||||||
|
|
||||||
|
let kickstart_filename = "inventory.kickstart".to_string();
|
||||||
|
let cluster_pubkey_filename = "cluster_ssh_key.pub".to_string();
|
||||||
|
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
||||||
|
|
||||||
|
let ipxe_score = OkdIpxeScore {
|
||||||
|
kickstart_filename,
|
||||||
|
harmony_inventory_agent,
|
||||||
|
cluster_pubkey_filename,
|
||||||
|
};
|
||||||
|
|
||||||
|
harmony_cli::run(inventory, topology, vec![Box::new(ipxe_score)], None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
77
examples/okd_pxe/src/topology.rs
Normal file
77
examples/okd_pxe/src/topology.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use cidr::Ipv4Cidr;
|
||||||
|
use harmony::{
|
||||||
|
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||||
|
infra::opnsense::OPNSenseManagementInterface,
|
||||||
|
inventory::Inventory,
|
||||||
|
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
|
||||||
|
};
|
||||||
|
use harmony_macros::{ip, ipv4};
|
||||||
|
use harmony_secret::{Secret, SecretManager};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{net::IpAddr, sync::Arc};
|
||||||
|
|
||||||
|
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
struct OPNSenseFirewallConfig {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_topology() -> HAClusterTopology {
|
||||||
|
let firewall = harmony::topology::LogicalHost {
|
||||||
|
ip: ip!("192.168.1.1"),
|
||||||
|
name: String::from("opnsense-1"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = SecretManager::get::<OPNSenseFirewallConfig>().await;
|
||||||
|
let config = config.unwrap();
|
||||||
|
|
||||||
|
let opnsense = Arc::new(
|
||||||
|
harmony::infra::opnsense::OPNSenseFirewall::new(
|
||||||
|
firewall,
|
||||||
|
None,
|
||||||
|
&config.username,
|
||||||
|
&config.password,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
let lan_subnet = ipv4!("192.168.1.0");
|
||||||
|
let gateway_ipv4 = ipv4!("192.168.1.1");
|
||||||
|
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
||||||
|
harmony::topology::HAClusterTopology {
|
||||||
|
domain_name: "demo.harmony.mcd".to_string(),
|
||||||
|
router: Arc::new(UnmanagedRouter::new(
|
||||||
|
gateway_ip,
|
||||||
|
Ipv4Cidr::new(lan_subnet, 24).unwrap(),
|
||||||
|
)),
|
||||||
|
load_balancer: opnsense.clone(),
|
||||||
|
firewall: opnsense.clone(),
|
||||||
|
tftp_server: opnsense.clone(),
|
||||||
|
http_server: opnsense.clone(),
|
||||||
|
dhcp_server: opnsense.clone(),
|
||||||
|
dns_server: opnsense.clone(),
|
||||||
|
control_plane: vec![LogicalHost {
|
||||||
|
ip: ip!("10.100.8.20"),
|
||||||
|
name: "cp0".to_string(),
|
||||||
|
}],
|
||||||
|
bootstrap_host: LogicalHost {
|
||||||
|
ip: ip!("10.100.8.20"),
|
||||||
|
name: "cp0".to_string(),
|
||||||
|
},
|
||||||
|
workers: vec![],
|
||||||
|
switch: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_inventory() -> Inventory {
|
||||||
|
Inventory {
|
||||||
|
location: Location::new(
|
||||||
|
"Some virtual machine or maybe a physical machine if you're cool".to_string(),
|
||||||
|
"testopnsense".to_string(),
|
||||||
|
),
|
||||||
|
switch: SwitchGroup::from([]),
|
||||||
|
firewall_mgmt: Box::new(OPNSenseManagementInterface::new()),
|
||||||
|
storage_host: vec![],
|
||||||
|
worker_host: vec![],
|
||||||
|
control_plane_host: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
7
examples/okd_pxe/ssh_example_key
Normal file
7
examples/okd_pxe/ssh_example_key
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACAcemw8pbwuvHFaYynxBbS0Cf3ThYuj1Utr7CDqjwySHAAAAJikacCNpGnA
|
||||||
|
jQAAAAtzc2gtZWQyNTUxOQAAACAcemw8pbwuvHFaYynxBbS0Cf3ThYuj1Utr7CDqjwySHA
|
||||||
|
AAAECiiKk4V6Q5cVs6axDM4sjAzZn/QCZLQekmYQXS9XbEYxx6bDylvC68cVpjKfEFtLQJ
|
||||||
|
/dOFi6PVS2vsIOqPDJIcAAAAEGplYW5nYWJAbGlsaWFuZTIBAgMEBQ==
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
1
examples/okd_pxe/ssh_example_key.pub
Normal file
1
examples/okd_pxe/ssh_example_key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBx6bDylvC68cVpjKfEFtLQJ/dOFi6PVS2vsIOqPDJIc jeangab@liliane2
|
||||||
@@ -8,7 +8,6 @@ use harmony::{
|
|||||||
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||||
infra::opnsense::OPNSenseManagementInterface,
|
infra::opnsense::OPNSenseManagementInterface,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
|
||||||
modules::{
|
modules::{
|
||||||
dummy::{ErrorScore, PanicScore, SuccessScore},
|
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
http::StaticFilesHttpScore,
|
http::StaticFilesHttpScore,
|
||||||
@@ -16,9 +15,10 @@ use harmony::{
|
|||||||
opnsense::OPNsenseShellCommandScore,
|
opnsense::OPNsenseShellCommandScore,
|
||||||
tftp::TftpScore,
|
tftp::TftpScore,
|
||||||
},
|
},
|
||||||
topology::{LogicalHost, UnmanagedRouter, Url},
|
topology::{LogicalHost, UnmanagedRouter},
|
||||||
};
|
};
|
||||||
use harmony_macros::{ip, mac_address};
|
use harmony_macros::{ip, mac_address};
|
||||||
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -63,8 +63,7 @@ async fn main() {
|
|||||||
"wk".to_string(),
|
"wk".to_string(),
|
||||||
),
|
),
|
||||||
switch: SwitchGroup::from([]),
|
switch: SwitchGroup::from([]),
|
||||||
firewall: FirewallGroup::from([PhysicalHost::empty(HostCategory::Firewall)
|
firewall_mgmt: Box::new(OPNSenseManagementInterface::new()),
|
||||||
.management(Arc::new(OPNSenseManagementInterface::new()))]),
|
|
||||||
storage_host: vec![],
|
storage_host: vec![],
|
||||||
worker_host: vec![],
|
worker_host: vec![],
|
||||||
control_plane_host: vec![
|
control_plane_host: vec![
|
||||||
@@ -81,23 +80,31 @@ async fn main() {
|
|||||||
let load_balancer_score = OKDLoadBalancerScore::new(&topology);
|
let load_balancer_score = OKDLoadBalancerScore::new(&topology);
|
||||||
|
|
||||||
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::new(Url::LocalFolder(
|
let http_score = StaticFilesHttpScore {
|
||||||
"./data/watchguard/pxe-http-files".to_string(),
|
folder_to_serve: Some(Url::LocalFolder(
|
||||||
));
|
"./data/watchguard/pxe-http-files".to_string(),
|
||||||
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
|
)),
|
||||||
maestro.register_all(vec![
|
files: vec![],
|
||||||
Box::new(dns_score),
|
};
|
||||||
Box::new(dhcp_score),
|
|
||||||
Box::new(load_balancer_score),
|
harmony_tui::run(
|
||||||
Box::new(tftp_score),
|
inventory,
|
||||||
Box::new(http_score),
|
topology,
|
||||||
Box::new(OPNsenseShellCommandScore {
|
vec![
|
||||||
opnsense: opnsense.get_opnsense_config(),
|
Box::new(dns_score),
|
||||||
command: "touch /tmp/helloharmonytouching".to_string(),
|
Box::new(dhcp_score),
|
||||||
}),
|
Box::new(load_balancer_score),
|
||||||
Box::new(SuccessScore {}),
|
Box::new(tftp_score),
|
||||||
Box::new(ErrorScore {}),
|
Box::new(http_score),
|
||||||
Box::new(PanicScore {}),
|
Box::new(OPNsenseShellCommandScore {
|
||||||
]);
|
opnsense: opnsense.get_opnsense_config(),
|
||||||
harmony_tui::init(maestro).await.unwrap();
|
command: "touch /tmp/helloharmonytouching".to_string(),
|
||||||
|
}),
|
||||||
|
Box::new(SuccessScore {}),
|
||||||
|
Box::new(ErrorScore {}),
|
||||||
|
Box::new(PanicScore {}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,21 @@ use std::{path::PathBuf, sync::Arc};
|
|||||||
|
|
||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
modules::{
|
||||||
modules::application::{
|
application::{
|
||||||
ApplicationScore, RustWebFramework, RustWebapp,
|
ApplicationScore, RustWebFramework, RustWebapp,
|
||||||
features::{ContinuousDelivery, Monitoring},
|
features::{ContinuousDelivery, Monitoring},
|
||||||
|
},
|
||||||
|
monitoring::alert_channel::{
|
||||||
|
discord_alert_channel::DiscordWebhook, webhook_receiver::WebhookReceiver,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
topology::{K8sAnywhereTopology, Url},
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
use harmony_cli::cli_logger;
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let cli_logger_handle = cli_logger::init();
|
|
||||||
|
|
||||||
let topology = K8sAnywhereTopology::from_env();
|
|
||||||
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let application = Arc::new(RustWebapp {
|
let application = Arc::new(RustWebapp {
|
||||||
name: "harmony-example-rust-webapp".to_string(),
|
name: "harmony-example-rust-webapp".to_string(),
|
||||||
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
|
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
|
||||||
@@ -27,6 +24,16 @@ async fn main() {
|
|||||||
framework: Some(RustWebFramework::Leptos),
|
framework: Some(RustWebFramework::Leptos),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let discord_receiver = DiscordWebhook {
|
||||||
|
name: "test-discord".to_string(),
|
||||||
|
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let webhook_receiver = WebhookReceiver {
|
||||||
|
name: "sample-webhook-receiver".to_string(),
|
||||||
|
url: Url::Url(url::Url::parse("https://webhook-doesnt-exist.com").unwrap()),
|
||||||
|
};
|
||||||
|
|
||||||
let app = ApplicationScore {
|
let app = ApplicationScore {
|
||||||
features: vec![
|
features: vec![
|
||||||
Box::new(ContinuousDelivery {
|
Box::new(ContinuousDelivery {
|
||||||
@@ -34,13 +41,19 @@ async fn main() {
|
|||||||
}),
|
}),
|
||||||
Box::new(Monitoring {
|
Box::new(Monitoring {
|
||||||
application: application.clone(),
|
application: application.clone(),
|
||||||
}), // TODO: add backups, multisite ha, etc.
|
alert_receiver: vec![Box::new(discord_receiver), Box::new(webhook_receiver)],
|
||||||
|
}),
|
||||||
|
// TODO add backups, multisite ha, etc
|
||||||
],
|
],
|
||||||
application,
|
application,
|
||||||
};
|
};
|
||||||
|
|
||||||
maestro.register_all(vec![Box::new(app)]);
|
harmony_cli::run(
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
Inventory::autoload(),
|
||||||
|
K8sAnywhereTopology::from_env(),
|
||||||
let _ = tokio::try_join!(cli_logger_handle);
|
vec![Box::new(app)],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use harmony::{
|
use harmony::{
|
||||||
data::Id,
|
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
|
||||||
modules::tenant::TenantScore,
|
modules::tenant::TenantScore,
|
||||||
topology::{K8sAnywhereTopology, tenant::TenantConfig},
|
topology::{K8sAnywhereTopology, tenant::TenantConfig},
|
||||||
};
|
};
|
||||||
|
use harmony_types::id::Id;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let tenant = TenantScore {
|
let tenant = TenantScore {
|
||||||
config: TenantConfig {
|
config: TenantConfig {
|
||||||
id: Id::from_str("test-tenant-id"),
|
id: Id::from_str("test-tenant-id").unwrap(),
|
||||||
name: "testtenant".to_string(),
|
name: "testtenant".to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
harmony_cli::run(
|
||||||
Inventory::autoload(),
|
Inventory::autoload(),
|
||||||
K8sAnywhereTopology::from_env(),
|
K8sAnywhereTopology::from_env(),
|
||||||
|
vec![Box::new(tenant)],
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
maestro.register_all(vec![Box::new(tenant)]);
|
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO write tests
|
// TODO write tests
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::net::{SocketAddr, SocketAddrV4};
|
|||||||
|
|
||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
|
||||||
modules::{
|
modules::{
|
||||||
dns::DnsScore,
|
dns::DnsScore,
|
||||||
dummy::{ErrorScore, PanicScore, SuccessScore},
|
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
@@ -16,18 +15,19 @@ use harmony_macros::ipv4;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let inventory = Inventory::autoload();
|
harmony_tui::run(
|
||||||
let topology = DummyInfra {};
|
Inventory::autoload(),
|
||||||
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
|
DummyInfra {},
|
||||||
|
vec![
|
||||||
maestro.register_all(vec![
|
Box::new(SuccessScore {}),
|
||||||
Box::new(SuccessScore {}),
|
Box::new(ErrorScore {}),
|
||||||
Box::new(ErrorScore {}),
|
Box::new(PanicScore {}),
|
||||||
Box::new(PanicScore {}),
|
Box::new(DnsScore::new(vec![], None)),
|
||||||
Box::new(DnsScore::new(vec![], None)),
|
Box::new(build_large_score()),
|
||||||
Box::new(build_large_score()),
|
],
|
||||||
]);
|
)
|
||||||
harmony_tui::init(maestro).await.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_large_score() -> LoadBalancerScore {
|
fn build_large_score() -> LoadBalancerScore {
|
||||||
|
|||||||
11
examples/validate_ceph_cluster_health/Cargo.toml
Normal file
11
examples/validate_ceph_cluster_health/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "example_validate_ceph_cluster_health"
|
||||||
|
edition = "2024"
|
||||||
|
version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
harmony = { version = "0.1.0", path = "../../harmony" }
|
||||||
|
harmony_cli = { version = "0.1.0", path = "../../harmony_cli" }
|
||||||
|
tokio.workspace = true
|
||||||
18
examples/validate_ceph_cluster_health/src/main.rs
Normal file
18
examples/validate_ceph_cluster_health/src/main.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use harmony::{
|
||||||
|
inventory::Inventory,
|
||||||
|
modules::storage::ceph::ceph_validate_health_score::CephVerifyClusterHealth,
|
||||||
|
topology::K8sAnywhereTopology,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let ceph_health_score = CephVerifyClusterHealth {
|
||||||
|
rook_ceph_namespace: "rook-ceph".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let topology = K8sAnywhereTopology::from_env();
|
||||||
|
let inventory = Inventory::autoload();
|
||||||
|
harmony_cli::run(inventory, topology, vec![Box::new(ceph_health_score)], None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
@@ -5,16 +5,17 @@ version.workspace = true
|
|||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
testing = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "0.9"
|
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
libredfish = "0.1.1"
|
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features = false }
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
|
||||||
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"
|
||||||
serde = { version = "1.0.209", features = ["derive", "rc"] }
|
serde.workspace = true
|
||||||
serde_json = "1.0.127"
|
serde_json.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
derive-new.workspace = true
|
derive-new.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
@@ -27,7 +28,7 @@ harmony_macros = { path = "../harmony_macros" }
|
|||||||
harmony_types = { path = "../harmony_types" }
|
harmony_types = { path = "../harmony_types" }
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
kube.workspace = true
|
kube = { workspace = true, features = ["derive"] }
|
||||||
k8s-openapi.workspace = true
|
k8s-openapi.workspace = true
|
||||||
serde_yaml.workspace = true
|
serde_yaml.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
@@ -35,8 +36,8 @@ serde-value.workspace = true
|
|||||||
helm-wrapper-rs = "0.4.0"
|
helm-wrapper-rs = "0.4.0"
|
||||||
non-blank-string-rs = "1.0.4"
|
non-blank-string-rs = "1.0.4"
|
||||||
k3d-rs = { path = "../k3d" }
|
k3d-rs = { path = "../k3d" }
|
||||||
directories = "6.0.0"
|
directories.workspace = true
|
||||||
lazy_static = "1.5.0"
|
lazy_static.workspace = true
|
||||||
dockerfile_builder = "0.1.5"
|
dockerfile_builder = "0.1.5"
|
||||||
temp-file = "0.1.9"
|
temp-file = "0.1.9"
|
||||||
convert_case.workspace = true
|
convert_case.workspace = true
|
||||||
@@ -56,12 +57,20 @@ similar.workspace = true
|
|||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
tokio-util = "0.7.15"
|
tokio-util = "0.7.15"
|
||||||
strum = { version = "0.27.1", features = ["derive"] }
|
strum = { version = "0.27.1", features = ["derive"] }
|
||||||
tempfile = "3.20.0"
|
tempfile.workspace = true
|
||||||
serde_with = "3.14.0"
|
serde_with = "3.14.0"
|
||||||
|
schemars = "0.8.22"
|
||||||
|
kube-derive = "1.1.0"
|
||||||
bollard.workspace = true
|
bollard.workspace = true
|
||||||
tar.workspace = true
|
tar.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
|
harmony_inventory_agent = { path = "../harmony_inventory_agent" }
|
||||||
|
harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" }
|
||||||
|
askama.workspace = true
|
||||||
|
sqlx.workspace = true
|
||||||
|
inquire.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
|
|||||||
BIN
harmony/harmony.rlib
Normal file
BIN
harmony/harmony.rlib
Normal file
Binary file not shown.
@@ -11,5 +11,13 @@ lazy_static! {
|
|||||||
pub static ref REGISTRY_PROJECT: String =
|
pub static ref REGISTRY_PROJECT: String =
|
||||||
std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string());
|
std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string());
|
||||||
pub static ref DRY_RUN: bool =
|
pub static ref DRY_RUN: bool =
|
||||||
std::env::var("HARMONY_DRY_RUN").map_or(true, |value| value.parse().unwrap_or(true));
|
std::env::var("HARMONY_DRY_RUN").is_ok_and(|value| value.parse().unwrap_or(false));
|
||||||
|
pub static ref DEFAULT_DATABASE_URL: String = "sqlite://harmony.sqlite".to_string();
|
||||||
|
pub static ref DATABASE_URL: String = std::env::var("HARMONY_DATABASE_URL")
|
||||||
|
.map(|value| if value.is_empty() {
|
||||||
|
(*DEFAULT_DATABASE_URL).clone()
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
})
|
||||||
|
.unwrap_or((*DEFAULT_DATABASE_URL).clone());
|
||||||
}
|
}
|
||||||
|
|||||||
22
harmony/src/domain/data/file.rs
Normal file
22
harmony/src/domain/data/file.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FileContent {
|
||||||
|
pub path: FilePath,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum FilePath {
|
||||||
|
Relative(String),
|
||||||
|
Absolute(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for FilePath {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
FilePath::Relative(path) => f.write_fmt(format_args!("./{path}")),
|
||||||
|
FilePath::Absolute(path) => f.write_fmt(format_args!("/{path}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use rand::distr::Alphanumeric;
|
use rand::distr::Alphanumeric;
|
||||||
use rand::distr::SampleString;
|
use rand::distr::SampleString;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use std::time::UNIX_EPOCH;
|
use std::time::UNIX_EPOCH;
|
||||||
|
|
||||||
@@ -24,12 +25,26 @@ pub struct Id {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Id {
|
impl Id {
|
||||||
pub fn from_string(value: String) -> Self {
|
pub fn empty() -> Self {
|
||||||
Self { value }
|
Id {
|
||||||
|
value: String::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_str(value: &str) -> Self {
|
impl FromStr for Id {
|
||||||
Self::from_string(value.to_string())
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Id {
|
||||||
|
value: s.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Id {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self { value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
mod id;
|
mod file;
|
||||||
mod version;
|
mod version;
|
||||||
pub use id::*;
|
pub use file::*;
|
||||||
pub use version::*;
|
pub use version::*;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ impl serde::Serialize for Version {
|
|||||||
|
|
||||||
impl std::fmt::Display for Version {
|
impl std::fmt::Display for Version {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
return self.value.fmt(f);
|
self.value.fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use harmony_types::net::IpAddress;
|
||||||
use super::topology::IpAddress;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ExecutorError {
|
pub enum ExecutorError {
|
||||||
|
|||||||
@@ -1,133 +1,230 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
|
use harmony_inventory_agent::hwinfo::{CPU, MemoryModule, NetworkInterface, StorageDrive};
|
||||||
use harmony_types::net::MacAddress;
|
use harmony_types::net::MacAddress;
|
||||||
use serde::{Serialize, Serializer, ser::SerializeStruct};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_value::Value;
|
use serde_value::Value;
|
||||||
|
|
||||||
pub type HostGroup = Vec<PhysicalHost>;
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PhysicalHost {
|
pub struct PhysicalHost {
|
||||||
|
pub id: Id,
|
||||||
pub category: HostCategory,
|
pub category: HostCategory,
|
||||||
pub network: Vec<NetworkInterface>,
|
pub network: Vec<NetworkInterface>,
|
||||||
pub management: Arc<dyn ManagementInterface>,
|
pub storage: Vec<StorageDrive>,
|
||||||
pub storage: Vec<Storage>,
|
|
||||||
pub labels: Vec<Label>,
|
pub labels: Vec<Label>,
|
||||||
pub memory_size: Option<u64>,
|
pub memory_modules: Vec<MemoryModule>,
|
||||||
pub cpu_count: Option<u64>,
|
pub cpus: Vec<CPU>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PhysicalHost {
|
impl PhysicalHost {
|
||||||
pub fn empty(category: HostCategory) -> Self {
|
pub fn empty(category: HostCategory) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
id: Id::empty(),
|
||||||
category,
|
category,
|
||||||
network: vec![],
|
network: vec![],
|
||||||
storage: vec![],
|
storage: vec![],
|
||||||
labels: vec![],
|
labels: vec![],
|
||||||
management: Arc::new(ManualManagementInterface {}),
|
memory_modules: vec![],
|
||||||
memory_size: None,
|
cpus: vec![],
|
||||||
cpu_count: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn summary(&self) -> String {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
|
||||||
|
// Part 1: System Model (from labels) or Category as a fallback
|
||||||
|
let model = self
|
||||||
|
.labels
|
||||||
|
.iter()
|
||||||
|
.find(|l| l.name == "system-product-name" || l.name == "model")
|
||||||
|
.map(|l| l.value.clone())
|
||||||
|
.unwrap_or_else(|| self.category.to_string());
|
||||||
|
parts.push(model);
|
||||||
|
|
||||||
|
// Part 2: CPU Information
|
||||||
|
if !self.cpus.is_empty() {
|
||||||
|
let cpu_count = self.cpus.len();
|
||||||
|
let total_cores = self.cpus.iter().map(|c| c.cores).sum::<u32>();
|
||||||
|
let total_threads = self.cpus.iter().map(|c| c.threads).sum::<u32>();
|
||||||
|
let model_name = &self.cpus[0].model;
|
||||||
|
|
||||||
|
let cpu_summary = if cpu_count > 1 {
|
||||||
|
format!(
|
||||||
|
"{}x {} ({}c/{}t)",
|
||||||
|
cpu_count, model_name, total_cores, total_threads
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("{} ({}c/{}t)", model_name, total_cores, total_threads)
|
||||||
|
};
|
||||||
|
parts.push(cpu_summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part 3: Memory Information
|
||||||
|
if !self.memory_modules.is_empty() {
|
||||||
|
let total_mem_bytes = self
|
||||||
|
.memory_modules
|
||||||
|
.iter()
|
||||||
|
.map(|m| m.size_bytes)
|
||||||
|
.sum::<u64>();
|
||||||
|
let total_mem_gb = (total_mem_bytes as f64 / (1024.0 * 1024.0 * 1024.0)).round() as u64;
|
||||||
|
|
||||||
|
// Find the most common speed among modules
|
||||||
|
let mut speeds = std::collections::HashMap::new();
|
||||||
|
for module in &self.memory_modules {
|
||||||
|
if let Some(speed) = module.speed_mhz {
|
||||||
|
*speeds.entry(speed).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let common_speed = speeds
|
||||||
|
.into_iter()
|
||||||
|
.max_by_key(|&(_, count)| count)
|
||||||
|
.map(|(speed, _)| speed);
|
||||||
|
|
||||||
|
if let Some(speed) = common_speed {
|
||||||
|
parts.push(format!("{} GB RAM @ {}MHz", total_mem_gb, speed));
|
||||||
|
} else {
|
||||||
|
parts.push(format!("{} GB RAM", total_mem_gb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part 4: Storage Information
|
||||||
|
if !self.storage.is_empty() {
|
||||||
|
let total_storage_bytes = self.storage.iter().map(|d| d.size_bytes).sum::<u64>();
|
||||||
|
let drive_count = self.storage.len();
|
||||||
|
let first_drive_model = &self.storage[0].model;
|
||||||
|
|
||||||
|
// Helper to format bytes into TB or GB
|
||||||
|
let format_storage = |bytes: u64| {
|
||||||
|
let tb = bytes as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0);
|
||||||
|
if tb >= 1.0 {
|
||||||
|
format!("{:.2} TB", tb)
|
||||||
|
} else {
|
||||||
|
let gb = bytes as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||||
|
format!("{:.0} GB", gb)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let storage_summary = if drive_count > 1 {
|
||||||
|
format!(
|
||||||
|
"{} Storage ({}x {})",
|
||||||
|
format_storage(total_storage_bytes),
|
||||||
|
drive_count,
|
||||||
|
first_drive_model
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} Storage ({})",
|
||||||
|
format_storage(total_storage_bytes),
|
||||||
|
first_drive_model
|
||||||
|
)
|
||||||
|
};
|
||||||
|
parts.push(storage_summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part 5: Network Information
|
||||||
|
// Prioritize an "up" interface with an IPv4 address
|
||||||
|
let best_nic = self
|
||||||
|
.network
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.is_up && !n.ipv4_addresses.is_empty())
|
||||||
|
.or_else(|| self.network.first());
|
||||||
|
|
||||||
|
if let Some(nic) = best_nic {
|
||||||
|
let speed = nic
|
||||||
|
.speed_mbps
|
||||||
|
.map(|s| format!("{}Gbps", s / 1000))
|
||||||
|
.unwrap_or_else(|| "N/A".to_string());
|
||||||
|
let mac = nic.mac_address.to_string();
|
||||||
|
let nic_summary = if let Some(ip) = nic.ipv4_addresses.first() {
|
||||||
|
format!("NIC: {} ({}, {})", speed, ip, mac)
|
||||||
|
} else {
|
||||||
|
format!("NIC: {} ({})", speed, mac)
|
||||||
|
};
|
||||||
|
parts.push(nic_summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.join(" | ")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cluster_mac(&self) -> MacAddress {
|
pub fn cluster_mac(&self) -> MacAddress {
|
||||||
self.network
|
self.network
|
||||||
.get(0)
|
.first()
|
||||||
.expect("Cluster physical host should have a network interface")
|
.expect("Cluster physical host should have a network interface")
|
||||||
.mac_address
|
.mac_address
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cpu(mut self, cpu_count: Option<u64>) -> Self {
|
|
||||||
self.cpu_count = cpu_count;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn memory_size(mut self, memory_size: Option<u64>) -> Self {
|
|
||||||
self.memory_size = memory_size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn storage(
|
|
||||||
mut self,
|
|
||||||
connection: StorageConnectionType,
|
|
||||||
kind: StorageKind,
|
|
||||||
size: u64,
|
|
||||||
serial: String,
|
|
||||||
) -> Self {
|
|
||||||
self.storage.push(Storage {
|
|
||||||
connection,
|
|
||||||
kind,
|
|
||||||
size,
|
|
||||||
serial,
|
|
||||||
});
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mac_address(mut self, mac_address: MacAddress) -> Self {
|
pub fn mac_address(mut self, mac_address: MacAddress) -> Self {
|
||||||
self.network.push(NetworkInterface {
|
self.network.push(NetworkInterface {
|
||||||
name: None,
|
name: String::new(),
|
||||||
mac_address,
|
mac_address,
|
||||||
speed: None,
|
speed_mbps: None,
|
||||||
|
is_up: false,
|
||||||
|
mtu: 0,
|
||||||
|
ipv4_addresses: vec![],
|
||||||
|
ipv6_addresses: vec![],
|
||||||
|
driver: String::new(),
|
||||||
|
firmware_version: None,
|
||||||
});
|
});
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn management(mut self, management: Arc<dyn ManagementInterface>) -> Self {
|
|
||||||
self.management = management;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom Serialize implementation for PhysicalHost
|
// Custom Serialize implementation for PhysicalHost
|
||||||
impl Serialize for PhysicalHost {
|
// impl Serialize for PhysicalHost {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
// fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
// where
|
||||||
S: Serializer,
|
// S: Serializer,
|
||||||
{
|
// {
|
||||||
// Determine the number of fields
|
// // Determine the number of fields
|
||||||
let mut num_fields = 5; // category, network, storage, labels, management
|
// let mut num_fields = 5; // category, network, storage, labels, management
|
||||||
if self.memory_size.is_some() {
|
// if self.memory_modules.is_some() {
|
||||||
num_fields += 1;
|
// num_fields += 1;
|
||||||
}
|
// }
|
||||||
if self.cpu_count.is_some() {
|
// if self.cpus.is_some() {
|
||||||
num_fields += 1;
|
// num_fields += 1;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
// // Create a serialization structure
|
||||||
|
// let mut state = serializer.serialize_struct("PhysicalHost", num_fields)?;
|
||||||
|
//
|
||||||
|
// // Serialize the standard fields
|
||||||
|
// state.serialize_field("category", &self.category)?;
|
||||||
|
// state.serialize_field("network", &self.network)?;
|
||||||
|
// state.serialize_field("storage", &self.storage)?;
|
||||||
|
// state.serialize_field("labels", &self.labels)?;
|
||||||
|
//
|
||||||
|
// // Serialize optional fields
|
||||||
|
// if let Some(memory) = self.memory_modules {
|
||||||
|
// state.serialize_field("memory_size", &memory)?;
|
||||||
|
// }
|
||||||
|
// if let Some(cpu) = self.cpus {
|
||||||
|
// state.serialize_field("cpu_count", &cpu)?;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let mgmt_data = self.management.serialize_management();
|
||||||
|
// // pub management: Arc<dyn ManagementInterface>,
|
||||||
|
//
|
||||||
|
// // Handle management interface - either as a field or flattened
|
||||||
|
// state.serialize_field("management", &mgmt_data)?;
|
||||||
|
//
|
||||||
|
// state.end()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// Create a serialization structure
|
|
||||||
let mut state = serializer.serialize_struct("PhysicalHost", num_fields)?;
|
|
||||||
|
|
||||||
// Serialize the standard fields
|
|
||||||
state.serialize_field("category", &self.category)?;
|
|
||||||
state.serialize_field("network", &self.network)?;
|
|
||||||
state.serialize_field("storage", &self.storage)?;
|
|
||||||
state.serialize_field("labels", &self.labels)?;
|
|
||||||
|
|
||||||
// Serialize optional fields
|
|
||||||
if let Some(memory) = self.memory_size {
|
|
||||||
state.serialize_field("memory_size", &memory)?;
|
|
||||||
}
|
|
||||||
if let Some(cpu) = self.cpu_count {
|
|
||||||
state.serialize_field("cpu_count", &cpu)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mgmt_data = self.management.serialize_management();
|
|
||||||
// pub management: Arc<dyn ManagementInterface>,
|
|
||||||
|
|
||||||
// Handle management interface - either as a field or flattened
|
|
||||||
state.serialize_field("management", &mgmt_data)?;
|
|
||||||
|
|
||||||
state.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(new, Serialize)]
|
#[derive(new, Serialize)]
|
||||||
pub struct ManualManagementInterface;
|
pub struct ManualManagementInterface;
|
||||||
@@ -172,66 +269,17 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum HostCategory {
|
pub enum HostCategory {
|
||||||
Server,
|
Server,
|
||||||
Firewall,
|
Firewall,
|
||||||
Switch,
|
Switch,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, new, Clone, Serialize)]
|
|
||||||
pub struct NetworkInterface {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub mac_address: MacAddress,
|
|
||||||
pub speed: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use harmony_macros::mac_address;
|
use harmony_macros::mac_address;
|
||||||
#[cfg(test)]
|
|
||||||
impl NetworkInterface {
|
|
||||||
pub fn dummy() -> Self {
|
|
||||||
Self {
|
|
||||||
name: Some(String::new()),
|
|
||||||
mac_address: mac_address!("00:00:00:00:00:00"),
|
|
||||||
speed: Some(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, new, Clone, Serialize)]
|
use harmony_types::id::Id;
|
||||||
pub enum StorageConnectionType {
|
|
||||||
Sata3g,
|
|
||||||
Sata6g,
|
|
||||||
Sas6g,
|
|
||||||
Sas12g,
|
|
||||||
PCIE,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub enum StorageKind {
|
|
||||||
SSD,
|
|
||||||
NVME,
|
|
||||||
HDD,
|
|
||||||
}
|
|
||||||
#[derive(Debug, new, Clone, Serialize)]
|
|
||||||
pub struct Storage {
|
|
||||||
pub connection: StorageConnectionType,
|
|
||||||
pub kind: StorageKind,
|
|
||||||
pub size: u64,
|
|
||||||
pub serial: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
impl Storage {
|
|
||||||
pub fn dummy() -> Self {
|
|
||||||
Self {
|
|
||||||
connection: StorageConnectionType::Sata3g,
|
|
||||||
kind: StorageKind::SSD,
|
|
||||||
size: 0,
|
|
||||||
serial: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Switch {
|
pub struct Switch {
|
||||||
@@ -239,7 +287,7 @@ pub struct Switch {
|
|||||||
_management_interface: NetworkInterface,
|
_management_interface: NetworkInterface,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, new, Clone, Serialize)]
|
#[derive(Debug, new, Clone, Serialize, Deserialize)]
|
||||||
pub struct Label {
|
pub struct Label {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
@@ -262,146 +310,65 @@ impl Location {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for HostCategory {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
HostCategory::Server => write!(f, "Server"),
|
||||||
|
HostCategory::Firewall => write!(f, "Firewall"),
|
||||||
|
HostCategory::Switch => write!(f, "Switch"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Label {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}: {}", self.name, self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Location {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Address: {}, Name: {}", self.address, self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for PhysicalHost {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.summary())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Switch {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Switch with {} interfaces", self._interface.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
// Mock implementation of ManagementInterface
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct MockHPIlo {
|
|
||||||
ip: String,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
firmware_version: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ManagementInterface for MockHPIlo {
|
|
||||||
fn boot_to_pxe(&self) {}
|
|
||||||
|
|
||||||
fn get_supported_protocol_names(&self) -> String {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Another mock implementation
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct MockDellIdrac {
|
|
||||||
hostname: String,
|
|
||||||
port: u16,
|
|
||||||
api_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ManagementInterface for MockDellIdrac {
|
|
||||||
fn boot_to_pxe(&self) {}
|
|
||||||
|
|
||||||
fn get_supported_protocol_names(&self) -> String {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_physical_host_with_hp_ilo() {
|
|
||||||
// Create a PhysicalHost with HP iLO management
|
|
||||||
let host = PhysicalHost {
|
|
||||||
category: HostCategory::Server,
|
|
||||||
network: vec![NetworkInterface::dummy()],
|
|
||||||
management: Arc::new(MockHPIlo {
|
|
||||||
ip: "192.168.1.100".to_string(),
|
|
||||||
username: "admin".to_string(),
|
|
||||||
password: "password123".to_string(),
|
|
||||||
firmware_version: "2.5.0".to_string(),
|
|
||||||
}),
|
|
||||||
storage: vec![Storage::dummy()],
|
|
||||||
labels: vec![Label::new("datacenter".to_string(), "us-east".to_string())],
|
|
||||||
memory_size: Some(64_000_000),
|
|
||||||
cpu_count: Some(16),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Serialize to JSON
|
|
||||||
let json = serde_json::to_string(&host).expect("Failed to serialize host");
|
|
||||||
|
|
||||||
// Check that the serialized JSON contains the HP iLO details
|
|
||||||
assert!(json.contains("192.168.1.100"));
|
|
||||||
assert!(json.contains("admin"));
|
|
||||||
assert!(json.contains("password123"));
|
|
||||||
assert!(json.contains("firmware_version"));
|
|
||||||
assert!(json.contains("2.5.0"));
|
|
||||||
|
|
||||||
// Parse back to verify structure (not the exact management interface)
|
|
||||||
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON");
|
|
||||||
|
|
||||||
// Verify basic structure
|
|
||||||
assert_eq!(parsed["cpu_count"], 16);
|
|
||||||
assert_eq!(parsed["memory_size"], 64_000_000);
|
|
||||||
assert_eq!(parsed["network"][0]["name"], "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_physical_host_with_dell_idrac() {
|
|
||||||
// Create a PhysicalHost with Dell iDRAC management
|
|
||||||
let host = PhysicalHost {
|
|
||||||
category: HostCategory::Server,
|
|
||||||
network: vec![NetworkInterface::dummy()],
|
|
||||||
management: Arc::new(MockDellIdrac {
|
|
||||||
hostname: "idrac-server01".to_string(),
|
|
||||||
port: 443,
|
|
||||||
api_token: "abcdef123456".to_string(),
|
|
||||||
}),
|
|
||||||
storage: vec![Storage::dummy()],
|
|
||||||
labels: vec![Label::new("env".to_string(), "production".to_string())],
|
|
||||||
memory_size: Some(128_000_000),
|
|
||||||
cpu_count: Some(32),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Serialize to JSON
|
|
||||||
let json = serde_json::to_string(&host).expect("Failed to serialize host");
|
|
||||||
|
|
||||||
// Check that the serialized JSON contains the Dell iDRAC details
|
|
||||||
assert!(json.contains("idrac-server01"));
|
|
||||||
assert!(json.contains("443"));
|
|
||||||
assert!(json.contains("abcdef123456"));
|
|
||||||
|
|
||||||
// Parse back to verify structure
|
|
||||||
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON");
|
|
||||||
|
|
||||||
// Verify basic structure
|
|
||||||
assert_eq!(parsed["cpu_count"], 32);
|
|
||||||
assert_eq!(parsed["memory_size"], 128_000_000);
|
|
||||||
assert_eq!(parsed["storage"][0]["path"], serde_json::Value::Null);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_different_management_implementations_produce_valid_json() {
|
fn test_different_management_implementations_produce_valid_json() {
|
||||||
// Create hosts with different management implementations
|
// Create hosts with different management implementations
|
||||||
let host1 = PhysicalHost {
|
let host1 = PhysicalHost {
|
||||||
|
id: Id::empty(),
|
||||||
category: HostCategory::Server,
|
category: HostCategory::Server,
|
||||||
network: vec![],
|
network: vec![],
|
||||||
management: Arc::new(MockHPIlo {
|
|
||||||
ip: "10.0.0.1".to_string(),
|
|
||||||
username: "root".to_string(),
|
|
||||||
password: "secret".to_string(),
|
|
||||||
firmware_version: "3.0.0".to_string(),
|
|
||||||
}),
|
|
||||||
storage: vec![],
|
storage: vec![],
|
||||||
labels: vec![],
|
labels: vec![],
|
||||||
memory_size: None,
|
memory_modules: vec![],
|
||||||
cpu_count: None,
|
cpus: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let host2 = PhysicalHost {
|
let host2 = PhysicalHost {
|
||||||
|
id: Id::empty(),
|
||||||
category: HostCategory::Server,
|
category: HostCategory::Server,
|
||||||
network: vec![],
|
network: vec![],
|
||||||
management: Arc::new(MockDellIdrac {
|
|
||||||
hostname: "server02-idrac".to_string(),
|
|
||||||
port: 8443,
|
|
||||||
api_token: "token123".to_string(),
|
|
||||||
}),
|
|
||||||
storage: vec![],
|
storage: vec![],
|
||||||
labels: vec![],
|
labels: vec![],
|
||||||
memory_size: None,
|
memory_modules: vec![],
|
||||||
cpu_count: None,
|
cpus: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Both should serialize successfully
|
// Both should serialize successfully
|
||||||
@@ -411,8 +378,5 @@ mod tests {
|
|||||||
// Both JSONs should be valid and parseable
|
// Both JSONs should be valid and parseable
|
||||||
let _: serde_json::Value = serde_json::from_str(&json1).expect("Invalid JSON for host1");
|
let _: serde_json::Value = serde_json::from_str(&json1).expect("Invalid JSON for host1");
|
||||||
let _: serde_json::Value = serde_json::from_str(&json2).expect("Invalid JSON for host2");
|
let _: serde_json::Value = serde_json::from_str(&json2).expect("Invalid JSON for host2");
|
||||||
|
|
||||||
// The JSONs should be different because they contain different management interfaces
|
|
||||||
assert_ne!(json1, json2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,84 @@
|
|||||||
use log::debug;
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use tokio::sync::broadcast;
|
use std::{collections::HashMap, sync::Mutex};
|
||||||
|
|
||||||
use super::interpret::{InterpretError, Outcome};
|
use crate::modules::application::ApplicationFeatureStatus;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
interpret::{InterpretError, Outcome},
|
||||||
|
topology::TopologyStatus,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum HarmonyEvent {
|
pub enum HarmonyEvent {
|
||||||
HarmonyStarted,
|
HarmonyStarted,
|
||||||
PrepareTopologyStarted {
|
HarmonyFinished,
|
||||||
topology: String,
|
|
||||||
},
|
|
||||||
TopologyPrepared {
|
|
||||||
topology: String,
|
|
||||||
outcome: Outcome,
|
|
||||||
},
|
|
||||||
InterpretExecutionStarted {
|
InterpretExecutionStarted {
|
||||||
|
execution_id: String,
|
||||||
topology: String,
|
topology: String,
|
||||||
interpret: String,
|
interpret: String,
|
||||||
|
score: String,
|
||||||
message: String,
|
message: String,
|
||||||
},
|
},
|
||||||
InterpretExecutionFinished {
|
InterpretExecutionFinished {
|
||||||
|
execution_id: String,
|
||||||
topology: String,
|
topology: String,
|
||||||
interpret: String,
|
interpret: String,
|
||||||
|
score: String,
|
||||||
outcome: Result<Outcome, InterpretError>,
|
outcome: Result<Outcome, InterpretError>,
|
||||||
},
|
},
|
||||||
|
TopologyStateChanged {
|
||||||
|
topology: String,
|
||||||
|
status: TopologyStatus,
|
||||||
|
message: Option<String>,
|
||||||
|
},
|
||||||
|
ApplicationFeatureStateChanged {
|
||||||
|
topology: String,
|
||||||
|
application: String,
|
||||||
|
feature: String,
|
||||||
|
status: ApplicationFeatureStatus,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
static HARMONY_EVENT_BUS: Lazy<broadcast::Sender<HarmonyEvent>> = Lazy::new(|| {
|
type Subscriber = Box<dyn Fn(&HarmonyEvent) + Send + Sync>;
|
||||||
// TODO: Adjust channel capacity
|
|
||||||
let (tx, _rx) = broadcast::channel(100);
|
|
||||||
tx
|
|
||||||
});
|
|
||||||
|
|
||||||
pub fn instrument(event: HarmonyEvent) -> Result<(), &'static str> {
|
static SUBSCRIBERS: Lazy<Mutex<HashMap<String, Subscriber>>> =
|
||||||
match HARMONY_EVENT_BUS.send(event) {
|
Lazy::new(|| Mutex::new(HashMap::new()));
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(_) => Err("send error: no subscribers"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn subscribe<F, Fut>(name: &str, mut handler: F)
|
/// Subscribes a listener to all instrumentation events.
|
||||||
|
///
|
||||||
|
/// Simply provide a unique name and a closure to run when an event happens.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use harmony::instrumentation;
|
||||||
|
/// instrumentation::subscribe("my_logger", |event| {
|
||||||
|
/// println!("Event occurred: {:?}", event);
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub fn subscribe<F>(name: &str, callback: F)
|
||||||
where
|
where
|
||||||
F: FnMut(HarmonyEvent) -> Fut + Send + 'static,
|
F: Fn(&HarmonyEvent) + Send + Sync + 'static,
|
||||||
Fut: Future<Output = bool> + Send,
|
|
||||||
{
|
{
|
||||||
let mut rx = HARMONY_EVENT_BUS.subscribe();
|
let mut subs = SUBSCRIBERS.lock().unwrap();
|
||||||
debug!("[{name}] Service started. Listening for events...");
|
subs.insert(name.to_string(), Box::new(callback));
|
||||||
loop {
|
}
|
||||||
match rx.recv().await {
|
|
||||||
Ok(event) => {
|
/// Instruments an event, notifying all subscribers.
|
||||||
if !handler(event).await {
|
///
|
||||||
debug!("[{name}] Handler requested exit.");
|
/// This will call every closure that was registered with `subscribe`.
|
||||||
break;
|
///
|
||||||
}
|
/// # Example
|
||||||
}
|
/// ```
|
||||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
/// use harmony::instrumentation;
|
||||||
debug!("[{name}] Lagged behind by {n} messages.");
|
/// use harmony::instrumentation::HarmonyEvent;
|
||||||
}
|
/// instrumentation::instrument(HarmonyEvent::HarmonyStarted);
|
||||||
Err(_) => break,
|
/// ```
|
||||||
}
|
pub fn instrument(event: HarmonyEvent) -> Result<(), &'static str> {
|
||||||
}
|
let subs = SUBSCRIBERS.lock().unwrap();
|
||||||
|
|
||||||
|
for callback in subs.values() {
|
||||||
|
callback(&event);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
|
use harmony_types::id::Id;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
data::{Id, Version},
|
data::Version, executors::ExecutorError, inventory::Inventory, topology::PreparationError,
|
||||||
executors::ExecutorError,
|
|
||||||
inventory::Inventory,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum InterpretName {
|
pub enum InterpretName {
|
||||||
@@ -23,6 +22,17 @@ pub enum InterpretName {
|
|||||||
TenantInterpret,
|
TenantInterpret,
|
||||||
Application,
|
Application,
|
||||||
ArgoCD,
|
ArgoCD,
|
||||||
|
Alerting,
|
||||||
|
Ntfy,
|
||||||
|
HelmChart,
|
||||||
|
HelmCommand,
|
||||||
|
K8sResource,
|
||||||
|
Lamp,
|
||||||
|
ApplicationMonitoring,
|
||||||
|
K8sPrometheusCrdAlerting,
|
||||||
|
DiscoverInventoryAgent,
|
||||||
|
CephClusterHealth,
|
||||||
|
Custom(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for InterpretName {
|
impl std::fmt::Display for InterpretName {
|
||||||
@@ -41,6 +51,17 @@ impl std::fmt::Display for InterpretName {
|
|||||||
InterpretName::TenantInterpret => f.write_str("Tenant"),
|
InterpretName::TenantInterpret => f.write_str("Tenant"),
|
||||||
InterpretName::Application => f.write_str("Application"),
|
InterpretName::Application => f.write_str("Application"),
|
||||||
InterpretName::ArgoCD => f.write_str("ArgoCD"),
|
InterpretName::ArgoCD => f.write_str("ArgoCD"),
|
||||||
|
InterpretName::Alerting => f.write_str("Alerting"),
|
||||||
|
InterpretName::Ntfy => f.write_str("Ntfy"),
|
||||||
|
InterpretName::HelmChart => f.write_str("HelmChart"),
|
||||||
|
InterpretName::HelmCommand => f.write_str("HelmCommand"),
|
||||||
|
InterpretName::K8sResource => f.write_str("K8sResource"),
|
||||||
|
InterpretName::Lamp => f.write_str("LAMP"),
|
||||||
|
InterpretName::ApplicationMonitoring => f.write_str("ApplicationMonitoring"),
|
||||||
|
InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"),
|
||||||
|
InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"),
|
||||||
|
InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"),
|
||||||
|
InterpretName::Custom(name) => f.write_str(name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,6 +134,14 @@ impl std::fmt::Display for InterpretError {
|
|||||||
}
|
}
|
||||||
impl Error for InterpretError {}
|
impl Error for InterpretError {}
|
||||||
|
|
||||||
|
impl From<PreparationError> for InterpretError {
|
||||||
|
fn from(value: PreparationError) -> Self {
|
||||||
|
Self {
|
||||||
|
msg: format!("InterpretError : {value}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<ExecutorError> for InterpretError {
|
impl From<ExecutorError> for InterpretError {
|
||||||
fn from(value: ExecutorError) -> Self {
|
fn from(value: ExecutorError) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
mod repository;
|
||||||
|
pub use repository::*;
|
||||||
|
|
||||||
#[derive(Debug, new, Clone)]
|
#[derive(Debug, new, Clone)]
|
||||||
pub struct InventoryFilter {
|
pub struct InventoryFilter {
|
||||||
target: Vec<Filter>,
|
target: Vec<Filter>,
|
||||||
@@ -14,6 +17,9 @@ impl InventoryFilter {
|
|||||||
|
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::hardware::{ManagementInterface, ManualManagementInterface};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
filter::Filter,
|
filter::Filter,
|
||||||
@@ -27,7 +33,7 @@ pub struct Inventory {
|
|||||||
// Firewall is really just a host but with somewhat specialized hardware
|
// Firewall is really just a host but with somewhat specialized hardware
|
||||||
// I'm not entirely sure it belongs to its own category but it helps make things easier and
|
// I'm not entirely sure it belongs to its own category but it helps make things easier and
|
||||||
// clearer for now so let's try it this way.
|
// clearer for now so let's try it this way.
|
||||||
pub firewall: FirewallGroup,
|
pub firewall_mgmt: Box<dyn ManagementInterface>,
|
||||||
pub worker_host: HostGroup,
|
pub worker_host: HostGroup,
|
||||||
pub storage_host: HostGroup,
|
pub storage_host: HostGroup,
|
||||||
pub control_plane_host: HostGroup,
|
pub control_plane_host: HostGroup,
|
||||||
@@ -38,7 +44,7 @@ impl Inventory {
|
|||||||
Self {
|
Self {
|
||||||
location: Location::new("Empty".to_string(), "location".to_string()),
|
location: Location::new("Empty".to_string(), "location".to_string()),
|
||||||
switch: vec![],
|
switch: vec![],
|
||||||
firewall: vec![],
|
firewall_mgmt: Box::new(ManualManagementInterface {}),
|
||||||
worker_host: vec![],
|
worker_host: vec![],
|
||||||
storage_host: vec![],
|
storage_host: vec![],
|
||||||
control_plane_host: vec![],
|
control_plane_host: vec![],
|
||||||
@@ -49,10 +55,18 @@ impl Inventory {
|
|||||||
Self {
|
Self {
|
||||||
location: Location::test_building(),
|
location: Location::test_building(),
|
||||||
switch: SwitchGroup::new(),
|
switch: SwitchGroup::new(),
|
||||||
firewall: FirewallGroup::new(),
|
firewall_mgmt: Box::new(ManualManagementInterface {}),
|
||||||
worker_host: HostGroup::new(),
|
worker_host: HostGroup::new(),
|
||||||
storage_host: HostGroup::new(),
|
storage_host: HostGroup::new(),
|
||||||
control_plane_host: HostGroup::new(),
|
control_plane_host: HostGroup::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, sqlx::Type)]
|
||||||
|
pub enum HostRole {
|
||||||
|
Bootstrap,
|
||||||
|
ControlPlane,
|
||||||
|
Worker,
|
||||||
|
Storage,
|
||||||
|
}
|
||||||
|
|||||||
38
harmony/src/domain/inventory/repository.rs
Normal file
38
harmony/src/domain/inventory/repository.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{hardware::PhysicalHost, interpret::InterpretError, inventory::HostRole};
|
||||||
|
|
||||||
|
/// Errors that can occur within the repository layer.
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum RepoError {
|
||||||
|
#[error("Database query failed: {0}")]
|
||||||
|
QueryFailed(String),
|
||||||
|
#[error("Data serialization failed: {0}")]
|
||||||
|
Serialization(String),
|
||||||
|
#[error("Data deserialization failed: {0}")]
|
||||||
|
Deserialization(String),
|
||||||
|
#[error("Could not connect to the database: {0}")]
|
||||||
|
ConnectionFailed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RepoError> for InterpretError {
|
||||||
|
fn from(value: RepoError) -> Self {
|
||||||
|
InterpretError::new(format!("Interpret error : {value}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Trait and Implementation ---
|
||||||
|
|
||||||
|
/// Defines the contract for inventory persistence.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait InventoryRepository: Send + Sync + 'static {
|
||||||
|
async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError>;
|
||||||
|
async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError>;
|
||||||
|
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>;
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use log::{debug, info, warn};
|
use log::{debug, warn};
|
||||||
|
|
||||||
use crate::instrumentation::{self, HarmonyEvent};
|
use crate::topology::TopologyStatus;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
interpret::{InterpretError, InterpretStatus, Outcome},
|
interpret::{InterpretError, Outcome},
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::Topology,
|
topology::{PreparationError, PreparationOutcome, Topology, TopologyState},
|
||||||
};
|
};
|
||||||
|
|
||||||
type ScoreVec<T> = Vec<Box<dyn Score<T>>>;
|
type ScoreVec<T> = Vec<Box<dyn Score<T>>>;
|
||||||
@@ -17,7 +17,7 @@ pub struct Maestro<T: Topology> {
|
|||||||
inventory: Inventory,
|
inventory: Inventory,
|
||||||
topology: T,
|
topology: T,
|
||||||
scores: Arc<RwLock<ScoreVec<T>>>,
|
scores: Arc<RwLock<ScoreVec<T>>>,
|
||||||
topology_preparation_result: Mutex<Option<Outcome>>,
|
topology_state: TopologyState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology> Maestro<T> {
|
impl<T: Topology> Maestro<T> {
|
||||||
@@ -25,41 +25,46 @@ impl<T: Topology> Maestro<T> {
|
|||||||
///
|
///
|
||||||
/// This should rarely be used. Most of the time Maestro::initialize should be used instead.
|
/// This should rarely be used. Most of the time Maestro::initialize should be used instead.
|
||||||
pub fn new_without_initialization(inventory: Inventory, topology: T) -> Self {
|
pub fn new_without_initialization(inventory: Inventory, topology: T) -> Self {
|
||||||
|
let topology_name = topology.name().to_string();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
inventory,
|
inventory,
|
||||||
topology,
|
topology,
|
||||||
scores: Arc::new(RwLock::new(Vec::new())),
|
scores: Arc::new(RwLock::new(Vec::new())),
|
||||||
topology_preparation_result: None.into(),
|
topology_state: TopologyState::new(topology_name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initialize(inventory: Inventory, topology: T) -> Result<Self, InterpretError> {
|
pub async fn initialize(inventory: Inventory, topology: T) -> Result<Self, PreparationError> {
|
||||||
let instance = Self::new_without_initialization(inventory, topology);
|
let mut instance = Self::new_without_initialization(inventory, topology);
|
||||||
instance.prepare_topology().await?;
|
instance.prepare_topology().await?;
|
||||||
Ok(instance)
|
Ok(instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensures the associated Topology is ready for operations.
|
/// Ensures the associated Topology is ready for operations.
|
||||||
/// Delegates the readiness check and potential setup actions to the Topology.
|
/// Delegates the readiness check and potential setup actions to the Topology.
|
||||||
pub async fn prepare_topology(&self) -> Result<Outcome, InterpretError> {
|
async fn prepare_topology(&mut self) -> Result<PreparationOutcome, PreparationError> {
|
||||||
instrumentation::instrument(HarmonyEvent::PrepareTopologyStarted {
|
self.topology_state.prepare();
|
||||||
topology: self.topology.name().to_string(),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let outcome = self.topology.ensure_ready().await?;
|
let result = self.topology.ensure_ready().await;
|
||||||
|
|
||||||
instrumentation::instrument(HarmonyEvent::TopologyPrepared {
|
match result {
|
||||||
topology: self.topology.name().to_string(),
|
Ok(outcome) => {
|
||||||
outcome: outcome.clone(),
|
match outcome.clone() {
|
||||||
})
|
PreparationOutcome::Success { details } => {
|
||||||
.unwrap();
|
self.topology_state.success(details);
|
||||||
|
}
|
||||||
self.topology_preparation_result
|
PreparationOutcome::Noop => {
|
||||||
.lock()
|
self.topology_state.noop();
|
||||||
.unwrap()
|
}
|
||||||
.replace(outcome.clone());
|
};
|
||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.topology_state.error(err.to_string());
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_all(&mut self, mut scores: ScoreVec<T>) {
|
pub fn register_all(&mut self, mut scores: ScoreVec<T>) {
|
||||||
@@ -68,15 +73,8 @@ impl<T: Topology> Maestro<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_topology_initialized(&self) -> bool {
|
fn is_topology_initialized(&self) -> bool {
|
||||||
let result = self.topology_preparation_result.lock().unwrap();
|
self.topology_state.status == TopologyStatus::Success
|
||||||
if let Some(outcome) = result.as_ref() {
|
|| self.topology_state.status == TopologyStatus::Noop
|
||||||
match outcome.status {
|
|
||||||
InterpretStatus::SUCCESS => return true,
|
|
||||||
_ => return false,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn interpret(&self, score: Box<dyn Score<T>>) -> Result<Outcome, InterpretError> {
|
pub async fn interpret(&self, score: Box<dyn Score<T>>) -> Result<Outcome, InterpretError> {
|
||||||
@@ -87,10 +85,8 @@ impl<T: Topology> Maestro<T> {
|
|||||||
self.topology.name(),
|
self.topology.name(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
debug!("Running score {score:?}");
|
debug!("Interpreting score {score:?}");
|
||||||
let interpret = score.create_interpret();
|
let result = score.interpret(&self.inventory, &self.topology).await;
|
||||||
debug!("Launching interpret {interpret:?}");
|
|
||||||
let result = interpret.execute(&self.inventory, &self.topology).await;
|
|
||||||
debug!("Got result {result:?}");
|
debug!("Got result {result:?}");
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,62 @@
|
|||||||
|
use harmony_types::id::Id;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_value::Value;
|
use serde_value::Value;
|
||||||
|
|
||||||
use super::{interpret::Interpret, topology::Topology};
|
use super::{
|
||||||
|
instrumentation::{self, HarmonyEvent},
|
||||||
|
interpret::{Interpret, InterpretError, Outcome},
|
||||||
|
inventory::Inventory,
|
||||||
|
topology::Topology,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
pub trait Score<T: Topology>:
|
pub trait Score<T: Topology>:
|
||||||
std::fmt::Debug + ScoreToString<T> + Send + Sync + CloneBoxScore<T> + SerializeScore<T>
|
std::fmt::Debug + ScoreToString<T> + Send + Sync + CloneBoxScore<T> + SerializeScore<T>
|
||||||
{
|
{
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>>;
|
async fn interpret(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
let id = Id::default();
|
||||||
|
let interpret = self.create_interpret();
|
||||||
|
|
||||||
|
instrumentation::instrument(HarmonyEvent::InterpretExecutionStarted {
|
||||||
|
execution_id: id.clone().to_string(),
|
||||||
|
topology: topology.name().into(),
|
||||||
|
interpret: interpret.get_name().to_string(),
|
||||||
|
score: self.name(),
|
||||||
|
message: format!("{} running...", interpret.get_name()),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let result = interpret.execute(inventory, topology).await;
|
||||||
|
|
||||||
|
instrumentation::instrument(HarmonyEvent::InterpretExecutionFinished {
|
||||||
|
execution_id: id.clone().to_string(),
|
||||||
|
topology: topology.name().into(),
|
||||||
|
interpret: interpret.get_name().to_string(),
|
||||||
|
score: self.name(),
|
||||||
|
outcome: result.clone(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SerializeScore<T: Topology> {
|
pub trait SerializeScore<T: Topology> {
|
||||||
fn serialize(&self) -> Value;
|
fn serialize(&self) -> Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de, S, T> SerializeScore<T> for S
|
impl<S, T> SerializeScore<T> for S
|
||||||
where
|
where
|
||||||
T: Topology,
|
T: Topology,
|
||||||
S: Score<T> + Serialize,
|
S: Score<T> + Serialize,
|
||||||
@@ -24,7 +64,7 @@ where
|
|||||||
fn serialize(&self) -> Value {
|
fn serialize(&self) -> Value {
|
||||||
// TODO not sure if this is the right place to handle the error or it should bubble
|
// TODO not sure if this is the right place to handle the error or it should bubble
|
||||||
// up?
|
// up?
|
||||||
serde_value::to_value(&self).expect("Score should serialize successfully")
|
serde_value::to_value(self).expect("Score should serialize successfully")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_macros::ip;
|
use harmony_macros::ip;
|
||||||
use harmony_types::net::MacAddress;
|
use harmony_types::net::MacAddress;
|
||||||
|
use harmony_types::net::Url;
|
||||||
|
use log::debug;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
|
use crate::data::FileContent;
|
||||||
use crate::executors::ExecutorError;
|
use crate::executors::ExecutorError;
|
||||||
use crate::interpret::InterpretError;
|
use crate::topology::PxeOptions;
|
||||||
use crate::interpret::Outcome;
|
|
||||||
|
|
||||||
use super::DHCPStaticEntry;
|
use super::DHCPStaticEntry;
|
||||||
use super::DhcpServer;
|
use super::DhcpServer;
|
||||||
@@ -19,11 +21,12 @@ use super::K8sclient;
|
|||||||
use super::LoadBalancer;
|
use super::LoadBalancer;
|
||||||
use super::LoadBalancerService;
|
use super::LoadBalancerService;
|
||||||
use super::LogicalHost;
|
use super::LogicalHost;
|
||||||
|
use super::PreparationError;
|
||||||
|
use super::PreparationOutcome;
|
||||||
use super::Router;
|
use super::Router;
|
||||||
use super::TftpServer;
|
use super::TftpServer;
|
||||||
|
|
||||||
use super::Topology;
|
use super::Topology;
|
||||||
use super::Url;
|
|
||||||
use super::k8s::K8sClient;
|
use super::k8s::K8sClient;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -48,10 +51,11 @@ impl Topology for HAClusterTopology {
|
|||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"HAClusterTopology"
|
"HAClusterTopology"
|
||||||
}
|
}
|
||||||
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
|
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
|
||||||
todo!(
|
debug!(
|
||||||
"ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready."
|
"ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready."
|
||||||
)
|
);
|
||||||
|
Ok(PreparationOutcome::Noop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,12 +157,18 @@ impl DhcpServer for HAClusterTopology {
|
|||||||
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> {
|
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> {
|
||||||
self.dhcp_server.list_static_mappings().await
|
self.dhcp_server.list_static_mappings().await
|
||||||
}
|
}
|
||||||
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> {
|
async fn set_pxe_options(&self, options: PxeOptions) -> Result<(), ExecutorError> {
|
||||||
self.dhcp_server.set_next_server(ip).await
|
self.dhcp_server.set_pxe_options(options).await
|
||||||
}
|
}
|
||||||
async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> {
|
|
||||||
self.dhcp_server.set_boot_filename(boot_filename).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()
|
||||||
}
|
}
|
||||||
@@ -168,16 +178,6 @@ impl DhcpServer for HAClusterTopology {
|
|||||||
async fn commit_config(&self) -> Result<(), ExecutorError> {
|
async fn commit_config(&self) -> Result<(), ExecutorError> {
|
||||||
self.dhcp_server.commit_config().await
|
self.dhcp_server.commit_config().await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> {
|
|
||||||
self.dhcp_server.set_filename(filename).await
|
|
||||||
}
|
|
||||||
async fn set_filename64(&self, filename64: &str) -> Result<(), ExecutorError> {
|
|
||||||
self.dhcp_server.set_filename64(filename64).await
|
|
||||||
}
|
|
||||||
async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError> {
|
|
||||||
self.dhcp_server.set_filenameipxe(filenameipxe).await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -221,17 +221,21 @@ impl HttpServer for HAClusterTopology {
|
|||||||
self.http_server.serve_files(url).await
|
self.http_server.serve_files(url).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> {
|
||||||
|
self.http_server.serve_file_content(file).await
|
||||||
|
}
|
||||||
|
|
||||||
fn get_ip(&self) -> IpAddress {
|
fn get_ip(&self) -> IpAddress {
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
self.http_server.get_ip()
|
||||||
}
|
}
|
||||||
async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
|
async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
self.http_server.ensure_initialized().await
|
||||||
}
|
}
|
||||||
async fn commit_config(&self) -> Result<(), ExecutorError> {
|
async fn commit_config(&self) -> Result<(), ExecutorError> {
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
self.http_server.commit_config().await
|
||||||
}
|
}
|
||||||
async fn reload_restart(&self) -> Result<(), ExecutorError> {
|
async fn reload_restart(&self) -> Result<(), ExecutorError> {
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
self.http_server.reload_restart().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,13 +245,15 @@ pub struct DummyInfra;
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Topology for DummyInfra {
|
impl Topology for DummyInfra {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
todo!()
|
"DummyInfra"
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
|
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
|
||||||
let dummy_msg = "This is a dummy infrastructure that does nothing";
|
let dummy_msg = "This is a dummy infrastructure that does nothing";
|
||||||
info!("{dummy_msg}");
|
info!("{dummy_msg}");
|
||||||
Ok(Outcome::success(dummy_msg.to_string()))
|
Ok(PreparationOutcome::Success {
|
||||||
|
details: dummy_msg.into(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,19 +303,14 @@ impl DhcpServer for DummyInfra {
|
|||||||
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> {
|
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> {
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||||
}
|
}
|
||||||
async fn set_next_server(&self, _ip: IpAddress) -> Result<(), ExecutorError> {
|
async fn set_pxe_options(&self, _options: PxeOptions) -> Result<(), ExecutorError> {
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||||
}
|
}
|
||||||
async fn set_boot_filename(&self, _boot_filename: &str) -> Result<(), ExecutorError> {
|
async fn set_dhcp_range(
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
&self,
|
||||||
}
|
start: &IpAddress,
|
||||||
async fn set_filename(&self, _filename: &str) -> Result<(), ExecutorError> {
|
end: &IpAddress,
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
) -> Result<(), ExecutorError> {
|
||||||
}
|
|
||||||
async fn set_filename64(&self, _filename: &str) -> Result<(), ExecutorError> {
|
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
|
||||||
}
|
|
||||||
async fn set_filenameipxe(&self, _filenameipxe: &str) -> Result<(), ExecutorError> {
|
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||||
}
|
}
|
||||||
fn get_ip(&self) -> IpAddress {
|
fn get_ip(&self) -> IpAddress {
|
||||||
@@ -379,6 +380,9 @@ impl HttpServer for DummyInfra {
|
|||||||
async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> {
|
async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> {
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||||
}
|
}
|
||||||
|
async fn serve_file_content(&self, _file: &FileContent) -> Result<(), ExecutorError> {
|
||||||
|
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||||
|
}
|
||||||
fn get_ip(&self) -> IpAddress {
|
fn get_ip(&self) -> IpAddress {
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use crate::executors::ExecutorError;
|
use crate::{data::FileContent, executors::ExecutorError};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use super::{IpAddress, Url};
|
use harmony_types::net::IpAddress;
|
||||||
|
use harmony_types::net::Url;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait HttpServer: Send + Sync {
|
pub trait HttpServer: Send + Sync {
|
||||||
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>;
|
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>;
|
||||||
|
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError>;
|
||||||
fn get_ip(&self) -> IpAddress;
|
fn get_ip(&self) -> IpAddress;
|
||||||
|
|
||||||
// async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError>;
|
// async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError>;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use futures_util::StreamExt;
|
|
||||||
use k8s_openapi::{
|
use k8s_openapi::{
|
||||||
ClusterResourceScope, NamespaceResourceScope,
|
ClusterResourceScope, NamespaceResourceScope,
|
||||||
api::{apps::v1::Deployment, core::v1::Pod},
|
api::{apps::v1::Deployment, core::v1::Pod},
|
||||||
};
|
};
|
||||||
use kube::{
|
use kube::{
|
||||||
Client, Config, Error, Resource,
|
Client, Config, Error, Resource,
|
||||||
api::{Api, AttachParams, ListParams, Patch, PatchParams, ResourceExt},
|
api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt},
|
||||||
config::{KubeConfigOptions, Kubeconfig},
|
config::{KubeConfigOptions, Kubeconfig},
|
||||||
core::ErrorResponse,
|
core::ErrorResponse,
|
||||||
runtime::reflector::Lookup,
|
runtime::reflector::Lookup,
|
||||||
@@ -17,14 +16,25 @@ use kube::{
|
|||||||
runtime::wait::await_condition,
|
runtime::wait::await_condition,
|
||||||
};
|
};
|
||||||
use log::{debug, error, trace};
|
use log::{debug, error, trace};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
use similar::{DiffableStr, TextDiff};
|
use serde_json::json;
|
||||||
|
use similar::TextDiff;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
#[derive(new, Clone)]
|
#[derive(new, Clone)]
|
||||||
pub struct K8sClient {
|
pub struct K8sClient {
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Serialize for K8sClient {
|
||||||
|
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for K8sClient {
|
impl std::fmt::Debug for K8sClient {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
// This is a poor man's debug implementation for now as kube::Client does not provide much
|
// This is a poor man's debug implementation for now as kube::Client does not provide much
|
||||||
@@ -43,6 +53,66 @@ impl K8sClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_deployment(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
namespace: Option<&str>,
|
||||||
|
) -> Result<Option<Deployment>, Error> {
|
||||||
|
let deps: Api<Deployment> = if let Some(ns) = namespace {
|
||||||
|
Api::namespaced(self.client.clone(), ns)
|
||||||
|
} else {
|
||||||
|
Api::default_namespaced(self.client.clone())
|
||||||
|
};
|
||||||
|
Ok(deps.get_opt(name).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_pod(&self, name: &str, namespace: Option<&str>) -> Result<Option<Pod>, Error> {
|
||||||
|
let pods: Api<Pod> = if let Some(ns) = namespace {
|
||||||
|
Api::namespaced(self.client.clone(), ns)
|
||||||
|
} else {
|
||||||
|
Api::default_namespaced(self.client.clone())
|
||||||
|
};
|
||||||
|
Ok(pods.get_opt(name).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn scale_deployment(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
namespace: Option<&str>,
|
||||||
|
replicas: u32,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let deployments: Api<Deployment> = if let Some(ns) = namespace {
|
||||||
|
Api::namespaced(self.client.clone(), ns)
|
||||||
|
} else {
|
||||||
|
Api::default_namespaced(self.client.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let patch = json!({
|
||||||
|
"spec": {
|
||||||
|
"replicas": replicas
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let pp = PatchParams::default();
|
||||||
|
let scale = Patch::Apply(&patch);
|
||||||
|
deployments.patch_scale(name, &pp, &scale).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_deployment(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
namespace: Option<&str>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let deployments: Api<Deployment> = if let Some(ns) = namespace {
|
||||||
|
Api::namespaced(self.client.clone(), ns)
|
||||||
|
} else {
|
||||||
|
Api::default_namespaced(self.client.clone())
|
||||||
|
};
|
||||||
|
let delete_params = DeleteParams::default();
|
||||||
|
deployments.delete(name, &delete_params).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn wait_until_deployment_ready(
|
pub async fn wait_until_deployment_ready(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -58,13 +128,78 @@ impl K8sClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let establish = await_condition(api, name.as_str(), conditions::is_deployment_completed());
|
let establish = await_condition(api, name.as_str(), conditions::is_deployment_completed());
|
||||||
let t = if let Some(t) = timeout { t } else { 300 };
|
let t = timeout.unwrap_or(300);
|
||||||
let res = tokio::time::timeout(std::time::Duration::from_secs(t), establish).await;
|
let res = tokio::time::timeout(std::time::Duration::from_secs(t), establish).await;
|
||||||
|
|
||||||
if let Ok(r) = res {
|
if res.is_ok() {
|
||||||
return Ok(());
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
return Err("timed out while waiting for deployment".to_string());
|
Err("timed out while waiting for deployment".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Will execute a commond in the first pod found that matches the specified label
|
||||||
|
/// '{label}={name}'
|
||||||
|
pub async fn exec_app_capture_output(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
label: String,
|
||||||
|
namespace: Option<&str>,
|
||||||
|
command: Vec<&str>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let api: Api<Pod>;
|
||||||
|
|
||||||
|
if let Some(ns) = namespace {
|
||||||
|
api = Api::namespaced(self.client.clone(), ns);
|
||||||
|
} else {
|
||||||
|
api = Api::default_namespaced(self.client.clone());
|
||||||
|
}
|
||||||
|
let pod_list = api
|
||||||
|
.list(&ListParams::default().labels(format!("{label}={name}").as_str()))
|
||||||
|
.await
|
||||||
|
.expect("couldn't get list of pods");
|
||||||
|
|
||||||
|
let res = api
|
||||||
|
.exec(
|
||||||
|
pod_list
|
||||||
|
.items
|
||||||
|
.first()
|
||||||
|
.expect("couldn't get pod")
|
||||||
|
.name()
|
||||||
|
.expect("couldn't get pod name")
|
||||||
|
.into_owned()
|
||||||
|
.as_str(),
|
||||||
|
command,
|
||||||
|
&AttachParams::default().stdout(true).stderr(true),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match res {
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
Ok(mut process) => {
|
||||||
|
let status = process
|
||||||
|
.take_status()
|
||||||
|
.expect("Couldn't get status")
|
||||||
|
.await
|
||||||
|
.expect("Couldn't unwrap status");
|
||||||
|
|
||||||
|
if let Some(s) = status.status {
|
||||||
|
let mut stdout_buf = String::new();
|
||||||
|
if let Some(mut stdout) = process.stdout().take() {
|
||||||
|
stdout
|
||||||
|
.read_to_string(&mut stdout_buf)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to get status stdout {e}"))?;
|
||||||
|
}
|
||||||
|
debug!("Status: {} - {:?}", s, status.details);
|
||||||
|
if s == "Success" {
|
||||||
|
Ok(stdout_buf)
|
||||||
|
} else {
|
||||||
|
Err(s)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("Couldn't get inner status of pod exec".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +238,7 @@ impl K8sClient {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
Err(e) => return Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
Ok(mut process) => {
|
Ok(mut process) => {
|
||||||
let status = process
|
let status = process
|
||||||
.take_status()
|
.take_status()
|
||||||
@@ -112,14 +247,10 @@ impl K8sClient {
|
|||||||
.expect("Couldn't unwrap status");
|
.expect("Couldn't unwrap status");
|
||||||
|
|
||||||
if let Some(s) = status.status {
|
if let Some(s) = status.status {
|
||||||
debug!("Status: {}", s);
|
debug!("Status: {} - {:?}", s, status.details);
|
||||||
if s == "Success" {
|
if s == "Success" { Ok(()) } else { Err(s) }
|
||||||
return Ok(());
|
|
||||||
} else {
|
|
||||||
return Err(s);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return Err("Couldn't get inner status of pod exec".to_string());
|
Err("Couldn't get inner status of pod exec".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,8 +291,9 @@ impl K8sClient {
|
|||||||
trace!("Received current value {current:#?}");
|
trace!("Received current value {current:#?}");
|
||||||
// The resource exists, so we calculate and display a diff.
|
// The resource exists, so we calculate and display a diff.
|
||||||
println!("\nPerforming dry-run for resource: '{}'", name);
|
println!("\nPerforming dry-run for resource: '{}'", name);
|
||||||
let mut current_yaml = serde_yaml::to_value(¤t)
|
let mut current_yaml = serde_yaml::to_value(¤t).unwrap_or_else(|_| {
|
||||||
.expect(&format!("Could not serialize current value : {current:#?}"));
|
panic!("Could not serialize current value : {current:#?}")
|
||||||
|
});
|
||||||
if current_yaml.is_mapping() && current_yaml.get("status").is_some() {
|
if current_yaml.is_mapping() && current_yaml.get("status").is_some() {
|
||||||
let map = current_yaml.as_mapping_mut().unwrap();
|
let map = current_yaml.as_mapping_mut().unwrap();
|
||||||
let removed = map.remove_entry("status");
|
let removed = map.remove_entry("status");
|
||||||
@@ -228,7 +360,7 @@ impl K8sClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn apply_many<K>(&self, resource: &Vec<K>, ns: Option<&str>) -> Result<Vec<K>, Error>
|
pub async fn apply_many<K>(&self, resource: &[K], ns: Option<&str>) -> Result<Vec<K>, Error>
|
||||||
where
|
where
|
||||||
K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
|
K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
|
||||||
<K as Resource>::Scope: ApplyStrategy<K>,
|
<K as Resource>::Scope: ApplyStrategy<K>,
|
||||||
@@ -244,7 +376,7 @@ impl K8sClient {
|
|||||||
|
|
||||||
pub async fn apply_yaml_many(
|
pub async fn apply_yaml_many(
|
||||||
&self,
|
&self,
|
||||||
yaml: &Vec<serde_yaml::Value>,
|
#[allow(clippy::ptr_arg)] yaml: &Vec<serde_yaml::Value>,
|
||||||
ns: Option<&str>,
|
ns: Option<&str>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
for y in yaml.iter() {
|
for y in yaml.iter() {
|
||||||
|
|||||||
@@ -7,22 +7,40 @@ use tokio::sync::OnceCell;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
executors::ExecutorError,
|
executors::ExecutorError,
|
||||||
interpret::{InterpretError, Outcome},
|
interpret::InterpretStatus,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::k3d::K3DInstallationScore,
|
modules::{
|
||||||
|
k3d::K3DInstallationScore,
|
||||||
|
monitoring::kube_prometheus::crd::{
|
||||||
|
crd_alertmanager_config::CRDPrometheus,
|
||||||
|
prometheus_operator::prometheus_operator_helm_chart_score,
|
||||||
|
},
|
||||||
|
prometheus::{
|
||||||
|
k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore,
|
||||||
|
prometheus::PrometheusApplicationMonitoring,
|
||||||
|
},
|
||||||
|
},
|
||||||
score::Score,
|
score::Score,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology,
|
DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, PreparationError,
|
||||||
|
PreparationOutcome, Topology,
|
||||||
k8s::K8sClient,
|
k8s::K8sClient,
|
||||||
tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager},
|
oberservability::monitoring::AlertReceiver,
|
||||||
|
tenant::{
|
||||||
|
TenantConfig, TenantManager,
|
||||||
|
k8s::K8sTenantManager,
|
||||||
|
network_policy::{
|
||||||
|
K3dNetworkPolicyStrategy, NetworkPolicyStrategy, NoopNetworkPolicyStrategy,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct K8sState {
|
struct K8sState {
|
||||||
client: Arc<K8sClient>,
|
client: Arc<K8sClient>,
|
||||||
_source: K8sSource,
|
source: K8sSource,
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +74,42 @@ impl K8sclient for K8sAnywhereTopology {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl PrometheusApplicationMonitoring<CRDPrometheus> for K8sAnywhereTopology {
|
||||||
|
async fn install_prometheus(
|
||||||
|
&self,
|
||||||
|
sender: &CRDPrometheus,
|
||||||
|
inventory: &Inventory,
|
||||||
|
receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>,
|
||||||
|
) -> Result<PreparationOutcome, PreparationError> {
|
||||||
|
let po_result = self.ensure_prometheus_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_k8s_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
|
||||||
S: serde::Serializer,
|
S: serde::Serializer,
|
||||||
{
|
{
|
||||||
@@ -82,6 +134,19 @@ impl K8sAnywhereTopology {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_k8s_prometheus_application_score(
|
||||||
|
&self,
|
||||||
|
sender: CRDPrometheus,
|
||||||
|
receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>,
|
||||||
|
) -> K8sPrometheusCRDAlertingScore {
|
||||||
|
K8sPrometheusCRDAlertingScore {
|
||||||
|
sender,
|
||||||
|
receivers: receivers.unwrap_or_default(),
|
||||||
|
service_monitors: vec![],
|
||||||
|
prometheus_rules: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
||||||
@@ -110,15 +175,23 @@ impl K8sAnywhereTopology {
|
|||||||
K3DInstallationScore::default()
|
K3DInstallationScore::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn try_install_k3d(&self) -> Result<(), InterpretError> {
|
async fn try_install_k3d(&self) -> Result<(), PreparationError> {
|
||||||
self.get_k3d_installation_score()
|
let result = self
|
||||||
.create_interpret()
|
.get_k3d_installation_score()
|
||||||
.execute(&Inventory::empty(), self)
|
.interpret(&Inventory::empty(), self)
|
||||||
.await?;
|
.await;
|
||||||
Ok(())
|
|
||||||
|
match result {
|
||||||
|
Ok(outcome) => match outcome.status {
|
||||||
|
InterpretStatus::SUCCESS => Ok(()),
|
||||||
|
InterpretStatus::NOOP => Ok(()),
|
||||||
|
_ => Err(PreparationError::new(outcome.message)),
|
||||||
|
},
|
||||||
|
Err(err) => Err(PreparationError::new(err.to_string())),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, InterpretError> {
|
async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, PreparationError> {
|
||||||
let k8s_anywhere_config = &self.config;
|
let k8s_anywhere_config = &self.config;
|
||||||
|
|
||||||
// TODO this deserves some refactoring, it is becoming a bit hard to figure out
|
// TODO this deserves some refactoring, it is becoming a bit hard to figure out
|
||||||
@@ -128,16 +201,16 @@ impl K8sAnywhereTopology {
|
|||||||
} else {
|
} else {
|
||||||
if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig {
|
if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig {
|
||||||
debug!("Loading kubeconfig {kubeconfig}");
|
debug!("Loading kubeconfig {kubeconfig}");
|
||||||
match self.try_load_kubeconfig(&kubeconfig).await {
|
match self.try_load_kubeconfig(kubeconfig).await {
|
||||||
Some(client) => {
|
Some(client) => {
|
||||||
return Ok(Some(K8sState {
|
return Ok(Some(K8sState {
|
||||||
client: Arc::new(client),
|
client: Arc::new(client),
|
||||||
_source: K8sSource::Kubeconfig,
|
source: K8sSource::Kubeconfig,
|
||||||
message: format!("Loaded k8s client from kubeconfig {kubeconfig}"),
|
message: format!("Loaded k8s client from kubeconfig {kubeconfig}"),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
return Err(InterpretError::new(format!(
|
return Err(PreparationError::new(format!(
|
||||||
"Failed to load kubeconfig from {kubeconfig}"
|
"Failed to load kubeconfig from {kubeconfig}"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -174,7 +247,7 @@ impl K8sAnywhereTopology {
|
|||||||
let state = match k3d.get_client().await {
|
let state = match k3d.get_client().await {
|
||||||
Ok(client) => K8sState {
|
Ok(client) => K8sState {
|
||||||
client: Arc::new(K8sClient::new(client)),
|
client: Arc::new(K8sClient::new(client)),
|
||||||
_source: K8sSource::LocalK3d,
|
source: K8sSource::LocalK3d,
|
||||||
message: "K8s client ready".to_string(),
|
message: "K8s client ready".to_string(),
|
||||||
},
|
},
|
||||||
Err(_) => todo!(),
|
Err(_) => todo!(),
|
||||||
@@ -183,15 +256,21 @@ impl K8sAnywhereTopology {
|
|||||||
Ok(Some(state))
|
Ok(Some(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_k8s_tenant_manager(&self) -> Result<(), String> {
|
async fn ensure_k8s_tenant_manager(&self, k8s_state: &K8sState) -> Result<(), String> {
|
||||||
if let Some(_) = self.tenant_manager.get() {
|
if self.tenant_manager.get().is_some() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tenant_manager
|
self.tenant_manager
|
||||||
.get_or_try_init(async || -> Result<K8sTenantManager, String> {
|
.get_or_try_init(async || -> Result<K8sTenantManager, String> {
|
||||||
let k8s_client = self.k8s_client().await?;
|
let k8s_client = self.k8s_client().await?;
|
||||||
Ok(K8sTenantManager::new(k8s_client))
|
let network_policy_strategy: Box<dyn NetworkPolicyStrategy> = match k8s_state.source
|
||||||
|
{
|
||||||
|
K8sSource::LocalK3d => Box::new(K3dNetworkPolicyStrategy::new()),
|
||||||
|
K8sSource::Kubeconfig => Box::new(NoopNetworkPolicyStrategy::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(K8sTenantManager::new(k8s_client, network_policy_strategy))
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -206,6 +285,55 @@ impl K8sAnywhereTopology {
|
|||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ensure_prometheus_operator(
|
||||||
|
&self,
|
||||||
|
sender: &CRDPrometheus,
|
||||||
|
) -> Result<PreparationOutcome, PreparationError> {
|
||||||
|
let status = Command::new("sh")
|
||||||
|
.args(["-c", "kubectl get crd -A | grep -i prometheuses"])
|
||||||
|
.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 => {
|
||||||
|
debug!("installing prometheus operator");
|
||||||
|
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 prometheus operator".into(),
|
||||||
|
}),
|
||||||
|
InterpretStatus::NOOP => Ok(PreparationOutcome::Noop),
|
||||||
|
_ => Err(PreparationError::new(
|
||||||
|
"failed to install prometheus operator (unknown error)".into(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
Err(err) => Err(PreparationError::new(err.to_string())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
K8sSource::Kubeconfig => {
|
||||||
|
debug!("unable to install prometheus operator, contact cluster admin");
|
||||||
|
return Ok(PreparationOutcome::Noop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Unable to detect k8s_state. Skipping Prometheus Operator install.");
|
||||||
|
return Ok(PreparationOutcome::Noop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Prometheus operator is already present, skipping install");
|
||||||
|
|
||||||
|
Ok(PreparationOutcome::Success {
|
||||||
|
details: "prometheus operator present in cluster".into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -263,26 +391,25 @@ impl Topology for K8sAnywhereTopology {
|
|||||||
"K8sAnywhereTopology"
|
"K8sAnywhereTopology"
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
|
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
|
||||||
let k8s_state = self
|
let k8s_state = self
|
||||||
.k8s_state
|
.k8s_state
|
||||||
.get_or_try_init(|| self.try_get_or_install_k8s_client())
|
.get_or_try_init(|| self.try_get_or_install_k8s_client())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let k8s_state: &K8sState = k8s_state.as_ref().ok_or(InterpretError::new(
|
let k8s_state: &K8sState = k8s_state.as_ref().ok_or(PreparationError::new(
|
||||||
"No K8s client could be found or installed".to_string(),
|
"no K8s client could be found or installed".to_string(),
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
self.ensure_k8s_tenant_manager()
|
self.ensure_k8s_tenant_manager(k8s_state)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| InterpretError::new(e))?;
|
.map_err(PreparationError::new)?;
|
||||||
|
|
||||||
match self.is_helm_available() {
|
match self.is_helm_available() {
|
||||||
Ok(()) => Ok(Outcome::success(format!(
|
Ok(()) => Ok(PreparationOutcome::Success {
|
||||||
"{} + helm available",
|
details: format!("{} + helm available", k8s_state.message.clone()),
|
||||||
k8s_state.message.clone()
|
}),
|
||||||
))),
|
Err(e) => Err(PreparationError::new(format!("helm unavailable: {}", e))),
|
||||||
Err(e) => Err(InterpretError::new(format!("helm unavailable: {}", e))),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ use async_trait::async_trait;
|
|||||||
use log::debug;
|
use log::debug;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use super::{IpAddress, LogicalHost};
|
use super::LogicalHost;
|
||||||
use crate::executors::ExecutorError;
|
use crate::executors::ExecutorError;
|
||||||
|
use harmony_types::net::IpAddress;
|
||||||
|
|
||||||
impl std::fmt::Debug for dyn LoadBalancer {
|
impl std::fmt::Debug for dyn LoadBalancer {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
|
|
||||||
use crate::interpret::{InterpretError, Outcome};
|
use super::{HelmCommand, PreparationError, PreparationOutcome, Topology};
|
||||||
|
|
||||||
use super::{HelmCommand, Topology};
|
|
||||||
|
|
||||||
#[derive(new)]
|
#[derive(new)]
|
||||||
pub struct LocalhostTopology;
|
pub struct LocalhostTopology;
|
||||||
@@ -14,10 +12,10 @@ impl Topology for LocalhostTopology {
|
|||||||
"LocalHostTopology"
|
"LocalHostTopology"
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
|
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
|
||||||
Ok(Outcome::success(
|
Ok(PreparationOutcome::Success {
|
||||||
"Localhost is Chuck Norris, always ready.".to_string(),
|
details: "Localhost is Chuck Norris, always ready.".into(),
|
||||||
))
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod ha_cluster;
|
mod ha_cluster;
|
||||||
|
use harmony_types::net::IpAddress;
|
||||||
mod host_binding;
|
mod host_binding;
|
||||||
mod http;
|
mod http;
|
||||||
pub mod installable;
|
pub mod installable;
|
||||||
@@ -6,6 +7,7 @@ mod k8s_anywhere;
|
|||||||
mod localhost;
|
mod localhost;
|
||||||
pub mod oberservability;
|
pub mod oberservability;
|
||||||
pub mod tenant;
|
pub mod tenant;
|
||||||
|
use derive_new::new;
|
||||||
pub use k8s_anywhere::*;
|
pub use k8s_anywhere::*;
|
||||||
pub use localhost::*;
|
pub use localhost::*;
|
||||||
pub mod k8s;
|
pub mod k8s;
|
||||||
@@ -26,9 +28,11 @@ pub use tftp::*;
|
|||||||
mod helm_command;
|
mod helm_command;
|
||||||
pub use helm_command::*;
|
pub use helm_command::*;
|
||||||
|
|
||||||
use std::net::IpAddr;
|
use super::{
|
||||||
|
executors::ExecutorError,
|
||||||
use super::interpret::{InterpretError, Outcome};
|
instrumentation::{self, HarmonyEvent},
|
||||||
|
};
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
/// Represents a logical view of an infrastructure environment providing specific capabilities.
|
/// Represents a logical view of an infrastructure environment providing specific capabilities.
|
||||||
///
|
///
|
||||||
@@ -57,9 +61,128 @@ pub trait Topology: Send + Sync {
|
|||||||
/// * **Internal Orchestration:** For complex topologies, this method might manage dependencies on other sub-topologies, ensuring *their* `ensure_ready` is called first. Using nested `Maestros` to run setup `Scores` against these sub-topologies is the recommended pattern for non-trivial bootstrapping, allowing reuse of Harmony's core orchestration logic.
|
/// * **Internal Orchestration:** For complex topologies, this method might manage dependencies on other sub-topologies, ensuring *their* `ensure_ready` is called first. Using nested `Maestros` to run setup `Scores` against these sub-topologies is the recommended pattern for non-trivial bootstrapping, allowing reuse of Harmony's core orchestration logic.
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// - `Ok(Outcome)`: Indicates the topology is now ready. The `Outcome` status might be `SUCCESS` if actions were taken, or `NOOP` if it was already ready. The message should provide context.
|
/// - `Ok(PreparationOutcome)`: Indicates the topology is now ready. The `Outcome` status might be `SUCCESS` if actions were taken, or `NOOP` if it was already ready. The message should provide context.
|
||||||
/// - `Err(TopologyError)`: Indicates the topology could not reach a ready state due to configuration issues, discovery failures, bootstrap errors, or unsupported environments.
|
/// - `Err(PreparationError)`: Indicates the topology could not reach a ready state due to configuration issues, discovery failures, bootstrap errors, or unsupported environments.
|
||||||
async fn ensure_ready(&self) -> Result<Outcome, InterpretError>;
|
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum PreparationOutcome {
|
||||||
|
Success { details: String },
|
||||||
|
Noop,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, new)]
|
||||||
|
pub struct PreparationError {
|
||||||
|
msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for PreparationError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&self.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for PreparationError {}
|
||||||
|
|
||||||
|
impl From<ExecutorError> for PreparationError {
|
||||||
|
fn from(value: ExecutorError) -> Self {
|
||||||
|
Self {
|
||||||
|
msg: format!("InterpretError : {value}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<kube::Error> for PreparationError {
|
||||||
|
fn from(value: kube::Error) -> Self {
|
||||||
|
Self {
|
||||||
|
msg: format!("PreparationError : {value}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for PreparationError {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self {
|
||||||
|
msg: format!("PreparationError : {value}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum TopologyStatus {
|
||||||
|
Queued,
|
||||||
|
Preparing,
|
||||||
|
Success,
|
||||||
|
Noop,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TopologyState {
|
||||||
|
pub topology: String,
|
||||||
|
pub status: TopologyStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TopologyState {
|
||||||
|
pub fn new(topology: String) -> Self {
|
||||||
|
let instance = Self {
|
||||||
|
topology,
|
||||||
|
status: TopologyStatus::Queued,
|
||||||
|
};
|
||||||
|
|
||||||
|
instrumentation::instrument(HarmonyEvent::TopologyStateChanged {
|
||||||
|
topology: instance.topology.clone(),
|
||||||
|
status: instance.status.clone(),
|
||||||
|
message: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
instance
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare(&mut self) {
|
||||||
|
self.status = TopologyStatus::Preparing;
|
||||||
|
|
||||||
|
instrumentation::instrument(HarmonyEvent::TopologyStateChanged {
|
||||||
|
topology: self.topology.clone(),
|
||||||
|
status: self.status.clone(),
|
||||||
|
message: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn success(&mut self, message: String) {
|
||||||
|
self.status = TopologyStatus::Success;
|
||||||
|
|
||||||
|
instrumentation::instrument(HarmonyEvent::TopologyStateChanged {
|
||||||
|
topology: self.topology.clone(),
|
||||||
|
status: self.status.clone(),
|
||||||
|
message: Some(message),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn noop(&mut self) {
|
||||||
|
self.status = TopologyStatus::Noop;
|
||||||
|
|
||||||
|
instrumentation::instrument(HarmonyEvent::TopologyStateChanged {
|
||||||
|
topology: self.topology.clone(),
|
||||||
|
status: self.status.clone(),
|
||||||
|
message: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(&mut self, message: String) {
|
||||||
|
self.status = TopologyStatus::Error;
|
||||||
|
|
||||||
|
instrumentation::instrument(HarmonyEvent::TopologyStateChanged {
|
||||||
|
topology: self.topology.clone(),
|
||||||
|
status: self.status.clone(),
|
||||||
|
message: Some(message),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -73,35 +196,6 @@ pub trait MultiTargetTopology: Topology {
|
|||||||
fn current_target(&self) -> DeploymentTarget;
|
fn current_target(&self) -> DeploymentTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type IpAddress = IpAddr;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Url {
|
|
||||||
LocalFolder(String),
|
|
||||||
Url(url::Url),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for Url {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Url::LocalFolder(path) => serializer.serialize_str(path),
|
|
||||||
Url::Url(url) => serializer.serialize_str(&url.as_str()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Url {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Url::LocalFolder(path) => write!(f, "{}", path),
|
|
||||||
Url::Url(url) => write!(f, "{}", url),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a logical member of a cluster that provides one or more services.
|
/// Represents a logical member of a cluster that provides one or more services.
|
||||||
///
|
///
|
||||||
/// A LogicalHost can represent various roles within the infrastructure, such as:
|
/// A LogicalHost can represent various roles within the infrastructure, such as:
|
||||||
@@ -140,7 +234,8 @@ impl LogicalHost {
|
|||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use std::str::FromStr;
|
/// use std::str::FromStr;
|
||||||
/// use harmony::topology::{IpAddress, LogicalHost};
|
/// use harmony::topology::{LogicalHost};
|
||||||
|
/// use harmony_types::net::IpAddress;
|
||||||
///
|
///
|
||||||
/// let start_ip = IpAddress::from_str("192.168.0.20").unwrap();
|
/// let start_ip = IpAddress::from_str("192.168.0.20").unwrap();
|
||||||
/// let hosts = LogicalHost::create_hosts(3, start_ip, "worker");
|
/// let hosts = LogicalHost::create_hosts(3, start_ip, "worker");
|
||||||
@@ -196,7 +291,7 @@ fn increment_ip(ip: IpAddress, increment: u32) -> Option<IpAddress> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use harmony_types::net::Url;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use std::{net::Ipv4Addr, str::FromStr, sync::Arc};
|
use std::{net::Ipv4Addr, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_types::net::MacAddress;
|
use harmony_types::net::{IpAddress, MacAddress};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::executors::ExecutorError;
|
use crate::executors::ExecutorError;
|
||||||
|
|
||||||
use super::{IpAddress, LogicalHost, k8s::K8sClient};
|
use super::{LogicalHost, k8s::K8sClient};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DHCPStaticEntry {
|
pub struct DHCPStaticEntry {
|
||||||
@@ -46,16 +46,21 @@ pub trait K8sclient: Send + Sync {
|
|||||||
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>;
|
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PxeOptions {
|
||||||
|
pub ipxe_filename: String,
|
||||||
|
pub bios_filename: String,
|
||||||
|
pub efi_filename: String,
|
||||||
|
pub tftp_ip: Option<IpAddress>,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait DhcpServer: Send + Sync + std::fmt::Debug {
|
pub trait DhcpServer: Send + Sync + std::fmt::Debug {
|
||||||
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>;
|
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>;
|
||||||
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_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError>;
|
async fn set_pxe_options(&self, pxe_options: PxeOptions) -> Result<(), ExecutorError>;
|
||||||
async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError>;
|
async fn set_dhcp_range(&self, start: &IpAddress, end: &IpAddress)
|
||||||
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError>;
|
-> Result<(), ExecutorError>;
|
||||||
async fn set_filename64(&self, filename64: &str) -> Result<(), ExecutorError>;
|
|
||||||
async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError>;
|
|
||||||
fn get_ip(&self) -> IpAddress;
|
fn get_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,12 +1,15 @@
|
|||||||
|
use std::any::Any;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{Id, Version},
|
data::Version,
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
topology::{Topology, installable::Installable},
|
topology::{Topology, installable::Installable},
|
||||||
};
|
};
|
||||||
|
use harmony_types::id::Id;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait AlertSender: Send + Sync + std::fmt::Debug {
|
pub trait AlertSender: Send + Sync + std::fmt::Debug {
|
||||||
@@ -43,7 +46,7 @@ impl<S: AlertSender + Installable<T>, T: Topology> Interpret<T> for AlertingInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_name(&self) -> InterpretName {
|
fn get_name(&self) -> InterpretName {
|
||||||
todo!()
|
InterpretName::Alerting
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_version(&self) -> Version {
|
fn get_version(&self) -> Version {
|
||||||
@@ -62,7 +65,9 @@ impl<S: AlertSender + Installable<T>, T: Topology> Interpret<T> for AlertingInte
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait AlertReceiver<S: AlertSender>: std::fmt::Debug + Send + Sync {
|
pub trait AlertReceiver<S: AlertSender>: std::fmt::Debug + Send + Sync {
|
||||||
async fn install(&self, sender: &S) -> Result<Outcome, InterpretError>;
|
async fn install(&self, sender: &S) -> Result<Outcome, InterpretError>;
|
||||||
|
fn name(&self) -> String;
|
||||||
fn clone_box(&self) -> Box<dyn AlertReceiver<S>>;
|
fn clone_box(&self) -> Box<dyn AlertReceiver<S>>;
|
||||||
|
fn as_any(&self) -> &dyn Any;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -72,6 +77,6 @@ pub trait AlertRule<S: AlertSender>: std::fmt::Debug + Send + Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ScrapeTarger<S: AlertSender> {
|
pub trait ScrapeTarget<S: AlertSender> {
|
||||||
async fn install(&self, sender: &S) -> Result<(), InterpretError>;
|
async fn install(&self, sender: &S) -> Result<(), InterpretError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ pub struct UnmanagedRouter {
|
|||||||
|
|
||||||
impl Router for UnmanagedRouter {
|
impl Router for UnmanagedRouter {
|
||||||
fn get_gateway(&self) -> IpAddress {
|
fn get_gateway(&self) -> IpAddress {
|
||||||
self.gateway.clone()
|
self.gateway
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cidr(&self) -> Ipv4Cidr {
|
fn get_cidr(&self) -> Ipv4Cidr {
|
||||||
self.cidr.clone()
|
self.cidr
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_host(&self) -> LogicalHost {
|
fn get_host(&self) -> LogicalHost {
|
||||||
|
|||||||
@@ -15,36 +15,38 @@ use k8s_openapi::{
|
|||||||
apimachinery::pkg::util::intstr::IntOrString,
|
apimachinery::pkg::util::intstr::IntOrString,
|
||||||
};
|
};
|
||||||
use kube::Resource;
|
use kube::Resource;
|
||||||
use log::{debug, info, warn};
|
use log::debug;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
use super::{TenantConfig, TenantManager};
|
use super::{TenantConfig, TenantManager, network_policy::NetworkPolicyStrategy};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug)]
|
||||||
pub struct K8sTenantManager {
|
pub struct K8sTenantManager {
|
||||||
k8s_client: Arc<K8sClient>,
|
k8s_client: Arc<K8sClient>,
|
||||||
k8s_tenant_config: Arc<OnceCell<TenantConfig>>,
|
k8s_tenant_config: Arc<OnceCell<TenantConfig>>,
|
||||||
|
network_policy_strategy: Box<dyn NetworkPolicyStrategy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl K8sTenantManager {
|
impl K8sTenantManager {
|
||||||
pub fn new(client: Arc<K8sClient>) -> Self {
|
pub fn new(
|
||||||
|
client: Arc<K8sClient>,
|
||||||
|
network_policy_strategy: Box<dyn NetworkPolicyStrategy>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
k8s_client: client,
|
k8s_client: client,
|
||||||
k8s_tenant_config: Arc::new(OnceCell::new()),
|
k8s_tenant_config: Arc::new(OnceCell::new()),
|
||||||
|
network_policy_strategy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl K8sTenantManager {
|
|
||||||
fn get_namespace_name(&self, config: &TenantConfig) -> String {
|
fn get_namespace_name(&self, config: &TenantConfig) -> String {
|
||||||
config.name.clone()
|
config.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_constraints(&self, _namespace: &Namespace) -> Result<(), ExecutorError> {
|
fn ensure_constraints(&self, _namespace: &Namespace) -> Result<(), ExecutorError> {
|
||||||
warn!("Validate that when tenant already exists (by id) that name has not changed");
|
// TODO: Ensure constraints are applied to namespace (https://git.nationtech.io/NationTech/harmony/issues/98)
|
||||||
warn!("Make sure other Tenant constraints are respected by this k8s implementation");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,24 +221,6 @@ impl K8sTenantManager {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"to": [
|
|
||||||
{
|
|
||||||
"ipBlock": {
|
|
||||||
"cidr": "10.43.0.1/32",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"to": [
|
|
||||||
{
|
|
||||||
"ipBlock": {
|
|
||||||
"cidr": "172.23.0.0/16",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"to": [
|
"to": [
|
||||||
{
|
{
|
||||||
@@ -304,19 +288,19 @@ impl K8sTenantManager {
|
|||||||
let ports: Option<Vec<NetworkPolicyPort>> =
|
let ports: Option<Vec<NetworkPolicyPort>> =
|
||||||
c.1.as_ref().map(|spec| match &spec.data {
|
c.1.as_ref().map(|spec| match &spec.data {
|
||||||
super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort {
|
super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort {
|
||||||
port: Some(IntOrString::Int(port.clone().into())),
|
port: Some(IntOrString::Int((*port).into())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort {
|
super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort {
|
||||||
port: Some(IntOrString::Int(start.clone().into())),
|
port: Some(IntOrString::Int((*start).into())),
|
||||||
end_port: Some(end.clone().into()),
|
end_port: Some((*end).into()),
|
||||||
protocol: None, // Not currently supported by Harmony
|
protocol: None, // Not currently supported by Harmony
|
||||||
}],
|
}],
|
||||||
|
|
||||||
super::PortSpecData::ListOfPorts(items) => items
|
super::PortSpecData::ListOfPorts(items) => items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| NetworkPolicyPort {
|
.map(|i| NetworkPolicyPort {
|
||||||
port: Some(IntOrString::Int(i.clone().into())),
|
port: Some(IntOrString::Int((*i).into())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -361,19 +345,19 @@ impl K8sTenantManager {
|
|||||||
let ports: Option<Vec<NetworkPolicyPort>> =
|
let ports: Option<Vec<NetworkPolicyPort>> =
|
||||||
c.1.as_ref().map(|spec| match &spec.data {
|
c.1.as_ref().map(|spec| match &spec.data {
|
||||||
super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort {
|
super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort {
|
||||||
port: Some(IntOrString::Int(port.clone().into())),
|
port: Some(IntOrString::Int((*port).into())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort {
|
super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort {
|
||||||
port: Some(IntOrString::Int(start.clone().into())),
|
port: Some(IntOrString::Int((*start).into())),
|
||||||
end_port: Some(end.clone().into()),
|
end_port: Some((*end).into()),
|
||||||
protocol: None, // Not currently supported by Harmony
|
protocol: None, // Not currently supported by Harmony
|
||||||
}],
|
}],
|
||||||
|
|
||||||
super::PortSpecData::ListOfPorts(items) => items
|
super::PortSpecData::ListOfPorts(items) => items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| NetworkPolicyPort {
|
.map(|i| NetworkPolicyPort {
|
||||||
port: Some(IntOrString::Int(i.clone().into())),
|
port: Some(IntOrString::Int((*i).into())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -406,12 +390,27 @@ impl K8sTenantManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Clone for K8sTenantManager {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
k8s_client: self.k8s_client.clone(),
|
||||||
|
k8s_tenant_config: self.k8s_tenant_config.clone(),
|
||||||
|
network_policy_strategy: self.network_policy_strategy.clone_box(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TenantManager for K8sTenantManager {
|
impl TenantManager for K8sTenantManager {
|
||||||
async fn provision_tenant(&self, config: &TenantConfig) -> Result<(), ExecutorError> {
|
async fn provision_tenant(&self, config: &TenantConfig) -> Result<(), ExecutorError> {
|
||||||
let namespace = self.build_namespace(config)?;
|
let namespace = self.build_namespace(config)?;
|
||||||
let resource_quota = self.build_resource_quota(config)?;
|
let resource_quota = self.build_resource_quota(config)?;
|
||||||
|
|
||||||
let network_policy = self.build_network_policy(config)?;
|
let network_policy = self.build_network_policy(config)?;
|
||||||
|
let network_policy = self
|
||||||
|
.network_policy_strategy
|
||||||
|
.adjust_policy(network_policy, config);
|
||||||
|
|
||||||
let resource_limit_range = self.build_limit_range(config)?;
|
let resource_limit_range = self.build_limit_range(config)?;
|
||||||
|
|
||||||
self.ensure_constraints(&namespace)?;
|
self.ensure_constraints(&namespace)?;
|
||||||
@@ -428,13 +427,14 @@ impl TenantManager for K8sTenantManager {
|
|||||||
debug!("Creating network_policy for tenant {}", config.name);
|
debug!("Creating network_policy for tenant {}", config.name);
|
||||||
self.apply_resource(network_policy, config).await?;
|
self.apply_resource(network_policy, config).await?;
|
||||||
|
|
||||||
info!(
|
debug!(
|
||||||
"Success provisionning K8s tenant id {} name {}",
|
"Success provisionning K8s tenant id {} name {}",
|
||||||
config.id, config.name
|
config.id, config.name
|
||||||
);
|
);
|
||||||
self.store_config(config);
|
self.store_config(config);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_tenant_config(&self) -> Option<TenantConfig> {
|
async fn get_tenant_config(&self) -> Option<TenantConfig> {
|
||||||
self.k8s_tenant_config.get().cloned()
|
self.k8s_tenant_config.get().cloned()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
pub mod k8s;
|
pub mod k8s;
|
||||||
mod manager;
|
mod manager;
|
||||||
use std::str::FromStr;
|
pub mod network_policy;
|
||||||
|
|
||||||
|
use harmony_types::id::Id;
|
||||||
pub use manager::*;
|
pub use manager::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::str::FromStr;
|
||||||
use crate::data::Id;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] // Assuming serde for Scores
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] // Assuming serde for Scores
|
||||||
pub struct TenantConfig {
|
pub struct TenantConfig {
|
||||||
|
|||||||
120
harmony/src/domain/topology/tenant/network_policy.rs
Normal file
120
harmony/src/domain/topology/tenant/network_policy.rs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
use k8s_openapi::api::networking::v1::{
|
||||||
|
IPBlock, NetworkPolicy, NetworkPolicyEgressRule, NetworkPolicyPeer, NetworkPolicySpec,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::TenantConfig;
|
||||||
|
|
||||||
|
pub trait NetworkPolicyStrategy: Send + Sync + std::fmt::Debug {
|
||||||
|
fn clone_box(&self) -> Box<dyn NetworkPolicyStrategy>;
|
||||||
|
|
||||||
|
fn adjust_policy(&self, policy: NetworkPolicy, config: &TenantConfig) -> NetworkPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct NoopNetworkPolicyStrategy {}
|
||||||
|
|
||||||
|
impl NoopNetworkPolicyStrategy {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NoopNetworkPolicyStrategy {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkPolicyStrategy for NoopNetworkPolicyStrategy {
|
||||||
|
fn clone_box(&self) -> Box<dyn NetworkPolicyStrategy> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adjust_policy(&self, policy: NetworkPolicy, _config: &TenantConfig) -> NetworkPolicy {
|
||||||
|
policy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct K3dNetworkPolicyStrategy {}
|
||||||
|
|
||||||
|
impl K3dNetworkPolicyStrategy {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for K3dNetworkPolicyStrategy {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkPolicyStrategy for K3dNetworkPolicyStrategy {
|
||||||
|
fn clone_box(&self) -> Box<dyn NetworkPolicyStrategy> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adjust_policy(&self, policy: NetworkPolicy, _config: &TenantConfig) -> NetworkPolicy {
|
||||||
|
let mut egress = policy
|
||||||
|
.spec
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.egress
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default();
|
||||||
|
egress.push(NetworkPolicyEgressRule {
|
||||||
|
to: Some(vec![NetworkPolicyPeer {
|
||||||
|
ip_block: Some(IPBlock {
|
||||||
|
cidr: "172.18.0.0/16".into(), // TODO: query the IP range https://git.nationtech.io/NationTech/harmony/issues/108
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
NetworkPolicy {
|
||||||
|
spec: Some(NetworkPolicySpec {
|
||||||
|
egress: Some(egress),
|
||||||
|
..policy.spec.unwrap_or_default()
|
||||||
|
}),
|
||||||
|
..policy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use k8s_openapi::api::networking::v1::{
|
||||||
|
IPBlock, NetworkPolicy, NetworkPolicyEgressRule, NetworkPolicyPeer, NetworkPolicySpec,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{K3dNetworkPolicyStrategy, NetworkPolicyStrategy};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn should_add_ip_block_for_k3d_harmony_server() {
|
||||||
|
let strategy = K3dNetworkPolicyStrategy::new();
|
||||||
|
|
||||||
|
let policy =
|
||||||
|
strategy.adjust_policy(NetworkPolicy::default(), &super::TenantConfig::default());
|
||||||
|
|
||||||
|
let expected_policy = NetworkPolicy {
|
||||||
|
spec: Some(NetworkPolicySpec {
|
||||||
|
egress: Some(vec![NetworkPolicyEgressRule {
|
||||||
|
to: Some(vec![NetworkPolicyPeer {
|
||||||
|
ip_block: Some(IPBlock {
|
||||||
|
cidr: "172.18.0.0/16".into(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert_eq!(expected_policy, policy);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::executors::ExecutorError;
|
use crate::executors::ExecutorError;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use super::{IpAddress, Url};
|
use harmony_types::net::{IpAddress, Url};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait TftpServer: Send + Sync {
|
pub trait TftpServer: Send + Sync {
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use russh::{client, keys::key};
|
use russh::{client, keys::key};
|
||||||
|
|
||||||
use crate::{
|
use crate::domain::executors::{ExecutorError, SshClient};
|
||||||
domain::executors::{ExecutorError, SshClient},
|
|
||||||
topology::IpAddress,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use harmony_types::net::IpAddress;
|
||||||
pub struct RusshClient;
|
pub struct RusshClient;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::hardware::ManagementInterface;
|
use crate::hardware::ManagementInterface;
|
||||||
use crate::topology::IpAddress;
|
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
|
use harmony_types::net::IpAddress;
|
||||||
use harmony_types::net::MacAddress;
|
use harmony_types::net::MacAddress;
|
||||||
use log::info;
|
use log::info;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|||||||
17
harmony/src/infra/inventory/mod.rs
Normal file
17
harmony/src/infra/inventory/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use crate::{
|
||||||
|
config::DATABASE_URL,
|
||||||
|
infra::inventory::sqlite::SqliteInventoryRepository,
|
||||||
|
inventory::{InventoryRepository, RepoError},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod sqlite;
|
||||||
|
|
||||||
|
pub struct InventoryRepositoryFactory;
|
||||||
|
|
||||||
|
impl InventoryRepositoryFactory {
|
||||||
|
pub async fn build() -> Result<Box<dyn InventoryRepository>, RepoError> {
|
||||||
|
Ok(Box::new(
|
||||||
|
SqliteInventoryRepository::new(&(*DATABASE_URL)).await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
148
harmony/src/infra/inventory/sqlite.rs
Normal file
148
harmony/src/infra/inventory/sqlite.rs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
use crate::{
|
||||||
|
hardware::PhysicalHost,
|
||||||
|
inventory::{HostRole, InventoryRepository, RepoError},
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use harmony_types::id::Id;
|
||||||
|
use log::info;
|
||||||
|
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||||
|
|
||||||
|
/// A thread-safe, connection-pooled repository using SQLite.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SqliteInventoryRepository {
|
||||||
|
pool: Pool<Sqlite>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqliteInventoryRepository {
|
||||||
|
pub async fn new(database_url: &str) -> Result<Self, RepoError> {
|
||||||
|
let pool = SqlitePool::connect(database_url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| RepoError::ConnectionFailed(e.to_string()))?;
|
||||||
|
|
||||||
|
info!("SQLite inventory repository initialized at '{database_url}'");
|
||||||
|
Ok(Self { pool })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl InventoryRepository for SqliteInventoryRepository {
|
||||||
|
async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError> {
|
||||||
|
let data = serde_json::to_vec(host).map_err(|e| RepoError::Serialization(e.to_string()))?;
|
||||||
|
|
||||||
|
let id = Id::default().to_string();
|
||||||
|
let host_id = host.id.to_string();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO physical_hosts (id, version_id, data) VALUES (?, ?, ?)",
|
||||||
|
host_id,
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("Saved new inventory version for host '{}'", host.id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError> {
|
||||||
|
let row = sqlx::query_as!(
|
||||||
|
DbHost,
|
||||||
|
r#"SELECT id, version_id, data as "data: Json<PhysicalHost>" FROM physical_hosts WHERE id = ? ORDER BY version_id DESC LIMIT 1"#,
|
||||||
|
host_id
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
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;
|
||||||
|
struct DbHost {
|
||||||
|
data: Json<PhysicalHost>,
|
||||||
|
id: String,
|
||||||
|
version_id: String,
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
pub mod executors;
|
pub mod executors;
|
||||||
pub mod hp_ilo;
|
pub mod hp_ilo;
|
||||||
pub mod intel_amt;
|
pub mod intel_amt;
|
||||||
|
pub mod inventory;
|
||||||
pub mod opnsense;
|
pub mod opnsense;
|
||||||
|
mod sqlx;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_types::net::MacAddress;
|
use harmony_types::net::MacAddress;
|
||||||
use log::debug;
|
use log::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
executors::ExecutorError,
|
executors::ExecutorError,
|
||||||
topology::{DHCPStaticEntry, DhcpServer, IpAddress, LogicalHost},
|
topology::{DHCPStaticEntry, DhcpServer, LogicalHost, PxeOptions},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::OPNSenseFirewall;
|
use super::OPNSenseFirewall;
|
||||||
|
use harmony_types::net::IpAddress;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl DhcpServer for OPNSenseFirewall {
|
impl DhcpServer for OPNSenseFirewall {
|
||||||
@@ -26,7 +27,7 @@ impl DhcpServer for OPNSenseFirewall {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Registered {:?}", entry);
|
info!("Registered {:?}", entry);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,57 +47,40 @@ impl DhcpServer for OPNSenseFirewall {
|
|||||||
self.host.clone()
|
self.host.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> {
|
async fn set_pxe_options(&self, options: PxeOptions) -> Result<(), ExecutorError> {
|
||||||
let ipv4 = match ip {
|
let mut writable_opnsense = self.opnsense_config.write().await;
|
||||||
std::net::IpAddr::V4(ipv4_addr) => ipv4_addr,
|
let PxeOptions {
|
||||||
std::net::IpAddr::V6(_) => todo!("ipv6 not supported yet"),
|
ipxe_filename,
|
||||||
};
|
bios_filename,
|
||||||
{
|
efi_filename,
|
||||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
tftp_ip,
|
||||||
writable_opnsense.dhcp().set_next_server(ipv4);
|
} = options;
|
||||||
debug!("OPNsense dhcp server set next server {ipv4}");
|
writable_opnsense
|
||||||
}
|
.dhcp()
|
||||||
|
.set_pxe_options(
|
||||||
Ok(())
|
tftp_ip.map(|i| i.to_string()),
|
||||||
|
bios_filename,
|
||||||
|
efi_filename,
|
||||||
|
ipxe_filename,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|dhcp_error| {
|
||||||
|
ExecutorError::UnexpectedError(format!("Failed to set_pxe_options : {dhcp_error}"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> {
|
async fn set_dhcp_range(
|
||||||
{
|
&self,
|
||||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
start: &IpAddress,
|
||||||
writable_opnsense.dhcp().set_boot_filename(boot_filename);
|
end: &IpAddress,
|
||||||
debug!("OPNsense dhcp server set boot filename {boot_filename}");
|
) -> Result<(), ExecutorError> {
|
||||||
}
|
let mut writable_opnsense = self.opnsense_config.write().await;
|
||||||
|
writable_opnsense
|
||||||
Ok(())
|
.dhcp()
|
||||||
}
|
.set_dhcp_range(&start.to_string(), &end.to_string())
|
||||||
|
.await
|
||||||
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> {
|
.map_err(|dhcp_error| {
|
||||||
{
|
ExecutorError::UnexpectedError(format!("Failed to set_dhcp_range : {dhcp_error}"))
|
||||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
})
|
||||||
writable_opnsense.dhcp().set_filename(filename);
|
|
||||||
debug!("OPNsense dhcp server set filename {filename}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_filename64(&self, filename: &str) -> Result<(), ExecutorError> {
|
|
||||||
{
|
|
||||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
|
||||||
writable_opnsense.dhcp().set_filename64(filename);
|
|
||||||
debug!("OPNsense dhcp server set filename {filename}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError> {
|
|
||||||
{
|
|
||||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
|
||||||
writable_opnsense.dhcp().set_filenameipxe(filenameipxe);
|
|
||||||
debug!("OPNsense dhcp server set filenameipxe {filenameipxe}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use crate::infra::opnsense::Host;
|
use crate::infra::opnsense::Host;
|
||||||
use crate::infra::opnsense::IpAddress;
|
|
||||||
use crate::infra::opnsense::LogicalHost;
|
use crate::infra::opnsense::LogicalHost;
|
||||||
use crate::{
|
use crate::{
|
||||||
executors::ExecutorError,
|
executors::ExecutorError,
|
||||||
topology::{DnsRecord, DnsServer},
|
topology::{DnsRecord, DnsServer},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use harmony_types::net::IpAddress;
|
||||||
|
|
||||||
use super::OPNSenseFirewall;
|
use super::OPNSenseFirewall;
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ impl DnsServer for OPNSenseFirewall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_ip(&self) -> IpAddress {
|
fn get_ip(&self) -> IpAddress {
|
||||||
OPNSenseFirewall::get_ip(&self)
|
OPNSenseFirewall::get_ip(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_host(&self) -> LogicalHost {
|
fn get_host(&self) -> LogicalHost {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
executors::ExecutorError,
|
executors::ExecutorError,
|
||||||
topology::{Firewall, FirewallRule, IpAddress, LogicalHost},
|
topology::{Firewall, FirewallRule, LogicalHost},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::OPNSenseFirewall;
|
use super::OPNSenseFirewall;
|
||||||
|
use harmony_types::net::IpAddress;
|
||||||
|
|
||||||
impl Firewall for OPNSenseFirewall {
|
impl Firewall for OPNSenseFirewall {
|
||||||
fn add_rule(&mut self, _rule: FirewallRule) -> Result<(), ExecutorError> {
|
fn add_rule(&mut self, _rule: FirewallRule) -> Result<(), ExecutorError> {
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{data::FileContent, executors::ExecutorError, topology::HttpServer};
|
||||||
executors::ExecutorError,
|
|
||||||
topology::{HttpServer, IpAddress, Url},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::OPNSenseFirewall;
|
use super::OPNSenseFirewall;
|
||||||
|
use harmony_types::net::IpAddress;
|
||||||
|
use harmony_types::net::Url;
|
||||||
|
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(&self, url: &Url) -> Result<(), ExecutorError> {
|
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> {
|
||||||
let http_root_path = "/usr/local/http";
|
|
||||||
|
|
||||||
let config = self.opnsense_config.read().await;
|
let config = self.opnsense_config.read().await;
|
||||||
info!("Uploading files from url {url} to {http_root_path}");
|
info!("Uploading files from url {url} to {OPNSENSE_HTTP_ROOT_PATH}");
|
||||||
match url {
|
match url {
|
||||||
Url::LocalFolder(path) => {
|
Url::LocalFolder(path) => {
|
||||||
config
|
config
|
||||||
.upload_files(path, http_root_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()))?;
|
||||||
}
|
}
|
||||||
@@ -27,8 +25,29 @@ impl HttpServer for OPNSenseFirewall {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> {
|
||||||
|
let path = match &file.path {
|
||||||
|
crate::data::FilePath::Relative(path) => {
|
||||||
|
format!("{OPNSENSE_HTTP_ROOT_PATH}/{}", path.to_string())
|
||||||
|
}
|
||||||
|
crate::data::FilePath::Absolute(path) => {
|
||||||
|
return Err(ExecutorError::ConfigurationError(format!(
|
||||||
|
"Cannot serve file from http server with absolute path : {path}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = self.opnsense_config.read().await;
|
||||||
|
info!("Uploading file content to {}", path);
|
||||||
|
config
|
||||||
|
.upload_file_content(&path, &file.content)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn get_ip(&self) -> IpAddress {
|
fn get_ip(&self) -> IpAddress {
|
||||||
todo!();
|
OPNSenseFirewall::get_ip(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn commit_config(&self) -> Result<(), ExecutorError> {
|
async fn commit_config(&self) -> Result<(), ExecutorError> {
|
||||||
@@ -48,7 +67,7 @@ impl HttpServer for OPNSenseFirewall {
|
|||||||
async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
|
async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
|
||||||
let mut config = self.opnsense_config.write().await;
|
let mut config = self.opnsense_config.write().await;
|
||||||
let caddy = config.caddy();
|
let caddy = config.caddy();
|
||||||
if let None = caddy.get_full_config() {
|
if caddy.get_full_config().is_none() {
|
||||||
info!("Http config not available in opnsense config, installing package");
|
info!("Http config not available in opnsense config, installing package");
|
||||||
config.install_package("os-caddy").await.map_err(|e| {
|
config.install_package("os-caddy").await.map_err(|e| {
|
||||||
ExecutorError::UnexpectedError(format!(
|
ExecutorError::UnexpectedError(format!(
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ use uuid::Uuid;
|
|||||||
use crate::{
|
use crate::{
|
||||||
executors::ExecutorError,
|
executors::ExecutorError,
|
||||||
topology::{
|
topology::{
|
||||||
BackendServer, HealthCheck, HttpMethod, HttpStatusCode, IpAddress, LoadBalancer,
|
BackendServer, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService,
|
||||||
LoadBalancerService, LogicalHost,
|
LogicalHost,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use harmony_types::net::IpAddress;
|
||||||
|
|
||||||
use super::OPNSenseFirewall;
|
use super::OPNSenseFirewall;
|
||||||
|
|
||||||
@@ -121,10 +122,12 @@ pub(crate) fn haproxy_xml_config_to_harmony_loadbalancer(
|
|||||||
|
|
||||||
LoadBalancerService {
|
LoadBalancerService {
|
||||||
backend_servers,
|
backend_servers,
|
||||||
listening_port: frontend.bind.parse().expect(&format!(
|
listening_port: frontend.bind.parse().unwrap_or_else(|_| {
|
||||||
"HAProxy frontend address should be a valid SocketAddr, got {}",
|
panic!(
|
||||||
frontend.bind
|
"HAProxy frontend address should be a valid SocketAddr, got {}",
|
||||||
)),
|
frontend.bind
|
||||||
|
)
|
||||||
|
}),
|
||||||
health_check,
|
health_check,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -167,28 +170,28 @@ pub(crate) fn get_health_check_for_backend(
|
|||||||
None => return None,
|
None => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let haproxy_health_check = match haproxy
|
let haproxy_health_check = haproxy
|
||||||
.healthchecks
|
.healthchecks
|
||||||
.healthchecks
|
.healthchecks
|
||||||
.iter()
|
.iter()
|
||||||
.find(|h| &h.uuid == health_check_uuid)
|
.find(|h| &h.uuid == health_check_uuid)?;
|
||||||
{
|
|
||||||
Some(health_check) => health_check,
|
|
||||||
None => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let binding = haproxy_health_check.health_check_type.to_uppercase();
|
let binding = haproxy_health_check.health_check_type.to_uppercase();
|
||||||
let uppercase = binding.as_str();
|
let uppercase = binding.as_str();
|
||||||
match uppercase {
|
match uppercase {
|
||||||
"TCP" => {
|
"TCP" => {
|
||||||
if let Some(checkport) = haproxy_health_check.checkport.content.as_ref() {
|
if let Some(checkport) = haproxy_health_check.checkport.content.as_ref() {
|
||||||
if checkport.len() > 0 {
|
if !checkport.is_empty() {
|
||||||
return Some(HealthCheck::TCP(Some(checkport.parse().expect(&format!(
|
return Some(HealthCheck::TCP(Some(checkport.parse().unwrap_or_else(
|
||||||
"HAProxy check port should be a valid port number, got {checkport}"
|
|_| {
|
||||||
)))));
|
panic!(
|
||||||
|
"HAProxy check port should be a valid port number, got {checkport}"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Some(HealthCheck::TCP(None));
|
Some(HealthCheck::TCP(None))
|
||||||
}
|
}
|
||||||
"HTTP" => {
|
"HTTP" => {
|
||||||
let path: String = haproxy_health_check
|
let path: String = haproxy_health_check
|
||||||
@@ -355,16 +358,13 @@ mod tests {
|
|||||||
|
|
||||||
// Create an HAProxy instance with servers
|
// Create an HAProxy instance with servers
|
||||||
let mut haproxy = HAProxy::default();
|
let mut haproxy = HAProxy::default();
|
||||||
let mut server = HAProxyServer::default();
|
let server = HAProxyServer {
|
||||||
server.uuid = "server1".to_string();
|
uuid: "server1".to_string(),
|
||||||
server.address = "192.168.1.1".to_string();
|
address: "192.168.1.1".to_string(),
|
||||||
server.port = 80;
|
port: 80,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
haproxy.servers.servers.push(server);
|
haproxy.servers.servers.push(server);
|
||||||
let mut server = HAProxyServer::default();
|
|
||||||
server.uuid = "server3".to_string();
|
|
||||||
server.address = "192.168.1.3".to_string();
|
|
||||||
server.port = 8080;
|
|
||||||
|
|
||||||
// Call the function
|
// Call the function
|
||||||
let result = get_servers_for_backend(&backend, &haproxy);
|
let result = get_servers_for_backend(&backend, &haproxy);
|
||||||
@@ -384,10 +384,12 @@ mod tests {
|
|||||||
let backend = HAProxyBackend::default();
|
let backend = HAProxyBackend::default();
|
||||||
// Create an HAProxy instance with servers
|
// Create an HAProxy instance with servers
|
||||||
let mut haproxy = HAProxy::default();
|
let mut haproxy = HAProxy::default();
|
||||||
let mut server = HAProxyServer::default();
|
let server = HAProxyServer {
|
||||||
server.uuid = "server1".to_string();
|
uuid: "server1".to_string(),
|
||||||
server.address = "192.168.1.1".to_string();
|
address: "192.168.1.1".to_string(),
|
||||||
server.port = 80;
|
port: 80,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
haproxy.servers.servers.push(server);
|
haproxy.servers.servers.push(server);
|
||||||
// Call the function
|
// Call the function
|
||||||
let result = get_servers_for_backend(&backend, &haproxy);
|
let result = get_servers_for_backend(&backend, &haproxy);
|
||||||
@@ -402,10 +404,12 @@ mod tests {
|
|||||||
backend.linked_servers.content = Some("server4,server5".to_string());
|
backend.linked_servers.content = Some("server4,server5".to_string());
|
||||||
// Create an HAProxy instance with servers
|
// Create an HAProxy instance with servers
|
||||||
let mut haproxy = HAProxy::default();
|
let mut haproxy = HAProxy::default();
|
||||||
let mut server = HAProxyServer::default();
|
let server = HAProxyServer {
|
||||||
server.uuid = "server1".to_string();
|
uuid: "server1".to_string(),
|
||||||
server.address = "192.168.1.1".to_string();
|
address: "192.168.1.1".to_string(),
|
||||||
server.port = 80;
|
port: 80,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
haproxy.servers.servers.push(server);
|
haproxy.servers.servers.push(server);
|
||||||
// Call the function
|
// Call the function
|
||||||
let result = get_servers_for_backend(&backend, &haproxy);
|
let result = get_servers_for_backend(&backend, &haproxy);
|
||||||
@@ -416,20 +420,28 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_get_servers_for_backend_multiple_linked_servers() {
|
fn test_get_servers_for_backend_multiple_linked_servers() {
|
||||||
// Create a backend with multiple linked servers
|
// Create a backend with multiple linked servers
|
||||||
|
#[allow(clippy::field_reassign_with_default)]
|
||||||
let mut backend = HAProxyBackend::default();
|
let mut backend = HAProxyBackend::default();
|
||||||
backend.linked_servers.content = Some("server1,server2".to_string());
|
backend.linked_servers.content = Some("server1,server2".to_string());
|
||||||
|
//
|
||||||
// Create an HAProxy instance with matching servers
|
// Create an HAProxy instance with matching servers
|
||||||
let mut haproxy = HAProxy::default();
|
let mut haproxy = HAProxy::default();
|
||||||
let mut server = HAProxyServer::default();
|
let server = HAProxyServer {
|
||||||
server.uuid = "server1".to_string();
|
uuid: "server1".to_string(),
|
||||||
server.address = "some-hostname.test.mcd".to_string();
|
address: "some-hostname.test.mcd".to_string(),
|
||||||
server.port = 80;
|
port: 80,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
haproxy.servers.servers.push(server);
|
haproxy.servers.servers.push(server);
|
||||||
let mut server = HAProxyServer::default();
|
|
||||||
server.uuid = "server2".to_string();
|
let server = HAProxyServer {
|
||||||
server.address = "192.168.1.2".to_string();
|
uuid: "server2".to_string(),
|
||||||
server.port = 8080;
|
address: "192.168.1.2".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
haproxy.servers.servers.push(server);
|
haproxy.servers.servers.push(server);
|
||||||
|
|
||||||
// Call the function
|
// Call the function
|
||||||
let result = get_servers_for_backend(&backend, &haproxy);
|
let result = get_servers_for_backend(&backend, &haproxy);
|
||||||
// Check the result
|
// Check the result
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ pub use management::*;
|
|||||||
use opnsense_config_xml::Host;
|
use opnsense_config_xml::Host;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{executors::ExecutorError, topology::LogicalHost};
|
||||||
executors::ExecutorError,
|
use harmony_types::net::IpAddress;
|
||||||
topology::{IpAddress, LogicalHost},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct OPNSenseFirewall {
|
pub struct OPNSenseFirewall {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{executors::ExecutorError, topology::TftpServer};
|
||||||
executors::ExecutorError,
|
use harmony_types::net::IpAddress;
|
||||||
topology::{IpAddress, TftpServer, Url},
|
use harmony_types::net::Url;
|
||||||
};
|
|
||||||
|
|
||||||
use super::OPNSenseFirewall;
|
use super::OPNSenseFirewall;
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ impl TftpServer for OPNSenseFirewall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_ip(&self) -> IpAddress {
|
fn get_ip(&self) -> IpAddress {
|
||||||
todo!()
|
OPNSenseFirewall::get_ip(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError> {
|
async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError> {
|
||||||
@@ -58,7 +57,7 @@ impl TftpServer for OPNSenseFirewall {
|
|||||||
async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
|
async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
|
||||||
let mut config = self.opnsense_config.write().await;
|
let mut config = self.opnsense_config.write().await;
|
||||||
let tftp = config.tftp();
|
let tftp = config.tftp();
|
||||||
if let None = tftp.get_full_config() {
|
if tftp.get_full_config().is_none() {
|
||||||
info!("Tftp config not available in opnsense config, installing package");
|
info!("Tftp config not available in opnsense config, installing package");
|
||||||
config.install_package("os-tftp").await.map_err(|e| {
|
config.install_package("os-tftp").await.map_err(|e| {
|
||||||
ExecutorError::UnexpectedError(format!(
|
ExecutorError::UnexpectedError(format!(
|
||||||
|
|||||||
36
harmony/src/infra/sqlx.rs
Normal file
36
harmony/src/infra/sqlx.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use crate::inventory::RepoError;
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for RepoError {
|
||||||
|
fn from(value: sqlx::Error) -> Self {
|
||||||
|
match value {
|
||||||
|
sqlx::Error::Configuration(_)
|
||||||
|
| sqlx::Error::Io(_)
|
||||||
|
| sqlx::Error::Tls(_)
|
||||||
|
| sqlx::Error::Protocol(_)
|
||||||
|
| sqlx::Error::PoolTimedOut
|
||||||
|
| sqlx::Error::PoolClosed
|
||||||
|
| sqlx::Error::WorkerCrashed => RepoError::ConnectionFailed(value.to_string()),
|
||||||
|
sqlx::Error::InvalidArgument(_)
|
||||||
|
| sqlx::Error::Database(_)
|
||||||
|
| sqlx::Error::RowNotFound
|
||||||
|
| sqlx::Error::TypeNotFound { .. }
|
||||||
|
| sqlx::Error::ColumnIndexOutOfBounds { .. }
|
||||||
|
| sqlx::Error::ColumnNotFound(_)
|
||||||
|
| sqlx::Error::AnyDriverError(_)
|
||||||
|
| sqlx::Error::Migrate(_)
|
||||||
|
| sqlx::Error::InvalidSavePointStatement
|
||||||
|
| sqlx::Error::BeginFailed => RepoError::QueryFailed(value.to_string()),
|
||||||
|
sqlx::Error::Encode(_) => RepoError::Serialization(value.to_string()),
|
||||||
|
sqlx::Error::Decode(_) | sqlx::Error::ColumnDecode { .. } => {
|
||||||
|
RepoError::Deserialization(value.to_string())
|
||||||
|
}
|
||||||
|
_ => RepoError::QueryFailed(value.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for RepoError {
|
||||||
|
fn from(value: serde_json::Error) -> Self {
|
||||||
|
RepoError::Serialization(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user