diff --git a/.github/workflows/nginx-container.yaml b/.github/workflows/nginx-container.yaml deleted file mode 100644 index bac86cd..0000000 --- a/.github/workflows/nginx-container.yaml +++ /dev/null @@ -1,101 +0,0 @@ -name: FeOS Nginx Container - -on: - push: - branches: - - master - paths-ignore: - - 'docs/**' - - '**/*.md' - -jobs: - build_nginx_container: - permissions: - contents: read - packages: write - - runs-on: ubuntu-latest - - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - uses: docker/metadata-action@v5 - id: meta - with: - images: | - ghcr.io/ironcore-dev/feos/feos-nginx - tags: | - type=ref,event=branch - type=sha,prefix={{branch}}- - type=raw,value=latest,enable={{is_default_branch}} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - install: true - - - name: Install Protobuf Compiler - run: sudo apt-get update && sudo apt-get install protobuf-compiler -y - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up cargo cache - uses: actions/cache@v4 - continue-on-error: false - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- - - - name: Run Tests - run: make test - - - name: Build build-container - run: make build-container - - - name: Set up kernel cache - uses: actions/cache@v4 - continue-on-error: false - with: - path: | - target/kernel/ - key: ${{ runner.os }}-kernel-${{ hashFiles('hack/kernel/**') }} - restore-keys: ${{ runner.os }}-kernel- - - - name: Build Kernel - run: make kernel - - - name: Build initramfs - run: make initramfs - - - name: Build UKI with secrets - run: | - CMDLINE="console=tty0 console=ttyS0,115200 intel_iommu=on iommu=pt" - echo $CMDLINE > target/cmdline - mkdir -p keys - echo "${{ secrets.SECUREBOOT_PRIVATEKEY }}" > keys/secureboot.key - echo "${{ secrets.SECUREBOOT_CERTIFICATE }}" > keys/secureboot.pem - openssl x509 -in keys/secureboot.pem -out keys/feos.crt -outform DER - make uki - - - name: Build and push multiarch nginx container - uses: docker/build-push-action@v5 - with: - context: . - file: hack/Dockerfile.nginx - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - diff --git a/.gitignore b/.gitignore index 2239ab5..ae5566b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /keys /.idea /.vscode +*.txt diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..22ac69a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# metalnet maintainers +* @ironcore-dev/networking \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 896c27c..0c58714 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,14 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "acpi_tables" -version = "0.1.0" -source = "git+https://github.com/rust-vmm/acpi_tables?branch=main#849d5950196f66dd10f2b2606d8fe8c7cb39ec24" -dependencies = [ - "zerocopy", -] - [[package]] name = "addr2line" version = "0.24.2" @@ -21,9 +13,22 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] [[package]] name = "aho-corasick" @@ -35,10 +40,10 @@ dependencies = [ ] [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "allocator-api2" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" @@ -49,20 +54,11 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -75,79 +71,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[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.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", + "once_cell_polyfill", "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" - -[[package]] -name = "api_client" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "thiserror 1.0.68", - "vmm-sys-util", -] - -[[package]] -name = "arc-swap" -version = "1.7.1" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - -[[package]] -name = "arch" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "anyhow", - "byteorder", - "fdt", - "hypervisor", - "libc", - "linux-loader", - "log", - "serde", - "thiserror 1.0.68", - "uuid", - "vm-fdt", - "vm-memory", - "vm-migration", - "vmm-sys-util", -] +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "async-stream" @@ -168,18 +129,27 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", ] [[package]] @@ -189,35 +159,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "atty" -version = "0.2.14" +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "aws-lc-rs" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "94b8ff6c09cd57b16da53641caa860168b88c172a5ee163b0288d3d6eea12786" dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", + "aws-lc-sys", + "zeroize", ] [[package]] -name = "autocfg" -version = "1.4.0" +name = "aws-lc-sys" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "0e44d16778acaf6a9ec9899b92cebd65580b83f685446bf2e1f5d3d732f99dcd" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] [[package]] name = "axum" -version = "0.7.7" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", + "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "http-body-util", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", "itoa", "matchit", "memchr", @@ -226,37 +209,34 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper 1.0.1", - "tower 0.5.1", + "sync_wrapper 0.1.2", + "tower 0.4.13", "tower-layer", "tower-service", ] [[package]] name = "axum-core" -version = "0.4.5" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", - "http-body-util", + "http 0.2.12", + "http-body 0.4.6", "mime", - "pin-project-lite", "rustversion", - "sync_wrapper 1.0.1", "tower-layer", "tower-service", ] [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -267,18 +247,60 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5032d51da2741729bfdaeb2664d9b8c6d9fd1e2b90715c660b6def36628499c2" +dependencies = [ + "byteorder", + "safemem", +] + [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.102", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -287,29 +309,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "block" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ - "byteorder", - "crc-any", - "libc", - "log", - "remain", "serde", - "smallvec", - "thiserror 1.0.68", - "uuid", - "virtio-bindings", - "virtio-queue", - "vm-memory", - "vm-virtio", - "vmm-sys-util", ] [[package]] @@ -323,9 +327,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "byteorder" @@ -335,40 +339,35 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] -name = "caps" -version = "0.5.5" +name = "cc" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190baaad529bcfbde9e1a19022c42781bdb6ff9de25721abdb8fd98c0807730b" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ + "jobserver", "libc", - "thiserror 1.0.68", + "shlex", ] [[package]] -name = "cc" -version = "1.1.37" +name = "cexpr" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "shlex", + "nom", ] [[package]] name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cfg_aliases" -version = "0.1.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -378,270 +377,246 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link 0.2.0", ] [[package]] -name = "clap" -version = "2.34.0" +name = "clang-sys" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim 0.8.0", - "textwrap", - "unicode-width", - "vec_map", + "glob", + "libc", + "libloading", ] [[package]] name = "clap" -version = "4.5.20" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] -name = "clap_lex" -version = "0.7.2" +name = "clap_derive" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.102", +] [[package]] -name = "colorchoice" -version = "1.0.3" +name = "clap_lex" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] -name = "colored" -version = "2.1.0" +name = "cloud-hypervisor-client" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +checksum = "a135a9339a5ad2775b8843dfad19cd5057e9d8092a3c09b523c5030fcc1348ee" dependencies = [ - "lazy_static", - "windows-sys 0.48.0", + "base64 0.7.0", + "futures", + "http 0.2.12", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "hyperlocal", + "serde", + "serde_json", + "serde_repr", + "thiserror 2.0.12", + "url", + "uuid", ] [[package]] -name = "core-foundation" -version = "0.9.4" +name = "cmake" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" dependencies = [ - "core-foundation-sys", - "libc", + "cc", ] [[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "colorchoice" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "cpufeatures" -version = "0.2.14" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" -dependencies = [ - "libc", -] +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] -name = "crc-any" -version = "2.5.0" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62ec9ff5f7965e4d7280bd5482acd20aadb50d632cf6c1d74493856b011fa73" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "debug-helper", + "core-foundation-sys", + "libc", ] [[package]] -name = "crc32fast" -version = "1.4.2" +name = "core-foundation" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ - "cfg-if", + "core-foundation-sys", + "libc", ] [[package]] -name = "crypto-common" -version = "0.1.6" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "darling" -version = "0.20.10" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "darling_core", - "darling_macro", + "libc", ] [[package]] -name = "darling_core" -version = "0.20.10" +name = "crc" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.87", + "crc-catalog", ] [[package]] -name = "darling_macro" -version = "0.20.10" +name = "crc-catalog" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.87", -] +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] -name = "data-encoding" -version = "2.6.0" +name = "critical-section" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] -name = "dataview" -version = "1.0.1" +name = "crossbeam-queue" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50eb3a329e19d78c3a3dfa4ec5a51ecb84fa3a20c06edad04be25356018218f9" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ - "derive_pod", + "crossbeam-utils", ] [[package]] -name = "debug-helper" -version = "0.3.13" +name = "crossbeam-utils" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "deranged" -version = "0.3.11" +name = "crossterm" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "powerfmt", + "bitflags 2.9.1", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", ] [[package]] -name = "derive_builder" -version = "0.20.2" +name = "crossterm_winapi" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ - "derive_builder_macro", + "winapi", ] [[package]] -name = "derive_builder_core" -version = "0.20.2" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.87", + "generic-array", + "typenum", ] [[package]] -name = "derive_builder_macro" -version = "0.20.2" +name = "data-encoding" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn 2.0.87", -] +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] -name = "derive_pod" -version = "0.1.2" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" - -[[package]] -name = "devices" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "acpi_tables", - "anyhow", - "arch", - "bitflags 2.6.0", - "byteorder", - "event_monitor", - "hypervisor", - "libc", - "log", - "num_enum", - "pci", - "serde", - "thiserror 1.0.68", - "tpm", - "vm-allocator", - "vm-device", - "vm-memory", - "vm-migration", - "vmm-sys-util", + "const-oid", + "pem-rfc7468", + "zeroize", ] [[package]] name = "dhcproto" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6794294f2c4665aae452e950c2803a1e487c5672dc8448f0bfa3f52ff67e270" +checksum = "f77998aa4d800695f2a6c746b2350c72ad4512ae556d515b69d12122d5467e05" dependencies = [ "dhcproto-macros", "hex", + "hickory-proto", "ipnet", - "rand", - "thiserror 1.0.68", - "trust-dns-proto", + "rand 0.9.2", + "thiserror 2.0.12", "url", ] @@ -658,6 +633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -670,155 +646,186 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] -name = "either" -version = "1.13.0" +name = "dotenvy" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] -name = "enum-as-inner" -version = "0.5.1" +name = "dunce" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] -name = "enumflags2" -version = "0.7.10" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" dependencies = [ - "enumflags2_derive", + "serde", ] [[package]] -name = "enumflags2_derive" -version = "0.7.10" +name = "enum-as-inner" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] -name = "epoll" -version = "4.3.3" +name = "env_filter" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74351c3392ea1ff6cd2628e0042d268ac2371cb613252ff383b6dfa50d22fa79" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ - "bitflags 2.6.0", - "libc", + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] -name = "event_monitor" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ - "flume", - "libc", - "once_cell", - "serde", - "serde_json", + "cfg-if", + "home", + "windows-sys 0.48.0", ] [[package]] -name = "fastrand" -version = "2.2.0" +name = "event-listener" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] -name = "fdt" -version = "0.1.5" +name = "fastrand" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784a4df722dc6267a04af36895398f59d21d07dce47232adf31ec0ff2fa45e67" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "feos" -version = "0.1.0" +version = "0.5.0" dependencies = [ "anyhow", - "api_client", - "async-stream", - "bitflags 2.6.0", "chrono", + "clap", "dhcproto", - "flate2", + "dotenvy", + "env_logger", + "feos-proto", + "feos-utils", "futures", - "futures-core", - "futures-util", - "hyper-util", + "host-service", + "image-service", "libc", - "libcgroups", - "libcontainer", "log", - "net_util", "netlink-packet-route", "nix 0.29.0", - "oci-distribution", - "openssl", - "pelite", + "once_cell", "pnet", "prost", - "rand", + "prost-types", "regex", "rtnetlink", - "serde", - "serde_json", - "serial_test", - "simple_logger", - "socket2", - "structopt", - "tar", - "thiserror 2.0.1", + "socket2 0.6.0", + "tempfile", + "termcolor", + "tokio", + "tokio-stream", + "tonic", + "tower 0.4.13", + "vm-service", +] + +[[package]] +name = "feos-cli" +version = "0.5.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "crossterm", + "digest", + "env_logger", + "feos-proto", + "hex", + "log", + "prost", + "sha2", "tokio", "tokio-stream", - "tokio-vsock", + "tonic", + "tower 0.4.13", +] + +[[package]] +name = "feos-proto" +version = "0.5.0" +dependencies = [ + "prost", + "prost-types", "tonic", "tonic-build", - "tower 0.5.1", - "uuid", - "vmm", ] [[package]] -name = "filetime" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +name = "feos-utils" +version = "0.5.0" dependencies = [ - "cfg-if", + "chrono", + "dhcproto", + "futures", "libc", - "libredox", - "windows-sys 0.59.0", + "log", + "netlink-packet-route", + "nix 0.29.0", + "pnet", + "rtnetlink", + "socket2 0.6.0", + "termcolor", + "tokio", ] [[package]] @@ -827,22 +834,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flate2" -version = "1.0.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "flume" version = "0.11.1" @@ -851,7 +842,6 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "nanorand", "spin", ] @@ -885,6 +875,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -925,7 +921,17 @@ dependencies = [ "futures-core", "futures-task", "futures-util", - "num_cpus", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", ] [[package]] @@ -942,7 +948,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] @@ -987,27 +993,25 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", - "js-sys", "libc", - "wasi", - "wasm-bindgen", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] -name = "getset" -version = "0.1.3" +name = "getrandom" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.87", + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -1018,23 +1022,42 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "h2" -version = "0.4.6" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.9.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http", - "indexmap 2.6.0", + "http 1.3.1", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -1049,17 +1072,37 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] -name = "heck" -version = "0.3.3" +name = "hashlink" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "unicode-segmentation", + "hashbrown 0.14.5", +] + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "byteorder", + "num-traits", ] [[package]] @@ -1067,6 +1110,9 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "heck" @@ -1075,25 +1121,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "hickory-proto" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.2", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "url", +] [[package]] -name = "hex" -version = "0.4.3" +name = "hkdf" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] [[package]] name = "hmac" @@ -1106,18 +1168,54 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "host-service" +version = "0.5.0" +dependencies = [ + "anyhow", + "digest", + "feos-proto", + "feos-utils", + "hex", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-util", + "log", + "nix 0.29.0", + "prost", + "prost-types", + "rustls-pki-types", + "sha2", + "tempfile", + "tokio", + "tokio-stream", + "tonic", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", ] [[package]] name = "http" -version = "1.1.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -1133,6 +1231,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1140,27 +1249,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.3.1", ] [[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", - "http-body", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1170,16 +1279,40 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.4.1" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.10", + "http 1.3.1", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -1190,18 +1323,35 @@ dependencies = [ ] [[package]] -name = "hyper-timeout" -version = "0.5.2" +name = "hyper-rustls" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "hyper", + "http 1.3.1", + "hyper 1.6.0", "hyper-util", - "pin-project-lite", + "log", + "rustls 0.23.31", + "rustls-native-certs", + "rustls-pki-types", "tokio", + "tokio-rustls", "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper 0.14.32", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1210,7 +1360,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "native-tls", "tokio", @@ -1220,52 +1370,54 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", - "http", - "http-body", - "hyper", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.6.0", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", ] [[package]] -name = "hypervisor" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ - "anyhow", - "byteorder", - "kvm-bindings", - "kvm-ioctls", - "libc", - "log", - "serde", - "serde_with", - "thiserror 1.0.68", - "vfio-ioctls", - "vm-memory", - "vmm-sys-util", + "hex", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", ] [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -1281,21 +1433,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1304,31 +1457,11 @@ 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.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1336,84 +1469,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.0.3" @@ -1427,14 +1530,33 @@ 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", ] +[[package]] +name = "image-service" +version = "0.5.0" +dependencies = [ + "anyhow", + "feos-proto", + "log", + "oci-distribution", + "prost", + "prost-types", + "serde", + "serde_json", + "tempfile", + "tokio", + "tokio-stream", + "tonic", + "uuid", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1447,19 +1569,30 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.4", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", ] [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "ipnetwork" @@ -1470,6 +1603,16 @@ dependencies = [ "serde", ] +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1478,75 +1621,76 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.10.5" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] -name = "js-sys" -version = "0.3.72" +name = "jiff" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ - "wasm-bindgen", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", ] [[package]] -name = "jwt" -version = "0.16.0" +name = "jiff-static" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ - "base64 0.13.1", - "crypto-common", - "digest", - "hmac", - "serde", - "serde_json", - "sha2", + "proc-macro2", + "quote", + "syn 2.0.102", ] [[package]] -name = "kvm-bindings" -version = "0.8.2" +name = "jobserver" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ac3147c9763fd8fa7865a90d6aee87f157b59167145b38e671bbc66b116f1e8" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "serde", - "vmm-sys-util", - "zerocopy", + "getrandom 0.3.3", + "libc", ] [[package]] -name = "kvm-ioctls" -version = "0.17.0" +name = "js-sys" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bedae2ca4a531bebe311abaf9691f5cc14eaa21475243caa2e39c43bb872947d" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ - "bitflags 2.6.0", - "kvm-bindings", - "libc", - "vmm-sys-util", + "once_cell", + "wasm-bindgen", ] [[package]] -name = "landlock" -version = "0.4.1" +name = "jwt" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18738c5d4c7fae6727a96adb94722ef7ce82f3eafea0a11777e258a93816537e" +checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" dependencies = [ - "enumflags2", - "libc", - "thiserror 1.0.68", + "base64 0.13.1", + "crypto-common", + "digest", + "hmac", + "serde", + "serde_json", + "sha2", ] [[package]] @@ -1554,94 +1698,60 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" -version = "0.2.162" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" - -[[package]] -name = "libcgroups" -version = "0.4.1" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6c844cd81f0e078bb07896a14fddcec9f9582833ce18f99c2d4c9b69081d53" -dependencies = [ - "fixedbitset 0.5.7", - "nix 0.28.0", - "oci-spec", - "procfs", - "serde", - "thiserror 1.0.68", - "tracing", -] +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] -name = "libcontainer" -version = "0.4.1" +name = "libloading" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e301f76db45c6b2612de0fb1978b9e245fd64a36898ff35928760aee7e34af70" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ - "bitflags 2.6.0", - "caps", - "chrono", - "fastrand", - "futures", - "libc", - "libcgroups", - "nc", - "nix 0.28.0", - "oci-spec", - "once_cell", - "prctl", - "procfs", - "protobuf", - "regex", - "rust-criu", - "safe-path", - "serde", - "serde_json", - "thiserror 1.0.68", - "tracing", + "cfg-if", + "windows-targets 0.52.6", ] [[package]] -name = "libredox" -version = "0.1.3" +name = "libm" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags 2.6.0", - "libc", - "redox_syscall", -] +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] -name = "linux-loader" -version = "0.11.0" +name = "libsqlite3-sys" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb68dd3452f25a8defaf0ae593509cff0c777683e4d8924f59ac7c5f89267a83" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ - "vm-memory", + "cc", + "pkg-config", + "vcpkg", ] [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1649,15 +1759,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "matches" -version = "0.1.10" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "matchit" @@ -1665,11 +1769,21 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" @@ -1680,62 +1794,61 @@ dependencies = [ "autocfg", ] -[[package]] -name = "micro_http" -version = "0.1.0" -source = "git+https://github.com/firecracker-microvm/micro-http?branch=main#8182cd5523b63ceb52ad9d0e7eb6fb95683e6d1b" -dependencies = [ - "libc", - "vmm-sys-util", -] - [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ - "hermit-abi 0.3.9", "libc", - "wasi", - "windows-sys 0.52.0", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.48.0", ] [[package]] -name = "multimap" -version = "0.8.3" +name = "mio" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] [[package]] -name = "nanorand" -version = "0.7.0" +name = "multimap" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -dependencies = [ - "getrandom", -] +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -1743,48 +1856,11 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] -[[package]] -name = "nc" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34566634a278b9af0f62b872339d884ea689982514825ba306705f264038144e" -dependencies = [ - "cc", -] - -[[package]] -name = "net_gen" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "vmm-sys-util", -] - -[[package]] -name = "net_util" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "epoll", - "getrandom", - "libc", - "log", - "net_gen", - "rate_limiter", - "serde", - "thiserror 1.0.68", - "virtio-bindings", - "virtio-queue", - "vm-memory", - "vm-virtio", - "vmm-sys-util", -] - [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -1819,29 +1895,28 @@ dependencies = [ "anyhow", "byteorder", "paste", - "thiserror 1.0.68", + "thiserror 1.0.69", ] [[package]] name = "netlink-proto" -version = "0.11.3" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b33524dc0968bfad349684447bfce6db937a9ac3332a1fe60c0c5a5ce63f21" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" dependencies = [ "bytes", "futures", "log", "netlink-packet-core", "netlink-sys", - "thiserror 1.0.68", - "tokio", + "thiserror 2.0.12", ] [[package]] name = "netlink-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416060d346fbaf1f23f9512963e3e878f1a78e707cb699ba9215761754244307" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" dependencies = [ "bytes", "futures", @@ -1856,22 +1931,9 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.6.0", - "cfg-if", - "libc", -] - -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "cfg-if", - "cfg_aliases 0.1.1", "libc", - "memoffset", ] [[package]] @@ -1880,20 +1942,14 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "memoffset", "pin-utils", ] -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "no-std-net" version = "0.6.0" @@ -1901,65 +1957,67 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" [[package]] -name = "num-conv" -version = "0.1.0" +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] [[package]] -name = "num-traits" -version = "0.2.19" +name = "num-bigint-dig" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ - "autocfg", + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", ] [[package]] -name = "num_cpus" -version = "1.16.0" +name = "num-integer" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" -dependencies = [ - "num_enum_derive", + "num-traits", ] [[package]] -name = "num_enum_derive" -version = "0.7.3" +name = "num-iter" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.87", + "autocfg", + "num-integer", + "num-traits", ] [[package]] -name = "num_threads" -version = "0.1.7" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "libc", + "autocfg", + "libm", ] [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -1973,7 +2031,7 @@ dependencies = [ "bytes", "chrono", "futures-util", - "http", + "http 1.3.1", "http-auth", "jwt", "lazy_static", @@ -1983,29 +2041,12 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 1.0.68", + "thiserror 1.0.69", "tokio", "tracing", "unicase", ] -[[package]] -name = "oci-spec" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f5a3fe998d50101ae009351fec56d88a69f4ed182e11000e711068c2f5abf72" -dependencies = [ - "derive_builder", - "getset", - "once_cell", - "regex", - "serde", - "serde_json", - "strum", - "strum_macros", - "thiserror 1.0.68", -] - [[package]] name = "olpc-cjson" version = "0.1.4" @@ -2019,17 +2060,27 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", @@ -2046,29 +2097,29 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.4.0+3.4.0" +version = "300.5.0+3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -2077,16 +2128,11 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "option_parser" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" - [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -2094,9 +2140,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -2112,46 +2158,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "pci" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "anyhow", - "byteorder", - "hypervisor", - "libc", - "log", - "serde", - "thiserror 1.0.68", - "vfio-bindings", - "vfio-ioctls", - "vfio_user", - "vm-allocator", - "vm-device", - "vm-memory", - "vm-migration", - "vmm-sys-util", -] - -[[package]] -name = "pelite" -version = "0.10.0" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ - "dataview", - "libc", - "no-std-compat", - "pelite-macros", - "winapi", + "base64ct", ] -[[package]] -name = "pelite-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b" - [[package]] name = "percent-encoding" version = "2.3.1" @@ -2164,35 +2178,35 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset 0.4.2", - "indexmap 2.6.0", + "fixedbitset", + "indexmap 2.9.0", ] [[package]] name = "pin-project" -version = "1.1.7" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.7" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2200,11 +2214,32 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "pnet" @@ -2251,7 +2286,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] @@ -2298,135 +2333,62 @@ dependencies = [ ] [[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prctl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "059a34f111a9dee2ce1ac2826a68b24601c4298cfeb1a587c3cb493d5ab46f52" -dependencies = [ - "libc", - "nix 0.29.0", -] - -[[package]] -name = "prettyplease" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" -dependencies = [ - "proc-macro2", - "syn 2.0.87", -] - -[[package]] -name = "proc-macro-crate" -version = "3.2.0" +name = "portable-atomic" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" -dependencies = [ - "toml_edit", -] +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "portable-atomic-util" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", + "portable-atomic", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "potential_utf" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ - "proc-macro2", - "quote", - "version_check", + "zerovec", ] [[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "proc-macro2", - "quote", + "zerocopy", ] [[package]] -name = "proc-macro-error2" -version = "2.0.1" +name = "prettyplease" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" dependencies = [ - "proc-macro-error-attr2", "proc-macro2", - "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] -[[package]] -name = "procfs" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" -dependencies = [ - "bitflags 2.6.0", - "chrono", - "flate2", - "hex", - "lazy_static", - "procfs-core", - "rustix", -] - -[[package]] -name = "procfs-core" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" -dependencies = [ - "bitflags 2.6.0", - "chrono", - "hex", -] - [[package]] name = "prost" -version = "0.13.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", "prost-derive", @@ -2434,9 +2396,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.13.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", "heck 0.5.0", @@ -2449,111 +2411,86 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.87", + "syn 2.0.102", "tempfile", ] [[package]] name = "prost-derive" -version = "0.13.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] name = "prost-types" -version = "0.13.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ "prost", ] [[package]] -name = "protobuf" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" -dependencies = [ - "once_cell", - "protobuf-support", - "thiserror 1.0.68", -] - -[[package]] -name = "protobuf-codegen" -version = "3.2.0" +name = "quote" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ - "anyhow", - "once_cell", - "protobuf", - "protobuf-parse", - "regex", - "tempfile", - "thiserror 1.0.68", + "proc-macro2", ] [[package]] -name = "protobuf-parse" -version = "3.2.0" +name = "r-efi" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" -dependencies = [ - "anyhow", - "indexmap 1.9.3", - "log", - "protobuf", - "protobuf-support", - "tempfile", - "thiserror 1.0.68", - "which", -] +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] -name = "protobuf-support" -version = "3.2.0" +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "thiserror 1.0.68", + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", ] [[package]] -name = "quote" -version = "1.0.37" +name = "rand" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "proc-macro2", + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] -name = "rand" -version = "0.8.5" +name = "rand_chacha" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "libc", - "rand_chacha", - "rand_core", + "ppv-lite86", + "rand_core 0.6.4", ] [[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", + "rand_core 0.9.3", ] [[package]] @@ -2562,35 +2499,32 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] -name = "rate_limiter" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "epoll", - "libc", - "log", - "thiserror 1.0.68", - "vmm-sys-util", + "getrandom 0.3.3", ] [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2600,9 +2534,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2615,56 +2549,77 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "remain" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46aef80f842736de545ada6ec65b81ee91504efd6853f4b96de7414c42ae7443" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "reqwest" -version = "0.12.9" +version = "0.12.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ "base64 0.22.1", "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", - "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tokio", "tokio-native-tls", "tokio-util", + "tower 0.5.2", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", ] [[package]] @@ -2681,124 +2636,177 @@ dependencies = [ "netlink-proto", "netlink-sys", "nix 0.27.1", - "thiserror 1.0.68", + "thiserror 1.0.69", "tokio", ] [[package]] -name = "rust-criu" -version = "0.4.0" +name = "rustc-demangle" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4737b28406b3395359f485127073117a11cedc8942738b69ba6ab9a79432acbc" -dependencies = [ - "anyhow", - "libc", - "protobuf", - "protobuf-codegen", -] +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "0.38.39" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] -name = "rustls-pemfile" -version = "2.2.0" +name = "rustls" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ - "rustls-pki-types", + "ring", + "rustls-webpki 0.101.7", + "sct", ] [[package]] -name = "rustls-pki-types" -version = "1.10.0" +name = "rustls" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.5", + "subtle", + "zeroize", +] [[package]] -name = "rustversion" -version = "1.0.18" +name = "rustls-native-certs" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.4.0", +] [[package]] -name = "ryu" -version = "1.0.18" +name = "rustls-pemfile" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] [[package]] -name = "safe-path" -version = "0.1.0" +name = "rustls-pki-types" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980abdd3220aa19b67ca3ea07b173ca36383f18ae48cde696d90c8af39447ffb" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "libc", + "zeroize", ] [[package]] -name = "scc" -version = "2.2.4" +name = "rustls-webpki" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d25269dd3a12467afe2e510f69fb0b46b698e5afb296b59f2145259deaf8e8" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "sdd", + "ring", + "untrusted", ] [[package]] -name = "schannel" -version = "0.1.26" +name = "rustls-webpki" +version = "0.103.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "b5a37813727b78798e53c2bec3f5e8fe12a6d6f8389bf9ca7802add4c9905ad8" dependencies = [ - "windows-sys 0.59.0", + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "rustversion" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] -name = "sdd" -version = "3.0.4" +name = "ryu" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49c1eeaf4b6a87c7479688c6d52b9f1153cedd3c489300564f932b065c6eab95" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] -name = "seccompiler" -version = "0.4.0" +name = "safemem" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345a3e4dddf721a478089d4697b83c6c0a8f5bf16086f6c13397e4534eb6e2e5" -dependencies = [ - "libc", -] +checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" [[package]] -name = "security-framework" +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", - "core-foundation", + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b369d18893388b345804dc0007963c99b7d665ae71d275812d828c6f089640" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2806,9 +2814,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -2816,29 +2824,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.214" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -2847,75 +2855,44 @@ dependencies = [ ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" -dependencies = [ - "serde", - "serde_derive", - "serde_with_macros", -] - -[[package]] -name = "serde_with_macros" -version = "3.11.0" +name = "serde_repr" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ - "darling", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] -name = "serial_buffer" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" - -[[package]] -name = "serial_test" -version = "3.2.0" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ - "futures", - "log", - "once_cell", - "parking_lot", - "scc", - "serial_test_derive", + "form_urlencoded", + "itoa", + "ryu", + "serde", ] [[package]] -name = "serial_test_derive" -version = "3.2.0" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", + "cfg-if", + "cpufeatures", + "digest", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -2930,33 +2907,42 @@ 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", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] [[package]] -name = "simple_logger" -version = "5.0.0" +name = "signature" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "colored", - "log", - "time", - "windows-sys 0.48.0", + "digest", + "rand_core 0.6.4", ] [[package]] @@ -2970,20 +2956,30 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -2994,66 +2990,249 @@ dependencies = [ ] [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] [[package]] -name = "strsim" -version = "0.8.0" +name = "sqlformat" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] [[package]] -name = "strsim" -version = "0.11.1" +name = "sqlx" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] [[package]] -name = "structopt" -version = "0.3.26" +name = "sqlx-core" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ - "clap 2.34.0", - "lazy_static", - "structopt-derive", + "ahash", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.9.0", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls 0.21.12", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", ] [[package]] -name = "structopt-derive" -version = "0.4.18" +name = "sqlx-macros" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" dependencies = [ - "heck 0.3.3", - "proc-macro-error", "proc-macro2", "quote", + "sqlx-core", + "sqlx-macros-core", "syn 1.0.109", ] [[package]] -name = "strum" -version = "0.26.3" +name = "sqlx-macros-core" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] [[package]] -name = "strum_macros" -version = "0.26.4" +name = "sqlx-mysql" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.87", + "atoi", + "base64 0.21.7", + "bitflags 2.9.1", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.9.1", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "urlencoding", + "uuid", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -3073,9 +3252,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462" dependencies = [ "proc-macro2", "quote", @@ -3090,135 +3269,91 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] [[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", - "syn 2.0.87", -] - -[[package]] -name = "tar" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" -dependencies = [ - "filetime", - "libc", - "xattr", + "syn 2.0.102", ] [[package]] name = "tempfile" -version = "3.14.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "termcolor" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ - "unicode-width", + "winapi-util", ] [[package]] name = "thiserror" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.68", + "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" -version = "2.0.1" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c1e40dd48a282ae8edc36c732cbc219144b87fb6a4c7316d611c6b1f06ec0c" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.1", + "thiserror-impl 2.0.12", ] [[package]] name = "thiserror-impl" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] name = "thiserror-impl" -version = "2.0.1" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874aa7e446f1da8d9c3a5c95b1c5eb41d800045252121dc7f8e0ba370cee55f5" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", -] - -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" -dependencies = [ - "num-conv", - "time-core", + "syn 2.0.102", ] [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -3226,9 +3361,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -3241,31 +3376,43 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", - "mio", + "mio 1.0.4", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", ] [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] @@ -3279,81 +3426,58 @@ dependencies = [ ] [[package]] -name = "tokio-stream" -version = "0.1.16" +name = "tokio-rustls" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "futures-core", - "pin-project-lite", + "rustls 0.23.31", "tokio", ] [[package]] -name = "tokio-util" -version = "0.7.12" +name = "tokio-stream" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ - "bytes", "futures-core", - "futures-sink", "pin-project-lite", "tokio", ] [[package]] -name = "tokio-vsock" -version = "0.5.0" -source = "git+https://github.com/rust-vsock/tokio-vsock?rev=3a41323#3a413237c67cbad699e9a5bd80598d5278fc1720" +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", - "futures", - "libc", + "futures-core", + "futures-sink", + "pin-project-lite", "tokio", - "tonic", - "vsock", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" - -[[package]] -name = "toml_edit" -version = "0.22.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" -dependencies = [ - "indexmap 2.6.0", - "toml_datetime", - "winnow", ] [[package]] name = "tonic" -version = "0.12.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.22.1", + "base64 0.21.7", "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", "hyper-timeout", - "hyper-util", "percent-encoding", "pin-project", "prost", - "socket2", "tokio", "tokio-stream", "tower 0.4.13", @@ -3364,16 +3488,15 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.12.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +checksum = "be4ef6dd70a610078cb4e338a0f79d06bc759ff1b22d2120c2ff02ae264ba9c2" dependencies = [ "prettyplease", "proc-macro2", "prost-build", - "prost-types", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] @@ -3384,10 +3507,11 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", + "hdrhistogram", "indexmap 1.9.3", "pin-project", "pin-project-lite", - "rand", + "rand 0.8.5", "slab", "tokio", "tokio-util", @@ -3398,61 +3522,54 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.2", + "tokio", "tower-layer", "tower-service", ] [[package]] -name = "tower-layer" -version = "0.3.3" +name = "tower-http" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", +] [[package]] -name = "tower-service" +name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tpm" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "anyhow", - "byteorder", - "libc", - "log", - "net_gen", - "thiserror 1.0.68", - "vmm-sys-util", -] +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] -name = "tracer" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "libc", - "log", - "once_cell", - "serde", - "serde_json", -] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -3462,48 +3579,24 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] -[[package]] -name = "trust-dns-proto" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna 0.2.3", - "ipnet", - "lazy_static", - "rand", - "smallvec", - "thiserror 1.0.68", - "tinyvec", - "tracing", - "url", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -3512,27 +3605,27 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicase" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" @@ -3543,6 +3636,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -3550,27 +3649,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] -name = "unicode-width" -version = "0.1.14" +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 1.0.3", + "idna", "percent-encoding", ] [[package]] -name = "utf16_iter" -version = "1.0.5" +name = "urlencoding" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf8_iter" @@ -3586,11 +3691,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom", + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", ] [[package]] @@ -3599,12 +3707,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.5" @@ -3612,302 +3714,109 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "vfio-bindings" -version = "0.4.0" -source = "git+https://github.com/rust-vmm/vfio?branch=main#03fc67acfd64f906578fb462b009b014b0cc9d8b" -dependencies = [ - "vmm-sys-util", -] - -[[package]] -name = "vfio-ioctls" -version = "0.2.0" -source = "git+https://github.com/rust-vmm/vfio?branch=main#03fc67acfd64f906578fb462b009b014b0cc9d8b" -dependencies = [ - "byteorder", - "kvm-bindings", - "kvm-ioctls", - "libc", - "log", - "thiserror 1.0.68", - "vfio-bindings", - "vm-memory", - "vmm-sys-util", -] - -[[package]] -name = "vfio_user" -version = "0.1.0" -source = "git+https://github.com/rust-vmm/vfio-user?branch=main#a1f6e52829e069b6d698b2cfeecac742e4653186" -dependencies = [ - "bitflags 1.3.2", - "libc", - "log", - "serde", - "serde_derive", - "serde_json", - "thiserror 1.0.68", - "vfio-bindings", - "vm-memory", - "vmm-sys-util", -] - -[[package]] -name = "vhost" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6be08d1166d41a78861ad50212ab3f9eca0729c349ac3a7a8f557c62406b87cc" -dependencies = [ - "bitflags 2.6.0", - "libc", - "vm-memory", - "vmm-sys-util", -] - -[[package]] -name = "virtio-bindings" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1711e61c00f8cb450bd15368152a1e37a12ef195008ddc7d0f4812f9e2b30a68" - -[[package]] -name = "virtio-devices" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "anyhow", - "arc-swap", - "block", - "byteorder", - "epoll", - "event_monitor", - "libc", - "log", - "net_gen", - "net_util", - "pci", - "rate_limiter", - "seccompiler", - "serde", - "serde_json", - "serde_with", - "serial_buffer", - "thiserror 1.0.68", - "vhost", - "virtio-bindings", - "virtio-queue", - "vm-allocator", - "vm-device", - "vm-memory", - "vm-migration", - "vm-virtio", - "vmm-sys-util", -] - -[[package]] -name = "virtio-queue" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d8406e7250c934462de585d8f2d2781c31819bca1fbb7c5e964ca6bbaabfe8" -dependencies = [ - "log", - "virtio-bindings", - "vm-memory", - "vmm-sys-util", -] - -[[package]] -name = "vm-allocator" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "arch", - "libc", - "vm-memory", -] - -[[package]] -name = "vm-device" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "anyhow", - "hypervisor", - "serde", - "thiserror 1.0.68", - "vfio-ioctls", - "vm-memory", - "vmm-sys-util", -] - -[[package]] -name = "vm-fdt" -version = "0.3.0" -source = "git+https://github.com/rust-vmm/vm-fdt?branch=main#ef5bd734f5f66fb07722d766981adbc915f0d941" - -[[package]] -name = "vm-memory" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3aba5064cc5f6f7740cddc8dae34d2d9a311cac69b60d942af7f3ab8fc49f4" -dependencies = [ - "arc-swap", - "libc", - "thiserror 1.0.68", - "winapi", -] - -[[package]] -name = "vm-migration" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "anyhow", - "serde", - "serde_json", - "thiserror 1.0.68", - "vm-memory", -] - -[[package]] -name = "vm-virtio" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" -dependencies = [ - "log", - "virtio-queue", - "vm-memory", -] - -[[package]] -name = "vmm" -version = "0.1.0" -source = "git+https://github.com/cloud-hypervisor/cloud-hypervisor?tag=v41.0#ea3e2ff625eba2c2576b0b68df4d649657ccf7cd" +name = "vm-service" +version = "0.5.0" dependencies = [ - "acpi_tables", "anyhow", - "arc-swap", - "arch", - "bitflags 2.6.0", - "block", - "cfg-if", - "clap 4.5.20", - "devices", - "epoll", - "event_monitor", - "flume", - "hypervisor", - "landlock", - "libc", - "linux-loader", + "cloud-hypervisor-client", + "dotenvy", + "feos-proto", + "hyper 1.6.0", + "hyper-util", + "hyperlocal", + "image-service", "log", - "micro_http", - "net_util", + "nix 0.29.0", "once_cell", - "option_parser", - "pci", - "rate_limiter", - "seccompiler", + "openssl", + "prost", + "prost-types", "serde", "serde_json", - "serial_buffer", - "signal-hook", - "thiserror 1.0.68", - "tracer", + "sqlx", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tonic", + "tower 0.4.13", + "urlencoding", "uuid", - "vfio-ioctls", - "vfio_user", - "virtio-devices", - "virtio-queue", - "vm-allocator", - "vm-device", - "vm-memory", - "vm-migration", - "vm-virtio", - "vmm-sys-util", - "zerocopy", ] [[package]] -name = "vmm-sys-util" -version = "0.12.1" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1435039746e20da4f8d507a72ee1b916f7b4b05af7a91c093d2c6561934ede" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "bitflags 1.3.2", - "libc", - "serde", - "serde_derive", + "try-lock", ] [[package]] -name = "vsock" -version = "0.5.1" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8b4d00e672f147fc86a09738fadb1445bd1c0a40542378dfb82909deeee688" -dependencies = [ - "libc", - "nix 0.29.0", -] +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "want" -version = "0.3.1" +name = "wasi" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ - "try-lock", + "wit-bindgen-rt", ] [[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +name = "wasite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3915,22 +3824,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-streams" @@ -3947,24 +3859,28 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "which" -version = "4.4.2" +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "whoami" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ - "either", - "home", - "once_cell", - "rustix", + "redox_syscall", + "wasite", ] [[package]] @@ -3983,6 +3899,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -3991,41 +3916,67 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", ] [[package]] -name = "windows-registry" -version = "0.2.0" +name = "windows-implement" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.52.6", + "proc-macro2", + "quote", + "syn 2.0.102", ] [[package]] -name = "windows-result" +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.102", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets 0.52.6", + "windows-link 0.1.3", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link 0.1.3", ] [[package]] @@ -4177,42 +4128,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.6.20" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "memchr", + "bitflags 2.9.1", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "xattr" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" -dependencies = [ - "libc", - "linux-raw-sys", - "rustix", -] +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yoke" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -4222,63 +4156,79 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -4287,11 +4237,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.102", ] diff --git a/Cargo.toml b/Cargo.toml index 2e80b39..7e99660 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,63 +1,48 @@ -[package] -name = "feos" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "feos-cli" -path = "src/bin/feos_cli/main.rs" - -[lints.rust] -unsafe_code = "deny" +[workspace] +members = [ + "feos", + "feos/services/vm-service", + "feos/services/host-service", + "feos/services/image-service", + "cli", + "feos/proto", + "feos/utils", +] +resolver = "2" -[lints.clippy] -enum_glob_use = "deny" +[workspace.package] +version = "0.5.0" +edition = "2021" -[dependencies] -futures = "0.3.31" -netlink-packet-route = "~0.19.0" -rtnetlink = "0.14.1" -nix = { version = "0.29.0", features = ["mount", "user", "reboot", "feature", "net", "aio", "signal", "process", "fs"] } -tokio = { version = "1.41.1", features = ["full", "macros", "rt-multi-thread"] } -log = "0.4.22" -simple_logger = "5.0.0" -tonic = "0.12.3" -prost = "0.13.3" -bitflags = "2.6.0" -uuid = { version = "1.11.0", features = ["v4"] } -oci-distribution = "0.11.0" -serde = "1.0.214" +[workspace.dependencies] +tokio = { version = "1.47.1", features = ["full"] } +tokio-stream = { version = "0.1.15", features = ["net"] } +tonic = "0.11.0" +prost = "0.12.3" +prost-types = "0.12.3" +anyhow = "1.0.80" +nix = { version = "0.29.0", features = ["mount", "user", "reboot", "feature", "net", "aio", "signal", "process", "fs", "hostname"] } +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.132" -api_client = {tag = "v41.0", git = "https://github.com/cloud-hypervisor/cloud-hypervisor" } -net_util = {tag = "v41.0", git = "https://github.com/cloud-hypervisor/cloud-hypervisor" } -vmm = { tag = "v41.0", git = "https://github.com/cloud-hypervisor/cloud-hypervisor", features = ["kvm"] } +uuid = { version = "1.18.1", features = ["v4"] } +tonic-build = "0.11.0" +log = "0.4.28" +env_logger = "0.11" openssl = { version = "0.10.72", features = ["vendored"] } -libcgroups = { version = "0.4.1", default-features = false, features = ["v2",] } -libcontainer = { version = "0.4.1", default-features = false, features = [ "v2",] } -dhcproto = "0.12.0" -rand = "0.8.5" -pnet = "0.35.0" -thiserror = "2" -anyhow = "1.0.93" -tar = "0.4" -flate2 = "1.0" -tokio-stream = "0.1.15" -chrono = "0.4" -futures-util = "0.3" -futures-core = "0.3" -async-stream = "0.3" -tokio-vsock = { git = "https://github.com/rust-vsock/tokio-vsock", rev = "3a41323", features = ["tonic-conn"]} -tower = "0.5" -hyper-util = "0.1" -socket2 = "0.5.7" +oci-distribution = "0.11.0" +tempfile = "3.22.0" +tower = { version = "0.4", features = ["full"] } +sha2 = "0.10" +hex = "0.4" +digest = "0.10" +clap = { version = "4.5.47", features = ["derive", "env"] } +dotenvy = { version = "0.15"} libc = "0.2" - -pelite = "0.10" -regex = "1.10" -structopt = "0.3.26" - -[build-dependencies] -tonic-build = "0.12.3" - -[dev-dependencies] -serial_test = "3.2" +rtnetlink = "0.14.1" +netlink-packet-route = "~0.19.0" +pnet = "0.35.0" +dhcproto = "0.13.0" +socket2 = "0.6.0" +futures = "0.3.31" +chrono = "0.4.42" +feos-proto = { path = "feos/proto" } \ No newline at end of file diff --git a/Makefile b/Makefile index 3632487..8ecc74e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ IPAM ?= +.PHONY: all clippy release run clean cli test + clippy: cargo clippy @@ -21,9 +23,4 @@ test: clippy include hack/hack.mk cli: - cargo build --bin feos-cli - -nginx: uki - @echo "Building nginx container with FeOS UKI..." - docker build -f hack/Dockerfile.nginx -t feos-nginx . - @echo "Built feos-nginx container successfully" + cargo build --package feos-cli --release diff --git a/README.md b/README.md index 40154f8..b4743ac 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # FeOS +[![REUSE status](https://api.reuse.software/badge/github.com/ironcore-dev/dpservice)](https://api.reuse.software/info/github.com/ironcore-dev/FeOS) +[![GitHub License](https://img.shields.io/static/v1?label=License&message=Apache-2.0&color=blue)](LICENSE) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://makeapullrequest.com) + ## Overview FeOS is a revolutionary init system for Linux, designed specifically for hypervisors and servers that run containers. Unlike traditional systems that use sysvinit or systemd, FeOS boots directly from the Linux Kernel. Written in Rust for enhanced security and memory safety, FeOS is an ideal solution for multi-tenant environments, offering robust protection against common vulnerabilities like buffer overflows. diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..20ecd30 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,34 @@ +version = 1 +SPDX-PackageName = "FeOS" +SPDX-PackageSupplier = "IronCore authors " +SPDX-PackageDownloadLocation = "https://github.com/ironcore-dev/FeOS" + + +[[annotations]] +path = [ + ".github/**", + "CODEOWNERS", + "Makefile", + "README.md", + "Cargo.lock", + "Cargo.toml", + ".gitignore", + "**/Cargo.toml", + "**/build.rs", + "**/*.sql", + "**/*.json", + "proto/v1/*.proto", +] +precedence = "aggregate" +SPDX-FileCopyrightText = "2023 SAP SE or an SAP affiliate company and IronCore contributors" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = [ + "hack/**", + "docs/**", + "**/README.md", +] +precedence = "aggregate" +SPDX-FileCopyrightText = "2023 SAP SE or an SAP affiliate company and IronCore contributors" +SPDX-License-Identifier = "Apache-2.0" \ No newline at end of file diff --git a/build.rs b/build.rs deleted file mode 100644 index 4ae9761..0000000 --- a/build.rs +++ /dev/null @@ -1,13 +0,0 @@ -fn main() -> Result<(), Box> { - tonic_build::configure() - .protoc_arg("--experimental_allow_proto3_optional") - .compile_protos( - &[ - "proto/feos.proto", - "proto/container.proto", - "proto/isolated_container.proto", - ], - &["proto"], - )?; - Ok(()) -} diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..9b8dd55 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "feos-cli" +version.workspace = true +edition.workspace = true +description = "A gRPC CLI for the FeOS control plane" + +[dependencies] +feos-proto = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } +digest = { workspace = true } + +# Workspace dependencies +tokio = { workspace = true } +tonic = { workspace = true } +anyhow = { workspace = true } +log = { workspace = true } +tokio-stream = { workspace = true } +prost = { workspace = true } +tower = { workspace = true } +clap = { workspace = true, features = ["derive", "env"] } + +# CLI specific dependencies +env_logger = { workspace = true } +crossterm = "0.27" +chrono = { workspace = true } \ No newline at end of file diff --git a/cli/src/host_commands.rs b/cli/src/host_commands.rs new file mode 100644 index 0000000..dac8835 --- /dev/null +++ b/cli/src/host_commands.rs @@ -0,0 +1,323 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Context, Result}; +use clap::{Args, Subcommand}; +use feos_proto::host_service::{ + host_service_client::HostServiceClient, GetCpuInfoRequest, GetNetworkInfoRequest, + GetVersionInfoRequest, HostnameRequest, MemoryRequest, RebootRequest, ShutdownRequest, + StreamFeosLogsRequest, StreamKernelLogsRequest, UpgradeFeosBinaryRequest, +}; +use tokio_stream::StreamExt; +use tonic::transport::Channel; + +#[derive(Args, Debug)] +pub struct HostArgs { + #[arg( + short, + long, + global = true, + env = "FEOS_ADDRESS", + default_value = "http://[::1]:1337" + )] + pub address: String, + + #[command(subcommand)] + command: HostCommand, +} + +#[derive(Subcommand, Debug)] +pub enum HostCommand { + /// Get the host machine's hostname + Hostname, + /// Display detailed memory information from /proc/meminfo + Memory, + /// Display CPU information from /proc/cpuinfo + CpuInfo, + /// Display network interface statistics + NetworkInfo, + /// Upgrade the FeOS binary from a remote URL + Upgrade { + #[arg(long, required = true, help = "URL to fetch the new FeOS binary from")] + url: String, + #[arg( + long, + required = true, + help = "Hex-encoded SHA256 checksum for verification" + )] + sha256_sum: String, + }, + /// Stream kernel logs from /dev/kmsg + Klogs, + /// Stream logs from the internal FeOS logger + Flogs, + /// Shutdown the host machine + Shutdown, + /// Reboot the host machine + Reboot, + /// Get kernel and FeOS version information + VersionInfo, +} + +pub async fn handle_host_command(args: HostArgs) -> Result<()> { + let mut client = HostServiceClient::connect(args.address) + .await + .context("Failed to connect to host service")?; + + match args.command { + HostCommand::Hostname => get_hostname(&mut client).await?, + HostCommand::Memory => get_memory(&mut client).await?, + HostCommand::CpuInfo => get_cpu_info(&mut client).await?, + HostCommand::NetworkInfo => get_network_info(&mut client).await?, + HostCommand::Upgrade { url, sha256_sum } => { + upgrade_feos(&mut client, url, sha256_sum).await? + } + HostCommand::Klogs => stream_klogs(&mut client).await?, + HostCommand::Flogs => stream_flogs(&mut client).await?, + HostCommand::Shutdown => shutdown_host(&mut client).await?, + HostCommand::Reboot => reboot_host(&mut client).await?, + HostCommand::VersionInfo => get_version_info(&mut client).await?, + } + + Ok(()) +} + +async fn get_hostname(client: &mut HostServiceClient) -> Result<()> { + let request = HostnameRequest {}; + let response = client.hostname(request).await?.into_inner(); + println!("{}", response.hostname); + Ok(()) +} + +async fn get_memory(client: &mut HostServiceClient) -> Result<()> { + let request = MemoryRequest {}; + let response = client.get_memory(request).await?.into_inner(); + + if let Some(mem_info) = response.mem_info { + println!("{:<20} {:>15} kB", "Key", "Value"); + println!("{:-<20} {:-<16}", "", ""); + println!("{:<20} {:>15} kB", "MemTotal:", mem_info.memtotal); + println!("{:<20} {:>15} kB", "MemFree:", mem_info.memfree); + println!("{:<20} {:>15} kB", "MemAvailable:", mem_info.memavailable); + println!("{:<20} {:>15} kB", "Buffers:", mem_info.buffers); + println!("{:<20} {:>15} kB", "Cached:", mem_info.cached); + println!("{:<20} {:>15} kB", "SwapCached:", mem_info.swapcached); + println!("{:<20} {:>15} kB", "Active:", mem_info.active); + println!("{:<20} {:>15} kB", "Inactive:", mem_info.inactive); + println!("{:<20} {:>15} kB", "Active(anon):", mem_info.activeanon); + println!("{:<20} {:>15} kB", "Inactive(anon):", mem_info.inactiveanon); + println!("{:<20} {:>15} kB", "Active(file):", mem_info.activefile); + println!("{:<20} {:>15} kB", "Inactive(file):", mem_info.inactivefile); + println!("{:<20} {:>15} kB", "Unevictable:", mem_info.unevictable); + println!("{:<20} {:>15} kB", "Mlocked:", mem_info.mlocked); + println!("{:<20} {:>15} kB", "SwapTotal:", mem_info.swaptotal); + println!("{:<20} {:>15} kB", "SwapFree:", mem_info.swapfree); + println!("{:<20} {:>15} kB", "Dirty:", mem_info.dirty); + println!("{:<20} {:>15} kB", "Writeback:", mem_info.writeback); + println!("{:<20} {:>15} kB", "AnonPages:", mem_info.anonpages); + println!("{:<20} {:>15} kB", "Mapped:", mem_info.mapped); + println!("{:<20} {:>15} kB", "Shmem:", mem_info.shmem); + println!("{:<20} {:>15} kB", "Slab:", mem_info.slab); + println!("{:<20} {:>15} kB", "SReclaimable:", mem_info.sreclaimable); + println!("{:<20} {:>15} kB", "SUnreclaim:", mem_info.sunreclaim); + println!("{:<20} {:>15} kB", "KernelStack:", mem_info.kernelstack); + println!("{:<20} {:>15} kB", "PageTables:", mem_info.pagetables); + println!("{:<20} {:>15} kB", "NFS_Unstable:", mem_info.nfsunstable); + println!("{:<20} {:>15} kB", "Bounce:", mem_info.bounce); + println!("{:<20} {:>15} kB", "WritebackTmp:", mem_info.writebacktmp); + println!("{:<20} {:>15} kB", "CommitLimit:", mem_info.commitlimit); + println!("{:<20} {:>15} kB", "Committed_AS:", mem_info.committedas); + println!("{:<20} {:>15} kB", "VmallocTotal:", mem_info.vmalloctotal); + println!("{:<20} {:>15} kB", "VmallocUsed:", mem_info.vmallocused); + println!("{:<20} {:>15} kB", "VmallocChunk:", mem_info.vmallocchunk); + println!( + "{:<20} {:>15} kB", + "HardwareCorrupted:", mem_info.hardwarecorrupted + ); + println!("{:<20} {:>15} kB", "AnonHugePages:", mem_info.anonhugepages); + println!( + "{:<20} {:>15} kB", + "ShmemHugePages:", mem_info.shmemhugepages + ); + println!( + "{:<20} {:>15} kB", + "ShmemPmdMapped:", mem_info.shmempmdmapped + ); + println!("{:<20} {:>15} kB", "CmaTotal:", mem_info.cmatotal); + println!("{:<20} {:>15} kB", "CmaFree:", mem_info.cmafree); + println!( + "{:<20} {:>15} kB", + "HugePages_Total:", mem_info.hugepagestotal + ); + println!( + "{:<20} {:>15} kB", + "HugePages_Free:", mem_info.hugepagesfree + ); + println!( + "{:<20} {:>15} kB", + "HugePages_Rsvd:", mem_info.hugepagesrsvd + ); + println!( + "{:<20} {:>15} kB", + "HugePages_Surp:", mem_info.hugepagessurp + ); + println!("{:<20} {:>15} kB", "Hugepagesize:", mem_info.hugepagesize); + println!("{:<20} {:>15} kB", "DirectMap4k:", mem_info.directmap4k); + println!("{:<20} {:>15} kB", "DirectMap2m:", mem_info.directmap2m); + println!("{:<20} {:>15} kB", "DirectMap1G:", mem_info.directmap1g); + } else { + println!("No memory information received from the host."); + } + Ok(()) +} + +async fn get_cpu_info(client: &mut HostServiceClient) -> Result<()> { + let request = GetCpuInfoRequest {}; + let response = client.get_cpu_info(request).await?.into_inner(); + + if response.cpu_info.is_empty() { + println!("No CPU information received from the host."); + return Ok(()); + } + + for (i, cpu) in response.cpu_info.iter().enumerate() { + println!("--- Processor {} ---", cpu.processor); + println!("{:<20}: {}", "Vendor ID", cpu.vendor_id); + println!("{:<20}: {}", "Model Name", cpu.model_name); + println!("{:<20}: {}", "CPU Family", cpu.cpu_family); + println!("{:<20}: {}", "Model", cpu.model); + println!("{:<20}: {}", "Stepping", cpu.stepping); + println!("{:<20}: {:.3} MHz", "CPU MHz", cpu.cpu_mhz); + println!("{:<20}: {}", "Cache Size", cpu.cache_size); + println!("{:<20}: {}", "Physical ID", cpu.physical_id); + println!("{:<20}: {}", "Core ID", cpu.core_id); + println!("{:<20}: {}", "CPU Cores", cpu.cpu_cores); + println!("{:<20}: {}", "Siblings", cpu.siblings); + println!("{:<20}: {}", "Address Sizes", cpu.address_sizes); + println!("{:<20}: {:.2}", "BogoMIPS", cpu.bogo_mips); + if i < response.cpu_info.len() - 1 { + println!(); + } + } + + Ok(()) +} + +async fn get_network_info(client: &mut HostServiceClient) -> Result<()> { + let request = GetNetworkInfoRequest {}; + let response = client.get_network_info(request).await?.into_inner(); + + if response.devices.is_empty() { + println!("No network devices found on the host."); + return Ok(()); + } + + for dev in response.devices { + println!("Interface: {}", dev.name); + println!(" RX"); + println!(" Bytes: {:>15}", dev.rx_bytes); + println!(" Packets: {:>15}", dev.rx_packets); + println!(" Errors: {:>15}", dev.rx_errors); + println!(" Dropped: {:>15}", dev.rx_dropped); + println!(" TX"); + println!(" Bytes: {:>15}", dev.tx_bytes); + println!(" Packets: {:>15}", dev.tx_packets); + println!(" Errors: {:>15}", dev.tx_errors); + println!(" Dropped: {:>15}", dev.tx_dropped); + println!(); + } + + Ok(()) +} + +async fn stream_klogs(client: &mut HostServiceClient) -> Result<()> { + println!("Streaming kernel logs... Press Ctrl+C to stop."); + let request = StreamKernelLogsRequest {}; + let mut stream = client.stream_kernel_logs(request).await?.into_inner(); + + while let Some(entry_res) = stream.next().await { + match entry_res { + Ok(entry) => { + println!("{}", entry.message); + } + Err(status) => { + eprintln!("Error in kernel log stream: {status}"); + break; + } + } + } + + Ok(()) +} + +async fn stream_flogs(client: &mut HostServiceClient) -> Result<()> { + println!("Streaming FeOS logs... Press Ctrl+C to stop."); + let request = StreamFeosLogsRequest {}; + let mut stream = client.stream_fe_os_logs(request).await?.into_inner(); + + while let Some(entry_res) = stream.next().await { + match entry_res { + Ok(entry) => { + let ts = entry + .timestamp + .map(|t| { + chrono::DateTime::from_timestamp(t.seconds, t.nanos as u32) + .unwrap_or_default() + .to_rfc3339() + }) + .unwrap_or_default(); + println!( + "[{ts} {:<5} {}] {}", + entry.level, entry.target, entry.message + ); + } + Err(status) => { + eprintln!("Error in FeOS log stream: {status}"); + break; + } + } + } + Ok(()) +} + +async fn upgrade_feos( + client: &mut HostServiceClient, + url: String, + sha256_sum: String, +) -> Result<()> { + println!("Requesting FeOS upgrade from URL: {url}"); + println!("Expected SHA256: {sha256_sum}"); + + let request = UpgradeFeosBinaryRequest { url, sha256_sum }; + + client.upgrade_feos_binary(request).await?; + + println!("Upgrade request accepted by host."); + + Ok(()) +} + +async fn get_version_info(client: &mut HostServiceClient) -> Result<()> { + println!("Requesting version information..."); + let request = GetVersionInfoRequest {}; + let response = client.get_version_info(request).await?.into_inner(); + println!("FeOS Version: {}", response.feos_version); + println!("Kernel Version: {}", response.kernel_version); + Ok(()) +} + +async fn shutdown_host(client: &mut HostServiceClient) -> Result<()> { + println!("Requesting host shutdown..."); + let request = ShutdownRequest {}; + client.shutdown(request).await?; + println!("Shutdown command sent successfully. Connection will be lost."); + Ok(()) +} + +async fn reboot_host(client: &mut HostServiceClient) -> Result<()> { + println!("Requesting host reboot..."); + let request = RebootRequest {}; + client.reboot(request).await?; + println!("Reboot command sent successfully. Connection will be lost."); + Ok(()) +} diff --git a/cli/src/image_commands.rs b/cli/src/image_commands.rs new file mode 100644 index 0000000..0816dc7 --- /dev/null +++ b/cli/src/image_commands.rs @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Context, Result}; +use clap::{Args, Subcommand}; +use feos_proto::image_service::{ + image_service_client::ImageServiceClient, DeleteImageRequest, ImageState, ListImagesRequest, + PullImageRequest, WatchImageStatusRequest, +}; +use std::path::PathBuf; +use tokio::net::UnixStream; +use tokio_stream::StreamExt; +use tonic::transport::{Channel, Endpoint, Uri}; +use tower::service_fn; + +#[derive(Args, Debug)] +pub struct ImageArgs { + #[arg( + short, + long, + global = true, + env = "FEOS_IMAGE_SOCKET", + default_value = "/tmp/image_service.sock" + )] + pub socket: PathBuf, + + #[command(subcommand)] + command: ImageCommand, +} + +#[derive(Subcommand, Debug)] +pub enum ImageCommand { + /// Pull a container image from a registry + Pull { + #[arg( + required = true, + help = "Container image reference to pull (e.g., docker.io/library/ubuntu:latest)" + )] + image_ref: String, + }, + /// List all local container images + List, + /// Watch the status of an image pull operation + Watch { + #[arg(required = true, help = "UUID of the image to watch")] + image_uuid: String, + }, + /// Delete a local container image + Delete { + #[arg(required = true, help = "UUID of the image to delete")] + image_uuid: String, + }, +} + +async fn get_image_client(socket: PathBuf) -> Result> { + let channel = Endpoint::try_from("http://[::1]:50051")? + .connect_with_connector(service_fn(move |_: Uri| { + UnixStream::connect(socket.clone()) + })) + .await + .context("Failed to connect to ImageService via Unix socket")?; + + Ok(ImageServiceClient::new(channel)) +} + +pub async fn handle_image_command(args: ImageArgs) -> Result<()> { + let mut client = get_image_client(args.socket).await?; + + match args.command { + ImageCommand::Pull { image_ref } => pull_image(&mut client, image_ref).await?, + ImageCommand::List => list_images(&mut client).await?, + ImageCommand::Watch { image_uuid } => watch_image(&mut client, image_uuid).await?, + ImageCommand::Delete { image_uuid } => delete_image(&mut client, image_uuid).await?, + } + + Ok(()) +} + +async fn pull_image(client: &mut ImageServiceClient, image_ref: String) -> Result<()> { + println!("Requesting image pull for: {image_ref}..."); + let request = PullImageRequest { image_ref }; + let response = client.pull_image(request).await?.into_inner(); + println!("Image pull initiated. UUID: {}", response.image_uuid); + println!( + "Use 'feos-cli image watch {}' to see progress.", + response.image_uuid + ); + Ok(()) +} + +async fn list_images(client: &mut ImageServiceClient) -> Result<()> { + let request = ListImagesRequest {}; + let response = client.list_images(request).await?.into_inner(); + if response.images.is_empty() { + println!("No local images found."); + return Ok(()); + } + + println!("{:<38} {:<12} REFERENCE", "UUID", "STATE"); + println!("{:-<38} {:-<12} {:-<40}", "", "", ""); + for image in response.images { + let state = ImageState::try_from(image.state).unwrap_or_default(); + println!( + "{:<38} {:<12} {}", + image.image_uuid, + format!("{state:?}"), + image.image_ref + ); + } + Ok(()) +} + +async fn watch_image(client: &mut ImageServiceClient, image_uuid: String) -> Result<()> { + println!("Watching status for image: {image_uuid}. Press Ctrl+C to stop."); + let request = WatchImageStatusRequest { + image_uuid: image_uuid.clone(), + }; + let mut stream = client.watch_image_status(request).await?.into_inner(); + + while let Some(status_res) = stream.next().await { + match status_res { + Ok(status) => { + let state = ImageState::try_from(status.state).unwrap_or_default(); + println!( + "Status: {:<12} | Progress: {:>3}% | Message: {}", + format!("{state:?}"), + status.progress_percent, + status.message + ); + if matches!(state, ImageState::Ready | ImageState::PullFailed) { + println!("Terminal state reached. Exiting watch."); + break; + } + } + Err(e) => { + eprintln!("\nError in watch stream: {e}"); + break; + } + } + } + Ok(()) +} + +async fn delete_image(client: &mut ImageServiceClient, image_uuid: String) -> Result<()> { + let request = DeleteImageRequest { + image_uuid: image_uuid.clone(), + }; + client.delete_image(request).await?; + println!("Successfully deleted image: {image_uuid}"); + Ok(()) +} diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..422a67d --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Result; +use clap::{Parser, Subcommand}; + +mod host_commands; +mod image_commands; +mod vm_commands; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + service: Service, +} + +#[derive(Subcommand, Debug)] +enum Service { + Vm(vm_commands::VmArgs), + Host(host_commands::HostArgs), + Image(image_commands::ImageArgs), +} + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::Builder::new() + .filter_level(log::LevelFilter::Warn) + .parse_default_env() + .init(); + + let cli = Cli::parse(); + + match cli.service { + Service::Vm(args) => vm_commands::handle_vm_command(args).await?, + Service::Host(args) => host_commands::handle_host_command(args).await?, + Service::Image(args) => image_commands::handle_image_command(args).await?, + } + + Ok(()) +} diff --git a/cli/src/vm_commands.rs b/cli/src/vm_commands.rs new file mode 100644 index 0000000..904cfc5 --- /dev/null +++ b/cli/src/vm_commands.rs @@ -0,0 +1,705 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Context, Result}; +use clap::{Args, Subcommand}; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; +use crossterm::tty::IsTty; +use feos_proto::vm_service::{ + net_config, stream_vm_console_request as console_input, vm_service_client::VmServiceClient, + AttachConsoleMessage, AttachDiskRequest, ConsoleData, CpuConfig, CreateVmRequest, + DeleteVmRequest, DiskConfig, GetVmRequest, ListVmsRequest, MemoryConfig, NetConfig, + PauseVmRequest, PingVmRequest, RemoveDiskRequest, ResumeVmRequest, ShutdownVmRequest, + StartVmRequest, StreamVmConsoleRequest, StreamVmEventsRequest, VfioPciConfig, VmConfig, + VmState, VmStateChangedEvent, +}; +use prost::Message; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::sync::mpsc; +use tokio_stream::StreamExt; +use tonic::transport::Channel; + +#[derive(Args, Debug)] +pub struct VmArgs { + #[arg( + short, + long, + global = true, + env = "FEOS_ADDRESS", + default_value = "http://[::1]:1337" + )] + pub address: String, + + #[command(subcommand)] + command: VmCommand, +} + +#[derive(Subcommand, Debug)] +pub enum VmCommand { + /// Create a new virtual machine with specified configuration + Create { + #[arg( + long, + required = true, + help = "Container image reference to use for the VM" + )] + image_ref: String, + + #[arg(long, default_value_t = 1, help = "Number of virtual CPUs to allocate")] + vcpus: u32, + + #[arg(long, default_value_t = 1024, help = "Memory size in MiB")] + memory: u64, + + #[arg(long, help = "Optional custom VM identifier")] + vm_id: Option, + + #[arg( + long, + help = "PCI device BDF to passthrough for networking (e.g., 0000:03:00.0)" + )] + pci_device: Vec, + + #[arg(long, help = "Enable hugepages for memory allocation")] + hugepages: bool, + }, + /// Start an existing virtual machine + Start { + #[arg(required = true, help = "VM identifier")] + vm_id: String, + }, + /// Get detailed information about a virtual machine + Info { + #[arg(required = true, help = "VM identifier")] + vm_id: String, + }, + /// List all virtual machines + List, + /// Ping a virtual machine's VMM to check status + Ping { + #[arg(required = true, help = "VM identifier")] + vm_id: String, + }, + /// Gracefully shutdown a virtual machine + Shutdown { + #[arg(required = true, help = "VM identifier")] + vm_id: String, + }, + /// Pause a running virtual machine + Pause { + #[arg(required = true, help = "VM identifier")] + vm_id: String, + }, + /// Resume a paused virtual machine + Resume { + #[arg(required = true, help = "VM identifier")] + vm_id: String, + }, + /// Delete a virtual machine + Delete { + #[arg(required = true, help = "VM identifier")] + vm_id: String, + }, + /// Create and start a virtual machine in one operation + CreateAndStart { + #[arg( + long, + required = true, + help = "Container image reference to use for the VM" + )] + image_ref: String, + + #[arg(long, default_value_t = 1, help = "Number of virtual CPUs to allocate")] + vcpus: u32, + + #[arg(long, default_value_t = 1024, help = "Memory size in MiB")] + memory: u64, + + #[arg(long, help = "Optional custom VM identifier")] + vm_id: Option, + + #[arg( + long, + help = "PCI device BDF to passthrough for networking (e.g., 0000:03:00.0)" + )] + pci_device: Vec, + + #[arg(long, help = "Enable hugepages for memory allocation")] + hugepages: bool, + }, + /// Watch virtual machine state change events + Events { + #[arg( + long, + help = "VM identifier (optional, if not provided watches all VMs)" + )] + vm_id: Option, + }, + /// Connect to a virtual machine's console + Console { + #[arg(required = true, help = "VM identifier")] + vm_id: String, + }, + /// Attach a disk to a running virtual machine + AttachDisk { + #[arg(long, required = true, help = "VM identifier")] + vm_id: String, + #[arg(long, required = true, help = "Path to the disk image file")] + path: String, + }, + /// Remove a disk from a virtual machine + RemoveDisk { + #[arg(long, required = true, help = "VM identifier")] + vm_id: String, + #[arg( + long, + required = true, + help = "Device identifier of the disk to remove" + )] + device_id: String, + }, +} + +pub async fn handle_vm_command(args: VmArgs) -> Result<()> { + let mut client = VmServiceClient::connect(args.address) + .await + .context("Failed to connect to VM service")?; + + match args.command { + VmCommand::Create { + image_ref, + vcpus, + memory, + vm_id, + pci_device, + hugepages, + } => { + create_vm( + &mut client, + image_ref, + vcpus, + memory, + vm_id, + pci_device, + hugepages, + ) + .await? + } + VmCommand::Start { vm_id } => start_vm(&mut client, vm_id).await?, + VmCommand::Info { vm_id } => get_vm_info(&mut client, vm_id).await?, + VmCommand::List => list_vms(&mut client).await?, + VmCommand::Ping { vm_id } => ping_vm(&mut client, vm_id).await?, + VmCommand::Shutdown { vm_id } => shutdown_vm(&mut client, vm_id).await?, + VmCommand::Pause { vm_id } => pause_vm(&mut client, vm_id).await?, + VmCommand::Resume { vm_id } => resume_vm(&mut client, vm_id).await?, + VmCommand::Delete { vm_id } => delete_vm(&mut client, vm_id).await?, + VmCommand::CreateAndStart { + image_ref, + vcpus, + memory, + vm_id, + pci_device, + hugepages, + } => { + create_and_start_vm( + &mut client, + image_ref, + vcpus, + memory, + vm_id, + pci_device, + hugepages, + ) + .await? + } + VmCommand::Events { vm_id } => watch_events(&mut client, vm_id).await?, + VmCommand::Console { vm_id } => console_vm(&mut client, vm_id).await?, + VmCommand::AttachDisk { vm_id, path } => attach_disk(&mut client, vm_id, path).await?, + VmCommand::RemoveDisk { vm_id, device_id } => { + remove_disk(&mut client, vm_id, device_id).await? + } + } + + Ok(()) +} + +async fn create_and_start_vm( + client: &mut VmServiceClient, + image_ref: String, + vcpus: u32, + memory: u64, + vm_id: Option, + pci_devices: Vec, + hugepages: bool, +) -> Result<()> { + println!("🚀 Starting create and start operation for VM with image: {image_ref}"); + + // Step 1: Create the VM + println!("📋 Step 1: Creating VM..."); + + let net_configs = pci_devices + .iter() + .map(|bdf| { + println!(" Adding PCI device: {bdf}"); + NetConfig { + backend: Some(net_config::Backend::VfioPci(VfioPciConfig { + bdf: bdf.clone(), + })), + ..Default::default() + } + }) + .collect(); + + let request = CreateVmRequest { + config: Some(VmConfig { + cpus: Some(CpuConfig { + boot_vcpus: vcpus, + max_vcpus: vcpus, + }), + memory: Some(MemoryConfig { + size_mib: memory, + hugepages, + }), + image_ref: image_ref.clone(), + net: net_configs, + ..Default::default() + }), + vm_id: vm_id.clone(), + }; + + let response = client.create_vm(request).await?.into_inner(); + let vm_id = response.vm_id; + println!("✅ VM created successfully with ID: {vm_id}"); + + // Step 2: Wait for VM to be in 'Created' state + println!("⏳ Step 2: Waiting for VM to reach 'Created' state..."); + wait_for_vm_state(client, &vm_id, VmState::Created).await?; + println!("✅ VM is now in 'Created' state"); + + // Step 3: Start the VM + println!("🔄 Step 3: Starting VM..."); + let start_request = StartVmRequest { + vm_id: vm_id.clone(), + }; + client.start_vm(start_request).await?; + println!("✅ Start request sent successfully"); + + // Step 4: Wait for VM to be in 'Running' state + println!("⏳ Step 4: Waiting for VM to reach 'Running' state..."); + wait_for_vm_state(client, &vm_id, VmState::Running).await?; + println!("🎉 VM '{vm_id}' is now running successfully!"); + + println!("Use 'feos-cli vm console {vm_id}' to connect to the VM console."); + + Ok(()) +} + +async fn wait_for_vm_state( + client: &mut VmServiceClient, + vm_id: &str, + target_state: VmState, +) -> Result<()> { + let request = StreamVmEventsRequest { + vm_id: Some(vm_id.to_string()), + ..Default::default() + }; + + let mut stream = client.stream_vm_events(request).await?.into_inner(); + + // First, check current state + let get_request = GetVmRequest { + vm_id: vm_id.to_string(), + }; + let current_vm = client.get_vm(get_request).await?.into_inner(); + let current_state = VmState::try_from(current_vm.state).unwrap_or(VmState::Unspecified); + + if current_state == target_state { + return Ok(()); + } + + println!(" Current state: {current_state:?}, waiting for: {target_state:?}"); + + // Listen for state changes + while let Some(event) = stream.next().await { + match event { + Ok(event) => { + if let Some(data) = event.data { + if data + .type_url + .contains("feos.vm.vmm.api.v1.VmStateChangedEvent") + { + let state_change = VmStateChangedEvent::decode(&*data.value)?; + let new_state = VmState::try_from(state_change.new_state) + .unwrap_or(VmState::Unspecified); + + println!( + " State transition: {:?} ({})", + new_state, state_change.reason + ); + + if new_state == target_state { + return Ok(()); + } + + // Check for error states + if new_state == VmState::Crashed { + anyhow::bail!("VM entered crashed state: {}", state_change.reason); + } + } + } + } + Err(status) => { + anyhow::bail!("Error in event stream: {}", status); + } + } + } + + anyhow::bail!( + "Event stream ended before reaching target state: {:?}", + target_state + ) +} + +async fn create_vm( + client: &mut VmServiceClient, + image_ref: String, + vcpus: u32, + memory: u64, + vm_id: Option, + pci_devices: Vec, + hugepages: bool, +) -> Result<()> { + println!("Requesting VM creation with image: {image_ref}..."); + + let net_configs = pci_devices + .into_iter() + .map(|bdf| { + println!("Adding PCI device: {bdf}"); + NetConfig { + backend: Some(net_config::Backend::VfioPci(VfioPciConfig { bdf })), + ..Default::default() + } + }) + .collect(); + + let request = CreateVmRequest { + config: Some(VmConfig { + cpus: Some(CpuConfig { + boot_vcpus: vcpus, + max_vcpus: vcpus, + }), + memory: Some(MemoryConfig { + size_mib: memory, + hugepages, + }), + image_ref, + net: net_configs, + ..Default::default() + }), + vm_id, + }; + + let response = client.create_vm(request).await?.into_inner(); + println!("VM creation initiated. VM ID: {}", response.vm_id); + println!( + "Use 'feos-cli vm events {}' to watch its progress.", + response.vm_id + ); + println!("Then 'feos-cli vm start {}' to start it.", response.vm_id); + + Ok(()) +} + +async fn start_vm(client: &mut VmServiceClient, vm_id: String) -> Result<()> { + let request = StartVmRequest { + vm_id: vm_id.clone(), + }; + client.start_vm(request).await?; + println!("Start request sent for VM: {vm_id}"); + Ok(()) +} + +async fn get_vm_info(client: &mut VmServiceClient, vm_id: String) -> Result<()> { + let request = GetVmRequest { + vm_id: vm_id.clone(), + }; + let response = client.get_vm(request).await?.into_inner(); + + println!("VM Info for: {vm_id}"); + println!( + " State: {:?}", + VmState::try_from(response.state).unwrap_or(VmState::Unspecified) + ); + if let Some(config) = response.config { + println!(" Config:"); + println!(" Image Ref: {}", config.image_ref); + if let Some(cpus) = config.cpus { + println!(" vCPUs: {}", cpus.boot_vcpus); + } + if let Some(mem) = config.memory { + println!(" Memory: {} MiB", mem.size_mib); + } + if !config.net.is_empty() { + println!(" Network Devices:"); + for (i, net_conf) in config.net.iter().enumerate() { + if let Some(backend) = &net_conf.backend { + match backend { + net_config::Backend::VfioPci(pci) => { + println!(" Device {}: PCI Passthrough - {}", i, pci.bdf); + } + net_config::Backend::Tap(tap) => { + println!(" Device {}: TAP - {}", i, tap.tap_name); + } + } + } + } + } + } + Ok(()) +} + +async fn list_vms(client: &mut VmServiceClient) -> Result<()> { + let request = ListVmsRequest {}; + let response = client.list_vms(request).await?.into_inner(); + + if response.vms.is_empty() { + println!("No VMs found."); + return Ok(()); + } + + println!("{:<38} {:<12} IMAGE_REF", "VM_ID", "STATE"); + println!("{:-<38} {:-<12} {:-<40}", "", "", ""); + for vm in response.vms { + let state = VmState::try_from(vm.state).unwrap_or(VmState::Unspecified); + let image_ref = vm.config.map(|c| c.image_ref).unwrap_or_default(); + println!( + "{:<38} {:<12} {}", + vm.vm_id, + format!("{state:?}"), + image_ref + ); + } + Ok(()) +} + +async fn ping_vm(client: &mut VmServiceClient, vm_id: String) -> Result<()> { + let request = PingVmRequest { + vm_id: vm_id.clone(), + }; + let response = client.ping_vm(request).await?.into_inner(); + + println!("VMM Ping response for: {vm_id}"); + println!(" PID: {}", response.pid); + println!(" Version: {}", response.version); + println!(" Build Version: {}", response.build_version); + println!(" Features: {:?}", response.features); + Ok(()) +} + +async fn shutdown_vm(client: &mut VmServiceClient, vm_id: String) -> Result<()> { + let request = ShutdownVmRequest { + vm_id: vm_id.clone(), + }; + client.shutdown_vm(request).await?; + println!("Shutdown request sent for VM: {vm_id}"); + Ok(()) +} + +async fn pause_vm(client: &mut VmServiceClient, vm_id: String) -> Result<()> { + let request = PauseVmRequest { + vm_id: vm_id.clone(), + }; + client.pause_vm(request).await?; + println!("Pause request sent for VM: {vm_id}"); + Ok(()) +} + +async fn resume_vm(client: &mut VmServiceClient, vm_id: String) -> Result<()> { + let request = ResumeVmRequest { + vm_id: vm_id.clone(), + }; + client.resume_vm(request).await?; + println!("Resume request sent for VM: {vm_id}"); + Ok(()) +} + +async fn delete_vm(client: &mut VmServiceClient, vm_id: String) -> Result<()> { + let request = DeleteVmRequest { + vm_id: vm_id.clone(), + }; + client.delete_vm(request).await?.into_inner(); + println!("Successfully deleted VM: {vm_id}"); + Ok(()) +} + +async fn watch_events(client: &mut VmServiceClient, vm_id: Option) -> Result<()> { + if let Some(id) = &vm_id { + println!("Watching events for VM: {id}. Press Ctrl+C to stop."); + } else { + println!("Watching events for all VMs. Press Ctrl+C to stop."); + } + + let request = StreamVmEventsRequest { + vm_id, + ..Default::default() + }; + let mut stream = client.stream_vm_events(request).await?.into_inner(); + + while let Some(event) = stream.next().await { + match event { + Ok(event) => { + println!("[{}] Event ID: {}", event.vm_id, event.id); + if let Some(data) = event.data { + if data + .type_url + .contains("feos.vm.vmm.api.v1.VmStateChangedEvent") + { + let state_change = VmStateChangedEvent::decode(&*data.value)?; + println!( + " New State: {:?} (Reason: {})", + VmState::try_from(state_change.new_state) + .unwrap_or(VmState::Unspecified), + state_change.reason + ); + } else { + println!(" Data Type: {}", data.type_url); + } + } + } + Err(status) => { + eprintln!("Error in event stream: {status}"); + break; + } + } + } + + Ok(()) +} + +async fn console_vm(client: &mut VmServiceClient, vm_id: String) -> Result<()> { + if !std::io::stdin().is_tty() { + anyhow::bail!("Cannot enter interactive console mode without a TTY."); + } + + println!("Connecting to console for VM: {vm_id}. Press Ctrl+] to exit."); + + struct RawModeGuard; + impl Drop for RawModeGuard { + fn drop(&mut self) { + if let Err(e) = disable_raw_mode() { + eprintln!("\r\nFailed to disable raw mode: {e}. Please reset your terminal.\r\n"); + } + } + } + + enable_raw_mode().context("Failed to enable terminal raw mode")?; + let _guard = RawModeGuard; + + let (input_tx, input_rx) = mpsc::channel(10); + let input_stream = tokio_stream::wrappers::ReceiverStream::new(input_rx); + + let response = client.stream_vm_console(input_stream).await?; + let mut output_stream = response.into_inner(); + + let attach_payload = console_input::Payload::Attach(AttachConsoleMessage { + vm_id: vm_id.clone(), + }); + let attach_input = StreamVmConsoleRequest { + payload: Some(attach_payload), + }; + input_tx + .send(attach_input) + .await + .context("Failed to send attach message")?; + + let output_task = tokio::spawn(async move { + let mut stdout = tokio::io::stdout(); + while let Some(result) = output_stream.next().await { + match result { + Ok(msg) => { + if let Err(e) = stdout.write_all(&msg.output).await { + eprintln!("\r\nError writing to stdout: {e}\r\n"); + break; + } + if let Err(e) = stdout.flush().await { + eprintln!("\r\nError flushing stdout: {e}\r\n"); + break; + } + } + Err(e) => { + eprintln!("\r\nError from server stream: {e}\r\n"); + break; + } + } + } + }); + + let input_task = tokio::spawn(async move { + let mut stdin = tokio::io::stdin(); + let mut buffer = vec![0; 1]; + loop { + match stdin.read(&mut buffer).await { + Ok(0) => break, + Ok(n) => { + if buffer[0] == 29 { + break; + } + let data_payload = console_input::Payload::Data(ConsoleData { + input: buffer[..n].to_vec(), + }); + let data_input = StreamVmConsoleRequest { + payload: Some(data_payload), + }; + if input_tx.send(data_input).await.is_err() { + break; + } + } + Err(e) => { + eprintln!("\r\nError reading from stdin: {e}\r\n"); + break; + } + } + } + }); + + tokio::select! { + _ = output_task => {}, + _ = input_task => {}, + } + + Ok(()) +} + +async fn attach_disk( + client: &mut VmServiceClient, + vm_id: String, + path: String, +) -> Result<()> { + let request = AttachDiskRequest { + vm_id: vm_id.clone(), + disk: Some(DiskConfig { + backend: Some(feos_proto::vm_service::disk_config::Backend::Path(path)), + ..Default::default() + }), + }; + let response = client.attach_disk(request).await?.into_inner(); + println!( + "Disk attach request sent for VM: {}. Assigned device_id: {}", + vm_id, response.device_id + ); + Ok(()) +} + +async fn remove_disk( + client: &mut VmServiceClient, + vm_id: String, + device_id: String, +) -> Result<()> { + let request = RemoveDiskRequest { + vm_id: vm_id.clone(), + device_id: device_id.clone(), + }; + client.remove_disk(request).await?; + println!("Disk remove request sent for device {device_id} on VM {vm_id}"); + Ok(()) +} diff --git a/docs/changelog/FeOS-0.5.0.md b/docs/changelog/FeOS-0.5.0.md new file mode 100644 index 0000000..51d6e18 --- /dev/null +++ b/docs/changelog/FeOS-0.5.0.md @@ -0,0 +1,23 @@ +# FeOS 0.5.0 Release Notes + +## 🚀 Highlights + +- **Internal Architecture Improved** + - Fully async, lock-free, event-driven approach + +- **Testability** + - Unit tests + - Nested virtualization-based tests + +- **Live Upgrade** + - Support for FeOS PID1 live upgrade + +- **Backend Flexibility** + - Pluggable Rust-based backends: + - VM-Service: Cloud Hypervisor / Other rust based vmms possible + +- **Cloud Hypervisor** + - Hard dependency eliminated + +- **Host Service** + - Enhanced features and improvements \ No newline at end of file diff --git a/feos/Cargo.toml b/feos/Cargo.toml new file mode 100644 index 0000000..4ee3a61 --- /dev/null +++ b/feos/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "feos" +version.workspace = true +edition.workspace = true + +[[bin]] +name = "feos" +path = "src/main.rs" + +[lib] +name = "main_server" +path = "src/lib.rs" + +[dependencies] +feos-utils = { path = "utils" } +vm-service = { path = "services/vm-service" } +host-service = { path = "services/host-service" } +image-service = { path = "services/image-service" } +feos-proto = { workspace = true } + +# Workspace dependencies +tokio = { workspace = true } +tokio-stream = { workspace = true } +tonic = { workspace = true } +anyhow = { workspace = true } +log = { workspace = true } +env_logger = { workspace = true } +nix = { workspace = true } +clap = { workspace = true, features = ["derive"] } +dotenvy = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +libc = { workspace = true } +rtnetlink = { workspace = true } +netlink-packet-route = { workspace = true } +pnet = { workspace = true } +dhcproto = { workspace = true } +socket2 = { workspace = true } +futures = { workspace = true } +chrono = { workspace = true } +termcolor = "1.1" + +[dev-dependencies] +feos-utils = { path = "utils" } +vm-service = { path = "services/vm-service" } +image-service = { path = "services/image-service"} +host-service = { path = "services/host-service"} +anyhow = { workspace = true } +nix = { workspace = true } +tokio = { workspace = true } +tonic = { workspace = true } +log = { workspace = true } +env_logger = { workspace = true } +tokio-stream = { workspace = true } +prost = { workspace = true } +once_cell = "1.19" +regex = "1.10" +tower = { workspace = true } +tempfile = "3.10.1" \ No newline at end of file diff --git a/feos/proto/Cargo.toml b/feos/proto/Cargo.toml new file mode 100644 index 0000000..443c019 --- /dev/null +++ b/feos/proto/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "feos-proto" +version.workspace = true +edition.workspace = true + +[lib] +path = "src/lib.rs" + +[dependencies] +tonic = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } + +[build-dependencies] +tonic-build = { workspace = true } \ No newline at end of file diff --git a/feos/proto/build.rs b/feos/proto/build.rs new file mode 100644 index 0000000..fd4bc3b --- /dev/null +++ b/feos/proto/build.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +fn main() -> Result<(), Box> { + let proto_dir = "../../proto/v1"; + + tonic_build::configure() + .protoc_arg("--experimental_allow_proto3_optional") + .compile( + &[ + format!("{proto_dir}/vm.proto"), + format!("{proto_dir}/host.proto"), + format!("{proto_dir}/image.proto"), + ], + &[proto_dir], + )?; + Ok(()) +} diff --git a/feos/proto/src/lib.rs b/feos/proto/src/lib.rs new file mode 100644 index 0000000..f0ac042 --- /dev/null +++ b/feos/proto/src/lib.rs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +pub mod vm_service { + tonic::include_proto!("feos.vm.vmm.api.v1"); +} +pub mod host_service { + tonic::include_proto!("feos.host.v1"); +} +pub mod image_service { + tonic::include_proto!("feos.image.vmm.api.v1"); +} diff --git a/feos/services/host-service/Cargo.toml b/feos/services/host-service/Cargo.toml new file mode 100644 index 0000000..a4eaf4b --- /dev/null +++ b/feos/services/host-service/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "host-service" +version.workspace = true +edition.workspace = true + +[dependencies] +feos-utils = { path = "../../utils" } +feos-proto = { workspace = true } +tempfile = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } +digest = { workspace = true } +hyper = "1.4.0" +hyper-util = { version = "0.1.3", features = ["full"] } +hyper-rustls = "0.27.2" +http-body-util = "0.1.2" +rustls-pki-types = "1.0" + +# Workspace dependencies +tokio = { workspace = true } +tokio-stream = { workspace = true } +tonic = { workspace = true } +anyhow = { workspace = true } +nix = { workspace = true , features = ["hostname", "reboot"] } +log = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } \ No newline at end of file diff --git a/feos/services/host-service/src/api.rs b/feos/services/host-service/src/api.rs new file mode 100644 index 0000000..7b473cc --- /dev/null +++ b/feos/services/host-service/src/api.rs @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::Command; +use feos_proto::host_service::{ + host_service_server::HostService, FeosLogEntry, GetCpuInfoRequest, GetCpuInfoResponse, + GetNetworkInfoRequest, GetNetworkInfoResponse, GetVersionInfoRequest, GetVersionInfoResponse, + HostnameRequest, HostnameResponse, KernelLogEntry, MemoryRequest, MemoryResponse, + RebootRequest, RebootResponse, ShutdownRequest, ShutdownResponse, StreamFeosLogsRequest, + StreamKernelLogsRequest, UpgradeFeosBinaryRequest, UpgradeFeosBinaryResponse, +}; +use log::info; +use std::pin::Pin; +use tokio::sync::{mpsc, oneshot}; +use tokio_stream::{wrappers::ReceiverStream, Stream}; +use tonic::{Request, Response, Status}; + +pub struct HostApiHandler { + dispatcher_tx: mpsc::Sender, +} + +impl HostApiHandler { + pub fn new(dispatcher_tx: mpsc::Sender) -> Self { + Self { dispatcher_tx } + } +} + +#[tonic::async_trait] +impl HostService for HostApiHandler { + type StreamKernelLogsStream = + Pin> + Send>>; + type StreamFeOSLogsStream = Pin> + Send>>; + + async fn hostname( + &self, + _request: Request, + ) -> Result, Status> { + info!("HostApi: Received Hostname request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::GetHostname(resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn get_memory( + &self, + _request: Request, + ) -> Result, Status> { + info!("HostApi: Received GetMemory request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::GetMemory(resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn get_cpu_info( + &self, + _request: Request, + ) -> Result, Status> { + info!("HostApi: Received GetCPUInfo request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::GetCPUInfo(resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn get_network_info( + &self, + _request: Request, + ) -> Result, Status> { + info!("HostApi: Received GetNetworkInfo request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::GetNetworkInfo(resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn shutdown( + &self, + request: Request, + ) -> Result, Status> { + info!("HostApi: Received Shutdown request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::Shutdown(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn reboot( + &self, + request: Request, + ) -> Result, Status> { + info!("HostApi: Received Reboot request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::Reboot(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn upgrade_feos_binary( + &self, + request: Request, + ) -> Result, Status> { + info!("HostApi: Received UpgradeFeosBinary request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::UpgradeFeosBinary(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn stream_kernel_logs( + &self, + _request: Request, + ) -> Result, Status> { + info!("HostApi: Received StreamKernelLogs request."); + let (stream_tx, stream_rx) = mpsc::channel(128); + let cmd = Command::StreamKernelLogs(stream_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + let output_stream = ReceiverStream::new(stream_rx); + Ok(Response::new(Box::pin(output_stream))) + } + + async fn stream_fe_os_logs( + &self, + _request: Request, + ) -> Result, Status> { + info!("HostApi: Received StreamFeOSLogs request."); + let (stream_tx, stream_rx) = mpsc::channel(128); + let cmd = Command::StreamFeOSLogs(stream_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + let output_stream = ReceiverStream::new(stream_rx); + Ok(Response::new(Box::pin(output_stream))) + } + + async fn get_version_info( + &self, + _request: Request, + ) -> Result, Status> { + info!("HostApi: Received GetVersionInfo request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::GetVersionInfo(resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } +} diff --git a/feos/services/host-service/src/dispatcher.rs b/feos/services/host-service/src/dispatcher.rs new file mode 100644 index 0000000..a7093c3 --- /dev/null +++ b/feos/services/host-service/src/dispatcher.rs @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{worker, Command, RestartSignal}; +use feos_utils::feos_logger::LogHandle; +use log::info; +use tokio::sync::mpsc; + +pub struct HostServiceDispatcher { + rx: mpsc::Receiver, + restart_tx: mpsc::Sender, + log_handle: LogHandle, +} + +impl HostServiceDispatcher { + pub fn new( + rx: mpsc::Receiver, + restart_tx: mpsc::Sender, + log_handle: LogHandle, + ) -> Self { + Self { + rx, + restart_tx, + log_handle, + } + } + + pub async fn run(mut self) { + info!("HostDispatcher: Running and waiting for commands."); + while let Some(cmd) = self.rx.recv().await { + match cmd { + Command::GetHostname(responder) => { + tokio::spawn(worker::handle_hostname(responder)); + } + Command::GetMemory(responder) => { + tokio::spawn(worker::handle_get_memory(responder)); + } + Command::GetCPUInfo(responder) => { + tokio::spawn(worker::handle_get_cpu_info(responder)); + } + Command::GetNetworkInfo(responder) => { + tokio::spawn(worker::handle_get_network_info(responder)); + } + Command::GetVersionInfo(responder) => { + tokio::spawn(worker::handle_get_version_info(responder)); + } + Command::UpgradeFeosBinary(req, responder) => { + let restart_tx = self.restart_tx.clone(); + tokio::spawn(worker::handle_upgrade(restart_tx, req, responder)); + } + Command::StreamKernelLogs(stream_tx) => { + tokio::spawn(worker::handle_stream_kernel_logs(stream_tx)); + } + Command::StreamFeOSLogs(stream_tx) => { + let log_handle = self.log_handle.clone(); + tokio::spawn(worker::handle_stream_feos_logs(log_handle, stream_tx)); + } + Command::Shutdown(req, responder) => { + tokio::spawn(worker::handle_shutdown(req, responder)); + } + Command::Reboot(req, responder) => { + tokio::spawn(worker::handle_reboot(req, responder)); + } + } + } + info!("HostDispatcher: Channel closed, shutting down."); + } +} diff --git a/feos/services/host-service/src/lib.rs b/feos/services/host-service/src/lib.rs new file mode 100644 index 0000000..eb947ba --- /dev/null +++ b/feos/services/host-service/src/lib.rs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use feos_proto::host_service::{ + FeosLogEntry, GetCpuInfoResponse, GetNetworkInfoResponse, GetVersionInfoResponse, + HostnameResponse, KernelLogEntry, MemoryResponse, RebootRequest, RebootResponse, + ShutdownRequest, ShutdownResponse, UpgradeFeosBinaryRequest, UpgradeFeosBinaryResponse, +}; +use std::path::PathBuf; +use tokio::sync::{mpsc, oneshot}; +use tonic::Status; + +pub mod api; +pub mod dispatcher; +pub mod worker; + +#[derive(Debug)] +pub enum Command { + GetHostname(oneshot::Sender>), + GetMemory(oneshot::Sender>), + GetCPUInfo(oneshot::Sender>), + GetNetworkInfo(oneshot::Sender>), + GetVersionInfo(oneshot::Sender>), + UpgradeFeosBinary( + UpgradeFeosBinaryRequest, + oneshot::Sender>, + ), + StreamKernelLogs(mpsc::Sender>), + StreamFeOSLogs(mpsc::Sender>), + Shutdown( + ShutdownRequest, + oneshot::Sender>, + ), + Reboot( + RebootRequest, + oneshot::Sender>, + ), +} + +#[derive(Debug)] +pub struct RestartSignal(pub PathBuf); diff --git a/feos/services/host-service/src/worker/info.rs b/feos/services/host-service/src/worker/info.rs new file mode 100644 index 0000000..42077b4 --- /dev/null +++ b/feos/services/host-service/src/worker/info.rs @@ -0,0 +1,326 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use feos_proto::host_service::{ + CpuInfo, GetCpuInfoResponse, GetNetworkInfoResponse, GetVersionInfoResponse, HostnameResponse, + MemInfo, MemoryResponse, NetDev, +}; +use log::{error, info, warn}; +use nix::unistd; +use std::collections::HashMap; +use std::path::Path; +use tokio::fs::{self, File}; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::sync::oneshot; +use tonic::Status; + +pub async fn handle_hostname(responder: oneshot::Sender>) { + info!("HostWorker: Processing Hostname request."); + let result = match unistd::gethostname() { + Ok(host) => { + let hostname = host + .into_string() + .unwrap_or_else(|_| "Invalid UTF-8".into()); + Ok(HostnameResponse { hostname }) + } + Err(e) => { + let msg = format!("Failed to get system hostname: {e}"); + error!("HostWorker: {msg}"); + Err(Status::internal(msg)) + } + }; + + if responder.send(result).is_err() { + error!("HostWorker: Failed to send response for Hostname. API handler may have timed out."); + } +} + +async fn read_and_parse_meminfo() -> Result { + let file = File::open("/proc/meminfo").await?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + + let mut values = HashMap::new(); + + while let Some(line) = lines.next_line().await? { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + let key = parts[0].trim_end_matches(':'); + if let Ok(value) = parts[1].parse::() { + values.insert(key.to_lowercase(), value); + } + } + } + + let get = |key: &str| -> u64 { + *values.get(key).unwrap_or_else(|| { + warn!("Memory key {key} not found in /proc/meminfo"); + &0 + }) + }; + + Ok(MemInfo { + memtotal: get("memtotal"), + memfree: get("memfree"), + memavailable: get("memavailable"), + buffers: get("buffers"), + cached: get("cached"), + swapcached: get("swapcached"), + active: get("active"), + inactive: get("inactive"), + activeanon: get("active(anon)"), + inactiveanon: get("inactive(anon)"), + activefile: get("active(file)"), + inactivefile: get("inactive(file)"), + unevictable: get("unevictable"), + mlocked: get("mlocked"), + swaptotal: get("swaptotal"), + swapfree: get("swapfree"), + dirty: get("dirty"), + writeback: get("writeback"), + anonpages: get("anonpages"), + mapped: get("mapped"), + shmem: get("shmem"), + slab: get("slab"), + sreclaimable: get("sreclaimable"), + sunreclaim: get("sunreclaim"), + kernelstack: get("kernelstack"), + pagetables: get("pagetables"), + nfsunstable: get("nfs_unstable"), + bounce: get("bounce"), + writebacktmp: get("writebacktmp"), + commitlimit: get("commitlimit"), + committedas: get("committed_as"), + vmalloctotal: get("vmalloctotal"), + vmallocused: get("vmallocused"), + vmallocchunk: get("vmallocchunk"), + hardwarecorrupted: get("hardwarecorrupted"), + anonhugepages: get("anonhugepages"), + shmemhugepages: get("shmemhugepages"), + shmempmdmapped: get("shmempmdmapped"), + cmatotal: get("cmatotal"), + cmafree: get("cmafree"), + hugepagestotal: get("hugepages_total"), + hugepagesfree: get("hugepages_free"), + hugepagesrsvd: get("hugepages_rsvd"), + hugepagessurp: get("hugepages_surp"), + hugepagesize: get("hugepagesize"), + directmap4k: get("directmap4k"), + directmap2m: get("directmap2m"), + directmap1g: get("directmap1g"), + }) +} + +pub async fn handle_get_memory(responder: oneshot::Sender>) { + info!("HostWorker: Processing GetMemory request."); + let result = match read_and_parse_meminfo().await { + Ok(mem_info) => Ok(MemoryResponse { + mem_info: Some(mem_info), + }), + Err(e) => { + error!("HostWorker: Failed to get memory info: {e}"); + Err(Status::internal(format!("Failed to get memory info: {e}"))) + } + }; + + if responder.send(result).is_err() { + error!( + "HostWorker: Failed to send response for GetMemory. API handler may have timed out." + ); + } +} + +fn parse_map_to_cpu_info(map: &HashMap) -> CpuInfo { + let get_string = |key: &str| -> String { map.get(key).cloned().unwrap_or_default() }; + let get_u32 = |key: &str| -> u32 { map.get(key).and_then(|v| v.parse().ok()).unwrap_or(0) }; + let get_f64 = |key: &str| -> f64 { map.get(key).and_then(|v| v.parse().ok()).unwrap_or(0.0) }; + let get_vec_string = |key: &str| -> Vec { + map.get(key) + .map(|v| v.split_whitespace().map(String::from).collect()) + .unwrap_or_default() + }; + + CpuInfo { + processor: get_u32("processor"), + vendor_id: get_string("vendor_id"), + cpu_family: get_string("cpu family"), + model: get_string("model"), + model_name: get_string("model name"), + stepping: get_string("stepping"), + microcode: get_string("microcode"), + cpu_mhz: get_f64("cpu mhz"), + cache_size: get_string("cache size"), + physical_id: get_string("physical id"), + siblings: get_u32("siblings"), + core_id: get_string("core id"), + cpu_cores: get_u32("cpu cores"), + apic_id: get_string("apicid"), + initial_apic_id: get_string("initial apicid"), + fpu: get_string("fpu"), + fpu_exception: get_string("fpu_exception"), + cpu_id_level: get_u32("cpuid level"), + wp: get_string("wp"), + flags: get_vec_string("flags"), + bugs: get_vec_string("bugs"), + bogo_mips: get_f64("bogomips"), + cl_flush_size: get_u32("clflush size"), + cache_alignment: get_u32("cache_alignment"), + address_sizes: get_string("address sizes"), + power_management: get_string("power management"), + } +} + +async fn read_and_parse_cpuinfo() -> Result, std::io::Error> { + let file = File::open("/proc/cpuinfo").await?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + + let mut cpus = Vec::new(); + let mut current_cpu_map = HashMap::new(); + + while let Some(line) = lines.next_line().await? { + if line.trim().is_empty() { + if !current_cpu_map.is_empty() { + let cpu_info = parse_map_to_cpu_info(¤t_cpu_map); + cpus.push(cpu_info); + current_cpu_map.clear(); + } + continue; + } + + let parts: Vec<&str> = line.splitn(2, ':').collect(); + if parts.len() == 2 { + let key = parts[0].trim().to_lowercase(); + let value = parts[1].trim().to_string(); + current_cpu_map.insert(key, value); + } + } + + if !current_cpu_map.is_empty() { + let cpu_info = parse_map_to_cpu_info(¤t_cpu_map); + cpus.push(cpu_info); + } + + Ok(cpus) +} + +pub async fn handle_get_cpu_info(responder: oneshot::Sender>) { + info!("HostWorker: Processing GetCPUInfo request."); + let result = match read_and_parse_cpuinfo().await { + Ok(cpu_info) => Ok(GetCpuInfoResponse { cpu_info }), + Err(e) => { + error!("HostWorker: Failed to get CPU info: {e}"); + Err(Status::internal(format!("Failed to get CPU info: {e}"))) + } + }; + + if responder.send(result).is_err() { + error!( + "HostWorker: Failed to send response for GetCPUInfo. API handler may have timed out." + ); + } +} + +async fn read_net_stat(base_path: &Path, stat_name: &str) -> u64 { + let stat_path = base_path.join(stat_name); + fs::read_to_string(stat_path) + .await + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0) +} + +async fn read_all_net_stats() -> Result, std::io::Error> { + let mut devices = Vec::new(); + let mut entries = fs::read_dir("/sys/class/net").await?; + + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let name = entry + .file_name() + .into_string() + .unwrap_or_else(|_| "invalid_utf8".to_string()); + let stats_path = path.join("statistics"); + + if !stats_path.is_dir() { + continue; + } + + let device = NetDev { + name, + rx_bytes: read_net_stat(&stats_path, "rx_bytes").await, + rx_packets: read_net_stat(&stats_path, "rx_packets").await, + rx_errors: read_net_stat(&stats_path, "rx_errors").await, + rx_dropped: read_net_stat(&stats_path, "rx_dropped").await, + rx_fifo: read_net_stat(&stats_path, "rx_fifo_errors").await, + rx_frame: read_net_stat(&stats_path, "rx_frame_errors").await, + rx_compressed: read_net_stat(&stats_path, "rx_compressed").await, + rx_multicast: read_net_stat(&stats_path, "multicast").await, + tx_bytes: read_net_stat(&stats_path, "tx_bytes").await, + tx_packets: read_net_stat(&stats_path, "tx_packets").await, + tx_errors: read_net_stat(&stats_path, "tx_errors").await, + tx_dropped: read_net_stat(&stats_path, "tx_dropped").await, + tx_fifo: read_net_stat(&stats_path, "tx_fifo_errors").await, + tx_collisions: read_net_stat(&stats_path, "collisions").await, + tx_carrier: read_net_stat(&stats_path, "tx_carrier_errors").await, + tx_compressed: read_net_stat(&stats_path, "tx_compressed").await, + }; + devices.push(device); + } + + Ok(devices) +} + +pub async fn handle_get_network_info( + responder: oneshot::Sender>, +) { + info!("HostWorker: Processing GetNetworkInfo request."); + let result = match read_all_net_stats().await { + Ok(devices) => Ok(GetNetworkInfoResponse { devices }), + Err(e) => { + error!("HostWorker: Failed to get network info: {e}"); + Err(Status::internal(format!( + "Failed to get network info from sysfs: {e}" + ))) + } + }; + + if responder.send(result).is_err() { + error!( + "HostWorker: Failed to send response for GetNetworkInfo. API handler may have timed out." + ); + } +} + +pub async fn handle_get_version_info( + responder: oneshot::Sender>, +) { + info!("HostWorker: Processing GetVersionInfo request."); + + let kernel_version_res = fs::read_to_string("/proc/version").await; + + let result = match kernel_version_res { + Ok(kernel_version) => { + let feos_version = env!("CARGO_PKG_VERSION").to_string(); + Ok(GetVersionInfoResponse { + kernel_version: kernel_version.trim().to_string(), + feos_version, + }) + } + Err(e) => { + let msg = format!("Failed to read kernel version from /proc/version: {e}"); + error!("HostWorker: {msg}"); + Err(Status::internal(msg)) + } + }; + + if responder.send(result).is_err() { + error!( + "HostWorker: Failed to send response for GetVersionInfo. API handler may have timed out." + ); + } +} diff --git a/feos/services/host-service/src/worker/mod.rs b/feos/services/host-service/src/worker/mod.rs new file mode 100644 index 0000000..191ee16 --- /dev/null +++ b/feos/services/host-service/src/worker/mod.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +pub mod info; +pub mod ops; +pub mod power; + +pub use info::{ + handle_get_cpu_info, handle_get_memory, handle_get_network_info, handle_get_version_info, + handle_hostname, +}; +pub use ops::{handle_stream_feos_logs, handle_stream_kernel_logs, handle_upgrade}; +pub use power::{handle_reboot, handle_shutdown}; diff --git a/feos/services/host-service/src/worker/ops.rs b/feos/services/host-service/src/worker/ops.rs new file mode 100644 index 0000000..f2a17fd --- /dev/null +++ b/feos/services/host-service/src/worker/ops.rs @@ -0,0 +1,256 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::RestartSignal; +use digest::Digest; +use feos_proto::host_service::{ + FeosLogEntry, KernelLogEntry, UpgradeFeosBinaryRequest, UpgradeFeosBinaryResponse, +}; +use feos_utils::feos_logger::LogHandle; +use http_body_util::{BodyExt, Empty}; +use hyper::body::Bytes; +use hyper_rustls::HttpsConnectorBuilder; +use hyper_util::{client::legacy::Client, rt::TokioExecutor}; +use log::{error, info, warn}; +use prost_types::Timestamp; +use sha2::Sha256; +use std::fs::Permissions; +use std::io::Write; +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; +use tempfile::NamedTempFile; +use tokio::fs::File; +use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader}; +use tokio::sync::{mpsc, oneshot}; +use tonic::Status; + +const UPGRADE_DIR: &str = "/var/lib/feos/upgrade"; +const ELF_MAGIC: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46]; +const KMSG_PATH: &str = "/dev/kmsg"; + +pub async fn handle_stream_feos_logs( + log_handle: LogHandle, + grpc_tx: mpsc::Sender>, +) { + info!("HostWorker: Starting new FeOS log stream."); + let mut reader = match log_handle.new_reader().await { + Ok(r) => r, + Err(e) => { + error!("HostWorker: Failed to create log reader: {e}"); + if grpc_tx + .send(Err(Status::internal("Failed to create log reader"))) + .await + .is_err() + { + warn!("HostWorker: gRPC client for FeOS logs disconnected before error could be sent."); + } + return; + } + }; + + while let Some(entry) = reader.next().await { + let feos_log_entry = FeosLogEntry { + seq: entry.seq, + timestamp: Some(Timestamp { + seconds: entry.timestamp.timestamp(), + nanos: entry.timestamp.timestamp_subsec_nanos() as i32, + }), + level: entry.level.to_string(), + target: entry.target, + message: entry.message, + }; + + if grpc_tx.send(Ok(feos_log_entry)).await.is_err() { + info!("HostWorker: Log stream client disconnected."); + break; + } + } + info!("HostWorker: FeOS log stream finished."); +} + +pub async fn handle_stream_kernel_logs(grpc_tx: mpsc::Sender>) { + info!("HostWorker: Opening {KMSG_PATH} for streaming kernel logs."); + + let file = match File::open(KMSG_PATH).await { + Ok(f) => f, + Err(e) => { + let msg = format!("Failed to open {KMSG_PATH}: {e}"); + error!("HostWorker: {msg}"); + if grpc_tx.send(Err(Status::internal(msg))).await.is_err() { + warn!("HostWorker: gRPC client for kernel logs disconnected before error could be sent."); + } + return; + } + }; + + let mut reader = BufReader::new(file).lines(); + info!("HostWorker: Streaming logs from {KMSG_PATH}."); + + loop { + tokio::select! { + biased; + _ = grpc_tx.closed() => { + info!("HostWorker: gRPC client for kernel logs disconnected. Closing stream."); + break; + } + line_res = reader.next_line() => { + match line_res { + Ok(Some(line)) => { + let entry = KernelLogEntry { message: line }; + if grpc_tx.send(Ok(entry)).await.is_err() { + info!("HostWorker: gRPC client for kernel logs disconnected. Stopping stream."); + break; + } + } + Ok(None) => { + info!("HostWorker: Reached EOF on {KMSG_PATH}. Stream finished."); + break; + } + Err(e) => { + let msg = format!("Error reading from {KMSG_PATH}: {e}"); + error!("HostWorker: {msg}"); + let _ = grpc_tx.send(Err(Status::internal(msg))).await; + break; + } + } + } + } + } +} + +async fn download_file(url: &str, temp_file_writer: &mut std::fs::File) -> Result<(), String> { + info!("HostWorker: Starting download from {url}"); + + let https = HttpsConnectorBuilder::new() + .with_native_roots() + .map_err(|e| format!("Could not load native root certificates: {e}"))? + .https_or_http() + .enable_http1() + .build(); + + let client: Client<_, Empty> = Client::builder(TokioExecutor::new()).build(https); + let uri = url.parse::().map_err(|e| e.to_string())?; + let mut res = client + .get(uri) + .await + .map_err(|e| format!("HTTP GET request failed: {e}"))?; + + info!("HostWorker: Download response status: {}", res.status()); + if !res.status().is_success() { + return Err(format!("Download failed with status: {}", res.status())); + } + + while let Some(next) = res.frame().await { + let frame = next.map_err(|e| format!("Error reading response frame: {e}"))?; + if let Some(chunk) = frame.data_ref() { + temp_file_writer + .write_all(chunk) + .map_err(|e| format!("Failed to write chunk to temp file: {e}"))?; + } + } + + info!("HostWorker: Download completed successfully."); + Ok(()) +} + +pub async fn handle_upgrade( + restart_tx: mpsc::Sender, + req: UpgradeFeosBinaryRequest, + responder: oneshot::Sender>, +) { + info!( + "HostWorker: Processing UpgradeFeosBinary request for url {}", + req.url + ); + + if responder.send(Ok(UpgradeFeosBinaryResponse {})).is_err() { + warn!("HostWorker: Could not send response for UpgradeFeosBinary. Client may have disconnected."); + } + + let temp_file = match tokio::task::block_in_place(|| { + std::fs::create_dir_all(UPGRADE_DIR)?; + NamedTempFile::new_in(UPGRADE_DIR) + }) { + Ok(f) => f, + Err(e) => { + error!("HostWorker: Failed to create temp file: {e}"); + return; + } + }; + + let mut temp_file_writer = match temp_file.reopen() { + Ok(f) => f, + Err(e) => { + error!("HostWorker: Failed to reopen temp file for writing: {e}"); + return; + } + }; + + if let Err(e) = download_file(&req.url, &mut temp_file_writer).await { + error!("HostWorker: Failed to download binary: {e}"); + return; + } + + let temp_path = temp_file.path().to_path_buf(); + let mut hasher = Sha256::new(); + let mut file_to_hash = match File::open(&temp_path).await { + Ok(f) => f, + Err(e) => { + error!("HostWorker: Failed to reopen temp file for validation: {e}"); + return; + } + }; + + let mut first_bytes = [0u8; 4]; + if file_to_hash.read_exact(&mut first_bytes).await.is_err() { + error!("HostWorker: Downloaded file is too small to be a valid binary."); + return; + } + + if first_bytes != ELF_MAGIC { + error!("HostWorker: Downloaded file is not a valid ELF binary."); + return; + } + hasher.update(first_bytes); + + let mut buf = [0; 8192]; + loop { + match file_to_hash.read(&mut buf).await { + Ok(0) => break, + Ok(n) => hasher.update(&buf[..n]), + Err(e) => { + error!("HostWorker: Failed to read temp file for hashing: {e}"); + return; + } + } + } + let actual_checksum = hex::encode(hasher.finalize()); + + if actual_checksum != req.sha256_sum { + error!( + "HostWorker: Checksum mismatch. Expected: {}, Got: {}", + req.sha256_sum, actual_checksum + ); + return; + } + info!("HostWorker: Checksum validation successful."); + + let final_path = PathBuf::from(UPGRADE_DIR).join("feos.new"); + if let Err(e) = tokio::task::block_in_place(|| { + let perms = Permissions::from_mode(0o755); + temp_file.persist(&final_path)?; + std::fs::set_permissions(&final_path, perms) + }) { + error!("HostWorker: Failed to persist and set permissions on new binary: {e}"); + return; + } + + info!("HostWorker: Staged new binary at {:?}", &final_path); + + if let Err(e) = restart_tx.send(RestartSignal(final_path)).await { + error!("HostWorker: CRITICAL - Failed to send restart signal to main process: {e}"); + return; + } + + info!("HostWorker: Restart signal sent."); +} diff --git a/feos/services/host-service/src/worker/power.rs b/feos/services/host-service/src/worker/power.rs new file mode 100644 index 0000000..a4cb3d0 --- /dev/null +++ b/feos/services/host-service/src/worker/power.rs @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use feos_proto::host_service::{RebootRequest, RebootResponse, ShutdownRequest, ShutdownResponse}; +use log::{error, info}; +use nix::sys::reboot::{reboot, RebootMode}; +use tokio::sync::oneshot; +use tonic::Status; + +pub async fn handle_shutdown( + _req: ShutdownRequest, + responder: oneshot::Sender>, +) { + info!("HostWorker: Processing Shutdown request."); + + if responder.send(Ok(ShutdownResponse {})).is_err() { + error!( + "HostWorker: Failed to send response for Shutdown. The client may have disconnected." + ); + } + + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + info!("HostWorker: Executing system shutdown."); + match reboot(RebootMode::RB_POWER_OFF) { + Ok(infallible) => match infallible {}, + Err(e) => { + error!("HostWorker: CRITICAL - Failed to execute system shutdown: {e}"); + } + } +} + +pub async fn handle_reboot( + _req: RebootRequest, + responder: oneshot::Sender>, +) { + info!("HostWorker: Processing Reboot request."); + + if responder.send(Ok(RebootResponse {})).is_err() { + error!("HostWorker: Failed to send response for Reboot. The client may have disconnected."); + } + + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + info!("HostWorker: Executing system reboot."); + match reboot(RebootMode::RB_AUTOBOOT) { + Ok(infallible) => match infallible {}, + Err(e) => { + error!("HostWorker: CRITICAL - Failed to execute system reboot: {e}"); + } + } +} diff --git a/feos/services/image-service/Cargo.toml b/feos/services/image-service/Cargo.toml new file mode 100644 index 0000000..967e56e --- /dev/null +++ b/feos/services/image-service/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "image-service" +version.workspace = true +edition.workspace = true + +[dependencies] +feos-proto = { workspace = true } +oci-distribution = { workspace = true } +tempfile = { workspace = true } + +# Workspace dependencies +tokio = { workspace = true } +tokio-stream = { workspace = true } +tonic = { workspace = true } +anyhow = { workspace = true } +uuid = { workspace = true } +log = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } \ No newline at end of file diff --git a/feos/services/image-service/src/api.rs b/feos/services/image-service/src/api.rs new file mode 100644 index 0000000..2f6ff9b --- /dev/null +++ b/feos/services/image-service/src/api.rs @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::Command; +use feos_proto::image_service::{ + image_service_server::ImageService, DeleteImageRequest, DeleteImageResponse, + ImageStatusResponse, ListImagesRequest, ListImagesResponse, PullImageRequest, + PullImageResponse, WatchImageStatusRequest, +}; +use log::info; +use std::pin::Pin; +use tokio::sync::{mpsc, oneshot}; +use tokio_stream::{wrappers::ReceiverStream, Stream}; +use tonic::{Request, Response, Status}; + +pub struct ImageApiHandler { + dispatcher_tx: mpsc::Sender, +} + +impl ImageApiHandler { + pub fn new(dispatcher_tx: mpsc::Sender) -> Self { + Self { dispatcher_tx } + } +} + +#[tonic::async_trait] +impl ImageService for ImageApiHandler { + type WatchImageStatusStream = + Pin> + Send>>; + + async fn pull_image( + &self, + request: Request, + ) -> Result, Status> { + info!("ImageApi: Received PullImage request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::PullImage(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn watch_image_status( + &self, + request: Request, + ) -> Result, Status> { + info!("ImageApi: Received WatchImageStatus stream request."); + let (stream_tx, stream_rx) = mpsc::channel(16); + let cmd = Command::WatchImageStatus(request.into_inner(), stream_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + let output_stream = ReceiverStream::new(stream_rx); + Ok(Response::new(Box::pin(output_stream))) + } + + async fn list_images( + &self, + request: Request, + ) -> Result, Status> { + info!("ImageApi: Received ListImages request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::ListImages(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn delete_image( + &self, + request: Request, + ) -> Result, Status> { + info!("ImageApi: Received DeleteImage request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::DeleteImage(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + + match resp_rx.await { + Ok(Ok(_result)) => Ok(Response::new(DeleteImageResponse {})), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } +} diff --git a/feos/services/image-service/src/dispatcher.rs b/feos/services/image-service/src/dispatcher.rs new file mode 100644 index 0000000..7bf4c00 --- /dev/null +++ b/feos/services/image-service/src/dispatcher.rs @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{Command, OrchestratorCommand}; +use log::info; +use tokio::sync::mpsc; + +pub struct ImageServiceDispatcher { + command_rx: mpsc::Receiver, + command_tx: mpsc::Sender, + orchestrator_tx: mpsc::Sender, +} + +impl ImageServiceDispatcher { + pub fn new(orchestrator_tx: mpsc::Sender) -> Self { + let (command_tx, command_rx) = mpsc::channel(32); + Self { + command_rx, + command_tx, + orchestrator_tx, + } + } + + pub fn get_command_sender(&self) -> mpsc::Sender { + self.command_tx.clone() + } + + pub async fn run(mut self) { + info!("GrpcDispatcher: Running and waiting for API commands."); + while let Some(cmd) = self.command_rx.recv().await { + self.handle_command(cmd).await; + } + info!("GrpcDispatcher: Channel closed, shutting down."); + } + + async fn handle_command(&mut self, cmd: Command) { + let orchestrator_cmd = match cmd { + Command::PullImage(req, responder) => OrchestratorCommand::PullImage { + image_ref: req.image_ref, + responder, + }, + Command::ListImages(_req, responder) => OrchestratorCommand::ListImages { responder }, + Command::DeleteImage(req, responder) => OrchestratorCommand::DeleteImage { + image_uuid: req.image_uuid, + responder, + }, + Command::WatchImageStatus(req, stream_sender) => { + OrchestratorCommand::WatchImageStatus { + image_uuid: req.image_uuid, + stream_sender, + } + } + }; + + if self.orchestrator_tx.send(orchestrator_cmd).await.is_err() { + log::error!("GrpcDispatcher: Failed to send command to Orchestrator. The actor may have shut down."); + } + } +} diff --git a/feos/services/image-service/src/filestore.rs b/feos/services/image-service/src/filestore.rs new file mode 100644 index 0000000..7fac5d9 --- /dev/null +++ b/feos/services/image-service/src/filestore.rs @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{FileCommand, IMAGE_DIR}; +use feos_proto::image_service::{ImageInfo, ImageState}; +use log::{error, info, warn}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::Path; +use tokio::{fs, io::AsyncWriteExt, sync::mpsc}; + +#[derive(Serialize, Deserialize)] +struct ImageMetadata { + image_ref: String, +} + +pub struct FileStore { + command_rx: mpsc::Receiver, + command_tx: mpsc::Sender, +} + +impl Default for FileStore { + fn default() -> Self { + Self::new() + } +} + +impl FileStore { + pub fn new() -> Self { + let (command_tx, command_rx) = mpsc::channel(32); + Self { + command_rx, + command_tx, + } + } + + pub fn get_command_sender(&self) -> mpsc::Sender { + self.command_tx.clone() + } + + pub async fn run(mut self) { + info!("FileStore: Running and waiting for file commands."); + while let Some(cmd) = self.command_rx.recv().await { + self.handle_command(cmd).await; + } + info!("FileStore: Channel closed, shutting down."); + } + + async fn handle_command(&mut self, cmd: FileCommand) { + match cmd { + FileCommand::StoreImage { + image_uuid, + image_ref, + image_data, + responder, + } => { + info!("FileStore: Storing image {image_uuid}"); + let final_dir = Path::new(IMAGE_DIR).join(&image_uuid); + let result = Self::store_image_impl(&final_dir, &image_data, &image_ref).await; + let _ = responder.send(result); + } + FileCommand::DeleteImage { + image_uuid, + responder, + } => { + info!("FileStore: Deleting image {image_uuid}"); + let image_dir = Path::new(IMAGE_DIR).join(&image_uuid); + let result = fs::remove_dir_all(&image_dir).await; + let _ = responder.send(result); + } + FileCommand::ScanExistingImages { responder } => { + info!("FileStore: Scanning for existing images..."); + let store = Self::scan_images_impl().await; + let _ = responder.send(store); + } + } + } + + async fn store_image_impl( + final_dir: &Path, + image_data: &[u8], + image_ref: &str, + ) -> Result<(), std::io::Error> { + fs::create_dir_all(final_dir).await?; + let final_disk_path = final_dir.join("disk.image"); + let mut file = fs::File::create(final_disk_path).await?; + file.write_all(image_data).await?; + + let metadata = ImageMetadata { + image_ref: image_ref.to_string(), + }; + let metadata_json = + serde_json::to_string_pretty(&metadata).map_err(std::io::Error::other)?; + fs::write(final_dir.join("metadata.json"), metadata_json).await?; + Ok(()) + } + + async fn scan_images_impl() -> HashMap { + let mut store = HashMap::new(); + let mut entries = match fs::read_dir(IMAGE_DIR).await { + Ok(entries) => entries, + Err(e) => { + error!("FileStore: Failed to read image directory {IMAGE_DIR}: {e}"); + return store; + } + }; + + while let Some(entry) = entries.next_entry().await.ok().flatten() { + let path = entry.path(); + if !path.is_dir() { + continue; + } + + if let Some(uuid) = path.file_name().and_then(|s| s.to_str()) { + let metadata_path = path.join("metadata.json"); + let disk_image_path = path.join("disk.image"); + + if metadata_path.exists() && disk_image_path.exists() { + if let Ok(content) = fs::read_to_string(&metadata_path).await { + if let Ok(metadata) = serde_json::from_str::(&content) { + let image_info = ImageInfo { + image_uuid: uuid.to_string(), + image_ref: metadata.image_ref, + state: ImageState::Ready as i32, + }; + store.insert(uuid.to_string(), image_info); + } else { + warn!("FileStore: Could not parse metadata for {uuid}"); + } + } else { + warn!("FileStore: Could not read metadata for {uuid}"); + } + } + } + } + info!( + "FileStore: Filesystem scan complete. Found {} images.", + store.len() + ); + store + } +} diff --git a/feos/services/image-service/src/lib.rs b/feos/services/image-service/src/lib.rs new file mode 100644 index 0000000..8a562c1 --- /dev/null +++ b/feos/services/image-service/src/lib.rs @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use feos_proto::image_service::{ + DeleteImageRequest, DeleteImageResponse, ImageInfo, ImageState, ImageStatusResponse, + ListImagesRequest, ListImagesResponse, PullImageRequest, PullImageResponse, + WatchImageStatusRequest, +}; +use std::collections::HashMap; +use tokio::sync::{mpsc, oneshot}; +use tonic::Status; +pub mod api; +pub mod dispatcher; +pub mod filestore; +pub mod worker; + +pub const IMAGE_DIR: &str = "/tmp/feos/images"; +pub const IMAGE_SERVICE_SOCKET: &str = "/tmp/feos/image_service.sock"; + +#[derive(Debug, Clone)] +pub struct ImageStateEvent { + pub image_uuid: String, + pub state: ImageState, +} + +#[derive(Debug)] +pub enum Command { + PullImage( + PullImageRequest, + oneshot::Sender>, + ), + WatchImageStatus( + WatchImageStatusRequest, + mpsc::Sender>, + ), + ListImages( + ListImagesRequest, + oneshot::Sender>, + ), + DeleteImage( + DeleteImageRequest, + oneshot::Sender>, + ), +} + +#[derive(Debug)] +pub enum OrchestratorCommand { + PullImage { + image_ref: String, + responder: oneshot::Sender>, + }, + FinalizePull { + image_uuid: String, + image_ref: String, + image_data: Vec, + }, + FailPull { + image_uuid: String, + error: String, + }, + WatchImageStatus { + image_uuid: String, + stream_sender: mpsc::Sender>, + }, + ListImages { + responder: oneshot::Sender>, + }, + DeleteImage { + image_uuid: String, + responder: oneshot::Sender>, + }, +} + +#[derive(Debug)] +pub enum FileCommand { + StoreImage { + image_uuid: String, + image_ref: String, + image_data: Vec, + responder: oneshot::Sender>, + }, + DeleteImage { + image_uuid: String, + responder: oneshot::Sender>, + }, + ScanExistingImages { + responder: oneshot::Sender>, + }, +} diff --git a/feos/services/image-service/src/worker.rs b/feos/services/image-service/src/worker.rs new file mode 100644 index 0000000..ad68dc7 --- /dev/null +++ b/feos/services/image-service/src/worker.rs @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{FileCommand, ImageStateEvent, OrchestratorCommand}; +use feos_proto::image_service::{ + DeleteImageResponse, ImageInfo, ImageState, ImageStatusResponse, ListImagesResponse, + PullImageResponse, +}; +use log::{error, info, warn}; +use oci_distribution::{ + client::ClientConfig, errors::OciDistributionError, secrets, Client, ParseError, Reference, +}; +use std::collections::HashMap; +use tokio::sync::{broadcast, mpsc, oneshot}; +use tonic::Status; +use uuid::Uuid; + +const SQUASHFS_MEDIA_TYPE: &str = "application/vnd.ironcore.image.squashfs.v1alpha1.squashfs"; +const INITRAMFS_MEDIA_TYPE: &str = "application/vnd.ironcore.image.initramfs.v1alpha1.initramfs"; +const VMLINUZ_MEDIA_TYPE: &str = "application/vnd.ironcore.image.vmlinuz.v1alpha1.vmlinuz"; +const ROOTFS_MEDIA_TYPE: &str = "application/vnd.ironcore.image.rootfs.v1alpha1.rootfs"; + +pub struct Orchestrator { + command_rx: mpsc::Receiver, + command_tx: mpsc::Sender, + broadcast_tx: broadcast::Sender, + filestore_tx: mpsc::Sender, + store: HashMap, +} + +impl Orchestrator { + pub fn new(filestore_tx: mpsc::Sender) -> Self { + let (command_tx, command_rx) = mpsc::channel(32); + let (broadcast_tx, _) = broadcast::channel(32); + Self { + command_rx, + command_tx, + broadcast_tx, + filestore_tx, + store: HashMap::new(), + } + } + + pub fn get_command_sender(&self) -> mpsc::Sender { + self.command_tx.clone() + } + + pub async fn run(mut self) { + let (responder, resp_rx) = oneshot::channel(); + if self + .filestore_tx + .send(FileCommand::ScanExistingImages { responder }) + .await + .is_ok() + { + if let Ok(initial_store) = resp_rx.await { + self.store = initial_store; + } + } + + info!("Orchestrator: Running and waiting for commands."); + while let Some(cmd) = self.command_rx.recv().await { + self.handle_command(cmd).await; + } + info!("Orchestrator: Channel closed, shutting down."); + } + + async fn handle_command(&mut self, cmd: OrchestratorCommand) { + match cmd { + OrchestratorCommand::PullImage { + image_ref, + responder, + } => { + let image_uuid = Uuid::new_v4().to_string(); + info!("Orchestrator: Start pull for '{image_ref}', assigned UUID {image_uuid}"); + + self.store.insert( + image_uuid.clone(), + ImageInfo { + image_uuid: image_uuid.clone(), + image_ref: image_ref.clone(), + state: ImageState::Downloading as i32, + }, + ); + self.broadcast_state_change(image_uuid.clone(), ImageState::Downloading); + + let _ = responder.send(Ok(PullImageResponse { + image_uuid: image_uuid.clone(), + })); + + tokio::spawn(pull_oci_image( + self.command_tx.clone(), + image_uuid, + image_ref, + )); + } + OrchestratorCommand::FinalizePull { + image_uuid, + image_ref, + image_data, + } => { + info!("Orchestrator: Finalizing pull for {image_uuid}"); + let (responder, resp_rx) = oneshot::channel(); + let file_cmd = FileCommand::StoreImage { + image_uuid: image_uuid.clone(), + image_ref, + image_data, + responder, + }; + + if self.filestore_tx.send(file_cmd).await.is_err() { + error!("Orchestrator: Failed to send StoreImage command to FileStore."); + self.update_and_broadcast_state(image_uuid, ImageState::PullFailed); + return; + } + + match resp_rx.await { + Ok(Ok(())) => { + info!("Orchestrator: FileStore successfully stored image {image_uuid}"); + self.update_and_broadcast_state(image_uuid, ImageState::Ready); + } + Ok(Err(e)) => { + error!("Orchestrator: FileStore failed to store image {image_uuid}: {e}"); + self.update_and_broadcast_state(image_uuid, ImageState::PullFailed); + } + Err(_) => { + error!("Orchestrator: FileStore actor dropped response channel for {image_uuid}"); + self.update_and_broadcast_state(image_uuid, ImageState::PullFailed); + } + } + } + OrchestratorCommand::FailPull { image_uuid, error } => { + error!("Orchestrator: Pull failed for {image_uuid}: {error}"); + self.update_and_broadcast_state(image_uuid, ImageState::PullFailed); + } + OrchestratorCommand::ListImages { responder } => { + let images = self.store.values().cloned().collect(); + let _ = responder.send(Ok(ListImagesResponse { images })); + } + OrchestratorCommand::DeleteImage { + image_uuid, + responder, + } => { + info!("Orchestrator: Deleting image {image_uuid}"); + self.store.remove(&image_uuid); + + let (file_resp_tx, file_resp_rx) = oneshot::channel(); + let file_cmd = FileCommand::DeleteImage { + image_uuid: image_uuid.clone(), + responder: file_resp_tx, + }; + + if self.filestore_tx.send(file_cmd).await.is_err() { + error!("Orchestrator: Failed to send DeleteImage command to FileStore."); + } else if let Ok(Err(e)) = file_resp_rx.await { + if e.kind() != std::io::ErrorKind::NotFound { + warn!("Orchestrator: FileStore failed to delete {image_uuid}: {e}"); + } + } + + self.broadcast_state_change(image_uuid, ImageState::NotFound); + let _ = responder.send(Ok(DeleteImageResponse {})); + } + OrchestratorCommand::WatchImageStatus { + image_uuid, + stream_sender, + } => { + let initial_state = self + .store + .get(&image_uuid) + .map(|info| ImageState::try_from(info.state).unwrap_or(ImageState::Unspecified)) + .unwrap_or(ImageState::NotFound); + + tokio::spawn(watch_image_status_stream( + image_uuid, + initial_state, + stream_sender, + self.broadcast_tx.subscribe(), + )); + } + } + } + + fn update_and_broadcast_state(&mut self, image_uuid: String, new_state: ImageState) { + if let Some(info) = self.store.get_mut(&image_uuid) { + info.state = new_state as i32; + } + self.broadcast_state_change(image_uuid, new_state); + } + + fn broadcast_state_change(&self, image_uuid: String, state: ImageState) { + let event = ImageStateEvent { image_uuid, state }; + if self.broadcast_tx.send(event).is_err() { + info!("Orchestrator: Broadcast failed, no active listeners."); + } + } +} + +#[derive(Debug)] +pub enum PullError { + Oci(OciDistributionError), + Parse(ParseError), + MissingLayer(String), +} + +impl From for String { + fn from(err: PullError) -> Self { + format!("{err:?}") + } +} + +async fn download_layer_data(image_ref: &str) -> Result, PullError> { + info!("ImagePuller: fetching image: {image_ref}"); + let reference = Reference::try_from(image_ref.to_string()).map_err(PullError::Parse)?; + let accepted_media_types = vec![ + ROOTFS_MEDIA_TYPE, + SQUASHFS_MEDIA_TYPE, + INITRAMFS_MEDIA_TYPE, + VMLINUZ_MEDIA_TYPE, + ]; + + let config = ClientConfig { + ..Default::default() + }; + + let client = Client::new(config); + + let image_data = client + .pull( + &reference, + &secrets::RegistryAuth::Anonymous, + accepted_media_types, + ) + .await + .map_err(PullError::Oci)?; + info!("ImagePuller: image data pulled for {image_ref}"); + + let rootfs_layer = image_data + .layers + .into_iter() + .find(|l| l.media_type == ROOTFS_MEDIA_TYPE) + .ok_or_else(|| PullError::MissingLayer(ROOTFS_MEDIA_TYPE.to_string()))?; + + Ok(rootfs_layer.data) +} + +pub async fn pull_oci_image( + command_tx: mpsc::Sender, + image_uuid: String, + image_ref: String, +) { + match download_layer_data(&image_ref).await { + Ok(image_data) => { + let cmd = OrchestratorCommand::FinalizePull { + image_uuid, + image_ref, + image_data, + }; + if command_tx.send(cmd).await.is_err() { + error!("ImagePuller: Failed to send FinalizePull command. Actor may be down."); + } + } + Err(e) => { + let cmd = OrchestratorCommand::FailPull { + image_uuid, + error: e.into(), + }; + if command_tx.send(cmd).await.is_err() { + error!("ImagePuller: Failed to send FailPull command. Actor may be down."); + } + } + } +} + +pub async fn watch_image_status_stream( + image_uuid_to_watch: String, + initial_state: ImageState, + stream_sender: mpsc::Sender>, + mut broadcast_rx: broadcast::Receiver, +) { + info!("ImageWatcher: Starting watch stream for {image_uuid_to_watch}"); + + let initial_response = ImageStatusResponse { + state: initial_state as i32, + progress_percent: if initial_state == ImageState::Ready { + 100 + } else { + 0 + }, + message: format!("Initial state: {initial_state:?}"), + }; + if stream_sender.send(Ok(initial_response)).await.is_err() { + info!( + "ImageWatcher: Client disconnected before initial state send for {image_uuid_to_watch}" + ); + return; + } + + if matches!( + initial_state, + ImageState::Ready | ImageState::PullFailed | ImageState::NotFound + ) { + info!( + "ImageWatcher: Closing stream for {image_uuid_to_watch} due to terminal initial state." + ); + return; + } + + loop { + match broadcast_rx.recv().await { + Ok(event) => { + if event.image_uuid == image_uuid_to_watch { + info!( + "ImageWatcher: Got relevant event for {image_uuid_to_watch}: {:?}", + event.state + ); + let response = ImageStatusResponse { + state: event.state as i32, + progress_percent: if event.state == ImageState::Ready { + 100 + } else { + 0 + }, + message: format!("New state: {:?}", event.state), + }; + + if stream_sender.send(Ok(response)).await.is_err() { + info!("ImageWatcher: Client disconnected. Closing watch for {image_uuid_to_watch}"); + break; + } + + if matches!( + event.state, + ImageState::Ready | ImageState::PullFailed | ImageState::NotFound + ) { + info!("ImageWatcher: Reached terminal state. Closing watch for {image_uuid_to_watch}"); + break; + } + } + } + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!("ImageWatcher: Stream for {image_uuid_to_watch} lagged by {n} messages. Continuing."); + } + Err(broadcast::error::RecvError::Closed) => { + info!("ImageWatcher: Broadcast channel closed. Shutting down watch for {image_uuid_to_watch}"); + break; + } + } + } +} diff --git a/feos/services/vm-service/.sqlx/query-135f3d50360087853fec6ae0342409a98f4a52d8862124a8b7f698c814325157.json b/feos/services/vm-service/.sqlx/query-135f3d50360087853fec6ae0342409a98f4a52d8862124a8b7f698c814325157.json new file mode 100644 index 0000000..20cf7c1 --- /dev/null +++ b/feos/services/vm-service/.sqlx/query-135f3d50360087853fec6ae0342409a98f4a52d8862124a8b7f698c814325157.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM vms WHERE vm_id = ?1", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "135f3d50360087853fec6ae0342409a98f4a52d8862124a8b7f698c814325157" +} diff --git a/feos/services/vm-service/.sqlx/query-370675f1c72e8dd9d6cfbd3aa2a0813dba1f31bc9b727972523fd17f9607dbbc.json b/feos/services/vm-service/.sqlx/query-370675f1c72e8dd9d6cfbd3aa2a0813dba1f31bc9b727972523fd17f9607dbbc.json new file mode 100644 index 0000000..c800dd1 --- /dev/null +++ b/feos/services/vm-service/.sqlx/query-370675f1c72e8dd9d6cfbd3aa2a0813dba1f31bc9b727972523fd17f9607dbbc.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR REPLACE INTO vms (vm_id, image_uuid, state, last_msg, pid, config_blob)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "370675f1c72e8dd9d6cfbd3aa2a0813dba1f31bc9b727972523fd17f9607dbbc" +} diff --git a/feos/services/vm-service/.sqlx/query-634d547d9ae7228d964e052292fe89d00c0b3d4f1e9f536e597652ce32a8e515.json b/feos/services/vm-service/.sqlx/query-634d547d9ae7228d964e052292fe89d00c0b3d4f1e9f536e597652ce32a8e515.json new file mode 100644 index 0000000..d6ee7dd --- /dev/null +++ b/feos/services/vm-service/.sqlx/query-634d547d9ae7228d964e052292fe89d00c0b3d4f1e9f536e597652ce32a8e515.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE vms\n SET state = ?1, last_msg = ?2\n WHERE vm_id = ?3\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "634d547d9ae7228d964e052292fe89d00c0b3d4f1e9f536e597652ce32a8e515" +} diff --git a/feos/services/vm-service/.sqlx/query-87733e46a88697f25589451e9a1fd11cbb450a9a6b875c0c6fa01bf349a085cd.json b/feos/services/vm-service/.sqlx/query-87733e46a88697f25589451e9a1fd11cbb450a9a6b875c0c6fa01bf349a085cd.json new file mode 100644 index 0000000..3276fc9 --- /dev/null +++ b/feos/services/vm-service/.sqlx/query-87733e46a88697f25589451e9a1fd11cbb450a9a6b875c0c6fa01bf349a085cd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE vms SET pid = ?1 WHERE vm_id = ?2", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "87733e46a88697f25589451e9a1fd11cbb450a9a6b875c0c6fa01bf349a085cd" +} diff --git a/feos/services/vm-service/Cargo.toml b/feos/services/vm-service/Cargo.toml new file mode 100644 index 0000000..53837ca --- /dev/null +++ b/feos/services/vm-service/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "vm-service" +version.workspace = true +edition.workspace = true + +[dependencies] +feos-proto = { workspace = true } +image-service = { path = "../image-service" } +hyper = { version = "1.6.0" } +cloud-hypervisor-client = { version = "0.3.3"} +once_cell = "1.19" +hyperlocal = "0.9.1" +hyper-util = { version = "0.1.14" } +thiserror = "1.0" +urlencoding = "2.1.3" +dotenvy = "0.15" +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "macros", "uuid"] } + +# Workspace dependencies +openssl = { workspace = true, features = ["vendored"] } +tokio = { workspace = true } +tokio-stream = { workspace = true } +tonic = { workspace = true } +anyhow = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +nix = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +uuid = { workspace = true } +log = { workspace = true } +tower = { workspace = true } \ No newline at end of file diff --git a/feos/services/vm-service/build.rs b/feos/services/vm-service/build.rs new file mode 100644 index 0000000..afe8248 --- /dev/null +++ b/feos/services/vm-service/build.rs @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +fn main() -> Result<(), Box> { + println!("cargo:rustc-env=SQLX_OFFLINE=true"); + Ok(()) +} diff --git a/feos/services/vm-service/migrations/20250713175451_create_vms_table.sql b/feos/services/vm-service/migrations/20250713175451_create_vms_table.sql new file mode 100644 index 0000000..9937366 --- /dev/null +++ b/feos/services/vm-service/migrations/20250713175451_create_vms_table.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS vms ( + -- The primary key for the VM, generated by the vm-service. + vm_id TEXT PRIMARY KEY NOT NULL, + -- The UUID of the root filesystem image used by this VM. + image_uuid TEXT NOT NULL, + -- The current state of the VM (e.g., 'Creating', 'Running'). Stored as text for readability. + state TEXT NOT NULL, + -- The last human-readable status message associated with the VM's state. + last_msg TEXT, + -- The process ID (PID) of the running hypervisor process, if any. + pid INTEGER, + -- A binary blob containing the serialized VmConfig protobuf message. + -- This allows for flexible configuration changes without schema migrations. + config_blob BLOB NOT NULL, + -- Timestamp of when the record was first created. + created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + -- Timestamp of the last update to the record. + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +-- A trigger to automatically update the 'updated_at' timestamp whenever a row is modified. +CREATE TRIGGER IF NOT EXISTS trigger_vms_updated_at +AFTER UPDATE ON vms +FOR EACH ROW +BEGIN + UPDATE vms SET updated_at = CURRENT_TIMESTAMP WHERE vm_id = OLD.vm_id; +END; diff --git a/feos/services/vm-service/src/api.rs b/feos/services/vm-service/src/api.rs new file mode 100644 index 0000000..6b35834 --- /dev/null +++ b/feos/services/vm-service/src/api.rs @@ -0,0 +1,282 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::Command; +use feos_proto::vm_service::{ + vm_service_server::VmService, AttachDiskRequest, AttachDiskResponse, CreateVmRequest, + CreateVmResponse, DeleteVmRequest, DeleteVmResponse, GetVmRequest, ListVmsRequest, + ListVmsResponse, PauseVmRequest, PauseVmResponse, PingVmRequest, PingVmResponse, + RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, + ShutdownVmResponse, StartVmRequest, StartVmResponse, StreamVmConsoleRequest, + StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, +}; +use log::info; +use std::pin::Pin; +use tokio::sync::{mpsc, oneshot}; +use tokio_stream::{wrappers::ReceiverStream, Stream}; +use tonic::{Request, Response, Status, Streaming}; + +pub struct VmApiHandler { + dispatcher_tx: mpsc::Sender, +} + +impl VmApiHandler { + pub fn new(dispatcher_tx: mpsc::Sender) -> Self { + Self { dispatcher_tx } + } +} + +#[tonic::async_trait] +impl VmService for VmApiHandler { + type StreamVmEventsStream = Pin> + Send>>; + type StreamVmConsoleStream = + Pin> + Send>>; + + async fn create_vm( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received CreateVm request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::CreateVm(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn start_vm( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received StartVm request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::StartVm(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn get_vm(&self, request: Request) -> Result, Status> { + info!("VmApi: Received GetVm request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::GetVm(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn stream_vm_events( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received StreamVmEvents stream request."); + let (stream_tx, stream_rx) = mpsc::channel(16); + let cmd = Command::StreamVmEvents(request.into_inner(), stream_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + let output_stream = ReceiverStream::new(stream_rx); + Ok(Response::new(Box::pin(output_stream))) + } + + async fn delete_vm( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received DeleteVm request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::DeleteVm(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn stream_vm_console( + &self, + request: Request>, + ) -> Result, Status> { + info!("VmApi: Received StreamVmConsole stream request."); + let grpc_input_stream = request.into_inner(); + let (grpc_output_tx, grpc_output_rx) = mpsc::channel(32); + let cmd = Command::StreamVmConsole(grpc_input_stream, grpc_output_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + let output_stream = ReceiverStream::new(grpc_output_rx); + Ok(Response::new(Box::pin(output_stream))) + } + + async fn list_vms( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received ListVms request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::ListVms(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn ping_vm( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received PingVm request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::PingVm(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn shutdown_vm( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received ShutdownVm request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::ShutdownVm(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn pause_vm( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received PauseVm request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::PauseVm(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn resume_vm( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received ResumeVm request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::ResumeVm(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn attach_disk( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received AttachDisk request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::AttachDisk(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } + + async fn remove_disk( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received RemoveDisk request."); + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::RemoveDisk(request.into_inner(), resp_tx); + self.dispatcher_tx + .send(cmd) + .await + .map_err(|e| Status::internal(format!("Failed to send command to dispatcher: {e}")))?; + match resp_rx.await { + Ok(Ok(result)) => Ok(Response::new(result)), + Ok(Err(status)) => Err(status), + Err(_) => Err(Status::internal( + "Dispatcher task dropped response channel.", + )), + } + } +} diff --git a/feos/services/vm-service/src/dispatcher.rs b/feos/services/vm-service/src/dispatcher.rs new file mode 100644 index 0000000..f4857c9 --- /dev/null +++ b/feos/services/vm-service/src/dispatcher.rs @@ -0,0 +1,220 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + dispatcher_handlers::{ + handle_create_vm_command, handle_delete_vm_command, handle_get_vm_command, + handle_list_vms_command, handle_stream_vm_events_command, perform_startup_sanity_check, + }, + persistence::repository::VmRepository, + vmm::{factory, Hypervisor, VmmType}, + worker, Command, VmEventWrapper, +}; +use anyhow::Result; +use feos_proto::vm_service::{VmState, VmStateChangedEvent}; +use log::{debug, error, info}; +use prost::Message; +use std::sync::Arc; +use tokio::sync::{broadcast, mpsc}; +use uuid::Uuid; + +pub struct VmServiceDispatcher { + rx: mpsc::Receiver, + event_bus_tx: mpsc::Sender, + event_bus_rx_for_dispatcher: mpsc::Receiver, + status_channel_tx: broadcast::Sender, + hypervisor: Arc, + repository: VmRepository, + healthcheck_cancel_bus: broadcast::Sender, +} + +impl VmServiceDispatcher { + pub async fn new(rx: mpsc::Receiver, db_url: &str) -> Result { + let (event_bus_tx, event_bus_rx_for_dispatcher) = mpsc::channel(32); + let (status_channel_tx, _) = broadcast::channel(32); + let (healthcheck_cancel_bus, _) = broadcast::channel::(32); + let hypervisor = Arc::from(factory(VmmType::CloudHypervisor)); + info!("VmDispatcher: Connecting to persistence layer at {db_url}..."); + let repository = VmRepository::connect(db_url).await?; + info!("VmDispatcher: Persistence layer connected successfully."); + Ok(Self { + rx, + event_bus_tx, + event_bus_rx_for_dispatcher, + status_channel_tx, + hypervisor, + repository, + healthcheck_cancel_bus, + }) + } + + pub async fn run(mut self) { + perform_startup_sanity_check( + &self.repository, + self.hypervisor.clone(), + self.event_bus_tx.clone(), + &self.healthcheck_cancel_bus, + ) + .await; + + info!("VmDispatcher: Running and waiting for commands and events."); + loop { + tokio::select! { + biased; + Some(cmd) = self.rx.recv() => { + let hypervisor = self.hypervisor.clone(); + let event_bus_tx = self.event_bus_tx.clone(); + let status_channel_tx = self.status_channel_tx.clone(); + let healthcheck_cancel_bus_tx = self.healthcheck_cancel_bus.clone(); + + match cmd { + Command::CreateVm(req, responder) => { + handle_create_vm_command(&self.repository, req, responder, hypervisor, event_bus_tx).await; + } + Command::StartVm(req, responder) => { + let cancel_bus = healthcheck_cancel_bus_tx.subscribe(); + tokio::spawn(worker::handle_start_vm(req, responder, hypervisor, event_bus_tx, cancel_bus)); + } + Command::GetVm(req, responder) => { + handle_get_vm_command(&self.repository, req, responder).await; + } + Command::StreamVmEvents(req, stream_tx) => { + handle_stream_vm_events_command(&self.repository, req, stream_tx, status_channel_tx).await; + } + Command::DeleteVm(req, responder) => { + handle_delete_vm_command(&self.repository, &self.healthcheck_cancel_bus, req, responder, hypervisor, event_bus_tx).await; + } + Command::StreamVmConsole(input_stream, output_tx) => { + tokio::spawn(worker::handle_stream_vm_console(input_stream, output_tx, hypervisor)); + } + Command::ListVms(req, responder) => { + handle_list_vms_command(&self.repository, req, responder).await; + } + Command::PingVm(req, responder) => { + tokio::spawn(worker::handle_ping_vm(req, responder, hypervisor)); + } + Command::ShutdownVm(req, responder) => { + tokio::spawn(worker::handle_shutdown_vm(req, responder, hypervisor, event_bus_tx)); + } + Command::PauseVm(req, responder) => { + tokio::spawn(worker::handle_pause_vm(req, responder, hypervisor, event_bus_tx)); + } + Command::ResumeVm(req, responder) => { + tokio::spawn(worker::handle_resume_vm(req, responder, hypervisor, event_bus_tx)); + } + Command::AttachDisk(req, responder) => { + tokio::spawn(worker::handle_attach_disk(req, responder, hypervisor)); + } + Command::RemoveDisk(req, responder) => { + tokio::spawn(worker::handle_remove_disk(req, responder, hypervisor)); + } + } + }, + Some(event) = self.event_bus_rx_for_dispatcher.recv() => { + self.handle_vm_event(event).await; + } + else => { + info!("VmDispatcher: A channel closed, shutting down."); + break; + } + } + } + } + + async fn handle_vm_event(&mut self, event_wrapper: VmEventWrapper) { + let event_to_forward = event_wrapper.clone(); + let event = event_wrapper.event; + + info!( + "VmDispatcher_LOGGER: Event for VM '{}': ID '{}', Component '{}', Data Type '{}'", + event.vm_id, + event.id, + event.component_id, + event.data.as_ref().map_or("None", |d| &d.type_url) + ); + + let vm_id_uuid = match Uuid::parse_str(&event.vm_id) { + Ok(id) => id, + Err(e) => { + error!( + "DatabaseUpdate: Could not parse UUID from event vm_id '{}': {e}", + &event.vm_id + ); + return; + } + }; + + if let Some(pid) = event_wrapper.process_id { + info!("DatabaseUpdate: Updating pid for VM {vm_id_uuid} to {pid}"); + if let Err(e) = self.repository.update_vm_pid(vm_id_uuid, pid).await { + error!("DatabaseUpdate: Failed to update pid for VM {vm_id_uuid}: {e}"); + } + } + + if let Some(data) = &event.data { + if data.type_url.contains("VmStateChangedEvent") { + self.handle_vm_state_changed_event( + data, + vm_id_uuid, + &event.vm_id, + event_to_forward, + ) + .await; + } + } + } + + async fn handle_vm_state_changed_event( + &mut self, + data: &prost_types::Any, + vm_id_uuid: Uuid, + vm_id: &str, + event_to_forward: VmEventWrapper, + ) { + match VmStateChangedEvent::decode(&*data.value) { + Ok(state_change) => { + let new_state = match VmState::try_from(state_change.new_state) { + Ok(s) => s, + Err(e) => { + error!( + "DatabaseUpdate: Invalid VmState value '{}' in event: {e}", + state_change.new_state + ); + return; + } + }; + + info!( + "DatabaseUpdate: Updating status for VM {vm_id_uuid} to {new_state:?} with message: '{}'", + state_change.reason + ); + match self + .repository + .update_vm_status(vm_id_uuid, new_state, &state_change.reason) + .await + { + Ok(true) => { + if let Err(e) = self.status_channel_tx.send(event_to_forward) { + debug!( + "VmDispatcher: Failed to forward successful VM status event for {vm_id}: {e}" + ); + } + } + Ok(false) => { + info!( + "DatabaseUpdate: Update for VM {vm_id_uuid} was a no-op (record likely already deleted). Event not forwarded." + ); + } + Err(e) => { + error!( + "DatabaseUpdate: Failed to execute status update for VM {vm_id_uuid}: {e}" + ); + } + } + } + Err(e) => { + error!("DatabaseUpdate: Failed to decode VmStateChangedEvent for VM {vm_id}: {e}"); + } + } + } +} diff --git a/feos/services/vm-service/src/dispatcher_handlers.rs b/feos/services/vm-service/src/dispatcher_handlers.rs new file mode 100644 index 0000000..5cfdf50 --- /dev/null +++ b/feos/services/vm-service/src/dispatcher_handlers.rs @@ -0,0 +1,533 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + persistence::{repository::VmRepository, VmRecord, VmStatus}, + vmm::Hypervisor, + worker, VmEventWrapper, +}; +use feos_proto::{ + image_service::{image_service_client::ImageServiceClient, PullImageRequest}, + vm_service::{ + CreateVmRequest, CreateVmResponse, DeleteVmRequest, DeleteVmResponse, GetVmRequest, + ListVmsRequest, ListVmsResponse, StreamVmEventsRequest, VmEvent, VmInfo, VmState, + VmStateChangedEvent, + }, +}; +use image_service::IMAGE_SERVICE_SOCKET; +use log::{error, info, warn}; +use nix::unistd::Pid; +use prost::Message; +use prost_types::Any; +use std::{path::PathBuf, sync::Arc}; +use tokio::sync::{broadcast, mpsc, oneshot}; +use tonic::{ + transport::{Channel, Endpoint, Error as TonicTransportError, Uri}, + Status, +}; +use tower::service_fn; +use uuid::Uuid; + +pub(crate) async fn get_image_service_client( +) -> Result, TonicTransportError> { + let socket_path = PathBuf::from(IMAGE_SERVICE_SOCKET); + Endpoint::try_from("http://[::1]:50051") + .unwrap() + .connect_with_connector(service_fn(move |_: Uri| { + tokio::net::UnixStream::connect(socket_path.clone()) + })) + .await + .map(ImageServiceClient::new) +} + +async fn initiate_image_pull_for_vm(req: &CreateVmRequest) -> Result { + let image_ref = match req.config.as_ref() { + Some(config) if !config.image_ref.is_empty() => config.image_ref.clone(), + _ => { + return Err(Status::invalid_argument( + "VmConfig with a non-empty image_ref is required", + )); + } + }; + + info!("VmDispatcher: Requesting image pull for {image_ref}"); + let mut client = get_image_service_client() + .await + .map_err(|e| Status::unavailable(format!("Could not connect to ImageService: {e}")))?; + + let response = client + .pull_image(PullImageRequest { + image_ref: image_ref.clone(), + }) + .await + .map_err(|status| { + let msg = format!("PullImage RPC failed for {image_ref}: {status}"); + error!("VmDispatcher: {msg}"); + Status::unavailable(msg) + })?; + + let image_uuid = response.into_inner().image_uuid; + info!("VmDispatcher: Image pull for {image_ref} initiated. UUID: {image_uuid}"); + Ok(image_uuid) +} + +pub(crate) async fn handle_create_vm_command( + repository: &VmRepository, + req: CreateVmRequest, + responder: oneshot::Sender>, + hypervisor: Arc, + event_bus_tx: mpsc::Sender, +) { + let vm_id_res: Result<(Uuid, bool), Status> = + if let Some(id_str) = req.vm_id.as_deref().filter(|s| !s.is_empty()) { + match Uuid::parse_str(id_str) { + Ok(id) if !id.is_nil() => Ok((id, true)), + Ok(_) => Err(Status::invalid_argument( + "Provided vm_id cannot be the nil UUID.", + )), + Err(_) => Err(Status::invalid_argument( + "Provided vm_id is not a valid UUID format.", + )), + } + } else { + Ok((Uuid::new_v4(), false)) + }; + + let (vm_id, is_user_provided) = match vm_id_res { + Ok(val) => val, + Err(status) => { + if responder.send(Err(status)).is_err() { + error!( + "VmDispatcher: Failed to send error response for CreateVm. Responder closed." + ); + } + return; + } + }; + + if is_user_provided { + match repository.get_vm(vm_id).await { + Ok(Some(_)) => { + let status = Status::already_exists(format!("VM with ID {vm_id} already exists.")); + if responder.send(Err(status)).is_err() { + error!("VmDispatcher: Failed to send error response for CreateVm. Responder closed."); + } + return; + } + Ok(None) => {} + Err(e) => { + let status = Status::internal(format!("Failed to check DB for existing VM: {e}")); + if responder.send(Err(status)).is_err() { + error!("VmDispatcher: Failed to send error response for CreateVm. Responder closed."); + } + return; + } + } + } + + match initiate_image_pull_for_vm(&req).await { + Ok(image_uuid_str) => { + let image_uuid = match Uuid::parse_str(&image_uuid_str) { + Ok(uuid) => uuid, + Err(e) => { + let status = Status::internal(format!("Failed to parse image UUID: {e}")); + if responder.send(Err(status)).is_err() { + error!("VmDispatcher: Failed to send error response for CreateVm. Responder closed."); + } + return; + } + }; + + let record = VmRecord { + vm_id, + image_uuid, + status: VmStatus { + state: VmState::Creating, + last_msg: "VM creation initiated".to_string(), + process_id: None, + }, + config: req.config.clone().unwrap(), + }; + + if let Err(e) = repository.save_vm(&record).await { + let status = Status::internal(format!("Failed to save VM to database: {e}")); + error!("VmDispatcher: {message}", message = status.message()); + if responder.send(Err(status)).is_err() { + error!("VmDispatcher: Failed to send error response for CreateVm. Responder closed."); + } + return; + } + info!("VmDispatcher: Saved initial record for VM {vm_id}"); + + tokio::spawn(worker::handle_create_vm( + vm_id.to_string(), + req, + image_uuid_str, + responder, + hypervisor, + event_bus_tx, + )); + } + Err(status) => { + if responder.send(Err(status)).is_err() { + error!( + "VmDispatcher: Failed to send error response for CreateVm. Responder closed." + ); + } + } + } +} + +pub(crate) async fn handle_get_vm_command( + repository: &VmRepository, + req: GetVmRequest, + responder: oneshot::Sender>, +) { + let vm_id = match Uuid::parse_str(&req.vm_id) { + Ok(id) => id, + Err(_) => { + let _ = responder.send(Err(Status::invalid_argument("Invalid VM ID format."))); + return; + } + }; + + match repository.get_vm(vm_id).await { + Ok(Some(record)) => { + let vm_info = VmInfo { + vm_id: record.vm_id.to_string(), + state: record.status.state as i32, + config: Some(record.config), + }; + let _ = responder.send(Ok(vm_info)); + } + Ok(None) => { + let _ = responder.send(Err(Status::not_found(format!( + "VM with ID {vm_id} not found" + )))); + } + Err(e) => { + error!("Failed to get VM from database: {e}"); + let _ = responder.send(Err(Status::internal("Failed to retrieve VM information."))); + } + } +} + +pub(crate) async fn handle_stream_vm_events_command( + repository: &VmRepository, + req: StreamVmEventsRequest, + stream_tx: mpsc::Sender>, + status_channel_tx: broadcast::Sender, +) { + if let Some(vm_id_str) = req.vm_id.clone() { + let vm_id = match Uuid::parse_str(&vm_id_str) { + Ok(id) => id, + Err(_) => { + if stream_tx + .send(Err(Status::invalid_argument("Invalid VM ID format."))) + .await + .is_err() + { + warn!( + "StreamEvents: Client for {vm_id_str} disconnected before error could be sent." + ); + } + return; + } + }; + + match repository.get_vm(vm_id).await { + Ok(Some(record)) => { + info!( + "StreamEvents: Sending initial state for VM {vm_id_str}: {:?}", + record.status.state + ); + let state_change_event = VmStateChangedEvent { + new_state: record.status.state as i32, + reason: record.status.last_msg, + }; + let initial_event = VmEvent { + vm_id: vm_id_str.clone(), + id: Uuid::new_v4().to_string(), + component_id: "vm-service-db".to_string(), + data: Some(Any { + type_url: "type.googleapis.com/feos.vm.vmm.api.v1.VmStateChangedEvent" + .to_string(), + value: state_change_event.encode_to_vec(), + }), + }; + + if stream_tx.send(Ok(initial_event)).await.is_err() { + info!( + "StreamEvents: Client for {vm_id_str} disconnected before live events could be streamed." + ); + return; + } + + tokio::spawn(worker::handle_stream_vm_events( + req, + stream_tx, + status_channel_tx, + )); + } + Ok(None) => { + warn!("VM with ID {vm_id} not found"); + if stream_tx + .send(Err(Status::not_found(format!( + "VM with ID {vm_id} not found" + )))) + .await + .is_err() + { + warn!( + "StreamEvents: Client for {vm_id_str} disconnected before not-found error could be sent." + ); + } + } + Err(e) => { + error!( + "StreamEvents: Failed to get VM {vm_id_str} from database for event stream: {e}" + ); + if stream_tx + .send(Err(Status::internal( + "Failed to retrieve VM information for event stream.", + ))) + .await + .is_err() + { + warn!( + "StreamEvents: Client for {vm_id_str} disconnected before internal-error could be sent." + ); + } + } + } + } else { + info!("StreamEvents: Request to stream events for all VMs received."); + match repository.list_all_vms().await { + Ok(records) => { + info!( + "StreamEvents: Found {} existing VMs to send initial state for.", + records.len() + ); + for record in records { + let state_change_event = VmStateChangedEvent { + new_state: record.status.state as i32, + reason: format!("Initial state from DB: {}", record.status.last_msg), + }; + let initial_event = VmEvent { + vm_id: record.vm_id.to_string(), + id: Uuid::new_v4().to_string(), + component_id: "vm-service-db".to_string(), + data: Some(Any { + type_url: "type.googleapis.com/feos.vm.vmm.api.v1.VmStateChangedEvent" + .to_string(), + value: state_change_event.encode_to_vec(), + }), + }; + + if stream_tx.send(Ok(initial_event)).await.is_err() { + info!("StreamEvents: Client for all VMs disconnected while sending initial states."); + return; + } + } + } + Err(e) => { + error!("StreamEvents: Failed to list all VMs from database for event stream: {e}"); + if stream_tx + .send(Err(Status::internal( + "Failed to retrieve initial VM list for event stream.", + ))) + .await + .is_err() + { + warn!("StreamEvents: Client for all VMs disconnected before internal-error could be sent."); + } + return; + } + } + + info!("StreamEvents: Initial states sent. Starting live event stream for all VMs."); + tokio::spawn(worker::handle_stream_vm_events( + req, + stream_tx, + status_channel_tx, + )); + } +} + +pub(crate) async fn handle_delete_vm_command( + repository: &VmRepository, + healthcheck_cancel_bus: &broadcast::Sender, + req: DeleteVmRequest, + responder: oneshot::Sender>, + hypervisor: Arc, + event_bus_tx: mpsc::Sender, +) { + let vm_id = match Uuid::parse_str(&req.vm_id) { + Ok(id) => id, + Err(_) => { + let _ = responder.send(Err(Status::invalid_argument("Invalid VM ID format."))); + return; + } + }; + + match repository.get_vm(vm_id).await { + Ok(Some(record)) => { + let image_uuid_to_delete = record.image_uuid.to_string(); + let process_id_to_kill = record.status.process_id; + + if let Err(e) = repository.delete_vm(vm_id).await { + error!("Failed to delete VM {vm_id} from database: {e}"); + let _ = responder.send(Err(Status::internal("Failed to delete VM from database."))); + return; + } + info!("VmDispatcher: Deleted record for VM {vm_id} from database."); + + if let Err(e) = healthcheck_cancel_bus.send(vm_id) { + warn!("VmDispatcher: Failed to send healthcheck cancellation for {vm_id}: {e}"); + } + + tokio::spawn(worker::handle_delete_vm( + req, + image_uuid_to_delete, + process_id_to_kill, + responder, + hypervisor, + event_bus_tx, + )); + } + Ok(None) => { + let msg = format!("VM with ID {vm_id} not found in database for deletion"); + warn!("VmDispatcher: {msg}. Still attempting hypervisor cleanup."); + + if let Err(e) = healthcheck_cancel_bus.send(vm_id) { + warn!("VmDispatcher: Failed to send healthcheck cancellation for {vm_id}: {e}"); + } + + tokio::spawn(worker::handle_delete_vm( + req, + String::new(), + None, + responder, + hypervisor, + event_bus_tx, + )); + } + Err(e) => { + error!("Failed to get VM {vm_id} from database: {e}"); + let _ = responder.send(Err(Status::internal("Failed to retrieve VM for deletion."))); + } + } +} + +pub(crate) async fn handle_list_vms_command( + repository: &VmRepository, + _req: ListVmsRequest, + responder: oneshot::Sender>, +) { + match repository.list_all_vms().await { + Ok(records) => { + let vms = records + .into_iter() + .map(|record| VmInfo { + vm_id: record.vm_id.to_string(), + state: record.status.state as i32, + config: Some(record.config), + }) + .collect(); + + let response = ListVmsResponse { vms }; + if responder.send(Ok(response)).is_err() { + error!("VmDispatcher: Failed to send response for ListVms."); + } + } + Err(e) => { + error!("VmDispatcher: Failed to list VMs from database: {e}"); + let status = Status::internal("Failed to retrieve VM list."); + if responder.send(Err(status)).is_err() { + error!("VmDispatcher: Failed to send error response for ListVms."); + } + } + } +} + +pub(crate) async fn check_and_cleanup_vms( + repository: &VmRepository, + hypervisor: Arc, + event_bus_tx: mpsc::Sender, + healthcheck_cancel_bus: &broadcast::Sender, + vms: Vec, +) { + for vm in vms { + if let Some(pid) = vm.status.process_id { + let pid_obj = Pid::from_raw(pid as i32); + let process_exists = nix::sys::signal::kill(pid_obj, None).is_ok(); + + if process_exists { + info!("VmDispatcher (Sanity Check): Found running VM {} (PID: {}) from previous session. Starting health monitor.", vm.vm_id, pid); + let cancel_bus = healthcheck_cancel_bus.subscribe(); + worker::start_healthcheck_monitor( + vm.vm_id.to_string(), + hypervisor.clone(), + event_bus_tx.clone(), + cancel_bus, + ); + } else { + warn!("VmDispatcher (Sanity Check): Found VM {} in DB with PID {}, but process does not exist. Cleaning up.", vm.vm_id, pid); + let (resp_tx, resp_rx) = oneshot::channel(); + let req = DeleteVmRequest { + vm_id: vm.vm_id.to_string(), + }; + let vm_id_for_log = vm.vm_id; + + handle_delete_vm_command( + repository, + healthcheck_cancel_bus, + req, + resp_tx, + hypervisor.clone(), + event_bus_tx.clone(), + ) + .await; + + match resp_rx.await { + Ok(Ok(_)) => info!("VmDispatcher (Sanity Check): Successfully cleaned up zombie VM {vm_id_for_log}."), + Ok(Err(status)) => error!("VmDispatcher (Sanity Check): Failed to clean up zombie VM {vm_id_for_log}: {status}"), + Err(_) => error!("VmDispatcher (Sanity Check): Cleanup task for zombie VM {vm_id_for_log} did not return a response."), + } + } + } + } +} + +pub(crate) async fn perform_startup_sanity_check( + repository: &VmRepository, + hypervisor: Arc, + event_bus_tx: mpsc::Sender, + healthcheck_cancel_bus: &broadcast::Sender, +) { + info!("VmDispatcher: Running initial sanity check..."); + match repository.list_all_vms().await { + Ok(vms) => { + if vms.is_empty() { + info!("VmDispatcher (Sanity Check): No VMs found in persistence, check complete."); + } else { + info!( + "VmDispatcher (Sanity Check): Found {} VMs in persistence, checking status...", + vms.len() + ); + check_and_cleanup_vms( + repository, + hypervisor, + event_bus_tx, + healthcheck_cancel_bus, + vms, + ) + .await; + info!("VmDispatcher (Sanity Check): Check complete."); + } + } + Err(e) => { + error!("VmDispatcher (Sanity Check): Failed to list VMs from repository: {e}. Skipping check."); + } + } +} diff --git a/feos/services/vm-service/src/lib.rs b/feos/services/vm-service/src/lib.rs new file mode 100644 index 0000000..4cb12d3 --- /dev/null +++ b/feos/services/vm-service/src/lib.rs @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use feos_proto::vm_service::{ + AttachDiskRequest, AttachDiskResponse, CreateVmRequest, CreateVmResponse, DeleteVmRequest, + DeleteVmResponse, GetVmRequest, ListVmsRequest, ListVmsResponse, PauseVmRequest, + PauseVmResponse, PingVmRequest, PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, + ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, + StartVmResponse, StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest, + VmEvent, VmInfo, +}; +use tokio::sync::{mpsc, oneshot}; +use tonic::{Status, Streaming}; + +pub mod api; +pub mod dispatcher; +pub mod dispatcher_handlers; +pub mod persistence; +pub mod vmm; +pub mod worker; + +pub const DEFAULT_VM_DB_URL: &str = "sqlite:/var/lib/feos/vms.db"; +pub const VM_API_SOCKET_DIR: &str = "/tmp/feos/vm_api_sockets"; +pub const VM_CH_BIN: &str = "cloud-hypervisor"; +pub const IMAGE_DIR: &str = "/tmp/feos/images"; +pub const VM_CONSOLE_DIR: &str = "/tmp/feos/consoles"; + +#[derive(Debug, Clone)] +pub struct VmEventWrapper { + pub event: VmEvent, + pub process_id: Option, +} + +pub enum Command { + CreateVm( + CreateVmRequest, + oneshot::Sender>, + ), + StartVm( + StartVmRequest, + oneshot::Sender>, + ), + GetVm(GetVmRequest, oneshot::Sender>), + StreamVmEvents(StreamVmEventsRequest, mpsc::Sender>), + DeleteVm( + DeleteVmRequest, + oneshot::Sender>, + ), + StreamVmConsole( + Streaming, + mpsc::Sender>, + ), + ListVms( + ListVmsRequest, + oneshot::Sender>, + ), + PingVm( + PingVmRequest, + oneshot::Sender>, + ), + ShutdownVm( + ShutdownVmRequest, + oneshot::Sender>, + ), + PauseVm( + PauseVmRequest, + oneshot::Sender>, + ), + ResumeVm( + ResumeVmRequest, + oneshot::Sender>, + ), + AttachDisk( + AttachDiskRequest, + oneshot::Sender>, + ), + RemoveDisk( + RemoveDiskRequest, + oneshot::Sender>, + ), +} + +impl std::fmt::Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Command::CreateVm(req, _) => f.debug_tuple("CreateVm").field(req).finish(), + Command::StartVm(req, _) => f.debug_tuple("StartVm").field(req).finish(), + Command::GetVm(req, _) => f.debug_tuple("GetVm").field(req).finish(), + Command::StreamVmEvents(req, _) => f.debug_tuple("StreamVmEvents").field(req).finish(), + Command::DeleteVm(req, _) => f.debug_tuple("DeleteVm").field(req).finish(), + Command::StreamVmConsole(_, _) => { + f.write_str("StreamVmConsole(, )") + } + Command::ListVms(req, _) => f.debug_tuple("ListVms").field(req).finish(), + Command::PingVm(req, _) => f.debug_tuple("PingVm").field(req).finish(), + Command::ShutdownVm(req, _) => f.debug_tuple("ShutdownVm").field(req).finish(), + Command::PauseVm(req, _) => f.debug_tuple("PauseVm").field(req).finish(), + Command::ResumeVm(req, _) => f.debug_tuple("ResumeVm").field(req).finish(), + Command::AttachDisk(req, _) => f.debug_tuple("AttachDisk").field(req).finish(), + Command::RemoveDisk(req, _) => f.debug_tuple("RemoveDisk").field(req).finish(), + } + } +} diff --git a/feos/services/vm-service/src/persistence/mod.rs b/feos/services/vm-service/src/persistence/mod.rs new file mode 100644 index 0000000..91d5087 --- /dev/null +++ b/feos/services/vm-service/src/persistence/mod.rs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use feos_proto::vm_service::{VmConfig, VmState}; +use uuid::Uuid; + +pub mod repository; + +#[derive(Debug, Clone)] +pub struct VmStatus { + pub state: VmState, + pub last_msg: String, + pub process_id: Option, +} + +#[derive(Debug, Clone)] +pub struct VmRecord { + pub vm_id: Uuid, + pub image_uuid: Uuid, + pub status: VmStatus, + pub config: VmConfig, +} diff --git a/feos/services/vm-service/src/persistence/repository.rs b/feos/services/vm-service/src/persistence/repository.rs new file mode 100644 index 0000000..7a61aa0 --- /dev/null +++ b/feos/services/vm-service/src/persistence/repository.rs @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::persistence::{VmRecord, VmStatus}; +use anyhow::{anyhow, Result}; +use feos_proto::vm_service::{VmConfig, VmState}; +use log::info; +use prost::Message; +use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; +use uuid::Uuid; + +#[derive(Clone)] +pub struct VmRepository { + pool: SqlitePool, +} + +#[derive(sqlx::FromRow, Debug)] +struct DbVmRow { + vm_id: Uuid, + image_uuid: Uuid, + state: String, + last_msg: String, + pid: Option, + config_blob: Vec, +} + +fn string_to_vm_state(s: &str) -> Result { + match s { + "VM_STATE_CREATING" => Ok(VmState::Creating), + "VM_STATE_CREATED" => Ok(VmState::Created), + "VM_STATE_RUNNING" => Ok(VmState::Running), + "VM_STATE_PAUSED" => Ok(VmState::Paused), + "VM_STATE_STOPPED" => Ok(VmState::Stopped), + "VM_STATE_CRASHED" => Ok(VmState::Crashed), + "VM_STATE_UNSPECIFIED" => Ok(VmState::Unspecified), + _ => Err(anyhow!("Invalid state string '{}' in database", s)), + } +} + +impl VmRepository { + pub async fn connect(db_url: &str) -> Result { + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect(db_url) + .await?; + + info!("Persistence: Running database migrations..."); + sqlx::migrate!("./migrations").run(&pool).await?; + info!("Persistence: Database migrations completed."); + + Ok(Self { pool }) + } + + pub async fn get_vm(&self, vm_id: Uuid) -> Result> { + let row_opt = sqlx::query_as::<_, DbVmRow>( + "SELECT vm_id, image_uuid, state, last_msg, pid, config_blob FROM vms WHERE vm_id = ?1", + ) + .bind(vm_id) + .fetch_optional(&self.pool) + .await?; + + if let Some(row) = row_opt { + let config = VmConfig::decode(&*row.config_blob) + .map_err(|e| anyhow!("Failed to decode VmConfig blob: {}", e))?; + + let state = string_to_vm_state(&row.state)?; + + let record = VmRecord { + vm_id: row.vm_id, + image_uuid: row.image_uuid, + status: VmStatus { + state, + last_msg: row.last_msg, + process_id: row.pid, + }, + config, + }; + Ok(Some(record)) + } else { + Ok(None) + } + } + + pub async fn list_all_vms(&self) -> Result> { + let rows = sqlx::query_as::<_, DbVmRow>( + "SELECT vm_id, image_uuid, state, last_msg, pid, config_blob FROM vms", + ) + .fetch_all(&self.pool) + .await?; + + let mut records = Vec::with_capacity(rows.len()); + for row in rows { + let config = VmConfig::decode(&*row.config_blob).map_err(|e| { + anyhow!( + "Failed to decode VmConfig blob for vm_id {}: {}", + row.vm_id, + e + ) + })?; + + let state = string_to_vm_state(&row.state)?; + + let record = VmRecord { + vm_id: row.vm_id, + image_uuid: row.image_uuid, + status: VmStatus { + state, + last_msg: row.last_msg, + process_id: row.pid, + }, + config, + }; + records.push(record); + } + + Ok(records) + } + + pub async fn save_vm(&self, vm: &VmRecord) -> Result<()> { + let mut config_blob = Vec::new(); + vm.config.encode(&mut config_blob)?; + + let state_str = format!("VM_STATE_{:?}", vm.status.state).to_uppercase(); + + sqlx::query_unchecked!( + r#" + INSERT OR REPLACE INTO vms (vm_id, image_uuid, state, last_msg, pid, config_blob) + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + "#, + vm.vm_id, + vm.image_uuid, + state_str, + vm.status.last_msg, + vm.status.process_id, + config_blob, + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn update_vm_status( + &self, + vm_id: Uuid, + new_state: VmState, + message: &str, + ) -> Result { + let state_str = format!("VM_STATE_{new_state:?}").to_uppercase(); + + let result = sqlx::query!( + r#" + UPDATE vms + SET state = ?1, last_msg = ?2 + WHERE vm_id = ?3 + "#, + state_str, + message, + vm_id, + ) + .execute(&self.pool) + .await?; + + Ok(result.rows_affected() > 0) + } + + pub async fn update_vm_pid(&self, vm_id: Uuid, pid: i64) -> Result<()> { + sqlx::query!("UPDATE vms SET pid = ?1 WHERE vm_id = ?2", pid, vm_id) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn delete_vm(&self, vm_id: Uuid) -> Result<()> { + let result = sqlx::query!("DELETE FROM vms WHERE vm_id = ?1", vm_id) + .execute(&self.pool) + .await?; + + if result.rows_affected() == 0 { + log::warn!("Attempted to delete VM {vm_id} from DB, but no record was found."); + } + + Ok(()) + } +} diff --git a/feos/services/vm-service/src/vmm/ch_adapter.rs b/feos/services/vm-service/src/vmm/ch_adapter.rs new file mode 100644 index 0000000..b7b304b --- /dev/null +++ b/feos/services/vm-service/src/vmm/ch_adapter.rs @@ -0,0 +1,461 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use super::{Hypervisor, VmmError}; +use crate::{VmEventWrapper, IMAGE_DIR, VM_API_SOCKET_DIR, VM_CONSOLE_DIR}; +use cloud_hypervisor_client::{ + apis::{configuration::Configuration, DefaultApi, DefaultApiClient}, + models::{ + self, console_config::Mode as ConsoleMode, vm_info::State as ChVmState, + VmmPingResponse as ChPingResponse, + }, +}; +use feos_proto::vm_service::{ + net_config, AttachDiskRequest, AttachDiskResponse, CreateVmRequest, DeleteVmRequest, + DeleteVmResponse, GetVmRequest, PauseVmRequest, PauseVmResponse, PingVmRequest, PingVmResponse, + RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, + ShutdownVmResponse, StartVmRequest, StartVmResponse, VmConfig, VmInfo, VmState, +}; +use hyper_util::client::legacy::Client; +use hyperlocal::{UnixClientExt, UnixConnector, Uri as HyperlocalUri}; +use log::{error, info, warn}; +use nix::sys::signal::{kill, Signal}; +use nix::unistd::{self, Pid}; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::process::Command as TokioCommand; +use tokio::sync::{broadcast, mpsc}; +use tokio::time::{self, timeout, Duration}; +use uuid::Uuid; + +pub struct CloudHypervisorAdapter { + ch_binary_path: PathBuf, +} + +impl CloudHypervisorAdapter { + pub fn new(ch_binary_path: PathBuf) -> Self { + Self { ch_binary_path } + } + + fn get_ch_api_client(&self, vm_id: &str) -> Result, VmmError> { + let socket_path = PathBuf::from(VM_API_SOCKET_DIR).join(vm_id); + if !socket_path.exists() { + return Err(VmmError::VmNotFound(vm_id.to_string())); + } + + let uri: hyper::Uri = HyperlocalUri::new(socket_path, "/api/v1").into(); + let client = Client::unix(); + + let configuration = Configuration { + base_path: uri.to_string(), + client, + user_agent: Some("FeOS-vm-service/1.0".to_string()), + basic_auth: None, + oauth_access_token: None, + api_key: None, + }; + + Ok(DefaultApiClient::new(Arc::new(configuration))) + } + + async fn perform_vm_creation( + &self, + vm_id: &str, + config: VmConfig, + image_uuid: String, + api_socket_path: &Path, + ) -> Result<(), VmmError> { + let wait_for_socket = async { + while !api_socket_path.exists() { + tokio::time::sleep(Duration::from_millis(50)).await; + } + }; + + if timeout(Duration::from_secs(5), wait_for_socket) + .await + .is_err() + { + return Err(VmmError::ApiConnectionFailed( + "Timed out waiting for API socket".to_string(), + )); + } + info!("CloudHypervisorAdapter ({vm_id}): API socket is available."); + + let client = self.get_ch_api_client(vm_id)?; + tokio::fs::create_dir_all(VM_CONSOLE_DIR) + .await + .map_err(|e| VmmError::Internal(format!("Failed to create console dir: {e}")))?; + + let rootfs_path_str = format!("{IMAGE_DIR}/{image_uuid}/disk.image"); + let console_socket_path = format!("{VM_CONSOLE_DIR}/{vm_id}.console"); + + let mut ch_vm_config = models::VmConfig { + payload: models::PayloadConfig { + firmware: Some("/usr/share/cloud-hypervisor/hypervisor-fw".to_string()), + ..Default::default() + }, + disks: Some(vec![models::DiskConfig { + path: Some(rootfs_path_str), + ..Default::default() + }]), + serial: Some(models::ConsoleConfig { + socket: Some(console_socket_path), + mode: ConsoleMode::Socket, + ..Default::default() + }), + console: Some(models::ConsoleConfig { + mode: ConsoleMode::Off, + ..Default::default() + }), + ..Default::default() + }; + + if let Some(cpus) = config.cpus { + ch_vm_config.cpus = Some(models::CpusConfig { + boot_vcpus: cpus.boot_vcpus as i32, + max_vcpus: cpus.max_vcpus as i32, + ..Default::default() + }); + } + + if let Some(mem) = config.memory { + ch_vm_config.memory = Some(models::MemoryConfig { + size: mem.size_mib as i64 * 1024 * 1024, + shared: Some(true), + hugepages: Some(mem.hugepages), + ..Default::default() + }); + } + + let mut ch_net_configs: Vec = Vec::new(); + let mut ch_device_configs: Vec = Vec::new(); + + for nc in config.net { + if let Some(backend) = nc.backend { + match backend { + net_config::Backend::VfioPci(vfio_pci) => { + let device_path = format!("/sys/bus/pci/devices/{}", vfio_pci.bdf); + ch_device_configs.push(models::DeviceConfig { + path: device_path, + ..Default::default() + }); + } + net_config::Backend::Tap(tap) => { + let mac = if nc.mac_address.is_empty() { + None + } else { + Some(nc.mac_address) + }; + + ch_net_configs.push(models::NetConfig { + tap: Some(tap.tap_name), + mac, + ..Default::default() + }); + } + } + } + } + + if !ch_net_configs.is_empty() { + ch_vm_config.net = Some(ch_net_configs); + } + + if !ch_device_configs.is_empty() { + ch_vm_config.devices = Some(ch_device_configs); + } + + if let Some(ignition_data) = config.ignition { + if !ignition_data.is_empty() { + ch_vm_config.platform = Some(models::PlatformConfig { + oem_strings: Some(vec![ignition_data]), + ..Default::default() + }); + } + } + + client + .create_vm(ch_vm_config) + .await + .map_err(|e| VmmError::ApiOperationFailed(format!("vm.create API call failed: {e}")))?; + + info!("CloudHypervisorAdapter ({vm_id}): vm.create API call successful."); + + Ok::<(), VmmError>(()) + } + + async fn cleanup_socket_file(&self, vm_id: &str, socket_path: &Path, socket_type: &str) { + if let Err(e) = tokio::fs::remove_file(socket_path).await { + if e.kind() != std::io::ErrorKind::NotFound { + warn!( + "CloudHypervisorAdapter ({vm_id}): Failed to remove {socket_type} socket {path}: {e}", + path = socket_path.display() + ); + } + } else { + info!( + "CloudHypervisorAdapter ({vm_id}): Successfully removed {socket_type} socket {path}", + path = socket_path.display() + ); + } + } +} + +#[tonic::async_trait] +impl Hypervisor for CloudHypervisorAdapter { + async fn create_vm( + &self, + vm_id: &str, + req: CreateVmRequest, + image_uuid: String, + ) -> Result, VmmError> { + info!("CloudHypervisorAdapter: Creating VM with provided ID: {vm_id}"); + + let config = req + .config + .ok_or_else(|| VmmError::InvalidConfig("VmConfig is required".to_string()))?; + + let api_socket_path = PathBuf::from(VM_API_SOCKET_DIR).join(vm_id); + + info!("CloudHypervisorAdapter ({vm_id}): Spawning cloud-hypervisor process..."); + let mut child = unsafe { + TokioCommand::new(&self.ch_binary_path) + .arg("--api-socket") + .arg(&api_socket_path) + .pre_exec(|| unistd::setsid().map(|_pid| ()).map_err(io::Error::other)) + .spawn() + } + .map_err(|e| VmmError::ProcessSpawnFailed(e.to_string()))?; + let pid = child.id().map(|id| id as i64); + + let vm_creation = self.perform_vm_creation(vm_id, config, image_uuid, &api_socket_path); + + tokio::select! { + biased; + exit_status_res = child.wait() => { + let status = exit_status_res.map_err(|e| VmmError::ProcessSpawnFailed(format!("Failed to wait for child process: {e}")))?; + Err(VmmError::ProcessSpawnFailed(format!("Process exited prematurely with status: {status}"))) + } + creation_result = vm_creation => { + match creation_result { + Ok(_) => Ok(pid), + Err(e) => { + if let Err(kill_err) = child.kill().await { + warn!("CloudHypervisorAdapter ({vm_id}): Failed to kill child process after creation failure: {kill_err}"); + } + let _ = child.wait().await; + Err(e) + } + } + } + } + } + + async fn start_vm(&self, req: StartVmRequest) -> Result { + let api_client = self.get_ch_api_client(&req.vm_id)?; + api_client + .boot_vm() + .await + .map_err(|e| VmmError::ApiOperationFailed(e.to_string()))?; + + Ok(StartVmResponse {}) + } + + async fn healthcheck_vm( + &self, + vm_id: String, + broadcast_tx: mpsc::Sender, + mut cancel_bus: broadcast::Receiver, + ) { + info!("CloudHypervisorAdapter ({vm_id}): Starting healthcheck monitoring."); + let mut interval = time::interval(Duration::from_secs(10)); + let vm_id_uuid = match Uuid::parse_str(&vm_id) { + Ok(id) => id, + Err(e) => { + error!("CloudHypervisorAdapter ({vm_id}): Invalid UUID format, cannot start healthcheck: {e}"); + return; + } + }; + + loop { + tokio::select! { + _ = interval.tick() => { + log::debug!("CloudHypervisorAdapter ({vm_id}): Performing healthcheck ping."); + let req = PingVmRequest { + vm_id: vm_id.clone(), + }; + + if let Err(e) = self.ping_vm(req).await { + warn!("CloudHypervisorAdapter ({vm_id}): Healthcheck failed: {e}. VM is considered unhealthy."); + super::broadcast_state_change_event( + &broadcast_tx, + &vm_id, + "vm-health-monitor", + feos_proto::vm_service::VmStateChangedEvent { + new_state: VmState::Crashed as i32, + reason: format!("Healthcheck failed: {e}"), + }, + None, + ) + .await; + break; + } else { + log::debug!("CloudHypervisorAdapter ({vm_id}): Healthcheck ping successful."); + } + } + Ok(cancelled_vm_id) = cancel_bus.recv() => { + if cancelled_vm_id == vm_id_uuid { + info!("CloudHypervisorAdapter ({vm_id}): Received cancellation signal. Stopping healthcheck."); + break; + } + } + else => { + info!("CloudHypervisorAdapter ({vm_id}): Healthcheck cancellation channel closed. Stopping healthcheck."); + break; + } + } + } + info!("CloudHypervisorAdapter ({vm_id}): Stopping healthcheck monitoring."); + } + + async fn get_vm(&self, req: GetVmRequest) -> Result { + let api_client = self.get_ch_api_client(&req.vm_id)?; + let ch_info = api_client + .vm_info_get() + .await + .map_err(|e| VmmError::ApiOperationFailed(e.to_string()))?; + + let state = match ch_info.state { + ChVmState::Created => VmState::Created, + ChVmState::Running => VmState::Running, + ChVmState::Paused => VmState::Paused, + ChVmState::Shutdown => VmState::Stopped, + }; + + Ok(VmInfo { + vm_id: req.vm_id, + state: state as i32, + config: None, + }) + } + + async fn delete_vm( + &self, + req: DeleteVmRequest, + process_id: Option, + ) -> Result { + if let Ok(api_client) = self.get_ch_api_client(&req.vm_id) { + if let Err(e) = api_client.delete_vm().await { + warn!( + "CloudHypervisorAdapter ({vm_id}): API call to delete VM failed: {e}. This might happen if the process is already gone. Continuing cleanup.", + vm_id = req.vm_id + ); + } else { + info!( + "CloudHypervisorAdapter ({vm_id}): Successfully deleted hypervisor process via API.", + vm_id = req.vm_id + ); + } + } + + if let Some(pid_val) = process_id { + info!( + "CloudHypervisorAdapter ({vm_id}): Attempting to kill process with PID: {pid_val}", + vm_id = req.vm_id + ); + let pid = Pid::from_raw(pid_val as i32); + match kill(pid, Signal::SIGKILL) { + Ok(_) => info!( + "CloudHypervisorAdapter ({vm_id}): Successfully sent SIGKILL to process {pid_val}.", + vm_id = req.vm_id + ), + Err(nix::Error::ESRCH) => info!( + "CloudHypervisorAdapter ({vm_id}): Process {pid_val} already exited.", + vm_id = req.vm_id + ), + Err(e) => warn!( + "CloudHypervisorAdapter ({vm_id}): Failed to kill process {pid_val}: {e}. It might already be gone.", + vm_id = req.vm_id + ), + } + } + + let api_socket_path = PathBuf::from(VM_API_SOCKET_DIR).join(&req.vm_id); + self.cleanup_socket_file(&req.vm_id, &api_socket_path, "API") + .await; + + let console_socket_path = + PathBuf::from(VM_CONSOLE_DIR).join(format!("{}.console", req.vm_id)); + self.cleanup_socket_file(&req.vm_id, &console_socket_path, "console") + .await; + + Ok(DeleteVmResponse {}) + } + + async fn get_console_socket_path(&self, vm_id: &str) -> Result { + let socket_path = PathBuf::from(VM_CONSOLE_DIR).join(format!("{vm_id}.console")); + if tokio::fs::try_exists(&socket_path) + .await + .map_err(|e| VmmError::Internal(e.to_string()))? + { + Ok(socket_path) + } else { + Err(VmmError::VmNotFound(vm_id.to_string())) + } + } + + async fn ping_vm(&self, req: PingVmRequest) -> Result { + let api_client = self.get_ch_api_client(&req.vm_id)?; + let ch_ping: ChPingResponse = api_client + .vmm_ping_get() + .await + .map_err(|e| VmmError::ApiOperationFailed(e.to_string()))?; + + Ok(PingVmResponse { + build_version: ch_ping.build_version.unwrap_or_default(), + version: ch_ping.version, + pid: ch_ping.pid.unwrap_or(0), + features: ch_ping.features.unwrap_or_default(), + }) + } + + async fn shutdown_vm(&self, req: ShutdownVmRequest) -> Result { + let api_client = self.get_ch_api_client(&req.vm_id)?; + api_client + .shutdown_vm() + .await + .map_err(|e| VmmError::ApiOperationFailed(e.to_string()))?; + Ok(ShutdownVmResponse {}) + } + + async fn pause_vm(&self, req: PauseVmRequest) -> Result { + let api_client = self.get_ch_api_client(&req.vm_id)?; + api_client + .pause_vm() + .await + .map_err(|e| VmmError::ApiOperationFailed(e.to_string()))?; + Ok(PauseVmResponse {}) + } + + async fn resume_vm(&self, req: ResumeVmRequest) -> Result { + let api_client = self.get_ch_api_client(&req.vm_id)?; + api_client + .resume_vm() + .await + .map_err(|e| VmmError::ApiOperationFailed(e.to_string()))?; + Ok(ResumeVmResponse {}) + } + + async fn attach_disk(&self, _req: AttachDiskRequest) -> Result { + Err(VmmError::Internal( + "AttachDisk not implemented for CloudHypervisorAdapter".to_string(), + )) + } + + async fn remove_disk(&self, _req: RemoveDiskRequest) -> Result { + Err(VmmError::Internal( + "RemoveDisk not implemented for CloudHypervisorAdapter".to_string(), + )) + } +} diff --git a/feos/services/vm-service/src/vmm/mod.rs b/feos/services/vm-service/src/vmm/mod.rs new file mode 100644 index 0000000..d15ca2b --- /dev/null +++ b/feos/services/vm-service/src/vmm/mod.rs @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::VmEventWrapper; +use feos_proto::vm_service::{ + AttachDiskRequest, AttachDiskResponse, CreateVmRequest, DeleteVmRequest, DeleteVmResponse, + GetVmRequest, PauseVmRequest, PauseVmResponse, PingVmRequest, PingVmResponse, + RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, + ShutdownVmResponse, StartVmRequest, StartVmResponse, VmEvent, VmInfo, VmStateChangedEvent, +}; +use prost::Message; +use prost_types::Any; +use std::path::{Path, PathBuf}; +use tokio::sync::{broadcast, mpsc}; +use tonic::Status; +use uuid::Uuid; + +pub mod ch_adapter; + +#[derive(Debug, thiserror::Error)] +pub enum VmmError { + #[error("Hypervisor process failed to start: {0}")] + ProcessSpawnFailed(String), + + #[error("The provided configuration is invalid for this hypervisor: {0}")] + InvalidConfig(String), + + #[error("Could not connect to the hypervisor's API socket: {0}")] + ApiConnectionFailed(String), + + #[error("The hypervisor's API returned an error: {0}")] + ApiOperationFailed(String), + + #[error("The requested VM (id: {0}) could not be found")] + VmNotFound(String), + + #[error("The image service returned an error: {0}")] + ImageServiceFailed(String), + + #[error("An internal or unexpected error occurred: {0}")] + Internal(String), +} + +impl From for Status { + fn from(err: VmmError) -> Self { + match err { + VmmError::VmNotFound(id) => Status::not_found(id), + VmmError::InvalidConfig(msg) => Status::invalid_argument(msg), + VmmError::ApiConnectionFailed(msg) | VmmError::ImageServiceFailed(msg) => { + Status::unavailable(msg) + } + VmmError::ProcessSpawnFailed(msg) + | VmmError::ApiOperationFailed(msg) + | VmmError::Internal(msg) => Status::internal(msg), + } + } +} + +#[tonic::async_trait] +pub trait Hypervisor: Send + Sync { + async fn create_vm( + &self, + vm_id: &str, + req: CreateVmRequest, + image_uuid: String, + ) -> Result, VmmError>; + + async fn start_vm(&self, req: StartVmRequest) -> Result; + + async fn healthcheck_vm( + &self, + vm_id: String, + broadcast_tx: mpsc::Sender, + cancel_bus: broadcast::Receiver, + ); + + async fn get_vm(&self, req: GetVmRequest) -> Result; + + async fn delete_vm( + &self, + req: DeleteVmRequest, + process_id: Option, + ) -> Result; + + async fn get_console_socket_path(&self, vm_id: &str) -> Result; + + async fn ping_vm(&self, req: PingVmRequest) -> Result; + async fn shutdown_vm(&self, req: ShutdownVmRequest) -> Result; + async fn pause_vm(&self, req: PauseVmRequest) -> Result; + async fn resume_vm(&self, req: ResumeVmRequest) -> Result; + async fn attach_disk(&self, req: AttachDiskRequest) -> Result; + async fn remove_disk(&self, req: RemoveDiskRequest) -> Result; +} + +pub async fn broadcast_state_change_event( + broadcast_tx: &mpsc::Sender, + vm_id: &str, + component: &str, + data: VmStateChangedEvent, + process_id: Option, +) { + let event = VmEvent { + vm_id: vm_id.to_string(), + id: Uuid::new_v4().to_string(), + component_id: component.to_string(), + data: Some(Any { + type_url: "type.googleapis.com/feos.vm.vmm.api.v1.VmStateChangedEvent".to_string(), + value: data.encode_to_vec(), + }), + }; + + if broadcast_tx + .send(VmEventWrapper { event, process_id }) + .await + .is_err() + { + log::warn!("Failed to broadcast event for VM '{vm_id}': channel closed."); + } +} + +pub enum VmmType { + CloudHypervisor, +} + +pub fn factory(vmm_type: VmmType) -> Box { + match vmm_type { + VmmType::CloudHypervisor => { + let ch_binary_path = Path::new(super::VM_CH_BIN).to_path_buf(); + Box::new(ch_adapter::CloudHypervisorAdapter::new(ch_binary_path)) + } + } +} diff --git a/feos/services/vm-service/src/worker.rs b/feos/services/vm-service/src/worker.rs new file mode 100644 index 0000000..dd883a7 --- /dev/null +++ b/feos/services/vm-service/src/worker.rs @@ -0,0 +1,552 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + dispatcher_handlers::get_image_service_client, vmm::Hypervisor, vmm::VmmError, VmEventWrapper, +}; +use feos_proto::{ + image_service::{ImageState as OciImageState, WatchImageStatusRequest}, + vm_service::{ + stream_vm_console_request as console_input, AttachConsoleMessage, AttachDiskRequest, + AttachDiskResponse, ConsoleData, CreateVmRequest, CreateVmResponse, DeleteVmRequest, + DeleteVmResponse, GetVmRequest, PauseVmRequest, PauseVmResponse, PingVmRequest, + PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, + ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, + StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, + VmState, VmStateChangedEvent, + }, +}; +use log::{error, info, warn}; +use std::{path::PathBuf, sync::Arc}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::UnixStream, + sync::{broadcast, mpsc, oneshot}, +}; +use tokio_stream::StreamExt; +use tonic::{Status, Streaming}; +use uuid::Uuid; + +async fn wait_for_image_ready(image_uuid: &str, image_ref: &str) -> Result<(), VmmError> { + let mut client = get_image_service_client().await.map_err(|e| { + VmmError::ImageServiceFailed(format!("Failed to connect to ImageService: {e}")) + })?; + + let mut stream = client + .watch_image_status(WatchImageStatusRequest { + image_uuid: image_uuid.to_string(), + }) + .await + .map_err(|e| { + VmmError::ImageServiceFailed(format!( + "WatchImageStatus RPC failed for {image_uuid}: {e}" + )) + })? + .into_inner(); + + while let Some(status_res) = stream.next().await { + let status = status_res.map_err(|e| { + VmmError::ImageServiceFailed(format!("Image stream error for {image_uuid}: {e}")) + })?; + let state = OciImageState::try_from(status.state).unwrap_or(OciImageState::Unspecified); + match state { + OciImageState::Ready => return Ok(()), + OciImageState::PullFailed => { + return Err(VmmError::ImageServiceFailed(format!( + "Image pull failed for {image_ref} (uuid: {image_uuid}): {}", + status.message + ))) + } + _ => continue, + } + } + Err(VmmError::ImageServiceFailed(format!( + "Image watch stream for {image_uuid} ended before reaching a terminal state." + ))) +} + +pub async fn handle_create_vm( + vm_id: String, + req: CreateVmRequest, + image_uuid: String, + responder: oneshot::Sender>, + hypervisor: Arc, + broadcast_tx: mpsc::Sender, +) { + if responder + .send(Ok(CreateVmResponse { + vm_id: vm_id.clone(), + })) + .is_err() + { + error!("VmWorker ({vm_id}): Client disconnected before immediate response could be sent. Aborting creation."); + return; + } + + info!("VmWorker ({vm_id}): Starting creation process."); + crate::vmm::broadcast_state_change_event( + &broadcast_tx, + &vm_id, + "vm-service", + VmStateChangedEvent { + new_state: VmState::Creating as i32, + reason: "VM creation process started".to_string(), + }, + None, + ) + .await; + + let image_ref = req + .config + .as_ref() + .map(|c| c.image_ref.clone()) + .unwrap_or_default(); + + info!( + "VmWorker ({vm_id}): Waiting for image '{image_ref}' (uuid: {image_uuid}) to be ready..." + ); + if let Err(e) = wait_for_image_ready(&image_uuid, &image_ref).await { + let error_msg = e.to_string(); + error!("VmWorker ({vm_id}): {error_msg}"); + crate::vmm::broadcast_state_change_event( + &broadcast_tx, + &vm_id, + "vm-service", + VmStateChangedEvent { + new_state: VmState::Crashed as i32, + reason: error_msg, + }, + None, + ) + .await; + return; + } + info!("VmWorker ({vm_id}): Image '{image_ref}' (uuid: {image_uuid}) is ready."); + + let result = hypervisor.create_vm(&vm_id, req, image_uuid).await; + + match result { + Ok(pid) => { + info!("VmWorker ({vm_id}): Background creation process completed successfully."); + crate::vmm::broadcast_state_change_event( + &broadcast_tx, + &vm_id, + "vm-service", + VmStateChangedEvent { + new_state: VmState::Created as i32, + reason: "Hypervisor process started and VM configured".to_string(), + }, + pid, + ) + .await; + } + Err(e) => { + let error_msg = e.to_string(); + error!("VmWorker ({vm_id}): Background creation process failed: {error_msg}"); + crate::vmm::broadcast_state_change_event( + &broadcast_tx, + &vm_id, + "vm-service", + VmStateChangedEvent { + new_state: VmState::Crashed as i32, + reason: error_msg, + }, + None, + ) + .await; + } + } +} + +pub fn start_healthcheck_monitor( + vm_id: String, + hypervisor: Arc, + broadcast_tx: mpsc::Sender, + cancel_bus: broadcast::Receiver, +) { + let health_hypervisor = hypervisor; + let health_broadcast_tx = broadcast_tx; + tokio::spawn(async move { + health_hypervisor + .healthcheck_vm(vm_id, health_broadcast_tx, cancel_bus) + .await; + }); +} + +pub async fn handle_start_vm( + req: StartVmRequest, + responder: oneshot::Sender>, + hypervisor: Arc, + broadcast_tx: mpsc::Sender, + cancel_bus: broadcast::Receiver, +) { + let vm_id = req.vm_id.clone(); + let result = hypervisor.start_vm(req).await; + + if result.is_ok() { + crate::vmm::broadcast_state_change_event( + &broadcast_tx, + &vm_id, + "vm-service", + VmStateChangedEvent { + new_state: VmState::Running as i32, + reason: "Start command successful".to_string(), + }, + None, + ) + .await; + + start_healthcheck_monitor(vm_id, hypervisor, broadcast_tx, cancel_bus); + } + + if responder.send(result.map_err(Into::into)).is_err() { + error!("VmWorker: Failed to send response for StartVm."); + } +} + +pub async fn handle_get_vm( + req: GetVmRequest, + responder: oneshot::Sender>, + hypervisor: Arc, +) { + let result = hypervisor.get_vm(req).await; + if responder.send(result.map_err(Into::into)).is_err() { + error!("VmWorker: Failed to send response for GetVm."); + } +} + +pub async fn handle_stream_vm_events( + req: StreamVmEventsRequest, + stream_tx: mpsc::Sender>, + broadcast_tx: broadcast::Sender, +) { + let mut broadcast_rx = broadcast_tx.subscribe(); + let vm_id_to_watch = req.vm_id; + + let watcher_desc = vm_id_to_watch + .clone() + .unwrap_or_else(|| "all VMs".to_string()); + + loop { + match broadcast_rx.recv().await { + Ok(VmEventWrapper { event, .. }) => { + if vm_id_to_watch.as_ref().is_none_or(|id| event.vm_id == *id) + && stream_tx.send(Ok(event)).await.is_err() + { + info!("VmWorker (Stream): Client for '{watcher_desc}' disconnected."); + break; + } + } + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!( + "VmWorker (Stream): Event stream for '{watcher_desc}' lagged by {n} messages." + ); + } + Err(broadcast::error::RecvError::Closed) => { + info!( + "VmWorker (Stream): Broadcast channel closed. Shutting down stream for '{watcher_desc}'." + ); + break; + } + } + } +} + +pub async fn handle_delete_vm( + req: DeleteVmRequest, + image_uuid: String, + process_id: Option, + responder: oneshot::Sender>, + hypervisor: Arc, + _broadcast_tx: mpsc::Sender, +) { + let vm_id = req.vm_id.clone(); + let result = hypervisor.delete_vm(req, process_id).await; + + if !image_uuid.is_empty() { + info!("VmWorker ({vm_id}): Attempting to delete associated image with UUID: {image_uuid}"); + match get_image_service_client().await { + Ok(mut client) => { + let delete_req = feos_proto::image_service::DeleteImageRequest { + image_uuid: image_uuid.clone(), + }; + if let Err(status) = client.delete_image(delete_req).await { + warn!( + "VmWorker ({vm_id}): Failed to delete image {image_uuid}: {message}. This may be expected if the image is shared or already deleted.", + message = status.message() + ); + } else { + info!( + "VmWorker ({vm_id}): Successfully requested deletion of image {image_uuid}" + ); + } + } + Err(e) => { + warn!("VmWorker ({vm_id}): Could not connect to ImageService to delete image {image_uuid}: {e}"); + } + } + } else { + info!("VmWorker ({vm_id}): No image UUID provided, skipping image deletion."); + } + + if responder.send(result.map_err(Into::into)).is_err() { + error!("VmWorker: Failed to send response for DeleteVm."); + } +} + +pub async fn handle_stream_vm_console( + mut input_stream: Streaming, + output_tx: mpsc::Sender>, + hypervisor: Arc, +) { + let vm_id = match get_attach_message(&mut input_stream).await { + Ok(id) => id, + Err(status) => { + let _ = output_tx.send(Err(status)).await; + return; + } + }; + + let socket_path = match hypervisor.get_console_socket_path(&vm_id).await { + Ok(path) => path, + Err(e) => { + let _ = output_tx.send(Err(e.into())).await; + return; + } + }; + + bridge_console_streams(socket_path, input_stream, output_tx).await; +} + +pub async fn handle_ping_vm( + req: PingVmRequest, + responder: oneshot::Sender>, + hypervisor: Arc, +) { + let result = hypervisor.ping_vm(req).await; + if responder.send(result.map_err(Into::into)).is_err() { + error!("VmWorker: Failed to send response for PingVm."); + } +} + +pub async fn handle_shutdown_vm( + req: ShutdownVmRequest, + responder: oneshot::Sender>, + hypervisor: Arc, + broadcast_tx: mpsc::Sender, +) { + let vm_id = req.vm_id.clone(); + let result = hypervisor.shutdown_vm(req).await; + + if result.is_ok() { + crate::vmm::broadcast_state_change_event( + &broadcast_tx, + &vm_id, + "vm-service", + VmStateChangedEvent { + new_state: VmState::Stopped as i32, + reason: "Shutdown command successful".to_string(), + }, + None, + ) + .await; + } + + if responder.send(result.map_err(Into::into)).is_err() { + error!("VmWorker: Failed to send response for ShutdownVm."); + } +} + +pub async fn handle_pause_vm( + req: PauseVmRequest, + responder: oneshot::Sender>, + hypervisor: Arc, + broadcast_tx: mpsc::Sender, +) { + let vm_id = req.vm_id.clone(); + let result = hypervisor.pause_vm(req).await; + + if result.is_ok() { + crate::vmm::broadcast_state_change_event( + &broadcast_tx, + &vm_id, + "vm-service", + VmStateChangedEvent { + new_state: VmState::Paused as i32, + reason: "Pause command successful".to_string(), + }, + None, + ) + .await; + } + + if responder.send(result.map_err(Into::into)).is_err() { + error!("VmWorker: Failed to send response for PauseVm."); + } +} + +pub async fn handle_resume_vm( + req: ResumeVmRequest, + responder: oneshot::Sender>, + hypervisor: Arc, + broadcast_tx: mpsc::Sender, +) { + let vm_id = req.vm_id.clone(); + let result = hypervisor.resume_vm(req).await; + + if result.is_ok() { + crate::vmm::broadcast_state_change_event( + &broadcast_tx, + &vm_id, + "vm-service", + VmStateChangedEvent { + new_state: VmState::Running as i32, + reason: "Resume command successful".to_string(), + }, + None, + ) + .await; + } + + if responder.send(result.map_err(Into::into)).is_err() { + error!("VmWorker: Failed to send response for ResumeVm."); + } +} + +pub async fn handle_attach_disk( + req: AttachDiskRequest, + responder: oneshot::Sender>, + hypervisor: Arc, +) { + let result = hypervisor.attach_disk(req).await; + if responder.send(result.map_err(Into::into)).is_err() { + error!("VmWorker: Failed to send response for AttachDisk."); + } +} + +pub async fn handle_remove_disk( + req: RemoveDiskRequest, + responder: oneshot::Sender>, + hypervisor: Arc, +) { + let result = hypervisor.remove_disk(req).await; + if responder.send(result.map_err(Into::into)).is_err() { + error!("VmWorker: Failed to send response for RemoveDisk."); + } +} + +async fn get_attach_message( + stream: &mut Streaming, +) -> Result { + match stream.next().await { + Some(Ok(msg)) => match msg.payload { + Some(console_input::Payload::Attach(AttachConsoleMessage { vm_id })) => Ok(vm_id), + _ => Err(Status::invalid_argument( + "First message must be an Attach message.", + )), + }, + Some(Err(e)) => Err(e), + None => Err(Status::invalid_argument( + "Client disconnected before sending Attach message.", + )), + } +} + +async fn bridge_console_streams( + socket_path: PathBuf, + mut grpc_input: Streaming, + grpc_output: mpsc::Sender>, +) { + let vm_id = socket_path + .file_stem() + .unwrap() + .to_str() + .unwrap_or("unknown") + .to_string(); + + let socket = match UnixStream::connect(&socket_path).await { + Ok(s) => s, + Err(e) => { + let err_msg = format!("Failed to connect to console socket at {socket_path:?}: {e}"); + let _ = grpc_output.send(Err(Status::unavailable(err_msg))).await; + return; + } + }; + + let (mut socket_reader, mut socket_writer) = tokio::io::split(socket); + let grpc_output_clone = grpc_output.clone(); + let read_task_vm_id = vm_id.clone(); + + let read_task = tokio::spawn(async move { + let mut buf = vec![0; 4096]; + loop { + tokio::select! { + biased; + _ = grpc_output_clone.closed() => { + info!("VmmHelper (Console {}): gRPC client disconnected, terminating read task.", &read_task_vm_id); + break; + } + read_result = socket_reader.read(&mut buf) => { + match read_result { + Ok(0) => { + info!("VmmHelper (Console {}): Console socket closed (EOF).", &read_task_vm_id); + break; + } + Ok(n) => { + let output_msg = StreamVmConsoleResponse { output: buf[..n].to_vec() }; + if grpc_output_clone.send(Ok(output_msg)).await.is_err() { + } + } + Err(e) => { + let err_msg = format!("Error reading from console socket: {e}"); + let _ = grpc_output_clone.send(Err(Status::internal(err_msg))).await; + break; + } + } + } + } + } + }); + + let write_task_vm_id = vm_id.clone(); + let write_task = tokio::spawn(async move { + while let Some(result) = grpc_input.next().await { + match result { + Ok(msg) => match msg.payload { + Some(console_input::Payload::Data(ConsoleData { input })) => { + if let Err(e) = socket_writer.write_all(&input).await { + warn!("VmmHelper (Console {}): Failed to write to socket: {}. VM may have shut down.", &write_task_vm_id, e); + break; + } + } + Some(console_input::Payload::Attach(_)) => { + let _ = grpc_output + .send(Err(Status::invalid_argument( + "Cannot send Attach message more than once.", + ))) + .await; + break; + } + None => { + let _ = grpc_output + .send(Err(Status::invalid_argument("Empty ConsoleInput payload."))) + .await; + break; + } + }, + Err(e) => { + warn!( + "VmmHelper (Console {}): Error reading from gRPC client stream: {}", + &write_task_vm_id, e + ); + break; + } + } + } + }); + + tokio::select! { + _ = read_task => {}, + _ = write_task => {}, + } +} diff --git a/feos/src/lib.rs b/feos/src/lib.rs new file mode 100644 index 0000000..cb6f625 --- /dev/null +++ b/feos/src/lib.rs @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +mod setup; + +use anyhow::Result; +use host_service::RestartSignal; +use image_service::IMAGE_SERVICE_SOCKET; +use log::{error, info, warn}; +use nix::unistd::Uid; +use setup::*; +use std::env; +use tokio::{fs, net::UnixListener, sync::mpsc}; +use tokio_stream::wrappers::UnixListenerStream; +use tonic::transport::Server; + +pub async fn run_server(restarted_after_upgrade: bool) -> Result<()> { + println!( + " + ███████╗███████╗ ██████╗ ███████╗ + ██╔════╝██╔════╝██╔═══██╗██╔════╝ + █████╗ █████╗ ██║ ██║███████╗ + ██╔══╝ ██╔══╝ ██║ ██║╚════██║ + ██║ ███████╗╚██████╔╝███████║ + ╚═╝ ╚══════╝ ╚═════╝ ╚══════╝ + v{} + ", + env!("CARGO_PKG_VERSION") + ); + + let log_handle = feos_utils::feos_logger::Builder::new() + .filter_level(log::LevelFilter::Info) + .max_history(150) + .init() + .expect("Failed to initialize feos_logger"); + + if !Uid::current().is_root() { + warn!("Not running as root! (uid: {})", Uid::current()); + } + + if !restarted_after_upgrade { + if std::process::id() == 1 { + perform_first_boot_initialization().await?; + } + } else { + info!("Main: Skipping one-time initialization on restart after upgrade."); + } + + let db_url = setup_database().await?; + + let (restart_tx, mut restart_rx) = mpsc::channel::(1); + + let vm_service = initialize_vm_service(&db_url).await?; + + let host_service = initialize_host_service(restart_tx.clone(), log_handle); + + let image_service = initialize_image_service().await?; + + let tcp_addr = "[::]:1337".parse().unwrap(); + let tcp_server = Server::builder() + .add_service(vm_service) + .add_service(host_service) + .serve(tcp_addr); + + fs::remove_file(IMAGE_SERVICE_SOCKET).await.ok(); + let uds = UnixListener::bind(IMAGE_SERVICE_SOCKET)?; + let uds_stream = UnixListenerStream::new(uds); + let unix_socket_server = Server::builder() + .add_service(image_service) + .serve_with_incoming(uds_stream); + + info!("Main: Public gRPC Server listening on {tcp_addr}"); + info!("Main: Internal ImageService listening on Unix socket {IMAGE_SERVICE_SOCKET}"); + + tokio::select! { + res = tcp_server => { + if let Err(e) = res { + error!("TCP server failed: {e}"); + } + }, + res = unix_socket_server => { + if let Err(e) = res { + error!("Unix socket server failed: {e}"); + } + }, + Some(RestartSignal(new_binary_path)) = restart_rx.recv() => { + if let Err(e) = handle_upgrade(&new_binary_path) { + error!("Upgrade failed: {e}"); + } + } + }; + + Ok(()) +} diff --git a/feos/src/main.rs b/feos/src/main.rs new file mode 100644 index 0000000..3d9a38e --- /dev/null +++ b/feos/src/main.rs @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Result; +use clap::Parser; +use feos_utils::filesystem::{get_root_fstype, move_root}; +use main_server::run_server; +use nix::unistd::execv; +use std::env; +use std::ffi::CString; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct ServerArgs { + #[arg(long, hide = true)] + restarted_after_upgrade: bool, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = ServerArgs::parse(); + + if std::process::id() == 1 { + let root_fstype = get_root_fstype().unwrap_or_else(|e| { + eprintln!("[feos] Failed to get root fstype: {e}"); + String::new() + }); + + if root_fstype == "rootfs" { + move_root().map_err(|e| anyhow::anyhow!("[feos] move_root failed: {}", e))?; + + let argv: Vec = env::args() + .map(|arg| CString::new(arg).unwrap_or_default()) + .collect(); + let _ = execv(&argv[0], &argv) + .map_err(|e| anyhow::anyhow!("[feos] execv failed: {}", e))?; + + return Err(anyhow::anyhow!("execv failed to replace process")); + } + } + + run_server(args.restarted_after_upgrade).await +} diff --git a/feos/src/setup.rs b/feos/src/setup.rs new file mode 100644 index 0000000..60e3a30 --- /dev/null +++ b/feos/src/setup.rs @@ -0,0 +1,247 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Result; +use feos_proto::{ + host_service::host_service_server::HostServiceServer, + image_service::image_service_server::ImageServiceServer, + vm_service::vm_service_server::VmServiceServer, +}; +use feos_utils::filesystem::mount_virtual_filesystems; +use feos_utils::host::info::is_running_on_vm; +use feos_utils::host::memory::configure_hugepages; +use feos_utils::network::{configure_network_devices, configure_sriov}; +use host_service::{ + api::HostApiHandler, dispatcher::HostServiceDispatcher, Command as HostCommand, RestartSignal, +}; +use image_service::{ + api::ImageApiHandler, dispatcher::ImageServiceDispatcher, filestore::FileStore, + worker::Orchestrator, IMAGE_DIR, +}; +use log::{error, info, warn}; +use nix::libc; +use std::env; +use std::ffi::CString; +use std::os::unix::ffi::OsStringExt; +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +use tokio::fs::{self, File}; +use tokio::sync::mpsc; +use vm_service::{ + api::VmApiHandler, dispatcher::VmServiceDispatcher, Command as VmCommand, DEFAULT_VM_DB_URL, + VM_API_SOCKET_DIR, VM_CONSOLE_DIR, +}; + +pub(crate) const VFS_NUM: u32 = 125; +pub(crate) const HUGEPAGES_NUM: u32 = 1024; + +pub(crate) async fn initialize_vm_service(db_url: &str) -> Result> { + info!("Main: Ensuring VM socket directory '{VM_API_SOCKET_DIR}' exists..."); + fs::create_dir_all(VM_API_SOCKET_DIR).await?; + info!("Main: Directory check complete. Path '{VM_API_SOCKET_DIR}' is ready."); + + info!("Main: Ensuring VM console directory '{VM_CONSOLE_DIR}' exists..."); + fs::create_dir_all(VM_CONSOLE_DIR).await?; + info!("Main: Directory check complete. Path '{VM_CONSOLE_DIR}' is ready."); + + let (vm_tx, vm_rx) = mpsc::channel::(32); + let vm_dispatcher = VmServiceDispatcher::new(vm_rx, db_url).await?; + tokio::spawn(async move { + vm_dispatcher.run().await; + }); + let vm_api_handler = VmApiHandler::new(vm_tx); + let vm_service = VmServiceServer::new(vm_api_handler); + info!("Main: VM Service is configured."); + + Ok(vm_service) +} + +pub(crate) fn initialize_host_service( + restart_tx: mpsc::Sender, + log_handle: feos_utils::feos_logger::LogHandle, +) -> HostServiceServer { + let (host_tx, host_rx) = mpsc::channel::(32); + let host_dispatcher = HostServiceDispatcher::new(host_rx, restart_tx, log_handle); + tokio::spawn(async move { + host_dispatcher.run().await; + }); + let host_api_handler = HostApiHandler::new(host_tx); + let host_service = HostServiceServer::new(host_api_handler); + info!("Main: Host Service is configured."); + + host_service +} + +pub(crate) async fn initialize_image_service() -> Result> { + info!("Main: Ensuring image directory '{IMAGE_DIR}' exists..."); + fs::create_dir_all(IMAGE_DIR).await?; + info!("Main: Directory check complete. Path '{IMAGE_DIR}' is ready."); + + let filestore_actor = FileStore::new(); + let filestore_tx = filestore_actor.get_command_sender(); + tokio::spawn(async move { + filestore_actor.run().await; + }); + info!("Main: FileStore actor for Image Service has been started."); + + let orchestrator_actor = Orchestrator::new(filestore_tx); + let orchestrator_tx = orchestrator_actor.get_command_sender(); + tokio::spawn(async move { + orchestrator_actor.run().await; + }); + info!("Main: Orchestrator actor for Image Service has been started."); + + let grpc_dispatcher = ImageServiceDispatcher::new(orchestrator_tx); + let grpc_dispatcher_tx = grpc_dispatcher.get_command_sender(); + tokio::spawn(async move { + grpc_dispatcher.run().await; + }); + info!("Main: gRPC Dispatcher for Image Service has been started."); + + let image_api_handler = ImageApiHandler::new(grpc_dispatcher_tx); + let image_service = ImageServiceServer::new(image_api_handler); + info!("Main: Image Service is configured."); + + Ok(image_service) +} + +pub(crate) async fn perform_first_boot_initialization() -> Result<()> { + info!("Main: Performing first-boot initialization..."); + info!("Main: Mounting virtual filesystems..."); + mount_virtual_filesystems(); + + info!("Main: Configuring hugepages..."); + if let Err(e) = configure_hugepages(HUGEPAGES_NUM).await { + warn!("Failed to configure hugepages: {e}"); + } + + let is_on_vm = is_running_on_vm().await.unwrap_or_else(|e| { + error!("Error checking VM status: {e}"); + false // Default to false in case of error + }); + + info!("Main: Configuring network devices..."); + if let Some((delegated_prefix, delegated_prefix_length)) = configure_network_devices() + .await + .expect("could not configure network devices") + { + info!("Main: Delegated prefix: {delegated_prefix}/{delegated_prefix_length}"); + } + + if !is_on_vm { + info!("configuring sriov..."); + if let Err(e) = configure_sriov(VFS_NUM).await { + warn!("failed to configure sriov: {e}") + } + } + + Ok(()) +} + +pub(crate) async fn setup_database() -> Result { + dotenvy::dotenv().ok(); + + let db_url = env::var("DATABASE_URL").unwrap_or_else(|_| { + info!("Main: DATABASE_URL not set, using default '{DEFAULT_VM_DB_URL}'"); + DEFAULT_VM_DB_URL.to_string() + }); + + if let Some(db_path_str) = db_url.strip_prefix("sqlite:") { + let db_path = Path::new(db_path_str); + if let Some(db_dir) = db_path.parent() { + info!( + "Main: Ensuring database directory '{}' exists...", + db_dir.display() + ); + fs::create_dir_all(db_dir).await?; + } + if !db_path.exists() { + info!( + "Main: Database file does not exist, creating at '{}'...", + db_path.display() + ); + File::create(db_path).await?; + } + } + + Ok(db_url) +} + +pub(crate) fn handle_upgrade(new_binary_path: &Path) -> Result<()> { + info!("Main: Upgrade signal received. New binary at {new_binary_path:?}. Preparing to execv."); + + let current_exe = match std::env::current_exe() { + Ok(path) => path, + Err(e) => { + // Using panic here as not knowing the current exe is a fatal state. + panic!("FATAL: Could not get current executable path: {e}"); + } + }; + info!("Main: Current binary is at {:?}", ¤t_exe); + + let rename_result = std::fs::rename(new_binary_path, ¤t_exe); + + match rename_result { + Ok(_) => { + info!("Main: Successfully replaced on-disk binary via atomic rename."); + } + Err(e) if e.raw_os_error() == Some(libc::EXDEV) => { + info!("Main: Cross-device link detected. Falling back to copy-then-rename strategy."); + let staging_path = current_exe.with_extension("staging"); + if let Err(copy_err) = std::fs::copy(new_binary_path, &staging_path) { + error!( + "CRITICAL: Failed to copy new binary to staging path {:?}: {}. Aborting upgrade.", + &staging_path, copy_err + ); + return Ok(()); + } + if let Err(perm_err) = + std::fs::set_permissions(&staging_path, std::fs::Permissions::from_mode(0o755)) + { + error!( + "CRITICAL: Failed to set permissions on staged binary {:?}: {}. Aborting upgrade.", + &staging_path, perm_err + ); + let _ = std::fs::remove_file(&staging_path); + return Ok(()); + } + if let Err(final_rename_err) = std::fs::rename(&staging_path, ¤t_exe) { + error!( + "CRITICAL: Failed to perform final atomic rename from {:?}: {}. Aborting upgrade.", + &staging_path, final_rename_err + ); + let _ = std::fs::remove_file(&staging_path); + return Ok(()); + } + let _ = std::fs::remove_file(new_binary_path); + info!("Main: Successfully replaced on-disk binary via copy-then-rename."); + } + Err(e) => { + error!( + "CRITICAL: Failed to rename new binary into place with an unexpected error: {e}. Aborting upgrade." + ); + return Ok(()); + } + } + + let mut args: Vec = std::env::args().collect(); + let restart_flag = "--restarted-after-upgrade"; + if !args.contains(&restart_flag.to_string()) { + args.push(restart_flag.to_string()); + } + + let cstr_args: Vec = args + .into_iter() + .map(|arg| CString::new(arg).unwrap()) + .collect(); + let cstr_path = CString::new(current_exe.into_os_string().into_vec()).unwrap(); + + info!( + "Main: Executing new binary with arguments: {:?}", + &cstr_args + ); + let Err(e) = nix::unistd::execv(&cstr_path, &cstr_args); + // execv replaces the current process, so it should not return. + // If it returns, it's a fatal error. + panic!("FATAL: execv failed after replacing binary: {e}"); +} diff --git a/feos/tests/integration_tests.rs b/feos/tests/integration_tests.rs new file mode 100644 index 0000000..9835d8e --- /dev/null +++ b/feos/tests/integration_tests.rs @@ -0,0 +1,709 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Context, Result}; +use feos_proto::{ + host_service::{ + host_service_client::HostServiceClient, GetCpuInfoRequest, GetNetworkInfoRequest, + HostnameRequest, MemoryRequest, + }, + image_service::{ + image_service_client::ImageServiceClient, DeleteImageRequest, ImageState, + ListImagesRequest, PullImageRequest, WatchImageStatusRequest, + }, + vm_service::{ + vm_service_client::VmServiceClient, CpuConfig, CreateVmRequest, DeleteVmRequest, + GetVmRequest, MemoryConfig, PingVmRequest, StartVmRequest, StreamVmEventsRequest, VmConfig, + VmEvent, VmState, VmStateChangedEvent, + }, +}; +use image_service::{IMAGE_DIR, IMAGE_SERVICE_SOCKET}; +use log::{error, info, warn}; +use nix::sys::signal::{kill, Signal}; +use nix::unistd::{self, Pid}; +use once_cell::sync::{Lazy, OnceCell as SyncOnceCell}; +use prost::Message; +use std::env; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::Path; +use std::process::Command; +use std::sync::Arc; +use std::time::Duration; +use tokio::net::UnixStream; +use tokio::sync::OnceCell as TokioOnceCell; +use tokio::time::timeout; +use tokio_stream::StreamExt; +use tonic::transport::{Channel, Endpoint, Uri}; +use tower::service_fn; +use vm_service::{VM_API_SOCKET_DIR, VM_CH_BIN}; + +const PUBLIC_SERVER_ADDRESS: &str = "http://[::1]:1337"; +const DEFAULT_TEST_IMAGE_REF: &str = "ghcr.io/ironcore-dev/os-images/gardenlinux-ch-dev"; +static TEST_IMAGE_REF: Lazy = + Lazy::new(|| env::var("TEST_IMAGE_REF").unwrap_or_else(|_| DEFAULT_TEST_IMAGE_REF.to_string())); + +static SERVER_RUNTIME: TokioOnceCell> = TokioOnceCell::const_new(); +static TEMP_DIR_GUARD: SyncOnceCell = SyncOnceCell::new(); + +async fn ensure_server() { + SERVER_RUNTIME + .get_or_init(|| async { setup_server().await }) + .await; +} + +async fn setup_server() -> Arc { + let temp_dir = TEMP_DIR_GUARD.get_or_init(|| { + tempfile::Builder::new() + .prefix("feos-test-") + .tempdir() + .expect("Failed to create temp dir") + }); + + let db_path = temp_dir.path().join("vms.db"); + let db_url = format!("sqlite:{}", db_path.to_str().unwrap()); + + env::set_var("DATABASE_URL", &db_url); + info!("Using temporary database for tests: {}", db_url); + + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("Failed to create a new Tokio runtime for the server"); + + runtime.spawn(async move { + if let Err(e) = main_server::run_server(false).await { + panic!("Test server failed to run: {}", e); + } + }); + + info!("Waiting for the server to start..."); + for _ in 0..20 { + if Channel::from_static(PUBLIC_SERVER_ADDRESS) + .connect() + .await + .is_ok() + { + info!("Server is up and running at {}", PUBLIC_SERVER_ADDRESS); + return Arc::new(runtime); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + error!("Server did not start in time."); + panic!("Server did not start in time."); +} + +async fn get_public_clients() -> Result<(VmServiceClient, HostServiceClient)> { + let vm_client = VmServiceClient::connect(PUBLIC_SERVER_ADDRESS).await?; + let host_client = HostServiceClient::connect(PUBLIC_SERVER_ADDRESS).await?; + Ok((vm_client, host_client)) +} + +async fn get_image_service_client() -> Result> { + let endpoint = Endpoint::from_static("http://[::1]:50051"); + let channel = endpoint + .connect_with_connector(service_fn(|_: Uri| { + UnixStream::connect(IMAGE_SERVICE_SOCKET) + })) + .await?; + Ok(ImageServiceClient::new(channel)) +} + +fn check_ch_binary() -> bool { + Command::new("which") + .arg(VM_CH_BIN) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +fn skip_if_ch_binary_missing() -> bool { + if !check_ch_binary() { + warn!( + "Skipping test because '{}' binary was not found in PATH.", + VM_CH_BIN + ); + return true; + } + false +} + +struct VmGuard { + vm_id: String, + pid: Option, + cleanup_disabled: bool, +} + +impl Drop for VmGuard { + fn drop(&mut self) { + if self.cleanup_disabled { + return; + } + info!("Cleaning up VM '{}'...", self.vm_id); + if let Some(pid) = self.pid { + info!("Killing process with PID: {}", pid); + let _ = kill(pid, Signal::SIGKILL); + } + let socket_path = format!("{}/{}", VM_API_SOCKET_DIR, self.vm_id); + if let Err(e) = std::fs::remove_file(&socket_path) { + if e.kind() != std::io::ErrorKind::NotFound { + warn!("Could not remove socket file '{}': {}", socket_path, e); + } + } else { + info!("Removed socket file '{}'", socket_path); + } + } +} + +async fn wait_for_vm_state( + stream: &mut tonic::Streaming, + target_state: VmState, +) -> Result<()> { + while let Some(event_res) = stream.next().await { + let event = event_res?; + let any_data = event.data.expect("Event should have data payload"); + if any_data.type_url == "type.googleapis.com/feos.vm.vmm.api.v1.VmStateChangedEvent" { + let state_change = VmStateChangedEvent::decode(&*any_data.value)?; + let new_state = + VmState::try_from(state_change.new_state).unwrap_or(VmState::Unspecified); + + info!( + "Received VM state change event: new_state={:?}, reason='{}'", + new_state, state_change.reason + ); + + if new_state == target_state { + return Ok(()); + } + + if new_state == VmState::Crashed { + let err_msg = format!("VM entered Crashed state. Reason: {}", state_change.reason); + error!("{}", &err_msg); + return Err(anyhow::anyhow!(err_msg)); + } + } + } + Err(anyhow::anyhow!( + "Event stream ended before VM reached {:?} state.", + target_state + )) +} + +async fn wait_for_target_state( + stream: &mut tonic::Streaming, + target_state: VmState, +) -> Result<()> { + while let Some(event_res) = stream.next().await { + let event = event_res?; + let any_data = event.data.expect("Event should have data payload"); + if any_data.type_url == "type.googleapis.com/feos.vm.vmm.api.v1.VmStateChangedEvent" { + let state_change = VmStateChangedEvent::decode(&*any_data.value)?; + let new_state = + VmState::try_from(state_change.new_state).unwrap_or(VmState::Unspecified); + + info!( + "Received VM state change event: new_state={:?}, reason='{}'", + new_state, state_change.reason + ); + + if new_state == target_state { + return Ok(()); + } + } + } + Err(anyhow::anyhow!( + "Event stream ended before VM reached {:?} state.", + target_state + )) +} + +#[tokio::test] +async fn test_create_and_start_vm() -> Result<()> { + if skip_if_ch_binary_missing() { + return Ok(()); + } + + ensure_server().await; + let (mut vm_client, _) = get_public_clients().await?; + + let image_ref = TEST_IMAGE_REF.clone(); + let vm_config = VmConfig { + cpus: Some(CpuConfig { + boot_vcpus: 2, + max_vcpus: 2, + }), + memory: Some(MemoryConfig { + size_mib: 2048, + hugepages: false, + }), + image_ref, + disks: vec![], + net: vec![], + ignition: None, + }; + let create_req = CreateVmRequest { + config: Some(vm_config), + vm_id: None, + }; + + info!("Sending CreateVm request"); + let create_res = vm_client.create_vm(create_req).await?.into_inner(); + let vm_id = create_res.vm_id; + info!("VM created with ID: {}", vm_id); + + let mut guard = VmGuard { + vm_id: vm_id.clone(), + pid: None, + cleanup_disabled: false, + }; + + info!("Connecting to StreamVmEvents stream for vm_id: {}", &vm_id); + let events_req = StreamVmEventsRequest { + vm_id: Some(vm_id.clone()), + ..Default::default() + }; + let mut stream = vm_client.stream_vm_events(events_req).await?.into_inner(); + + timeout( + Duration::from_secs(180), + wait_for_vm_state(&mut stream, VmState::Created), + ) + .await + .expect("Timed out waiting for VM to become created")?; + info!("VM is in CREATED state"); + + info!("Sending StartVm request for vm_id: {}", &vm_id); + let start_req = StartVmRequest { + vm_id: vm_id.clone(), + }; + vm_client.start_vm(start_req).await?; + + timeout( + Duration::from_secs(30), + wait_for_vm_state(&mut stream, VmState::Running), + ) + .await + .expect("Timed out waiting for VM to become running")?; + info!("VM is in RUNNING state"); + + let get_req = GetVmRequest { + vm_id: vm_id.clone(), + }; + let info_res = vm_client.get_vm(get_req).await?.into_inner(); + assert_eq!( + VmState::try_from(info_res.state).unwrap(), + VmState::Running, + "VM state from GetVm should be RUNNING" + ); + + info!("Pinging VMM for vm_id: {}", &vm_id); + let ping_req = PingVmRequest { + vm_id: vm_id.clone(), + }; + let ping_res = vm_client.ping_vm(ping_req).await?.into_inner(); + info!("VMM Ping successful, PID: {}", ping_res.pid); + guard.pid = Some(Pid::from_raw(ping_res.pid as i32)); + + info!("Deleting VM: {}", &vm_id); + let delete_req = DeleteVmRequest { + vm_id: vm_id.clone(), + }; + vm_client.delete_vm(delete_req).await?.into_inner(); + info!("DeleteVm call successful"); + + let socket_path = format!("{}/{}", VM_API_SOCKET_DIR, &vm_id); + assert!( + !Path::new(&socket_path).exists(), + "Socket file '{}' should not exist after DeleteVm", + socket_path + ); + info!("Verified VM API socket is deleted: {}", socket_path); + + guard.cleanup_disabled = true; + Ok(()) +} + +#[tokio::test] +async fn test_vm_healthcheck_and_crash_recovery() -> Result<()> { + if skip_if_ch_binary_missing() { + return Ok(()); + } + + ensure_server().await; + let (mut vm_client, _) = get_public_clients().await?; + + let image_ref = TEST_IMAGE_REF.clone(); + let vm_config = VmConfig { + cpus: Some(CpuConfig { + boot_vcpus: 1, + max_vcpus: 1, + }), + memory: Some(MemoryConfig { + size_mib: 1024, + hugepages: false, + }), + image_ref, + disks: vec![], + net: vec![], + ignition: None, + }; + let create_req = CreateVmRequest { + config: Some(vm_config), + vm_id: None, + }; + + info!("Sending CreateVm request for healthcheck test"); + let create_res = vm_client.create_vm(create_req).await?.into_inner(); + let vm_id = create_res.vm_id; + info!("VM created with ID: {}", vm_id); + + let mut guard = VmGuard { + vm_id: vm_id.clone(), + pid: None, + cleanup_disabled: false, + }; + + info!("Connecting to StreamVmEvents stream for vm_id: {}", &vm_id); + let events_req = StreamVmEventsRequest { + vm_id: Some(vm_id.clone()), + ..Default::default() + }; + let mut stream = vm_client.stream_vm_events(events_req).await?.into_inner(); + + timeout( + Duration::from_secs(180), + wait_for_vm_state(&mut stream, VmState::Created), + ) + .await + .expect("Timed out waiting for VM to become created")?; + info!("VM is in CREATED state"); + + info!("Sending StartVm request for vm_id: {}", &vm_id); + let start_req = StartVmRequest { + vm_id: vm_id.clone(), + }; + vm_client.start_vm(start_req).await?; + + timeout( + Duration::from_secs(30), + wait_for_vm_state(&mut stream, VmState::Running), + ) + .await + .expect("Timed out waiting for VM to become running")?; + info!("VM is in RUNNING state"); + + info!("Pinging VMM for vm_id: {}", &vm_id); + let ping_req = PingVmRequest { + vm_id: vm_id.clone(), + }; + let ping_res = vm_client.ping_vm(ping_req).await?.into_inner(); + info!("VMM Ping successful, PID: {}", ping_res.pid); + let pid_to_kill = Pid::from_raw(ping_res.pid as i32); + guard.pid = Some(pid_to_kill); + + info!( + "Forcefully killing hypervisor process with PID: {}", + pid_to_kill + ); + kill(pid_to_kill, Signal::SIGKILL).context("Failed to kill hypervisor process")?; + info!("Successfully sent SIGKILL to process {}", pid_to_kill); + + timeout( + Duration::from_secs(30), + wait_for_target_state(&mut stream, VmState::Crashed), + ) + .await + .expect("Timed out waiting for VM to enter Crashed state")?; + info!("VM is in CRASHED state as expected"); + + info!("Deleting crashed VM: {}", &vm_id); + let delete_req = DeleteVmRequest { + vm_id: vm_id.clone(), + }; + vm_client.delete_vm(delete_req).await?.into_inner(); + info!("DeleteVm call successful for crashed VM"); + + let socket_path = format!("{}/{}", VM_API_SOCKET_DIR, &vm_id); + assert!( + !Path::new(&socket_path).exists(), + "Socket file '{}' should not exist after DeleteVm", + socket_path + ); + info!("Verified VM API socket is deleted: {}", socket_path); + + guard.cleanup_disabled = true; + Ok(()) +} + +#[tokio::test] +async fn test_hostname_retrieval() -> Result<()> { + ensure_server().await; + let (_, mut host_client) = get_public_clients().await?; + + let response = host_client.hostname(HostnameRequest {}).await?; + let remote_hostname = response.into_inner().hostname; + let local_hostname = unistd::gethostname()? + .into_string() + .expect("Hostname is not valid UTF-8"); + + info!( + "Hostname from API: '{}', Hostname from local call: '{}'", + remote_hostname, local_hostname + ); + assert_eq!( + remote_hostname, local_hostname, + "The hostname from the API should match the local system's hostname" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_get_memory_info() -> Result<()> { + ensure_server().await; + let (_, mut host_client) = get_public_clients().await?; + + let file = File::open("/proc/meminfo")?; + let reader = BufReader::new(file); + let mut local_memtotal = 0; + for line in reader.lines() { + let line = line?; + if line.starts_with("MemTotal:") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + local_memtotal = parts[1].parse::()?; + } + break; + } + } + + assert!( + local_memtotal > 0, + "Failed to parse MemTotal from local /proc/meminfo" + ); + info!("Local MemTotal from /proc/meminfo: {} kB", local_memtotal); + + info!("Sending GetMemory request"); + let response = host_client.get_memory(MemoryRequest {}).await?.into_inner(); + + let mem_info = response + .mem_info + .context("MemoryInfo was not present in the response")?; + info!( + "Remote MemTotal from gRPC response: {} kB", + mem_info.memtotal + ); + + assert_eq!( + mem_info.memtotal, local_memtotal, + "MemTotal from API should match the local system's MemTotal" + ); + assert!( + mem_info.memfree <= mem_info.memtotal, + "MemFree should not be greater than MemTotal" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_get_cpu_info() -> Result<()> { + ensure_server().await; + let (_, mut host_client) = get_public_clients().await?; + + let file = File::open("/proc/cpuinfo")?; + let reader = BufReader::new(file); + let mut local_processor_count = 0; + let mut local_vendor_id = String::new(); + for line in reader.lines() { + let line = line?; + if line.starts_with("processor") { + local_processor_count += 1; + } + if line.starts_with("vendor_id") && local_vendor_id.is_empty() { + let parts: Vec<&str> = line.splitn(2, ':').collect(); + if parts.len() == 2 { + local_vendor_id = parts[1].trim().to_string(); + } + } + } + + assert!( + local_processor_count > 0, + "Failed to parse processor count from /proc/cpuinfo" + ); + assert!( + !local_vendor_id.is_empty(), + "Failed to parse vendor_id from /proc/cpuinfo" + ); + info!( + "Local data from /proc/cpuinfo: {} processors, vendor_id: {}", + local_processor_count, local_vendor_id + ); + + info!("Sending GetCPUInfo request"); + let response = host_client + .get_cpu_info(GetCpuInfoRequest {}) + .await? + .into_inner(); + + let remote_cpu_info = response.cpu_info; + info!( + "Remote data from gRPC: {} processors", + remote_cpu_info.len() + ); + + assert_eq!( + remote_cpu_info.len(), + local_processor_count, + "Processor count from API should match local count" + ); + + let first_cpu = remote_cpu_info + .first() + .context("CPU info list should not be empty")?; + info!("Remote vendor_id: {}", first_cpu.vendor_id); + + assert_eq!( + first_cpu.vendor_id, local_vendor_id, + "Vendor ID of first CPU should match" + ); + assert!( + first_cpu.cpu_mhz > 0.0, + "CPU MHz should be a positive value" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_get_network_info() -> Result<()> { + ensure_server().await; + let (_, mut host_client) = get_public_clients().await?; + + info!("Sending GetNetworkInfo request"); + let response = host_client + .get_network_info(GetNetworkInfoRequest {}) + .await? + .into_inner(); + + assert!( + !response.devices.is_empty(), + "The list of network devices should not be empty" + ); + info!("Received {} network devices", response.devices.len()); + + let lo = response + .devices + .iter() + .find(|d| d.name == "lo") + .context("Could not find the loopback interface 'lo'")?; + + info!("Found loopback interface 'lo'"); + assert_eq!(lo.name, "lo"); + assert!( + lo.rx_packets > 0 || lo.tx_packets > 0, + "Loopback interface should have some packets transferred" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_image_lifecycle() -> Result<()> { + if skip_if_ch_binary_missing() { + return Ok(()); + } + ensure_server().await; + let mut image_client = get_image_service_client().await?; + + let image_ref = TEST_IMAGE_REF.clone(); + info!("Pulling image: {}", image_ref); + let pull_req = PullImageRequest { + image_ref: image_ref.clone(), + }; + let pull_res = image_client.pull_image(pull_req).await?.into_inner(); + let image_uuid = pull_res.image_uuid; + info!("Image pull initiated with UUID: {}", image_uuid); + + let watch_req = WatchImageStatusRequest { + image_uuid: image_uuid.clone(), + }; + let mut stream = image_client + .watch_image_status(watch_req) + .await? + .into_inner(); + + timeout(Duration::from_secs(120), wait_for_image_ready(&mut stream)) + .await + .expect("Timed out waiting for image to become ready")?; + + info!("Verifying image {} is in the list...", image_uuid); + let list_req = ListImagesRequest {}; + let list_res = image_client.list_images(list_req).await?.into_inner(); + let found_image = list_res + .images + .iter() + .find(|i| i.image_uuid == image_uuid) + .expect("Image UUID should be in the list after pulling"); + assert_eq!(found_image.state, ImageState::Ready as i32); + + let image_path = Path::new(IMAGE_DIR).join(&image_uuid); + info!("Verifying filesystem path: {}", image_path.display()); + assert!(image_path.exists(), "Image directory should exist"); + assert!(image_path.join("disk.image").exists()); + assert!(image_path.join("metadata.json").exists()); + + info!("Deleting image: {}", image_uuid); + let delete_req = DeleteImageRequest { + image_uuid: image_uuid.clone(), + }; + image_client.delete_image(delete_req).await?; + + info!("Verifying image {} is NOT in the list...", image_uuid); + let list_req_after_delete = ListImagesRequest {}; + let list_res_after_delete = image_client + .list_images(list_req_after_delete) + .await? + .into_inner(); + assert!(!list_res_after_delete + .images + .iter() + .any(|i| i.image_uuid == image_uuid)); + + info!( + "Verifying filesystem path is gone: {}", + image_path.display() + ); + assert!(!image_path.exists(), "Image directory should be deleted"); + + Ok(()) +} + +async fn wait_for_image_ready(mut stream: S) -> anyhow::Result<()> +where + S: tokio_stream::Stream< + Item = Result, + > + Unpin, +{ + let mut saw_downloading = false; + while let Some(status_res) = stream.next().await { + let status = status_res?; + let state = ImageState::try_from(status.state).unwrap(); + info!("Received image status update: {:?}", state); + if state == ImageState::Downloading { + saw_downloading = true; + } + if state == ImageState::Ready { + assert!( + saw_downloading, + "Should have seen DOWNLOADING state before READY" + ); + return Ok(()); + } + if state == ImageState::PullFailed { + panic!("Image pull failed unexpectedly: {}", status.message); + } + } + anyhow::bail!("Stream ended before image became ready"); +} diff --git a/feos/utils/Cargo.toml b/feos/utils/Cargo.toml new file mode 100644 index 0000000..0ff81c2 --- /dev/null +++ b/feos/utils/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "feos-utils" +version.workspace = true +edition.workspace = true + +[dependencies] +log = { workspace = true } +nix = { workspace = true } +chrono = { workspace = true } +tokio = { workspace = true } +termcolor = "1.1" +dhcproto = { workspace = true } +futures = { workspace = true } +netlink-packet-route = { workspace = true } +pnet = { workspace = true } +rtnetlink = { workspace = true } +socket2 = { workspace = true } +libc = { workspace = true } \ No newline at end of file diff --git a/feos/utils/src/feos_logger/mod.rs b/feos/utils/src/feos_logger/mod.rs new file mode 100644 index 0000000..232019b --- /dev/null +++ b/feos/utils/src/feos_logger/mod.rs @@ -0,0 +1,272 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use chrono::{DateTime, Utc}; +use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError}; +use std::collections::VecDeque; +use std::fmt; +use std::io::Write; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; +use tokio::sync::{broadcast, mpsc, oneshot}; + +#[derive(Clone, Debug)] +pub struct LogEntry { + pub seq: u64, + pub timestamp: DateTime, + pub level: Level, + pub target: String, + pub message: String, +} + +impl fmt::Display for LogEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "[{} {:<5} {}] {}", + self.timestamp.format("%Y-%m-%d %H:%M:%S%.3f"), + self.level, + self.target, + self.message + ) + } +} + +#[derive(Clone)] +pub struct LogHandle { + history_requester: mpsc::Sender, + broadcast_sender: broadcast::Sender, +} + +pub struct LogReader { + history_snapshot: VecDeque, + receiver: broadcast::Receiver, +} + +pub struct Builder { + filter: LevelFilter, + max_history: usize, + broadcast_capacity: usize, + mpsc_capacity: usize, + log_to_stdout: bool, +} + +impl Default for Builder { + fn default() -> Self { + Self { + filter: LevelFilter::Info, + max_history: 1000, + broadcast_capacity: 1024, + mpsc_capacity: 4096, + log_to_stdout: true, + } + } +} + +impl Builder { + pub fn new() -> Self { + Self::default() + } + + pub fn filter_level(mut self, level: LevelFilter) -> Self { + self.filter = level; + self + } + + pub fn max_history(mut self, size: usize) -> Self { + self.max_history = size; + self + } + + pub fn log_to_stdout(mut self, enabled: bool) -> Self { + self.log_to_stdout = enabled; + self + } + + pub fn init(self) -> Result { + let (log_tx, log_rx) = mpsc::channel::(self.mpsc_capacity); + let (history_tx, history_rx) = mpsc::channel(32); + let (broadcast_tx, _) = broadcast::channel(self.broadcast_capacity); + + let logger_frontend = FeosLogger { + sender: log_tx, + filter: self.filter, + }; + + let actor = LoggerActor { + log_receiver: log_rx, + history_requester: history_rx, + broadcast_sender: broadcast_tx.clone(), + history: VecDeque::with_capacity(self.max_history), + max_history: self.max_history, + seq_counter: 0, + log_to_stdout: self.log_to_stdout, + stdout_writer: StandardStream::stdout(ColorChoice::Auto), + }; + + tokio::spawn(actor.run()); + + let handle = LogHandle { + history_requester: history_tx, + broadcast_sender: broadcast_tx, + }; + + log::set_boxed_logger(Box::new(logger_frontend))?; + log::set_max_level(self.filter); + + Ok(handle) + } +} + +impl LogHandle { + pub async fn new_reader(&self) -> Result { + let (resp_tx, resp_rx) = oneshot::channel(); + if self.history_requester.send(resp_tx).await.is_err() { + return Err("Logger actor has shut down"); + } + + let history_snapshot = match resp_rx.await { + Ok(history) => history, + Err(_) => return Err("Failed to receive history from logger actor"), + }; + + let receiver = self.broadcast_sender.subscribe(); + + Ok(LogReader { + history_snapshot, + receiver, + }) + } +} + +impl LogReader { + pub async fn next(&mut self) -> Option { + if let Some(entry) = self.history_snapshot.pop_front() { + return Some(entry); + } + + match self.receiver.recv().await { + Ok(entry) => Some(entry), + Err(broadcast::error::RecvError::Lagged(_)) => { + eprintln!( + "[LOG READER WARNING] Reader lagged and missed messages. Closing stream." + ); + None + } + Err(broadcast::error::RecvError::Closed) => None, + } + } +} + +type HistoryRequest = oneshot::Sender>; + +struct LogMessage { + level: Level, + target: String, + message: String, +} + +struct FeosLogger { + sender: mpsc::Sender, + filter: LevelFilter, +} + +impl Log for FeosLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= self.filter + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + + let msg = LogMessage { + level: record.level(), + target: record.target().to_string(), + message: format!("{}", record.args()), + }; + + if self.sender.try_send(msg).is_err() { + eprintln!("[LOGGER WARNING] Log channel is full. Dropping log message."); + } + } + + fn flush(&self) {} +} + +struct LoggerActor { + log_receiver: mpsc::Receiver, + history_requester: mpsc::Receiver, + broadcast_sender: broadcast::Sender, + history: VecDeque, + max_history: usize, + seq_counter: u64, + log_to_stdout: bool, + stdout_writer: StandardStream, +} + +impl LoggerActor { + async fn run(mut self) { + loop { + tokio::select! { + Some(msg) = self.log_receiver.recv() => { + self.seq_counter += 1; + + let entry = LogEntry { + seq: self.seq_counter, + timestamp: Utc::now(), + level: msg.level, + target: msg.target, + message: msg.message, + }; + + if self.log_to_stdout { + let _ = self.write_log_entry_to_stdout(&entry); + } + + self.history.push_back(entry.clone()); + if self.history.len() > self.max_history { + self.history.pop_front(); + } + + let _ = self.broadcast_sender.send(entry); + }, + + Some(responder) = self.history_requester.recv() => { + let _ = responder.send(self.history.clone()); + }, + + else => { break; } + } + } + } + + fn write_log_entry_to_stdout(&mut self, entry: &LogEntry) -> std::io::Result<()> { + let mut level_spec = ColorSpec::new(); + match entry.level { + Level::Error => level_spec.set_fg(Some(Color::Red)).set_bold(true), + Level::Warn => level_spec.set_fg(Some(Color::Yellow)).set_bold(true), + Level::Info => level_spec.set_fg(Some(Color::Green)).set_bold(true), + Level::Debug => level_spec.set_fg(Some(Color::Blue)).set_bold(true), + Level::Trace => level_spec.set_fg(Some(Color::Magenta)).set_bold(true), + }; + + write!( + &mut self.stdout_writer, + "[{} ", + entry.timestamp.format("%Y-%m-%dT%H:%M:%SZ") + )?; + + self.stdout_writer.set_color(&level_spec)?; + write!(&mut self.stdout_writer, "{:<5}", entry.level.to_string())?; + + self.stdout_writer.reset()?; + writeln!( + &mut self.stdout_writer, + " {target}] {message}", + target = entry.target, + message = entry.message + )?; + Ok(()) + } +} diff --git a/src/fsmount.rs b/feos/utils/src/filesystem/fsmount.rs similarity index 91% rename from src/fsmount.rs rename to feos/utils/src/filesystem/fsmount.rs index 0edd99e..da47e4d 100644 --- a/src/fsmount.rs +++ b/feos/utils/src/filesystem/fsmount.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + use libc::{syscall, SYS_fsconfig, SYS_fsmount, SYS_fsopen}; use nix::{errno::Errno, Result}; use std::{ diff --git a/feos/utils/src/filesystem/mod.rs b/feos/utils/src/filesystem/mod.rs new file mode 100644 index 0000000..a13d747 --- /dev/null +++ b/feos/utils/src/filesystem/mod.rs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +mod fsmount; +mod mount; +mod r#move; + +pub use mount::mount_virtual_filesystems; +pub use r#move::{get_root_fstype, move_root}; diff --git a/src/filesystem.rs b/feos/utils/src/filesystem/mount.rs similarity index 90% rename from src/filesystem.rs rename to feos/utils/src/filesystem/mount.rs index 335a56d..7132edd 100644 --- a/src/filesystem.rs +++ b/feos/utils/src/filesystem/mount.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + use log::debug; use nix::mount::{mount, MsFlags}; diff --git a/src/move_root.rs b/feos/utils/src/filesystem/move.rs similarity index 94% rename from src/move_root.rs rename to feos/utils/src/filesystem/move.rs index 269457f..c6ee5df 100644 --- a/src/move_root.rs +++ b/feos/utils/src/filesystem/move.rs @@ -1,4 +1,7 @@ -use crate::fsmount::{fsconfig, fsmount, fsopen, FSCONFIG_CMD_CREATE, FSCONFIG_SET_STRING}; +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use super::fsmount::{fsconfig, fsmount, fsopen, FSCONFIG_CMD_CREATE, FSCONFIG_SET_STRING}; use nix::{ fcntl::{openat, OFlag}, mount::{mount, MsFlags}, diff --git a/src/host/info.rs b/feos/utils/src/host/info.rs similarity index 62% rename from src/host/info.rs rename to feos/utils/src/host/info.rs index c67d988..b874e75 100644 --- a/src/host/info.rs +++ b/feos/utils/src/host/info.rs @@ -1,9 +1,14 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + use log::info; use nix::errno::Errno; use nix::sys::sysinfo::sysinfo; use nix::unistd::sysconf; use nix::unistd::SysconfVar; use std::fs; +use tokio::fs::File; +use tokio::io::AsyncReadExt; #[derive(Default)] pub struct HostInfo { @@ -22,7 +27,7 @@ pub struct Interface { } fn get_pci_address(interface_name: &str) -> Option { - let path = format!("/sys/class/net/{}/device", interface_name); + let path = format!("/sys/class/net/{interface_name}/device"); if let Ok(device_path) = fs::read_link(path) { let pci_address = device_path.file_name()?.to_str()?.to_string(); return Some(pci_address); @@ -31,7 +36,7 @@ fn get_pci_address(interface_name: &str) -> Option { } fn get_mac_address(interface_name: &str) -> Option { - let path = format!("/sys/class/net/{}/address", interface_name); + let path = format!("/sys/class/net/{interface_name}/address"); if let Ok(mac) = fs::read_to_string(path) { Some(mac.trim().to_string()) } else { @@ -43,7 +48,7 @@ fn get_interfaces() -> Result, Errno> { let mut interfaces = Vec::new(); let ifaces = nix::net::if_::if_nameindex()?; for iface in &ifaces { - info!("found network interface: {:?}", iface); + info!("found network interface: {iface:?}"); let name = iface.name().to_str().unwrap(); let interface = Interface { name: name.to_string(), @@ -62,10 +67,10 @@ pub fn check_info() -> HostInfo { match sysconf(SysconfVar::_NPROCESSORS_ONLN) { Ok(Some(num_cores)) => match u64::try_from(num_cores) { Ok(num_cores) => host.num_cores = num_cores, - Err(err) => info!("Error getting number of CPU cores: {}", err), + Err(err) => info!("Error getting number of CPU cores: {err}"), }, Ok(None) => (), - Err(err) => info!("Error getting number of CPU cores: {}", err), + Err(err) => info!("Error getting number of CPU cores: {err}"), } match sysinfo() { @@ -74,15 +79,37 @@ pub fn check_info() -> HostInfo { host.ram_total = info.ram_total(); host.ram_unused = info.ram_unused(); } - Err(err) => info!("Error getting sysinfo: {}", err), + Err(err) => info!("Error getting sysinfo: {err}"), } match get_interfaces() { Ok(ifs) => { host.net_interfaces = ifs; } - Err(err) => info!("Error getting network interfaces: {}", err), + Err(err) => info!("Error getting network interfaces: {err}"), } host } + +pub async fn is_running_on_vm() -> Result> { + let files = [ + "/sys/class/dmi/id/product_name", + "/sys/class/dmi/id/sys_vendor", + ]; + + let mut match_count = 0; + + for file_path in files.iter() { + let mut file = File::open(file_path).await?; + let mut contents = String::new(); + file.read_to_string(&mut contents).await?; + + let lowercase_contents = contents.to_lowercase(); + if lowercase_contents.contains("cloud") && lowercase_contents.contains("hypervisor") { + match_count += 1; + } + } + + Ok(match_count == 2) +} diff --git a/feos/utils/src/host/memory.rs b/feos/utils/src/host/memory.rs new file mode 100644 index 0000000..c148d7c --- /dev/null +++ b/feos/utils/src/host/memory.rs @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use log::{info, warn}; +use nix::mount::{mount, MsFlags}; +use std::io; +use tokio::fs; + +const HUGEPAGE_FS_TYPE: &[u8] = b"hugetlbfs"; +const HUGEPAGE_MOUNT_POINT: &str = "/dev/hugepages"; + +pub async fn configure_hugepages(num_pages: u32) -> io::Result<()> { + let nr_hugepages_path = "/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages"; + + info!("Attempting to allocate {num_pages} hugepages..."); + fs::write(nr_hugepages_path, num_pages.to_string()).await?; + info!("Successfully wrote to {nr_hugepages_path}"); + + let allocated_pages_str = fs::read_to_string(nr_hugepages_path).await?; + let allocated_pages = allocated_pages_str.trim().parse::().unwrap_or(0); + + if allocated_pages < num_pages { + warn!( + "System only allocated {allocated_pages} of the requested {num_pages} hugepages. This might happen due to memory fragmentation." + ); + } else { + info!("System successfully allocated {allocated_pages} hugepages."); + } + + if !is_mounted(HUGEPAGE_MOUNT_POINT).await { + info!("Mounting hugetlbfs at {HUGEPAGE_MOUNT_POINT}..."); + fs::create_dir_all(HUGEPAGE_MOUNT_POINT).await?; + mount_hugetlbfs()?; + info!("Successfully mounted hugetlbfs."); + } else { + info!("hugetlbfs is already mounted at {HUGEPAGE_MOUNT_POINT}."); + } + + Ok(()) +} + +fn mount_hugetlbfs() -> Result<(), io::Error> { + const NONE: Option<&'static [u8]> = None; + mount( + Some(b"none".as_ref()), + HUGEPAGE_MOUNT_POINT, + Some(HUGEPAGE_FS_TYPE), + MsFlags::empty(), + NONE, + ) + .map_err(|e| io::Error::other(format!("Failed to mount hugetlbfs: {e}"))) +} + +async fn is_mounted(path: &str) -> bool { + let Ok(mounts) = fs::read_to_string("/proc/mounts").await else { + return false; + }; + mounts.lines().any(|line| { + let parts: Vec<&str> = line.split_whitespace().collect(); + parts.get(1) == Some(&path) + }) +} diff --git a/feos/utils/src/host/mod.rs b/feos/utils/src/host/mod.rs new file mode 100644 index 0000000..d28e6a8 --- /dev/null +++ b/feos/utils/src/host/mod.rs @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +pub mod info; +pub mod memory; +pub mod power; diff --git a/src/host/power.rs b/feos/utils/src/host/power.rs similarity index 69% rename from src/host/power.rs rename to feos/utils/src/host/power.rs index 257ffeb..24a6fb3 100644 --- a/src/host/power.rs +++ b/feos/utils/src/host/power.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + use nix::sys::reboot::RebootMode; use std::convert::Infallible; diff --git a/feos/utils/src/lib.rs b/feos/utils/src/lib.rs new file mode 100644 index 0000000..dd82dcd --- /dev/null +++ b/feos/utils/src/lib.rs @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +pub mod feos_logger; +pub mod filesystem; +pub mod host; +pub mod network; diff --git a/feos/utils/src/network/dhcpv6.rs b/feos/utils/src/network/dhcpv6.rs new file mode 100644 index 0000000..af42038 --- /dev/null +++ b/feos/utils/src/network/dhcpv6.rs @@ -0,0 +1,545 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use dhcproto::v6::*; +use futures::stream::TryStreamExt; +use log::{error, info, warn}; +use netlink_packet_route::route::{ + RouteAddress, RouteAttribute, RouteProtocol, RouteScope, RouteType, +}; +use netlink_packet_route::AddressFamily; +use nix::net::if_::if_nametoindex; +use pnet::packet::icmpv6::Icmpv6Code; +use pnet::{ + datalink::{self, Channel::Ethernet, NetworkInterface}, + packet::{ + ethernet::{EtherTypes, EthernetPacket, MutableEthernetPacket}, + icmpv6::{checksum, ndp::*, Icmpv6Packet, Icmpv6Types, MutableIcmpv6Packet}, + ip::IpNextHeaderProtocols, + ipv6::{Ipv6Packet, MutableIpv6Packet}, + Packet, + }, + util::MacAddr, +}; +use rtnetlink::{new_connection, Error, Handle}; +use socket2::{Domain, Protocol, SockAddr, Socket, Type}; +use std::io; +use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6}; +use std::thread::sleep; +use std::time::Duration; +use tokio::net::UdpSocket; +use tokio::task; + +pub fn mac_to_ipv6_link_local(mac_address: &[u8]) -> Option { + if mac_address.len() == 6 { + let mut bytes = [0u8; 16]; + bytes[0] = 0xfe; + bytes[1] = 0x80; + bytes[8] = mac_address[0] ^ 0b00000010; + bytes[9] = mac_address[1]; + bytes[10] = mac_address[2]; + bytes[11] = 0xff; + bytes[12] = 0xfe; + bytes[13] = mac_address[3]; + bytes[14] = mac_address[4]; + bytes[15] = mac_address[5]; + Some(Ipv6Addr::from(bytes)) + } else { + None + } +} + +pub fn send_neigh_solicitation( + interface_name: String, + target_address: &Ipv6Addr, + src_address: &Ipv6Addr, +) { + let interface_names_match = |iface: &datalink::NetworkInterface| iface.name == interface_name; + + let interfaces = datalink::interfaces(); + let interface = match interfaces.into_iter().find(interface_names_match) { + Some(iface) => iface, + None => { + error!("Error getting interface"); + return; + } + }; + + let (mut tx, _rx) = match datalink::channel(&interface, Default::default()) { + Ok(Ethernet(tx, rx)) => (tx, rx), + Ok(_) => { + error!("Unhandled channel type"); + return; + } + Err(e) => { + error!("Error creating channel: {e}"); + return; + } + }; + + let mut packet_buffer = [0u8; 86]; + let mut ethernet_packet = MutableEthernetPacket::new(&mut packet_buffer).unwrap(); + + ethernet_packet.set_destination(MacAddr::broadcast()); + ethernet_packet.set_source(interface.mac.unwrap()); + ethernet_packet.set_ethertype(EtherTypes::Ipv6); + + let mut ipv6_and_icmp_buffer = [0u8; 72]; + let mut ipv6_packet = MutableIpv6Packet::new(&mut ipv6_and_icmp_buffer[..40]).unwrap(); + ipv6_packet.set_version(6); + ipv6_packet.set_next_header(IpNextHeaderProtocols::Icmpv6); + ipv6_packet.set_payload_length(32); + ipv6_packet.set_hop_limit(255); + ipv6_packet.set_source(*src_address); + ipv6_packet.set_destination(*target_address); + + let mut icmp_packet = MutableIcmpv6Packet::new(&mut ipv6_and_icmp_buffer[40..]).unwrap(); + icmp_packet.set_icmpv6_type(Icmpv6Types::NeighborSolicit); + icmp_packet.set_icmpv6_code(Icmpv6Code(0)); + icmp_packet.set_checksum(0); + + let mut icmp_payload = [0u8; 28]; + icmp_payload[4..20].copy_from_slice(&target_address.octets()); + icmp_payload[20] = 1; + icmp_payload[21] = 1; + icmp_payload[22..28].copy_from_slice(&interface.mac.unwrap().octets()); + icmp_packet.set_payload(&icmp_payload); + + let checksum = checksum( + &Icmpv6Packet::new(icmp_packet.packet()).unwrap(), + src_address, + target_address, + ); + icmp_packet.set_checksum(checksum); + + ethernet_packet.set_payload(&ipv6_and_icmp_buffer); + + if tx + .send_to(ethernet_packet.packet(), Some(interface.clone())) + .is_none() + { + error!("Failed to send neighbor solicitation"); + } +} + +fn send_router_solicitation(interface: &NetworkInterface, tx: &mut dyn datalink::DataLinkSender) { + let source_ip = Ipv6Addr::UNSPECIFIED; + let destination_ip = "ff02::2".parse::().unwrap(); + + let mut packet_buffer = [0u8; 128]; + let mut ethernet_packet = MutableEthernetPacket::new(&mut packet_buffer).unwrap(); + + ethernet_packet.set_destination(MacAddr::broadcast()); + ethernet_packet.set_source(interface.mac.unwrap()); + ethernet_packet.set_ethertype(EtherTypes::Ipv6); + + let mut ipv6_and_icmp_buffer = [0u8; 48]; + let mut ipv6_packet = MutableIpv6Packet::new(&mut ipv6_and_icmp_buffer[..40]).unwrap(); + ipv6_packet.set_version(6); + ipv6_packet.set_next_header(IpNextHeaderProtocols::Icmpv6); + ipv6_packet.set_payload_length(8); + ipv6_packet.set_hop_limit(255); + ipv6_packet.set_source(source_ip); + ipv6_packet.set_destination(destination_ip); + + let mut icmp_packet = MutableIcmpv6Packet::new(&mut ipv6_and_icmp_buffer[40..]).unwrap(); + icmp_packet.set_icmpv6_type(Icmpv6Types::RouterSolicit); + + let checksum = checksum( + &Icmpv6Packet::new(icmp_packet.packet()).unwrap(), + &source_ip, + &destination_ip, + ); + icmp_packet.set_checksum(checksum); + + ethernet_packet.set_payload(&ipv6_and_icmp_buffer); + + if tx + .send_to(ethernet_packet.packet(), Some(interface.clone())) + .is_none() + { + error!("Failed to send router solicitation"); + } +} + +pub fn is_dhcpv6_needed(interface_name: String, ignore_ra_flag: bool) -> Option { + let mut sender_ipv6_address: Option = None; + let interfaces = datalink::interfaces(); + let interface = interfaces + .into_iter() + .find(|iface| iface.name == interface_name)?; + let (mut tx, mut rx) = match datalink::channel(&interface, Default::default()) { + Ok(Ethernet(tx, rx)) => (tx, rx), + _ => return None, + }; + + info!("Sending Router Solicitation ..."); + sleep(Duration::from_secs(5)); + send_router_solicitation(&interface, &mut *tx); + + while let Ok(raw_packet) = rx.next() { + if let Some(eth_packet) = EthernetPacket::new(raw_packet) { + if eth_packet.get_ethertype() == EtherTypes::Ipv6 { + if let Some(ipv6_packet) = Ipv6Packet::new(eth_packet.payload()) { + sender_ipv6_address = Some(ipv6_packet.get_source()); + if let Some(icmp_packet) = Icmpv6Packet::new(ipv6_packet.payload()) { + if icmp_packet.get_icmpv6_type() == Icmpv6Types::RouterAdvert { + if let Some(ra_packet) = RouterAdvertPacket::new(ipv6_packet.payload()) + { + if (ra_packet.get_flags() & 0xC0) == 0xC0 || ignore_ra_flag { + break; + } + } + } + } + } + } + } + } + sender_ipv6_address +} + +#[derive(Debug)] +pub struct PrefixInfo { + pub prefix: Ipv6Addr, + pub prefix_length: u8, +} + +#[derive(Debug)] +pub struct Dhcpv6Result { + pub address: Ipv6Addr, + pub prefix: Option, +} + +pub async fn run_dhcpv6_client( + interface_name: String, +) -> Result> { + let chaddr = vec![ + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, + ]; + let random_xid: [u8; 3] = [0x12, 0x34, 0x56]; + let multicast_address = "[FF02::1:2]:547".parse::().unwrap(); + let mut ia_addr_confirm: Option = None; + let mut ia_pd_confirm: Option = None; + + let interface_index = get_interface_index(interface_name.clone()).await?; + let socket = create_multicast_socket(&interface_name, interface_index, 546)?; + + let mut msg = Message::new(MessageType::Solicit); + msg.opts_mut().insert(DhcpOption::ClientId(chaddr.clone())); + msg.opts_mut().insert(DhcpOption::ElapsedTime(0)); + msg.set_xid(random_xid); + + msg.opts_mut().insert(DhcpOption::RapidCommit); + + let mut oro = ORO { opts: Vec::new() }; + oro.opts.push(OptionCode::DomainNameServers); + oro.opts.push(OptionCode::DomainSearchList); + oro.opts.push(OptionCode::ClientFqdn); + oro.opts.push(OptionCode::SntpServers); + oro.opts.push(OptionCode::RapidCommit); + oro.opts.push(OptionCode::IAPD); + oro.opts.push(OptionCode::IAPrefix); + + msg.opts_mut().insert(DhcpOption::ORO(oro)); + + let ia_addr_instance = IAAddr { + addr: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), + preferred_life: 3000, + valid_life: 5000, + opts: DhcpOptions::default(), + }; + + let mut iana_opts = DhcpOptions::default(); + iana_opts.insert(DhcpOption::IAAddr(ia_addr_instance)); + + let iana_instance = IANA { + id: 123, + t1: 3600, + t2: 7200, + opts: iana_opts, + }; + + msg.opts_mut().insert(DhcpOption::IANA(iana_instance)); + + // Request Prefix Delegation + let iaprefix_instance = IAPrefix { + preferred_lifetime: 0, + prefix_len: 80, + opts: DhcpOptions::default(), + valid_lifetime: 0, + prefix_ip: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), + }; + + let mut iapd_opts = DhcpOptions::default(); + iapd_opts.insert(DhcpOption::IAPrefix(iaprefix_instance)); + + let iapd_instance = IAPD { + id: 456, + t1: 3600, + t2: 7200, + opts: iapd_opts, + }; + + msg.opts_mut().insert(DhcpOption::IAPD(iapd_instance)); + + let mut buf = Vec::new(); + let mut encoder = Encoder::new(&mut buf); + msg.encode(&mut encoder)?; + socket.send_to(&buf, multicast_address).await?; + + let mut recv_buf = [0; 1500]; + loop { + let (size, _) = socket.recv_from(&mut recv_buf).await?; + let response = Message::decode(&mut dhcproto::v6::Decoder::new(&recv_buf[..size]))?; + let mut serverid: Option<&DhcpOption> = None; + let mut ia_addr: Option<&DhcpOption> = None; + let mut ia_pd: Option<&DhcpOption> = None; + + match response.msg_type() { + MessageType::Advertise => { + info!("DHCPv6 processing in progress..."); + if let Some(DhcpOption::IANA(iana)) = response.opts().get(OptionCode::IANA) { + if let Some(ia_addr_opt) = iana.opts.get(OptionCode::IAAddr) { + ia_addr = Some(ia_addr_opt); + } + } + if let Some(DhcpOption::IAPD(iapd)) = response.opts().get(OptionCode::IAPD) { + if let Some(iaprefix_opt) = iapd.opts.get(OptionCode::IAPrefix) { + ia_pd = Some(iaprefix_opt); + } + } + if let Some(server_option) = response.opts().get(OptionCode::ServerId) { + serverid = Some(server_option); + } + + let mut request_msg = Message::new(MessageType::Request); + request_msg.set_xid(random_xid); + request_msg + .opts_mut() + .insert(DhcpOption::ClientId(chaddr.clone())); + request_msg.opts_mut().insert(DhcpOption::ElapsedTime(0)); + if let Some(DhcpOption::ServerId(duid)) = serverid { + request_msg + .opts_mut() + .insert(DhcpOption::ServerId((*duid).clone())); + } else { + warn!("Server ID was not found or not a ServerId type."); + } + + if let Some(DhcpOption::IAAddr(ia_a)) = ia_addr { + let ia_addr_instance = IAAddr { + addr: ia_a.addr, + preferred_life: 3000, + valid_life: 5000, + opts: DhcpOptions::default(), + }; + let mut iana_opts = DhcpOptions::default(); + iana_opts.insert(DhcpOption::IAAddr(ia_addr_instance)); + + let iana_instance = IANA { + id: 123, + t1: 3600, + t2: 7200, + opts: iana_opts, + }; + request_msg + .opts_mut() + .insert(DhcpOption::IANA(iana_instance)); + } else { + warn!("No IP was found in Advertise message"); + } + + if let Some(DhcpOption::IAPrefix(iaprefix)) = ia_pd { + let iapd_instance = IAPD { + id: 456, + t1: 3600, + t2: 7200, + opts: { + let mut opts = DhcpOptions::default(); + opts.insert(DhcpOption::IAPrefix((*iaprefix).clone())); + opts + }, + }; + request_msg + .opts_mut() + .insert(DhcpOption::IAPD(iapd_instance)); + } + + buf.clear(); + request_msg.encode(&mut Encoder::new(&mut buf))?; + socket.send_to(&buf, multicast_address).await?; + } + MessageType::Reply => { + if let Some(DhcpOption::IANA(iana)) = response.opts().get(OptionCode::IANA) { + if let Some(ia_addr_opt) = iana.opts.get(OptionCode::IAAddr) { + ia_addr_confirm = Some((*ia_addr_opt).clone()); + } + } + if let Some(DhcpOption::IAPD(iapd)) = response.opts().get(OptionCode::IAPD) { + if let Some(DhcpOption::IAPrefix(iaprefix)) = + iapd.opts.get(OptionCode::IAPrefix) + { + ia_pd_confirm = Some((*iaprefix).clone()); + } + } + + let mut confirm_msg = Message::new(MessageType::Confirm); + confirm_msg.set_xid(random_xid); + buf.clear(); + confirm_msg.encode(&mut Encoder::new(&mut buf))?; + socket.send_to(&buf, multicast_address).await?; + + break; + } + _ => { + // Ignore other message types + continue; + } + } + } + + if let Some(DhcpOption::IAAddr(ia_a)) = ia_addr_confirm { + let (connection, handle, _) = new_connection()?; + tokio::spawn(connection); + + set_ipv6_address(&handle, &interface_name, ia_a.addr, 128).await?; + info!( + "DHCPv6 processing finished, setting IPv6 address {}", + ia_a.addr + ); + + let prefix_info = ia_pd_confirm.map(|iaprefix| PrefixInfo { + prefix: iaprefix.prefix_ip, + prefix_length: iaprefix.prefix_len, + }); + + if let Some(ref pfx) = prefix_info { + info!( + "Received delegated prefix {} with length {}", + pfx.prefix, pfx.prefix_length + ); + } else { + info!("No prefix delegation received."); + } + + return Ok(Dhcpv6Result { + address: ia_a.addr, + prefix: prefix_info, + }); + } + + Err("No valid address received".into()) +} + +pub async fn set_ipv6_address( + handle: &Handle, + interface_name: &str, + ipv6_addr: Ipv6Addr, + pfx_len: u8, +) -> Result<(), Error> { + let link = handle + .link() + .get() + .match_name(interface_name.to_string()) + .execute() + .try_next() + .await? + .ok_or(Error::RequestFailed)?; + handle + .address() + .add(link.header.index, ipv6_addr.into(), pfx_len) + .execute() + .await +} + +pub async fn get_interface_index(interface_name: String) -> io::Result { + task::spawn_blocking(move || { + if_nametoindex(interface_name.as_str()) + .map_err(|e| io::Error::other(format!("Error getting index: {e}"))) + }) + .await? +} + +fn create_multicast_socket( + interface_name: &str, + interface_index: u32, + lport: u16, +) -> Result> { + let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?; + socket.set_reuse_address(true)?; + socket.set_multicast_if_v6(interface_index)?; + socket.join_multicast_v6(&"ff02::1:2".parse()?, interface_index)?; + socket.bind(&SockAddr::from(SocketAddrV6::new( + Ipv6Addr::UNSPECIFIED, + lport, + 0, + 0, + )))?; + socket.bind_device(Some(interface_name.as_bytes()))?; + socket.set_nonblocking(true)?; + Ok(UdpSocket::from_std(socket.into())?) +} + +pub async fn add_ipv6_route( + handle: &Handle, + interface_name: &str, + destination: Ipv6Addr, + prefix_length: u8, + gateway: Option, + metric: u32, + route_type: RouteType, +) -> Result<(), Error> { + let link = handle + .link() + .get() + .match_name(interface_name.to_string()) + .execute() + .try_next() + .await? + .ok_or(Error::RequestFailed)?; + let mut req = handle.route().add(); + let msg = req.message_mut(); + msg.header.address_family = AddressFamily::Inet6; + msg.header.scope = RouteScope::Universe; + msg.header.protocol = RouteProtocol::Static; + msg.header.kind = route_type; + msg.header.destination_prefix_length = prefix_length; + msg.attributes + .push(RouteAttribute::Destination(RouteAddress::from(destination))); + if route_type == RouteType::Unicast { + if let Some(gw) = gateway { + msg.attributes + .push(RouteAttribute::Gateway(RouteAddress::from(gw))); + } + } + msg.attributes.push(RouteAttribute::Oif(link.header.index)); + msg.attributes.push(RouteAttribute::Priority(metric)); + req.execute().await +} + +pub async fn set_ipv6_gateway( + handle: &Handle, + interface_name: &str, + ipv6_gateway: Ipv6Addr, +) -> Result<(), Error> { + let link = handle + .link() + .get() + .match_name(interface_name.to_string()) + .execute() + .try_next() + .await? + .ok_or(Error::RequestFailed)?; + let mut req = handle.route().add(); + let msg = req.message_mut(); + msg.header.address_family = AddressFamily::Inet6; + msg.header.scope = RouteScope::Universe; + msg.header.protocol = RouteProtocol::Static; + msg.header.kind = RouteType::Unicast; + msg.header.destination_prefix_length = 0; + msg.attributes + .push(RouteAttribute::Gateway(RouteAddress::Inet6(ipv6_gateway))); + msg.attributes.push(RouteAttribute::Oif(link.header.index)); + req.execute().await +} diff --git a/feos/utils/src/network/mod.rs b/feos/utils/src/network/mod.rs new file mode 100644 index 0000000..e0df00a --- /dev/null +++ b/feos/utils/src/network/mod.rs @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +pub mod dhcpv6; +pub mod utils; + +pub use utils::configure_network_devices; +pub use utils::configure_sriov; diff --git a/feos/utils/src/network/utils.rs b/feos/utils/src/network/utils.rs new file mode 100644 index 0000000..5b1306d --- /dev/null +++ b/feos/utils/src/network/utils.rs @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +use super::dhcpv6::*; +use futures::stream::TryStreamExt; +use log::{error, info, warn}; +use netlink_packet_route::route::RouteType; +use rtnetlink::new_connection; +use std::fs::File; +use std::io; +use std::io::Write; +use std::net::Ipv6Addr; +use tokio::fs::{read_link, OpenOptions}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::time::{sleep, Duration}; + +pub const INTERFACE_NAME: &str = "eth0"; + +pub async fn configure_sriov(num_vfs: u32) -> Result<(), String> { + let base_path = format!("/sys/class/net/{INTERFACE_NAME}/device"); + + let autoprobe_path = format!("{base_path}/sriov_drivers_autoprobe"); + info!("Disabling sriov_drivers_autoprobe at {autoprobe_path}"); + let mut autoprobe_file = OpenOptions::new() + .write(true) + .open(&autoprobe_path) + .await + .map_err(|e| format!("Failed to open sriov_drivers_autoprobe: {e}"))?; + autoprobe_file + .write_all(b"0\n") + .await + .map_err(|e| format!("Failed to disable autoprobe: {e}"))?; + + let numvfs_path = format!("{base_path}/sriov_numvfs"); + let mut numvfs_file = OpenOptions::new() + .write(true) + .open(&numvfs_path) + .await + .map_err(|e| e.to_string())?; + + info!("Resetting VFs to 0 for {INTERFACE_NAME}"); + numvfs_file + .write_all(b"0\n") + .await + .map_err(|e| format!("Failed to write 0 to sriov_numvfs: {e}"))?; + sleep(Duration::from_secs(1)).await; + + info!("Creating {num_vfs} sriov virtual functions for {INTERFACE_NAME}"); + numvfs_file + .write_all(format!("{num_vfs}\n").as_bytes()) + .await + .map_err(|e| format!("Failed to write to sriov_numvfs: {e}"))?; + sleep(Duration::from_secs(2)).await; + + let device_path = read_link(&base_path).await.map_err(|e| e.to_string())?; + let pci_address = device_path + .file_name() + .ok_or("No PCI address found".to_string())?; + let pci_address = pci_address.to_str().ok_or("No PCI address found")?; + + info!("Found PCI address of {INTERFACE_NAME}: {pci_address}"); + + let sriov_offset = get_device_information(pci_address, "sriov_offset") + .await + .map_err(|e| e.to_string())?; + + let sriov_offset = sriov_offset.parse::().map_err(|e| e.to_string())?; + + let base_pci_address = parse_pci_address(pci_address)?; + + let virtual_funcs: Vec = (0..num_vfs) + .map(|x| nth_next_pci_address(base_pci_address, x + sriov_offset)) + .map(format_pci_address) + .collect(); + + for vf_pci in virtual_funcs.iter() { + if let Err(e) = bind_vf_to_vfio(vf_pci).await { + return Err(format!("Failed to bind VF {vf_pci} to vfio-pci: {e}")); + } + } + + Ok(()) +} + +fn parse_pci_address(address: &str) -> Result<(u16, u8, u8, u8), String> { + let parts: Vec<&str> = address.split(&[':', '.', ' '][..]).collect(); + if parts.len() != 4 { + return Err("Invalid PCI address format".to_string()); + } + + let domain = u16::from_str_radix(parts[0], 16).map_err(|_| "Invalid domain".to_string())?; + let bus = u8::from_str_radix(parts[1], 16).map_err(|_| "Invalid bus".to_string())?; + let slot = u8::from_str_radix(parts[2], 16).map_err(|_| "Invalid slot".to_string())?; + let function = u8::from_str_radix(parts[3], 16).map_err(|_| "Invalid function".to_string())?; + + Ok((domain, bus, slot, function)) +} + +fn nth_next_pci_address(address: (u16, u8, u8, u8), n: u32) -> (u16, u8, u8, u8) { + let (domain, bus, slot, function) = address; + let total_functions = (domain as u32) * 256 * 32 * 8 + + (bus as u32) * 32 * 8 + + (slot as u32) * 8 + + function as u32 + + n; + + let new_domain = (total_functions / (256 * 32 * 8)) as u16; + let remaining = total_functions % (256 * 32 * 8); + let new_bus = (remaining / (32 * 8)) as u8; + let remaining = remaining % (32 * 8); + let new_slot = (remaining / 8) as u8; + let new_function = (remaining % 8) as u8; + + (new_domain, new_bus, new_slot, new_function) +} + +fn format_pci_address(address: (u16, u8, u8, u8)) -> String { + let (domain, bus, slot, function) = address; + format!("{domain:04x}:{bus:02x}:{slot:02x}.{function}") +} + +async fn bind_vf_to_vfio(pci_address: &str) -> Result<(), io::Error> { + let override_path = format!("/sys/bus/pci/devices/{pci_address}/driver_override"); + let mut override_file = OpenOptions::new().write(true).open(&override_path).await?; + override_file.write_all(b"vfio-pci").await?; + + let bind_path = "/sys/bus/pci/drivers/vfio-pci/bind"; + let mut bind_file = OpenOptions::new().write(true).open(bind_path).await?; + bind_file.write_all(pci_address.as_bytes()).await?; + + Ok(()) +} + +async fn get_device_information(pci: &str, field: &str) -> Result { + let path = format!("/sys/bus/pci/devices/{pci}/{field}"); + let mut file = OpenOptions::new().read(true).open(&path).await?; + + let mut dst = String::new(); + file.read_to_string(&mut dst).await?; + + Ok(dst.trim().to_string()) +} + +pub async fn configure_network_devices() -> Result, String> { + let ignore_ra_flag = true; // Till the RA has the correct flags (O or M), ignore the flag + let interface_name = String::from(INTERFACE_NAME); + let (connection, handle, _) = new_connection().unwrap(); + let mut delegated_prefix_option: Option<(Ipv6Addr, u8)> = None; + tokio::spawn(connection); + + enable_ipv6_forwarding().map_err(|e| format!("Failed to enable ipv6 forwarding: {e}"))?; + + let mut link_ts = handle + .link() + .get() + .match_name(interface_name.clone()) + .execute(); + + let link = link_ts + .try_next() + .await + .map_err(|e| format!("{interface_name} not found: {e}"))? + .ok_or("Link not found".to_string())?; + + handle + .link() + .set(link.header.index) + .up() + .execute() + .await + .map_err(|e| format!("{interface_name} can not be set up: {e}"))?; + + info!("{interface_name}:"); + for attr in link.attributes { + match attr { + netlink_packet_route::link::LinkAttribute::Address(mac_bytes) => { + info!(" mac: {}", format_mac(mac_bytes.clone())); + } + netlink_packet_route::link::LinkAttribute::Carrier(carrier) => { + info!(" carrier: {carrier}"); + } + netlink_packet_route::link::LinkAttribute::Mtu(mtu) => { + info!(" mtu: {mtu}"); + } + _ => (), + } + } + + if let Some(ipv6_gateway) = is_dhcpv6_needed(interface_name.clone(), ignore_ra_flag) { + sleep(Duration::from_secs(4)).await; + match run_dhcpv6_client(interface_name.clone()).await { + Ok(result) => { + send_neigh_solicitation(interface_name.clone(), &ipv6_gateway, &result.address); + if let Some(prefix_info) = result.prefix { + let delegated_prefix = prefix_info.prefix; + let prefix_length = prefix_info.prefix_length; + info!( + "Received delegated prefix {delegated_prefix} with length {prefix_length}" + ); + delegated_prefix_option = Some((delegated_prefix, prefix_length)); + if let Err(e) = add_ipv6_route( + &handle, + INTERFACE_NAME, + delegated_prefix, + prefix_length, + None, + 1024, + RouteType::Unreachable, + ) + .await + { + error!("Failed to add unreachable IPv6 route: {e}"); + } + } else { + info!("No prefix delegation received."); + } + info!("Setting IPv6 gateway to {ipv6_gateway} on interface {interface_name}"); + if let Err(e) = set_ipv6_gateway(&handle, &interface_name, ipv6_gateway).await { + warn!("Failed to set IPv6 gateway: {e}"); + } + } + Err(e) => warn!("Error running DHCPv6 client: {e}"), + } + } + + Ok(delegated_prefix_option) +} + +pub fn enable_ipv6_forwarding() -> Result<(), std::io::Error> { + File::create("/proc/sys/net/ipv6/conf/all/forwarding")?.write_all(b"1")?; + Ok(()) +} + +fn format_mac(bytes: Vec) -> String { + bytes + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::>() + .join(":") +} diff --git a/hack/Dockerfile.nginx b/hack/Dockerfile.nginx deleted file mode 100644 index d99f5d5..0000000 --- a/hack/Dockerfile.nginx +++ /dev/null @@ -1,14 +0,0 @@ -FROM nginx:alpine - -# Copy the UKI file to nginx html directory -COPY target/uki.efi /usr/share/nginx/html/feos.uki - -# Add MIME type for .uki files -RUN echo "types {" > /etc/nginx/mime.types -RUN echo " application/efi uki;" >> /etc/nginx/mime.types -RUN echo "}" >> /etc/nginx/mime.types - -# Create a simple index page -RUN echo '

FeOS UKI Server

Download FeOS UKI (x86_64)

' > /usr/share/nginx/html/index.html - -EXPOSE 80 diff --git a/hack/kernel/cmdline.txt b/hack/kernel/cmdline.txt index 934257a..235f637 100644 --- a/hack/kernel/cmdline.txt +++ b/hack/kernel/cmdline.txt @@ -1 +1 @@ -console=tty0 console=ttyS0,115200 intel_iommu=on iommu=pt +console=tty0 diff --git a/hack/kernel/config/feos-linux-6.12.21.config b/hack/kernel/config/feos-linux-6.12.21.config index f7b4cc5..7e69cd1 100644 --- a/hack/kernel/config/feos-linux-6.12.21.config +++ b/hack/kernel/config/feos-linux-6.12.21.config @@ -2986,7 +2986,7 @@ CONFIG_AMD_IOMMU=y CONFIG_DMAR_TABLE=y CONFIG_INTEL_IOMMU=y CONFIG_INTEL_IOMMU_SVM=y -# CONFIG_INTEL_IOMMU_DEFAULT_ON is not set +CONFIG_INTEL_IOMMU_DEFAULT_ON=y CONFIG_INTEL_IOMMU_FLOPPY_WA=y CONFIG_INTEL_IOMMU_SCALABLE_MODE_DEFAULT_ON=y CONFIG_INTEL_IOMMU_PERF_EVENTS=y diff --git a/proto/container.proto b/proto/container.proto deleted file mode 100644 index 930a20f..0000000 --- a/proto/container.proto +++ /dev/null @@ -1,41 +0,0 @@ -syntax = "proto3"; -package container; - -service ContainerService { - rpc CreateContainer (CreateContainerRequest) returns (CreateContainerResponse); - rpc RunContainer (RunContainerRequest) returns (RunContainerResponse); - rpc KillContainer (KillContainerRequest) returns (KillContainerResponse); - rpc StateContainer (StateContainerRequest) returns (StateContainerResponse); - rpc DeleteContainer (DeleteContainerRequest) returns (DeleteContainerResponse); -} - -message CreateContainerRequest { - string image = 1; - repeated string command = 2; -} -message CreateContainerResponse { - string uuid = 1; -} - -message RunContainerRequest { - string uuid = 1; -} -message RunContainerResponse {} - -message KillContainerRequest { - string uuid = 1; -} -message KillContainerResponse {} - -message StateContainerRequest { - string uuid = 1; -} -message StateContainerResponse { - string state = 1; - optional int32 pid = 2; -} - -message DeleteContainerRequest { - string uuid = 1; -} -message DeleteContainerResponse {} \ No newline at end of file diff --git a/proto/feos.proto b/proto/feos.proto deleted file mode 100644 index 3df8009..0000000 --- a/proto/feos.proto +++ /dev/null @@ -1,126 +0,0 @@ -syntax = "proto3"; -package feos_grpc; - -service FeosGrpc { - rpc Reboot (RebootRequest) returns (RebootResponse); - rpc Shutdown (ShutdownRequest) returns (ShutdownResponse); - rpc HostInfo (HostInfoRequest) returns (HostInfoResponse); - - rpc Ping (Empty) returns (Empty); - - rpc FetchImage (FetchImageRequest) returns (FetchImageResponse); - - rpc CreateVM (CreateVMRequest) returns (CreateVMResponse); - rpc GetVM (GetVMRequest) returns (GetVMResponse); - rpc BootVM (BootVMRequest) returns (BootVMResponse); - rpc ConsoleVM (stream ConsoleVMRequest) returns (stream ConsoleVMResponse); - rpc AttachNicVM (AttachNicVMRequest) returns (AttachNicVMResponse); - rpc ShutdownVM (ShutdownVMRequest) returns (ShutdownVMResponse); - rpc PingVM (PingVMRequest) returns (PingVMResponse); - - rpc GetFeOSKernelLogs (GetFeOSKernelLogRequest) returns (stream GetFeOSKernelLogResponse); - rpc GetFeOSLogs(GetFeOsLogRequest) returns (stream GetFeOsLogResponse); -} - -enum NicType { - MAC = 0; - PCI = 1; - TAP = 2; -} - -message ConsoleVMRequest { - string uuid = 1; - string input = 2; -} - -message ConsoleVMResponse { - string message = 1; -} - -message GetFeOSKernelLogRequest {} - -message GetFeOSKernelLogResponse { - string message = 1; -} - -message GetFeOsLogRequest {} - -message GetFeOsLogResponse { - string message = 1; -} - -message AttachNicVMRequest { - string uuid = 1; - NicType nic_type = 2; - - oneof nic_data { - string mac_address = 3; - string pci_address = 4; - } -} - -message AttachNicVMResponse {} - -message RebootRequest {} -message RebootResponse {} - -message ShutdownRequest {} -message ShutdownResponse {} - -message HostInfoRequest {} -message HostInfoResponse { - uint64 uptime = 1; - uint64 ram_total = 2; - uint64 ram_unused = 3; - uint64 num_cores = 4; - repeated NetInterface net_interfaces = 5; -} - -message NetInterface { - string name = 1; - string pci_address = 2; - string mac_address = 3; -} - -message Empty {} - -message FetchImageRequest { - string image = 1; -} - -message FetchImageResponse { - string uuid = 1; -} - -message CreateVMRequest { - uint32 cpu = 1; - uint64 memory_bytes = 2; - string image_uuid = 3; - optional string ignition = 4; -} - -message CreateVMResponse { - string uuid = 1; -} - -message GetVMRequest { - string uuid = 1; -} -message GetVMResponse { - string info = 1; -} - -message BootVMRequest { - string uuid = 1; -} -message BootVMResponse {} - -message ShutdownVMRequest { - string uuid = 1; -} -message ShutdownVMResponse {} - -message PingVMRequest { - string uuid = 1; -} -message PingVMResponse {} diff --git a/proto/isolated_container.proto b/proto/isolated_container.proto deleted file mode 100644 index bbad95a..0000000 --- a/proto/isolated_container.proto +++ /dev/null @@ -1,35 +0,0 @@ -syntax = "proto3"; -package isolated_container; - -service IsolatedContainerService { - rpc CreateContainer (CreateContainerRequest) returns (CreateContainerResponse); - rpc RunContainer (RunContainerRequest) returns (RunContainerResponse); - rpc StopContainer (StopContainerRequest) returns (StopContainerResponse); - rpc StateContainer (StateContainerRequest) returns (StateContainerResponse); -} - -message CreateContainerRequest { - string image = 1; - repeated string command = 2; -} -message CreateContainerResponse { - string uuid = 1; -} - -message RunContainerRequest { - string uuid = 1; -} -message RunContainerResponse {} - -message StopContainerRequest { - string uuid = 1; -} -message StopContainerResponse {} - -message StateContainerRequest { - string uuid = 1; -} -message StateContainerResponse { - string state = 1; - optional int32 pid = 2; -} diff --git a/proto/v1/host.proto b/proto/v1/host.proto new file mode 100644 index 0000000..c1cde71 --- /dev/null +++ b/proto/v1/host.proto @@ -0,0 +1,196 @@ +syntax = "proto3"; + +package feos.host.v1; + +import "google/protobuf/timestamp.proto"; + +// HostService provides information about the host system. +service HostService { + // Retrieves the hostname of the machine running the service. + rpc Hostname(HostnameRequest) returns (HostnameResponse); + rpc GetMemory(MemoryRequest) returns (MemoryResponse); + rpc GetCPUInfo(GetCPUInfoRequest) returns (GetCPUInfoResponse); + // Retrieves statistics for all network interfaces. + rpc GetNetworkInfo(GetNetworkInfoRequest) returns (GetNetworkInfoResponse); + rpc Shutdown(ShutdownRequest) returns (ShutdownResponse); + rpc Reboot(RebootRequest) returns (RebootResponse); + + // Triggers an upgrade of the running FeOS binary by pulling it from a URL. + rpc UpgradeFeosBinary(UpgradeFeosBinaryRequest) returns (UpgradeFeosBinaryResponse); + + // Streams kernel log messages from /dev/kmsg. + rpc StreamKernelLogs(StreamKernelLogsRequest) returns (stream KernelLogEntry); + + // Streams logs from the internal FeOS logger. + rpc StreamFeOSLogs(StreamFeosLogsRequest) returns (stream FeosLogEntry); + + // Retrieves version information about the host system. + rpc GetVersionInfo(GetVersionInfoRequest) returns (GetVersionInfoResponse); +} + +message HostnameRequest {} + +message HostnameResponse { + string hostname = 1; +} + +message UpgradeFeosBinaryRequest { + // The URL from which to download the new binary. + string url = 1; + // The SHA256 checksum of the binary file, hex-encoded, for verification. + string sha256_sum = 2; +} + +message UpgradeFeosBinaryResponse {} + +message StreamKernelLogsRequest {} + +message KernelLogEntry { + // A single raw log line from the kernel log buffer. + string message = 1; +} + +message ShutdownRequest {} + +message ShutdownResponse {} + +message RebootRequest {} + +message RebootResponse {} + +message MemoryRequest {} + +message MemoryResponse { + MemInfo mem_info = 1; +} + +message MemInfo { + uint64 memtotal = 1; + uint64 memfree = 2; + uint64 memavailable = 3; + uint64 buffers = 4; + uint64 cached = 5; + uint64 swapcached = 6; + uint64 active = 7; + uint64 inactive = 8; + uint64 activeanon = 9; + uint64 inactiveanon = 10; + uint64 activefile = 11; + uint64 inactivefile = 12; + uint64 unevictable = 13; + uint64 mlocked = 14; + uint64 swaptotal = 15; + uint64 swapfree = 16; + uint64 dirty = 17; + uint64 writeback = 18; + uint64 anonpages = 19; + uint64 mapped = 20; + uint64 shmem = 21; + uint64 slab = 22; + uint64 sreclaimable = 23; + uint64 sunreclaim = 24; + uint64 kernelstack = 25; + uint64 pagetables = 26; + uint64 nfsunstable = 27; + uint64 bounce = 28; + uint64 writebacktmp = 29; + uint64 commitlimit = 30; + uint64 committedas = 31; + uint64 vmalloctotal = 32; + uint64 vmallocused = 33; + uint64 vmallocchunk = 34; + uint64 hardwarecorrupted = 35; + uint64 anonhugepages = 36; + uint64 shmemhugepages = 37; + uint64 shmempmdmapped = 38; + uint64 cmatotal = 39; + uint64 cmafree = 40; + uint64 hugepagestotal = 41; + uint64 hugepagesfree = 42; + uint64 hugepagesrsvd = 43; + uint64 hugepagessurp = 44; + uint64 hugepagesize = 45; + uint64 directmap4k = 46; + uint64 directmap2m = 47; + uint64 directmap1g = 48; +} + +message GetCPUInfoRequest {} + +message GetCPUInfoResponse { + repeated CPUInfo cpu_info = 1; +} + +message CPUInfo { + uint32 processor = 1; + string vendor_id = 2; + string cpu_family = 3; + string model = 4; + string model_name = 5; + string stepping = 6; + string microcode = 7; + double cpu_mhz = 8; + string cache_size = 9; + string physical_id = 10; + uint32 siblings = 11; + string core_id = 12; + uint32 cpu_cores = 13; + string apic_id = 14; + string initial_apic_id = 15; + string fpu = 16; + string fpu_exception = 17; + uint32 cpu_id_level = 18; + string wp = 19; + repeated string flags = 20; + repeated string bugs = 21; + double bogo_mips = 22; + uint32 cl_flush_size = 23; + uint32 cache_alignment = 24; + string address_sizes = 25; + string power_management = 26; +} + +message GetNetworkInfoRequest {} + +message GetNetworkInfoResponse { + repeated NetDev devices = 1; +} + +message NetDev { + string name = 1; + uint64 rx_bytes = 2; + uint64 rx_packets = 3; + uint64 rx_errors = 4; + uint64 rx_dropped = 5; + uint64 rx_fifo = 6; + uint64 rx_frame = 7; + uint64 rx_compressed = 8; + uint64 rx_multicast = 9; + uint64 tx_bytes = 10; + uint64 tx_packets = 11; + uint64 tx_errors = 12; + uint64 tx_dropped = 13; + uint64 tx_fifo = 14; + uint64 tx_collisions = 15; + uint64 tx_carrier = 16; + uint64 tx_compressed = 17; +} + +message StreamFeosLogsRequest {} + +message FeosLogEntry { + uint64 seq = 1; + google.protobuf.Timestamp timestamp = 2; + string level = 3; + string target = 4; + string message = 5; +} + +message GetVersionInfoRequest {} + +message GetVersionInfoResponse { + // The version of the Linux kernel (e.g., from /proc/version). + string kernel_version = 1; + // The version of the running FeOS binary. + string feos_version = 2; +} \ No newline at end of file diff --git a/proto/v1/image.proto b/proto/v1/image.proto new file mode 100644 index 0000000..cb6b764 --- /dev/null +++ b/proto/v1/image.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +package feos.image.vmm.api.v1; + +// Image Service + +// ImageService handles the lifecycle of OCI images used for booting VMs. +service ImageService { + // Initiates the pull of an OCI image from a remote registry. + // This call is non-blocking. It returns a UUID for the image immediately, + // which can then be used to track the download status. + rpc PullImage(PullImageRequest) returns (PullImageResponse); + + // Watches the status of an image pull operation. This is a server-streaming + // call that will send updates as the image's state changes. The stream + // closes when the image pull reaches a terminal state (READY or PULL_FAILED). + rpc WatchImageStatus(WatchImageStatusRequest) returns (stream ImageStatusResponse); + + // Lists all images available locally in the service's cache. + rpc ListImages(ListImagesRequest) returns (ListImagesResponse); + + // Removes a locally cached image. + rpc DeleteImage(DeleteImageRequest) returns (DeleteImageResponse); +} + +enum ImageState { + IMAGE_STATE_UNSPECIFIED = 0; + // The requested image UUID is not known to the service. + NOT_FOUND = 1; + // The image pull is in progress. + DOWNLOADING = 2; + // The image is downloaded, unpacked, and ready for use. + READY = 3; + // The image pull failed. The 'message' field in ImageStatusResponse will have details. + PULL_FAILED = 4; +} + +message ImageInfo { + // Server-generated unique ID for the image. + string image_uuid = 1; + // The original reference used to pull the image (e.g., "alpine:latest"). + string image_ref = 2; + // The current state of the image. + ImageState state = 3; +} + +message PullImageRequest { + // The full reference to the OCI image, including the registry and tag. + // e.g., "docker.io/library/alpine:latest" + string image_ref = 1; +} + +message PullImageResponse { + // The server-generated unique ID for the image being pulled. + // Use this UUID to watch the download status and to create a VM. + string image_uuid = 1; +} + +message WatchImageStatusRequest { + string image_uuid = 1; +} + +message ImageStatusResponse { + ImageState state = 1; + // Optional: Progress percentage (0-100) when state is DOWNLOADING. + uint32 progress_percent = 2; + // Optional: A human-readable message, especially useful for PULL_FAILED state. + string message = 3; +} + +message ListImagesRequest {} + +message ListImagesResponse { + repeated ImageInfo images = 1; +} + +message DeleteImageRequest { + string image_uuid = 1; +} + +message DeleteImageResponse {} \ No newline at end of file diff --git a/proto/v1/vm.proto b/proto/v1/vm.proto new file mode 100644 index 0000000..bcf40e0 --- /dev/null +++ b/proto/v1/vm.proto @@ -0,0 +1,244 @@ +syntax = "proto3"; + +package feos.vm.vmm.api.v1; + +import "google/protobuf/any.proto"; + +// VMService is a service that manages multiple Cloud Hypervisor +// instances. It abstracts the underlying REST API of each individual VMM process, +// allowing a client to create, control, and delete virtual machines and their +// devices without dealing with process management or API sockets. +service VMService { + // Creates a new Virtual Machine but does not boot it. + // This starts a cloud-hypervisor process, sets up its initial resources, + // and makes it ready for booting. The returned 'vm_id' must be used for + // all subsequent operations on this VM. + rpc CreateVm(CreateVmRequest) returns (CreateVmResponse); + // Starts a previously created Virtual Machine. + rpc StartVm(StartVmRequest) returns (StartVmResponse); + // Retrieves detailed information about a specific Virtual Machine. + rpc GetVm(GetVmRequest) returns (VmInfo); + // Retrieves the events stream for a specific Virtual Machine. + rpc StreamVmEvents(StreamVmEventsRequest) returns (stream VmEvent); + // Deletes an existing Virtual Machine and frees up its resources. + rpc DeleteVm(DeleteVmRequest) returns (DeleteVmResponse); + // Provides an interactive console to a running VM. + // This is a bidirectional stream. The client first sends an 'attach' message + // with the VM ID, then streams user input. The server streams back the + // VM's console output. + rpc StreamVmConsole (stream StreamVmConsoleRequest) returns (stream StreamVmConsoleResponse); + // Lists all Virtual Machines currently managed by this service. + rpc ListVms(ListVmsRequest) returns (ListVmsResponse); + // Pings the VMM process for a specific VM to check for liveness. + rpc PingVm(PingVmRequest) returns (PingVmResponse); + // Shuts down a running Virtual Machine by sending an ACPI shutdown signal. + rpc ShutdownVm(ShutdownVmRequest) returns (ShutdownVmResponse); + // Pauses a running Virtual Machine. + rpc PauseVm(PauseVmRequest) returns (PauseVmResponse); + // Resumes a paused Virtual Machine. + rpc ResumeVm(ResumeVmRequest) returns (ResumeVmResponse); + // Hot-plugs a new disk to a running VM. + rpc AttachDisk(AttachDiskRequest) returns (AttachDiskResponse); + // Hot-unplugs a disk from a running VM. + rpc RemoveDisk(RemoveDiskRequest) returns (RemoveDiskResponse); +} + +// Request stream from client to server for StreamVmConsole +message StreamVmConsoleRequest { + // The first message from the client MUST be an 'attach' message. + // All subsequent messages MUST be 'data' messages. + oneof payload { + AttachConsoleMessage attach = 1; + ConsoleData data = 2; + } +} + +// Initial message to specify which VM to connect to. +message AttachConsoleMessage { + string vm_id = 1; +} + +// Subsequent messages carrying user input. +message ConsoleData { + bytes input = 1; +} + +// Response stream from server to client for StreamVmConsole +message StreamVmConsoleResponse { + bytes output = 1; +} + +message VmStateChangedEvent { + VmState new_state = 1; + // An optional human-readable reason for the state change. + // e.g., "VM booted successfully" or "Shutdown signal received" + string reason = 2; +} + +message StreamVmEventsRequest { + // The ID of the Virtual Machine for which to retrieve events. + // If not provided, the stream will start by sending the current state + // of all existing VMs, and then continue to stream all subsequent events + // from all VMs. + optional string vm_id = 1; + // Filter the stream to only include events from a specific "component" in VM + string with_component_id = 5; + + oneof streaming_mode { + // 1. Get the last N events + int32 tail_events = 2; + // 2. Get all events that have occurred since a specific event ID + string tail_id = 3; + // 3. Get all events from the last N seconds + int32 tail_seconds = 4; + } +} + +message VmEvent { + string vm_id = 1; + google.protobuf.Any data = 2; + // A unique identifier for the event within the VM context. + string id = 3; + // The ID of the component that generated this event. + string component_id = 4; +} + +message CreateVmRequest { + VmConfig config = 1; + optional string vm_id = 2; +} + +message CreateVmResponse { + string vm_id = 1; +} + +message GetVmRequest { + string vm_id = 1; +} + +message VmConfig { + CpuConfig cpus = 1; + MemoryConfig memory = 2; + // The full reference to the OCI image, including the registry and tag. + string image_ref = 3; + // Additional data disks to attach to the VM. The root filesystem is + // expected to be part of the OCI image specified by vm_image_uuid. + repeated DiskConfig disks = 4; + repeated NetConfig net = 5; + optional string ignition = 6; +} + +message CpuConfig { + uint32 boot_vcpus = 1; + uint32 max_vcpus = 2; +} + +message MemoryConfig { + uint64 size_mib = 1; // Memory size in Megabytes (MiB). + bool hugepages = 2; +} + +message DiskConfig { + string device_id = 1; + oneof backend { + string path = 2; // Path on the host to the disk image file. + VfioPciConfig vfio_pci = 3; + } + bool readonly = 4; +} + +message NetConfig { + string device_id = 1; + oneof backend { + TapConfig tap = 2; + VfioPciConfig vfio_pci = 3; + } + string mac_address = 4; +} + +message TapConfig { + string tap_name = 1; +} + +message VfioPciConfig { + string bdf = 1; // e.g., "0000:03:00.0" +} + +enum VmState { + VM_STATE_UNSPECIFIED = 0; + VM_STATE_CREATING = 1; + VM_STATE_CREATED = 2; + VM_STATE_RUNNING = 3; + VM_STATE_PAUSED = 4; + VM_STATE_STOPPED = 5; + VM_STATE_CRASHED = 6; +} + +message VmInfo { + string vm_id = 1; + VmState state = 2; + VmConfig config = 3; +} + +message PingVmRequest { + string vm_id = 1; +} + +message PingVmResponse { + string build_version = 1; + string version = 2; + int64 pid = 3; + repeated string features = 4; +} + +message DeleteVmRequest { + string vm_id = 1; +} + +message StartVmRequest { + string vm_id = 1; +} + +message ListVmsRequest {} + +message ListVmsResponse { + repeated VmInfo vms = 1; +} + +message ShutdownVmRequest { + string vm_id = 1; +} + +message PauseVmRequest { + string vm_id = 1; +} + +message ResumeVmRequest { + string vm_id = 1; +} + +message AttachDiskRequest { + string vm_id = 1; + DiskConfig disk = 2; +} + +message AttachDiskResponse { + string device_id = 1; +} + +message RemoveDiskRequest { + string vm_id = 1; + string device_id = 2; +} + +message StartVmResponse {} + +message DeleteVmResponse {} + +message ShutdownVmResponse {} + +message PauseVmResponse {} + +message ResumeVmResponse {} + +message RemoveDiskResponse {} \ No newline at end of file diff --git a/src/bin/feos_cli/client.rs b/src/bin/feos_cli/client.rs deleted file mode 100644 index fbd60df..0000000 --- a/src/bin/feos_cli/client.rs +++ /dev/null @@ -1,242 +0,0 @@ -use std::time::Duration; -use structopt::StructOpt; -use tokio::io::{self, AsyncBufReadExt}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; -use tonic::transport::Endpoint; -use tonic::Request; - -use crate::client_container::ContainerCommand; -use crate::client_isolated_container::IsolatedContainerCommand; -use feos_grpc::feos_grpc_client::FeosGrpcClient; -use feos_grpc::*; - -pub mod feos_grpc { - tonic::include_proto!("feos_grpc"); -} - -#[derive(StructOpt, Debug)] -#[structopt(name = "feos-cli")] -pub struct Opt { - #[structopt(short, long, default_value = "::1")] - pub server_ip: String, - #[structopt(short, long, default_value = "1337")] - pub port: u16, - #[structopt(subcommand)] - pub cmd: Command, -} - -#[derive(StructOpt, Debug)] -pub enum Command { - Reboot, - Shutdown, - HostInfo, - Ping, - FetchImage { - image: String, - }, - CreateVM { - cpu: u32, - memory_bytes: u64, - image_uuid: String, - ignition: Option, - }, - GetVM { - uuid: String, - }, - BootVM { - uuid: String, - }, - ShutdownVM { - uuid: String, - }, - PingVM { - uuid: String, - }, - AttachNicVM { - uuid: String, - #[structopt(long)] - mac_address: Option, - #[structopt(long)] - pci_address: Option, - }, - GetFeOSKernelLogs, - GetFeOSLogs, - ConsoleVM { - uuid: String, - }, - Container(ContainerCommand), - IsolatedContainer(IsolatedContainerCommand), -} - -fn format_address(ip: &str, port: u16) -> String { - if ip.contains(':') { - // IPv6 address - format!("http://[{}]:{}", ip, port) - } else { - // IPv4 address - format!("http://{}:{}", ip, port) - } -} - -pub async fn run_client(opt: Opt) -> Result<(), Box> { - let address = format_address(&opt.server_ip, opt.port); - let endpoint = Endpoint::from_shared(address)? - .keep_alive_while_idle(true) - .keep_alive_timeout(Duration::from_secs(20)); - - let channel = endpoint.connect().await?; - let mut client = FeosGrpcClient::new(channel); - - match opt.cmd { - Command::Container(container_cmd) => { - crate::client_container::run_container_client(opt.server_ip, opt.port, container_cmd) - .await?; - } - Command::IsolatedContainer(container_cmd) => { - crate::client_isolated_container::run_isolated_container_client( - opt.server_ip, - opt.port, - container_cmd, - ) - .await?; - } - Command::Reboot => { - let request = Request::new(RebootRequest {}); - let response = client.reboot(request).await?; - println!("REBOOT RESPONSE={:?}", response); - } - Command::Shutdown => { - let request = Request::new(ShutdownRequest {}); - let response = client.shutdown(request).await?; - println!("SHUTDOWN RESPONSE={:?}", response); - } - Command::HostInfo => { - let request = Request::new(HostInfoRequest {}); - let response = client.host_info(request).await?; - println!("HOST INFO RESPONSE={:?}", response); - } - Command::Ping => { - let request = Request::new(Empty {}); - let response = client.ping(request).await?; - println!("PING RESPONSE={:?}", response); - } - Command::FetchImage { image } => { - let request = Request::new(FetchImageRequest { image }); - let response = client.fetch_image(request).await?; - println!("FETCH IMAGE RESPONSE={:?}", response); - } - Command::CreateVM { - cpu, - memory_bytes, - image_uuid, - ignition, - } => { - let request = Request::new(CreateVmRequest { - cpu, - memory_bytes, - image_uuid, - ignition, - }); - let response = client.create_vm(request).await?; - println!("CREATE VM RESPONSE={:?}", response); - } - Command::GetVM { uuid } => { - let request = Request::new(GetVmRequest { uuid }); - let response = client.get_vm(request).await?; - println!("GET VM RESPONSE={:?}", response); - } - Command::BootVM { uuid } => { - let request = Request::new(BootVmRequest { uuid }); - let response = client.boot_vm(request).await?; - println!("BOOT VM RESPONSE={:?}", response); - } - Command::PingVM { uuid } => { - let request = Request::new(PingVmRequest { uuid }); - let response = client.ping_vm(request).await?; - println!("PING VM RESPONSE={:?}", response); - } - Command::ShutdownVM { uuid } => { - let request = Request::new(ShutdownVmRequest { uuid }); - let response = client.shutdown_vm(request).await?; - println!("SHUTDOWN VM RESPONSE={:?}", response); - } - Command::AttachNicVM { - uuid, - mac_address, - pci_address, - } => { - let (nic_type, nic_data) = match (&mac_address, &pci_address) { - (Some(mac), None) => ( - feos_grpc::NicType::Mac as i32, - Some(attach_nic_vm_request::NicData::MacAddress(mac.clone())), - ), - (None, Some(pci)) => ( - feos_grpc::NicType::Pci as i32, - Some(attach_nic_vm_request::NicData::PciAddress(pci.clone())), - ), - (None, None) => (feos_grpc::NicType::Tap as i32, None), - (Some(_), Some(_)) => { - eprintln!("Error: Provide either --mac_address or --pci_address, not both."); - return Err(Box::new(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "Provide either --mac_address or --pci_address, not both.", - ))); - } - }; - - let request = Request::new(AttachNicVmRequest { - uuid, - nic_type, - nic_data, - }); - - let response = client.attach_nic_vm(request).await?; - println!("ATTACH NIC VM RESPONSE={:?}", response); - } - Command::GetFeOSKernelLogs => { - let request = Request::new(GetFeOsKernelLogRequest {}); - let mut response = client.get_fe_os_kernel_logs(request).await?.into_inner(); - - while let Some(log_response) = response.message().await? { - println!("FEOS KERNEL LOG RESPONSE={:?}", log_response); - } - } - Command::GetFeOSLogs => { - let request = Request::new(GetFeOsLogRequest {}); - let mut response = client.get_fe_os_logs(request).await?.into_inner(); - - while let Some(log_response) = response.message().await? { - println!("FEOS LOG RESPONSE={:?}", log_response); - } - } - Command::ConsoleVM { uuid } => { - let (tx, rx) = mpsc::channel(4); - - tokio::spawn(async move { - let mut reader = io::BufReader::new(io::stdin()).lines(); - while let Some(line) = reader.next_line().await.unwrap_or_else(|e| { - println!("Failed to read line from stdin: {:?}", e); - None - }) { - let request = ConsoleVmRequest { - uuid: uuid.clone(), - input: line, - }; - if tx.send(request).await.is_err() { - break; - } - } - }); - - let request_stream = ReceiverStream::new(rx); - let mut response = client.console_vm(request_stream).await?.into_inner(); - - while let Some(response) = response.message().await? { - print!("{}", response.message); - } - } - } - - Ok(()) -} diff --git a/src/bin/feos_cli/client_container.rs b/src/bin/feos_cli/client_container.rs deleted file mode 100644 index 970667f..0000000 --- a/src/bin/feos_cli/client_container.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::time::Duration; -use structopt::StructOpt; -use tokio::time::timeout; -use tonic::transport::Endpoint; -use tonic::Request; - -use container_grpc::container_service_client::ContainerServiceClient; -use container_grpc::*; - -pub mod container_grpc { - tonic::include_proto!("container"); -} - -#[derive(StructOpt, Debug)] -pub enum ContainerCommand { - Create { - image: String, - #[structopt(name = "COMMAND", required = true, min_values = 1)] - command: Vec, - }, - Run { - uuid: String, - }, - Kill { - uuid: String, - }, - State { - uuid: String, - }, - Delete { - uuid: String, - }, -} - -pub async fn run_container_client( - server_ip: String, - port: u16, - cmd: ContainerCommand, -) -> Result<(), Box> { - let address = format!("http://{}:{}", server_ip, port); - let channel = Endpoint::from_shared(address)?.connect().await?; - let mut client = ContainerServiceClient::new(channel); - - match cmd { - ContainerCommand::Create { image, command } => { - let request = Request::new(CreateContainerRequest { image, command }); - match timeout(Duration::from_secs(30), client.create_container(request)).await { - Ok(response) => match response { - Ok(response) => println!("CREATE CONTAINER RESPONSE={:?}", response), - Err(e) => eprintln!("Error creating container: {:?}", e), - }, - Err(_) => eprintln!("Request timed out after 30 seconds"), - } - } - ContainerCommand::Run { uuid } => { - let request = Request::new(RunContainerRequest { uuid }); - match client.run_container(request).await { - Ok(response) => println!("RUN CONTAINER RESPONSE={:?}", response), - Err(e) => eprintln!("Error running container: {:?}", e), - } - } - ContainerCommand::Kill { uuid } => { - let request = Request::new(KillContainerRequest { uuid }); - match client.kill_container(request).await { - Ok(response) => println!("KILL CONTAINER RESPONSE={:?}", response), - Err(e) => eprintln!("Error killing container: {:?}", e), - } - } - ContainerCommand::State { uuid } => { - let request = Request::new(StateContainerRequest { uuid }); - match client.state_container(request).await { - Ok(response) => println!("STATE CONTAINER RESPONSE={:?}", response), - Err(e) => eprintln!("Error getting container state: {:?}", e), - } - } - ContainerCommand::Delete { uuid } => { - let request = Request::new(DeleteContainerRequest { uuid }); - match client.delete_container(request).await { - Ok(response) => println!("DELETE CONTAINER RESPONSE={:?}", response), - Err(e) => eprintln!("Error deleting container: {:?}", e), - } - } - } - - Ok(()) -} diff --git a/src/bin/feos_cli/client_isolated_container.rs b/src/bin/feos_cli/client_isolated_container.rs deleted file mode 100644 index 5801b98..0000000 --- a/src/bin/feos_cli/client_isolated_container.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::time::Duration; -use structopt::StructOpt; -use tokio::time::timeout; -use tonic::transport::Endpoint; -use tonic::Request; - -use isolated_container_grpc::isolated_container_service_client::IsolatedContainerServiceClient; -use isolated_container_grpc::*; - -pub mod isolated_container_grpc { - tonic::include_proto!("isolated_container"); -} - -#[derive(StructOpt, Debug)] -pub enum IsolatedContainerCommand { - Create { - image: String, - #[structopt(name = "COMMAND", required = true, min_values = 1)] - command: Vec, - }, - Run { - uuid: String, - }, - Stop { - uuid: String, - }, - State { - uuid: String, - }, -} - -pub async fn run_isolated_container_client( - server_ip: String, - port: u16, - cmd: IsolatedContainerCommand, -) -> Result<(), Box> { - let address = format!("http://{}:{}", server_ip, port); - let channel = Endpoint::from_shared(address)?.connect().await?; - let mut client = IsolatedContainerServiceClient::new(channel); - - match cmd { - IsolatedContainerCommand::Create { image, command } => { - let request = Request::new(CreateContainerRequest { image, command }); - match timeout(Duration::from_secs(30), client.create_container(request)).await { - Ok(response) => match response { - Ok(response) => println!("CREATE ISOLATED CONTAINER RESPONSE={:?}", response), - Err(e) => eprintln!("Error creating isolated container: {:?}", e), - }, - Err(_) => eprintln!("Request timed out after 30 seconds"), - } - } - IsolatedContainerCommand::Run { uuid } => { - let request = Request::new(RunContainerRequest { uuid }); - match client.run_container(request).await { - Ok(response) => println!("RUN ISOLATED CONTAINER RESPONSE={:?}", response), - Err(e) => eprintln!("Error running isolated container: {:?}", e), - } - } - IsolatedContainerCommand::Stop { uuid } => { - let request = Request::new(StopContainerRequest { uuid }); - match client.stop_container(request).await { - Ok(response) => println!("STOP ISOLATED CONTAINER RESPONSE={:?}", response), - Err(e) => eprintln!("Error stopping isolated container: {:?}", e), - } - } - IsolatedContainerCommand::State { uuid } => { - let request = Request::new(StateContainerRequest { uuid }); - match client.state_container(request).await { - Ok(response) => println!("STATE ISOLATED CONTAINER RESPONSE={:?}", response), - Err(e) => eprintln!("Error getting isolated container state: {:?}", e), - } - } - } - - Ok(()) -} diff --git a/src/bin/feos_cli/main.rs b/src/bin/feos_cli/main.rs deleted file mode 100644 index 377b3b1..0000000 --- a/src/bin/feos_cli/main.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod client; -mod client_container; -mod client_isolated_container; - -use client::Opt; -use structopt::StructOpt; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let opt = Opt::from_args(); - client::run_client(opt).await -} diff --git a/src/container/mod.rs b/src/container/mod.rs deleted file mode 100644 index 147ab0c..0000000 --- a/src/container/mod.rs +++ /dev/null @@ -1,240 +0,0 @@ -use container_service::container_service_server::ContainerService; -use libcontainer::container::builder::ContainerBuilder; -use libcontainer::container::Container; -use libcontainer::signal::Signal; -use libcontainer::syscall::syscall::SyscallType; -use log::info; -use oci::{fetch_image, DEFAULT_IMAGE_PATH}; -use std::fs::File; -use std::{fmt::Debug, path::PathBuf}; -use tonic::{Request, Response, Status}; -pub mod oci; -use flate2::read::GzDecoder; -use libcontainer::oci_spec::runtime::{LinuxNamespace, Mount, Spec}; -use libcontainer::workload::default::DefaultExecutor; -use serde_json::to_writer_pretty; -use std::fs; -use std::io::BufReader; -use std::io::{BufWriter, Write}; -use tar::Archive; -use uuid::Uuid; - -pub const DEFAULT_CONTAINER_PATH: &str = "/var/lib/feos/containers"; - -pub mod container_service { - tonic::include_proto!("container"); -} - -#[derive(Debug, Default)] -pub struct ContainerAPI {} - -fn create( - id: String, - bundle: PathBuf, - _socket: Option, -) -> anyhow::Result { - let container = ContainerBuilder::new(id, SyscallType::default()) - .with_executor(DefaultExecutor {}) - .with_root_path("/var/lib/feos/youki")? - .validate_id()? - .as_init(bundle) - .with_systemd(false) - .with_detach(true) - .build()?; - - Ok(container) -} - -#[tonic::async_trait] -impl ContainerService for ContainerAPI { - async fn create_container( - &self, - request: Request, - ) -> Result, Status> { - info!("Got create_container request"); - - let id: Uuid = Uuid::new_v4(); - - let digest = fetch_image(request.get_ref().image.to_owned()) - .await - .map_err(|e| { - Status::new( - tonic::Code::Internal, - format!("failed to fetch image: {}", e), - ) - })?; - - let mut bundle_path = PathBuf::from(DEFAULT_CONTAINER_PATH); - bundle_path.push(id.to_string()); - - let mut rootfs_path = bundle_path.clone(); - rootfs_path.push("rootfs"); - - fs::create_dir_all(&rootfs_path)?; - fs::create_dir_all("/var/lib/feos/youki")?; - - info!("unpacking image content"); - let src = format!("{}/{}", DEFAULT_IMAGE_PATH, digest); - let paths = fs::read_dir(PathBuf::from(&src))?; - for path in paths { - let path = path?.path(); - - if path.is_file() { - let file = File::open(&path)?; - let gz_decoder = GzDecoder::new(BufReader::new(file)); - let mut archive = Archive::new(gz_decoder); - - archive.unpack(&rootfs_path)?; - } - } - - let mut spec = Spec::default(); - let linux = spec - .linux_mut() - .as_mut() - .ok_or_else(|| Status::new(tonic::Code::Internal, ""))?; - let ns: &mut Vec = linux - .namespaces_mut() - .as_mut() - .ok_or_else(|| Status::new(tonic::Code::Internal, ""))?; - ns.retain(|_| false); - - linux.set_masked_paths(None); - linux.set_readonly_paths(None); - - let mount: &mut Vec = spec - .mounts_mut() - .as_mut() - .ok_or_else(|| Status::new(tonic::Code::Internal, ""))?; - - let drop_mounts = vec![ - PathBuf::from("/dev/pts"), - PathBuf::from("/dev/shm"), - PathBuf::from("/dev/mqueue"), - PathBuf::from("/sys/fs/cgroup"), - ]; - - for mnt in drop_mounts { - mount.retain(|m| m.destination().to_str() != mnt.to_str()) - } - - if let Some(mut process) = spec.process_mut().take() { - process.set_args(Some(request.get_ref().command.to_owned())); - spec.set_process(Some(process)); - } - - info!("writing container config"); - let mut cfg_file = bundle_path.clone(); - cfg_file.push("config.json"); - let cfg_file = File::create(cfg_file).expect("msg"); - let mut writer = BufWriter::new(cfg_file); - to_writer_pretty(&mut writer, &spec).expect("msg"); - writer.flush().expect("msg"); - - info!("creating container"); - info!("bundle_path: {:?}", bundle_path.to_str()); - match create(id.to_string(), bundle_path, None) { - Ok(_) => info!("container created"), - Err(x) => { - info!("failed to create container: {:?}", x); - return Err(Status::new( - tonic::Code::Internal, - "failed to create container ", - )); - } - } - - Ok(Response::new(container_service::CreateContainerResponse { - uuid: id.to_string(), - })) - } - - async fn run_container( - &self, - request: Request, - ) -> Result, Status> { - info!("Got run_container request"); - - let container_id: String = request.get_ref().uuid.clone(); - - let container_root = PathBuf::from(format!("/var/lib/feos/youki/{}", container_id)); - if !container_root.exists() { - info!("container {} does not exist.", container_id) - } - - let mut container = Container::load(container_root).expect("msg"); - container.start().expect("msg"); - - Ok(Response::new(container_service::RunContainerResponse {})) - } - - async fn kill_container( - &self, - request: Request, - ) -> Result, Status> { - info!("Got kill_container request"); - - let container_id: String = request.get_ref().uuid.clone(); - - let container_root = PathBuf::from(format!("/var/lib/feos/youki/{}", container_id)); - if !container_root.exists() { - info!("container {} does not exist.", container_id) - } - - let signal: Signal = "9".try_into().expect("msg"); - - let mut container = Container::load(container_root).expect("msg"); - container.kill(signal, false).expect("msg"); - - Ok(Response::new(container_service::KillContainerResponse {})) - } - - async fn state_container( - &self, - request: Request, - ) -> Result, Status> { - info!("Got state_container request"); - - let container_id: String = request.get_ref().uuid.clone(); - - let container_root = PathBuf::from(format!("/var/lib/feos/youki/{}", container_id)); - if !container_root.exists() { - info!("container {} does not exist.", container_id) - } - - let mut container = Container::load(container_root.clone()).expect("msg"); - let _ = container.refresh_status().expect("msg"); - - info!("{:?}", container.state); - - Ok(Response::new(container_service::StateContainerResponse { - state: container.state.status.to_string(), - pid: container.state.pid, - })) - } - - async fn delete_container( - &self, - request: Request, - ) -> Result, Status> { - info!("Got delete_container request"); - - let container_id: String = request.get_ref().uuid.clone(); - - let container_root = PathBuf::from(format!("/var/lib/feos/youki/{}", container_id)); - if !container_root.exists() { - info!("container {} does not exist.", container_id); - return Ok(Response::new(container_service::DeleteContainerResponse {})); - } - - let mut container = Container::load(container_root).expect("msg"); - container.delete(false).expect("msg"); - - let bundle_path = PathBuf::from(format!("{}/{}", DEFAULT_CONTAINER_PATH, container_id)); - if bundle_path.exists() { - fs::remove_dir_all(bundle_path).expect("msg"); - } - - Ok(Response::new(container_service::DeleteContainerResponse {})) - } -} diff --git a/src/container/oci.rs b/src/container/oci.rs deleted file mode 100644 index 1abd28b..0000000 --- a/src/container/oci.rs +++ /dev/null @@ -1,83 +0,0 @@ -use log::info; -use oci_distribution::manifest::OciManifest; -use oci_distribution::{secrets, Client, Reference}; -use std::{ - fs::{self, File}, - io::Write, - path::{Path, PathBuf}, -}; - -const LAYER_COMPRESSED: &str = "application/vnd.oci.image.layer.v1.tar+gzip"; -pub const DEFAULT_IMAGE_PATH: &str = "/var/lib/feos/images"; - -pub async fn fetch_image(image: String) -> Result { - info!("fetching image: {}", image); - - let mut reference = Reference::try_from(image.clone()).map_err(|e| e.to_string())?; - - let c = Client::default(); - let file_path = PathBuf::from(DEFAULT_IMAGE_PATH); - - let manifest = c - .pull_manifest(&reference, &secrets::RegistryAuth::Anonymous) - .await - .map_err(|e| e.to_string())?; - match manifest.0 { - OciManifest::Image(_) => {} - OciManifest::ImageIndex(index) => { - let layer = index - .manifests - .iter() - .find(|x1| { - let platform = match &x1.platform { - Some(p) => p, - None => return false, - }; - - if platform.os == "linux" && platform.architecture == "amd64" { - return true; - } - - false - }) - .unwrap(); - reference = Reference::with_digest( - reference.registry().to_string(), - reference.repository().to_string(), - layer.digest.to_string(), - ); - } - }; - - if let Some(digest) = reference.digest() { - let mut path = file_path.clone(); - path.push(digest); - if Path::new(&path).is_dir() { - info!("image already present"); - return Ok(digest.to_string()); - } - } - - let media_type = vec![LAYER_COMPRESSED]; - let data = c - .pull(&reference, &secrets::RegistryAuth::Anonymous, media_type) - .await - .map_err(|e| e.to_string())?; - info!("image pulled"); - - let mut path = file_path.clone(); - let digest = data.digest.unwrap().to_string(); - path.push(digest.clone()); - fs::create_dir_all(path.clone()).map_err(|e| e.to_string())?; - - info!("writing layers to disk"); - for layer in data.layers { - let mut path = path.clone(); - path.push(layer.sha256_digest()); - - let mut file = File::create(path).map_err(|e| e.to_string())?; - file.write_all(&layer.data).map_err(|e| e.to_string())?; - } - - Ok(digest) -} diff --git a/src/daemon.rs b/src/daemon.rs deleted file mode 100644 index 392e0d2..0000000 --- a/src/daemon.rs +++ /dev/null @@ -1,667 +0,0 @@ -use log::{error, info, warn}; -use std::net::Ipv6Addr; -use std::path::PathBuf; -use std::{env, io}; -use tonic::{transport::Server, Request, Response, Status}; - -use crate::feos_grpc; -use crate::feos_grpc::feos_grpc_server::*; -use crate::feos_grpc::{ - attach_nic_vm_request::NicData, AttachNicVmRequest, AttachNicVmResponse, BootVmRequest, - BootVmResponse, ConsoleVmResponse, CreateVmRequest, CreateVmResponse, Empty, FetchImageRequest, - FetchImageResponse, GetFeOsKernelLogRequest, GetFeOsKernelLogResponse, GetFeOsLogRequest, - GetFeOsLogResponse, GetVmRequest, GetVmResponse, HostInfoRequest, HostInfoResponse, - NetInterface, NicType as ProtoNicType, PingVmRequest, PingVmResponse, RebootRequest, - RebootResponse, ShutdownRequest, ShutdownResponse, ShutdownVmRequest, ShutdownVmResponse, -}; -use crate::host; -use crate::ringbuffer::*; -use crate::vm::{self}; -use crate::vm::{image, Manager}; -use crate::{container, network}; -use hyper_util::rt::TokioIo; -use nix::libc::VMADDR_CID_ANY; -use nix::unistd::Uid; -use std::sync::Arc; -use tokio::{ - fs::File, - io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, - net::UnixStream, - sync::{mpsc, Mutex}, - time::{sleep, Duration}, -}; -use tokio_stream::wrappers::ReceiverStream; -use tokio_vsock::{VsockAddr, VsockListener}; -use tonic::transport::{Endpoint, Uri}; -use tower::service_fn; -use uuid::Uuid; - -use crate::filesystem::mount_virtual_filesystems; -use crate::isolated_container::{isolated_container_service, IsolatedContainerAPI}; -use crate::network::configure_network_devices; - -#[derive(Debug)] -pub struct FeOSAPI { - vmm: Arc, - buffer: Arc, - log_receiver: Arc>>, - network: Arc, -} - -impl FeOSAPI { - pub fn new( - vmm: Arc, - buffer: Arc, - log_receiver: Arc>>, - network: Arc, - ) -> Self { - FeOSAPI { - vmm, - buffer, - log_receiver, - network, - } - } - - fn handle_error(&self, e: vm::Error) -> Status { - match e { - vm::Error::AlreadyExists => { - Status::new(tonic::Code::AlreadyExists, "vm already exists") - } - vm::Error::NotFound => Status::new(tonic::Code::NotFound, "vm not found"), - vm::Error::SocketFailure(e) => { - info!("socket error: {:?}", e); - Status::new(tonic::Code::Internal, "failed to ") - } - vm::Error::InvalidInput(e) => { - info!("invalid input error: {:?}", e); - Status::new(tonic::Code::Internal, "invalid input") - } - vm::Error::CHCommandFailure(e) => { - info!("failed to connect to cloud hypervisor: {:?}", e); - Status::new( - tonic::Code::Internal, - "failed to connect to cloud hypervisor", - ) - } - vm::Error::NetworkingError(e) => { - info!("failed to prepare network {:?}", e); - Status::new( - tonic::Code::Internal, - format!("failed to prepare network: {:?}", e), - ) - } - vm::Error::CHApiFailure(e) => { - info!("failed to connect to cloud hypervisor api: {:?}", e); - Status::new( - tonic::Code::Internal, - "failed to connect to cloud hypervisor api", - ) - } - } - } -} - -#[tonic::async_trait] -impl FeosGrpc for FeOSAPI { - async fn reboot(&self, _: Request) -> Result, Status> { - info!("Got reboot request"); - tokio::spawn(async { - sleep(Duration::from_secs(1)).await; - match host::power::reboot() { - Ok(_) => info!("reboot"), - Err(e) => info!("failed to reboot: {:?}", e), - } - }); - Ok(Response::new(feos_grpc::RebootResponse {})) - } - async fn shutdown( - &self, - _: Request, - ) -> Result, Status> { - info!("Got shutdown request"); - tokio::spawn(async { - sleep(Duration::from_secs(1)).await; - match host::power::shutdown() { - Ok(_) => info!("shutdown"), - Err(e) => info!("failed to shutdown: {:?}", e), - } - }); - Ok(Response::new(feos_grpc::ShutdownResponse {})) - } - async fn host_info( - &self, - _: Request, - ) -> Result, Status> { - info!("Got host info request"); - - let host = host::info::check_info(); - - let mut interfaces = Vec::new(); - for interface in host.net_interfaces { - interfaces.push(NetInterface { - name: interface.name, - pci_address: interface.pci_address.unwrap_or_default(), - mac_address: interface.mac_address.unwrap_or_default(), - }) - } - - Ok(Response::new(feos_grpc::HostInfoResponse { - uptime: host.uptime, - ram_total: host.ram_total, - ram_unused: host.ram_unused, - num_cores: host.num_cores, - net_interfaces: interfaces, - })) - } - - async fn ping( - &self, - request: Request, // Accept request of type HelloRequest - ) -> Result, Status> { - // Return an instance of type HelloReply - info!("Got a request: {:?}", request); - - let reply = feos_grpc::Empty {}; - - Ok(Response::new(reply)) // Send back our formatted greeting - } - - async fn fetch_image( - &self, - request: Request, - ) -> Result, Status> { - info!("Got fetch_image request"); - - let id = Uuid::new_v4(); - let path: PathBuf = PathBuf::from(format!("./images/{}", id.clone())); - tokio::spawn(async move { - match vm::image::fetch_image(request.get_ref().image.clone(), path).await { - Ok(_) => info!("image pulled"), - Err(image::ImageError::IOError(e)) => { - info!("failed to pull image: io error: {:?}", e) - } - Err(image::ImageError::PullError(e)) => info!("failed to pull image: {:?}", e), - Err(image::ImageError::InvalidReference(e)) => { - info!("failed to pull image: invalid reference: {:?}", e) - } - Err(image::ImageError::MissingLayer(e)) => { - info!("failed to pull image: missing layer: {:?}", e) - } - } - }); - - Ok(Response::new(feos_grpc::FetchImageResponse { - uuid: id.to_string(), - })) - } - - async fn create_vm( - &self, - request: Request, - ) -> Result, Status> { - info!("Got create_vm request"); - - let id = Uuid::new_v4(); - self.vmm - .init_vmm(id, true) - .map_err(|e| self.handle_error(e))?; - - let root_fs = PathBuf::from(format!( - "./images/{}/application.vnd.ironcore.image.rootfs.v1alpha1.rootfs", - request.get_ref().image_uuid - )); - self.vmm - .create_vm( - id, - request.get_ref().cpu, - request.get_ref().memory_bytes, - vm::BootMode::FirmwareBoot(vm::FirmwareBootMode { root_fs }), - request.get_ref().ignition.clone(), - ) - .map_err(|e| self.handle_error(e))?; - - Ok(Response::new(feos_grpc::CreateVmResponse { - uuid: id.to_string(), - })) - } - - async fn get_vm( - &self, - request: Request, - ) -> Result, Status> { - info!("Got get_vm request"); - - let id = request.get_ref().uuid.to_owned(); - let id = - Uuid::parse_str(&id).map_err(|_| Status::invalid_argument("failed to parse uuid"))?; - self.vmm.ping_vmm(id).map_err(|e| self.handle_error(e))?; - let vm_status = self.vmm.get_vm(id).map_err(|e| self.handle_error(e))?; - - Ok(Response::new(feos_grpc::GetVmResponse { info: vm_status })) - } - - async fn boot_vm( - &self, - request: Request, - ) -> Result, Status> { - info!("Received boot_vm request"); - - let id = Uuid::parse_str(&request.get_ref().uuid) - .map_err(|_| Status::invalid_argument("Failed to parse UUID"))?; - - self.vmm.boot_vm(id).map_err(|e| self.handle_error(e))?; - //TODO remove this sleep - sleep(Duration::from_secs(2)).await; - self.network - .start_dhcp(id) - .await - .map_err(vm::Error::NetworkingError) - .map_err(|e| self.handle_error(e))?; - - Ok(Response::new(feos_grpc::BootVmResponse {})) - } - - type ConsoleVMStream = ReceiverStream>; - - async fn console_vm( - &self, - request: Request>, - ) -> Result, Status> { - let mut input_stream = request.into_inner(); - let (tx, rx) = mpsc::channel(4); - - info!("Got console_vm request"); - let initial_request = match input_stream.message().await { - Ok(Some(request)) => request, - Ok(None) => { - return Err(Status::new( - tonic::Code::InvalidArgument, - "No initial request received", - )) - } - Err(status) => return Err(status), - }; - let id = Uuid::parse_str(&initial_request.uuid) - .map_err(|_| Status::invalid_argument("failed to parse uuid"))?; - let socket_path = self - .vmm - .get_vm_console_path(id) - .map_err(|e| self.handle_error(e))?; - - tokio::spawn(async move { - let stream = match UnixStream::connect(&socket_path).await { - Ok(stream) => stream, - Err(e) => { - error!("Failed to connect to Unix socket: {:?}", e); - return; - } - }; - - let (reader, writer) = stream.into_split(); - - tokio::spawn(async move { - let mut writer = writer; - while let Ok(Some(req)) = input_stream.message().await { - let input_with_newline = format!("{}\n", req.input); - if let Err(e) = writer.write_all(input_with_newline.as_bytes()).await { - error!("Failed to write to console: {:?}", e); - break; - } - } - }); - - let mut reader = reader; - let mut buffer = vec![0; 1024]; - loop { - match reader.read(&mut buffer).await { - Ok(0) => break, // EOF - Ok(n) => { - let message = String::from_utf8_lossy(&buffer[..n]).to_string(); - let response = ConsoleVmResponse { message }; - if tx.send(Ok(response)).await.is_err() { - error!("Failed to send response through channel"); - break; - } - } - Err(e) => { - error!("Failed to read from stream: {:?}", e); - break; - } - } - } - }); - - Ok(Response::new(ReceiverStream::new(rx))) - } - - async fn attach_nic_vm( - &self, - request: Request, - ) -> Result, Status> { - info!("Received AttachNicVM request"); - - let req = request.into_inner(); - - let vm_uuid = Uuid::parse_str(&req.uuid) - .map_err(|_| Status::invalid_argument("Failed to parse UUID"))?; - - let net_config = match ProtoNicType::try_from(req.nic_type) { - Ok(ProtoNicType::Mac) => { - if let Some(NicData::MacAddress(mac)) = req.nic_data { - vm::NetworkMode::MACAddress(mac) - } else { - return Err(Status::invalid_argument( - "mac_address must be provided for NIC type MAC", - )); - } - } - Ok(ProtoNicType::Pci) => { - if let Some(NicData::PciAddress(pci)) = req.nic_data { - vm::NetworkMode::PCIAddress(pci) - } else { - return Err(Status::invalid_argument( - "pci_address must be provided for NIC type PCI", - )); - } - } - Ok(ProtoNicType::Tap) => { - let tap_name = network::Manager::device_name(&vm_uuid); - vm::NetworkMode::TAPDeviceName(tap_name) - } - Err(_) => { - return Err(Status::invalid_argument("Invalid NIC type provided")); - } - }; - - self.vmm - .add_net_device(vm_uuid, net_config) - .map_err(|e| self.handle_error(e))?; - - Ok(Response::new(AttachNicVmResponse {})) - } - - async fn shutdown_vm( - &self, - request: Request, - ) -> Result, Status> { - info!("Received shutdown_vm request"); - - let id = Uuid::parse_str(&request.get_ref().uuid) - .map_err(|_| Status::invalid_argument("Failed to parse UUID"))?; - - self.network - .stop_dhcp(id) - .await - .map_err(vm::Error::NetworkingError) - .map_err(|e| self.handle_error(e))?; - - // TODO differentiate between kill and shutdown - self.vmm.kill_vm(id).map_err(|e| self.handle_error(e))?; - - Ok(Response::new(feos_grpc::ShutdownVmResponse {})) - } - - async fn ping_vm( - &self, - request: Request, - ) -> Result, Status> { - info!("Received ping_vm request"); - - let id = Uuid::parse_str(&request.get_ref().uuid) - .map_err(|_| Status::invalid_argument("Failed to parse UUID"))?; - let path = format!("vsock{}.sock", network::Manager::vm_tap_name(&id)); - let path_clone = path.clone(); - - let channel = Endpoint::try_from("http://[::]:50051") - .map_err(|e| Status::internal(format!("Failed to create endpoint: {}", e)))? - .connect_with_connector(service_fn(move |_: Uri| { - let path = path_clone.clone(); - async move { - let mut stream = UnixStream::connect(&path).await.map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("UnixStream connect error: {}", e), - ) - })?; - let connect_cmd = format!("CONNECT {}\n", 1337); - stream - .write_all(connect_cmd.as_bytes()) - .await - .map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("Write error: {}", e)) - })?; - - let mut buffer = [0u8; 128]; - let n = stream.read(&mut buffer).await.map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("Read error: {}", e)) - })?; - let response = String::from_utf8_lossy(&buffer[..n]); - // Parse the response - if !response.starts_with("OK") { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("Failed to connect to vsock: {}", response.trim()), - )); - } - info!("Connected to vsock: {}", response.trim()); - // Connect to an Uds socket - Ok::<_, io::Error>(TokioIo::new(stream)) - } - })) - .await - .map_err(|e| Status::internal(format!("Failed to connect: {}", e)))?; - - let mut client = feos_grpc::feos_grpc_client::FeosGrpcClient::new(channel); - let request = tonic::Request::new(Empty {}); - let _response = client.ping(request).await?; - - Ok(Response::new(feos_grpc::PingVmResponse {})) - } - - type GetFeOSKernelLogsStream = ReceiverStream>; - - async fn get_fe_os_kernel_logs( - &self, - _: Request, - ) -> Result, Status> { - let (tx, rx) = mpsc::channel(4); - let tx = tx.clone(); - - tokio::spawn(async move { - let file = File::open("/dev/kmsg") - .await - .expect("Failed to open /dev/kmsg"); - let reader = BufReader::new(file); - let mut lines = reader.lines(); - - while let Some(line) = lines.next_line().await.unwrap() { - let response = GetFeOsKernelLogResponse { message: line }; - if tx.send(Ok(response)).await.is_err() { - break; - } - } - }); - - Ok(Response::new(ReceiverStream::new(rx))) - } - - type GetFeOSLogsStream = ReceiverStream>; - - async fn get_fe_os_logs( - &self, - _: Request, - ) -> Result>>, Status> { - let (tx, rx) = mpsc::channel(4); - let buffer = self.buffer.clone(); - let log_receiver = self.log_receiver.clone(); - - tokio::spawn(async move { - let logs = buffer.get_lines().await; - for log in logs { - let response = GetFeOsLogResponse { message: log }; - if tx.send(Ok(response)).await.is_err() { - break; - } - } - - let mut log_receiver = log_receiver.lock().await; - while let Some(log_entry) = log_receiver.recv().await { - let response = GetFeOsLogResponse { message: log_entry }; - if tx.send(Ok(response)).await.is_err() { - break; - } - } - }); - - Ok(Response::new(ReceiverStream::new(rx))) - } -} - -pub async fn daemon_start( - vmm: Arc, - network: Arc, - buffer: Arc, - log_receiver: Arc>>, - is_nested: bool, -) -> Result<(), Box> { - let api = FeOSAPI::new(vmm.clone(), buffer, log_receiver, network.clone()); - let isolated_container_api = IsolatedContainerAPI::new(vmm, network); - - if is_nested { - let sockaddr = VsockAddr::new(VMADDR_CID_ANY, 1337); - let vsock_listener = VsockListener::bind(sockaddr)?; - Server::builder() - .add_service(FeosGrpcServer::new(api)) - .add_service( - container::container_service::container_service_server::ContainerServiceServer::new( - container::ContainerAPI {}, - ), - ) - .serve_with_incoming(vsock_listener.incoming()) - .await?; - } else { - let addr = "[::]:1337".parse()?; - Server::builder() - .timeout(Duration::from_secs(30)) - .add_service(FeosGrpcServer::new(api)) - .add_service( - container::container_service::container_service_server::ContainerServiceServer::new( - container::ContainerAPI {}, - ), - ) - .add_service(isolated_container_service::isolated_container_service_server::IsolatedContainerServiceServer::new(isolated_container_api)) - .serve(addr) - .await?; - } - - Ok(()) -} - -pub async fn start_feos(mut ipv6_address: Ipv6Addr, mut prefix_length: u8) -> Result<(), String> { - println!( - " - ███████╗███████╗ ██████╗ ███████╗ - ██╔════╝██╔════╝██╔═══██╗██╔════╝ - █████╗ █████╗ ██║ ██║███████╗ - ██╔══╝ ██╔══╝ ██║ ██║╚════██║ - ██║ ███████╗╚██████╔╝███████║ - ╚═╝ ╚══════╝ ╚═════╝ ╚══════╝ - v{} - ", - env!("CARGO_PKG_VERSION") - ); - - const FEOS_RINGBUFFER_CAP: usize = 100; - let buffer = RingBuffer::new(FEOS_RINGBUFFER_CAP); - let log_receiver = init_logger(buffer.clone()); - - // If not run as root, print warning. - if !Uid::current().is_root() { - warn!("Not running as root! (uid: {})", Uid::current()); - } - - if ipv6_address == Ipv6Addr::UNSPECIFIED { - info!("No --ipam flag found. Expecting Prefix Delegation from the dhcpv6 server"); - } - - if std::process::id() == 1 { - info!("Mounting virtual filesystems..."); - mount_virtual_filesystems(); - } else { - info!( - "IPv6 Address: {}, Prefix Length: {}", - ipv6_address, prefix_length - ); - } - - let is_nested = is_running_on_vm().await.unwrap_or_else(|e| { - error!("Error checking VM status: {}", e); - false // Default to false in case of error - }); - - if std::process::id() == 1 { - info!("Configuring network devices..."); - if let Some((delegated_prefix, delegated_prefix_length)) = configure_network_devices() - .await - .expect("could not configure network devices") - { - ipv6_address = delegated_prefix; - prefix_length = delegated_prefix_length; - } - } - - // Special stuff for pid 1 - if std::process::id() == 1 && !is_nested { - info!("Skip configuring sriov..."); - /*const VFS_NUM: u32 = 125; - if let Err(e) = configure_sriov(VFS_NUM).await { - warn!("failed to configure sriov: {}", e.to_string()) - }*/ - } - - let vmm = Manager::new(String::from("cloud-hypervisor")); - let network_manager = network::Manager::new(ipv6_address, prefix_length); - - info!("Starting FeOS daemon..."); - match daemon_start( - Arc::new(vmm), - Arc::new(network_manager), - buffer, - log_receiver, - is_nested, - ) - .await - { - Err(e) => { - error!("FeOS daemon crashed: {}", e); - Err(format!("FeOS daemon crashed: {}", e)) - } - Ok(_) => { - error!("FeOS daemon exited."); - Err("FeOS exited".to_string()) - } - } -} - -async fn is_running_on_vm() -> Result> { - let files = [ - "/sys/class/dmi/id/product_name", - "/sys/class/dmi/id/sys_vendor", - ]; - - let mut match_count = 0; - - for file_path in files.iter() { - let mut file = File::open(file_path).await?; - let mut contents = String::new(); - file.read_to_string(&mut contents).await?; - - let lowercase_contents = contents.to_lowercase(); - if lowercase_contents.contains("cloud") && lowercase_contents.contains("hypervisor") { - match_count += 1; - } - } - - Ok(match_count == 2) -} diff --git a/src/host/mod.rs b/src/host/mod.rs deleted file mode 100644 index 1f8ecdc..0000000 --- a/src/host/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod info; -pub mod power; diff --git a/src/isolated_container/mod.rs b/src/isolated_container/mod.rs deleted file mode 100644 index 9d66f07..0000000 --- a/src/isolated_container/mod.rs +++ /dev/null @@ -1,334 +0,0 @@ -use crate::container::container_service::container_service_client::ContainerServiceClient; -use crate::container::container_service::{ - CreateContainerRequest, RunContainerRequest, StateContainerRequest, -}; -use crate::vm::NetworkMode; -use crate::{network, vm}; -use hyper_util::rt::TokioIo; -use isolated_container_service::isolated_container_service_server::IsolatedContainerService; -use log::info; -use std::sync::Arc; -use std::time::Duration; -use std::{collections::HashMap, sync::Mutex}; -use std::{fmt::Debug, io, path::PathBuf}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::UnixStream; -use tokio::time::sleep; -use tonic::transport::{Channel, Endpoint, Uri}; -use tonic::{transport, Request, Response, Status}; -use tower::service_fn; -use uuid::Uuid; - -pub mod isolated_container_service { - tonic::include_proto!("isolated_container"); -} - -#[derive(Debug, Default)] -pub struct IsolatedContainerAPI { - vmm: Arc, - network: Arc, - vm_to_container: Mutex>, -} - -#[derive(Debug, Clone)] -struct IsolatedContainerInfo { - pub container_id: Uuid, - pub sock: Channel, -} - -impl IsolatedContainerAPI { - pub fn new(vmm: Arc, network: Arc) -> Self { - IsolatedContainerAPI { - vmm, - network, - vm_to_container: Mutex::new(HashMap::new()), - } - } -} - -#[derive(Debug)] -pub enum Error { - VMConnectionError(transport::Error), - VMConnectionMaxRetriesError, - VMError(vm::Error), - NetworkingError(network::Error), - InvalidID, - Error(String), -} - -async fn get_channel(path: String) -> Result { - async fn establish_connection(path: String) -> Result { - let endpoint = Endpoint::try_from("http://[::]:50051")?; - - let connector = service_fn(move |_: Uri| { - let path = path.clone(); - async move { - let mut stream = UnixStream::connect(&path).await?; - - let connect_cmd = format!("CONNECT {}\n", 1337); - stream - .write_all(connect_cmd.as_bytes()) - .await - .map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("Write error: {}", e)) - })?; - - let mut buffer = [0u8; 128]; - let n = stream.read(&mut buffer).await?; - - let response = String::from_utf8_lossy(&buffer[..n]); - if !response.starts_with("OK") { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("Failed to connect to vsock: {}", response.trim()), - )); - } - - info!("Connected to vsock: {}", response.trim()); - Ok::<_, io::Error>(TokioIo::new(stream)) - } - }); - - endpoint.connect_with_connector(connector).await - } - - const RETRIES: u8 = 20; - const DELAY: tokio::time::Duration = tokio::time::Duration::from_millis(2000); - - for attempt in 0..RETRIES { - match establish_connection(path.clone()).await { - Ok(channel) => return Ok(channel), - Err(e) => { - info!("Attempt {} failed: {:?}", attempt + 1, e); - if attempt < RETRIES - 1 { - info!("Retrying in {:?}", DELAY); - tokio::time::sleep(DELAY).await; - } - } - } - } - - Err(Error::VMConnectionMaxRetriesError) -} - -impl IsolatedContainerAPI { - fn prepare_vm(&self, id: uuid::Uuid) -> Result<(), Error> { - self.vmm.init_vmm(id, true).map_err(Error::VMError)?; - self.vmm - .create_vm( - id, - 2, - // TODO make configurable through container request - 4294967296, - vm::BootMode::KernelBoot(vm::KernelBootMode { - kernel: PathBuf::from("/usr/share/feos/vmlinuz"), - initramfs: PathBuf::from("/usr/share/feos/initramfs"), - // TODO - cmdline: "console=tty0 console=ttyS0,115200 intel_iommu=on iommu=pt" - .to_string(), - }), - None, - ) - .map_err(Error::VMError)?; - - self.vmm - .add_net_device( - id, - NetworkMode::TAPDeviceName(network::Manager::device_name(&id)), - ) - .map_err(Error::VMError)?; - - self.vmm.boot_vm(id).map_err(Error::VMError)?; - - Ok(()) - } - - fn handle_error(&self, e: Error) -> tonic::Status { - match e { - Error::VMConnectionError(e) => Status::new( - tonic::Code::Internal, - format!("failed to connect to vm: {}", e), - ), - Error::VMConnectionMaxRetriesError => Status::new( - tonic::Code::Internal, - format!("failed to connect to vm: mac retries reached: {:?}", e), - ), - Error::VMError(e) => Status::new( - tonic::Code::Internal, - format!("failed to prepare vm: {:?}", e), - ), - Error::NetworkingError(e) => Status::new( - tonic::Code::Internal, - format!("failed to prepare network: {:?}", e), - ), - Error::InvalidID => Status::invalid_argument("failed to parse uuid"), - Error::Error(m) => Status::internal(m), - } - } - - fn get_container_info(&self, vm_id: Uuid) -> Result { - let container_id = { - let vm_to_container = self - .vm_to_container - .lock() - .map_err(|_| Error::Error("Failed to lock mutex".to_string()))?; - vm_to_container - .get(&vm_id) - .cloned() - .ok_or_else(|| Error::Error(format!("VM with ID '{}' not found", vm_id)))? - }; - - Ok(container_id) - } -} - -#[tonic::async_trait] -impl IsolatedContainerService for IsolatedContainerAPI { - async fn create_container( - &self, - request: Request, - ) -> Result, Status> { - info!("Got create_container request"); - - let id = Uuid::new_v4(); - - self.prepare_vm(id).map_err(|e| self.handle_error(e))?; - //TODO get rid of sleep - sleep(Duration::from_secs(2)).await; - - self.network - .start_dhcp(id) - .await - .map_err(Error::NetworkingError) - .map_err(|e| self.handle_error(e))?; - - let path = format!("vsock{}.sock", network::Manager::device_name(&id)); - let channel = get_channel(path).await.map_err(|e| self.handle_error(e))?; - - let mut client = ContainerServiceClient::new(channel.clone()); - let request = tonic::Request::new(CreateContainerRequest { - image: request.get_ref().image.to_string(), - command: request.get_ref().command.clone(), - }); - let response = client - .create_container(request) - .await - .map_err(|e| { - info!("Failed to create container: {:?}", e); - match self.vmm.kill_vm(id) { - Ok(_) => Error::Error("failed to create container".to_string()), - Err(kill_error) => Error::Error(format!( - "failed to create container: {:?}, kill VM error: {:?}", - e, kill_error - )), - } - }) - .map_err(|e| self.handle_error(e))?; - - info!("created container with id: {}", response.get_ref().uuid); - - let container_id = Uuid::parse_str(&response.get_ref().uuid) - .map_err(|_| Error::InvalidID) - .map_err(|e| self.handle_error(e))?; - - let mut vm_to_container = self.vm_to_container.lock().unwrap(); - vm_to_container.insert( - id, - IsolatedContainerInfo { - container_id, - sock: channel, - }, - ); - - Ok(Response::new( - isolated_container_service::CreateContainerResponse { - uuid: id.to_string(), - }, - )) - } - - async fn run_container( - &self, - request: Request, - ) -> Result, Status> { - info!("Got run_container request"); - - let vm_id: String = request.get_ref().uuid.clone(); - let vm_id = Uuid::parse_str(&vm_id) - .map_err(|_| Error::InvalidID) - .map_err(|e| self.handle_error(e))?; - - let container = self - .get_container_info(vm_id) - .map_err(|e| self.handle_error(e))?; - - let mut client = ContainerServiceClient::new(container.sock.clone()); - let request = tonic::Request::new(RunContainerRequest { - uuid: container.container_id.to_string(), - }); - client.run_container(request).await?; - - Ok(Response::new( - isolated_container_service::RunContainerResponse {}, - )) - } - - async fn stop_container( - &self, - request: Request, - ) -> Result, Status> { - info!("Got stop_container request"); - - let vm_id: String = request.get_ref().uuid.clone(); - let vm_id = Uuid::parse_str(&vm_id) - .map_err(|_| Error::InvalidID) - .map_err(|e| self.handle_error(e))?; - - self.network - .stop_dhcp(vm_id) - .await - .map_err(Error::NetworkingError) - .map_err(|e| self.handle_error(e))?; - - self.vmm - .kill_vm(vm_id) - .map_err(Error::VMError) - .map_err(|e| self.handle_error(e))?; - - let mut vm_to_container = self.vm_to_container.lock().unwrap(); - vm_to_container.remove(&vm_id); - - Ok(Response::new( - isolated_container_service::StopContainerResponse {}, - )) - } - - async fn state_container( - &self, - request: Request, - ) -> Result, Status> { - info!("Got state_container request"); - - let vm_id: String = request.get_ref().uuid.clone(); - let vm_id = Uuid::parse_str(&vm_id) - .map_err(|_| Error::InvalidID) - .map_err(|e| self.handle_error(e))?; - - let container = self - .get_container_info(vm_id) - .map_err(|e| self.handle_error(e))?; - - let mut client = ContainerServiceClient::new(container.sock.clone()); - let request = tonic::Request::new(StateContainerRequest { - uuid: container.container_id.to_string(), - }); - let response = client.state_container(request).await?; - - Ok(Response::new( - isolated_container_service::StateContainerResponse { - state: response.get_ref().state.to_string(), - pid: response.get_ref().pid, - }, - )) - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 1df036a..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub mod container; -pub mod daemon; -pub mod filesystem; -pub mod fsmount; -pub mod host; -pub mod isolated_container; -pub mod move_root; -pub mod network; -pub mod ringbuffer; -pub mod vm; - -pub mod feos_grpc { - tonic::include_proto!("feos_grpc"); -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index e96f6ef..0000000 --- a/src/main.rs +++ /dev/null @@ -1,91 +0,0 @@ -extern crate nix; -use feos::daemon::start_feos; -use feos::move_root::{get_root_fstype, move_root}; -use nix::unistd::execv; -use std::env; -use std::net::Ipv6Addr; -use std::str::FromStr; -use std::{env::args, ffi::CString}; -use tokio::io; -use tokio::io::{AsyncBufReadExt, BufReader}; - -//TODO remove this in future, the reason https://github.com/youki-dev/youki/issues/2144 -#[tokio::main(flavor = "current_thread")] -async fn main() -> Result<(), String> { - let mut ipv6_address = Ipv6Addr::UNSPECIFIED; - let mut prefix_length = 64; - - if std::process::id() == 1 { - let root_fstype = get_root_fstype().unwrap_or_default(); - if root_fstype == "rootfs" { - move_root().map_err(|e| format!("move_root: {}", e))?; - - let argv: Vec = args() - .map(|arg| CString::new(arg).unwrap_or_default()) - .collect(); - execv(&argv[0], &argv).map_err(|e| format!("execv: {}", e))?; - } - } else { - (ipv6_address, prefix_length) = parse_command_line()?; - } - - start_feos(ipv6_address, prefix_length).await?; - Err("FeOS exited".to_string()) -} - -fn parse_command_line() -> Result<(Ipv6Addr, u8), String> { - let args: Vec = env::args().collect(); - - if args.len() < 2 { - return Ok((Ipv6Addr::UNSPECIFIED, 64)); - } - - if args[1] != "--ipam" { - return Err("Expected '--ipam' flag".into()); - } - - if args.len() != 3 { - return Err("Usage: --ipam /".into()); - } - - let prefix_input = &args[2]; - let parts: Vec<&str> = prefix_input.split('/').collect(); - - if parts.len() != 2 { - return Err("Invalid IPv6 prefix format. Use /".into()); - } - - let ipv6_address = - Ipv6Addr::from_str(parts[0]).map_err(|_| "Invalid IPv6 address".to_string())?; - - let prefix_length: u8 = parts[1] - .parse() - .map_err(|_| "Invalid prefix length".to_string())?; - - if prefix_length > 128 { - return Err("Prefix length must be between 0 and 128".into()); - } - - Ok((ipv6_address, prefix_length)) -} - -async fn _read_and_echo() { - let stdin = io::stdin(); - let mut reader = BufReader::new(stdin); - let mut line = String::new(); - - loop { - line.clear(); - let bytes_read = reader.read_line(&mut line).await.unwrap(); - - // If no bytes were read, it means EOF has been reached - if bytes_read == 0 { - break; - } - - // Trim the newline character - let trimmed_line = line.trim_end(); - - println!("this is echoed \"{}\"", trimmed_line); - } -} diff --git a/src/network/dhcpv6.rs b/src/network/dhcpv6.rs deleted file mode 100644 index d0c53d2..0000000 --- a/src/network/dhcpv6.rs +++ /dev/null @@ -1,1044 +0,0 @@ -use dhcproto::v6::Status::{NoAddrsAvail, NoBinding}; -use dhcproto::v6::*; -use futures::stream::TryStreamExt; -use log::{debug, error, info, warn}; -use netlink_packet_route::route::{ - RouteAddress, RouteAttribute, RouteHeader, RouteProtocol, RouteScope, RouteType, -}; -use netlink_packet_route::AddressFamily; -use nix::net::if_::if_nametoindex; -use pnet::packet::icmpv6::Icmpv6Code; -use pnet::{ - datalink::{self, Channel::Ethernet, NetworkInterface}, - packet::{ - ethernet::{EtherTypes, EthernetPacket, MutableEthernetPacket}, - icmpv6::{checksum, ndp::*, Icmpv6Packet, Icmpv6Types, MutableIcmpv6Packet}, - ip::IpNextHeaderProtocols, - ipv6::{Ipv6Packet, MutableIpv6Packet}, - Packet, - }, - util::MacAddr, -}; -use rtnetlink::{new_connection, Error, Handle}; -use socket2::{Domain, Protocol, SockAddr, Socket, Type}; -use std::collections::HashMap; -use std::io; -use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6}; -use std::thread::sleep; -use std::time::Duration; -use tokio::net::UdpSocket; -use tokio::task; - -#[derive(Clone)] -pub struct IpRange { - pub start: Ipv6Addr, - pub end: Ipv6Addr, -} - -#[derive(Debug, Clone)] -struct ClientInfo { - duid: Vec, - mac: Vec, -} - -pub fn mac_to_ipv6_link_local(mac_address: &[u8]) -> Option { - if mac_address.len() == 6 { - let mut bytes = [0u8; 16]; - bytes[0] = 0xfe; - bytes[1] = 0x80; - bytes[8] = mac_address[0] ^ 0b00000010; - bytes[9] = mac_address[1]; - bytes[10] = mac_address[2]; - bytes[11] = 0xff; - bytes[12] = 0xfe; - bytes[13] = mac_address[3]; - bytes[14] = mac_address[4]; - bytes[15] = mac_address[5]; - Some(Ipv6Addr::from(bytes)) - } else { - None - } -} - -pub fn send_neigh_solicitation( - interface_name: String, - target_address: &Ipv6Addr, - src_address: &Ipv6Addr, -) { - let interface_names_match = |iface: &datalink::NetworkInterface| iface.name == interface_name; - - let interfaces = datalink::interfaces(); - let interface = match interfaces.into_iter().find(interface_names_match) { - Some(iface) => iface, - None => { - error!("Error getting interface"); - return; - } - }; - - let (mut tx, mut _rx) = match datalink::channel(&interface, Default::default()) { - Ok(Ethernet(tx, rx)) => (tx, rx), - Ok(_) => { - error!("Unhandled channel type"); - return; - } - Err(e) => { - error!("Error creating channel: {}", e); - return; - } - }; - - let mut packet_buffer = [0u8; 86]; - let mut ethernet_packet = match MutableEthernetPacket::new(&mut packet_buffer) { - Some(packet) => packet, - None => { - error!("Failed to create Ethernet packet"); - return; - } - }; - - ethernet_packet.set_destination(MacAddr::broadcast()); - ethernet_packet.set_source(match interface.mac { - Some(mac) => mac, - None => { - error!("Interface MAC address not available"); - return; - } - }); - ethernet_packet.set_ethertype(EtherTypes::Ipv6); - - let mut ipv6_and_icmp_buffer = [0u8; 72]; - - let mut ipv6_packet = match MutableIpv6Packet::new(&mut ipv6_and_icmp_buffer[..40]) { - Some(packet) => packet, - None => { - error!("Failed to create IPv6 packet"); - return; - } - }; - ipv6_packet.set_version(6); - ipv6_packet.set_next_header(IpNextHeaderProtocols::Icmpv6); - ipv6_packet.set_payload_length(32); - ipv6_packet.set_hop_limit(255); - ipv6_packet.set_source(*src_address); - ipv6_packet.set_destination(*target_address); - - let mut icmp_packet = match MutableIcmpv6Packet::new(&mut ipv6_and_icmp_buffer[40..]) { - Some(packet) => packet, - None => { - error!("Failed to create ICMPv6 packet"); - return; - } - }; - icmp_packet.set_icmpv6_type(Icmpv6Types::NeighborSolicit); - icmp_packet.set_icmpv6_code(Icmpv6Code(0)); - icmp_packet.set_checksum(0); - - let mut icmp_payload = [0u8; 28]; - icmp_payload[4..20].copy_from_slice(&target_address.octets()); - icmp_payload[20] = 1; - icmp_payload[21] = 1; - icmp_payload[22..28].copy_from_slice(&match interface.mac { - Some(mac) => mac.octets(), - None => { - error!("Interface MAC address not available"); - return; - } - }); - icmp_packet.set_payload(&icmp_payload); - - let checksum = checksum( - &Icmpv6Packet::new(icmp_packet.packet()).unwrap(), - src_address, - target_address, - ); - icmp_packet.set_checksum(checksum); - - ethernet_packet.set_payload(&ipv6_and_icmp_buffer); - - match tx.send_to(ethernet_packet.packet(), Some(interface.clone())) { - Some(Ok(_)) => info!("Neighbor solicitation sent."), - Some(Err(e)) => error!("Failed to send neighbor solicitation: {}", e), - None => error!("Failed to send neighbor solicitation: send_to returned None"), - } -} - -fn send_router_solicitation(interface: &NetworkInterface, tx: &mut dyn datalink::DataLinkSender) { - let source_ip = Ipv6Addr::UNSPECIFIED; - let destination_ip = "ff02::2".parse::().unwrap(); - - let mut packet_buffer = [0u8; 128]; - let mut ethernet_packet = MutableEthernetPacket::new(&mut packet_buffer).unwrap(); - - ethernet_packet.set_destination(MacAddr::broadcast()); - ethernet_packet.set_source(interface.mac.unwrap()); - ethernet_packet.set_ethertype(EtherTypes::Ipv6); - - let mut ipv6_and_icmp_buffer = [0u8; 48]; - - let mut ipv6_packet = MutableIpv6Packet::new(&mut ipv6_and_icmp_buffer[..40]).unwrap(); - ipv6_packet.set_version(6); - ipv6_packet.set_next_header(IpNextHeaderProtocols::Icmpv6); - ipv6_packet.set_payload_length(8); - ipv6_packet.set_hop_limit(255); - ipv6_packet.set_source(source_ip); - ipv6_packet.set_destination(destination_ip); - - let mut icmp_packet = MutableIcmpv6Packet::new(&mut ipv6_and_icmp_buffer[40..]).unwrap(); - icmp_packet.set_icmpv6_type(Icmpv6Types::RouterSolicit); - - let checksum = checksum( - &Icmpv6Packet::new(icmp_packet.packet()).unwrap(), - &source_ip, - &destination_ip, - ); - icmp_packet.set_checksum(checksum); - - ethernet_packet.set_payload(&ipv6_and_icmp_buffer); - - match tx.send_to(ethernet_packet.packet(), Some(interface.clone())) { - Some(Ok(_)) => info!("Router solicitation sent."), - Some(Err(e)) => error!("Failed to send router solicitation: {}", e), - None => error!("Failed to send router solicitation: send_to returned None"), - } -} - -pub fn is_dhcpv6_needed(interface_name: String, ignore_ra_flag: bool) -> Option { - let interface_names_match = |iface: &datalink::NetworkInterface| iface.name == interface_name; - let mut sender_ipv6_address: Option = None; - - let interfaces = datalink::interfaces(); - let interface = interfaces - .into_iter() - .find(interface_names_match) - .expect("Error getting interface"); - - let (mut tx, mut rx) = match datalink::channel(&interface, Default::default()) { - Ok(Ethernet(tx, rx)) => (tx, rx), - Ok(_) => panic!("Unhandled channel type"), - Err(e) => panic!("Error creating channel: {}", e), - }; - - info!("Sending Router Solicitation ..."); - sleep(Duration::from_secs(5)); - send_router_solicitation(&interface, &mut *tx); - - while let Ok(raw_packet) = rx.next() { - let ethernet_packet = EthernetPacket::new(raw_packet).unwrap(); - if ethernet_packet.get_ethertype() == EtherTypes::Ipv6 { - info!("Router Advertisement processing starting ... "); - let payload = ethernet_packet.payload(); - let ipv6_packet = Ipv6Packet::new(payload).unwrap(); - sender_ipv6_address = Some(ipv6_packet.get_source()); - info!("Router Address received: {}", sender_ipv6_address.unwrap()); - if let Some(icmp_packet) = Icmpv6Packet::new(ipv6_packet.payload()) { - if icmp_packet.get_icmpv6_type() == Icmpv6Types::RouterAdvert { - if let Some(router_advert) = RouterAdvertPacket::new(ipv6_packet.payload()) { - info!("Router Flags: {}", router_advert.get_flags()); - if (router_advert.get_flags() & 0xC0) == 0xC0 || ignore_ra_flag { - break; - } - } else { - warn!("Failed to parse Router Advertisement packet"); - } - } else { - warn!("Received ICMPv6 type: {:?}", icmp_packet.get_icmpv6_type()); - } - } else { - warn!("Failed to parse as ICMPv6 Packet"); - } - } - } - sender_ipv6_address -} - -pub struct PrefixInfo { - pub prefix: Ipv6Addr, - pub prefix_length: u8, -} - -pub struct Dhcpv6Result { - pub address: Ipv6Addr, - pub prefix: Option, -} - -pub async fn run_dhcpv6_client( - interface_name: String, -) -> Result> { - let chaddr = vec![ - 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, - ]; - let random_xid: [u8; 3] = [0x12, 0x34, 0x56]; - let multicast_address = "[FF02::1:2]:547".parse::().unwrap(); - let mut ia_addr_confirm: Option = None; - let mut ia_pd_confirm: Option = None; - - let interface_index = get_interface_index(interface_name.clone()).await?; - let socket = create_multicast_socket(interface_name.clone(), interface_index, 546)?; - - let mut msg = Message::new(MessageType::Solicit); - msg.opts_mut().insert(DhcpOption::ClientId(chaddr.clone())); - msg.opts_mut().insert(DhcpOption::ElapsedTime(0)); - msg.set_xid(random_xid); - - msg.opts_mut().insert(DhcpOption::RapidCommit); - - let mut oro = ORO { opts: Vec::new() }; - oro.opts.push(OptionCode::DomainNameServers); - oro.opts.push(OptionCode::DomainSearchList); - oro.opts.push(OptionCode::ClientFqdn); - oro.opts.push(OptionCode::SntpServers); - oro.opts.push(OptionCode::RapidCommit); - oro.opts.push(OptionCode::IAPD); - oro.opts.push(OptionCode::IAPrefix); - - msg.opts_mut().insert(DhcpOption::ORO(oro)); - - let ia_addr_instance = IAAddr { - addr: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), - preferred_life: 3000, - valid_life: 5000, - opts: DhcpOptions::default(), - }; - - let mut iana_opts = DhcpOptions::default(); - iana_opts.insert(DhcpOption::IAAddr(ia_addr_instance)); - - let iana_instance = IANA { - id: 123, - t1: 3600, - t2: 7200, - opts: iana_opts, - }; - - msg.opts_mut().insert(DhcpOption::IANA(iana_instance)); - - // Request Prefix Delegation - let iaprefix_instance = IAPrefix { - preferred_lifetime: 0, - prefix_len: 80, - opts: DhcpOptions::default(), - valid_lifetime: 0, - prefix_ip: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), - }; - - let mut iapd_opts = DhcpOptions::default(); - iapd_opts.insert(DhcpOption::IAPrefix(iaprefix_instance)); - - let iapd_instance = IAPD { - id: 456, - t1: 3600, - t2: 7200, - opts: iapd_opts, - }; - - msg.opts_mut().insert(DhcpOption::IAPD(iapd_instance)); - - let mut buf = Vec::new(); - let mut encoder = Encoder::new(&mut buf); - msg.encode(&mut encoder)?; - socket.send_to(&buf, multicast_address).await?; - - let mut recv_buf = [0; 1500]; - loop { - let (size, _) = socket.recv_from(&mut recv_buf).await?; - let response = Message::decode(&mut dhcproto::v6::Decoder::new(&recv_buf[..size]))?; - let mut serverid: Option<&DhcpOption> = None; - let mut ia_addr: Option<&DhcpOption> = None; - let mut ia_pd: Option<&DhcpOption> = None; - - match response.msg_type() { - MessageType::Advertise => { - info!("DHCPv6 processing in progress..."); - if let Some(DhcpOption::IANA(iana)) = response.opts().get(OptionCode::IANA) { - if let Some(ia_addr_opt) = iana.opts.get(OptionCode::IAAddr) { - ia_addr = Some(ia_addr_opt); - } - } - if let Some(DhcpOption::IAPD(iapd)) = response.opts().get(OptionCode::IAPD) { - if let Some(iaprefix_opt) = iapd.opts.get(OptionCode::IAPrefix) { - ia_pd = Some(iaprefix_opt); - } - } - if let Some(server_option) = response.opts().get(OptionCode::ServerId) { - serverid = Some(server_option); - } - - let mut request_msg = Message::new(MessageType::Request); - request_msg.set_xid(random_xid); - request_msg - .opts_mut() - .insert(DhcpOption::ClientId(chaddr.clone())); - request_msg.opts_mut().insert(DhcpOption::ElapsedTime(0)); - if let Some(DhcpOption::ServerId(duid)) = serverid { - request_msg - .opts_mut() - .insert(DhcpOption::ServerId((*duid).clone())); - } else { - warn!("Server ID was not found or not a ServerId type."); - } - - if let Some(DhcpOption::IAAddr(ia_a)) = ia_addr { - let ia_addr_instance = IAAddr { - addr: ia_a.addr, - preferred_life: 3000, - valid_life: 5000, - opts: DhcpOptions::default(), - }; - let mut iana_opts = DhcpOptions::default(); - iana_opts.insert(DhcpOption::IAAddr(ia_addr_instance)); - - let iana_instance = IANA { - id: 123, - t1: 3600, - t2: 7200, - opts: iana_opts, - }; - request_msg - .opts_mut() - .insert(DhcpOption::IANA(iana_instance)); - } else { - warn!("No IP was found in Advertise message"); - } - - if let Some(DhcpOption::IAPrefix(iaprefix)) = ia_pd { - let iapd_instance = IAPD { - id: 456, - t1: 3600, - t2: 7200, - opts: { - let mut opts = DhcpOptions::default(); - opts.insert(DhcpOption::IAPrefix((*iaprefix).clone())); - opts - }, - }; - request_msg - .opts_mut() - .insert(DhcpOption::IAPD(iapd_instance)); - } - - buf.clear(); - request_msg.encode(&mut Encoder::new(&mut buf))?; - socket.send_to(&buf, multicast_address).await?; - } - MessageType::Reply => { - if let Some(DhcpOption::IANA(iana)) = response.opts().get(OptionCode::IANA) { - if let Some(ia_addr_opt) = iana.opts.get(OptionCode::IAAddr) { - ia_addr_confirm = Some((*ia_addr_opt).clone()); - } - } - if let Some(DhcpOption::IAPD(iapd)) = response.opts().get(OptionCode::IAPD) { - if let Some(DhcpOption::IAPrefix(iaprefix)) = - iapd.opts.get(OptionCode::IAPrefix) - { - ia_pd_confirm = Some((*iaprefix).clone()); - } - } - - let mut confirm_msg = Message::new(MessageType::Confirm); - confirm_msg.set_xid(random_xid); - buf.clear(); - confirm_msg.encode(&mut Encoder::new(&mut buf))?; - socket.send_to(&buf, multicast_address).await?; - - break; - } - _ => { - // Ignore other message types - continue; - } - } - } - - if let Some(DhcpOption::IAAddr(ia_a)) = ia_addr_confirm { - let (connection, handle, _) = new_connection()?; - tokio::spawn(connection); - - set_ipv6_address(&handle, &interface_name, ia_a.addr, 128).await?; - info!( - "DHCPv6 processing finished, setting IPv6 address {}", - ia_a.addr - ); - - let prefix_info = ia_pd_confirm.map(|iaprefix| PrefixInfo { - prefix: iaprefix.prefix_ip, - prefix_length: iaprefix.prefix_len, - }); - - if let Some(ref pfx) = prefix_info { - info!( - "Received delegated prefix {} with length {}", - pfx.prefix, pfx.prefix_length - ); - } else { - info!("No prefix delegation received."); - } - - return Ok(Dhcpv6Result { - address: ia_a.addr, - prefix: prefix_info, - }); - } - - Err("No valid address received".into()) -} - -pub async fn set_ipv6_address( - handle: &Handle, - interface_name: &str, - ipv6_addr: Ipv6Addr, - pfx_len: u8, -) -> Result<(), Error> { - let mut links = handle - .link() - .get() - .match_name(interface_name.to_string()) - .execute(); - let link = match links.try_next().await { - Ok(Some(link)) => link, - Ok(None) => return Err(Error::RequestFailed), - Err(e) => return Err(e), - }; - - let address = ipv6_addr; - - handle - .address() - .add(link.header.index, address.into(), pfx_len) - .execute() - .await -} - -pub async fn get_interface_index(interface_name: String) -> io::Result { - task::spawn_blocking(move || { - if_nametoindex(interface_name.as_str()).map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("Error getting index: {}", e)) - }) - }) - .await? -} - -fn create_multicast_socket( - interface_name: String, - interface_index: u32, - lport: u16, -) -> Result> { - let multicast_addr: Ipv6Addr = "ff02::1:2".parse().unwrap(); - - let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?; - socket.set_reuse_address(true)?; - socket.set_reuse_port(true)?; - socket.set_multicast_if_v6(interface_index)?; - socket.join_multicast_v6(&multicast_addr, interface_index)?; - - socket.bind(&SockAddr::from(SocketAddr::V6(SocketAddrV6::new( - Ipv6Addr::UNSPECIFIED, - lport, - 0, - 0, - ))))?; - - socket.bind_device(Some(interface_name.as_bytes()))?; - - socket.set_nonblocking(true)?; - - let udp_socket = UdpSocket::from_std(socket.into())?; - - Ok(udp_socket) -} - -pub async fn run_dhcpv6_server( - interface_name: String, - ip_range: IpRange, -) -> Result<(), Box> { - let interface_index = get_interface_index(interface_name.clone()).await?; - let socket = create_multicast_socket(interface_name.clone(), interface_index, 547)?; - - info!( - "DHCPv6 server listening on interface {} [::]:547 and joined multicast group ff02::1:2", - interface_name - ); - - let mut allocations: HashMap, Ipv6Addr> = HashMap::new(); - let mut available_addresses: Vec = generate_ip_pool(ip_range); - let server_duid = vec![0x00, 0x01, 0x00, 0x01, 0x00, 0x0c, 0x29, 0x3e, 0x5c, 0x3d]; - let mut buf = [0u8; 1500]; - - loop { - let (size, client_addr) = socket.recv_from(&mut buf).await?; - - let message = match Message::decode(&mut Decoder::new(&buf[..size])) { - Ok(msg) => msg, - Err(e) => { - info!("Failed to decode message: {}", e); - continue; - } - }; - - debug!("Received DHCPv6 message: {:?}", message.msg_type()); - - let client_duid_option = message.opts().get(OptionCode::ClientId); - let client_info = match client_duid_option { - Some(DhcpOption::ClientId(duid)) => { - let duid = duid.clone(); - let mac = extract_mac_from_duid(&duid).unwrap_or_else(|| duid.clone()); - ClientInfo { duid, mac } - } - _ => { - debug!("Solicit/Request message without valid Client ID"); - continue; - } - }; - - match message.msg_type() { - MessageType::Solicit => { - handle_solicit( - &socket, - &message, - &client_info, - &mut allocations, - &mut available_addresses, - &server_duid, - client_addr, - ) - .await?; - } - MessageType::Request => { - handle_request( - &socket, - &message, - &client_info, - &mut allocations, - &server_duid, - client_addr, - ) - .await?; - } - _ => { - info!("Unhandled DHCPv6 message type: {:?}", message.msg_type()); - continue; - } - } - } -} - -async fn handle_solicit( - socket: &UdpSocket, - message: &Message, - client_info: &ClientInfo, - allocations: &mut HashMap, Ipv6Addr>, - available_addresses: &mut Vec, - server_duid: &[u8], - client_addr: SocketAddr, -) -> Result<(), Box> { - let iana_option = message.opts().get(OptionCode::IANA); - let iana = match iana_option { - Some(DhcpOption::IANA(iana)) => iana, - _ => { - info!("Solicit message without IA_NA option"); - return Ok(()); - } - }; - - let iaid = iana.id; - - let rapid_commit_requested = message.opts().get(OptionCode::RapidCommit).is_some(); - - let option_request = message.opts().get(OptionCode::ORO); - - let rapid_commit_in_oro = if let Some(DhcpOption::ORO(option_codes)) = option_request { - option_codes.opts.contains(&OptionCode::RapidCommit) - } else { - false - }; - - let rapid_commit = rapid_commit_requested && rapid_commit_in_oro; - - if rapid_commit { - debug!( - "Rapid Commit option detected in Solicit message from client DUID {:?}", - client_info.duid - ); - - let allocated_ip = if let Some(ip) = allocations.get(&client_info.mac) { - *ip - } else if let Some(ip) = available_addresses.pop() { - allocations.insert(client_info.mac.clone(), ip); - ip - } else { - let reply_msg = create_reply_message( - message.xid(), - server_duid, - &client_info.duid, - None, - Some(StatusCode { - status: NoAddrsAvail, - msg: "No addresses available".into(), - }), - ); - - let mut send_buf = Vec::new(); - reply_msg.encode(&mut Encoder::new(&mut send_buf))?; - socket.send_to(&send_buf, &client_addr).await?; - - debug!( - "No available IPs. Sent Reply with NoAddrsAvail to client DUID {:?}", - client_info.duid - ); - return Ok(()); - }; - - info!( - "Assigning IP {} to client DUID {:?} via Rapid Commit", - allocated_ip, client_info.duid - ); - - let ia_addr = IAAddr { - addr: allocated_ip, - preferred_life: 0xFFFFFFFF, - valid_life: 0xFFFFFFFF, - opts: DhcpOptions::default(), - }; - - let mut iana_opts = DhcpOptions::default(); - iana_opts.insert(DhcpOption::IAAddr(ia_addr)); - - let iana = IANA { - id: iaid, - t1: 0xFFFFFFFF, - t2: 0xFFFFFFFF, - opts: iana_opts, - }; - - let mut reply_msg = create_reply_message( - message.xid(), - server_duid, - &client_info.duid, - Some(iana), - Some(StatusCode { - status: dhcproto::v6::Status::Success, - msg: "Success".into(), - }), - ); - - reply_msg.opts_mut().insert(DhcpOption::RapidCommit); - - let mut send_buf = Vec::new(); - reply_msg.encode(&mut Encoder::new(&mut send_buf))?; - socket.send_to(&send_buf, &client_addr).await?; - - debug!( - "Sent Reply message to client DUID {:?} with IP {} via Rapid Commit", - client_info.duid, allocated_ip - ); - - return Ok(()); - } - - debug!( - "Handling Solicit without Rapid Commit for client DUID (Not implemented yet) {:?}", - client_info.duid - ); - //TODO Advertise handling - - Ok(()) -} - -async fn handle_request( - socket: &UdpSocket, - message: &Message, - client_info: &ClientInfo, - allocations: &mut HashMap, Ipv6Addr>, - server_duid: &[u8], - client_addr: SocketAddr, -) -> Result<(), Box> { - let iana_option = message.opts().get(OptionCode::IANA); - let iana = match iana_option { - Some(DhcpOption::IANA(iana)) => iana, - _ => { - info!("Request message without IA_NA option"); - return Ok(()); - } - }; - - let iaid = iana.id; - - let ia_addr_option = iana.opts.get(OptionCode::IAAddr); - let requested_ip = match ia_addr_option { - Some(DhcpOption::IAAddr(ia_addr)) => ia_addr.addr, - _ => { - info!("IA_NA option without IAAddr"); - return Ok(()); - } - }; - - if let Some(allocated_ip) = allocations.get(&client_info.mac) { - if *allocated_ip == requested_ip { - let ia_addr = IAAddr { - addr: requested_ip, - preferred_life: 0xFFFFFFFF, - valid_life: 0xFFFFFFFF, - opts: DhcpOptions::default(), - }; - - let mut iana_opts = DhcpOptions::default(); - iana_opts.insert(DhcpOption::IAAddr(ia_addr)); - - let iana = IANA { - id: iaid, - t1: 0xFFFFFFFF, - t2: 0xFFFFFFFF, - opts: iana_opts, - }; - - let reply_msg = create_reply_message( - message.xid(), - server_duid, - &client_info.duid, - Some(iana), - Some(StatusCode { - status: dhcproto::v6::Status::Success, - msg: "Success".into(), - }), - ); - - let mut send_buf = Vec::new(); - reply_msg.encode(&mut Encoder::new(&mut send_buf))?; - socket.send_to(&send_buf, &client_addr).await?; - - info!( - "Confirmed IP {} for client DUID {:?}", - requested_ip, client_info.duid - ); - } else { - let reply_msg = create_reply_message( - message.xid(), - server_duid, - &client_info.duid, - None, - Some(StatusCode { - status: NoBinding, - msg: "No binding for requested IP".into(), - }), - ); - - let mut send_buf = Vec::new(); - reply_msg.encode(&mut Encoder::new(&mut send_buf))?; - socket.send_to(&send_buf, &client_addr).await?; - - info!( - "No binding for requested IP {} from client DUID {:?}", - requested_ip, client_info.duid - ); - } - } else { - let reply_msg = create_reply_message( - message.xid(), - server_duid, - &client_info.duid, - None, - Some(StatusCode { - status: NoBinding, - msg: "No binding for requested IP".into(), - }), - ); - - let mut send_buf = Vec::new(); - reply_msg.encode(&mut Encoder::new(&mut send_buf))?; - socket.send_to(&send_buf, &client_addr).await?; - - info!( - "No binding for requested IP {} from client DUID {:?}", - requested_ip, client_info.duid - ); - } - - Ok(()) -} - -fn create_reply_message( - xid: [u8; 3], - server_duid: &[u8], - client_duid: &[u8], - iana: Option, - status_code: Option, -) -> Message { - let mut reply_msg = Message::new(MessageType::Reply); - reply_msg.set_xid(xid); - - reply_msg - .opts_mut() - .insert(DhcpOption::ServerId(server_duid.to_vec())); - - reply_msg - .opts_mut() - .insert(DhcpOption::ClientId(client_duid.to_vec())); - - if let Some(iana) = iana { - reply_msg.opts_mut().insert(DhcpOption::IANA(iana)); - } - - if let Some(status) = status_code { - reply_msg.opts_mut().insert(DhcpOption::StatusCode(status)); - } - - reply_msg -} - -fn generate_ip_pool(ip_range: IpRange) -> Vec { - let start_u128 = u128::from(ip_range.start); - let end_u128 = u128::from(ip_range.end); - - let range_size = end_u128 - start_u128 + 1; - if range_size > 1000 { - error!("IP range too large"); - } - - let mut ips = Vec::new(); - for addr_u128 in start_u128..=end_u128 { - let ip = Ipv6Addr::from(addr_u128); - ips.push(ip); - } - ips -} - -fn extract_mac_from_duid(duid: &[u8]) -> Option> { - if duid.len() < 2 { - return None; - } - let duid_type = u16::from_be_bytes([duid[0], duid[1]]); - match duid_type { - 1 => { - if duid.len() < 8 { - return None; - } - let mac = duid[8..].to_vec(); - Some(mac) - } - 3 => { - if duid.len() < 4 { - return None; - } - let mac = duid[4..].to_vec(); - Some(mac) - } - _ => { - // Other DUID types - None - } - } -} - -pub async fn add_ipv6_route( - handle: &Handle, - interface_name: &str, - destination: Ipv6Addr, - prefix_length: u8, - gateway: Option, - metric: u32, - route_type: RouteType, -) -> Result<(), Error> { - let mut links = handle - .link() - .get() - .match_name(interface_name.to_string()) - .execute(); - - let link = match links.try_next().await { - Ok(Some(link)) => link, - Ok(None) => return Err(Error::RequestFailed), - Err(e) => return Err(e), - }; - - let mut route_add_request = handle.route().add(); - let route_msg = route_add_request.message_mut(); - - route_msg.header.address_family = AddressFamily::Inet6; - route_msg.header.scope = RouteScope::Universe; - route_msg.header.protocol = RouteProtocol::Static; - route_msg.header.kind = route_type; - route_msg.header.destination_prefix_length = prefix_length; - route_msg.header.table = RouteHeader::RT_TABLE_MAIN; - - route_msg - .attributes - .push(RouteAttribute::Destination(RouteAddress::from(destination))); - - if route_type == RouteType::Unicast { - if let Some(gw) = gateway { - route_msg - .attributes - .push(RouteAttribute::Gateway(RouteAddress::from(gw))); - } - } - - route_msg - .attributes - .push(RouteAttribute::Oif(link.header.index)); - - route_msg.attributes.push(RouteAttribute::Priority(metric)); - - route_add_request.execute().await -} - -pub fn adjust_base_ip(base_ip: Ipv6Addr, prefix_length: u8, prefix_count: u16) -> Ipv6Addr { - let base_ip_u128: u128 = base_ip.into(); - let subnet_shift = 128 - (prefix_length as u32 + 16); - let subnet_mask: u128 = !(0xFFFFu128 << subnet_shift); - let adjusted_base_ip_u128 = - (base_ip_u128 & subnet_mask) | ((prefix_count as u128) << subnet_shift); - Ipv6Addr::from(adjusted_base_ip_u128) -} - -pub fn add_to_ipv6(addr: Ipv6Addr, prefix_length: u8, increment: u128) -> Ipv6Addr { - let addr_u128: u128 = addr.into(); - let host_bits = 128 - prefix_length as usize; - let host_mask: u128 = if host_bits == 0 { - 0 - } else { - (1u128 << host_bits) - 1 - }; - let host_part = addr_u128 & host_mask; - let new_host = host_part.wrapping_add(increment); - - if new_host > host_mask { - error!("Host address overflow"); - } - - let new_addr = (addr_u128 & !host_mask) | (new_host & host_mask); - Ipv6Addr::from(new_addr) -} - -pub async fn set_ipv6_gateway( - handle: &Handle, - interface_name: &str, - ipv6_gateway: Ipv6Addr, -) -> Result<(), Error> { - let mut links = handle - .link() - .get() - .match_name(interface_name.to_string()) - .execute(); - let link = match links.try_next().await { - Ok(Some(link)) => link, - Ok(None) => return Err(Error::RequestFailed), - Err(e) => return Err(e), - }; - - let mut route_add_request = handle.route().add(); - - let route_msg = route_add_request.message_mut(); - route_msg.header.address_family = AddressFamily::Inet6; - route_msg.header.scope = RouteScope::Universe; - route_msg.header.protocol = RouteProtocol::Static; - route_msg.header.kind = RouteType::Unicast; - route_msg.header.destination_prefix_length = 0; - route_msg - .attributes - .push(RouteAttribute::Gateway(RouteAddress::Inet6(ipv6_gateway))); - route_msg - .attributes - .push(RouteAttribute::Oif(link.header.index)); - - route_add_request.execute().await -} diff --git a/src/network/mod.rs b/src/network/mod.rs deleted file mode 100644 index e4f9531..0000000 --- a/src/network/mod.rs +++ /dev/null @@ -1,167 +0,0 @@ -use log::{debug, error, info}; -use netlink_packet_route::route::RouteType; -use rtnetlink::new_connection; -use std::collections::HashMap; -use std::net::Ipv6Addr; -use std::sync::atomic::{AtomicU16, Ordering}; -use std::sync::{Arc, Mutex}; -use tokio::spawn; -use tokio::task::JoinHandle; -use uuid::Uuid; - -pub mod dhcpv6; -pub mod radv; -mod utils; - -use crate::network::dhcpv6::{ - add_ipv6_route, add_to_ipv6, adjust_base_ip, run_dhcpv6_server, IpRange, -}; -use radv::start_radv_server; -pub use utils::_configure_sriov; -pub use utils::{configure_network_devices, INTERFACE_NAME}; - -#[derive(Debug)] -pub enum Error { - Failed, - AlreadyExists, -} - -#[derive(Debug)] -pub struct Manager { - ipv6_address: Ipv6Addr, - prefix_length: u8, - prefix_count: AtomicU16, - instances: Mutex>, -} - -#[derive(Debug)] -struct Handles { - pub radv_handle: Arc>, - pub dhcpv6_handle: Arc>, -} - -impl Default for Manager { - fn default() -> Self { - Self { - ipv6_address: Ipv6Addr::UNSPECIFIED, - prefix_length: 64, - prefix_count: AtomicU16::new(1), - instances: Mutex::new(HashMap::new()), - } - } -} - -impl Manager { - pub fn new(ipv6_address: Ipv6Addr, prefix_length: u8) -> Self { - Self { - ipv6_address, - prefix_length, - prefix_count: AtomicU16::new(1), - instances: Mutex::new(HashMap::new()), - } - } - - pub fn vm_tap_name(id: &Uuid) -> String { - format!("vmtap{}", &id.to_string()[..8]) - } - - fn exists(&self, id: Uuid) -> Result<(), Error> { - let instances = self.instances.lock().unwrap(); - if instances.contains_key(&id) { - return Err(Error::AlreadyExists); - } - Ok(()) - } - - pub async fn stop_dhcp(&self, id: Uuid) -> Result<(), Error> { - let mut instances = self.instances.lock().unwrap(); - if let Some(handle) = instances.remove(&id) { - handle.radv_handle.abort(); - handle.dhcpv6_handle.abort(); - } - - Ok(()) - } - pub async fn start_dhcp(&self, id: Uuid) -> Result<(), Error> { - self.exists(id)?; - - let interface_name = Manager::device_name(&id); - - info!("created tap device: {}", &interface_name); - - let (base_ip, prefix_length, prefix_count) = self.get_ipv6_info(); - let adjusted_base_ip = adjust_base_ip(base_ip, prefix_length, prefix_count); - let new_prefix_length = prefix_length + 16; - - let ip_start = add_to_ipv6(adjusted_base_ip, new_prefix_length, 100); - let ip_end = add_to_ipv6(adjusted_base_ip, new_prefix_length, 200); - debug!("IP Range: {} - {}", ip_start, ip_end); - - let ip_range = IpRange { - start: ip_start, - end: ip_end, - }; - - let radv_handle = { - let interface_name = interface_name.clone(); - spawn(async move { - if let Err(e) = start_radv_server( - interface_name.to_string(), - adjusted_base_ip, - new_prefix_length, - ) - .await - { - error!("Failed to start RADV server: {}", e); - } - }) - }; - - let dhcpv6_handle = { - let interface_name = interface_name.clone(); - spawn(async move { - if let Err(e) = run_dhcpv6_server(interface_name.to_string(), ip_range).await { - error!("Failed to run DHCPv6 server: {}", e); - } - }) - }; - - { - let mut instances = self.instances.lock().unwrap(); - instances.insert( - id, - Handles { - radv_handle: Arc::new(radv_handle), - dhcpv6_handle: Arc::new(dhcpv6_handle), - }, - ); - } - - let (connection, handle, _) = new_connection().map_err(|_| Error::Failed)?; - spawn(connection); - - add_ipv6_route( - &handle, - &interface_name, - adjusted_base_ip, - new_prefix_length, - None, - 1024, - RouteType::Unicast, - ) - .await - .map_err(|_| Error::Failed)?; - - Ok(()) - } - - pub fn device_name(id: &Uuid) -> String { - format!("vmtap{}", &id.to_string()[..8]) - } - - fn get_ipv6_info(&self) -> (Ipv6Addr, u8, u16) { - let new_count = self.prefix_count.fetch_add(1, Ordering::SeqCst) + 1; - - (self.ipv6_address, self.prefix_length, new_count) - } -} diff --git a/src/network/radv.rs b/src/network/radv.rs deleted file mode 100644 index 9198b32..0000000 --- a/src/network/radv.rs +++ /dev/null @@ -1,221 +0,0 @@ -use log::{debug, error, info}; -use pnet::datalink::{self, Channel::Ethernet, NetworkInterface}; -use pnet::packet::icmpv6::checksum as icmpv6_checksum; -use pnet::packet::icmpv6::{ - ndp::{MutableRouterAdvertPacket, NdpOption, NdpOptionTypes}, - Icmpv6Code, Icmpv6Packet, Icmpv6Types, -}; -use pnet::packet::ipv6::MutableIpv6Packet; -use pnet::packet::{ - ethernet::{EtherTypes, EthernetPacket, MutableEthernetPacket}, - Packet, -}; -use pnet::util::MacAddr; -use std::error::Error; -use std::net::Ipv6Addr; -use std::str::FromStr; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::task; -const M_FLAG: u8 = 1 << 7; -const O_FLAG: u8 = 1 << 6; - -pub async fn start_radv_server( - interface_name: String, - prefix: Ipv6Addr, - prefix_length: u8, -) -> Result<(), Box> { - let interfaces = datalink::interfaces(); - let interface = interfaces - .into_iter() - .find(|iface| iface.name == interface_name) - .ok_or("Interface not found")?; - let interface = Arc::new(interface); - - let (mut tx, mut rx) = match datalink::channel(&interface, Default::default())? { - Ethernet(tx, rx) => (tx, rx), - _ => return Err("Unhandled channel type".into()), - }; - - info!("Listening for Router Solicitations on {}", interface.name); - - let interface_clone = Arc::clone(&interface); - let prefix_clone = prefix; - let prefix_length_clone = prefix_length; - - task::spawn_blocking(move || { - let mut last_unsolicited = Instant::now(); - - loop { - // Handle incoming packets if available - if let Ok(packet) = rx.next() { - if let Err(e) = handle_packet( - &interface_clone, - &mut *tx, - packet, - prefix_clone, - prefix_length_clone, - ) { - error!("Error handling packet: {}", e); - } - } - - if last_unsolicited.elapsed() >= Duration::from_secs(600) { - if let Err(e) = send_router_advertisement( - &interface_clone, - &mut *tx, - Ipv6Addr::from_str("ff02::1").unwrap(), - prefix_clone, - prefix_length_clone, - ) { - error!("Failed to send unsolicited Router Advertisement: {}", e); - } else { - info!("Sent unsolicited Router Advertisement"); - } - last_unsolicited = Instant::now(); - } - - std::thread::sleep(Duration::from_millis(100)); - } - }); - - Ok(()) -} - -fn handle_packet( - interface: &NetworkInterface, - tx: &mut dyn datalink::DataLinkSender, - raw_packet: &[u8], - prefix: Ipv6Addr, - prefix_length: u8, -) -> Result<(), Box> { - let ethernet_packet = - EthernetPacket::new(raw_packet).ok_or("Failed to parse Ethernet packet")?; - - if ethernet_packet.get_ethertype() == EtherTypes::Ipv6 { - let ipv6_packet = pnet::packet::ipv6::Ipv6Packet::new(ethernet_packet.payload()) - .ok_or("Failed to parse IPv6 packet")?; - - if let Some(icmp_packet) = Icmpv6Packet::new(ipv6_packet.payload()) { - if icmp_packet.get_icmpv6_type() == Icmpv6Types::RouterSolicit { - debug!( - "Received Router Solicitation from {}", - ipv6_packet.get_source() - ); - send_router_advertisement( - interface, - tx, - ipv6_packet.get_source(), - prefix, - prefix_length, - )?; - } - } - } - Ok(()) -} - -fn send_router_advertisement( - interface: &NetworkInterface, - tx: &mut dyn pnet::datalink::DataLinkSender, - destination_ip: Ipv6Addr, - prefix: Ipv6Addr, - prefix_length: u8, -) -> Result<(), Box> { - let source_mac = interface.mac.ok_or("Interface has no MAC address")?; - let dest_mac = ipv6_multicast_to_mac(destination_ip); - - let mut ethernet_buffer = [0u8; 1500]; - let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap(); - ethernet_packet.set_destination(dest_mac); - ethernet_packet.set_source(source_mac); - ethernet_packet.set_ethertype(EtherTypes::Ipv6); - - let source_ip = get_link_local_addr(interface)?; - let mut ipv6_buffer = [0u8; 1280]; - let mut ipv6_packet = MutableIpv6Packet::new(&mut ipv6_buffer).unwrap(); - ipv6_packet.set_version(6); - ipv6_packet.set_source(source_ip); - ipv6_packet.set_destination(destination_ip); - ipv6_packet.set_next_header(pnet::packet::ip::IpNextHeaderProtocols::Icmpv6); - ipv6_packet.set_hop_limit(255); - - const RA_LENGTH: usize = 16 + 32; // 16 bytes fixed fields + 32 bytes Prefix Information - let mut ra_buffer = [0u8; RA_LENGTH]; - let mut ra_packet = - MutableRouterAdvertPacket::new(&mut ra_buffer).ok_or("Failed to create RA packet")?; - ra_packet.set_icmpv6_type(Icmpv6Types::RouterAdvert); - ra_packet.set_icmpv6_code(Icmpv6Code(0)); - ra_packet.set_hop_limit(64); - ra_packet.set_flags(M_FLAG | O_FLAG); // Set M and O flags - ra_packet.set_lifetime(1800); - ra_packet.set_reachable_time(0); - ra_packet.set_retrans_time(0); - - let mut pi_data = [0u8; 30]; - pi_data[0] = prefix_length; - pi_data[1] = 0b10000000; - pi_data[2..6].copy_from_slice(&1800u32.to_be_bytes()); - pi_data[6..10].copy_from_slice(&0u32.to_be_bytes()); - pi_data[14..30].copy_from_slice(&prefix.octets()); - - let prefix_option = NdpOption { - option_type: NdpOptionTypes::PrefixInformation, - length: 4, - data: pi_data.to_vec(), - }; - - ra_packet.set_options(&[prefix_option]); - - let checksum = icmpv6_checksum( - &Icmpv6Packet::new(ra_packet.packet()) - .ok_or("Failed to create ICMPv6 packet for checksum")?, - &source_ip, - &destination_ip, - ); - ra_packet.set_checksum(checksum); - - ipv6_packet.set_payload_length(RA_LENGTH as u16); - ipv6_packet.set_payload(&ra_packet.packet()[..RA_LENGTH]); - - ethernet_packet.set_payload(ipv6_packet.packet()); - let ethernet_size = RA_LENGTH + 14 + 40; // ethernet + ipv6 header size - - tx.send_to( - ðernet_packet.packet()[..ethernet_size], - Some(interface.clone()), - ) - .ok_or("Failed to send Router Advertisement")??; - - debug!("Sent Router Advertisement to {}", destination_ip); - Ok(()) -} - -fn get_link_local_addr( - interface: &NetworkInterface, -) -> Result> { - for ip in &interface.ips { - if let std::net::IpAddr::V6(ipv6) = ip.ip() { - if is_unicast_link_local(&ipv6) { - return Ok(ipv6); - } - } - } - Err("No link-local address found on interface".into()) -} - -fn ipv6_multicast_to_mac(ipv6: Ipv6Addr) -> MacAddr { - let segments = ipv6.octets(); - MacAddr::new( - 0x33, - 0x33, - segments[12], - segments[13], - segments[14], - segments[15], - ) -} - -fn is_unicast_link_local(addr: &Ipv6Addr) -> bool { - addr.segments()[0] & 0xffc0 == 0xfe80 -} diff --git a/src/network/utils.rs b/src/network/utils.rs deleted file mode 100644 index 95c2e64..0000000 --- a/src/network/utils.rs +++ /dev/null @@ -1,619 +0,0 @@ -use std::fs::File; -use std::io; -use std::io::Write; - -use crate::network::dhcpv6::*; -use futures::stream::TryStreamExt; -use log::{debug, error, info, warn}; -use rtnetlink::{new_connection, Handle, IpVersion}; -use std::net::Ipv6Addr; -use tokio::fs::{read_link, OpenOptions}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::time::{self, sleep, Duration}; - -use pnet::datalink::{self, Channel::Ethernet, Config}; -use pnet::packet::ethernet::EthernetPacket; -use pnet::packet::icmp::IcmpPacket; -use pnet::packet::icmpv6::{Icmpv6Packet, Icmpv6Types}; -use pnet::packet::ipv4::Ipv4Packet; -use pnet::packet::ipv6::Ipv6Packet; -use pnet::packet::tcp::TcpPacket; -use pnet::packet::udp::UdpPacket; -use pnet::packet::Packet; - -use netlink_packet_route::neighbour::*; -use netlink_packet_route::route::{RouteAddress, RouteAttribute, RouteType}; - -pub const INTERFACE_NAME: &str = "eth0"; - -pub async fn configure_network_devices() -> Result, String> { - let ignore_ra_flag = true; // Till the RA has the correct flags (O or M), ignore the flag - let interface_name = String::from(INTERFACE_NAME); - let (connection, handle, _) = new_connection().unwrap(); - let mut mac_bytes_option: Option> = None; - let mut delegated_prefix_option: Option<(Ipv6Addr, u8)> = None; - tokio::spawn(connection); - - enable_ipv6_forwarding().map_err(|e| format!("Failed to enable ipv6 forwarding: {}", e))?; - - let mut link_ts = handle - .link() - .get() - .match_name(interface_name.clone()) - .execute(); - - let link = link_ts - .try_next() - .await - .map_err(|e| format!("{} not found: {}", interface_name, e))? - .ok_or("option A empty".to_string())?; - - handle - .link() - .set(link.header.index) - .up() - .execute() - .await - .map_err(|e| format!("{} can not be set up: {}", interface_name, e))?; - - info!("{}:", interface_name); - for attr in link.attributes { - match attr { - netlink_packet_route::link::LinkAttribute::Address(mac_bytes) => { - info!(" mac: {}", format_mac(mac_bytes.clone())); - mac_bytes_option = Some(mac_bytes); - } - netlink_packet_route::link::LinkAttribute::Carrier(carrier) => { - info!(" carrier: {}", carrier); - } - netlink_packet_route::link::LinkAttribute::Mtu(mtu) => { - info!(" mtu: {}", mtu); - } - netlink_packet_route::link::LinkAttribute::MaxMtu(max_mtu) => { - info!(" max_mtu: {}", max_mtu); - } - netlink_packet_route::link::LinkAttribute::OperState(state) => { - let state = match state { - netlink_packet_route::link::State::Unknown => String::from("unknown"), - netlink_packet_route::link::State::NotPresent => String::from("not present"), - netlink_packet_route::link::State::Down => String::from("down"), - netlink_packet_route::link::State::LowerLayerDown => { - String::from("lower layer down") - } - netlink_packet_route::link::State::Testing => String::from("testing"), - netlink_packet_route::link::State::Dormant => String::from("dormant"), - netlink_packet_route::link::State::Up => String::from("up"), - netlink_packet_route::link::State::Other(x) => { - format!("other ({})", x) - } - _ => String::from("unknown state"), - }; - info!(" state: {}", state); - } - _ => (), - } - } - - if let Some(mac_bytes) = mac_bytes_option { - match mac_to_ipv6_link_local(&mac_bytes) { - Some(ipv6_ll_addr) => { - let result = set_ipv6_address(&handle, &interface_name, ipv6_ll_addr, 64).await; - if let Err(e) = result { - warn!( - "{} cannot set link local IPv6 address: {}", - interface_name, e - ); - } - } - None => warn!("Invalid MAC address length"), - } - } else { - warn!("No MAC address found for IPv6 link-local address calculation"); - } - - if let Some(ipv6_gateway) = is_dhcpv6_needed(interface_name.clone(), ignore_ra_flag) { - time::sleep(Duration::from_secs(4)).await; - match run_dhcpv6_client(interface_name.clone()).await { - Ok(result) => { - send_neigh_solicitation(interface_name.clone(), &ipv6_gateway, &result.address); - if let Some(prefix_info) = result.prefix { - let delegated_prefix = prefix_info.prefix; - let prefix_length = prefix_info.prefix_length; - info!( - "Received delegated prefix {} with length {}", - delegated_prefix, prefix_length - ); - delegated_prefix_option = Some((delegated_prefix, prefix_length)); - if let Err(e) = add_ipv6_route( - &handle, - INTERFACE_NAME, - delegated_prefix, - prefix_length, - None, - 1024, - RouteType::Unreachable, - ) - .await - { - error!("Failed to add unreachable IPv6 route: {}", e); - } - } else { - info!("No prefix delegation received."); - } - info!( - "Setting IPv6 gateway to {} on interface {}", - ipv6_gateway, interface_name - ); - if let Err(e) = set_ipv6_gateway(&handle, &interface_name, ipv6_gateway).await { - warn!("Failed to set IPv6 gateway: {}", e); - } - } - Err(e) => warn!("Error: {}", e), - } - } - - let mut addr_ts = handle - .address() - .get() - .set_link_index_filter(link.header.index) - .execute(); - - while let Some(addr_msg) = addr_ts - .try_next() - .await - .map_err(|e| format!("Could not get addr: {}", e))? - { - for attr in addr_msg.attributes { - if let netlink_packet_route::address::AddressAttribute::Address(addr) = attr { - info!("- {}/{}", addr, addr_msg.header.prefix_len); - } - } - } - - Ok(delegated_prefix_option) -} - -pub fn enable_ipv6_forwarding() -> Result<(), std::io::Error> { - let forwarding_paths = ["/proc/sys/net/ipv6/conf/all/forwarding"]; - - for path in forwarding_paths { - let mut file = File::create(path)?; - file.write_all(b"1")?; - } - - Ok(()) -} - -// Keep for debugging purposes -async fn _print_ipv6_routes( - handle: &Handle, - iface_index: u32, - interface_name: &str, -) -> Result<(), Box> { - info!("IPv6 Routes:"); - - let mut route_ts = handle.route().get(IpVersion::V6).execute(); - - while let Some(route_msg) = route_ts - .try_next() - .await - .map_err(|e| format!("Could not get route: {}", e))? - { - let mut destination: Option = None; - let mut gateway: Option = None; - let mut oif: Option = None; - - for attr in &route_msg.attributes { - match attr { - RouteAttribute::Oif(oif_idx) => { - oif = Some(*oif_idx); - debug!("Route OIF: {}", oif_idx); - } - - RouteAttribute::Destination(dest) => { - match dest { - RouteAddress::Inet6(addr) => { - destination = Some(format!( - "{}/{}", - addr, route_msg.header.destination_prefix_length - )); - debug!("Parsed IPv6 Destination: {}", addr); - } - RouteAddress::Other(v) => { - if v.is_empty() { - destination = Some("::/0".to_string()); - debug!("Parsed Default Route"); - } else { - // Unknown or unsupported address - let hex_str = v - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(":"); - destination = Some(format!("unknown({})", hex_str)); - debug!("Parsed Unknown Destination: {}", hex_str); - } - } - _ => { - debug!("Unhandled Destination variant"); - } - } - } - - RouteAttribute::Gateway(gw) => match gw { - RouteAddress::Inet6(addr) => { - gateway = Some(addr.to_string()); - debug!("Parsed IPv6 Gateway: {}", addr); - } - RouteAddress::Other(v) => { - if v.is_empty() { - debug!("Parsed Empty Gateway"); - } else { - let hex_str = v - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(":"); - gateway = Some(format!("unknown({})", hex_str)); - debug!("Parsed Unknown Gateway: {}", hex_str); - } - } - _ => { - debug!("Unhandled Gateway variant"); - } - }, - _ => {} - } - } - - let is_unreachable = route_msg.header.kind == RouteType::Unreachable; - - if !is_unreachable && oif != Some(iface_index) { - debug!( - "Skipping route not associated with interface '{}'", - interface_name - ); - continue; - } - - if route_msg.header.destination_prefix_length == 0 && destination.is_none() { - destination = Some("::/0".to_string()); - debug!("Default route detected (no destination attribute)"); - } - - let dest_str = destination.unwrap_or_else(|| { - if is_unreachable { - "unreachable".to_string() - } else { - "unknown".to_string() - } - }); - - let mut route_str = dest_str.to_string(); - - if let Some(gw) = gateway { - route_str.push_str(&format!(" via {}", gw)); - } - - if oif.is_some() { - if is_unreachable { - route_str.push_str(&format!(" dev {} [unreachable]", interface_name)); - } else { - route_str.push_str(&format!(" dev {}", interface_name)); - } - } else if is_unreachable { - route_str.push_str(" [unreachable]"); - } - - info!("- {}", route_str); - } - Ok(()) -} - -fn format_mac(bytes: Vec) -> String { - bytes - .iter() - .map(|byte| format!("{:02x}", byte)) - .collect::>() - .join(":") -} - -pub async fn _configure_sriov(num_vfs: u32) -> Result<(), String> { - let base_path = format!("/sys/class/net/{}/device", INTERFACE_NAME); - - let file_path = format!("{}/sriov_numvfs", base_path); - let mut file = OpenOptions::new() - .write(true) - .open(&file_path) - .await - .map_err(|e| e.to_string())?; - - let value = format!("{}\n", num_vfs); - if let Err(e) = file.write_all(value.as_bytes()).await { - return Err(format!("Failed to write to the file: {}", e)); - } - info!("Created {} sriov virtual functions", num_vfs); - - let device_path = read_link(base_path).await.map_err(|e| e.to_string())?; - let pci_address = device_path - .file_name() - .ok_or("No PCI address found".to_string())?; - let pci_address = pci_address.to_str().ok_or("No PCI address found")?; - - info!("Found PCI address of {}: {}", INTERFACE_NAME, pci_address); - - let sriov_offset = get_device_information(pci_address, "sriov_offset") - .await - .map_err(|e| e.to_string())?; - - let sriov_offset = match sriov_offset.parse::() { - Ok(n) => n, - Err(e) => return Err(e.to_string()), - }; - - let base_pci_address = parse_pci_address(pci_address)?; - - let virtual_funcs: Vec = (0..num_vfs) - .map(|x| nth_next_pci_address(base_pci_address, x + sriov_offset)) - .map(format_pci_address) - .collect(); - - const RETRIES: i32 = 5; - for (index, vf) in virtual_funcs.iter().enumerate() { - for i in 1..RETRIES { - info!("try to unbind device {}: {:?}/{}", vf, i, RETRIES); - if let Err(e) = unbind_device(vf).await { - warn!("failed to unbind device {}: {}", vf, e.to_string()); - sleep(Duration::from_secs(2)).await; - } else { - info!("successfull unbound device {}", vf); - - if let Err(e) = bind_device(index, vf).await { - warn!("failed to bind devices: {}", e.to_string()) - } - break; - } - } - } - - Ok(()) -} - -fn parse_pci_address(address: &str) -> Result<(u16, u8, u8, u8), String> { - let parts: Vec<&str> = address.split(&[':', '.', ' '][..]).collect(); - if parts.len() != 4 { - return Err("Invalid PCI address format".to_string()); - } - - let domain = u16::from_str_radix(parts[0], 16).map_err(|_| "Invalid domain".to_string())?; - let bus = u8::from_str_radix(parts[1], 16).map_err(|_| "Invalid bus".to_string())?; - let slot = u8::from_str_radix(parts[2], 16).map_err(|_| "Invalid slot".to_string())?; - let function = u8::from_str_radix(parts[3], 16).map_err(|_| "Invalid function".to_string())?; - - Ok((domain, bus, slot, function)) -} - -fn nth_next_pci_address(address: (u16, u8, u8, u8), n: u32) -> (u16, u8, u8, u8) { - let (domain, bus, slot, function) = address; - let total_functions = (domain as u32) * 256 * 32 * 8 - + (bus as u32) * 32 * 8 - + (slot as u32) * 8 - + function as u32 - + n; - - let new_domain = (total_functions / (256 * 32 * 8)) as u16; - let remaining = total_functions % (256 * 32 * 8); - let new_bus = (remaining / (32 * 8)) as u8; - let remaining = remaining % (32 * 8); - let new_slot = (remaining / 8) as u8; - let new_function = (remaining % 8) as u8; - - (new_domain, new_bus, new_slot, new_function) -} - -fn format_pci_address(address: (u16, u8, u8, u8)) -> String { - let (domain, bus, slot, function) = address; - format!("{:04x}:{:02x}:{:02x}.{}", domain, bus, slot, function) -} - -async fn unbind_device(pci: &str) -> Result<(), io::Error> { - let unbind_path = format!("/sys/bus/pci/devices/{}/driver/unbind", pci); - let mut file = OpenOptions::new().write(true).open(&unbind_path).await?; - - file.write_all(pci.as_bytes()).await?; - info!("unbound device: {}", pci); - Ok(()) -} - -async fn bind_device(index: usize, pci_address: &str) -> Result<(), io::Error> { - info!("try to bind device to vfio: {}", pci_address); - if index == 0 { - vfio_new_id(pci_address).await - } else { - vfio_bind(pci_address).await - } -} - -async fn vfio_new_id(pci_address: &str) -> Result<(), io::Error> { - let vendor = get_device_information(pci_address, "vendor").await?; - let vendor = vendor[2..].to_string(); - - let device = get_device_information(pci_address, "device").await?; - let device = device[2..].to_string(); - - let mut file = OpenOptions::new() - .write(true) - .open("/sys/bus/pci/drivers/vfio-pci/new_id") - .await?; - - let content = format!("{} {}", vendor, device); - file.write_all(content.as_bytes()).await?; - info!("bound devices ({}) to vfio-pci", pci_address); - Ok(()) -} - -async fn vfio_bind(pci_address: &str) -> Result<(), io::Error> { - let mut file = OpenOptions::new() - .write(true) - .open("/sys/bus/pci/drivers/vfio-pci/bind") - .await?; - - file.write_all(pci_address.as_bytes()).await?; - info!("bound devices ({}) to vfio-pci", pci_address); - Ok(()) -} - -async fn get_device_information(pci: &str, field: &str) -> Result { - let path = format!("/sys/bus/pci/devices/{}/{}", pci, field); - let mut file = OpenOptions::new().read(true).open(&path).await?; - - let mut dst = String::new(); - file.read_to_string(&mut dst).await?; - - Ok(dst.trim().to_string()) -} - -// Print all packets to the console for debugging purposes -pub async fn _capture_packets(interface_name: String) { - let interfaces = datalink::interfaces(); - let interface = interfaces - .into_iter() - .find(|iface| iface.name == interface_name) - .expect("Network interface not found"); - - let config = Config { - promiscuous: true, - ..Default::default() - }; - - let (_, mut rx) = match datalink::channel(&interface, config) { - Ok(Ethernet(tx, rx)) => (tx, rx), - Ok(_) => panic!("Unhandled channel type"), - Err(e) => panic!( - "An error occurred when creating the datalink channel: {}", - e - ), - }; - - info!("Capturing packets on interface: {}", interface_name); - - loop { - match rx.next() { - Ok(packet) => { - let ethernet = EthernetPacket::new(packet).unwrap(); - info!("Ethernet packet: {:?}", ethernet); - - match ethernet.get_ethertype() { - pnet::packet::ethernet::EtherTypes::Ipv4 => { - if let Some(ipv4) = Ipv4Packet::new(ethernet.payload()) { - info!("IPv4 packet: {:?}", ipv4); - match ipv4.get_next_level_protocol() { - pnet::packet::ip::IpNextHeaderProtocols::Tcp => { - if let Some(tcp) = TcpPacket::new(ipv4.payload()) { - info!("TCP packet: {:?}", tcp); - } - } - pnet::packet::ip::IpNextHeaderProtocols::Udp => { - if let Some(udp) = UdpPacket::new(ipv4.payload()) { - info!("UDP packet: {:?}", udp); - } - } - pnet::packet::ip::IpNextHeaderProtocols::Icmp => { - if let Some(icmp) = IcmpPacket::new(ipv4.payload()) { - info!("ICMP packet: {:?}", icmp); - } - } - _ => info!("Unknown IPv4 L4 protocol"), - } - } - } - pnet::packet::ethernet::EtherTypes::Ipv6 => { - if let Some(ipv6) = Ipv6Packet::new(ethernet.payload()) { - info!("IPv6 packet: {:?}", ipv6); - match ipv6.get_next_header() { - pnet::packet::ip::IpNextHeaderProtocols::Tcp => { - if let Some(tcp) = TcpPacket::new(ipv6.payload()) { - info!("TCP packet: {:?}", tcp); - } - } - pnet::packet::ip::IpNextHeaderProtocols::Udp => { - if let Some(udp) = UdpPacket::new(ipv6.payload()) { - info!("UDP packet: {:?}", udp); - } - } - pnet::packet::ip::IpNextHeaderProtocols::Icmpv6 => { - if let Some(icmpv6) = Icmpv6Packet::new(ipv6.payload()) { - info!("ICMPv6 packet: {:?}", icmpv6); - match icmpv6.get_icmpv6_type() { - Icmpv6Types::RouterSolicit => { - info!("Router Solicitation") - } - Icmpv6Types::RouterAdvert => { - info!("Router Advertisement") - } - Icmpv6Types::NeighborSolicit => { - info!("Neighbor Solicitation") - } - Icmpv6Types::NeighborAdvert => { - info!("Neighbor Advertisement") - } - Icmpv6Types::Redirect => info!("Redirect"), - _ => info!("Other ICMPv6 type"), - } - } - } - pnet::packet::ip::IpNextHeaderProtocols::Hopopt => { - info!("IPv6 Hop-by-Hop Options header"); - } - _ => info!( - "Unknown or unsupported next header: {:?}", - ipv6.get_next_header() - ), - } - } - } - _ => info!("Unknown packet type"), - } - } - Err(e) => { - info!("An error occurred while reading: {}", e); - tokio::task::yield_now().await; - } - } - } -} - -async fn _get_nd_cache() -> Result<(), Box> { - let (connection, handle, _) = new_connection().unwrap(); - tokio::spawn(connection); - - let mut neighbors = handle.neighbours().get().execute(); - - while let Ok(Some(neigh)) = neighbors.try_next().await { - for attr in &neigh.attributes { - match attr { - NeighbourAttribute::Destination(addr) => { - info!("IP Address: {:?}", addr); - } - NeighbourAttribute::LinkLocalAddress(lladdr) => { - let hex_address: String = lladdr - .iter() - .map(|byte| format!("{:02x}", byte)) - .collect::>() - .join(":"); - info!("Link-layer Address: {:?}", hex_address); - } - NeighbourAttribute::CacheInfo(info) => { - info!("Cache Info: {:?}", info); - } - _ => { - info!("Other attribute: {:?}", attr); - } - } - } - info!("------------------------"); - } - info!(""); - info!(""); - Ok(()) -} diff --git a/src/ringbuffer.rs b/src/ringbuffer.rs deleted file mode 100644 index 19908cc..0000000 --- a/src/ringbuffer.rs +++ /dev/null @@ -1,89 +0,0 @@ -use chrono::{DateTime, Utc}; -use log::{LevelFilter, Metadata, Record}; -use std::collections::VecDeque; -use std::sync::Arc; -use tokio::sync::{mpsc, Mutex, RwLock}; - -#[derive(Debug)] -pub struct RingBuffer { - buffer: RwLock>, - capacity: usize, -} - -impl RingBuffer { - pub fn new(capacity: usize) -> Arc { - Arc::new(Self { - buffer: RwLock::new(VecDeque::with_capacity(capacity)), - capacity, - }) - } - - pub async fn push(&self, value: String) { - let mut buffer = self.buffer.write().await; - if buffer.len() == self.capacity { - buffer.pop_front(); - } - buffer.push_back(value); - } - - pub async fn get_lines(&self) -> Vec { - let buffer = self.buffer.read().await; - buffer.iter().cloned().collect() - } -} - -impl Default for RingBuffer { - fn default() -> Self { - RingBuffer { - buffer: RwLock::new(VecDeque::with_capacity(10)), - capacity: 10, - } - } -} - -pub struct SimpleLogger { - buffer: Arc, - sender: mpsc::Sender, -} - -impl SimpleLogger { - pub fn new(buffer: Arc, sender: mpsc::Sender) -> Self { - SimpleLogger { buffer, sender } - } -} - -impl log::Log for SimpleLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= log::max_level() - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - let now: DateTime = Utc::now(); - let log_message = format!( - "{} {} [{}] - {}", - now.to_rfc3339(), - record.level(), - record.target(), - record.args() - ); - let buffer = self.buffer.clone(); - let sender = self.sender.clone(); - println!("{}", log_message); - tokio::spawn(async move { - buffer.push(log_message.clone()).await; - let _ = sender.send(log_message).await; - }); - } - } - - fn flush(&self) {} -} - -pub fn init_logger(buffer: Arc) -> Arc>> { - let (sender, receiver) = mpsc::channel(100); - let logger = SimpleLogger::new(buffer, sender); - log::set_boxed_logger(Box::new(logger)).unwrap(); - log::set_max_level(LevelFilter::Info); - Arc::new(Mutex::new(receiver)) -} diff --git a/src/vm/config.rs b/src/vm/config.rs deleted file mode 100644 index 8562adc..0000000 --- a/src/vm/config.rs +++ /dev/null @@ -1,114 +0,0 @@ -use vmm::config::{ - default_netconfig_ip, default_netconfig_mac, default_netconfig_mask, DebugConsoleConfig, - RngConfig, VhostMode, -}; -use vmm::{ - config::{ - ConsoleConfig, ConsoleOutputMode, CpusConfig, DiskConfig, MemoryConfig, PayloadConfig, - }, - vm_config::{NetConfig, VmConfig}, -}; - -pub fn default_vm_cfg() -> VmConfig { - VmConfig { - cpus: CpusConfig { - ..Default::default() - }, - memory: MemoryConfig { - ..Default::default() - }, - payload: Some(PayloadConfig { - kernel: None, - cmdline: None, - initramfs: None, - firmware: None, - }), - disks: None, - serial: ConsoleConfig { - socket: None, - mode: ConsoleOutputMode::Off, - file: None, - iommu: false, - }, - - rate_limit_groups: None, - net: None, - rng: RngConfig { - ..Default::default() - }, - balloon: None, - fs: None, - pmem: None, - console: ConsoleConfig { - file: None, - mode: ConsoleOutputMode::Off, - iommu: false, - socket: None, - }, - debug_console: DebugConsoleConfig { - file: None, - mode: ConsoleOutputMode::Off, - iobase: None, - }, - devices: None, - user_devices: None, - vdpa: None, - vsock: None, - pvpanic: false, - iommu: false, - sgx_epc: None, - numa: None, - watchdog: false, - pci_segments: None, - platform: None, - tpm: None, - preserved_fds: None, - landlock_enable: false, - landlock_rules: None, - } -} - -pub fn default_disk_cfg() -> DiskConfig { - DiskConfig { - path: None, - readonly: false, - direct: false, - iommu: false, - num_queues: 1, - queue_size: 128, - vhost_user: false, - vhost_socket: None, - id: None, - disable_io_uring: false, - disable_aio: false, - rate_limit_group: None, - rate_limiter_config: None, - pci_segment: 0, - serial: None, - queue_affinity: None, - } -} - -pub fn _default_net_cfg() -> NetConfig { - NetConfig { - tap: None, - ip: default_netconfig_ip(), - mask: default_netconfig_mask(), - mac: default_netconfig_mac(), - host_mac: None, - mtu: None, - iommu: false, - num_queues: 2, - queue_size: 256, - vhost_user: false, - vhost_socket: None, - vhost_mode: VhostMode::Client, - id: None, - fds: None, - rate_limiter_config: None, - pci_segment: 0, - offload_tso: true, - offload_ufo: true, - offload_csum: true, - } -} diff --git a/src/vm/image.rs b/src/vm/image.rs deleted file mode 100644 index 09e00cc..0000000 --- a/src/vm/image.rs +++ /dev/null @@ -1,53 +0,0 @@ -use log::info; -use oci_distribution::{errors::OciDistributionError, secrets, Client, ParseError, Reference}; -use std::{ - fs::{self, File}, - io::Write, - path, -}; - -const ROOTFS: &str = "application/vnd.ironcore.image.rootfs.v1alpha1.rootfs"; -const SQUASHFS: &str = "application/vnd.ironcore.image.squashfs.v1alpha1.squashfs"; -const INITRAMFS: &str = "application/vnd.ironcore.image.initramfs.v1alpha1.initramfs"; -const VMLINUZ: &str = "application/vnd.ironcore.image.vmlinuz.v1alpha1.vmlinuz"; - -#[derive(Debug)] -pub enum ImageError { - InvalidReference(ParseError), - PullError(OciDistributionError), - MissingLayer(String), - IOError(std::io::Error), -} - -pub async fn fetch_image(image: String, file_path: path::PathBuf) -> Result<(), ImageError> { - info!("fetching image: {}", image); - let reference = Reference::try_from(image.clone()).map_err(ImageError::InvalidReference)?; - - let c = Client::default(); - - let media_type = vec![ROOTFS, SQUASHFS, INITRAMFS, VMLINUZ]; - let data = c - .pull(&reference, &secrets::RegistryAuth::Anonymous, media_type) - .await - .map_err(ImageError::PullError)?; - info!("image pulled"); - - fs::create_dir_all(file_path.clone()).map_err(ImageError::IOError)?; - - let layers = vec![ROOTFS]; - for layer in layers { - let kernel = match data.layers.iter().find(|x| x.media_type == layer) { - Some(x) => x, - None => return Err(ImageError::MissingLayer(layer.to_string())), - }; - - let mut path = file_path.clone(); - path.push(str::replace(layer, "/", ".")); - - info!("saving layer: location {:?}, layer: {}", file_path, layer); - let mut file = File::create(path).map_err(ImageError::IOError)?; - file.write_all(&kernel.data).map_err(ImageError::IOError)?; - } - - Ok(()) -} diff --git a/src/vm/mod.rs b/src/vm/mod.rs deleted file mode 100644 index b399cec..0000000 --- a/src/vm/mod.rs +++ /dev/null @@ -1,405 +0,0 @@ -use log::info; -use serde_json::json; -use std::{ - collections::HashMap, - num::TryFromIntError, - os::unix::net::UnixStream, - path::{Path, PathBuf}, - process::{Child, Command}, - sync::Mutex, - thread::sleep, - time, -}; -use uuid::Uuid; -use vmm::vm_config; - -use vmm::config::{ - ConsoleConfig, ConsoleOutputMode, CpusConfig, DiskConfig, MemoryConfig, PayloadConfig, - PlatformConfig, VsockConfig, -}; - -use crate::network; -use net_util::MacAddr; - -pub mod config; -pub mod image; - -#[derive(Debug)] -pub enum Error { - AlreadyExists, - NotFound, - SocketFailure(std::io::Error), - InvalidInput(TryFromIntError), - CHCommandFailure(std::io::Error), - CHApiFailure(api_client::Error), - NetworkingError(network::Error), -} - -pub enum BootMode { - FirmwareBoot(FirmwareBootMode), - KernelBoot(KernelBootMode), -} - -pub struct FirmwareBootMode { - pub root_fs: PathBuf, -} - -pub struct KernelBootMode { - pub kernel: PathBuf, - pub initramfs: PathBuf, - pub cmdline: String, -} - -pub enum NetworkMode { - PCIAddress(String), - MACAddress(String), - TAPDeviceName(String), -} - -#[derive(Debug)] -struct VmInfo { - child: Child, -} - -#[derive(Debug)] -pub struct Manager { - pub ch_bin: String, - vms: Mutex>, -} - -impl Default for Manager { - fn default() -> Self { - Self { - ch_bin: String::default(), - vms: Mutex::new(HashMap::new()), - } - } -} - -impl Manager { - pub fn new(ch_bin: String) -> Self { - Self { - ch_bin, - vms: Mutex::new(HashMap::new()), - } - } - - pub fn init_vmm(&self, id: Uuid, wait: bool) -> Result<(), Error> { - let mut vms = self.vms.lock().unwrap(); - if vms.contains_key(&id) { - return Err(Error::AlreadyExists); - } - - let vm = Command::new(self.ch_bin.clone()) - .arg("--api-socket") - .arg(id.to_string()) - .spawn() - .map_err(Error::CHCommandFailure)?; - - vms.insert(id, VmInfo { child: vm }); - - info!("created vmm with id: {}", id.to_string()); - - if !wait { - return Ok(()); - } - - let tries = 3; - for i in 0..tries { - info!( - "waiting until socket is open: vmm: {}, tries: {}/{}", - id.to_string(), - i + 1, - tries - ); - if Path::new(&id.to_string()).exists() { - info!("socket open: vmm: {}", id.to_string()); - return Ok(()); - } - - sleep(time::Duration::from_millis(250)); - } - info!("retries exceeded: vmm: {}", id.to_string()); - - Ok(()) - } - - pub fn create_vm( - &self, - id: Uuid, - cpu: u32, - memory: u64, - boot_mode: BootMode, - ignition: Option, - ) -> Result<(), Error> { - let vms = self.vms.lock().unwrap(); - if !vms.contains_key(&id) { - return Err(Error::NotFound); - } - - let cpu = u8::try_from(cpu).map_err(Error::InvalidInput)?; - - let mut vm_config = config::default_vm_cfg(); - vm_config.cpus = CpusConfig { - boot_vcpus: cpu, - max_vcpus: cpu, - ..Default::default() - }; - vm_config.memory = MemoryConfig { - size: memory, - shared: true, - ..Default::default() - }; - - let mut disks = vec![]; - match boot_mode { - BootMode::FirmwareBoot(firmware_boot) => { - vm_config.payload = Some(PayloadConfig { - firmware: Some(PathBuf::from("/usr/share/cloud-hypervisor/hypervisor-fw")), - kernel: None, - cmdline: None, - initramfs: None, - }); - disks.push(DiskConfig { - path: Some(firmware_boot.root_fs), - ..config::default_disk_cfg() - }); - } - BootMode::KernelBoot(kernel_boot) => { - vm_config.payload = Some(PayloadConfig { - kernel: Some(kernel_boot.kernel), - cmdline: Some(kernel_boot.cmdline.clone()), - initramfs: Some(kernel_boot.initramfs), - firmware: None, - }); - } - }; - - vm_config.vsock = Some(VsockConfig { - cid: 33, - socket: PathBuf::from(format!("vsock{}.sock", network::Manager::device_name(&id))), - id: None, - iommu: false, - pci_segment: 0, - }); - // vm_config.net = Some(vec![NetConfig { - // tap: Some(Manager::vm_tap_name(&id)), - // ..config::_default_net_cfg() - // }]); - - if !disks.is_empty() { - vm_config.disks = Some(disks); - } - - vm_config.serial = ConsoleConfig { - socket: Some(PathBuf::from(id.to_string() + ".console")), - mode: ConsoleOutputMode::Socket, - file: None, - iommu: false, - }; - - if let Some(ignition) = ignition { - info!("configured ignition for vm {}", id.to_string()); - vm_config.platform = Some(PlatformConfig { - num_pci_segments: vm_config::default_platformconfig_num_pci_segments(), - iommu_segments: None, - serial_number: None, - uuid: None, - oem_strings: Some(vec![ignition]), - }) - } - - let vm_config = json!(vm_config); - - let mut socket = UnixStream::connect(id.to_string()).map_err(Error::SocketFailure)?; - let response = api_client::simple_api_full_command_and_response( - &mut socket, - "PUT", - "vm.create", - Some(&vm_config.to_string()), - ) - .map_err(Error::CHApiFailure)?; - if let Some(response) = response { - info!("create vm: id {}, response: {}", id.to_string(), response) - } - - info!("created vm with id: {}", id.to_string()); - Ok(()) - } - - pub fn boot_vm(&self, id: Uuid) -> Result<(), Error> { - let vms = self.vms.lock().unwrap(); - if !vms.contains_key(&id) { - return Err(Error::NotFound); - } - - let mut socket = UnixStream::connect(id.to_string()).map_err(Error::SocketFailure)?; - let response = - api_client::simple_api_full_command_and_response(&mut socket, "PUT", "vm.boot", None) - .map_err(Error::CHApiFailure)?; - if response.is_some() { - info!( - "boot vm: id {}, response: {}", - id.to_string(), - response.unwrap() - ) - } - - info!("booted vm with id: {}", id.to_string()); - Ok(()) - } - - pub fn get_vm_console_path(&self, id: Uuid) -> Result { - let vms = self.vms.lock().unwrap(); - if !vms.contains_key(&id) { - return Err(Error::NotFound); - } - - let socket_path = id.to_string() + ".console"; - if !Path::new(&socket_path).exists() { - return Err(Error::NotFound); - } - - Ok(socket_path) - } - - pub fn add_net_device(&self, id: Uuid, config: NetworkMode) -> Result<(), Error> { - let vms = self.vms.lock().unwrap(); - if !vms.contains_key(&id) { - return Err(Error::NotFound); - } - - let mut socket = UnixStream::connect(id.to_string()).map_err(Error::SocketFailure)?; - - let request: (String, String); - - match config { - NetworkMode::PCIAddress(pci) => { - let path = PathBuf::from(format!("/sys/bus/pci/devices/{}/", pci)); - info!("check if path exists {}", path.display()); - if !path.exists() { - info!("The path {} does not exist.", path.display()); - return Err(Error::NotFound); - } - - info!("add device"); - request = ( - "vm.add-device".to_string(), - json!(vm_config::DeviceConfig { - path, - iommu: false, - id: None, - pci_segment: 0, - x_nv_gpudirect_clique: None, - }) - .to_string(), - ); - } - NetworkMode::MACAddress(mac) => { - let mac = MacAddr::parse_str(&mac).map_err(Error::CHCommandFailure)?; - let mut net_config = config::_default_net_cfg(); - net_config.host_mac = Some(mac); - request = ("vm.add-net".to_string(), json!(net_config).to_string()); - } - NetworkMode::TAPDeviceName(tap) => { - let mut net_config = config::_default_net_cfg(); - net_config.tap = Some(tap); - request = ("vm.add-net".to_string(), json!(net_config).to_string()); - } - } - - let response = api_client::simple_api_full_command_and_response( - &mut socket, - "PUT", - &request.0, - Some(&request.1), - ) - .map_err(Error::CHApiFailure)?; - if response.is_some() { - info!( - "{} to vm: id {}, response: {}", - request.0, - id.to_string(), - response.unwrap() - ) - } - - Ok(()) - } - - pub fn ping_vmm(&self, id: Uuid) -> Result<(), Error> { - let vms = self.vms.lock().unwrap(); - if !vms.contains_key(&id) { - return Err(Error::NotFound); - } - - let mut socket = UnixStream::connect(id.to_string()).map_err(Error::SocketFailure)?; - let response = - api_client::simple_api_full_command_and_response(&mut socket, "GET", "vmm.ping", None) - .map_err(Error::CHApiFailure)?; - if response.is_some() { - info!( - "ping vmm: id {}, response: {}", - id.to_string(), - response.unwrap() - ); - } - - Ok(()) - } - - pub fn get_vm(&self, id: Uuid) -> Result { - let vms = self.vms.lock().unwrap(); - if !vms.contains_key(&id) { - return Err(Error::NotFound); - } - - let mut socket = UnixStream::connect(id.to_string()).map_err(Error::SocketFailure)?; - let response = - api_client::simple_api_full_command_and_response(&mut socket, "GET", "vm.info", None) - .map_err(Error::CHApiFailure)?; - - if let Some(x) = response { - info!("get vm: id {}, response: {}", id.to_string(), x); - return Ok(x); - } - Ok(String::new()) - } - - pub fn kill_vm(&self, id: Uuid) -> Result { - let mut vms = self.vms.lock().unwrap(); - - let mut socket = UnixStream::connect(id.to_string()).map_err(Error::SocketFailure)?; - let response = api_client::simple_api_full_command_and_response( - &mut socket, - "PUT", - "vm.shutdown", - None, - ) - .map_err(Error::CHApiFailure)?; - - if let Some(x) = &response { - info!("shutdown vm: id {}, response: {}", id, x); - } - - let response = api_client::simple_api_full_command_and_response( - &mut socket, - "PUT", - "vmm.shutdown", - None, - ) - .map_err(Error::CHApiFailure)?; - - if let Some(x) = &response { - info!("shutdown vmm: id {}, response: {}", id, x); - } - - if let Some(mut info) = vms.remove(&id) { - if let Err(e) = info.child.kill() { - info!("failed to kill vm process {}: {}", id, e); - } - } - - Ok(String::new()) - } -}