mirror of
https://github.com/edera-dev/krata.git
synced 2025-08-03 13:11:31 +00:00
Compare commits
80 Commits
Author | SHA1 | Date | |
---|---|---|---|
f1e3d59b6a | |||
0106b85de9 | |||
96ccbd50bb | |||
41aa1aa707 | |||
ec74bc8d2b | |||
694de5d1fd | |||
f2db826ba6 | |||
7f5609a846 | |||
adb7b29354 | |||
bd448ee8d9 | |||
1647a07226 | |||
151b43eeec | |||
1123a1a50a | |||
6a6b5b6e0b | |||
274136825a | |||
2ab2cda937 | |||
2519d76479 | |||
dbeb8bf43b | |||
6093627bdd | |||
1d75dfb88a | |||
18bf370f74 | |||
506d2ccf46 | |||
6096dee2fe | |||
bf3b73bf24 | |||
87530edf70 | |||
1dca770091 | |||
01a94ad23e | |||
2a107a370f | |||
313d3f72a5 | |||
5ec3d9d5c1 | |||
1cf03a460e | |||
ffc9dcc0ea | |||
0358c9c775 | |||
dcffaf110e | |||
b81ae5d01a | |||
1756bc6647 | |||
6bf3741ec9 | |||
b7d41ee9f4 | |||
53059e8cca | |||
11bb99b1e4 | |||
eaa84089ce | |||
680244fc5e | |||
d469da4d9b | |||
99091df3cf | |||
08b30c2eaa | |||
224fdbe227 | |||
62569f6c59 | |||
0b991f454e | |||
75aba8a1e3 | |||
8216ab3602 | |||
902fffe207 | |||
45cfc6bb27 | |||
146bda0810 | |||
45e7d7515b | |||
f161b5afd6 | |||
7fe3e2c7cb | |||
3a5be71db4 | |||
d1b910f5c4 | |||
8806a79161 | |||
c8795fa08d | |||
d792eb5439 | |||
398e555bd3 | |||
75901233b1 | |||
04665ce690 | |||
481a5884d9 | |||
5ee1035896 | |||
9bd8d1bb1d | |||
3bada811b2 | |||
e08d25ebde | |||
2c884a6882 | |||
d756fa82f4 | |||
6e051f52b9 | |||
b2fba6400e | |||
b26469be28 | |||
28d63d7d70 | |||
6b91f0be94 | |||
9e91ffe065 | |||
b57d95c610 | |||
de6bfe38fe | |||
f6dffd6e17 |
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@ -5,10 +5,10 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
production-version-updates:
|
||||
dep-updates:
|
||||
dependency-type: "production"
|
||||
applies-to: "version-updates"
|
||||
development-version-updates:
|
||||
dev-updates:
|
||||
dependency-type: "development"
|
||||
applies-to: "version-updates"
|
||||
- package-ecosystem: "cargo"
|
||||
@ -16,20 +16,20 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
production-version-updates:
|
||||
dep-updates:
|
||||
dependency-type: "production"
|
||||
applies-to: "version-updates"
|
||||
development-version-updates:
|
||||
dev-updates:
|
||||
dependency-type: "development"
|
||||
applies-to: "version-updates"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
directory: "/images"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
production-version-updates:
|
||||
dep-updates:
|
||||
dependency-type: "production"
|
||||
applies-to: "version-updates"
|
||||
development-version-updates:
|
||||
dev-updates:
|
||||
dependency-type: "development"
|
||||
applies-to: "version-updates"
|
||||
|
191
.github/workflows/check.yml
vendored
191
.github/workflows/check.yml
vendored
@ -7,30 +7,195 @@ on:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
fmt:
|
||||
name: fmt
|
||||
rustfmt:
|
||||
name: rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
# Temporarily ignored: https://github.com/edera-dev/krata/issues/206
|
||||
- run: ./hack/build/cargo.sh fmt --all -- --check || true
|
||||
- name: install stable rust toolchain with rustfmt
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
rustup component add rustfmt
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: cargo fmt
|
||||
run: ./hack/build/cargo.sh fmt --all -- --check
|
||||
shellcheck:
|
||||
name: shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- run: ./hack/code/shellcheck.sh
|
||||
- name: shellcheck
|
||||
run: ./hack/code/shellcheck.sh
|
||||
full-build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: full build linux-${{ matrix.arch }}
|
||||
steps:
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: install stable rust toolchain
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: cargo build
|
||||
run: ./hack/build/cargo.sh build
|
||||
full-test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: full test linux-${{ matrix.arch }}
|
||||
steps:
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: install stable rust toolchain
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: cargo test
|
||||
run: ./hack/build/cargo.sh test
|
||||
full-clippy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: full clippy linux-${{ matrix.arch }}
|
||||
steps:
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: install stable rust toolchain with clippy
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
rustup component add clippy
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: cargo clippy
|
||||
run: ./hack/build/cargo.sh clippy
|
||||
zone-initrd:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: zone initrd linux-${{ matrix.arch }}
|
||||
steps:
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: install stable rust toolchain with ${{ matrix.arch }}-unknown-linux-gnu and ${{ matrix.arch }}-unknown-linux-musl rust targets
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
rustup target add ${{ matrix.arch }}-unknown-linux-gnu ${{ matrix.arch }}-unknown-linux-musl
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: initrd build
|
||||
run: ./hack/initrd/build.sh
|
||||
kratactl-build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- { os: linux, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: linux, arch: aarch64, on: ubuntu-latest, deps: linux }
|
||||
- { os: darwin, arch: x86_64, on: macos-14, deps: darwin }
|
||||
- { os: darwin, arch: aarch64, on: macos-14, deps: darwin }
|
||||
- { os: freebsd, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: windows, arch: x86_64, on: windows-latest, deps: windows }
|
||||
env:
|
||||
TARGET_OS: "${{ matrix.platform.os }}"
|
||||
TARGET_ARCH: "${{ matrix.platform.arch }}"
|
||||
runs-on: "${{ matrix.platform.on }}"
|
||||
name: kratactl build ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: configure git line endings
|
||||
run: git config --global core.autocrlf false && git config --global core.eol lf
|
||||
if: ${{ matrix.platform.os == 'windows' }}
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: install stable rust toolchain
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
- name: install ${{ matrix.platform.arch }}-apple-darwin rust target
|
||||
run: "rustup target add --toolchain stable ${{ matrix.platform.arch }}-apple-darwin"
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- name: setup homebrew
|
||||
uses: homebrew/actions/setup-homebrew@4b34604e75af8f8b23b454f0b5ffb7c5d8ce0056 # master
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- name: install ${{ matrix.platform.deps }} dependencies
|
||||
run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
|
||||
- name: cargo build kratactl
|
||||
run: ./hack/build/cargo.sh build --bin kratactl
|
||||
|
47
.github/workflows/client.yml
vendored
47
.github/workflows/client.yml
vendored
@ -1,47 +0,0 @@
|
||||
name: client
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- { os: linux, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: linux, arch: aarch64, on: ubuntu-latest, deps: linux }
|
||||
- { os: darwin, arch: x86_64, on: macos-14, deps: darwin }
|
||||
- { os: darwin, arch: aarch64, on: macos-14, deps: darwin }
|
||||
- { os: freebsd, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: windows, arch: x86_64, on: windows-latest, deps: windows }
|
||||
env:
|
||||
TARGET_OS: "${{ matrix.platform.os }}"
|
||||
TARGET_ARCH: "${{ matrix.platform.arch }}"
|
||||
runs-on: "${{ matrix.platform.on }}"
|
||||
name: client build ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- run: git config --global core.autocrlf false && git config --global core.eol lf
|
||||
if: ${{ matrix.platform.os == 'windows' }}
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
if: ${{ matrix.platform.os != 'darwin' }}
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
with:
|
||||
targets: "${{ matrix.platform.arch }}-apple-darwin"
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- uses: homebrew/actions/setup-homebrew@4b34604e75af8f8b23b454f0b5ffb7c5d8ce0056 # master
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
|
||||
- run: ./hack/build/cargo.sh build --bin kratactl
|
122
.github/workflows/nightly.yml
vendored
122
.github/workflows/nightly.yml
vendored
@ -5,10 +5,8 @@ on:
|
||||
- cron: "0 10 * * *"
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
jobs:
|
||||
server:
|
||||
full-build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@ -18,45 +16,49 @@ jobs:
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: nightly server ${{ matrix.arch }}
|
||||
CI_NEEDS_FPM: "1"
|
||||
name: nightly full build linux-${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
with:
|
||||
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/dist/bundle.sh
|
||||
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
- name: install stable rust toolchain with ${{ matrix.arch }}-unknown-linux-gnu and ${{ matrix.arch }}-unknown-linux-musl rust targets
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
rustup target add ${{ matrix.arch }}-unknown-linux-gnu ${{ matrix.arch }}-unknown-linux-musl
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: build systemd bundle
|
||||
run: ./hack/dist/bundle.sh
|
||||
- name: upload systemd bundle
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: krata-bundle-systemd-${{ matrix.arch }}
|
||||
path: "target/dist/bundle-systemd-${{ matrix.arch }}.tgz"
|
||||
compression-level: 0
|
||||
- run: ./hack/dist/deb.sh
|
||||
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
- name: build deb package
|
||||
run: ./hack/dist/deb.sh
|
||||
- name: upload deb package
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: krata-debian-${{ matrix.arch }}
|
||||
path: "target/dist/*.deb"
|
||||
compression-level: 0
|
||||
- run: ./hack/dist/apk.sh
|
||||
env:
|
||||
KRATA_KERNEL_BUILD_SKIP: "1"
|
||||
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
- name: build apk package
|
||||
run: ./hack/dist/apk.sh
|
||||
- name: upload apk package
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: krata-alpine-${{ matrix.arch }}
|
||||
path: "target/dist/*_${{ matrix.arch }}.apk"
|
||||
compression-level: 0
|
||||
- run: ./hack/os/build.sh
|
||||
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
with:
|
||||
name: krata-os-${{ matrix.arch }}
|
||||
path: "target/os/krata-${{ matrix.arch }}.qcow2"
|
||||
compression-level: 0
|
||||
client:
|
||||
kratactl-build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -71,40 +73,49 @@ jobs:
|
||||
TARGET_OS: "${{ matrix.platform.os }}"
|
||||
TARGET_ARCH: "${{ matrix.platform.arch }}"
|
||||
runs-on: "${{ matrix.platform.on }}"
|
||||
name: nightly client ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
name: nightly kratactl build ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- run: git config --global core.autocrlf false && git config --global core.eol lf
|
||||
- name: configure git line endings
|
||||
run: git config --global core.autocrlf false && git config --global core.eol lf
|
||||
if: ${{ matrix.platform.os == 'windows' }}
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
if: ${{ matrix.platform.os != 'darwin' }}
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
with:
|
||||
targets: "${{ matrix.platform.arch }}-apple-darwin"
|
||||
- name: install stable rust toolchain
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
- name: install ${{ matrix.platform.arch }}-apple-darwin rust target
|
||||
run: "rustup target add --toolchain stable ${{ matrix.platform.arch }}-apple-darwin"
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- uses: homebrew/actions/setup-homebrew@4b34604e75af8f8b23b454f0b5ffb7c5d8ce0056 # master
|
||||
- name: setup homebrew
|
||||
uses: homebrew/actions/setup-homebrew@4b34604e75af8f8b23b454f0b5ffb7c5d8ce0056 # master
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
|
||||
- run: ./hack/build/cargo.sh build --release --bin kratactl
|
||||
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
- name: install ${{ matrix.platform.deps }} dependencies
|
||||
run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
|
||||
- name: cargo build kratactl
|
||||
run: ./hack/build/cargo.sh build --release --bin kratactl
|
||||
- name: upload kratactl
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: kratactl-${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
path: "target/*/release/kratactl"
|
||||
if: ${{ matrix.platform.os != 'windows' }}
|
||||
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
- name: upload kratactl
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: kratactl-${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
path: "target/*/release/kratactl.exe"
|
||||
if: ${{ matrix.platform.os == 'windows' }}
|
||||
oci:
|
||||
oci-build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@ -113,31 +124,42 @@ jobs:
|
||||
- kratactl
|
||||
- kratad
|
||||
- kratanet
|
||||
- krata-guest-init
|
||||
name: "oci build ${{ matrix.component }}"
|
||||
- krata-zone
|
||||
name: nightly oci build ${{ matrix.component }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
|
||||
- uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3.4.0
|
||||
- uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
|
||||
- name: install cosign
|
||||
uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3.6.0
|
||||
- name: setup docker buildx
|
||||
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||
- name: login to container registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: "${{ github.actor }}"
|
||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- uses: docker/build-push-action@1a162644f9a7e87d8f4b053101d1d9a712edc18c # v6.3.0
|
||||
- name: docker build and push ${{ matrix.component }}
|
||||
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
||||
id: push
|
||||
with:
|
||||
file: ./images/Dockerfile.${{ matrix.component }}
|
||||
platforms: linux/amd64,linux/aarch64
|
||||
tags: "ghcr.io/edera-dev/${{ matrix.component }}:nightly"
|
||||
push: true
|
||||
- env:
|
||||
- name: cosign sign ${{ matrix.component }}
|
||||
run: cosign sign --yes "${TAGS}@${DIGEST}"
|
||||
env:
|
||||
DIGEST: "${{ steps.push.outputs.digest }}"
|
||||
TAGS: "ghcr.io/edera-dev/${{ matrix.component }}:nightly"
|
||||
COSIGN_EXPERIMENTAL: "true"
|
||||
run: cosign sign --yes "${TAGS}@${DIGEST}"
|
||||
|
37
.github/workflows/os.yml
vendored
37
.github/workflows/os.yml
vendored
@ -1,37 +0,0 @@
|
||||
name: os
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: os build ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
with:
|
||||
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/os/build.sh
|
||||
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
with:
|
||||
name: krata-os-${{ matrix.arch }}
|
||||
path: "target/os/krata-${{ matrix.arch }}.qcow2"
|
||||
compression-level: 0
|
172
.github/workflows/release-assets.yml
vendored
Normal file
172
.github/workflows/release-assets.yml
vendored
Normal file
@ -0,0 +1,172 @@
|
||||
name: release-assets
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: true
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
jobs:
|
||||
services:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
CI_NEEDS_FPM: "1"
|
||||
name: release-assets services ${{ matrix.arch }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: install stable rust toolchain with ${{ matrix.arch }}-unknown-linux-gnu and ${{ matrix.arch }}-unknown-linux-musl rust targets
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
rustup target add ${{ matrix.arch }}-unknown-linux-gnu ${{ matrix.arch }}-unknown-linux-musl
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: build systemd bundle
|
||||
run: ./hack/dist/bundle.sh
|
||||
- name: assemble systemd bundle
|
||||
run: "./hack/ci/assemble-release-assets.sh bundle-systemd ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/bundle-systemd-${{ matrix.arch }}.tgz"
|
||||
- name: build deb package
|
||||
run: ./hack/dist/deb.sh
|
||||
- name: assemble deb package
|
||||
run: "./hack/ci/assemble-release-assets.sh debian ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/*.deb"
|
||||
- name: build apk package
|
||||
run: ./hack/dist/apk.sh
|
||||
- name: assemble apk package
|
||||
run: "./hack/ci/assemble-release-assets.sh alpine ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/*_${{ matrix.arch }}.apk"
|
||||
- name: upload release artifacts
|
||||
run: "./hack/ci/upload-release-assets.sh ${{ github.event.release.tag_name }}"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
kratactl:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- { os: linux, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: linux, arch: aarch64, on: ubuntu-latest, deps: linux }
|
||||
- { os: darwin, arch: x86_64, on: macos-14, deps: darwin }
|
||||
- { os: darwin, arch: aarch64, on: macos-14, deps: darwin }
|
||||
- { os: freebsd, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: windows, arch: x86_64, on: windows-latest, deps: windows }
|
||||
env:
|
||||
TARGET_OS: "${{ matrix.platform.os }}"
|
||||
TARGET_ARCH: "${{ matrix.platform.arch }}"
|
||||
runs-on: "${{ matrix.platform.on }}"
|
||||
name: release-assets kratactl ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: install stable rust toolchain
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
- name: install ${{ matrix.platform.arch }}-apple-darwin rust target
|
||||
run: "rustup target add --toolchain stable ${{ matrix.platform.arch }}-apple-darwin"
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- name: setup homebrew
|
||||
uses: homebrew/actions/setup-homebrew@4b34604e75af8f8b23b454f0b5ffb7c5d8ce0056 # master
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- name: install ${{ matrix.platform.deps }} dependencies
|
||||
run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
|
||||
- name: cargo build kratactl
|
||||
run: ./hack/build/cargo.sh build --release --bin kratactl
|
||||
- name: assemble kratactl executable
|
||||
run: "./hack/ci/assemble-release-assets.sh kratactl ${{ github.event.release.tag_name }} ${{ matrix.platform.os }}-${{ matrix.platform.arch }} target/*/release/kratactl"
|
||||
if: ${{ matrix.platform.os != 'windows' }}
|
||||
- name: assemble kratactl executable
|
||||
run: "./hack/ci/assemble-release-assets.sh kratactl ${{ github.event.release.tag_name }} ${{ matrix.platform.os }}-${{ matrix.platform.arch }} target/*/release/kratactl.exe"
|
||||
if: ${{ matrix.platform.os == 'windows' }}
|
||||
- name: upload release artifacts
|
||||
run: "./hack/ci/upload-release-assets.sh ${{ github.event.release.tag_name }}"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
oci:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
component:
|
||||
- kratactl
|
||||
- kratad
|
||||
- kratanet
|
||||
- krata-zone
|
||||
name: release-assets oci ${{ matrix.component }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
packages: write
|
||||
steps:
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: install cosign
|
||||
uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3.6.0
|
||||
- name: setup docker buildx
|
||||
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||
- name: login to container registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: "${{ github.actor }}"
|
||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: capture krata version
|
||||
id: version
|
||||
run: |
|
||||
echo "KRATA_VERSION=$(./hack/dist/version.sh)" >> "${GITHUB_OUTPUT}"
|
||||
- name: docker build and push ${{ matrix.component }}
|
||||
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
||||
id: push
|
||||
with:
|
||||
file: ./images/Dockerfile.${{ matrix.component }}
|
||||
platforms: linux/amd64,linux/aarch64
|
||||
tags: "ghcr.io/edera-dev/${{ matrix.component }}:${{ steps.version.outputs.KRATA_VERSION }},ghcr.io/edera-dev/${{ matrix.component }}:latest"
|
||||
push: true
|
||||
- name: cosign sign ${{ matrix.component }}:${{ steps.version.outputs.KRATA_VERSION }}
|
||||
run: cosign sign --yes "${TAGS}@${DIGEST}"
|
||||
env:
|
||||
DIGEST: "${{ steps.push.outputs.digest }}"
|
||||
TAGS: "ghcr.io/edera-dev/${{ matrix.component }}:${{ steps.version.outputs.KRATA_VERSION }}"
|
||||
COSIGN_EXPERIMENTAL: "true"
|
||||
- name: cosign sign ${{ matrix.component }}:latest
|
||||
run: cosign sign --yes "${TAGS}@${DIGEST}"
|
||||
env:
|
||||
DIGEST: "${{ steps.push.outputs.digest }}"
|
||||
TAGS: "ghcr.io/edera-dev/${{ matrix.component }}:latest"
|
||||
COSIGN_EXPERIMENTAL: "true"
|
134
.github/workflows/release-binaries.yml
vendored
134
.github/workflows/release-binaries.yml
vendored
@ -1,134 +0,0 @@
|
||||
name: release-binaries
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: true
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
jobs:
|
||||
server:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: release-binaries server ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
with:
|
||||
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/dist/bundle.sh
|
||||
- run: "./hack/ci/assemble-release-assets.sh bundle-systemd ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/bundle-systemd-${{ matrix.arch }}.tgz"
|
||||
- run: ./hack/dist/deb.sh
|
||||
- run: "./hack/ci/assemble-release-assets.sh debian ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/*.deb"
|
||||
- run: ./hack/dist/apk.sh
|
||||
- run: "./hack/ci/assemble-release-assets.sh alpine ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/*_${{ matrix.arch }}.apk"
|
||||
- run: ./hack/os/build.sh
|
||||
- run: "./hack/ci/assemble-release-assets.sh os ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/os/krata-${{ matrix.arch }}.qcow2"
|
||||
- run: "./hack/ci/upload-release-assets.sh ${{ github.event.release.tag_name }}"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
client:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- { os: linux, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: linux, arch: aarch64, on: ubuntu-latest, deps: linux }
|
||||
- { os: darwin, arch: x86_64, on: macos-14, deps: darwin }
|
||||
- { os: darwin, arch: aarch64, on: macos-14, deps: darwin }
|
||||
- { os: freebsd, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: windows, arch: x86_64, on: windows-latest, deps: windows }
|
||||
env:
|
||||
TARGET_OS: "${{ matrix.platform.os }}"
|
||||
TARGET_ARCH: "${{ matrix.platform.arch }}"
|
||||
runs-on: "${{ matrix.platform.on }}"
|
||||
name: release-binaries client ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
if: ${{ matrix.platform.os != 'darwin' }}
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: "${{ matrix.platform.arch }}-apple-darwin"
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- uses: homebrew/actions/setup-homebrew@4b34604e75af8f8b23b454f0b5ffb7c5d8ce0056 # master
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
|
||||
- run: ./hack/build/cargo.sh build --release --bin kratactl
|
||||
- run: "./hack/ci/assemble-release-assets.sh kratactl ${{ github.event.release.tag_name }} ${{ matrix.platform.os }}-${{ matrix.platform.arch }} target/*/release/kratactl"
|
||||
if: ${{ matrix.platform.os != 'windows' }}
|
||||
- run: "./hack/ci/assemble-release-assets.sh kratactl ${{ github.event.release.tag_name }} ${{ matrix.platform.os }}-${{ matrix.platform.arch }} target/*/release/kratactl.exe"
|
||||
if: ${{ matrix.platform.os == 'windows' }}
|
||||
- run: "./hack/ci/upload-release-assets.sh ${{ github.event.release.tag_name }}"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
oci:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
component:
|
||||
- kratactl
|
||||
- kratad
|
||||
- kratanet
|
||||
- krata-guest-init
|
||||
name: "release-binaries oci ${{ matrix.component }}"
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
|
||||
- uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3.4.0
|
||||
- uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: "${{ github.actor }}"
|
||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- id: version
|
||||
run: |
|
||||
echo "KRATA_VERSION=$(./hack/dist/version.sh)" >> "${GITHUB_OUTPUT}"
|
||||
- uses: docker/build-push-action@1a162644f9a7e87d8f4b053101d1d9a712edc18c # v6.3.0
|
||||
id: push
|
||||
with:
|
||||
file: ./images/Dockerfile.${{ matrix.component }}
|
||||
platforms: linux/amd64,linux/aarch64
|
||||
tags: "ghcr.io/edera-dev/${{ matrix.component }}:${{ steps.version.outputs.KRATA_VERSION }}"
|
||||
push: true
|
||||
- env:
|
||||
DIGEST: "${{ steps.push.outputs.digest }}"
|
||||
TAGS: "ghcr.io/edera-dev/${{ matrix.component }}:${{ steps.version.outputs.KRATA_VERSION }}"
|
||||
COSIGN_EXPERIMENTAL: "true"
|
||||
run: cosign sign --yes "${TAGS}@${DIGEST}"
|
25
.github/workflows/release-plz.yml
vendored
25
.github/workflows/release-plz.yml
vendored
@ -1,7 +1,4 @@
|
||||
name: release-plz
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@ -13,24 +10,34 @@ jobs:
|
||||
release-plz:
|
||||
name: release-plz
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/create-github-app-token@31c86eb3b33c9b601a1f60f98dcbfd1d70f379b4 # v1.10.3
|
||||
- name: generate cultivator token
|
||||
uses: actions/create-github-app-token@31c86eb3b33c9b601a1f60f98dcbfd1d70f379b4 # v1.10.3
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
|
||||
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
token: "${{ steps.generate-token.outputs.token }}"
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- name: install stable rust toolchain
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: release-plz
|
||||
uses: MarcoIeni/release-plz-action@86afd21a7b114234aab55ba0005eed52f77d89e4 # v0.5.62
|
||||
uses: MarcoIeni/release-plz-action@e28810957ef1fedfa89b5e9692e750ce45f62a67 # v0.5.65
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
|
||||
CARGO_REGISTRY_TOKEN: "${{ secrets.KRATA_RELEASE_CARGO_TOKEN }}"
|
||||
|
94
.github/workflows/server.yml
vendored
94
.github/workflows/server.yml
vendored
@ -1,94 +0,0 @@
|
||||
name: server
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: server build ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/build/cargo.sh build
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: server test ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/build/cargo.sh test
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: server clippy ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
with:
|
||||
components: clippy
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/build/cargo.sh clippy
|
||||
initrd:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: server initrd ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@d388a4836fcdbde0e50e395dc79a2670ccdef13f # stable
|
||||
with:
|
||||
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/initrd/build.sh
|
80
CHANGELOG.md
80
CHANGELOG.md
@ -6,6 +6,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.0.19](https://github.com/edera-dev/krata/compare/v0.0.18...v0.0.19) - 2024-08-25
|
||||
|
||||
### Added
|
||||
- *(config)* write default config to config.toml on startup ([#356](https://github.com/edera-dev/krata/pull/356))
|
||||
- *(ctl)* add --format option to host status and improve cpu topology format ([#355](https://github.com/edera-dev/krata/pull/355))
|
||||
|
||||
### Fixed
|
||||
- *(zone-exec)* ensure that the underlying process is killed when rpc is closed ([#361](https://github.com/edera-dev/krata/pull/361))
|
||||
- *(rpc)* rename HostStatus to GetHostStatus ([#360](https://github.com/edera-dev/krata/pull/360))
|
||||
- *(console)* don't replay history when attaching to the console ([#358](https://github.com/edera-dev/krata/pull/358))
|
||||
- *(zone-exec)* catch panic errors and show all errors immediately ([#359](https://github.com/edera-dev/krata/pull/359))
|
||||
|
||||
### Other
|
||||
- *(control)* split out all of the rpc calls into their own files ([#357](https://github.com/edera-dev/krata/pull/357))
|
||||
|
||||
## [0.0.18](https://github.com/edera-dev/krata/compare/v0.0.17...v0.0.18) - 2024-08-22
|
||||
|
||||
### Added
|
||||
- *(zone)* kernel command line control on launch ([#351](https://github.com/edera-dev/krata/pull/351))
|
||||
- *(xen-preflight)* test for hypervisor presence explicitly and error if missing ([#347](https://github.com/edera-dev/krata/pull/347))
|
||||
|
||||
### Fixed
|
||||
- *(network)* allocate host ip from allocation pool ([#353](https://github.com/edera-dev/krata/pull/353))
|
||||
- *(daemon)* turn off trace logging ([#352](https://github.com/edera-dev/krata/pull/352))
|
||||
|
||||
### Other
|
||||
- Add support for reading hypervisor console ([#344](https://github.com/edera-dev/krata/pull/344))
|
||||
- *(ctl)* move logic for branching ctl run steps into ControlCommands ([#342](https://github.com/edera-dev/krata/pull/342))
|
||||
- update Cargo.toml dependencies
|
||||
|
||||
## [0.0.17](https://github.com/edera-dev/krata/compare/v0.0.16...v0.0.17) - 2024-08-15
|
||||
|
||||
### Added
|
||||
- *(krata)* first pass on cpu hotplug support ([#340](https://github.com/edera-dev/krata/pull/340))
|
||||
- *(exec)* implement tty support (fixes [#335](https://github.com/edera-dev/krata/pull/335)) ([#336](https://github.com/edera-dev/krata/pull/336))
|
||||
- *(krata)* dynamic resource allocation (closes [#298](https://github.com/edera-dev/krata/pull/298)) ([#333](https://github.com/edera-dev/krata/pull/333))
|
||||
|
||||
### Other
|
||||
- update Cargo.toml dependencies
|
||||
|
||||
## [0.0.16](https://github.com/edera-dev/krata/compare/v0.0.15...v0.0.16) - 2024-08-14
|
||||
|
||||
### Added
|
||||
- *(krata)* prepare for workload rework ([#276](https://github.com/edera-dev/krata/pull/276))
|
||||
|
||||
### Fixed
|
||||
- *(idm)* reimplement packet processing algorithm ([#330](https://github.com/edera-dev/krata/pull/330))
|
||||
- *(power-trap-eacces)* gracefully handle hypercall errors in power management ([#325](https://github.com/edera-dev/krata/pull/325))
|
||||
|
||||
### Other
|
||||
- *(o11y)* add more debug logs to daemon & runtime ([#318](https://github.com/edera-dev/krata/pull/318))
|
||||
|
||||
## [0.0.15](https://github.com/edera-dev/krata/compare/v0.0.14...v0.0.15) - 2024-08-06
|
||||
|
||||
### Fixed
|
||||
- *(zone)* waitpid should be limited when no child processes exist (fixes [#304](https://github.com/edera-dev/krata/pull/304)) ([#305](https://github.com/edera-dev/krata/pull/305))
|
||||
|
||||
## [0.0.14](https://github.com/edera-dev/krata/compare/v0.0.13...v0.0.14) - 2024-08-06
|
||||
|
||||
### Added
|
||||
- *(oci)* use local index as resolution cache when appropriate, fixes [#289](https://github.com/edera-dev/krata/pull/289) ([#294](https://github.com/edera-dev/krata/pull/294))
|
||||
|
||||
### Fixed
|
||||
- *(idm)* process all idm messages in the same frame and use childwait exit notification for exec (fixes [#290](https://github.com/edera-dev/krata/pull/290)) ([#302](https://github.com/edera-dev/krata/pull/302))
|
||||
|
||||
### Other
|
||||
- init: mount /proc with hidepid=1 ([#277](https://github.com/edera-dev/krata/pull/277))
|
||||
- update Cargo.toml dependencies
|
||||
|
||||
## [0.0.13](https://github.com/edera-dev/krata/compare/v0.0.12...v0.0.13) - 2024-07-19
|
||||
|
||||
### Added
|
||||
- *(kratactl)* rework cli to use subcommands ([#268](https://github.com/edera-dev/krata/pull/268))
|
||||
- *(krata)* rename guest to zone ([#266](https://github.com/edera-dev/krata/pull/266))
|
||||
|
||||
### Other
|
||||
- *(deps)* upgrade dependencies, fix hyper io traits issue ([#252](https://github.com/edera-dev/krata/pull/252))
|
||||
- update Cargo.lock dependencies
|
||||
- update Cargo.toml dependencies
|
||||
|
||||
## [0.0.12](https://github.com/edera-dev/krata/compare/v0.0.11...v0.0.12) - 2024-07-12
|
||||
|
||||
### Added
|
||||
|
1470
Cargo.lock
generated
1470
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
67
Cargo.toml
67
Cargo.toml
@ -3,7 +3,7 @@ members = [
|
||||
"crates/build",
|
||||
"crates/krata",
|
||||
"crates/oci",
|
||||
"crates/guest",
|
||||
"crates/zone",
|
||||
"crates/runtime",
|
||||
"crates/daemon",
|
||||
"crates/network",
|
||||
@ -18,35 +18,37 @@ members = [
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.12"
|
||||
version = "0.0.19"
|
||||
homepage = "https://krata.dev"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/edera-dev/krata"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0"
|
||||
arrayvec = "0.7.4"
|
||||
async-compression = "0.4.11"
|
||||
arrayvec = "0.7.6"
|
||||
async-compression = "0.4.12"
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.81"
|
||||
backhand = "0.15.0"
|
||||
backhand = "0.18.0"
|
||||
base64 = "0.22.1"
|
||||
byteorder = "1"
|
||||
bytes = "1.5.0"
|
||||
bytes = "1.7.1"
|
||||
c2rust-bitfields = "0.18.0"
|
||||
cgroups-rs = "0.3.4"
|
||||
circular-buffer = "0.1.7"
|
||||
comfy-table = "7.1.1"
|
||||
crossterm = "0.27.0"
|
||||
ctrlc = "3.4.4"
|
||||
crossterm = "0.28.1"
|
||||
ctrlc = "3.4.5"
|
||||
elf = "0.7.4"
|
||||
env_logger = "0.11.0"
|
||||
etherparse = "0.14.3"
|
||||
env_logger = "0.11.5"
|
||||
etherparse = "0.15.0"
|
||||
fancy-duration = "0.9.2"
|
||||
flate2 = "1.0"
|
||||
futures = "0.3.30"
|
||||
hyper = "1.4.1"
|
||||
hyper-util = "0.1.7"
|
||||
human_bytes = "0.4"
|
||||
indexmap = "2.2.6"
|
||||
indexmap = "2.4.0"
|
||||
indicatif = "0.17.8"
|
||||
ipnetwork = "0.20.0"
|
||||
libc = "0.2"
|
||||
@ -56,54 +58,57 @@ krata-advmac = "1.1.0"
|
||||
krata-tokio-tar = "0.4.0"
|
||||
memchr = "2"
|
||||
nix = "0.29.0"
|
||||
oci-spec = "0.6.7"
|
||||
oci-spec = "0.6.8"
|
||||
once_cell = "1.19.0"
|
||||
path-absolutize = "3.1.1"
|
||||
path-clean = "1.0.1"
|
||||
pin-project-lite = "0.2.14"
|
||||
platform-info = "2.0.3"
|
||||
prost = "0.12.6"
|
||||
prost-build = "0.12.6"
|
||||
prost-reflect-build = "0.13.0"
|
||||
prost-types = "0.12.6"
|
||||
prost = "0.13.1"
|
||||
prost-build = "0.13.1"
|
||||
prost-reflect-build = "0.14.0"
|
||||
prost-types = "0.13.1"
|
||||
pty-process = "0.4.0"
|
||||
rand = "0.8.5"
|
||||
ratatui = "0.26.3"
|
||||
ratatui = "0.28.0"
|
||||
redb = "2.1.1"
|
||||
regex = "1.10.5"
|
||||
regex = "1.10.6"
|
||||
rtnetlink = "0.14.1"
|
||||
scopeguard = "1.2.0"
|
||||
serde_json = "1.0.120"
|
||||
serde_json = "1.0.125"
|
||||
serde_yaml = "0.9"
|
||||
sha256 = "1.5.0"
|
||||
signal-hook = "0.3.17"
|
||||
slice-copy = "0.3.0"
|
||||
smoltcp = "0.11.0"
|
||||
sysinfo = "0.30.13"
|
||||
termtree = "0.4.1"
|
||||
sysinfo = "0.31.2"
|
||||
termtree = "0.5.1"
|
||||
thiserror = "1.0"
|
||||
tokio-tun = "0.11.5"
|
||||
toml = "0.8.14"
|
||||
tonic-build = "0.11.0"
|
||||
tower = "0.4.13"
|
||||
udp-stream = "0.0.11"
|
||||
tokio-util = "0.7.11"
|
||||
toml = "0.8.19"
|
||||
tonic-build = "0.12.1"
|
||||
tower = "0.5.0"
|
||||
udp-stream = "0.0.12"
|
||||
url = "2.5.2"
|
||||
walkdir = "2"
|
||||
xz2 = "0.1"
|
||||
|
||||
[workspace.dependencies.clap]
|
||||
version = "4.5.9"
|
||||
version = "4.5.16"
|
||||
features = ["derive"]
|
||||
|
||||
[workspace.dependencies.prost-reflect]
|
||||
version = "0.13.1"
|
||||
version = "0.14.0"
|
||||
features = ["derive"]
|
||||
|
||||
[workspace.dependencies.reqwest]
|
||||
version = "0.12.5"
|
||||
version = "0.12.7"
|
||||
default-features = false
|
||||
features = ["rustls-tls"]
|
||||
|
||||
[workspace.dependencies.serde]
|
||||
version = "1.0.204"
|
||||
version = "1.0.208"
|
||||
features = ["derive"]
|
||||
|
||||
[workspace.dependencies.sys-mount]
|
||||
@ -111,7 +116,7 @@ version = "3.0.0"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.tokio]
|
||||
version = "1.38.0"
|
||||
version = "1.39.3"
|
||||
features = ["full"]
|
||||
|
||||
[workspace.dependencies.tokio-stream]
|
||||
@ -119,7 +124,7 @@ version = "0.1"
|
||||
features = ["io-util", "net"]
|
||||
|
||||
[workspace.dependencies.tonic]
|
||||
version = "0.11.0"
|
||||
version = "0.12.1"
|
||||
features = ["tls"]
|
||||
|
||||
[workspace.dependencies.uuid]
|
||||
|
57
DEV.md
57
DEV.md
@ -4,25 +4,25 @@
|
||||
|
||||
krata is composed of four major executables:
|
||||
|
||||
| Executable | Runs On | User Interaction | Dev Runner | Code Path |
|
||||
| ---------- | ------- | ---------------- | ------------------------ | ----------------- |
|
||||
| kratad | host | backend daemon | ./hack/debug/kratad.sh | crates/daemon |
|
||||
| kratanet | host | backend daemon | ./hack/debug/kratanet.sh | crates/network |
|
||||
| kratactl | host | CLI tool | ./hack/debug/kratactl.sh | crates/ctl |
|
||||
| krataguest | guest | none, guest init | N/A | crates/guest |
|
||||
| Executable | Runs On | User Interaction | Dev Runner | Code Path |
|
||||
|------------|---------|------------------|--------------------------|----------------|
|
||||
| kratad | host | backend daemon | ./hack/debug/kratad.sh | crates/daemon |
|
||||
| kratanet | host | backend daemon | ./hack/debug/kratanet.sh | crates/network |
|
||||
| kratactl | host | CLI tool | ./hack/debug/kratactl.sh | crates/ctl |
|
||||
| kratazone | zone | none, zone init | N/A | crates/zone |
|
||||
|
||||
You will find the code to each executable available in the bin/ and src/ directories inside
|
||||
it's corresponding code path from the above table.
|
||||
|
||||
## Environment
|
||||
|
||||
| Component | Specification | Notes |
|
||||
| ------------- | ------------- | --------------------------------------------------------------------------------- |
|
||||
| Architecture | x86_64 | aarch64 support is still in development |
|
||||
| Memory | At least 6GB | dom0 will need to be configured with lower memory limit to give krata guests room |
|
||||
| Xen | 4.17 | Temporary due to hardcoded interface version constants |
|
||||
| Debian | stable / sid | Debian is recommended due to the ease of Xen setup |
|
||||
| rustup | any | Install Rustup from https://rustup.rs |
|
||||
| Component | Specification | Notes |
|
||||
|--------------|---------------|----------------------------------------------------------------------------------|
|
||||
| Architecture | x86_64 | aarch64 support is still in development |
|
||||
| Memory | At least 6GB | dom0 will need to be configured with lower memory limit to give krata zones room |
|
||||
| Xen | 4.17+ | |
|
||||
| Debian | stable / sid | Debian is recommended due to the ease of Xen setup |
|
||||
| rustup | any | Install Rustup from https://rustup.rs |
|
||||
|
||||
## Setup Guide
|
||||
|
||||
@ -31,8 +31,7 @@ it's corresponding code path from the above table.
|
||||
2. Install required packages:
|
||||
|
||||
```sh
|
||||
$ apt install git xen-system-amd64 build-essential \
|
||||
libclang-dev musl-tools flex bison libelf-dev libssl-dev bc \
|
||||
$ apt install git xen-system-amd64 build-essential musl-tools \
|
||||
protobuf-compiler libprotobuf-dev squashfs-tools erofs-utils
|
||||
```
|
||||
|
||||
@ -45,10 +44,10 @@ $ rustup target add x86_64-unknown-linux-gnu
|
||||
$ rustup target add x86_64-unknown-linux-musl
|
||||
```
|
||||
|
||||
4. Configure `/etc/default/grub.d/xen.cfg` to give krata guests some room:
|
||||
4. Configure `/etc/default/grub.d/xen.cfg` to give krata zones some room:
|
||||
|
||||
```sh
|
||||
# Configure dom0_mem to be 4GB, but leave the rest of the RAM for krata guests.
|
||||
# Configure dom0_mem to be 4GB, but leave the rest of the RAM for krata zones.
|
||||
GRUB_CMDLINE_XEN_DEFAULT="dom0_mem=4G,max:4G"
|
||||
```
|
||||
|
||||
@ -64,36 +63,36 @@ $ git clone https://github.com/edera-dev/krata.git krata
|
||||
$ cd krata
|
||||
```
|
||||
|
||||
6. Fetch the guest kernel image:
|
||||
6. Fetch the zone kernel image:
|
||||
|
||||
```sh
|
||||
$ ./hack/kernel/fetch.sh -u
|
||||
```
|
||||
|
||||
7. Copy the guest kernel artifacts to `/var/lib/krata/guest/kernel` so it is automatically detected by kratad:
|
||||
7. Copy the zone kernel artifacts to `/var/lib/krata/zone/kernel` so it is automatically detected by kratad:
|
||||
|
||||
```sh
|
||||
$ mkdir -p /var/lib/krata/guest
|
||||
$ cp target/kernel/kernel-x86_64 /var/lib/krata/guest/kernel
|
||||
$ cp target/kernel/addons-x86_64.squashfs /var/lib/krata/guest/addons.squashfs
|
||||
$ mkdir -p /var/lib/krata/zone
|
||||
$ cp target/kernel/kernel-x86_64 /var/lib/krata/zone/kernel
|
||||
$ cp target/kernel/addons-x86_64.squashfs /var/lib/krata/zone/addons.squashfs
|
||||
```
|
||||
|
||||
8. Launch `./hack/debug/kratad.sh` and keep it running in the foreground.
|
||||
9. Launch `./hack/debug/kratanet.sh` and keep it running in the foreground.
|
||||
10. Run `kratactl` to launch a guest:
|
||||
10. Run `kratactl` to launch a zone:
|
||||
|
||||
```sh
|
||||
$ ./hack/debug/kratactl.sh launch --attach alpine:latest
|
||||
$ ./hack/debug/kratactl.sh zone launch --attach alpine:latest
|
||||
```
|
||||
|
||||
To detach from the guest console, use `Ctrl + ]` on your keyboard.
|
||||
To detach from the zone console, use `Ctrl + ]` on your keyboard.
|
||||
|
||||
To list the running guests, run:
|
||||
To list the running zones, run:
|
||||
```sh
|
||||
$ ./hack/debug/kratactl.sh list
|
||||
$ ./hack/debug/kratactl.sh zone list
|
||||
```
|
||||
|
||||
To destroy a running guest, copy it's UUID from either the launch command or the guest list and run:
|
||||
To destroy a running zone, copy it's UUID from either the launch command or the zone list and run:
|
||||
```sh
|
||||
$ ./hack/debug/kratactl.sh destroy GUEST_UUID
|
||||
$ ./hack/debug/kratactl.sh zone destroy ZONE_UUID
|
||||
```
|
||||
|
2
FAQ.md
2
FAQ.md
@ -12,4 +12,4 @@ Xen is a very interesting technology, and Edera believes that type-1 hypervisors
|
||||
|
||||
## Why not utilize pvcalls to provide access to the host network?
|
||||
|
||||
pvcalls is extremely interesting, and although it is certainly possible to utilize pvcalls to get the job done, we chose to utilize userspace networking technology in order to enhance security. Our goal is to drop the use of all xen networking backend drivers within the kernel and have the guest talk directly to a userspace daemon, bypassing the vif (xen-netback) driver. Currently, in order to develop the networking layer, we utilize xen-netback and then use raw sockets to provide the userspace networking layer on the host.
|
||||
pvcalls is fascinating, and although it is certainly possible to utilize pvcalls to get the job done, we chose to utilize userspace networking technology in order to enhance security. Our goal is to drop the use of all xen networking backend drivers within the kernel and have the guest talk directly to a userspace daemon, bypassing the vif (xen-netback) driver. Currently, in order to develop the networking layer, we utilize xen-netback and then use raw sockets to provide the userspace networking layer on the host.
|
||||
|
12
README.md
12
README.md
@ -2,6 +2,10 @@
|
||||
|
||||
An isolation engine for securing compute workloads.
|
||||
|
||||
```bash
|
||||
$ kratactl zone launch -a alpine:latest
|
||||
```
|
||||
|
||||

|
||||

|
||||
[](https://github.com/edera-dev/krata/actions/workflows/check.yml)
|
||||
@ -22,7 +26,7 @@ krata utilizes the core of the Xen hypervisor with a fully memory-safe Rust cont
|
||||
|
||||
## Hardware Support
|
||||
|
||||
| Architecture | Completion Level | Hardware Virtualization |
|
||||
| ------------ | ---------------- | ------------------------------- |
|
||||
| x86_64 | 100% Completed | None, Intel VT-x, AMD-V |
|
||||
| aarch64 | 10% Completed | AArch64 virtualization |
|
||||
| Architecture | Completion Level | Hardware Virtualization |
|
||||
|--------------|------------------|-------------------------|
|
||||
| x86_64 | 100% Completed | None, Intel VT-x, AMD-V |
|
||||
| aarch64 | 10% Completed | AArch64 virtualization |
|
||||
|
@ -16,7 +16,7 @@ oci-spec = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
krata-oci = { path = "../oci", version = "^0.0.12" }
|
||||
krata-oci = { path = "../oci", version = "^0.0.19" }
|
||||
krata-tokio-tar = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
|
@ -39,7 +39,7 @@ async fn main() -> Result<()> {
|
||||
);
|
||||
|
||||
let image = ImageName::parse(&args().nth(1).unwrap())?;
|
||||
let mut cache_dir = std::env::temp_dir().clone();
|
||||
let mut cache_dir = env::temp_dir().clone();
|
||||
cache_dir.push(format!("krata-cache-{}", Uuid::new_v4()));
|
||||
fs::create_dir_all(&cache_dir).await?;
|
||||
|
||||
@ -50,7 +50,7 @@ async fn main() -> Result<()> {
|
||||
let (context, _) = OciProgressContext::create();
|
||||
let service = OciPackerService::new(None, &cache_dir, platform).await?;
|
||||
let packed = service
|
||||
.request(image.clone(), OciPackedFormat::Tar, false, context)
|
||||
.request(image.clone(), OciPackedFormat::Tar, false, true, context)
|
||||
.await?;
|
||||
let annotations = packed
|
||||
.manifest
|
||||
|
@ -20,7 +20,7 @@ env_logger = { workspace = true }
|
||||
fancy-duration = { workspace = true }
|
||||
human_bytes = { workspace = true }
|
||||
indicatif = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.12" }
|
||||
krata = { path = "../krata", version = "^0.0.19" }
|
||||
log = { workspace = true }
|
||||
prost-reflect = { workspace = true, features = ["serde"] }
|
||||
prost-types = { workspace = true }
|
||||
|
@ -1,46 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::v1::control::{control_service_client::ControlServiceClient, HostCpuTopologyRequest};
|
||||
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
fn class_to_str(input: i32) -> String {
|
||||
match input {
|
||||
0 => "Standard".to_string(),
|
||||
1 => "Performance".to_string(),
|
||||
2 => "Efficiency".to_string(),
|
||||
_ => "???".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Display information about a host's CPU topology")]
|
||||
pub struct CpuTopologyCommand {}
|
||||
|
||||
impl CpuTopologyCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
println!(
|
||||
"{0:<10} {1:<10} {2:<10} {3:<10} {4:<10} {5:<10}",
|
||||
"CPUID", "Node", "Socket", "Core", "Thread", "Class"
|
||||
);
|
||||
|
||||
let response = client
|
||||
.get_host_cpu_topology(Request::new(HostCpuTopologyRequest {}))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
for (i, cpu) in response.cpus.iter().enumerate() {
|
||||
println!(
|
||||
"{0:<10} {1:<10} {2:<10} {3:<10} {4:<10} {5:<10}",
|
||||
i,
|
||||
cpu.node,
|
||||
cpu.socket,
|
||||
cpu.core,
|
||||
cpu.thread,
|
||||
class_to_str(cpu.class)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::GuestStatus,
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
DestroyGuestRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use log::error;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::cli::resolve_guest;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Destroy a guest")]
|
||||
pub struct DestroyCommand {
|
||||
#[arg(
|
||||
short = 'W',
|
||||
long,
|
||||
help = "Wait for the destruction of the guest to complete"
|
||||
)]
|
||||
wait: bool,
|
||||
#[arg(help = "Guest to destroy, either the name or the uuid")]
|
||||
guest: String,
|
||||
}
|
||||
|
||||
impl DestroyCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
|
||||
let _ = client
|
||||
.destroy_guest(Request::new(DestroyGuestRequest {
|
||||
guest_id: guest_id.clone(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if self.wait {
|
||||
wait_guest_destroyed(&guest_id, events).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_guest_destroyed(id: &str, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
let Event::GuestChanged(changed) = event;
|
||||
let Some(guest) = changed.guest else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if guest.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(state) = guest.state else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(ref error) = state.error_info {
|
||||
if state.status() == GuestStatus::Failed {
|
||||
error!("destroy failed: {}", error.message);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
error!("guest error: {}", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if state.status() == GuestStatus::Destroyed {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -12,7 +12,7 @@ use tonic::transport::Channel;
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum ListDevicesFormat {
|
||||
enum DeviceListFormat {
|
||||
Table,
|
||||
Json,
|
||||
JsonPretty,
|
||||
@ -23,13 +23,13 @@ enum ListDevicesFormat {
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "List the devices on the isolation engine")]
|
||||
pub struct ListDevicesCommand {
|
||||
#[command(about = "List device information")]
|
||||
pub struct DeviceListCommand {
|
||||
#[arg(short, long, default_value = "table", help = "Output format")]
|
||||
format: ListDevicesFormat,
|
||||
format: DeviceListFormat,
|
||||
}
|
||||
|
||||
impl ListDevicesCommand {
|
||||
impl DeviceListCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
@ -44,26 +44,26 @@ impl ListDevicesCommand {
|
||||
devices.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
match self.format {
|
||||
ListDevicesFormat::Table => {
|
||||
DeviceListFormat::Table => {
|
||||
self.print_devices_table(devices)?;
|
||||
}
|
||||
|
||||
ListDevicesFormat::Simple => {
|
||||
DeviceListFormat::Simple => {
|
||||
for device in devices {
|
||||
println!("{}\t{}\t{}", device.name, device.claimed, device.owner);
|
||||
}
|
||||
}
|
||||
|
||||
ListDevicesFormat::Json | ListDevicesFormat::JsonPretty | ListDevicesFormat::Yaml => {
|
||||
DeviceListFormat::Json | DeviceListFormat::JsonPretty | DeviceListFormat::Yaml => {
|
||||
let mut values = Vec::new();
|
||||
for device in devices {
|
||||
let message = proto2dynamic(device)?;
|
||||
values.push(serde_json::to_value(message)?);
|
||||
}
|
||||
let value = Value::Array(values);
|
||||
let encoded = if self.format == ListDevicesFormat::JsonPretty {
|
||||
let encoded = if self.format == DeviceListFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == ListDevicesFormat::Yaml {
|
||||
} else if self.format == DeviceListFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
@ -71,14 +71,14 @@ impl ListDevicesCommand {
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
ListDevicesFormat::Jsonl => {
|
||||
DeviceListFormat::Jsonl => {
|
||||
for device in devices {
|
||||
let message = proto2dynamic(device)?;
|
||||
println!("{}", serde_json::to_string(&message)?);
|
||||
}
|
||||
}
|
||||
|
||||
ListDevicesFormat::KeyValue => {
|
||||
DeviceListFormat::KeyValue => {
|
||||
self.print_key_value(devices)?;
|
||||
}
|
||||
}
|
44
crates/ctl/src/cli/device/mod.rs
Normal file
44
crates/ctl/src/cli/device/mod.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use krata::events::EventStream;
|
||||
use krata::v1::control::control_service_client::ControlServiceClient;
|
||||
|
||||
use crate::cli::device::list::DeviceListCommand;
|
||||
|
||||
pub mod list;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Manage the devices on the isolation engine")]
|
||||
pub struct DeviceCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: DeviceCommands,
|
||||
}
|
||||
|
||||
impl DeviceCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
self.subcommand.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum DeviceCommands {
|
||||
List(DeviceListCommand),
|
||||
}
|
||||
|
||||
impl DeviceCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
DeviceCommands::List(list) => list.run(client, events).await,
|
||||
}
|
||||
}
|
||||
}
|
104
crates/ctl/src/cli/host/cpu_topology.rs
Normal file
104
crates/ctl/src/cli/host/cpu_topology.rs
Normal file
@ -0,0 +1,104 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::presets::UTF8_FULL_CONDENSED;
|
||||
use comfy_table::{Cell, Table};
|
||||
use krata::v1::control::{
|
||||
control_service_client::ControlServiceClient, GetHostCpuTopologyRequest, HostCpuTopologyClass,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv};
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
fn class_to_str(input: HostCpuTopologyClass) -> String {
|
||||
match input {
|
||||
HostCpuTopologyClass::Standard => "Standard".to_string(),
|
||||
HostCpuTopologyClass::Performance => "Performance".to_string(),
|
||||
HostCpuTopologyClass::Efficiency => "Efficiency".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum HostCpuTopologyFormat {
|
||||
Table,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Jsonl,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Display information about the host CPU topology")]
|
||||
pub struct HostCpuTopologyCommand {
|
||||
#[arg(short, long, default_value = "table", help = "Output format")]
|
||||
format: HostCpuTopologyFormat,
|
||||
}
|
||||
|
||||
impl HostCpuTopologyCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let response = client
|
||||
.get_host_cpu_topology(Request::new(GetHostCpuTopologyRequest {}))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
match self.format {
|
||||
HostCpuTopologyFormat::Table => {
|
||||
let mut table = Table::new();
|
||||
table.load_preset(UTF8_FULL_CONDENSED);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
table.set_header(vec!["id", "node", "socket", "core", "thread", "class"]);
|
||||
|
||||
for (i, cpu) in response.cpus.iter().enumerate() {
|
||||
table.add_row(vec![
|
||||
Cell::new(i),
|
||||
Cell::new(cpu.node),
|
||||
Cell::new(cpu.socket),
|
||||
Cell::new(cpu.core),
|
||||
Cell::new(cpu.thread),
|
||||
Cell::new(class_to_str(cpu.class())),
|
||||
]);
|
||||
}
|
||||
|
||||
if !table.is_empty() {
|
||||
println!("{}", table);
|
||||
}
|
||||
}
|
||||
|
||||
HostCpuTopologyFormat::Json
|
||||
| HostCpuTopologyFormat::JsonPretty
|
||||
| HostCpuTopologyFormat::Yaml => {
|
||||
let mut values = Vec::new();
|
||||
for cpu in response.cpus {
|
||||
let message = proto2dynamic(cpu)?;
|
||||
values.push(serde_json::to_value(message)?);
|
||||
}
|
||||
let value = Value::Array(values);
|
||||
let encoded = if self.format == HostCpuTopologyFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == HostCpuTopologyFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
HostCpuTopologyFormat::Jsonl => {
|
||||
for cpu in response.cpus {
|
||||
let message = proto2dynamic(cpu)?;
|
||||
println!("{}", serde_json::to_string(&message)?);
|
||||
}
|
||||
}
|
||||
|
||||
HostCpuTopologyFormat::KeyValue => {
|
||||
for cpu in response.cpus {
|
||||
let kvs = proto2kv(cpu)?;
|
||||
println!("{}", kv2line(kvs),);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
23
crates/ctl/src/cli/host/hv_console.rs
Normal file
23
crates/ctl/src/cli/host/hv_console.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::v1::control::{
|
||||
control_service_client::ControlServiceClient, ReadHypervisorConsoleRequest,
|
||||
};
|
||||
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Display hypervisor console output")]
|
||||
pub struct HostHvConsoleCommand {}
|
||||
|
||||
impl HostHvConsoleCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let response = client
|
||||
.read_hypervisor_console(Request::new(ReadHypervisorConsoleRequest {}))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
print!("{}", response.data);
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ use tonic::transport::Channel;
|
||||
use crate::format::{kv2line, proto2dynamic, value2kv};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum IdmSnoopFormat {
|
||||
enum HostIdmSnoopFormat {
|
||||
Simple,
|
||||
Jsonl,
|
||||
KeyValue,
|
||||
@ -23,12 +23,12 @@ enum IdmSnoopFormat {
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Snoop on the IDM bus")]
|
||||
pub struct IdmSnoopCommand {
|
||||
pub struct HostIdmSnoopCommand {
|
||||
#[arg(short, long, default_value = "simple", help = "Output format")]
|
||||
format: IdmSnoopFormat,
|
||||
format: HostIdmSnoopFormat,
|
||||
}
|
||||
|
||||
impl IdmSnoopCommand {
|
||||
impl HostIdmSnoopCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
@ -43,16 +43,16 @@ impl IdmSnoopCommand {
|
||||
};
|
||||
|
||||
match self.format {
|
||||
IdmSnoopFormat::Simple => {
|
||||
HostIdmSnoopFormat::Simple => {
|
||||
self.print_simple(line)?;
|
||||
}
|
||||
|
||||
IdmSnoopFormat::Jsonl => {
|
||||
HostIdmSnoopFormat::Jsonl => {
|
||||
let encoded = serde_json::to_string(&line)?;
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
IdmSnoopFormat::KeyValue => {
|
||||
HostIdmSnoopFormat::KeyValue => {
|
||||
self.print_key_value(line)?;
|
||||
}
|
||||
}
|
59
crates/ctl/src/cli/host/mod.rs
Normal file
59
crates/ctl/src/cli/host/mod.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use krata::events::EventStream;
|
||||
use krata::v1::control::control_service_client::ControlServiceClient;
|
||||
|
||||
use crate::cli::host::cpu_topology::HostCpuTopologyCommand;
|
||||
use crate::cli::host::hv_console::HostHvConsoleCommand;
|
||||
use crate::cli::host::idm_snoop::HostIdmSnoopCommand;
|
||||
use crate::cli::host::status::HostStatusCommand;
|
||||
|
||||
pub mod cpu_topology;
|
||||
pub mod hv_console;
|
||||
pub mod idm_snoop;
|
||||
pub mod status;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Manage the host of the isolation engine")]
|
||||
pub struct HostCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: HostCommands,
|
||||
}
|
||||
|
||||
impl HostCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
self.subcommand.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum HostCommands {
|
||||
CpuTopology(HostCpuTopologyCommand),
|
||||
Status(HostStatusCommand),
|
||||
IdmSnoop(HostIdmSnoopCommand),
|
||||
HvConsole(HostHvConsoleCommand),
|
||||
}
|
||||
|
||||
impl HostCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
HostCommands::CpuTopology(cpu_topology) => cpu_topology.run(client).await,
|
||||
|
||||
HostCommands::Status(status) => status.run(client).await,
|
||||
|
||||
HostCommands::IdmSnoop(snoop) => snoop.run(client, events).await,
|
||||
|
||||
HostCommands::HvConsole(hvconsole) => hvconsole.run(client).await,
|
||||
}
|
||||
}
|
||||
}
|
60
crates/ctl/src/cli/host/status.rs
Normal file
60
crates/ctl/src/cli/host/status.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::v1::control::{control_service_client::ControlServiceClient, GetHostStatusRequest};
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv};
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum HostStatusFormat {
|
||||
Simple,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Get information about the host")]
|
||||
pub struct HostStatusCommand {
|
||||
#[arg(short, long, default_value = "simple", help = "Output format")]
|
||||
format: HostStatusFormat,
|
||||
}
|
||||
|
||||
impl HostStatusCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let response = client
|
||||
.get_host_status(Request::new(GetHostStatusRequest {}))
|
||||
.await?
|
||||
.into_inner();
|
||||
match self.format {
|
||||
HostStatusFormat::Simple => {
|
||||
println!("Host UUID: {}", response.host_uuid);
|
||||
println!("Host Domain: {}", response.host_domid);
|
||||
println!("Krata Version: {}", response.krata_version);
|
||||
println!("Host IPv4: {}", response.host_ipv4);
|
||||
println!("Host IPv6: {}", response.host_ipv6);
|
||||
println!("Host Ethernet Address: {}", response.host_mac);
|
||||
}
|
||||
|
||||
HostStatusFormat::Json | HostStatusFormat::JsonPretty | HostStatusFormat::Yaml => {
|
||||
let message = proto2dynamic(response)?;
|
||||
let value = serde_json::to_value(message)?;
|
||||
let encoded = if self.format == HostStatusFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == HostStatusFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
HostStatusFormat::KeyValue => {
|
||||
let kvs = proto2kv(response)?;
|
||||
println!("{}", kv2line(kvs),);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::v1::control::{control_service_client::ControlServiceClient, IdentifyHostRequest};
|
||||
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Identify information about the host")]
|
||||
pub struct IdentifyHostCommand {}
|
||||
|
||||
impl IdentifyHostCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let response = client
|
||||
.identify_host(Request::new(IdentifyHostRequest {}))
|
||||
.await?
|
||||
.into_inner();
|
||||
println!("Host UUID: {}", response.host_uuid);
|
||||
println!("Host Domain: {}", response.host_domid);
|
||||
println!("Krata Version: {}", response.krata_version);
|
||||
Ok(())
|
||||
}
|
||||
}
|
44
crates/ctl/src/cli/image/mod.rs
Normal file
44
crates/ctl/src/cli/image/mod.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use krata::events::EventStream;
|
||||
use krata::v1::control::control_service_client::ControlServiceClient;
|
||||
|
||||
use crate::cli::image::pull::ImagePullCommand;
|
||||
|
||||
pub mod pull;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Manage the images on the isolation engine")]
|
||||
pub struct ImageCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: ImageCommands,
|
||||
}
|
||||
|
||||
impl ImageCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
self.subcommand.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ImageCommands {
|
||||
Pull(ImagePullCommand),
|
||||
}
|
||||
|
||||
impl ImageCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
ImageCommands::Pull(pull) => pull.run(client).await,
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ use tonic::transport::Channel;
|
||||
use crate::pull::pull_interactive_progress;
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum PullImageFormat {
|
||||
pub enum ImagePullImageFormat {
|
||||
Squashfs,
|
||||
Erofs,
|
||||
Tar,
|
||||
@ -18,26 +18,27 @@ pub enum PullImageFormat {
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Pull an image into the cache")]
|
||||
pub struct PullCommand {
|
||||
pub struct ImagePullCommand {
|
||||
#[arg(help = "Image name")]
|
||||
image: String,
|
||||
#[arg(short = 's', long, default_value = "squashfs", help = "Image format")]
|
||||
image_format: PullImageFormat,
|
||||
image_format: ImagePullImageFormat,
|
||||
#[arg(short = 'o', long, help = "Overwrite image cache")]
|
||||
overwrite_cache: bool,
|
||||
}
|
||||
|
||||
impl PullCommand {
|
||||
impl ImagePullCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let response = client
|
||||
.pull_image(PullImageRequest {
|
||||
image: self.image.clone(),
|
||||
format: match self.image_format {
|
||||
PullImageFormat::Squashfs => OciImageFormat::Squashfs.into(),
|
||||
PullImageFormat::Erofs => OciImageFormat::Erofs.into(),
|
||||
PullImageFormat::Tar => OciImageFormat::Tar.into(),
|
||||
ImagePullImageFormat::Squashfs => OciImageFormat::Squashfs.into(),
|
||||
ImagePullImageFormat::Erofs => OciImageFormat::Erofs.into(),
|
||||
ImagePullImageFormat::Tar => OciImageFormat::Tar.into(),
|
||||
},
|
||||
overwrite_cache: self.overwrite_cache,
|
||||
update: true,
|
||||
})
|
||||
.await?;
|
||||
let reply = pull_interactive_progress(response.into_inner()).await?;
|
@ -1,174 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::{presets::UTF8_FULL_CONDENSED, Cell, Color, Table};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::{Guest, GuestStatus},
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, ListGuestsRequest, ResolveGuestRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use serde_json::Value;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::format::{guest_simple_line, guest_status_text, kv2line, proto2dynamic, proto2kv};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum ListFormat {
|
||||
Table,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Jsonl,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
Simple,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "List the guests on the isolation engine")]
|
||||
pub struct ListCommand {
|
||||
#[arg(short, long, default_value = "table", help = "Output format")]
|
||||
format: ListFormat,
|
||||
#[arg(help = "Limit to a single guest, either the name or the uuid")]
|
||||
guest: Option<String>,
|
||||
}
|
||||
|
||||
impl ListCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
let mut guests = if let Some(ref guest) = self.guest {
|
||||
let reply = client
|
||||
.resolve_guest(Request::new(ResolveGuestRequest {
|
||||
name: guest.clone(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if let Some(guest) = reply.guest {
|
||||
vec![guest]
|
||||
} else {
|
||||
return Err(anyhow!("unable to resolve guest '{}'", guest));
|
||||
}
|
||||
} else {
|
||||
client
|
||||
.list_guests(Request::new(ListGuestsRequest {}))
|
||||
.await?
|
||||
.into_inner()
|
||||
.guests
|
||||
};
|
||||
|
||||
guests.sort_by(|a, b| {
|
||||
a.spec
|
||||
.as_ref()
|
||||
.map(|x| x.name.as_str())
|
||||
.unwrap_or("")
|
||||
.cmp(b.spec.as_ref().map(|x| x.name.as_str()).unwrap_or(""))
|
||||
});
|
||||
|
||||
match self.format {
|
||||
ListFormat::Table => {
|
||||
self.print_guest_table(guests)?;
|
||||
}
|
||||
|
||||
ListFormat::Simple => {
|
||||
for guest in guests {
|
||||
println!("{}", guest_simple_line(&guest));
|
||||
}
|
||||
}
|
||||
|
||||
ListFormat::Json | ListFormat::JsonPretty | ListFormat::Yaml => {
|
||||
let mut values = Vec::new();
|
||||
for guest in guests {
|
||||
let message = proto2dynamic(guest)?;
|
||||
values.push(serde_json::to_value(message)?);
|
||||
}
|
||||
let value = Value::Array(values);
|
||||
let encoded = if self.format == ListFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == ListFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
ListFormat::Jsonl => {
|
||||
for guest in guests {
|
||||
let message = proto2dynamic(guest)?;
|
||||
println!("{}", serde_json::to_string(&message)?);
|
||||
}
|
||||
}
|
||||
|
||||
ListFormat::KeyValue => {
|
||||
self.print_key_value(guests)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_guest_table(&self, guests: Vec<Guest>) -> Result<()> {
|
||||
let mut table = Table::new();
|
||||
table.load_preset(UTF8_FULL_CONDENSED);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
table.set_header(vec!["name", "uuid", "status", "ipv4", "ipv6"]);
|
||||
for guest in guests {
|
||||
let ipv4 = guest
|
||||
.state
|
||||
.as_ref()
|
||||
.and_then(|x| x.network.as_ref())
|
||||
.map(|x| x.guest_ipv4.as_str())
|
||||
.unwrap_or("n/a");
|
||||
let ipv6 = guest
|
||||
.state
|
||||
.as_ref()
|
||||
.and_then(|x| x.network.as_ref())
|
||||
.map(|x| x.guest_ipv6.as_str())
|
||||
.unwrap_or("n/a");
|
||||
let Some(spec) = guest.spec else {
|
||||
continue;
|
||||
};
|
||||
let status = guest.state.as_ref().cloned().unwrap_or_default().status();
|
||||
let status_text = guest_status_text(status);
|
||||
|
||||
let status_color = match status {
|
||||
GuestStatus::Destroyed | GuestStatus::Failed => Color::Red,
|
||||
GuestStatus::Destroying | GuestStatus::Exited | GuestStatus::Starting => {
|
||||
Color::Yellow
|
||||
}
|
||||
GuestStatus::Started => Color::Green,
|
||||
_ => Color::Reset,
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
Cell::new(spec.name),
|
||||
Cell::new(guest.id),
|
||||
Cell::new(status_text).fg(status_color),
|
||||
Cell::new(ipv4.to_string()),
|
||||
Cell::new(ipv6.to_string()),
|
||||
]);
|
||||
}
|
||||
if table.is_empty() {
|
||||
if self.guest.is_none() {
|
||||
println!("no guests have been launched");
|
||||
}
|
||||
} else {
|
||||
println!("{}", table);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key_value(&self, guests: Vec<Guest>) -> Result<()> {
|
||||
for guest in guests {
|
||||
let kvs = proto2kv(guest)?;
|
||||
println!("{}", kv2line(kvs),);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,36 +1,21 @@
|
||||
pub mod attach;
|
||||
pub mod cpu_topology;
|
||||
pub mod destroy;
|
||||
pub mod exec;
|
||||
pub mod identify_host;
|
||||
pub mod idm_snoop;
|
||||
pub mod launch;
|
||||
pub mod list;
|
||||
pub mod list_devices;
|
||||
pub mod logs;
|
||||
pub mod metrics;
|
||||
pub mod pull;
|
||||
pub mod resolve;
|
||||
pub mod top;
|
||||
pub mod watch;
|
||||
pub mod device;
|
||||
pub mod host;
|
||||
pub mod image;
|
||||
pub mod zone;
|
||||
|
||||
use crate::cli::device::DeviceCommand;
|
||||
use crate::cli::host::HostCommand;
|
||||
use crate::cli::image::ImageCommand;
|
||||
use crate::cli::zone::ZoneCommand;
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
client::ControlClientProvider,
|
||||
events::EventStream,
|
||||
v1::control::{control_service_client::ControlServiceClient, ResolveGuestRequest},
|
||||
v1::control::{control_service_client::ControlServiceClient, ResolveZoneIdRequest},
|
||||
};
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use self::{
|
||||
attach::AttachCommand, cpu_topology::CpuTopologyCommand, destroy::DestroyCommand,
|
||||
exec::ExecCommand, identify_host::IdentifyHostCommand, idm_snoop::IdmSnoopCommand,
|
||||
launch::LaunchCommand, list::ListCommand, list_devices::ListDevicesCommand, logs::LogsCommand,
|
||||
metrics::MetricsCommand, pull::PullCommand, resolve::ResolveCommand, top::TopCommand,
|
||||
watch::WatchCommand,
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about = "Control the krata isolation engine")]
|
||||
pub struct ControlCommand {
|
||||
@ -43,112 +28,58 @@ pub struct ControlCommand {
|
||||
connection: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
command: ControlCommands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
Launch(LaunchCommand),
|
||||
Destroy(DestroyCommand),
|
||||
List(ListCommand),
|
||||
ListDevices(ListDevicesCommand),
|
||||
Attach(AttachCommand),
|
||||
Pull(PullCommand),
|
||||
Logs(LogsCommand),
|
||||
Watch(WatchCommand),
|
||||
Resolve(ResolveCommand),
|
||||
Metrics(MetricsCommand),
|
||||
IdmSnoop(IdmSnoopCommand),
|
||||
Top(TopCommand),
|
||||
IdentifyHost(IdentifyHostCommand),
|
||||
Exec(ExecCommand),
|
||||
CpuTopology(CpuTopologyCommand),
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Parser)]
|
||||
pub enum ControlCommands {
|
||||
Zone(ZoneCommand),
|
||||
Image(ImageCommand),
|
||||
Device(DeviceCommand),
|
||||
Host(HostCommand),
|
||||
}
|
||||
|
||||
impl ControlCommand {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let client = ControlClientProvider::dial(self.connection.parse()?).await?;
|
||||
let events = EventStream::open(client.clone()).await?;
|
||||
|
||||
match self.command {
|
||||
Commands::Launch(launch) => {
|
||||
launch.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::Destroy(destroy) => {
|
||||
destroy.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::Attach(attach) => {
|
||||
attach.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::Logs(logs) => {
|
||||
logs.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::List(list) => {
|
||||
list.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::Watch(watch) => {
|
||||
watch.run(events).await?;
|
||||
}
|
||||
|
||||
Commands::Resolve(resolve) => {
|
||||
resolve.run(client).await?;
|
||||
}
|
||||
|
||||
Commands::Metrics(metrics) => {
|
||||
metrics.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::IdmSnoop(snoop) => {
|
||||
snoop.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::Top(top) => {
|
||||
top.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::Pull(pull) => {
|
||||
pull.run(client).await?;
|
||||
}
|
||||
|
||||
Commands::IdentifyHost(identify) => {
|
||||
identify.run(client).await?;
|
||||
}
|
||||
|
||||
Commands::Exec(exec) => {
|
||||
exec.run(client).await?;
|
||||
}
|
||||
|
||||
Commands::ListDevices(list) => {
|
||||
list.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::CpuTopology(cpu_topology) => {
|
||||
cpu_topology.run(client).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
self.command.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_guest(
|
||||
impl ControlCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
ControlCommands::Zone(zone) => zone.run(client, events).await,
|
||||
|
||||
ControlCommands::Image(image) => image.run(client, events).await,
|
||||
|
||||
ControlCommands::Device(device) => device.run(client, events).await,
|
||||
|
||||
ControlCommands::Host(host) => host.run(client, events).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_zone(
|
||||
client: &mut ControlServiceClient<Channel>,
|
||||
name: &str,
|
||||
) -> Result<String> {
|
||||
let reply = client
|
||||
.resolve_guest(Request::new(ResolveGuestRequest {
|
||||
.resolve_zone_id(Request::new(ResolveZoneIdRequest {
|
||||
name: name.to_string(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
if let Some(guest) = reply.guest {
|
||||
Ok(guest.id)
|
||||
if !reply.zone_id.is_empty() {
|
||||
Ok(reply.zone_id)
|
||||
} else {
|
||||
Err(anyhow!("unable to resolve guest '{}'", name))
|
||||
Err(anyhow!("unable to resolve zone '{}'", name))
|
||||
}
|
||||
}
|
||||
|
@ -7,27 +7,27 @@ use tonic::transport::Channel;
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
use super::resolve_guest;
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Attach to the guest console")]
|
||||
pub struct AttachCommand {
|
||||
#[arg(help = "Guest to attach to, either the name or the uuid")]
|
||||
guest: String,
|
||||
#[command(about = "Attach to the zone console")]
|
||||
pub struct ZoneAttachCommand {
|
||||
#[arg(help = "Zone to attach to, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl AttachCommand {
|
||||
impl ZoneAttachCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
|
||||
let input = StdioConsoleStream::stdin_stream(guest_id.clone()).await;
|
||||
let output = client.console_data(input).await?.into_inner();
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let input = StdioConsoleStream::stdin_stream(zone_id.clone(), false).await;
|
||||
let output = client.attach_zone_console(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
|
||||
let exit_hook_task = StdioConsoleStream::guest_exit_hook(guest_id.clone(), events).await?;
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output, true).await });
|
||||
let exit_hook_task = StdioConsoleStream::zone_exit_hook(zone_id.clone(), events).await?;
|
||||
let code = select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
78
crates/ctl/src/cli/zone/destroy.rs
Normal file
78
crates/ctl/src/cli/zone/destroy.rs
Normal file
@ -0,0 +1,78 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event, DestroyZoneRequest,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
use krata::v1::common::ZoneState;
|
||||
use log::error;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Destroy a zone")]
|
||||
pub struct ZoneDestroyCommand {
|
||||
#[arg(
|
||||
short = 'W',
|
||||
long,
|
||||
help = "Wait for the destruction of the zone to complete"
|
||||
)]
|
||||
wait: bool,
|
||||
#[arg(help = "Zone to destroy, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ZoneDestroyCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let _ = client
|
||||
.destroy_zone(Request::new(DestroyZoneRequest {
|
||||
zone_id: zone_id.clone(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if self.wait {
|
||||
wait_zone_destroyed(&zone_id, events).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_zone_destroyed(id: &str, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
let Event::ZoneChanged(changed) = event;
|
||||
let Some(zone) = changed.zone else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if zone.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(ref error) = status.error_status {
|
||||
if status.state() == ZoneState::Failed {
|
||||
error!("destroy failed: {}", error.message);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
error!("zone error: {}", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if status.state() == ZoneState::Destroyed {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -4,57 +4,64 @@ use anyhow::Result;
|
||||
|
||||
use clap::Parser;
|
||||
use krata::v1::{
|
||||
common::{GuestTaskSpec, GuestTaskSpecEnvVar},
|
||||
control::{control_service_client::ControlServiceClient, ExecGuestRequest},
|
||||
common::{ZoneTaskSpec, ZoneTaskSpecEnvVar},
|
||||
control::{control_service_client::ControlServiceClient, ExecInsideZoneRequest},
|
||||
};
|
||||
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
use super::resolve_guest;
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Execute a command inside the guest")]
|
||||
pub struct ExecCommand {
|
||||
#[command(about = "Execute a command inside the zone")]
|
||||
pub struct ZoneExecCommand {
|
||||
#[arg[short, long, help = "Environment variables"]]
|
||||
env: Option<Vec<String>>,
|
||||
#[arg(short = 'w', long, help = "Working directory")]
|
||||
working_directory: Option<String>,
|
||||
#[arg(help = "Guest to exec inside, either the name or the uuid")]
|
||||
guest: String,
|
||||
#[arg(short = 't', long, help = "Allocate tty")]
|
||||
tty: bool,
|
||||
#[arg(help = "Zone to exec inside, either the name or the uuid")]
|
||||
zone: String,
|
||||
#[arg(
|
||||
allow_hyphen_values = true,
|
||||
trailing_var_arg = true,
|
||||
help = "Command to run inside the guest"
|
||||
help = "Command to run inside the zone"
|
||||
)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
impl ExecCommand {
|
||||
impl ZoneExecCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
|
||||
let initial = ExecGuestRequest {
|
||||
guest_id,
|
||||
task: Some(GuestTaskSpec {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let initial = ExecInsideZoneRequest {
|
||||
zone_id,
|
||||
task: Some(ZoneTaskSpec {
|
||||
environment: env_map(&self.env.unwrap_or_default())
|
||||
.iter()
|
||||
.map(|(key, value)| GuestTaskSpecEnvVar {
|
||||
.map(|(key, value)| ZoneTaskSpecEnvVar {
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect(),
|
||||
command: self.command,
|
||||
working_directory: self.working_directory.unwrap_or_default(),
|
||||
tty: self.tty,
|
||||
}),
|
||||
data: vec![],
|
||||
stdin: vec![],
|
||||
stdin_closed: false,
|
||||
};
|
||||
|
||||
let stream = StdioConsoleStream::stdin_stream_exec(initial).await;
|
||||
|
||||
let response = client.exec_guest(Request::new(stream)).await?.into_inner();
|
||||
let response = client
|
||||
.exec_inside_zone(Request::new(stream))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
let code = StdioConsoleStream::exec_output(response).await?;
|
||||
let code = StdioConsoleStream::exec_output(response, self.tty).await?;
|
||||
std::process::exit(code);
|
||||
}
|
||||
}
|
@ -6,12 +6,13 @@ use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::{
|
||||
guest_image_spec::Image, GuestImageSpec, GuestOciImageSpec, GuestSpec, GuestSpecDevice,
|
||||
GuestStatus, GuestTaskSpec, GuestTaskSpecEnvVar, OciImageFormat,
|
||||
zone_image_spec::Image, OciImageFormat, ZoneImageSpec, ZoneKernelOptionsSpec,
|
||||
ZoneOciImageSpec, ZoneResourceSpec, ZoneSpec, ZoneSpecDevice, ZoneState, ZoneTaskSpec,
|
||||
ZoneTaskSpecEnvVar,
|
||||
},
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
CreateGuestRequest, PullImageRequest,
|
||||
CreateZoneRequest, PullImageRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -28,61 +29,83 @@ pub enum LaunchImageFormat {
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Launch a new guest")]
|
||||
pub struct LaunchCommand {
|
||||
#[command(about = "Launch a new zone")]
|
||||
pub struct ZoneLaunchCommand {
|
||||
#[arg(long, default_value = "squashfs", help = "Image format")]
|
||||
image_format: LaunchImageFormat,
|
||||
#[arg(long, help = "Overwrite image cache on pull")]
|
||||
pull_overwrite_cache: bool,
|
||||
#[arg(short, long, help = "Name of the guest")]
|
||||
#[arg(long, help = "Update image on pull")]
|
||||
pull_update: bool,
|
||||
#[arg(short, long, help = "Name of the zone")]
|
||||
name: Option<String>,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
short = 'C',
|
||||
long = "max-cpus",
|
||||
default_value_t = 4,
|
||||
help = "Maximum vCPUs available for the zone"
|
||||
)]
|
||||
max_cpus: u32,
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long = "target-cpus",
|
||||
default_value_t = 1,
|
||||
help = "vCPUs available to the guest"
|
||||
help = "Target vCPUs for the zone to use"
|
||||
)]
|
||||
cpus: u32,
|
||||
target_cpus: u32,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value_t = 512,
|
||||
help = "Memory available to the guest, in megabytes"
|
||||
short = 'M',
|
||||
long = "max-memory",
|
||||
default_value_t = 1024,
|
||||
help = "Maximum memory available to the zone, in megabytes"
|
||||
)]
|
||||
mem: u64,
|
||||
#[arg[short = 'D', long = "device", help = "Devices to request for the guest"]]
|
||||
max_memory: u64,
|
||||
#[arg(
|
||||
short = 'm',
|
||||
long = "target-memory",
|
||||
default_value_t = 1024,
|
||||
help = "Target memory for the zone to use, in megabytes"
|
||||
)]
|
||||
target_memory: u64,
|
||||
#[arg[short = 'D', long = "device", help = "Devices to request for the zone"]]
|
||||
device: Vec<String>,
|
||||
#[arg[short, long, help = "Environment variables set in the guest"]]
|
||||
#[arg[short, long, help = "Environment variables set in the zone"]]
|
||||
env: Option<Vec<String>>,
|
||||
#[arg(short = 't', long, help = "Allocate tty for task")]
|
||||
tty: bool,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "Attach to the guest after guest starts, implies --wait"
|
||||
help = "Attach to the zone after zone starts, implies --wait"
|
||||
)]
|
||||
attach: bool,
|
||||
#[arg(
|
||||
short = 'W',
|
||||
long,
|
||||
help = "Wait for the guest to start, implied by --attach"
|
||||
help = "Wait for the zone to start, implied by --attach"
|
||||
)]
|
||||
wait: bool,
|
||||
#[arg(short = 'k', long, help = "OCI kernel image for guest to use")]
|
||||
#[arg(short = 'k', long, help = "OCI kernel image for zone to use")]
|
||||
kernel: Option<String>,
|
||||
#[arg(short = 'I', long, help = "OCI initrd image for guest to use")]
|
||||
#[arg(short = 'I', long, help = "OCI initrd image for zone to use")]
|
||||
initrd: Option<String>,
|
||||
#[arg(short = 'w', long, help = "Working directory")]
|
||||
working_directory: Option<String>,
|
||||
#[arg(help = "Container image for guest to use")]
|
||||
#[arg(long, help = "Enable verbose logging on the kernel")]
|
||||
kernel_verbose: bool,
|
||||
#[arg(long, help = "Additional kernel cmdline options")]
|
||||
kernel_cmdline_append: Option<String>,
|
||||
#[arg(help = "Container image for zone to use")]
|
||||
oci: String,
|
||||
#[arg(
|
||||
allow_hyphen_values = true,
|
||||
trailing_var_arg = true,
|
||||
help = "Command to run inside the guest"
|
||||
help = "Command to run inside the zone"
|
||||
)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
impl LaunchCommand {
|
||||
impl ZoneLaunchCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
@ -117,49 +140,58 @@ impl LaunchCommand {
|
||||
None
|
||||
};
|
||||
|
||||
let request = CreateGuestRequest {
|
||||
spec: Some(GuestSpec {
|
||||
let request = CreateZoneRequest {
|
||||
spec: Some(ZoneSpec {
|
||||
name: self.name.unwrap_or_default(),
|
||||
image: Some(image),
|
||||
kernel,
|
||||
initrd,
|
||||
vcpus: self.cpus,
|
||||
mem: self.mem,
|
||||
task: Some(GuestTaskSpec {
|
||||
initial_resources: Some(ZoneResourceSpec {
|
||||
max_memory: self.max_memory,
|
||||
target_memory: self.target_memory,
|
||||
max_cpus: self.max_cpus,
|
||||
target_cpus: self.target_cpus,
|
||||
}),
|
||||
task: Some(ZoneTaskSpec {
|
||||
environment: env_map(&self.env.unwrap_or_default())
|
||||
.iter()
|
||||
.map(|(key, value)| GuestTaskSpecEnvVar {
|
||||
.map(|(key, value)| ZoneTaskSpecEnvVar {
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect(),
|
||||
command: self.command,
|
||||
working_directory: self.working_directory.unwrap_or_default(),
|
||||
tty: self.tty,
|
||||
}),
|
||||
annotations: vec![],
|
||||
devices: self
|
||||
.device
|
||||
.iter()
|
||||
.map(|name| GuestSpecDevice { name: name.clone() })
|
||||
.map(|name| ZoneSpecDevice { name: name.clone() })
|
||||
.collect(),
|
||||
kernel_options: Some(ZoneKernelOptionsSpec {
|
||||
verbose: self.kernel_verbose,
|
||||
cmdline_append: self.kernel_cmdline_append.clone().unwrap_or_default(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
let response = client
|
||||
.create_guest(Request::new(request))
|
||||
.create_zone(Request::new(request))
|
||||
.await?
|
||||
.into_inner();
|
||||
let id = response.guest_id;
|
||||
let id = response.zone_id;
|
||||
|
||||
if self.wait || self.attach {
|
||||
wait_guest_started(&id, events.clone()).await?;
|
||||
wait_zone_started(&id, events.clone()).await?;
|
||||
}
|
||||
|
||||
let code = if self.attach {
|
||||
let input = StdioConsoleStream::stdin_stream(id.clone()).await;
|
||||
let output = client.console_data(input).await?.into_inner();
|
||||
let input = StdioConsoleStream::stdin_stream(id.clone(), true).await;
|
||||
let output = client.attach_zone_console(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
|
||||
let exit_hook_task = StdioConsoleStream::guest_exit_hook(id.clone(), events).await?;
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output, true).await });
|
||||
let exit_hook_task = StdioConsoleStream::zone_exit_hook(id.clone(), events).await?;
|
||||
select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
||||
@ -180,17 +212,18 @@ impl LaunchCommand {
|
||||
client: &mut ControlServiceClient<Channel>,
|
||||
image: &str,
|
||||
format: OciImageFormat,
|
||||
) -> Result<GuestImageSpec> {
|
||||
) -> Result<ZoneImageSpec> {
|
||||
let response = client
|
||||
.pull_image(PullImageRequest {
|
||||
image: image.to_string(),
|
||||
format: format.into(),
|
||||
overwrite_cache: self.pull_overwrite_cache,
|
||||
update: self.pull_update,
|
||||
})
|
||||
.await?;
|
||||
let reply = pull_interactive_progress(response.into_inner()).await?;
|
||||
Ok(GuestImageSpec {
|
||||
image: Some(Image::Oci(GuestOciImageSpec {
|
||||
Ok(ZoneImageSpec {
|
||||
image: Some(Image::Oci(ZoneOciImageSpec {
|
||||
digest: reply.digest,
|
||||
format: reply.format,
|
||||
})),
|
||||
@ -198,38 +231,38 @@ impl LaunchCommand {
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_guest_started(id: &str, events: EventStream) -> Result<()> {
|
||||
async fn wait_zone_started(id: &str, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
match event {
|
||||
Event::GuestChanged(changed) => {
|
||||
let Some(guest) = changed.guest else {
|
||||
Event::ZoneChanged(changed) => {
|
||||
let Some(zone) = changed.zone else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if guest.id != id {
|
||||
if zone.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(state) = guest.state else {
|
||||
let Some(status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(ref error) = state.error_info {
|
||||
if state.status() == GuestStatus::Failed {
|
||||
if let Some(ref error) = status.error_status {
|
||||
if status.state() == ZoneState::Failed {
|
||||
error!("launch failed: {}", error.message);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
error!("guest error: {}", error.message);
|
||||
error!("zone error: {}", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if state.status() == GuestStatus::Destroyed {
|
||||
error!("guest destroyed");
|
||||
if status.state() == ZoneState::Destroyed {
|
||||
error!("zone destroyed");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if state.status() == GuestStatus::Started {
|
||||
if status.state() == ZoneState::Created {
|
||||
break;
|
||||
}
|
||||
}
|
181
crates/ctl/src/cli/zone/list.rs
Normal file
181
crates/ctl/src/cli/zone/list.rs
Normal file
@ -0,0 +1,181 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::{presets::UTF8_FULL_CONDENSED, Cell, Color, Table};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::Zone,
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, ListZonesRequest, ResolveZoneIdRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv, zone_simple_line, zone_state_text};
|
||||
use krata::v1::common::ZoneState;
|
||||
use krata::v1::control::GetZoneRequest;
|
||||
use serde_json::Value;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum ZoneListFormat {
|
||||
Table,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Jsonl,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
Simple,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "List zone information")]
|
||||
pub struct ZoneListCommand {
|
||||
#[arg(short, long, default_value = "table", help = "Output format")]
|
||||
format: ZoneListFormat,
|
||||
#[arg(help = "Limit to a single zone, either the name or the uuid")]
|
||||
zone: Option<String>,
|
||||
}
|
||||
|
||||
impl ZoneListCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
let mut zones = if let Some(ref zone) = self.zone {
|
||||
let reply = client
|
||||
.resolve_zone_id(Request::new(ResolveZoneIdRequest { name: zone.clone() }))
|
||||
.await?
|
||||
.into_inner();
|
||||
if !reply.zone_id.is_empty() {
|
||||
let reply = client
|
||||
.get_zone(Request::new(GetZoneRequest {
|
||||
zone_id: reply.zone_id,
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if let Some(zone) = reply.zone {
|
||||
vec![zone]
|
||||
} else {
|
||||
return Err(anyhow!("unable to resolve zone '{}'", zone));
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!("unable to resolve zone '{}'", zone));
|
||||
}
|
||||
} else {
|
||||
client
|
||||
.list_zones(Request::new(ListZonesRequest {}))
|
||||
.await?
|
||||
.into_inner()
|
||||
.zones
|
||||
};
|
||||
|
||||
zones.sort_by(|a, b| {
|
||||
a.spec
|
||||
.as_ref()
|
||||
.map(|x| x.name.as_str())
|
||||
.unwrap_or("")
|
||||
.cmp(b.spec.as_ref().map(|x| x.name.as_str()).unwrap_or(""))
|
||||
});
|
||||
|
||||
match self.format {
|
||||
ZoneListFormat::Table => {
|
||||
self.print_zone_table(zones)?;
|
||||
}
|
||||
|
||||
ZoneListFormat::Simple => {
|
||||
for zone in zones {
|
||||
println!("{}", zone_simple_line(&zone));
|
||||
}
|
||||
}
|
||||
|
||||
ZoneListFormat::Json | ZoneListFormat::JsonPretty | ZoneListFormat::Yaml => {
|
||||
let mut values = Vec::new();
|
||||
for zone in zones {
|
||||
let message = proto2dynamic(zone)?;
|
||||
values.push(serde_json::to_value(message)?);
|
||||
}
|
||||
let value = Value::Array(values);
|
||||
let encoded = if self.format == ZoneListFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == ZoneListFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
ZoneListFormat::Jsonl => {
|
||||
for zone in zones {
|
||||
let message = proto2dynamic(zone)?;
|
||||
println!("{}", serde_json::to_string(&message)?);
|
||||
}
|
||||
}
|
||||
|
||||
ZoneListFormat::KeyValue => {
|
||||
self.print_key_value(zones)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_zone_table(&self, zones: Vec<Zone>) -> Result<()> {
|
||||
let mut table = Table::new();
|
||||
table.load_preset(UTF8_FULL_CONDENSED);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
table.set_header(vec!["name", "uuid", "state", "ipv4", "ipv6"]);
|
||||
for zone in zones {
|
||||
let ipv4 = zone
|
||||
.status
|
||||
.as_ref()
|
||||
.and_then(|x| x.network_status.as_ref())
|
||||
.map(|x| x.zone_ipv4.as_str())
|
||||
.unwrap_or("n/a");
|
||||
let ipv6 = zone
|
||||
.status
|
||||
.as_ref()
|
||||
.and_then(|x| x.network_status.as_ref())
|
||||
.map(|x| x.zone_ipv6.as_str())
|
||||
.unwrap_or("n/a");
|
||||
let Some(spec) = zone.spec else {
|
||||
continue;
|
||||
};
|
||||
let state = zone.status.as_ref().cloned().unwrap_or_default().state();
|
||||
let status_text = zone_state_text(state);
|
||||
|
||||
let status_color = match state {
|
||||
ZoneState::Destroyed | ZoneState::Failed => Color::Red,
|
||||
ZoneState::Destroying | ZoneState::Exited | ZoneState::Creating => Color::Yellow,
|
||||
ZoneState::Created => Color::Green,
|
||||
_ => Color::Reset,
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
Cell::new(spec.name),
|
||||
Cell::new(zone.id),
|
||||
Cell::new(status_text).fg(status_color),
|
||||
Cell::new(ipv4.to_string()),
|
||||
Cell::new(ipv6.to_string()),
|
||||
]);
|
||||
}
|
||||
if table.is_empty() {
|
||||
if self.zone.is_none() {
|
||||
println!("no zones have been launched");
|
||||
}
|
||||
} else {
|
||||
println!("{}", table);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key_value(&self, zones: Vec<Zone>) -> Result<()> {
|
||||
for zone in zones {
|
||||
let kvs = proto2kv(zone)?;
|
||||
println!("{}", kv2line(kvs),);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ use async_stream::stream;
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::control::{control_service_client::ControlServiceClient, ConsoleDataRequest},
|
||||
v1::control::{control_service_client::ControlServiceClient, ZoneConsoleRequest},
|
||||
};
|
||||
|
||||
use tokio::select;
|
||||
@ -12,39 +12,39 @@ use tonic::transport::Channel;
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
use super::resolve_guest;
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "View the logs of a guest")]
|
||||
pub struct LogsCommand {
|
||||
#[arg(short, long, help = "Follow output from the guest")]
|
||||
#[command(about = "View the logs of a zone")]
|
||||
pub struct ZoneLogsCommand {
|
||||
#[arg(short, long, help = "Follow output from the zone")]
|
||||
follow: bool,
|
||||
#[arg(help = "Guest to show logs for, either the name or the uuid")]
|
||||
guest: String,
|
||||
#[arg(help = "Zone to show logs for, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl LogsCommand {
|
||||
impl ZoneLogsCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
|
||||
let guest_id_stream = guest_id.clone();
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let zone_id_stream = zone_id.clone();
|
||||
let follow = self.follow;
|
||||
let input = stream! {
|
||||
yield ConsoleDataRequest { guest_id: guest_id_stream, data: Vec::new() };
|
||||
yield ZoneConsoleRequest { zone_id: zone_id_stream, replay_history: true, data: Vec::new() };
|
||||
if follow {
|
||||
let mut pending = pending::<ConsoleDataRequest>();
|
||||
let mut pending = pending::<ZoneConsoleRequest>();
|
||||
while let Some(x) = pending.next().await {
|
||||
yield x;
|
||||
}
|
||||
}
|
||||
};
|
||||
let output = client.console_data(input).await?.into_inner();
|
||||
let output = client.attach_zone_console(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
|
||||
let exit_hook_task = StdioConsoleStream::guest_exit_hook(guest_id.clone(), events).await?;
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output, false).await });
|
||||
let exit_hook_task = StdioConsoleStream::zone_exit_hook(zone_id.clone(), events).await?;
|
||||
let code = select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
@ -3,8 +3,8 @@ use clap::{Parser, ValueEnum};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::GuestMetricNode,
|
||||
control::{control_service_client::ControlServiceClient, ReadGuestMetricsRequest},
|
||||
common::ZoneMetricNode,
|
||||
control::{control_service_client::ControlServiceClient, ReadZoneMetricsRequest},
|
||||
},
|
||||
};
|
||||
|
||||
@ -12,10 +12,10 @@ use tonic::transport::Channel;
|
||||
|
||||
use crate::format::{kv2line, metrics_flat, metrics_tree, proto2dynamic};
|
||||
|
||||
use super::resolve_guest;
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum MetricsFormat {
|
||||
enum ZoneMetricsFormat {
|
||||
Tree,
|
||||
Json,
|
||||
JsonPretty,
|
||||
@ -24,37 +24,37 @@ enum MetricsFormat {
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Read metrics from the guest")]
|
||||
pub struct MetricsCommand {
|
||||
#[command(about = "Read metrics from the zone")]
|
||||
pub struct ZoneMetricsCommand {
|
||||
#[arg(short, long, default_value = "tree", help = "Output format")]
|
||||
format: MetricsFormat,
|
||||
#[arg(help = "Guest to read metrics for, either the name or the uuid")]
|
||||
guest: String,
|
||||
format: ZoneMetricsFormat,
|
||||
#[arg(help = "Zone to read metrics for, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl MetricsCommand {
|
||||
impl ZoneMetricsCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let root = client
|
||||
.read_guest_metrics(ReadGuestMetricsRequest { guest_id })
|
||||
.read_zone_metrics(ReadZoneMetricsRequest { zone_id })
|
||||
.await?
|
||||
.into_inner()
|
||||
.root
|
||||
.unwrap_or_default();
|
||||
match self.format {
|
||||
MetricsFormat::Tree => {
|
||||
ZoneMetricsFormat::Tree => {
|
||||
self.print_metrics_tree(root)?;
|
||||
}
|
||||
|
||||
MetricsFormat::Json | MetricsFormat::JsonPretty | MetricsFormat::Yaml => {
|
||||
ZoneMetricsFormat::Json | ZoneMetricsFormat::JsonPretty | ZoneMetricsFormat::Yaml => {
|
||||
let value = serde_json::to_value(proto2dynamic(root)?)?;
|
||||
let encoded = if self.format == MetricsFormat::JsonPretty {
|
||||
let encoded = if self.format == ZoneMetricsFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == MetricsFormat::Yaml {
|
||||
} else if self.format == ZoneMetricsFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
@ -62,7 +62,7 @@ impl MetricsCommand {
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
MetricsFormat::KeyValue => {
|
||||
ZoneMetricsFormat::KeyValue => {
|
||||
self.print_key_value(root)?;
|
||||
}
|
||||
}
|
||||
@ -70,12 +70,12 @@ impl MetricsCommand {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_metrics_tree(&self, root: GuestMetricNode) -> Result<()> {
|
||||
fn print_metrics_tree(&self, root: ZoneMetricNode) -> Result<()> {
|
||||
print!("{}", metrics_tree(root));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key_value(&self, metrics: GuestMetricNode) -> Result<()> {
|
||||
fn print_key_value(&self, metrics: ZoneMetricNode) -> Result<()> {
|
||||
let kvs = metrics_flat(metrics);
|
||||
println!("{}", kv2line(kvs));
|
||||
Ok(())
|
95
crates/ctl/src/cli/zone/mod.rs
Normal file
95
crates/ctl/src/cli/zone/mod.rs
Normal file
@ -0,0 +1,95 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use krata::events::EventStream;
|
||||
use krata::v1::control::control_service_client::ControlServiceClient;
|
||||
|
||||
use crate::cli::zone::attach::ZoneAttachCommand;
|
||||
use crate::cli::zone::destroy::ZoneDestroyCommand;
|
||||
use crate::cli::zone::exec::ZoneExecCommand;
|
||||
use crate::cli::zone::launch::ZoneLaunchCommand;
|
||||
use crate::cli::zone::list::ZoneListCommand;
|
||||
use crate::cli::zone::logs::ZoneLogsCommand;
|
||||
use crate::cli::zone::metrics::ZoneMetricsCommand;
|
||||
use crate::cli::zone::resolve::ZoneResolveCommand;
|
||||
use crate::cli::zone::top::ZoneTopCommand;
|
||||
use crate::cli::zone::update_resources::ZoneUpdateResourcesCommand;
|
||||
use crate::cli::zone::watch::ZoneWatchCommand;
|
||||
|
||||
pub mod attach;
|
||||
pub mod destroy;
|
||||
pub mod exec;
|
||||
pub mod launch;
|
||||
pub mod list;
|
||||
pub mod logs;
|
||||
pub mod metrics;
|
||||
pub mod resolve;
|
||||
pub mod top;
|
||||
pub mod update_resources;
|
||||
pub mod watch;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Manage the zones on the isolation engine")]
|
||||
pub struct ZoneCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: ZoneCommands,
|
||||
}
|
||||
|
||||
impl ZoneCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
self.subcommand.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Subcommand)]
|
||||
pub enum ZoneCommands {
|
||||
Attach(ZoneAttachCommand),
|
||||
List(ZoneListCommand),
|
||||
Launch(ZoneLaunchCommand),
|
||||
Destroy(ZoneDestroyCommand),
|
||||
Exec(ZoneExecCommand),
|
||||
Logs(ZoneLogsCommand),
|
||||
Metrics(ZoneMetricsCommand),
|
||||
Resolve(ZoneResolveCommand),
|
||||
Top(ZoneTopCommand),
|
||||
Watch(ZoneWatchCommand),
|
||||
UpdateResources(ZoneUpdateResourcesCommand),
|
||||
}
|
||||
|
||||
impl ZoneCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
ZoneCommands::Launch(launch) => launch.run(client, events).await,
|
||||
|
||||
ZoneCommands::Destroy(destroy) => destroy.run(client, events).await,
|
||||
|
||||
ZoneCommands::Attach(attach) => attach.run(client, events).await,
|
||||
|
||||
ZoneCommands::Logs(logs) => logs.run(client, events).await,
|
||||
|
||||
ZoneCommands::List(list) => list.run(client, events).await,
|
||||
|
||||
ZoneCommands::Watch(watch) => watch.run(events).await,
|
||||
|
||||
ZoneCommands::Resolve(resolve) => resolve.run(client).await,
|
||||
|
||||
ZoneCommands::Metrics(metrics) => metrics.run(client, events).await,
|
||||
|
||||
ZoneCommands::Top(top) => top.run(client, events).await,
|
||||
|
||||
ZoneCommands::Exec(exec) => exec.run(client).await,
|
||||
|
||||
ZoneCommands::UpdateResources(update_resources) => update_resources.run(client).await,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +1,26 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::v1::control::{control_service_client::ControlServiceClient, ResolveGuestRequest};
|
||||
use krata::v1::control::{control_service_client::ControlServiceClient, ResolveZoneIdRequest};
|
||||
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Resolve a guest name to a uuid")]
|
||||
pub struct ResolveCommand {
|
||||
#[arg(help = "Guest name")]
|
||||
guest: String,
|
||||
#[command(about = "Resolve a zone name to a uuid")]
|
||||
pub struct ZoneResolveCommand {
|
||||
#[arg(help = "Zone name")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ResolveCommand {
|
||||
impl ZoneResolveCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let reply = client
|
||||
.resolve_guest(Request::new(ResolveGuestRequest {
|
||||
name: self.guest.clone(),
|
||||
.resolve_zone_id(Request::new(ResolveZoneIdRequest {
|
||||
name: self.zone.clone(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if let Some(guest) = reply.guest {
|
||||
println!("{}", guest.id);
|
||||
if !reply.zone_id.is_empty() {
|
||||
println!("{}", reply.zone_id);
|
||||
} else {
|
||||
std::process::exit(1);
|
||||
}
|
@ -24,19 +24,19 @@ use ratatui::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
format::guest_status_text,
|
||||
format::zone_state_text,
|
||||
metrics::{
|
||||
lookup_metric_value, MultiMetricCollector, MultiMetricCollectorHandle, MultiMetricState,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Dashboard for running guests")]
|
||||
pub struct TopCommand {}
|
||||
#[command(about = "Dashboard for running zones")]
|
||||
pub struct ZoneTopCommand {}
|
||||
|
||||
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
impl TopCommand {
|
||||
impl ZoneTopCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
@ -44,14 +44,14 @@ impl TopCommand {
|
||||
) -> Result<()> {
|
||||
let collector = MultiMetricCollector::new(client, events, Duration::from_millis(200))?;
|
||||
let collector = collector.launch().await?;
|
||||
let mut tui = TopCommand::init()?;
|
||||
let mut app = TopApp {
|
||||
metrics: MultiMetricState { guests: vec![] },
|
||||
let mut tui = ZoneTopCommand::init()?;
|
||||
let mut app = ZoneTopApp {
|
||||
metrics: MultiMetricState { zones: vec![] },
|
||||
exit: false,
|
||||
table: TableState::new(),
|
||||
};
|
||||
app.run(collector, &mut tui).await?;
|
||||
TopCommand::restore()?;
|
||||
ZoneTopCommand::restore()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -68,13 +68,13 @@ impl TopCommand {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TopApp {
|
||||
pub struct ZoneTopApp {
|
||||
table: TableState,
|
||||
metrics: MultiMetricState,
|
||||
exit: bool,
|
||||
}
|
||||
|
||||
impl TopApp {
|
||||
impl ZoneTopApp {
|
||||
pub async fn run(
|
||||
&mut self,
|
||||
mut collector: MultiMetricCollectorHandle,
|
||||
@ -106,13 +106,13 @@ impl TopApp {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_frame(&mut self, frame: &mut Frame) {
|
||||
frame.render_widget(self, frame.size());
|
||||
frame.render_widget(self, frame.area());
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: Event) -> io::Result<()> {
|
||||
@ -136,7 +136,7 @@ impl TopApp {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut TopApp {
|
||||
impl Widget for &mut ZoneTopApp {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let title = Title::from(" krata isolation engine ".bold());
|
||||
let instructions = Title::from(vec![" Quit ".into(), "<Q> ".blue().bold()]);
|
||||
@ -152,12 +152,12 @@ impl Widget for &mut TopApp {
|
||||
|
||||
let mut rows = vec![];
|
||||
|
||||
for ms in &self.metrics.guests {
|
||||
let Some(ref spec) = ms.guest.spec else {
|
||||
for ms in &self.metrics.zones {
|
||||
let Some(ref spec) = ms.zone.spec else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(ref state) = ms.guest.state else {
|
||||
let Some(ref status) = ms.zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@ -176,8 +176,8 @@ impl Widget for &mut TopApp {
|
||||
|
||||
let row = Row::new(vec![
|
||||
spec.name.clone(),
|
||||
ms.guest.id.clone(),
|
||||
guest_status_text(state.status()),
|
||||
ms.zone.id.clone(),
|
||||
zone_state_text(status.state()),
|
||||
memory_total.unwrap_or_default(),
|
||||
memory_used.unwrap_or_default(),
|
||||
memory_free.unwrap_or_default(),
|
93
crates/ctl/src/cli/zone/update_resources.rs
Normal file
93
crates/ctl/src/cli/zone/update_resources.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::v1::{
|
||||
common::ZoneResourceSpec,
|
||||
control::{control_service_client::ControlServiceClient, UpdateZoneResourcesRequest},
|
||||
};
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
use krata::v1::control::GetZoneRequest;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Update the available resources to a zone")]
|
||||
pub struct ZoneUpdateResourcesCommand {
|
||||
#[arg(help = "Zone to update resources of, either the name or the uuid")]
|
||||
zone: String,
|
||||
#[arg(
|
||||
short = 'C',
|
||||
long = "max-cpus",
|
||||
default_value_t = 0,
|
||||
help = "Maximum vCPUs available to the zone (0 means previous value)"
|
||||
)]
|
||||
max_cpus: u32,
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long = "target-cpus",
|
||||
default_value_t = 0,
|
||||
help = "Target vCPUs for the zone to use (0 means previous value)"
|
||||
)]
|
||||
target_cpus: u32,
|
||||
#[arg(
|
||||
short = 'M',
|
||||
long = "max-memory",
|
||||
default_value_t = 0,
|
||||
help = "Maximum memory available to the zone, in megabytes (0 means previous value)"
|
||||
)]
|
||||
max_memory: u64,
|
||||
#[arg(
|
||||
short = 'm',
|
||||
long = "target-memory",
|
||||
default_value_t = 0,
|
||||
help = "Target memory for the zone to use, in megabytes (0 means previous value)"
|
||||
)]
|
||||
target_memory: u64,
|
||||
}
|
||||
|
||||
impl ZoneUpdateResourcesCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let zone_id = resolve_zone(&mut client, &self.zone).await?;
|
||||
let zone = client
|
||||
.get_zone(GetZoneRequest { zone_id })
|
||||
.await?
|
||||
.into_inner()
|
||||
.zone
|
||||
.unwrap_or_default();
|
||||
let active_resources = zone
|
||||
.status
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.resource_status
|
||||
.unwrap_or_default()
|
||||
.active_resources
|
||||
.unwrap_or_default();
|
||||
client
|
||||
.update_zone_resources(Request::new(UpdateZoneResourcesRequest {
|
||||
zone_id: zone.id.clone(),
|
||||
resources: Some(ZoneResourceSpec {
|
||||
max_memory: if self.max_memory == 0 {
|
||||
active_resources.max_memory
|
||||
} else {
|
||||
self.max_memory
|
||||
},
|
||||
target_memory: if self.target_memory == 0 {
|
||||
active_resources.target_memory
|
||||
} else {
|
||||
self.target_memory
|
||||
},
|
||||
max_cpus: if self.max_cpus == 0 {
|
||||
active_resources.max_cpus
|
||||
} else {
|
||||
self.max_cpus
|
||||
},
|
||||
target_cpus: if self.target_cpus == 0 {
|
||||
active_resources.target_cpus
|
||||
} else {
|
||||
self.target_cpus
|
||||
},
|
||||
}),
|
||||
}))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -2,53 +2,48 @@ use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{common::Guest, control::watch_events_reply::Event},
|
||||
v1::{common::Zone, control::watch_events_reply::Event},
|
||||
};
|
||||
use prost_reflect::ReflectMessage;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::format::{guest_simple_line, kv2line, proto2dynamic, proto2kv};
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv, zone_simple_line};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum WatchFormat {
|
||||
enum ZoneWatchFormat {
|
||||
Simple,
|
||||
Json,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Watch for guest changes")]
|
||||
pub struct WatchCommand {
|
||||
#[command(about = "Watch for zone changes")]
|
||||
pub struct ZoneWatchCommand {
|
||||
#[arg(short, long, default_value = "simple", help = "Output format")]
|
||||
format: WatchFormat,
|
||||
format: ZoneWatchFormat,
|
||||
}
|
||||
|
||||
impl WatchCommand {
|
||||
impl ZoneWatchCommand {
|
||||
pub async fn run(self, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
loop {
|
||||
let event = stream.recv().await?;
|
||||
|
||||
let Event::GuestChanged(changed) = event;
|
||||
let guest = changed.guest.clone();
|
||||
self.print_event("guest.changed", changed, guest)?;
|
||||
let Event::ZoneChanged(changed) = event;
|
||||
let zone = changed.zone.clone();
|
||||
self.print_event("zone.changed", changed, zone)?;
|
||||
}
|
||||
}
|
||||
|
||||
fn print_event(
|
||||
&self,
|
||||
typ: &str,
|
||||
event: impl ReflectMessage,
|
||||
guest: Option<Guest>,
|
||||
) -> Result<()> {
|
||||
fn print_event(&self, typ: &str, event: impl ReflectMessage, zone: Option<Zone>) -> Result<()> {
|
||||
match self.format {
|
||||
WatchFormat::Simple => {
|
||||
if let Some(guest) = guest {
|
||||
println!("{}", guest_simple_line(&guest));
|
||||
ZoneWatchFormat::Simple => {
|
||||
if let Some(zone) = zone {
|
||||
println!("{}", zone_simple_line(&zone));
|
||||
}
|
||||
}
|
||||
|
||||
WatchFormat::Json => {
|
||||
ZoneWatchFormat::Json => {
|
||||
let message = proto2dynamic(event)?;
|
||||
let mut value = serde_json::to_value(&message)?;
|
||||
if let Value::Object(ref mut map) = value {
|
||||
@ -57,7 +52,7 @@ impl WatchCommand {
|
||||
println!("{}", serde_json::to_string(&value)?);
|
||||
}
|
||||
|
||||
WatchFormat::KeyValue => {
|
||||
ZoneWatchFormat::KeyValue => {
|
||||
let mut map = proto2kv(event)?;
|
||||
map.insert("event.type".to_string(), typ.to_string());
|
||||
println!("{}", kv2line(map),);
|
@ -1,17 +1,15 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::Result;
|
||||
use async_stream::stream;
|
||||
use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled},
|
||||
tty::IsTty,
|
||||
};
|
||||
use krata::v1::common::ZoneState;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::GuestStatus,
|
||||
control::{
|
||||
watch_events_reply::Event, ConsoleDataReply, ConsoleDataRequest, ExecGuestReply,
|
||||
ExecGuestRequest,
|
||||
},
|
||||
v1::control::{
|
||||
watch_events_reply::Event, ExecInsideZoneReply, ExecInsideZoneRequest, ZoneConsoleReply,
|
||||
ZoneConsoleRequest,
|
||||
},
|
||||
};
|
||||
use log::debug;
|
||||
@ -25,10 +23,13 @@ use tonic::Streaming;
|
||||
pub struct StdioConsoleStream;
|
||||
|
||||
impl StdioConsoleStream {
|
||||
pub async fn stdin_stream(guest: String) -> impl Stream<Item = ConsoleDataRequest> {
|
||||
pub async fn stdin_stream(
|
||||
zone: String,
|
||||
replay_history: bool,
|
||||
) -> impl Stream<Item = ZoneConsoleRequest> {
|
||||
let mut stdin = stdin();
|
||||
stream! {
|
||||
yield ConsoleDataRequest { guest_id: guest, data: vec![] };
|
||||
yield ZoneConsoleRequest { zone_id: zone, replay_history, data: vec![] };
|
||||
|
||||
let mut buffer = vec![0u8; 60];
|
||||
loop {
|
||||
@ -43,14 +44,14 @@ impl StdioConsoleStream {
|
||||
if size == 1 && buffer[0] == 0x1d {
|
||||
break;
|
||||
}
|
||||
yield ConsoleDataRequest { guest_id: String::default(), data };
|
||||
yield ZoneConsoleRequest { zone_id: String::default(), replay_history, data };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stdin_stream_exec(
|
||||
initial: ExecGuestRequest,
|
||||
) -> impl Stream<Item = ExecGuestRequest> {
|
||||
initial: ExecInsideZoneRequest,
|
||||
) -> impl Stream<Item = ExecInsideZoneRequest> {
|
||||
let mut stdin = stdin();
|
||||
stream! {
|
||||
yield initial;
|
||||
@ -64,17 +65,21 @@ impl StdioConsoleStream {
|
||||
break;
|
||||
}
|
||||
};
|
||||
let data = buffer[0..size].to_vec();
|
||||
let stdin = buffer[0..size].to_vec();
|
||||
if size == 1 && buffer[0] == 0x1d {
|
||||
break;
|
||||
}
|
||||
yield ExecGuestRequest { guest_id: String::default(), task: None, data };
|
||||
let stdin_closed = size == 0;
|
||||
yield ExecInsideZoneRequest { zone_id: String::default(), task: None, stdin, stdin_closed, };
|
||||
if stdin_closed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stdout(mut stream: Streaming<ConsoleDataReply>) -> Result<()> {
|
||||
if stdin().is_tty() {
|
||||
pub async fn stdout(mut stream: Streaming<ZoneConsoleReply>, raw: bool) -> Result<()> {
|
||||
if raw && stdin().is_tty() {
|
||||
enable_raw_mode()?;
|
||||
StdioConsoleStream::register_terminal_restore_hook()?;
|
||||
}
|
||||
@ -90,7 +95,11 @@ impl StdioConsoleStream {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn exec_output(mut stream: Streaming<ExecGuestReply>) -> Result<i32> {
|
||||
pub async fn exec_output(mut stream: Streaming<ExecInsideZoneReply>, raw: bool) -> Result<i32> {
|
||||
if raw && stdin().is_tty() {
|
||||
enable_raw_mode()?;
|
||||
StdioConsoleStream::register_terminal_restore_hook()?;
|
||||
}
|
||||
let mut stdout = stdout();
|
||||
let mut stderr = stderr();
|
||||
while let Some(reply) = stream.next().await {
|
||||
@ -106,42 +115,47 @@ impl StdioConsoleStream {
|
||||
}
|
||||
|
||||
if reply.exited {
|
||||
if reply.error.is_empty() {
|
||||
return Ok(reply.exit_code);
|
||||
return if reply.error.is_empty() {
|
||||
Ok(reply.exit_code)
|
||||
} else {
|
||||
return Err(anyhow!("exec failed: {}", reply.error));
|
||||
}
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
stderr
|
||||
.write_all(format!("Error: exec failed: {}\n", reply.error).as_bytes())
|
||||
.await?;
|
||||
stderr.flush().await?;
|
||||
Ok(-1)
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(-1)
|
||||
}
|
||||
|
||||
pub async fn guest_exit_hook(
|
||||
pub async fn zone_exit_hook(
|
||||
id: String,
|
||||
events: EventStream,
|
||||
) -> Result<JoinHandle<Option<i32>>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
let Event::GuestChanged(changed) = event;
|
||||
let Some(guest) = changed.guest else {
|
||||
let Event::ZoneChanged(changed) = event;
|
||||
let Some(zone) = changed.zone else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(state) = guest.state else {
|
||||
let Some(status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if guest.id != id {
|
||||
if zone.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(exit_info) = state.exit_info {
|
||||
return Some(exit_info.code);
|
||||
if let Some(exit_status) = status.exit_status {
|
||||
return Some(exit_status.code);
|
||||
}
|
||||
|
||||
let status = state.status();
|
||||
if status == GuestStatus::Destroying || status == GuestStatus::Destroyed {
|
||||
let state = status.state();
|
||||
if state == ZoneState::Destroying || state == ZoneState::Destroyed {
|
||||
return Some(10);
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,12 @@ use std::{collections::HashMap, time::Duration};
|
||||
use anyhow::Result;
|
||||
use fancy_duration::FancyDuration;
|
||||
use human_bytes::human_bytes;
|
||||
use krata::v1::common::{Guest, GuestMetricFormat, GuestMetricNode, GuestStatus};
|
||||
use prost_reflect::{DynamicMessage, ReflectMessage};
|
||||
use prost_types::Value;
|
||||
use termtree::Tree;
|
||||
|
||||
use krata::v1::common::{Zone, ZoneMetricFormat, ZoneMetricNode, ZoneState};
|
||||
|
||||
pub fn proto2dynamic(proto: impl ReflectMessage) -> Result<DynamicMessage> {
|
||||
Ok(DynamicMessage::decode(
|
||||
proto.descriptor(),
|
||||
@ -75,32 +76,31 @@ pub fn kv2line(map: HashMap<String, String>) -> String {
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
pub fn guest_status_text(status: GuestStatus) -> String {
|
||||
pub fn zone_state_text(status: ZoneState) -> String {
|
||||
match status {
|
||||
GuestStatus::Starting => "starting",
|
||||
GuestStatus::Started => "started",
|
||||
GuestStatus::Destroying => "destroying",
|
||||
GuestStatus::Destroyed => "destroyed",
|
||||
GuestStatus::Exited => "exited",
|
||||
GuestStatus::Failed => "failed",
|
||||
ZoneState::Creating => "creating",
|
||||
ZoneState::Created => "created",
|
||||
ZoneState::Destroying => "destroying",
|
||||
ZoneState::Destroyed => "destroyed",
|
||||
ZoneState::Exited => "exited",
|
||||
ZoneState::Failed => "failed",
|
||||
_ => "unknown",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn guest_simple_line(guest: &Guest) -> String {
|
||||
let state = guest_status_text(
|
||||
guest
|
||||
.state
|
||||
pub fn zone_simple_line(zone: &Zone) -> String {
|
||||
let state = zone_state_text(
|
||||
zone.status
|
||||
.as_ref()
|
||||
.map(|x| x.status())
|
||||
.unwrap_or(GuestStatus::Unknown),
|
||||
.map(|x| x.state())
|
||||
.unwrap_or(ZoneState::Unknown),
|
||||
);
|
||||
let name = guest.spec.as_ref().map(|x| x.name.as_str()).unwrap_or("");
|
||||
let network = guest.state.as_ref().and_then(|x| x.network.as_ref());
|
||||
let ipv4 = network.map(|x| x.guest_ipv4.as_str()).unwrap_or("");
|
||||
let ipv6 = network.map(|x| x.guest_ipv6.as_str()).unwrap_or("");
|
||||
format!("{}\t{}\t{}\t{}\t{}", guest.id, state, name, ipv4, ipv6)
|
||||
let name = zone.spec.as_ref().map(|x| x.name.as_str()).unwrap_or("");
|
||||
let network_status = zone.status.as_ref().and_then(|x| x.network_status.as_ref());
|
||||
let ipv4 = network_status.map(|x| x.zone_ipv4.as_str()).unwrap_or("");
|
||||
let ipv6 = network_status.map(|x| x.zone_ipv6.as_str()).unwrap_or("");
|
||||
format!("{}\t{}\t{}\t{}\t{}", zone.id, state, name, ipv4, ipv6)
|
||||
}
|
||||
|
||||
fn metrics_value_string(value: Value) -> String {
|
||||
@ -116,18 +116,18 @@ fn metrics_value_numeric(value: Value) -> f64 {
|
||||
string.parse::<f64>().ok().unwrap_or(f64::NAN)
|
||||
}
|
||||
|
||||
pub fn metrics_value_pretty(value: Value, format: GuestMetricFormat) -> String {
|
||||
pub fn metrics_value_pretty(value: Value, format: ZoneMetricFormat) -> String {
|
||||
match format {
|
||||
GuestMetricFormat::Bytes => human_bytes(metrics_value_numeric(value)),
|
||||
GuestMetricFormat::Integer => (metrics_value_numeric(value) as u64).to_string(),
|
||||
GuestMetricFormat::DurationSeconds => {
|
||||
ZoneMetricFormat::Bytes => human_bytes(metrics_value_numeric(value)),
|
||||
ZoneMetricFormat::Integer => (metrics_value_numeric(value) as u64).to_string(),
|
||||
ZoneMetricFormat::DurationSeconds => {
|
||||
FancyDuration(Duration::from_secs_f64(metrics_value_numeric(value))).to_string()
|
||||
}
|
||||
_ => metrics_value_string(value),
|
||||
}
|
||||
}
|
||||
|
||||
fn metrics_flat_internal(prefix: &str, node: GuestMetricNode, map: &mut HashMap<String, String>) {
|
||||
fn metrics_flat_internal(prefix: &str, node: ZoneMetricNode, map: &mut HashMap<String, String>) {
|
||||
if let Some(value) = node.value {
|
||||
map.insert(prefix.to_string(), metrics_value_string(value));
|
||||
}
|
||||
@ -142,13 +142,13 @@ fn metrics_flat_internal(prefix: &str, node: GuestMetricNode, map: &mut HashMap<
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metrics_flat(root: GuestMetricNode) -> HashMap<String, String> {
|
||||
pub fn metrics_flat(root: ZoneMetricNode) -> HashMap<String, String> {
|
||||
let mut map = HashMap::new();
|
||||
metrics_flat_internal("", root, &mut map);
|
||||
map
|
||||
}
|
||||
|
||||
pub fn metrics_tree(node: GuestMetricNode) -> Tree<String> {
|
||||
pub fn metrics_tree(node: ZoneMetricNode) -> Tree<String> {
|
||||
let mut name = node.name.to_string();
|
||||
let format = node.format();
|
||||
if let Some(value) = node.value {
|
||||
|
@ -1,11 +1,13 @@
|
||||
use crate::format::metrics_value_pretty;
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::ZoneState;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::{Guest, GuestMetricNode, GuestStatus},
|
||||
common::{Zone, ZoneMetricNode},
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
ListGuestsRequest, ReadGuestMetricsRequest,
|
||||
ListZonesRequest, ReadZoneMetricsRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -19,15 +21,13 @@ use tokio::{
|
||||
};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::format::metrics_value_pretty;
|
||||
|
||||
pub struct MetricState {
|
||||
pub guest: Guest,
|
||||
pub root: Option<GuestMetricNode>,
|
||||
pub zone: Zone,
|
||||
pub root: Option<ZoneMetricNode>,
|
||||
}
|
||||
|
||||
pub struct MultiMetricState {
|
||||
pub guests: Vec<MetricState>,
|
||||
pub zones: Vec<MetricState>,
|
||||
}
|
||||
|
||||
pub struct MultiMetricCollector {
|
||||
@ -72,26 +72,26 @@ impl MultiMetricCollector {
|
||||
|
||||
pub async fn process(&mut self, sender: Sender<MultiMetricState>) -> Result<()> {
|
||||
let mut events = self.events.subscribe();
|
||||
let mut guests: Vec<Guest> = self
|
||||
let mut zones: Vec<Zone> = self
|
||||
.client
|
||||
.list_guests(ListGuestsRequest {})
|
||||
.list_zones(ListZonesRequest {})
|
||||
.await?
|
||||
.into_inner()
|
||||
.guests;
|
||||
.zones;
|
||||
loop {
|
||||
let collect = select! {
|
||||
x = events.recv() => match x {
|
||||
Ok(event) => {
|
||||
let Event::GuestChanged(changed) = event;
|
||||
let Some(guest) = changed.guest else {
|
||||
let Event::ZoneChanged(changed) = event;
|
||||
let Some(zone) = changed.zone else {
|
||||
continue;
|
||||
};
|
||||
let Some(ref state) = guest.state else {
|
||||
let Some(ref status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
guests.retain(|x| x.id != guest.id);
|
||||
if state.status() != GuestStatus::Destroying {
|
||||
guests.push(guest);
|
||||
zones.retain(|x| x.id != zone.id);
|
||||
if status.state() != ZoneState::Destroying {
|
||||
zones.push(zone);
|
||||
}
|
||||
false
|
||||
},
|
||||
@ -111,19 +111,19 @@ impl MultiMetricCollector {
|
||||
}
|
||||
|
||||
let mut metrics = Vec::new();
|
||||
for guest in &guests {
|
||||
let Some(ref state) = guest.state else {
|
||||
for zone in &zones {
|
||||
let Some(ref status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if state.status() != GuestStatus::Started {
|
||||
if status.state() != ZoneState::Created {
|
||||
continue;
|
||||
}
|
||||
|
||||
let root = timeout(
|
||||
Duration::from_secs(5),
|
||||
self.client.read_guest_metrics(ReadGuestMetricsRequest {
|
||||
guest_id: guest.id.clone(),
|
||||
self.client.read_zone_metrics(ReadZoneMetricsRequest {
|
||||
zone_id: zone.id.clone(),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
@ -132,16 +132,16 @@ impl MultiMetricCollector {
|
||||
.map(|x| x.into_inner())
|
||||
.and_then(|x| x.root);
|
||||
metrics.push(MetricState {
|
||||
guest: guest.clone(),
|
||||
zone: zone.clone(),
|
||||
root,
|
||||
});
|
||||
}
|
||||
sender.send(MultiMetricState { guests: metrics }).await?;
|
||||
sender.send(MultiMetricState { zones: metrics }).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup<'a>(node: &'a GuestMetricNode, path: &str) -> Option<&'a GuestMetricNode> {
|
||||
pub fn lookup<'a>(node: &'a ZoneMetricNode, path: &str) -> Option<&'a ZoneMetricNode> {
|
||||
let Some((what, b)) = path.split_once('/') else {
|
||||
return node.children.iter().find(|x| x.name == path);
|
||||
};
|
||||
@ -149,7 +149,7 @@ pub fn lookup<'a>(node: &'a GuestMetricNode, path: &str) -> Option<&'a GuestMetr
|
||||
return lookup(next, b);
|
||||
}
|
||||
|
||||
pub fn lookup_metric_value(node: &GuestMetricNode, path: &str) -> Option<String> {
|
||||
pub fn lookup_metric_value(node: &ZoneMetricNode, path: &str) -> Option<String> {
|
||||
lookup(node, path).and_then(|x| {
|
||||
x.value
|
||||
.as_ref()
|
||||
|
@ -9,6 +9,7 @@ edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
krata-advmac = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
async-stream = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
@ -17,14 +18,16 @@ circular-buffer = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.12" }
|
||||
krata-oci = { path = "../oci", version = "^0.0.12" }
|
||||
krata-runtime = { path = "../runtime", version = "^0.0.12" }
|
||||
ipnetwork = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.19" }
|
||||
krata-oci = { path = "../oci", version = "^0.0.19" }
|
||||
krata-runtime = { path = "../runtime", version = "^0.0.19" }
|
||||
log = { workspace = true }
|
||||
prost = { workspace = true }
|
||||
redb = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
signal-hook = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
|
@ -15,7 +15,7 @@ use kratad::command::DaemonCommand;
|
||||
async fn main() -> Result<()> {
|
||||
let mut builder = env_logger::Builder::new();
|
||||
builder
|
||||
.filter_level(LevelFilter::Trace)
|
||||
.filter_level(LevelFilter::Info)
|
||||
.parse_default_env()
|
||||
.filter(Some("backhand::filesystem::writer"), LevelFilter::Warn);
|
||||
|
||||
|
@ -10,6 +10,8 @@ pub struct DaemonConfig {
|
||||
pub oci: OciConfig,
|
||||
#[serde(default)]
|
||||
pub pci: DaemonPciConfig,
|
||||
#[serde(default = "default_network")]
|
||||
pub network: DaemonNetworkConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
@ -49,15 +51,74 @@ pub enum DaemonPciDeviceRdmReservePolicy {
|
||||
Relaxed,
|
||||
}
|
||||
|
||||
impl DaemonConfig {
|
||||
pub async fn load(path: &Path) -> Result<DaemonConfig> {
|
||||
if path.exists() {
|
||||
let content = fs::read_to_string(path).await?;
|
||||
let config: DaemonConfig = toml::from_str(&content)?;
|
||||
Ok(config)
|
||||
} else {
|
||||
fs::write(&path, "").await?;
|
||||
Ok(DaemonConfig::default())
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct DaemonNetworkConfig {
|
||||
#[serde(default = "default_network_nameservers")]
|
||||
pub nameservers: Vec<String>,
|
||||
#[serde(default = "default_network_ipv4")]
|
||||
pub ipv4: DaemonIpv4NetworkConfig,
|
||||
#[serde(default = "default_network_ipv6")]
|
||||
pub ipv6: DaemonIpv6NetworkConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct DaemonIpv4NetworkConfig {
|
||||
#[serde(default = "default_network_ipv4_subnet")]
|
||||
pub subnet: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct DaemonIpv6NetworkConfig {
|
||||
#[serde(default = "default_network_ipv6_subnet")]
|
||||
pub subnet: String,
|
||||
}
|
||||
|
||||
fn default_network() -> DaemonNetworkConfig {
|
||||
DaemonNetworkConfig {
|
||||
nameservers: default_network_nameservers(),
|
||||
ipv4: default_network_ipv4(),
|
||||
ipv6: default_network_ipv6(),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_network_nameservers() -> Vec<String> {
|
||||
vec![
|
||||
"1.1.1.1".to_string(),
|
||||
"1.0.0.1".to_string(),
|
||||
"2606:4700:4700::1111".to_string(),
|
||||
"2606:4700:4700::1001".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
fn default_network_ipv4() -> DaemonIpv4NetworkConfig {
|
||||
DaemonIpv4NetworkConfig {
|
||||
subnet: default_network_ipv4_subnet(),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_network_ipv4_subnet() -> String {
|
||||
"10.75.0.0/16".to_string()
|
||||
}
|
||||
|
||||
fn default_network_ipv6() -> DaemonIpv6NetworkConfig {
|
||||
DaemonIpv6NetworkConfig {
|
||||
subnet: default_network_ipv6_subnet(),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_network_ipv6_subnet() -> String {
|
||||
"fdd4:1476:6c7e::/48".to_string()
|
||||
}
|
||||
|
||||
impl DaemonConfig {
|
||||
pub async fn load(path: &Path) -> Result<DaemonConfig> {
|
||||
if !path.exists() {
|
||||
let config: DaemonConfig = toml::from_str("")?;
|
||||
let content = toml::to_string_pretty(&config)?;
|
||||
fs::write(&path, content).await?;
|
||||
}
|
||||
let content = fs::read_to_string(path).await?;
|
||||
let config: DaemonConfig = toml::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ use tokio::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::glt::GuestLookupTable;
|
||||
use crate::zlt::ZoneLookupTable;
|
||||
|
||||
const CONSOLE_BUFFER_SIZE: usize = 1024 * 1024;
|
||||
type RawConsoleBuffer = CircularBuffer<CONSOLE_BUFFER_SIZE, u8>;
|
||||
@ -24,7 +24,7 @@ type BufferMap = Arc<Mutex<HashMap<u32, ConsoleBuffer>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonConsoleHandle {
|
||||
glt: GuestLookupTable,
|
||||
zlt: ZoneLookupTable,
|
||||
listeners: ListenerMap,
|
||||
buffers: BufferMap,
|
||||
sender: Sender<(u32, Vec<u8>)>,
|
||||
@ -57,7 +57,7 @@ impl DaemonConsoleHandle {
|
||||
uuid: Uuid,
|
||||
sender: Sender<Vec<u8>>,
|
||||
) -> Result<DaemonConsoleAttachHandle> {
|
||||
let Some(domid) = self.glt.lookup_domid_by_uuid(&uuid).await else {
|
||||
let Some(domid) = self.zlt.lookup_domid_by_uuid(&uuid).await else {
|
||||
return Err(anyhow!("unable to find domain {}", uuid));
|
||||
};
|
||||
let buffers = self.buffers.lock().await;
|
||||
@ -84,7 +84,7 @@ impl Drop for DaemonConsoleHandle {
|
||||
}
|
||||
|
||||
pub struct DaemonConsole {
|
||||
glt: GuestLookupTable,
|
||||
zlt: ZoneLookupTable,
|
||||
listeners: ListenerMap,
|
||||
buffers: BufferMap,
|
||||
receiver: Receiver<(u32, Option<Vec<u8>>)>,
|
||||
@ -93,14 +93,14 @@ pub struct DaemonConsole {
|
||||
}
|
||||
|
||||
impl DaemonConsole {
|
||||
pub async fn new(glt: GuestLookupTable) -> Result<DaemonConsole> {
|
||||
pub async fn new(zlt: ZoneLookupTable) -> Result<DaemonConsole> {
|
||||
let (service, sender, receiver) =
|
||||
ChannelService::new("krata-console".to_string(), Some(0)).await?;
|
||||
let task = service.launch().await?;
|
||||
let listeners = Arc::new(Mutex::new(HashMap::new()));
|
||||
let buffers = Arc::new(Mutex::new(HashMap::new()));
|
||||
Ok(DaemonConsole {
|
||||
glt,
|
||||
zlt,
|
||||
listeners,
|
||||
buffers,
|
||||
receiver,
|
||||
@ -110,7 +110,7 @@ impl DaemonConsole {
|
||||
}
|
||||
|
||||
pub async fn launch(mut self) -> Result<DaemonConsoleHandle> {
|
||||
let glt = self.glt.clone();
|
||||
let zlt = self.zlt.clone();
|
||||
let listeners = self.listeners.clone();
|
||||
let buffers = self.buffers.clone();
|
||||
let sender = self.sender.clone();
|
||||
@ -120,7 +120,7 @@ impl DaemonConsole {
|
||||
}
|
||||
});
|
||||
Ok(DaemonConsoleHandle {
|
||||
glt,
|
||||
zlt,
|
||||
listeners,
|
||||
buffers,
|
||||
sender,
|
||||
|
@ -1,609 +0,0 @@
|
||||
use async_stream::try_stream;
|
||||
use futures::Stream;
|
||||
use krata::{
|
||||
idm::internal::{
|
||||
exec_stream_request_update::Update, request::Request as IdmRequestType,
|
||||
response::Response as IdmResponseType, ExecEnvVar, ExecStreamRequestStart,
|
||||
ExecStreamRequestStdin, ExecStreamRequestUpdate, MetricsRequest, Request as IdmRequest,
|
||||
},
|
||||
v1::{
|
||||
common::{Guest, GuestState, GuestStatus, OciImageFormat},
|
||||
control::{
|
||||
control_service_server::ControlService, ConsoleDataReply, ConsoleDataRequest,
|
||||
CreateGuestReply, CreateGuestRequest, DestroyGuestReply, DestroyGuestRequest,
|
||||
DeviceInfo, ExecGuestReply, ExecGuestRequest, HostCpuTopologyInfo,
|
||||
HostCpuTopologyReply, HostCpuTopologyRequest, HostPowerManagementPolicy,
|
||||
IdentifyHostReply, IdentifyHostRequest, ListDevicesReply, ListDevicesRequest,
|
||||
ListGuestsReply, ListGuestsRequest, PullImageReply, PullImageRequest,
|
||||
ReadGuestMetricsReply, ReadGuestMetricsRequest, ResolveGuestReply, ResolveGuestRequest,
|
||||
SnoopIdmReply, SnoopIdmRequest, WatchEventsReply, WatchEventsRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
use krataoci::{
|
||||
name::ImageName,
|
||||
packer::{service::OciPackerService, OciPackedFormat, OciPackedImage},
|
||||
progress::{OciProgress, OciProgressContext},
|
||||
};
|
||||
use kratart::Runtime;
|
||||
use std::{pin::Pin, str::FromStr};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::mpsc::{channel, Sender},
|
||||
task::JoinError,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
command::DaemonCommand, console::DaemonConsoleHandle, db::GuestStore,
|
||||
devices::DaemonDeviceManager, event::DaemonEventContext, glt::GuestLookupTable,
|
||||
idm::DaemonIdmHandle, metrics::idm_metric_to_api, oci::convert_oci_progress,
|
||||
};
|
||||
|
||||
pub struct ApiError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ApiError {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
ApiError {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiError> for Status {
|
||||
fn from(value: ApiError) -> Self {
|
||||
Status::unknown(value.message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonControlService {
|
||||
glt: GuestLookupTable,
|
||||
devices: DaemonDeviceManager,
|
||||
events: DaemonEventContext,
|
||||
console: DaemonConsoleHandle,
|
||||
idm: DaemonIdmHandle,
|
||||
guests: GuestStore,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
packer: OciPackerService,
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl DaemonControlService {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
glt: GuestLookupTable,
|
||||
devices: DaemonDeviceManager,
|
||||
events: DaemonEventContext,
|
||||
console: DaemonConsoleHandle,
|
||||
idm: DaemonIdmHandle,
|
||||
guests: GuestStore,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
packer: OciPackerService,
|
||||
runtime: Runtime,
|
||||
) -> Self {
|
||||
Self {
|
||||
glt,
|
||||
devices,
|
||||
events,
|
||||
console,
|
||||
idm,
|
||||
guests,
|
||||
guest_reconciler_notify,
|
||||
packer,
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ConsoleDataSelect {
|
||||
Read(Option<Vec<u8>>),
|
||||
Write(Option<Result<ConsoleDataRequest, tonic::Status>>),
|
||||
}
|
||||
|
||||
enum PullImageSelect {
|
||||
Progress(Option<OciProgress>),
|
||||
Completed(Result<Result<OciPackedImage, anyhow::Error>, JoinError>),
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl ControlService for DaemonControlService {
|
||||
type ExecGuestStream =
|
||||
Pin<Box<dyn Stream<Item = Result<ExecGuestReply, Status>> + Send + 'static>>;
|
||||
|
||||
type ConsoleDataStream =
|
||||
Pin<Box<dyn Stream<Item = Result<ConsoleDataReply, Status>> + Send + 'static>>;
|
||||
|
||||
type PullImageStream =
|
||||
Pin<Box<dyn Stream<Item = Result<PullImageReply, Status>> + Send + 'static>>;
|
||||
|
||||
type WatchEventsStream =
|
||||
Pin<Box<dyn Stream<Item = Result<WatchEventsReply, Status>> + Send + 'static>>;
|
||||
|
||||
type SnoopIdmStream =
|
||||
Pin<Box<dyn Stream<Item = Result<SnoopIdmReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn identify_host(
|
||||
&self,
|
||||
request: Request<IdentifyHostRequest>,
|
||||
) -> Result<Response<IdentifyHostReply>, Status> {
|
||||
let _ = request.into_inner();
|
||||
Ok(Response::new(IdentifyHostReply {
|
||||
host_domid: self.glt.host_domid(),
|
||||
host_uuid: self.glt.host_uuid().to_string(),
|
||||
krata_version: DaemonCommand::version(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn create_guest(
|
||||
&self,
|
||||
request: Request<CreateGuestRequest>,
|
||||
) -> Result<Response<CreateGuestReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
let Some(spec) = request.spec else {
|
||||
return Err(ApiError {
|
||||
message: "guest spec not provided".to_string(),
|
||||
}
|
||||
.into());
|
||||
};
|
||||
let uuid = Uuid::new_v4();
|
||||
self.guests
|
||||
.update(
|
||||
uuid,
|
||||
Guest {
|
||||
id: uuid.to_string(),
|
||||
state: Some(GuestState {
|
||||
status: GuestStatus::Starting.into(),
|
||||
network: None,
|
||||
exit_info: None,
|
||||
error_info: None,
|
||||
host: self.glt.host_uuid().to_string(),
|
||||
domid: u32::MAX,
|
||||
}),
|
||||
spec: Some(spec),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
self.guest_reconciler_notify
|
||||
.send(uuid)
|
||||
.await
|
||||
.map_err(|x| ApiError {
|
||||
message: x.to_string(),
|
||||
})?;
|
||||
Ok(Response::new(CreateGuestReply {
|
||||
guest_id: uuid.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn exec_guest(
|
||||
&self,
|
||||
request: Request<Streaming<ExecGuestRequest>>,
|
||||
) -> Result<Response<Self::ExecGuestStream>, Status> {
|
||||
let mut input = request.into_inner();
|
||||
let Some(request) = input.next().await else {
|
||||
return Err(ApiError {
|
||||
message: "expected to have at least one request".to_string(),
|
||||
}
|
||||
.into());
|
||||
};
|
||||
let request = request?;
|
||||
|
||||
let Some(task) = request.task else {
|
||||
return Err(ApiError {
|
||||
message: "task is missing".to_string(),
|
||||
}
|
||||
.into());
|
||||
};
|
||||
|
||||
let uuid = Uuid::from_str(&request.guest_id).map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let idm = self.idm.client(uuid).await.map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
|
||||
let idm_request = IdmRequest {
|
||||
request: Some(IdmRequestType::ExecStream(ExecStreamRequestUpdate {
|
||||
update: Some(Update::Start(ExecStreamRequestStart {
|
||||
environment: task
|
||||
.environment
|
||||
.into_iter()
|
||||
.map(|x| ExecEnvVar {
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
})
|
||||
.collect(),
|
||||
command: task.command,
|
||||
working_directory: task.working_directory,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
let output = try_stream! {
|
||||
let mut handle = idm.send_stream(idm_request).await.map_err(|x| ApiError {
|
||||
message: x.to_string(),
|
||||
})?;
|
||||
|
||||
loop {
|
||||
select! {
|
||||
x = input.next() => if let Some(update) = x {
|
||||
let update: Result<ExecGuestRequest, Status> = update.map_err(|error| ApiError {
|
||||
message: error.to_string()
|
||||
}.into());
|
||||
|
||||
if let Ok(update) = update {
|
||||
if !update.data.is_empty() {
|
||||
let _ = handle.update(IdmRequest {
|
||||
request: Some(IdmRequestType::ExecStream(ExecStreamRequestUpdate {
|
||||
update: Some(Update::Stdin(ExecStreamRequestStdin {
|
||||
data: update.data,
|
||||
})),
|
||||
}))}).await;
|
||||
}
|
||||
}
|
||||
},
|
||||
x = handle.receiver.recv() => match x {
|
||||
Some(response) => {
|
||||
let Some(IdmResponseType::ExecStream(update)) = response.response else {
|
||||
break;
|
||||
};
|
||||
let reply = ExecGuestReply {
|
||||
exited: update.exited,
|
||||
error: update.error,
|
||||
exit_code: update.exit_code,
|
||||
stdout: update.stdout,
|
||||
stderr: update.stderr
|
||||
};
|
||||
yield reply;
|
||||
},
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Response::new(Box::pin(output) as Self::ExecGuestStream))
|
||||
}
|
||||
|
||||
async fn destroy_guest(
|
||||
&self,
|
||||
request: Request<DestroyGuestRequest>,
|
||||
) -> Result<Response<DestroyGuestReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
let uuid = Uuid::from_str(&request.guest_id).map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let Some(mut guest) = self.guests.read(uuid).await.map_err(ApiError::from)? else {
|
||||
return Err(ApiError {
|
||||
message: "guest not found".to_string(),
|
||||
}
|
||||
.into());
|
||||
};
|
||||
|
||||
guest.state = Some(guest.state.as_mut().cloned().unwrap_or_default());
|
||||
|
||||
if guest.state.as_ref().unwrap().status() == GuestStatus::Destroyed {
|
||||
return Err(ApiError {
|
||||
message: "guest already destroyed".to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
guest.state.as_mut().unwrap().status = GuestStatus::Destroying.into();
|
||||
self.guests
|
||||
.update(uuid, guest)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
self.guest_reconciler_notify
|
||||
.send(uuid)
|
||||
.await
|
||||
.map_err(|x| ApiError {
|
||||
message: x.to_string(),
|
||||
})?;
|
||||
Ok(Response::new(DestroyGuestReply {}))
|
||||
}
|
||||
|
||||
async fn list_guests(
|
||||
&self,
|
||||
request: Request<ListGuestsRequest>,
|
||||
) -> Result<Response<ListGuestsReply>, Status> {
|
||||
let _ = request.into_inner();
|
||||
let guests = self.guests.list().await.map_err(ApiError::from)?;
|
||||
let guests = guests.into_values().collect::<Vec<Guest>>();
|
||||
Ok(Response::new(ListGuestsReply { guests }))
|
||||
}
|
||||
|
||||
async fn resolve_guest(
|
||||
&self,
|
||||
request: Request<ResolveGuestRequest>,
|
||||
) -> Result<Response<ResolveGuestReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
let guests = self.guests.list().await.map_err(ApiError::from)?;
|
||||
let guests = guests
|
||||
.into_values()
|
||||
.filter(|x| {
|
||||
let comparison_spec = x.spec.as_ref().cloned().unwrap_or_default();
|
||||
(!request.name.is_empty() && comparison_spec.name == request.name)
|
||||
|| x.id == request.name
|
||||
})
|
||||
.collect::<Vec<Guest>>();
|
||||
Ok(Response::new(ResolveGuestReply {
|
||||
guest: guests.first().cloned(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn console_data(
|
||||
&self,
|
||||
request: Request<Streaming<ConsoleDataRequest>>,
|
||||
) -> Result<Response<Self::ConsoleDataStream>, Status> {
|
||||
let mut input = request.into_inner();
|
||||
let Some(request) = input.next().await else {
|
||||
return Err(ApiError {
|
||||
message: "expected to have at least one request".to_string(),
|
||||
}
|
||||
.into());
|
||||
};
|
||||
let request = request?;
|
||||
let uuid = Uuid::from_str(&request.guest_id).map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let (sender, mut receiver) = channel(100);
|
||||
let console = self
|
||||
.console
|
||||
.attach(uuid, sender)
|
||||
.await
|
||||
.map_err(|error| ApiError {
|
||||
message: format!("failed to attach to console: {}", error),
|
||||
})?;
|
||||
|
||||
let output = try_stream! {
|
||||
yield ConsoleDataReply { data: console.initial.clone(), };
|
||||
loop {
|
||||
let what = select! {
|
||||
x = receiver.recv() => ConsoleDataSelect::Read(x),
|
||||
x = input.next() => ConsoleDataSelect::Write(x),
|
||||
};
|
||||
|
||||
match what {
|
||||
ConsoleDataSelect::Read(Some(data)) => {
|
||||
yield ConsoleDataReply { data, };
|
||||
},
|
||||
|
||||
ConsoleDataSelect::Read(None) => {
|
||||
break;
|
||||
}
|
||||
|
||||
ConsoleDataSelect::Write(Some(request)) => {
|
||||
let request = request?;
|
||||
if !request.data.is_empty() {
|
||||
console.send(request.data).await.map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
}
|
||||
},
|
||||
|
||||
ConsoleDataSelect::Write(None) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Response::new(Box::pin(output) as Self::ConsoleDataStream))
|
||||
}
|
||||
|
||||
async fn read_guest_metrics(
|
||||
&self,
|
||||
request: Request<ReadGuestMetricsRequest>,
|
||||
) -> Result<Response<ReadGuestMetricsReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
let uuid = Uuid::from_str(&request.guest_id).map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let client = self.idm.client(uuid).await.map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
|
||||
let response = client
|
||||
.send(IdmRequest {
|
||||
request: Some(IdmRequestType::Metrics(MetricsRequest {})),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
|
||||
let mut reply = ReadGuestMetricsReply::default();
|
||||
if let Some(IdmResponseType::Metrics(metrics)) = response.response {
|
||||
reply.root = metrics.root.map(idm_metric_to_api);
|
||||
}
|
||||
Ok(Response::new(reply))
|
||||
}
|
||||
|
||||
async fn pull_image(
|
||||
&self,
|
||||
request: Request<PullImageRequest>,
|
||||
) -> Result<Response<Self::PullImageStream>, Status> {
|
||||
let request = request.into_inner();
|
||||
let name = ImageName::parse(&request.image).map_err(|err| ApiError {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
let format = match request.format() {
|
||||
OciImageFormat::Unknown => OciPackedFormat::Squashfs,
|
||||
OciImageFormat::Squashfs => OciPackedFormat::Squashfs,
|
||||
OciImageFormat::Erofs => OciPackedFormat::Erofs,
|
||||
OciImageFormat::Tar => OciPackedFormat::Tar,
|
||||
};
|
||||
let (context, mut receiver) = OciProgressContext::create();
|
||||
let our_packer = self.packer.clone();
|
||||
|
||||
let output = try_stream! {
|
||||
let mut task = tokio::task::spawn(async move {
|
||||
our_packer.request(name, format, request.overwrite_cache, context).await
|
||||
});
|
||||
let abort_handle = task.abort_handle();
|
||||
let _task_cancel_guard = scopeguard::guard(abort_handle, |handle| {
|
||||
handle.abort();
|
||||
});
|
||||
|
||||
loop {
|
||||
let what = select! {
|
||||
x = receiver.changed() => match x {
|
||||
Ok(_) => PullImageSelect::Progress(Some(receiver.borrow_and_update().clone())),
|
||||
Err(_) => PullImageSelect::Progress(None),
|
||||
},
|
||||
x = &mut task => PullImageSelect::Completed(x),
|
||||
};
|
||||
match what {
|
||||
PullImageSelect::Progress(Some(progress)) => {
|
||||
let reply = PullImageReply {
|
||||
progress: Some(convert_oci_progress(progress)),
|
||||
digest: String::new(),
|
||||
format: OciImageFormat::Unknown.into(),
|
||||
};
|
||||
yield reply;
|
||||
},
|
||||
|
||||
PullImageSelect::Completed(result) => {
|
||||
let result = result.map_err(|err| ApiError {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
let packed = result.map_err(|err| ApiError {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
let reply = PullImageReply {
|
||||
progress: None,
|
||||
digest: packed.digest,
|
||||
format: match packed.format {
|
||||
OciPackedFormat::Squashfs => OciImageFormat::Squashfs.into(),
|
||||
OciPackedFormat::Erofs => OciImageFormat::Erofs.into(),
|
||||
OciPackedFormat::Tar => OciImageFormat::Tar.into(),
|
||||
},
|
||||
};
|
||||
yield reply;
|
||||
break;
|
||||
},
|
||||
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Response::new(Box::pin(output) as Self::PullImageStream))
|
||||
}
|
||||
|
||||
async fn watch_events(
|
||||
&self,
|
||||
request: Request<WatchEventsRequest>,
|
||||
) -> Result<Response<Self::WatchEventsStream>, Status> {
|
||||
let _ = request.into_inner();
|
||||
let mut events = self.events.subscribe();
|
||||
let output = try_stream! {
|
||||
while let Ok(event) = events.recv().await {
|
||||
yield WatchEventsReply { event: Some(event), };
|
||||
}
|
||||
};
|
||||
Ok(Response::new(Box::pin(output) as Self::WatchEventsStream))
|
||||
}
|
||||
|
||||
async fn snoop_idm(
|
||||
&self,
|
||||
request: Request<SnoopIdmRequest>,
|
||||
) -> Result<Response<Self::SnoopIdmStream>, Status> {
|
||||
let _ = request.into_inner();
|
||||
let mut messages = self.idm.snoop();
|
||||
let glt = self.glt.clone();
|
||||
let output = try_stream! {
|
||||
while let Ok(event) = messages.recv().await {
|
||||
let Some(from_uuid) = glt.lookup_uuid_by_domid(event.from).await else {
|
||||
continue;
|
||||
};
|
||||
let Some(to_uuid) = glt.lookup_uuid_by_domid(event.to).await else {
|
||||
continue;
|
||||
};
|
||||
yield SnoopIdmReply { from: from_uuid.to_string(), to: to_uuid.to_string(), packet: Some(event.packet) };
|
||||
}
|
||||
};
|
||||
Ok(Response::new(Box::pin(output) as Self::SnoopIdmStream))
|
||||
}
|
||||
|
||||
async fn list_devices(
|
||||
&self,
|
||||
request: Request<ListDevicesRequest>,
|
||||
) -> Result<Response<ListDevicesReply>, Status> {
|
||||
let _ = request.into_inner();
|
||||
let mut devices = Vec::new();
|
||||
let state = self.devices.copy().await.map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
for (name, state) in state {
|
||||
devices.push(DeviceInfo {
|
||||
name,
|
||||
claimed: state.owner.is_some(),
|
||||
owner: state.owner.map(|x| x.to_string()).unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
Ok(Response::new(ListDevicesReply { devices }))
|
||||
}
|
||||
|
||||
async fn get_host_cpu_topology(
|
||||
&self,
|
||||
request: Request<HostCpuTopologyRequest>,
|
||||
) -> Result<Response<HostCpuTopologyReply>, Status> {
|
||||
let _ = request.into_inner();
|
||||
let power = self
|
||||
.runtime
|
||||
.power_management_context()
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
let cputopo = power.cpu_topology().await.map_err(ApiError::from)?;
|
||||
let mut cpus = vec![];
|
||||
|
||||
for cpu in cputopo {
|
||||
cpus.push(HostCpuTopologyInfo {
|
||||
core: cpu.core,
|
||||
socket: cpu.socket,
|
||||
node: cpu.node,
|
||||
thread: cpu.thread,
|
||||
class: cpu.class as i32,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(Response::new(HostCpuTopologyReply { cpus }))
|
||||
}
|
||||
|
||||
async fn set_host_power_management_policy(
|
||||
&self,
|
||||
request: Request<HostPowerManagementPolicy>,
|
||||
) -> Result<Response<HostPowerManagementPolicy>, Status> {
|
||||
let policy = request.into_inner();
|
||||
let power = self
|
||||
.runtime
|
||||
.power_management_context()
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
let scheduler = &policy.scheduler;
|
||||
|
||||
power
|
||||
.set_smt_policy(policy.smt_awareness)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
power
|
||||
.set_scheduler_policy(scheduler)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
Ok(Response::new(HostPowerManagementPolicy {
|
||||
scheduler: scheduler.to_string(),
|
||||
smt_awareness: policy.smt_awareness,
|
||||
}))
|
||||
}
|
||||
}
|
84
crates/daemon/src/control/attach_zone_console.rs
Normal file
84
crates/daemon/src/control/attach_zone_console.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_stream::try_stream;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::channel;
|
||||
use tokio_stream::{Stream, StreamExt};
|
||||
use tonic::{Status, Streaming};
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::v1::control::{ZoneConsoleReply, ZoneConsoleRequest};
|
||||
|
||||
use crate::console::DaemonConsoleHandle;
|
||||
use crate::control::ApiError;
|
||||
|
||||
enum ConsoleDataSelect {
|
||||
Read(Option<Vec<u8>>),
|
||||
Write(Option<Result<ZoneConsoleRequest, Status>>),
|
||||
}
|
||||
|
||||
pub struct AttachZoneConsoleRpc {
|
||||
console: DaemonConsoleHandle,
|
||||
}
|
||||
|
||||
impl AttachZoneConsoleRpc {
|
||||
pub fn new(console: DaemonConsoleHandle) -> Self {
|
||||
Self { console }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
mut input: Streaming<ZoneConsoleRequest>,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<ZoneConsoleReply, Status>> + Send + 'static>>>
|
||||
{
|
||||
let Some(request) = input.next().await else {
|
||||
return Err(anyhow!("expected to have at least one request"));
|
||||
};
|
||||
let request = request?;
|
||||
let uuid = Uuid::from_str(&request.zone_id)?;
|
||||
let (sender, mut receiver) = channel(100);
|
||||
let console = self
|
||||
.console
|
||||
.attach(uuid, sender)
|
||||
.await
|
||||
.map_err(|error| anyhow!("failed to attach to console: {}", error))?;
|
||||
|
||||
let output = try_stream! {
|
||||
if request.replay_history {
|
||||
yield ZoneConsoleReply { data: console.initial.clone(), };
|
||||
}
|
||||
loop {
|
||||
let what = select! {
|
||||
x = receiver.recv() => ConsoleDataSelect::Read(x),
|
||||
x = input.next() => ConsoleDataSelect::Write(x),
|
||||
};
|
||||
|
||||
match what {
|
||||
ConsoleDataSelect::Read(Some(data)) => {
|
||||
yield ZoneConsoleReply { data, };
|
||||
},
|
||||
|
||||
ConsoleDataSelect::Read(None) => {
|
||||
break;
|
||||
}
|
||||
|
||||
ConsoleDataSelect::Write(Some(request)) => {
|
||||
let request = request?;
|
||||
if !request.data.is_empty() {
|
||||
console.send(request.data).await.map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
}
|
||||
},
|
||||
|
||||
ConsoleDataSelect::Write(None) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Box::pin(output))
|
||||
}
|
||||
}
|
56
crates/daemon/src/control/create_zone.rs
Normal file
56
crates/daemon/src/control/create_zone.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use crate::db::zone::ZoneStore;
|
||||
use crate::zlt::ZoneLookupTable;
|
||||
use anyhow::{anyhow, Result};
|
||||
use krata::v1::common::{Zone, ZoneState, ZoneStatus};
|
||||
use krata::v1::control::{CreateZoneReply, CreateZoneRequest};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct CreateZoneRpc {
|
||||
zones: ZoneStore,
|
||||
zlt: ZoneLookupTable,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
}
|
||||
|
||||
impl CreateZoneRpc {
|
||||
pub fn new(
|
||||
zones: ZoneStore,
|
||||
zlt: ZoneLookupTable,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
) -> Self {
|
||||
Self {
|
||||
zones,
|
||||
zlt,
|
||||
zone_reconciler_notify,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process(self, request: CreateZoneRequest) -> Result<CreateZoneReply> {
|
||||
let Some(spec) = request.spec else {
|
||||
return Err(anyhow!("zone spec not provided"));
|
||||
};
|
||||
let uuid = Uuid::new_v4();
|
||||
self.zones
|
||||
.update(
|
||||
uuid,
|
||||
Zone {
|
||||
id: uuid.to_string(),
|
||||
status: Some(ZoneStatus {
|
||||
state: ZoneState::Creating.into(),
|
||||
network_status: None,
|
||||
exit_status: None,
|
||||
error_status: None,
|
||||
resource_status: None,
|
||||
host: self.zlt.host_uuid().to_string(),
|
||||
domid: u32::MAX,
|
||||
}),
|
||||
spec: Some(spec),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.zone_reconciler_notify.send(uuid).await?;
|
||||
Ok(CreateZoneReply {
|
||||
zone_id: uuid.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
42
crates/daemon/src/control/destroy_zone.rs
Normal file
42
crates/daemon/src/control/destroy_zone.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::v1::common::ZoneState;
|
||||
use krata::v1::control::{DestroyZoneReply, DestroyZoneRequest};
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
|
||||
pub struct DestroyZoneRpc {
|
||||
zones: ZoneStore,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
}
|
||||
|
||||
impl DestroyZoneRpc {
|
||||
pub fn new(zones: ZoneStore, zone_reconciler_notify: Sender<Uuid>) -> Self {
|
||||
Self {
|
||||
zones,
|
||||
zone_reconciler_notify,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process(self, request: DestroyZoneRequest) -> Result<DestroyZoneReply> {
|
||||
let uuid = Uuid::from_str(&request.zone_id)?;
|
||||
let Some(mut zone) = self.zones.read(uuid).await? else {
|
||||
return Err(anyhow!("zone not found"));
|
||||
};
|
||||
|
||||
zone.status = Some(zone.status.as_mut().cloned().unwrap_or_default());
|
||||
|
||||
if zone.status.as_ref().unwrap().state() == ZoneState::Destroyed {
|
||||
return Err(anyhow!("zone already destroyed"));
|
||||
}
|
||||
|
||||
zone.status.as_mut().unwrap().state = ZoneState::Destroying.into();
|
||||
self.zones.update(uuid, zone).await?;
|
||||
self.zone_reconciler_notify.send(uuid).await?;
|
||||
Ok(DestroyZoneReply {})
|
||||
}
|
||||
}
|
116
crates/daemon/src/control/exec_inside_zone.rs
Normal file
116
crates/daemon/src/control/exec_inside_zone.rs
Normal file
@ -0,0 +1,116 @@
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_stream::try_stream;
|
||||
use tokio::select;
|
||||
use tokio_stream::{Stream, StreamExt};
|
||||
use tonic::{Status, Streaming};
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::idm::internal::Request;
|
||||
use krata::{
|
||||
idm::internal::{
|
||||
exec_stream_request_update::Update, request::Request as IdmRequestType,
|
||||
response::Response as IdmResponseType, ExecEnvVar, ExecStreamRequestStart,
|
||||
ExecStreamRequestStdin, ExecStreamRequestUpdate, Request as IdmRequest,
|
||||
},
|
||||
v1::control::{ExecInsideZoneReply, ExecInsideZoneRequest},
|
||||
};
|
||||
|
||||
use crate::control::ApiError;
|
||||
use crate::idm::DaemonIdmHandle;
|
||||
|
||||
pub struct ExecInsideZoneRpc {
|
||||
idm: DaemonIdmHandle,
|
||||
}
|
||||
|
||||
impl ExecInsideZoneRpc {
|
||||
pub fn new(idm: DaemonIdmHandle) -> Self {
|
||||
Self { idm }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
mut input: Streaming<ExecInsideZoneRequest>,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<ExecInsideZoneReply, Status>> + Send + 'static>>>
|
||||
{
|
||||
let Some(request) = input.next().await else {
|
||||
return Err(anyhow!("expected to have at least one request"));
|
||||
};
|
||||
let request = request?;
|
||||
|
||||
let Some(task) = request.task else {
|
||||
return Err(anyhow!("task is missing"));
|
||||
};
|
||||
|
||||
let uuid = Uuid::from_str(&request.zone_id)?;
|
||||
let idm = self.idm.client(uuid).await?;
|
||||
|
||||
let idm_request = Request {
|
||||
request: Some(IdmRequestType::ExecStream(ExecStreamRequestUpdate {
|
||||
update: Some(Update::Start(ExecStreamRequestStart {
|
||||
environment: task
|
||||
.environment
|
||||
.into_iter()
|
||||
.map(|x| ExecEnvVar {
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
})
|
||||
.collect(),
|
||||
command: task.command,
|
||||
working_directory: task.working_directory,
|
||||
tty: task.tty,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
let output = try_stream! {
|
||||
let mut handle = idm.send_stream(idm_request).await.map_err(|x| ApiError {
|
||||
message: x.to_string(),
|
||||
})?;
|
||||
|
||||
loop {
|
||||
select! {
|
||||
x = input.next() => if let Some(update) = x {
|
||||
let update: Result<ExecInsideZoneRequest, Status> = update.map_err(|error| ApiError {
|
||||
message: error.to_string()
|
||||
}.into());
|
||||
|
||||
if let Ok(update) = update {
|
||||
if !update.stdin.is_empty() {
|
||||
let _ = handle.update(IdmRequest {
|
||||
request: Some(IdmRequestType::ExecStream(ExecStreamRequestUpdate {
|
||||
update: Some(Update::Stdin(ExecStreamRequestStdin {
|
||||
data: update.stdin,
|
||||
closed: update.stdin_closed,
|
||||
})),
|
||||
}))}).await;
|
||||
}
|
||||
}
|
||||
},
|
||||
x = handle.receiver.recv() => match x {
|
||||
Some(response) => {
|
||||
let Some(IdmResponseType::ExecStream(update)) = response.response else {
|
||||
break;
|
||||
};
|
||||
let reply = ExecInsideZoneReply {
|
||||
exited: update.exited,
|
||||
error: update.error,
|
||||
exit_code: update.exit_code,
|
||||
stdout: update.stdout,
|
||||
stderr: update.stderr,
|
||||
};
|
||||
yield reply;
|
||||
},
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Box::pin(output))
|
||||
}
|
||||
}
|
33
crates/daemon/src/control/get_host_cpu_topology.rs
Normal file
33
crates/daemon/src/control/get_host_cpu_topology.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use anyhow::Result;
|
||||
use krata::v1::control::{GetHostCpuTopologyReply, GetHostCpuTopologyRequest, HostCpuTopologyInfo};
|
||||
use kratart::Runtime;
|
||||
|
||||
pub struct GetHostCpuTopologyRpc {
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl GetHostCpuTopologyRpc {
|
||||
pub fn new(runtime: Runtime) -> Self {
|
||||
Self { runtime }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
_request: GetHostCpuTopologyRequest,
|
||||
) -> Result<GetHostCpuTopologyReply> {
|
||||
let power = self.runtime.power_management_context().await?;
|
||||
let cpu_topology = power.cpu_topology().await?;
|
||||
let mut cpus = vec![];
|
||||
|
||||
for cpu in cpu_topology {
|
||||
cpus.push(HostCpuTopologyInfo {
|
||||
core: cpu.core,
|
||||
socket: cpu.socket,
|
||||
node: cpu.node,
|
||||
thread: cpu.thread,
|
||||
class: cpu.class as i32,
|
||||
})
|
||||
}
|
||||
Ok(GetHostCpuTopologyReply { cpus })
|
||||
}
|
||||
}
|
37
crates/daemon/src/control/get_host_status.rs
Normal file
37
crates/daemon/src/control/get_host_status.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use crate::command::DaemonCommand;
|
||||
use crate::ip::assignment::IpAssignment;
|
||||
use crate::zlt::ZoneLookupTable;
|
||||
use anyhow::Result;
|
||||
use krata::v1::control::{GetHostStatusReply, GetHostStatusRequest};
|
||||
|
||||
pub struct GetHostStatusRpc {
|
||||
ip: IpAssignment,
|
||||
zlt: ZoneLookupTable,
|
||||
}
|
||||
|
||||
impl GetHostStatusRpc {
|
||||
pub fn new(ip: IpAssignment, zlt: ZoneLookupTable) -> Self {
|
||||
Self { ip, zlt }
|
||||
}
|
||||
|
||||
pub async fn process(self, _request: GetHostStatusRequest) -> Result<GetHostStatusReply> {
|
||||
let host_reservation = self.ip.retrieve(self.zlt.host_uuid()).await?;
|
||||
Ok(GetHostStatusReply {
|
||||
host_domid: self.zlt.host_domid(),
|
||||
host_uuid: self.zlt.host_uuid().to_string(),
|
||||
krata_version: DaemonCommand::version(),
|
||||
host_ipv4: host_reservation
|
||||
.as_ref()
|
||||
.map(|x| format!("{}/{}", x.ipv4, x.ipv4_prefix))
|
||||
.unwrap_or_default(),
|
||||
host_ipv6: host_reservation
|
||||
.as_ref()
|
||||
.map(|x| format!("{}/{}", x.ipv6, x.ipv6_prefix))
|
||||
.unwrap_or_default(),
|
||||
host_mac: host_reservation
|
||||
.as_ref()
|
||||
.map(|x| x.mac.to_string().to_lowercase().replace('-', ":"))
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
24
crates/daemon/src/control/get_zone.rs
Normal file
24
crates/daemon/src/control/get_zone.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::v1::control::{GetZoneReply, GetZoneRequest};
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
|
||||
pub struct GetZoneRpc {
|
||||
zones: ZoneStore,
|
||||
}
|
||||
|
||||
impl GetZoneRpc {
|
||||
pub fn new(zones: ZoneStore) -> Self {
|
||||
Self { zones }
|
||||
}
|
||||
|
||||
pub async fn process(self, request: GetZoneRequest) -> Result<GetZoneReply> {
|
||||
let mut zones = self.zones.list().await?;
|
||||
let zone = zones.remove(&Uuid::from_str(&request.zone_id)?);
|
||||
Ok(GetZoneReply { zone })
|
||||
}
|
||||
}
|
28
crates/daemon/src/control/list_devices.rs
Normal file
28
crates/daemon/src/control/list_devices.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use krata::v1::control::{DeviceInfo, ListDevicesReply, ListDevicesRequest};
|
||||
|
||||
use crate::devices::DaemonDeviceManager;
|
||||
|
||||
pub struct ListDevicesRpc {
|
||||
devices: DaemonDeviceManager,
|
||||
}
|
||||
|
||||
impl ListDevicesRpc {
|
||||
pub fn new(devices: DaemonDeviceManager) -> Self {
|
||||
Self { devices }
|
||||
}
|
||||
|
||||
pub async fn process(self, _request: ListDevicesRequest) -> Result<ListDevicesReply> {
|
||||
let mut devices = Vec::new();
|
||||
let state = self.devices.copy().await?;
|
||||
for (name, state) in state {
|
||||
devices.push(DeviceInfo {
|
||||
name,
|
||||
claimed: state.owner.is_some(),
|
||||
owner: state.owner.map(|x| x.to_string()).unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
Ok(ListDevicesReply { devices })
|
||||
}
|
||||
}
|
21
crates/daemon/src/control/list_zones.rs
Normal file
21
crates/daemon/src/control/list_zones.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::Zone;
|
||||
use krata::v1::control::{ListZonesReply, ListZonesRequest};
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
|
||||
pub struct ListZonesRpc {
|
||||
zones: ZoneStore,
|
||||
}
|
||||
|
||||
impl ListZonesRpc {
|
||||
pub fn new(zones: ZoneStore) -> Self {
|
||||
Self { zones }
|
||||
}
|
||||
|
||||
pub async fn process(self, _request: ListZonesRequest) -> Result<ListZonesReply> {
|
||||
let zones = self.zones.list().await?;
|
||||
let zones = zones.into_values().collect::<Vec<Zone>>();
|
||||
Ok(ListZonesReply { zones })
|
||||
}
|
||||
}
|
351
crates/daemon/src/control/mod.rs
Normal file
351
crates/daemon/src/control/mod.rs
Normal file
@ -0,0 +1,351 @@
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Error;
|
||||
use futures::Stream;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::v1::control::{
|
||||
control_service_server::ControlService, CreateZoneReply, CreateZoneRequest, DestroyZoneReply,
|
||||
DestroyZoneRequest, ExecInsideZoneReply, ExecInsideZoneRequest, GetHostCpuTopologyReply,
|
||||
GetHostCpuTopologyRequest, GetHostStatusReply, GetHostStatusRequest, ListDevicesReply,
|
||||
ListDevicesRequest, ListZonesReply, ListZonesRequest, PullImageReply, PullImageRequest,
|
||||
ReadHypervisorConsoleReply, ReadHypervisorConsoleRequest, ReadZoneMetricsReply,
|
||||
ReadZoneMetricsRequest, ResolveZoneIdReply, ResolveZoneIdRequest, SnoopIdmReply,
|
||||
SnoopIdmRequest, UpdateZoneResourcesReply, UpdateZoneResourcesRequest, WatchEventsReply,
|
||||
WatchEventsRequest, ZoneConsoleReply, ZoneConsoleRequest,
|
||||
};
|
||||
use krata::v1::control::{
|
||||
GetZoneReply, GetZoneRequest, SetHostPowerManagementPolicyReply,
|
||||
SetHostPowerManagementPolicyRequest,
|
||||
};
|
||||
use krataoci::packer::service::OciPackerService;
|
||||
use kratart::Runtime;
|
||||
|
||||
use crate::control::attach_zone_console::AttachZoneConsoleRpc;
|
||||
use crate::control::create_zone::CreateZoneRpc;
|
||||
use crate::control::destroy_zone::DestroyZoneRpc;
|
||||
use crate::control::exec_inside_zone::ExecInsideZoneRpc;
|
||||
use crate::control::get_host_cpu_topology::GetHostCpuTopologyRpc;
|
||||
use crate::control::get_host_status::GetHostStatusRpc;
|
||||
use crate::control::get_zone::GetZoneRpc;
|
||||
use crate::control::list_devices::ListDevicesRpc;
|
||||
use crate::control::list_zones::ListZonesRpc;
|
||||
use crate::control::pull_image::PullImageRpc;
|
||||
use crate::control::read_hypervisor_console::ReadHypervisorConsoleRpc;
|
||||
use crate::control::read_zone_metrics::ReadZoneMetricsRpc;
|
||||
use crate::control::resolve_zone_id::ResolveZoneIdRpc;
|
||||
use crate::control::set_host_power_management_policy::SetHostPowerManagementPolicyRpc;
|
||||
use crate::control::snoop_idm::SnoopIdmRpc;
|
||||
use crate::control::update_zone_resources::UpdateZoneResourcesRpc;
|
||||
use crate::control::watch_events::WatchEventsRpc;
|
||||
use crate::db::zone::ZoneStore;
|
||||
use crate::ip::assignment::IpAssignment;
|
||||
use crate::{
|
||||
console::DaemonConsoleHandle, devices::DaemonDeviceManager, event::DaemonEventContext,
|
||||
idm::DaemonIdmHandle, zlt::ZoneLookupTable,
|
||||
};
|
||||
|
||||
pub mod attach_zone_console;
|
||||
pub mod create_zone;
|
||||
pub mod destroy_zone;
|
||||
pub mod exec_inside_zone;
|
||||
pub mod get_host_cpu_topology;
|
||||
pub mod get_host_status;
|
||||
pub mod get_zone;
|
||||
pub mod list_devices;
|
||||
pub mod list_zones;
|
||||
pub mod pull_image;
|
||||
pub mod read_hypervisor_console;
|
||||
pub mod read_zone_metrics;
|
||||
pub mod resolve_zone_id;
|
||||
pub mod set_host_power_management_policy;
|
||||
pub mod snoop_idm;
|
||||
pub mod update_zone_resources;
|
||||
pub mod watch_events;
|
||||
|
||||
pub struct ApiError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl From<Error> for ApiError {
|
||||
fn from(value: Error) -> Self {
|
||||
ApiError {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiError> for Status {
|
||||
fn from(value: ApiError) -> Self {
|
||||
Status::unknown(value.message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonControlService {
|
||||
zlt: ZoneLookupTable,
|
||||
devices: DaemonDeviceManager,
|
||||
events: DaemonEventContext,
|
||||
console: DaemonConsoleHandle,
|
||||
idm: DaemonIdmHandle,
|
||||
zones: ZoneStore,
|
||||
ip: IpAssignment,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
packer: OciPackerService,
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl DaemonControlService {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
zlt: ZoneLookupTable,
|
||||
devices: DaemonDeviceManager,
|
||||
events: DaemonEventContext,
|
||||
console: DaemonConsoleHandle,
|
||||
idm: DaemonIdmHandle,
|
||||
zones: ZoneStore,
|
||||
ip: IpAssignment,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
packer: OciPackerService,
|
||||
runtime: Runtime,
|
||||
) -> Self {
|
||||
Self {
|
||||
zlt,
|
||||
devices,
|
||||
events,
|
||||
console,
|
||||
idm,
|
||||
zones,
|
||||
ip,
|
||||
zone_reconciler_notify,
|
||||
packer,
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl ControlService for DaemonControlService {
|
||||
async fn get_host_status(
|
||||
&self,
|
||||
request: Request<GetHostStatusRequest>,
|
||||
) -> Result<Response<GetHostStatusReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
GetHostStatusRpc::new(self.ip.clone(), self.zlt.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
type SnoopIdmStream =
|
||||
Pin<Box<dyn Stream<Item = Result<SnoopIdmReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn snoop_idm(
|
||||
&self,
|
||||
request: Request<SnoopIdmRequest>,
|
||||
) -> Result<Response<Self::SnoopIdmStream>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
SnoopIdmRpc::new(self.idm.clone(), self.zlt.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_host_cpu_topology(
|
||||
&self,
|
||||
request: Request<GetHostCpuTopologyRequest>,
|
||||
) -> Result<Response<GetHostCpuTopologyReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
GetHostCpuTopologyRpc::new(self.runtime.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn set_host_power_management_policy(
|
||||
&self,
|
||||
request: Request<SetHostPowerManagementPolicyRequest>,
|
||||
) -> Result<Response<SetHostPowerManagementPolicyReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
SetHostPowerManagementPolicyRpc::new(self.runtime.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn list_devices(
|
||||
&self,
|
||||
request: Request<ListDevicesRequest>,
|
||||
) -> Result<Response<ListDevicesReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
ListDevicesRpc::new(self.devices.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
type PullImageStream =
|
||||
Pin<Box<dyn Stream<Item = Result<PullImageReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn pull_image(
|
||||
&self,
|
||||
request: Request<PullImageRequest>,
|
||||
) -> Result<Response<Self::PullImageStream>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
PullImageRpc::new(self.packer.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn create_zone(
|
||||
&self,
|
||||
request: Request<CreateZoneRequest>,
|
||||
) -> Result<Response<CreateZoneReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
CreateZoneRpc::new(
|
||||
self.zones.clone(),
|
||||
self.zlt.clone(),
|
||||
self.zone_reconciler_notify.clone(),
|
||||
)
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn destroy_zone(
|
||||
&self,
|
||||
request: Request<DestroyZoneRequest>,
|
||||
) -> Result<Response<DestroyZoneReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
DestroyZoneRpc::new(self.zones.clone(), self.zone_reconciler_notify.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn resolve_zone_id(
|
||||
&self,
|
||||
request: Request<ResolveZoneIdRequest>,
|
||||
) -> Result<Response<ResolveZoneIdReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
ResolveZoneIdRpc::new(self.zones.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_zone(
|
||||
&self,
|
||||
request: Request<GetZoneRequest>,
|
||||
) -> Result<Response<GetZoneReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(GetZoneRpc::new(self.zones.clone()).process(request).await)
|
||||
}
|
||||
|
||||
async fn update_zone_resources(
|
||||
&self,
|
||||
request: Request<UpdateZoneResourcesRequest>,
|
||||
) -> Result<Response<UpdateZoneResourcesReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
UpdateZoneResourcesRpc::new(self.runtime.clone(), self.zones.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn list_zones(
|
||||
&self,
|
||||
request: Request<ListZonesRequest>,
|
||||
) -> Result<Response<ListZonesReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(ListZonesRpc::new(self.zones.clone()).process(request).await)
|
||||
}
|
||||
|
||||
type AttachZoneConsoleStream =
|
||||
Pin<Box<dyn Stream<Item = Result<ZoneConsoleReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn attach_zone_console(
|
||||
&self,
|
||||
request: Request<Streaming<ZoneConsoleRequest>>,
|
||||
) -> Result<Response<Self::AttachZoneConsoleStream>, Status> {
|
||||
let input = request.into_inner();
|
||||
adapt(
|
||||
AttachZoneConsoleRpc::new(self.console.clone())
|
||||
.process(input)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
type ExecInsideZoneStream =
|
||||
Pin<Box<dyn Stream<Item = Result<ExecInsideZoneReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn exec_inside_zone(
|
||||
&self,
|
||||
request: Request<Streaming<ExecInsideZoneRequest>>,
|
||||
) -> Result<Response<Self::ExecInsideZoneStream>, Status> {
|
||||
let input = request.into_inner();
|
||||
adapt(
|
||||
ExecInsideZoneRpc::new(self.idm.clone())
|
||||
.process(input)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn read_zone_metrics(
|
||||
&self,
|
||||
request: Request<ReadZoneMetricsRequest>,
|
||||
) -> Result<Response<ReadZoneMetricsReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
ReadZoneMetricsRpc::new(self.idm.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
type WatchEventsStream =
|
||||
Pin<Box<dyn Stream<Item = Result<WatchEventsReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn watch_events(
|
||||
&self,
|
||||
request: Request<WatchEventsRequest>,
|
||||
) -> Result<Response<Self::WatchEventsStream>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
WatchEventsRpc::new(self.events.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn read_hypervisor_console(
|
||||
&self,
|
||||
request: Request<ReadHypervisorConsoleRequest>,
|
||||
) -> Result<Response<ReadHypervisorConsoleReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
adapt(
|
||||
ReadHypervisorConsoleRpc::new(self.runtime.clone())
|
||||
.process(request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn adapt<T>(result: anyhow::Result<T>) -> Result<Response<T>, Status> {
|
||||
result
|
||||
.map(Response::new)
|
||||
.map_err(|error| Status::unknown(error.to_string()))
|
||||
}
|
100
crates/daemon/src/control/pull_image.rs
Normal file
100
crates/daemon/src/control/pull_image.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use crate::control::ApiError;
|
||||
use crate::oci::convert_oci_progress;
|
||||
use anyhow::Result;
|
||||
use async_stream::try_stream;
|
||||
use krata::v1::common::OciImageFormat;
|
||||
use krata::v1::control::{PullImageReply, PullImageRequest};
|
||||
use krataoci::name::ImageName;
|
||||
use krataoci::packer::service::OciPackerService;
|
||||
use krataoci::packer::{OciPackedFormat, OciPackedImage};
|
||||
use krataoci::progress::{OciProgress, OciProgressContext};
|
||||
use std::pin::Pin;
|
||||
use tokio::select;
|
||||
use tokio::task::JoinError;
|
||||
use tokio_stream::Stream;
|
||||
use tonic::Status;
|
||||
|
||||
enum PullImageSelect {
|
||||
Progress(Option<OciProgress>),
|
||||
Completed(Result<Result<OciPackedImage, anyhow::Error>, JoinError>),
|
||||
}
|
||||
|
||||
pub struct PullImageRpc {
|
||||
packer: OciPackerService,
|
||||
}
|
||||
|
||||
impl PullImageRpc {
|
||||
pub fn new(packer: OciPackerService) -> Self {
|
||||
Self { packer }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
request: PullImageRequest,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<PullImageReply, Status>> + Send + 'static>>> {
|
||||
let name = ImageName::parse(&request.image)?;
|
||||
let format = match request.format() {
|
||||
OciImageFormat::Unknown => OciPackedFormat::Squashfs,
|
||||
OciImageFormat::Squashfs => OciPackedFormat::Squashfs,
|
||||
OciImageFormat::Erofs => OciPackedFormat::Erofs,
|
||||
OciImageFormat::Tar => OciPackedFormat::Tar,
|
||||
};
|
||||
let (context, mut receiver) = OciProgressContext::create();
|
||||
let our_packer = self.packer;
|
||||
|
||||
let output = try_stream! {
|
||||
let mut task = tokio::task::spawn(async move {
|
||||
our_packer.request(name, format, request.overwrite_cache, request.update, context).await
|
||||
});
|
||||
let abort_handle = task.abort_handle();
|
||||
let _task_cancel_guard = scopeguard::guard(abort_handle, |handle| {
|
||||
handle.abort();
|
||||
});
|
||||
|
||||
loop {
|
||||
let what = select! {
|
||||
x = receiver.changed() => match x {
|
||||
Ok(_) => PullImageSelect::Progress(Some(receiver.borrow_and_update().clone())),
|
||||
Err(_) => PullImageSelect::Progress(None),
|
||||
},
|
||||
x = &mut task => PullImageSelect::Completed(x),
|
||||
};
|
||||
match what {
|
||||
PullImageSelect::Progress(Some(progress)) => {
|
||||
let reply = PullImageReply {
|
||||
progress: Some(convert_oci_progress(progress)),
|
||||
digest: String::new(),
|
||||
format: OciImageFormat::Unknown.into(),
|
||||
};
|
||||
yield reply;
|
||||
},
|
||||
|
||||
PullImageSelect::Completed(result) => {
|
||||
let result = result.map_err(|err| ApiError {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
let packed = result.map_err(|err| ApiError {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
let reply = PullImageReply {
|
||||
progress: None,
|
||||
digest: packed.digest,
|
||||
format: match packed.format {
|
||||
OciPackedFormat::Squashfs => OciImageFormat::Squashfs.into(),
|
||||
OciPackedFormat::Erofs => OciImageFormat::Erofs.into(),
|
||||
OciPackedFormat::Tar => OciImageFormat::Tar.into(),
|
||||
},
|
||||
};
|
||||
yield reply;
|
||||
break;
|
||||
},
|
||||
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Box::pin(output))
|
||||
}
|
||||
}
|
23
crates/daemon/src/control/read_hypervisor_console.rs
Normal file
23
crates/daemon/src/control/read_hypervisor_console.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use anyhow::Result;
|
||||
use krata::v1::control::{ReadHypervisorConsoleReply, ReadHypervisorConsoleRequest};
|
||||
use kratart::Runtime;
|
||||
|
||||
pub struct ReadHypervisorConsoleRpc {
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl ReadHypervisorConsoleRpc {
|
||||
pub fn new(runtime: Runtime) -> Self {
|
||||
Self { runtime }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
_: ReadHypervisorConsoleRequest,
|
||||
) -> Result<ReadHypervisorConsoleReply> {
|
||||
let data = self.runtime.read_hypervisor_console(false).await?;
|
||||
Ok(ReadHypervisorConsoleReply {
|
||||
data: data.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
40
crates/daemon/src/control/read_zone_metrics.rs
Normal file
40
crates/daemon/src/control/read_zone_metrics.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::idm::internal::MetricsRequest;
|
||||
use krata::idm::internal::{
|
||||
request::Request as IdmRequestType, response::Response as IdmResponseType,
|
||||
Request as IdmRequest,
|
||||
};
|
||||
use krata::v1::control::{ReadZoneMetricsReply, ReadZoneMetricsRequest};
|
||||
|
||||
use crate::idm::DaemonIdmHandle;
|
||||
use crate::metrics::idm_metric_to_api;
|
||||
|
||||
pub struct ReadZoneMetricsRpc {
|
||||
idm: DaemonIdmHandle,
|
||||
}
|
||||
|
||||
impl ReadZoneMetricsRpc {
|
||||
pub fn new(idm: DaemonIdmHandle) -> Self {
|
||||
Self { idm }
|
||||
}
|
||||
|
||||
pub async fn process(self, request: ReadZoneMetricsRequest) -> Result<ReadZoneMetricsReply> {
|
||||
let uuid = Uuid::from_str(&request.zone_id)?;
|
||||
let client = self.idm.client(uuid).await?;
|
||||
let response = client
|
||||
.send(IdmRequest {
|
||||
request: Some(IdmRequestType::Metrics(MetricsRequest {})),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut reply = ReadZoneMetricsReply::default();
|
||||
if let Some(IdmResponseType::Metrics(metrics)) = response.response {
|
||||
reply.root = metrics.root.map(idm_metric_to_api);
|
||||
}
|
||||
Ok(reply)
|
||||
}
|
||||
}
|
30
crates/daemon/src/control/resolve_zone_id.rs
Normal file
30
crates/daemon/src/control/resolve_zone_id.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::Zone;
|
||||
use krata::v1::control::{ResolveZoneIdReply, ResolveZoneIdRequest};
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
|
||||
pub struct ResolveZoneIdRpc {
|
||||
zones: ZoneStore,
|
||||
}
|
||||
|
||||
impl ResolveZoneIdRpc {
|
||||
pub fn new(zones: ZoneStore) -> Self {
|
||||
Self { zones }
|
||||
}
|
||||
|
||||
pub async fn process(self, request: ResolveZoneIdRequest) -> Result<ResolveZoneIdReply> {
|
||||
let zones = self.zones.list().await?;
|
||||
let zones = zones
|
||||
.into_values()
|
||||
.filter(|x| {
|
||||
let comparison_spec = x.spec.as_ref().cloned().unwrap_or_default();
|
||||
(!request.name.is_empty() && comparison_spec.name == request.name)
|
||||
|| x.id == request.name
|
||||
})
|
||||
.collect::<Vec<Zone>>();
|
||||
Ok(ResolveZoneIdReply {
|
||||
zone_id: zones.first().cloned().map(|x| x.id).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
use anyhow::Result;
|
||||
use krata::v1::control::{SetHostPowerManagementPolicyReply, SetHostPowerManagementPolicyRequest};
|
||||
use kratart::Runtime;
|
||||
|
||||
pub struct SetHostPowerManagementPolicyRpc {
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl SetHostPowerManagementPolicyRpc {
|
||||
pub fn new(runtime: Runtime) -> Self {
|
||||
Self { runtime }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
request: SetHostPowerManagementPolicyRequest,
|
||||
) -> Result<SetHostPowerManagementPolicyReply> {
|
||||
let power = self.runtime.power_management_context().await?;
|
||||
let scheduler = &request.scheduler;
|
||||
|
||||
power.set_smt_policy(request.smt_awareness).await?;
|
||||
power.set_scheduler_policy(scheduler).await?;
|
||||
Ok(SetHostPowerManagementPolicyReply {})
|
||||
}
|
||||
}
|
39
crates/daemon/src/control/snoop_idm.rs
Normal file
39
crates/daemon/src/control/snoop_idm.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use crate::idm::DaemonIdmHandle;
|
||||
use crate::zlt::ZoneLookupTable;
|
||||
use anyhow::Result;
|
||||
use async_stream::try_stream;
|
||||
use krata::v1::control::{SnoopIdmReply, SnoopIdmRequest};
|
||||
use std::pin::Pin;
|
||||
use tokio_stream::Stream;
|
||||
use tonic::Status;
|
||||
|
||||
pub struct SnoopIdmRpc {
|
||||
idm: DaemonIdmHandle,
|
||||
zlt: ZoneLookupTable,
|
||||
}
|
||||
|
||||
impl SnoopIdmRpc {
|
||||
pub fn new(idm: DaemonIdmHandle, zlt: ZoneLookupTable) -> Self {
|
||||
Self { idm, zlt }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
_request: SnoopIdmRequest,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<SnoopIdmReply, Status>> + Send + 'static>>> {
|
||||
let mut messages = self.idm.snoop();
|
||||
let zlt = self.zlt.clone();
|
||||
let output = try_stream! {
|
||||
while let Ok(event) = messages.recv().await {
|
||||
let Some(from_uuid) = zlt.lookup_uuid_by_domid(event.from).await else {
|
||||
continue;
|
||||
};
|
||||
let Some(to_uuid) = zlt.lookup_uuid_by_domid(event.to).await else {
|
||||
continue;
|
||||
};
|
||||
yield SnoopIdmReply { from: from_uuid.to_string(), to: to_uuid.to_string(), packet: Some(event.packet) };
|
||||
}
|
||||
};
|
||||
Ok(Box::pin(output))
|
||||
}
|
||||
}
|
82
crates/daemon/src/control/update_zone_resources.rs
Normal file
82
crates/daemon/src/control/update_zone_resources.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use uuid::Uuid;
|
||||
|
||||
use krata::v1::common::{ZoneResourceStatus, ZoneState};
|
||||
use krata::v1::control::{UpdateZoneResourcesReply, UpdateZoneResourcesRequest};
|
||||
use kratart::Runtime;
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
|
||||
pub struct UpdateZoneResourcesRpc {
|
||||
runtime: Runtime,
|
||||
zones: ZoneStore,
|
||||
}
|
||||
|
||||
impl UpdateZoneResourcesRpc {
|
||||
pub fn new(runtime: Runtime, zones: ZoneStore) -> Self {
|
||||
Self { runtime, zones }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
request: UpdateZoneResourcesRequest,
|
||||
) -> Result<UpdateZoneResourcesReply> {
|
||||
let uuid = Uuid::from_str(&request.zone_id)?;
|
||||
let Some(mut zone) = self.zones.read(uuid).await? else {
|
||||
return Err(anyhow!("zone not found"));
|
||||
};
|
||||
|
||||
let Some(ref mut status) = zone.status else {
|
||||
return Err(anyhow!("zone state not available"));
|
||||
};
|
||||
|
||||
if status.state() != ZoneState::Created {
|
||||
return Err(anyhow!("zone is in an invalid state"));
|
||||
}
|
||||
|
||||
if status.domid == 0 || status.domid == u32::MAX {
|
||||
return Err(anyhow!("zone domid is invalid"));
|
||||
}
|
||||
|
||||
let mut resources = request.resources.unwrap_or_default();
|
||||
if resources.target_memory > resources.max_memory {
|
||||
resources.max_memory = resources.target_memory;
|
||||
}
|
||||
|
||||
if resources.target_cpus < 1 {
|
||||
resources.target_cpus = 1;
|
||||
}
|
||||
|
||||
let initial_resources = zone
|
||||
.spec
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.initial_resources
|
||||
.unwrap_or_default();
|
||||
if resources.target_cpus > initial_resources.max_cpus {
|
||||
resources.target_cpus = initial_resources.max_cpus;
|
||||
}
|
||||
resources.max_cpus = initial_resources.max_cpus;
|
||||
|
||||
self.runtime
|
||||
.set_memory_resources(
|
||||
status.domid,
|
||||
resources.target_memory * 1024 * 1024,
|
||||
resources.max_memory * 1024 * 1024,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| anyhow!("failed to set memory resources: {}", error))?;
|
||||
self.runtime
|
||||
.set_cpu_resources(status.domid, resources.target_cpus)
|
||||
.await
|
||||
.map_err(|error| anyhow!("failed to set cpu resources: {}", error))?;
|
||||
status.resource_status = Some(ZoneResourceStatus {
|
||||
active_resources: Some(resources),
|
||||
});
|
||||
|
||||
self.zones.update(uuid, zone).await?;
|
||||
Ok(UpdateZoneResourcesReply {})
|
||||
}
|
||||
}
|
31
crates/daemon/src/control/watch_events.rs
Normal file
31
crates/daemon/src/control/watch_events.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use crate::event::DaemonEventContext;
|
||||
use anyhow::Result;
|
||||
use async_stream::try_stream;
|
||||
use krata::v1::control::{WatchEventsReply, WatchEventsRequest};
|
||||
use std::pin::Pin;
|
||||
use tokio_stream::Stream;
|
||||
use tonic::Status;
|
||||
|
||||
pub struct WatchEventsRpc {
|
||||
events: DaemonEventContext,
|
||||
}
|
||||
|
||||
impl WatchEventsRpc {
|
||||
pub fn new(events: DaemonEventContext) -> Self {
|
||||
Self { events }
|
||||
}
|
||||
|
||||
pub async fn process(
|
||||
self,
|
||||
_request: WatchEventsRequest,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<WatchEventsReply, Status>> + Send + 'static>>>
|
||||
{
|
||||
let mut events = self.events.subscribe();
|
||||
let output = try_stream! {
|
||||
while let Ok(event) = events.recv().await {
|
||||
yield WatchEventsReply { event: Some(event), };
|
||||
}
|
||||
};
|
||||
Ok(Box::pin(output))
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
use std::{collections::HashMap, path::Path, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::Guest;
|
||||
use log::error;
|
||||
use prost::Message;
|
||||
use redb::{Database, ReadableTable, TableDefinition};
|
||||
use uuid::Uuid;
|
||||
|
||||
const GUESTS: TableDefinition<u128, &[u8]> = TableDefinition::new("guests");
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GuestStore {
|
||||
database: Arc<Database>,
|
||||
}
|
||||
|
||||
impl GuestStore {
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let database = Database::create(path)?;
|
||||
let write = database.begin_write()?;
|
||||
let _ = write.open_table(GUESTS);
|
||||
write.commit()?;
|
||||
Ok(GuestStore {
|
||||
database: Arc::new(database),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn read(&self, id: Uuid) -> Result<Option<Guest>> {
|
||||
let read = self.database.begin_read()?;
|
||||
let table = read.open_table(GUESTS)?;
|
||||
let Some(entry) = table.get(id.to_u128_le())? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let bytes = entry.value();
|
||||
Ok(Some(Guest::decode(bytes)?))
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<HashMap<Uuid, Guest>> {
|
||||
let mut guests: HashMap<Uuid, Guest> = HashMap::new();
|
||||
let read = self.database.begin_read()?;
|
||||
let table = read.open_table(GUESTS)?;
|
||||
for result in table.iter()? {
|
||||
let (key, value) = result?;
|
||||
let uuid = Uuid::from_u128_le(key.value());
|
||||
let state = match Guest::decode(value.value()) {
|
||||
Ok(state) => state,
|
||||
Err(error) => {
|
||||
error!(
|
||||
"found invalid guest state in database for uuid {}: {}",
|
||||
uuid, error
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
guests.insert(uuid, state);
|
||||
}
|
||||
Ok(guests)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, entry: Guest) -> Result<()> {
|
||||
let write = self.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(GUESTS)?;
|
||||
let bytes = entry.encode_to_vec();
|
||||
table.insert(id.to_u128_le(), bytes.as_slice())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove(&self, id: Uuid) -> Result<()> {
|
||||
let write = self.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(GUESTS)?;
|
||||
table.remove(id.to_u128_le())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
118
crates/daemon/src/db/ip.rs
Normal file
118
crates/daemon/src/db/ip.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use crate::db::KrataDatabase;
|
||||
use advmac::MacAddr6;
|
||||
use anyhow::Result;
|
||||
use log::error;
|
||||
use redb::{ReadableTable, TableDefinition};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use uuid::Uuid;
|
||||
|
||||
const IP_RESERVATION_TABLE: TableDefinition<u128, &[u8]> = TableDefinition::new("ip-reservation");
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IpReservationStore {
|
||||
db: KrataDatabase,
|
||||
}
|
||||
|
||||
impl IpReservationStore {
|
||||
pub fn open(db: KrataDatabase) -> Result<Self> {
|
||||
let write = db.database.begin_write()?;
|
||||
let _ = write.open_table(IP_RESERVATION_TABLE);
|
||||
write.commit()?;
|
||||
Ok(IpReservationStore { db })
|
||||
}
|
||||
|
||||
pub async fn read(&self, id: Uuid) -> Result<Option<IpReservation>> {
|
||||
let read = self.db.database.begin_read()?;
|
||||
let table = read.open_table(IP_RESERVATION_TABLE)?;
|
||||
let Some(entry) = table.get(id.to_u128_le())? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let bytes = entry.value();
|
||||
Ok(Some(serde_json::from_slice(bytes)?))
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<HashMap<Uuid, IpReservation>> {
|
||||
enum ListEntry {
|
||||
Valid(Uuid, IpReservation),
|
||||
Invalid(Uuid),
|
||||
}
|
||||
let mut reservations: HashMap<Uuid, IpReservation> = HashMap::new();
|
||||
|
||||
let corruptions = {
|
||||
let read = self.db.database.begin_read()?;
|
||||
let table = read.open_table(IP_RESERVATION_TABLE)?;
|
||||
table
|
||||
.iter()?
|
||||
.flat_map(|result| {
|
||||
result.map(|(key, value)| {
|
||||
let uuid = Uuid::from_u128_le(key.value());
|
||||
match serde_json::from_slice::<IpReservation>(value.value()) {
|
||||
Ok(reservation) => ListEntry::Valid(uuid, reservation),
|
||||
Err(error) => {
|
||||
error!(
|
||||
"found invalid ip reservation in database for uuid {}: {}",
|
||||
uuid, error
|
||||
);
|
||||
ListEntry::Invalid(uuid)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.filter_map(|entry| match entry {
|
||||
ListEntry::Valid(uuid, reservation) => {
|
||||
reservations.insert(uuid, reservation);
|
||||
None
|
||||
}
|
||||
|
||||
ListEntry::Invalid(uuid) => Some(uuid),
|
||||
})
|
||||
.collect::<Vec<Uuid>>()
|
||||
};
|
||||
|
||||
if !corruptions.is_empty() {
|
||||
let write = self.db.database.begin_write()?;
|
||||
let mut table = write.open_table(IP_RESERVATION_TABLE)?;
|
||||
for corruption in corruptions {
|
||||
table.remove(corruption.to_u128_le())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(reservations)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, entry: IpReservation) -> Result<()> {
|
||||
let write = self.db.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(IP_RESERVATION_TABLE)?;
|
||||
let bytes = serde_json::to_vec(&entry)?;
|
||||
table.insert(id.to_u128_le(), bytes.as_slice())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove(&self, id: Uuid) -> Result<()> {
|
||||
let write = self.db.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(IP_RESERVATION_TABLE)?;
|
||||
table.remove(id.to_u128_le())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct IpReservation {
|
||||
pub uuid: String,
|
||||
pub ipv4: Ipv4Addr,
|
||||
pub ipv6: Ipv6Addr,
|
||||
pub mac: MacAddr6,
|
||||
pub ipv4_prefix: u8,
|
||||
pub ipv6_prefix: u8,
|
||||
pub gateway_ipv4: Ipv4Addr,
|
||||
pub gateway_ipv6: Ipv6Addr,
|
||||
pub gateway_mac: MacAddr6,
|
||||
}
|
21
crates/daemon/src/db/mod.rs
Normal file
21
crates/daemon/src/db/mod.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use anyhow::Result;
|
||||
use redb::Database;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod ip;
|
||||
pub mod zone;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KrataDatabase {
|
||||
pub database: Arc<Database>,
|
||||
}
|
||||
|
||||
impl KrataDatabase {
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let database = Database::create(path)?;
|
||||
Ok(KrataDatabase {
|
||||
database: Arc::new(database),
|
||||
})
|
||||
}
|
||||
}
|
78
crates/daemon/src/db/zone.rs
Normal file
78
crates/daemon/src/db/zone.rs
Normal file
@ -0,0 +1,78 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::db::KrataDatabase;
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::Zone;
|
||||
use log::error;
|
||||
use prost::Message;
|
||||
use redb::{ReadableTable, TableDefinition};
|
||||
use uuid::Uuid;
|
||||
|
||||
const ZONE_TABLE: TableDefinition<u128, &[u8]> = TableDefinition::new("zone");
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ZoneStore {
|
||||
db: KrataDatabase,
|
||||
}
|
||||
|
||||
impl ZoneStore {
|
||||
pub fn open(db: KrataDatabase) -> Result<Self> {
|
||||
let write = db.database.begin_write()?;
|
||||
let _ = write.open_table(ZONE_TABLE);
|
||||
write.commit()?;
|
||||
Ok(ZoneStore { db })
|
||||
}
|
||||
|
||||
pub async fn read(&self, id: Uuid) -> Result<Option<Zone>> {
|
||||
let read = self.db.database.begin_read()?;
|
||||
let table = read.open_table(ZONE_TABLE)?;
|
||||
let Some(entry) = table.get(id.to_u128_le())? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let bytes = entry.value();
|
||||
Ok(Some(Zone::decode(bytes)?))
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<HashMap<Uuid, Zone>> {
|
||||
let mut zones: HashMap<Uuid, Zone> = HashMap::new();
|
||||
let read = self.db.database.begin_read()?;
|
||||
let table = read.open_table(ZONE_TABLE)?;
|
||||
for result in table.iter()? {
|
||||
let (key, value) = result?;
|
||||
let uuid = Uuid::from_u128_le(key.value());
|
||||
let state = match Zone::decode(value.value()) {
|
||||
Ok(state) => state,
|
||||
Err(error) => {
|
||||
error!(
|
||||
"found invalid zone state in database for uuid {}: {}",
|
||||
uuid, error
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
zones.insert(uuid, state);
|
||||
}
|
||||
Ok(zones)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, entry: Zone) -> Result<()> {
|
||||
let write = self.db.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(ZONE_TABLE)?;
|
||||
let bytes = entry.encode_to_vec();
|
||||
table.insert(id.to_u128_le(), bytes.as_slice())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove(&self, id: Uuid) -> Result<()> {
|
||||
let write = self.db.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(ZONE_TABLE)?;
|
||||
table.remove(id.to_u128_le())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@ impl DaemonDeviceManager {
|
||||
let mut devices = self.devices.write().await;
|
||||
let Some(state) = devices.get_mut(device) else {
|
||||
return Err(anyhow!(
|
||||
"unable to claim unknown device '{}' for guest {}",
|
||||
"unable to claim unknown device '{}' for zone {}",
|
||||
device,
|
||||
uuid
|
||||
));
|
||||
@ -39,7 +39,7 @@ impl DaemonDeviceManager {
|
||||
|
||||
if let Some(owner) = state.owner {
|
||||
return Err(anyhow!(
|
||||
"unable to claim device '{}' for guest {}: already claimed by {}",
|
||||
"unable to claim device '{}' for zone {}: already claimed by {}",
|
||||
device,
|
||||
uuid,
|
||||
owner
|
||||
@ -92,7 +92,7 @@ impl DaemonDeviceManager {
|
||||
|
||||
for (name, uuid) in &claims {
|
||||
if !devices.contains_key(name) {
|
||||
warn!("unknown device '{}' assigned to guest {}", name, uuid);
|
||||
warn!("unknown device '{}' assigned to zone {}", name, uuid);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,13 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::db::zone::ZoneStore;
|
||||
use crate::idm::DaemonIdmHandle;
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::ZoneExitStatus;
|
||||
use krata::{
|
||||
idm::{internal::event::Event as EventType, internal::Event},
|
||||
v1::common::{GuestExitInfo, GuestState, GuestStatus},
|
||||
v1::common::{ZoneState, ZoneStatus},
|
||||
};
|
||||
use log::{error, warn};
|
||||
use tokio::{
|
||||
@ -21,8 +24,6 @@ use tokio::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{db::GuestStore, idm::DaemonIdmHandle};
|
||||
|
||||
pub type DaemonEvent = krata::v1::control::watch_events_reply::Event;
|
||||
|
||||
const EVENT_CHANNEL_QUEUE_LEN: usize = 1000;
|
||||
@ -45,8 +46,8 @@ impl DaemonEventContext {
|
||||
}
|
||||
|
||||
pub struct DaemonEventGenerator {
|
||||
guests: GuestStore,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
zones: ZoneStore,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
feed: broadcast::Receiver<DaemonEvent>,
|
||||
idm: DaemonIdmHandle,
|
||||
idms: HashMap<u32, (Uuid, JoinHandle<()>)>,
|
||||
@ -57,15 +58,15 @@ pub struct DaemonEventGenerator {
|
||||
|
||||
impl DaemonEventGenerator {
|
||||
pub async fn new(
|
||||
guests: GuestStore,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
zones: ZoneStore,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
idm: DaemonIdmHandle,
|
||||
) -> Result<(DaemonEventContext, DaemonEventGenerator)> {
|
||||
let (sender, _) = broadcast::channel(EVENT_CHANNEL_QUEUE_LEN);
|
||||
let (idm_sender, idm_receiver) = channel(IDM_EVENT_CHANNEL_QUEUE_LEN);
|
||||
let generator = DaemonEventGenerator {
|
||||
guests,
|
||||
guest_reconciler_notify,
|
||||
zones,
|
||||
zone_reconciler_notify,
|
||||
feed: sender.subscribe(),
|
||||
idm,
|
||||
idms: HashMap::new(),
|
||||
@ -78,20 +79,20 @@ impl DaemonEventGenerator {
|
||||
}
|
||||
|
||||
async fn handle_feed_event(&mut self, event: &DaemonEvent) -> Result<()> {
|
||||
let DaemonEvent::GuestChanged(changed) = event;
|
||||
let Some(ref guest) = changed.guest else {
|
||||
let DaemonEvent::ZoneChanged(changed) = event;
|
||||
let Some(ref zone) = changed.zone else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(ref state) = guest.state else {
|
||||
let Some(ref status) = zone.status else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let status = state.status();
|
||||
let id = Uuid::from_str(&guest.id)?;
|
||||
let domid = state.domid;
|
||||
match status {
|
||||
GuestStatus::Started => {
|
||||
let state = status.state();
|
||||
let id = Uuid::from_str(&zone.id)?;
|
||||
let domid = status.domid;
|
||||
match state {
|
||||
ZoneState::Created => {
|
||||
if let Entry::Vacant(e) = self.idms.entry(domid) {
|
||||
let client = self.idm.client_by_domid(domid).await?;
|
||||
let mut receiver = client.subscribe().await?;
|
||||
@ -111,7 +112,7 @@ impl DaemonEventGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
GuestStatus::Destroyed => {
|
||||
ZoneState::Destroyed => {
|
||||
if let Some((_, handle)) = self.idms.remove(&domid) {
|
||||
handle.abort();
|
||||
}
|
||||
@ -130,18 +131,19 @@ impl DaemonEventGenerator {
|
||||
}
|
||||
|
||||
async fn handle_exit_code(&mut self, id: Uuid, code: i32) -> Result<()> {
|
||||
if let Some(mut guest) = self.guests.read(id).await? {
|
||||
guest.state = Some(GuestState {
|
||||
status: GuestStatus::Exited.into(),
|
||||
network: guest.state.clone().unwrap_or_default().network,
|
||||
exit_info: Some(GuestExitInfo { code }),
|
||||
error_info: None,
|
||||
host: guest.state.clone().map(|x| x.host).unwrap_or_default(),
|
||||
domid: guest.state.clone().map(|x| x.domid).unwrap_or(u32::MAX),
|
||||
if let Some(mut zone) = self.zones.read(id).await? {
|
||||
zone.status = Some(ZoneStatus {
|
||||
state: ZoneState::Exited.into(),
|
||||
network_status: zone.status.clone().unwrap_or_default().network_status,
|
||||
exit_status: Some(ZoneExitStatus { code }),
|
||||
error_status: None,
|
||||
resource_status: zone.status.clone().unwrap_or_default().resource_status,
|
||||
host: zone.status.clone().map(|x| x.host).unwrap_or_default(),
|
||||
domid: zone.status.clone().map(|x| x.domid).unwrap_or(u32::MAX),
|
||||
});
|
||||
|
||||
self.guests.update(id, guest).await?;
|
||||
self.guest_reconciler_notify.send(id).await?;
|
||||
self.zones.update(id, zone).await?;
|
||||
self.zone_reconciler_notify.send(id).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ use krata::idm::{
|
||||
transport::IdmTransportPacket,
|
||||
};
|
||||
use kratart::channel::ChannelService;
|
||||
use log::{error, warn};
|
||||
use log::{debug, error, warn};
|
||||
use prost::Message;
|
||||
use tokio::{
|
||||
select,
|
||||
@ -24,14 +24,14 @@ use tokio::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::glt::GuestLookupTable;
|
||||
use crate::zlt::ZoneLookupTable;
|
||||
|
||||
type BackendFeedMap = Arc<Mutex<HashMap<u32, Sender<IdmTransportPacket>>>>;
|
||||
type ClientMap = Arc<Mutex<HashMap<u32, IdmInternalClient>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonIdmHandle {
|
||||
glt: GuestLookupTable,
|
||||
zlt: ZoneLookupTable,
|
||||
clients: ClientMap,
|
||||
feeds: BackendFeedMap,
|
||||
tx_sender: Sender<(u32, IdmTransportPacket)>,
|
||||
@ -45,7 +45,7 @@ impl DaemonIdmHandle {
|
||||
}
|
||||
|
||||
pub async fn client(&self, uuid: Uuid) -> Result<IdmInternalClient> {
|
||||
let Some(domid) = self.glt.lookup_domid_by_uuid(&uuid).await else {
|
||||
let Some(domid) = self.zlt.lookup_domid_by_uuid(&uuid).await else {
|
||||
return Err(anyhow!("unable to find domain {}", uuid));
|
||||
};
|
||||
self.client_by_domid(domid).await
|
||||
@ -72,7 +72,7 @@ pub struct DaemonIdmSnoopPacket {
|
||||
}
|
||||
|
||||
pub struct DaemonIdm {
|
||||
glt: GuestLookupTable,
|
||||
zlt: ZoneLookupTable,
|
||||
clients: ClientMap,
|
||||
feeds: BackendFeedMap,
|
||||
tx_sender: Sender<(u32, IdmTransportPacket)>,
|
||||
@ -84,16 +84,21 @@ pub struct DaemonIdm {
|
||||
}
|
||||
|
||||
impl DaemonIdm {
|
||||
pub async fn new(glt: GuestLookupTable) -> Result<DaemonIdm> {
|
||||
pub async fn new(zlt: ZoneLookupTable) -> Result<DaemonIdm> {
|
||||
debug!("allocating channel service for idm");
|
||||
let (service, tx_raw_sender, rx_receiver) =
|
||||
ChannelService::new("krata-channel".to_string(), None).await?;
|
||||
let (tx_sender, tx_receiver) = channel(100);
|
||||
let (snoop_sender, _) = broadcast::channel(100);
|
||||
|
||||
debug!("starting idm channel service");
|
||||
let task = service.launch().await?;
|
||||
|
||||
let clients = Arc::new(Mutex::new(HashMap::new()));
|
||||
let feeds = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
Ok(DaemonIdm {
|
||||
glt,
|
||||
zlt,
|
||||
rx_receiver,
|
||||
tx_receiver,
|
||||
tx_sender,
|
||||
@ -106,7 +111,7 @@ impl DaemonIdm {
|
||||
}
|
||||
|
||||
pub async fn launch(mut self) -> Result<DaemonIdmHandle> {
|
||||
let glt = self.glt.clone();
|
||||
let zlt = self.zlt.clone();
|
||||
let clients = self.clients.clone();
|
||||
let feeds = self.feeds.clone();
|
||||
let tx_sender = self.tx_sender.clone();
|
||||
@ -119,7 +124,7 @@ impl DaemonIdm {
|
||||
}
|
||||
});
|
||||
Ok(DaemonIdmHandle {
|
||||
glt,
|
||||
zlt,
|
||||
clients,
|
||||
feeds,
|
||||
tx_sender,
|
||||
@ -128,50 +133,99 @@ impl DaemonIdm {
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_rx_packet(
|
||||
&mut self,
|
||||
domid: u32,
|
||||
data: Option<Vec<u8>>,
|
||||
buffers: &mut HashMap<u32, BytesMut>,
|
||||
) -> Result<()> {
|
||||
// check if data is present, if it is not, that signals a closed channel.
|
||||
if let Some(data) = data {
|
||||
let buffer = buffers.entry(domid).or_insert_with_key(|_| BytesMut::new());
|
||||
buffer.extend_from_slice(&data);
|
||||
loop {
|
||||
// check if the buffer is less than the header size, if so, wait for more data
|
||||
if buffer.len() < 6 {
|
||||
break;
|
||||
}
|
||||
|
||||
// check for the magic bytes 0xff, 0xff at the start of the message, if that doesn't
|
||||
// exist, clear the buffer. this ensures that partial messages won't be processed.
|
||||
if buffer[0] != 0xff || buffer[1] != 0xff {
|
||||
buffer.clear();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// read the size from the buffer as a little endian u32
|
||||
let size = (buffer[2] as u32
|
||||
| (buffer[3] as u32) << 8
|
||||
| (buffer[4] as u32) << 16
|
||||
| (buffer[5] as u32) << 24) as usize;
|
||||
let needed = size + 6;
|
||||
if buffer.len() < needed {
|
||||
return Ok(());
|
||||
}
|
||||
let mut packet = buffer.split_to(needed);
|
||||
// advance the buffer by the header, leaving only the raw data.
|
||||
packet.advance(6);
|
||||
match IdmTransportPacket::decode(packet) {
|
||||
Ok(packet) => {
|
||||
let _ =
|
||||
client_or_create(domid, &self.tx_sender, &self.clients, &self.feeds)
|
||||
.await?;
|
||||
let guard = self.feeds.lock().await;
|
||||
if let Some(feed) = guard.get(&domid) {
|
||||
let _ = feed.try_send(packet.clone());
|
||||
}
|
||||
let _ = self.snoop_sender.send(DaemonIdmSnoopPacket {
|
||||
from: domid,
|
||||
to: 0,
|
||||
packet,
|
||||
});
|
||||
}
|
||||
|
||||
Err(packet) => {
|
||||
warn!("received invalid packet from domain {}: {}", domid, packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut clients = self.clients.lock().await;
|
||||
let mut feeds = self.feeds.lock().await;
|
||||
clients.remove(&domid);
|
||||
feeds.remove(&domid);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn tx_packet(&mut self, domid: u32, packet: IdmTransportPacket) -> Result<()> {
|
||||
let data = packet.encode_to_vec();
|
||||
let mut buffer = vec![0u8; 6];
|
||||
let length = data.len() as u32;
|
||||
// magic bytes
|
||||
buffer[0] = 0xff;
|
||||
buffer[1] = 0xff;
|
||||
// little endian u32 for message size
|
||||
buffer[2] = length as u8;
|
||||
buffer[3] = (length << 8) as u8;
|
||||
buffer[4] = (length << 16) as u8;
|
||||
buffer[5] = (length << 24) as u8;
|
||||
buffer.extend_from_slice(&data);
|
||||
self.tx_raw_sender.send((domid, buffer)).await?;
|
||||
let _ = self.snoop_sender.send(DaemonIdmSnoopPacket {
|
||||
from: 0,
|
||||
to: domid,
|
||||
packet,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process(&mut self, buffers: &mut HashMap<u32, BytesMut>) -> Result<()> {
|
||||
loop {
|
||||
select! {
|
||||
x = self.rx_receiver.recv() => match x {
|
||||
Some((domid, data)) => {
|
||||
if let Some(data) = data {
|
||||
let buffer = buffers.entry(domid).or_insert_with_key(|_| BytesMut::new());
|
||||
buffer.extend_from_slice(&data);
|
||||
if buffer.len() < 6 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if buffer[0] != 0xff || buffer[1] != 0xff {
|
||||
buffer.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
let size = (buffer[2] as u32 | (buffer[3] as u32) << 8 | (buffer[4] as u32) << 16 | (buffer[5] as u32) << 24) as usize;
|
||||
let needed = size + 6;
|
||||
if buffer.len() < needed {
|
||||
continue;
|
||||
}
|
||||
let mut packet = buffer.split_to(needed);
|
||||
packet.advance(6);
|
||||
match IdmTransportPacket::decode(packet) {
|
||||
Ok(packet) => {
|
||||
let _ = client_or_create(domid, &self.tx_sender, &self.clients, &self.feeds).await?;
|
||||
let guard = self.feeds.lock().await;
|
||||
if let Some(feed) = guard.get(&domid) {
|
||||
let _ = feed.try_send(packet.clone());
|
||||
}
|
||||
let _ = self.snoop_sender.send(DaemonIdmSnoopPacket { from: domid, to: 0, packet });
|
||||
}
|
||||
|
||||
Err(packet) => {
|
||||
warn!("received invalid packet from domain {}: {}", domid, packet);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut clients = self.clients.lock().await;
|
||||
let mut feeds = self.feeds.lock().await;
|
||||
clients.remove(&domid);
|
||||
feeds.remove(&domid);
|
||||
}
|
||||
self.process_rx_packet(domid, data, buffers).await?;
|
||||
},
|
||||
|
||||
None => {
|
||||
@ -180,25 +234,14 @@ impl DaemonIdm {
|
||||
},
|
||||
x = self.tx_receiver.recv() => match x {
|
||||
Some((domid, packet)) => {
|
||||
let data = packet.encode_to_vec();
|
||||
let mut buffer = vec![0u8; 6];
|
||||
let length = data.len() as u32;
|
||||
buffer[0] = 0xff;
|
||||
buffer[1] = 0xff;
|
||||
buffer[2] = length as u8;
|
||||
buffer[3] = (length << 8) as u8;
|
||||
buffer[4] = (length << 16) as u8;
|
||||
buffer[5] = (length << 24) as u8;
|
||||
buffer.extend_from_slice(&data);
|
||||
self.tx_raw_sender.send((domid, buffer)).await?;
|
||||
let _ = self.snoop_sender.send(DaemonIdmSnoopPacket { from: 0, to: domid, packet });
|
||||
self.tx_packet(domid, packet).await?;
|
||||
},
|
||||
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -247,9 +290,9 @@ pub struct IdmDaemonBackend {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl IdmBackend for IdmDaemonBackend {
|
||||
async fn recv(&mut self) -> Result<IdmTransportPacket> {
|
||||
async fn recv(&mut self) -> Result<Vec<IdmTransportPacket>> {
|
||||
if let Some(packet) = self.rx_receiver.recv().await {
|
||||
Ok(packet)
|
||||
Ok(vec![packet])
|
||||
} else {
|
||||
Err(anyhow!("idm receive channel closed"))
|
||||
}
|
||||
|
198
crates/daemon/src/ip/assignment.rs
Normal file
198
crates/daemon/src/ip/assignment.rs
Normal file
@ -0,0 +1,198 @@
|
||||
use advmac::MacAddr6;
|
||||
use anyhow::{anyhow, Result};
|
||||
use ipnetwork::{Ipv4Network, Ipv6Network};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db::ip::{IpReservation, IpReservationStore};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct IpAssignmentState {
|
||||
pub ipv4: HashMap<Ipv4Addr, IpReservation>,
|
||||
pub ipv6: HashMap<Ipv6Addr, IpReservation>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IpAssignment {
|
||||
ipv4_network: Ipv4Network,
|
||||
ipv6_network: Ipv6Network,
|
||||
gateway_ipv4: Ipv4Addr,
|
||||
gateway_ipv6: Ipv6Addr,
|
||||
gateway_mac: MacAddr6,
|
||||
store: IpReservationStore,
|
||||
state: Arc<RwLock<IpAssignmentState>>,
|
||||
}
|
||||
|
||||
impl IpAssignment {
|
||||
pub async fn new(
|
||||
host_uuid: Uuid,
|
||||
ipv4_network: Ipv4Network,
|
||||
ipv6_network: Ipv6Network,
|
||||
store: IpReservationStore,
|
||||
) -> Result<Self> {
|
||||
let mut state = IpAssignment::fetch_current_state(&store).await?;
|
||||
let gateway_reservation = if let Some(reservation) = store.read(Uuid::nil()).await? {
|
||||
reservation
|
||||
} else {
|
||||
IpAssignment::allocate(
|
||||
&mut state,
|
||||
&store,
|
||||
Uuid::nil(),
|
||||
ipv4_network,
|
||||
ipv6_network,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
if store.read(host_uuid).await?.is_none() {
|
||||
let _ = IpAssignment::allocate(
|
||||
&mut state,
|
||||
&store,
|
||||
host_uuid,
|
||||
ipv4_network,
|
||||
ipv6_network,
|
||||
Some(gateway_reservation.gateway_ipv4),
|
||||
Some(gateway_reservation.gateway_ipv6),
|
||||
Some(gateway_reservation.gateway_mac),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let assignment = IpAssignment {
|
||||
ipv4_network,
|
||||
ipv6_network,
|
||||
gateway_ipv4: gateway_reservation.ipv4,
|
||||
gateway_ipv6: gateway_reservation.ipv6,
|
||||
gateway_mac: gateway_reservation.mac,
|
||||
store,
|
||||
state: Arc::new(RwLock::new(state)),
|
||||
};
|
||||
Ok(assignment)
|
||||
}
|
||||
|
||||
async fn fetch_current_state(store: &IpReservationStore) -> Result<IpAssignmentState> {
|
||||
let reservations = store.list().await?;
|
||||
let mut state = IpAssignmentState::default();
|
||||
for reservation in reservations.values() {
|
||||
state.ipv4.insert(reservation.ipv4, reservation.clone());
|
||||
state.ipv6.insert(reservation.ipv6, reservation.clone());
|
||||
}
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn allocate(
|
||||
state: &mut IpAssignmentState,
|
||||
store: &IpReservationStore,
|
||||
uuid: Uuid,
|
||||
ipv4_network: Ipv4Network,
|
||||
ipv6_network: Ipv6Network,
|
||||
gateway_ipv4: Option<Ipv4Addr>,
|
||||
gateway_ipv6: Option<Ipv6Addr>,
|
||||
gateway_mac: Option<MacAddr6>,
|
||||
) -> Result<IpReservation> {
|
||||
let found_ipv4: Option<Ipv4Addr> = ipv4_network
|
||||
.iter()
|
||||
.filter(|ip| {
|
||||
ip.is_private() && !(ip.is_loopback() || ip.is_multicast() || ip.is_broadcast())
|
||||
})
|
||||
.filter(|ip| {
|
||||
let last = ip.octets()[3];
|
||||
// filter for IPs ending in .1 to .250 because .250+ can have special meaning
|
||||
(1..250).contains(&last)
|
||||
})
|
||||
.find(|ip| !state.ipv4.contains_key(ip));
|
||||
|
||||
let found_ipv6: Option<Ipv6Addr> = ipv6_network
|
||||
.iter()
|
||||
.filter(|ip| !ip.is_loopback() && !ip.is_multicast())
|
||||
.filter(|ip| {
|
||||
let last = ip.octets()[15];
|
||||
last > 0
|
||||
})
|
||||
.find(|ip| !state.ipv6.contains_key(ip));
|
||||
|
||||
let Some(ipv4) = found_ipv4 else {
|
||||
return Err(anyhow!(
|
||||
"unable to allocate ipv4 address, assigned network is exhausted"
|
||||
));
|
||||
};
|
||||
|
||||
let Some(ipv6) = found_ipv6 else {
|
||||
return Err(anyhow!(
|
||||
"unable to allocate ipv6 address, assigned network is exhausted"
|
||||
));
|
||||
};
|
||||
|
||||
let mut mac = MacAddr6::random();
|
||||
mac.set_local(true);
|
||||
mac.set_multicast(false);
|
||||
|
||||
let reservation = IpReservation {
|
||||
uuid: uuid.to_string(),
|
||||
ipv4,
|
||||
ipv6,
|
||||
mac,
|
||||
ipv4_prefix: ipv4_network.prefix(),
|
||||
ipv6_prefix: ipv6_network.prefix(),
|
||||
gateway_ipv4: gateway_ipv4.unwrap_or(ipv4),
|
||||
gateway_ipv6: gateway_ipv6.unwrap_or(ipv6),
|
||||
gateway_mac: gateway_mac.unwrap_or(mac),
|
||||
};
|
||||
state.ipv4.insert(ipv4, reservation.clone());
|
||||
state.ipv6.insert(ipv6, reservation.clone());
|
||||
store.update(uuid, reservation.clone()).await?;
|
||||
Ok(reservation)
|
||||
}
|
||||
|
||||
pub async fn assign(&self, uuid: Uuid) -> Result<IpReservation> {
|
||||
let mut state = self.state.write().await;
|
||||
let reservation = IpAssignment::allocate(
|
||||
&mut state,
|
||||
&self.store,
|
||||
uuid,
|
||||
self.ipv4_network,
|
||||
self.ipv6_network,
|
||||
Some(self.gateway_ipv4),
|
||||
Some(self.gateway_ipv6),
|
||||
Some(self.gateway_mac),
|
||||
)
|
||||
.await?;
|
||||
Ok(reservation)
|
||||
}
|
||||
|
||||
pub async fn recall(&self, uuid: Uuid) -> Result<()> {
|
||||
let mut state = self.state.write().await;
|
||||
self.store.remove(uuid).await?;
|
||||
state
|
||||
.ipv4
|
||||
.retain(|_, reservation| reservation.uuid != uuid.to_string());
|
||||
state
|
||||
.ipv6
|
||||
.retain(|_, reservation| reservation.uuid != uuid.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn retrieve(&self, uuid: Uuid) -> Result<Option<IpReservation>> {
|
||||
self.store.read(uuid).await
|
||||
}
|
||||
|
||||
pub async fn reload(&self) -> Result<()> {
|
||||
let mut state = self.state.write().await;
|
||||
let intermediate = IpAssignment::fetch_current_state(&self.store).await?;
|
||||
*state = intermediate;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read(&self) -> Result<IpAssignmentState> {
|
||||
Ok(self.state.read().await.clone())
|
||||
}
|
||||
}
|
1
crates/daemon/src/ip/mod.rs
Normal file
1
crates/daemon/src/ip/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod assignment;
|
@ -1,19 +1,23 @@
|
||||
use std::{net::SocketAddr, path::PathBuf, str::FromStr, sync::Arc};
|
||||
|
||||
use crate::db::ip::IpReservationStore;
|
||||
use crate::db::zone::ZoneStore;
|
||||
use crate::db::KrataDatabase;
|
||||
use crate::ip::assignment::IpAssignment;
|
||||
use anyhow::{anyhow, Result};
|
||||
use config::DaemonConfig;
|
||||
use console::{DaemonConsole, DaemonConsoleHandle};
|
||||
use control::DaemonControlService;
|
||||
use db::GuestStore;
|
||||
use devices::DaemonDeviceManager;
|
||||
use event::{DaemonEventContext, DaemonEventGenerator};
|
||||
use glt::GuestLookupTable;
|
||||
use idm::{DaemonIdm, DaemonIdmHandle};
|
||||
use ipnetwork::{Ipv4Network, Ipv6Network};
|
||||
use krata::{dial::ControlDialAddress, v1::control::control_service_server::ControlServiceServer};
|
||||
use krataoci::{packer::service::OciPackerService, registry::OciPlatform};
|
||||
use kratart::Runtime;
|
||||
use log::info;
|
||||
use reconcile::guest::GuestReconciler;
|
||||
use log::{debug, info};
|
||||
use reconcile::zone::ZoneReconciler;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use std::{net::SocketAddr, path::PathBuf, str::FromStr, sync::Arc};
|
||||
use tokio::{
|
||||
fs,
|
||||
net::UnixListener,
|
||||
@ -23,6 +27,7 @@ use tokio::{
|
||||
use tokio_stream::wrappers::UnixListenerStream;
|
||||
use tonic::transport::{Identity, Server, ServerTlsConfig};
|
||||
use uuid::Uuid;
|
||||
use zlt::ZoneLookupTable;
|
||||
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
@ -31,21 +36,22 @@ pub mod control;
|
||||
pub mod db;
|
||||
pub mod devices;
|
||||
pub mod event;
|
||||
pub mod glt;
|
||||
pub mod idm;
|
||||
pub mod ip;
|
||||
pub mod metrics;
|
||||
pub mod oci;
|
||||
pub mod reconcile;
|
||||
|
||||
pub mod zlt;
|
||||
pub struct Daemon {
|
||||
store: String,
|
||||
_config: Arc<DaemonConfig>,
|
||||
glt: GuestLookupTable,
|
||||
zlt: ZoneLookupTable,
|
||||
devices: DaemonDeviceManager,
|
||||
guests: GuestStore,
|
||||
zones: ZoneStore,
|
||||
ip: IpAssignment,
|
||||
events: DaemonEventContext,
|
||||
guest_reconciler_task: JoinHandle<()>,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
zone_reconciler_task: JoinHandle<()>,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
generator_task: JoinHandle<()>,
|
||||
idm: DaemonIdmHandle,
|
||||
console: DaemonConsoleHandle,
|
||||
@ -53,23 +59,27 @@ pub struct Daemon {
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
const GUEST_RECONCILER_QUEUE_LEN: usize = 1000;
|
||||
const ZONE_RECONCILER_QUEUE_LEN: usize = 1000;
|
||||
|
||||
impl Daemon {
|
||||
pub async fn new(store: String) -> Result<Self> {
|
||||
let store_dir = PathBuf::from(store.clone());
|
||||
debug!("loading configuration");
|
||||
let mut config_path = store_dir.clone();
|
||||
config_path.push("config.toml");
|
||||
|
||||
let config = DaemonConfig::load(&config_path).await?;
|
||||
let config = Arc::new(config);
|
||||
debug!("initializing device manager");
|
||||
let devices = DaemonDeviceManager::new(config.clone());
|
||||
|
||||
debug!("validating image cache directory");
|
||||
let mut image_cache_dir = store_dir.clone();
|
||||
image_cache_dir.push("cache");
|
||||
image_cache_dir.push("image");
|
||||
fs::create_dir_all(&image_cache_dir).await?;
|
||||
|
||||
debug!("loading zone0 uuid");
|
||||
let mut host_uuid_path = store_dir.clone();
|
||||
host_uuid_path.push("host.uuid");
|
||||
let host_uuid = if host_uuid_path.is_file() {
|
||||
@ -89,60 +99,76 @@ impl Daemon {
|
||||
generated
|
||||
};
|
||||
|
||||
let initrd_path = detect_guest_path(&store, "initrd")?;
|
||||
let kernel_path = detect_guest_path(&store, "kernel")?;
|
||||
let addons_path = detect_guest_path(&store, "addons.squashfs")?;
|
||||
debug!("validating zone asset directories");
|
||||
let initrd_path = detect_zone_path(&store, "initrd")?;
|
||||
let kernel_path = detect_zone_path(&store, "kernel")?;
|
||||
let addons_path = detect_zone_path(&store, "addons.squashfs")?;
|
||||
|
||||
debug!("initializing caches and hydrating zone state");
|
||||
let seed = config.oci.seed.clone().map(PathBuf::from);
|
||||
let packer = OciPackerService::new(seed, &image_cache_dir, OciPlatform::current()).await?;
|
||||
let runtime = Runtime::new(host_uuid).await?;
|
||||
let glt = GuestLookupTable::new(0, host_uuid);
|
||||
let guests_db_path = format!("{}/guests.db", store);
|
||||
let guests = GuestStore::open(&PathBuf::from(guests_db_path))?;
|
||||
let (guest_reconciler_notify, guest_reconciler_receiver) =
|
||||
channel::<Uuid>(GUEST_RECONCILER_QUEUE_LEN);
|
||||
let idm = DaemonIdm::new(glt.clone()).await?;
|
||||
debug!("initializing core runtime");
|
||||
let runtime = Runtime::new().await?;
|
||||
let zlt = ZoneLookupTable::new(0, host_uuid);
|
||||
let db_path = format!("{}/krata.db", store);
|
||||
let database = KrataDatabase::open(Path::new(&db_path))?;
|
||||
let zones = ZoneStore::open(database.clone())?;
|
||||
let (zone_reconciler_notify, zone_reconciler_receiver) =
|
||||
channel::<Uuid>(ZONE_RECONCILER_QUEUE_LEN);
|
||||
debug!("starting IDM service");
|
||||
let idm = DaemonIdm::new(zlt.clone()).await?;
|
||||
let idm = idm.launch().await?;
|
||||
let console = DaemonConsole::new(glt.clone()).await?;
|
||||
debug!("initializing console interfaces");
|
||||
let console = DaemonConsole::new(zlt.clone()).await?;
|
||||
let console = console.launch().await?;
|
||||
let (events, generator) =
|
||||
DaemonEventGenerator::new(guests.clone(), guest_reconciler_notify.clone(), idm.clone())
|
||||
DaemonEventGenerator::new(zones.clone(), zone_reconciler_notify.clone(), idm.clone())
|
||||
.await?;
|
||||
let runtime_for_reconciler = runtime.dupe().await?;
|
||||
let guest_reconciler = GuestReconciler::new(
|
||||
let ipv4_network = Ipv4Network::from_str(&config.network.ipv4.subnet)?;
|
||||
let ipv6_network = Ipv6Network::from_str(&config.network.ipv6.subnet)?;
|
||||
let ip_reservation_store = IpReservationStore::open(database)?;
|
||||
let ip =
|
||||
IpAssignment::new(host_uuid, ipv4_network, ipv6_network, ip_reservation_store).await?;
|
||||
debug!("initializing zone reconciler");
|
||||
let zone_reconciler = ZoneReconciler::new(
|
||||
devices.clone(),
|
||||
glt.clone(),
|
||||
guests.clone(),
|
||||
zlt.clone(),
|
||||
zones.clone(),
|
||||
events.clone(),
|
||||
runtime_for_reconciler,
|
||||
packer.clone(),
|
||||
guest_reconciler_notify.clone(),
|
||||
zone_reconciler_notify.clone(),
|
||||
kernel_path,
|
||||
initrd_path,
|
||||
addons_path,
|
||||
ip.clone(),
|
||||
config.clone(),
|
||||
)?;
|
||||
|
||||
let guest_reconciler_task = guest_reconciler.launch(guest_reconciler_receiver).await?;
|
||||
let zone_reconciler_task = zone_reconciler.launch(zone_reconciler_receiver).await?;
|
||||
let generator_task = generator.launch().await?;
|
||||
|
||||
// TODO: Create a way of abstracting early init tasks in kratad.
|
||||
// TODO: Make initial power management policy configurable.
|
||||
// FIXME: Power management hypercalls fail when running as an L1 hypervisor.
|
||||
// let power = runtime.power_management_context().await?;
|
||||
// power.set_smt_policy(true).await?;
|
||||
// power
|
||||
// .set_scheduler_policy("performance".to_string())
|
||||
// .await?;
|
||||
let power = runtime.power_management_context().await?;
|
||||
power.set_smt_policy(true).await?;
|
||||
power
|
||||
.set_scheduler_policy("performance".to_string())
|
||||
.await?;
|
||||
info!("power management initialized");
|
||||
|
||||
info!("krata daemon initialized");
|
||||
Ok(Self {
|
||||
store,
|
||||
_config: config,
|
||||
glt,
|
||||
zlt,
|
||||
devices,
|
||||
guests,
|
||||
zones,
|
||||
ip,
|
||||
events,
|
||||
guest_reconciler_task,
|
||||
guest_reconciler_notify,
|
||||
zone_reconciler_task,
|
||||
zone_reconciler_notify,
|
||||
generator_task,
|
||||
idm,
|
||||
console,
|
||||
@ -152,14 +178,16 @@ impl Daemon {
|
||||
}
|
||||
|
||||
pub async fn listen(&mut self, addr: ControlDialAddress) -> Result<()> {
|
||||
debug!("starting control service");
|
||||
let control_service = DaemonControlService::new(
|
||||
self.glt.clone(),
|
||||
self.zlt.clone(),
|
||||
self.devices.clone(),
|
||||
self.events.clone(),
|
||||
self.console.clone(),
|
||||
self.idm.clone(),
|
||||
self.guests.clone(),
|
||||
self.guest_reconciler_notify.clone(),
|
||||
self.zones.clone(),
|
||||
self.ip.clone(),
|
||||
self.zone_reconciler_notify.clone(),
|
||||
self.packer.clone(),
|
||||
self.runtime.clone(),
|
||||
);
|
||||
@ -181,6 +209,8 @@ impl Daemon {
|
||||
server = server.tls_config(tls_config)?;
|
||||
}
|
||||
|
||||
server = server.http2_keepalive_interval(Some(Duration::from_secs(10)));
|
||||
|
||||
let server = server.add_service(ControlServiceServer::new(control_service));
|
||||
info!("listening on address {}", addr);
|
||||
match addr {
|
||||
@ -214,20 +244,20 @@ impl Daemon {
|
||||
|
||||
impl Drop for Daemon {
|
||||
fn drop(&mut self) {
|
||||
self.guest_reconciler_task.abort();
|
||||
self.zone_reconciler_task.abort();
|
||||
self.generator_task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_guest_path(store: &str, name: &str) -> Result<PathBuf> {
|
||||
let mut path = PathBuf::from(format!("{}/guest/{}", store, name));
|
||||
fn detect_zone_path(store: &str, name: &str) -> Result<PathBuf> {
|
||||
let mut path = PathBuf::from(format!("{}/zone/{}", store, name));
|
||||
if path.is_file() {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
path = PathBuf::from(format!("/usr/share/krata/guest/{}", name));
|
||||
path = PathBuf::from(format!("/usr/share/krata/zone/{}", name));
|
||||
if path.is_file() {
|
||||
return Ok(path);
|
||||
}
|
||||
Err(anyhow!("unable to find required guest file: {}", name))
|
||||
Err(anyhow!("unable to find required zone file: {}", name))
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
use krata::{
|
||||
idm::internal::{MetricFormat, MetricNode},
|
||||
v1::common::{GuestMetricFormat, GuestMetricNode},
|
||||
v1::common::{ZoneMetricFormat, ZoneMetricNode},
|
||||
};
|
||||
|
||||
fn idm_metric_format_to_api(format: MetricFormat) -> GuestMetricFormat {
|
||||
fn idm_metric_format_to_api(format: MetricFormat) -> ZoneMetricFormat {
|
||||
match format {
|
||||
MetricFormat::Unknown => GuestMetricFormat::Unknown,
|
||||
MetricFormat::Bytes => GuestMetricFormat::Bytes,
|
||||
MetricFormat::Integer => GuestMetricFormat::Integer,
|
||||
MetricFormat::DurationSeconds => GuestMetricFormat::DurationSeconds,
|
||||
MetricFormat::Unknown => ZoneMetricFormat::Unknown,
|
||||
MetricFormat::Bytes => ZoneMetricFormat::Bytes,
|
||||
MetricFormat::Integer => ZoneMetricFormat::Integer,
|
||||
MetricFormat::DurationSeconds => ZoneMetricFormat::DurationSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn idm_metric_to_api(node: MetricNode) -> GuestMetricNode {
|
||||
pub fn idm_metric_to_api(node: MetricNode) -> ZoneMetricNode {
|
||||
let format = node.format();
|
||||
GuestMetricNode {
|
||||
ZoneMetricNode {
|
||||
name: node.name,
|
||||
value: node.value,
|
||||
format: idm_metric_format_to_api(format).into(),
|
||||
|
@ -1,374 +0,0 @@
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use krata::v1::{
|
||||
common::{Guest, GuestErrorInfo, GuestExitInfo, GuestNetworkState, GuestState, GuestStatus},
|
||||
control::GuestChangedEvent,
|
||||
};
|
||||
use krataoci::packer::service::OciPackerService;
|
||||
use kratart::{GuestInfo, Runtime};
|
||||
use log::{error, info, trace, warn};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Mutex, RwLock,
|
||||
},
|
||||
task::JoinHandle,
|
||||
time::sleep,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
db::GuestStore,
|
||||
devices::DaemonDeviceManager,
|
||||
event::{DaemonEvent, DaemonEventContext},
|
||||
glt::GuestLookupTable,
|
||||
};
|
||||
|
||||
use self::start::GuestStarter;
|
||||
|
||||
mod start;
|
||||
|
||||
const PARALLEL_LIMIT: u32 = 5;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum GuestReconcilerResult {
|
||||
Unchanged,
|
||||
Changed { rerun: bool },
|
||||
}
|
||||
|
||||
struct GuestReconcilerEntry {
|
||||
task: JoinHandle<()>,
|
||||
sender: Sender<()>,
|
||||
}
|
||||
|
||||
impl Drop for GuestReconcilerEntry {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GuestReconciler {
|
||||
devices: DaemonDeviceManager,
|
||||
glt: GuestLookupTable,
|
||||
guests: GuestStore,
|
||||
events: DaemonEventContext,
|
||||
runtime: Runtime,
|
||||
packer: OciPackerService,
|
||||
kernel_path: PathBuf,
|
||||
initrd_path: PathBuf,
|
||||
addons_path: PathBuf,
|
||||
tasks: Arc<Mutex<HashMap<Uuid, GuestReconcilerEntry>>>,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
reconcile_lock: Arc<RwLock<()>>,
|
||||
}
|
||||
|
||||
impl GuestReconciler {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
devices: DaemonDeviceManager,
|
||||
glt: GuestLookupTable,
|
||||
guests: GuestStore,
|
||||
events: DaemonEventContext,
|
||||
runtime: Runtime,
|
||||
packer: OciPackerService,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
kernel_path: PathBuf,
|
||||
initrd_path: PathBuf,
|
||||
modules_path: PathBuf,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
devices,
|
||||
glt,
|
||||
guests,
|
||||
events,
|
||||
runtime,
|
||||
packer,
|
||||
kernel_path,
|
||||
initrd_path,
|
||||
addons_path: modules_path,
|
||||
tasks: Arc::new(Mutex::new(HashMap::new())),
|
||||
guest_reconciler_notify,
|
||||
reconcile_lock: Arc::new(RwLock::with_max_readers((), PARALLEL_LIMIT)),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn launch(self, mut notify: Receiver<Uuid>) -> Result<JoinHandle<()>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
if let Err(error) = self.reconcile_runtime(true).await {
|
||||
error!("runtime reconciler failed: {}", error);
|
||||
}
|
||||
|
||||
loop {
|
||||
select! {
|
||||
x = notify.recv() => match x {
|
||||
None => {
|
||||
break;
|
||||
},
|
||||
|
||||
Some(uuid) => {
|
||||
if let Err(error) = self.launch_task_if_needed(uuid).await {
|
||||
error!("failed to start guest reconciler task {}: {}", uuid, error);
|
||||
}
|
||||
|
||||
let map = self.tasks.lock().await;
|
||||
if let Some(entry) = map.get(&uuid) {
|
||||
if let Err(error) = entry.sender.send(()).await {
|
||||
error!("failed to notify guest reconciler task {}: {}", uuid, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_ = sleep(Duration::from_secs(15)) => {
|
||||
if let Err(error) = self.reconcile_runtime(false).await {
|
||||
error!("runtime reconciler failed: {}", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn reconcile_runtime(&self, initial: bool) -> Result<()> {
|
||||
let _permit = self.reconcile_lock.write().await;
|
||||
trace!("reconciling runtime");
|
||||
let runtime_guests = self.runtime.list().await?;
|
||||
let stored_guests = self.guests.list().await?;
|
||||
|
||||
let non_existent_guests = runtime_guests
|
||||
.iter()
|
||||
.filter(|x| !stored_guests.iter().any(|g| *g.0 == x.uuid))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for guest in non_existent_guests {
|
||||
warn!("destroying unknown runtime guest {}", guest.uuid);
|
||||
if let Err(error) = self.runtime.destroy(guest.uuid).await {
|
||||
error!(
|
||||
"failed to destroy unknown runtime guest {}: {}",
|
||||
guest.uuid, error
|
||||
);
|
||||
}
|
||||
self.guests.remove(guest.uuid).await?;
|
||||
}
|
||||
|
||||
let mut device_claims = HashMap::new();
|
||||
|
||||
for (uuid, mut stored_guest) in stored_guests {
|
||||
let previous_guest = stored_guest.clone();
|
||||
let runtime_guest = runtime_guests.iter().find(|x| x.uuid == uuid);
|
||||
match runtime_guest {
|
||||
None => {
|
||||
let mut state = stored_guest.state.as_mut().cloned().unwrap_or_default();
|
||||
if state.status() == GuestStatus::Started {
|
||||
state.status = GuestStatus::Starting.into();
|
||||
}
|
||||
stored_guest.state = Some(state);
|
||||
}
|
||||
|
||||
Some(runtime) => {
|
||||
self.glt.associate(uuid, runtime.domid).await;
|
||||
let mut state = stored_guest.state.as_mut().cloned().unwrap_or_default();
|
||||
if let Some(code) = runtime.state.exit_code {
|
||||
state.status = GuestStatus::Exited.into();
|
||||
state.exit_info = Some(GuestExitInfo { code });
|
||||
} else {
|
||||
state.status = GuestStatus::Started.into();
|
||||
}
|
||||
|
||||
for device in &stored_guest
|
||||
.spec
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.devices
|
||||
{
|
||||
device_claims.insert(device.name.clone(), uuid);
|
||||
}
|
||||
|
||||
state.network = Some(guestinfo_to_networkstate(runtime));
|
||||
stored_guest.state = Some(state);
|
||||
}
|
||||
}
|
||||
|
||||
let changed = stored_guest != previous_guest;
|
||||
|
||||
if changed || initial {
|
||||
self.guests.update(uuid, stored_guest).await?;
|
||||
let _ = self.guest_reconciler_notify.try_send(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
self.devices.update_claims(device_claims).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reconcile(&self, uuid: Uuid) -> Result<bool> {
|
||||
let _runtime_reconcile_permit = self.reconcile_lock.read().await;
|
||||
let Some(mut guest) = self.guests.read(uuid).await? else {
|
||||
warn!(
|
||||
"notified of reconcile for guest {} but it didn't exist",
|
||||
uuid
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
info!("reconciling guest {}", uuid);
|
||||
|
||||
self.events
|
||||
.send(DaemonEvent::GuestChanged(GuestChangedEvent {
|
||||
guest: Some(guest.clone()),
|
||||
}))?;
|
||||
|
||||
let start_status = guest.state.as_ref().map(|x| x.status()).unwrap_or_default();
|
||||
let result = match start_status {
|
||||
GuestStatus::Starting => self.start(uuid, &mut guest).await,
|
||||
GuestStatus::Exited => self.exited(&mut guest).await,
|
||||
GuestStatus::Destroying => self.destroy(uuid, &mut guest).await,
|
||||
_ => Ok(GuestReconcilerResult::Unchanged),
|
||||
};
|
||||
|
||||
let result = match result {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
guest.state = Some(guest.state.as_mut().cloned().unwrap_or_default());
|
||||
guest.state.as_mut().unwrap().status = GuestStatus::Failed.into();
|
||||
guest.state.as_mut().unwrap().error_info = Some(GuestErrorInfo {
|
||||
message: error.to_string(),
|
||||
});
|
||||
warn!("failed to start guest {}: {}", guest.id, error);
|
||||
GuestReconcilerResult::Changed { rerun: false }
|
||||
}
|
||||
};
|
||||
|
||||
info!("reconciled guest {}", uuid);
|
||||
|
||||
let status = guest.state.as_ref().map(|x| x.status()).unwrap_or_default();
|
||||
let destroyed = status == GuestStatus::Destroyed;
|
||||
|
||||
let rerun = if let GuestReconcilerResult::Changed { rerun } = result {
|
||||
let event = DaemonEvent::GuestChanged(GuestChangedEvent {
|
||||
guest: Some(guest.clone()),
|
||||
});
|
||||
|
||||
if destroyed {
|
||||
self.guests.remove(uuid).await?;
|
||||
let mut map = self.tasks.lock().await;
|
||||
map.remove(&uuid);
|
||||
} else {
|
||||
self.guests.update(uuid, guest.clone()).await?;
|
||||
}
|
||||
|
||||
self.events.send(event)?;
|
||||
rerun
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(rerun)
|
||||
}
|
||||
|
||||
async fn start(&self, uuid: Uuid, guest: &mut Guest) -> Result<GuestReconcilerResult> {
|
||||
let starter = GuestStarter {
|
||||
devices: &self.devices,
|
||||
kernel_path: &self.kernel_path,
|
||||
initrd_path: &self.initrd_path,
|
||||
addons_path: &self.addons_path,
|
||||
packer: &self.packer,
|
||||
glt: &self.glt,
|
||||
runtime: &self.runtime,
|
||||
};
|
||||
starter.start(uuid, guest).await
|
||||
}
|
||||
|
||||
async fn exited(&self, guest: &mut Guest) -> Result<GuestReconcilerResult> {
|
||||
if let Some(ref mut state) = guest.state {
|
||||
state.set_status(GuestStatus::Destroying);
|
||||
Ok(GuestReconcilerResult::Changed { rerun: true })
|
||||
} else {
|
||||
Ok(GuestReconcilerResult::Unchanged)
|
||||
}
|
||||
}
|
||||
|
||||
async fn destroy(&self, uuid: Uuid, guest: &mut Guest) -> Result<GuestReconcilerResult> {
|
||||
if let Err(error) = self.runtime.destroy(uuid).await {
|
||||
trace!("failed to destroy runtime guest {}: {}", uuid, error);
|
||||
}
|
||||
|
||||
let domid = guest.state.as_ref().map(|x| x.domid);
|
||||
|
||||
if let Some(domid) = domid {
|
||||
self.glt.remove(uuid, domid).await;
|
||||
}
|
||||
|
||||
info!("destroyed guest {}", uuid);
|
||||
guest.state = Some(GuestState {
|
||||
status: GuestStatus::Destroyed.into(),
|
||||
network: None,
|
||||
exit_info: None,
|
||||
error_info: None,
|
||||
host: self.glt.host_uuid().to_string(),
|
||||
domid: domid.unwrap_or(u32::MAX),
|
||||
});
|
||||
self.devices.release_all(uuid).await?;
|
||||
Ok(GuestReconcilerResult::Changed { rerun: false })
|
||||
}
|
||||
|
||||
async fn launch_task_if_needed(&self, uuid: Uuid) -> Result<()> {
|
||||
let mut map = self.tasks.lock().await;
|
||||
match map.entry(uuid) {
|
||||
Entry::Occupied(_) => {}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(self.launch_task(uuid).await?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn launch_task(&self, uuid: Uuid) -> Result<GuestReconcilerEntry> {
|
||||
let this = self.clone();
|
||||
let (sender, mut receiver) = channel(10);
|
||||
let task = tokio::task::spawn(async move {
|
||||
'notify_loop: loop {
|
||||
if receiver.recv().await.is_none() {
|
||||
break 'notify_loop;
|
||||
}
|
||||
|
||||
'rerun_loop: loop {
|
||||
let rerun = match this.reconcile(uuid).await {
|
||||
Ok(rerun) => rerun,
|
||||
Err(error) => {
|
||||
error!("failed to reconcile guest {}: {}", uuid, error);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if rerun {
|
||||
continue 'rerun_loop;
|
||||
}
|
||||
break 'rerun_loop;
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(GuestReconcilerEntry { task, sender })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn guestinfo_to_networkstate(info: &GuestInfo) -> GuestNetworkState {
|
||||
GuestNetworkState {
|
||||
guest_ipv4: info.guest_ipv4.map(|x| x.to_string()).unwrap_or_default(),
|
||||
guest_ipv6: info.guest_ipv6.map(|x| x.to_string()).unwrap_or_default(),
|
||||
guest_mac: info.guest_mac.as_ref().cloned().unwrap_or_default(),
|
||||
gateway_ipv4: info.gateway_ipv4.map(|x| x.to_string()).unwrap_or_default(),
|
||||
gateway_ipv6: info.gateway_ipv6.map(|x| x.to_string()).unwrap_or_default(),
|
||||
gateway_mac: info.gateway_mac.as_ref().cloned().unwrap_or_default(),
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
pub mod guest;
|
||||
pub mod zone;
|
||||
|
@ -1,45 +1,45 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::StreamExt;
|
||||
use krata::launchcfg::LaunchPackedFormat;
|
||||
use krata::v1::common::{OciImageFormat, Zone, ZoneState, ZoneStatus};
|
||||
use krata::v1::common::{ZoneOciImageSpec, ZoneResourceStatus};
|
||||
use krataoci::packer::{service::OciPackerService, OciPackedFormat};
|
||||
use kratart::launch::{PciBdf, PciDevice, PciRdmReservePolicy, ZoneLaunchNetwork};
|
||||
use kratart::{launch::ZoneLaunchRequest, Runtime};
|
||||
use log::info;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::StreamExt;
|
||||
use krata::launchcfg::LaunchPackedFormat;
|
||||
use krata::v1::common::GuestOciImageSpec;
|
||||
use krata::v1::common::{guest_image_spec::Image, Guest, GuestState, GuestStatus, OciImageFormat};
|
||||
use krataoci::packer::{service::OciPackerService, OciPackedFormat};
|
||||
use kratart::launch::{PciBdf, PciDevice, PciRdmReservePolicy};
|
||||
use kratart::{launch::GuestLaunchRequest, Runtime};
|
||||
use log::info;
|
||||
|
||||
use crate::config::{DaemonConfig, DaemonPciDeviceRdmReservePolicy};
|
||||
use crate::devices::DaemonDeviceManager;
|
||||
use crate::ip::assignment::IpAssignment;
|
||||
use crate::reconcile::zone::ip_reservation_to_network_status;
|
||||
use crate::{reconcile::zone::ZoneReconcilerResult, zlt::ZoneLookupTable};
|
||||
use krata::v1::common::zone_image_spec::Image;
|
||||
use tokio::fs::{self, File};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio_tar::Archive;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config::DaemonPciDeviceRdmReservePolicy;
|
||||
use crate::devices::DaemonDeviceManager;
|
||||
use crate::{
|
||||
glt::GuestLookupTable,
|
||||
reconcile::guest::{guestinfo_to_networkstate, GuestReconcilerResult},
|
||||
};
|
||||
|
||||
pub struct GuestStarter<'a> {
|
||||
pub struct ZoneCreator<'a> {
|
||||
pub devices: &'a DaemonDeviceManager,
|
||||
pub kernel_path: &'a Path,
|
||||
pub initrd_path: &'a Path,
|
||||
pub addons_path: &'a Path,
|
||||
pub packer: &'a OciPackerService,
|
||||
pub glt: &'a GuestLookupTable,
|
||||
pub ip_assignment: &'a IpAssignment,
|
||||
pub zlt: &'a ZoneLookupTable,
|
||||
pub runtime: &'a Runtime,
|
||||
pub config: &'a DaemonConfig,
|
||||
}
|
||||
|
||||
impl GuestStarter<'_> {
|
||||
impl ZoneCreator<'_> {
|
||||
pub async fn oci_spec_tar_read_file(
|
||||
&self,
|
||||
file: &Path,
|
||||
oci: &GuestOciImageSpec,
|
||||
oci: &ZoneOciImageSpec,
|
||||
) -> Result<Vec<u8>> {
|
||||
if oci.format() != OciImageFormat::Tar {
|
||||
return Err(anyhow!(
|
||||
@ -75,9 +75,9 @@ impl GuestStarter<'_> {
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn start(&self, uuid: Uuid, guest: &mut Guest) -> Result<GuestReconcilerResult> {
|
||||
let Some(ref spec) = guest.spec else {
|
||||
return Err(anyhow!("guest spec not specified"));
|
||||
pub async fn create(&self, uuid: Uuid, zone: &mut Zone) -> Result<ZoneReconcilerResult> {
|
||||
let Some(ref mut spec) = zone.spec else {
|
||||
return Err(anyhow!("zone spec not specified"));
|
||||
};
|
||||
|
||||
let Some(ref image) = spec.image else {
|
||||
@ -100,7 +100,7 @@ impl GuestStarter<'_> {
|
||||
OciImageFormat::Squashfs => OciPackedFormat::Squashfs,
|
||||
OciImageFormat::Erofs => OciPackedFormat::Erofs,
|
||||
OciImageFormat::Tar => {
|
||||
return Err(anyhow!("tar image format is not supported for guests"));
|
||||
return Err(anyhow!("tar image format is not supported for zones"));
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -174,10 +174,29 @@ impl GuestStarter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
let reservation = self.ip_assignment.assign(uuid).await?;
|
||||
|
||||
let mut initial_resources = spec.initial_resources.unwrap_or_default();
|
||||
if initial_resources.target_cpus < 1 {
|
||||
initial_resources.target_cpus = 1;
|
||||
}
|
||||
if initial_resources.target_cpus > initial_resources.max_cpus {
|
||||
initial_resources.max_cpus = initial_resources.target_cpus;
|
||||
}
|
||||
spec.initial_resources = Some(initial_resources);
|
||||
let kernel_options = spec.kernel_options.clone().unwrap_or_default();
|
||||
let info = self
|
||||
.runtime
|
||||
.launch(GuestLaunchRequest {
|
||||
format: LaunchPackedFormat::Squashfs,
|
||||
.launch(ZoneLaunchRequest {
|
||||
format: match image.format {
|
||||
OciPackedFormat::Squashfs => LaunchPackedFormat::Squashfs,
|
||||
OciPackedFormat::Erofs => LaunchPackedFormat::Erofs,
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"oci image is in an invalid format, which isn't compatible with launch"
|
||||
));
|
||||
}
|
||||
},
|
||||
uuid: Some(uuid),
|
||||
name: if spec.name.is_empty() {
|
||||
None
|
||||
@ -187,8 +206,10 @@ impl GuestStarter<'_> {
|
||||
image,
|
||||
kernel,
|
||||
initrd,
|
||||
vcpus: spec.vcpus,
|
||||
mem: spec.mem,
|
||||
target_cpus: initial_resources.target_cpus,
|
||||
max_cpus: initial_resources.max_cpus,
|
||||
max_memory: initial_resources.max_memory,
|
||||
target_memory: initial_resources.target_memory,
|
||||
pcis,
|
||||
env: task
|
||||
.environment
|
||||
@ -196,22 +217,36 @@ impl GuestStarter<'_> {
|
||||
.map(|x| (x.key.clone(), x.value.clone()))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
run: empty_vec_optional(task.command.clone()),
|
||||
debug: false,
|
||||
kernel_verbose: kernel_options.verbose,
|
||||
kernel_cmdline_append: kernel_options.cmdline_append,
|
||||
addons_image: Some(self.addons_path.to_path_buf()),
|
||||
network: ZoneLaunchNetwork {
|
||||
ipv4: reservation.ipv4.to_string(),
|
||||
ipv4_prefix: reservation.ipv4_prefix,
|
||||
ipv6: reservation.ipv6.to_string(),
|
||||
ipv6_prefix: reservation.ipv6_prefix,
|
||||
gateway_ipv4: reservation.gateway_ipv4.to_string(),
|
||||
gateway_ipv6: reservation.gateway_ipv6.to_string(),
|
||||
zone_mac: reservation.mac,
|
||||
nameservers: self.config.network.nameservers.clone(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
self.glt.associate(uuid, info.domid).await;
|
||||
info!("started guest {}", uuid);
|
||||
guest.state = Some(GuestState {
|
||||
status: GuestStatus::Started.into(),
|
||||
network: Some(guestinfo_to_networkstate(&info)),
|
||||
exit_info: None,
|
||||
error_info: None,
|
||||
host: self.glt.host_uuid().to_string(),
|
||||
self.zlt.associate(uuid, info.domid).await;
|
||||
info!("created zone {}", uuid);
|
||||
zone.status = Some(ZoneStatus {
|
||||
state: ZoneState::Created.into(),
|
||||
network_status: Some(ip_reservation_to_network_status(&reservation)),
|
||||
exit_status: None,
|
||||
error_status: None,
|
||||
resource_status: Some(ZoneResourceStatus {
|
||||
active_resources: Some(initial_resources),
|
||||
}),
|
||||
host: self.zlt.host_uuid().to_string(),
|
||||
domid: info.domid,
|
||||
});
|
||||
success.store(true, Ordering::Release);
|
||||
Ok(GuestReconcilerResult::Changed { rerun: false })
|
||||
Ok(ZoneReconcilerResult::Changed { rerun: false })
|
||||
}
|
||||
}
|
||||
|
381
crates/daemon/src/reconcile/zone/mod.rs
Normal file
381
crates/daemon/src/reconcile/zone/mod.rs
Normal file
@ -0,0 +1,381 @@
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use self::create::ZoneCreator;
|
||||
use crate::config::DaemonConfig;
|
||||
use crate::db::ip::IpReservation;
|
||||
use crate::ip::assignment::IpAssignment;
|
||||
use crate::{
|
||||
db::zone::ZoneStore,
|
||||
devices::DaemonDeviceManager,
|
||||
event::{DaemonEvent, DaemonEventContext},
|
||||
zlt::ZoneLookupTable,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use krata::v1::{
|
||||
common::{Zone, ZoneErrorStatus, ZoneExitStatus, ZoneNetworkStatus, ZoneState, ZoneStatus},
|
||||
control::ZoneChangedEvent,
|
||||
};
|
||||
use krataoci::packer::service::OciPackerService;
|
||||
use kratart::Runtime;
|
||||
use log::{error, info, trace, warn};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
RwLock,
|
||||
},
|
||||
task::JoinHandle,
|
||||
time::sleep,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod create;
|
||||
|
||||
const PARALLEL_LIMIT: u32 = 5;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ZoneReconcilerResult {
|
||||
Unchanged,
|
||||
Changed { rerun: bool },
|
||||
}
|
||||
|
||||
struct ZoneReconcilerEntry {
|
||||
sender: Sender<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ZoneReconciler {
|
||||
devices: DaemonDeviceManager,
|
||||
zlt: ZoneLookupTable,
|
||||
zones: ZoneStore,
|
||||
events: DaemonEventContext,
|
||||
runtime: Runtime,
|
||||
packer: OciPackerService,
|
||||
kernel_path: PathBuf,
|
||||
initrd_path: PathBuf,
|
||||
addons_path: PathBuf,
|
||||
tasks: Arc<RwLock<HashMap<Uuid, ZoneReconcilerEntry>>>,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
zone_reconcile_lock: Arc<RwLock<()>>,
|
||||
ip_assignment: IpAssignment,
|
||||
config: Arc<DaemonConfig>,
|
||||
}
|
||||
|
||||
impl ZoneReconciler {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
devices: DaemonDeviceManager,
|
||||
zlt: ZoneLookupTable,
|
||||
zones: ZoneStore,
|
||||
events: DaemonEventContext,
|
||||
runtime: Runtime,
|
||||
packer: OciPackerService,
|
||||
zone_reconciler_notify: Sender<Uuid>,
|
||||
kernel_path: PathBuf,
|
||||
initrd_path: PathBuf,
|
||||
modules_path: PathBuf,
|
||||
ip_assignment: IpAssignment,
|
||||
config: Arc<DaemonConfig>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
devices,
|
||||
zlt,
|
||||
zones,
|
||||
events,
|
||||
runtime,
|
||||
packer,
|
||||
kernel_path,
|
||||
initrd_path,
|
||||
addons_path: modules_path,
|
||||
tasks: Arc::new(RwLock::new(HashMap::new())),
|
||||
zone_reconciler_notify,
|
||||
zone_reconcile_lock: Arc::new(RwLock::with_max_readers((), PARALLEL_LIMIT)),
|
||||
ip_assignment,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn launch(self, mut notify: Receiver<Uuid>) -> Result<JoinHandle<()>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
if let Err(error) = self.reconcile_runtime(true).await {
|
||||
error!("runtime reconciler failed: {}", error);
|
||||
}
|
||||
|
||||
loop {
|
||||
select! {
|
||||
x = notify.recv() => match x {
|
||||
None => {
|
||||
break;
|
||||
},
|
||||
|
||||
Some(uuid) => {
|
||||
if let Err(error) = self.launch_task_if_needed(uuid).await {
|
||||
error!("failed to start zone reconciler task {}: {}", uuid, error);
|
||||
}
|
||||
|
||||
let map = self.tasks.read().await;
|
||||
if let Some(entry) = map.get(&uuid) {
|
||||
if let Err(error) = entry.sender.send(()).await {
|
||||
error!("failed to notify zone reconciler task {}: {}", uuid, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_ = sleep(Duration::from_secs(15)) => {
|
||||
if let Err(error) = self.reconcile_runtime(false).await {
|
||||
error!("runtime reconciler failed: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn reconcile_runtime(&self, initial: bool) -> Result<()> {
|
||||
let _permit = self.zone_reconcile_lock.write().await;
|
||||
trace!("reconciling runtime");
|
||||
let runtime_zones = self.runtime.list().await?;
|
||||
let stored_zones = self.zones.list().await?;
|
||||
|
||||
let non_existent_zones = runtime_zones
|
||||
.iter()
|
||||
.filter(|x| !stored_zones.iter().any(|g| *g.0 == x.uuid))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for zone in non_existent_zones {
|
||||
warn!("destroying unknown runtime zone {}", zone.uuid);
|
||||
if let Err(error) = self.runtime.destroy(zone.uuid).await {
|
||||
error!(
|
||||
"failed to destroy unknown runtime zone {}: {}",
|
||||
zone.uuid, error
|
||||
);
|
||||
}
|
||||
self.zones.remove(zone.uuid).await?;
|
||||
}
|
||||
|
||||
let mut device_claims = HashMap::new();
|
||||
|
||||
for (uuid, mut stored_zone) in stored_zones {
|
||||
let previous_zone = stored_zone.clone();
|
||||
let runtime_zone = runtime_zones.iter().find(|x| x.uuid == uuid);
|
||||
match runtime_zone {
|
||||
None => {
|
||||
let mut status = stored_zone.status.as_mut().cloned().unwrap_or_default();
|
||||
if status.state() == ZoneState::Created {
|
||||
status.state = ZoneState::Creating.into();
|
||||
}
|
||||
stored_zone.status = Some(status);
|
||||
}
|
||||
|
||||
Some(runtime) => {
|
||||
self.zlt.associate(uuid, runtime.domid).await;
|
||||
let mut status = stored_zone.status.as_mut().cloned().unwrap_or_default();
|
||||
if let Some(code) = runtime.state.exit_code {
|
||||
status.state = ZoneState::Exited.into();
|
||||
status.exit_status = Some(ZoneExitStatus { code });
|
||||
} else {
|
||||
status.state = ZoneState::Created.into();
|
||||
}
|
||||
|
||||
for device in &stored_zone
|
||||
.spec
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.devices
|
||||
{
|
||||
device_claims.insert(device.name.clone(), uuid);
|
||||
}
|
||||
|
||||
if let Some(reservation) = self.ip_assignment.retrieve(uuid).await? {
|
||||
status.network_status =
|
||||
Some(ip_reservation_to_network_status(&reservation));
|
||||
}
|
||||
stored_zone.status = Some(status);
|
||||
}
|
||||
}
|
||||
|
||||
let changed = stored_zone != previous_zone;
|
||||
|
||||
if changed || initial {
|
||||
self.zones.update(uuid, stored_zone).await?;
|
||||
let _ = self.zone_reconciler_notify.try_send(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
self.devices.update_claims(device_claims).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reconcile(&self, uuid: Uuid) -> Result<bool> {
|
||||
let _runtime_reconcile_permit = self.zone_reconcile_lock.read().await;
|
||||
let Some(mut zone) = self.zones.read(uuid).await? else {
|
||||
warn!(
|
||||
"notified of reconcile for zone {} but it didn't exist",
|
||||
uuid
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
info!("reconciling zone {}", uuid);
|
||||
|
||||
self.events
|
||||
.send(DaemonEvent::ZoneChanged(ZoneChangedEvent {
|
||||
zone: Some(zone.clone()),
|
||||
}))?;
|
||||
|
||||
let start_state = zone.status.as_ref().map(|x| x.state()).unwrap_or_default();
|
||||
let result = match start_state {
|
||||
ZoneState::Creating => self.create(uuid, &mut zone).await,
|
||||
ZoneState::Exited => self.exited(&mut zone).await,
|
||||
ZoneState::Destroying => self.destroy(uuid, &mut zone).await,
|
||||
_ => Ok(ZoneReconcilerResult::Unchanged),
|
||||
};
|
||||
|
||||
let result = match result {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
zone.status = Some(zone.status.as_mut().cloned().unwrap_or_default());
|
||||
zone.status.as_mut().unwrap().state = ZoneState::Failed.into();
|
||||
zone.status.as_mut().unwrap().error_status = Some(ZoneErrorStatus {
|
||||
message: error.to_string(),
|
||||
});
|
||||
warn!("failed to start zone {}: {}", zone.id, error);
|
||||
ZoneReconcilerResult::Changed { rerun: false }
|
||||
}
|
||||
};
|
||||
|
||||
info!("reconciled zone {}", uuid);
|
||||
|
||||
let state = zone.status.as_ref().map(|x| x.state()).unwrap_or_default();
|
||||
let destroyed = state == ZoneState::Destroyed;
|
||||
|
||||
let rerun = if let ZoneReconcilerResult::Changed { rerun } = result {
|
||||
let event = DaemonEvent::ZoneChanged(ZoneChangedEvent {
|
||||
zone: Some(zone.clone()),
|
||||
});
|
||||
|
||||
if destroyed {
|
||||
self.zones.remove(uuid).await?;
|
||||
let mut map = self.tasks.write().await;
|
||||
map.remove(&uuid);
|
||||
} else {
|
||||
self.zones.update(uuid, zone.clone()).await?;
|
||||
}
|
||||
|
||||
self.events.send(event)?;
|
||||
rerun
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(rerun)
|
||||
}
|
||||
|
||||
async fn create(&self, uuid: Uuid, zone: &mut Zone) -> Result<ZoneReconcilerResult> {
|
||||
let starter = ZoneCreator {
|
||||
devices: &self.devices,
|
||||
kernel_path: &self.kernel_path,
|
||||
initrd_path: &self.initrd_path,
|
||||
addons_path: &self.addons_path,
|
||||
packer: &self.packer,
|
||||
ip_assignment: &self.ip_assignment,
|
||||
zlt: &self.zlt,
|
||||
runtime: &self.runtime,
|
||||
config: &self.config,
|
||||
};
|
||||
starter.create(uuid, zone).await
|
||||
}
|
||||
|
||||
async fn exited(&self, zone: &mut Zone) -> Result<ZoneReconcilerResult> {
|
||||
if let Some(ref mut status) = zone.status {
|
||||
status.set_state(ZoneState::Destroying);
|
||||
Ok(ZoneReconcilerResult::Changed { rerun: true })
|
||||
} else {
|
||||
Ok(ZoneReconcilerResult::Unchanged)
|
||||
}
|
||||
}
|
||||
|
||||
async fn destroy(&self, uuid: Uuid, zone: &mut Zone) -> Result<ZoneReconcilerResult> {
|
||||
if let Err(error) = self.runtime.destroy(uuid).await {
|
||||
trace!("failed to destroy runtime zone {}: {}", uuid, error);
|
||||
}
|
||||
|
||||
let domid = zone.status.as_ref().map(|x| x.domid);
|
||||
|
||||
if let Some(domid) = domid {
|
||||
self.zlt.remove(uuid, domid).await;
|
||||
}
|
||||
|
||||
info!("destroyed zone {}", uuid);
|
||||
self.ip_assignment.recall(uuid).await?;
|
||||
zone.status = Some(ZoneStatus {
|
||||
state: ZoneState::Destroyed.into(),
|
||||
network_status: None,
|
||||
exit_status: None,
|
||||
error_status: None,
|
||||
resource_status: None,
|
||||
host: self.zlt.host_uuid().to_string(),
|
||||
domid: domid.unwrap_or(u32::MAX),
|
||||
});
|
||||
self.devices.release_all(uuid).await?;
|
||||
Ok(ZoneReconcilerResult::Changed { rerun: false })
|
||||
}
|
||||
|
||||
async fn launch_task_if_needed(&self, uuid: Uuid) -> Result<()> {
|
||||
let mut map = self.tasks.write().await;
|
||||
match map.entry(uuid) {
|
||||
Entry::Occupied(_) => {}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(self.launch_task(uuid).await?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn launch_task(&self, uuid: Uuid) -> Result<ZoneReconcilerEntry> {
|
||||
let this = self.clone();
|
||||
let (sender, mut receiver) = channel(10);
|
||||
tokio::task::spawn(async move {
|
||||
'notify_loop: loop {
|
||||
if receiver.recv().await.is_none() {
|
||||
break 'notify_loop;
|
||||
}
|
||||
|
||||
'rerun_loop: loop {
|
||||
let rerun = match this.reconcile(uuid).await {
|
||||
Ok(rerun) => rerun,
|
||||
Err(error) => {
|
||||
error!("failed to reconcile zone {}: {}", uuid, error);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if rerun {
|
||||
continue 'rerun_loop;
|
||||
}
|
||||
break 'rerun_loop;
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(ZoneReconcilerEntry { sender })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ip_reservation_to_network_status(ip: &IpReservation) -> ZoneNetworkStatus {
|
||||
ZoneNetworkStatus {
|
||||
zone_ipv4: format!("{}/{}", ip.ipv4, ip.ipv4_prefix),
|
||||
zone_ipv6: format!("{}/{}", ip.ipv6, ip.ipv6_prefix),
|
||||
zone_mac: ip.mac.to_string().to_lowercase().replace('-', ":"),
|
||||
gateway_ipv4: format!("{}/{}", ip.gateway_ipv4, ip.ipv4_prefix),
|
||||
gateway_ipv6: format!("{}/{}", ip.gateway_ipv6, ip.ipv6_prefix),
|
||||
gateway_mac: ip.gateway_mac.to_string().to_lowercase().replace('-', ":"),
|
||||
}
|
||||
}
|
@ -3,18 +3,18 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
struct GuestLookupTableState {
|
||||
struct ZoneLookupTableState {
|
||||
domid_to_uuid: HashMap<u32, Uuid>,
|
||||
uuid_to_domid: HashMap<Uuid, u32>,
|
||||
}
|
||||
|
||||
impl GuestLookupTableState {
|
||||
impl ZoneLookupTableState {
|
||||
pub fn new(host_uuid: Uuid) -> Self {
|
||||
let mut domid_to_uuid = HashMap::new();
|
||||
let mut uuid_to_domid = HashMap::new();
|
||||
domid_to_uuid.insert(0, host_uuid);
|
||||
uuid_to_domid.insert(host_uuid, 0);
|
||||
GuestLookupTableState {
|
||||
ZoneLookupTableState {
|
||||
domid_to_uuid,
|
||||
uuid_to_domid,
|
||||
}
|
||||
@ -22,18 +22,18 @@ impl GuestLookupTableState {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GuestLookupTable {
|
||||
pub struct ZoneLookupTable {
|
||||
host_domid: u32,
|
||||
host_uuid: Uuid,
|
||||
state: Arc<RwLock<GuestLookupTableState>>,
|
||||
state: Arc<RwLock<ZoneLookupTableState>>,
|
||||
}
|
||||
|
||||
impl GuestLookupTable {
|
||||
impl ZoneLookupTable {
|
||||
pub fn new(host_domid: u32, host_uuid: Uuid) -> Self {
|
||||
GuestLookupTable {
|
||||
ZoneLookupTable {
|
||||
host_domid,
|
||||
host_uuid,
|
||||
state: Arc::new(RwLock::new(GuestLookupTableState::new(host_uuid))),
|
||||
state: Arc::new(RwLock::new(ZoneLookupTableState::new(host_uuid))),
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use env_logger::Env;
|
||||
use krataguest::{death, init::GuestInit};
|
||||
use log::error;
|
||||
use std::env;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env::set_var("RUST_BACKTRACE", "1");
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init();
|
||||
if env::var("KRATA_UNSAFE_ALWAYS_ALLOW_INIT").unwrap_or("0".to_string()) != "1" {
|
||||
let pid = std::process::id();
|
||||
if pid > 3 {
|
||||
return Err(anyhow!(
|
||||
"not running because the pid of {} indicates this is probably not \
|
||||
the right context for the init daemon. \
|
||||
run with KRATA_UNSAFE_ALWAYS_ALLOW_INIT=1 to bypass this check",
|
||||
pid
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut guest = GuestInit::new();
|
||||
if let Err(error) = guest.init().await {
|
||||
error!("failed to initialize guest: {}", error);
|
||||
death(127).await?;
|
||||
return Ok(());
|
||||
}
|
||||
death(1).await?;
|
||||
Ok(())
|
||||
}
|
@ -1,172 +0,0 @@
|
||||
use std::{collections::HashMap, process::Stdio};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use krata::idm::{
|
||||
client::IdmClientStreamResponseHandle,
|
||||
internal::{
|
||||
exec_stream_request_update::Update, request::Request as RequestType,
|
||||
ExecStreamResponseUpdate,
|
||||
},
|
||||
internal::{response::Response as ResponseType, Request, Response},
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
join,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
pub struct GuestExecTask {
|
||||
pub handle: IdmClientStreamResponseHandle<Request>,
|
||||
}
|
||||
|
||||
impl GuestExecTask {
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
let mut receiver = self.handle.take().await?;
|
||||
|
||||
let Some(ref request) = self.handle.initial.request else {
|
||||
return Err(anyhow!("request was empty"));
|
||||
};
|
||||
|
||||
let RequestType::ExecStream(update) = request else {
|
||||
return Err(anyhow!("request was not an exec update"));
|
||||
};
|
||||
|
||||
let Some(Update::Start(ref start)) = update.update else {
|
||||
return Err(anyhow!("first request did not contain a start update"));
|
||||
};
|
||||
|
||||
let mut cmd = start.command.clone();
|
||||
if cmd.is_empty() {
|
||||
return Err(anyhow!("command line was empty"));
|
||||
}
|
||||
let exe = cmd.remove(0);
|
||||
let mut env = HashMap::new();
|
||||
for entry in &start.environment {
|
||||
env.insert(entry.key.clone(), entry.value.clone());
|
||||
}
|
||||
|
||||
if !env.contains_key("PATH") {
|
||||
env.insert(
|
||||
"PATH".to_string(),
|
||||
"/bin:/usr/bin:/usr/local/bin".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let dir = if start.working_directory.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
start.working_directory.clone()
|
||||
};
|
||||
|
||||
let mut child = Command::new(exe)
|
||||
.args(cmd)
|
||||
.envs(env)
|
||||
.current_dir(dir)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.map_err(|error| anyhow!("failed to spawn: {}", error))?;
|
||||
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("stdin was missing"))?;
|
||||
let mut stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("stdout was missing"))?;
|
||||
let mut stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("stderr was missing"))?;
|
||||
|
||||
let stdout_handle = self.handle.clone();
|
||||
let stdout_task = tokio::task::spawn(async move {
|
||||
let mut stdout_buffer = vec![0u8; 8 * 1024];
|
||||
loop {
|
||||
let Ok(size) = stdout.read(&mut stdout_buffer).await else {
|
||||
break;
|
||||
};
|
||||
if size > 0 {
|
||||
let response = Response {
|
||||
response: Some(ResponseType::ExecStream(ExecStreamResponseUpdate {
|
||||
exited: false,
|
||||
exit_code: 0,
|
||||
error: String::new(),
|
||||
stdout: stdout_buffer[0..size].to_vec(),
|
||||
stderr: vec![],
|
||||
})),
|
||||
};
|
||||
let _ = stdout_handle.respond(response).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let stderr_handle = self.handle.clone();
|
||||
let stderr_task = tokio::task::spawn(async move {
|
||||
let mut stderr_buffer = vec![0u8; 8 * 1024];
|
||||
loop {
|
||||
let Ok(size) = stderr.read(&mut stderr_buffer).await else {
|
||||
break;
|
||||
};
|
||||
if size > 0 {
|
||||
let response = Response {
|
||||
response: Some(ResponseType::ExecStream(ExecStreamResponseUpdate {
|
||||
exited: false,
|
||||
exit_code: 0,
|
||||
error: String::new(),
|
||||
stdout: vec![],
|
||||
stderr: stderr_buffer[0..size].to_vec(),
|
||||
})),
|
||||
};
|
||||
let _ = stderr_handle.respond(response).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let stdin_task = tokio::task::spawn(async move {
|
||||
loop {
|
||||
let Some(request) = receiver.recv().await else {
|
||||
break;
|
||||
};
|
||||
|
||||
let Some(RequestType::ExecStream(update)) = request.request else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(Update::Stdin(update)) = update.update else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if stdin.write_all(&update.data).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let exit = child.wait().await?;
|
||||
let code = exit.code().unwrap_or(-1);
|
||||
|
||||
let _ = join!(stdout_task, stderr_task);
|
||||
stdin_task.abort();
|
||||
|
||||
let response = Response {
|
||||
response: Some(ResponseType::ExecStream(ExecStreamResponseUpdate {
|
||||
exited: true,
|
||||
exit_code: code,
|
||||
error: String::new(),
|
||||
stdout: vec![],
|
||||
stderr: vec![],
|
||||
})),
|
||||
};
|
||||
self.handle.respond(response).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ bytes = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
pin-project-lite = { workspace = true }
|
||||
prost = { workspace = true }
|
||||
prost-reflect = { workspace = true }
|
||||
prost-types = { workspace = true }
|
||||
@ -27,6 +28,8 @@ tower = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
hyper = { workspace = true }
|
||||
hyper-util = { workspace = true }
|
||||
nix = { workspace = true, features = ["term"] }
|
||||
|
||||
[build-dependencies]
|
||||
|
@ -45,10 +45,12 @@ message ExecStreamRequestStart {
|
||||
repeated ExecEnvVar environment = 1;
|
||||
repeated string command = 2;
|
||||
string working_directory = 3;
|
||||
bool tty = 4;
|
||||
}
|
||||
|
||||
message ExecStreamRequestStdin {
|
||||
bytes data = 1;
|
||||
bool closed = 2;
|
||||
}
|
||||
|
||||
message ExecStreamRequestUpdate {
|
||||
|
@ -8,32 +8,44 @@ option java_outer_classname = "CommonProto";
|
||||
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
message Guest {
|
||||
message Zone {
|
||||
string id = 1;
|
||||
GuestSpec spec = 2;
|
||||
GuestState state = 3;
|
||||
ZoneSpec spec = 2;
|
||||
ZoneStatus status = 3;
|
||||
}
|
||||
|
||||
message GuestSpec {
|
||||
message ZoneSpec {
|
||||
string name = 1;
|
||||
GuestImageSpec image = 2;
|
||||
ZoneImageSpec image = 2;
|
||||
// If not specified, defaults to the daemon default kernel.
|
||||
GuestImageSpec kernel = 3;
|
||||
ZoneImageSpec kernel = 3;
|
||||
// If not specified, defaults to the daemon default initrd.
|
||||
GuestImageSpec initrd = 4;
|
||||
uint32 vcpus = 5;
|
||||
uint64 mem = 6;
|
||||
GuestTaskSpec task = 7;
|
||||
repeated GuestSpecAnnotation annotations = 8;
|
||||
repeated GuestSpecDevice devices = 9;
|
||||
ZoneImageSpec initrd = 4;
|
||||
ZoneResourceSpec initial_resources = 5;
|
||||
ZoneTaskSpec task = 6;
|
||||
repeated ZoneSpecAnnotation annotations = 7;
|
||||
repeated ZoneSpecDevice devices = 8;
|
||||
ZoneKernelOptionsSpec kernel_options = 9;
|
||||
}
|
||||
|
||||
message GuestImageSpec {
|
||||
message ZoneResourceSpec {
|
||||
uint64 max_memory = 1;
|
||||
uint64 target_memory = 2;
|
||||
uint32 max_cpus = 3;
|
||||
uint32 target_cpus = 4;
|
||||
}
|
||||
|
||||
message ZoneImageSpec {
|
||||
oneof image {
|
||||
GuestOciImageSpec oci = 1;
|
||||
ZoneOciImageSpec oci = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message ZoneKernelOptionsSpec {
|
||||
bool verbose = 1;
|
||||
string cmdline_append = 2;
|
||||
}
|
||||
|
||||
enum OciImageFormat {
|
||||
OCI_IMAGE_FORMAT_UNKNOWN = 0;
|
||||
OCI_IMAGE_FORMAT_SQUASHFS = 1;
|
||||
@ -42,77 +54,83 @@ enum OciImageFormat {
|
||||
OCI_IMAGE_FORMAT_TAR = 3;
|
||||
}
|
||||
|
||||
message GuestOciImageSpec {
|
||||
message ZoneOciImageSpec {
|
||||
string digest = 1;
|
||||
OciImageFormat format = 2;
|
||||
}
|
||||
|
||||
message GuestTaskSpec {
|
||||
repeated GuestTaskSpecEnvVar environment = 1;
|
||||
message ZoneTaskSpec {
|
||||
repeated ZoneTaskSpecEnvVar environment = 1;
|
||||
repeated string command = 2;
|
||||
string working_directory = 3;
|
||||
bool tty = 4;
|
||||
}
|
||||
|
||||
message GuestTaskSpecEnvVar {
|
||||
message ZoneTaskSpecEnvVar {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message GuestSpecAnnotation {
|
||||
message ZoneSpecAnnotation {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message GuestSpecDevice {
|
||||
message ZoneSpecDevice {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message GuestState {
|
||||
GuestStatus status = 1;
|
||||
GuestNetworkState network = 2;
|
||||
GuestExitInfo exit_info = 3;
|
||||
GuestErrorInfo error_info = 4;
|
||||
message ZoneStatus {
|
||||
ZoneState state = 1;
|
||||
ZoneNetworkStatus network_status = 2;
|
||||
ZoneExitStatus exit_status = 3;
|
||||
ZoneErrorStatus error_status = 4;
|
||||
string host = 5;
|
||||
uint32 domid = 6;
|
||||
ZoneResourceStatus resource_status = 7;
|
||||
}
|
||||
|
||||
enum GuestStatus {
|
||||
GUEST_STATUS_UNKNOWN = 0;
|
||||
GUEST_STATUS_STARTING = 1;
|
||||
GUEST_STATUS_STARTED = 2;
|
||||
GUEST_STATUS_EXITED = 3;
|
||||
GUEST_STATUS_DESTROYING = 4;
|
||||
GUEST_STATUS_DESTROYED = 5;
|
||||
GUEST_STATUS_FAILED = 6;
|
||||
enum ZoneState {
|
||||
ZONE_STATE_UNKNOWN = 0;
|
||||
ZONE_STATE_CREATING = 1;
|
||||
ZONE_STATE_CREATED = 2;
|
||||
ZONE_STATE_EXITED = 3;
|
||||
ZONE_STATE_DESTROYING = 4;
|
||||
ZONE_STATE_DESTROYED = 5;
|
||||
ZONE_STATE_FAILED = 6;
|
||||
}
|
||||
|
||||
message GuestNetworkState {
|
||||
string guest_ipv4 = 1;
|
||||
string guest_ipv6 = 2;
|
||||
string guest_mac = 3;
|
||||
message ZoneNetworkStatus {
|
||||
string zone_ipv4 = 1;
|
||||
string zone_ipv6 = 2;
|
||||
string zone_mac = 3;
|
||||
string gateway_ipv4 = 4;
|
||||
string gateway_ipv6 = 5;
|
||||
string gateway_mac = 6;
|
||||
}
|
||||
|
||||
message GuestExitInfo {
|
||||
message ZoneExitStatus {
|
||||
int32 code = 1;
|
||||
}
|
||||
|
||||
message GuestErrorInfo {
|
||||
message ZoneErrorStatus {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
message GuestMetricNode {
|
||||
string name = 1;
|
||||
google.protobuf.Value value = 2;
|
||||
GuestMetricFormat format = 3;
|
||||
repeated GuestMetricNode children = 4;
|
||||
message ZoneResourceStatus {
|
||||
ZoneResourceSpec active_resources = 1;
|
||||
}
|
||||
|
||||
enum GuestMetricFormat {
|
||||
GUEST_METRIC_FORMAT_UNKNOWN = 0;
|
||||
GUEST_METRIC_FORMAT_BYTES = 1;
|
||||
GUEST_METRIC_FORMAT_INTEGER = 2;
|
||||
GUEST_METRIC_FORMAT_DURATION_SECONDS = 3;
|
||||
message ZoneMetricNode {
|
||||
string name = 1;
|
||||
google.protobuf.Value value = 2;
|
||||
ZoneMetricFormat format = 3;
|
||||
repeated ZoneMetricNode children = 4;
|
||||
}
|
||||
|
||||
enum ZoneMetricFormat {
|
||||
ZONE_METRIC_FORMAT_UNKNOWN = 0;
|
||||
ZONE_METRIC_FORMAT_BYTES = 1;
|
||||
ZONE_METRIC_FORMAT_INTEGER = 2;
|
||||
ZONE_METRIC_FORMAT_DURATION_SECONDS = 3;
|
||||
}
|
||||
|
@ -10,71 +10,90 @@ import "krata/idm/transport.proto";
|
||||
import "krata/v1/common.proto";
|
||||
|
||||
service ControlService {
|
||||
rpc IdentifyHost(IdentifyHostRequest) returns (IdentifyHostReply);
|
||||
|
||||
rpc CreateGuest(CreateGuestRequest) returns (CreateGuestReply);
|
||||
rpc DestroyGuest(DestroyGuestRequest) returns (DestroyGuestReply);
|
||||
rpc ResolveGuest(ResolveGuestRequest) returns (ResolveGuestReply);
|
||||
rpc ListGuests(ListGuestsRequest) returns (ListGuestsReply);
|
||||
rpc ListDevices(ListDevicesRequest) returns (ListDevicesReply);
|
||||
|
||||
rpc ExecGuest(stream ExecGuestRequest) returns (stream ExecGuestReply);
|
||||
|
||||
rpc ConsoleData(stream ConsoleDataRequest) returns (stream ConsoleDataReply);
|
||||
rpc ReadGuestMetrics(ReadGuestMetricsRequest) returns (ReadGuestMetricsReply);
|
||||
|
||||
rpc GetHostStatus(GetHostStatusRequest) returns (GetHostStatusReply);
|
||||
rpc SnoopIdm(SnoopIdmRequest) returns (stream SnoopIdmReply);
|
||||
rpc WatchEvents(WatchEventsRequest) returns (stream WatchEventsReply);
|
||||
rpc GetHostCpuTopology(GetHostCpuTopologyRequest) returns (GetHostCpuTopologyReply);
|
||||
rpc SetHostPowerManagementPolicy(SetHostPowerManagementPolicyRequest) returns (SetHostPowerManagementPolicyReply);
|
||||
|
||||
rpc ListDevices(ListDevicesRequest) returns (ListDevicesReply);
|
||||
|
||||
rpc PullImage(PullImageRequest) returns (stream PullImageReply);
|
||||
|
||||
rpc GetHostCpuTopology(HostCpuTopologyRequest) returns (HostCpuTopologyReply);
|
||||
rpc SetHostPowerManagementPolicy(HostPowerManagementPolicy) returns (HostPowerManagementPolicy);
|
||||
rpc CreateZone(CreateZoneRequest) returns (CreateZoneReply);
|
||||
rpc DestroyZone(DestroyZoneRequest) returns (DestroyZoneReply);
|
||||
|
||||
rpc ResolveZoneId(ResolveZoneIdRequest) returns (ResolveZoneIdReply);
|
||||
|
||||
rpc GetZone(GetZoneRequest) returns (GetZoneReply);
|
||||
|
||||
rpc UpdateZoneResources(UpdateZoneResourcesRequest) returns (UpdateZoneResourcesReply);
|
||||
|
||||
rpc ListZones(ListZonesRequest) returns (ListZonesReply);
|
||||
|
||||
rpc AttachZoneConsole(stream ZoneConsoleRequest) returns (stream ZoneConsoleReply);
|
||||
rpc ExecInsideZone(stream ExecInsideZoneRequest) returns (stream ExecInsideZoneReply);
|
||||
rpc ReadZoneMetrics(ReadZoneMetricsRequest) returns (ReadZoneMetricsReply);
|
||||
|
||||
rpc WatchEvents(WatchEventsRequest) returns (stream WatchEventsReply);
|
||||
|
||||
rpc ReadHypervisorConsole(ReadHypervisorConsoleRequest) returns (ReadHypervisorConsoleReply);
|
||||
}
|
||||
|
||||
message IdentifyHostRequest {}
|
||||
message GetHostStatusRequest {}
|
||||
|
||||
message IdentifyHostReply {
|
||||
message GetHostStatusReply {
|
||||
string host_uuid = 1;
|
||||
uint32 host_domid = 2;
|
||||
string krata_version = 3;
|
||||
string host_ipv4 = 4;
|
||||
string host_ipv6 = 5;
|
||||
string host_mac = 6;
|
||||
}
|
||||
|
||||
message CreateGuestRequest {
|
||||
krata.v1.common.GuestSpec spec = 1;
|
||||
message CreateZoneRequest {
|
||||
krata.v1.common.ZoneSpec spec = 1;
|
||||
}
|
||||
|
||||
message CreateGuestReply {
|
||||
string guest_id = 1;
|
||||
message CreateZoneReply {
|
||||
string zone_id = 1;
|
||||
}
|
||||
|
||||
message DestroyGuestRequest {
|
||||
string guest_id = 1;
|
||||
message DestroyZoneRequest {
|
||||
string zone_id = 1;
|
||||
}
|
||||
|
||||
message DestroyGuestReply {}
|
||||
message DestroyZoneReply {}
|
||||
|
||||
message ResolveGuestRequest {
|
||||
message ResolveZoneIdRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message ResolveGuestReply {
|
||||
krata.v1.common.Guest guest = 1;
|
||||
message ResolveZoneIdReply {
|
||||
string zone_id = 1;
|
||||
}
|
||||
|
||||
message ListGuestsRequest {}
|
||||
|
||||
message ListGuestsReply {
|
||||
repeated krata.v1.common.Guest guests = 1;
|
||||
message GetZoneRequest {
|
||||
string zone_id = 1;
|
||||
}
|
||||
|
||||
message ExecGuestRequest {
|
||||
string guest_id = 1;
|
||||
krata.v1.common.GuestTaskSpec task = 2;
|
||||
bytes data = 3;
|
||||
message GetZoneReply {
|
||||
krata.v1.common.Zone zone = 1;
|
||||
}
|
||||
|
||||
message ExecGuestReply {
|
||||
message ListZonesRequest {}
|
||||
|
||||
message ListZonesReply {
|
||||
repeated krata.v1.common.Zone zones = 1;
|
||||
}
|
||||
|
||||
message ExecInsideZoneRequest {
|
||||
string zone_id = 1;
|
||||
krata.v1.common.ZoneTaskSpec task = 2;
|
||||
bytes stdin = 3;
|
||||
bool stdin_closed = 4;
|
||||
}
|
||||
|
||||
message ExecInsideZoneReply {
|
||||
bool exited = 1;
|
||||
string error = 2;
|
||||
int32 exit_code = 3;
|
||||
@ -82,12 +101,13 @@ message ExecGuestReply {
|
||||
bytes stderr = 5;
|
||||
}
|
||||
|
||||
message ConsoleDataRequest {
|
||||
string guest_id = 1;
|
||||
message ZoneConsoleRequest {
|
||||
string zone_id = 1;
|
||||
bytes data = 2;
|
||||
bool replay_history = 3;
|
||||
}
|
||||
|
||||
message ConsoleDataReply {
|
||||
message ZoneConsoleReply {
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
@ -95,20 +115,20 @@ message WatchEventsRequest {}
|
||||
|
||||
message WatchEventsReply {
|
||||
oneof event {
|
||||
GuestChangedEvent guest_changed = 1;
|
||||
ZoneChangedEvent zone_changed = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message GuestChangedEvent {
|
||||
krata.v1.common.Guest guest = 1;
|
||||
message ZoneChangedEvent {
|
||||
krata.v1.common.Zone zone = 1;
|
||||
}
|
||||
|
||||
message ReadGuestMetricsRequest {
|
||||
string guest_id = 1;
|
||||
message ReadZoneMetricsRequest {
|
||||
string zone_id = 1;
|
||||
}
|
||||
|
||||
message ReadGuestMetricsReply {
|
||||
krata.v1.common.GuestMetricNode root = 1;
|
||||
message ReadZoneMetricsReply {
|
||||
krata.v1.common.ZoneMetricNode root = 1;
|
||||
}
|
||||
|
||||
message SnoopIdmRequest {}
|
||||
@ -184,6 +204,7 @@ message PullImageRequest {
|
||||
string image = 1;
|
||||
krata.v1.common.OciImageFormat format = 2;
|
||||
bool overwrite_cache = 3;
|
||||
bool update = 4;
|
||||
}
|
||||
|
||||
message PullImageReply {
|
||||
@ -205,9 +226,9 @@ message ListDevicesReply {
|
||||
}
|
||||
|
||||
enum HostCpuTopologyClass {
|
||||
CPU_CLASS_STANDARD = 0;
|
||||
CPU_CLASS_PERFORMANCE = 1;
|
||||
CPU_CLASS_EFFICIENCY = 2;
|
||||
HOST_CPU_TOPOLOGY_CLASS_STANDARD = 0;
|
||||
HOST_CPU_TOPOLOGY_CLASS_PERFORMANCE = 1;
|
||||
HOST_CPU_TOPOLOGY_CLASS_EFFICIENCY = 2;
|
||||
}
|
||||
|
||||
message HostCpuTopologyInfo {
|
||||
@ -218,15 +239,28 @@ message HostCpuTopologyInfo {
|
||||
HostCpuTopologyClass class = 5;
|
||||
}
|
||||
|
||||
message HostCpuTopologyRequest {}
|
||||
message GetHostCpuTopologyRequest {}
|
||||
|
||||
message HostCpuTopologyReply {
|
||||
message GetHostCpuTopologyReply {
|
||||
repeated HostCpuTopologyInfo cpus = 1;
|
||||
}
|
||||
|
||||
message HostPowerManagementPolicyRequest {}
|
||||
|
||||
message HostPowerManagementPolicy {
|
||||
message SetHostPowerManagementPolicyRequest {
|
||||
string scheduler = 1;
|
||||
bool smt_awareness = 2;
|
||||
}
|
||||
|
||||
message SetHostPowerManagementPolicyReply {}
|
||||
|
||||
message UpdateZoneResourcesRequest {
|
||||
string zone_id = 1;
|
||||
krata.v1.common.ZoneResourceSpec resources = 2;
|
||||
}
|
||||
|
||||
message UpdateZoneResourcesReply {}
|
||||
|
||||
message ReadHypervisorConsoleRequest {}
|
||||
|
||||
message ReadHypervisorConsoleReply {
|
||||
string data = 1;
|
||||
}
|
||||
|
@ -1,14 +1,10 @@
|
||||
#[cfg(unix)]
|
||||
use crate::unix::HyperUnixConnector;
|
||||
use crate::{dial::ControlDialAddress, v1::control::control_service_client::ControlServiceClient};
|
||||
#[cfg(not(unix))]
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
#[cfg(unix)]
|
||||
use tonic::transport::Uri;
|
||||
use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
|
||||
#[cfg(unix)]
|
||||
use tower::service_fn;
|
||||
|
||||
pub struct ControlClientProvider {}
|
||||
|
||||
@ -52,10 +48,7 @@ impl ControlClientProvider {
|
||||
async fn dial_unix_socket(path: String) -> Result<Channel> {
|
||||
// This URL is not actually used but is required to be specified.
|
||||
Ok(Endpoint::try_from(format!("unix://localhost/{}", path))?
|
||||
.connect_with_connector(service_fn(|uri: Uri| {
|
||||
let path = uri.path().to_string();
|
||||
UnixStream::connect(path)
|
||||
}))
|
||||
.connect_with_connector(HyperUnixConnector {})
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
@ -9,12 +9,13 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use log::{debug, error};
|
||||
use nix::sys::termios::{cfmakeraw, tcgetattr, tcsetattr, SetArg};
|
||||
use prost::Message;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{unix::AsyncFd, AsyncReadExt, AsyncWriteExt},
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
select,
|
||||
sync::{
|
||||
broadcast,
|
||||
@ -33,7 +34,7 @@ use super::{
|
||||
|
||||
type OneshotRequestMap<R> = Arc<Mutex<HashMap<u64, oneshot::Sender<<R as IdmRequest>::Response>>>>;
|
||||
type StreamRequestMap<R> = Arc<Mutex<HashMap<u64, Sender<<R as IdmRequest>::Response>>>>;
|
||||
type StreamRequestUpdateMap<R> = Arc<Mutex<HashMap<u64, mpsc::Sender<R>>>>;
|
||||
type StreamRequestUpdateMap<R> = Arc<Mutex<HashMap<u64, Sender<R>>>>;
|
||||
pub type IdmInternalClient = IdmClient<internal::Request, internal::Event>;
|
||||
|
||||
const IDM_PACKET_QUEUE_LEN: usize = 100;
|
||||
@ -42,12 +43,13 @@ const IDM_PACKET_MAX_SIZE: usize = 20 * 1024 * 1024;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait IdmBackend: Send {
|
||||
async fn recv(&mut self) -> Result<IdmTransportPacket>;
|
||||
async fn recv(&mut self) -> Result<Vec<IdmTransportPacket>>;
|
||||
async fn send(&mut self, packet: IdmTransportPacket) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct IdmFileBackend {
|
||||
read_fd: Arc<Mutex<AsyncFd<File>>>,
|
||||
read: Arc<Mutex<File>>,
|
||||
read_buffer: BytesMut,
|
||||
write: Arc<Mutex<File>>,
|
||||
}
|
||||
|
||||
@ -56,7 +58,8 @@ impl IdmFileBackend {
|
||||
IdmFileBackend::set_raw_port(&read_file)?;
|
||||
IdmFileBackend::set_raw_port(&write_file)?;
|
||||
Ok(IdmFileBackend {
|
||||
read_fd: Arc::new(Mutex::new(AsyncFd::new(read_file)?)),
|
||||
read: Arc::new(Mutex::new(read_file)),
|
||||
read_buffer: BytesMut::new(),
|
||||
write: Arc::new(Mutex::new(write_file)),
|
||||
})
|
||||
}
|
||||
@ -71,35 +74,69 @@ impl IdmFileBackend {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl IdmBackend for IdmFileBackend {
|
||||
async fn recv(&mut self) -> Result<IdmTransportPacket> {
|
||||
let mut fd = self.read_fd.lock().await;
|
||||
let mut guard = fd.readable_mut().await?;
|
||||
let b1 = guard.get_inner_mut().read_u8().await?;
|
||||
if b1 != 0xff {
|
||||
return Ok(IdmTransportPacket::default());
|
||||
}
|
||||
let b2 = guard.get_inner_mut().read_u8().await?;
|
||||
if b2 != 0xff {
|
||||
return Ok(IdmTransportPacket::default());
|
||||
}
|
||||
let size = guard.get_inner_mut().read_u32_le().await?;
|
||||
if size == 0 {
|
||||
return Ok(IdmTransportPacket::default());
|
||||
}
|
||||
let mut buffer = vec![0u8; size as usize];
|
||||
guard.get_inner_mut().read_exact(&mut buffer).await?;
|
||||
match IdmTransportPacket::decode(buffer.as_slice()) {
|
||||
Ok(packet) => Ok(packet),
|
||||
Err(error) => Err(anyhow!("received invalid idm packet: {}", error)),
|
||||
async fn recv(&mut self) -> Result<Vec<IdmTransportPacket>> {
|
||||
let mut data = vec![0; 8192];
|
||||
let mut first = true;
|
||||
'read_more: loop {
|
||||
let mut packets = Vec::new();
|
||||
if !first {
|
||||
if !packets.is_empty() {
|
||||
return Ok(packets);
|
||||
}
|
||||
let size = self.read.lock().await.read(&mut data).await?;
|
||||
self.read_buffer.extend_from_slice(&data[0..size]);
|
||||
}
|
||||
first = false;
|
||||
loop {
|
||||
if self.read_buffer.len() < 6 {
|
||||
continue 'read_more;
|
||||
}
|
||||
|
||||
let b1 = self.read_buffer[0];
|
||||
let b2 = self.read_buffer[1];
|
||||
|
||||
if b1 != 0xff || b2 != 0xff {
|
||||
self.read_buffer.clear();
|
||||
continue 'read_more;
|
||||
}
|
||||
|
||||
let size = (self.read_buffer[2] as u32
|
||||
| (self.read_buffer[3] as u32) << 8
|
||||
| (self.read_buffer[4] as u32) << 16
|
||||
| (self.read_buffer[5] as u32) << 24) as usize;
|
||||
let needed = size + 6;
|
||||
if self.read_buffer.len() < needed {
|
||||
continue 'read_more;
|
||||
}
|
||||
|
||||
let mut packet = self.read_buffer.split_to(needed);
|
||||
packet.advance(6);
|
||||
|
||||
match IdmTransportPacket::decode(packet) {
|
||||
Ok(packet) => {
|
||||
packets.push(packet);
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(anyhow!("received invalid idm packet: {}", error));
|
||||
}
|
||||
}
|
||||
|
||||
if self.read_buffer.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(packets);
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(&mut self, packet: IdmTransportPacket) -> Result<()> {
|
||||
let mut file = self.write.lock().await;
|
||||
let data = packet.encode_to_vec();
|
||||
file.write_all(&[0xff, 0xff]).await?;
|
||||
file.write_u32_le(data.len() as u32).await?;
|
||||
file.write_all(&data).await?;
|
||||
let length = packet.encoded_len();
|
||||
let mut buffer = BytesMut::with_capacity(6 + length);
|
||||
buffer.put_slice(&[0xff, 0xff]);
|
||||
buffer.put_u32_le(length as u32);
|
||||
packet.encode(&mut buffer)?;
|
||||
file.write_all(&buffer).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -400,8 +437,9 @@ impl<R: IdmRequest, E: IdmSerializable> IdmClient<R, E> {
|
||||
loop {
|
||||
select! {
|
||||
x = backend.recv() => match x {
|
||||
Ok(packet) => {
|
||||
if packet.channel != channel {
|
||||
Ok(packets) => {
|
||||
for packet in packets {
|
||||
if packet.channel != channel {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -457,6 +495,7 @@ impl<R: IdmRequest, E: IdmSerializable> IdmClient<R, E> {
|
||||
IdmTransportPacketForm::StreamRequestClosed => {
|
||||
let mut update_streams = request_update_streams.lock().await;
|
||||
update_streams.remove(&packet.id);
|
||||
println!("stream request closed: {}", packet.id);
|
||||
}
|
||||
|
||||
IdmTransportPacketForm::StreamResponseUpdate => {
|
||||
@ -475,6 +514,7 @@ impl<R: IdmRequest, E: IdmSerializable> IdmClient<R, E> {
|
||||
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Err(error) => {
|
||||
@ -488,7 +528,7 @@ impl<R: IdmRequest, E: IdmSerializable> IdmClient<R, E> {
|
||||
error!("unable to send idm packet, packet size exceeded (tried to send {} bytes)", length);
|
||||
continue;
|
||||
}
|
||||
backend.send(packet).await?;
|
||||
backend.send(packet.clone()).await?;
|
||||
},
|
||||
|
||||
None => {
|
||||
|
@ -12,6 +12,9 @@ pub mod launchcfg;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod ethtool;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod unix;
|
||||
|
||||
pub static DESCRIPTOR_POOL: Lazy<DescriptorPool> = Lazy::new(|| {
|
||||
DescriptorPool::decode(
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin")).as_ref(),
|
||||
|
73
crates/krata/src/unix.rs
Normal file
73
crates/krata/src/unix.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use std::future::Future;
|
||||
use std::io::Error;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use hyper::rt::ReadBufCursor;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use pin_project_lite::pin_project;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::net::UnixStream;
|
||||
use tonic::transport::Uri;
|
||||
use tower::Service;
|
||||
|
||||
pin_project! {
|
||||
#[derive(Debug)]
|
||||
pub struct HyperUnixStream {
|
||||
#[pin]
|
||||
pub stream: UnixStream,
|
||||
}
|
||||
}
|
||||
|
||||
impl hyper::rt::Read for HyperUnixStream {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: ReadBufCursor<'_>,
|
||||
) -> Poll<Result<(), Error>> {
|
||||
let mut tokio = TokioIo::new(self.project().stream);
|
||||
Pin::new(&mut tokio).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl hyper::rt::Write for HyperUnixStream {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, Error>> {
|
||||
self.project().stream.poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
self.project().stream.poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
self.project().stream.poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HyperUnixConnector;
|
||||
|
||||
impl Service<Uri> for HyperUnixConnector {
|
||||
type Response = HyperUnixStream;
|
||||
type Error = Error;
|
||||
#[allow(clippy::type_complexity)]
|
||||
type Future =
|
||||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
||||
|
||||
fn call(&mut self, req: Uri) -> Self::Future {
|
||||
let fut = async move {
|
||||
let path = req.path().to_string();
|
||||
let stream = UnixStream::connect(path).await?;
|
||||
Ok(HyperUnixStream { stream })
|
||||
};
|
||||
|
||||
Box::pin(fut)
|
||||
}
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ clap = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
etherparse = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.12" }
|
||||
krata = { path = "../krata", version = "^0.0.19" }
|
||||
krata-advmac = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
@ -2,10 +2,10 @@ use anyhow::Result;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::Guest,
|
||||
common::Zone,
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
ListGuestsRequest,
|
||||
ListZonesRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -33,7 +33,7 @@ pub struct NetworkSide {
|
||||
pub struct NetworkMetadata {
|
||||
pub domid: u32,
|
||||
pub uuid: Uuid,
|
||||
pub guest: NetworkSide,
|
||||
pub zone: NetworkSide,
|
||||
pub gateway: NetworkSide,
|
||||
}
|
||||
|
||||
@ -60,65 +60,65 @@ impl AutoNetworkWatcher {
|
||||
}
|
||||
|
||||
pub async fn read(&mut self) -> Result<Vec<NetworkMetadata>> {
|
||||
let mut all_guests: HashMap<Uuid, Guest> = HashMap::new();
|
||||
for guest in self
|
||||
let mut all_zones: HashMap<Uuid, Zone> = HashMap::new();
|
||||
for zone in self
|
||||
.control
|
||||
.list_guests(ListGuestsRequest {})
|
||||
.list_zones(ListZonesRequest {})
|
||||
.await?
|
||||
.into_inner()
|
||||
.guests
|
||||
.zones
|
||||
{
|
||||
let Ok(uuid) = Uuid::from_str(&guest.id) else {
|
||||
let Ok(uuid) = Uuid::from_str(&zone.id) else {
|
||||
continue;
|
||||
};
|
||||
all_guests.insert(uuid, guest);
|
||||
all_zones.insert(uuid, zone);
|
||||
}
|
||||
|
||||
let mut networks: Vec<NetworkMetadata> = Vec::new();
|
||||
for (uuid, guest) in &all_guests {
|
||||
let Some(ref state) = guest.state else {
|
||||
for (uuid, zone) in &all_zones {
|
||||
let Some(ref status) = zone.status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if state.domid == u32::MAX {
|
||||
if status.domid == u32::MAX {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(ref network) = state.network else {
|
||||
let Some(ref network_status) = status.network_status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(guest_ipv4_cidr) = Ipv4Cidr::from_str(&network.guest_ipv4) else {
|
||||
let Ok(zone_ipv4_cidr) = Ipv4Cidr::from_str(&network_status.zone_ipv4) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(guest_ipv6_cidr) = Ipv6Cidr::from_str(&network.guest_ipv6) else {
|
||||
let Ok(zone_ipv6_cidr) = Ipv6Cidr::from_str(&network_status.zone_ipv6) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(guest_mac) = EthernetAddress::from_str(&network.guest_mac) else {
|
||||
let Ok(zone_mac) = EthernetAddress::from_str(&network_status.zone_mac) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(gateway_ipv4_cidr) = Ipv4Cidr::from_str(&network.gateway_ipv4) else {
|
||||
let Ok(gateway_ipv4_cidr) = Ipv4Cidr::from_str(&network_status.gateway_ipv4) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(gateway_ipv6_cidr) = Ipv6Cidr::from_str(&network.gateway_ipv6) else {
|
||||
let Ok(gateway_ipv6_cidr) = Ipv6Cidr::from_str(&network_status.gateway_ipv6) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(gateway_mac) = EthernetAddress::from_str(&network.gateway_mac) else {
|
||||
let Ok(gateway_mac) = EthernetAddress::from_str(&network_status.gateway_mac) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
networks.push(NetworkMetadata {
|
||||
domid: state.domid,
|
||||
domid: status.domid,
|
||||
uuid: *uuid,
|
||||
guest: NetworkSide {
|
||||
ipv4: guest_ipv4_cidr,
|
||||
ipv6: guest_ipv6_cidr,
|
||||
mac: guest_mac,
|
||||
zone: NetworkSide {
|
||||
ipv4: zone_ipv4_cidr,
|
||||
ipv6: zone_ipv6_cidr,
|
||||
mac: zone_mac,
|
||||
},
|
||||
gateway: NetworkSide {
|
||||
ipv4: gateway_ipv4_cidr,
|
||||
@ -175,7 +175,7 @@ impl AutoNetworkWatcher {
|
||||
loop {
|
||||
select! {
|
||||
x = receiver.recv() => match x {
|
||||
Ok(Event::GuestChanged(_)) => {
|
||||
Ok(Event::ZoneChanged(_)) => {
|
||||
break;
|
||||
},
|
||||
|
||||
@ -187,7 +187,7 @@ impl AutoNetworkWatcher {
|
||||
_ = sleep(Duration::from_secs(10)) => {
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -54,11 +54,11 @@ impl NetworkStack<'_> {
|
||||
match what {
|
||||
NetworkStackSelect::Receive(Some(packet)) => {
|
||||
if let Err(error) = self.bridge.to_bridge_sender.try_send(packet.clone()) {
|
||||
trace!("failed to send guest packet to bridge: {}", error);
|
||||
trace!("failed to send zone packet to bridge: {}", error);
|
||||
}
|
||||
|
||||
if let Err(error) = self.nat.receive_sender.try_send(packet.clone()) {
|
||||
trace!("failed to send guest packet to nat: {}", error);
|
||||
trace!("failed to send zone packet to nat: {}", error);
|
||||
}
|
||||
|
||||
self.udev.rx = Some(packet);
|
||||
@ -127,7 +127,8 @@ impl NetworkBackend {
|
||||
let (tx_sender, tx_receiver) = channel::<BytesMut>(TX_CHANNEL_BUFFER_LEN);
|
||||
let mut udev = ChannelDevice::new(mtu, Medium::Ethernet, tx_sender.clone());
|
||||
let mac = self.metadata.gateway.mac;
|
||||
let nat = Nat::new(mtu, proxy, mac, addresses.clone(), tx_sender.clone())?;
|
||||
let local_cidrs = addresses.clone();
|
||||
let nat = Nat::new(mtu, proxy, mac, local_cidrs, tx_sender.clone())?;
|
||||
let hardware_addr = HardwareAddress::Ethernet(mac);
|
||||
let config = Config::new(hardware_addr);
|
||||
let mut iface = Interface::new(config, &mut udev, Instant::now());
|
||||
@ -137,7 +138,7 @@ impl NetworkBackend {
|
||||
.expect("failed to set ip addresses");
|
||||
});
|
||||
let sockets = SocketSet::new(vec![]);
|
||||
let handle = self.bridge.join(self.metadata.guest.mac).await?;
|
||||
let handle = self.bridge.join(self.metadata.zone.mac).await?;
|
||||
let kdev = AsyncRawSocketChannel::new(mtu, kdev)?;
|
||||
Ok(NetworkStack {
|
||||
tx: tx_receiver,
|
||||
@ -153,12 +154,12 @@ impl NetworkBackend {
|
||||
pub async fn launch(self) -> Result<JoinHandle<()>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
info!(
|
||||
"launched network backend for krata guest {}",
|
||||
"launched network backend for krata zone {}",
|
||||
self.metadata.uuid
|
||||
);
|
||||
if let Err(error) = self.run().await {
|
||||
warn!(
|
||||
"network backend for krata guest {} failed: {}",
|
||||
"network backend for krata zone {} failed: {}",
|
||||
self.metadata.uuid, error
|
||||
);
|
||||
}
|
||||
@ -169,7 +170,7 @@ impl NetworkBackend {
|
||||
impl Drop for NetworkBackend {
|
||||
fn drop(&mut self) {
|
||||
info!(
|
||||
"destroyed network backend for krata guest {}",
|
||||
"destroyed network backend for krata zone {}",
|
||||
self.metadata.uuid
|
||||
);
|
||||
}
|
||||
|
@ -1,21 +1,15 @@
|
||||
use std::{
|
||||
io::ErrorKind,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
};
|
||||
use std::{io::ErrorKind, net::IpAddr};
|
||||
|
||||
use advmac::MacAddr6;
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::BytesMut;
|
||||
use futures::TryStreamExt;
|
||||
use log::error;
|
||||
use smoltcp::wire::EthernetAddress;
|
||||
use smoltcp::wire::{EthernetAddress, Ipv4Cidr, Ipv6Cidr};
|
||||
use tokio::{select, task::JoinHandle};
|
||||
use tokio_tun::Tun;
|
||||
|
||||
use crate::vbridge::{BridgeJoinHandle, VirtualBridge};
|
||||
|
||||
const HOST_IPV4_ADDR: Ipv4Addr = Ipv4Addr::new(10, 75, 0, 1);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum HostBridgeProcessSelect {
|
||||
Send(Option<BytesMut>),
|
||||
@ -27,7 +21,14 @@ pub struct HostBridge {
|
||||
}
|
||||
|
||||
impl HostBridge {
|
||||
pub async fn new(mtu: usize, interface: String, bridge: &VirtualBridge) -> Result<HostBridge> {
|
||||
pub async fn new(
|
||||
mtu: usize,
|
||||
interface: String,
|
||||
bridge: &VirtualBridge,
|
||||
ipv4: Ipv4Cidr,
|
||||
ipv6: Ipv6Cidr,
|
||||
mac: EthernetAddress,
|
||||
) -> Result<HostBridge> {
|
||||
let tun = Tun::builder()
|
||||
.name(&interface)
|
||||
.tap(true)
|
||||
@ -38,10 +39,6 @@ impl HostBridge {
|
||||
let (connection, handle, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
let mut mac = MacAddr6::random();
|
||||
mac.set_local(true);
|
||||
mac.set_multicast(false);
|
||||
|
||||
let mut links = handle.link().get().match_name(interface.clone()).execute();
|
||||
let link = links.try_next().await?;
|
||||
if link.is_none() {
|
||||
@ -54,25 +51,32 @@ impl HostBridge {
|
||||
|
||||
handle
|
||||
.address()
|
||||
.add(link.header.index, IpAddr::V4(HOST_IPV4_ADDR), 16)
|
||||
.add(
|
||||
link.header.index,
|
||||
IpAddr::V4(ipv4.address().into()),
|
||||
ipv4.prefix_len(),
|
||||
)
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
handle
|
||||
.address()
|
||||
.add(link.header.index, IpAddr::V6(mac.to_link_local_ipv6()), 10)
|
||||
.add(
|
||||
link.header.index,
|
||||
IpAddr::V6(ipv6.address().into()),
|
||||
ipv6.prefix_len(),
|
||||
)
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
handle
|
||||
.link()
|
||||
.set(link.header.index)
|
||||
.address(mac.to_array().to_vec())
|
||||
.address(mac.0.to_vec())
|
||||
.up()
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
let mac = EthernetAddress(mac.to_array());
|
||||
let bridge_handle = bridge.join(mac).await?;
|
||||
|
||||
let task = tokio::task::spawn(async move {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user