From e041d4f739a261209bdcedd313290cf50e1b812a Mon Sep 17 00:00:00 2001 From: Dave Parfitt Date: Tue, 10 May 2016 16:28:18 -0400 Subject: [PATCH] add hab-director Signed-off-by: Dave Parfitt --- Makefile | 4 + components/core/src/fs.rs | 5 + components/core/src/service.rs | 28 + components/director/Cargo.lock | 889 +++++++++++++++++++++++ components/director/Cargo.toml | 48 ++ components/director/README.md | 5 + components/director/build.rs | 10 + components/director/habitat/default.toml | 1 + components/director/habitat/plan.sh | 61 ++ components/director/src/config.rs | 167 +++++ components/director/src/controller.rs | 323 ++++++++ components/director/src/error.rs | 66 ++ components/director/src/lib.rs | 134 ++++ components/director/src/main.rs | 197 +++++ components/director/src/task.rs | 467 ++++++++++++ components/director/tests/functional.rs | 105 +++ components/sup/src/supervisor.rs | 3 +- 17 files changed, 2512 insertions(+), 1 deletion(-) create mode 100644 components/director/Cargo.lock create mode 100644 components/director/Cargo.toml create mode 100644 components/director/README.md create mode 100644 components/director/build.rs create mode 100644 components/director/habitat/default.toml create mode 100644 components/director/habitat/plan.sh create mode 100644 components/director/src/config.rs create mode 100644 components/director/src/controller.rs create mode 100644 components/director/src/error.rs create mode 100644 components/director/src/lib.rs create mode 100644 components/director/src/main.rs create mode 100644 components/director/src/task.rs create mode 100644 components/director/tests/functional.rs diff --git a/Makefile b/Makefile index 3e26db2d09e..30124abb543 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,7 @@ all: image ## builds all the project's Rust components $(run) sh -c 'cd components/core && cargo build' $(run) sh -c 'cd components/depot-core && cargo build' $(run) sh -c 'cd components/depot-client && cargo build' + $(run) sh -c 'cd components/director && cargo build' $(MAKE) bin test: image ## tests the project's Rust components @@ -59,6 +60,7 @@ test: image ## tests the project's Rust components $(run) sh -c 'cd components/common && cargo test' $(run) sh -c 'cd components/sup && cargo test ' $(run) sh -c 'cd components/depot && cargo test' + $(run) sh -c 'cd components/director && cargo test' unit: image ## executes the components' unit test suites $(run) sh -c 'cd components/builder-api && cargo test --lib' @@ -69,6 +71,7 @@ unit: image ## executes the components' unit test suites $(run) sh -c 'cd components/core && cargo test' $(run) sh -c 'cd components/depot-core && cargo test --lib' $(run) sh -c 'cd components/depot-client && cargo test --lib' + $(run) sh -c 'cd components/director && cargo test --lib' $(run) sh -c 'cd components/common && cargo test --lib' $(run) sh -c 'cd components/sup && cargo test --lib' $(run) sh -c 'cd components/depot && cargo test --lib' @@ -89,6 +92,7 @@ clean: ## cleans up the project tree $(run) sh -c 'cd components/depot-client && cargo clean' $(run) sh -c 'cd components/depot-core && cargo clean' $(run) sh -c 'cd components/depot && cargo clean' + $(run) sh -c 'cd components/director && cargo clean' $(run) sh -c 'cd components/hab && cargo clean' $(run) sh -c 'cd components/net && cargo clean' $(run) sh -c 'cd components/sodiumoxide && cargo clean' diff --git a/components/core/src/fs.rs b/components/core/src/fs.rs index 2994f2e4962..0d22f905180 100644 --- a/components/core/src/fs.rs +++ b/components/core/src/fs.rs @@ -49,6 +49,11 @@ pub fn cache_src_path(fs_root_path: Option<&Path>) -> PathBuf { } } +/// Returns the root path containing all runtime service directories and files +pub fn svc_root() -> PathBuf { + Path::new("/").join(SVC_PATH).to_path_buf() +} + /// Returns the root path for a given service's configuration, files, and data. pub fn svc_path(service_name: &str) -> PathBuf { Path::new("/").join(SVC_PATH).join(service_name) diff --git a/components/core/src/service.rs b/components/core/src/service.rs index 07c33be1f93..a120a84c5c4 100644 --- a/components/core/src/service.rs +++ b/components/core/src/service.rs @@ -36,6 +36,11 @@ impl ServiceGroup { organization: organization, } } + + // returns ".org" if self.organization is Some, otherwise an empty string + pub fn dotted_org_or_empty(&self) -> String { + self.organization.as_ref().map_or("".to_string(), |s| format!(".{}", &s)) + } } impl fmt::Display for ServiceGroup { @@ -149,4 +154,27 @@ mod test { fn from_str_not_enough_periods() { ServiceGroup::from_str("oh-noes").unwrap(); } + + #[test] + fn service_groups_with_org() { + let x = ServiceGroup::from_str("foo.bar").unwrap(); + assert!(x.service == "foo".to_string()); + assert!(x.group == "bar".to_string()); + assert!(x.organization.is_none()); + + let y = ServiceGroup::from_str("foo.bar@baz").unwrap(); + assert!(y.service == "foo".to_string()); + assert!(y.group == "bar".to_string()); + assert!(y.organization.unwrap() == "baz"); + + assert!(ServiceGroup::from_str("foo.bar@").is_err()); + assert!(ServiceGroup::from_str("f.oo.bar@baz").is_err()); + assert!(ServiceGroup::from_str("foo@baz").is_err()); + } + + #[test] + fn org_or_empty() { + assert!("" == ServiceGroup::from_str("foo.bar").unwrap().dotted_org_or_empty()); + assert!(".baz" == ServiceGroup::from_str("foo.bar@baz").unwrap().dotted_org_or_empty()); + } } diff --git a/components/director/Cargo.lock b/components/director/Cargo.lock new file mode 100644 index 00000000000..ed14ec4bf51 --- /dev/null +++ b/components/director/Cargo.lock @@ -0,0 +1,889 @@ +[root] +name = "habitat_director" +version = "0.5.0" +dependencies = [ + "ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "habitat_core 0.5.0", + "habitat_sup 0.5.0", + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.69 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "wonder 0.1.0", +] + +[[package]] +name = "aho-corasick" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ansi_term" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bodyparser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "persistent 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "broadcast" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "clap" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "conduit-mime-types" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cookie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "openssl 0.7.12 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "env_logger" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.69 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "error" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "traitobject 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gcc" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "gdi32-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "habitat_common" +version = "0.5.0" +dependencies = [ + "ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "habitat_core 0.5.0", + "habitat_depot_client 0.5.0", + "habitat_depot_core 0.5.0", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.7.12 (registry+https://github.com/rust-lang/crates.io-index)", + "pbr 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.69 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "habitat_core" +version = "0.5.0" +dependencies = [ + "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libarchive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libsodium-sys 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.69 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "sodiumoxide 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "habitat_depot_client" +version = "0.5.0" +dependencies = [ + "broadcast 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "habitat_core 0.5.0", + "habitat_depot_core 0.5.0", + "hyper 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "pbr 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "tee 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "habitat_depot_core" +version = "0.5.0" +dependencies = [ + "habitat_core 0.5.0", + "hyper 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)", + "redis 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "habitat_sup" +version = "0.5.0" +dependencies = [ + "ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "habitat_common 0.5.0", + "habitat_core 0.5.0", + "habitat_depot_client 0.5.0", + "habitat_depot_core 0.5.0", + "hyper 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mustache 0.6.1 (git+https://github.com/adamhjk/rust-mustache?branch=fallback_on_missing_extension)", + "openssl 0.7.12 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.69 (registry+https://github.com/rust-lang/crates.io-index)", + "router 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "temp_utp 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "threadpool 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "urlencoded 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "wonder 0.1.0", +] + +[[package]] +name = "hpack" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "httparse" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "hyper" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cookie 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.1.2 (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.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hyper" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cookie 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.1.2 (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.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.7.12 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-verify 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "iron" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "modifier 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "url 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (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 = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libarchive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libarchive3-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libarchive3-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libc" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libressl-pnacl-sys" +version = "2.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libsodium-sys" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "log" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "matches" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mime" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "modifier" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "mustache" +version = "0.6.1" +source = "git+https://github.com/adamhjk/rust-mustache?branch=fallback_on_missing_extension#ecc021d1ad0bc2f5832fd36a24dece8ff58e6a3a" +dependencies = [ + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-bigint 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-complex 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-rational 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-bigint" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-complex" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-iter" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-rational" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-bigint 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "num_cpus" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "openssl" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.7.12 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys-extras 0.7.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "openssl-sys" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "gdi32-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "user32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "openssl-sys-extras" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.7.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "openssl-verify" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "openssl 0.7.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pbr" +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)", + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "persistent" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pkg-config" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "plugin" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pnacl-build-helper" +version = "1.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redis" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "sha1 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "url 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "0.1.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "route-recognizer" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "router" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "route-recognizer 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc-serialize" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc_version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_json" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha1" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sodiumoxide" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "libsodium-sys 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "solicit" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "strsim" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "tee" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "temp_utp" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tempdir" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread-id" +version = "2.0.0" +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)", + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "threadpool" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "time" +version = "0.1.35" +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)", + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "toml" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "traitobject" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "traitobject" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "typeable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "typemap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unsafe-any 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicase" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-width" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unsafe-any" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "traitobject 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "url" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "url" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "url" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "urlencoded" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bodyparser 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "user32-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "utf8-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "uuid" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "uuid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "vec_map" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wonder" +version = "0.1.0" +dependencies = [ + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", +] + diff --git a/components/director/Cargo.toml b/components/director/Cargo.toml new file mode 100644 index 00000000000..3158edd70cb --- /dev/null +++ b/components/director/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "habitat_director" +version = "0.5.0" +authors = ["Adam Jacob ", "Jamie Winsor ", "Fletcher Nichol ", "Joshua Timberman ", "Dave Parfitt "] +build = "build.rs" + +[lib] +name = "habitat_director" + +[[bin]] +name = "hab-director" +doc = false + +[[test]] +name = "functional" + +[dependencies] +ansi_term = "*" +env_logger = "*" +libc = "*" +log = "*" +regex = "*" +rustc-serialize = "*" +time = "*" +toml = "*" + +[dependencies.clap] +version = "*" +features = [ "suggestions", "color", "unstable" ] + +[dependencies.habitat_core] +path = "../core" + +[dependencies.habitat_sup] +path = "../sup" + +[dependencies.uuid] +version = "*" +features = ["rustc-serialize"] + +[dependencies.wonder] +path = "../../vendor/wonder" + +[dev-dependencies] +tempdir = "*" + +[profile.release] +lto = true diff --git a/components/director/README.md b/components/director/README.md new file mode 100644 index 00000000000..55a8f3a6c6f --- /dev/null +++ b/components/director/README.md @@ -0,0 +1,5 @@ +# Habitat Director + +Please see `src/main.rs` for documentation. + + diff --git a/components/director/build.rs b/components/director/build.rs new file mode 100644 index 00000000000..b9c3534433a --- /dev/null +++ b/components/director/build.rs @@ -0,0 +1,10 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::Path; + +fn main() { + let version = env::var("PLAN_VERSION").unwrap_or(env::var("CARGO_PKG_VERSION").unwrap()); + let mut f = File::create(Path::new(&env::var("OUT_DIR").unwrap()).join("VERSION")).unwrap(); + f.write_all(version.as_bytes()).unwrap(); +} diff --git a/components/director/habitat/default.toml b/components/director/habitat/default.toml new file mode 100644 index 00000000000..5b301928968 --- /dev/null +++ b/components/director/habitat/default.toml @@ -0,0 +1 @@ +path = "/hab/svc/hab-director/data" diff --git a/components/director/habitat/plan.sh b/components/director/habitat/plan.sh new file mode 100644 index 00000000000..e7fd498fe53 --- /dev/null +++ b/components/director/habitat/plan.sh @@ -0,0 +1,61 @@ +pkg_name=hab-director +pkg_origin=core +pkg_version=0.5.0 +pkg_maintainer="The Habitat Maintainers " +pkg_license=('apachev2') +pkg_source=nosuchfile.tar.gz +pkg_deps=(core/glibc core/gcc-libs core/libarchive core/libsodium core/openssl) +pkg_build_deps=(core/coreutils core/cacerts core/rust core/gcc) +pkg_bin_dirs=(bin) +srv_bin="hab-director" +pkg_service_run="bin/$srv_bin start -c ${pkg_svc_path}/config.toml" + +do_prepare() { + # Used by the `build.rs` program to set the version of the binaries + export PLAN_VERSION="${pkg_version}/${pkg_release}" + build_line "Setting PLAN_VERSION=$PLAN_VERSION" + + # Used by Cargo to fetch registries/crates/etc. + export SSL_CERT_FILE=$(pkg_path_for cacerts)/ssl/cert.pem + build_line "Setting SSL_CERT_FILE=$SSL_CERT_FILE" + + export rustc_target="x86_64-unknown-linux-gnu" + build_line "Setting rustc_target=$rustc_target" + + export LIBARCHIVE_LIB_DIR=$(pkg_path_for libarchive)/lib + export LIBARCHIVE_INCLUDE_DIR=$(pkg_path_for libarchive)/include + export OPENSSL_LIB_DIR=$(pkg_path_for openssl)/lib + export OPENSSL_INCLUDE_DIR=$(pkg_path_for openssl)/include + export SODIUM_LIB_DIR=$(pkg_path_for libsodium)/lib +} + +do_build() { + pushd $PLAN_CONTEXT > /dev/null + cargo clean --target=$rustc_target --verbose + cargo build \ + -j $(nproc) \ + --target=$rustc_target \ + --verbose + popd > /dev/null +} + +do_install() { + install -v -D $PLAN_CONTEXT/../target/$rustc_target/debug/$srv_bin \ + $pkg_prefix/bin/$srv_bin +} + +do_strip() { + return 0 +} + +do_download() { + return 0 +} + +do_verify() { + return 0 +} + +do_unpack() { + return 0 +} diff --git a/components/director/src/config.rs b/components/director/src/config.rs new file mode 100644 index 00000000000..5510c218414 --- /dev/null +++ b/components/director/src/config.rs @@ -0,0 +1,167 @@ +// Copyright:: Copyright (c) 2015-2016 The Habitat Maintainers +// +// The terms of the Evaluation Agreement (Habitat) between Chef Software Inc. +// and the party accessing this file ("Licensee") apply to Licensee's use of +// the Software until such time that the Software is made available under an +// open source license such as the Apache 2.0 License. + +use std::str::FromStr; + +use hcore::config::ConfigFile; + +use toml; +use error::{Error, Result}; +use super::ServiceDef; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Config { + pub service_defs: Vec, +} + +impl ConfigFile for Config { + type Error = Error; + + fn from_toml(toml: toml::Value) -> Result { + let mut cfg = Config::default(); + let c = toml.lookup("cfg.services").unwrap(); + let paths = Config::traverse(c); + + for p in &paths { + let mut sd = ServiceDef::from_str(p).unwrap(); + println!("Loaded service def: {}", &sd.to_string()); + let sdname = sd.to_string(); + sd.cli_args = Self::lookup_service_param(&toml, &sdname, "start"); + sd.ident.release = Self::lookup_service_param(&toml, &sdname, "release"); + sd.ident.version = Self::lookup_service_param(&toml, &sdname, "version"); + cfg.service_defs.push(sd); + } + Ok(cfg) + } + +} + +impl Default for Config { + fn default() -> Self { + Config { service_defs: Vec::new() } + } +} + +impl Config { + /// Perform a lookup on a dynamic toml path as part of a service. + /// For example, for a value `valuename` in service name + /// `origin.name.group.org`, we'll perform a lookup on + /// `cfg.services.origin.name.group.org.valuename` and return + /// a Some(string) if it's available. + fn lookup_service_param(toml: &toml::Value, + service_name: &str, + param_name: &str) + -> Option { + let key = format!("cfg.services.{}.{}", service_name, param_name); + if let Some(k) = toml.lookup(&key) { + if let Some(k) = k.as_str() { + return Some(k.to_string()); + } + } + None + } + + /// traverse a toml tree of Tables, return a list of + /// Strings for each unique path + fn traverse(v: &toml::Value) -> Vec { + fn _traverse(v: &toml::Value, path: &mut Vec, paths: &mut Vec) { + let current_path = path.join("."); + if let Some(tbl) = v.as_table() { + // return if this table doesn't have any child tables + if tbl.values().all(|ref v| v.as_table().is_none()) { + paths.push(current_path); + return; + } + for (k, v) in tbl.iter() { + path.push(k.clone()); + _traverse(v, path, paths); + let _ = path.pop(); + } + } + } + + let mut path: Vec = Vec::new(); + let mut paths: Vec = Vec::new(); + _traverse(v, &mut path, &mut paths); + paths + } +} + +#[cfg(test)] +mod tests { + use toml; + + use hcore::config::ConfigFile; + use super::*; + + #[test] + fn test_parse_traversal() { + // NOTE: these toml tables DO NOT contain the cfg.services prefix, + // that will be pulled out via toml.lookup() + let service_toml = r#" + [core.redis.somegroup.someorg] + start = "foo" + [core.rngd.foo.someorg] + start = "bar" + [myorigin.xyz.foo.otherorg] + [foo] + "#; + + let root: toml::Value = service_toml.parse().unwrap(); + let paths = Config::traverse(&root); + assert!(paths.contains(&"core.redis.somegroup.someorg".to_string())); + assert!(paths.contains(&"core.rngd.foo.someorg".to_string())); + assert!(paths.contains(&"myorigin.xyz.foo.otherorg".to_string())); + // segment length doesn't matter, we'll filter those out later + assert!(paths.contains(&"foo".to_string())); + } + + + #[test] + fn test_from_toml() { + let service_toml = r#" + [cfg.services.core.redis.somegroup.someorg] + start = "foo" + + # Comment + [cfg.services.core.rngd.foo.someorg] + start = "bar" + # release + version must be in double quotes! + release="20160427205048" + version="1.1" + + [cfg.services.myorigin.xyz.foo.otherorg] + + [cfg.foo] + # foo should be skipped. check the count of services, we should have + # 3, not 4. + "#; + + let root: toml::Value = service_toml.parse().unwrap(); + let cfg = Config::from_toml(root).unwrap(); + assert_eq!(3, cfg.service_defs.len()); + + // first service + assert_eq!("core.redis.somegroup.someorg", cfg.service_defs[0].to_string()); + assert_eq!(Some("foo".to_string()), cfg.service_defs[0].cli_args); + assert_eq!(None, cfg.service_defs[0].ident.release); + assert_eq!(None, cfg.service_defs[0].ident.version); + + // second service + assert_eq!("core.rngd.foo.someorg", cfg.service_defs[1].to_string()); + assert_eq!(Some("bar".to_string()), cfg.service_defs[1].cli_args); + assert_eq!(Some("20160427205048".to_string()), cfg.service_defs[1].ident.release); + assert_eq!(Some("1.1".to_string()), cfg.service_defs[1].ident.version); + + // third service + assert_eq!("myorigin.xyz.foo.otherorg", cfg.service_defs[2].to_string()); + assert_eq!(None, cfg.service_defs[2].cli_args); + assert_eq!(None, cfg.service_defs[2].ident.release); + assert_eq!(None, cfg.service_defs[2].ident.version); + + } +} diff --git a/components/director/src/controller.rs b/components/director/src/controller.rs new file mode 100644 index 00000000000..748be2cb7b7 --- /dev/null +++ b/components/director/src/controller.rs @@ -0,0 +1,323 @@ +// Copyright:: Copyright (c) 2015-2016 The Habitat Maintainers +// +// The terms of the Evaluation Agreement (Habitat) between Chef Software Inc. +// and the party accessing this file ("Licensee") apply to Licensee's use of +// the Software until such time that the Software is made available under an +// open source license such as the Apache 2.0 License. + +use std::net::Ipv4Addr; +use std::net::SocketAddrV4; +use std::str::FromStr; +use std::sync::mpsc::TryRecvError; +use std::thread; +use std::time::Duration; + +use time::SteadyTime; +use wonder; + +use error::Result; +use hcore::util::sys::ip; +use hsup::util::signals::SignalNotifier; +use hsup::util::signals; + +use super::Config; +use super::task::{Task, ExecContext, ExecParams}; + +/// we check child processes at most once every MINIMUM_LOOP_TIME_MS +static MINIMUM_LOOP_TIME_MS: i64 = 200; +static LOGKEY: &'static str = "CTRL"; +pub const FIRST_GOSSIP_PORT: u16 = 9000; +pub const FIRST_SIDECAR_PORT: u16 = 8000; + +pub struct Controller { + pub config: Config, + pub exec_ctx: ExecContext, + pub handler: wonder::actor::Actor, + pub children: Option>, +} + +impl Controller { + pub fn new(config: Config, exec_ctx: ExecContext) -> Controller { + Controller { + config: config, + exec_ctx: exec_ctx, + handler: wonder::actor::Builder::new(SignalNotifier) + .name("signal-handler".to_string()) + .start(()) + .unwrap(), + children: None, + } + } + + /// iterate through the config ServiceDefs and create `Task` + /// instances. A Controller contains "all the tasks", so + /// it calculate gossip_port + sidecar_port #s accordingly. + pub fn create_children(&mut self) -> Result<()> { + let mut children = Vec::new(); + let mut next_gossip_port = FIRST_GOSSIP_PORT; + let mut next_sidecar_port = FIRST_SIDECAR_PORT; + + let default_ip = try!(ip()); + let listen_ip = try!(Ipv4Addr::from_str(&default_ip)); + + let mut initial_peer: Option = None; + + for sd in &self.config.service_defs { + let exec_ctx = self.exec_ctx.clone(); + let exec_params = ExecParams::new(SocketAddrV4::new(listen_ip, next_gossip_port), + SocketAddrV4::new(listen_ip, next_sidecar_port), + initial_peer); + + // after the first iteration, each child will connect to the previous + initial_peer = Some(exec_params.gossip_listen.clone()); + + let dc = Task::new(exec_ctx, exec_params, sd.clone()); + children.push(dc); + + // this will have to be more intelligent if we + // let users define gossip/sidecar ports + next_gossip_port += 1; + next_sidecar_port += 1; + } + self.children = Some(children); + Ok(()) + } + + /// Process config to create children, then run in a loop forever. + pub fn start(&mut self) -> Result<()> { + try!(self.create_children()); + + if let None = self.children { + outputln!("No services defined"); + return Ok(()); + } + + loop { + let start_time = SteadyTime::now(); + + // do the main loop "stuff" + if !try!(self.next_iteration()) { + // we received a signal, break out of this loop + break; + } + + // Slow down our loop + let elapsed_time = SteadyTime::now() - start_time; + let elapsed_millis = elapsed_time.num_milliseconds(); + + if elapsed_millis < MINIMUM_LOOP_TIME_MS { + thread::sleep(Duration::from_millis((MINIMUM_LOOP_TIME_MS - elapsed_millis) as u64)); + } + } + Ok(()) + } + + /// This is called at each iteration in the self::start() loop. + /// It's pulled out into it's own function so it can be tested. + pub fn next_iteration(&mut self) -> Result { + match self.handler.receiver.try_recv() { + Ok(wonder::actor::Message::Cast(signals::Message::Signal(sig))) => { + debug!("SIG = {:?}", sig); + match sig { + signals::Signal::SIGINT | signals::Signal::SIGTERM => { + // Director shuts down, no return + outputln!("Shutting down"); + return Ok(false); + } + _ => { + let mut children = self.children.as_mut().unwrap(); + for child in children.iter_mut() { + if let Some(pid) = child.pid { + outputln!("Sending {:?} to child {} (pid {})", + &sig, + &child.service_def.to_string(), + &pid); + if let Err(e) = signals::send_signal_to_pid(pid, sig.clone()) { + outputln!("Error sending {:?} to {} (pid {}): {}", + &sig, + &child.service_def.to_string(), + &pid, + e); + + } + } + } + } + }; + } + Ok(_) => {} + Err(TryRecvError::Empty) => {} + Err(TryRecvError::Disconnected) => { + panic!("signal handler crashed!"); + } + }; + // leaving this here as I unwrap a couple lines down + // and I don't want to try and defeat the type system + if let None = self.children { + debug!("No children"); + return Ok(false); + } + let mut children = self.children.as_mut().unwrap(); + for child in children.iter_mut() { + if let Err(e) = child.check_process() { + outputln!("Failed to check child process {}: {}", + &child.service_def.to_string(), + e); + } + if child.is_down() { + match child.start() { + // the Task prints out a better "Started" message than + // we could (including ports etc) + Ok(_) => debug!("Started {}", &child.service_def.to_string()), + Err(e) => { + outputln!("Failed to start {}: {}", &child.service_def.to_string(), e) + } + }; + } + } + Ok(true) + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + use time; + use toml; + + use hcore::config::ConfigFile; + use hcore::util::sys::ip; + use task::ExecContext; + use config::Config; + use super::*; + + fn get_test_config() -> Config { + let service_toml = r#" + [cfg.services.core.redis.somegroup.someorg] + start = "foo" + [cfg.services.core.rngd.foo.someorg] + start = "bar" + [cfg.services.myorigin.xyz.foo.otherorg] + [foo] + "#; + + let root: toml::Value = service_toml.parse().unwrap(); + Config::from_toml(root).unwrap() + } + + // call a closure in a loop until it returns true + // or timeout after 30 seconds and return false + pub fn wait_until_true(c: &mut T, some_fn: F) -> bool + where F: Fn(&mut T) -> bool + { + let wait_duration = time::Duration::seconds(30); + let current_time = time::now_utc().to_timespec(); + let stop_time = current_time + wait_duration; + while time::now_utc().to_timespec() < stop_time { + if some_fn(c) { + return true; + } + } + false + } + + /// parse some config, make sure the arguments are generated correctly + /// and then start some child processes and see if they restart + /// when killed. We don't start full hab-sup processes. + /// + /// NOTE: The controller uses an instance of SignalNotifier, + /// which is a Wonder actor that catches OS signals for us. + /// It can only be started once per process (in "this" test process), + /// so we can't currently share it between tests. Also, as tests + /// are run concurrently, it wouldn't be possible to use a shared + /// instance of SignalNotifier anyways. + #[test] + fn test_controller() { + let mut ec = ExecContext::default(); + ec.sup_path = PathBuf::from("/bin/false"); + ec.service_root = PathBuf::from("/tmp"); + + let config = get_test_config(); + let mut controller = Controller::new(config, ec); + controller.create_children().unwrap(); + assert_eq!(3, controller.children.as_ref().unwrap().len()); + + let test_ip = ip().unwrap(); + { + + let child = &controller.children.as_ref().unwrap()[0]; + let args = child.get_cmd_args().unwrap(); + assert_eq!(args.as_slice(), + ["start", + "core/redis", + "foo", + "--listen-peer", + format!("{}:9000", test_ip).as_str(), + "--listen-sidecar", + format!("{}:8000", test_ip).as_str(), + "--group", + "somegroup", + "--org", + "someorg"]); + } + { + let child = &controller.children.as_ref().unwrap()[1]; + let args = child.get_cmd_args().unwrap(); + assert_eq!(args.as_slice(), + ["start", + "core/rngd", + "bar", + "--listen-peer", + // did we increment the port? + format!("{}:9001", test_ip).as_str(), + "--listen-sidecar", + // did we increment the port? + format!("{}:8001", test_ip).as_str(), + "--group", + "foo", + "--org", + "someorg", + "--peer", + // is the peer set to the previous port? + format!("{}:9000", test_ip).as_str()]); + } + + { + let child = &controller.children.as_ref().unwrap()[2]; + let args = child.get_cmd_args().unwrap(); + + assert_eq!(args.as_slice(), + ["start", + "myorigin/xyz", + "--listen-peer", + // did we increment the port? + format!("{}:9002", test_ip).as_str(), + "--listen-sidecar", + // did we increment the port? + format!("{}:8002", test_ip).as_str(), + "--group", + "foo", + "--org", + "otherorg", + "--peer", + // is the peer set to the previous port? + format!("{}:9001", test_ip).as_str()]); + } + + controller.next_iteration().unwrap(); + + assert_eq!(1, controller.children.as_ref().unwrap()[0].starts); + // We gave the child process bad args, so it won't start. + // Lets wait for 30 seconds to see if we register restarts for the + // children + // let killpid = &controller.children.as_ref().unwrap()[0].pid.unwrap(); + // signals::send_signal_to_pid(*killpid, signals::Signal::SIGKILL).unwrap(); + assert!(wait_until_true(&mut controller, |c| { + c.next_iteration().unwrap(); + c.children.as_ref().unwrap()[0].starts > 1 && + c.children.as_ref().unwrap()[1].starts > 1 && + c.children.as_ref().unwrap()[2].starts > 1 + })); + + } +} diff --git a/components/director/src/error.rs b/components/director/src/error.rs new file mode 100644 index 00000000000..8630ea1347c --- /dev/null +++ b/components/director/src/error.rs @@ -0,0 +1,66 @@ +// Copyright:: Copyright (c) 2015-2016 The Habitat Maintainers +// +// The terms of the Evaluation Agreement (Habitat) between Chef Software Inc. +// and the party accessing this file ("Licensee") apply to Licensee's use of +// the Software until such time that the Software is made available under an +// open source license such as the Apache 2.0 License. + +use std::error; +use std::io; +use std::fmt; +use std::net; +use std::result; + +use hcore; + +#[derive(Debug)] +pub enum Error { + AddrParseError(net::AddrParseError), + DirectorError(String), + HabitatCore(hcore::Error), + IO(io::Error), +} + +pub type Result = result::Result; + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let msg = match *self { + Error::AddrParseError(ref e) => format!("Can't parse IP address {}", e), + Error::DirectorError(ref e) => format!("Director error {}", e), + Error::HabitatCore(ref e) => format!("{}", e), + Error::IO(ref e) => format!("{}", e), + }; + write!(f, "{}", msg) + } +} + +impl error::Error for Error { + fn description(&self) -> &str { + match *self { + Error::AddrParseError(_) => "Can't parse IP address", + Error::DirectorError(_) => "Director Error", + Error::HabitatCore(ref err) => err.description(), + Error::IO(ref err) => err.description(), + } + } +} + +impl From for Error { + fn from(err: hcore::Error) -> Error { + Error::HabitatCore(err) + } +} + + +impl From for Error { + fn from(err: io::Error) -> Error { + Error::IO(err) + } +} + +impl From for Error { + fn from(err: net::AddrParseError) -> Error { + Error::AddrParseError(err) + } +} diff --git a/components/director/src/lib.rs b/components/director/src/lib.rs new file mode 100644 index 00000000000..d636d7935b0 --- /dev/null +++ b/components/director/src/lib.rs @@ -0,0 +1,134 @@ +// Copyright:: Copyright (c) 2015-2016 The Habitat Maintainers +// +// The terms of the Evaluation Agreement (Habitat) between Chef Software Inc. +// and the party accessing this file ("Licensee") apply to Licensee's use of +// the Software until such time that the Software is made available under an +// open source license such as the Apache 2.0 License. + +extern crate habitat_core as hcore; +#[macro_use] +extern crate habitat_sup as hsup; +#[macro_use] +extern crate libc; +#[macro_use] +extern crate log; +#[macro_use] +extern crate rustc_serialize; +extern crate time; +extern crate toml; +extern crate wonder; + +pub mod config; +pub mod error; +pub mod task; +pub mod controller; + +pub use self::config::Config; +pub use self::error::{Error, Result}; + +use std::fmt; +use std::result; +use std::str::FromStr; + +use hcore::package::PackageIdent; +use hcore::service::ServiceGroup; + +/// ServiceDef is a combination of a PackageIdent and ServiceGroup +/// that a user has specified via config file. It represents +/// what the user wants to run, along with command line args and +/// any other params that a user can tweak via config. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ServiceDef { + pub ident: PackageIdent, + pub service_group: ServiceGroup, + pub cli_args: Option, +} + +impl ServiceDef { + pub fn new(ident: PackageIdent, service_group: ServiceGroup) -> ServiceDef { + ServiceDef { + ident: ident, + service_group: service_group, + cli_args: None, + } + } +} + +impl fmt::Display for ServiceDef { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "{}.{}.{}{}", + &self.ident.origin, + &self.ident.name, + &self.service_group.group, + &self.service_group.dotted_org_or_empty()) + } +} + +impl AsRef for ServiceDef { + fn as_ref(&self) -> &ServiceDef { + self + } +} + +impl FromStr for ServiceDef { + type Err = Error; + + fn from_str(value: &str) -> result::Result { + let chunks: Vec<&str> = value.split(".").collect(); + let (origin, name, group, org) = match chunks.len() { + 3 => (chunks[0], chunks[1], chunks[2], None), + 4 => (chunks[0], chunks[1], chunks[2], Some(chunks[3].to_string())), + _ => return Err(Error::DirectorError(format!("Invalid service descriptor: {}", value))), + }; + + let ident = PackageIdent::new(origin, name, None, None); + let sg = ServiceGroup::new(name, group, org); + let sd = ServiceDef::new(ident, sg); + Ok(sd) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + #[test] + fn test_parse_service_without_org() { + let sd = ServiceDef::from_str("core.redis.somegroup").unwrap(); + assert_eq!("core.redis.somegroup", sd.to_string()); + assert_eq!("core", sd.ident.origin); + assert_eq!("redis", sd.ident.name); + assert_eq!(None, sd.ident.version); + assert_eq!(None, sd.ident.release); + assert_eq!("redis", sd.service_group.service); + assert_eq!("somegroup", sd.service_group.group); + assert_eq!(None, sd.service_group.organization); + } + + #[test] + fn test_parse_service_org() { + let sd = ServiceDef::from_str("core.redis.somegroup.someorg").unwrap(); + assert_eq!("core.redis.somegroup.someorg", sd.to_string()); + assert_eq!("core", sd.ident.origin); + assert_eq!("redis", sd.ident.name); + assert_eq!(None, sd.ident.version); + assert_eq!(None, sd.ident.release); + assert_eq!("redis", sd.service_group.service); + assert_eq!("somegroup", sd.service_group.group); + assert_eq!(Some("someorg".to_string()), sd.service_group.organization); + + + } + + #[test] + fn test_parse_bad_service_desc() { + assert!(ServiceDef::from_str("").is_err()); + assert!(ServiceDef::from_str("x").is_err()); + assert!(ServiceDef::from_str("x:y").is_err()); + assert!(ServiceDef::from_str("a.b").is_err()); + assert!(ServiceDef::from_str("a.b.c.d.e").is_err()); + } +} diff --git a/components/director/src/main.rs b/components/director/src/main.rs new file mode 100644 index 00000000000..2b08a72bb5e --- /dev/null +++ b/components/director/src/main.rs @@ -0,0 +1,197 @@ +// Copyright:: Copyright (c) 2015-2016 The Habitat Maintainers +// +// The terms of the Evaluation Agreement (Habitat) between Chef Software Inc. +// and the party accessing this file ("Licensee") apply to Licensee's use of +// the Software until such time that the Software is made available under an +// open source license such as the Apache 2.0 License. + +//! The Habitat Director is a supervisor for a group of `hab-sup` processes. +//! It loads packages to start from it's `config.toml` file. The director +//! will automatically restart child process upon failure detection. Each +//! child service runs in it's own `hab-sup` process. The director can be +//! run inside of a `hab-sup` instance as well. +//! +//! ### Components +//! - `Task` +//! - manages a hab-sup as a child process +//! - tracks a single child process PID +//! - generates CLI arguments for `hab-sup start` +//! - creates a PID file for it's child process +//! - starts a thread to read stdout from the child process +//! +//! - `Controller` +//! - A controller "has" and supervises many Tasks (children) +//! - calculates gossip and sidecar port #'s for all children before starting. +//! - runs in a tight loop to see if children are down and start/restarts them. +//! - catches OS signals +//! +//! - `ExecContext` +//! - A task "execution context". The `ExecContext` is used to +//! decouple service root directory and path to a supervisor executable. +//! Decoupling these values into a struct allows us to easily test +//! `Tasks` + `Controllers`. +//! +//! - `ExecParams` +//! - Config values for a `Task` that the `Controller` calculates during +//! startup. `ExecParams` currently includes: +//! - gossip_listen +//! - sidecar_listen +//! - Option +//! +//! - `ServiceDef` +//! - A combination of `PackageIdent`, `ServiceGroup`, and CLI args. These +//! values are loaded from the config file and are set by the user, as +//! opposed to `ExecContext` values which are set by the `Controller`. +//! - Examples: +//! - `core.redis.somegroup.someorg` corresponds to the +//! `core/redis` `PackageIdent`, and the +//! `redis.somegroup@someorg` `ServiceGroup`. +//! - `core.redis.somegroup` corresponds to the +//! `core/redis` `PackageIdent`, and the `redis.somegroup` `ServiceGroup` +//! (org-less). +//! +//! ``` +//! ┌──────────┐ +//! │ hab-sup │ +//! └──────────┘ +//! │ +//! │ +//! │ ┌───────────────┐ +//! │ │ │ +//! └───▶│ Controller │────┐ ┌────────────┐ ┌────────┐ ┌──────────┐ +//! │ │ ├───▶│ ExecParams │──▶│ Task │───▶│ hab-sup │ +//! └───────────────┘ │ └────────────┘ └────────┘ └──────────┘ +//! ┌─────────────┐ │ ┌────────────┐ ┌────────┐ ┌──────────┐ +//! │ ExecContext │────┼───▶│ ExecParams │──▶│ Task │───▶│ hab-sup │ +//! └─────────────┘ │ └────────────┘ └────────┘ └──────────┘ +//! │ ┌────────────┐ ┌────────┐ ┌──────────┐ +//! └───▶│ ExecParams │──▶│ Task │───▶│ hab-sup │ +//! └────────────┘ └────────┘ └──────────┘ +//! ``` +//! ### Config file format +//! +//! `ServiceDef`s are parsed from the `config.toml` file upon startup. +//! +//! All services must be described as children of the `services` toml table. +//! Note, when toml is rendered, the values for `services` will be +//! located under `cfg.services.*`. +//! +//! Each service definition is a `.` separated list of values as a TOML table name. +//! +//! `[services....]` +//! +//! or rendered by `hab-sup`: +//! +//! `[cfg.services....]` +//! +//! A service definition can additionally specify a `start` key/value under +//! the service table definition: +//! +//! ``` +//! # Start core/redis with --group somegroup and --org someorg +//! # Additionally, pass in --permanent-peer to the start CLI +//! [cfg.services.core.redis.somegroup.someorg] +//! start = "--permanent-peer" +//! +//! [cfg.services.core.rngd.foo.someorg] +//! start = "--permanent-peer --foo=bar" +//! ``` +//! ### Signal handling +//! +//! - If started from bash: when hab-director receives SIGINT or SIGTERM, +//! the director exits. Child processes will have already been sent the signal +//! from bash because they are in the same session group, and will die as well. +//! - TODO: If NOT started from bash: when hab-director receives SIGINT or SIGTERM, +//! signal behavior is undefined and signals are NOT forwarded to child tasks. +//! - When hab-director receives any other signal (that doesn't +//! kill *this* process), they are re-sent to each `Task` in the same +//! order that services are defined in `config.toml`. +//! + +#[macro_use] +extern crate habitat_director as director; +extern crate habitat_core as hcore; +#[macro_use] +extern crate habitat_sup as hsup; +#[macro_use] +extern crate clap; +extern crate env_logger; +#[macro_use] +extern crate log; + +use std::process; + +use hcore::config::ConfigFile; + +use director::config::Config; +use director::controller::Controller; +use director::task::ExecContext; +use director::error::{Error, Result}; + +const VERSION: &'static str = include_str!(concat!(env!("OUT_DIR"), "/VERSION")); +const CFG_DEFAULT_PATH: &'static str = "/hab/svc/hab-director/config.toml"; + +static LOGKEY: &'static str = "DIR"; + +fn main() { + env_logger::init().unwrap(); + let matches = app().get_matches(); + debug!("CLI matches: {:?}", matches); + let config = match config_from_args(&matches) { + Ok(result) => result, + Err(e) => return exit_with(e, 1), + }; + match dispatch(config, &matches) { + Ok(_) => std::process::exit(0), + Err(e) => exit_with(e, 1), + } +} + +fn app<'a, 'b>() -> clap::App<'a, 'b> { + clap_app!(Director => + (version: VERSION) + (about: "Launch and supervise multiple Habitat services") + (@setting VersionlessSubcommands) + (@setting SubcommandRequiredElseHelp) + (@subcommand start => + (about: "Run a Habitat package Depot") + (@arg config: -c --config +takes_value +global + "Path to configuration file. [default: config.toml]") + ) + ) +} + +fn config_from_args(matches: &clap::ArgMatches) -> Result { + let cmd = matches.subcommand_name().unwrap(); + let args = matches.subcommand_matches(cmd).unwrap(); + + let config = match args.value_of("config") { + Some(cfg_path) => try!(Config::from_file(cfg_path)), + None => Config::from_file(CFG_DEFAULT_PATH).unwrap_or(Config::default()), + }; + Ok(config) +} + +fn dispatch(config: Config, matches: &clap::ArgMatches) -> Result<()> { + match matches.subcommand_name() { + Some("start") => start(config), + Some(cmd) => { + debug!("Dispatch failed, no match for command: {:?}", cmd); + Ok(()) + } + None => Ok(()), + } +} + +fn start(config: Config) -> Result<()> { + outputln!("Starting Controller"); + let ec = ExecContext::default(); + let mut controller = Controller::new(config, ec); + try!(controller.start()); + Ok(()) +} + +fn exit_with(err: Error, code: i32) { + println!("{}", err); + process::exit(code) +} diff --git a/components/director/src/task.rs b/components/director/src/task.rs new file mode 100644 index 00000000000..cd296b23a6e --- /dev/null +++ b/components/director/src/task.rs @@ -0,0 +1,467 @@ +// Copyright:: Copyright (c) 2015-2016 Chef Software, Inc. +// +// The terms of the Evaluation Agreement (Habitat) between Chef Software Inc. +// and the party accessing this file ("Licensee") apply to Licensee's use of +// the Software until such time that the Software is made available under an +// open source license such as the Apache 2.0 License. + +use std::fs::{self, File}; +use std::io::prelude::*; +use std::net::SocketAddrV4; +use std::path::PathBuf; +use std::process::{Command, Stdio, Child}; +use std::str; +use std::thread; + +use libc::{pid_t, c_int}; +use time::SteadyTime; + +use error::Result; +use hcore; +use hsup::supervisor::{WEXITSTATUS, WIFEXITED, WIFSIGNALED, WTERMSIG, Pid}; +use super::ServiceDef; +use super::error::Error; + +const READER_BUF_SIZE: usize = 512; +const HAB_SUP_PATH: &'static str = "/src/components/sup/target/debug/hab-sup"; +static LOGKEY: &'static str = "TASK"; + +// Functions from POSIX libc. +extern "C" { + fn waitpid(pid: pid_t, status: *mut c_int, options: c_int) -> pid_t; +} + + +/// Where and with what command a Task runs +/// This is most useful for testing. +#[derive(Debug, Clone)] +pub struct ExecContext { + pub sup_path: PathBuf, + pub service_root: PathBuf, +} + +impl ExecContext { + pub fn new(sup_path: PathBuf, service_root: PathBuf) -> ExecContext { + ExecContext { + sup_path: sup_path, + service_root: service_root, + } + } +} + +impl Default for ExecContext { + fn default() -> ExecContext { + ExecContext::new(PathBuf::from(HAB_SUP_PATH), hcore::fs::svc_root()) + } +} + +/// Values for a Task that are generated by the Controller. +/// These values will be unique per Task. +#[derive(Debug, Clone)] +pub struct ExecParams { + pub gossip_listen: SocketAddrV4, + pub sidecar_listen: SocketAddrV4, + pub initial_peer: Option, +} + + +impl ExecParams { + pub fn new(gossip_listen: SocketAddrV4, + sidecar_listen: SocketAddrV4, + initial_peer: Option) + -> ExecParams { + ExecParams { + gossip_listen: gossip_listen, + sidecar_listen: sidecar_listen, + initial_peer: initial_peer, + } + } +} + + +/// A Task watches a child hab-sup process (a "supervisor supervisor"). +/// It knows how to generate CLI args for `hab-sup start` based on +/// it's ServiceDef, ExecContext, and ExecParams. +#[derive(Debug)] +pub struct Task { + pub pid: Option, + pub service_def: ServiceDef, + pub exec_ctx: ExecContext, + pub exec_params: ExecParams, + pub state_entered: SteadyTime, + pub starts: u64, +} + +impl Task { + pub fn new(exec_ctx: ExecContext, exec_params: ExecParams, service_def: ServiceDef) -> Task { + Task { + pid: None, + service_def: service_def, + exec_ctx: exec_ctx, + exec_params: exec_params, + state_entered: SteadyTime::now(), + starts: 0, + } + } + + pub fn status(&self) -> String { + let desc = if self.is_up() { + "Started" + } else { + "Stopped" + }; + format!("{}: {} for {}", + self.service_def.to_string(), + desc, + SteadyTime::now() - self.state_entered) + } + + pub fn get_cmd_args(&self) -> Result> { + let mut args = match self.service_def.cli_args.as_ref() { + Some(s) => s.as_str().split(" ").map(|s| s.to_string()).collect::>(), + None => Vec::new(), + }; + + args.insert(0, "start".to_string()); + args.insert(1, self.service_def.ident.to_string()); + args.push("--listen-peer".to_string()); + args.push(self.exec_params.gossip_listen.to_string()); + args.push("--listen-sidecar".to_string()); + args.push(self.exec_params.sidecar_listen.to_string()); + args.push("--group".to_string()); + args.push(self.service_def.service_group.group.clone()); + if let Some(org) = self.service_def.service_group.organization.as_ref() { + args.push("--org".to_string()); + args.push(org.clone()); + } + if let Some(initial_peer) = self.exec_params.initial_peer { + args.push("--peer".to_string()); + args.push(initial_peer.to_string()); + } + + // remove whitespace, it messes up the Command args + args.retain(|s| s != ""); + for arg in &args { + debug!("ARG [{}]", &arg); + } + Ok(args) + } + + pub fn start(&mut self) -> Result<()> { + if self.pid.is_none() { + let args = try!(self.get_cmd_args()); + + debug!("hab-up exe = [{}]", + &self.exec_ctx.sup_path.to_string_lossy()); + + outputln!("Starting {} [gossip {}, http API: {}, peer: {}]", + &self.service_def.to_string(), + &self.exec_params.gossip_listen, + &self.exec_params.sidecar_listen, + &self.exec_params.initial_peer + .as_ref() + .map_or("None".to_string(), |v| v.to_string()), + ); + + let mut child = try!(Command::new(&self.exec_ctx.sup_path) + .args(&args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()); + self.pid = Some(child.id()); + + outputln!("Started {} [gossip {}, http API: {}, peer: {}, pid: {}]", + &self.service_def.to_string(), + &self.exec_params.gossip_listen, + &self.exec_params.sidecar_listen, + &self.exec_params.initial_peer + .as_ref() + .map_or("None".to_string(), |v| v.to_string()), + &child.id()); + + try!(self.transition_to_started()); + let name = self.service_def.to_string(); + try!(thread::Builder::new() + .name(String::from(name.clone())) + .spawn(move || -> Result<()> { child_reader(&mut child, name) })); + + } else { + outputln!("{} already started", &self.service_def.to_string()); + } + Ok(()) + } + + pub fn is_up(&self) -> bool { + self.pid.is_some() + } + + pub fn is_down(&self) -> bool { + self.pid.is_none() + } + + // if the child process exists, check it's status via waitpid(). + // Returns true if the process is still running, false if it has died. + pub fn check_process(&mut self) -> Result<()> { + if self.pid.is_none() { + return Ok(()); + } + + unsafe { + let mut status: c_int = 0; + let cpid = self.pid.unwrap() as pid_t; + match waitpid(cpid, &mut status, 1 as c_int) { + 0 => {} // Nothing returned, + pid if pid == cpid => { + if WIFEXITED(status) { + let exit_code = WEXITSTATUS(status); + outputln!("{} - process {} died with exit code {}", + self.service_def.ident.name, + pid, + exit_code); + } else if WIFSIGNALED(status) { + let exit_signal = WTERMSIG(status); + outputln!("{} - process {} died with signal {}", + self.service_def.ident.name, + pid, + exit_signal); + } else { + outputln!("{} - process {} died, but I don't know how.", + self.service_def.ident.name, + pid); + } + try!(self.transition_to_stopped()); + outputln!("{} - Service exited", self.service_def.ident.name); + } + // ZOMBIES! Bad zombies! We listen for zombies. ZOMBOCOM! + pid => { + if WIFEXITED(status) { + let exit_code = WEXITSTATUS(status); + debug!("Process {} died with exit code {}", pid, exit_code); + } else if WIFSIGNALED(status) { + let exit_signal = WTERMSIG(status); + debug!("Process {} terminated with signal {}", pid, exit_signal); + } else { + debug!("Process {} died, but I don't know how.", pid); + } + } + } + } + Ok(()) + } + + fn transition_to_stopped(&mut self) -> Result<()> { + self.pid = None; + self.cleanup_pidfile(); + self.state_entered = SteadyTime::now(); + Ok(()) + } + + fn transition_to_started(&mut self) -> Result<()> { + try!(self.create_pidfile()); + self.starts += 1; + self.state_entered = SteadyTime::now(); + Ok(()) + } + + pub fn service_dir(&self) -> PathBuf { + PathBuf::from(&self.exec_ctx.service_root).join(&self.service_def.ident.name) + } + + + pub fn pid_file(&self) -> PathBuf { + let sd = &self.service_def.to_string().replace(".", "-"); + let filename = format!("{}.pid", sd); + self.service_dir().join(filename) + } + + // Create a pid file for a package + // The existence of this file does not guarantee that a + // process exists at the PID contained within. + pub fn create_pidfile(&self) -> Result<()> { + match self.pid { + Some(ref pid) => { + let pid_file = self.pid_file(); + debug!("Creating PID file for child {} -> {:?}", + pid_file.display(), + pid); + if let Some(parent) = pid_file.parent() { + if let Err(e) = fs::create_dir_all(parent) { + // in most cases, the directory already exists + debug!("Couldn't make pid directory: {}", e); + } + } + let mut f = try!(File::create(pid_file)); + try!(write!(f, "{}", pid)); + Ok(()) + } + None => Ok(()), + } + } + + // Remove a pidfile for this package if it exists. + // Do NOT fail if there is an error removing the PIDFILE + pub fn cleanup_pidfile(&self) { + let pid_file = self.pid_file(); + debug!("Attempting to clean up pid file {}", &pid_file.display()); + match fs::remove_file(pid_file) { + Ok(_) => { + debug!("Removed pid file"); + } + Err(e) => { + debug!("Error removing pidfile: {}, continuing", e); + } + }; + } + + // attempt to read the pidfile for this package. + // If the pidfile does not exist, then return None, + // otherwise, return Some(pid, uptime_seconds). + pub fn read_pidfile(&self) -> Result> { + let pid_file = self.pid_file(); + debug!("Reading pidfile {}", &pid_file.display()); + let mut f = try!(File::open(pid_file)); + let mut contents = String::new(); + try!(f.read_to_string(&mut contents)); + debug!("pidfile contents = {}", contents); + let pid = match contents.parse::() { + Ok(pid) => pid, + Err(e) => { + debug!("Error reading pidfile: {}", e); + return Err(Error::DirectorError("Invalid pid file".to_string())); + } + }; + Ok(Some(pid)) + } +} + +impl Drop for Task { + fn drop(&mut self) { + let _ = self.cleanup_pidfile(); + } +} + + + +// Consume output from a child process until EOF, then finish +fn child_reader(child: &mut Child, child_name: String) -> Result<()> { + debug!("Started reader for {}", &child_name); + let mut c_stdout = match child.stdout { + Some(ref mut s) => s, + None => return Err(Error::DirectorError(format!("Can't read {} stdout", &child_name))), + }; + + let mut line = output_format!(preamble &child_name, logkey "D"); + loop { + let mut buf = [0u8; READER_BUF_SIZE]; // Our byte buffer + let len = try!(c_stdout.read(&mut buf)); + match len { + 0 => { + // 0 == EOF, so stop writing and finish progress + break; + } + _ => { + // Write the buffer to the BufWriter on the Heap + let buf_string = str::from_utf8(&buf[0..len]).unwrap(); + line.push_str(&buf_string); + if line.contains("\n") { + print!("{}", line); + line = output_format!(preamble &child_name, logkey "O"); + } + } + } + } + debug!("child_reader exiting"); + Ok(()) +} + + +#[cfg(test)] +mod tests { + use std::net::SocketAddrV4; + use std::path::PathBuf; + use std::str::FromStr; + + // it's in lib.rs + use super::super::ServiceDef; + + use super::*; + + fn get_test_dc() -> Task { + let mut sd = ServiceDef::from_str("core.redis.somegroup.someorg").unwrap(); + sd.cli_args = Some("-v -foo=bar".to_string()); + let exec_ctx = ExecContext::default(); + let exec_params = ExecParams::new(SocketAddrV4::from_str("127.0.0.1:9000").unwrap(), + SocketAddrV4::from_str("127.0.0.1:8000").unwrap(), + None); + Task::new(exec_ctx, exec_params, sd) + } + + /// parse args, inject listen-peer and listen-sidecar, no peer + #[test] + fn cmd_args_parsing_no_peer() { + let dc = get_test_dc(); + let args = dc.get_cmd_args().unwrap(); + // core/redis is specified in get_test_dc() + assert!(args.as_slice() == + ["start", + "core/redis", + "-v", + "-foo=bar", + "--listen-peer", + "127.0.0.1:9000", + "--listen-sidecar", + "127.0.0.1:8000", + "--group", + "somegroup", + "--org", + "someorg"]); + } + + /// parse args, inject listen-peer, listen-sidecar, peer + #[test] + fn cmd_args_parsing_peer() { + let mut dc = get_test_dc(); + let peer = SocketAddrV4::from_str("127.0.0.1:9876").unwrap(); + // override the test default peer + dc.exec_params.initial_peer = Some(peer); + let args = dc.get_cmd_args().unwrap(); + // core / redis is specified in get_test_dc() + assert!(args.as_slice() == + ["start", + "core/redis", + "-v", + "-foo=bar", + "--listen-peer", + "127.0.0.1:9000", + "--listen-sidecar", + "127.0.0.1:8000", + "--group", + "somegroup", + "--org", + "someorg", + "--peer", + "127.0.0.1:9876"]); + } + + + /// test the pid filename using the default service directory + #[test] + fn pid_file_name_from_default_exec_ctx() { + let dc = get_test_dc(); + let path = dc.pid_file().clone(); + let path = path.to_str().unwrap(); + assert!("/hab/svc/redis/core-redis-somegroup-someorg.pid" == path); + } + + // test the pid filename using a custom service directory + #[test] + fn pid_file_name_from_custom_exec_ctx() { + let mut dc = get_test_dc(); + dc.exec_ctx.service_root = PathBuf::from("/tmp"); + let path = dc.pid_file().clone(); + let path = path.to_str().unwrap(); + println!("[{}]", path); + assert!("/tmp/redis/core-redis-somegroup-someorg.pid" == path); + } +} diff --git a/components/director/tests/functional.rs b/components/director/tests/functional.rs new file mode 100644 index 00000000000..f815c9b6c48 --- /dev/null +++ b/components/director/tests/functional.rs @@ -0,0 +1,105 @@ +// Copyright:: Copyright (c) 2015-2016 The Habitat Maintainers +// +// The terms of the Evaluation Agreement (Habitat) between Chef Software Inc. +// and the party accessing this file ("Licensee") apply to Licensee's use of +// the Software until such time that the Software is made available under an +// open source license such as the Apache 2.0 License. + +extern crate habitat_director as director; +extern crate habitat_core as hcore; +extern crate tempdir; +extern crate time; + +use std::net::SocketAddrV4; +use std::path::PathBuf; +use std::str::FromStr; + +use tempdir::TempDir; + +use director::*; +use director::task::{Task, ExecContext, ExecParams}; + +// call a closure in a loop until it returns true +// or timeout after 30 seconds and return false +pub fn wait_until_true(dc: &mut Task, some_fn: F) -> bool + where F: Fn(&mut Task) -> bool +{ + let wait_duration = time::Duration::seconds(30); + let current_time = time::now_utc().to_timespec(); + let stop_time = current_time + wait_duration; + while time::now_utc().to_timespec() < stop_time { + if some_fn(dc) { + return true; + } + } + false +} + +// Create a Task for testing +fn get_test_dc(name: &str) -> (PathBuf, Task) { + let tmp_service_path = TempDir::new(name).unwrap(); + let mut sd = ServiceDef::from_str("core.functional_test.somegroup.someorg").unwrap(); + sd.cli_args = Some("-v".to_string()); + let mut exec_ctx = ExecContext::default(); + exec_ctx.sup_path = PathBuf::from("/bin/bash"); + let tsp = tmp_service_path.into_path(); + let tsp2 = tsp.clone(); + exec_ctx.service_root = PathBuf::from(tsp); + + let exec_params = ExecParams::new(SocketAddrV4::from_str("127.0.0.1:9000").unwrap(), + SocketAddrV4::from_str("127.0.0.1:8000").unwrap(), + None); + + (tsp2, Task::new(exec_ctx, exec_params, sd)) +} + +/// This test starts a Task process using `bash -c sleep 1` +/// as it's executable. This allows me to check and see if the states +/// are correct before starting, when stopped, and when restarted. +/// The ExecContext allows us to change the name of the hab-sup +/// binary and the path to the service directory. +#[test] +fn task_state_test() { + + let (_tmp, mut dc) = get_test_dc("first"); + dc.service_def.cli_args = Some("-c sleep 1".to_string()); + + // check unstarted state + assert_eq!(None, dc.pid); + assert_eq!(0, dc.starts); + assert!(dc.is_down()); + + // check started state + dc.start().unwrap(); + assert_eq!(1, dc.starts); + assert!(dc.pid.is_some()); + assert!(dc.is_up()); + assert!(dc.pid_file().is_file()); + // does the contents of the pidfile match dc.pid? + assert_eq!(dc.pid.unwrap(), dc.read_pidfile().unwrap().unwrap()); + + assert!(wait_until_true(&mut dc, |d| { + d.check_process().unwrap(); + d.pid.is_none() && d.is_down() + })); + + // pidfile shouldn't exist anymore + assert!(!dc.pid_file().is_file()); + + // make sure we can start it again + dc.start().unwrap(); + assert_eq!(2, dc.starts); + assert!(dc.pid.is_some()); + assert!(dc.is_up()); + assert!(dc.pid_file().is_file()); + // does the contents of the pidfile match dc.pid? + assert_eq!(dc.pid.unwrap(), dc.read_pidfile().unwrap().unwrap()); + + assert!(wait_until_true(&mut dc, |d| { + d.check_process().unwrap(); + d.pid.is_none() && d.is_down() + })); + + // pidfile shouldn't exist anymore + assert!(!dc.pid_file().is_file()); +} diff --git a/components/sup/src/supervisor.rs b/components/sup/src/supervisor.rs index 926f6a081ad..0881250e7a5 100644 --- a/components/sup/src/supervisor.rs +++ b/components/sup/src/supervisor.rs @@ -28,6 +28,7 @@ use util::signals; const PIDFILE_NAME: &'static str = "PID"; static LOGKEY: &'static str = "SV"; +const READER_BUF_SIZE : usize = 512; // Functions from POSIX libc. extern "C" { @@ -384,7 +385,7 @@ fn child_reader(child: &mut Child, package_name: String) -> Result<()> { let mut line = output_format!(preamble &package_name, logkey "O"); loop { - let mut buf = [0u8; 1]; // Our byte buffer + let mut buf = [0u8; READER_BUF_SIZE]; // Our byte buffer let len = try!(c_stdout.read(&mut buf)); match len { 0 => {