diff --git a/Cargo.lock b/Cargo.lock index 130660c12e3..265c900fbd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,12 +14,12 @@ version = "0.1.0" dependencies = [ "arch 0.1.0", "devices 0.1.0", + "epoll 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "fc_util 0.1.0", - "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.11.16 (registry+https://github.com/rust-lang/crates.io-index)", "kernel 0.1.0", "logger 0.1.0", "memory_model 0.1.0", + "micro_http 0.1.0", "mmds 0.1.0", "net_util 0.1.0", "rate_limiter 0.1.0", @@ -27,9 +27,6 @@ dependencies = [ "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", "sys_util 0.1.0", - "tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-uds 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "vmm 0.1.0", ] @@ -43,7 +40,7 @@ dependencies = [ "fc_util 0.1.0", "kvm-bindings 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "kvm-ioctls 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "memory_model 0.1.0", ] @@ -58,7 +55,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -67,17 +64,8 @@ name = "backtrace-sys" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cc 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "base64" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "safemem 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.46 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -95,15 +83,6 @@ name = "byteorder" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "c2-chacha" version = "0.2.2" @@ -115,7 +94,7 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -154,7 +133,7 @@ dependencies = [ "dumbo 0.1.0", "epoll 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "fc_util 0.1.0", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "logger 0.1.0", "memory_model 0.1.0", "net_gen 0.1.0", @@ -183,14 +162,14 @@ version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "fc_util" version = "0.1.0" dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -210,41 +189,13 @@ dependencies = [ "vmm 0.1.0", ] -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "futures" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "futures-cpupool" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "getrandom" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -253,42 +204,6 @@ name = "glob" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "httparse" -version = "1.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "hyper" -version = "0.11.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", - "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 2.5.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "ipnetwork" version = "0.14.0" @@ -308,7 +223,7 @@ version = "0.19.0" dependencies = [ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "fc_util 0.1.0", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "sys_util 0.1.0", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -342,14 +257,9 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "kvm-bindings 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "language-tags" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "lazy_static" version = "1.4.0" @@ -357,7 +267,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "libc" -version = "0.2.62" +version = "0.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -382,7 +292,7 @@ version = "0.1.0" dependencies = [ "fc_util 0.1.0", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", @@ -399,55 +309,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "memory_model" version = "0.1.0" dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "micro_http" version = "0.1.0" - -[[package]] -name = "mime" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "mio" -version = "0.6.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "mio-uds" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "miow" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "epoll 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -459,16 +329,6 @@ dependencies = [ "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "net2" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "net_gen" version = "0.1.0" @@ -477,7 +337,7 @@ version = "0.1.0" name = "net_util" version = "0.1.0" dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "net_gen 0.1.0", "pnet 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", @@ -485,19 +345,6 @@ dependencies = [ "sys_util 0.1.0", ] -[[package]] -name = "num_cpus" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "percent-encoding" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "pnet" version = "0.22.0" @@ -522,7 +369,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "ipnetwork 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "pnet_base 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "pnet_sys 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -563,7 +410,7 @@ name = "pnet_sys" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -573,7 +420,7 @@ name = "pnet_transport" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "pnet_base 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "pnet_packet 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "pnet_sys 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -606,7 +453,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -642,7 +489,7 @@ name = "rate_limiter" version = "0.1.0" dependencies = [ "fc_util 0.1.0", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "logger 0.1.0", "timerfd 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -669,14 +516,6 @@ name = "regex-syntax" version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "relay" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "remove_dir_all" version = "0.5.2" @@ -697,24 +536,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "ryu" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "safemem" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "scoped-tls" -version = "0.1.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "seccomp" version = "0.1.0" dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -738,15 +567,10 @@ version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "slab" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "syn" version = "1.0.5" @@ -771,7 +595,7 @@ name = "syntex_errors" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", "syntex_pos 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -793,7 +617,7 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", "syntex_errors 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -806,7 +630,7 @@ dependencies = [ name = "sys_util" version = "0.1.0" dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -816,7 +640,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -848,79 +672,12 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "time" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "timerfd" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-core" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", - "scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-io" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-service" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-uds" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", - "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "unicase" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -943,11 +700,6 @@ name = "utf8-ranges" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "version_check" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "virtio_gen" version = "0.1.0" @@ -961,11 +713,10 @@ dependencies = [ "devices 0.1.0", "epoll 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "fc_util 0.1.0", - "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", "kernel 0.1.0", "kvm-bindings 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "kvm-ioctls 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", "logger 0.1.0", "memory_model 0.1.0", "mmds 0.1.0", @@ -1027,44 +778,27 @@ dependencies = [ "checksum aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" "checksum backtrace 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "1371048253fa3bac6704bfd6bbfc922ee9bdcee8881330d40f308b81cc5adc55" "checksum backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b" -"checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" "checksum bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4f67931368edf3a9a51d29886d245f1c3db2f1ef0dcc9e35ff70341b78c10d23" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "652805b7e73fada9d85e9a6682a4abd490cb52d96aeecc12e33a0de34dfd0d23" -"checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" "checksum c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101" -"checksum cc 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)" = "4fc9a35e1f4290eb9e5fc54ba6cf40671ed2a2514c3eeb2b2a908dda2ea5a1be" +"checksum cc 1.0.46 (registry+https://github.com/rust-lang/crates.io-index)" = "0213d356d3c4ea2c18c40b037c3be23cd639825c18f25ee670ac7813beeef99c" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" "checksum device_tree 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f18f717c5c7c2e3483feb64cccebd077245ad6d19007c2db0fd341d38595353c" "checksum epoll 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3f0680f2a6f2a17fa7a8668a27c54e45e1ad1cf8a632f56a7c19b9e4e3bbe8a" -"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" -"checksum futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0bab5b5e94f5c31fc764ba5dd9ad16568aae5d4825538c01d6bca680c9bf94a7" -"checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" "checksum getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "473a1265acc8ff1e808cd0a1af8cee3c2ee5200916058a2ca113c29f2d903571" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" -"checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" -"checksum hyper 0.11.16 (registry+https://github.com/rust-lang/crates.io-index)" = "6a82c41828dd6f271f4d6ebc3f1db78239a4b2b3d355dfdb5f8bbf55f004463a" -"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" "checksum ipnetwork 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3d862c86f7867f19b693ec86765e0252d82e53d4240b9b629815675a0714ad1" "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum kvm-bindings 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c223e8703d2eb76d990c5f58e29c85b0f6f50e24b823babde927948e7c71fc03" "checksum kvm-ioctls 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f718efcd8039893a172114c6a12d76ebad3e5dd15f64dc0aa0199029779b211b" -"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -"checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba" +"checksum libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)" = "74dfca3d9957906e8d1e6a0b641dc9a59848e793f1da2165889fd4f62d10d79c" "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" -"checksum mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "dd1d63acd1b78403cc0c325605908475dd9b9a3acbf65ed8bcab97e27014afcf" -"checksum mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)" = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23" -"checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" -"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" -"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" -"checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273" -"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" "checksum pnet 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "63d693c84430248366146e3181ff9d330243464fa9e6146c372b2f3eb2e2d8e7" "checksum pnet_base 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4df28acf2fcc77436dd2b91a9a0c2bb617f9ca5f2acefee1a4135058b9f9801f" "checksum pnet_datalink 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b34f8ca857599d05b6b082e9baff8d27c54cb9c26568cf3c0993a5755816966" @@ -1083,17 +817,13 @@ dependencies = [ "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum regex 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ee84f70c8c08744ea9641a731c7fadb475bf2ecc52d7f627feb833e0b3990467" "checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716" -"checksum relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a" "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" "checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" -"checksum ryu 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "19d2271fa48eaf61e53cc88b4ad9adcbafa2d512c531e7fadb6dc11a4d3656c5" -"checksum safemem 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d2b08423011dae9a5ca23f07cf57dac3857f5c885d352b76f6d95f4aea9434d0" -"checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" +"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" "checksum serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)" = "9796c9b7ba2ffe7a9ce53c2287dfc48080f4b2b362fcc245a259b3a7201119dd" "checksum serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)" = "4b133a43a1ecd55d4086bd5b4dc6c1751c68b1bfbeba7a5040442022c7e7c02e" "checksum serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)" = "2f72eb2a68a7dc3f9a691bfda9305a1c017a6215e5a4545c258500d2099a37c2" -"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf" "checksum syntex 0.42.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0a30b08a6b383a22e5f6edc127d169670d48f905bb00ca79a00ea3e442ebe317" "checksum syntex_errors 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "04c48f32867b6114449155b2a82114b86d4b09e1bddb21c47ff104ab9172b646" @@ -1103,18 +833,11 @@ dependencies = [ "checksum term 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "fa63644f74ce96fbeb9b794f66aff2a52d601cbd5e80f4b97123e3899f4570f1" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" -"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" "checksum timerfd 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6c9a7822e546fa39d0b5ae14a93a33903975b62af6597288aea77f0580a6abbe" -"checksum tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "52b4e32d8edbf29501aabb3570f027c6ceb00ccef6538f4bddba0200503e74e8" -"checksum tokio-io 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b9532748772222bf70297ec0e2ad0f17213b4a7dd0e6afb68e0a0768f69f4e4f" -"checksum tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24da22d077e0f15f55162bdbdc661228c1581892f52074fb242678d015b45162" -"checksum tokio-uds 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "65ae5d255ce739e8537221ed2942e0445f4b3b813daebac1c0050ddaaa3587f9" -"checksum unicase 2.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2e2e6bd1e59e56598518beb94fd6db628ded570326f0a98c679a304bd9f00150" "checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20" "checksum unicode-xid 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "36dff09cafb4ec7c8cf0023eb0b686cb6ce65499116a12201c9e11840ca01beb" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum utf8-ranges 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" -"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" "checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" diff --git a/api_server/Cargo.toml b/api_server/Cargo.toml index c9836e8e003..8d811d8b52c 100644 --- a/api_server/Cargo.toml +++ b/api_server/Cargo.toml @@ -4,17 +4,14 @@ version = "0.1.0" authors = ["Amazon Firecracker team "] [dependencies] -futures = { version = "=0.1.18", default-features = false} -hyper = { version = "=0.11.16", default-features = false } serde = ">=1.0.27" serde_derive = ">=1.0.27" serde_json = ">=1.0.9" -tokio-core = "=0.1.12" -tokio-uds = "=0.1.7" -tokio-io = "=0.1.5" +epoll = "=4.0.1" fc_util = { path = "../fc_util" } logger = { path = "../logger" } +micro_http = { path = "../micro_http" } mmds = { path = "../mmds" } sys_util = { path = "../sys_util" } vmm = { path = "../vmm" } diff --git a/api_server/src/http_service.rs b/api_server/src/http_service.rs deleted file mode 100644 index 2e751ac92ca..00000000000 --- a/api_server/src/http_service.rs +++ /dev/null @@ -1,1476 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::rc::Rc; -use std::result; -use std::str; -use std::sync::mpsc; -use std::sync::{Arc, Mutex, RwLock}; - -use futures::future::{self, Either}; -use futures::{Future, Stream}; - -use hyper::{self, Chunk, Headers, Method, StatusCode}; -use serde_json; - -use super::VmmRequest; -use logger::{Metric, METRICS}; -use mmds::data_store::{self, Mmds}; -use request::actions::ActionBody; -use request::drive::PatchDrivePayload; -use request::{GenerateHyperResponse, IntoParsedRequest, ParsedRequest}; -use sys_util::EventFd; -use vmm::vmm_config::boot_source::BootSourceConfig; -use vmm::vmm_config::drive::BlockDeviceConfig; -use vmm::vmm_config::instance_info::InstanceInfo; -use vmm::vmm_config::logger::LoggerConfig; -use vmm::vmm_config::machine_config::VmConfig; -use vmm::vmm_config::net::{NetworkInterfaceConfig, NetworkInterfaceUpdateConfig}; -use vmm::vmm_config::vsock::VsockDeviceConfig; - -fn build_response_base>( - status: StatusCode, - maybe_headers: Option, - maybe_body: Option, -) -> hyper::Response { - let mut response = hyper::Response::new().with_status(status); - if let Some(headers) = maybe_headers { - response = response.with_headers(headers); - } - if let Some(body) = maybe_body { - response.set_body(body); - } - response -} - -// An HTTP response with just a status code. -pub fn empty_response(status: StatusCode) -> hyper::Response { - build_response_base::(status, None, None) -} - -// An HTTP response which also includes a body. -pub fn json_response>(status: StatusCode, body: T) -> hyper::Response { - let mut headers = Headers::new(); - headers.set(hyper::header::ContentType::json()); - build_response_base(status, Some(headers), Some(body)) -} - -// Builds a string that looks like (where $ stands for substitution): -// { -// "$k": "$v" -// } -// Mainly used for building fault message response json bodies. -fn basic_json_body, V: AsRef>(k: K, v: V) -> String { - format!("{{\n \"{}\": \"{}\"\n}}", k.as_ref(), v.as_ref()) -} - -pub fn json_fault_message>(msg: T) -> String { - basic_json_body("fault_message", msg) -} - -enum Error<'a> { - // A generic error, with a given status code and message to be turned into a fault message. - Generic(StatusCode, String), - // The resource ID is empty. - EmptyID, - // The resource ID must only contain alphanumeric characters and '_'. - InvalidID, - // The HTTP method & request path combination is not valid. - InvalidPathMethod(&'a str, Method), - // An error occurred when deserializing the json body of a request. - SerdeJson(serde_json::Error), -} - -// It's convenient to turn errors into HTTP responses directly. -impl<'a> Into for Error<'a> { - fn into(self) -> hyper::Response { - match self { - Error::Generic(status, msg) => json_response(status, json_fault_message(msg)), - Error::EmptyID => json_response( - StatusCode::BadRequest, - json_fault_message("The ID cannot be empty."), - ), - Error::InvalidID => json_response( - StatusCode::BadRequest, - json_fault_message( - "API Resource IDs can only contain alphanumeric characters and underscores.", - ), - ), - Error::InvalidPathMethod(path, method) => json_response( - StatusCode::BadRequest, - json_fault_message(format!( - "Invalid request method and/or path: {} {}", - method, path - )), - ), - Error::SerdeJson(e) => { - json_response(StatusCode::BadRequest, json_fault_message(e.to_string())) - } - } - } -} - -type Result<'a, T> = result::Result>; - -// Turns a GET/PUT /actions HTTP request into a ParsedRequest -fn parse_actions_req<'a>(path: &'a str, method: Method, body: &Chunk) -> Result<'a, ParsedRequest> { - let path_tokens: Vec<&str> = path[1..].split_terminator('/').collect(); - - match path_tokens.len() { - 1 if method == Method::Put => { - METRICS.put_api_requests.actions_count.inc(); - Ok(serde_json::from_slice::(body.as_ref()) - .map_err(|e| { - METRICS.put_api_requests.actions_fails.inc(); - Error::SerdeJson(e) - })? - .into_parsed_request(None, method) - .map_err(|msg| { - METRICS.put_api_requests.actions_fails.inc(); - Error::Generic(StatusCode::BadRequest, msg) - })?) - } - _ => Err(Error::InvalidPathMethod(path, method)), - } -} - -// This function is supposed to do id validation for requests. -fn checked_id(id: &str) -> Result<&str> { - // todo: are there any checks we want to do on id's? - // not allow them to be empty strings maybe? - // check: ensure string is not empty - if id.is_empty() { - return Err(Error::EmptyID); - } - // check: ensure string is alphanumeric - if !id.chars().all(|c| c == '_' || c.is_alphanumeric()) { - return Err(Error::InvalidID); - } - Ok(id) -} - -// Turns a GET/PUT /boot-source HTTP request into a ParsedRequest -fn parse_boot_source_req<'a>( - path: &'a str, - method: Method, - body: &Chunk, -) -> Result<'a, ParsedRequest> { - let path_tokens: Vec<&str> = path[1..].split_terminator('/').collect(); - - match path_tokens[1..].len() { - 0 if method == Method::Put => { - METRICS.put_api_requests.boot_source_count.inc(); - Ok(serde_json::from_slice::(body) - .map_err(|e| { - METRICS.put_api_requests.boot_source_fails.inc(); - Error::SerdeJson(e) - })? - .into_parsed_request(None, method) - .map_err(|s| { - METRICS.put_api_requests.boot_source_fails.inc(); - Error::Generic(StatusCode::BadRequest, s) - })?) - } - _ => Err(Error::InvalidPathMethod(path, method)), - } -} - -// Turns HTTP requests on /mmds into a ParsedRequest -// This is a rather dummy method with the purpose of keeping the same code structure as before. -// We will need to refactor this as some point. -fn parse_mmds_request<'a>( - path: &'a str, - method: Method, - body: &Chunk, -) -> Result<'a, ParsedRequest> { - let path_tokens: Vec<&str> = path[1..].split_terminator('/').collect(); - - match path_tokens[1..].len() { - 0 if method == Method::Get => Ok(ParsedRequest::GetMMDS), - 0 if method == Method::Put => match serde_json::from_slice(&body) { - Ok(val) => Ok(ParsedRequest::PutMMDS(val)), - Err(e) => Err(Error::SerdeJson(e)), - }, - 0 if method == Method::Patch => match serde_json::from_slice(&body) { - Ok(val) => Ok(ParsedRequest::PatchMMDS(val)), - Err(e) => Err(Error::SerdeJson(e)), - }, - _ => Err(Error::InvalidPathMethod(path, method)), - } -} - -// Turns a GET/PUT /drives HTTP request into a ParsedRequest -fn parse_drives_req<'a>(path: &'a str, method: Method, body: &Chunk) -> Result<'a, ParsedRequest> { - let path_tokens: Vec<&str> = path[1..].split_terminator('/').collect(); - let id_from_path = if path_tokens.len() > 1 { - checked_id(path_tokens[1])? - } else { - return Err(Error::EmptyID); - }; - - match path_tokens[1..].len() { - 1 if method == Method::Put => { - METRICS.put_api_requests.drive_count.inc(); - - let device_cfg = serde_json::from_slice::(body).map_err(|e| { - METRICS.put_api_requests.drive_fails.inc(); - Error::SerdeJson(e) - })?; - Ok(device_cfg - .into_parsed_request(Some(id_from_path.to_string()), method) - .map_err(|s| { - METRICS.put_api_requests.drive_fails.inc(); - Error::Generic(StatusCode::BadRequest, s) - })?) - } - - 1 if method == Method::Patch => { - METRICS.patch_api_requests.drive_count.inc(); - - Ok(PatchDrivePayload { - fields: serde_json::from_slice(body).map_err(|e| { - METRICS.patch_api_requests.drive_fails.inc(); - Error::SerdeJson(e) - })?, - } - .into_parsed_request(Some(id_from_path.to_string()), method) - .map_err(|s| { - METRICS.patch_api_requests.drive_fails.inc(); - Error::Generic(StatusCode::BadRequest, s) - })?) - } - - _ => Err(Error::InvalidPathMethod(path, method)), - } -} - -// Turns a GET/PUT /logger HTTP request into a ParsedRequest -fn parse_logger_req<'a>(path: &'a str, method: Method, body: &Chunk) -> Result<'a, ParsedRequest> { - let path_tokens: Vec<&str> = path[1..].split_terminator('/').collect(); - - match path_tokens[1..].len() { - 0 if method == Method::Put => { - METRICS.put_api_requests.logger_count.inc(); - Ok(serde_json::from_slice::(body) - .map_err(|e| { - METRICS.put_api_requests.logger_fails.inc(); - Error::SerdeJson(e) - })? - .into_parsed_request(None, method) - .map_err(|s| { - METRICS.put_api_requests.logger_fails.inc(); - Error::Generic(StatusCode::BadRequest, s) - })?) - } - _ => Err(Error::InvalidPathMethod(path, method)), - } -} - -// Turns a GET/PUT /machine-config HTTP request into a ParsedRequest -fn parse_machine_config_req<'a>( - path: &'a str, - method: Method, - body: &Chunk, -) -> Result<'a, ParsedRequest> { - let path_tokens: Vec<&str> = path[1..].split_terminator('/').collect(); - - match path_tokens[1..].len() { - 0 if method == Method::Get => { - METRICS.get_api_requests.machine_cfg_count.inc(); - let empty_machine_config = VmConfig { - vcpu_count: None, - mem_size_mib: None, - ht_enabled: None, - cpu_template: None, - }; - Ok(empty_machine_config - .into_parsed_request(None, method) - .map_err(|s| { - METRICS.get_api_requests.machine_cfg_fails.inc(); - Error::Generic(StatusCode::BadRequest, s) - })?) - } - - 0 if method == Method::Put => { - METRICS.put_api_requests.machine_cfg_count.inc(); - Ok(serde_json::from_slice::(body) - .map_err(|e| { - METRICS.put_api_requests.machine_cfg_fails.inc(); - Error::SerdeJson(e) - })? - .into_parsed_request(None, method) - .map_err(|s| { - METRICS.put_api_requests.machine_cfg_fails.inc(); - Error::Generic(StatusCode::BadRequest, s) - })?) - } - - 0 if method == Method::Patch => { - METRICS.patch_api_requests.machine_cfg_count.inc(); - Ok(serde_json::from_slice::(body) - .map_err(|e| { - METRICS.patch_api_requests.machine_cfg_fails.inc(); - Error::SerdeJson(e) - })? - .into_parsed_request(None, method) - .map_err(|s| { - METRICS.patch_api_requests.machine_cfg_fails.inc(); - Error::Generic(StatusCode::BadRequest, s) - })?) - } - _ => Err(Error::InvalidPathMethod(path, method)), - } -} - -// Turns a GET/PUT /network-interfaces HTTP request into a ParsedRequest -fn parse_netif_req<'a>(path: &'a str, method: Method, body: &Chunk) -> Result<'a, ParsedRequest> { - let path_tokens: Vec<&str> = path[1..].split_terminator('/').collect(); - let id_from_path = if path_tokens.len() > 1 { - checked_id(path_tokens[1])? - } else { - return Err(Error::EmptyID); - }; - - match path_tokens[1..].len() { - 1 if method == Method::Put => { - METRICS.put_api_requests.network_count.inc(); - - Ok(serde_json::from_slice::(body) - .map_err(|e| { - METRICS.put_api_requests.network_fails.inc(); - Error::SerdeJson(e) - })? - .into_parsed_request(Some(id_from_path.to_string()), method) - .map_err(|s| { - METRICS.put_api_requests.network_fails.inc(); - Error::Generic(StatusCode::BadRequest, s) - })?) - } - 1 if method == Method::Patch => { - METRICS.patch_api_requests.network_count.inc(); - - Ok(serde_json::from_slice::(body) - .map_err(|e| { - METRICS.patch_api_requests.network_fails.inc(); - Error::SerdeJson(e) - })? - .into_parsed_request(Some(id_from_path.to_string()), method) - .map_err(|s| { - METRICS.patch_api_requests.network_fails.inc(); - Error::Generic(StatusCode::BadRequest, s) - })?) - } - _ => Err(Error::InvalidPathMethod(path, method)), - } -} - -// Turns a GET/PUT /vsock HTTP request into a ParsedRequest. -fn parse_vsock_req<'a>(path: &'a str, method: Method, body: &Chunk) -> Result<'a, ParsedRequest> { - let path_tokens: Vec<&str> = path[1..].split_terminator('/').collect(); - - match path_tokens.len() { - 1 if method == Method::Put => Ok(serde_json::from_slice::(body) - .map_err(Error::SerdeJson)? - .into_parsed_request(None, method) - .map_err(|s| { - METRICS.put_api_requests.network_fails.inc(); - Error::Generic(StatusCode::BadRequest, s) - })?), - _ => Err(Error::InvalidPathMethod(path, method)), - } -} - -// This turns an incoming HTTP request into a ParsedRequest, which is an item containing both the -// message to be passed to the VMM, and associated entities, such as channels which allow the -// reception of the response back from the VMM. -// TODO: finish implementing/parsing all possible requests. -fn parse_request<'a>(method: Method, path: &'a str, body: &Chunk) -> Result<'a, ParsedRequest> { - // Commenting this out for now. - /* - if cfg!(debug_assertions) { - println!( - "{}", - format!( - "got req: {} {}\n{}", - method, - path, - str::from_utf8(body.as_ref()).unwrap() - // when time will come, we could better do - // serde_json::from_slice(&body).unwrap() - ) - ); - } - */ - - if !path.starts_with('/') { - return Err(Error::InvalidPathMethod(path, method)); - } - - // We use path[1..] here to skip the initial '/'. - let path_tokens: Vec<&str> = path[1..].split_terminator('/').collect(); - - if path_tokens.is_empty() { - if method == Method::Get { - return Ok(ParsedRequest::GetInstanceInfo); - } else { - return Err(Error::InvalidPathMethod(path, method)); - } - } - - match path_tokens[0] { - "actions" => parse_actions_req(path, method, body), - "boot-source" => parse_boot_source_req(path, method, body), - "drives" => parse_drives_req(path, method, body), - "logger" => parse_logger_req(path, method, body), - "machine-config" => parse_machine_config_req(path, method, body), - "network-interfaces" => parse_netif_req(path, method, body), - "mmds" => parse_mmds_request(path, method, body), - "vsock" => parse_vsock_req(path, method, body), - _ => Err(Error::InvalidPathMethod(path, method)), - } -} - -// A helper function which is always used when a message is placed into the communication channel -// with the VMM (so we don't forget to write to the EventFd). -fn send_to_vmm( - req: VmmRequest, - sender: &mpsc::Sender, - send_event: &EventFd, -) -> result::Result<(), ()> { - sender.send(req).map_err(|_| ())?; - send_event.write(1).map_err(|_| ()) -} - -// In hyper, a struct that implements the Service trait is created to handle each incoming -// request. This is the one for our ApiServer. -pub struct ApiServerHttpService { - // MMDS info directly accessible from this API thread. - mmds_info: Arc>, - // VMM instance info directly accessible from this API thread. - vmm_shared_info: Arc>, - // This allows sending messages to the VMM thread. It makes sense to use a Rc for the sender - // (instead of cloning) because everything happens on a single thread, so there's no risk of - // having races (if that was even a problem to begin with). - api_request_sender: Rc>, - // We write to this EventFd to let the VMM know about new messages. - vmm_send_event: Rc, -} - -impl ApiServerHttpService { - pub fn new( - mmds_info: Arc>, - vmm_shared_info: Arc>, - api_request_sender: Rc>, - vmm_send_event: Rc, - ) -> Self { - ApiServerHttpService { - mmds_info, - vmm_shared_info, - api_request_sender, - vmm_send_event, - } - } -} - -impl hyper::server::Service for ApiServerHttpService { - type Request = hyper::Request; - type Response = hyper::Response; - type Error = hyper::error::Error; - type Future = Box>; - - // This function returns a future that will resolve at some point to the response for - // the HTTP request contained in req. - fn call(&self, req: Self::Request) -> Self::Future { - // We do all this cloning to be able too move everything we need - // into the closure that follows. - let mmds_info = self.mmds_info.clone(); - let method = req.method().clone(); - let method_copy = req.method().clone(); - let path = String::from(req.path()); - let shared_info_lock = self.vmm_shared_info.clone(); - let api_request_sender = self.api_request_sender.clone(); - let vmm_send_event = self.vmm_send_event.clone(); - - // for nice looking match arms - use request::ParsedRequest::*; - - // The request body is itself a future (a stream of Chunks to be more precise), - // so we have to define a future that waits for all the pieces first (via concat2), - // and then does something with the newly available body (via and_then). - Box::new(req.body().concat2().and_then(move |b| { - // When this will be executed, the body is available. We start by parsing the request. - match parse_request(method, path.as_ref(), &b) { - Ok(parsed_req) => match parsed_req { - GetInstanceInfo => { - METRICS.get_api_requests.instance_info_count.inc(); - log_received_api_request(describe(&method_copy, &path, &None)); - // unwrap() to crash if the other thread poisoned this lock - let shared_info = shared_info_lock - .read() - .expect("Failed to read shared_info due to poisoned lock"); - // Serialize it to a JSON string. - let body_result = serde_json::to_string(&(*shared_info)); - match body_result { - Ok(body) => Either::A(future::ok(json_response(StatusCode::Ok, body))), - Err(e) => { - // This is an api server metrics as the shared info is obtained internally. - METRICS.get_api_requests.instance_info_fails.inc(); - Either::A(future::ok(json_response( - StatusCode::InternalServerError, - json_fault_message(e.to_string()), - ))) - } - } - } - PatchMMDS(json_value) => { - // Requests on /mmds should not have the body in the logs as the data - // store contains customer data. - log_received_api_request(describe(&method_copy, &path, &None)); - let response = mmds_info - .lock() - .expect("Failed to acquire lock on MMDS info") - .patch_data(json_value); - match response { - Ok(_) => Either::A(future::ok(empty_response(StatusCode::NoContent))), - Err(e) => match e { - data_store::Error::NotFound => { - Either::A(future::ok(json_response( - StatusCode::NotFound, - json_fault_message(e.to_string()), - ))) - } - data_store::Error::UnsupportedValueType => { - Either::A(future::ok(json_response( - StatusCode::BadRequest, - json_fault_message(e.to_string()), - ))) - } - }, - } - } - PutMMDS(json_value) => { - // Requests on /mmds should not have the body in the logs as the data - // store contains customer data. - log_received_api_request(describe(&method_copy, &path, &None)); - let response = mmds_info - .lock() - .expect("Failed to acquire lock on MMDS info") - .put_data(json_value); - match response { - Ok(_) => Either::A(future::ok(empty_response(StatusCode::NoContent))), - Err(e) => Either::A(future::ok(json_response( - StatusCode::BadRequest, - json_fault_message(e.to_string()), - ))), - } - } - GetMMDS => { - log_received_api_request(describe(&method_copy, &path, &None)); - Either::A(future::ok(json_response( - StatusCode::Ok, - mmds_info - .lock() - .expect("Failed to acquire lock on MMDS info") - .get_data_str(), - ))) - } - Sync(vmm_req, response_receiver) => { - // It only makes sense that we firstly log the incoming API request and - // only after that we forward it to the VMM. - let body_desc = match method_copy { - Method::Get => None, - _ => Some(String::from_utf8_lossy(&b.to_vec()).to_string()), - }; - log_received_api_request(describe(&method_copy, &path, &body_desc)); - - if send_to_vmm(vmm_req, &api_request_sender, &vmm_send_event).is_err() { - METRICS.api_server.sync_vmm_send_timeout_count.inc(); - return Either::A(future::err(hyper::Error::Timeout)); - } - - // metric-logging related variables for being able to log response details - let path_copy = path.clone(); - - // We need to clone the description of the request because these are moved - // in the below closure. - let path_copy_err = path_copy.clone(); - let method_copy_err = method_copy.clone(); - let body_desc_err = body_desc.clone(); - - // Sync requests don't receive a response until the response is returned. - // Once more, this just registers a closure to run when the result is - // available. - Either::B( - response_receiver - .map(move |result| { - let description = - describe(&method_copy, &path_copy, &body_desc); - // `generate_response` and `err` both consume the inner error. - // Errors aren't `Clone`-able so we can't back it up either, - // so we'll rely on the fact that the error was previously - // logged at its point of origin and not log it again. - let response = result.generate_response(); - let status_code = response.status(); - match result { - Ok(_) => { - info!( - "The {} was executed successfully. Status code: {}.", - description, status_code - ); - } - Err(e) => { - error!( - "Received Error on {}. Status code: {}. Message: {}", - description, - status_code, - e - ); - } - } - response - }) - .map_err(move |_| { - error!( - "Timeout on {}", - describe(&method_copy_err, &path_copy_err, &body_desc_err) - ); - METRICS.api_server.sync_response_fails.inc(); - hyper::Error::Timeout - }), - ) - } - }, - Err(e) => Either::A(future::ok(e.into())), - } - })) - } -} - -/// Helper function for writing the received API requests to the log. -/// -/// The `info` macro is used for logging. -#[inline] -fn log_received_api_request(api_description: String) { - info!("The API server received a {}.", api_description); -} - -/// Helper function for metric-logging purposes on API requests. -/// -/// # Arguments -/// -/// * `method` - one of `GET`, `PATCH`, `PUT` -/// * `path` - path of the API request -/// * `body` - body of the API request -/// -fn describe(method: &Method, path: &str, body: &Option) -> String { - match body { - Some(value) => format!( - "synchronous {:?} request on {:?} with body {:?}", - method, path, value - ), - None => format!("synchronous {:?} request on {:?}", method, path), - } -} - -#[cfg(test)] -mod tests { - extern crate net_util; - - use self::net_util::MacAddr; - use super::super::VmmAction; - use super::*; - - use serde_json::{Map, Value}; - use std::path::PathBuf; - use std::result; - - use futures::sync::oneshot; - use hyper::header::{ContentType, Headers}; - use hyper::Body; - use vmm::vmm_config::logger::LoggerLevel; - use vmm::vmm_config::machine_config::CpuFeaturesTemplate; - - impl<'a> std::fmt::Debug for Error<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Generic(code, name) => write!(f, "Generic({:?}, {})", code, name), - Error::EmptyID => write!(f, "EmptyID"), - Error::InvalidID => write!(f, "InvalidID"), - Error::InvalidPathMethod(path, method) => { - write!(f, "InvalidPathMethod({}, {:?})", path, method) - } - Error::SerdeJson(err) => write!(f, "SerdeJson({:?})", err), - } - } - } - - impl<'a> PartialEq for Error<'a> { - fn eq(&self, other: &Error<'a>) -> bool { - use super::Error::*; - - match (self, other) { - (Generic(sts, err), Generic(other_sts, other_err)) => { - sts == other_sts && err == other_err - } - (EmptyID, EmptyID) => true, - (InvalidID, InvalidID) => true, - (InvalidPathMethod(path, method), InvalidPathMethod(other_path, other_method)) => { - path == other_path && method == other_method - } - // Serde Errors do not implement PartialEq. - (SerdeJson(_), SerdeJson(_)) => true, - _ => false, - } - } - } - - fn body_to_string(body: hyper::Body) -> String { - let ret = body - .fold(Vec::new(), |mut acc, chunk| { - acc.extend_from_slice(&*chunk); - Ok::<_, hyper::Error>(acc) - }) - .and_then(Ok); - - String::from_utf8_lossy( - &ret.wait() - .expect("Couldn't convert request body into String due to Future polling failure"), - ) - .into() - } - - fn get_dummy_serde_error() -> serde_json::Error { - // Returns a dummy serde error. This is used for testing that the errors returned - // by parsing requests are SerdeJson(SerdeError(..)). - let serde_json_err: result::Result = - serde_json::from_str("{"); - serde_json_err.unwrap_err() - } - - #[derive(Serialize, Deserialize)] - struct Foo { - bar: u32, - } - - #[test] - fn test_build_response_base() { - let mut headers = Headers::new(); - let content_type_hdr = ContentType::plaintext(); - headers.set(ContentType::plaintext()); - let body = String::from("This is a test"); - let resp = build_response_base::(StatusCode::Ok, Some(headers), Some(body.clone())); - - assert_eq!(resp.status(), StatusCode::Ok); - assert_eq!(resp.headers().len(), 1); - assert_eq!(resp.headers().get::(), Some(&content_type_hdr)); - assert_eq!(body_to_string(resp.body()), body); - } - - #[test] - fn test_empty_response() { - let resp = empty_response(StatusCode::Ok); - assert_eq!(resp.status(), StatusCode::Ok); - assert_eq!(resp.headers().len(), 0); - assert_eq!(body_to_string(resp.body()), body_to_string(Body::empty())); - } - - #[test] - fn test_json_response() { - let body = String::from("This is not a valid JSON string, but the function works"); - let resp = json_response::(StatusCode::Ok, body.clone()); - assert_eq!(resp.status(), StatusCode::Ok); - assert_eq!(resp.headers().len(), 1); - assert_eq!( - resp.headers().get::(), - Some(&ContentType::json()) - ); - assert_eq!(body_to_string(resp.body()), body); - } - - #[test] - fn test_basic_json_body() { - let body = basic_json_body("42", "the answer to life, the universe and everything"); - assert_eq!( - body, - "{\n \"42\": \"the answer to life, the universe and everything\"\n}" - ); - } - - #[test] - fn test_json_fault_message() { - let body = json_fault_message("This is an error message"); - assert_eq!( - body, - "{\n \"fault_message\": \"This is an error message\"\n}" - ); - } - - #[test] - fn test_error_to_response() { - let json_err_key = "fault_message"; - let json_err_val = "This is an error message"; - let err_message = format!("{{\n \"{}\": \"{}\"\n}}", &json_err_key, &json_err_val); - let message = String::from("This is an error message"); - let mut response: hyper::Response = - Error::Generic(StatusCode::ServiceUnavailable, message).into(); - assert_eq!(response.status(), StatusCode::ServiceUnavailable); - assert_eq!( - response.headers().get::(), - Some(&ContentType::json()) - ); - assert_eq!(body_to_string(response.body()), err_message); - - response = Error::EmptyID.into(); - let json_err_val = "The ID cannot be empty."; - let err_message = format!("{{\n \"{}\": \"{}\"\n}}", &json_err_key, &json_err_val); - assert_eq!(response.status(), StatusCode::BadRequest); - assert_eq!( - response.headers().get::(), - Some(&ContentType::json()) - ); - assert_eq!(body_to_string(response.body()), err_message); - - let path = String::from("/foo"); - let method = Method::Options; - response = Error::InvalidPathMethod(&path, method.clone()).into(); - let json_err_val = format!("Invalid request method and/or path: {} {}", &method, &path); - let err_message = format!("{{\n \"{}\": \"{}\"\n}}", &json_err_key, &json_err_val); - assert_eq!(response.status(), StatusCode::BadRequest); - assert_eq!( - response.headers().get::(), - Some(&ContentType::json()) - ); - assert_eq!(body_to_string(response.body()), err_message); - - let res = serde_json::from_str::(&"foo"); - match res { - Ok(_) => {} - Err(e) => { - response = Error::SerdeJson(e).into(); - assert_eq!(response.status(), StatusCode::BadRequest); - assert_eq!( - response.headers().get::(), - Some(&ContentType::json()) - ); - } - } - } - - #[test] - fn test_checked_id() { - assert!(checked_id("dummy").is_ok()); - assert!(checked_id("dummy_1").is_ok()); - assert!(checked_id("") == Err(Error::EmptyID)); - assert!(checked_id("dummy!!") == Err(Error::InvalidID)); - } - - #[test] - fn test_parse_actions_req() { - // PUT InstanceStart - let json = "{ - \"action_type\": \"InstanceStart\" - }"; - let body: Chunk = Chunk::from(json); - let path = "/foo"; - - let (sender, receiver) = oneshot::channel(); - - assert!(parse_actions_req(path, Method::Put, &body) - .unwrap() - .eq(&ParsedRequest::Sync( - VmmRequest::new(VmmAction::StartMicroVm, sender), - receiver - ))); - - // PUT BlockDeviceRescan - let json = r#"{ - "action_type": "BlockDeviceRescan", - "payload": "dummy_id" - }"#; - let body: Chunk = Chunk::from(json); - let path = "/foo"; - let (sender, receiver) = oneshot::channel(); - - assert!(parse_actions_req(path, Method::Put, &body) - .unwrap() - .eq(&ParsedRequest::Sync( - VmmRequest::new(VmmAction::RescanBlockDevice("dummy_id".to_string()), sender), - receiver - ))); - - // Error cases - - // Test PUT with invalid path. - let path = "/foo/bar/baz"; - let expected_err = Error::InvalidPathMethod(path, Method::Put); - assert!(parse_actions_req(path, Method::Put, &Chunk::from("foo")) == Err(expected_err)); - - // Test PUT with invalid action body (serde erorr). - let actions_path = "/actions"; - assert!( - parse_actions_req(actions_path, Method::Put, &Chunk::from("foo")) - == Err(Error::SerdeJson(get_dummy_serde_error())) - ); - - // Test PUT BadRequest due to invalid payload. - let expected_err = Error::Generic( - StatusCode::BadRequest, - "InstanceStart does not support a payload.".to_string(), - ); - let body = r#"{ - "action_type": "InstanceStart", - "payload": { - "foo": "bar" - } - }"#; - assert!( - parse_actions_req(actions_path, Method::Put, &Chunk::from(body)) == Err(expected_err) - ); - - // Test invalid method. - let expected_err = Error::InvalidPathMethod(actions_path, Method::Post); - assert!( - parse_actions_req( - actions_path, - Method::Post, - &Chunk::from("{\"action_type\": \"InstanceStart\"}") - ) == Err(expected_err) - ); - } - - #[test] - fn test_parse_boot_source_req() { - let boot_source_path = "/boot-source"; - let boot_source_json = r#"{ - "kernel_image_path": "/foo/bar", - "boot_args": "baz" - }"#; - let body: Chunk = Chunk::from(boot_source_json); - - // PUT - // Falling back to json deserialization for constructing the "correct" request because not - // all of BootSourceBody's members are accessible. Rather than making them all public just - // for the purpose of unit tests, it's preferable to trust the deserialization. - let boot_source_cfg = serde_json::from_slice::(&body).unwrap(); - let (sender, receiver) = oneshot::channel(); - - assert!(parse_boot_source_req(boot_source_path, Method::Put, &body) - .unwrap() - .eq(&ParsedRequest::Sync( - VmmRequest::new(VmmAction::ConfigureBootSource(boot_source_cfg), sender), - receiver, - ))); - - // Error cases - // Test case for invalid path. - let dummy_path = "/boot-source/dummy"; - let expected_err = Error::InvalidPathMethod(dummy_path, Method::Put); - assert!( - parse_boot_source_req(dummy_path, Method::Put, &Chunk::from(boot_source_json)) - == Err(expected_err) - ); - - // Test case for invalid method (GET). - let expected_err = Error::InvalidPathMethod(boot_source_path, Method::Get); - assert!( - parse_boot_source_req(boot_source_path, Method::Get, &Chunk::from("{}")) - == Err(expected_err) - ); - - // Test case for invalid body (serde error). - assert!( - parse_boot_source_req(boot_source_path, Method::Put, &Chunk::from("foo")) - == Err(Error::SerdeJson(get_dummy_serde_error())) - ); - } - - #[test] - fn test_parse_drives_req() { - let valid_drive_path = "/drives/id_1"; - let json = "{ - \"drive_id\": \"id_1\", - \"path_on_host\": \"/foo/bar\", - \"is_root_device\": true, - \"is_read_only\": true - }"; - let body: Chunk = Chunk::from(json); - - // PUT - let drive_desc = BlockDeviceConfig { - drive_id: String::from("id_1"), - path_on_host: PathBuf::from(String::from("/foo/bar")), - is_root_device: true, - partuuid: None, - is_read_only: true, - rate_limiter: None, - }; - - assert!(drive_desc - .into_parsed_request(Some(String::from("id_1")), Method::Put) - .unwrap() - .eq(&parse_drives_req(valid_drive_path, Method::Put, &body).unwrap())); - - // Error Cases - // Test Case for invalid payload (id from path does not match the id from the body). - let expected_error = Err(Error::Generic( - StatusCode::BadRequest, - String::from("The id from the path does not match the id from the body!"), - )); - let path = "/drives/invalid_id"; - assert!(parse_drives_req(path, Method::Put, &body) == expected_error); - - // Serde Error: Payload does not serialize to BlockDeviceConfig struct. - assert!( - parse_drives_req(valid_drive_path, Method::Put, &Chunk::from("dummy_payload")) - == Err(Error::SerdeJson(get_dummy_serde_error())) - ); - - // Test Case for invalid path (path does not contain the id). - assert!(parse_drives_req("/foo", Method::Put, &body) == Err(Error::EmptyID)); - - // Test Case for invalid path (more than 2 tokens in path). - let path = "/a/b/c"; - let expected_error = Err(Error::InvalidPathMethod(path, Method::Put)); - assert!(parse_drives_req(path, Method::Put, &body) == expected_error); - - // PATCH - let json = r#"{ - "drive_id": "id_1", - "path_on_host": "dummy" - }"#; - let valid_body: Chunk = Chunk::from(json); - let mut payload_map = Map::new(); - payload_map.insert( - String::from("drive_id"), - Value::String(String::from("id_1")), - ); - payload_map.insert( - String::from("path_on_host"), - Value::String(String::from("dummy")), - ); - let patch_payload = PatchDrivePayload { - fields: Value::Object(payload_map), - }; - - assert!(patch_payload - .into_parsed_request(Some("id_1".to_string()), Method::Patch) - .unwrap() - .eq(&parse_drives_req(valid_drive_path, Method::Patch, &valid_body).unwrap())); - - // Test case where id from path is different. - let expected_error = Err(Error::Generic( - StatusCode::BadRequest, - String::from("The id from the path does not match the id from the body!"), - )); - let path = "/drives/invalid_id"; - - assert!(parse_drives_req(path, Method::Patch, &valid_body) == expected_error); - - // Serde Error: Payload is an invalid JSON object. - assert!( - parse_drives_req( - valid_drive_path, - Method::Patch, - &Chunk::from("{drive_id: 1234}") - ) == Err(Error::SerdeJson(get_dummy_serde_error())) - ); - - // Deserializing to a BlockDeviceConfig should fail when mandatory fields are missing. - let json = "{ - \"drive_id\": \"bar\" - }"; - let expected_error = Err(Error::Generic( - StatusCode::BadRequest, - String::from("Required key path_on_host not present in the json."), - )); - let body: Chunk = Chunk::from(json); - assert!(parse_drives_req("/foo/bar", Method::Patch, &body) == expected_error); - } - - #[test] - fn test_parse_logger_source_req() { - let logger_path = "/logger"; - - // PUT with default values for optional fields. - let default_json = r#"{ - "log_fifo": "tmp1", - "metrics_fifo": "tmp2" - }"#; - let logger_body: Chunk = Chunk::from(default_json); - let logger_config = - serde_json::from_slice::(&logger_body).expect("deserialization failed"); - assert_eq!(logger_config.level, LoggerLevel::Warning); - assert_eq!(logger_config.show_log_origin, false); - assert_eq!(logger_config.show_level, false); - - let json = "{ - \"log_fifo\": \"tmp1\", - \"metrics_fifo\": \"tmp2\", - \"level\": \"Info\", - \"show_level\": true, - \"show_log_origin\": true - }"; - let logger_body: Chunk = Chunk::from(json); - - // PUT - let logger_config = - serde_json::from_slice::(&logger_body).expect("deserialization failed"); - - let (sender, receiver) = oneshot::channel(); - assert!(parse_logger_req(logger_path, Method::Put, &logger_body) - .unwrap() - .eq(&ParsedRequest::Sync( - VmmRequest::new(VmmAction::ConfigureLogger(logger_config), sender), - receiver, - ))); - - // Error cases - // Error Case: Serde Deserialization fails due to invalid payload. - assert!( - parse_logger_req(logger_path, Method::Put, &Chunk::from("foo")) - == Err(Error::SerdeJson(get_dummy_serde_error())) - ); - - // Error Case: Invalid path. - let expected_err = Err(Error::InvalidPathMethod("/foo/bar", Method::Put)); - assert!(parse_logger_req(&"/foo/bar", Method::Put, &Chunk::from("foo")) == expected_err); - } - - #[test] - fn test_parse_machine_config_req() { - let path = "/machine-config"; - let json = "{ - \"vcpu_count\": 32, - \"mem_size_mib\": 1025, - \"ht_enabled\": true, - \"cpu_template\": \"T2\" - }"; - let body: Chunk = Chunk::from(json); - - // GET - assert!(parse_machine_config_req(path, Method::Get, &body).is_ok()); - - // Error Cases - // Error Case: Invalid Path. - let expected_err = Err(Error::InvalidPathMethod("/foo/bar", Method::Get)); - assert!(parse_machine_config_req("/foo/bar", Method::Get, &body) == expected_err); - - // PUT - let vm_config = VmConfig { - vcpu_count: Some(32), - mem_size_mib: Some(1025), - ht_enabled: Some(true), - cpu_template: Some(CpuFeaturesTemplate::T2), - }; - - assert!(vm_config - .into_parsed_request(None, Method::Put) - .unwrap() - .eq(&parse_machine_config_req(&path, Method::Put, &body).unwrap())); - - // PATCH - let vm_config = VmConfig { - vcpu_count: Some(32), - mem_size_mib: None, - ht_enabled: None, - cpu_template: None, - }; - let body = r#"{ - "vcpu_count": 32 - }"#; - assert!(vm_config - .into_parsed_request(None, Method::Patch) - .unwrap() - .eq(&parse_machine_config_req(&path, Method::Patch, &Chunk::from(body)).unwrap())); - - // Error cases - // Error Case: Invalid payload (cannot deserialize the body into a VmConfig object). - assert!( - parse_machine_config_req(path, Method::Put, &Chunk::from("foo bar")) - == Err(Error::SerdeJson(get_dummy_serde_error())) - ); - - // Error Case: Invalid payload (payload is empty). - let expected_err = Err(Error::Generic( - StatusCode::BadRequest, - String::from("Empty PATCH request."), - )); - assert!(parse_machine_config_req(path, Method::Patch, &Chunk::from("{}")) == expected_err); - - // Error Case: cpu count exceeds limitation - let json = "{ - \"vcpu_count\": 33, - \"mem_size_mib\": 1025, - \"ht_enabled\": true, - \"cpu_template\": \"T2\" - }"; - let body: Chunk = Chunk::from(json); - if let Err(Error::SerdeJson(e)) = parse_machine_config_req(path, Method::Put, &body) { - assert!(e.is_data()); - } else { - panic!(); - } - - // Error Case: PUT request with missing parameter - let json = "{ - \"mem_size_mib\": 1025, - \"ht_enabled\": true, - \"cpu_template\": \"T2\" - }"; - let body: Chunk = Chunk::from(json); - let expected_err = Err(Error::Generic( - StatusCode::BadRequest, - String::from("Missing mandatory fields."), - )); - assert!(parse_machine_config_req(path, Method::Put, &body) == expected_err); - } - - #[test] - fn test_parse_netif_req() { - let path = "/network-interfaces/id_1"; - let net_id = String::from("id_1"); - let json = "{ - \"iface_id\": \"id_1\", - \"host_dev_name\": \"foo\", - \"guest_mac\": \"12:34:56:78:9a:BC\" - }"; - let body: Chunk = Chunk::from(json); - - // PUT - let netif = NetworkInterfaceConfig { - iface_id: net_id.clone(), - host_dev_name: String::from("foo"), - guest_mac: Some(MacAddr::parse_str("12:34:56:78:9a:BC").unwrap()), - rx_rate_limiter: None, - tx_rate_limiter: None, - allow_mmds_requests: false, - }; - - assert!(netif - .into_parsed_request(Some(net_id), Method::Put) - .unwrap() - .eq(&parse_netif_req(&path, Method::Put, &body).unwrap())); - - // Error cases - // Error Case: The id from the path does not match the id from the body. - let expected_err = Err(Error::Generic( - StatusCode::BadRequest, - String::from("The id from the path does not match the id from the body!"), - )); - let path = "/network-interfaces/invalid_id"; - - assert!(parse_netif_req(path, Method::Put, &body) == expected_err); - - // Error Case: Invalid payload (cannot deserialize the body into a NetworkInterfaceBody object). - assert!( - parse_netif_req(path, Method::Put, &Chunk::from("foo bar")) - == Err(Error::SerdeJson(get_dummy_serde_error())) - ); - - // Error Case: Invalid method. - assert!( - parse_netif_req(path, Method::Options, &body,) - == Err(Error::InvalidPathMethod(path, Method::Options)) - ); - - // PATCH tests - - // Fail when path ID != body ID. - assert!(NetworkInterfaceUpdateConfig { - iface_id: "1".to_string(), - rx_rate_limiter: None, - tx_rate_limiter: None, - } - .into_parsed_request(Some("2".to_string()), Method::Patch) - .is_err()); - - let json = r#"{ - "iface_id": "1", - "rx_rate_limiter": { - "bandwidth": { - "size": 1024, - "refill_time": 100 - } - } - }"#; - let body = Chunk::from(json); - let nuc = serde_json::from_slice::(json.as_bytes()).unwrap(); - let nuc_pr = nuc - .into_parsed_request(Some("1".to_string()), Method::Patch) - .unwrap(); - assert!( - nuc_pr.eq(&parse_netif_req(&"/network-interfaces/1", Method::Patch, &body).unwrap()) - ); - - let json = r#"{ - "iface_id": "1", - "invalid_key": true - }"#; - let body = Chunk::from(json); - assert!(parse_netif_req(&"/network-interfaces/1", Method::Patch, &body).is_err()); - - let json = r#"{ - "iface_id": "1" - }"#; - let body = Chunk::from(json); - assert!(parse_netif_req(&"/network-interfaces/2", Method::Patch, &body).is_err()); - } - - #[test] - fn test_parse_mmds_request() { - let path = "/mmds"; - let empty_json = "{}"; - let body = Chunk::from(empty_json); - - // Test for GET request - assert!(parse_mmds_request(path, Method::Get, &body) - .unwrap() - .eq(&ParsedRequest::GetMMDS)); - - let dummy_json = "{\ - \"latest\": {\ - \"meta-data\": {\ - \"iam\": \"dummy\"\ - },\ - \"user-data\": 1522850095\ - } - }"; - - // Test for PUT request - let body = Chunk::from(dummy_json); - assert!(parse_mmds_request(path, Method::Put, &body) - .unwrap() - .eq(&ParsedRequest::PutMMDS( - serde_json::from_slice(&body).unwrap() - ))); - - // Test for PATCH request - let patch_json = "{\"user-data\": 15}"; - let body = Chunk::from(patch_json); - - assert!(parse_mmds_request(path, Method::Patch, &body).unwrap().eq( - &ParsedRequest::PatchMMDS(serde_json::from_slice(&body).unwrap()) - )); - - // Test for invalid json on PUT - let invalid_json = "\"latest\": {}}"; - let body = Chunk::from(invalid_json); - assert!( - parse_mmds_request(path, Method::Put, &body) - == Err(Error::SerdeJson(get_dummy_serde_error())) - ); - - // Test for invalid json on PATCH - let invalid_json = "\"latest\": {}}"; - let body = Chunk::from(invalid_json); - assert!( - parse_mmds_request(path, Method::Patch, &body) - == Err(Error::SerdeJson(get_dummy_serde_error())) - ); - - // Test for invalid path - let path = "/mmds/something"; - let expected_err = Err(Error::InvalidPathMethod(path, Method::Get)); - assert!(parse_mmds_request(path, Method::Get, &body) == expected_err); - } - - #[test] - fn test_parse_vsock_req() { - let valid_vsock_path = "/vsock"; - let json = r#"{ - "vsock_id": "foo", - "guest_cid": 3, - "uds_path": "v.sock" - }"#; - - // Test for PUT request - let body: Chunk = Chunk::from(json); - let vsock_cfg = serde_json::from_slice::(&body).unwrap(); - - let (sender, receiver) = oneshot::channel(); - assert!(parse_vsock_req(valid_vsock_path, Method::Put, &body) - .unwrap() - .eq(&ParsedRequest::Sync( - VmmRequest::new(VmmAction::SetVsockDevice(vsock_cfg), sender), - receiver, - ))); - - // Error case: invalid path. - let path = "/vsock/invalid_path"; - assert!( - parse_vsock_req(path, Method::Put, &body) - == Err(Error::InvalidPathMethod(path, Method::Put)) - ); - - // Serde Error: invalid VsockDeviceConfig body. - assert!( - parse_vsock_req(valid_vsock_path, Method::Put, &Chunk::from("foo")) - == Err(Error::SerdeJson(get_dummy_serde_error())) - ); - } - - #[test] - fn test_parse_request() { - let body: Chunk = Chunk::from("{ \"foo\": \"bar\" }"); - - assert!(parse_request(Method::Get, "foo/bar", &body).is_err()); - - let all_methods = vec![ - Method::Put, - Method::Options, - Method::Post, - Method::Delete, - Method::Head, - Method::Trace, - Method::Connect, - Method::Patch, - Method::Extension(String::from("foobar")), - ]; - - for method in &all_methods { - assert!(parse_request(method.clone(), "/foo", &body).is_err()); - } - - // Test empty request - assert!(parse_request(Method::Get, "/", &body) - .unwrap() - .eq(&ParsedRequest::GetInstanceInfo)); - for method in &all_methods { - if *method != Method::Get { - assert!(parse_request(method.clone(), "/", &body).is_err()); - } - } - - // Test all valid requests - // Each request type is unit tested separately - for path in &[ - "/boot-source", - "/drives", - "/machine-config", - "/network-interfaces", - ] { - for method in &all_methods { - if *method != Method::Get && *method != Method::Put { - assert!(parse_request(method.clone(), path, &body).is_err()); - } - } - } - } - - #[test] - fn test_describe() { - let body: String = String::from("{ \"foo\": \"bar\" }"); - let msj = describe(&Method::Get, &String::from("/foo/bar"), &Some(body.clone())); - assert_eq!( - msj, - "synchronous Get request on \"/foo/bar\" with body \"{ \\\"foo\\\": \\\"bar\\\" }\"" - ); - let msj = describe(&Method::Put, &String::from("/foo/bar"), &Some(body)); - assert_eq!( - msj, - "synchronous Put request on \"/foo/bar\" with body \"{ \\\"foo\\\": \\\"bar\\\" }\"" - ); - let msj = describe(&Method::Patch, &String::from("/foo/bar"), &None); - assert_eq!(msj, "synchronous Patch request on \"/foo/bar\"") - } -} diff --git a/api_server/src/lib.rs b/api_server/src/lib.rs index 076c2e7bfa3..c01f3ff4672 100644 --- a/api_server/src/lib.rs +++ b/api_server/src/lib.rs @@ -1,40 +1,35 @@ // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -extern crate futures; -extern crate hyper; extern crate serde; #[macro_use] extern crate serde_derive; extern crate serde_json; -extern crate tokio_core; -extern crate tokio_uds; +extern crate epoll; extern crate fc_util; #[macro_use] extern crate logger; +extern crate micro_http; extern crate mmds; extern crate sys_util; extern crate vmm; -mod http_service; -pub mod request; +mod parsed_request; +mod request; use std::path::PathBuf; -use std::rc::Rc; -use std::sync::mpsc; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::{mpsc, Arc, Mutex, RwLock}; use std::{fmt, io}; -use futures::sync::oneshot; -use futures::{Future, Stream}; -use hyper::server::Http; -use tokio_core::reactor::Core; -use tokio_uds::UnixListener; - -use http_service::ApiServerHttpService; use logger::{Metric, METRICS}; +pub use micro_http::{ + Body, HttpServer, Method, Request, RequestError, Response, ServerError, ServerRequest, + ServerResponse, StatusCode, Version, +}; +use mmds::data_store; use mmds::data_store::Mmds; +use parsed_request::ParsedRequest; use sys_util::EventFd; use vmm::default_syscalls; use vmm::vmm_config::boot_source::BootSourceConfig; @@ -102,37 +97,6 @@ pub enum VmmData { MachineConfiguration(VmConfig), } -/// One shot channel used by VMM to send a response. -pub type ResponseSender = oneshot::Sender>; -/// One shot channel used to receive a response from the VMM. -pub type ResponseReceiver = oneshot::Receiver>; - -/// Wrapper over requested action to be done by the VMM and sender where the response will go. -/// This is wrapped in a struct to allow custom PartialEq. -pub struct VmmRequest { - inner: Box<(VmmAction, ResponseSender)>, -} - -impl VmmRequest { - /// Create a VmmRequest from given VmmAction and ResponseSender. - pub fn new(action: VmmAction, response_sender: ResponseSender) -> VmmRequest { - VmmRequest { - inner: Box::new((action, response_sender)), - } - } - - pub fn unpack(self) -> (VmmAction, ResponseSender) { - let inner_box = *self.inner; - (inner_box.0, inner_box.1) - } -} - -impl PartialEq for VmmRequest { - fn eq(&self, other: &VmmRequest) -> bool { - self.inner.0 == other.inner.0 - } -} - pub enum Error { Io(io::Error), Eventfd(io::Error), @@ -158,14 +122,21 @@ impl fmt::Debug for Error { pub type Result = std::result::Result; +pub type VmmRequest = Box; +pub type VmmResponse = Box>; + pub struct ApiServer { - // MMDS info directly accessible from the API thread. + /// MMDS info directly accessible from the API thread. mmds_info: Arc>, - // VMM instance info directly accessible from the API thread. + /// VMM instance info directly accessible from the API thread. vmm_shared_info: Arc>, - // Sender which allows passing messages to the VMM. - api_request_sender: Rc>, - efd: Rc, + /// Sender which allows passing messages to the VMM. + api_request_sender: mpsc::Sender, + /// Receiver which collects messages from the VMM. + vmm_response_receiver: mpsc::Receiver, + /// FD on which we notify the VMM that we have sent at least one + /// `VmmRequest`. + to_vmm_fd: EventFd, } impl ApiServer { @@ -173,28 +144,26 @@ impl ApiServer { mmds_info: Arc>, vmm_shared_info: Arc>, api_request_sender: mpsc::Sender, - kick_vmm_efd: EventFd, + vmm_response_receiver: mpsc::Receiver, + to_vmm_fd: EventFd, ) -> Result { Ok(ApiServer { mmds_info, vmm_shared_info, - api_request_sender: Rc::new(api_request_sender), - efd: Rc::new(kick_vmm_efd), + api_request_sender, + vmm_response_receiver, + to_vmm_fd, }) } - // TODO: does tokio_uds also support abstract domain sockets? pub fn bind_and_run( - &self, + &mut self, path: PathBuf, start_time_us: Option, start_time_cpu_us: Option, seccomp_level: u32, ) -> Result<()> { - let mut core = Core::new().map_err(Error::Io)?; - let handle = Rc::new(core.handle()); - - let listener = UnixListener::bind(path, &handle).map_err(Error::Io)?; + let mut server = HttpServer::new(path).unwrap(); if let Some(start_time) = start_time_us { let delta_us = @@ -214,27 +183,6 @@ impl ApiServer { .add(delta_us as usize); } - let http: Http = Http::new(); - - let f = listener - .incoming() - .for_each(|(stream, _)| { - // For the sake of clarity: when we use self.efd.clone(), the intent is to - // clone the wrapping Rc, not the EventFd itself. - let service = ApiServerHttpService::new( - self.mmds_info.clone(), - self.vmm_shared_info.clone(), - self.api_request_sender.clone(), - self.efd.clone(), - ); - let connection = http.serve_connection(stream, service); - // todo: is spawn() any better/worse than execute()? - // We have to adjust the future item and error, to fit spawn()'s definition. - handle.spawn(connection.map(|_| ()).map_err(|_| ())); - Ok(()) - }) - .map_err(Error::Io); - // Load seccomp filters on the API thread. // Execution panics if filters cannot be loaded, use --seccomp-level=0 if skipping filters // altogether is the desired behaviour. @@ -245,11 +193,135 @@ impl ApiServer { ); } - // This runs forever, unless an error is returned somewhere within f (but nothing happens - // for errors which might arise inside the connections we spawn from f, unless we explicitly - // do something in their future chain). When this returns, ongoing connections will be - // interrupted, and other futures will not complete, as the event loop stops working. - core.run(f) + server.start_server().unwrap(); + loop { + match server.requests() { + Ok(request_vec) => { + for server_request in request_vec { + server + .respond( + // Use `self.handle_request()` as the processing callback. + server_request.process(|request| self.handle_request(request)), + ) + .or_else(|e| { + error!("API Server encountered an error on response: {}", e); + Ok(()) + })?; + } + } + Err(e) => { + error!( + "API Server error on retrieving incoming request. Error: {}", + e + ); + } + } + } + } + + fn handle_request(&self, request: &Request) -> Response { + match ParsedRequest::try_from_request(request) { + Ok(ParsedRequest::Sync(vmm_action)) => self.serve_vmm_action_request(vmm_action), + Ok(ParsedRequest::GetInstanceInfo) => self.get_instance_info(), + Ok(ParsedRequest::GetMMDS) => self.get_mmds(), + Ok(ParsedRequest::PatchMMDS(value)) => self.patch_mmds(value), + Ok(ParsedRequest::PutMMDS(value)) => self.put_mmds(value), + Err(e) => e.into(), + } + } + + fn serve_vmm_action_request(&self, vmm_action: VmmAction) -> Response { + self.api_request_sender.send(Box::new(vmm_action)).unwrap(); + self.to_vmm_fd.write(1).unwrap(); + let vmm_outcome = *(self.vmm_response_receiver.recv().unwrap()); + ParsedRequest::convert_to_response(vmm_outcome) + } + + fn get_instance_info(&self) -> Response { + let shared_info_lock = self.vmm_shared_info.clone(); + // unwrap() to crash if the other thread poisoned this lock + let shared_info = shared_info_lock + .read() + .expect("Failed to read shared_info due to poisoned lock"); + // Serialize it to a JSON string. + let body_result = serde_json::to_string(&(*shared_info)); + match body_result { + Ok(body) => ApiServer::json_response(StatusCode::OK, body), + Err(e) => { + // This is an api server metrics as the shared info is obtained internally. + METRICS.get_api_requests.instance_info_fails.inc(); + ApiServer::json_response( + StatusCode::BadRequest, + ApiServer::json_fault_message(e.to_string()), + ) + } + } + } + + fn get_mmds(&self) -> Response { + ApiServer::json_response( + StatusCode::OK, + self.mmds_info + .lock() + .expect("Failed to acquire lock on MMDS info") + .get_data_str(), + ) + } + + fn patch_mmds(&self, value: serde_json::Value) -> Response { + let mmds_response = self + .mmds_info + .lock() + .expect("Failed to acquire lock on MMDS info") + .patch_data(value); + match mmds_response { + Ok(_) => Response::new(Version::Http11, StatusCode::NoContent), + Err(e) => match e { + data_store::Error::NotFound => ApiServer::json_response( + StatusCode::NotFound, + ApiServer::json_fault_message(e.to_string()), + ), + data_store::Error::UnsupportedValueType => ApiServer::json_response( + StatusCode::BadRequest, + ApiServer::json_fault_message(e.to_string()), + ), + }, + } + } + + fn put_mmds(&self, value: serde_json::Value) -> Response { + let mmds_response = self + .mmds_info + .lock() + .expect("Failed to acquire lock on MMDS info") + .put_data(value); + match mmds_response { + Ok(_) => Response::new(Version::Http11, StatusCode::NoContent), + Err(e) => ApiServer::json_response( + StatusCode::BadRequest, + ApiServer::json_fault_message(e.to_string()), + ), + } + } + + /// An HTTP response which also includes a body. + pub fn json_response>(status: StatusCode, body: T) -> Response { + let mut response = Response::new(Version::Http11, status); + response.set_body(Body::new(body.into())); + response + } + + // Builds a string that looks like (where $ stands for substitution): + // { + // "$k": "$v" + // } + // Mainly used for building fault message response json bodies. + fn basic_json_body, V: AsRef>(k: K, v: V) -> String { + format!("{{\n \"{}\": \"{}\"\n}}", k.as_ref(), v.as_ref()) + } + + fn json_fault_message>(msg: T) -> String { + ApiServer::basic_json_body("fault_message", msg) } } @@ -257,6 +329,17 @@ impl ApiServer { mod tests { use super::*; + use std::io::{Read, Write}; + use std::os::unix::net::UnixStream; + use std::sync::mpsc::channel; + use std::{fs, thread}; + + use micro_http::HttpConnection; + use mmds::MMDS; + use std::time::Duration; + use vmm::vmm_config::instance_info::{InstanceInfo, InstanceState}; + use vmm::{ErrorKind, StartMicrovmError, VmmActionError}; + #[test] fn test_error_messages() { let e = Error::Io(io::Error::from_raw_os_error(0)); @@ -284,4 +367,333 @@ mod tests { format!("EventFd error: {}", io::Error::from_raw_os_error(0)) ); } + + #[test] + fn test_serve_vmm_action_request() { + let vmm_shared_info = Arc::new(RwLock::new(InstanceInfo { + state: InstanceState::Uninitialized, + id: "test_serve_action_req".to_string(), + vmm_version: "version 0.1.0".to_string(), + })); + + let to_vmm_fd = EventFd::new().unwrap(); + let (api_request_sender, _from_api) = channel(); + let (to_api, vmm_response_receiver) = channel(); + let mmds_info = MMDS.clone(); + + let api_server = ApiServer::new( + mmds_info, + vmm_shared_info, + api_request_sender, + vmm_response_receiver, + to_vmm_fd, + ) + .unwrap(); + + to_api + .send(Box::new(Err(VmmActionError::StartMicrovm( + ErrorKind::User, + StartMicrovmError::EventFd, + )))) + .unwrap(); + let response = api_server.serve_vmm_action_request(VmmAction::StartMicroVm); + assert_eq!(response.status(), StatusCode::BadRequest); + } + + #[test] + fn test_get_instance_info() { + let vmm_shared_info = Arc::new(RwLock::new(InstanceInfo { + state: InstanceState::Uninitialized, + id: "test_get_instance_info".to_string(), + vmm_version: "version 0.1.0".to_string(), + })); + + let to_vmm_fd = EventFd::new().unwrap(); + let (api_request_sender, _from_api) = channel(); + let (_to_api, vmm_response_receiver) = channel(); + let mmds_info = MMDS.clone(); + + let api_server = ApiServer::new( + mmds_info, + vmm_shared_info, + api_request_sender, + vmm_response_receiver, + to_vmm_fd, + ) + .unwrap(); + + let response = api_server.get_instance_info(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[test] + fn test_get_mmds() { + let vmm_shared_info = Arc::new(RwLock::new(InstanceInfo { + state: InstanceState::Uninitialized, + id: "test_get_mmds".to_string(), + vmm_version: "version 0.1.0".to_string(), + })); + + let to_vmm_fd = EventFd::new().unwrap(); + let (api_request_sender, _from_api) = channel(); + let (_to_api, vmm_response_receiver) = channel(); + let mmds_info = MMDS.clone(); + + let api_server = ApiServer::new( + mmds_info, + vmm_shared_info, + api_request_sender, + vmm_response_receiver, + to_vmm_fd, + ) + .unwrap(); + + let response = api_server.get_mmds(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[test] + fn test_put_mmds() { + let vmm_shared_info = Arc::new(RwLock::new(InstanceInfo { + state: InstanceState::Uninitialized, + id: "test_put_mmds".to_string(), + vmm_version: "version 0.1.0".to_string(), + })); + + let to_vmm_fd = EventFd::new().unwrap(); + let (api_request_sender, _from_api) = channel(); + let (_to_api, vmm_response_receiver) = channel(); + let mmds_info = MMDS.clone(); + + let api_server = ApiServer::new( + mmds_info, + vmm_shared_info, + api_request_sender, + vmm_response_receiver, + to_vmm_fd, + ) + .unwrap(); + + let response = api_server.put_mmds(serde_json::Value::String("string".to_string())); + assert_eq!(response.status(), StatusCode::NoContent); + + let response = api_server.put_mmds(serde_json::Value::Bool(true)); + assert_eq!(response.status(), StatusCode::BadRequest); + } + + #[test] + fn test_patch_mmds() { + let vmm_shared_info = Arc::new(RwLock::new(InstanceInfo { + state: InstanceState::Uninitialized, + id: "test_patch_mmds".to_string(), + vmm_version: "version 0.1.0".to_string(), + })); + + let to_vmm_fd = EventFd::new().unwrap(); + let (api_request_sender, _from_api) = channel(); + let (_to_api, vmm_response_receiver) = channel(); + let mmds_info = MMDS.clone(); + + let api_server = ApiServer::new( + mmds_info, + vmm_shared_info, + api_request_sender, + vmm_response_receiver, + to_vmm_fd, + ) + .unwrap(); + + let response = api_server.put_mmds(serde_json::Value::String("string".to_string())); + assert_eq!(response.status(), StatusCode::NoContent); + + let response = api_server.patch_mmds(serde_json::Value::String("string".to_string())); + assert_eq!(response.status(), StatusCode::NoContent); + + let response = api_server.patch_mmds(serde_json::Value::Bool(true)); + assert_eq!(response.status(), StatusCode::BadRequest); + } + + #[test] + fn test_handle_request() { + let vmm_shared_info = Arc::new(RwLock::new(InstanceInfo { + state: InstanceState::Uninitialized, + id: "test_handle_request".to_string(), + vmm_version: "version 0.1.0".to_string(), + })); + + let to_vmm_fd = EventFd::new().unwrap(); + let (api_request_sender, _from_api) = channel(); + let (to_api, vmm_response_receiver) = channel(); + let mmds_info = MMDS.clone(); + + let api_server = ApiServer::new( + mmds_info, + vmm_shared_info, + api_request_sender, + vmm_response_receiver, + to_vmm_fd, + ) + .unwrap(); + to_api + .send(Box::new(Err(VmmActionError::StartMicrovm( + ErrorKind::User, + StartMicrovmError::EventFd, + )))) + .unwrap(); + + // Test an Actions request. + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PUT /actions HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 49\r\n\r\n{ \ + \"action_type\": \"Invalid\", \ + \"payload\": \"string\" \ + }", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + let response = api_server.handle_request(&req); + assert_eq!(response.status(), StatusCode::BadRequest); + + // Test a Get Info request. + sender.write_all(b"GET / HTTP/1.1\r\n\r\n").unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + let response = api_server.handle_request(&req); + assert_eq!(response.status(), StatusCode::OK); + + // Test a Get Mmds request. + sender.write_all(b"GET /mmds HTTP/1.1\r\n\r\n").unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + let response = api_server.handle_request(&req); + assert_eq!(response.status(), StatusCode::OK); + + // Test a Put Mmds request. + sender + .write_all( + b"PUT /mmds HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 2\r\n\r\n{}", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + let response = api_server.handle_request(&req); + assert_eq!(response.status(), StatusCode::NoContent); + + // Test a Patch Mmds request. + sender + .write_all( + b"PATCH /mmds HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 2\r\n\r\n{}", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + let response = api_server.handle_request(&req); + assert_eq!(response.status(), StatusCode::NoContent); + + // Test erroneous request. + sender + .write_all( + b"GET /mmds HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 2\r\n\r\n{}", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + let response = api_server.handle_request(&req); + assert_eq!(response.status(), StatusCode::BadRequest); + } + + #[test] + fn test_bind_and_run() { + let path_to_socket = "/tmp/api_server_test_socket.sock"; + fs::remove_file(path_to_socket).unwrap_or_default(); + let vmm_shared_info = Arc::new(RwLock::new(InstanceInfo { + state: InstanceState::Uninitialized, + id: "test_handle_request".to_string(), + vmm_version: "version 0.1.0".to_string(), + })); + + let to_vmm_fd = EventFd::new().unwrap(); + let (api_request_sender, _from_api) = channel(); + let (_to_api, vmm_response_receiver) = channel(); + let mmds_info = MMDS.clone(); + + thread::Builder::new() + .name("fc_api_test".to_owned()) + .spawn(move || { + ApiServer::new( + mmds_info, + vmm_shared_info, + api_request_sender, + vmm_response_receiver, + to_vmm_fd, + ) + .expect("Cannot create API server") + .bind_and_run( + PathBuf::from(path_to_socket.to_string()), + Some(1), + Some(1), + 0, + ) + .unwrap(); + }) + .unwrap(); + + // Wait for the server to set itself up. + thread::sleep(Duration::new(0, 10_000_000)); + let mut sock = UnixStream::connect(PathBuf::from(path_to_socket.to_string())).unwrap(); + + // Send a GET instance-info request. + assert!(sock.write_all(b"GET / HTTP/1.1\r\n\r\n").is_ok()); + let mut buf: [u8; 100] = [0; 100]; + assert!(sock.read(&mut buf[..]).unwrap() > 0); + + // Send an erroneous request. + assert!(sock.write_all(b"OPTIONS / HTTP/1.1\r\n\r\n").is_ok()); + let mut buf: [u8; 100] = [0; 100]; + assert!(sock.read(&mut buf[..]).unwrap() > 0); + } + + #[test] + #[should_panic] + fn test_invalid_seccomp() { + let path_to_socket = "/tmp/api_server_test_socket2.sock"; + fs::remove_file(path_to_socket).unwrap_or_default(); + let vmm_shared_info = Arc::new(RwLock::new(InstanceInfo { + state: InstanceState::Uninitialized, + id: "test_handle_request".to_string(), + vmm_version: "version 0.1.0".to_string(), + })); + + let to_vmm_fd = EventFd::new().unwrap(); + let (api_request_sender, _from_api) = channel(); + let (_to_api, vmm_response_receiver) = channel(); + let mmds_info = MMDS.clone(); + + ApiServer::new( + mmds_info, + vmm_shared_info, + api_request_sender, + vmm_response_receiver, + to_vmm_fd, + ) + .expect("Cannot create API server") + .bind_and_run( + PathBuf::from(path_to_socket.to_string()), + Some(1), + Some(1), + 89, + ) + .unwrap(); + } } diff --git a/api_server/src/parsed_request.rs b/api_server/src/parsed_request.rs new file mode 100644 index 00000000000..f4713078843 --- /dev/null +++ b/api_server/src/parsed_request.rs @@ -0,0 +1,793 @@ +// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use serde_json::Value; + +use micro_http::{Body, Method, Request, Response, StatusCode, Version}; +use request::actions::parse_put_actions; +use request::boot_source::parse_put_boot_source; +use request::drive::{parse_patch_drive, parse_put_drive}; +use request::instance_info::parse_get_instance_info; +use request::logger::parse_put_logger; +use request::machine_configuration::{ + parse_get_machine_config, parse_patch_machine_config, parse_put_machine_config, +}; +use request::mmds::{parse_get_mmds, parse_patch_mmds, parse_put_mmds}; +use request::net::{parse_patch_net, parse_put_net}; +use request::vsock::parse_put_vsock; +use {ApiServer, VmmAction, VmmData}; + +#[allow(clippy::large_enum_variant)] +pub enum ParsedRequest { + GetInstanceInfo, + GetMMDS, + PatchMMDS(Value), + PutMMDS(Value), + Sync(VmmAction), +} + +impl ParsedRequest { + pub fn try_from_request(request: &Request) -> Result { + let request_uri = request.uri().get_abs_path().to_string(); + log_received_api_request(describe( + request.method(), + request_uri.as_str(), + request.body.as_ref(), + )); + let path_tokens: Vec<&str> = request_uri[1..].split_terminator('/').collect(); + let path = if path_tokens.is_empty() { + "" + } else { + path_tokens[0] + }; + + match (request.method(), path, request.body.as_ref()) { + (Method::Get, "", None) => parse_get_instance_info(), + (Method::Get, "machine-config", None) => parse_get_machine_config(), + (Method::Get, "mmds", None) => parse_get_mmds(), + (Method::Get, _, Some(_)) => method_to_error(Method::Get), + (Method::Put, "actions", Some(body)) => parse_put_actions(body), + (Method::Put, "boot-source", Some(body)) => parse_put_boot_source(body), + (Method::Put, "drives", Some(body)) => parse_put_drive(body, path_tokens.get(1)), + (Method::Put, "logger", Some(body)) => parse_put_logger(body), + (Method::Put, "machine-config", Some(body)) => parse_put_machine_config(body), + (Method::Put, "mmds", Some(body)) => parse_put_mmds(body), + (Method::Put, "network-interfaces", Some(body)) => { + parse_put_net(body, path_tokens.get(1)) + } + (Method::Put, "vsock", Some(body)) => parse_put_vsock(body), + (Method::Put, _, None) => method_to_error(Method::Put), + (Method::Patch, "drives", Some(body)) => parse_patch_drive(body, path_tokens.get(1)), + (Method::Patch, "machine-config", Some(body)) => parse_patch_machine_config(body), + (Method::Patch, "mmds", Some(body)) => parse_patch_mmds(body), + (Method::Patch, "network-interfaces", Some(body)) => { + parse_patch_net(body, path_tokens.get(1)) + } + (Method::Patch, _, None) => method_to_error(Method::Patch), + (method, unknown_uri, _) => { + Err(Error::InvalidPathMethod(unknown_uri.to_string(), method)) + } + } + } + + pub fn convert_to_response( + request_outcome: std::result::Result, + ) -> Response { + match request_outcome { + Ok(vmm_data) => match vmm_data { + VmmData::Empty => { + info!("The request was executed successfully. Status code: 204 No Content."); + Response::new(Version::Http11, StatusCode::NoContent) + } + VmmData::MachineConfiguration(vm_config) => { + info!("The request was executed successfully. Status code: 200 OK."); + let mut response = Response::new(Version::Http11, StatusCode::OK); + response.set_body(Body::new(vm_config.to_string())); + response + } + }, + Err(vmm_action_error) => { + error!( + "Received Error. Status code: 400 Bad Request. Message: {}", + vmm_action_error + ); + let mut response = Response::new(Version::Http11, StatusCode::BadRequest); + response.set_body(Body::new(vmm_action_error.to_string())); + response + } + } + } +} + +/// Helper function for writing the received API requests to the log. +/// +/// The `info` macro is used for logging. +#[inline] +fn log_received_api_request(api_description: String) { + info!("The API server received a {}.", api_description); +} + +/// Helper function for metric-logging purposes on API requests. +/// +/// # Arguments +/// +/// * `method` - one of `GET`, `PATCH`, `PUT` +/// * `path` - path of the API request +/// * `body` - body of the API request +/// +fn describe(method: Method, path: &str, body: Option<&Body>) -> String { + match (path, body) { + ("/mmds", Some(_)) | (_, None) => format!("synchronous {:?} request on {:?}", method, path), + (_, Some(value)) => format!( + "synchronous {:?} request on {:?} with body {:?}", + method, + path, + std::str::from_utf8(value.body.as_slice()) + .unwrap_or("inconvertible to UTF-8") + .to_string() + ), + } +} + +/// Generates a `GenericError` for each request method. +pub fn method_to_error(method: Method) -> Result { + match method { + Method::Get => Err(Error::Generic( + StatusCode::BadRequest, + "GET request cannot have a body.".to_string(), + )), + Method::Put => Err(Error::Generic( + StatusCode::BadRequest, + "Empty PUT request.".to_string(), + )), + Method::Patch => Err(Error::Generic( + StatusCode::BadRequest, + "Empty PATCH request.".to_string(), + )), + } +} + +#[derive(Debug)] +pub enum Error { + // A generic error, with a given status code and message to be turned into a fault message. + Generic(StatusCode, String), + // The resource ID is empty. + EmptyID, + // The resource ID must only contain alphanumeric characters and '_'. + InvalidID, + // The HTTP method & request path combination is not valid. + InvalidPathMethod(String, Method), + // An error occurred when deserializing the json body of a request. + SerdeJson(serde_json::Error), +} + +// It's convenient to turn errors into HTTP responses directly. +impl Into for Error { + fn into(self) -> Response { + match self { + Error::Generic(status, msg) => { + ApiServer::json_response(status, ApiServer::json_fault_message(msg)) + } + Error::EmptyID => ApiServer::json_response( + StatusCode::BadRequest, + ApiServer::json_fault_message("The ID cannot be empty."), + ), + Error::InvalidID => ApiServer::json_response( + StatusCode::BadRequest, + ApiServer::json_fault_message( + "API Resource IDs can only contain alphanumeric characters and underscores.", + ), + ), + Error::InvalidPathMethod(path, method) => ApiServer::json_response( + StatusCode::BadRequest, + ApiServer::json_fault_message(format!( + "Invalid request method and/or path: {} {}", + std::str::from_utf8(method.raw()).unwrap(), + path + )), + ), + Error::SerdeJson(e) => ApiServer::json_response( + StatusCode::BadRequest, + ApiServer::json_fault_message(e.to_string()), + ), + } + } +} + +// This function is supposed to do id validation for requests. +pub fn checked_id(id: &str) -> Result<&str, Error> { + // todo: are there any checks we want to do on id's? + // not allow them to be empty strings maybe? + // check: ensure string is not empty + if id.is_empty() { + return Err(Error::EmptyID); + } + // check: ensure string is alphanumeric + if !id.chars().all(|c| c == '_' || c.is_alphanumeric()) { + return Err(Error::InvalidID); + } + Ok(id) +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::io::Write; + use std::os::unix::net::UnixStream; + use std::str::FromStr; + + use micro_http::HttpConnection; + use vmm::vmm_config::machine_config::VmConfig; + use vmm::{StartMicrovmError, VmmActionError}; + + impl PartialEq for ParsedRequest { + fn eq(&self, other: &ParsedRequest) -> bool { + match (self, other) { + (&ParsedRequest::Sync(ref sync_req), &ParsedRequest::Sync(ref other_sync_req)) => { + sync_req == other_sync_req + } + (&ParsedRequest::GetInstanceInfo, &ParsedRequest::GetInstanceInfo) => true, + (&ParsedRequest::GetMMDS, &ParsedRequest::GetMMDS) => true, + (&ParsedRequest::PutMMDS(ref val), &ParsedRequest::PutMMDS(ref other_val)) => { + val == other_val + } + (&ParsedRequest::PatchMMDS(ref val), &ParsedRequest::PatchMMDS(ref other_val)) => { + val == other_val + } + _ => false, + } + } + } + + #[test] + fn test_checked_id() { + assert!(checked_id("dummy").is_ok()); + assert!(checked_id("dummy_1").is_ok()); + match checked_id("") { + Err(Error::EmptyID) => {} + _ => panic!("Test failed."), + } + match checked_id("dummy!!") { + Err(Error::InvalidID) => {} + _ => panic!("Test failed."), + } + } + + #[test] + fn test_invalid_get() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"GET /mmds HTTP/1.1\r\n\ + Content-Type: text/plain\r\n\ + Content-Length: 4\r\n\r\nbody", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + match ParsedRequest::try_from_request(&req) { + Err(Error::Generic(StatusCode::BadRequest, err_msg)) => { + if err_msg != "GET request cannot have a body." { + panic!("GET request with body."); + } + } + _ => panic!("GET request with body."), + }; + } + + #[test] + fn test_invalid_put() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PUT /mmds HTTP/1.1\r\n\ + Content-Type: application/json\r\n\r\n", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + match ParsedRequest::try_from_request(&req) { + Err(Error::Generic(StatusCode::BadRequest, err_msg)) => { + if err_msg != "Empty PUT request." { + panic!("Empty PUT request."); + } + } + _ => panic!("Empty PUT request."), + }; + } + + #[test] + fn test_invalid_patch() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PATCH /mmds HTTP/1.1\r\n\ + Content-Type: application/json\r\n\r\n", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + match ParsedRequest::try_from_request(&req) { + Err(Error::Generic(StatusCode::BadRequest, err_msg)) => { + if err_msg != "Empty PATCH request." { + panic!("Empty PATCH request."); + } + } + _ => panic!("Empty PATCH request."), + }; + } + + #[test] + fn test_error_into_response() { + // Generic error. + let mut buf: [u8; 150] = [0; 150]; + let response: Response = + Error::Generic(StatusCode::BadRequest, "message".to_string()).into(); + assert!(response.write_all(&mut buf.as_mut()).is_ok()); + let expected_response = format!( + "HTTP/1.1 400 \r\n\ + Server: Firecracker API\r\n\ + Connection: keep-alive\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 32\r\n\r\n\ + {}", + ApiServer::basic_json_body("fault_message", "message") + ); + assert_eq!(&buf[..], expected_response.as_bytes()); + + // Empty ID error. + let mut buf: [u8; 166] = [0; 166]; + let response: Response = Error::EmptyID.into(); + assert!(response.write_all(&mut buf.as_mut()).is_ok()); + let expected_response = format!( + "HTTP/1.1 400 \r\n\ + Server: Firecracker API\r\n\ + Connection: keep-alive\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 48\r\n\r\n\ + {}", + ApiServer::basic_json_body("fault_message", "The ID cannot be empty.") + ); + assert_eq!(&buf[..], expected_response.as_bytes()); + + // Invalid ID error. + let mut buf: [u8; 217] = [0; 217]; + let response: Response = Error::InvalidID.into(); + assert!(response.write_all(&mut buf.as_mut()).is_ok()); + let expected_response = format!( + "HTTP/1.1 400 \r\n\ + Server: Firecracker API\r\n\ + Connection: keep-alive\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 99\r\n\r\n\ + {}", + ApiServer::basic_json_body( + "fault_message", + "API Resource IDs can only contain alphanumeric characters and underscores." + ) + ); + assert_eq!(&buf[..], expected_response.as_bytes()); + + // Invalid path or method error. + let mut buf: [u8; 187] = [0; 187]; + let response: Response = Error::InvalidPathMethod("path".to_string(), Method::Get).into(); + assert!(response.write_all(&mut buf.as_mut()).is_ok()); + let expected_response = format!( + "HTTP/1.1 400 \r\n\ + Server: Firecracker API\r\n\ + Connection: keep-alive\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 69\r\n\r\n\ + {}", + ApiServer::basic_json_body( + "fault_message", + format!( + "Invalid request method and/or path: {} {}", + std::str::from_utf8(Method::Get.raw()).unwrap(), + "path" + ) + ) + ); + assert_eq!(&buf[..], expected_response.as_bytes()); + + // Serde error. + let mut buf: [u8; 187] = [0; 187]; + let serde_error = serde_json::Value::from_str("").unwrap_err(); + let response: Response = Error::SerdeJson(serde_error).into(); + assert!(response.write_all(&mut buf.as_mut()).is_ok()); + let expected_response = format!( + "HTTP/1.1 400 \r\n\ + Server: Firecracker API\r\n\ + Connection: keep-alive\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 69\r\n\r\n\ + {}", + ApiServer::basic_json_body( + "fault_message", + "EOF while parsing a value at line 1 column 0" + ) + ); + assert_eq!(&buf[..], expected_response.as_bytes()); + } + + #[test] + fn test_describe() { + assert_eq!( + describe(Method::Get, "path", None), + "synchronous Get request on \"path\"" + ); + assert_eq!( + describe(Method::Put, "/mmds", None), + "synchronous Put request on \"/mmds\"" + ); + assert_eq!( + describe(Method::Put, "path", Some(&Body::new("body"))), + "synchronous Put request on \"path\" with body \"body\"" + ); + } + + #[test] + fn test_convert_to_response() { + // Empty Vmm data. + let mut buf: [u8; 66] = [0; 66]; + let response = ParsedRequest::convert_to_response(Ok(VmmData::Empty)); + assert!(response.write_all(&mut buf.as_mut()).is_ok()); + let expected_response = "HTTP/1.1 204 \r\n\ + Server: Firecracker API\r\n\ + Connection: keep-alive\r\n\r\n" + .to_string(); + assert_eq!(&buf[..], expected_response.as_bytes()); + + // With Vmm data. + let mut buf: [u8; 214] = [0; 214]; + let response = ParsedRequest::convert_to_response(Ok(VmmData::MachineConfiguration( + VmConfig::default(), + ))); + assert!(response.write_all(&mut buf.as_mut()).is_ok()); + let expected_response = format!( + "HTTP/1.1 200 \r\n\ + Server: Firecracker API\r\n\ + Connection: keep-alive\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 96\r\n\r\n{}", + VmConfig::default().to_string() + ); + assert_eq!(&buf[..], expected_response.as_bytes()); + + // Error. + let mut buf: [u8; 160] = [0; 160]; + let response = ParsedRequest::convert_to_response(Err(VmmActionError::from( + StartMicrovmError::EventFd, + ))); + assert!(response.write_all(&mut buf.as_mut()).is_ok()); + let expected_response = format!( + "HTTP/1.1 400 \r\n\ + Server: Firecracker API\r\n\ + Connection: keep-alive\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 42\r\n\r\n{}", + VmmActionError::from(StartMicrovmError::EventFd).to_string() + ); + assert_eq!(&buf[..], expected_response.as_bytes()); + } + + #[test] + fn test_try_from_get_info() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender.write_all(b"GET / HTTP/1.1\r\n\r\n").unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_get_machine_config() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all(b"GET /machine-config HTTP/1.1\r\n\r\n") + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_get_mmds() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender.write_all(b"GET /mmds HTTP/1.1\r\n\r\n").unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_put_actions() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PUT /actions HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 59\r\n\r\n{ \ + \"action_type\": \"BlockDeviceRescan\", \ + \"payload\": \"string\" \ + }", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_put_boot() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PUT /boot-source HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 56\r\n\r\n{ \ + \"kernel_image_path\": \"string\", \ + \"boot_args\": \"string\" \ + }", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_put_drives() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PUT /drives/string HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 266\r\n\r\n{ \ + \"drive_id\": \"string\", \ + \"path_on_host\": \"string\", \ + \"is_root_device\": true, \ + \"partuuid\": \"string\", \ + \"is_read_only\": true, \ + \"rate_limiter\": { \ + \"bandwidth\": { \ + \"size\": 0, \ + \"one_time_burst\": 0, \ + \"refill_time\": 0 \ + }, \ + \"ops\": { \ + \"size\": 0, \ + \"one_time_burst\": 0, \ + \"refill_time\": 0 \ + } \ + } \ + }", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_put_logger() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + + #[cfg(target_arch = "x86_64")] + let req_as_bytes = b"PUT /logger HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 142\r\n\r\n{ \ + \"log_fifo\": \"string\", \ + \"metrics_fifo\": \"string\", \ + \"level\": \"Warning\", \ + \"show_level\": false, \ + \"show_log_origin\": false, \ + \"options\": [ \ + \"string\" \ + ] \ + }"; + // `options` field in logger config is only available on x86_64. + #[cfg(not(target_arch = "x86_64"))] + let req_as_bytes = b"PUT /logger HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 117\r\n\r\n{ \ + \"log_fifo\": \"string\", \ + \"metrics_fifo\": \"string\", \ + \"level\": \"Warning\", \ + \"show_level\": false, \ + \"show_log_origin\": false \ + }"; + + sender.write_all(req_as_bytes).unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_put_machine_config() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PUT /machine-config HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 80\r\n\r\n{ \ + \"vcpu_count\": 0, \ + \"mem_size_mib\": 0, \ + \"ht_enabled\": true, \ + \"cpu_template\": \"C3\" \ + }", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_put_mmds() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PUT /mmds HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 2\r\n\r\n{}", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_put_netif() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PUT /network-interfaces/string HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 416\r\n\r\n{ \ + \"iface_id\": \"string\", \ + \"guest_mac\": \"12:34:56:78:9a:BC\", \ + \"host_dev_name\": \"string\", \ + \"allow_mmds_requests\": true, \ + \"rx_rate_limiter\": { \ + \"bandwidth\": { \ + \"size\": 0, \ + \"one_time_burst\": 0, \ + \"refill_time\": 0 \ + }, \ + \"ops\": { \ + \"size\": 0, \ + \"one_time_burst\": 0, \ + \"refill_time\": 0 \ + } \ + }, \ + \"tx_rate_limiter\": { \ + \"bandwidth\": { \ + \"size\": 0, \ + \"one_time_burst\": 0, \ + \"refill_time\": 0 \ + }, \ + \"ops\": { \ + \"size\": 0, \ + \"one_time_burst\": 0, \ + \"refill_time\": 0 \ + } \ + } \ + }", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_put_vsock() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PUT /vsock HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 62\r\n\r\n{ \ + \"vsock_id\": \"string\", \ + \"guest_cid\": 0, \ + \"uds_path\": \"string\" \ + }", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_patch_drives() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PATCH /drives/string HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 50\r\n\r\n{ \ + \"drive_id\": \"string\", \ + \"path_on_host\": \"string\" \ + }", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_patch_machine_config() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PATCH /machine-config HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 80\r\n\r\n{ \ + \"vcpu_count\": 0, \ + \"mem_size_mib\": 0, \ + \"ht_enabled\": true, \ + \"cpu_template\": \"C3\" \ + }", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_patch_mmds() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PATCH /mmds HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 2\r\n\r\n{}", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } + + #[test] + fn test_try_from_patch_netif() { + let (mut sender, receiver) = UnixStream::pair().unwrap(); + let mut connection = HttpConnection::new(receiver); + sender + .write_all( + b"PATCH /network-interfaces/string HTTP/1.1\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 24\r\n\r\n{ \ + \"iface_id\": \"string\" \ + }", + ) + .unwrap(); + assert!(connection.try_read().is_ok()); + let req = connection.pop_parsed_request().unwrap(); + assert!(ParsedRequest::try_from_request(&req).is_ok()); + } +} diff --git a/api_server/src/request/actions.rs b/api_server/src/request/actions.rs index a9d6cb62b2e..f01e8150713 100644 --- a/api_server/src/request/actions.rs +++ b/api_server/src/request/actions.rs @@ -1,14 +1,11 @@ // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -use std::result; - -use futures::sync::oneshot; -use hyper::Method; use serde_json::Value; -use super::{VmmAction, VmmRequest}; -use request::{IntoParsedRequest, ParsedRequest}; +use super::super::VmmAction; +use logger::{Metric, METRICS}; +use request::{Body, Error, ParsedRequest, StatusCode}; // The names of the members from this enum must precisely correspond (as a string) to the possible // values of "action_type" from the json request body. This is useful to get a strongly typed @@ -31,29 +28,33 @@ pub struct ActionBody { payload: Option, } -fn validate_payload(action_body: &ActionBody) -> Result<(), String> { +fn validate_payload(action_body: &ActionBody) -> Result<(), Error> { match action_body.action_type { ActionType::BlockDeviceRescan => { match action_body.payload { Some(ref payload) => { // Expecting to have drive_id as a String in the payload. if !payload.is_string() { - return Err( + return Err(Error::Generic( + StatusCode::BadRequest, "Invalid payload type. Expected a string representing the drive_id" .to_string(), - ); + )); } Ok(()) } - None => Err("Payload is required for block device rescan.".to_string()), + None => Err(Error::Generic( + StatusCode::BadRequest, + "Payload is required for block device rescan.".to_string(), + )), } } ActionType::FlushMetrics | ActionType::InstanceStart | ActionType::SendCtrlAltDel => { // Neither FlushMetrics nor InstanceStart should have a payload. if action_body.payload.is_some() { - return Err(format!( - "{:?} does not support a payload.", - action_body.action_type + return Err(Error::Generic( + StatusCode::BadRequest, + format!("{:?} does not support a payload.", action_body.action_type), )); } Ok(()) @@ -61,51 +62,34 @@ fn validate_payload(action_body: &ActionBody) -> Result<(), String> { } } -impl IntoParsedRequest for ActionBody { - fn into_parsed_request( - self, - _: Option, - _: Method, - ) -> result::Result { - validate_payload(&self)?; - match self.action_type { - ActionType::BlockDeviceRescan => { - // Safe to unwrap because we validated the payload in the validate_payload func. - let block_device_id = self.payload.unwrap().as_str().unwrap().to_string(); - let (sync_sender, sync_receiver) = oneshot::channel(); - Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::RescanBlockDevice(block_device_id), sync_sender), - sync_receiver, - )) - } - ActionType::FlushMetrics => { - let (sync_sender, sync_receiver) = oneshot::channel(); - Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::FlushMetrics, sync_sender), - sync_receiver, - )) - } - ActionType::InstanceStart => { - let (sync_sender, sync_receiver) = oneshot::channel(); - Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::StartMicroVm, sync_sender), - sync_receiver, - )) - } - ActionType::SendCtrlAltDel => { - // SendCtrlAltDel not supported on aarch64. - #[cfg(target_arch = "aarch64")] - return Err("SendCtrlAltDel does not supported on aarch64.".to_string()); +pub fn parse_put_actions(body: &Body) -> Result { + METRICS.put_api_requests.actions_count.inc(); + let action_body = serde_json::from_slice::(body.raw()).map_err(|e| { + METRICS.put_api_requests.actions_fails.inc(); + Error::SerdeJson(e) + })?; - #[cfg(target_arch = "x86_64")] - { - let (sync_sender, sync_receiver) = oneshot::channel(); - Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::SendCtrlAltDel, sync_sender), - sync_receiver, - )) - } - } + validate_payload(&action_body)?; + match action_body.action_type { + ActionType::BlockDeviceRescan => { + // Safe to unwrap because we validated the payload in the validate_payload func. + let block_device_id = action_body.payload.unwrap().as_str().unwrap().to_string(); + Ok(ParsedRequest::Sync(VmmAction::RescanBlockDevice( + block_device_id, + ))) + } + ActionType::FlushMetrics => Ok(ParsedRequest::Sync(VmmAction::FlushMetrics)), + ActionType::InstanceStart => Ok(ParsedRequest::Sync(VmmAction::StartMicroVm)), + ActionType::SendCtrlAltDel => { + // SendCtrlAltDel not supported on aarch64. + #[cfg(target_arch = "aarch64")] + return Err(Error::Generic( + StatusCode::BadRequest, + "SendCtrlAltDel does not supported on aarch64.".to_string(), + )); + + #[cfg(target_arch = "x86_64")] + Ok(ParsedRequest::Sync(VmmAction::SendCtrlAltDel)) } } } @@ -113,7 +97,6 @@ impl IntoParsedRequest for ActionBody { #[cfg(test)] mod tests { use super::*; - use serde_json; #[test] fn test_validate_payload() { @@ -163,7 +146,6 @@ mod tests { }; let res = validate_payload(&action_body); assert!(res.is_err()); - assert_eq!(res.unwrap_err(), "FlushMetrics does not support a payload."); // Test SendCtrlAltDel. let action_body = ActionBody { @@ -182,23 +164,16 @@ mod tests { #[test] fn test_into_parsed_request() { { + assert!(parse_put_actions(&Body::new("invalid_body")).is_err()); + let json = r#"{ "action_type": "BlockDeviceRescan", "payload": "dummy_id" }"#; - let (sender, receiver) = oneshot::channel(); - let req = ParsedRequest::Sync( - VmmRequest::new(VmmAction::RescanBlockDevice("dummy_id".to_string()), sender), - receiver, - ); - - let result: Result = serde_json::from_str(json); + let req = ParsedRequest::Sync(VmmAction::RescanBlockDevice("dummy_id".to_string())); + let result = parse_put_actions(&Body::new(json)); assert!(result.is_ok()); - assert!(result - .unwrap() - .into_parsed_request(None, Method::Put) - .unwrap() - .eq(&req)); + assert!(result.unwrap().eq(&req)); } { @@ -206,16 +181,10 @@ mod tests { "action_type": "InstanceStart" }"#; - let (sender, receiver) = oneshot::channel(); - let req: ParsedRequest = - ParsedRequest::Sync(VmmRequest::new(VmmAction::StartMicroVm, sender), receiver); - let result: Result = serde_json::from_str(json); + let req: ParsedRequest = ParsedRequest::Sync(VmmAction::StartMicroVm); + let result = parse_put_actions(&Body::new(json)); assert!(result.is_ok()); - assert!(result - .unwrap() - .into_parsed_request(None, Method::Put) - .unwrap() - .eq(&req)); + assert!(result.unwrap().eq(&req)); } #[cfg(target_arch = "x86_64")] @@ -224,16 +193,20 @@ mod tests { "action_type": "SendCtrlAltDel" }"#; - let (sender, receiver) = oneshot::channel(); - let req: ParsedRequest = - ParsedRequest::Sync(VmmRequest::new(VmmAction::SendCtrlAltDel, sender), receiver); - let result: Result = serde_json::from_str(json); + let req: ParsedRequest = ParsedRequest::Sync(VmmAction::SendCtrlAltDel); + let result = parse_put_actions(&Body::new(json)); assert!(result.is_ok()); - assert!(result - .unwrap() - .into_parsed_request(None, Method::Put) - .unwrap() - .eq(&req)); + assert!(result.unwrap().eq(&req)); + } + + #[cfg(target_arch = "aarch64")] + { + let json = r#"{ + "action_type": "SendCtrlAltDel" + }"#; + + let result = parse_put_actions(&Body::new(json)); + assert!(result.is_err()); } { @@ -241,27 +214,17 @@ mod tests { "action_type": "FlushMetrics" }"#; - let (sender, receiver) = oneshot::channel(); - let req: ParsedRequest = - ParsedRequest::Sync(VmmRequest::new(VmmAction::FlushMetrics, sender), receiver); - let result: Result = serde_json::from_str(json); + let req: ParsedRequest = ParsedRequest::Sync(VmmAction::FlushMetrics); + let result = parse_put_actions(&Body::new(json)); assert!(result.is_ok()); - assert!(result - .unwrap() - .into_parsed_request(None, Method::Put) - .unwrap() - .eq(&req)); + assert!(result.unwrap().eq(&req)); let json = r#"{ "action_type": "FlushMetrics", "payload": "metrics-payload" }"#; - - let result: Result = serde_json::from_str(json); - assert!(result.is_ok()); - let res = result.unwrap().into_parsed_request(None, Method::Put); - assert!(res.is_err()); - assert!(res == Err("FlushMetrics does not support a payload.".to_string())); + let result = parse_put_actions(&Body::new(json)); + assert!(result.is_err()); } } } diff --git a/api_server/src/request/boot_source.rs b/api_server/src/request/boot_source.rs index 45507ba99fd..fc29b5934e5 100644 --- a/api_server/src/request/boot_source.rs +++ b/api_server/src/request/boot_source.rs @@ -1,27 +1,19 @@ // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -use std::result; - -use futures::sync::oneshot; -use hyper::Method; - -use super::{VmmAction, VmmRequest}; -use request::{IntoParsedRequest, ParsedRequest}; +use super::super::VmmAction; +use logger::{Metric, METRICS}; +use request::{Body, Error, ParsedRequest}; use vmm::vmm_config::boot_source::BootSourceConfig; -impl IntoParsedRequest for BootSourceConfig { - fn into_parsed_request( - self, - _: Option, - _: Method, - ) -> result::Result { - let (sender, receiver) = oneshot::channel(); - Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::ConfigureBootSource(self), sender), - receiver, - )) - } +pub fn parse_put_boot_source(body: &Body) -> Result { + METRICS.put_api_requests.boot_source_count.inc(); + Ok(ParsedRequest::Sync(VmmAction::ConfigureBootSource( + serde_json::from_slice::(body.raw()).map_err(|e| { + METRICS.put_api_requests.boot_source_fails.inc(); + Error::SerdeJson(e) + })?, + ))) } #[cfg(test)] @@ -29,21 +21,21 @@ mod tests { use super::*; #[test] - fn test_into_parsed_request() { - let body = BootSourceConfig { - kernel_image_path: String::from("/foo/bar"), - boot_args: Some(String::from("foobar")), - }; + fn test_parse_boot_request() { + assert!(parse_put_boot_source(&Body::new("invalid_payload")).is_err()); + + let body = r#"{ + "kernel_image_path": "/foo/bar", + "boot_args": "foobar" + }"#; let same_body = BootSourceConfig { kernel_image_path: String::from("/foo/bar"), boot_args: Some(String::from("foobar")), }; - let (sender, receiver) = oneshot::channel(); - assert!(body - .into_parsed_request(None, Method::Put) - .eq(&Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::ConfigureBootSource(same_body), sender), - receiver - )))) + let result = parse_put_boot_source(&Body::new(body)); + assert!(result.is_ok()); + let parsed_req = result.unwrap_or_else(|_e| panic!("Failed test.")); + + assert!(parsed_req == ParsedRequest::Sync(VmmAction::ConfigureBootSource(same_body))); } } diff --git a/api_server/src/request/drive.rs b/api_server/src/request/drive.rs index 6f92490414c..795e4467e75 100644 --- a/api_server/src/request/drive.rs +++ b/api_server/src/request/drive.rs @@ -1,17 +1,13 @@ // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -use std::result; - -use futures::sync::oneshot; -use hyper::Method; use serde_json::{Map, Value}; -use super::{VmmAction, VmmRequest}; +use super::super::VmmAction; +use logger::{Metric, METRICS}; +use request::{checked_id, Body, Error, ParsedRequest, StatusCode}; use vmm::vmm_config::drive::BlockDeviceConfig; -use request::{IntoParsedRequest, ParsedRequest}; - #[derive(Clone)] pub struct PatchDrivePayload { // Leaving `fields` pub because ownership on it needs to be yielded to the @@ -41,24 +37,30 @@ impl PatchDrivePayload { } /// Validates that only path_on_host and drive_id are present in the payload. - fn validate(&self) -> result::Result<(), String> { + fn validate(&self) -> Result<(), Error> { match self.fields.as_object() { Some(fields_map) => { // Check that field `drive_id` exists and its type is String. - PatchDrivePayload::check_field_is_string(fields_map, "drive_id")?; + PatchDrivePayload::check_field_is_string(fields_map, "drive_id") + .map_err(|e| Error::Generic(StatusCode::BadRequest, e))?; // Check that field `drive_id` exists and its type is String. - PatchDrivePayload::check_field_is_string(fields_map, "path_on_host")?; + PatchDrivePayload::check_field_is_string(fields_map, "path_on_host") + .map_err(|e| Error::Generic(StatusCode::BadRequest, e))?; // Check that there are no other fields in the object. if fields_map.len() > 2 { - return Err( + return Err(Error::Generic( + StatusCode::BadRequest, "Invalid PATCH payload. Only updates on path_on_host are allowed." .to_string(), - ); + )); } Ok(()) } - _ => Err("Invalid json.".to_string()), + _ => Err(Error::Generic( + StatusCode::BadRequest, + "Invalid json.".to_string(), + )), } } @@ -74,222 +76,189 @@ impl PatchDrivePayload { } } -impl IntoParsedRequest for PatchDrivePayload { - fn into_parsed_request( - self, - id_from_path: Option, - method: Method, - ) -> result::Result { - match method { - Method::Patch => { - self.validate()?; - let drive_id: String = self.get_string_field_unchecked("drive_id"); - let path_on_host: String = self.get_string_field_unchecked("path_on_host"); +pub fn parse_put_drive(body: &Body, id_from_path: Option<&&str>) -> Result { + METRICS.put_api_requests.drive_count.inc(); + let id = match id_from_path { + Some(&id) => checked_id(id)?, + None => { + return Err(Error::EmptyID); + } + }; - let id_from_path = id_from_path.unwrap_or_default(); - if id_from_path != drive_id { - return Err(String::from( - "The id from the path does not match the id from the body!", - )); - } + let device_cfg = serde_json::from_slice::(body.raw()).map_err(|e| { + METRICS.put_api_requests.drive_fails.inc(); + Error::SerdeJson(e) + })?; - let (sender, receiver) = oneshot::channel(); - Ok(ParsedRequest::Sync( - VmmRequest::new( - VmmAction::UpdateBlockDevicePath(drive_id, path_on_host), - sender, - ), - receiver, - )) - } - _ => Err(format!("Invalid method {}!", method)), - } + if id != device_cfg.drive_id { + METRICS.put_api_requests.drive_fails.inc(); + Err(Error::Generic( + StatusCode::BadRequest, + "The id from the path does not match the id from the body!".to_string(), + )) + } else { + Ok(ParsedRequest::Sync(VmmAction::InsertBlockDevice( + device_cfg, + ))) } } -impl IntoParsedRequest for BlockDeviceConfig { - fn into_parsed_request( - self, - id_from_path: Option, - method: Method, - ) -> result::Result { - let id_from_path = id_from_path.unwrap_or_default(); - if id_from_path != self.drive_id { - return Err(String::from( - "The id from the path does not match the id from the body!", - )); - } - let (sender, receiver) = oneshot::channel(); - match method { - Method::Put => Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::InsertBlockDevice(self), sender), - receiver, - )), - _ => Err(String::from("Invalid method.")), +pub fn parse_patch_drive(body: &Body, id_from_path: Option<&&str>) -> Result { + METRICS.patch_api_requests.drive_count.inc(); + let id = match id_from_path { + Some(&id) => checked_id(id)?, + None => { + return Err(Error::EmptyID); } + }; + + METRICS.patch_api_requests.drive_count.inc(); + let patch_drive_payload = PatchDrivePayload { + fields: serde_json::from_slice(body.raw()).map_err(|e| { + METRICS.patch_api_requests.drive_fails.inc(); + Error::SerdeJson(e) + })?, + }; + + patch_drive_payload.validate()?; + let drive_id: String = patch_drive_payload.get_string_field_unchecked("drive_id"); + let path_on_host: String = patch_drive_payload.get_string_field_unchecked("path_on_host"); + + if id != drive_id.as_str() { + METRICS.patch_api_requests.drive_fails.inc(); + return Err(Error::Generic( + StatusCode::BadRequest, + String::from("The id from the path does not match the id from the body!"), + )); } + + Ok(ParsedRequest::Sync(VmmAction::UpdateBlockDevicePath( + drive_id, + path_on_host, + ))) } #[cfg(test)] mod tests { use super::*; - use serde_json::Number; - use std::path::PathBuf; - #[test] - fn test_patch_into_parsed_request() { + fn test_parse_patch_request() { + assert!(parse_patch_drive(&Body::new("invalid_payload"), Some(&"id")).is_err()); + // PATCH with invalid fields. - let mut payload_map = Map::::new(); - payload_map.insert(String::from("drive_id"), Value::String(String::from("bar"))); - payload_map.insert(String::from("is_read_only"), Value::Bool(false)); - let patch_payload = PatchDrivePayload { - fields: Value::Object(payload_map), - }; - let expected_err = Err("Required key path_on_host not present in the json.".to_string()); - assert!(patch_payload.into_parsed_request(None, Method::Patch) == expected_err); + let body = r#"{ + "drive_id": "bar", + "is_read_only": false + }"#; + assert!(parse_patch_drive(&Body::new(body), Some(&"2")).is_err()); // PATCH with invalid types on fields. Adding a drive_id as number instead of string. - let mut payload_map = Map::::new(); - payload_map.insert(String::from("drive_id"), Value::Number(Number::from(1000))); - payload_map.insert( - String::from("path_on_host"), - Value::String(String::from("dummy")), - ); - let patch_payload = PatchDrivePayload { - fields: Value::Object(payload_map), - }; - let expected_err = Err("Invalid type for key drive_id.".to_string()); - assert!(patch_payload.into_parsed_request(None, Method::Patch) == expected_err); + let body = r#"{ + "drive_id": 1000, + "path_on_host": "dummy" + }"#; + let res = parse_patch_drive(&Body::new(body), Some(&"1000")); + assert!(res.is_err()); // PATCH with invalid types on fields. Adding a path_on_host as bool instead of string. - let mut payload_map = Map::::new(); - payload_map.insert( - String::from("drive_id"), - Value::String(String::from("dummy_id")), - ); - payload_map.insert(String::from("path_on_host"), Value::Bool(true)); - let patch_payload = PatchDrivePayload { - fields: Value::Object(payload_map), - }; - assert!(patch_payload - .into_parsed_request(None, Method::Patch) - .is_err()); + let body = r#"{ + "drive_id": 1000, + "path_on_host": true + }"#; + let res = parse_patch_drive(&Body::new(body), Some(&"1000")); + assert!(res.is_err()); // PATCH with missing path_on_host field. - let mut payload_map = Map::::new(); - payload_map.insert( - String::from("drive_id"), - Value::String(String::from("dummy_id")), - ); - let patch_payload = PatchDrivePayload { - fields: Value::Object(payload_map), - }; - let expected_err = Err("Required key path_on_host not present in the json.".to_string()); - assert!(patch_payload.into_parsed_request(None, Method::Patch) == expected_err); + let body = r#"{ + "drive_id": "dummy_id" + }"#; + let res = parse_patch_drive(&Body::new(body), Some(&"dummy_id")); + assert!(res.is_err()); // PATCH with missing drive_id field. - let mut payload_map = Map::::new(); - payload_map.insert(String::from("path_on_host"), Value::Bool(true)); - let patch_payload = PatchDrivePayload { - fields: Value::Object(payload_map), - }; - let expected_err = Err("Required key drive_id not present in the json.".to_string()); - assert!(patch_payload.into_parsed_request(None, Method::Patch) == expected_err); + let body = r#"{ + "path_on_host": true + }"#; + let res = parse_patch_drive(&Body::new(body), Some(&"1000")); + assert!(res.is_err()); // PATCH that tries to update something else other than path_on_host. - let mut payload_map = Map::new(); - payload_map.insert( - String::from("drive_id"), - Value::String(String::from("1234")), - ); - payload_map.insert( - String::from("path_on_host"), - Value::String(String::from("dummy")), - ); - payload_map.insert(String::from("is_read_only"), Value::Bool(false)); - - let patch_payload = PatchDrivePayload { - fields: Value::Object(payload_map), - }; - let expected_err = - Err("Invalid PATCH payload. Only updates on path_on_host are allowed.".to_string()); - assert!(patch_payload.into_parsed_request(None, Method::Patch) == expected_err); + let body = r#"{ + "drive_id": 1234, + "path_on_host": "dummy", + "is_read_only": false + }"#; + let res = parse_patch_drive(&Body::new(body), Some(&"1234")); + assert!(res.is_err()); // PATCH with payload that is not a json. - let patch_payload = PatchDrivePayload { - fields: Value::String(String::from("dummy_payload")), - }; - assert!( - patch_payload.into_parsed_request(None, Method::Patch) - == Err("Invalid json.".to_string()) - ); + let body = r#"{ + "fields": "dummy_field" + }"#; + assert!(parse_patch_drive(&Body::new(body), Some(&"1234")).is_err()); - let mut payload_map = Map::::new(); - payload_map.insert(String::from("drive_id"), Value::String(String::from("foo"))); - payload_map.insert( - String::from("path_on_host"), - Value::String(String::from("dummy")), - ); - let pdp = PatchDrivePayload { - fields: Value::Object(payload_map), + let body = r#"{ + "drive_id": "foo", + "path_on_host": "dummy" + }"#; + match parse_patch_drive(&Body::new(body), Some(&"foo")) { + Ok(ParsedRequest::Sync(VmmAction::UpdateBlockDevicePath(a, b))) => { + assert_eq!(a, "foo".to_string()); + assert_eq!(b, "dummy".to_string()); + } + Err(_e) => panic!("Test failed."), + _ => panic!("Test failed: Invalid parameters"), }; - let (sender, receiver) = oneshot::channel(); - - assert!(pdp - .clone() - .into_parsed_request(Some("foo".to_string()), Method::Patch) - .eq(&Ok(ParsedRequest::Sync( - VmmRequest::new( - VmmAction::UpdateBlockDevicePath("foo".to_string(), "dummy".to_string()), - sender - ), - receiver - )))); - assert!( - pdp.into_parsed_request(None, Method::Put) == Err(String::from("Invalid method PUT!")) - ); + let body = r#"{ + "drive_id": "foo", + "path_on_host": "dummy" + }"#; + assert!(parse_patch_drive(&Body::new(body), Some(&"bar")).is_err()); } #[test] - fn test_into_parsed_request() { - let desc = BlockDeviceConfig { - drive_id: String::from("foo"), - path_on_host: PathBuf::from(String::from("/foo/bar")), - is_root_device: true, - is_read_only: true, - partuuid: None, - rate_limiter: None, - }; - assert!( - desc.into_parsed_request(Some(String::from("foo")), Method::Options) - == Err(String::from("Invalid method.")) - ); + fn test_parse_put_request() { + assert!(parse_put_drive(&Body::new("invalid_payload"), Some(&"id")).is_err()); - // BlockDeviceConfig doesn't implement Clone so we have to define multiple identical vars. - let desc = BlockDeviceConfig { - drive_id: String::from("foo"), - path_on_host: PathBuf::from(String::from("/foo/bar")), - is_root_device: true, - is_read_only: true, - partuuid: None, - rate_limiter: None, - }; - let same_desc = BlockDeviceConfig { - drive_id: String::from("foo"), - path_on_host: PathBuf::from(String::from("/foo/bar")), - is_root_device: true, - is_read_only: true, - partuuid: None, - rate_limiter: None, + // PATCH with invalid fields. + let body = r#"{ + "drive_id": "bar", + "is_read_only": false + }"#; + assert!(parse_put_drive(&Body::new(body), Some(&"2")).is_err()); + + // PATCH with invalid types on fields. Adding a drive_id as number instead of string. + let body = r#"{ + "drive_id": "1000", + "path_on_host": "dummy", + "is_root_device": true, + "partuuid": "string", + "is_read_only": true, + "rate_limiter": { + "bandwidth": { + "size": 0, + "one_time_burst": 0, + "refill_time": 0 + }, + "ops": { + "size": 0, + "one_time_burst": 0, + "refill_time": 0 + } + } + }"#; + assert!(parse_put_drive(&Body::new(body), Some(&"1000")).is_ok()); + + assert!(parse_put_drive(&Body::new(body), Some(&"foo")).is_err()); + } + + #[test] + fn test_validate() { + let pdp = PatchDrivePayload { + fields: Value::Null, }; - let (sender, receiver) = oneshot::channel(); - assert!(desc - .into_parsed_request(Some(String::from("foo")), Method::Put) - .eq(&Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::InsertBlockDevice(same_desc), sender), - receiver - )))); + assert!(pdp.validate().is_err()); } } diff --git a/api_server/src/request/instance_info.rs b/api_server/src/request/instance_info.rs new file mode 100644 index 00000000000..8dd2171cd4b --- /dev/null +++ b/api_server/src/request/instance_info.rs @@ -0,0 +1,23 @@ +// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use logger::{Metric, METRICS}; +use request::{Error, ParsedRequest}; + +pub fn parse_get_instance_info() -> Result { + METRICS.get_api_requests.instance_info_count.inc(); + Ok(ParsedRequest::GetInstanceInfo) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_request() { + match parse_get_instance_info() { + Ok(ParsedRequest::GetInstanceInfo) => {} + _ => panic!("Test failed."), + } + } +} diff --git a/api_server/src/request/logger.rs b/api_server/src/request/logger.rs index a3354b5f680..f9c29771c13 100644 --- a/api_server/src/request/logger.rs +++ b/api_server/src/request/logger.rs @@ -1,27 +1,19 @@ // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -use std::result; - -use futures::sync::oneshot; -use hyper::Method; - -use super::{VmmAction, VmmRequest}; -use request::{IntoParsedRequest, ParsedRequest}; +use super::super::VmmAction; +use logger::{Metric, METRICS}; +use request::{Body, Error, ParsedRequest}; use vmm::vmm_config::logger::LoggerConfig; -impl IntoParsedRequest for LoggerConfig { - fn into_parsed_request( - self, - _: Option, - _: Method, - ) -> result::Result { - let (sender, receiver) = oneshot::channel(); - Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::ConfigureLogger(self), sender), - receiver, - )) - } +pub fn parse_put_logger(body: &Body) -> Result { + METRICS.put_api_requests.logger_count.inc(); + Ok(ParsedRequest::Sync(VmmAction::ConfigureLogger( + serde_json::from_slice::(body.raw()).map_err(|e| { + METRICS.put_api_requests.logger_fails.inc(); + Error::SerdeJson(e) + })?, + ))) } #[cfg(test)] @@ -33,8 +25,18 @@ mod tests { use vmm::vmm_config::logger::LoggerLevel; #[test] - fn test_into_parsed_request() { - let desc = LoggerConfig { + #[cfg(target_arch = "x86_64")] + fn test_parse_logger_request_x64() { + let body = r#"{ + "log_fifo": "log", + "metrics_fifo": "metrics", + "level": "Warning", + "show_level": false, + "show_log_origin": false, + "options": [] + }"#; + + let desc_clone = LoggerConfig { log_fifo: String::from("log"), metrics_fifo: String::from("metrics"), level: LoggerLevel::Warning, @@ -43,15 +45,37 @@ mod tests { #[cfg(target_arch = "x86_64")] options: Value::Array(vec![]), }; - format!("{:?}", desc); - assert!(&desc.clone().into_parsed_request(None, Method::Put).is_ok()); - let (sender, receiver) = oneshot::channel(); - assert!(&desc - .clone() - .into_parsed_request(None, Method::Put) - .eq(&Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::ConfigureLogger(desc), sender), - receiver - )))); + match parse_put_logger(&Body::new(body)) { + Ok(ParsedRequest::Sync(VmmAction::ConfigureLogger(desc))) => { + assert_eq!(desc, desc_clone) + } + _ => panic!("Test failed."), + } + } + + #[test] + #[cfg(not(target_arch = "x86_64"))] + fn test_parse_logger_request() { + let body = r#"{ + "log_fifo": "log", + "metrics_fifo": "metrics", + "level": "Warning", + "show_level": false, + "show_log_origin": false + }"#; + + let desc_clone = LoggerConfig { + log_fifo: String::from("log"), + metrics_fifo: String::from("metrics"), + level: LoggerLevel::Warning, + show_level: false, + show_log_origin: false, + }; + match parse_put_logger(&Body::new(body)) { + Ok(ParsedRequest::Sync(VmmAction::ConfigureLogger(desc))) => { + assert_eq!(desc, desc_clone) + } + _ => panic!("Test failed."), + } } } diff --git a/api_server/src/request/machine_configuration.rs b/api_server/src/request/machine_configuration.rs index d09e3e6bfb8..1c67e45bcd0 100644 --- a/api_server/src/request/machine_configuration.rs +++ b/api_server/src/request/machine_configuration.rs @@ -1,127 +1,112 @@ // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -use std::result; - -use futures::sync::oneshot; -use hyper::{Method, Response, StatusCode}; - -use super::{VmmAction, VmmRequest}; -use http_service::json_response; -use request::{GenerateHyperResponse, IntoParsedRequest, ParsedRequest}; +use super::super::VmmAction; +use logger::{Metric, METRICS}; +use request::{method_to_error, Body, Error, Method, ParsedRequest, StatusCode}; use vmm::vmm_config::machine_config::VmConfig; -impl GenerateHyperResponse for VmConfig { - fn generate_response(&self) -> Response { - let vcpu_count = self.vcpu_count.unwrap_or(1); - let mem_size = self.mem_size_mib.unwrap_or(128); - let ht_enabled = self.ht_enabled.unwrap_or(false); - let cpu_template = self - .cpu_template - .map_or("Uninitialized".to_string(), |c| c.to_string()); +pub fn parse_get_machine_config() -> Result { + METRICS.get_api_requests.machine_cfg_count.inc(); + Ok(ParsedRequest::Sync(VmmAction::GetVmConfiguration)) +} - json_response( - StatusCode::Ok, - format!( - "{{ \"vcpu_count\": {:?}, \"mem_size_mib\": {:?}, \"ht_enabled\": {:?}, \"cpu_template\": {:?} }}", - vcpu_count, mem_size, ht_enabled, cpu_template - ), - ) +pub fn parse_put_machine_config(body: &Body) -> Result { + METRICS.put_api_requests.machine_cfg_count.inc(); + let vm_config = serde_json::from_slice::(body.raw()).map_err(|e| { + METRICS.put_api_requests.machine_cfg_fails.inc(); + Error::SerdeJson(e) + })?; + if vm_config.vcpu_count.is_none() + || vm_config.mem_size_mib.is_none() + || vm_config.ht_enabled.is_none() + { + return Err(Error::Generic( + StatusCode::BadRequest, + "Missing mandatory fields.".to_string(), + )); } + Ok(ParsedRequest::Sync(VmmAction::SetVmConfiguration( + vm_config, + ))) } -impl IntoParsedRequest for VmConfig { - fn into_parsed_request( - self, - _: Option, - method: Method, - ) -> result::Result { - let (sender, receiver) = oneshot::channel(); - match method { - Method::Get => Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::GetVmConfiguration, sender), - receiver, - )), - Method::Patch => { - if self.vcpu_count.is_none() - && self.mem_size_mib.is_none() - && self.cpu_template.is_none() - && self.ht_enabled.is_none() - { - return Err(String::from("Empty PATCH request.")); - } - Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::SetVmConfiguration(self), sender), - receiver, - )) - } - Method::Put => { - if self.vcpu_count.is_none() - || self.mem_size_mib.is_none() - || self.ht_enabled.is_none() - { - return Err(String::from("Missing mandatory fields.")); - } - Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::SetVmConfiguration(self), sender), - receiver, - )) - } - _ => Err(String::from("Invalid method.")), - } +pub fn parse_patch_machine_config(body: &Body) -> Result { + METRICS.patch_api_requests.machine_cfg_count.inc(); + let vm_config = serde_json::from_slice::(body.raw()).map_err(|e| { + METRICS.patch_api_requests.machine_cfg_fails.inc(); + Error::SerdeJson(e) + })?; + if vm_config.vcpu_count.is_none() + && vm_config.mem_size_mib.is_none() + && vm_config.cpu_template.is_none() + && vm_config.ht_enabled.is_none() + { + return method_to_error(Method::Patch); } + Ok(ParsedRequest::Sync(VmmAction::SetVmConfiguration( + vm_config, + ))) } #[cfg(test)] mod tests { use super::*; + use vmm::vmm_config::machine_config::CpuFeaturesTemplate; #[test] - fn test_into_parsed_request() { - let body = VmConfig { + fn test_parse_get_machine_config_request() { + assert!(parse_get_machine_config().is_ok()); + } + + #[test] + fn test_parse_put_machine_config_request() { + assert!(parse_put_machine_config(&Body::new("invalid_payload")).is_err()); + + let config_clone = VmConfig { vcpu_count: Some(8), mem_size_mib: Some(1024), ht_enabled: Some(true), cpu_template: Some(CpuFeaturesTemplate::T2), }; - let (sender, receiver) = oneshot::channel(); - assert!(body - .clone() - .into_parsed_request(None, Method::Put) - .eq(&Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::SetVmConfiguration(body), sender), - receiver - )))); + let body = r#"{ + "vcpu_count": 8, + "mem_size_mib": 1024, + "ht_enabled": true, + "cpu_template": "T2" + }"#; + match parse_put_machine_config(&Body::new(body)) { + Ok(ParsedRequest::Sync(VmmAction::SetVmConfiguration(config))) => { + assert_eq!(config, config_clone) + } + _ => panic!("Test failed."), + } - let uninitialized = VmConfig { - vcpu_count: None, - mem_size_mib: None, - ht_enabled: None, - cpu_template: None, - }; - assert!(uninitialized - .clone() - .into_parsed_request(None, Method::Get) - .is_ok()); + let body = r#"{ + "vcpu_count": 8, + "mem_size_mib": 1024 + }"#; + assert!(parse_put_machine_config(&Body::new(body)).is_err()); + } - // Empty PATCH - assert!(uninitialized - .clone() - .into_parsed_request(None, Method::Patch) - .is_err()); + #[test] + fn test_parse_patch_machine_config_request() { + assert!(parse_patch_machine_config(&Body::new("invalid_payload")).is_err()); - // Incomplete PUT payload - let body = VmConfig { - vcpu_count: Some(8), - mem_size_mib: Some(1024), - ht_enabled: None, - cpu_template: Some(CpuFeaturesTemplate::T2), - }; - if let Err(e) = body.into_parsed_request(None, Method::Put) { - assert_eq!(e, String::from("Missing mandatory fields.")); - } else { - panic!(); - } + let body = r#"{}"#; + assert!(parse_patch_machine_config(&Body::new(body)).is_err()); + + let body = r#"{ + "vcpu_count": 8, + "mem_size_mib": 1024 + }"#; + assert!(parse_patch_machine_config(&Body::new(body)).is_ok()); + let body = r#"{ + "vcpu_count": 8, + "mem_size_mib": 1024, + "ht_enabled": false + }"#; + assert!(parse_patch_machine_config(&Body::new(body)).is_ok()); } } diff --git a/api_server/src/request/mmds.rs b/api_server/src/request/mmds.rs new file mode 100644 index 00000000000..107888caa5d --- /dev/null +++ b/api_server/src/request/mmds.rs @@ -0,0 +1,52 @@ +// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use request::{Body, Error, ParsedRequest}; + +pub fn parse_get_mmds() -> Result { + Ok(ParsedRequest::GetMMDS) +} + +pub fn parse_put_mmds(body: &Body) -> Result { + Ok(ParsedRequest::PutMMDS( + serde_json::from_slice(body.raw()).map_err(Error::SerdeJson)?, + )) +} + +pub fn parse_patch_mmds(body: &Body) -> Result { + Ok(ParsedRequest::PatchMMDS( + serde_json::from_slice(body.raw()).map_err(Error::SerdeJson)?, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_get_mmds_request() { + assert!(parse_get_mmds().is_ok()); + } + + #[test] + fn test_parse_put_mmds_request() { + let body = r#"{ + "foo": "bar" + }"#; + assert!(parse_put_mmds(&Body::new(body)).is_ok()); + + let body = "invalid_body"; + assert!(parse_put_mmds(&Body::new(body)).is_err()); + } + + #[test] + fn test_parse_patch_mmds_request() { + let body = r#"{ + "foo": "bar" + }"#; + assert!(parse_put_mmds(&Body::new(body)).is_ok()); + + let body = "invalid_body"; + assert!(parse_put_mmds(&Body::new(body)).is_err()); + } +} diff --git a/api_server/src/request/mod.rs b/api_server/src/request/mod.rs index 5c0aeff2d40..4900bed6493 100644 --- a/api_server/src/request/mod.rs +++ b/api_server/src/request/mod.rs @@ -4,393 +4,13 @@ pub mod actions; pub mod boot_source; pub mod drive; +pub mod instance_info; pub mod logger; pub mod machine_configuration; +pub mod mmds; pub mod net; pub mod vsock; - -use serde_json::Value; -use std::result; - -use hyper; -use hyper::{Method, StatusCode}; - -use super::{ResponseReceiver, VmmAction, VmmData, VmmRequest}; -use http_service::{empty_response, json_fault_message, json_response}; -use vmm::{ErrorKind, VmmActionError}; - -pub enum ParsedRequest { - GetInstanceInfo, - GetMMDS, - PatchMMDS(Value), - PutMMDS(Value), - Sync(VmmRequest, ResponseReceiver), -} - -pub trait IntoParsedRequest { - fn into_parsed_request( - self, - resource_id: Option, - method: Method, - ) -> result::Result; -} - -// Sync requests have outcomes which implement this trait. The idea is for each outcome to be a -// struct which is cheaply and quickly instantiated by the VMM thread, then passed back the the API -// thread, and then unpacked into a http response using the implementation of -// the generate_response() method. -pub trait GenerateHyperResponse { - fn generate_response(&self) -> hyper::Response; -} - -impl GenerateHyperResponse for result::Result { - fn generate_response(&self) -> hyper::Response { - match *self { - Ok(ref data) => data.generate_response(), - Err(ref error) => error.generate_response(), - } - } -} - -impl GenerateHyperResponse for VmmData { - fn generate_response(&self) -> hyper::Response { - match *self { - VmmData::MachineConfiguration(ref machine_config) => machine_config.generate_response(), - VmmData::Empty => empty_response(StatusCode::NoContent), - } - } -} - -impl GenerateHyperResponse for VmmActionError { - fn generate_response(&self) -> hyper::Response { - use self::ErrorKind::*; - - let status_code = match self.kind() { - User => StatusCode::BadRequest, - Internal => StatusCode::InternalServerError, - }; - - json_response(status_code, json_fault_message(self.to_string())) - } -} - -#[cfg(test)] -mod tests { - extern crate arch; - extern crate devices; - extern crate kernel; - extern crate memory_model; - extern crate net_util; - - use self::devices::virtio::net::Error as VirtioNetError; - use self::memory_model::GuestMemoryError; - use self::net_util::TapError; - use super::*; - - use std::io; - - use vmm::error::StartMicrovmError; - use vmm::vmm_config::boot_source::BootSourceConfigError; - use vmm::vmm_config::drive::DriveError; - use vmm::vmm_config::logger::LoggerConfigError; - use vmm::vmm_config::machine_config::{VmConfig, VmConfigError}; - use vmm::vmm_config::net::NetworkInterfaceError; - - use futures::{Future, Stream}; - use hyper::{Body, Response}; - use serde_json; - use std; - use vmm::vmm_config::vsock::VsockError; - - impl PartialEq for ParsedRequest { - fn eq(&self, other: &ParsedRequest) -> bool { - match (self, other) { - ( - &ParsedRequest::Sync(ref sync_req, _), - &ParsedRequest::Sync(ref other_sync_req, _), - ) => sync_req == other_sync_req, - (&ParsedRequest::GetInstanceInfo, &ParsedRequest::GetInstanceInfo) => true, - (&ParsedRequest::GetMMDS, &ParsedRequest::GetMMDS) => true, - (&ParsedRequest::PutMMDS(ref val), &ParsedRequest::PutMMDS(ref other_val)) => { - val == other_val - } - (&ParsedRequest::PatchMMDS(ref val), &ParsedRequest::PatchMMDS(ref other_val)) => { - val == other_val - } - _ => false, - } - } - } - - fn get_body( - response: Response, - ) -> std::result::Result { - let body = response - .body() - .map_err(|_| ()) - .fold(vec![], |mut acc, chunk| { - acc.extend_from_slice(&chunk); - Ok(acc) - }) - .and_then(|v| String::from_utf8(v).map_err(|_| ())); - serde_json::from_str::(body.wait().unwrap().as_ref()) - } - - fn check_error_response(error: VmmActionError, status_code: StatusCode) { - let hyper_resp = Err(error).generate_response(); - assert_eq!(hyper_resp.status(), status_code); - assert!(get_body(hyper_resp).is_ok()); - } - - #[test] - fn test_generate_response() { - // Test OK Empty response from VMM. - let vmm_resp = Ok(VmmData::Empty); - let hyper_resp = vmm_resp.generate_response(); - assert_eq!(hyper_resp.status(), StatusCode::NoContent); - // assert that the body is empty. When the JSON is empty, serde returns and EOF error. - let body_err = get_body(hyper_resp).unwrap_err(); - assert_eq!( - body_err.to_string(), - "EOF while parsing a value at line 1 column 0" - ); - - // Test OK response from VMM that contains the Machine Configuration. - let vmm_resp = Ok(VmmData::MachineConfiguration(VmConfig::default())); - let hyper_resp = vmm_resp.generate_response(); - assert_eq!(hyper_resp.status(), StatusCode::Ok); - let vm_config_json = r#"{ - "vcpu_count": 1, - "mem_size_mib": 128, - "ht_enabled": false, - "cpu_template": "Uninitialized" - }"#; - let vm_config_json: serde_json::Value = serde_json::from_str(vm_config_json).unwrap(); - assert_eq!(get_body(hyper_resp).unwrap(), vm_config_json); - - // Tests Error Cases - // Tests for BootSource Errors. - let vmm_resp = VmmActionError::BootSource( - ErrorKind::User, - BootSourceConfigError::InvalidKernelPath(std::io::Error::from_raw_os_error(2)), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::BootSource( - ErrorKind::User, - BootSourceConfigError::InvalidKernelCommandLine( - kernel::cmdline::Error::HasSpace.to_string(), - ), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::BootSource( - ErrorKind::User, - BootSourceConfigError::UpdateNotAllowedPostBoot, - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - - // Tests for DriveConfig Errors. - let vmm_resp = - VmmActionError::DriveConfig(ErrorKind::User, DriveError::CannotOpenBlockDevice); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = - VmmActionError::DriveConfig(ErrorKind::User, DriveError::InvalidBlockDeviceID); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = - VmmActionError::DriveConfig(ErrorKind::User, DriveError::InvalidBlockDevicePath); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = - VmmActionError::DriveConfig(ErrorKind::User, DriveError::BlockDevicePathAlreadyExists); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = - VmmActionError::DriveConfig(ErrorKind::User, DriveError::OperationNotAllowedPreBoot); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = - VmmActionError::DriveConfig(ErrorKind::User, DriveError::RootBlockDeviceAlreadyAdded); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = - VmmActionError::DriveConfig(ErrorKind::User, DriveError::UpdateNotAllowedPostBoot); - check_error_response(vmm_resp, StatusCode::BadRequest); - - // Tests for Logger Errors. - let vmm_resp = VmmActionError::Logger( - ErrorKind::User, - LoggerConfigError::InitializationFailure( - "Could not open logging fifo: dummy".to_string(), - ), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - - // Tests for MachineConfig Errors. - let vmm_resp = - VmmActionError::MachineConfig(ErrorKind::User, VmConfigError::InvalidVcpuCount); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = - VmmActionError::MachineConfig(ErrorKind::User, VmConfigError::InvalidMemorySize); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = - VmmActionError::MachineConfig(ErrorKind::User, VmConfigError::UpdateNotAllowedPostBoot); - check_error_response(vmm_resp, StatusCode::BadRequest); - - // Tests for NetworkConfig Errors. - let vmm_resp = VmmActionError::NetworkConfig( - ErrorKind::User, - NetworkInterfaceError::OpenTap(TapError::OpenTun(io::Error::from_raw_os_error(22))), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::NetworkConfig( - ErrorKind::User, - NetworkInterfaceError::GuestMacAddressInUse(String::from("12:34:56:78:9a:bc")), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::NetworkConfig( - ErrorKind::User, - NetworkInterfaceError::UpdateNotAllowedPostBoot, - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::NetworkConfig( - ErrorKind::User, - NetworkInterfaceError::HostDeviceNameInUse(String::from("tap_name")), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - - // Tests for MicrovmStart Errors. - // RegisterBlockDevice, RegisterNetDevice, and LegacyIOBus cannot be tested because the - // device manager is a private module in the vmm crate. - // ConfigureVm, Vcpu and VcpuConfigure cannot be tested because vstate is a private module - // in the vmm crate. - let vmm_resp = - VmmActionError::StartMicrovm(ErrorKind::User, StartMicrovmError::MicroVMAlreadyRunning); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = - VmmActionError::StartMicrovm(ErrorKind::User, StartMicrovmError::MissingKernelConfig); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::Internal, - StartMicrovmError::GuestMemory(GuestMemoryError::MemoryNotInitialized), - ); - check_error_response(vmm_resp, StatusCode::InternalServerError); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::Internal, - StartMicrovmError::KernelCmdline(String::from("dummy error.")), - ); - check_error_response(vmm_resp, StatusCode::InternalServerError); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::Internal, - StartMicrovmError::CreateBlockDevice(io::Error::from_raw_os_error(22)), - ); - check_error_response(vmm_resp, StatusCode::InternalServerError); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::OpenBlockDevice(io::Error::from_raw_os_error(22)), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::Internal, - StartMicrovmError::NetDeviceNotConfigured, - ); - check_error_response(vmm_resp, StatusCode::InternalServerError); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::Internal, - StartMicrovmError::CreateNetDevice(VirtioNetError::TapOpen(TapError::OpenTun( - io::Error::from_raw_os_error(22), - ))), - ); - check_error_response(vmm_resp, StatusCode::InternalServerError); - #[cfg(target_arch = "x86_64")] - { - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::Internal, - StartMicrovmError::ConfigureSystem(arch::Error::ZeroPagePastRamEnd), - ); - check_error_response(vmm_resp, StatusCode::InternalServerError); - } - let vmm_resp = - VmmActionError::StartMicrovm(ErrorKind::Internal, StartMicrovmError::EventFd); - check_error_response(vmm_resp, StatusCode::InternalServerError); - let vmm_resp = - VmmActionError::StartMicrovm(ErrorKind::Internal, StartMicrovmError::RegisterEvent); - check_error_response(vmm_resp, StatusCode::InternalServerError); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::Internal, - StartMicrovmError::VcpusNotConfigured, - ); - check_error_response(vmm_resp, StatusCode::InternalServerError); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::Internal, - StartMicrovmError::VcpuSpawn(std::io::Error::from_raw_os_error(11)), - ); - check_error_response(vmm_resp, StatusCode::InternalServerError); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::KernelLoader(kernel::loader::Error::BigEndianElfOnLittle), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::LoadCommandline(kernel::cmdline::Error::CommandLineCopy), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::LoadCommandline(kernel::cmdline::Error::CommandLineOverflow), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::KernelLoader(kernel::loader::Error::InvalidElfMagicNumber), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::KernelLoader(kernel::loader::Error::InvalidEntryAddress), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::KernelLoader(kernel::loader::Error::InvalidProgramHeaderSize), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::KernelLoader(kernel::loader::Error::InvalidProgramHeaderOffset), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::KernelLoader(kernel::loader::Error::InvalidProgramHeaderAddress), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::KernelLoader(kernel::loader::Error::ReadKernelDataStruct( - "Failed to read data structure from kernel image", - )), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::KernelLoader(kernel::loader::Error::ReadKernelImage), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::KernelLoader(kernel::loader::Error::SeekKernelStart), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::KernelLoader(kernel::loader::Error::SeekKernelImage), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - let vmm_resp = VmmActionError::StartMicrovm( - ErrorKind::User, - StartMicrovmError::KernelLoader(kernel::loader::Error::SeekProgramHeader), - ); - check_error_response(vmm_resp, StatusCode::BadRequest); - - // Tests for VsockConfig Errors. - let vmm_resp = - VmmActionError::VsockConfig(ErrorKind::User, VsockError::UpdateNotAllowedPostBoot); - check_error_response(vmm_resp, StatusCode::BadRequest); - } -} +pub use micro_http::{ + Body, HttpServer, Method, Request, RequestError, Response, StatusCode, Version, +}; +use parsed_request::{checked_id, method_to_error, Error, ParsedRequest}; diff --git a/api_server/src/request/net.rs b/api_server/src/request/net.rs index 5c48dff745e..8058ffad82c 100644 --- a/api_server/src/request/net.rs +++ b/api_server/src/request/net.rs @@ -1,55 +1,56 @@ // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -use std::result; - -use futures::sync::oneshot; -use hyper::Method; - -use super::{VmmAction, VmmRequest}; -use request::{IntoParsedRequest, ParsedRequest}; +use super::super::VmmAction; +use logger::{Metric, METRICS}; +use request::{checked_id, Body, Error, ParsedRequest, StatusCode}; use vmm::vmm_config::net::{NetworkInterfaceConfig, NetworkInterfaceUpdateConfig}; -impl IntoParsedRequest for NetworkInterfaceConfig { - fn into_parsed_request( - self, - id_from_path: Option, - _: Method, - ) -> result::Result { - let id_from_path = id_from_path.unwrap_or_default(); - if id_from_path != self.iface_id { - return Err(String::from( - "The id from the path does not match the id from the body!", - )); +pub fn parse_put_net(body: &Body, id_from_path: Option<&&str>) -> Result { + METRICS.patch_api_requests.network_count.inc(); + let id = match id_from_path { + Some(&id) => checked_id(id)?, + None => { + return Err(Error::EmptyID); } - - let (sender, receiver) = oneshot::channel(); - Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::InsertNetworkDevice(self), sender), - receiver, - )) + }; + + let netif = serde_json::from_slice::(body.raw()).map_err(|e| { + METRICS.put_api_requests.network_fails.inc(); + Error::SerdeJson(e) + })?; + if id != netif.iface_id.as_str() { + return Err(Error::Generic( + StatusCode::BadRequest, + "The id from the path does not match the id from the body!".to_string(), + )); } + Ok(ParsedRequest::Sync(VmmAction::InsertNetworkDevice(netif))) } -impl IntoParsedRequest for NetworkInterfaceUpdateConfig { - fn into_parsed_request( - self, - id_from_path: Option, - _: Method, - ) -> result::Result { - let id_from_path = id_from_path.unwrap_or_default(); - if id_from_path != self.iface_id { - return Err(String::from( - "The id from the path does not match the id from the body!", - )); +pub fn parse_patch_net(body: &Body, id_from_path: Option<&&str>) -> Result { + METRICS.put_api_requests.network_count.inc(); + let id = match id_from_path { + Some(&id) => checked_id(id)?, + None => { + return Err(Error::EmptyID); } - - let (sender, receiver) = oneshot::channel(); - Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::UpdateNetworkInterface(self), sender), - receiver, - )) + }; + + let netif = + serde_json::from_slice::(body.raw()).map_err(|e| { + METRICS.patch_api_requests.network_fails.inc(); + Error::SerdeJson(e) + })?; + if id != netif.iface_id { + return Err(Error::Generic( + StatusCode::BadRequest, + "The id from the path does not match the id from the body!".to_string(), + )); } + Ok(ParsedRequest::Sync(VmmAction::UpdateNetworkInterface( + netif, + ))) } #[cfg(test)] @@ -80,39 +81,32 @@ mod tests { } #[test] - fn test_netif_into_parsed_request() { - let netif = get_dummy_netif( - String::from("foo"), - String::from("bar"), - "12:34:56:78:9A:BC", - ); - assert!(netif - .into_parsed_request(Some(String::from("bar")), Method::Put) - .is_err()); + fn test_parse_netif_request() { + let body = r#"{ + "iface_id": "foo", + "host_dev_name": "bar", + "guest_mac": "12:34:56:78:9A:BC", + "allow_mmds_requests": false + }"#; + assert!(parse_put_net(&Body::new(body), Some(&"bar")).is_err()); + assert!(parse_put_net(&Body::new(body), Some(&"foo")).is_ok()); - let (sender, receiver) = oneshot::channel(); - let netif = get_dummy_netif( - String::from("foo"), - String::from("bar"), - "12:34:56:78:9A:BC", - ); - // NetworkInterfaceConfig does not implement clone, let's create the same object again. let netif_clone = get_dummy_netif( String::from("foo"), String::from("bar"), "12:34:56:78:9A:BC", ); - assert!(netif - .into_parsed_request(Some(String::from("foo")), Method::Put) - .eq(&Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::InsertNetworkDevice(netif_clone), sender), - receiver - )))); + match parse_put_net(&Body::new(body), Some(&"foo")) { + Ok(ParsedRequest::Sync(VmmAction::InsertNetworkDevice(netif))) => { + assert_eq!(netif, netif_clone) + } + _ => panic!("Test failed."), + } } #[test] fn test_network_interface_body_serialization_and_deserialization() { - let netif = NetworkInterfaceConfig { + let netif_clone = NetworkInterfaceConfig { iface_id: String::from("foo"), host_dev_name: String::from("bar"), guest_mac: Some(MacAddr::parse_str("12:34:56:78:9A:BC").unwrap()), @@ -122,7 +116,7 @@ mod tests { }; // This is the json encoding of the netif variable. - let jstr = r#"{ + let body = r#"{ "iface_id": "foo", "host_dev_name": "bar", "guest_mac": "12:34:56:78:9A:bc", @@ -133,15 +127,22 @@ mod tests { "allow_mmds_requests": true }"#; - let x = serde_json::from_str(jstr).expect("deserialization failed."); - assert_eq!(netif, x); + match parse_put_net(&Body::new(body), Some(&"foo")) { + Ok(ParsedRequest::Sync(VmmAction::InsertNetworkDevice(netif))) => { + assert_eq!(netif, netif_clone) + } + _ => panic!("Test failed."), + } // Check that guest_mac and rate limiters are truly optional. - let jstr_no_mac = r#"{ + let body_no_mac = r#"{ "iface_id": "foo", "host_dev_name": "bar" }"#; - assert!(serde_json::from_str::(jstr_no_mac).is_ok()) + assert!(serde_json::from_str::(body_no_mac).is_ok()); + + assert!(parse_put_net(&Body::new(body), Some(&"bar")).is_err()); + assert!(parse_patch_net(&Body::new(body), Some(&"bar")).is_err()); } } diff --git a/api_server/src/request/vsock.rs b/api_server/src/request/vsock.rs index 254db4f8457..ec781223489 100644 --- a/api_server/src/request/vsock.rs +++ b/api_server/src/request/vsock.rs @@ -1,27 +1,14 @@ // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -use std::result; - -use futures::sync::oneshot; -use hyper::Method; - -use super::{VmmAction, VmmRequest}; -use request::{IntoParsedRequest, ParsedRequest}; +use super::super::VmmAction; +use request::{Body, Error, ParsedRequest}; use vmm::vmm_config::vsock::VsockDeviceConfig; -impl IntoParsedRequest for VsockDeviceConfig { - fn into_parsed_request( - self, - _: Option, - _: Method, - ) -> result::Result { - let (sender, receiver) = oneshot::channel(); - Ok(ParsedRequest::Sync( - VmmRequest::new(VmmAction::SetVsockDevice(self), sender), - receiver, - )) - } +pub fn parse_put_vsock(body: &Body) -> Result { + Ok(ParsedRequest::Sync(VmmAction::SetVsockDevice( + serde_json::from_slice::(body.raw()).map_err(Error::SerdeJson)?, + ))) } #[cfg(test)] @@ -29,15 +16,19 @@ mod tests { use super::*; #[test] - fn test_vsock_into_parsed_request() { - let vsock = VsockDeviceConfig { - vsock_id: String::from("foo"), - guest_cid: 42, - uds_path: "vsock.sock".to_string(), - }; - assert!(vsock - .clone() - .into_parsed_request(Some(String::from("foo")), Method::Put) - .is_ok()); + fn test_parse_vsock_request() { + let body = r#"{ + "vsock_id": "foo", + "guest_cid": 42, + "uds_path": "vsock.sock" + }"#; + assert!(parse_put_vsock(&Body::new(body)).is_ok()); + + let body = r#"{ + "vsock_id": "foo", + "guest_cid": 42, + "invalid_field": false + }"#; + assert!(parse_put_vsock(&Body::new(body)).is_err()); } } diff --git a/micro_http/Cargo.toml b/micro_http/Cargo.toml index 2e5513f5851..23d6ea30e9a 100644 --- a/micro_http/Cargo.toml +++ b/micro_http/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" authors = ["Amazon Firecracker team "] [dependencies] +epoll = "=4.0.1" diff --git a/micro_http/src/common/headers.rs b/micro_http/src/common/headers.rs index 1effae299b6..8e99bd5b2bb 100644 --- a/micro_http/src/common/headers.rs +++ b/micro_http/src/common/headers.rs @@ -116,7 +116,7 @@ impl Headers { Header::ContentType => { match MediaType::try_from(entry[1].trim().as_bytes()) { Ok(_) => Ok(()), - Err(_) => Err(RequestError::InvalidHeader), + Err(_) => Err(RequestError::UnsupportedHeader), } } Header::TransferEncoding => match entry[1].trim() { @@ -223,7 +223,7 @@ pub enum MediaType { impl Default for MediaType { fn default() -> Self { - MediaType::PlainText + MediaType::ApplicationJson } } @@ -234,7 +234,7 @@ impl MediaType { } let utf8_slice = String::from_utf8(bytes.to_vec()).map_err(|_| RequestError::InvalidRequest)?; - match utf8_slice.as_str() { + match utf8_slice.as_str().trim() { "text/plain" => Ok(MediaType::PlainText), "application/json" => Ok(MediaType::ApplicationJson), _ => Err(RequestError::InvalidRequest), @@ -341,12 +341,12 @@ mod tests { RequestError::InvalidHeader ); - // Invalid media type. + // Unsupported media type. assert_eq!( header .parse_header_line(b"Content-Type: application/json-patch") .unwrap_err(), - RequestError::InvalidHeader + RequestError::UnsupportedHeader ); // Invalid input format. diff --git a/micro_http/src/common/mod.rs b/micro_http/src/common/mod.rs index 4d902be60df..a058ec32f5e 100644 --- a/micro_http/src/common/mod.rs +++ b/micro_http/src/common/mod.rs @@ -1,6 +1,8 @@ // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +use std::fmt::{Display, Error, Formatter}; + pub mod headers; pub mod ascii { @@ -28,6 +30,19 @@ pub enum RequestError { InvalidRequest, } +impl Display for RequestError { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match self { + RequestError::InvalidHttpMethod(inner) => write!(f, "Invalid HTTP Method: {}", inner), + RequestError::InvalidUri(inner) => write!(f, "Invalid URI: {}", inner), + RequestError::InvalidHttpVersion(inner) => write!(f, "Invalid HTTP Version: {}", inner), + RequestError::UnsupportedHeader => write!(f, "Unsupported header."), + RequestError::InvalidHeader => write!(f, "Invalid header."), + RequestError::InvalidRequest => write!(f, "Invalid request."), + } + } +} + /// Errors associated with a HTTP Connection. #[derive(Debug)] pub enum ConnectionError { @@ -41,6 +56,38 @@ pub enum ConnectionError { InvalidWrite, } +impl Display for ConnectionError { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match self { + ConnectionError::ParseError(inner) => write!(f, "Parsing error: {}", inner), + ConnectionError::StreamError(inner) => write!(f, "Stream error: {}", inner), + ConnectionError::ConnectionClosed => write!(f, "Connection closed."), + ConnectionError::InvalidWrite => write!(f, "Invalid write attempt."), + } + } +} + +/// Errors pertaining to `HttpServer`. +#[derive(Debug)] +pub enum ServerError { + /// Epoll operations failed. + IOError(std::io::Error), + /// Error from one of the connections. + ConnectionError(ConnectionError), + /// Server maximum capacity has been reached. + ServerFull, +} + +impl Display for ServerError { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match self { + ServerError::IOError(inner) => write!(f, "IO error: {}", inner), + ServerError::ConnectionError(inner) => write!(f, "Connection error: {}", inner), + ServerError::ServerFull => write!(f, "Server is full."), + } + } +} + /// The Body associated with an HTTP Request or Response. /// /// ## Examples @@ -233,4 +280,77 @@ mod tests { // Test for raw assert_eq!(body.raw(), b"This is a body."); } + + #[test] + fn test_display_request_error() { + assert_eq!( + format!("{}", RequestError::InvalidHttpMethod("test")), + "Invalid HTTP Method: test" + ); + assert_eq!( + format!("{}", RequestError::InvalidUri("test")), + "Invalid URI: test" + ); + assert_eq!( + format!("{}", RequestError::InvalidHttpVersion("test")), + "Invalid HTTP Version: test" + ); + assert_eq!( + format!("{}", RequestError::InvalidHeader), + "Invalid header." + ); + assert_eq!( + format!("{}", RequestError::UnsupportedHeader), + "Unsupported header." + ); + assert_eq!( + format!("{}", RequestError::InvalidRequest), + "Invalid request." + ); + } + + #[test] + fn test_display_connection_error() { + assert_eq!( + format!( + "{}", + ConnectionError::ParseError(RequestError::InvalidRequest) + ), + "Parsing error: Invalid request." + ); + assert_eq!( + format!( + "{}", + ConnectionError::StreamError(std::io::Error::from_raw_os_error(11)) + ), + "Stream error: Resource temporarily unavailable (os error 11)" + ); + assert_eq!( + format!("{}", ConnectionError::ConnectionClosed), + "Connection closed." + ); + assert_eq!( + format!("{}", ConnectionError::InvalidWrite), + "Invalid write attempt." + ); + } + + #[test] + fn test_display_server_error() { + assert_eq!( + format!( + "{}", + ServerError::ConnectionError(ConnectionError::ConnectionClosed) + ), + "Connection error: Connection closed." + ); + assert_eq!(format!("{}", ServerError::ServerFull), "Server is full."); + assert_eq!( + format!( + "{}", + ServerError::IOError(std::io::Error::from_raw_os_error(11)) + ), + "IO error: Resource temporarily unavailable (os error 11)" + ); + } } diff --git a/micro_http/src/connection.rs b/micro_http/src/connection.rs index 9cd8971aba6..db1693214a1 100644 --- a/micro_http/src/connection.rs +++ b/micro_http/src/connection.rs @@ -71,6 +71,11 @@ impl HttpConnection { /// Tries to read new bytes from the stream and automatically update the request. /// Meant to be used only with non-blocking streams and an `EPOLL` structure. /// Should be called whenever an `EPOLLIN` event is signaled. + /// + /// # Errors + /// `StreamError` is returned when an IO operation fails. + /// `ConnectionClosed` is returned when a client prematurely closes the connection. + /// `ParseError` is returned when a parsing operation fails. pub fn try_read(&mut self) -> Result<(), ConnectionError> { // Read some bytes from the stream, which will be appended to what is already // present in the buffer from a previous call of `try_read`. There are already @@ -293,6 +298,12 @@ impl HttpConnection { /// Tries to write the first available response to the provided stream. /// Meant to be used only with non-blocking streams and an `EPOLL` structure. /// Should be called whenever an `EPOLLOUT` event is signaled. + /// + /// # Errors + /// `StreamError` is returned when an IO operation fails. + /// `ConnectionClosed` is returned when trying to write on a closed connection. + /// `InvalidWrite` is returned when trying to write on a connection with an + /// empty outgoing buffer. pub fn try_write(&mut self) -> Result<(), ConnectionError> { if self.response_buffer.is_none() { if let Some(response) = self.response_queue.pop_front() { @@ -357,10 +368,16 @@ impl HttpConnection { self.read_cursor = end_cursor - line_start_index; } - /// Returns the first parsed request in the queue. + /// Returns the first parsed request in the queue or `None` if the queue + /// is empty. pub fn pop_parsed_request(&mut self) -> Option { self.parsed_requests.pop_front() } + + /// Returns `true` if there are bytes waiting to be written into the stream. + pub fn pending_write(&self) -> bool { + self.response_buffer.is_some() || !self.response_queue.is_empty() + } } #[cfg(test)] diff --git a/micro_http/src/lib.rs b/micro_http/src/lib.rs index 69a75246cc2..266123451e0 100644 --- a/micro_http/src/lib.rs +++ b/micro_http/src/lib.rs @@ -56,11 +56,12 @@ //! ## Example for creating an HTTP Response //! ``` //! extern crate micro_http; -//! use micro_http::{Body, Response, StatusCode, Version}; +//! use micro_http::{Body, Response, StatusCode, Version, MediaType}; //! //! let mut response = Response::new(Version::Http10, StatusCode::OK); //! let body = String::from("This is a test"); //! response.set_body(Body::new(body.clone())); +//! response.set_content_type(MediaType::PlainText); //! //! assert!(response.status() == StatusCode::OK); //! assert_eq!(response.body().unwrap(), Body::new(body)); @@ -74,12 +75,14 @@ mod common; mod connection; mod request; mod response; +mod server; use common::ascii; use common::headers; -pub use connection::HttpConnection; +pub use connection::{ConnectionError, HttpConnection}; pub use request::{Request, RequestError}; pub use response::{Response, StatusCode}; +pub use server::{HttpServer, ServerError, ServerRequest, ServerResponse}; -pub use common::headers::Headers; +pub use common::headers::{Headers, MediaType}; pub use common::{Body, Method, Version}; diff --git a/micro_http/src/request.rs b/micro_http/src/request.rs index 019802d9679..53188c7c460 100644 --- a/micro_http/src/request.rs +++ b/micro_http/src/request.rs @@ -396,7 +396,7 @@ mod tests { body: None, headers: Headers::default(), }; - let request_bytes = b"GET http://localhost/home HTTP/1.0\r\n \ + let request_bytes = b"GET http://localhost/home HTTP/1.0\r\n\ Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT\r\n\r\n"; let request = Request::try_from(request_bytes).unwrap(); assert_eq!(request, expected_request); @@ -413,9 +413,9 @@ mod tests { // Test for a request with the headers we are looking for. let request = Request::try_from( - b"PATCH http://localhost/home HTTP/1.1\r\n \ - Expect: 100-continue\r\n \ - Transfer-Encoding: chunked\r\n \ + b"PATCH http://localhost/home HTTP/1.1\r\n\ + Expect: 100-continue\r\n\ + Transfer-Encoding: chunked\r\n\ Content-Length: 26\r\n\r\nthis is not\n\r\na json \nbody", ) .unwrap(); @@ -437,9 +437,9 @@ mod tests { // Test for an invalid encoding. let request = Request::try_from( - b"PATCH http://localhost/home HTTP/1.1\r\n \ - Expect: 100-continue\r\n \ - Transfer-Encoding: identity; q=0\r\n \ + b"PATCH http://localhost/home HTTP/1.1\r\n\ + Expect: 100-continue\r\n\ + Transfer-Encoding: identity; q=0\r\n\ Content-Length: 26\r\n\r\nthis is not\n\r\na json \nbody", ) .unwrap_err(); @@ -447,8 +447,8 @@ mod tests { // Test for an invalid content length. let request = Request::try_from( - b"PATCH http://localhost/home HTTP/1.1\r\n \ - Expect: 100-continue\r\n \ + b"PATCH http://localhost/home HTTP/1.1\r\n\ + Expect: 100-continue\r\n\ Content-Length: 5000\r\n\r\nthis is a short body", ) .unwrap_err(); @@ -456,7 +456,7 @@ mod tests { // Test for a request without a body and an optional header. let request = Request::try_from( - b"GET http://localhost/ HTTP/1.0\r\n \ + b"GET http://localhost/ HTTP/1.0\r\n\ Accept-Encoding: gzip\r\n\r\n", ) .unwrap(); diff --git a/micro_http/src/response.rs b/micro_http/src/response.rs index 160c716c8e4..bd9ac37f8bc 100644 --- a/micro_http/src/response.rs +++ b/micro_http/src/response.rs @@ -12,7 +12,7 @@ use headers::{Header, MediaType}; /// The status code is defined as specified in the /// [RFC](https://tools.ietf.org/html/rfc7231#section-6). #[allow(dead_code)] -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum StatusCode { /// 100, Continue Continue, @@ -31,7 +31,8 @@ pub enum StatusCode { } impl StatusCode { - fn raw(self) -> &'static [u8; 3] { + /// Returns the status code as bytes. + pub fn raw(self) -> &'static [u8; 3] { match self { StatusCode::Continue => b"100", StatusCode::OK => b"200", @@ -97,12 +98,12 @@ impl ResponseHeaders { buf.write_all(b"Connection: keep-alive")?; buf.write_all(&[CR, LF])?; - buf.write_all(Header::ContentType.raw())?; - buf.write_all(&[COLON, SP])?; - buf.write_all(self.content_type.as_str().as_bytes())?; - buf.write_all(&[CR, LF])?; - if self.content_length != 0 { + buf.write_all(Header::ContentType.raw())?; + buf.write_all(&[COLON, SP])?; + buf.write_all(self.content_type.as_str().as_bytes())?; + buf.write_all(&[CR, LF])?; + buf.write_all(Header::ContentLength.raw())?; buf.write_all(&[COLON, SP])?; buf.write_all(self.content_length.to_string().as_bytes())?; @@ -153,12 +154,16 @@ impl Response { /// /// This function has side effects because it also updates the headers: /// - `ContentLength`: this is set to the length of the specified body. - /// - `MediaType`: this is set to "text/plain". pub fn set_body(&mut self, body: Body) { self.headers.set_content_length(body.len() as i32); self.body = Some(body); } + /// Updates the content type of the `Response`. + pub fn set_content_type(&mut self, content_type: MediaType) { + self.headers.set_content_type(content_type); + } + /// Sets the HTTP response server. pub fn set_server(&mut self, server: &str) { self.headers.set_server(server); @@ -219,6 +224,7 @@ mod tests { let mut response = Response::new(Version::Http10, StatusCode::OK); let body = "This is a test"; response.set_body(Body::new(body)); + response.set_content_type(MediaType::PlainText); assert!(response.status() == StatusCode::OK); assert_eq!(response.body().unwrap(), Body::new(body)); @@ -248,6 +254,7 @@ mod tests { let body = "This is a test"; let server = "rust-vmm API"; response.set_body(Body::new(body)); + response.set_content_type(MediaType::PlainText); response.set_server(server); assert!(response.status() == StatusCode::OK); diff --git a/micro_http/src/server.rs b/micro_http/src/server.rs new file mode 100644 index 00000000000..f775d2544e5 --- /dev/null +++ b/micro_http/src/server.rs @@ -0,0 +1,834 @@ +// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +extern crate epoll; + +use std::io::{Read, Write}; +use std::os::unix::io::AsRawFd; +use std::os::unix::io::RawFd; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::Path; + +use common::{Body, Version}; +pub use common::{ConnectionError, RequestError, ServerError}; +use connection::HttpConnection; +use request::Request; +use response::{Response, StatusCode}; +use std::collections::HashMap; + +static SERVER_FULL_ERROR_MESSAGE: &[u8] = b"HTTP/1.1 503\r\n\ + Server: Firecracker API\r\n\ + Connection: close\r\n\ + Content-Length: 40\r\n\r\n{ \"error\": \"Too many open connections\" }"; +const MAX_CONNECTIONS: usize = 10; + +type Result = std::result::Result; + +/// Wrapper over `Request` which adds an identification token. +pub struct ServerRequest { + /// Inner request. + pub request: Request, + /// Identification token. + id: u64, +} + +impl ServerRequest { + /// Creates a new `ServerRequest` object from an existing `Request`, + /// adding an identification token. + pub fn new(request: Request, id: u64) -> Self { + ServerRequest { request, id } + } + + /// Returns a reference to the inner request. + pub fn inner(&self) -> &Request { + &self.request + } + + /// Calls the function provided on the inner request to obtain the response. + /// The response is then wrapped in a `ServerResponse`. + /// + /// Returns a `ServerResponse` ready for yielding to the server + pub fn process(&self, callable: F) -> ServerResponse + where + F: Fn(&Request) -> Response, + { + let http_response = callable(self.inner()); + ServerResponse::new(http_response, self.id) + } +} + +/// Wrapper over `Response` which adds an identification token. +pub struct ServerResponse { + /// Inner response. + response: Response, + /// Identification token. + id: u64, +} + +impl ServerResponse { + fn new(response: Response, id: u64) -> ServerResponse { + ServerResponse { response, id } + } +} + +/// Describes the state of the connection as far as data exchange +/// on the stream is concerned. +#[derive(PartialOrd, PartialEq)] +enum ClientConnectionState { + AwaitingIncoming, + AwaitingOutgoing, + Closed, +} + +/// Wrapper over `HttpConnection` which keeps track of yielded +/// requests and absorbed responses. +struct ClientConnection { + /// The `HttpConnection` object which handles data exchange. + connection: HttpConnection, + /// The state of the connection in the `epoll` structure. + state: ClientConnectionState, + /// Represents the difference between yielded requests and + /// absorbed responses. + /// This has to be `0` if we want to drop the connection. + in_flight_response_count: u32, +} + +impl ClientConnection { + fn new(connection: HttpConnection) -> Self { + ClientConnection { + connection, + state: ClientConnectionState::AwaitingIncoming, + in_flight_response_count: 0, + } + } + + fn read(&mut self) -> Result> { + // Data came into the connection. + let mut parsed_requests = vec![]; + match self.connection.try_read() { + Err(ConnectionError::ConnectionClosed) => { + // Connection timeout. + self.state = ClientConnectionState::Closed; + // We don't want to propagate this to the server and we will + // return no requests and wait for the connection to become + // safe to drop. + return Ok(vec![]); + } + Err(ConnectionError::StreamError(inner)) => { + // Reading from the connection failed. + // We should try to write an error message regardless. + let mut internal_error_response = + Response::new(Version::Http11, StatusCode::InternalServerError); + internal_error_response.set_body(Body::new(inner.to_string())); + self.connection.enqueue_response(internal_error_response); + } + Err(ConnectionError::ParseError(inner)) => { + // An error occurred while parsing the read bytes. + // Check if there are any valid parsed requests in the queue. + while let Some(_discarded_request) = self.connection.pop_parsed_request() {} + + // Send an error response for the request that gave us the error. + let mut error_response = Response::new(Version::Http11, StatusCode::BadRequest); + error_response.set_body(Body::new( + format!( + "{{ \"error\": \"{}\nAll previous unanswered requests will be dropped.\" }}", + inner.to_string() + ) + .to_string(), + )); + self.connection.enqueue_response(error_response); + } + Err(ConnectionError::InvalidWrite) => { + // This is unreachable because `HttpConnection::try_read()` cannot return this error variant. + unreachable!(); + } + Ok(()) => { + while let Some(request) = self.connection.pop_parsed_request() { + // Add all valid requests to `parsed_requests`. + parsed_requests.push(request); + } + } + } + self.in_flight_response_count += parsed_requests.len() as u32; + // If the state of the connection has changed, we need to update + // the event set in the `epoll` structure. + if self.connection.pending_write() { + self.state = ClientConnectionState::AwaitingOutgoing; + } + + Ok(parsed_requests) + } + + fn write(&mut self) -> Result<()> { + // The stream is available for writing. + match self.connection.try_write() { + Err(ConnectionError::ConnectionClosed) | Err(ConnectionError::StreamError(_)) => { + // Writing to the stream failed so it will be removed. + self.state = ClientConnectionState::Closed; + } + Err(ConnectionError::InvalidWrite) => { + // A `try_write` call was performed on a connection that has nothing + // to write. + return Err(ServerError::ConnectionError(ConnectionError::InvalidWrite)); + } + _ => { + // Check if we still have bytes to write for this connection. + if !self.connection.pending_write() { + self.state = ClientConnectionState::AwaitingIncoming; + } + } + } + Ok(()) + } + + fn enqueue_response(&mut self, response: Response) { + if self.state != ClientConnectionState::Closed { + self.connection.enqueue_response(response); + } + self.in_flight_response_count -= 1; + } + + // Returns `true` if the connection is closed and safe to drop. + fn is_done(&self) -> bool { + self.state == ClientConnectionState::Closed + && !self.connection.pending_write() + && self.in_flight_response_count == 0 + } +} + +/// HTTP Server implementation using Unix Domain Sockets and `EPOLL` to +/// handle multiple connections on the same thread. +/// +/// The function that does the data exchange is `handle_notifications`. +/// It can be called in a loop, which will render the thread that the +/// server runs on incapable of performing other operations, or it can +/// be used in another `EPOLL` structure, as it provides its `epoll_fd`, +/// the file descriptor of the epoll structure used within the server, +/// and it can be added to another one using the `EPOLLIN` flag. Whenever +/// there is a notification on that fd, `handle_notifications` should be +/// called once. +/// +/// # Example +/// +/// ## Starting and running the server +/// +/// ``` +/// use micro_http::{HttpServer, Response, StatusCode}; +/// +/// let path_to_socket = "/tmp/example.sock"; +/// std::fs::remove_file(path_to_socket).unwrap_or_default(); +/// +/// // Start the server. +/// let mut server = HttpServer::new(path_to_socket).unwrap(); +/// server.start_server().unwrap(); +/// +/// // Connect a client to the server so it doesn't block in our example. +/// let mut socket = std::os::unix::net::UnixStream::connect(path_to_socket).unwrap(); +/// +/// // Server loop processing requests. +/// loop { +/// for request in server.requests().unwrap() { +/// let response = request.process(|request| { +/// // Your code here. +/// Response::new(request.http_version(), StatusCode::NoContent) +/// }); +/// server.respond(response); +/// } +/// // Break this example loop. +/// break; +/// } +/// ``` +pub struct HttpServer { + /// Socket on which we listen for new connections. + socket: UnixListener, + /// File descriptor of the server's epoll structure. + epoll_fd: RawFd, + /// Holds the token-connection pairs of the server. + /// Each connection has an associated identification token, which is + /// the file descriptor of the underlying stream. + /// We use the file descriptor of the stream as the key for mapping + /// connections because the 1-to-1 relation is guaranteed by the OS. + connections: HashMap>, +} + +impl HttpServer { + /// Constructor for `HttpServer`. + /// + /// Returns the newly formed `HttpServer`. + /// + /// # Errors + /// Returns an `IOError` when binding or `epoll::create` fails. + /// + pub fn new>(path_to_socket: P) -> Result { + let socket = UnixListener::bind(path_to_socket).map_err(ServerError::IOError)?; + let epoll_fd = epoll::create(true).map_err(ServerError::IOError)?; + Ok(HttpServer { + socket, + epoll_fd, + connections: HashMap::new(), + }) + } + + /// Starts the HTTP Server. + pub fn start_server(&mut self) -> Result<()> { + // Add the socket on which we listen for new connections to the + // `epoll` structure. + Self::epoll_add(self.epoll_fd, self.socket.as_raw_fd()) + } + + /// This function is responsible for the data exchange with the clients and should + /// be called when we are either notified through `epoll` that we need to exchange + /// data with at least a client or when we don't need to perform any other operations + /// on this thread and we can afford to call it in a loop. + /// + /// Note that this function will block the current thread if there are no notifications + /// to be handled by the server. + /// + /// Returns a collection of complete and valid requests to be processed by the user + /// of the server. Once processed, responses should be sent using `enqueue_responses()`. + /// + /// # Errors + /// `IOError` is returned when `read`, `write` or `epoll::ctl` operations fail. + /// `ServerFull` is returned when a client is trying to connect to the server, but + /// full capacity has already been reached. + /// `InvalidWrite` is returned when the server attempted to perform a write operation + /// on a connection on which it is not possible. + pub fn requests(&mut self) -> Result> { + let mut parsed_requests: Vec = vec![]; + let mut events = vec![epoll::Event::new(epoll::Events::empty(), 0); MAX_CONNECTIONS]; + + // This is a wrapper over the syscall `epoll_wait` and it will block the + // current thread until at least one event is received. + // The received notifications will then populate the `events` array with + // `event_count` elements, where 1 <= event_count <= MAX_CONNECTIONS. + let event_count = + epoll::wait(self.epoll_fd, -1, &mut events[..]).map_err(ServerError::IOError)?; + // We use `take()` on the iterator over `events` as, even though only + // `events_count` events have been inserted into `events`, the size of + // the array is still `MAX_CONNECTIONS`, so we discard empty elements + // at the end of the array. + for e in events.iter().take(event_count) { + // Check the file descriptor which produced the notification `e`. + // It could be that we have a new connection, or one of our open + // connections is ready to exchange data with a client. + if e.data as RawFd == self.socket.as_raw_fd() { + // We have received a notification on the listener socket, which + // means we have a new connection to accept. + match self.handle_new_connection() { + // If the server is full, we send a message to the client + // notifying them that we will close the connection, then + // we discard it. + Err(ServerError::ServerFull) => { + self.socket + .accept() + .map_err(ServerError::IOError) + .and_then(move |(mut stream, _)| { + stream + .write(SERVER_FULL_ERROR_MESSAGE) + .map_err(ServerError::IOError) + })?; + } + // An internal error will compromise any in-flight requests. + Err(error) => return Err(error), + Ok(()) => {} + }; + } else { + // We have a notification on one of our open connections. + let fd = e.data as RawFd; + let client_connection = self.connections.get_mut(&fd).unwrap(); + if e.events & epoll::Events::EPOLLIN.bits() != 0 { + // We have bytes to read from this connection. + // If our `read` yields `Request` objects, we wrap them with an ID before + // handing them to the user. + parsed_requests.append( + &mut client_connection + .read()? + .into_iter() + .map(|request| ServerRequest::new(request, fd as u64)) + .collect(), + ); + // If the connection was incoming before we read and we now have to write + // either an error message or an `expect` response, we change its `epoll` + // event set to notify us when the stream is ready for writing. + if client_connection.state == ClientConnectionState::AwaitingOutgoing { + Self::epoll_mod(self.epoll_fd, fd, epoll::Events::EPOLLOUT)?; + } + } else if e.events & epoll::Events::EPOLLOUT.bits() != 0 { + // We have bytes to write on this connection. + client_connection.write()?; + // If the connection was outgoing before we tried to write the responses + // and we don't have any more responses to write, we change the `epoll` + // event set to notify us when we have bytes to read from the stream. + if client_connection.state == ClientConnectionState::AwaitingIncoming { + Self::epoll_mod(self.epoll_fd, fd, epoll::Events::EPOLLIN)?; + } + } + } + } + + // Remove dead connections. + self.connections + .retain(|_, client_connection| !client_connection.is_done()); + + Ok(parsed_requests) + } + + /// The file descriptor of the `epoll` structure can enable the server to become + /// a non-blocking structure in an application. + /// + /// Returns the file descriptor of the server's internal `epoll` structure. + /// + /// # Example + /// + /// ## Non-blocking server + /// ``` + /// extern crate epoll; + /// + /// use micro_http::{HttpServer, Response, StatusCode}; + /// + /// // Create our epoll manager. + /// let epoll_fd = epoll::create(true).unwrap(); + /// + /// let path_to_socket = "/tmp/epoll_example.sock"; + /// std::fs::remove_file(path_to_socket).unwrap_or_default(); + /// + /// // Start the server. + /// let mut server = HttpServer::new(path_to_socket).unwrap(); + /// server.start_server().unwrap(); + /// + /// // Add our server to the `epoll` manager. + /// epoll::ctl( + /// epoll_fd, + /// epoll::ControlOptions::EPOLL_CTL_ADD, + /// server.epoll_fd(), + /// epoll::Event::new(epoll::Events::EPOLLIN, 1234u64), + /// ).unwrap(); + /// + /// // Connect a client to the server so it doesn't block in our example. + /// let mut socket = std::os::unix::net::UnixStream::connect(path_to_socket).unwrap(); + /// + /// // Control loop of the application. + /// let mut events = Vec::with_capacity(10); + /// loop { + /// let num_ev = epoll::wait(epoll_fd, -1, events.as_mut_slice()); + /// for event in events { + /// match event.data { + /// // The server notification. + /// 1234 => { + /// let request = server.requests(); + /// // Process... + /// } + /// // Other `epoll` notifications. + /// _ => { + /// // Do other computation. + /// } + /// } + /// } + /// // Break this example loop. + /// break; + /// } + /// ``` + pub fn epoll_fd(&self) -> RawFd { + self.epoll_fd + } + + /// Enqueues the provided responses in the outgoing connection. + /// + /// # Errors + /// `IOError` is returned when an `epoll::ctl` operation fails. + pub fn enqueue_responses(&mut self, responses: Vec) -> Result<()> { + for response in responses { + self.respond(response)?; + } + + Ok(()) + } + + /// Adds the provided response to the outgoing buffer in the corresponding connection. + /// + /// # Errors + /// `IOError` is returned when an `epoll::ctl` operation fails. + pub fn respond(&mut self, response: ServerResponse) -> Result<()> { + if let Some(client_connection) = self.connections.get_mut(&(response.id as i32)) { + // If the connection was incoming before we enqueue the response, we change its + // `epoll` event set to notify us when the stream is ready for writing. + if let ClientConnectionState::AwaitingIncoming = client_connection.state { + client_connection.state = ClientConnectionState::AwaitingOutgoing; + Self::epoll_mod(self.epoll_fd, response.id as RawFd, epoll::Events::EPOLLOUT)?; + } + client_connection.enqueue_response(response.response); + } + Ok(()) + } + + /// Accepts a new incoming connection and adds it to the `epoll` notification structure. + /// + /// # Errors + /// `IOError` is returned when an `epoll::ctl` operation fails. + fn handle_new_connection(&mut self) -> Result<()> { + if self.connections.len() == MAX_CONNECTIONS { + // If we want a replacement policy for connections + // this is where we will have it. + return Err(ServerError::ServerFull); + } + + self.socket + .accept() + .map_err(ServerError::IOError) + .and_then(|(stream, _)| { + // `HttpConnection` is supposed to work with non-blocking streams. + stream + .set_nonblocking(true) + .map(|_| stream) + .map_err(ServerError::IOError) + }) + .and_then(|stream| { + // Add the stream to the `epoll` structure and listen for bytes to be read. + Self::epoll_add(self.epoll_fd, stream.as_raw_fd())?; + // Then add it to our open connections. + self.connections.insert( + stream.as_raw_fd(), + ClientConnection::new(HttpConnection::new(stream)), + ); + Ok(()) + }) + } + + /// Changes the event type for a connection to either listen for incoming bytes + /// or for when the stream is ready for writing. + fn epoll_mod(epoll_fd: RawFd, stream_fd: RawFd, evset: epoll::Events) -> Result<()> { + let event = epoll::Event::new(evset, stream_fd as u64); + epoll::ctl( + epoll_fd, + epoll::ControlOptions::EPOLL_CTL_MOD, + stream_fd, + event, + ) + .map_err(ServerError::IOError) + } + + /// Adds a stream to the `epoll` notification structure with the `EPOLLIN` event set. + fn epoll_add(epoll_fd: RawFd, stream_fd: RawFd) -> Result<()> { + epoll::ctl( + epoll_fd, + epoll::ControlOptions::EPOLL_CTL_ADD, + stream_fd, + epoll::Event::new(epoll::Events::EPOLLIN, stream_fd as u64), + ) + .map_err(ServerError::IOError) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::os::unix::net::UnixStream; + + use common::Body; + use std::io::Read; + use std::io::Write; + + #[test] + fn test_wait_one_connection() { + let path_to_socket = "/tmp/test_socket_http_server1.sock"; + fs::remove_file(path_to_socket).unwrap_or_default(); + + let mut server = HttpServer::new(path_to_socket.to_string()).unwrap(); + server.start_server().unwrap(); + + // Test one incoming connection. + let mut socket = UnixStream::connect(path_to_socket).unwrap(); + assert!(server.requests().unwrap().is_empty()); + + socket + .write_all( + b"PATCH /machine-config HTTP/1.1\r\n\ + Content-Length: 13\r\n\ + Content-Type: application/json\r\n\r\nwhatever body", + ) + .unwrap(); + + let mut req_vec = server.requests().unwrap(); + let server_request = req_vec.remove(0); + + server + .respond(server_request.process(|_request| { + let mut response = Response::new(Version::Http11, StatusCode::OK); + let response_body = b"response body"; + response.set_body(Body::new(response_body.to_vec())); + response + })) + .unwrap(); + assert!(server.requests().unwrap().is_empty()); + + let mut buf: [u8; 1024] = [0; 1024]; + assert!(socket.read(&mut buf[..]).unwrap() > 0); + fs::remove_file(path_to_socket).unwrap(); + } + + #[test] + fn test_wait_concurrent_connections() { + let path_to_socket = "/tmp/test_socket_http_server2.sock"; + fs::remove_file(path_to_socket).unwrap_or_default(); + + let mut server = HttpServer::new(path_to_socket.to_string()).unwrap(); + server.start_server().unwrap(); + + // Test two concurrent connections. + let mut first_socket = UnixStream::connect(path_to_socket).unwrap(); + assert!(server.requests().unwrap().is_empty()); + + first_socket + .write_all( + b"PATCH /machine-config HTTP/1.1\r\n\ + Content-Length: 13\r\n\ + Content-Type: application/json\r\n\r\nwhatever body", + ) + .unwrap(); + let mut second_socket = UnixStream::connect(path_to_socket).unwrap(); + + let mut req_vec = server.requests().unwrap(); + let server_request = req_vec.remove(0); + + server + .respond(server_request.process(|_request| { + let mut response = Response::new(Version::Http11, StatusCode::OK); + let response_body = b"response body"; + response.set_body(Body::new(response_body.to_vec())); + response + })) + .unwrap(); + second_socket + .write_all( + b"GET /machine-config HTTP/1.1\r\n\ + Content-Length: 20\r\n\ + Content-Type: application/json\r\n\r\nwhatever second body", + ) + .unwrap(); + + let mut req_vec = server.requests().unwrap(); + let second_server_request = req_vec.remove(0); + + assert_eq!( + second_server_request.request, + Request::try_from( + b"GET /machine-config HTTP/1.1\r\n\ + Content-Length: 20\r\n\ + Content-Type: application/json\r\n\r\nwhatever second body" + ) + .unwrap() + ); + + let mut buf: [u8; 1024] = [0; 1024]; + assert!(first_socket.read(&mut buf[..]).unwrap() > 0); + first_socket.shutdown(std::net::Shutdown::Both).unwrap(); + + server + .respond(second_server_request.process(|_request| { + let mut response = Response::new(Version::Http11, StatusCode::OK); + let response_body = b"response second body"; + response.set_body(Body::new(response_body.to_vec())); + response + })) + .unwrap(); + + assert!(server.requests().unwrap().is_empty()); + let mut buf: [u8; 1024] = [0; 1024]; + assert!(second_socket.read(&mut buf[..]).unwrap() > 0); + second_socket.shutdown(std::net::Shutdown::Both).unwrap(); + assert!(server.requests().unwrap().is_empty()); + fs::remove_file(path_to_socket).unwrap(); + } + + #[test] + fn test_wait_expect_connection() { + let path_to_socket = "/tmp/test_socket_http_server3.sock"; + fs::remove_file(path_to_socket).unwrap_or_default(); + + let mut server = HttpServer::new(path_to_socket.to_string()).unwrap(); + server.start_server().unwrap(); + + // Test one incoming connection with `Expect: 100-continue`. + let mut socket = UnixStream::connect(path_to_socket).unwrap(); + assert!(server.requests().unwrap().is_empty()); + + socket + .write_all( + b"PATCH /machine-config HTTP/1.1\r\n\ + Content-Length: 13\r\n\ + Expect: 100-continue\r\n\r\n", + ) + .unwrap(); + // `wait` on server to receive what the client set on the socket. + // This will set the stream direction to `Outgoing`, as we need to send a `100 CONTINUE` response. + let req_vec = server.requests().unwrap(); + assert!(req_vec.is_empty()); + // Another `wait`, this time to send the response. + // Will be called because of an `EPOLLOUT` notification. + let req_vec = server.requests().unwrap(); + assert!(req_vec.is_empty()); + let mut buf: [u8; 1024] = [0; 1024]; + assert!(socket.read(&mut buf[..]).unwrap() > 0); + + socket.write_all(b"whatever body").unwrap(); + let mut req_vec = server.requests().unwrap(); + let server_request = req_vec.remove(0); + + server + .respond(server_request.process(|_request| { + let mut response = Response::new(Version::Http11, StatusCode::OK); + let response_body = b"response body"; + response.set_body(Body::new(response_body.to_vec())); + response + })) + .unwrap(); + + let req_vec = server.requests().unwrap(); + assert!(req_vec.is_empty()); + + let mut buf: [u8; 1024] = [0; 1024]; + assert!(socket.read(&mut buf[..]).unwrap() > 0); + fs::remove_file(path_to_socket).unwrap(); + } + + #[test] + fn test_wait_many_connections() { + let path_to_socket = "/tmp/test_socket_http_server4.sock"; + fs::remove_file(path_to_socket).unwrap_or_default(); + + let mut server = HttpServer::new(path_to_socket.to_string()).unwrap(); + server.start_server().unwrap(); + + let mut sockets: Vec = Vec::with_capacity(11); + for _ in 0..MAX_CONNECTIONS { + sockets.push(UnixStream::connect(path_to_socket).unwrap()); + assert!(server.requests().unwrap().is_empty()); + } + + sockets.push(UnixStream::connect(path_to_socket).unwrap()); + assert!(server.requests().unwrap().is_empty()); + let mut buf: [u8; 120] = [0; 120]; + sockets[MAX_CONNECTIONS].read_exact(&mut buf).unwrap(); + assert_eq!(&buf[..], SERVER_FULL_ERROR_MESSAGE); + + fs::remove_file(path_to_socket).unwrap(); + } + + #[test] + fn test_wait_parse_error() { + let path_to_socket = "/tmp/test_socket_http_server5.sock"; + fs::remove_file(path_to_socket).unwrap_or_default(); + + let mut server = HttpServer::new(path_to_socket.to_string()).unwrap(); + server.start_server().unwrap(); + + // Test one incoming connection. + let mut socket = UnixStream::connect(path_to_socket).unwrap(); + socket.set_nonblocking(true).unwrap(); + assert!(server.requests().unwrap().is_empty()); + + socket + .write_all( + b"PATCH /machine-config HTTP/1.1\r\n\ + Content-Length: alpha\r\n\ + Content-Type: application/json\r\n\r\nwhatever body", + ) + .unwrap(); + + assert!(server.requests().unwrap().is_empty()); + assert!(server.requests().unwrap().is_empty()); + let mut buf: [u8; 198] = [0; 198]; + assert!(socket.read(&mut buf[..]).unwrap() > 0); + let error_message = b"HTTP/1.1 400 \r\n\ + Server: Firecracker API\r\n\ + Connection: keep-alive\r\n\ + Content-Type: application/json\r\n\ + Content-Length: 80\r\n\r\n{ \"error\": \"Invalid header.\n\ + All previous unanswered requests will be dropped.\" }"; + assert_eq!(&buf[..], &error_message[..]); + + fs::remove_file(path_to_socket).unwrap(); + } + + #[test] + fn test_wait_in_flight_responses() { + let path_to_socket = "/tmp/test_socket_http_server6.sock"; + fs::remove_file(path_to_socket).unwrap_or_default(); + + let mut server = HttpServer::new(path_to_socket.to_string()).unwrap(); + server.start_server().unwrap(); + + // Test a connection dropped and then a new one appearing + // before the user had a chance to send the response to the + // first one. + let mut first_socket = UnixStream::connect(path_to_socket).unwrap(); + assert!(server.requests().unwrap().is_empty()); + + first_socket + .write_all( + b"PATCH /machine-config HTTP/1.1\r\n\ + Content-Length: 13\r\n\ + Content-Type: application/json\r\n\r\nwhatever body", + ) + .unwrap(); + + let mut req_vec = server.requests().unwrap(); + let server_request = req_vec.remove(0); + + first_socket.shutdown(std::net::Shutdown::Both).unwrap(); + assert!(server.requests().unwrap().is_empty()); + let mut second_socket = UnixStream::connect(path_to_socket).unwrap(); + second_socket.set_nonblocking(true).unwrap(); + assert!(server.requests().unwrap().is_empty()); + + server + .enqueue_responses(vec![server_request.process(|_request| { + let mut response = Response::new(Version::Http11, StatusCode::OK); + let response_body = b"response body"; + response.set_body(Body::new(response_body.to_vec())); + response + })]) + .unwrap(); + assert!(server.requests().unwrap().is_empty()); + assert_eq!(server.connections.len(), 1); + let mut buf: [u8; 1024] = [0; 1024]; + assert!(second_socket.read(&mut buf[..]).is_err()); + + second_socket + .write_all( + b"GET /machine-config HTTP/1.1\r\n\ + Content-Length: 20\r\n\ + Content-Type: application/json\r\n\r\nwhatever second body", + ) + .unwrap(); + + let mut req_vec = server.requests().unwrap(); + let second_server_request = req_vec.remove(0); + + assert_eq!( + second_server_request.request, + Request::try_from( + b"GET /machine-config HTTP/1.1\r\n\ + Content-Length: 20\r\n\ + Content-Type: application/json\r\n\r\nwhatever second body" + ) + .unwrap() + ); + + server + .respond(second_server_request.process(|_request| { + let mut response = Response::new(Version::Http11, StatusCode::OK); + let response_body = b"response second body"; + response.set_body(Body::new(response_body.to_vec())); + response + })) + .unwrap(); + + assert!(server.requests().unwrap().is_empty()); + let mut buf: [u8; 1024] = [0; 1024]; + assert!(second_socket.read(&mut buf[..]).unwrap() > 0); + second_socket.shutdown(std::net::Shutdown::Both).unwrap(); + assert!(server.requests().is_ok()); + fs::remove_file(path_to_socket).unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs index 373ac012d42..d54adf2001b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,11 +23,11 @@ use std::io; use std::panic; use std::path::PathBuf; use std::process; -use std::sync::mpsc::{channel, Receiver, TryRecvError}; +use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError}; use std::sync::{Arc, RwLock}; use std::thread; -use api_server::{ApiServer, Error}; +use api_server::{ApiServer, Error, VmmRequest, VmmResponse}; use fc_util::validators::validate_instance_id; use logger::{Metric, LOGGER, METRICS}; use mmds::MMDS; @@ -186,26 +186,36 @@ fn main() { vmm_version: crate_version!().to_string(), })); - let (to_vmm, from_api) = channel(); - let api_event_fd = EventFd::new() + let request_event_fd = EventFd::new() .map_err(Error::Eventfd) .expect("Cannot create API Eventfd."); + let (to_vmm, from_api) = channel(); + let (to_api, from_vmm) = channel(); // Api enabled. if !no_api { // MMDS only supported with API. let mmds_info = MMDS.clone(); - - let kick_vmm_efd = api_event_fd.try_clone().expect("cannot clone Eventfd."); let vmm_shared_info = api_shared_info.clone(); + let to_vmm_event_fd = request_event_fd.try_clone().unwrap(); thread::Builder::new() .name("fc_api".to_owned()) .spawn(move || { - match ApiServer::new(mmds_info, vmm_shared_info, to_vmm, kick_vmm_efd) - .expect("Cannot create API server") - .bind_and_run(bind_path, start_time_us, start_time_cpu_us, seccomp_level) - { + match ApiServer::new( + mmds_info, + vmm_shared_info, + to_vmm, + from_vmm, + to_vmm_event_fd, + ) + .expect("Cannot create API server") + .bind_and_run( + bind_path, + start_time_us, + start_time_cpu_us, + seccomp_level, + ) { Ok(_) => (), Err(Error::Io(inner)) => match inner.kind() { io::ErrorKind::AddrInUse => { @@ -226,8 +236,9 @@ fn main() { start_vmm( api_shared_info, - api_event_fd, + request_event_fd, from_api, + to_api, seccomp_level, vmm_config_json, ); @@ -248,7 +259,8 @@ fn main() { fn start_vmm( api_shared_info: Arc>, api_event_fd: EventFd, - from_api: Receiver, + from_api: Receiver, + to_api: Sender, seccomp_level: u32, config_json: Option, ) { @@ -286,7 +298,9 @@ fn start_vmm( break vmm::FC_EXIT_CODE_OK; } EventLoopExitReason::ControlAction => { - if let Err(exit_code) = vmm_control_event(&mut vmm, &api_event_fd, &from_api) { + if let Err(exit_code) = + vmm_control_event(&mut vmm, &api_event_fd, &from_api, &to_api) + { break exit_code; } } @@ -303,7 +317,8 @@ fn start_vmm( fn vmm_control_event( vmm: &mut Vmm, api_event_fd: &EventFd, - from_api: &Receiver, + from_api: &Receiver, + to_api: &Sender, ) -> Result<(), u8> { api_event_fd.read().map_err(|e| { error!("VMM: Failed to read the API event_fd: {}", e); @@ -313,7 +328,7 @@ fn vmm_control_event( match from_api.try_recv() { Ok(vmm_request) => { use api_server::VmmAction::*; - let (action_request, sender) = vmm_request.unpack(); + let action_request = *vmm_request; let response = match action_request { ConfigureBootSource(boot_source_body) => vmm .configure_boot_source(boot_source_body) @@ -351,8 +366,8 @@ fn vmm_control_event( .map(|_| api_server::VmmData::Empty), }; // Run the requested action and send back the result. - sender - .send(response) + to_api + .send(Box::new(response)) .map_err(|_| ()) .expect("one-shot channel closed"); } diff --git a/tests/integration_tests/build/test_coverage.py b/tests/integration_tests/build/test_coverage.py index 4fd94289594..52dd81a0d72 100644 --- a/tests/integration_tests/build/test_coverage.py +++ b/tests/integration_tests/build/test_coverage.py @@ -19,7 +19,7 @@ import host_tools.cargo_build as host # pylint: disable=import-error -COVERAGE_TARGET_PCT = 85.6 +COVERAGE_TARGET_PCT = 85.7 COVERAGE_MAX_DELTA = 0.01 CARGO_KCOV_REL_PATH = os.path.join(host.CARGO_BUILD_REL_PATH, 'kcov') diff --git a/tests/integration_tests/functional/test_logging.py b/tests/integration_tests/functional/test_logging.py index b950ec22eda..f350ef4784b 100644 --- a/tests/integration_tests/functional/test_logging.py +++ b/tests/integration_tests/functional/test_logging.py @@ -265,9 +265,7 @@ def test_api_requests_logs(test_microvm_with_api): fault_message = "The kernel file cannot be opened: No such file or " \ "directory (os error 2)" assert fault_message in response.text - expected_log_strings.append("Received Error on synchronous Put request " - "on \"/boot-source\" with body \"{{\\\"kernel_" - "image_path\\\": \\\"inexistent_path\\\"}}\". " + expected_log_strings.append("Received Error. " "Status code: 400 Bad Request. " "Message: {}".format(fault_message)) diff --git a/vmm/Cargo.toml b/vmm/Cargo.toml index e0c63c5667e..62174ffe48d 100644 --- a/vmm/Cargo.toml +++ b/vmm/Cargo.toml @@ -8,7 +8,6 @@ kvm-bindings = "0.1" kvm-ioctls = "0.2" libc = ">=0.2.39" epoll = "=4.0.1" -futures = ">=0.1.18" serde = ">=1.0.27" serde_derive = ">=1.0.27" serde_json = ">=1.0.9" diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index c945a9b8e48..c9d5c903d14 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -10,7 +10,6 @@ //! machine (microVM). #![deny(missing_docs)] extern crate epoll; -extern crate futures; extern crate kvm_bindings; extern crate kvm_ioctls; extern crate libc; @@ -235,7 +234,11 @@ impl EpollContext { /// Given a file descriptor `fd`, and an EpollDispatch token `token`, /// associate `token` with an `EPOLLIN` event for `fd`, through the /// `dispatch_table`. - fn add_event(&mut self, fd: &T, token: EpollDispatch) -> Result<()> { + fn add_epollin_event( + &mut self, + fd: &T, + token: EpollDispatch, + ) -> Result<()> { // The index in the dispatch where the new token will be added. let dispatch_index = self.dispatch_table.len() as u64; @@ -419,14 +422,14 @@ impl Vmm { let mut epoll_context = EpollContext::new()?; // If this fails, it's fatal; using expect() to crash. epoll_context - .add_event(control_fd, EpollDispatch::VmmActionRequest) + .add_epollin_event(control_fd, EpollDispatch::VmmActionRequest) .expect("Cannot add vmm control_fd to epoll."); let write_metrics_event_fd = TimerFd::new_custom(ClockId::Monotonic, true, true).map_err(Error::TimerFd)?; epoll_context - .add_event( + .add_epollin_event( // non-blocking & close on exec &write_metrics_event_fd, EpollDispatch::WriteMetrics, @@ -1059,7 +1062,7 @@ impl Vmm { .map_err(|_| EventFd)?; self.epoll_context - .add_event(&exit_poll_evt_fd, EpollDispatch::Exit) + .add_epollin_event(&exit_poll_evt_fd, EpollDispatch::Exit) .map_err(|_| RegisterEvent)?; self.exit_evt = Some(exit_poll_evt_fd); @@ -2213,12 +2216,12 @@ mod tests { } #[test] - fn add_event_test() { + fn add_epollin_event_test() { let mut ep = EpollContext::new().unwrap(); let evfd = EventFd::new().unwrap(); // adding new event should work - assert!(ep.add_event(&evfd, EpollDispatch::Exit).is_ok()); + assert!(ep.add_epollin_event(&evfd, EpollDispatch::Exit).is_ok()); } #[test] @@ -2227,7 +2230,7 @@ mod tests { let evfd = EventFd::new().unwrap(); // adding new event should work - assert!(ep.add_event(&evfd, EpollDispatch::Exit).is_ok()); + assert!(ep.add_epollin_event(&evfd, EpollDispatch::Exit).is_ok()); let evpoll_events_len = 10; let mut events = vec![epoll::Event::new(epoll::Events::empty(), 0); evpoll_events_len]; diff --git a/vmm/src/vmm_config/machine_config.rs b/vmm/src/vmm_config/machine_config.rs index 59d8dac4f66..cda7a13ebf5 100644 --- a/vmm/src/vmm_config/machine_config.rs +++ b/vmm/src/vmm_config/machine_config.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use serde::{de, Deserialize}; -use std::fmt::{Display, Formatter, Result}; +use std::fmt; /// Firecracker aims to support small scale workloads only, so limit the maximum /// vCPUs supported. @@ -20,8 +20,8 @@ pub enum VmConfigError { UpdateNotAllowedPostBoot, } -impl Display for VmConfigError { - fn fmt(&self, f: &mut Formatter) -> Result { +impl fmt::Display for VmConfigError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::VmConfigError::*; match *self { InvalidVcpuCount => write!( @@ -71,6 +71,20 @@ impl Default for VmConfig { } } +impl fmt::Display for VmConfig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let vcpu_count = self.vcpu_count.unwrap_or(1); + let mem_size = self.mem_size_mib.unwrap_or(128); + let ht_enabled = self.ht_enabled.unwrap_or(false); + let cpu_template = self + .cpu_template + .map_or("Uninitialized".to_string(), |c| c.to_string()); + + write!(f, "{{ \"vcpu_count\": {:?}, \"mem_size_mib\": {:?}, \"ht_enabled\": {:?}, \"cpu_template\": {:?} }}", + vcpu_count, mem_size, ht_enabled, cpu_template) + } +} + fn validate_vcpu_num<'de, D>(d: D) -> std::result::Result, D::Error> where D: de::Deserializer<'de>, @@ -97,8 +111,8 @@ pub enum CpuFeaturesTemplate { T2, } -impl Display for CpuFeaturesTemplate { - fn fmt(&self, f: &mut Formatter) -> Result { +impl fmt::Display for CpuFeaturesTemplate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { CpuFeaturesTemplate::C3 => write!(f, "C3"), CpuFeaturesTemplate::T2 => write!(f, "T2"),