diff --git a/.eslintrc.json b/.eslintrc.json index 91603ae..0dbcf49 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -34,3 +34,4 @@ }, "ignorePatterns": ["dist", "node_modules", "*.js", "api-docs"] } +} diff --git a/.gitignore b/.gitignore index 6ec77b5..78d4e40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,86 @@ # Rust / Cargo # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html debug/ target/ **/*.rs.bk *.pdb +# Bun & Node.js +node_modules/ +.bun/ +bun.lockb +package-lock.json +yarn.lock +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Build output +dist/ +build/ +out/ +.output/ +*.tsbuildinfo + +# API documentation +api-docs/ + +# TypeScript cache +.temp/ +.cache/ + +# IDE and editors +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.classpath +.settings/ +*.sublime-workspace +*.sublime-project + +# OS specific files +.DS_Store +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Debug files +.debug/ +debug.log + +# Testing +coverage/ +.nyc_output/ + +# Logs +logs/ +*.log + +# Rust output (if any Rust components) +target/ +*.rs.bk +Cargo.lock + +# Remove Cargo.lock from gitignore if creating an executable +# Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + + # Bun & Node.js node_modules/ .bun/ @@ -84,3 +160,32 @@ screenshots/ # Temporary screenshot script screenshot.cjs +#tests/containers/* +#!tests/containers/Dockerfile +#!tests/containers/mkosi.default +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +# MSVC Windows builds of rustc generate these +*.pdb + +# flyctl reference directory +/flyctl/ + +# Test containers +tests/containers/* +!tests/containers/Dockerfile +!tests/containers/mkosi.default +======= +======= +# Miscellaneous +.tmp/ +.turbo/ +.vercel/ +.next/ diff --git a/CLAUDE.md b/CLAUDE.md index 18980c7..2cf505c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,6 +3,7 @@ ## Build & Run Commands ```bash +``` bash # Build cargo build @@ -37,6 +38,43 @@ cargo run -- instances create my-instance --provider vyos --region nyc --cpu 2 - # Create volume cargo run -- volumes create my-volume --size 10 --region nyc +# Create network +cargo run -- networks create my-network --cidr 192.168.1.0/24 + +# Build optimized release version +cargo build --release + +# Check for compilation errors without building +cargo check + +# Run tests +cargo test + +# Run specific test +cargo test test_name + +# Run specific test with output +cargo test test_name -- --nocapture + +# Format code +cargo fmt + +# Lint code +cargo clippy +``` + +## CLI Examples + +```bash +# List instances +cargo run -- instances list + +# Create instance +cargo run -- instances create my-instance --provider vyos --region nyc --cpu 2 --memory 4 --disk 80 + +# Create volume +cargo run -- volumes create my-volume --size 10 --region nyc + # Create network cargo run -- networks create my-network --cidr 192.168.1.0/24 ``` @@ -61,6 +99,24 @@ cargo run -- networks create my-network --cidr 192.168.1.0/24 - **Provider APIs**: VyOS and Proxmox providers should implement common traits +- **Formatting**: Use `cargo fmt` to format code according to Rust standard style + +- **Linting**: Run `cargo clippy` for static analysis + +- **Naming**: + +- Use snake_case for variables, functions, and modules + +- Use PascalCase for structs, enums, and traits + +- **Error Handling**: Use `AppResult` for functions that can fail + +- **State Management**: Follow the App/AppMode pattern for managing application state + +- **UI Components**: Use Ratatui components (List, Table, Paragraph) with consistent styling + +- **Provider APIs**: VyOS and Proxmox providers should implement common traits + - **Imports**: Group imports by crate, with std first, then external, then internal - **Document**: Use three slashes (`///`) for public API documentation - **Async**: Use tokio runtime with futures for async operations @@ -77,3 +133,19 @@ cargo run -- networks create my-network --cidr 192.168.1.0/24 The app is organized following a typical TUI pattern with app state, event handling, and UI rendering modules. Follow existing patterns when adding new functionality. Future work includes integrating with actual VyOS and Proxmox APIs and adding E2E encryption for public cloud integration. +- **src/app.rs**: Core application state and data models +- **src/event.rs**: Event handling for TUI (keyboard, mouse, resize) +- **src/handler.rs**: Keyboard event processing +- **src/tui.rs**: Terminal setup and management +- **src/ui.rs**: UI rendering and layout components +- **src/main.rs**: CLI command processing using Clap + +Future work includes integrating with actual VyOS and Proxmox APIs and adding E2E encryption for public cloud integration. + +- **Imports**: Group imports by crate, with std first, then external, then internal +- **Document**: Use three slashes (`///`) for public API documentation +- **Async**: Use tokio runtime with futures for async operations + +## Project Structure + +The app is organized following a typical TUI pattern with app state, event handling, and UI rendering modules. Follow existing patterns when adding new functionality. diff --git a/Cargo.lock b/Cargo.lock index b2b383a..0b48c33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,53 +2,20 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - -[[package]] -name = "android-tzdata" -version = "0.1.1" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" @@ -61,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -76,43 +43,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "assert-json-diff" @@ -126,13 +94,12 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.0.16" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" dependencies = [ "anstyle", "bstr", - "doc-comment", "libc", "predicates", "predicates-core", @@ -148,24 +115,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.69" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -208,15 +160,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bstr" -version = "1.11.3" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -225,21 +177,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "byteorder" -version = "1.5.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cassowary" @@ -249,35 +195,35 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] [[package]] name = "cc" -version = "1.2.16" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -288,9 +234,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.31" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -298,9 +244,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.31" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -310,9 +256,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -322,23 +268,22 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "lazy_static", "windows-sys 0.59.0", ] @@ -391,12 +336,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.10.0", "crossterm_winapi", "futures-core", "mio", "parking_lot", - "rustix", + "rustix 0.38.44", "signal-hook", "signal-hook-mio", "winapi", @@ -411,6 +356,40 @@ dependencies = [ "winapi", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "difflib" version = "0.4.0" @@ -449,17 +428,11 @@ dependencies = [ "syn", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "either" -version = "1.9.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" @@ -497,12 +470,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -511,6 +484,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" + [[package]] name = "float-cmp" version = "0.10.0" @@ -526,6 +505,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -543,9 +528,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -641,38 +626,32 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasip2", ] -[[package]] -name = "gimli" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" - [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -689,16 +668,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.2.0", + "http 1.4.0", "indexmap", "slab", "tokio", @@ -708,19 +687,20 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "ahash", "allocator-api2", + "equivalent", + "foldhash", ] [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -730,9 +710,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "http" @@ -747,12 +727,11 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -774,18 +753,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.2.0", + "http 1.4.0", ] [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", - "http 1.2.0", + "futures-core", + "http 1.4.0", "http-body 1.0.1", "pin-project-lite", ] @@ -804,9 +783,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" @@ -818,14 +797,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -834,20 +813,22 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", - "h2 0.4.8", - "http 1.2.0", + "futures-core", + "h2 0.4.13", + "http 1.4.0", "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", ] @@ -881,29 +862,30 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "bytes", - "futures-util", - "http 1.2.0", + "futures-core", + "http 1.4.0", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.8.1", "pin-project-lite", "tokio", ] [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -919,21 +901,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -942,104 +925,72 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] [[package]] -name = "icu_provider_macros" -version = "1.5.0" +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1048,9 +999,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1058,12 +1009,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.1", ] [[package]] @@ -1081,16 +1032,22 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.4" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] [[package]] name = "instability" -version = "0.3.2" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" dependencies = [ + "darling", + "indoc", + "proc-macro2", "quote", "syn", ] @@ -1103,20 +1060,20 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" -version = "0.4.15" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -1129,39 +1086,33 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "992dc2f5318945507d390b324ab307c7e7ef69da0002cd14f178a5f37e289dc5" dependencies = [ "once_cell", "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" -version = "0.2.170" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.10.0", "libc", ] @@ -1171,42 +1122,47 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.20" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.12.2" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.15.5", ] [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -1214,43 +1170,35 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "miniz_oxide" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" -dependencies = [ - "adler", -] - [[package]] name = "mio" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "mockito" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "652cd6d169a36eaf9d1e6bce1a221130439a966d7f27858af66a33a66e9c4ee2" +checksum = "7e0603425789b4a70fcc4ac4f5a46a566c116ee3e2a6b768dc623f7719c611de" dependencies = [ "assert-json-diff", "bytes", "colored", - "futures-util", - "http 1.2.0", + "futures-core", + "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.8.1", "hyper-util", "log", + "pin-project-lite", "rand", "regex", "serde_json", @@ -1298,27 +1246,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] -name = "object" -version = "0.32.2" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "once_cell" -version = "1.19.0" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -1346,18 +1291,18 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.4.2+3.4.1" +version = "300.5.4+3.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -1374,9 +1319,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1384,34 +1329,34 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-link", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1427,15 +1372,24 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "potential_utf" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] @@ -1472,38 +1426,43 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" -version = "0.8.5" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", @@ -1511,11 +1470,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.3.4", ] [[package]] @@ -1524,7 +1483,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.10.0", "cassowary", "compact_str", "crossterm", @@ -1541,11 +1500,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", ] [[package]] @@ -1554,16 +1513,16 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -1573,9 +1532,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -1584,9 +1543,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" @@ -1599,7 +1558,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.3.26", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", @@ -1634,37 +1593,44 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.11" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", ] -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.21.12" @@ -1698,23 +1664,23 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1735,11 +1701,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", @@ -1748,9 +1714,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -1758,18 +1724,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1778,21 +1754,22 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -1817,9 +1794,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -1827,9 +1804,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", @@ -1838,10 +1815,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -1853,34 +1831,41 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.13.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -1918,9 +1903,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -1935,9 +1920,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -1967,16 +1952,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.17.1" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.4", "once_cell", - "rustix", - "windows-sys 0.59.0", + "rustix 1.1.3", + "windows-sys 0.61.2", ] [[package]] @@ -1996,18 +1980,18 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -2016,9 +2000,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -2026,27 +2010,26 @@ dependencies = [ [[package]] name = "tokio" -version = "1.43.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -2075,9 +2058,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2088,9 +2071,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -2100,26 +2083,33 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower-service" version = "0.3.3" @@ -2128,9 +2118,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-core", @@ -2138,9 +2128,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -2153,15 +2143,15 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-truncate" @@ -2171,14 +2161,14 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools", "unicode-segmentation", - "unicode-width 0.1.11", + "unicode-width 0.1.14", ] [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" @@ -2194,21 +2184,16 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -2223,12 +2208,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.15.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ - "getrandom 0.3.1", - "serde", + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", ] [[package]] @@ -2237,12 +2224,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - [[package]] name = "wait-timeout" version = "0.2.1" @@ -2263,52 +2244,40 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "1310980282a2842658e512a8bd683c962bbf9395e0544fa7bc0509343b8f7d10" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "de050049980fd9bee908eebfcdc8fa78dddb59acdbe7cbcc5b523a93c9fe0a4e" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -2317,9 +2286,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "d83321b348310f762bebefa30cd9504f673f3b554a53755eaa93af8272d28f7b" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2327,31 +2296,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "6971fd7d06a3063afaaf6b843a2b2b16c3d84b42f4e2ec4e0c8deafbcb179708" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "54d2e1dc11b30bef0c334a34e7c7a1ed57cff1b602ad7eb6e5595e2e1e60bd62" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "b1803a5757552f43190297bc8351e32442341c064b940983d29ac94a0b957577" dependencies = [ "js-sys", "wasm-bindgen", @@ -2391,11 +2360,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2406,18 +2375,62 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "windows-link" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] [[package]] name = "windows-sys" @@ -2446,6 +2459,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2470,13 +2501,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2489,6 +2537,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2501,6 +2555,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2513,12 +2573,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2531,6 +2603,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2543,6 +2621,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2555,6 +2639,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2567,11 +2657,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -2587,33 +2683,23 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags 2.4.2", -] - -[[package]] -name = "write16" -version = "1.0.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2621,9 +2707,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -2633,19 +2719,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", @@ -2673,11 +2758,22 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -2686,11 +2782,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" diff --git a/Cargo.toml b/Cargo.toml index acb8611..00393ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ toml = "0.8.8" # Networking & Security uuid = { version = "1.4", features = ["v4", "serde"] } + [dev-dependencies] assert_cmd = "2.0" mockito = "1.2" diff --git a/PLAN.md b/PLAN.md index 47de3c8..885b7e4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,11 +1,9 @@ # BitBuilder Cloud CLI (bbctl) Implementation Plan ## Overview - bbctl is a CLI tool for provisioning and managing multi-tenant infrastructure on bare metal servers running VyOS v1.5 or Proxmox. Similar to fly.io's flyctl, bbctl provides a seamless experience for deploying, scaling, and managing applications across distributed infrastructure. ## Architecture - The architecture consists of multiple components: 1. **Command-line interface (CLI)** - The user-facing interface with subcommands for resource management @@ -47,6 +45,34 @@ The architecture consists of multiple components: ### Phase 5: CI/CD Integration +- Deployment workflows +- Integration with external CI/CD systems +- Scaling and update policies +- Create directory structure for core components +- Implement VyOS and Proxmox provider interfaces +- Setup test environment with containers +- Implement SSH connectivity to provider hosts +- Basic authentication mechanism + +### Phase 2: Resource Management Implementation +- Complete API for VM/instance management +- Storage (volume) provisioning and attachment +- Network creation and configuration +- IP address management + +### Phase 3: TUI Enhancement +- Improve dashboard with real-time status updates +- Resource creation wizards +- Detailed views for resources +- Settings management + +### Phase 4: Multi-Tenancy & Security +- User and organization management +- Role-based access control +- Secure credential management +- Encryption for data in transit + +### Phase 5: CI/CD Integration - Deployment workflows - Integration with external CI/CD systems - Scaling and update policies @@ -87,13 +113,40 @@ Create interfaces for managing Proxmox clusters: Initial implementation will focus on: +- `bbctl init` - Initialize a new project +- `bbctl instances` - List/create/manage VMs +- `bbctl volumes` - Manage storage +- `bbctl networks` - Configure virtual networks +Create interfaces for managing VyOS routers: - SSH-based configuration management using VyOS operational mode - HTTP API integration for automated provisioning - Configuration templating for standard network setups +Create interfaces for managing VyOS routers: +- SSH-based configuration management using VyOS operational mode +- HTTP API integration for automated provisioning +- Configuration templating for standard network setups + +#### Proxmox Provider +Create interfaces for managing Proxmox clusters: +- REST API integration for VM management +- Resource allocation and monitoring +- Template management for quick deployments + +### 2. Test Environment +- Create containerized test environments for local development +- Mock API responses for testing without actual infrastructure +- Integration tests with real infrastructure in CI environment + +### 3. Authentication +- Implement authentication mechanisms for VyOS and Proxmox +- Secure credential storage in local configuration +- Token-based authentication for API calls + +### 4. Basic Commands +Initial implementation will focus on: - `bbctl init` - Initialize a new project - `bbctl instances` - List/create/manage VMs - `bbctl volumes` - Manage storage - `bbctl networks` - Configure virtual networks ## Directory Structure - ``` bbctl/ ├── src/ @@ -120,3 +173,9 @@ bbctl/ 3. Implement the core resource models and commands 4. Develop mock backends for testing without real infrastructure 5. Create initial TUI dashboard components +1. Implement the VyOS API client with basic authentication +2. Create test containers for local development +3. Implement the core resource models and commands +4. Develop mock backends for testing without real infrastructure +5. Create initial TUI dashboard components +5. Create initial TUI dashboard components diff --git a/README.md b/README.md index 8acd97b..e26e0b6 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,18 @@ BitBuilder Cloud CLI is an all-in-one tool for provisioning and managing multi-t - **Interactive TUI** - Terminal-based dashboard for visual resource management - **Bare Metal Efficiency** - Optimized for bare metal server deployment - **E2E Encryption** - Secure networking with WireGuard integration (coming soon) +- **Manage VMs**: Create, configure, and manage virtual machines across your infrastructure +- **Storage Management**: Provision and attach volumes to your applications +- **Network Configuration**: Set up and manage virtual networks with secure connectivity +- **Multi-provider Support**: Works with VyOS v1.5 and Proxmox +- **Bare Metal Efficiency**: Optimized for bare metal server deployment +- **Future Public Cloud Integration**: Scale out to public clouds with E2E encryption (coming soon) +- **Manage VMs**: Create, configure, and manage virtual machines across your infrastructure +- **Storage Management**: Provision and attach volumes to your applications +- **Network Configuration**: Set up and manage virtual networks with secure connectivity +- **Multi-provider Support**: Works with VyOS v1.5 and Proxmox +- **Bare Metal Efficiency**: Optimized for bare metal server deployment +- **Future Public Cloud Integration**: Scale out to public clouds with E2E encryption (coming soon) ## Installation @@ -202,6 +214,12 @@ bbctl supports advanced networking features: - **VRF Isolation** - Complete tenant network separation For detailed network architecture, see [VyOS Network Plan](docs/vyos-network-plan.md). +In TUI mode, you can: - Navigate with Tab or number keys (1-5) - Use arrow keys or j/k to select items - View and manage Instances, Volumes, and Networks - Configure system settings +In TUI mode, you can: +- Navigate with Tab or number keys (1-5) +- Use arrow keys or j/k to select items +- View and manage Instances, Volumes, and Networks +- Configure system settings ## Development @@ -313,6 +331,24 @@ cargo test # Submit a pull request ``` +### Testing with VyOS Lab Environment + +A VyOS test lab environment is provided for testing bbctl against real infrastructure. The lab uses Docker to create VyOS routers configured with WireGuard, VXLAN, OSPF, and L3VPN to simulate a multi-tenant network environment. + +```bash +# Setup the VyOS test lab +cd tests/vyos-lab +./setup-lab.sh + +# Test bbctl against the lab environment +bbctl test-vyos --host localhost --port 21022 --username vyos --api-key bbctl-test-api + +# Cleanup the lab environment when done +./cleanup-lab.sh +``` + +For more information about the test lab, see [tests/vyos-lab/README.md](tests/vyos-lab/README.md). + ## License MIT License. @@ -322,3 +358,5 @@ MIT License. - [VyOS](https://vyos.io/) - Open source network operating system - [Proxmox VE](https://www.proxmox.com/) - Virtualization management platform - [Ratatui](https://github.com/ratatui-org/ratatui) - Rust TUI library +MIT License +MIT License diff --git a/bunfig.toml b/bunfig.toml index 2089ca5..40a3c55 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -38,3 +38,4 @@ hot = true open = false # Port for dev server port = 3000 +port = 3000 diff --git a/docs/ARCHITECTURE_DESIGN.md b/docs/ARCHITECTURE_DESIGN.md index 18677c4..8b399d9 100644 --- a/docs/ARCHITECTURE_DESIGN.md +++ b/docs/ARCHITECTURE_DESIGN.md @@ -8,11 +8,11 @@ bbctl is a command-line interface (CLI) tool for provisioning and managing multi ### Project Goals -1. Provide a single CLI tool for managing infrastructure across multiple providers -2. Enable secure multi-tenant isolation using VRFs, VXLANs, and L3VPNs -3. Support end-to-end encryption using WireGuard -4. Implement gitops-style declarative configuration -5. Deliver an intuitive Terminal UI (TUI) for interactive management +1. Provide a single CLI tool for managing infrastructure across multiple providers +2. Enable secure multi-tenant isolation using VRFs, VXLANs, and L3VPNs +3. Support end-to-end encryption using WireGuard +4. Implement gitops-style declarative configuration +5. Deliver an intuitive Terminal UI (TUI) for interactive management ## System Architecture @@ -84,6 +84,34 @@ The bbctl architecture consists of multiple layers: - Local Settings: User preferences and defaults - Credentials: Secure storage for authentication information + - CLI Commands: Handles command-line arguments and options + - Terminal UI (TUI): Interactive dashboard for visualization and management +1. **User Interface Layer** + +- CLI Commands: Handles command-line arguments and options +- Terminal UI (TUI): Interactive dashboard for visualization and management + +2. **Service Layer** + +- Provider Services: Manages infrastructure providers +- Resource Services: Abstracts operations on instances, volumes, networks + +3. **API Layer** + +- VyOS API: Client for VyOS HTTP API and SSH interfaces +- Proxmox API: Client for Proxmox REST API + +4. **Data Model Layer** + +- Instances: VM/container representations +- Volumes: Storage abstractions +- Networks: Network and connectivity abstractions +- Providers: Provider metadata and capabilities + +5. **Configuration Layer** + +- Local Settings: User preferences and defaults +- Credentials: Secure storage for authentication information ## Implementation Details @@ -143,6 +171,16 @@ pub trait Provider { /// Check connection status fn check_connection(&self) -> Result; +```rust +``` rust +```rust +pub trait Provider { + /// Connect to the provider + fn connect(&self) -> Result<()>; + + /// Check connection status + fn check_connection(&self) -> Result; + /// Get provider name fn name(&self) -> &str; } @@ -154,6 +192,15 @@ The VyOS API client supports: - SSH-based configuration management - HTTP API in #### Proxmox API Client +The Proxmox API client supports: - REST API integration for VM management - Resource allocation and monitoring - Template management for deployments - Both token and username/password authentication +The VyOS API client supports: +- SSH-based configuration management +- HTTP API integration for automated provisioning +- WireGuard key generation and management +- L3VPN and VXLAN configuration + +#### Proxmox API Client + The Proxmox API client supports: - REST API integration for VM management - Resource allocation and monitoring - Template management for deployments - Both token and username/password authentication ### 3. Data Models @@ -163,6 +210,9 @@ The Proxmox API client supports: - REST API integration for VM management - Reso Represents virtual machines and containers: ```bash +```rust +``` rust +```rust pub struct Instance { pub id: Uuid, pub name: String, @@ -183,6 +233,9 @@ pub struct Instance { Represents storage volumes: ```bash +```rust +``` rust +```rust pub struct Volume { pub id: Uuid, pub name: String, @@ -205,6 +258,9 @@ pub struct Volume { Represents virtual networks: ```bash +```rust +``` rust +```rust pub struct Network { pub id: Uuid, pub name: String, @@ -232,6 +288,9 @@ pub struct Network { Manages infrastructure providers, their credentials, and connections: ```bash +```rust +``` rust +```rust pub struct ProviderService { providers: Providers, credentials: Credentials, @@ -243,6 +302,9 @@ pub struct ProviderService { Handles VM/container lifecycle operations: ```bash +```rust +``` rust +```rust pub struct InstanceService { storage: InstanceStorage, provider_service: ProviderService, @@ -264,21 +326,21 @@ Configuration is stored in the user's home directory: The CLI supports the following main commands: -- `bbctl init` - Initialize a new project -- `bbctl instances` - List/create/manage VMs -- `bbctl volumes` - Manage storage -- `bbctl networks` - Configure virtual networks -- `bbctl test-vyos` - Test connectivity to VyOS router +- `bbctl init` - Initialize a new project +- `bbctl instances` - List/create/manage VMs +- `bbctl volumes` - Manage storage +- `bbctl networks` - Configure virtual networks +- `bbctl test-vyos` - Test connectivity to VyOS router ### 7. Terminal UI (TUI) The TUI provides an interactive dashboard with: -- Instances view -- Volumes view -- Networks view -- Settings management -- Real-time status updates +- Instances view +- Volumes view +- Networks view +- Settings management +- Real-time status updates ## Test Environment @@ -309,21 +371,21 @@ The test lab simulates a multi-tenant infrastructure using Docker containers run The test lab implements: -1. **WireGuard Control Plane**: Secure management and control plane using WireGuard VPN -2. **BGP EVPN**: Control plane for multi-tenant VXLAN networks -3. **L3VPN**: Tenant isolation using VRFs and route targets -4. **HTTP API**: Endpoints for bbctl to manage infrastructure +1. **WireGuard Control Plane**: Secure management and control plane using WireGuard VPN +2. **BGP EVPN**: Control plane for multi-tenant VXLAN networks +3. **L3VPN**: Tenant isolation using VRFs and route targets +4. **HTTP API**: Endpoints for bbctl to manage infrastructure ### Test Scripts The test environment is managed by a set of scripts: -- `setup-base.sh` - Sets up base infrastructure -- `setup-vyos-container.sh` - Deploys VyOS containers -- `configure-l3vpn-evpn.sh` - Configures L3VPN with EVPN -- `configure-wireguard.sh` - Sets up WireGuard secure management -- `setup-lab.sh` - Main orchestration script -- `cleanup-lab.sh` - Teardown script +- `setup-base.sh` - Sets up base infrastructure +- `setup-vyos-container.sh` - Deploys VyOS containers +- `configure-l3vpn-evpn.sh` - Configures L3VPN with EVPN +- `configure-wireguard.sh` - Sets up WireGuard secure management +- `setup-lab.sh` - Main orchestration script +- `cleanup-lab.sh` - Teardown script ## Current Status @@ -368,6 +430,48 @@ The test environment is managed by a set of scripts: - ✅ VyOS lab setup scripts - ✅ L3VPN and EVPN configuration - ✅ WireGuard secure management + - ✅ Provider interface trait + - ✅ VyOS API client + - ✅ Proxmox API client +1. **API Layer** + +- ✅ Provider interface trait +- ✅ VyOS API client +- ✅ Proxmox API client + +2. **Data Models** + +- ✅ Instance model +- ✅ Volume model +- ✅ Network model +- ✅ Provider model + +3. **Configuration Management** + +- ✅ Settings model and storage +- ✅ Provider configuration +- ✅ Credential management + +4. **Basic Services** + +- ✅ Provider service +- ✅ Instance service (partial) + +5. **CLI Interface** + +- ✅ Basic command structure +- ✅ VyOS connectivity testing + +6. **Terminal UI** + +- ✅ Basic TUI framework +- ✅ Navigation and layout + +7. **Test Environment** + +- ✅ VyOS lab setup scripts +- ✅ L3VPN and EVPN configuration +- ✅ WireGuard secure management ### Work in Progress @@ -386,6 +490,24 @@ The test environment is managed by a set of scripts: - 🔄 Real-time data updates - 🔄 Resource management wizards + - 🔄 Volume service implementation + - 🔄 Network service implementation + - 🔄 API integration for resources +1. **Service Layer** + +- 🔄 Volume service implementation +- 🔄 Network service implementation +- 🔄 API integration for resources + +2. **CLI Interface** + +- 🔄 Complete command implementations +- 🔄 Error handling and user feedback + +3. **Terminal UI** + +- 🔄 Real-time data updates +- 🔄 Resource management wizards ### Planned Work @@ -413,6 +535,33 @@ The test environment is managed by a set of scripts: - 📝 Public cloud integration - 📝 CI/CD workflows - 📝 Integration with external tools + - 📝 Persistence layer for local state + - 📝 Synchronization with remote state + - 📝 Event system for notifications +1. **Service Layer** + +- 📝 Persistence layer for local state +- 📝 Synchronization with remote state +- 📝 Event system for notifications + +2. **Security Features** + +- 📝 Token rotation +- 📝 Credential encryption +- 📝 Secure remote execution + +3. **Advanced Features** + +- 📝 Multi-tenant management +- 📝 Role-based access control +- 📝 Audit logging +- 📝 Resource quotas and limits + +4. **Integration** + +- 📝 Public cloud integration +- 📝 CI/CD workflows +- 📝 Integration with external tools ## Implementation Roadmap @@ -450,32 +599,64 @@ The test environment is managed by a set of scripts: - 📝 Deployment workflows - 📝 Integration with external CI/CD systems - 📝 Scaling and update policies +- ✅ Create directory structure for core components +- ✅ Implement VyOS and Proxmox provider interfaces +- ✅ Setup test environment with containers +- ✅ Implement SSH connectivity to provider hosts +- ✅ Basic authentication mechanism + +### Phase 2: Resource Management + +- 🔄 Complete API for VM/instance management +- 📝 Storage (volume) provisioning and attachment +- 📝 Network creation and configuration +- 📝 IP address management + +### Phase 3: TUI Enhancement + +- 📝 Improve dashboard with real-time status updates +- 📝 Resource creation wizards +- 📝 Detailed views for resources +- 📝 Settings management + +### Phase 4: Multi-Tenancy & Security + +- 📝 User and organization management +- 📝 Role-based access control +- 📝 Secure credential management +- 📝 Encryption for data in transit + +### Phase 5: CI/CD Integration + +- 📝 Deployment workflows +- 📝 Integration with external CI/CD systems +- 📝 Scaling and update policies ## Design Decisions ### 1. Language and Framework Selection -- **Rust**: Selected for its performance, safety, and excellent async support -- **Tokio**: Used for async runtime -- **Ratatui**: Chosen for TUI implementation due to its flexibility and performance +- **Rust**: Selected for its performance, safety, and excellent async support +- **Tokio**: Used for async runtime +- **Ratatui**: Chosen for TUI implementation due to its flexibility and performance ### 2. API Design -- **Trait-based API**: Uses traits to define common provider interfaces -- **Async-first**: Designed with async operations in mind to prevent UI blocking -- **Error handling**: Consistent error propagation using `anyhow` for user-friendly messages +- **Trait-based API**: Uses traits to define common provider interfaces +- **Async-first**: Designed with async operations in mind to prevent UI blocking +- **Error handling**: Consistent error propagation using `anyhow` for user-friendly messages ### 3. Configuration Storage -- **TOML format**: Selected for human-readability and easy editing -- **User directory storage**: Uses `~/.bbctl` to store user configurations -- **Credential separation**: Stores credentials in a separate file for better security +- **TOML format**: Selected for human-readability and easy editing +- **User directory storage**: Uses `~/.bbctl` to store user configurations +- **Credential separation**: Stores credentials in a separate file for better security ### 4. Network Architecture -- **L3VPN with EVPN**: Chosen for scalable multi-tenant isolation -- **WireGuard**: Selected for secure management plane due to its simplicity and strong encryption -- **VXLAN**: Used for tenant traffic encapsulation to support network virtualization +- **L3VPN with EVPN**: Chosen for scalable multi-tenant isolation +- **WireGuard**: Selected for secure management plane due to its simplicity and strong encryption +- **VXLAN**: Used for tenant traffic encapsulation to support network virtualization ## Development Guidelines @@ -486,17 +667,29 @@ The test environment is managed by a set of scripts: - **Naming**: - Use snake_case for variables, functions, and modules - Use PascalCase for structs, enums, and traits +- **Naming**: + - Use snake_case for variables, functions, and modules + - Use PascalCase for structs, enums, and traits - **Error Handling**: Use `AppResult` for functions that can fail - **Imports**: Group imports by crate, with std first, then external, then internal - **Document**: Use three slashes (`///`) for public API documentation - **Async**: Use tokio runtime with futures for async operations +- **Formatting**: Use `cargo fmt` to format code according to Rust standard style +- **Linting**: Run `cargo clippy` for static analysis +- **Naming**: +- Use snake_case for variables, functions, and modules +- Use PascalCase for structs, enums, and traits +- **Error Handling**: Use `AppResult` for functions that can fail +- **Imports**: Group imports by crate, with std first, then external, then internal +- **Document**: Use three slashes (`///`) for public API documentation +- **Async**: Use tokio runtime with futures for async operations ### Testing Strategy -1. **Unit Tests**: Test individual components in isolation -2. **Integration Tests**: Test component interactions -3. **System Tests**: Test against the VyOS lab environment -4. **Manual Testing**: Interactive testing of the TUI +1. **Unit Tests**: Test individual components in isolation +2. **Integration Tests**: Test component interactions +3. **System Tests**: Test against the VyOS lab environment +4. **Manual Testing**: Interactive testing of the TUI ## Conclusion @@ -505,3 +698,4 @@ The bbctl project is a comprehensive tool for managing multi-tenant infrastructu Phase 1 of the implementation has been completed, establishing the core infrastructure, API clients, data models, and test environment. Ongoing work focuses on completing the service layer implementations and enhancing the CLI and TUI interfaces. The project follows a clear roadmap with well-defined phases, targeting a complete infrastructure management solution that supports secure multi-tenancy and seamless operations across different providers. +The project follows a clear roadmap with well-defined phases, targeting a complete infrastructure management solution that supports secure multi-tenancy and seamless operations across different providers. diff --git a/docs/api-readme.md b/docs/api-readme.md index 5471a52..c43a1e1 100644 --- a/docs/api-readme.md +++ b/docs/api-readme.md @@ -13,6 +13,10 @@ The API schema is defined using [Zod], a TypeScript-first schema validation libr - Static TypeScript types - OpenAPI documentation - API client generation capabilities +- Runtime type validation +- Static TypeScript types +- OpenAPI documentation +- API client generation capabilities ## Getting Started @@ -22,6 +26,11 @@ The API schema is defined using [Zod], a TypeScript-first schema validation libr ### Installation +```bash +- Bun 1.0 or higher + +### Installation + ```bash cd bitbuilder.io/bbctl bun install @@ -32,6 +41,7 @@ bun install To generate the OpenAPI schema and documentation: ```bash +``` bash bun run generate-openapi ``` @@ -46,6 +56,7 @@ Open `api-docs/index.html` in your browser to view the interactive API documenta You can use the Zod schemas to validate data at runtime: ```typescript +``` typescript import { InstanceSchema } from './schema.js'; // Data from API or user input @@ -54,6 +65,10 @@ const instanceData = { name: 'web-server-1', status: 'Running', provider: 'VyOS', + id: "550e8400-e29b-41d4-a716-446655440000", + name: "web-server-1", + status: "Running", + provider: "VyOS", // ... }; @@ -63,6 +78,9 @@ try { console.log('Valid instance:', validatedInstance); } catch (error) { console.error('Invalid instance data:', error); + console.log("Valid instance:", validatedInstance); +} catch (error) { + console.error("Invalid instance data:", error); } ``` @@ -71,12 +89,15 @@ try { The schemas also provide TypeScript types: ```typescript +``` typescript import { Instance, InstanceStatus } from './schema.js'; // Type-safe instance object const instance: Instance = { id: '550e8400-e29b-41d4-a716-446655440000', name: 'web-server-1', + id: "550e8400-e29b-41d4-a716-446655440000", + name: "web-server-1", status: InstanceStatus.Running, // ... }; @@ -89,6 +110,9 @@ The Zod/OpenAPI schema and the Rust CLI share the same data models. When updatin 1. Modify both the Rust structs (`src/models/*.rs`) and the TypeScript schemas (`schema.ts`) 2. Regenerate the OpenAPI documentation 3. Update any dependent code in both languages +1. Modify both the Rust structs (`src/models/*.rs`) and the TypeScript schemas (`schema.ts`) +2. Regenerate the OpenAPI documentation +3. Update any dependent code in both languages ## API Endpoints @@ -98,6 +122,10 @@ The OpenAPI documentation details all available endpoints: - `/instances` - Create and manage virtual machines - `/volumes` - Manage storage volumes - `/networks` - Configure virtual networks +- `/providers` - Manage infrastructure providers +- `/instances` - Create and manage virtual machines +- `/volumes` - Manage storage volumes +- `/networks` - Configure virtual networks For detailed parameters and response formats, refer to the Swagger UI documentation. @@ -109,6 +137,10 @@ To extend the API schema: 2. Register your schemas with the OpenAPI registry 3. Define new paths and operations in the OpenAPI schema 4. Regenerate the documentation +1. Add new Zod schemas in `schema.ts` +2. Register your schemas with the OpenAPI registry +3. Define new paths and operations in the OpenAPI schema +4. Regenerate the documentation ## Testing with the API @@ -116,6 +148,8 @@ The OpenAPI documentation can be used to generate clients in various languages u - [OpenAPI Generator] - [Swagger Codegen] +- [OpenAPI Generator] +- [Swagger Codegen] [OpenAPI Generator]: https://github.com/OpenAPITools/openapi-generator [Swagger Codegen]: https://github.com/swagger-api/swagger-codegen @@ -123,6 +157,7 @@ The OpenAPI documentation can be used to generate clients in various languages u For example, to generate a TypeScript client: ```bash +``` bash bunx --bun @openapitools/openapi-generator-cli generate \ -i api-docs/openapi.json \ -g typescript-axios \ diff --git a/docs/command-reference.md b/docs/command-reference.md index b7667cf..3e7f841 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -8,6 +8,7 @@ The following options can be used with any command: | Option | Description | | --------------------- | ---------------------------------------- | +|-----------------------|------------------------------------------| | `--help`, `-h` | Show help information | | `--version`, `-V` | Show version information | | `--log-level=` | Set log level (debug, info, warn, error) | @@ -541,6 +542,7 @@ bbctl | Key | Action | | ---------- | -------------------- | +|------------|----------------------| | 1-5 | Switch tabs | | Tab | Next tab | | Shift+Tab | Previous tab | @@ -558,6 +560,7 @@ The following environment variables can be used to override configuration: | Variable | Description | | ------------------------ | ------------------------------------ | +|--------------------------|--------------------------------------| | `BBCTL_LOG_LEVEL` | Log level (debug, info, warn, error) | | `BBCTL_CONFIG_DIR` | Custom configuration directory | | `BBCTL_DEFAULT_PROVIDER` | Default provider | diff --git a/docs/configuration-guide.md b/docs/configuration-guide.md index 36c8804..ae88762 100644 --- a/docs/configuration-guide.md +++ b/docs/configuration-guide.md @@ -10,6 +10,7 @@ bbctl uses the following configuration files, located in the `~/.bbctl/` directo | File | Purpose | | ------------------ | --------------------------------------------------- | +|--------------------|-----------------------------------------------------| | `settings.toml` | Global settings and defaults | | `providers.toml` | Provider configurations | | `credentials.toml` | Authentication credentials (API keys, tokens, etc.) | @@ -19,6 +20,7 @@ bbctl uses the following configuration files, located in the `~/.bbctl/` directo The `settings.toml` file contains global configuration for bbctl behavior: ```toml +``` toml # Default provider to use when not specified default_provider = "vyos-router" @@ -48,6 +50,7 @@ log_level = "info" You can modify settings using the config command: ```bash +``` bash # Set default provider bbctl config set default_provider vyos-router @@ -60,6 +63,8 @@ bbctl config set log_level debug The `providers.toml` file defines infrastructure providers and regions: ```bash +``` toml +```toml # Provider configurations [providers] @@ -100,6 +105,7 @@ limits = { max_instances = 5, max_cpu_per_instance = 4 } Provider configuration can be managed using CLI commands: ```bash +``` bash # Add a new VyOS provider bbctl providers add vyos-router2 --type vyos --host 192.168.1.3 --username vyos @@ -115,6 +121,8 @@ bbctl providers remove vyos-router2 The `credentials.toml` file stores authentication information for providers: ```bash +``` toml +```toml [credentials] [credentials.vyos-router] @@ -134,12 +142,17 @@ verify_ssl = false 1. Use API tokens instead of passwords when possible 2. Ensure proper file permissions (600) on credentials.toml 3. Consider using environment variables for sensitive credentials +1. Use API tokens instead of passwords when possible +2. Ensure proper file permissions (600) on credentials.toml +3. Consider using environment variables for sensitive credentials ## Network Configuration Network configuration is stored within the provider settings: ```bash +``` toml +```toml [networks.app-network] id = "net-01234567" name = "app-network" @@ -156,6 +169,8 @@ dns_servers = ["1.1.1.1", "8.8.8.8"] For secure encrypted networks using WireGuard: ```bash +``` toml +```toml [networks.secure-net] id = "net-89abcdef" name = "secure-net" @@ -172,6 +187,7 @@ You can override configuration using environment variables: | Variable | Description | | ------------------------ | --------------------------- | +|--------------------------|-----------------------------| | `BBCTL_LOG_LEVEL` | Override log level | | `BBCTL_CONFIG_DIR` | Use custom config directory | | `BBCTL_DEFAULT_PROVIDER` | Override default provider | @@ -180,6 +196,7 @@ You can override configuration using environment variables: Example: ```bash +``` bash export BBCTL_LOG_LEVEL=debug export BBCTL_DEFAULT_PROVIDER=vyos-router bbctl instances list # Will use debug logging and vyos-router as default @@ -192,6 +209,8 @@ bbctl instances list # Will use debug logging and vyos-router as default Configure resource limits by tenant: ```bash +``` toml +```toml [tenants.eng-team] max_instances = 20 max_volumes = 40 @@ -206,6 +225,8 @@ regions = ["nyc", "sfo"] Define templates for quick provisioning: ```bash +``` toml +```toml [templates.web-server] cpu = 2 memory_gb = 4 @@ -220,12 +241,14 @@ disk_gb = 200 volumes = [ { name = "data", size_gb = 100, type = "ssd" }, { name = "backup", size_gb = 200, type = "hdd" }, + { name = "backup", size_gb = 200, type = "hdd" } ] ``` Usage: ```bash +``` bash bbctl instances create web1 --template web-server ``` @@ -234,6 +257,8 @@ bbctl instances create web1 --template web-server Configure the API server component: ```bash +``` toml +```toml [api] enabled = true listen = "127.0.0.1" @@ -247,6 +272,8 @@ cors_origins = ["http://localhost:3000"] Configure SSH keys for instance access: ```bash +``` toml +```toml [ssh] default_key = "~/.ssh/id_ed25519" additional_keys = ["~/.ssh/id_rsa", "~/.ssh/custom_key"] @@ -262,6 +289,13 @@ additional_keys = ["~/.ssh/id_rsa", "~/.ssh/custom_key"] ### Debugging Configuration +```bash +1. **Connection Problems**: Check host, port, and credentials +2. **Permission Errors**: Verify API key permissions and SSH key access +3. **File Format Errors**: Validate TOML syntax in configuration files + +### Debugging Configuration + ```bash # Show current configuration bbctl config show @@ -278,6 +312,7 @@ bbctl config validate If you need to reset your configuration: ```bash +``` bash # Reset specific section bbctl config reset --section credentials @@ -298,6 +333,17 @@ bbctl config reset --all - [User Guide] - Comprehensive usage instructions - [Command Reference] - Detailed command documentation - [API Documentation] - API schema and integration details +1. **Organize by Environment**: Use naming conventions like `prod-`, `staging-` prefixes +2. **Document Custom Settings**: Add comments to configuration files +3. **Version Control**: Consider storing non-sensitive configuration in version control +4. **Regular Backups**: Back up your configuration directory regularly +5. **Security**: Never expose credentials in scripts or version control + +## Further Reading + +- [User Guide] - Comprehensive usage instructions +- [Command Reference] - Detailed command documentation +- [API Documentation] - API schema and integration details [User Guide]: user-guide.md [Command Reference]: command-reference.md diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index b08eb35..523c217 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -12,6 +12,10 @@ BitBuilder Cloud CLI is designed around a consistent infrastructure-as-code appr - **Templates**: Reusable configurations for deployment - **Environments**: Distinct deployment targets (development, staging, production) - **Workspaces**: Isolated deployment contexts for multi-tenant usage +- **Resources**: The building blocks of your infrastructure (instances, volumes, networks) +- **Templates**: Reusable configurations for deployment +- **Environments**: Distinct deployment targets (development, staging, production) +- **Workspaces**: Isolated deployment contexts for multi-tenant usage ### Deployment Workflow @@ -23,6 +27,12 @@ The typical deployment workflow consists of: 4. **Configure**: Apply post-deployment configuration 5. **Verify**: Confirm successful deployment 6. **Monitor**: Track performance and health +1. **Define**: Create deployment configuration and resources +2. **Validate**: Verify configuration and check dependencies +3. **Deploy**: Provision resources and deploy applications +4. **Configure**: Apply post-deployment configuration +5. **Verify**: Confirm successful deployment +6. **Monitor**: Track performance and health ## Deployment Configuration @@ -31,6 +41,8 @@ The typical deployment workflow consists of: BitBuilder Cloud CLI uses TOML configuration files for deployments. The main deployment file is typically named `deploy.toml`: ```bash +``` toml +```toml [app] name = "my-web-app" version = "1.0.0" @@ -63,6 +75,8 @@ subdomain = "web-app" For environment-specific configurations, use separate files or environment sections: ```bash +``` toml +```toml [environments.development] instances = { count = 1, size = "small" } enable_metrics = false @@ -87,6 +101,17 @@ bbctl init --name my-web-app 3. Deploy the application: +```bash +1. Initialize a new project: + +```bash +bbctl init --name my-web-app +``` + +2. Create a `deploy.toml` file in the project directory + +3. Deploy the application: + ```bash bbctl deploy ``` @@ -94,6 +119,7 @@ bbctl deploy ### Deployment Options ```bash +``` bash # Deploy with a specific configuration file bbctl deploy --config custom-deploy.toml @@ -114,6 +140,8 @@ bbctl deploy --force For complex applications with dependencies, use multi-stage deployments: ```bash +``` toml +```toml [stages] order = ["infrastructure", "database", "application", "monitoring"] @@ -138,6 +166,8 @@ depends_on = ["application"] Minimize downtime using rolling deployments: ```bash +``` toml +```toml [deployment.strategy] type = "rolling" batch_size = 1 @@ -154,6 +184,11 @@ Implement blue-green deployment strategy: [deployment.strategy] type = "blue-green" traffic_shift = "instant" # or "gradual" +``` toml +```toml +[deployment.strategy] +type = "blue-green" +traffic_shift = "instant" # or "gradual" verification_period = "2m" rollback_on_failure = true ``` @@ -173,6 +208,15 @@ terraform init -plugin-dir=~/.terraform.d/plugins 2. Create a Terraform configuration using bbctl resources: ```bash +1. Install the bbctl Terraform provider: + +```bash +terraform init -plugin-dir=~/.terraform.d/plugins +``` + +2. Create a Terraform configuration using bbctl resources: + +```hcl provider "bbctl" { config_path = "~/.bbctl/config.toml" } @@ -188,6 +232,9 @@ resource "bbctl_instance" "web" { 3. Apply the Terraform configuration: +```bash +3. Apply the Terraform configuration: + ```bash terraform apply ``` @@ -209,6 +256,21 @@ const instance = new bbctl.Instance('web-server', { region: 'nyc', size: 'standard', networks: [network.id], +``` typescript +```typescript +import * as bbctl from "@pulumi/bbctl"; + +const network = new bbctl.Network("app-network", { + cidr: "10.0.0.0/24", + provider: "vyos-router", + region: "nyc", +}); + +const instance = new bbctl.Instance("web-server", { + provider: "vyos-router", + region: "nyc", + size: "standard", + networks: [network.id], }); export const instanceIp = instance.publicIp; @@ -221,6 +283,8 @@ export const instanceIp = instance.publicIp; Example GitHub Actions workflow: ```bash +``` yaml +```yaml name: Deploy Application on: @@ -252,6 +316,8 @@ jobs: Example GitLab CI pipeline: ```bash +``` yaml +```yaml stages: - test - build @@ -275,6 +341,8 @@ deploy: Inject environment variables into your instances: ```bash +``` toml +```toml [instances.web.env] DATABASE_URL = "postgres://user:pass@db.internal:5432/mydb" REDIS_HOST = "redis.internal" @@ -286,6 +354,8 @@ LOG_LEVEL = "info" Deploy configuration files to instances: ```bash +``` toml +```toml [instances.web.files] "/etc/nginx/nginx.conf" = { source = "./configs/nginx.conf" } "/etc/app/config.json" = { content = '{"debug": false, "port": 3000}' } @@ -296,6 +366,8 @@ Deploy configuration files to instances: Secure handling of sensitive information: ```bash +``` toml +```toml [secrets] provider = "vault" path = "secret/my-app" @@ -313,6 +385,11 @@ Deploy across multiple regions: [regions] enabled = ["nyc", "sfo", "fra"] strategy = "all" # or "weighted" +``` toml +```toml +[regions] +enabled = ["nyc", "sfo", "fra"] +strategy = "all" # or "weighted" [regions.nyc] weight = 60 @@ -332,12 +409,15 @@ instances = { count = 1 } Configure highly available deployments: ```bash +``` toml +```toml [availability] zones = ["a", "b", "c"] distribution = "spread" [instances] count = 6 # 2 instances per zone +count = 6 # 2 instances per zone [database] replicas = 3 @@ -349,6 +429,8 @@ failover = "automatic" Configure monitoring for deployments: ```bash +``` toml +```toml [monitoring] enable = true provider = "prometheus" @@ -365,6 +447,8 @@ options = { tag = "app-logs" } ### Pre-deployment Testing ```bash +``` toml +```toml [testing.pre_deployment] enabled = true command = "./scripts/pre-deploy-test.sh" @@ -375,11 +459,14 @@ fail_on_error = true ### Smoke Testing ```bash +``` toml +```toml [testing.smoke] enabled = true endpoints = [ { url = "/health", expect_status = 200 }, { url = "/api/status", expect_contains = "running" }, + { url = "/api/status", expect_contains = "running" } ] timeout = "30s" retries = 3 @@ -388,6 +475,8 @@ retries = 3 ### Load Testing ```bash +``` toml +```toml [testing.load] enabled = true tool = "k6" @@ -402,6 +491,8 @@ threshold = "p95(http_req_duration) < 200" ### Security Configurations ```bash +``` toml +```toml [security] ssl_enabled = true certificate = "acme" @@ -416,6 +507,8 @@ headers = { ### Compliance Checks ```bash +``` toml +```toml [compliance] enabled = true standards = ["pci-dss", "gdpr"] @@ -428,6 +521,7 @@ scans = ["vulnerability", "configuration"] To roll back to a previous deployment: ```bash +``` bash # List deployments bbctl deployments list @@ -459,12 +553,25 @@ bbctl deployments rollback --previous - Validate application configuration - Check for dependency issues - Examine application logs with `bbctl instances logs i-01234567` +1. **Resource Provisioning Failures** + - Check provider connectivity + - Verify resource limits and quotas + - Review error logs with `bbctl logs get d-01234567` +2. **Network Configuration Issues** + - Verify CIDR blocks don't overlap + - Ensure security groups allow required traffic + - Check DNS resolution with `bbctl network test-dns net-01234567` +3. **Application Deployment Failures** + - Validate application configuration + - Check for dependency issues + - Examine application logs with `bbctl instances logs i-01234567` ### Deployment Logs Access deployment logs: ```bash +``` bash # Get summary of deployment logs bbctl deployments logs d-01234567 @@ -485,6 +592,10 @@ BitBuilder Cloud CLI provides a powerful platform for deploying and managing inf - [Command Reference] - Detailed command documentation - [Configuration Guide] - Configuration file reference - [Architecture Design] - System architecture details +- [User Guide] - Comprehensive usage instructions +- [Command Reference] - Detailed command documentation +- [Configuration Guide] - Configuration file reference +- [Architecture Design] - System architecture details [User Guide]: user-guide.md [Command Reference]: command-reference.md diff --git a/docs/index.md b/docs/index.md index 374f9e2..0f2db69 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,6 +15,17 @@ Welcome to the BitBuilder Cloud CLI (bbctl) documentation. This index provides a | [Rust Integration] | Guide for maintaining Rust and TypeScript compatibility | | [VyOS Network Plan] | Comprehensive networking architecture design | | [VyOS Test Lab Setup] | Instructions for setting up a test environment | +| Document | Description | +|--------------------------------|----------------------------------------| +| [User Guide] | Comprehensive guide for using bbctl | +| [Command Reference] | Detailed reference of all bbctl commands | +| [Configuration Guide] | Guide for configuring bbctl | +| [Deployment Guide] | Guide for deploying applications with bbctl | +| [Architecture Design] | Technical architecture and system design | +| [API Documentation] | API schema and OpenAPI integration details | +| [Rust Integration] | Guide for maintaining Rust and TypeScript compatibility | +| [VyOS Network Plan] | Comprehensive networking architecture design | +| [VyOS Test Lab Setup] | Instructions for setting up a test environment | [User Guide]: user-guide.md [Command Reference]: command-reference.md @@ -35,6 +46,11 @@ If you're new to the project, we recommend starting with: 3. [Command Reference] for detailed command usage 4. [Architecture Design] to understand the system 5. [VyOS Test Lab Setup] to create a test environment +1. The main [README] for an overview of capabilities +2. [User Guide] for a comprehensive introduction +3. [Command Reference] for detailed command usage +4. [Architecture Design] to understand the system +5. [VyOS Test Lab Setup] to create a test environment [README]: ../README.md [User Guide]: user-guide.md @@ -51,6 +67,11 @@ For developers looking to integrate or extend bbctl: - [Configuration Guide] explains configuration options and customization - [Deployment Guide] covers advanced deployment scenarios and CI/CD integration - Run the [examples] to understand API usage +- [API Documentation] contains schema details and API reference +- [Rust Integration] provides guidance for maintaining compatibility between Rust and TypeScript +- [Configuration Guide] explains configuration options and customization +- [Deployment Guide] covers advanced deployment scenarios and CI/CD integration +- Run the [examples] to understand API usage [API Documentation]: api-readme.md [Rust Integration]: rust-integration.md @@ -64,6 +85,8 @@ The networking components are documented in: - [VyOS Network Plan] for detailed network design - [VyOS Test Lab Setup] for lab environment configuration +- [VyOS Network Plan] for detailed network design +- [VyOS Test Lab Setup] for lab environment configuration [VyOS Network Plan]: vyos-network-plan.md [VyOS Test Lab Setup]: vyos-test-lab-setup.md @@ -85,6 +108,11 @@ When contributing code, ensure compatibility between the Rust backend and TypeSc - View API documentation by running `bun run generate-openapi` and opening the HTML page - Check the [Command Reference] for all available commands - See the [User Guide] for tutorials and examples +- [GitHub Repository] +- [Issue Tracker] +- View API documentation by running `bun run generate-openapi` and opening the HTML page +- Check the [Command Reference] for all available commands +- See the [User Guide] for tutorials and examples [GitHub Repository]: https://github.com/bitbuilder-io/bbctl [Issue Tracker]: https://github.com/bitbuilder-io/bbctl/issues diff --git a/docs/rust-integration.md b/docs/rust-integration.md index 2c3e660..b438df4 100644 --- a/docs/rust-integration.md +++ b/docs/rust-integration.md @@ -28,6 +28,19 @@ Both codebases need to share consistent data models. This document outlines the | `struct` | `z.object()` | | `Uuid` | `z.string().uuid()` | | `DateTime` | `z.string().datetime()` | +| Rust Type | TypeScript/Zod Type | +|-----------|---------------------| +| `String` | `z.string()` | +| `i32`, `u32`, etc. | `z.number().int()` | +| `f32`, `f64` | `z.number()` | +| `bool` | `z.boolean()` | +| `Option` | `z.optional()` | +| `Vec` | `z.array(...)` | +| `HashMap` | `z.record(...)` | +| `enum` | `z.enum()` or `z.discriminatedUnion()` | +| `struct` | `z.object()` | +| `Uuid` | `z.string().uuid()` | +| `DateTime` | `z.string().datetime()` | ## Development Workflow @@ -114,6 +127,7 @@ pub struct Instance { // ES module import syntax import { z } from 'zod'; import { InstanceNetworkSchema, InstanceSizeSchema, InstanceStatusEnum, ProviderTypeEnum, TagsSchema } from './schemas.js'; +import { InstanceStatusEnum, ProviderTypeEnum, InstanceSizeSchema, InstanceNetworkSchema, TagsSchema } from './schemas.js'; export const InstanceSchema = z.object({ id: z.string().uuid(), @@ -146,3 +160,8 @@ export const InstanceSchema = z.object({ 5. **UUID representation**: Use the same format (hyphenated, lowercase) 6. **ES module issues**: Remember to add `.js` extensions to imports in TypeScript 7. **Bun compatibility**: Ensure all dependencies are compatible with Bun's runtime +3. **Enum handling differences**: Map string values consistently +4. **Date/time format issues**: Use ISO 8601 format (RFC 3339) consistently +5. **UUID representation**: Use the same format (hyphenated, lowercase) +6. **ES module issues**: Remember to add `.js` extensions to imports in TypeScript +7. **Bun compatibility**: Ensure all dependencies are compatible with Bun's runtime diff --git a/docs/user-guide.md b/docs/user-guide.md index 93a9497..7481a2a 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -15,6 +15,7 @@ This guide will help you understand how to use bbctl effectively, covering insta If you have Rust installed, the simplest way to install bbctl is via Cargo: ```bash +``` bash cargo install bbctl ``` @@ -26,6 +27,10 @@ For systems without Rust, download pre-compiled binaries: 2. Download the appropriate binary for your platform 3. Make it executable: `chmod +x bbctl` 4. Move it to your PATH: `sudo mv bbctl /usr/local/bin/` +1. Visit the [releases page] +2. Download the appropriate binary for your platform +3. Make it executable: `chmod +x bbctl` +4. Move it to your PATH: `sudo mv bbctl /usr/local/bin/` [releases page]: https://github.com/bitbuilder-io/bbctl/releases @@ -34,6 +39,7 @@ For systems without Rust, download pre-compiled binaries: To build the latest version from source: ```bash +``` bash git clone https://github.com/bitbuilder-io/bbctl.git cd bbctl cargo build --release @@ -46,6 +52,7 @@ The compiled binary will be in `target/release/bbctl`. When running bbctl for the first time, you'll need to set up your provider credentials: ```bash +``` bash # Initialize bbctl configuration bbctl init @@ -65,24 +72,32 @@ BitBuilder Cloud CLI organizes resources into the following categories: - **Instances**: Virtual machines running on the providers - **Volumes**: Storage volumes that can be attached to instances - **Networks**: Virtual networks for connecting instances +- **Providers**: Infrastructure providers like VyOS routers or Proxmox hosts +- **Regions**: Logical groupings of infrastructure, typically by location +- **Instances**: Virtual machines running on the providers +- **Volumes**: Storage volumes that can be attached to instances +- **Networks**: Virtual networks for connecting instances ## Working with Providers ### Listing Providers ```bash +``` bash bbctl providers list ``` ### Testing Provider Connectivity ```bash +``` bash bbctl providers test vyos-router ``` ### Removing a Provider ```bash +``` bash bbctl providers remove vyos-router ``` @@ -91,6 +106,7 @@ bbctl providers remove vyos-router ### Creating an Instance ```bash +``` bash bbctl instances create web-server-1 \ --provider vyos-router \ --region nyc \ @@ -102,12 +118,14 @@ bbctl instances create web-server-1 \ ### Listing Instances ```bash +``` bash bbctl instances list ``` ### Starting and Stopping Instances ```bash +``` bash # Start an instance bbctl instances start i-01234567 @@ -118,12 +136,14 @@ bbctl instances stop i-01234567 ### Getting Instance Details ```bash +``` bash bbctl instances show i-01234567 ``` ### Deleting an Instance ```bash +``` bash bbctl instances delete i-01234567 ``` @@ -132,6 +152,7 @@ bbctl instances delete i-01234567 ### Creating a Volume ```bash +``` bash bbctl volumes create db-data \ --size 100 \ --region nyc @@ -140,12 +161,14 @@ bbctl volumes create db-data \ ### Listing Volumes ```bash +``` bash bbctl volumes list ``` ### Attaching a Volume to an Instance ```bash +``` bash bbctl volumes attach vol-01234567 \ --instance i-01234567 ``` @@ -153,6 +176,7 @@ bbctl volumes attach vol-01234567 \ ### Detaching a Volume ```bash +``` bash bbctl volumes detach vol-01234567 ``` @@ -161,6 +185,7 @@ bbctl volumes detach vol-01234567 ### Creating a Network ```bash +``` bash bbctl networks create app-network \ --cidr 192.168.1.0/24 ``` @@ -168,12 +193,14 @@ bbctl networks create app-network \ ### Listing Networks ```bash +``` bash bbctl networks list ``` ### Connecting an Instance to a Network ```bash +``` bash bbctl networks connect net-01234567 \ --instance i-01234567 ``` @@ -181,6 +208,7 @@ bbctl networks connect net-01234567 \ ### Disconnecting an Instance ```bash +``` bash bbctl networks disconnect net-01234567 \ --instance i-01234567 ``` @@ -203,11 +231,24 @@ BitBuilder Cloud CLI includes an interactive terminal interface that can be laun 3. **Volumes**: Manage storage volumes 4. **Networks**: Configure virtual networks 5. **Settings**: Configure bbctl options +- Use Tab or number keys (1-5) to switch between views +- Use arrow keys or j/k to select items in lists +- Press Enter to view or interact with a selected item +- Press ? to view help + +### TUI Views + +1. **Home**: Dashboard with summary information +2. **Instances**: List and manage virtual machines +3. **Volumes**: Manage storage volumes +4. **Networks**: Configure virtual networks +5. **Settings**: Configure bbctl options ### TUI Key Bindings | Key | Action | | --------- | -------------------------- | +|-----------|----------------------------| | 1-5 | Switch to numbered view | | Tab | Next view | | Shift+Tab | Previous view | @@ -232,6 +273,13 @@ BitBuilder Cloud CLI uses the following configuration files in `~/.bbctl/`: ### Example Settings File ```bash +- `settings.toml`: Global settings for bbctl +- `providers.toml`: Provider configurations +- `credentials.toml`: Authentication credentials (API keys, tokens, etc.) + +### Example Settings File + +```toml default_provider = "vyos-router" default_region = "nyc" telemetry_enabled = false @@ -250,6 +298,7 @@ log_level = "info" You can use environment variables to override configuration values: ```bash +``` bash export BBCTL_DEFAULT_PROVIDER=vyos-router export BBCTL_LOG_LEVEL=debug ``` @@ -259,6 +308,7 @@ export BBCTL_LOG_LEVEL=debug For scripting, you can use the `--json` flag with most commands to get machine-readable output: ```bash +``` bash bbctl instances list --json > instances.json ``` @@ -267,6 +317,7 @@ bbctl instances list --json > instances.json BitBuilder Cloud CLI supports setting up WireGuard for secure connectivity: ```bash +``` bash bbctl networks create secure-net \ --cidr 10.10.0.0/24 \ --wireguard enabled @@ -281,6 +332,7 @@ bbctl networks create secure-net \ If you're having trouble connecting to a provider: ```bash +``` bash # Test provider connectivity with verbose output bbctl providers test vyos-router --verbose @@ -293,6 +345,7 @@ bbctl providers update vyos-router --api-key new-api-key For detailed error information, increase the log level: ```bash +``` bash bbctl --log-level debug instances list ``` @@ -301,6 +354,7 @@ bbctl --log-level debug instances list If you suspect configuration problems: ```bash +``` bash # View current configuration bbctl config show @@ -313,6 +367,7 @@ bbctl config reset For additional help with specific commands: ```bash +``` bash bbctl help bbctl instances --help ``` @@ -326,6 +381,9 @@ For more detailed information, refer to the other documentation: - [Architecture Design] - [VyOS Test Lab Setup] - [API Reference] +- [Architecture Design] +- [VyOS Test Lab Setup] +- [API Reference] [Architecture Design]: ARCHITECTURE_DESIGN.md [VyOS Test Lab Setup]: vyos-test-lab-setup.md @@ -335,6 +393,8 @@ For more detailed information, refer to the other documentation: - [GitHub Repository] - [Issue Tracker] +- [GitHub Repository] +- [Issue Tracker] [GitHub Repository]: https://github.com/bitbuilder-io/bbctl [Issue Tracker]: https://github.com/bitbuilder-io/bbctl/issues diff --git a/docs/vyos-network-plan.md b/docs/vyos-network-plan.md index cb0bc49..fe16ca5 100644 --- a/docs/vyos-network-plan.md +++ b/docs/vyos-network-plan.md @@ -74,7 +74,7 @@ graph LR The physical infrastructure consists of: -- **Datacenter 1**: +- **Datacenter 1**: - Public Block: 5.254.54.0/26 (62 usable IPs) - Networking: 4x Intel X710 (10G) + 2x Mellanox CX4 (25G) - Management: IPMI via dedicated 1GbE NIC @@ -84,6 +84,15 @@ The physical infrastructure consists of: - Additional Block: 5.254.43.208/29 (6 usable IPs) - Networking: 4x Intel X710 (10G) + 2x Mellanox CX4 (25G) - Management: IPMI via dedicated 1GbE NIC +- **Datacenter 1**: + - Public Block: 5.254.54.0/26 (62 usable IPs) + - Networking: 4x Intel X710 (10G) + 2x Mellanox CX4 (25G) + - Management: IPMI via dedicated 1GbE NIC +- **Datacenter 2**: + - Public Block: 5.254.43.160/27 (30 usable IPs) + - Additional Block: 5.254.43.208/29 (6 usable IPs) + - Networking: 4x Intel X710 (10G) + 2x Mellanox CX4 (25G) + - Management: IPMI via dedicated 1GbE NIC ### 2. Hypervisor Layer Configuration @@ -95,7 +104,6 @@ Each bare metal server runs: 4. systemd-vmspawn for VM deployment **NIC Configuration**: - ```bash #!/bin/bash @@ -278,15 +286,15 @@ Automate tenant onboarding and provisioning with cloud-init: vyos_config_commands: # Create Tenant VRF - set vrf name ${TENANT_VRF} table '${VRF_TABLE_ID}' - + # Configure VXLAN for Tenant - set interfaces vxlan vxlan${VNI} vni '${VNI}' - set interfaces vxlan vxlan${VNI} vrf '${TENANT_VRF}' - + # Configure BGP for Tenant - set vrf name ${TENANT_VRF} protocols bgp address-family ipv4-unicast route-target vpn export '65000:${TENANT_ID}' - set vrf name ${TENANT_VRF} protocols bgp address-family ipv4-unicast route-target vpn import '65000:${TENANT_ID}' - + # Configure WireGuard for Tenant - set interfaces wireguard wg${TENANT_ID} address '100.64.${TENANT_ID}.1/24' - set interfaces wireguard wg${TENANT_ID} vrf '${TENANT_VRF}' @@ -1026,7 +1034,7 @@ tenant_policies: destination: type: service service: web-servers - + - id: 2 description: "Allow Database Access" action: accept @@ -1038,7 +1046,7 @@ tenant_policies: destination: type: service service: database-servers - + - id: 3 description: "Block External SSH" action: drop @@ -1048,13 +1056,13 @@ tenant_policies: type: external destination: type: any - + services: - id: web-servers addresses: - 100.64.1.10/32 - 100.64.1.11/32 - + - id: database-servers addresses: - 100.64.1.20/32 @@ -1139,7 +1147,7 @@ monitoring: high_resolution: 24h medium_resolution: 7d low_resolution: 90d - + metrics: - name: interface_utilization description: "Network interface utilization percentage" @@ -1152,7 +1160,7 @@ monitoring: warning: 70 critical: 85 duration: 5m - + - name: bgp_session_status description: "BGP session state" type: state @@ -1164,7 +1172,7 @@ monitoring: warning: "Connect" critical: "Idle" duration: 2m - + - name: memory_utilization description: "System memory utilization" type: gauge @@ -1176,7 +1184,7 @@ monitoring: warning: 80 critical: 90 duration: 5m - + alerting: routes: - name: critical @@ -1185,22 +1193,22 @@ monitoring: address: network-ops@example.com - type: pagerduty service_key: 1234567890abcdef - + - name: warning targets: - type: email address: monitoring@example.com - type: slack webhook: https://hooks.slack.com/services/XXX/YYY/ZZZ - + dashboards: - name: Network Overview panels: - title: Interface Utilization type: graph - metrics: + metrics: - interface_utilization - + - title: BGP Session Status type: state metrics: @@ -1240,4 +1248,3 @@ This architecture provides a robust, secure, and scalable network overlay that: 5. Leverages automation for deployment and management By combining the strengths of VyOS, WireGuard, EVPN, and L3VPN technologies, this design creates a network infrastructure that balances security, performance, and operational simplicity. -```bash diff --git a/docs/vyos-test-lab-setup.md b/docs/vyos-test-lab-setup.md index 7d652f0..72b0095 100644 --- a/docs/vyos-test-lab-setup.md +++ b/docs/vyos-test-lab-setup.md @@ -49,6 +49,7 @@ The lab will consist of: - **Network Configuration**: - Management network (172.27.0.0/16) + - Management network (172.27.0.0/16) - Backbone network (172.16.0.0/16) - Public IP space simulation (5.254.54.0/26) - Tenant space (100.64.0.0/16) @@ -57,6 +58,21 @@ The lab will consist of: We'll create two types of VyOS images: +1. **Base VyOS Image**: Minimal image with core functionality +2. **Provider Edge Router Image**: Pre-configured with L3VPN, EVPN, and WireGuard +- **Host Setup**: + - Arch Linux (as specified in your vyos-network-plan.md) + - systemd-vmspawn for container deployment + - Linux bridge setup for network connectivity +- **Network Configuration**: + - Management network (172.27.0.0/16) + - Backbone network (172.16.0.0/16) + - Public IP space simulation (5.254.54.0/26) + - Tenant space (100.64.0.0/16) + +### 2. VyOS Images + +We'll create two types of VyOS images: 1. **Base VyOS Image**: Minimal image with core functionality 2. **Provider Edge Router Image**: Pre-configured with L3VPN, EVPN, and WireGuard @@ -503,3 +519,10 @@ The following methods can be used to verify and troubleshoot the test environmen 3. Add CI/CD pipeline for continuous testing 4. Extend the lab with additional provider types (Proxmox) 5. Implement high availability scenarios +5. Implement high availability scenarios +1. Add support for Docker container deployment +2. Implement automated testing with the lab +3. Add CI/CD pipeline for continuous testing +4. Extend the lab with additional provider types (Proxmox) +5. Implement high availability scenarios +5. Implement high availability scenarios diff --git a/examples/validate-instance.ts b/examples/validate-instance.ts index a5d9b3a..bc252a6 100644 --- a/examples/validate-instance.ts +++ b/examples/validate-instance.ts @@ -16,6 +16,7 @@ const validInstance = { cpu: 2, memoryGb: 4, diskGb: 80, + diskGb: 80 }, networks: [ { @@ -24,6 +25,8 @@ const validInstance = { interface: 'eth0', mac: '00:0a:95:9d:68:16', }, + mac: '00:0a:95:9d:68:16' + } ], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -32,6 +35,8 @@ const validInstance = { application: 'web-api', owner: 'devops', }, + owner: 'devops' + } }; // Invalid instance data (missing required fields) @@ -49,6 +54,8 @@ const invalidInstance = { tags: { environment: 'production', }, + environment: 'production' + } }; // Function to validate instance data @@ -88,6 +95,7 @@ if (instance) { console.log(`RAM: ${instance.size.memoryGb} GB`); console.log(`Disk: ${instance.size.diskGb} GB`); + // Safe access to optional fields const primaryIp = instance.networks[0]?.ip || 'No IP assigned'; console.log(`Primary IP: ${primaryIp}`); @@ -108,3 +116,4 @@ if (isInstance(someData)) { } // Run this example with: bun run examples/validate-instance.ts +// Run this example with: bun run examples/validate-instance.ts diff --git a/package.json b/package.json index f3f6e00..02fcd86 100644 --- a/package.json +++ b/package.json @@ -54,3 +54,4 @@ "cli" ] } +} diff --git a/schema.ts b/schema.ts index 3b746f0..cd0771b 100644 --- a/schema.ts +++ b/schema.ts @@ -1,6 +1,9 @@ import { zodToOpenAPI } from '@asteasolutions/zod-to-openapi'; import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; import { z } from 'zod'; +import { z } from 'zod'; +import { zodToOpenAPI } from '@asteasolutions/zod-to-openapi'; +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; // Initialize the OpenAPI registry const registry = new OpenAPIRegistry(); @@ -22,6 +25,7 @@ export const InstanceStatusEnum = z.enum([ 'Restarting', 'Deleting', 'Unknown', + 'Unknown' ]); export type InstanceStatus = z.infer; @@ -33,6 +37,7 @@ export const VolumeStatusEnum = z.enum([ 'Deleting', 'Error', 'Unknown', + 'Unknown' ]); export type VolumeStatus = z.infer; @@ -43,6 +48,7 @@ export const VolumeTypeEnum = z.enum([ 'NVMe', 'HDD', 'Network', + 'Network' ]); export type VolumeType = z.infer; @@ -53,6 +59,7 @@ export const NetworkStatusEnum = z.enum([ 'Deleting', 'Error', 'Unknown', + 'Unknown' ]); export type NetworkStatus = z.infer; @@ -63,6 +70,7 @@ export const NetworkTypeEnum = z.enum([ 'Isolated', 'VXLAN', 'VPN', + 'VPN' ]); export type NetworkType = z.infer; @@ -259,7 +267,11 @@ export const NetworkSchema = z.object({ provider: ProviderTypeEnum, providerId: z.string(), region: z.string(), - cidr: z.string().regex(/^([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]{1,2}$/), + cidr: z + .string() + .regex( + /^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\/(3[0-2]|[12]?\d)$/ + ), networkType: NetworkTypeEnum, gateway: z.string().ip().optional(), dnsServers: z.array(z.string().ip()), @@ -972,6 +984,34 @@ export { VyOSCredentialsSchema, WireGuardConfigSchema, WireGuardPeerSchema, + ProviderTypeEnum, + InstanceStatusEnum, + VolumeStatusEnum, + VolumeTypeEnum, + NetworkStatusEnum, + NetworkTypeEnum, + ResourceLimitsSchema, + ProviderConfigSchema, + RegionSchema, + VyOSCredentialsSchema, + ProxmoxTokenAuthSchema, + ProxmoxUserPassAuthSchema, + ProxmoxCredentialsSchema, + ProviderCredentialsSchema, + InstanceSizeSchema, + InstanceNetworkSchema, + InstanceSchema, + CreateInstanceRequestSchema, + VolumeSchema, + CreateVolumeRequestSchema, + AttachVolumeRequestSchema, + IpAllocationSchema, + NetworkSchema, + CreateNetworkRequestSchema, + ConnectNetworkRequestSchema, + WireGuardPeerSchema, + WireGuardConfigSchema, + openApiSchema, }; // For backward compatibility @@ -1007,3 +1047,4 @@ export default { }, openApiSchema, }; +}; diff --git a/scripts/generateOpenApi.ts b/scripts/generateOpenApi.ts index 0ed84f9..9628c50 100644 --- a/scripts/generateOpenApi.ts +++ b/scripts/generateOpenApi.ts @@ -2,6 +2,10 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { openApiSchema } from '../schema'; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { openApiSchema } from "../schema"; // Get current file directory with ESM compatibility const __filename = fileURLToPath(import.meta.url); @@ -9,6 +13,8 @@ const __dirname = dirname(__filename); const OUTPUT_DIR = join(__dirname, '../api-docs'); const OUTPUT_FILE = join(OUTPUT_DIR, 'openapi.json'); +const OUTPUT_DIR = join(__dirname, "../api-docs"); +const OUTPUT_FILE = join(OUTPUT_DIR, "openapi.json"); // Create directory if it doesn't exist if (!existsSync(OUTPUT_DIR)) { @@ -22,11 +28,16 @@ try { console.log(`Successfully generated OpenAPI schema: ${OUTPUT_FILE}`); } catch (error) { console.error('Error generating OpenAPI schema:', error); + writeFileSync(OUTPUT_FILE, JSON.stringify(openApiSchema, null, 2), "utf8"); + console.log(`Successfully generated OpenAPI schema: ${OUTPUT_FILE}`); +} catch (error) { + console.error("Error generating OpenAPI schema:", error); process.exit(1); } // Generate a simple HTML to view the schema with Swagger UI const HTML_FILE = join(OUTPUT_DIR, 'index.html'); +const HTML_FILE = join(OUTPUT_DIR, "index.html"); const htmlContent = ` @@ -97,6 +108,7 @@ const htmlContent = ` try { writeFileSync(HTML_FILE, htmlContent, 'utf8'); + writeFileSync(HTML_FILE, htmlContent, "utf8"); console.log(`Successfully generated Swagger UI HTML: ${HTML_FILE}`); console.log(`Open ${HTML_FILE} in your browser to view the API documentation`); console.log(`Documentation links have been added to the UI:`); @@ -105,4 +117,5 @@ try { console.log(`- Architecture Design: docs/ARCHITECTURE_DESIGN.md`); } catch (error) { console.error('Error generating Swagger UI HTML:', error); + console.error("Error generating Swagger UI HTML:", error); } diff --git a/src/api/mod.rs b/src/api/mod.rs index 3cc2ece..c34e6c8 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,5 @@ -pub mod vyos; pub mod proxmox; +pub mod vyos; use anyhow::Result; @@ -7,13 +7,13 @@ use anyhow::Result; pub trait Provider { /// Connect to the provider fn connect(&self) -> Result<()>; - + /// Check connection status fn check_connection(&self) -> Result; - + /// Get provider name fn name(&self) -> &str; } /// Result type for provider operations -pub type ProviderResult = Result; \ No newline at end of file +pub type ProviderResult = Result; diff --git a/src/api/proxmox.rs b/src/api/proxmox.rs index 20ea66d..62f31b5 100644 --- a/src/api/proxmox.rs +++ b/src/api/proxmox.rs @@ -1,8 +1,8 @@ -use anyhow::{Result, Context, anyhow}; +use anyhow::{anyhow, Context, Result}; +use log::{debug, error, info}; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use std::time::Duration; -use log::{debug, error, info}; use crate::api::Provider; @@ -74,7 +74,7 @@ impl ProxmoxClient { connected: false, } } - + /// Initialize HTTP client fn init_http_client(&mut self) -> Result<()> { if self.http_client.is_none() { @@ -83,100 +83,137 @@ impl ProxmoxClient { .danger_accept_invalid_certs(!self.config.verify_ssl) .build() .context("Failed to build HTTP client")?; - + self.http_client = Some(client); } Ok(()) } - + /// Login to Proxmox and get authentication ticket pub async fn login(&mut self) -> Result<()> { // Ensure HTTP client is initialized self.init_http_client()?; - + let client = self.http_client.as_ref().unwrap(); - let url = format!("https://{}:{}/api2/json/access/ticket", self.config.host, self.config.port); - + let url = format!( + "https://{}:{}/api2/json/access/ticket", + self.config.host, self.config.port + ); + debug!("Logging in to Proxmox: {}", url); - + match &self.config.auth { - ProxmoxAuth::UserPass { username, password, realm } => { + ProxmoxAuth::UserPass { + username, + password, + realm, + } => { // Build form data for username/password auth let params = [ ("username", username.as_str()), ("password", password.as_str()), ("realm", realm.as_str()), ]; - - let response = client.post(&url) + + let response = client + .post(&url) .form(¶ms) .send() .await .context("Failed to send login request")?; - + if response.status().is_success() { - let json: serde_json::Value = response.json() + let json: serde_json::Value = response + .json() .await .context("Failed to parse login response")?; - + // Extract authentication data if let Some(data) = json.get("data") { - self.ticket = data.get("ticket").and_then(|v| v.as_str()).map(|s| s.to_string()); - self.csrf_token = data.get("CSRFPreventionToken").and_then(|v| v.as_str()).map(|s| s.to_string()); - + self.ticket = data + .get("ticket") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + self.csrf_token = data + .get("CSRFPreventionToken") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + if self.ticket.is_some() && self.csrf_token.is_some() { self.connected = true; info!("Successfully logged in to Proxmox: {}", self.config.host); return Ok(()); } } - + Err(anyhow!("Failed to extract auth data from login response")) } else { let status = response.status(); let body = response.text().await.unwrap_or_default(); Err(anyhow!("Login failed: {} - {}", status, body)) } - }, - ProxmoxAuth::ApiToken { token_id, token_secret } => { + } + ProxmoxAuth::ApiToken { + token_id, + token_secret, + } => { // API token auth doesn't need a login step, just verify we can access the API let auth_header = format!("PVEAPIToken={}={}", token_id, token_secret); - + // Test connection with a simple API call - let response = client.get(&format!("https://{}:{}/api2/json/version", self.config.host, self.config.port)) + let response = client + .get(&format!( + "https://{}:{}/api2/json/version", + self.config.host, self.config.port + )) .header("Authorization", auth_header) .send() .await .context("Failed to test API token authentication")?; - + if response.status().is_success() { self.connected = true; - info!("Successfully authenticated to Proxmox with API token: {}", self.config.host); + info!( + "Successfully authenticated to Proxmox with API token: {}", + self.config.host + ); Ok(()) } else { let status = response.status(); let body = response.text().await.unwrap_or_default(); - Err(anyhow!("API token authentication failed: {} - {}", status, body)) + Err(anyhow!( + "API token authentication failed: {} - {}", + status, + body + )) } } } } - + /// Make an API call to the Proxmox API - pub async fn api_call(&mut self, path: &str, method: &str, data: Option) -> Result { + pub async fn api_call( + &mut self, + path: &str, + method: &str, + data: Option, + ) -> Result { // Ensure we're authenticated if !self.connected { self.login().await?; } - + // Ensure HTTP client is initialized self.init_http_client()?; - + let client = self.http_client.as_ref().unwrap(); - let url = format!("https://{}:{}/api2/json/{}", self.config.host, self.config.port, path); - + let url = format!( + "https://{}:{}/api2/json/{}", + self.config.host, self.config.port, path + ); + debug!("Making API call: {} {}", method, url); - + let mut request_builder = match method { "GET" => client.get(&url), "POST" => client.post(&url), @@ -184,14 +221,15 @@ impl ProxmoxClient { "DELETE" => client.delete(&url), _ => return Err(anyhow!("Unsupported HTTP method: {}", method)), }; - + // Add authentication match &self.config.auth { ProxmoxAuth::UserPass { .. } => { // Cookie-based auth if let Some(ticket) = &self.ticket { - request_builder = request_builder.header("Cookie", format!("PVEAuthCookie={}", ticket)); - + request_builder = + request_builder.header("Cookie", format!("PVEAuthCookie={}", ticket)); + // Add CSRF token for non-GET requests if method != "GET" { if let Some(csrf) = &self.csrf_token { @@ -201,31 +239,36 @@ impl ProxmoxClient { } else { return Err(anyhow!("Not authenticated - missing ticket")); } - }, - ProxmoxAuth::ApiToken { token_id, token_secret } => { + } + ProxmoxAuth::ApiToken { + token_id, + token_secret, + } => { // API token auth let auth_header = format!("PVEAPIToken={}={}", token_id, token_secret); request_builder = request_builder.header("Authorization", auth_header); } } - + // Add JSON body if provided if let Some(json_data) = data { request_builder = request_builder.json(&json_data); } - + // Execute the request - let response = request_builder.send() + let response = request_builder + .send() .await .context("Failed to execute API request")?; - + let status = response.status(); - + if status.is_success() { - let body = response.json::() + let body = response + .json::() .await .context("Failed to parse API response")?; - + // Check for error in response body (Proxmox might return 200 OK with error in body) if let Some(data) = body.get("data") { Ok(data.clone()) @@ -237,55 +280,81 @@ impl ProxmoxClient { Err(anyhow!("API request failed: {} - {}", status, body)) } } - + /// Get cluster resources - pub async fn get_resources(&mut self, resource_type: Option<&str>) -> Result { + pub async fn get_resources( + &mut self, + resource_type: Option<&str>, + ) -> Result { let path = match resource_type { Some(rtype) => format!("cluster/resources?type={}", rtype), None => "cluster/resources".to_string(), }; - + self.api_call(&path, "GET", None).await } - + /// Get list of nodes in the cluster pub async fn get_nodes(&mut self) -> Result { self.api_call("nodes", "GET", None).await } - + /// Get list of VMs on a specific node pub async fn get_vms(&mut self, node: &str) -> Result { - self.api_call(&format!("nodes/{}/qemu", node), "GET", None).await + self.api_call(&format!("nodes/{}/qemu", node), "GET", None) + .await } - + /// Get VM status pub async fn get_vm_status(&mut self, node: &str, vmid: u64) -> Result { - self.api_call(&format!("nodes/{}/qemu/{}/status/current", node, vmid), "GET", None).await + self.api_call( + &format!("nodes/{}/qemu/{}/status/current", node, vmid), + "GET", + None, + ) + .await } - + /// Start a VM pub async fn start_vm(&mut self, node: &str, vmid: u64) -> Result { - self.api_call(&format!("nodes/{}/qemu/{}/status/start", node, vmid), "POST", None).await + self.api_call( + &format!("nodes/{}/qemu/{}/status/start", node, vmid), + "POST", + None, + ) + .await } - + /// Stop a VM pub async fn stop_vm(&mut self, node: &str, vmid: u64) -> Result { - self.api_call(&format!("nodes/{}/qemu/{}/status/stop", node, vmid), "POST", None).await + self.api_call( + &format!("nodes/{}/qemu/{}/status/stop", node, vmid), + "POST", + None, + ) + .await } - + /// Create a new VM - pub async fn create_vm(&mut self, node: &str, params: serde_json::Value) -> Result { - self.api_call(&format!("nodes/{}/qemu", node), "POST", Some(params)).await + pub async fn create_vm( + &mut self, + node: &str, + params: serde_json::Value, + ) -> Result { + self.api_call(&format!("nodes/{}/qemu", node), "POST", Some(params)) + .await } - + /// Delete a VM pub async fn delete_vm(&mut self, node: &str, vmid: u64) -> Result { - self.api_call(&format!("nodes/{}/qemu/{}", node, vmid), "DELETE", None).await + self.api_call(&format!("nodes/{}/qemu/{}", node, vmid), "DELETE", None) + .await } - + /// Get storage information pub async fn get_storage(&mut self, node: &str) -> Result { - self.api_call(&format!("nodes/{}/storage", node), "GET", None).await + self.api_call(&format!("nodes/{}/storage", node), "GET", None) + .await } } @@ -299,12 +368,12 @@ impl Provider for ProxmoxClient { Err(anyhow!("Not connected to Proxmox")) } } - + fn check_connection(&self) -> Result { Ok(self.connected) } - + fn name(&self) -> &str { "Proxmox" } -} \ No newline at end of file +} diff --git a/src/api/vyos.rs b/src/api/vyos.rs index 2e4d2de..54e649a 100644 --- a/src/api/vyos.rs +++ b/src/api/vyos.rs @@ -1,10 +1,10 @@ -use anyhow::{Result, Context, anyhow}; +use anyhow::{anyhow, Context, Result}; +use log::{debug, error, info}; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use std::process::Command; -use tokio::process::Command as AsyncCommand; use std::time::Duration; -use log::{debug, error, info}; +use tokio::process::Command as AsyncCommand; use crate::api::Provider; @@ -61,22 +61,24 @@ impl VyOSClient { connected: false, } } - + /// Execute a command over SSH pub async fn execute_ssh_command(&self, command: &str) -> Result { debug!("Executing SSH command: {}", command); - - let mut ssh_command = format!("ssh -o StrictHostKeyChecking=no -p {} {}@{}", - self.config.ssh_port, self.config.username, self.config.host); - + + let mut ssh_command = format!( + "ssh -o StrictHostKeyChecking=no -p {} {}@{}", + self.config.ssh_port, self.config.username, self.config.host + ); + // Add key if specified if let Some(key_path) = &self.config.key_path { ssh_command = format!("{} -i {}", ssh_command, key_path); } - + // Add the actual command ssh_command = format!("{} '{}'", ssh_command, command); - + // Execute the command let output = AsyncCommand::new("sh") .arg("-c") @@ -84,7 +86,7 @@ impl VyOSClient { .output() .await .context("Failed to execute SSH command")?; - + if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout).to_string(); debug!("SSH command output: {}", stdout); @@ -95,35 +97,45 @@ impl VyOSClient { Err(anyhow!("SSH command failed: {}", stderr)) } } - + /// Initialize HTTP client for API operations fn init_http_client(&mut self) -> Result<()> { if self.http_client.is_none() { let client = Client::builder() .timeout(Duration::from_secs(self.config.timeout)) - .danger_accept_invalid_certs(true) // VyOS might use self-signed certs .build() .context("Failed to build HTTP client")?; - + self.http_client = Some(client); } Ok(()) } - + /// Make an API call to the VyOS HTTP API - pub async fn api_call(&mut self, path: &str, method: &str, data: Option) -> Result { + pub async fn api_call( + &mut self, + path: &str, + method: &str, + data: Option, + ) -> Result { // Ensure HTTP client is initialized self.init_http_client()?; - + // Ensure API key is available - let api_key = self.config.api_key.clone() + let api_key = self + .config + .api_key + .clone() .ok_or_else(|| anyhow!("API key is required for HTTP API operations"))?; - + let client = self.http_client.as_ref().unwrap(); - let url = format!("https://{}:{}/api/{}", self.config.host, self.config.api_port, path); - + let url = format!( + "https://{}:{}/api/{}", + self.config.host, self.config.api_port, path + ); + debug!("Making API call: {} {}", method, url); - + let request_builder = match method { "GET" => client.get(&url), "POST" => client.post(&url), @@ -131,64 +143,73 @@ impl VyOSClient { "DELETE" => client.delete(&url), _ => return Err(anyhow!("Unsupported HTTP method: {}", method)), }; - + // Add API key header let request_builder = request_builder.header("X-API-Key", api_key); - + // Add JSON body if provided let request_builder = if let Some(json_data) = data { request_builder.json(&json_data) } else { request_builder }; - + // Execute the request - let response = request_builder.send() + let response = request_builder + .send() .await .context("Failed to execute API request")?; - + let status = response.status(); - let body = response.json::() + let body = response + .json::() .await .context("Failed to parse API response")?; - + if status.is_success() { Ok(body) } else { Err(anyhow!("API request failed: {} - {}", status, body)) } } - + /// Get configuration from VyOS pub async fn get_config(&mut self, path: &str) -> Result { - self.api_call(&format!("config/{}", path), "GET", None).await + self.api_call(&format!("config/{}", path), "GET", None) + .await } - + /// Set configuration in VyOS - pub async fn set_config(&mut self, path: &str, value: serde_json::Value) -> Result { - self.api_call(&format!("config/{}", path), "PUT", Some(value)).await + pub async fn set_config( + &mut self, + path: &str, + value: serde_json::Value, + ) -> Result { + self.api_call(&format!("config/{}", path), "PUT", Some(value)) + .await } - + /// Delete configuration in VyOS pub async fn delete_config(&mut self, path: &str) -> Result { - self.api_call(&format!("config/{}", path), "DELETE", None).await + self.api_call(&format!("config/{}", path), "DELETE", None) + .await } - + /// Commit configuration changes pub async fn commit(&mut self) -> Result { self.api_call("commit", "POST", None).await } - + /// Save configuration pub async fn save(&mut self) -> Result { self.api_call("save", "POST", None).await } - + /// Check if connected to VyOS pub fn is_connected(&self) -> bool { self.connected } - + /// Get system information pub async fn get_system_info(&mut self) -> Result { self.api_call("system", "GET", None).await @@ -200,22 +221,22 @@ impl Provider for VyOSClient { // Synchronous version for the Provider trait let mut cmd = Command::new("ssh"); cmd.arg("-o") - .arg("StrictHostKeyChecking=no") - .arg("-p") - .arg(self.config.ssh_port.to_string()) - .arg(format!("{}@{}", self.config.username, self.config.host)) - .arg("show system version"); - + .arg("StrictHostKeyChecking=no") + .arg("-p") + .arg(self.config.ssh_port.to_string()) + .arg(format!("{}@{}", self.config.username, self.config.host)) + .arg("show system version"); + // Add key if specified if let Some(key_path) = &self.config.key_path { cmd.arg("-i").arg(key_path); } - + let output = cmd.output().context("Failed to execute SSH command")?; - + if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); - + if stdout.contains("VyOS") { info!("Successfully connected to VyOS: {}", self.config.host); // We would set self.connected = true here, but self is immutable @@ -229,14 +250,14 @@ impl Provider for VyOSClient { Err(anyhow!("Failed to connect to VyOS: {}", stderr)) } } - + fn check_connection(&self) -> Result { // For simplicity, just check if we're marked as connected // In a real implementation, we'd do a lightweight check Ok(self.connected) } - + fn name(&self) -> &str { "VyOS" } -} \ No newline at end of file +} diff --git a/src/app.rs b/src/app.rs index 598a20c..78692e8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -75,7 +75,7 @@ impl Default for App { memory_gb: 4, disk_gb: 80, }); - + instances.push(Instance { id: "i-89abcdef".to_string(), name: "db-1".to_string(), @@ -87,7 +87,7 @@ impl Default for App { memory_gb: 16, disk_gb: 160, }); - + let mut volumes = Vec::new(); volumes.push(Volume { id: "vol-01234567".to_string(), @@ -96,7 +96,7 @@ impl Default for App { attached_to: Some("i-89abcdef".to_string()), region: "nyc".to_string(), }); - + let mut networks = Vec::new(); networks.push(Network { id: "net-01234567".to_string(), @@ -104,7 +104,7 @@ impl Default for App { cidr: "192.168.1.0/24".to_string(), instances: vec!["i-01234567".to_string(), "i-89abcdef".to_string()], }); - + Self { running: true, mode: AppMode::Home, @@ -129,7 +129,7 @@ impl App { pub fn quit(&mut self) { self.running = false; } - + pub fn next_item(&mut self) { let max_index = match self.mode { AppMode::Instances => self.instances.len().saturating_sub(1), @@ -137,7 +137,7 @@ impl App { AppMode::Networks => self.networks.len().saturating_sub(1), _ => 0, }; - + if max_index > 0 { self.selected_index = if self.selected_index >= max_index { 0 @@ -146,7 +146,7 @@ impl App { }; } } - + pub fn previous_item(&mut self) { let max_index = match self.mode { AppMode::Instances => self.instances.len().saturating_sub(1), @@ -154,7 +154,7 @@ impl App { AppMode::Networks => self.networks.len().saturating_sub(1), _ => 0, }; - + if max_index > 0 { self.selected_index = if self.selected_index == 0 { max_index @@ -163,7 +163,7 @@ impl App { }; } } - + pub fn change_mode(&mut self, mode: AppMode) { self.mode = mode; self.selected_index = 0; diff --git a/src/config/credentials.rs b/src/config/credentials.rs index 1bda418..5771399 100644 --- a/src/config/credentials.rs +++ b/src/config/credentials.rs @@ -1,7 +1,7 @@ +use anyhow::{anyhow, Context, Result}; +use log::{debug, error, info}; use serde::{Deserialize, Serialize}; -use anyhow::{Result, Context, anyhow}; use std::collections::HashMap; -use log::{debug, info, error}; use crate::config::{read_config_file, write_config_file, CREDENTIALS_FILE}; use crate::models::provider::ProviderType; @@ -84,7 +84,7 @@ impl Credentials { /// Load credentials from file pub fn load() -> Result { debug!("Loading credentials from file"); - + // Read credentials file let content = match read_config_file(CREDENTIALS_FILE) { Ok(content) => content, @@ -93,30 +93,29 @@ impl Credentials { return Ok(Self::default()); } }; - + // Parse TOML - let credentials: Credentials = toml::from_str(&content) - .context("Failed to parse credentials TOML")?; - + let credentials: Credentials = + toml::from_str(&content).context("Failed to parse credentials TOML")?; + Ok(credentials) } - + /// Save credentials to file pub fn save(&self) -> Result<()> { debug!("Saving credentials to file"); - + // Serialize to TOML - let content = toml::to_string_pretty(self) - .context("Failed to serialize credentials")?; - + let content = toml::to_string_pretty(self).context("Failed to serialize credentials")?; + // Write to file write_config_file(CREDENTIALS_FILE, &content) .context("Failed to write credentials file")?; - + info!("Credentials saved successfully"); Ok(()) } - + /// Add VyOS credentials pub fn add_vyos_credentials( &mut self, @@ -136,12 +135,13 @@ impl Credentials { ssh_port, api_port, }; - - self.credentials.insert(provider_name.to_string(), ProviderCredentials::VyOS(creds)); + + self.credentials + .insert(provider_name.to_string(), ProviderCredentials::VyOS(creds)); info!("Added VyOS credentials for provider: {}", provider_name); Ok(()) } - + /// Add Proxmox token credentials pub fn add_proxmox_token_credentials( &mut self, @@ -155,7 +155,7 @@ impl Credentials { token_id: token_id.to_string(), token_secret: token_secret.to_string(), }; - + let creds = ProxmoxCredentials { port, use_token_auth: true, @@ -163,12 +163,18 @@ impl Credentials { user_pass_auth: None, verify_ssl, }; - - self.credentials.insert(provider_name.to_string(), ProviderCredentials::Proxmox(creds)); - info!("Added Proxmox token credentials for provider: {}", provider_name); + + self.credentials.insert( + provider_name.to_string(), + ProviderCredentials::Proxmox(creds), + ); + info!( + "Added Proxmox token credentials for provider: {}", + provider_name + ); Ok(()) } - + /// Add Proxmox username/password credentials pub fn add_proxmox_user_pass_credentials( &mut self, @@ -184,7 +190,7 @@ impl Credentials { password: password.to_string(), realm: realm.to_string(), }; - + let creds = ProxmoxCredentials { port, use_token_auth: false, @@ -192,43 +198,64 @@ impl Credentials { user_pass_auth: Some(user_pass_auth), verify_ssl, }; - - self.credentials.insert(provider_name.to_string(), ProviderCredentials::Proxmox(creds)); - info!("Added Proxmox user/pass credentials for provider: {}", provider_name); + + self.credentials.insert( + provider_name.to_string(), + ProviderCredentials::Proxmox(creds), + ); + info!( + "Added Proxmox user/pass credentials for provider: {}", + provider_name + ); Ok(()) } - + /// Remove credentials for a provider pub fn remove_credentials(&mut self, provider_name: &str) -> Result<()> { if !self.credentials.contains_key(provider_name) { - return Err(anyhow!("Credentials for provider '{}' do not exist", provider_name)); + return Err(anyhow!( + "Credentials for provider '{}' do not exist", + provider_name + )); } - + self.credentials.remove(provider_name); info!("Removed credentials for provider: {}", provider_name); Ok(()) } - + /// Get credentials for a provider pub fn get_credentials(&self, provider_name: &str) -> Option<&ProviderCredentials> { self.credentials.get(provider_name) } - + /// Get VyOS credentials for a provider pub fn get_vyos_credentials(&self, provider_name: &str) -> Result<&VyOSCredentials> { match self.credentials.get(provider_name) { Some(ProviderCredentials::VyOS(creds)) => Ok(creds), - Some(_) => Err(anyhow!("Provider '{}' does not have VyOS credentials", provider_name)), - None => Err(anyhow!("No credentials found for provider '{}'", provider_name)), + Some(_) => Err(anyhow!( + "Provider '{}' does not have VyOS credentials", + provider_name + )), + None => Err(anyhow!( + "No credentials found for provider '{}'", + provider_name + )), } } - + /// Get Proxmox credentials for a provider pub fn get_proxmox_credentials(&self, provider_name: &str) -> Result<&ProxmoxCredentials> { match self.credentials.get(provider_name) { Some(ProviderCredentials::Proxmox(creds)) => Ok(creds), - Some(_) => Err(anyhow!("Provider '{}' does not have Proxmox credentials", provider_name)), - None => Err(anyhow!("No credentials found for provider '{}'", provider_name)), + Some(_) => Err(anyhow!( + "Provider '{}' does not have Proxmox credentials", + provider_name + )), + None => Err(anyhow!( + "No credentials found for provider '{}'", + provider_name + )), } } -} \ No newline at end of file +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 702d61d..e3ec110 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,12 +1,12 @@ +pub mod credentials; pub mod provider; pub mod settings; -pub mod credentials; -use std::path::{Path, PathBuf}; -use anyhow::{Result, Context, anyhow}; -use log::{debug, info, error}; -use std::fs; +use anyhow::{anyhow, Context, Result}; use dirs::home_dir; +use log::{debug, error, info}; +use std::fs; +use std::path::{Path, PathBuf}; /// Constants pub const APP_DIR_NAME: &str = ".bbctl"; @@ -18,14 +18,13 @@ pub const CREDENTIALS_FILE: &str = "credentials.toml"; pub fn get_config_dir() -> Result { let home = home_dir().ok_or_else(|| anyhow!("Failed to determine home directory"))?; let config_dir = home.join(APP_DIR_NAME); - + // Create the directory if it doesn't exist if !config_dir.exists() { debug!("Creating config directory: {}", config_dir.display()); - fs::create_dir_all(&config_dir) - .context("Failed to create config directory")?; + fs::create_dir_all(&config_dir).context("Failed to create config directory")?; } - + Ok(config_dir) } @@ -47,25 +46,25 @@ pub fn read_config_file(file_name: &str) -> Result { if !path.exists() { return Err(anyhow!("Config file does not exist: {}", path.display())); } - - fs::read_to_string(&path) - .context(format!("Failed to read config file: {}", path.display())) + + fs::read_to_string(&path).context(format!("Failed to read config file: {}", path.display())) } /// Write a string to a config file pub fn write_config_file(file_name: &str, content: &str) -> Result<()> { let path = get_config_file(file_name)?; - + // Ensure the directory exists if let Some(parent) = path.parent() { if !parent.exists() { - fs::create_dir_all(parent) - .context(format!("Failed to create parent directory: {}", parent.display()))?; + fs::create_dir_all(parent).context(format!( + "Failed to create parent directory: {}", + parent.display() + ))?; } } - - fs::write(&path, content) - .context(format!("Failed to write config file: {}", path.display())) + + fs::write(&path, content).context(format!("Failed to write config file: {}", path.display())) } /// Delete a config file @@ -83,7 +82,7 @@ pub fn init_config() -> Result<()> { // Create config directory let config_dir = get_config_dir()?; info!("Initializing configuration in: {}", config_dir.display()); - + // Create default settings file if it doesn't exist let settings_path = config_dir.join(SETTINGS_FILE); if !settings_path.exists() { @@ -91,21 +90,25 @@ pub fn init_config() -> Result<()> { let default_settings = settings::Settings::default(); let toml = toml::to_string_pretty(&default_settings) .context("Failed to serialize default settings")?; - fs::write(&settings_path, toml) - .context(format!("Failed to write settings file: {}", settings_path.display()))?; + fs::write(&settings_path, toml).context(format!( + "Failed to write settings file: {}", + settings_path.display() + ))?; } - + // Create empty providers file if it doesn't exist let providers_path = config_dir.join(PROVIDERS_FILE); if !providers_path.exists() { debug!("Creating empty providers file"); let providers = provider::Providers::default(); - let toml = toml::to_string_pretty(&providers) - .context("Failed to serialize empty providers")?; - fs::write(&providers_path, toml) - .context(format!("Failed to write providers file: {}", providers_path.display()))?; + let toml = + toml::to_string_pretty(&providers).context("Failed to serialize empty providers")?; + fs::write(&providers_path, toml).context(format!( + "Failed to write providers file: {}", + providers_path.display() + ))?; } - + // Create empty credentials file if it doesn't exist let credentials_path = config_dir.join(CREDENTIALS_FILE); if !credentials_path.exists() { @@ -113,10 +116,12 @@ pub fn init_config() -> Result<()> { let credentials = credentials::Credentials::default(); let toml = toml::to_string_pretty(&credentials) .context("Failed to serialize empty credentials")?; - fs::write(&credentials_path, toml) - .context(format!("Failed to write credentials file: {}", credentials_path.display()))?; + fs::write(&credentials_path, toml).context(format!( + "Failed to write credentials file: {}", + credentials_path.display() + ))?; } - + info!("Configuration initialized successfully"); Ok(()) -} \ No newline at end of file +} diff --git a/src/config/provider.rs b/src/config/provider.rs index 1b53a35..449f58e 100644 --- a/src/config/provider.rs +++ b/src/config/provider.rs @@ -1,10 +1,10 @@ +use anyhow::{anyhow, Context, Result}; +use log::{debug, error, info}; use serde::{Deserialize, Serialize}; -use anyhow::{Result, Context, anyhow}; use std::collections::HashMap; -use log::{debug, info, error}; use crate::config::{read_config_file, write_config_file, PROVIDERS_FILE}; -use crate::models::provider::{ProviderType, ProviderConfig, Region}; +use crate::models::provider::{ProviderConfig, ProviderType, Region}; /// Provider configuration storage #[derive(Debug, Clone, Serialize, Deserialize)] @@ -28,7 +28,7 @@ impl Providers { /// Load providers from file pub fn load() -> Result { debug!("Loading providers from file"); - + // Read providers file let content = match read_config_file(PROVIDERS_FILE) { Ok(content) => content, @@ -37,115 +37,125 @@ impl Providers { return Ok(Self::default()); } }; - + // Parse TOML - let providers: Providers = toml::from_str(&content) - .context("Failed to parse providers TOML")?; - + let providers: Providers = + toml::from_str(&content).context("Failed to parse providers TOML")?; + Ok(providers) } - + /// Save providers to file pub fn save(&self) -> Result<()> { debug!("Saving providers to file"); - + // Serialize to TOML - let content = toml::to_string_pretty(self) - .context("Failed to serialize providers")?; - + let content = toml::to_string_pretty(self).context("Failed to serialize providers")?; + // Write to file - write_config_file(PROVIDERS_FILE, &content) - .context("Failed to write providers file")?; - + write_config_file(PROVIDERS_FILE, &content).context("Failed to write providers file")?; + info!("Providers saved successfully"); Ok(()) } - + /// Add a new provider - pub fn add_provider(&mut self, name: &str, provider_type: ProviderType, host: &str, params: HashMap) -> Result<()> { + pub fn add_provider( + &mut self, + name: &str, + provider_type: ProviderType, + host: &str, + params: HashMap, + ) -> Result<()> { if self.providers.contains_key(name) { return Err(anyhow!("Provider with name '{}' already exists", name)); } - + let config = ProviderConfig { provider_type, name: name.to_string(), host: host.to_string(), params, }; - + self.providers.insert(name.to_string(), config); info!("Added provider: {}", name); Ok(()) } - + /// Remove a provider pub fn remove_provider(&mut self, name: &str) -> Result<()> { if !self.providers.contains_key(name) { return Err(anyhow!("Provider with name '{}' does not exist", name)); } - + self.providers.remove(name); - + // Also remove any regions that belong to this provider - self.regions.retain(|_, region| region.provider.to_string() != name); - + self.regions + .retain(|_, region| region.provider.to_string() != name); + info!("Removed provider: {}", name); Ok(()) } - + /// Add a new region pub fn add_region(&mut self, region: Region) -> Result<()> { if self.regions.contains_key(®ion.id) { return Err(anyhow!("Region with ID '{}' already exists", region.id)); } - + // Ensure the provider exists let provider_name = region.provider.to_string(); - if !self.providers.iter().any(|(name, p)| p.provider_type == region.provider) { + if !self + .providers + .iter() + .any(|(name, p)| p.provider_type == region.provider) + { return Err(anyhow!("Provider '{}' does not exist", provider_name)); } - + self.regions.insert(region.id.clone(), region.clone()); info!("Added region: {}", region.id); Ok(()) } - + /// Remove a region pub fn remove_region(&mut self, id: &str) -> Result<()> { if !self.regions.contains_key(id) { return Err(anyhow!("Region with ID '{}' does not exist", id)); } - + self.regions.remove(id); info!("Removed region: {}", id); Ok(()) } - + /// Get provider by name pub fn get_provider(&self, name: &str) -> Option<&ProviderConfig> { self.providers.get(name) } - + /// Get region by ID pub fn get_region(&self, id: &str) -> Option<&Region> { self.regions.get(id) } - + /// Get regions by provider pub fn get_regions_by_provider(&self, provider_type: ProviderType) -> Vec<&Region> { - self.regions.values() + self.regions + .values() .filter(|r| r.provider == provider_type) .collect() } - + /// Get all providers pub fn get_all_providers(&self) -> &HashMap { &self.providers } - + /// Get all regions pub fn get_all_regions(&self) -> &HashMap { &self.regions } -} \ No newline at end of file +} diff --git a/src/config/settings.rs b/src/config/settings.rs index 7ea0149..3336a0d 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -1,7 +1,7 @@ +use anyhow::{anyhow, Context, Result}; +use log::{debug, error, info}; use serde::{Deserialize, Serialize}; -use anyhow::{Result, Context, anyhow}; use std::fs; -use log::{debug, info, error}; use crate::config::{read_config_file, write_config_file, SETTINGS_FILE}; @@ -48,7 +48,7 @@ impl Settings { /// Load settings from file pub fn load() -> Result { debug!("Loading settings from file"); - + // Read settings file let content = match read_config_file(SETTINGS_FILE) { Ok(content) => content, @@ -57,75 +57,83 @@ impl Settings { return Ok(Self::default()); } }; - + // Parse TOML - let settings: Settings = toml::from_str(&content) - .context("Failed to parse settings TOML")?; - + let settings: Settings = + toml::from_str(&content).context("Failed to parse settings TOML")?; + Ok(settings) } - + /// Save settings to file pub fn save(&self) -> Result<()> { debug!("Saving settings to file"); - + // Serialize to TOML - let content = toml::to_string_pretty(self) - .context("Failed to serialize settings")?; - + let content = toml::to_string_pretty(self).context("Failed to serialize settings")?; + // Write to file - write_config_file(SETTINGS_FILE, &content) - .context("Failed to write settings file")?; - + write_config_file(SETTINGS_FILE, &content).context("Failed to write settings file")?; + info!("Settings saved successfully"); Ok(()) } - + /// Update a setting pub fn update(&mut self, key: &str, value: &str) -> Result<()> { match key { "default_provider" => { self.default_provider = Some(value.to_string()); - }, + } "default_region" => { self.default_region = Some(value.to_string()); - }, + } "telemetry_enabled" => { - self.telemetry_enabled = value.parse::() + self.telemetry_enabled = value + .parse::() .context("Invalid boolean value for telemetry_enabled")?; - }, + } "auto_update_enabled" => { - self.auto_update_enabled = value.parse::() + self.auto_update_enabled = value + .parse::() .context("Invalid boolean value for auto_update_enabled")?; - }, + } "colors_enabled" => { - self.colors_enabled = value.parse::() + self.colors_enabled = value + .parse::() .context("Invalid boolean value for colors_enabled")?; - }, + } "default_cpu" => { - self.default_cpu = value.parse::() + self.default_cpu = value + .parse::() .context("Invalid value for default_cpu")?; - }, + } "default_memory_gb" => { - self.default_memory_gb = value.parse::() + self.default_memory_gb = value + .parse::() .context("Invalid value for default_memory_gb")?; - }, + } "default_disk_gb" => { - self.default_disk_gb = value.parse::() + self.default_disk_gb = value + .parse::() .context("Invalid value for default_disk_gb")?; - }, + } "log_level" => { // Validate log level match value.to_lowercase().as_str() { "trace" | "debug" | "info" | "warn" | "error" => { self.log_level = value.to_lowercase(); - }, - _ => return Err(anyhow!("Invalid log level. Use: trace, debug, info, warn, error")), + } + _ => { + return Err(anyhow!( + "Invalid log level. Use: trace, debug, info, warn, error" + )) + } } - }, + } _ => return Err(anyhow!("Unknown setting: {}", key)), } - + Ok(()) } -} \ No newline at end of file +} diff --git a/src/handler.rs b/src/handler.rs index 4ed5b8b..148ac45 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -25,8 +25,8 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { KeyCode::Up | KeyCode::Char('k') => { app.previous_item(); } - - // Mode switching + + // Mode switching KeyCode::Char('1') => { app.change_mode(AppMode::Home); } @@ -45,31 +45,27 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { KeyCode::Char('?') => { app.change_mode(AppMode::Help); } - + // Tab navigation - KeyCode::Tab => { - match app.mode { - AppMode::Home => app.change_mode(AppMode::Instances), - AppMode::Instances => app.change_mode(AppMode::Volumes), - AppMode::Volumes => app.change_mode(AppMode::Networks), - AppMode::Networks => app.change_mode(AppMode::Settings), - AppMode::Settings => app.change_mode(AppMode::Help), - AppMode::Help => app.change_mode(AppMode::Home), - } - } - + KeyCode::Tab => match app.mode { + AppMode::Home => app.change_mode(AppMode::Instances), + AppMode::Instances => app.change_mode(AppMode::Volumes), + AppMode::Volumes => app.change_mode(AppMode::Networks), + AppMode::Networks => app.change_mode(AppMode::Settings), + AppMode::Settings => app.change_mode(AppMode::Help), + AppMode::Help => app.change_mode(AppMode::Home), + }, + // Shift+Tab for reverse navigation - KeyCode::BackTab => { - match app.mode { - AppMode::Home => app.change_mode(AppMode::Help), - AppMode::Instances => app.change_mode(AppMode::Home), - AppMode::Volumes => app.change_mode(AppMode::Instances), - AppMode::Networks => app.change_mode(AppMode::Volumes), - AppMode::Settings => app.change_mode(AppMode::Networks), - AppMode::Help => app.change_mode(AppMode::Settings), - } - } - + KeyCode::BackTab => match app.mode { + AppMode::Home => app.change_mode(AppMode::Help), + AppMode::Instances => app.change_mode(AppMode::Home), + AppMode::Volumes => app.change_mode(AppMode::Instances), + AppMode::Networks => app.change_mode(AppMode::Volumes), + AppMode::Settings => app.change_mode(AppMode::Networks), + AppMode::Help => app.change_mode(AppMode::Settings), + }, + // Other handlers _ => {} } diff --git a/src/lib.rs b/src/lib.rs index c42b758..3f009f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,20 +1,20 @@ +pub mod api; pub mod app; +pub mod config; pub mod event; pub mod handler; -pub mod tui; -pub mod ui; -pub mod api; pub mod models; -pub mod config; pub mod services; +pub mod tui; +pub mod ui; // Re-export commonly used types pub use app::AppResult; -pub use models::provider::ProviderType; pub use models::instance::InstanceStatus; -pub use models::volume::VolumeStatus; pub use models::network::NetworkStatus; +pub use models::provider::ProviderType; +pub use models::volume::VolumeStatus; // Version information pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); \ No newline at end of file +pub const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); diff --git a/src/main.rs b/src/main.rs index 8c8517e..e3656cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -use std::io; use std::env; +use std::io; use clap::{Parser, Subcommand}; use ratatui::{backend::CrosstermBackend, Terminal}; @@ -11,15 +11,15 @@ use crate::{ tui::Tui, }; +pub mod api; pub mod app; +pub mod config; pub mod event; pub mod handler; -pub mod tui; -pub mod ui; -pub mod api; pub mod models; -pub mod config; pub mod services; +pub mod tui; +pub mod ui; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -60,23 +60,23 @@ enum Commands { /// VyOS host to connect to #[arg(long, default_value = "5.254.54.3")] host: String, - + /// VyOS SSH port #[arg(long, default_value = "60022")] port: u16, - + /// VyOS username #[arg(long, default_value = "vyos")] username: String, - + /// VyOS password (optional) #[arg(long)] password: Option, - + /// Path to SSH key (optional) #[arg(long)] key_path: Option, - + /// API key for HTTP API (optional) #[arg(long)] api_key: Option, @@ -102,21 +102,13 @@ enum InstancesCommands { disk: Option, }, /// Delete an instance - Delete { - id: String, - }, + Delete { id: String }, /// Start an instance - Start { - id: String, - }, + Start { id: String }, /// Stop an instance - Stop { - id: String, - }, + Stop { id: String }, /// Get instance details - Show { - id: String, - }, + Show { id: String }, } #[derive(Subcommand)] @@ -132,9 +124,7 @@ enum VolumesCommands { region: Option, }, /// Delete a volume - Delete { - id: String, - }, + Delete { id: String }, /// Attach a volume to an instance Attach { id: String, @@ -142,13 +132,9 @@ enum VolumesCommands { instance: String, }, /// Detach a volume from an instance - Detach { - id: String, - }, + Detach { id: String }, /// Get volume details - Show { - id: String, - }, + Show { id: String }, } #[derive(Subcommand)] @@ -162,9 +148,7 @@ enum NetworksCommands { cidr: String, }, /// Delete a network - Delete { - id: String, - }, + Delete { id: String }, /// Connect an instance to a network Connect { id: String, @@ -178,119 +162,140 @@ enum NetworksCommands { instance: String, }, /// Get network details - Show { - id: String, - }, + Show { id: String }, } fn cli_handler(cli: Cli) -> AppResult<()> { match cli.command { Some(Commands::Init { name }) => { - println!("Initializing BitBuilder Cloud project: {}", - name.unwrap_or_else(|| "bitbuilder-app".to_string())); + println!( + "Initializing BitBuilder Cloud project: {}", + name.unwrap_or_else(|| "bitbuilder-app".to_string()) + ); // Actual implementation would initialize config files, etc. } Some(Commands::Deploy { config }) => { - println!("Deploying to BitBuilder Cloud using config: {}", - config.unwrap_or_else(|| "fly.toml".to_string())); + println!( + "Deploying to BitBuilder Cloud using config: {}", + config.unwrap_or_else(|| "fly.toml".to_string()) + ); // Actual implementation would handle the deployment } - Some(Commands::Instances { action }) => { - match action { - InstancesCommands::List => { - println!("Listing instances..."); - println!("ID\t\tNAME\tSTATUS\tREGION\tPROVIDER"); - println!("i-01234567\tweb-1\trunning\tnyc\tvyos"); - println!("i-89abcdef\tdb-1\trunning\tnyc\tproxmox"); - } - InstancesCommands::Create { name, provider, region, cpu, memory, disk } => { - println!("Creating instance '{}' with provider '{}' in region '{}'", - name, provider, region); - println!("Resources: CPU: {}, Memory: {} GB, Disk: {} GB", - cpu.unwrap_or(1), memory.unwrap_or(2), disk.unwrap_or(10)); - } - InstancesCommands::Delete { id } => { - println!("Deleting instance '{}'", id); - } - InstancesCommands::Start { id } => { - println!("Starting instance '{}'", id); - } - InstancesCommands::Stop { id } => { - println!("Stopping instance '{}'", id); - } - InstancesCommands::Show { id } => { - println!("Instance details for '{}':", id); - println!("ID: {}", id); - println!("Name: web-1"); - println!("Status: running"); - println!("Provider: vyos"); - println!("Region: nyc"); - println!("IP: 192.168.1.10"); - println!("CPU: 2"); - println!("Memory: 4 GB"); - println!("Disk: 80 GB"); - } + Some(Commands::Instances { action }) => match action { + InstancesCommands::List => { + println!("Listing instances..."); + println!("ID\t\tNAME\tSTATUS\tREGION\tPROVIDER"); + println!("i-01234567\tweb-1\trunning\tnyc\tvyos"); + println!("i-89abcdef\tdb-1\trunning\tnyc\tproxmox"); } - } - Some(Commands::Volumes { action }) => { - match action { - VolumesCommands::List => { - println!("Listing volumes..."); - println!("ID\t\tNAME\tSIZE\tREGION\tATTACHED TO"); - println!("vol-01234567\tdb-data\t100 GB\tnyc\ti-89abcdef"); - } - VolumesCommands::Create { name, size, region } => { - println!("Creating volume '{}' with size {} GB in region '{}'", - name, size, region.unwrap_or_else(|| "nyc".to_string())); - } - VolumesCommands::Delete { id } => { - println!("Deleting volume '{}'", id); - } - VolumesCommands::Attach { id, instance } => { - println!("Attaching volume '{}' to instance '{}'", id, instance); - } - VolumesCommands::Detach { id } => { - println!("Detaching volume '{}'", id); - } - VolumesCommands::Show { id } => { - println!("Volume details for '{}':", id); - println!("ID: {}", id); - println!("Name: db-data"); - println!("Size: 100 GB"); - println!("Region: nyc"); - println!("Attached to: i-89abcdef (db-1)"); - } + InstancesCommands::Create { + name, + provider, + region, + cpu, + memory, + disk, + } => { + println!( + "Creating instance '{}' with provider '{}' in region '{}'", + name, provider, region + ); + println!( + "Resources: CPU: {}, Memory: {} GB, Disk: {} GB", + cpu.unwrap_or(1), + memory.unwrap_or(2), + disk.unwrap_or(10) + ); } - } - Some(Commands::Networks { action }) => { - match action { - NetworksCommands::List => { - println!("Listing networks..."); - println!("ID\t\tNAME\tCIDR\t\tINSTANCES"); - println!("net-01234567\tdefault\t192.168.1.0/24\t2"); - } - NetworksCommands::Create { name, cidr } => { - println!("Creating network '{}' with CIDR '{}'", name, cidr); - } - NetworksCommands::Delete { id } => { - println!("Deleting network '{}'", id); - } - NetworksCommands::Connect { id, instance } => { - println!("Connecting instance '{}' to network '{}'", instance, id); - } - NetworksCommands::Disconnect { id, instance } => { - println!("Disconnecting instance '{}' from network '{}'", instance, id); - } - NetworksCommands::Show { id } => { - println!("Network details for '{}':", id); - println!("ID: {}", id); - println!("Name: default"); - println!("CIDR: 192.168.1.0/24"); - println!("Instances: i-01234567 (web-1), i-89abcdef (db-1)"); - } + InstancesCommands::Delete { id } => { + println!("Deleting instance '{}'", id); } - } - Some(Commands::TestVyOS { host, port, username, .. }) => { + InstancesCommands::Start { id } => { + println!("Starting instance '{}'", id); + } + InstancesCommands::Stop { id } => { + println!("Stopping instance '{}'", id); + } + InstancesCommands::Show { id } => { + println!("Instance details for '{}':", id); + println!("ID: {}", id); + println!("Name: web-1"); + println!("Status: running"); + println!("Provider: vyos"); + println!("Region: nyc"); + println!("IP: 192.168.1.10"); + println!("CPU: 2"); + println!("Memory: 4 GB"); + println!("Disk: 80 GB"); + } + }, + Some(Commands::Volumes { action }) => match action { + VolumesCommands::List => { + println!("Listing volumes..."); + println!("ID\t\tNAME\tSIZE\tREGION\tATTACHED TO"); + println!("vol-01234567\tdb-data\t100 GB\tnyc\ti-89abcdef"); + } + VolumesCommands::Create { name, size, region } => { + println!( + "Creating volume '{}' with size {} GB in region '{}'", + name, + size, + region.unwrap_or_else(|| "nyc".to_string()) + ); + } + VolumesCommands::Delete { id } => { + println!("Deleting volume '{}'", id); + } + VolumesCommands::Attach { id, instance } => { + println!("Attaching volume '{}' to instance '{}'", id, instance); + } + VolumesCommands::Detach { id } => { + println!("Detaching volume '{}'", id); + } + VolumesCommands::Show { id } => { + println!("Volume details for '{}':", id); + println!("ID: {}", id); + println!("Name: db-data"); + println!("Size: 100 GB"); + println!("Region: nyc"); + println!("Attached to: i-89abcdef (db-1)"); + } + }, + Some(Commands::Networks { action }) => match action { + NetworksCommands::List => { + println!("Listing networks..."); + println!("ID\t\tNAME\tCIDR\t\tINSTANCES"); + println!("net-01234567\tdefault\t192.168.1.0/24\t2"); + } + NetworksCommands::Create { name, cidr } => { + println!("Creating network '{}' with CIDR '{}'", name, cidr); + } + NetworksCommands::Delete { id } => { + println!("Deleting network '{}'", id); + } + NetworksCommands::Connect { id, instance } => { + println!("Connecting instance '{}' to network '{}'", instance, id); + } + NetworksCommands::Disconnect { id, instance } => { + println!( + "Disconnecting instance '{}' from network '{}'", + instance, id + ); + } + NetworksCommands::Show { id } => { + println!("Network details for '{}':", id); + println!("ID: {}", id); + println!("Name: default"); + println!("CIDR: 192.168.1.0/24"); + println!("Instances: i-01234567 (web-1), i-89abcdef (db-1)"); + } + }, + Some(Commands::TestVyOS { + host, + port, + username, + .. + }) => { // This would block, so we need to call it outside the CLI handler // Will be implemented in main() return Err("Use tokio runtime to test VyOS connectivity".into()); @@ -301,7 +306,7 @@ fn cli_handler(cli: Cli) -> AppResult<()> { return Ok(()); } } - + Ok(()) } @@ -338,27 +343,34 @@ async fn run_tui() -> AppResult<()> { async fn main() -> AppResult<()> { // Setup logging env_logger::init(); - + // Initialize configuration if let Err(e) = crate::config::init_config() { eprintln!("Warning: Failed to initialize configuration: {}", e); eprintln!("Some functionality may be limited."); } - + // Parse command line arguments let cli = Cli::parse(); - + // If we have command-line arguments, handle them if env::args().len() > 1 { // Handle special async commands first match &cli.command { - Some(Commands::TestVyOS { host, port, username, password, key_path, api_key }) => { + Some(Commands::TestVyOS { + host, + port, + username, + password, + key_path, + api_key, + }) => { println!("Testing connection to VyOS router at {}:{}...", host, port); - + // Create a VyOS client using our API use crate::api::vyos::{VyOSClient, VyOSConfig}; use crate::api::Provider; - + let config = VyOSConfig { host: host.clone(), ssh_port: *port, @@ -369,50 +381,56 @@ async fn main() -> AppResult<()> { api_key: api_key.clone(), timeout: 30, }; - + let client = VyOSClient::new(config); - + // First try the synchronous connection test match client.connect() { Ok(_) => { println!("\n✅ SSH connection successful!"); - + // If API key is provided, also test the API - if let Some(api_key) = &api_key { + if let Some(_api_key) = &api_key { println!("\nTesting VyOS HTTP API..."); - + let mut client_mut = client; match client_mut.get_system_info().await { Ok(info) => { println!("\n✅ API connection successful!"); println!("\nVyOS system information:"); - println!("{}", serde_json::to_string_pretty(&info).unwrap_or_else(|_| info.to_string())); - }, + println!( + "{}", + serde_json::to_string_pretty(&info) + .unwrap_or_else(|_| info.to_string()) + ); + } Err(e) => { println!("\n❌ API connection failed: {}", e); } } } - + return Ok(()); - }, + } Err(e) => { // Fallback to manual SSH connection if the client connect fails println!("VyOS client connection failed: {}", e); println!("Falling back to direct SSH connection..."); - + // Let's try connecting interactively - we'll just verify the connection first - let ssh_command = format!("ssh -o StrictHostKeyChecking=no -p {} {}@{}", - port, username, host); + let ssh_command = format!( + "ssh -o StrictHostKeyChecking=no -p {} {}@{}", + port, username, host + ); println!("Running: {}", ssh_command); - + let output = tokio::process::Command::new("sh") .arg("-c") .arg(ssh_command) .output() .await .map_err(|e| format!("Failed to execute SSH command: {}", e))?; - + if output.status.success() || output.status.code() == Some(255) { // If we got output, even with a non-zero exit code, that likely means // we connected successfully but then got disconnected properly after the welcome message @@ -429,7 +447,10 @@ async fn main() -> AppResult<()> { } return Ok(()); } else { - return Err(format!("Connected but did not receive VyOS welcome message").into()); + return Err(format!( + "Connected but did not receive VyOS welcome message" + ) + .into()); } } else { let error = String::from_utf8_lossy(&output.stderr); @@ -437,13 +458,13 @@ async fn main() -> AppResult<()> { } } } - }, + } _ => { // For other commands, use the synchronous handler cli_handler(cli)?; } } - + Ok(()) } else { // Otherwise, launch the TUI diff --git a/src/models/instance.rs b/src/models/instance.rs index 3bfc8fc..ca4ee8f 100644 --- a/src/models/instance.rs +++ b/src/models/instance.rs @@ -1,7 +1,7 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use uuid::Uuid; use std::collections::HashMap; -use chrono::{DateTime, Utc}; +use uuid::Uuid; use crate::models::provider::ProviderType; @@ -114,15 +114,22 @@ impl Instance { tags: HashMap::new(), } } - + /// Get primary IP address pub fn primary_ip(&self) -> Option<&str> { - self.networks.first() + self.networks + .first() .and_then(|network| network.ip.as_deref()) } - + /// Add a network to the instance - pub fn add_network(&mut self, network_id: String, ip: Option, interface: Option, mac: Option) { + pub fn add_network( + &mut self, + network_id: String, + ip: Option, + interface: Option, + mac: Option, + ) { self.networks.push(InstanceNetwork { network_id, ip, @@ -131,19 +138,19 @@ impl Instance { }); self.updated_at = Utc::now(); } - + /// Update instance status pub fn update_status(&mut self, status: InstanceStatus) { self.status = status; self.updated_at = Utc::now(); } - + /// Add a tag to the instance pub fn add_tag(&mut self, key: String, value: String) { self.tags.insert(key, value); self.updated_at = Utc::now(); } - + /// Remove a tag from the instance pub fn remove_tag(&mut self, key: &str) -> Option { let result = self.tags.remove(key); @@ -152,4 +159,4 @@ impl Instance { } result } -} \ No newline at end of file +} diff --git a/src/models/mod.rs b/src/models/mod.rs index ac9f5a6..f6154fb 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,4 @@ pub mod instance; -pub mod volume; pub mod network; -pub mod provider; \ No newline at end of file +pub mod provider; +pub mod volume; diff --git a/src/models/network.rs b/src/models/network.rs index 772aa19..495f99b 100644 --- a/src/models/network.rs +++ b/src/models/network.rs @@ -1,8 +1,8 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use uuid::Uuid; use std::collections::{HashMap, HashSet}; -use chrono::{DateTime, Utc}; use std::net::IpAddr; +use uuid::Uuid; use crate::models::provider::ProviderType; @@ -125,7 +125,13 @@ pub struct Network { impl Network { /// Create a new network - pub fn new(name: String, provider: ProviderType, region: String, cidr: String, network_type: NetworkType) -> Self { + pub fn new( + name: String, + provider: ProviderType, + region: String, + cidr: String, + network_type: NetworkType, + ) -> Self { let now = Utc::now(); Self { id: Uuid::new_v4(), @@ -146,19 +152,19 @@ impl Network { config: HashMap::new(), } } - + /// Update network status pub fn update_status(&mut self, status: NetworkStatus) { self.status = status; self.updated_at = Utc::now(); } - + /// Add a gateway IP pub fn set_gateway(&mut self, gateway: IpAddr) { self.gateway = Some(gateway); self.updated_at = Utc::now(); } - + /// Add a DNS server pub fn add_dns_server(&mut self, dns_server: IpAddr) { if !self.dns_servers.contains(&dns_server) { @@ -166,7 +172,7 @@ impl Network { self.updated_at = Utc::now(); } } - + /// Remove a DNS server pub fn remove_dns_server(&mut self, dns_server: &IpAddr) { if let Some(idx) = self.dns_servers.iter().position(|ip| ip == dns_server) { @@ -174,7 +180,7 @@ impl Network { self.updated_at = Utc::now(); } } - + /// Connect an instance to the network pub fn connect_instance(&mut self, instance_id: Uuid) -> bool { let result = self.instances.insert(instance_id); @@ -183,41 +189,42 @@ impl Network { } result } - + /// Disconnect an instance from the network pub fn disconnect_instance(&mut self, instance_id: &Uuid) -> bool { let result = self.instances.remove(instance_id); if result { // Also remove any IP allocations for this instance - self.ip_allocations.retain(|alloc| alloc.instance_id != Some(*instance_id)); + self.ip_allocations + .retain(|alloc| alloc.instance_id != Some(*instance_id)); self.updated_at = Utc::now(); } result } - + /// Allocate an IP address to an instance pub fn allocate_ip(&mut self, ip: IpAddr, instance_id: Uuid) -> Result<(), &'static str> { // Check if IP is already allocated if self.ip_allocations.iter().any(|alloc| alloc.ip == ip) { return Err("IP address already allocated"); } - + // Ensure instance is connected to this network if !self.instances.contains(&instance_id) { return Err("Instance not connected to this network"); } - + // Allocate the IP self.ip_allocations.push(IpAllocation { ip, instance_id: Some(instance_id), assigned_at: Some(Utc::now()), }); - + self.updated_at = Utc::now(); Ok(()) } - + /// Release an IP address pub fn release_ip(&mut self, ip: &IpAddr) -> Result<(), &'static str> { if let Some(idx) = self.ip_allocations.iter().position(|alloc| &alloc.ip == ip) { @@ -228,13 +235,13 @@ impl Network { Err("IP address not found") } } - + /// Add a tag to the network pub fn add_tag(&mut self, key: String, value: String) { self.tags.insert(key, value); self.updated_at = Utc::now(); } - + /// Remove a tag from the network pub fn remove_tag(&mut self, key: &str) -> Option { let result = self.tags.remove(key); @@ -243,18 +250,18 @@ impl Network { } result } - + /// Set a configuration parameter pub fn set_config(&mut self, key: String, value: String) { self.config.insert(key, value); self.updated_at = Utc::now(); } - + /// Get a configuration parameter pub fn get_config(&self, key: &str) -> Option<&String> { self.config.get(key) } - + /// Remove a configuration parameter pub fn remove_config(&mut self, key: &str) -> Option { let result = self.config.remove(key); @@ -263,4 +270,4 @@ impl Network { } result } -} \ No newline at end of file +} diff --git a/src/models/provider.rs b/src/models/provider.rs index c506cab..05aeb01 100644 --- a/src/models/provider.rs +++ b/src/models/provider.rs @@ -85,4 +85,4 @@ impl Default for ResourceLimits { max_disk_per_instance: None, } } -} \ No newline at end of file +} diff --git a/src/models/volume.rs b/src/models/volume.rs index cc0c610..95e1237 100644 --- a/src/models/volume.rs +++ b/src/models/volume.rs @@ -1,7 +1,7 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use uuid::Uuid; use std::collections::HashMap; -use chrono::{DateTime, Utc}; +use uuid::Uuid; use crate::models::provider::ProviderType; @@ -110,7 +110,13 @@ pub struct Volume { impl Volume { /// Create a new volume - pub fn new(name: String, provider: ProviderType, region: String, size_gb: u16, volume_type: VolumeType) -> Self { + pub fn new( + name: String, + provider: ProviderType, + region: String, + size_gb: u16, + volume_type: VolumeType, + ) -> Self { let now = Utc::now(); Self { id: Uuid::new_v4(), @@ -128,13 +134,13 @@ impl Volume { tags: HashMap::new(), } } - + /// Update volume status pub fn update_status(&mut self, status: VolumeStatus) { self.status = status; self.updated_at = Utc::now(); } - + /// Attach volume to an instance pub fn attach(&mut self, instance_id: Uuid, device: Option) { self.attached_to = Some(instance_id); @@ -142,7 +148,7 @@ impl Volume { self.status = VolumeStatus::InUse; self.updated_at = Utc::now(); } - + /// Detach volume from an instance pub fn detach(&mut self) { self.attached_to = None; @@ -150,24 +156,24 @@ impl Volume { self.status = VolumeStatus::Available; self.updated_at = Utc::now(); } - + /// Extend volume size pub fn extend(&mut self, new_size_gb: u16) -> Result<(), &'static str> { if new_size_gb <= self.size_gb { return Err("New size must be larger than current size"); } - + self.size_gb = new_size_gb; self.updated_at = Utc::now(); Ok(()) } - + /// Add a tag to the volume pub fn add_tag(&mut self, key: String, value: String) { self.tags.insert(key, value); self.updated_at = Utc::now(); } - + /// Remove a tag from the volume pub fn remove_tag(&mut self, key: &str) -> Option { let result = self.tags.remove(key); @@ -176,4 +182,4 @@ impl Volume { } result } -} \ No newline at end of file +} diff --git a/src/services/instance.rs b/src/services/instance.rs index 6682c87..346adee 100644 --- a/src/services/instance.rs +++ b/src/services/instance.rs @@ -1,11 +1,11 @@ -use anyhow::{Result, Context, anyhow}; -use log::{debug, info, error}; +use anyhow::{anyhow, Context, Result}; +use chrono::Utc; +use log::{debug, error, info}; +use serde_json::json; use std::collections::HashMap; use uuid::Uuid; -use serde_json::json; -use chrono::Utc; -use crate::models::instance::{Instance, InstanceStatus, InstanceSize, InstanceNetwork}; +use crate::models::instance::{Instance, InstanceNetwork, InstanceSize, InstanceStatus}; use crate::models::provider::ProviderType; use crate::services::provider::ProviderService; @@ -22,42 +22,44 @@ impl InstanceStorage { instances: HashMap::new(), } } - + /// Add an instance pub fn add_instance(&mut self, instance: Instance) { self.instances.insert(instance.id, instance); } - + /// Get an instance by ID pub fn get_instance(&self, id: &Uuid) -> Option<&Instance> { self.instances.get(id) } - + /// Get a mutable reference to an instance pub fn get_instance_mut(&mut self, id: &Uuid) -> Option<&mut Instance> { self.instances.get_mut(id) } - + /// Remove an instance pub fn remove_instance(&mut self, id: &Uuid) -> Option { self.instances.remove(id) } - + /// Get all instances pub fn get_all_instances(&self) -> Vec<&Instance> { self.instances.values().collect() } - + /// Get instances by provider pub fn get_instances_by_provider(&self, provider: ProviderType) -> Vec<&Instance> { - self.instances.values() + self.instances + .values() .filter(|i| i.provider == provider) .collect() } - + /// Get instances by region pub fn get_instances_by_region(&self, region: &str) -> Vec<&Instance> { - self.instances.values() + self.instances + .values() .filter(|i| i.region == region) .collect() } @@ -77,17 +79,17 @@ impl InstanceService { provider_service, } } - + /// List all instances pub fn list_instances(&self) -> Vec<&Instance> { self.storage.get_all_instances() } - + /// Get an instance by ID pub fn get_instance(&self, id: &Uuid) -> Option<&Instance> { self.storage.get_instance(id) } - + /// Create a new instance on a VyOS provider pub async fn create_vyos_instance( &mut self, @@ -99,7 +101,7 @@ impl InstanceService { ) -> Result { // Get VyOS client let mut client = self.provider_service.get_vyos_client(provider_name)?; - + // Create a new instance object let mut instance = Instance::new( name.to_string(), @@ -107,45 +109,50 @@ impl InstanceService { region.to_string(), size.clone(), ); - + info!("Creating VyOS instance '{}' in region '{}'", name, region); - + // Use VyOS API to create the VM // This is a simplified example - in a real implementation, you would call the VyOS API // to create a VM and get the provider-specific ID - + // Example: Use the VyOS API to get information about the router let result = client.get_system_info().await; - + match result { Ok(info) => { debug!("VyOS system info: {:?}", info); - + // In a real implementation, you would parse the response and set the provider ID instance.provider_id = format!("vyos-{}", Uuid::new_v4()); - + // Add network if specified if let Some(net_id) = network_id { - instance.add_network(net_id, Some("192.168.1.100".to_string()), Some("eth0".to_string()), None); + instance.add_network( + net_id, + Some("192.168.1.100".to_string()), + Some("eth0".to_string()), + None, + ); } - + // Set status to running instance.update_status(InstanceStatus::Running); - + // Store the instance let id = instance.id; self.storage.add_instance(instance); - + info!("Successfully created VyOS instance: {}", id); Ok(id) - }, + } Err(e) => { error!("Failed to create VyOS instance: {}", e); Err(anyhow!("Failed to create VyOS instance: {}", e)) } } } - + /// Create a new instance on a Proxmox provider pub async fn create_proxmox_instance( &mut self, @@ -157,7 +164,7 @@ impl InstanceService { ) -> Result { // Get Proxmox client let mut client = self.provider_service.get_proxmox_client(provider_name)?; - + // Create a new instance object let mut instance = Instance::new( name.to_string(), @@ -165,15 +172,18 @@ impl InstanceService { region.to_string(), size.clone(), ); - - info!("Creating Proxmox instance '{}' in region '{}'", name, region); - + + info!( + "Creating Proxmox instance '{}' in region '{}'", + name, region + ); + // Ensure client is connected client.login().await?; - + // Get first node in the cluster let nodes = client.get_nodes().await?; - + if let Some(nodes_array) = nodes.as_array() { if let Some(first_node) = nodes_array.first() { if let Some(node_name) = first_node["node"].as_str() { @@ -186,36 +196,41 @@ impl InstanceService { "disk": format!("{}G", size.disk_gb), "net0": "virtio,bridge=vmbr0", }); - + // Create the VM let result = client.create_vm(node_name, vm_params).await; - + match result { Ok(response) => { debug!("Proxmox VM creation response: {:?}", response); - + // Set provider ID to the VMID if let Some(vmid) = response["vmid"].as_u64() { instance.provider_id = vmid.to_string(); - + // Add network if specified if let Some(net_id) = network_id { - instance.add_network(net_id, Some("192.168.1.100".to_string()), Some("eth0".to_string()), None); + instance.add_network( + net_id, + Some("192.168.1.100".to_string()), + Some("eth0".to_string()), + None, + ); } - + // Set status to running instance.update_status(InstanceStatus::Running); - + // Store the instance let id = instance.id; self.storage.add_instance(instance); - + info!("Successfully created Proxmox instance: {}", id); return Ok(id); } else { return Err(anyhow!("Failed to get VMID from Proxmox response")); } - }, + } Err(e) => { error!("Failed to create Proxmox instance: {}", e); return Err(anyhow!("Failed to create Proxmox instance: {}", e)); @@ -223,78 +238,83 @@ impl InstanceService { } } } - + Err(anyhow!("No nodes found in Proxmox cluster")) } else { Err(anyhow!("Invalid response from Proxmox API")) } } - + /// Start an instance pub async fn start_instance(&mut self, id: &Uuid) -> Result<()> { // Get the instance - let instance = self.storage.get_instance(id) + let instance = self + .storage + .get_instance(id) .ok_or_else(|| anyhow!("Instance not found: {}", id))?; - + // Clone necessary values for the match block let provider = instance.provider; let provider_id = instance.provider_id.clone(); - + // Find the provider name let provider_name = self.find_provider_name(instance)?; - + match provider { ProviderType::VyOS => { // Get VyOS client let mut client = self.provider_service.get_vyos_client(&provider_name)?; - + // Use VyOS API to start the VM // Example: Send commands over SSH - let result = client.execute_ssh_command(&format!("start vm {}", provider_id)).await; - + let result = client + .execute_ssh_command(&format!("start vm {}", provider_id)) + .await; + match result { Ok(_) => { // Update instance status if let Some(instance) = self.storage.get_instance_mut(id) { instance.update_status(InstanceStatus::Running); } - + info!("Successfully started VyOS instance: {}", id); Ok(()) - }, + } Err(e) => { error!("Failed to start VyOS instance: {}", e); Err(anyhow!("Failed to start VyOS instance: {}", e)) } } - }, + } ProviderType::Proxmox => { // Get Proxmox client let mut client = self.provider_service.get_proxmox_client(&provider_name)?; - + // Ensure client is connected client.login().await?; - + // Get the node name from the provider ID // In a real implementation, you would store or lookup the node name - let node_name = "pve"; // Placeholder - + let node_name = "pve"; // Placeholder + // Start the VM - let vmid = provider_id.parse::() + let vmid = provider_id + .parse::() .context("Invalid VMID in provider_id")?; - + let result = client.start_vm(node_name, vmid).await; - + match result { Ok(_) => { // Update instance status if let Some(instance) = self.storage.get_instance_mut(id) { instance.update_status(InstanceStatus::Running); } - + info!("Successfully started Proxmox instance: {}", id); Ok(()) - }, + } Err(e) => { error!("Failed to start Proxmox instance: {}", e); Err(anyhow!("Failed to start Proxmox instance: {}", e)) @@ -303,72 +323,77 @@ impl InstanceService { } } } - + /// Stop an instance pub async fn stop_instance(&mut self, id: &Uuid) -> Result<()> { // Get the instance - let instance = self.storage.get_instance(id) + let instance = self + .storage + .get_instance(id) .ok_or_else(|| anyhow!("Instance not found: {}", id))?; - + // Clone necessary values for the match block let provider = instance.provider; let provider_id = instance.provider_id.clone(); - + // Find the provider name let provider_name = self.find_provider_name(instance)?; - + match provider { ProviderType::VyOS => { // Get VyOS client let mut client = self.provider_service.get_vyos_client(&provider_name)?; - + // Use VyOS API to stop the VM // Example: Send commands over SSH - let result = client.execute_ssh_command(&format!("stop vm {}", provider_id)).await; - + let result = client + .execute_ssh_command(&format!("stop vm {}", provider_id)) + .await; + match result { Ok(_) => { // Update instance status if let Some(instance) = self.storage.get_instance_mut(id) { instance.update_status(InstanceStatus::Stopped); } - + info!("Successfully stopped VyOS instance: {}", id); Ok(()) - }, + } Err(e) => { error!("Failed to stop VyOS instance: {}", e); Err(anyhow!("Failed to stop VyOS instance: {}", e)) } } - }, + } ProviderType::Proxmox => { // Get Proxmox client let mut client = self.provider_service.get_proxmox_client(&provider_name)?; - + // Ensure client is connected client.login().await?; - + // Get the node name from the provider ID // In a real implementation, you would store or lookup the node name - let node_name = "pve"; // Placeholder - + let node_name = "pve"; // Placeholder + // Stop the VM - let vmid = provider_id.parse::() + let vmid = provider_id + .parse::() .context("Invalid VMID in provider_id")?; - + let result = client.stop_vm(node_name, vmid).await; - + match result { Ok(_) => { // Update instance status if let Some(instance) = self.storage.get_instance_mut(id) { instance.update_status(InstanceStatus::Stopped); } - + info!("Successfully stopped Proxmox instance: {}", id); Ok(()) - }, + } Err(e) => { error!("Failed to stop Proxmox instance: {}", e); Err(anyhow!("Failed to stop Proxmox instance: {}", e)) @@ -377,68 +402,73 @@ impl InstanceService { } } } - + /// Delete an instance pub async fn delete_instance(&mut self, id: &Uuid) -> Result<()> { // Get the instance - let instance = self.storage.get_instance(id) + let instance = self + .storage + .get_instance(id) .ok_or_else(|| anyhow!("Instance not found: {}", id))?; - + // Clone necessary values for the match block let provider = instance.provider; let provider_id = instance.provider_id.clone(); - + // Find the provider name let provider_name = self.find_provider_name(instance)?; - + match provider { ProviderType::VyOS => { // Get VyOS client let mut client = self.provider_service.get_vyos_client(&provider_name)?; - + // Use VyOS API to delete the VM // Example: Send commands over SSH - let result = client.execute_ssh_command(&format!("delete vm {}", provider_id)).await; - + let result = client + .execute_ssh_command(&format!("delete vm {}", provider_id)) + .await; + match result { Ok(_) => { // Remove the instance from storage self.storage.remove_instance(id); - + info!("Successfully deleted VyOS instance: {}", id); Ok(()) - }, + } Err(e) => { error!("Failed to delete VyOS instance: {}", e); Err(anyhow!("Failed to delete VyOS instance: {}", e)) } } - }, + } ProviderType::Proxmox => { // Get Proxmox client let mut client = self.provider_service.get_proxmox_client(&provider_name)?; - + // Ensure client is connected client.login().await?; - + // Get the node name from the provider ID // In a real implementation, you would store or lookup the node name - let node_name = "pve"; // Placeholder - + let node_name = "pve"; // Placeholder + // Delete the VM - let vmid = provider_id.parse::() + let vmid = provider_id + .parse::() .context("Invalid VMID in provider_id")?; - + let result = client.delete_vm(node_name, vmid).await; - + match result { Ok(_) => { // Remove the instance from storage self.storage.remove_instance(id); - + info!("Successfully deleted Proxmox instance: {}", id); Ok(()) - }, + } Err(e) => { error!("Failed to delete Proxmox instance: {}", e); Err(anyhow!("Failed to delete Proxmox instance: {}", e)) @@ -447,7 +477,7 @@ impl InstanceService { } } } - + /// Helper method to find provider name for an instance fn find_provider_name(&self, instance: &Instance) -> Result { // Iterate through providers to find a matching one @@ -456,7 +486,7 @@ impl InstanceService { return Ok(name.clone()); } } - + Err(anyhow!("No provider found for instance: {}", instance.id)) } -} \ No newline at end of file +} diff --git a/src/services/mod.rs b/src/services/mod.rs index c004d9d..f6154fb 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,4 +1,4 @@ -pub mod provider; pub mod instance; +pub mod network; +pub mod provider; pub mod volume; -pub mod network; \ No newline at end of file diff --git a/src/services/network.rs b/src/services/network.rs index 739b229..5f4908e 100644 --- a/src/services/network.rs +++ b/src/services/network.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, anyhow}; +use anyhow::{anyhow, Result}; use log::info; use std::collections::HashMap; use std::net::IpAddr; @@ -21,42 +21,44 @@ impl NetworkStorage { networks: HashMap::new(), } } - + /// Add a network pub fn add_network(&mut self, network: Network) { self.networks.insert(network.id, network); } - + /// Get a network by ID pub fn get_network(&self, id: &Uuid) -> Option<&Network> { self.networks.get(id) } - + /// Get a mutable reference to a network pub fn get_network_mut(&mut self, id: &Uuid) -> Option<&mut Network> { self.networks.get_mut(id) } - + /// Remove a network pub fn remove_network(&mut self, id: &Uuid) -> Option { self.networks.remove(id) } - + /// Get all networks pub fn get_all_networks(&self) -> Vec<&Network> { self.networks.values().collect() } - + /// Get networks by provider pub fn get_networks_by_provider(&self, provider: ProviderType) -> Vec<&Network> { - self.networks.values() + self.networks + .values() .filter(|n| n.provider == provider) .collect() } - + /// Get networks by region pub fn get_networks_by_region(&self, region: &str) -> Vec<&Network> { - self.networks.values() + self.networks + .values() .filter(|n| n.region == region) .collect() } @@ -78,17 +80,17 @@ impl NetworkService { provider_service, } } - + /// List all networks pub fn list_networks(&self) -> Vec<&Network> { self.storage.get_all_networks() } - + /// Get a network by ID pub fn get_network(&self, id: &Uuid) -> Option<&Network> { self.storage.get_network(id) } - + /// Create a new network pub async fn create_network( &mut self, @@ -108,7 +110,7 @@ impl NetworkService { cidr.to_string(), network_type, ); - + // Set gateway and DNS servers if provided if let Some(gw) = gateway { network.set_gateway(gw); @@ -116,68 +118,105 @@ impl NetworkService { for dns in dns_servers { network.add_dns_server(dns); } - - info!("Creating network '{}' with CIDR {} in region '{}'", name, cidr, region); - + + info!( + "Creating network '{}' with CIDR {} in region '{}'", + name, cidr, region + ); + // Store the network let id = network.id; self.storage.add_network(network); - + info!("Successfully created network: {}", id); Ok(id) } - + /// Connect an instance to a network - pub async fn connect_instance(&mut self, network_id: &Uuid, instance_id: &Uuid, ip: IpAddr) -> Result<()> { + pub async fn connect_instance( + &mut self, + network_id: &Uuid, + instance_id: &Uuid, + ip: IpAddr, + ) -> Result<()> { // Get the network - let network = self.storage.get_network_mut(network_id) + let network = self + .storage + .get_network_mut(network_id) .ok_or_else(|| anyhow!("Network not found: {}", network_id))?; - + // Check if instance is already connected if network.instances.contains(instance_id) { - return Err(anyhow!("Instance {} is already connected to network {}", instance_id, network_id)); + return Err(anyhow!( + "Instance {} is already connected to network {}", + instance_id, + network_id + )); } - + // Connect the instance and allocate IP network.connect_instance(*instance_id); - network.allocate_ip(ip, *instance_id).map_err(|e| anyhow!(e))?; - - info!("Connected instance {} to network {} with IP {}", instance_id, network_id, ip); + network + .allocate_ip(ip, *instance_id) + .map_err(|e| anyhow!(e))?; + + info!( + "Connected instance {} to network {} with IP {}", + instance_id, network_id, ip + ); Ok(()) } - + /// Disconnect an instance from a network - pub async fn disconnect_instance(&mut self, network_id: &Uuid, instance_id: &Uuid) -> Result<()> { + pub async fn disconnect_instance( + &mut self, + network_id: &Uuid, + instance_id: &Uuid, + ) -> Result<()> { // Get the network - let network = self.storage.get_network_mut(network_id) + let network = self + .storage + .get_network_mut(network_id) .ok_or_else(|| anyhow!("Network not found: {}", network_id))?; - + // Check if instance is connected if !network.instances.contains(instance_id) { - return Err(anyhow!("Instance {} is not connected to network {}", instance_id, network_id)); + return Err(anyhow!( + "Instance {} is not connected to network {}", + instance_id, + network_id + )); } - + // Disconnect the instance (this also removes IP allocation) network.disconnect_instance(instance_id); - - info!("Disconnected instance {} from network {}", instance_id, network_id); + + info!( + "Disconnected instance {} from network {}", + instance_id, network_id + ); Ok(()) } - + /// Delete a network pub async fn delete_network(&mut self, network_id: &Uuid) -> Result<()> { // Get the network - let network = self.storage.get_network(network_id) + let network = self + .storage + .get_network(network_id) .ok_or_else(|| anyhow!("Network not found: {}", network_id))?; - + // Check if any instances are connected if !network.instances.is_empty() { - return Err(anyhow!("Cannot delete network {} with connected instances. Disconnect them first.", network_id)); + return Err(anyhow!( + "Cannot delete network {} with connected instances. Disconnect them first.", + network_id + )); } - + // Remove the network self.storage.remove_network(network_id); - + info!("Deleted network {}", network_id); Ok(()) } diff --git a/src/services/provider.rs b/src/services/provider.rs index 8bd63fb..1fafa35 100644 --- a/src/services/provider.rs +++ b/src/services/provider.rs @@ -1,11 +1,14 @@ -use anyhow::{Result, Context, anyhow}; -use log::{debug, info, error}; +use anyhow::{anyhow, Context, Result}; +use log::{debug, error, info}; use std::collections::HashMap; -use crate::models::provider::{ProviderType, ProviderConfig, Region, ResourceLimits}; -use crate::config::provider::Providers; +use crate::api::{ + proxmox::ProxmoxAuth, proxmox::ProxmoxClient, proxmox::ProxmoxConfig, vyos::VyOSClient, + vyos::VyOSConfig, Provider, +}; use crate::config::credentials::{Credentials, ProviderCredentials}; -use crate::api::{Provider, vyos::VyOSClient, vyos::VyOSConfig, proxmox::ProxmoxClient, proxmox::ProxmoxConfig, proxmox::ProxmoxAuth}; +use crate::config::provider::Providers; +use crate::models::provider::{ProviderConfig, ProviderType, Region, ResourceLimits}; /// Provider service for managing infrastructure providers pub struct ProviderService { @@ -18,28 +21,28 @@ impl ProviderService { pub fn new() -> Result { let providers = Providers::load()?; let credentials = Credentials::load()?; - + Ok(Self { providers, credentials, }) } - + /// Get provider configs pub fn get_providers(&self) -> &HashMap { self.providers.get_all_providers() } - + /// Get regions pub fn get_regions(&self) -> &HashMap { self.providers.get_all_regions() } - + /// Get regions by provider pub fn get_regions_by_provider(&self, provider_type: ProviderType) -> Vec<&Region> { self.providers.get_regions_by_provider(provider_type) } - + /// Add a new VyOS provider pub fn add_vyos_provider( &mut self, @@ -54,29 +57,24 @@ impl ProviderService { ) -> Result<()> { // Create provider params let mut params = HashMap::new(); - + // Add provider - self.providers.add_provider(name, ProviderType::VyOS, host, params)?; - + self.providers + .add_provider(name, ProviderType::VyOS, host, params)?; + // Add credentials self.credentials.add_vyos_credentials( - name, - username, - password, - key_path, - api_key, - ssh_port, - api_port, + name, username, password, key_path, api_key, ssh_port, api_port, )?; - + // Save changes self.providers.save()?; self.credentials.save()?; - + info!("Added VyOS provider: {}", name); Ok(()) } - + /// Add a new Proxmox provider with token auth pub fn add_proxmox_provider_with_token( &mut self, @@ -89,10 +87,11 @@ impl ProviderService { ) -> Result<()> { // Create provider params let mut params = HashMap::new(); - + // Add provider - self.providers.add_provider(name, ProviderType::Proxmox, host, params)?; - + self.providers + .add_provider(name, ProviderType::Proxmox, host, params)?; + // Add credentials self.credentials.add_proxmox_token_credentials( name, @@ -101,15 +100,15 @@ impl ProviderService { port, verify_ssl, )?; - + // Save changes self.providers.save()?; self.credentials.save()?; - + info!("Added Proxmox provider with token auth: {}", name); Ok(()) } - + /// Add a new Proxmox provider with username/password auth pub fn add_proxmox_provider_with_user_pass( &mut self, @@ -123,46 +122,41 @@ impl ProviderService { ) -> Result<()> { // Create provider params let mut params = HashMap::new(); - + // Add provider - self.providers.add_provider(name, ProviderType::Proxmox, host, params)?; - + self.providers + .add_provider(name, ProviderType::Proxmox, host, params)?; + // Add credentials - self.credentials.add_proxmox_user_pass_credentials( - name, - username, - password, - realm, - port, - verify_ssl, - )?; - + self.credentials + .add_proxmox_user_pass_credentials(name, username, password, realm, port, verify_ssl)?; + // Save changes self.providers.save()?; self.credentials.save()?; - + info!("Added Proxmox provider with user/pass auth: {}", name); Ok(()) } - + /// Remove a provider pub fn remove_provider(&mut self, name: &str) -> Result<()> { // Remove provider config self.providers.remove_provider(name)?; - + // Remove credentials if let Err(e) = self.credentials.remove_credentials(name) { debug!("No credentials found for provider '{}': {}", name, e); } - + // Save changes self.providers.save()?; self.credentials.save()?; - + info!("Removed provider: {}", name); Ok(()) } - + /// Add a new region pub fn add_region( &mut self, @@ -181,37 +175,42 @@ impl ProviderService { available, limits: limits.unwrap_or_default(), }; - + self.providers.add_region(region)?; self.providers.save()?; - + info!("Added region: {}", id); Ok(()) } - + /// Remove a region pub fn remove_region(&mut self, id: &str) -> Result<()> { self.providers.remove_region(id)?; self.providers.save()?; - + info!("Removed region: {}", id); Ok(()) } - + /// Get a VyOS client for a provider pub fn get_vyos_client(&self, provider_name: &str) -> Result { // Get provider config - let provider = self.providers.get_provider(provider_name) + let provider = self + .providers + .get_provider(provider_name) .ok_or_else(|| anyhow!("Provider not found: {}", provider_name))?; - + // Ensure it's a VyOS provider if provider.provider_type != ProviderType::VyOS { - return Err(anyhow!("Provider '{}' is not a VyOS provider", provider_name)); + return Err(anyhow!( + "Provider '{}' is not a VyOS provider", + provider_name + )); } - + // Get credentials let creds = self.credentials.get_vyos_credentials(provider_name)?; - + // Create client config let config = VyOSConfig { host: provider.host.clone(), @@ -223,27 +222,32 @@ impl ProviderService { api_key: creds.api_key.clone(), timeout: 30, }; - + // Create client let client = VyOSClient::new(config); - + Ok(client) } - + /// Get a Proxmox client for a provider pub fn get_proxmox_client(&self, provider_name: &str) -> Result { // Get provider config - let provider = self.providers.get_provider(provider_name) + let provider = self + .providers + .get_provider(provider_name) .ok_or_else(|| anyhow!("Provider not found: {}", provider_name))?; - + // Ensure it's a Proxmox provider if provider.provider_type != ProviderType::Proxmox { - return Err(anyhow!("Provider '{}' is not a Proxmox provider", provider_name)); + return Err(anyhow!( + "Provider '{}' is not a Proxmox provider", + provider_name + )); } - + // Get credentials let creds = self.credentials.get_proxmox_credentials(provider_name)?; - + // Create auth config let auth = if creds.use_token_auth { if let Some(token) = &creds.token_auth { @@ -265,7 +269,7 @@ impl ProviderService { return Err(anyhow!("Proxmox provider '{}' is configured to use user/pass auth, but no credentials are provided", provider_name)); } }; - + // Create client config let config = ProxmoxConfig { host: provider.host.clone(), @@ -274,50 +278,61 @@ impl ProviderService { timeout: 30, verify_ssl: creds.verify_ssl, }; - + // Create client let client = ProxmoxClient::new(config); - + Ok(client) } - + /// Test connection to a provider pub async fn test_connection(&self, provider_name: &str) -> Result { // Get provider config - let provider = self.providers.get_provider(provider_name) + let provider = self + .providers + .get_provider(provider_name) .ok_or_else(|| anyhow!("Provider not found: {}", provider_name))?; - + match provider.provider_type { ProviderType::VyOS => { let client = self.get_vyos_client(provider_name)?; - + // Use the synchronous connect method for testing match client.connect() { Ok(_) => { info!("Successfully connected to VyOS provider: {}", provider_name); Ok(true) - }, + } Err(e) => { - error!("Failed to connect to VyOS provider '{}': {}", provider_name, e); + error!( + "Failed to connect to VyOS provider '{}': {}", + provider_name, e + ); Ok(false) } } - }, + } ProviderType::Proxmox => { let mut client = self.get_proxmox_client(provider_name)?; - + // Login to test connection match client.login().await { Ok(_) => { - info!("Successfully connected to Proxmox provider: {}", provider_name); + info!( + "Successfully connected to Proxmox provider: {}", + provider_name + ); Ok(true) - }, + } Err(e) => { - error!("Failed to connect to Proxmox provider '{}': {}", provider_name, e); + error!( + "Failed to connect to Proxmox provider '{}': {}", + provider_name, e + ); Ok(false) } } } } } -} \ No newline at end of file +} diff --git a/src/services/volume.rs b/src/services/volume.rs index 58f16e3..98686ca 100644 --- a/src/services/volume.rs +++ b/src/services/volume.rs @@ -1,10 +1,10 @@ -use anyhow::{Result, anyhow}; +use anyhow::{anyhow, Result}; use log::info; use std::collections::HashMap; use uuid::Uuid; -use crate::models::volume::{Volume, VolumeStatus, VolumeType}; use crate::models::provider::ProviderType; +use crate::models::volume::{Volume, VolumeStatus, VolumeType}; use crate::services::provider::ProviderService; /// Storage for volume data @@ -20,42 +20,44 @@ impl VolumeStorage { volumes: HashMap::new(), } } - + /// Add a volume pub fn add_volume(&mut self, volume: Volume) { self.volumes.insert(volume.id, volume); } - + /// Get a volume by ID pub fn get_volume(&self, id: &Uuid) -> Option<&Volume> { self.volumes.get(id) } - + /// Get a mutable reference to a volume pub fn get_volume_mut(&mut self, id: &Uuid) -> Option<&mut Volume> { self.volumes.get_mut(id) } - + /// Remove a volume pub fn remove_volume(&mut self, id: &Uuid) -> Option { self.volumes.remove(id) } - + /// Get all volumes pub fn get_all_volumes(&self) -> Vec<&Volume> { self.volumes.values().collect() } - + /// Get volumes by provider pub fn get_volumes_by_provider(&self, provider: ProviderType) -> Vec<&Volume> { - self.volumes.values() + self.volumes + .values() .filter(|v| v.provider == provider) .collect() } - + /// Get volumes by region pub fn get_volumes_by_region(&self, region: &str) -> Vec<&Volume> { - self.volumes.values() + self.volumes + .values() .filter(|v| v.region == region) .collect() } @@ -77,17 +79,17 @@ impl VolumeService { provider_service, } } - + /// List all volumes pub fn list_volumes(&self) -> Vec<&Volume> { self.storage.get_all_volumes() } - + /// Get a volume by ID pub fn get_volume(&self, id: &Uuid) -> Option<&Volume> { self.storage.get_volume(id) } - + /// Create a new volume pub async fn create_volume( &mut self, @@ -105,67 +107,90 @@ impl VolumeService { size_gb, volume_type, ); - - info!("Creating volume '{}' with size {} GB in region '{}'", name, size_gb, region); - + + info!( + "Creating volume '{}' with size {} GB in region '{}'", + name, size_gb, region + ); + // Store the volume let id = volume.id; self.storage.add_volume(volume); - + info!("Successfully created volume: {}", id); Ok(id) } - + /// Attach a volume to an instance - pub async fn attach_volume(&mut self, volume_id: &Uuid, instance_id: &Uuid, device: &str) -> Result<()> { + pub async fn attach_volume( + &mut self, + volume_id: &Uuid, + instance_id: &Uuid, + device: &str, + ) -> Result<()> { // Get the volume - let volume = self.storage.get_volume_mut(volume_id) + let volume = self + .storage + .get_volume_mut(volume_id) .ok_or_else(|| anyhow!("Volume not found: {}", volume_id))?; - + // Check if already attached if volume.attached_to.is_some() { return Err(anyhow!("Volume {} is already attached", volume_id)); } - + // Attach the volume volume.attach(*instance_id, Some(device.to_string())); - - info!("Attached volume {} to instance {} at {}", volume_id, instance_id, device); + + info!( + "Attached volume {} to instance {} at {}", + volume_id, instance_id, device + ); Ok(()) } - + /// Detach a volume from an instance pub async fn detach_volume(&mut self, volume_id: &Uuid) -> Result<()> { // Get the volume - let volume = self.storage.get_volume_mut(volume_id) + let volume = self + .storage + .get_volume_mut(volume_id) .ok_or_else(|| anyhow!("Volume not found: {}", volume_id))?; - + // Check if attached if volume.attached_to.is_none() { - return Err(anyhow!("Volume {} is not attached to any instance", volume_id)); + return Err(anyhow!( + "Volume {} is not attached to any instance", + volume_id + )); } - + // Detach the volume volume.detach(); - + info!("Detached volume {}", volume_id); Ok(()) } - + /// Delete a volume pub async fn delete_volume(&mut self, volume_id: &Uuid) -> Result<()> { // Get the volume - let volume = self.storage.get_volume(volume_id) + let volume = self + .storage + .get_volume(volume_id) .ok_or_else(|| anyhow!("Volume not found: {}", volume_id))?; - + // Check if attached if volume.attached_to.is_some() { - return Err(anyhow!("Cannot delete attached volume {}. Detach it first.", volume_id)); + return Err(anyhow!( + "Cannot delete attached volume {}. Detach it first.", + volume_id + )); } - + // Remove the volume self.storage.remove_volume(volume_id); - + info!("Deleted volume {}", volume_id); Ok(()) } diff --git a/src/ui.rs b/src/ui.rs index a6f5baf..116f1a6 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -2,7 +2,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span, Text}, - widgets::{Block, BorderType, List, ListItem, ListState, Paragraph, Table, Row, Cell, Tabs}, + widgets::{Block, BorderType, Cell, List, ListItem, ListState, Paragraph, Row, Table, Tabs}, Frame, }; @@ -25,7 +25,14 @@ pub fn render(app: &mut App, frame: &mut Frame) { .split(frame.area()); // Render the title bar - let titles = vec!["Home", "Instances", "Volumes", "Networks", "Settings", "Help"]; + let titles = vec![ + "Home", + "Instances", + "Volumes", + "Networks", + "Settings", + "Help", + ]; let tabs = Tabs::new( titles .iter() @@ -40,7 +47,11 @@ pub fn render(app: &mut App, frame: &mut Frame) { ) .select(app.mode as usize) .style(Style::default()) - .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)); + .highlight_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ); frame.render_widget(tabs, chunks[0]); @@ -85,7 +96,11 @@ fn render_home(app: &mut App, frame: &mut Frame, area: Rect) { ]; let paragraph = Paragraph::new(text) - .block(Block::bordered().title("Dashboard").border_type(BorderType::Rounded)) + .block( + Block::bordered() + .title("Dashboard") + .border_type(BorderType::Rounded), + ) .alignment(Alignment::Left); frame.render_widget(paragraph, area); @@ -95,7 +110,7 @@ fn render_instances(app: &mut App, frame: &mut Frame, area: Rect) { let instances = Block::bordered() .title("Instances") .border_type(BorderType::Rounded); - + if app.instances.is_empty() { let text = Text::from("No instances found. Press 'a' to add a new instance."); let paragraph = Paragraph::new(text) @@ -117,7 +132,10 @@ fn render_instances(app: &mut App, frame: &mut Frame, area: Rect) { ListItem::new(vec![ Line::from(vec![ - Span::styled(format!("{}: ", instance.name), Style::default().fg(Color::Cyan)), + Span::styled( + format!("{}: ", instance.name), + Style::default().fg(Color::Cyan), + ), Span::styled(instance.status.clone(), status_style), ]), Line::from(vec![ @@ -127,9 +145,11 @@ fn render_instances(app: &mut App, frame: &mut Frame, area: Rect) { Line::from(vec![ Span::styled(format!("IP: {}", instance.ip), Style::default()), Span::styled( - format!(" | CPU: {} | Memory: {} GB | Disk: {} GB", - instance.cpu, instance.memory_gb, instance.disk_gb), - Style::default() + format!( + " | CPU: {} | Memory: {} GB | Disk: {} GB", + instance.cpu, instance.memory_gb, instance.disk_gb + ), + Style::default(), ), ]), Line::from(""), @@ -145,7 +165,7 @@ fn render_instances(app: &mut App, frame: &mut Frame, area: Rect) { // Use a stateful widget let mut state = ListState::default(); state.select(Some(app.selected_index)); - + frame.render_stateful_widget(list, area, &mut state); } @@ -169,18 +189,22 @@ fn render_volumes(app: &mut App, frame: &mut Frame, area: Rect) { .map(|volume| { let attached_info = match &volume.attached_to { Some(instance_id) => { - let instance_name = app.instances + let instance_name = app + .instances .iter() .find(|i| &i.id == instance_id) .map_or(instance_id.as_str(), |i| i.name.as_str()); format!("Attached to: {}", instance_name) - }, + } None => "Not attached".to_string(), }; ListItem::new(vec![ Line::from(vec![ - Span::styled(format!("{}: ", volume.name), Style::default().fg(Color::Cyan)), + Span::styled( + format!("{}: ", volume.name), + Style::default().fg(Color::Cyan), + ), Span::styled(format!("{} GB", volume.size_gb), Style::default()), ]), Line::from(vec![ @@ -200,7 +224,7 @@ fn render_volumes(app: &mut App, frame: &mut Frame, area: Rect) { // Use a stateful widget let mut state = ListState::default(); state.select(Some(app.selected_index)); - + frame.render_stateful_widget(list, area, &mut state); } @@ -223,20 +247,23 @@ fn render_networks(app: &mut App, frame: &mut Frame, area: Rect) { .iter() .map(|network| { let instance_count = network.instances.len(); - + ListItem::new(vec![ - Line::from(vec![ - Span::styled(format!("{}: ", network.name), Style::default().fg(Color::Cyan)), - Span::styled(network.cidr.clone(), Style::default()), - ]), Line::from(vec![ Span::styled( - format!("{} instance{} connected", - instance_count, - if instance_count == 1 { "" } else { "s" }), - Style::default() + format!("{}: ", network.name), + Style::default().fg(Color::Cyan), ), + Span::styled(network.cidr.clone(), Style::default()), ]), + Line::from(vec![Span::styled( + format!( + "{} instance{} connected", + instance_count, + if instance_count == 1 { "" } else { "s" } + ), + Style::default(), + )]), Line::from(""), ]) }) @@ -250,7 +277,7 @@ fn render_networks(app: &mut App, frame: &mut Frame, area: Rect) { // Use a stateful widget let mut state = ListState::default(); state.select(Some(app.selected_index)); - + frame.render_stateful_widget(list, area, &mut state); } @@ -270,15 +297,26 @@ fn render_settings(_app: &mut App, frame: &mut Frame, area: Rect) { ]) }); - let table = Table::new(rows, [Constraint::Percentage(50), Constraint::Percentage(50)]) - .block(Block::bordered().title("Settings").border_type(BorderType::Rounded)) - .header( - Row::new(vec![ - Cell::from(Span::styled("Setting", Style::default().add_modifier(Modifier::BOLD))), - Cell::from(Span::styled("Value", Style::default().add_modifier(Modifier::BOLD))), - ]) - ) - .column_spacing(2); + let table = Table::new( + rows, + [Constraint::Percentage(50), Constraint::Percentage(50)], + ) + .block( + Block::bordered() + .title("Settings") + .border_type(BorderType::Rounded), + ) + .header(Row::new(vec![ + Cell::from(Span::styled( + "Setting", + Style::default().add_modifier(Modifier::BOLD), + )), + Cell::from(Span::styled( + "Value", + Style::default().add_modifier(Modifier::BOLD), + )), + ])) + .column_spacing(2); frame.render_widget(table, area); } @@ -303,9 +341,16 @@ fn render_help(_app: &mut App, frame: &mut Frame, area: Rect) { ]) }); - let table = Table::new(rows, [Constraint::Percentage(20), Constraint::Percentage(80)]) - .block(Block::bordered().title("Keyboard Shortcuts").border_type(BorderType::Rounded)) - .column_spacing(2); + let table = Table::new( + rows, + [Constraint::Percentage(20), Constraint::Percentage(80)], + ) + .block( + Block::bordered() + .title("Keyboard Shortcuts") + .border_type(BorderType::Rounded), + ) + .column_spacing(2); frame.render_widget(table, area); } diff --git a/tests/vyos-lab/README.md b/tests/vyos-lab/README.md index 29f6c0d..5bfb45d 100644 --- a/tests/vyos-lab/README.md +++ b/tests/vyos-lab/README.md @@ -146,3 +146,4 @@ For more details on the technologies used in this lab, refer to: - [VyOS L3VPN/EVPN Documentation](https://docs.vyos.io/en/latest/configexamples/autotest/L3VPN_EVPN/L3VPN_EVPN.html) - [VyOS WireGuard Documentation](https://docs.vyos.io/en/latest/configexamples/autotest/Wireguard/Wireguard.html) - [VyOS VRF Documentation](https://docs.vyos.io/en/latest/configuration/vrf/index.html) +- [VyOS VRF Documentation](https://docs.vyos.io/en/latest/configuration/vrf/index.html) diff --git a/vyos-lab/ANALYSIS.md b/vyos-lab/ANALYSIS.md index 4927e4b..d1fe0ad 100644 --- a/vyos-lab/ANALYSIS.md +++ b/vyos-lab/ANALYSIS.md @@ -9,7 +9,7 @@ - The lab setup script exists but there's no integration with the main bbctl command line tool to deploy test topologies 3. **Multi-tenant Network Configuration** - - Despite detailed architecture plans, there's no implementation of tenant isolation via VRFs and VXLAN + - Despite detailed architecture plans, there's no implementation of tenant isolation via VRFs and VXLAN 4. **Key Management System** - The documented key rotation system for WireGuard isn't implemented in the codebase diff --git a/vyos-lab/PLAN.md b/vyos-lab/PLAN.md index 3929461..22f6940 100644 --- a/vyos-lab/PLAN.md +++ b/vyos-lab/PLAN.md @@ -179,6 +179,8 @@ set vrf name tenant1 interfaces 'br100' ## Security Considerations + +## Security Considerations - Isolation between tenants using VRFs - Encryption of management traffic with WireGuard - API access control and authentication @@ -196,3 +198,4 @@ set vrf name tenant1 interfaces 'br100' ## Conclusion This lab architecture provides a comprehensive environment for testing multi-tenant network isolation using VyOS and modern networking concepts. It combines the security of WireGuard with the flexibility and scalability of EVPN to create isolated tenant environments that closely mirror cloud deployment scenarios. +This lab architecture provides a comprehensive environment for testing multi-tenant network isolation using VyOS and modern networking concepts. It combines the security of WireGuard with the flexibility and scalability of EVPN to create isolated tenant environments that closely mirror cloud deployment scenarios. diff --git a/vyos-lab/configs/router1-config.yaml b/vyos-lab/configs/router1-config.yaml index 0752c67..947871f 100644 --- a/vyos-lab/configs/router1-config.yaml +++ b/vyos-lab/configs/router1-config.yaml @@ -12,11 +12,16 @@ write_files: [Network] Address=10.0.1.1/24 + + [Network] + Address=10.0.1.1/24 + - path: /etc/systemd/network/30-eth2.network content: | [Match] Name=eth2 + [Network] Address=10.0.2.1/24 @@ -35,3 +40,4 @@ runcmd: - systemctl restart systemd-networkd - systemctl restart frr - echo "Router 1 setup complete" + - echo "Router 1 setup complete" diff --git a/vyos-lab/configs/router2-config.yaml b/vyos-lab/configs/router2-config.yaml index 25c178c..d50a394 100644 --- a/vyos-lab/configs/router2-config.yaml +++ b/vyos-lab/configs/router2-config.yaml @@ -12,11 +12,16 @@ write_files: [Network] Address=10.0.2.2/24 + + [Network] + Address=10.0.2.2/24 + - path: /etc/systemd/network/30-eth2.network content: | [Match] Name=eth2 + [Network] Address=10.0.3.1/24 @@ -35,3 +40,4 @@ runcmd: - systemctl restart systemd-networkd - systemctl restart frr - echo "Router 2 setup complete" + - echo "Router 2 setup complete" diff --git a/vyos-lab/configs/router3-config.yaml b/vyos-lab/configs/router3-config.yaml index e6a9ad2..9c17b6f 100644 --- a/vyos-lab/configs/router3-config.yaml +++ b/vyos-lab/configs/router3-config.yaml @@ -12,11 +12,16 @@ write_files: [Network] Address=10.0.1.2/24 + + [Network] + Address=10.0.1.2/24 + - path: /etc/systemd/network/30-eth2.network content: | [Match] Name=eth2 + [Network] Address=10.0.3.2/24 @@ -25,6 +30,7 @@ write_files: [Match] Name=eth3 + [Network] Address=192.168.100.1/24 # This network simulates internet access @@ -48,3 +54,4 @@ runcmd: - systemctl restart frr - iptables -t nat -A POSTROUTING -o eth3 -s 10.0.0.0/8 -j MASQUERADE - echo "Router 3 setup complete" + - echo "Router 3 setup complete"