diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6292c36..f5e01b90 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,7 @@ jobs: env: RUSTFLAGS: "-D warnings -C opt-level=z -C target-cpu=x86-64 -C debuginfo=1" X86_64_PC_WINDOWS_MSVC_OPENSSL_DIR: c:/vcpkg/installed/x64-windows + rust_stable: 1.70.0 runs-on: ${{ matrix.os }} strategy: matrix: @@ -26,10 +27,10 @@ jobs: - name: Checkout uses: actions/checkout@v1 - - name: Install Last Stable Rust + - name: Install Rust ${{ env.rust_stable }} uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: ${{ env.rust_stable }} - name: Cache cargo registry uses: actions/cache@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ddb8097f..14f8ff72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,8 +6,9 @@ on: - pre-rel-* env: - self-test-img_tag: pre-rel-v0.1.0 + self-test-img_tag: v0.1.4 self-test-img_repository: golemfactory/ya-self-test-img + rust_stable: 1.70.0 jobs: create-release: @@ -112,9 +113,10 @@ jobs: console.log(release.data.upload_url); return release.data.upload_url - - uses: actions-rs/toolchain@v1 + - name: Install Rust ${{ env.rust_stable }} + uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: ${{ env.rust_stable }} target: x86_64-unknown-linux-musl override: true - name: Build diff --git a/Cargo.lock b/Cargo.lock index a4cb02a9..886fcb4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,6 +507,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.5" @@ -816,6 +835,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1193,6 +1221,26 @@ dependencies = [ "regex", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.12" @@ -1297,6 +1345,26 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" +[[package]] +name = "kqueue" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1440,6 +1508,24 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" +[[package]] +name = "notify" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" +dependencies = [ + "bitflags", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1989,6 +2075,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -2610,6 +2705,16 @@ dependencies = [ "quote", ] +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" @@ -2747,13 +2852,37 @@ dependencies = [ "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -2762,15 +2891,21 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", "windows_i686_gnu 0.48.0", "windows_i686_msvc 0.48.0", "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.48.0", "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" @@ -2783,6 +2918,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.0" @@ -2795,6 +2936,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.0" @@ -2807,6 +2954,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.0" @@ -2819,12 +2972,24 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" @@ -2837,6 +3002,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.0" @@ -2920,6 +3091,7 @@ dependencies = [ "env_logger 0.10.0", "futures", "log", + "notify", "pnet", "rand 0.8.4", "raw-cpuid", diff --git a/qemu/Dockerfile b/qemu/Dockerfile old mode 100644 new mode 100755 diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml old mode 100644 new mode 100755 index 612f23d9..94288766 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -32,6 +32,9 @@ bollard-stubs = "1.40.2" crc = "1.8" futures = "0.3" log = "0.4.8" +# "crossbeam-channel" and "macos_fsevent" are default features. +# Remove `macos_fsevent` if `macos` build will get dropped. +notify = { version = "6.0", features = ["crossbeam-channel", "macos_fsevent"] } rand = "0.8" raw-cpuid = "10.7" serde = { version = "^1.0", features = ["derive"] } diff --git a/runtime/src/deploy.rs b/runtime/src/deploy.rs index 0211f8e1..81b6851b 100644 --- a/runtime/src/deploy.rs +++ b/runtime/src/deploy.rs @@ -52,7 +52,6 @@ impl Deployment { input.take(json_len as u64).read_to_string(&mut buf).await?; buf }; - if crc32::checksum_ieee(json.as_bytes()) != crc { return Err(anyhow::anyhow!("Invalid ContainerConfig crc32 sum")); } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs old mode 100644 new mode 100755 index 8bcecf0a..158c508e --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -13,6 +13,7 @@ use std::convert::TryFrom; use std::env; use std::path::{Component, Path, PathBuf}; use std::sync::Arc; +use std::time::Duration; use structopt::StructOpt; use tokio::{ fs, @@ -68,6 +69,11 @@ pub struct Cli { /// INET endpoint address #[structopt(long)] inet_endpoint: Option, + /// PCI device identifier + #[structopt(long, env = "YA_RUNTIME_VM_PCI_DEVICE")] + pci_device: Option, + #[structopt(flatten)] + test_config: TestConfig, } #[derive(ya_runtime_sdk::RuntimeDef, Default)] @@ -76,6 +82,25 @@ pub struct Runtime { data: Arc>, } +#[derive(StructOpt, Clone, Default)] +struct TestConfig { + /// Test process timeout (in sec) + #[structopt(long, env = "YA_RUNTIME_VM_TEST_TIMEOUT", default_value = "10")] + test_timeout: u64, + /// Number of logical CPU cores for test process + #[structopt(long, env = "YA_RUNTIME_VM_TEST_CPU_CORES", default_value = "1")] + test_cpu_cores: usize, + /// Amount of RAM for test process [GiB] + #[structopt(long, env = "YA_RUNTIME_VM_TEST_MEM_GIB", default_value = "0.5")] + test_mem_gib: f64, +} + +impl TestConfig { + fn test_timeout(&self) -> Duration { + Duration::from_secs(self.test_timeout) + } +} + impl ya_runtime_sdk::Runtime for Runtime { fn deploy<'a>(&mut self, ctx: &mut Context) -> OutputResponse<'a> { let workdir = ctx.cli.workdir.clone().expect("Workdir not provided"); @@ -101,6 +126,7 @@ impl ya_runtime_sdk::Runtime for Runtime { let vpn_endpoint = ctx.cli.runtime.vpn_endpoint.clone(); let inet_endpoint = ctx.cli.runtime.inet_endpoint.clone(); + let pci_device_id = ctx.cli.runtime.pci_device.clone(); log::info!("VPN endpoint: {vpn_endpoint:?}"); log::info!("INET endpoint: {inet_endpoint:?}"); @@ -123,7 +149,9 @@ impl ya_runtime_sdk::Runtime for Runtime { async move { { let mut data = data.lock().await; - + if let Some(pci_device_id) = pci_device_id { + data.pci_device_id.replace(pci_device_id); + } if let Some(vpn_endpoint) = vpn_endpoint { let endpoint = ContainerEndpoint::try_from(vpn_endpoint).map_err(Error::from)?; @@ -156,17 +184,24 @@ impl ya_runtime_sdk::Runtime for Runtime { &mut self, command: server::RunProcess, mode: RuntimeMode, - _: &mut Context, + ctx: &mut Context, ) -> ProcessIdResponse<'a> { if let RuntimeMode::Command = mode { return async move { Err(anyhow::anyhow!("CLI `run` is not supported")) } .map_err(Into::into) .boxed_local(); } - - run_command(self.data.clone(), command) - .map_err(Into::into) - .boxed_local() + let pci_device_id = ctx.cli.runtime.pci_device.clone(); + let data = self.data.clone(); + async move { + if let Some(pci_device_id) = pci_device_id { + let mut runtime_data = data.lock().await; + runtime_data.pci_device_id.replace(pci_device_id); + } + run_command(data.clone(), command).await + } + .map_err(Into::into) + .boxed_local() } fn kill_command<'a>( @@ -179,20 +214,28 @@ impl ya_runtime_sdk::Runtime for Runtime { .boxed_local() } - fn offer<'a>(&mut self, _: &mut Context) -> OutputResponse<'a> { - self_test::run_self_test(|self_test_result| { - self_test::verify_status(self_test_result) - .and_then(|self_test_result| Ok(serde_json::from_str(&self_test_result)?)) - .and_then(offer) - .map(|offer| serde_json::Value::to_string(&offer)) - }) + fn offer<'a>(&mut self, ctx: &mut Context) -> OutputResponse<'a> { + let pci_device_id = ctx.cli.runtime.pci_device.clone(); + let test_config = ctx.cli.runtime.test_config.clone(); + self_test::run_self_test( + |self_test_result| { + self_test::verify_status(self_test_result) + .and_then(|self_test_result| Ok(serde_json::from_str(&self_test_result)?)) + .and_then(offer) + .map(|offer| serde_json::Value::to_string(&offer)) + }, + pci_device_id, + test_config, + ) // Dead code. ya_runtime_api::server::run_async requires killing the process to stop app .map(|_| Ok(None)) .boxed_local() } - fn test<'a>(&mut self, _ctx: &mut Context) -> EmptyResponse<'a> { - self_test::test().boxed_local() + fn test<'a>(&mut self, ctx: &mut Context) -> EmptyResponse<'a> { + let pci_device_id = ctx.cli.runtime.pci_device.clone(); + let test_config = ctx.cli.runtime.test_config.clone(); + self_test::test(pci_device_id, test_config).boxed_local() } fn join_network<'a>( @@ -336,27 +379,40 @@ fn offer(self_test_result: serde_json::Value) -> anyhow::Result bool { - // NYI - false + self_test_result + .as_object() + .and_then(|root| root.get("gpu")) + .is_some() } async fn join_network( diff --git a/runtime/src/main.rs b/runtime/src/main.rs old mode 100644 new mode 100755 diff --git a/runtime/src/self_test.rs b/runtime/src/self_test.rs index 78cc5cc4..1b32a593 100644 --- a/runtime/src/self_test.rs +++ b/runtime/src/self_test.rs @@ -1,110 +1,102 @@ use anyhow::bail; use futures::lock::Mutex; -use std::path::Path; -use std::sync::{mpsc, Arc}; +use notify::event::{AccessKind, AccessMode}; +use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +use serde_json::Value; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; use tokio::fs; +use tokio::sync::Notify; +use uuid::Uuid; +use ya_runtime_sdk::runtime_api::deploy::ContainerVolume; use ya_runtime_sdk::runtime_api::server::RuntimeHandler; -use ya_runtime_sdk::{runtime_api::server, server::Server, Context, ErrorExt, EventEmitter}; -use ya_runtime_sdk::{Error, ProcessStatus, RuntimeStatus}; +use ya_runtime_sdk::{runtime_api::server, server::Server, Context, Error, ErrorExt, EventEmitter}; +use ya_runtime_sdk::{ProcessStatus, RunProcess, RuntimeStatus}; use crate::deploy::Deployment; use crate::vmrt::{runtime_dir, RuntimeData}; -use crate::Runtime; +use crate::{Runtime, TestConfig}; const FILE_TEST_IMAGE: &str = "self-test.gvmi"; +const FILE_TEST_EXECUTABLE: &str = "ya-self-test"; -pub(crate) async fn test() -> Result<(), Error> { - run_self_test(verify_status).await; +pub(crate) async fn test( + pci_device_id: Option, + test_config: TestConfig, +) -> Result<(), Error> { + run_self_test(verify_status, pci_device_id, test_config).await; // Dead code. ya_runtime_api::server::run_async requires killing a process to stop Ok(()) } -pub(crate) fn verify_status(status: anyhow::Result) -> anyhow::Result { - let Ok(status) = status else { - bail!("Failed to get self test status: {err}"); - }; - if status.return_code == 0 { - return Ok(String::from_utf8(status.stdout)?); - } - match String::from_utf8(status.stderr) { - Ok(stderr) => anyhow::bail!( - "Process failed, code: {}, stderr: {stderr}", - status.return_code - ), - Err(err) => { - anyhow::bail!( - "Process failed, code: {}. Failed to parse err output: {err}", - status.return_code - ) - } - } +pub(crate) fn verify_status(status: anyhow::Result) -> anyhow::Result { + let status = status?; + Ok(serde_json::to_string(&status)?) } -pub(crate) async fn run_self_test(handle_result: HANDLER) -where - HANDLER: Fn(anyhow::Result) -> anyhow::Result, +pub(crate) async fn run_self_test( + handle_result: HANDLER, + pci_device_id: Option, + test_config: TestConfig, +) where + HANDLER: Fn(anyhow::Result) -> anyhow::Result, { let work_dir = std::env::temp_dir(); - let deployment = self_test_deployment(&work_dir) + let deployment = self_test_deployment(&work_dir, &test_config) .await .expect("Prepares self test img deployment"); - let runtime_data = RuntimeData { - deployment: Some(deployment), - ..Default::default() - }; - let runtime = Runtime { - data: Arc::new(Mutex::new(runtime_data)), - }; + let output_volume = + get_self_test_only_volume(&deployment).expect("Self test image has an output volume"); + let output_file_name = format!("out_{}.json", Uuid::new_v4()); + let output_file_vm = PathBuf::from_str(&output_volume.path) + .expect("Can create self test volume path") + .join(&output_file_name); + let output_dir = work_dir.join(output_volume.name); + let output_file = output_dir.join(&output_file_name); + + let runtime = self_test_runtime(deployment, pci_device_id); - server::run_async(|e| async { + server::run_async(|emitter| async { let ctx = Context::try_new().expect("Creates runtime context"); log::info!("Starting runtime"); - let (status_sender, mut status_receiver) = mpsc::channel(); - let emitter = EventEmitter::spawn(ProcessOutputHandler { - handler: Box::new(e), - status_sender, - }); - let start_response = crate::start(work_dir.clone(), runtime.data.clone(), emitter.clone()) + let start_response = start_runtime(emitter, work_dir.clone(), runtime.data.clone()) .await .expect("Starts runtime"); log::info!("Runtime start response {:?}", start_response); - let run_process: ya_runtime_sdk::RunProcess = server::RunProcess { - bin: "/ya-self-test".into(), - args: vec!["ya-self-test".into()], - work_dir: "/".into(), - ..Default::default() - }; - log::info!("Runtime: {:?}", runtime.data); - log::info!("Self test process: {run_process:?}"); - - let pid: u64 = crate::run_command(runtime.data.clone(), run_process) - .await - .expect("Runs command"); + log::info!("Running self test command"); + let timeout = test_config.test_timeout(); + run_self_test_command( + runtime.data.clone(), + &output_dir, + &output_file, + &output_file_vm, + timeout, + ) + .await + .expect("Can run self-test command"); - let (final_status_sender, final_status_receiver) = tokio::sync::oneshot::channel(); - tokio::spawn(async move { - let status = collect_process_status(&mut status_receiver, pid).await; - final_status_sender.send(status) - }); - let process_result = final_status_receiver + log::info!("Stopping runtime"); + crate::stop(runtime.data.clone()) .await - .expect("Receives process status"); + .expect("Stops runtime"); - log::info!("Process finished"); - let result = handle_result(process_result).expect("Handles test result"); + log::info!("Handling result"); + let out_result = read_json(&output_file); + let result = handle_result(out_result).expect("Handles test result"); if !result.is_empty() { - println!("{result}"); + // the server refuses to stop by itself; print output to stdout + print!("{result}"); } - log::info!("Stopping runtime"); - crate::stop(runtime.data.clone()) - .await - .expect("Stops runtime"); + log::debug!("Deleting output files"); + std::fs::remove_dir_all(output_dir).expect("Removes self-test output dir"); tokio::spawn(async move { // the server refuses to stop by itself; force quit @@ -116,20 +108,39 @@ where .await; } -async fn self_test_deployment(work_dir: &Path) -> anyhow::Result { +fn self_test_runtime(deployment: Deployment, pci_device_id: Option) -> Runtime { + let runtime_data = RuntimeData { + deployment: Some(deployment), + pci_device_id, + ..Default::default() + }; + Runtime { + data: Arc::new(Mutex::new(runtime_data)), + } +} + +/// Builds self test deployment based on `FILE_TEST_IMAGE` from path returned by `runtime_dir()` +async fn self_test_deployment( + work_dir: &Path, + test_config: &TestConfig, +) -> anyhow::Result { let package_path = runtime_dir() .expect("Runtime directory not found") .join(FILE_TEST_IMAGE) .canonicalize() .expect("Test image not found"); + let cpu_cores = test_config.test_cpu_cores; + let mem_gib = test_config.test_mem_gib; log::info!("Task package: {}", package_path.display()); + let mem_mib = (mem_gib * 1024.) as usize; let package_file = fs::File::open(package_path.clone()) .await .or_err("Error reading package file")?; - let deployment = Deployment::try_from_input(package_file, 1, 125, package_path.clone()) - .await - .or_err("Error reading package metadata")?; + let deployment = + Deployment::try_from_input(package_file, cpu_cores, mem_mib, package_path.clone()) + .await + .or_err("Error reading package metadata")?; for vol in &deployment.volumes { let vol_dir = work_dir.join(&vol.name); log::debug!("Creating volume dir: {vol_dir:?} for path {}", vol.path); @@ -140,48 +151,116 @@ async fn self_test_deployment(work_dir: &Path) -> anyhow::Result { Ok(deployment) } -struct ProcessOutputHandler { - status_sender: mpsc::Sender, +/// Returns path to self test image only volume. +/// Fails if `self_test_deployment` has no volumes or more than one. +fn get_self_test_only_volume(self_test_deployment: &Deployment) -> anyhow::Result { + if self_test_deployment.volumes.len() != 1 { + bail!("Self test image has to have one volume"); + } + Ok(self_test_deployment.volumes.first().unwrap().clone()) +} + +/// Starts runtime with runtime handler wrapped to log process stdout and stdderr +async fn start_runtime( + handler: HANDLER, + work_dir: PathBuf, + runtime_data: Arc>, +) -> anyhow::Result> { + let emitter = ProcessOutputLogger::new(handler); + let emitter = EventEmitter::spawn(emitter); + crate::start(work_dir.clone(), runtime_data, emitter.clone()).await +} + +/// Runs command, monitors `output_dir` looking for `output_file`. +/// Fails if `output_file` not created before `timeout`. +async fn run_self_test_command( + runtime_data: Arc>, + output_dir: &Path, + output_file: &Path, + output_file_vm: &Path, + timeout: Duration, +) -> anyhow::Result<()> { + let run_process: RunProcess = server::RunProcess { + bin: format!("/{FILE_TEST_EXECUTABLE}"), + args: vec![ + FILE_TEST_EXECUTABLE.into(), + output_file_vm.to_string_lossy().into(), + ], + ..Default::default() + }; + log::info!("Self test process: {run_process:?}"); + + let output_notification = Arc::new(Notify::new()); + // Keep `_watcher` . Watcher shutdowns when dropped. + let _watcher = spawn_output_watcher(output_notification.clone(), output_dir, output_file)?; + + if let Err(err) = crate::run_command(runtime_data, run_process).await { + bail!("Code: {}, msg: {}", err.code, err.message); + }; + + if let Err(err) = tokio::time::timeout(timeout, output_notification.notified()).await { + log::error!("File {output_file:?} not created before timeout of {timeout:?}s. Err: {err}."); + }; + Ok(()) +} + +fn spawn_output_watcher( + output_notification: Arc, + output_dir: &Path, + output_file: &Path, +) -> anyhow::Result { + let output_file = output_file.into(); + let mut watcher = notify::recommended_watcher(move |res| match res { + Ok(Event { + kind: EventKind::Access(AccessKind::Close(AccessMode::Write)), + paths, + .. + }) if paths.contains(&output_file) => output_notification.notify_waiters(), + Ok(event) => { + log::trace!("Output file watch event: {:?}", event); + } + Err(error) => { + log::error!("Output file watch error: {:?}", error); + } + })?; + + watcher.watch(output_dir, RecursiveMode::Recursive)?; + Ok(watcher) +} + +fn read_json(output_file: &Path) -> anyhow::Result { + let output_file = std::fs::File::open(output_file)?; + Ok(serde_json::from_reader(&output_file)?) +} +struct ProcessOutputLogger { handler: Box, } -impl RuntimeHandler for ProcessOutputHandler { +impl ProcessOutputLogger { + fn new(handler: HANDLER) -> Self { + let handler = Box::new(handler); + Self { handler } + } +} + +impl RuntimeHandler for ProcessOutputLogger { fn on_process_status<'a>(&self, status: ProcessStatus) -> futures::future::BoxFuture<'a, ()> { - if let Err(err) = self.status_sender.send(status.clone()) { - log::warn!("Failed to send process status {err}"); + if !status.stdout.is_empty() { + log::debug!( + "PID: {}, stdout: {}", + status.pid, + String::from_utf8_lossy(&status.stdout) + ); + } else if !status.stderr.is_empty() { + log::debug!( + "PID: {}, stderr: {}", + status.pid, + String::from_utf8_lossy(&status.stderr) + ); } self.handler.on_process_status(status) } - fn on_runtime_status<'a>(&self, status: RuntimeStatus) -> futures::future::BoxFuture<'a, ()> { self.handler.on_runtime_status(status) } } - -async fn collect_process_status( - status_receiver: &mut mpsc::Receiver, - pid: u64, -) -> anyhow::Result { - log::debug!("Start listening on process: {pid}"); - let mut stdout = Vec::new(); - let mut stderr = Vec::new(); - let mut return_code = 0; - while let Ok(status) = status_receiver.recv() { - if status.pid != pid { - continue; - } - stdout.append(&mut status.stdout.clone()); - stderr.append(&mut status.stderr.clone()); - return_code = status.return_code; - if !status.running { - break; - } - } - Ok(ProcessStatus { - pid, - running: false, - return_code, - stdout, - stderr, - }) -} diff --git a/runtime/src/vmrt.rs b/runtime/src/vmrt.rs old mode 100644 new mode 100755 index dd8eb648..354b4f42 --- a/runtime/src/vmrt.rs +++ b/runtime/src/vmrt.rs @@ -29,6 +29,7 @@ pub struct RuntimeData { pub inet: Option, pub deployment: Option, pub ga: Option>>, + pub pci_device_id: Option, } impl RuntimeData { @@ -74,8 +75,6 @@ pub async fn start_vmrt( "-m", format!("{}M", deployment.mem_mib).as_str(), "-nographic", - "-vga", - "none", "-kernel", FILE_VMLINUZ, "-initrd", @@ -108,6 +107,14 @@ pub async fn start_vmrt( "-no-reboot", ]); + if let Some(pci_device_id) = &data.pci_device_id { + cmd.arg("-device"); + cmd.arg(format!("vfio-pci,host={}", pci_device_id).as_str()); + } else { + cmd.arg("-vga"); + cmd.arg("none"); + } + let (vpn, inet) = // backward-compatibility mode if vpn_remote.is_none() && inet_remote.is_none() { diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..2989cbb2 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.70.0" +components = ["rustfmt", "clippy"] +targets = ["x86_64-unknown-linux-musl"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..e278f857 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,61 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 4 +newline_style = "Auto" +use_small_heuristics = "Default" +#indent_style = "Block" +#wrap_comments = false +#comment_width = 80 +#normalize_comments = false +#license_template_path = "" +#format_strings = false +#format_macro_matchers = false +#format_macro_bodies = true +#empty_item_single_line = true +#struct_lit_single_line = true +#fn_single_line = false +#where_single_line = false +#imports_indent = "Block" +#imports_layout = "Mixed" +#merge_imports = false +reorder_imports = true +reorder_modules = true +#reorder_impl_items = false +#type_punctuation_density = "Wide" +#space_before_colon = false +#space_after_colon = true +#spaces_around_ranges = false +#binop_separator = "Front" +remove_nested_parens = true +#combine_control_expr = true +#struct_field_align_threshold = 0 +#match_arm_blocks = true +#force_multiline_blocks = false +#fn_args_density = "Tall" +#brace_style = "SameLineWhere" +#control_brace_style = "AlwaysSameLine" +#trailing_semicolon = true +#trailing_comma = "Vertical" +#match_block_trailing_comma = false +#blank_lines_upper_bound = 1 +#blank_lines_lower_bound = 0 +#edition = "Edition2015" +merge_derives = true +use_try_shorthand = false +use_field_init_shorthand = false +force_explicit_abi = true +#condense_wildcard_suffixes = false +#color = "Auto" +#required_version = "0.99.1" +#unstable_features = true +#disable_all_formatting = false +#skip_children = false +#hide_parse_errors = false +#error_on_line_overflow = false +#error_on_unformatted = false +#report_todo = "Never" +#report_fixme = "Never" +#ignore = [] +#emit_mode = "Files" +#make_backup = false +#inline_attribute_width=50 # unstable