├── .github └── workflows │ └── code_quality-aarch64-darwin.yml ├── .gitignore ├── CODEOWNERS ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── build.rs ├── create_release_podman.sh ├── docs └── usage.md ├── krunkit.entitlements └── src ├── cmdline.rs ├── context.rs ├── main.rs ├── status.rs └── virtio.rs /.github/workflows/code_quality-aarch64-darwin.yml: -------------------------------------------------------------------------------- 1 | name: Code Quality (rustfmt and clippy) 2 | on: [pull_request, create] 3 | 4 | jobs: 5 | build: 6 | if: github.event_name == 'pull_request' 7 | name: Code Quality (clippy, rustfmt) 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | rust: 12 | - stable 13 | target: 14 | - aarch64-apple-darwin 15 | steps: 16 | - name: Code checkout 17 | uses: actions/checkout@v2 18 | - name: Install Rust toolchain (${{ matrix.rust }}) 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: ${{ matrix.rust }} 22 | target: ${{ matrix.target }} 23 | override: true 24 | components: rustfmt, clippy 25 | 26 | - name: Formatting (rustfmt) 27 | run: cargo fmt -- --check 28 | 29 | - name: Clippy (default features) 30 | run: cargo clippy --target aarch64-apple-darwin -- -D warnings 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jakecorrenti @tylerfanelli @slp 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.11" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "utf8parse", 26 | ] 27 | 28 | [[package]] 29 | name = "anstyle" 30 | version = "1.0.6" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 33 | 34 | [[package]] 35 | name = "anstyle-parse" 36 | version = "0.2.3" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 39 | dependencies = [ 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-query" 45 | version = "1.0.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 48 | dependencies = [ 49 | "windows-sys", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-wincon" 54 | version = "3.0.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 57 | dependencies = [ 58 | "anstyle", 59 | "windows-sys", 60 | ] 61 | 62 | [[package]] 63 | name = "anyhow" 64 | version = "1.0.79" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" 67 | 68 | [[package]] 69 | name = "autocfg" 70 | version = "1.1.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 73 | 74 | [[package]] 75 | name = "bitflags" 76 | version = "1.3.2" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 79 | 80 | [[package]] 81 | name = "cc" 82 | version = "1.0.83" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 85 | dependencies = [ 86 | "libc", 87 | ] 88 | 89 | [[package]] 90 | name = "cfg-if" 91 | version = "1.0.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 94 | 95 | [[package]] 96 | name = "clap" 97 | version = "4.5.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" 100 | dependencies = [ 101 | "clap_builder", 102 | "clap_derive", 103 | ] 104 | 105 | [[package]] 106 | name = "clap_builder" 107 | version = "4.5.0" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" 110 | dependencies = [ 111 | "anstream", 112 | "anstyle", 113 | "clap_lex", 114 | "strsim", 115 | ] 116 | 117 | [[package]] 118 | name = "clap_derive" 119 | version = "4.5.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" 122 | dependencies = [ 123 | "heck", 124 | "proc-macro2", 125 | "quote", 126 | "syn", 127 | ] 128 | 129 | [[package]] 130 | name = "clap_lex" 131 | version = "0.7.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 134 | 135 | [[package]] 136 | name = "colorchoice" 137 | version = "1.0.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 140 | 141 | [[package]] 142 | name = "core-foundation-sys" 143 | version = "0.8.7" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 146 | 147 | [[package]] 148 | name = "crossbeam-deque" 149 | version = "0.8.5" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 152 | dependencies = [ 153 | "crossbeam-epoch", 154 | "crossbeam-utils", 155 | ] 156 | 157 | [[package]] 158 | name = "crossbeam-epoch" 159 | version = "0.9.18" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 162 | dependencies = [ 163 | "crossbeam-utils", 164 | ] 165 | 166 | [[package]] 167 | name = "crossbeam-utils" 168 | version = "0.8.20" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 171 | 172 | [[package]] 173 | name = "either" 174 | version = "1.13.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 177 | 178 | [[package]] 179 | name = "env_filter" 180 | version = "0.1.3" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 183 | dependencies = [ 184 | "log", 185 | "regex", 186 | ] 187 | 188 | [[package]] 189 | name = "env_logger" 190 | version = "0.11.8" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 193 | dependencies = [ 194 | "anstream", 195 | "anstyle", 196 | "env_filter", 197 | "jiff", 198 | "log", 199 | ] 200 | 201 | [[package]] 202 | name = "heck" 203 | version = "0.4.1" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 206 | 207 | [[package]] 208 | name = "jiff" 209 | version = "0.2.10" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" 212 | dependencies = [ 213 | "jiff-static", 214 | "log", 215 | "portable-atomic", 216 | "portable-atomic-util", 217 | "serde", 218 | ] 219 | 220 | [[package]] 221 | name = "jiff-static" 222 | version = "0.2.10" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" 225 | dependencies = [ 226 | "proc-macro2", 227 | "quote", 228 | "syn", 229 | ] 230 | 231 | [[package]] 232 | name = "krunkit" 233 | version = "0.2.1" 234 | dependencies = [ 235 | "anyhow", 236 | "clap", 237 | "env_logger", 238 | "log", 239 | "mac_address", 240 | "sysinfo", 241 | ] 242 | 243 | [[package]] 244 | name = "libc" 245 | version = "0.2.153" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 248 | 249 | [[package]] 250 | name = "log" 251 | version = "0.4.27" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 254 | 255 | [[package]] 256 | name = "mac_address" 257 | version = "1.1.5" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "4863ee94f19ed315bf3bc00299338d857d4b5bc856af375cc97d237382ad3856" 260 | dependencies = [ 261 | "nix", 262 | "winapi", 263 | ] 264 | 265 | [[package]] 266 | name = "memchr" 267 | version = "2.7.4" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 270 | 271 | [[package]] 272 | name = "memoffset" 273 | version = "0.6.5" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 276 | dependencies = [ 277 | "autocfg", 278 | ] 279 | 280 | [[package]] 281 | name = "nix" 282 | version = "0.23.2" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" 285 | dependencies = [ 286 | "bitflags", 287 | "cc", 288 | "cfg-if", 289 | "libc", 290 | "memoffset", 291 | ] 292 | 293 | [[package]] 294 | name = "ntapi" 295 | version = "0.4.1" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" 298 | dependencies = [ 299 | "winapi", 300 | ] 301 | 302 | [[package]] 303 | name = "portable-atomic" 304 | version = "1.11.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 307 | 308 | [[package]] 309 | name = "portable-atomic-util" 310 | version = "0.2.4" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 313 | dependencies = [ 314 | "portable-atomic", 315 | ] 316 | 317 | [[package]] 318 | name = "proc-macro2" 319 | version = "1.0.95" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 322 | dependencies = [ 323 | "unicode-ident", 324 | ] 325 | 326 | [[package]] 327 | name = "quote" 328 | version = "1.0.40" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 331 | dependencies = [ 332 | "proc-macro2", 333 | ] 334 | 335 | [[package]] 336 | name = "rayon" 337 | version = "1.10.0" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 340 | dependencies = [ 341 | "either", 342 | "rayon-core", 343 | ] 344 | 345 | [[package]] 346 | name = "rayon-core" 347 | version = "1.12.1" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 350 | dependencies = [ 351 | "crossbeam-deque", 352 | "crossbeam-utils", 353 | ] 354 | 355 | [[package]] 356 | name = "regex" 357 | version = "1.11.1" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 360 | dependencies = [ 361 | "aho-corasick", 362 | "memchr", 363 | "regex-automata", 364 | "regex-syntax", 365 | ] 366 | 367 | [[package]] 368 | name = "regex-automata" 369 | version = "0.4.9" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 372 | dependencies = [ 373 | "aho-corasick", 374 | "memchr", 375 | "regex-syntax", 376 | ] 377 | 378 | [[package]] 379 | name = "regex-syntax" 380 | version = "0.8.5" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 383 | 384 | [[package]] 385 | name = "serde" 386 | version = "1.0.219" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 389 | dependencies = [ 390 | "serde_derive", 391 | ] 392 | 393 | [[package]] 394 | name = "serde_derive" 395 | version = "1.0.219" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 398 | dependencies = [ 399 | "proc-macro2", 400 | "quote", 401 | "syn", 402 | ] 403 | 404 | [[package]] 405 | name = "strsim" 406 | version = "0.11.0" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 409 | 410 | [[package]] 411 | name = "syn" 412 | version = "2.0.101" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 415 | dependencies = [ 416 | "proc-macro2", 417 | "quote", 418 | "unicode-ident", 419 | ] 420 | 421 | [[package]] 422 | name = "sysinfo" 423 | version = "0.31.4" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" 426 | dependencies = [ 427 | "core-foundation-sys", 428 | "libc", 429 | "memchr", 430 | "ntapi", 431 | "rayon", 432 | "windows", 433 | ] 434 | 435 | [[package]] 436 | name = "unicode-ident" 437 | version = "1.0.12" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 440 | 441 | [[package]] 442 | name = "utf8parse" 443 | version = "0.2.1" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 446 | 447 | [[package]] 448 | name = "winapi" 449 | version = "0.3.9" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 452 | dependencies = [ 453 | "winapi-i686-pc-windows-gnu", 454 | "winapi-x86_64-pc-windows-gnu", 455 | ] 456 | 457 | [[package]] 458 | name = "winapi-i686-pc-windows-gnu" 459 | version = "0.4.0" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 462 | 463 | [[package]] 464 | name = "winapi-x86_64-pc-windows-gnu" 465 | version = "0.4.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 468 | 469 | [[package]] 470 | name = "windows" 471 | version = "0.57.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" 474 | dependencies = [ 475 | "windows-core", 476 | "windows-targets", 477 | ] 478 | 479 | [[package]] 480 | name = "windows-core" 481 | version = "0.57.0" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" 484 | dependencies = [ 485 | "windows-implement", 486 | "windows-interface", 487 | "windows-result", 488 | "windows-targets", 489 | ] 490 | 491 | [[package]] 492 | name = "windows-implement" 493 | version = "0.57.0" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" 496 | dependencies = [ 497 | "proc-macro2", 498 | "quote", 499 | "syn", 500 | ] 501 | 502 | [[package]] 503 | name = "windows-interface" 504 | version = "0.57.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" 507 | dependencies = [ 508 | "proc-macro2", 509 | "quote", 510 | "syn", 511 | ] 512 | 513 | [[package]] 514 | name = "windows-result" 515 | version = "0.1.2" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" 518 | dependencies = [ 519 | "windows-targets", 520 | ] 521 | 522 | [[package]] 523 | name = "windows-sys" 524 | version = "0.52.0" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 527 | dependencies = [ 528 | "windows-targets", 529 | ] 530 | 531 | [[package]] 532 | name = "windows-targets" 533 | version = "0.52.6" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 536 | dependencies = [ 537 | "windows_aarch64_gnullvm", 538 | "windows_aarch64_msvc", 539 | "windows_i686_gnu", 540 | "windows_i686_gnullvm", 541 | "windows_i686_msvc", 542 | "windows_x86_64_gnu", 543 | "windows_x86_64_gnullvm", 544 | "windows_x86_64_msvc", 545 | ] 546 | 547 | [[package]] 548 | name = "windows_aarch64_gnullvm" 549 | version = "0.52.6" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 552 | 553 | [[package]] 554 | name = "windows_aarch64_msvc" 555 | version = "0.52.6" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 558 | 559 | [[package]] 560 | name = "windows_i686_gnu" 561 | version = "0.52.6" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 564 | 565 | [[package]] 566 | name = "windows_i686_gnullvm" 567 | version = "0.52.6" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 570 | 571 | [[package]] 572 | name = "windows_i686_msvc" 573 | version = "0.52.6" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 576 | 577 | [[package]] 578 | name = "windows_x86_64_gnu" 579 | version = "0.52.6" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 582 | 583 | [[package]] 584 | name = "windows_x86_64_gnullvm" 585 | version = "0.52.6" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 588 | 589 | [[package]] 590 | name = "windows_x86_64_msvc" 591 | version = "0.52.6" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 594 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "krunkit" 3 | version = "0.2.1" 4 | authors = ["Tyler Fanelli "] 5 | edition = "2021" 6 | description = "CLI tool to start VMs with libkrun" 7 | readme = "README.md" 8 | license = "Apache-2.0" 9 | build = "build.rs" 10 | 11 | [dependencies] 12 | anyhow = "1.0.79" 13 | clap = { version = "4.5.0", features = ["derive"] } 14 | mac_address = "1.1.5" 15 | sysinfo = "0.31.4" 16 | log = "0.4.0" 17 | env_logger = "0.11.8" 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OS = $(shell uname -s) 2 | KRUNKIT_RELEASE = target/release/krunkit 3 | KRUNKIT_DEBUG = target/debug/krunkit 4 | KRUNKIT_HOMEBREW = /opt/homebrew/opt/libkrun-efi/lib/libkrun-efi.dylib 5 | 6 | ifeq ($(PREFIX),) 7 | PREFIX := /usr/local 8 | endif 9 | 10 | .PHONY: install clean 11 | 12 | all: $(KRUNKIT_RELEASE) 13 | 14 | debug: $(KRUNKIT_DEBUG) 15 | 16 | $(KRUNKIT_RELEASE): 17 | cargo build --release 18 | ifeq ($(OS),Darwin) 19 | ifneq ($(LIBKRUN_EFI),) 20 | install_name_tool -change $(KRUNKIT_HOMEBREW) $(LIBKRUN_EFI) $@ 21 | endif 22 | codesign --entitlements krunkit.entitlements --force -s - $@ 23 | endif 24 | 25 | $(KRUNKIT_DEBUG): 26 | cargo build --debug 27 | 28 | install: $(KRUNKIT_RELEASE) 29 | install -d $(DESTDIR)$(PREFIX)/bin 30 | install -m 755 $(KRUNKIT_RELEASE) $(DESTDIR)$(PREFIX)/bin 31 | 32 | clean: 33 | cargo clean 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # krunkit 2 | 3 | `krunkit` is a tool to launch configurable virtual machines using the [libkrun](https://github.com/containers/libkrun) platform. 4 | 5 | ## Installation 6 | 7 | `krunkit` relies on the `efi` flavor of `libkrun`. At present, `libkrun-efi` is only available on macOS. We provide a Homebrew repository to install `krunkit` and all of its dependencies, installable with: 8 | 9 | ``` 10 | $ brew tap slp/krunkit 11 | $ brew install krunkit 12 | ``` 13 | 14 | ## Building from source 15 | 16 | As noted above, `krunkit` relies on the `efi` flavor of `libkrun`. Ensure that is installed on your system. 17 | 18 | ``` 19 | # If libkrun-efi.dylib is not located at /opt/homebrew/opt/libkrun-efi/lib/ 20 | # provide the path at which it's located using the LIBKRUN_EFI variable. Otherwise, 21 | # the Makefile will default to using the /opt/homebrew/... path. 22 | $ make LIBKRUN_EFI= 23 | 24 | $ sudo make install 25 | ``` 26 | 27 | ## Usage 28 | 29 | See [`docs/usage.md`](./docs/usage.md). 30 | 31 | License: Apache-2.0 32 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // SDPX-License-Identifier: Apache-2.0 2 | 3 | fn main() { 4 | #[cfg(target_os = "macos")] 5 | println!("cargo:rustc-link-search=/opt/homebrew/lib"); 6 | } 7 | -------------------------------------------------------------------------------- /create_release_podman.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z $HOMEBREW_PREFIX ]; then 4 | echo "Can't find Homebrew prefix" 5 | exit -1 6 | fi 7 | 8 | RPATH=/opt/podman/lib 9 | VERSION=`grep ^version Cargo.toml | sed -E 's/version = "(.*)"/\1/'` 10 | 11 | mkdir -p release 12 | mkdir -p release/bin 13 | mkdir -p release/lib 14 | 15 | BIN=release/bin 16 | LIB=release/lib 17 | 18 | cp $HOMEBREW_PREFIX/bin/krunkit $BIN 19 | install_name_tool -change /opt/homebrew/opt/libkrun-efi/lib/libkrun-efi.dylib @rpath/libkrun-efi.dylib $BIN/krunkit 20 | install_name_tool -add_rpath $RPATH $BIN/krunkit 21 | codesign --remove-signature $BIN/krunkit 22 | 23 | cp $HOMEBREW_PREFIX/lib/libkrun-efi.dylib $LIB 24 | install_name_tool -id @rpath/libkrun-efi.dylib $LIB/libkrun-efi.dylib 25 | install_name_tool -change /opt/homebrew/opt/libepoxy/lib/libepoxy.0.dylib @rpath/libepoxy.0.dylib $LIB/libkrun-efi.dylib 26 | install_name_tool -change /opt/homebrew/opt/virglrenderer/lib/libvirglrenderer.1.dylib @rpath/libvirglrenderer.1.dylib $LIB/libkrun-efi.dylib 27 | codesign --remove-signature $LIB/libkrun-efi.dylib 28 | 29 | cp $HOMEBREW_PREFIX/lib/libepoxy.0.dylib $LIB 30 | install_name_tool -id @rpath/libepoxy.0.dylib $LIB/libepoxy.0.dylib 31 | codesign --remove-signature $LIB/libepoxy.0.dylib 32 | 33 | cp $HOMEBREW_PREFIX/lib/libvirglrenderer.1.dylib $LIB 34 | install_name_tool -id @rpath/libvirglrenderer.1.dylib $LIB/libvirglrenderer.1.dylib 35 | install_name_tool -change /opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib @rpath/libMoltenVK.dylib $LIB/libvirglrenderer.1.dylib 36 | install_name_tool -change /opt/homebrew/opt/libepoxy/lib/libepoxy.0.dylib @rpath/libepoxy.0.dylib $LIB/libvirglrenderer.1.dylib 37 | codesign --remove-signature $LIB/libvirglrenderer.1.dylib 38 | 39 | cp $HOMEBREW_PREFIX/lib/libMoltenVK.dylib $LIB 40 | install_name_tool -id @rpath/libMoltenVK.dylib $LIB/libMoltenVK.dylib 41 | codesign --remove-signature $LIB/libMoltenVK.dylib 42 | 43 | cd release 44 | tar czf ../krunkit-podman-unsigned-$VERSION.tgz * 45 | 46 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # krunkit Command Line 2 | 3 | `krunkit` can launch configurable virtual machines using macOS's hypervisor framework and the `libkrun` virtual 4 | machine monitor library. The `libkrun` virtual machine configuration can be specified from command line arguments. 5 | 6 | Specifying a virtual machine's vCPU and RAM allocation is required. Adding devices is optional, yet most workloads 7 | will require a root disk to be useful. 8 | 9 | ## Generic Options 10 | 11 | - `--krun-log-level` 12 | 13 | Set the log level for libkrun. Supported values: 0=off, 1=error, 2=warn, 3=info (default), 4=debug, 5 or more=trace. 14 | 15 | - `--restful-uri` 16 | 17 | The URI (address) of the RESTful service. If not specified, defaults to `tcp://localhost:8081`. `tcp` is the only 18 | valid scheme. 19 | 20 | ### Virtual Machine Resources 21 | 22 | - `--cpus` 23 | 24 | Number of vCPUs available to a virtual machine. 25 | 26 | - `--memory` 27 | 28 | Amount of RAM available to a virtual machine. Value is in MiB (mebibytes, 1024^2 bytes). 29 | 30 | #### Example 31 | 32 | This configures a virtual machine to use two vCPUs and 2048 MiB of RAM: 33 | 34 | ``` 35 | --cpus 2 --memory 2048 36 | ``` 37 | 38 | ## Device Configuration 39 | 40 | Various virtio devices can be added to a virtual machine. They are all paravirtualized devices that can be 41 | specified using the `--device` flag. 42 | 43 | ### Disk 44 | 45 | The `virtio-blk` option adds a disk to a virtual machine. This disk is backed by an image file on the host 46 | machine. At least one virtio-blk device must be specified on the commandline. The first virtio-blk argument 47 | will be used as a virtual machine's root disk (`/dev/vda`). The subsequent virtio-blk arguments will be used 48 | as a virtual machine's data disk(s) (`/dev/vd[b-z]`). 49 | 50 | #### Arguments 51 | 52 | - `path`: Path to the disk image file. 53 | - `format`: Format of the disk image. Supported formats: raw, qcow2. 54 | 55 | #### Example 56 | 57 | This adds a virtio-blk device to a virtual machine which will be backed by an image at 58 | `/Users/user/disk-image.raw`: 59 | 60 | ``` 61 | --device virtio-blk,path=/Users/user/disk-image.raw,format=raw 62 | ``` 63 | 64 | ### Networking 65 | 66 | The `virtio-net` option adds a network interface to a virtual machine. 67 | 68 | #### Arguments 69 | 70 | - `unixSocketPath`: Path to a UNIX socket to attach to the guest network interface. 71 | - `mac`: MAC address of a virtual machine. 72 | 73 | #### Example 74 | 75 | This adds a virtio-net device to a virtual machine and redirects all guest network traffic to the corresponding 76 | socket at `/Users/user/vm-network.sock` with a MAC address of `ff:ff:ff:ff:ff:ff`: 77 | 78 | ``` 79 | --device virtio-net,unixSocketPath=/Users/user/vm-network.sock,mac=ff:ff:ff:ff:ff:ff 80 | ``` 81 | 82 | ### Serial Port 83 | 84 | The `virtio-serial` option adds a serial device to a virtual machine. This allows for redirection of virtual 85 | machine text output. 86 | 87 | #### Arguments 88 | 89 | - `logFilePath`: Path to a file in which a virtual machine's serial port output should be written. 90 | 91 | #### Example 92 | 93 | This adds a virtio-serial device to a virtual machine, and will redirect the virtual machine's text output to 94 | `/Users/user/vm-output.log`: 95 | 96 | ``` 97 | --device virtio-serial,logFilePath=/Users/user/vm-output.log 98 | ``` 99 | 100 | ### vsock 101 | 102 | The `virtio-vsock` option adds a vsock communication channel between the host and guest. macOS does not have host 103 | support for `AF_VSOCK` sockets, so the virtual machine monitor will maintain a vsock-UNIX socket proxy to 104 | facilitate communication between the two. 105 | 106 | Multiple instances of a `virtio-vsock` device can be specified, yet port numbers for these sockets must be unique. 107 | 108 | #### Arguments 109 | 110 | - `port`: `AF_VSOCK` port to connect to on the guest. 111 | - `socketURL`: Path to the UNIX socket on the host. 112 | 113 | #### Example 114 | 115 | This adds a virtio-vsock device to a virtual machine, and will forward all guest socket communication to 116 | `/Users/user/vm-socket.sock` (a virtual machine can connect to the vsock on port `1024`): 117 | 118 | ``` 119 | --device virtio-vsock,port=1024,socketURL=/Users/user/vm-socket.sock 120 | ``` 121 | 122 | ### File Sharing 123 | 124 | The `virtio-fs` option allows a guest to share a file system directory with a host. The directory can be mounted 125 | in the guest with `mount -t virtiofs MOUNT_TAG /mnt`, with `MOUNT_TAG` corresponding to the mount tag specified in 126 | the arguments. 127 | 128 | #### Arguments 129 | 130 | - `sharedDir`: Path to the host directory that will be shared with the guest. 131 | - `mountTag`: Tag to be used to mount the shared directory in the guest. 132 | 133 | #### Example 134 | 135 | This will share `/Users/user/shared-dir` with the guest: 136 | 137 | ``` 138 | --device virtio-fs,sharedDir=/Users/user/shared-dir,mountTag=MOUNT_TAG 139 | ``` 140 | 141 | ## Restful Service 142 | 143 | Recall that the RESTful service is started at the address specified in the `--restful-uri` argument (or 144 | `tcp://localhost:8081` if not specified). 145 | 146 | ### Getting a virtual machine's state 147 | 148 | Used to obtain the state of a running virtual machine. 149 | 150 | `GET /vm/state` 151 | 152 | Response: `VirtualMachineState{Running, Stopped}` 153 | 154 | ### Stopping a virtual machine 155 | 156 | `POST /vm/state` `{ "state": "Stop" }` 157 | 158 | Response: `VirtualMachineStateStopped` 159 | -------------------------------------------------------------------------------- /krunkit.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.hypervisor 6 | 7 | com.apple.security.cs.disable-library-validationr 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/cmdline.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | use crate::{status::RestfulUriAddr, virtio::VirtioDeviceConfig}; 4 | 5 | use std::{collections::HashMap, path::PathBuf, str::FromStr}; 6 | 7 | use anyhow::{anyhow, Context, Result}; 8 | use clap::Parser; 9 | 10 | /// Command line arguments to configure a krun VM. 11 | #[derive(Clone, Debug, Parser)] 12 | #[command(version, about, long_about = None)] 13 | pub struct Args { 14 | /// Number of vCPUs for the VM. 15 | #[arg(long, short)] 16 | pub cpus: u8, 17 | 18 | /// Amount of RAM available to VM. 19 | #[arg(long, short)] 20 | pub memory: u32, 21 | 22 | /// Bootloader configuration. 23 | #[arg(long)] 24 | pub bootloader: Option, 25 | 26 | /// virtio devices to configure in the VM. 27 | #[arg(long = "device")] 28 | pub devices: Vec, 29 | 30 | /// URI of the status/shutdown listener. 31 | #[arg(long = "restful-uri")] 32 | pub restful_uri: Option, 33 | 34 | /// GUI option for compatibility with vfkit (ignored). 35 | #[arg(long, default_value_t = false)] 36 | pub gui: bool, 37 | 38 | /// SMBIOS OEM String 39 | #[arg(long = "oem-string")] 40 | pub oem_strings: Option>, 41 | 42 | /// Log level for libkrun (0=off, 1=error, 2=warn, 3=info, 4=debug, 5 or higher=trace) 43 | #[arg(long = "krun-log-level", default_value_t = 3)] 44 | pub krun_log_level: u32, 45 | 46 | /// Enable Nested Virtualization. 47 | #[arg(long, short)] 48 | pub nested: bool, 49 | } 50 | 51 | /// Parse the input string into a hash map of key value pairs, associating the argument with its 52 | /// respective value. 53 | pub fn parse_args(s: String) -> Result, anyhow::Error> { 54 | let mut map: HashMap = HashMap::new(); 55 | let list: Vec = s.split(',').map(|s| s.to_string()).collect(); 56 | 57 | for arg in list { 58 | let arg_parts: Vec<&str> = arg.split('=').collect(); 59 | let key = arg_parts[0].to_string(); 60 | let val = match arg_parts.len() { 61 | 1 => String::new(), 62 | 2 => arg_parts[1].to_string(), 63 | _ => return Err(anyhow!(format!("invalid argument format: {arg}"))), 64 | }; 65 | let res = map.insert(key, val); 66 | if res.is_some() { 67 | return Err(anyhow!(format!("argument {arg} is only expected once"))); 68 | } 69 | } 70 | 71 | Ok(map) 72 | } 73 | 74 | /// Check the arguments hash map if all required arguments are present 75 | pub fn check_required_args( 76 | args: &HashMap, 77 | label: &str, 78 | required: &[&str], 79 | ) -> Result<(), anyhow::Error> { 80 | for &r in required { 81 | if !args.contains_key(r) { 82 | return Err(anyhow!(format!("{label} is missing argument: {r}"))); 83 | } 84 | } 85 | 86 | Ok(()) 87 | } 88 | 89 | /// Check the arguments hash map if any unknown arguments exist 90 | pub fn check_unknown_args(args: HashMap, label: &str) -> Result<(), anyhow::Error> { 91 | if !args.is_empty() { 92 | let unknown_args: Vec = args 93 | .into_iter() 94 | .map(|arg| format!("{}={}", arg.0, arg.1)) 95 | .collect(); 96 | return Err(anyhow!(format!( 97 | "unknown {} arguments: {:?}", 98 | label, unknown_args 99 | ))); 100 | } 101 | 102 | Ok(()) 103 | } 104 | 105 | /// A wrapper of all data associated with the bootloader argument. 106 | mod bootloader { 107 | use super::*; 108 | 109 | #[derive(Clone, Debug)] 110 | pub struct Config { 111 | fw: BootloaderFw, 112 | vstore: PathBuf, 113 | action: Action, 114 | } 115 | 116 | impl FromStr for Config { 117 | type Err = anyhow::Error; 118 | 119 | fn from_str(s: &str) -> Result { 120 | let mut args = parse_args(s.to_string())?; 121 | check_required_args(&args, "bootloader", &["efi", "variable-store", "create"])?; 122 | 123 | let fw = args.remove("efi").unwrap(); 124 | if !fw.is_empty() { 125 | return Err(anyhow!(format!("unknown bootloader argument: efi={fw}"))); 126 | } 127 | 128 | let v = args.remove("variable-store").unwrap(); 129 | 130 | let action = args.remove("create").unwrap(); 131 | if !action.is_empty() { 132 | return Err(anyhow!(format!( 133 | "unknown bootloader argument: create={action}" 134 | ))); 135 | } 136 | 137 | check_unknown_args(args, "bootloader")?; 138 | 139 | let fw = BootloaderFw::from_str("efi")?; 140 | let v = Vstore::from_str(v.as_str())?; 141 | let action = Action::from_str("create")?; 142 | 143 | Ok(Self { 144 | fw, 145 | vstore: v.0, 146 | action, 147 | }) 148 | } 149 | } 150 | 151 | /// Bootloader firmware identifier. 152 | #[derive(Clone, Debug)] 153 | pub enum BootloaderFw { 154 | Efi, 155 | } 156 | 157 | impl FromStr for BootloaderFw { 158 | type Err = anyhow::Error; 159 | 160 | fn from_str(s: &str) -> Result { 161 | let string = s.to_string().to_lowercase(); 162 | 163 | match string.as_str() { 164 | "efi" => Ok(Self::Efi), 165 | _ => Err(anyhow!("invalid bootloader firmware option: {}", string)), 166 | } 167 | } 168 | } 169 | 170 | /// Variable store. 171 | #[derive(Clone, Debug)] 172 | pub struct Vstore(PathBuf); 173 | 174 | impl FromStr for Vstore { 175 | type Err = anyhow::Error; 176 | 177 | fn from_str(s: &str) -> Result { 178 | Ok(Self( 179 | PathBuf::from_str(s).context("variable-store argument not a valid path")?, 180 | )) 181 | } 182 | } 183 | 184 | /// Bootloader action. 185 | #[derive(Clone, Debug)] 186 | pub enum Action { 187 | Create, 188 | } 189 | 190 | impl FromStr for Action { 191 | type Err = anyhow::Error; 192 | 193 | fn from_str(s: &str) -> Result { 194 | let string = s.to_string().to_lowercase(); 195 | 196 | match string.as_str() { 197 | "create" => Ok(Self::Create), 198 | _ => Err(anyhow!("invalid bootloader action: {}", string)), 199 | } 200 | } 201 | } 202 | } 203 | 204 | mod tests { 205 | #[test] 206 | fn virtio_blk_argument_ordering() { 207 | let in_order = 208 | super::parse_args(String::from("path=/Users/user/disk-image.raw,format=raw")).unwrap(); 209 | let out_of_order = 210 | super::parse_args(String::from("format=raw,path=/Users/user/disk-image.raw")).unwrap(); 211 | 212 | let mut expected = std::collections::HashMap::new(); 213 | expected.insert("path".to_string(), "/Users/user/disk-image.raw".to_string()); 214 | expected.insert("format".to_string(), "raw".to_string()); 215 | 216 | assert_eq!(in_order, out_of_order); 217 | assert_eq!(in_order, expected); 218 | } 219 | 220 | #[test] 221 | fn virtio_net_argument_ordering() { 222 | let in_order = super::parse_args(String::from( 223 | "unixSocketPath=/Users/user/vm-network.sock,mac=ff:ff:ff:ff:ff:ff", 224 | )) 225 | .unwrap(); 226 | let out_of_order = super::parse_args(String::from( 227 | "mac=ff:ff:ff:ff:ff:ff,unixSocketPath=/Users/user/vm-network.sock", 228 | )) 229 | .unwrap(); 230 | 231 | let mut expected = std::collections::HashMap::new(); 232 | expected.insert( 233 | "unixSocketPath".to_string(), 234 | "/Users/user/vm-network.sock".to_string(), 235 | ); 236 | expected.insert("mac".to_string(), "ff:ff:ff:ff:ff:ff".to_string()); 237 | 238 | assert_eq!(in_order, out_of_order); 239 | assert_eq!(in_order, expected); 240 | } 241 | 242 | #[test] 243 | fn virtio_vsock_argument_ordering() { 244 | let in_order = super::parse_args(String::from( 245 | "port=1025,socketURL=/Users/user/vsock2.sock,listen", 246 | )) 247 | .unwrap(); 248 | let out_of_order = super::parse_args(String::from( 249 | "port=1025,listen,socketURL=/Users/user/vsock2.sock", 250 | )) 251 | .unwrap(); 252 | 253 | let mut expected = std::collections::HashMap::new(); 254 | expected.insert("port".to_string(), "1025".to_string()); 255 | expected.insert( 256 | "socketURL".to_string(), 257 | "/Users/user/vsock2.sock".to_string(), 258 | ); 259 | expected.insert("listen".to_string(), String::new()); 260 | 261 | assert_eq!(in_order, out_of_order); 262 | assert_eq!(in_order, expected); 263 | } 264 | 265 | #[test] 266 | fn virtio_fs_argument_ordering() { 267 | let in_order = super::parse_args(String::from( 268 | "sharedDir=/Users/user/shared-dir,mountTag=MOUNT_TAG", 269 | )) 270 | .unwrap(); 271 | let out_of_order = super::parse_args(String::from( 272 | "mountTag=MOUNT_TAG,sharedDir=/Users/user/shared-dir", 273 | )) 274 | .unwrap(); 275 | 276 | let mut expected = std::collections::HashMap::new(); 277 | expected.insert( 278 | "sharedDir".to_string(), 279 | "/Users/user/shared-dir".to_string(), 280 | ); 281 | expected.insert("mountTag".to_string(), "MOUNT_TAG".to_string()); 282 | 283 | assert_eq!(in_order, out_of_order); 284 | assert_eq!(in_order, expected); 285 | } 286 | 287 | #[test] 288 | fn virtio_gpu_argument_ordering() { 289 | let in_order = super::parse_args(String::from("height=50,width=25")).unwrap(); 290 | let out_of_order = super::parse_args(String::from("width=25,height=50")).unwrap(); 291 | 292 | let mut expected = std::collections::HashMap::new(); 293 | expected.insert("height".to_string(), "50".to_string()); 294 | expected.insert("width".to_string(), "25".to_string()); 295 | 296 | assert_eq!(in_order, out_of_order); 297 | assert_eq!(in_order, expected); 298 | } 299 | 300 | #[test] 301 | fn argument_parsing() { 302 | let s = String::from("port=1025,socketURL=/Users/user/vsock2.sock,listen"); 303 | let args = super::parse_args(s).unwrap(); 304 | 305 | let mut expected = std::collections::HashMap::new(); 306 | expected.insert("port".to_string(), "1025".to_string()); 307 | expected.insert( 308 | "socketURL".to_string(), 309 | "/Users/user/vsock2.sock".to_string(), 310 | ); 311 | expected.insert("listen".to_string(), String::new()); 312 | assert_eq!(expected, args); 313 | } 314 | 315 | #[test] 316 | fn required_args() { 317 | let required = &["port", "socketURL"]; 318 | let s = String::from("port=1025,socketURL=/Users/user/vsock2.sock,listen"); 319 | let args = super::parse_args(s).unwrap(); 320 | 321 | assert_eq!( 322 | super::check_required_args(&args, "", required).is_ok(), 323 | true 324 | ); 325 | 326 | let required = &["port", "wrong"]; 327 | assert_ne!( 328 | super::check_required_args(&args, "", required).is_ok(), 329 | true 330 | ); 331 | } 332 | 333 | #[test] 334 | fn unknown_args() { 335 | use std::collections::HashMap; 336 | 337 | let args: HashMap = HashMap::new(); 338 | assert_eq!(super::check_unknown_args(args, "").is_ok(), true); 339 | 340 | let mut args: HashMap = HashMap::new(); 341 | args.insert("foo".to_string(), "bar".to_string()); 342 | assert_ne!(super::check_unknown_args(args, "").is_ok(), true); 343 | } 344 | 345 | #[cfg(target_os = "macos")] 346 | #[test] 347 | fn mac_cmdline_ordering_argtest() { 348 | use super::*; 349 | use crate::virtio::*; 350 | 351 | use std::net::Ipv4Addr; 352 | 353 | use mac_address::MacAddress; 354 | 355 | let cmdline = vec![ 356 | "krunkit", 357 | "--cpus", 358 | "4", 359 | "--memory", 360 | "2048", 361 | "--bootloader", 362 | "efi,variable-store=/Users/user/bootloader,create", 363 | "--device", 364 | "virtio-blk,path=/Users/user/root.qcow2,format=qcow2", 365 | "--device", 366 | "virtio-rng", 367 | "--device", 368 | "virtio-serial,logFilePath=/Users/user/serial.log", 369 | "--device", 370 | "virtio-blk,path=/Users/user/data.raw,format=raw", 371 | "--device", 372 | "virtio-vsock,port=1024,socketURL=/Users/user/vsock1.sock,listen", 373 | "--device", 374 | "virtio-net,unixSocketPath=/Users/user/net.sock,mac=00:00:00:00:00:00", 375 | "--device", 376 | "virtio-fs,sharedDir=/Users/user/fs,mountTag=guest-dir", 377 | "--device", 378 | "virtio-vsock,port=1025,socketURL=/Users/user/vsock2.sock,listen", 379 | "--device", 380 | "virtio-gpu,width=800,height=600", 381 | "--device", 382 | "virtio-input,keyboard", 383 | "--restful-uri", 384 | "tcp://localhost:49573", 385 | "--gui", 386 | "--krun-log-level", 387 | "5", 388 | ]; 389 | 390 | let mut args = Args::try_parse_from(cmdline).unwrap(); 391 | 392 | let input = args 393 | .devices 394 | .pop() 395 | .expect("expected 10th virtio device config"); 396 | if let VirtioDeviceConfig::Input(input) = input { 397 | assert_eq!(input, InputConfig::Keyboard); 398 | } else { 399 | panic!("expected virtio-input device as 10th device config argument"); 400 | } 401 | 402 | let gpu = args 403 | .devices 404 | .pop() 405 | .expect("expected 9th virtio device config"); 406 | if let VirtioDeviceConfig::Gpu(gpu) = gpu { 407 | assert_eq!(gpu.width, 800); 408 | assert_eq!(gpu.height, 600); 409 | } else { 410 | panic!("expected virtio-gpu device as 9th device config argument"); 411 | } 412 | 413 | let vsock = args 414 | .devices 415 | .pop() 416 | .expect("expected 8th virtio device config"); 417 | if let VirtioDeviceConfig::Vsock(v) = vsock { 418 | assert_eq!(v.port, 1025); 419 | assert_eq!( 420 | v.socket_url, 421 | PathBuf::from_str("/Users/user/vsock2.sock").unwrap() 422 | ); 423 | assert_eq!(v.action, VsockAction::Listen); 424 | } else { 425 | panic!("expected virtio-vsock device as 8th device config argument"); 426 | } 427 | 428 | let fs = args 429 | .devices 430 | .pop() 431 | .expect("expected 7th virtio device config"); 432 | if let VirtioDeviceConfig::Fs(fs) = fs { 433 | assert_eq!(fs.shared_dir, PathBuf::from_str("/Users/user/fs").unwrap()); 434 | assert_eq!(fs.mount_tag, PathBuf::from_str("guest-dir").unwrap()); 435 | } else { 436 | panic!("expected virtio-fs device as 7th device config argument"); 437 | } 438 | 439 | let net = args 440 | .devices 441 | .pop() 442 | .expect("expected 6th virtio device config"); 443 | if let VirtioDeviceConfig::Net(net) = net { 444 | assert_eq!( 445 | net.unix_socket_path, 446 | PathBuf::from_str("/Users/user/net.sock").unwrap() 447 | ); 448 | assert_eq!(net.mac_address, MacAddress::new([0, 0, 0, 0, 0, 0])); 449 | } else { 450 | panic!("expected virtio-net device as 6th device config argument"); 451 | } 452 | 453 | let vsock = args 454 | .devices 455 | .pop() 456 | .expect("expected 5th virtio device config"); 457 | if let VirtioDeviceConfig::Vsock(v) = vsock { 458 | assert_eq!(v.port, 1024); 459 | assert_eq!( 460 | v.socket_url, 461 | PathBuf::from_str("/Users/user/vsock1.sock").unwrap() 462 | ); 463 | assert_eq!(v.action, VsockAction::Listen); 464 | } else { 465 | panic!("expected virtio-vsock device as 5th device config argument"); 466 | } 467 | 468 | let blk = args 469 | .devices 470 | .pop() 471 | .expect("expected 4th virtio device config"); 472 | if let VirtioDeviceConfig::Blk(blk) = blk { 473 | assert_eq!(blk.path, PathBuf::from_str("/Users/user/data.raw").unwrap()); 474 | assert_eq!(blk.format, DiskImageFormat::Raw); 475 | } else { 476 | panic!("expected virtio-blk device as 4th device config argument"); 477 | } 478 | 479 | let serial = args 480 | .devices 481 | .pop() 482 | .expect("expected 3rd virtio device config"); 483 | if let VirtioDeviceConfig::Serial(serial) = serial { 484 | assert_eq!( 485 | serial.log_file_path, 486 | PathBuf::from_str("/Users/user/serial.log").unwrap() 487 | ); 488 | } else { 489 | panic!("expected virtio-serial device as 3rd device config argument"); 490 | } 491 | 492 | let rng = args 493 | .devices 494 | .pop() 495 | .expect("expected 2nd virtio device config"); 496 | 497 | if VirtioDeviceConfig::Rng != rng { 498 | panic!("expected virtio-rng device as 2nd device config argument"); 499 | } 500 | 501 | let blk = args 502 | .devices 503 | .pop() 504 | .expect("expected 1st virtio device config"); 505 | if let VirtioDeviceConfig::Blk(blk) = blk { 506 | assert_eq!( 507 | blk.path, 508 | PathBuf::from_str("/Users/user/root.qcow2").unwrap() 509 | ); 510 | assert_eq!(blk.format, DiskImageFormat::Qcow2); 511 | } else { 512 | panic!("expected virtio-blk device as 1st device config argument"); 513 | } 514 | 515 | let restful_uri = args.restful_uri.expect("restful-uri argument not found"); 516 | 517 | assert_eq!(restful_uri.ip_addr, Ipv4Addr::new(127, 0, 0, 1)); 518 | assert_eq!(restful_uri.port, 49573); 519 | 520 | assert_eq!(args.gui, true); 521 | assert_eq!(args.krun_log_level, 5); 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | use super::*; 4 | 5 | use crate::{ 6 | status::{get_shutdown_eventfd, status_listener}, 7 | virtio::KrunContextSet, 8 | }; 9 | 10 | use std::ffi::{c_char, CString}; 11 | use std::{convert::TryFrom, ptr, thread}; 12 | 13 | use anyhow::{anyhow, Context}; 14 | 15 | #[link(name = "krun-efi")] 16 | extern "C" { 17 | fn krun_create_ctx() -> i32; 18 | fn krun_set_log_level(level: u32) -> i32; 19 | fn krun_set_gpu_options2(ctx_id: u32, virgl_flags: u32, shm_size: u64) -> i32; 20 | fn krun_set_vm_config(ctx_id: u32, num_vcpus: u8, ram_mib: u32) -> i32; 21 | fn krun_set_smbios_oem_strings(ctx_id: u32, oem_strings: *const *const c_char) -> i32; 22 | fn krun_set_nested_virt(ctx_id: u32, enabled: bool) -> i32; 23 | fn krun_check_nested_virt() -> i32; 24 | fn krun_start_enter(ctx_id: u32) -> i32; 25 | } 26 | 27 | const VIRGLRENDERER_VENUS: u32 = 1 << 6; 28 | const VIRGLRENDERER_NO_VIRGL: u32 = 1 << 7; 29 | 30 | /// A wrapper of all data used to configure the krun VM. 31 | pub struct KrunContext { 32 | id: u32, 33 | args: Args, 34 | } 35 | 36 | /// Create a krun context from the command line arguments. 37 | impl TryFrom for KrunContext { 38 | type Error = anyhow::Error; 39 | 40 | fn try_from(args: Args) -> Result { 41 | // Start by setting up the desired log level for libkrun. 42 | unsafe { krun_set_log_level(args.krun_log_level) }; 43 | 44 | let log_level = match args.krun_log_level { 45 | 0 => "off", 46 | 1 => "error", 47 | 2 => "warn", 48 | 3 => "info", 49 | 4 => "debug", 50 | _ => "trace", 51 | }; 52 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level)) 53 | .init(); 54 | 55 | // Create a new context in libkrun. Store identifier to later use to configure VM 56 | // resources and devices. 57 | let id = unsafe { krun_create_ctx() }; 58 | if id < 0 { 59 | return Err(anyhow!("unable to create libkrun context")); 60 | } 61 | 62 | // Safe to unwrap, as it's already ensured that id >= 0. 63 | let id = u32::try_from(id).unwrap(); 64 | 65 | // Set the krun VM's number of vCPUs and amount of memory allocated. 66 | // 67 | // libkrun has a max of 8 vCPUs allowed. 68 | if args.cpus == 0 { 69 | return Err(anyhow!("zero vcpus inputted (invalid)")); 70 | } else if args.cpus > 8 { 71 | return Err(anyhow!("too many vCPUs configured (max 8)")); 72 | } 73 | 74 | if args.memory == 0 { 75 | return Err(anyhow!("zero MiB RAM inputted (invalid)")); 76 | } else if args.memory > 61440 { 77 | // Limit RAM to 60 GiB of the 62 GiB upper bound to leave room for VRAM. 78 | return Err(anyhow!( 79 | "requested RAM larger than upper limit of 61440 MiB" 80 | )); 81 | } 82 | 83 | if unsafe { krun_set_vm_config(id, args.cpus, args.memory) } < 0 { 84 | return Err(anyhow!("unable to set krun vCPU/RAM configuration")); 85 | } 86 | 87 | // Temporarily enable GPU by default 88 | let virgl_flags = VIRGLRENDERER_VENUS | VIRGLRENDERER_NO_VIRGL; 89 | let sys = sysinfo::System::new_all(); 90 | // Limit RAM + VRAM to 64 GB (36 bit IPA address limit) minus 2 GB (start address plus rounding). 91 | let rounded_mem = ((args.memory as u64) / 1024 + 1) * 1024; 92 | let vram = std::cmp::min((63488 - rounded_mem) * 1024 * 1024, sys.total_memory()); 93 | if unsafe { krun_set_gpu_options2(id, virgl_flags, vram) } < 0 { 94 | return Err(anyhow!("unable to set krun vCPU/RAM configuration")); 95 | } 96 | 97 | // Configure each virtio device to include in the VM. 98 | for device in &args.devices { 99 | unsafe { device.krun_ctx_set(id)? } 100 | } 101 | 102 | set_smbios_oem_strings(id, &args.oem_strings)?; 103 | 104 | if args.nested { 105 | match unsafe { krun_check_nested_virt() } { 106 | 1 => { 107 | if unsafe { krun_set_nested_virt(id, args.nested) } < 0 { 108 | return Err(anyhow!("krun nested virtualization reported as supported, yet failed to enable")); 109 | } 110 | } 111 | 0 => log::debug!("nested virtualization is not supported on this host. -n,--nested argument ignored"), 112 | _ => return Err(anyhow!("unable to check nested virtualization is supported on this host")), 113 | } 114 | } 115 | 116 | Ok(Self { id, args }) 117 | } 118 | } 119 | 120 | impl KrunContext { 121 | /// Spawn a thread to listen for shutdown requests and run the workload. If behaving properly, 122 | /// the main thread will never return from this function. 123 | pub fn run(&self) -> Result<(), anyhow::Error> { 124 | // Get the krun shutdown file descriptor and listen to shutdown requests on a new thread. 125 | let shutdown_eventfd = unsafe { get_shutdown_eventfd(self.id) }; 126 | let uri = self.args.restful_uri.clone(); 127 | 128 | thread::spawn(move || status_listener(shutdown_eventfd, uri).unwrap()); 129 | 130 | // Run the workload. 131 | if unsafe { krun_start_enter(self.id) } < 0 { 132 | return Err(anyhow!("unable to begin running krun workload")); 133 | } 134 | 135 | Ok(()) 136 | } 137 | } 138 | 139 | fn set_smbios_oem_strings( 140 | ctx_id: u32, 141 | oem_strings: &Option>, 142 | ) -> Result<(), anyhow::Error> { 143 | let Some(oem_strings) = oem_strings else { 144 | return Ok(()); 145 | }; 146 | 147 | if oem_strings.len() > u8::MAX as usize { 148 | return Err(anyhow!("invalid number of SMBIOS OEM strings")); 149 | } 150 | 151 | let mut cstr_vec = Vec::with_capacity(oem_strings.len()); 152 | for s in oem_strings { 153 | let cs = CString::new(s.as_str()).context("invalid SMBIOS OEM string")?; 154 | cstr_vec.push(cs); 155 | } 156 | let mut ptr_vec: Vec<_> = cstr_vec.iter().map(|s| s.as_ptr()).collect(); 157 | // libkrun requires an NULL terminator to indicate the end of the array 158 | ptr_vec.push(ptr::null()); 159 | 160 | let ret = unsafe { krun_set_smbios_oem_strings(ctx_id, ptr_vec.as_ptr()) }; 161 | if ret < 0 { 162 | return Err(anyhow!("unable to set SMBIOS OEM Strings")); 163 | } 164 | Ok(()) 165 | } 166 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | #![allow(dead_code)] 4 | 5 | mod cmdline; 6 | mod context; 7 | mod status; 8 | mod virtio; 9 | 10 | use cmdline::Args; 11 | use context::KrunContext; 12 | 13 | use clap::Parser; 14 | 15 | fn main() -> Result<(), anyhow::Error> { 16 | // Gather the krun context from the command line arguments and configure the workload 17 | // accordingly. 18 | let ctx = KrunContext::try_from(Args::parse())?; 19 | 20 | // Run the workload. If behaving properly, the main thread will not return from this 21 | // function. 22 | ctx.run()?; 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /src/status.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | use std::{ 4 | fs::File, 5 | io::{Read, Write}, 6 | net::{Ipv4Addr, TcpListener}, 7 | os::fd::{FromRawFd, RawFd}, 8 | str::FromStr, 9 | }; 10 | 11 | use anyhow::{anyhow, Context}; 12 | use clap::Parser; 13 | 14 | #[link(name = "krun-efi")] 15 | extern "C" { 16 | fn krun_get_shutdown_eventfd(ctx_id: u32) -> i32; 17 | } 18 | 19 | const HTTP_RUNNING: &str = 20 | "HTTP/1.1 200 OK\r\nContent-type: application/json\r\n\r\n{\"state\": \"VirtualMachineStateRunning\"}\0"; 21 | 22 | const HTTP_STOPPING: &str = 23 | "HTTP/1.1 200 OK\r\nContent-type: application/json\r\n\r\n{\"state\": \"VirtualMachineStateStopping\"}\0"; 24 | 25 | /// Socket address in which the restful URI socket should listen on. Identical to Rust's 26 | /// SocketAddrV4, but requires a modified FromStr implementation due to how the address is 27 | /// presented on the command line. 28 | #[derive(Clone, Debug, Parser)] 29 | pub struct RestfulUriAddr { 30 | pub ip_addr: Ipv4Addr, 31 | pub port: u16, 32 | } 33 | 34 | impl FromStr for RestfulUriAddr { 35 | type Err = anyhow::Error; 36 | 37 | fn from_str(s: &str) -> Result { 38 | let mut string = String::from(s); 39 | 40 | if let Some(removed) = string.strip_prefix("tcp://") { 41 | string = String::from(removed); 42 | } 43 | 44 | let mut parts: Vec = string.split(':').map(|s| s.to_string()).collect(); 45 | if parts.len() != 2 { 46 | return Err(anyhow!("restful URI formatted incorrectly")); 47 | } 48 | 49 | // Ipv4Address's FromStr does not understand that the "localhost" IP address translates to 50 | // 127.0.0.1, this must be manually translated. 51 | if &parts[0][..] == "localhost" { 52 | parts[0] = String::from("127.0.0.1"); 53 | } 54 | 55 | let ip_addr = Ipv4Addr::from_str(&parts[0]) 56 | .context("restful URI IP address formatted incorrectly")?; 57 | let port = 58 | u16::from_str(&parts[1]).context("restful URI port number formatted incorrectly")?; 59 | 60 | Ok(Self { ip_addr, port }) 61 | } 62 | } 63 | 64 | impl Default for RestfulUriAddr { 65 | fn default() -> Self { 66 | Self { 67 | ip_addr: Ipv4Addr::new(127, 0, 0, 1), 68 | port: 8081, 69 | } 70 | } 71 | } 72 | 73 | /// Retrieve the shutdown event file descriptor initialized by libkrun. 74 | pub unsafe fn get_shutdown_eventfd(ctx_id: u32) -> i32 { 75 | let fd = krun_get_shutdown_eventfd(ctx_id); 76 | if fd < 0 { 77 | panic!("unable to retrieve krun shutdown file descriptor"); 78 | } 79 | fd 80 | } 81 | 82 | /// Listen for status and shutdown requests from the client. Shut down the krun VM when prompted. 83 | pub fn status_listener( 84 | shutdown_eventfd: RawFd, 85 | addr: Option, 86 | ) -> Result<(), anyhow::Error> { 87 | // VM is shut down by writing to the shutdown event file. 88 | let mut shutdown = unsafe { File::from_raw_fd(shutdown_eventfd) }; 89 | 90 | let addr = addr.unwrap_or_default(); 91 | 92 | let listener = TcpListener::bind((addr.ip_addr, addr.port)).unwrap(); 93 | 94 | for stream in listener.incoming() { 95 | let mut buf = [0u8; 4096]; 96 | let mut stream = stream.unwrap(); 97 | 98 | match stream.read(&mut buf) { 99 | Ok(_sz) => { 100 | let request = String::from_utf8_lossy(&buf); 101 | if request.contains("POST") { 102 | // Send a VirtualMachineStateStopping message to the client. 103 | if let Err(e) = stream.write_all(HTTP_STOPPING.as_bytes()) { 104 | println!("Error writting POST response: {e}"); 105 | } 106 | 107 | // Shut down the VM. 108 | if let Err(e) = shutdown.write_all(&1u64.to_le_bytes()) { 109 | println!("Error writting to shutdown fd: {e}"); 110 | } 111 | } else if let Err(e) = stream.write_all(HTTP_RUNNING.as_bytes()) { 112 | println!("Error writting GET response: {e}"); 113 | } 114 | } 115 | Err(e) => println!("Error reading stream: {}", e), 116 | } 117 | } 118 | 119 | Ok(()) 120 | } 121 | -------------------------------------------------------------------------------- /src/virtio.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | use crate::cmdline::{check_required_args, check_unknown_args, parse_args}; 4 | 5 | use std::{ 6 | ffi::{c_char, CString}, 7 | os::unix::ffi::OsStrExt, 8 | path::{Path, PathBuf}, 9 | str::FromStr, 10 | }; 11 | 12 | use anyhow::{anyhow, Context, Result}; 13 | use mac_address::MacAddress; 14 | 15 | #[link(name = "krun-efi")] 16 | extern "C" { 17 | fn krun_add_disk2( 18 | ctx_id: u32, 19 | c_block_id: *const c_char, 20 | c_disk_path: *const c_char, 21 | disk_format: u32, 22 | read_only: bool, 23 | ) -> i32; 24 | fn krun_add_vsock_port(ctx_id: u32, port: u32, c_filepath: *const c_char) -> i32; 25 | fn krun_add_virtiofs(ctx_id: u32, c_tag: *const c_char, c_path: *const c_char) -> i32; 26 | fn krun_set_gvproxy_path(ctx_id: u32, c_path: *const c_char) -> i32; 27 | fn krun_set_net_mac(ctx_id: u32, c_mac: *const u8) -> i32; 28 | fn krun_set_console_output(ctx_id: u32, c_filepath: *const c_char) -> i32; 29 | } 30 | 31 | #[repr(u32)] 32 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] 33 | pub enum DiskImageFormat { 34 | #[default] 35 | Raw = 0, 36 | Qcow2 = 1, 37 | } 38 | 39 | impl FromStr for DiskImageFormat { 40 | type Err = anyhow::Error; 41 | 42 | fn from_str(s: &str) -> Result { 43 | match s.to_lowercase().as_str() { 44 | "raw" => Ok(DiskImageFormat::Raw), 45 | "qcow2" => Ok(DiskImageFormat::Qcow2), 46 | _ => Err(anyhow!("unsupported disk image format")), 47 | } 48 | } 49 | } 50 | 51 | /// Each virito device configures itself with krun differently. This is used by each virtio device 52 | /// to set their respective configurations with libkrun. 53 | pub trait KrunContextSet { 54 | unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error>; 55 | } 56 | 57 | /// virtio device configurations. 58 | #[derive(Clone, Debug, PartialEq)] 59 | pub enum VirtioDeviceConfig { 60 | Blk(BlkConfig), 61 | Rng, 62 | Serial(SerialConfig), 63 | Vsock(VsockConfig), 64 | Net(NetConfig), 65 | Fs(FsConfig), 66 | Gpu(GpuConfig), 67 | Input(InputConfig), 68 | } 69 | 70 | /// Parse a virtio device configuration with its respective information/data. 71 | impl FromStr for VirtioDeviceConfig { 72 | type Err = anyhow::Error; 73 | 74 | fn from_str(s: &str) -> Result { 75 | let args: Vec = s.split(',').map(|s| s.to_string()).collect(); 76 | 77 | if args.is_empty() { 78 | return Err(anyhow!("no virtio device config found")); 79 | } 80 | 81 | // The first string is the virtio device identifier. Subsequent arguments are 82 | // device-specific. 83 | let rest = args[1..].join(","); 84 | 85 | match &args[0][..] { 86 | "virtio-blk" => Ok(Self::Blk(BlkConfig::from_str(&rest)?)), 87 | "virtio-rng" => Ok(Self::Rng), 88 | "virtio-serial" => Ok(Self::Serial(SerialConfig::from_str(&rest)?)), 89 | "virtio-vsock" => Ok(Self::Vsock(VsockConfig::from_str(&rest)?)), 90 | "virtio-net" => Ok(Self::Net(NetConfig::from_str(&rest)?)), 91 | "virtio-fs" => Ok(Self::Fs(FsConfig::from_str(&rest)?)), 92 | "virtio-gpu" => Ok(Self::Gpu(GpuConfig::from_str(&rest)?)), 93 | "virtio-input" => Ok(Self::Input(InputConfig::from_str(&rest)?)), 94 | _ => Err(anyhow!(format!( 95 | "invalid virtio device label specified: {}", 96 | args[0] 97 | ))), 98 | } 99 | } 100 | } 101 | 102 | /// Configure the device in the krun context based on which underlying device is contained. 103 | impl KrunContextSet for VirtioDeviceConfig { 104 | unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { 105 | match self { 106 | Self::Blk(blk) => blk.krun_ctx_set(id), 107 | Self::Vsock(vsock) => vsock.krun_ctx_set(id), 108 | Self::Net(net) => net.krun_ctx_set(id), 109 | Self::Fs(fs) => fs.krun_ctx_set(id), 110 | Self::Serial(serial) => serial.krun_ctx_set(id), 111 | 112 | // virtio-input, virtio-gpu, and virtio-rng devices are currently not configured in 113 | // krun. 114 | _ => Ok(()), 115 | } 116 | } 117 | } 118 | 119 | /// Configuration of a virtio-blk device. 120 | #[derive(Clone, Debug, Default, PartialEq)] 121 | pub struct BlkConfig { 122 | /// Path of the file to store as the root disk. 123 | pub path: PathBuf, 124 | 125 | /// Format of the disk image. 126 | pub format: DiskImageFormat, 127 | } 128 | 129 | impl FromStr for BlkConfig { 130 | type Err = anyhow::Error; 131 | 132 | fn from_str(s: &str) -> Result { 133 | let mut blk_config = Self::default(); 134 | let mut args = parse_args(s.to_string())?; 135 | check_required_args(&args, "virtio-blk", &["path"])?; 136 | 137 | let path = args.remove("path").unwrap(); 138 | blk_config.path = 139 | PathBuf::from_str(path.as_str()).context("path argument not a valid path")?; 140 | 141 | if let Some(f) = args.remove("format") { 142 | blk_config.format = DiskImageFormat::from_str(f.as_str())?; 143 | } 144 | 145 | check_unknown_args(args, "virtio-blk")?; 146 | 147 | Ok(blk_config) 148 | } 149 | } 150 | 151 | /// Set the virtio-blk device to be the krun VM's root disk. 152 | impl KrunContextSet for BlkConfig { 153 | unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { 154 | let basename = match self.path.file_name() { 155 | Some(osstr) => osstr.to_str().unwrap_or("disk"), 156 | None => "disk", 157 | }; 158 | let block_id_cstr = CString::new(basename).context("can't convert basename to cstring")?; 159 | let path_cstr = path_to_cstring(&self.path)?; 160 | 161 | if krun_add_disk2( 162 | id, 163 | block_id_cstr.as_ptr(), 164 | path_cstr.as_ptr(), 165 | self.format as u32, 166 | false, 167 | ) < 0 168 | { 169 | return Err(anyhow!(format!( 170 | "unable to set virtio-blk disk for {}", 171 | self.path.display() 172 | ))); 173 | } 174 | 175 | Ok(()) 176 | } 177 | } 178 | 179 | /// Configuration of a virtio-serial device. 180 | #[derive(Clone, Debug, PartialEq)] 181 | pub struct SerialConfig { 182 | /// Path of a file to use as the device's log. 183 | pub log_file_path: PathBuf, 184 | } 185 | 186 | impl FromStr for SerialConfig { 187 | type Err = anyhow::Error; 188 | 189 | fn from_str(s: &str) -> Result { 190 | let mut args = parse_args(s.to_string())?; 191 | check_required_args(&args, "virtio-serial", &["logFilePath"])?; 192 | 193 | let log_file_path = args.remove("logFilePath").unwrap(); 194 | check_unknown_args(args, "virtio-serial")?; 195 | 196 | Ok(Self { 197 | log_file_path: PathBuf::from_str(log_file_path.as_str()) 198 | .context("logFilePath argument not a valid path")?, 199 | }) 200 | } 201 | } 202 | 203 | /// Set the krun console output to be written to the virtio-serial's log file. 204 | impl KrunContextSet for SerialConfig { 205 | unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { 206 | let path_cstr = path_to_cstring(&self.log_file_path)?; 207 | 208 | if krun_set_console_output(id, path_cstr.as_ptr()) < 0 { 209 | return Err(anyhow!( 210 | "unable to set krun console output redirection to virtio-serial log file" 211 | )); 212 | } 213 | 214 | Ok(()) 215 | } 216 | } 217 | 218 | /// Configuration of a virtio-vsock device. 219 | #[derive(Clone, Debug, Default, PartialEq)] 220 | pub struct VsockConfig { 221 | /// Port to connect to on VM. 222 | pub port: u32, 223 | 224 | /// Path of underlying socket. 225 | pub socket_url: PathBuf, 226 | 227 | /// Action of socket. 228 | pub action: VsockAction, 229 | } 230 | 231 | impl FromStr for VsockConfig { 232 | type Err = anyhow::Error; 233 | 234 | fn from_str(s: &str) -> Result { 235 | let mut vsock_config = Self::default(); 236 | let mut args = parse_args(s.to_string())?; 237 | check_required_args(&args, "virtio-vsock", &["port", "socketURL"])?; 238 | 239 | let port = args.remove("port").unwrap(); 240 | vsock_config.port = u32::from_str(port.as_str()).context("port argument invalid")?; 241 | 242 | let socket_url = args.remove("socketURL").unwrap(); 243 | vsock_config.socket_url = PathBuf::from_str(socket_url.as_str()) 244 | .context("socketURL argument not a valid path")?; 245 | 246 | if let Some(v) = args.remove("listen") { 247 | if !v.is_empty() { 248 | return Err(anyhow!(format!( 249 | "unexpected value for virtio-vsock argument: listen={v}" 250 | ))); 251 | } 252 | vsock_config.action = VsockAction::from_str("listen")? 253 | } 254 | 255 | check_unknown_args(args, "virtio-vsock")?; 256 | 257 | Ok(vsock_config) 258 | } 259 | } 260 | 261 | /// Map the virtio-vsock's guest port and host path to enable the krun VM to communicate with the 262 | /// socket on the host. 263 | impl KrunContextSet for VsockConfig { 264 | unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { 265 | let path_cstr = path_to_cstring(&self.socket_url)?; 266 | 267 | if krun_add_vsock_port(id, self.port, path_cstr.as_ptr()) < 0 { 268 | return Err(anyhow!(format!( 269 | "unable to add vsock port {} for path {}", 270 | self.port, 271 | &self.socket_url.display() 272 | ))); 273 | } 274 | 275 | Ok(()) 276 | } 277 | } 278 | 279 | /// virtio-vsock action. 280 | #[derive(Clone, Debug, Default, PartialEq)] 281 | pub enum VsockAction { 282 | #[default] 283 | Listen, 284 | } 285 | 286 | impl FromStr for VsockAction { 287 | type Err = anyhow::Error; 288 | 289 | fn from_str(s: &str) -> Result { 290 | let s = s.to_string().to_lowercase(); 291 | 292 | match &s[..] { 293 | "listen" => Ok(Self::Listen), 294 | _ => Err(anyhow!("invalid vsock action")), 295 | } 296 | } 297 | } 298 | 299 | /// Configuration of a virtio-net device. 300 | #[derive(Clone, Debug, Default, PartialEq)] 301 | pub struct NetConfig { 302 | /// Path to underlying gvproxy socket. 303 | pub unix_socket_path: PathBuf, 304 | 305 | /// Network MAC address. 306 | pub mac_address: MacAddress, 307 | } 308 | 309 | impl FromStr for NetConfig { 310 | type Err = anyhow::Error; 311 | 312 | fn from_str(s: &str) -> Result { 313 | let mut net_config = Self::default(); 314 | let mut args = parse_args(s.to_string())?; 315 | check_required_args(&args, "virtio-net", &["unixSocketPath", "mac"])?; 316 | 317 | let unix_socket_path = args.remove("unixSocketPath").unwrap(); 318 | net_config.unix_socket_path = PathBuf::from_str(unix_socket_path.as_str()) 319 | .context("unixSocketPath argument not a valid path")?; 320 | 321 | let mac = args.remove("mac").unwrap(); 322 | net_config.mac_address = MacAddress::from_str(mac.as_str()) 323 | .context("unable to parse mac address from argument")?; 324 | 325 | check_unknown_args(args, "virtio-net")?; 326 | 327 | Ok(net_config) 328 | } 329 | } 330 | 331 | /// Set the gvproxy's path and network MAC address. 332 | impl KrunContextSet for NetConfig { 333 | unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { 334 | let path_cstr = path_to_cstring(&self.unix_socket_path)?; 335 | let mac = self.mac_address.bytes(); 336 | 337 | if krun_set_gvproxy_path(id, path_cstr.as_ptr()) < 0 { 338 | return Err(anyhow!(format!( 339 | "unable to set gvproxy path {}", 340 | &self.unix_socket_path.display() 341 | ))); 342 | } 343 | 344 | if krun_set_net_mac(id, mac.as_ptr()) < 0 { 345 | return Err(anyhow!(format!( 346 | "unable to set net MAC address {}", 347 | self.mac_address 348 | ))); 349 | } 350 | 351 | Ok(()) 352 | } 353 | } 354 | 355 | /// Configuration of a virtio-fs device. 356 | #[derive(Clone, Debug, Default, PartialEq)] 357 | pub struct FsConfig { 358 | /// Shared directory with the host. 359 | pub shared_dir: PathBuf, 360 | 361 | /// Guest mount tag for shared directory. 362 | pub mount_tag: PathBuf, 363 | } 364 | 365 | impl FromStr for FsConfig { 366 | type Err = anyhow::Error; 367 | 368 | fn from_str(s: &str) -> Result { 369 | let mut fs_config = FsConfig::default(); 370 | let mut args = parse_args(s.to_string())?; 371 | check_required_args(&args, "virtio-fs", &["sharedDir", "mountTag"])?; 372 | 373 | let shared_dir = args.remove("sharedDir").unwrap(); 374 | fs_config.shared_dir = PathBuf::from_str(shared_dir.as_str()) 375 | .context("sharedDir argument is not a valid path")?; 376 | 377 | let mount_tag = args.remove("mountTag").unwrap(); 378 | fs_config.mount_tag = 379 | PathBuf::from_str(mount_tag.as_str()).context("mountTag argument not a valid path")?; 380 | 381 | check_unknown_args(args, "virtio-fs")?; 382 | 383 | Ok(fs_config) 384 | } 385 | } 386 | 387 | /// Set the shared directory with its guest mount tag. 388 | impl KrunContextSet for FsConfig { 389 | unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { 390 | let shared_dir_cstr = path_to_cstring(&self.shared_dir)?; 391 | let mount_tag_cstr = path_to_cstring(&self.mount_tag)?; 392 | 393 | if krun_add_virtiofs(id, mount_tag_cstr.as_ptr(), shared_dir_cstr.as_ptr()) < 0 { 394 | return Err(anyhow!(format!( 395 | "unable to add virtiofs shared directory {} with mount tag {}", 396 | &self.shared_dir.display(), 397 | &self.mount_tag.display() 398 | ))); 399 | } 400 | 401 | Ok(()) 402 | } 403 | } 404 | 405 | /// Configuration of a virtio-gpu device. 406 | #[derive(Clone, Debug, Default, PartialEq)] 407 | pub struct GpuConfig { 408 | /// Width (pixels). 409 | pub width: u32, 410 | 411 | /// Height (pixels). 412 | pub height: u32, 413 | } 414 | 415 | impl FromStr for GpuConfig { 416 | type Err = anyhow::Error; 417 | 418 | fn from_str(s: &str) -> Result { 419 | let mut gpu_config = GpuConfig::default(); 420 | let mut args = parse_args(s.to_string())?; 421 | check_required_args(&args, "virtio-gpu", &["height", "width"])?; 422 | 423 | let width = args.remove("width").unwrap(); 424 | gpu_config.width = u32::from_str(width.as_str()).context(format!( 425 | "GPU width argument out of range (0x0 - 0x{:x})", 426 | u32::MAX 427 | ))?; 428 | 429 | let height = args.remove("height").unwrap(); 430 | gpu_config.height = u32::from_str(height.as_str()).context(format!( 431 | "GPU height argument out of range (0x0 - 0x{:x})", 432 | u32::MAX 433 | ))?; 434 | 435 | check_unknown_args(args, "virtio-gpu")?; 436 | 437 | Ok(gpu_config) 438 | } 439 | } 440 | 441 | /// Configuration of a virtio-input device. This is an enum indicating which virtio-input device a 442 | /// user would like to include with the VM. 443 | #[derive(Clone, Debug, PartialEq)] 444 | pub enum InputConfig { 445 | Keyboard, 446 | Pointing, 447 | } 448 | 449 | impl FromStr for InputConfig { 450 | type Err = anyhow::Error; 451 | 452 | fn from_str(s: &str) -> Result { 453 | let args = parse_args(s.to_string())?; 454 | 455 | if args.len() != 1 { 456 | return Err(anyhow!("invalid virtio-input config: {s}")); 457 | } 458 | 459 | let (key, value) = args.into_iter().next().unwrap(); 460 | if !value.is_empty() { 461 | return Err(anyhow!(format!( 462 | "unexpected value for virtio-input argument: {key}={value}" 463 | ))); 464 | } 465 | match key.as_str() { 466 | "keyboard" => Ok(Self::Keyboard), 467 | "pointing" => Ok(Self::Pointing), 468 | _ => Err(anyhow!("unknown virtio-input argument: {key}")), 469 | } 470 | } 471 | } 472 | 473 | /// Construct a NULL-terminated C string from a Rust Path object. 474 | fn path_to_cstring(path: &Path) -> Result { 475 | let cstring = CString::new(path.as_os_str().as_bytes()).context(format!( 476 | "unable to convert path {} into NULL-terminated C string", 477 | path.display() 478 | ))?; 479 | 480 | Ok(cstring) 481 | } 482 | --------------------------------------------------------------------------------