From da830c7ce4e1975451b9ee1335e0d4e37c7ac177 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Wed, 11 Aug 2021 19:36:22 -0300 Subject: [PATCH 01/14] Add inteliJ configs to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index ea8c4bf..19c012f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +.idea + From 9d3faf789aaab6d2ff7eabe8ce1547ae4f05b769 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Wed, 11 Aug 2021 20:53:13 -0300 Subject: [PATCH 02/14] Create a makefile to have some Installation logic --- Makefile | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..09ec636 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +DESTDIR= +MODE=debug +USER=root +BIN_GROUP=bin +SYS_GROUP=sys + +.PHONY: all + +ifeq ($(MODE), release) +cargo_args=--release +else +cargo_args= +endif + +build: target/$(MODE)/metadata + +target/$(MODE)/metadata: + cargo build $(cargo_args) + +install: build + mkdir -p $(DESTDIR)/usr/lib + mkdir -p $(DESTDIR)/lib/svc/manifest/system + install -m 0755 -u $(USER) -g $(BIN_GROUP) target/$(MODE)/metadata $(DESTDIR)/usr/lib/metadata + install -m 0755 -u $(USER) -g $(BIN_GROUP) userscript.sh $(DESTDIR)/usr/lib/userscript.sh + install -m 0755 -u $(USER) -g $(SYS_GROUP) metadata.xml $(DESTDIR)/lib/svc/manifest/system/metadata.xml + install -m 0755 -u $(USER) -g $(SYS_GROUP) userscript.xml $(DESTDIR)/lib/svc/manifest/system/userscript.xml From 87ef672b3a751a5a582c642ff739235fbd5d7bd4 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Wed, 11 Aug 2021 21:10:40 -0300 Subject: [PATCH 03/14] Fixes Makefile so it's simpler to use for packagers and people installing directly (do the later on your own risk!) --- Makefile | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 09ec636..e4473c6 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,3 @@ -DESTDIR= MODE=debug USER=root BIN_GROUP=bin @@ -6,10 +5,18 @@ SYS_GROUP=sys .PHONY: all +# Convenience magic so packagers don't accidentally package debug builds +ifdef DESTDIR +MODE=release +endif + +ifndef DESTDIR +ADDITIONAL_INSTALL_ARGS_BIN += -u $(USER) -g $(BIN_GROUP) +ADDITIONAL_INSTALL_ARGS_SMF += -u $(USER) -g $(SYS_GROUP) +endif + ifeq ($(MODE), release) -cargo_args=--release -else -cargo_args= +cargo_args += --release endif build: target/$(MODE)/metadata @@ -20,7 +27,7 @@ target/$(MODE)/metadata: install: build mkdir -p $(DESTDIR)/usr/lib mkdir -p $(DESTDIR)/lib/svc/manifest/system - install -m 0755 -u $(USER) -g $(BIN_GROUP) target/$(MODE)/metadata $(DESTDIR)/usr/lib/metadata - install -m 0755 -u $(USER) -g $(BIN_GROUP) userscript.sh $(DESTDIR)/usr/lib/userscript.sh - install -m 0755 -u $(USER) -g $(SYS_GROUP) metadata.xml $(DESTDIR)/lib/svc/manifest/system/metadata.xml - install -m 0755 -u $(USER) -g $(SYS_GROUP) userscript.xml $(DESTDIR)/lib/svc/manifest/system/userscript.xml + install -m 0755 $(ADDITIONAL_INSTALL_ARGS_BIN) target/$(MODE)/metadata $(DESTDIR)/usr/lib/metadata + install -m 0755 $(ADDITIONAL_INSTALL_ARGS_BIN) userscript.sh $(DESTDIR)/usr/lib/userscript.sh + install -m 0644 $(ADDITIONAL_INSTALL_ARGS_SMF) metadata.xml $(DESTDIR)/lib/svc/manifest/system/metadata.xml + install -m 0644 $(ADDITIONAL_INSTALL_ARGS_SMF) userscript.xml $(DESTDIR)/lib/svc/manifest/system/userscript.xml From 8987bd7a88e7b86319da6e4b64d88a818b33f7aa Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Wed, 11 Aug 2021 21:15:38 -0300 Subject: [PATCH 04/14] I do not understand our install utility --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index e4473c6..ac38399 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ target/$(MODE)/metadata: install: build mkdir -p $(DESTDIR)/usr/lib mkdir -p $(DESTDIR)/lib/svc/manifest/system - install -m 0755 $(ADDITIONAL_INSTALL_ARGS_BIN) target/$(MODE)/metadata $(DESTDIR)/usr/lib/metadata - install -m 0755 $(ADDITIONAL_INSTALL_ARGS_BIN) userscript.sh $(DESTDIR)/usr/lib/userscript.sh - install -m 0644 $(ADDITIONAL_INSTALL_ARGS_SMF) metadata.xml $(DESTDIR)/lib/svc/manifest/system/metadata.xml - install -m 0644 $(ADDITIONAL_INSTALL_ARGS_SMF) userscript.xml $(DESTDIR)/lib/svc/manifest/system/userscript.xml + install -c $(DESTDIR)/usr/lib -m 0755 $(ADDITIONAL_INSTALL_ARGS_BIN) target/$(MODE)/metadata + install -c $(DESTDIR)/usr/lib -m 0755 $(ADDITIONAL_INSTALL_ARGS_BIN) userscript.sh + install -c $(DESTDIR)/lib/svc/manifest/system -m 0644 $(ADDITIONAL_INSTALL_ARGS_SMF) metadata.xml + install -c $(DESTDIR)/lib/svc/manifest/system -m 0644 $(ADDITIONAL_INSTALL_ARGS_SMF) userscript.xml From 3512c66b0ae9ffec11fc8fe573e6bada3bb3fd94 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Wed, 11 Aug 2021 21:23:19 -0300 Subject: [PATCH 05/14] Add documentation of makefile targets and packaging relevant invocations --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9b65e0b..03b2aa3 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ distributions: ## Building and Usage -This software must be built with Rust and Cargo. +This software must be built with Rust and Cargo. for convenience a Makefile is provided ``` -$ cargo build --release +$ gmake MODE=release ``` The built artefact, `target/release/metadata`, is intended to be installed as @@ -45,8 +45,23 @@ for both the metadata service (`metadata.xml`) and the service which executes a user-provided script (`userscript.xml`), and are intended to be included in the image in `/lib/svc/manifest/system`. +The Makefile automates this aswell if wanted +``` +$ gmake install MODE=release +``` + It is desirable to include these services in the SMF seed repository for an image so that they are already imported when the image first boots in the guest. The services include dependent relationships with several early boot networking and identity services in an attempt to ensure the metadata agent runs before network services are completely online. + +## Packaging +If you would like to package this binary use the following command in your build +system to create a prototype directory tree. + +`proto` can be any directory path of your choosing. + +``` +$ gmake DESTDIR=proto +``` \ No newline at end of file From 1e6bec71efbe33d952c0bc660d992efb6e4a5242 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Wed, 11 Aug 2021 21:24:22 -0300 Subject: [PATCH 06/14] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 03b2aa3..2c44e6a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ distributions: ## Building and Usage -This software must be built with Rust and Cargo. for convenience a Makefile is provided +This software must be built with Rust and Cargo. For convenience a Makefile is provided ``` $ gmake MODE=release From a51d65cbddcd1c7bf60732c411ab283b67826b5f Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Wed, 5 Jan 2022 12:52:37 -0300 Subject: [PATCH 07/14] WIP: Cloud init formats support --- Cargo.lock | 213 +++- Cargo.toml | 15 +- Makefile | 2 +- hack/init_fallback_with_dhcp.sh | 16 + sample_data/mime_message.txt | 48 + sample_data/network_config_v1/test_bond.yaml | 21 + .../network_config_v1/test_bonded_vlan.yaml | 26 + .../network_config_v1/test_bridge.yaml | 28 + .../network_config_v1/test_multiple_vlan.yaml | 67 ++ .../network_config_v1/test_nameserver.yaml | 17 + .../network_config_v1/test_physical.yaml | 7 + .../network_config_v1/test_physical_2.yaml | 19 + sample_data/network_config_v1/test_route.yaml | 14 + .../network_config_v1/test_subnet_dhcp.yaml | 8 + .../test_subnet_multiple.yaml | 16 + .../network_config_v1/test_subnet_static.yaml | 15 + .../test_subnet_with_routes.yaml | 18 + sample_data/network_config_v1/test_vlan.yaml | 13 + sample_data/user-data | 26 + src/common.rs | 3 +- src/file.rs | 101 ++ src/main.rs | 965 ++++++++++++++---- src/public_keys.rs | 37 + src/userdata.rs | 125 +++ src/userdata/cloudconfig.rs | 168 +++ src/userdata/multiformat_deserialize.rs | 9 + src/userdata/networkconfig.rs | 834 +++++++++++++++ src/users.rs | 25 + src/users/ffi.rs | 162 +++ src/users/libc.rs | 126 +++ src/zpool.rs | 18 +- 31 files changed, 2970 insertions(+), 192 deletions(-) create mode 100644 hack/init_fallback_with_dhcp.sh create mode 100644 sample_data/mime_message.txt create mode 100644 sample_data/network_config_v1/test_bond.yaml create mode 100644 sample_data/network_config_v1/test_bonded_vlan.yaml create mode 100644 sample_data/network_config_v1/test_bridge.yaml create mode 100644 sample_data/network_config_v1/test_multiple_vlan.yaml create mode 100644 sample_data/network_config_v1/test_nameserver.yaml create mode 100644 sample_data/network_config_v1/test_physical.yaml create mode 100644 sample_data/network_config_v1/test_physical_2.yaml create mode 100644 sample_data/network_config_v1/test_route.yaml create mode 100644 sample_data/network_config_v1/test_subnet_dhcp.yaml create mode 100644 sample_data/network_config_v1/test_subnet_multiple.yaml create mode 100644 sample_data/network_config_v1/test_subnet_static.yaml create mode 100644 sample_data/network_config_v1/test_subnet_with_routes.yaml create mode 100644 sample_data/network_config_v1/test_vlan.yaml create mode 100644 sample_data/user-data create mode 100644 src/file.rs create mode 100644 src/public_keys.rs create mode 100644 src/userdata.rs create mode 100644 src/userdata/cloudconfig.rs create mode 100644 src/userdata/multiformat_deserialize.rs create mode 100644 src/userdata/networkconfig.rs create mode 100644 src/users.rs create mode 100644 src/users/ffi.rs create mode 100644 src/users/libc.rs diff --git a/Cargo.lock b/Cargo.lock index 5b92824..b5d4809 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,20 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "anyhow" version = "1.0.34" @@ -35,6 +50,29 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + [[package]] name = "base64" version = "0.13.0" @@ -64,6 +102,12 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "0.5.6" @@ -82,6 +126,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "charset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f426e64df1c3de26cbf44593c6ffff5dbfd43bbf9de0d075058558126b3fc73" +dependencies = [ + "base64 0.10.1", + "encoding_rs", +] + [[package]] name = "chrono" version = "0.4.19" @@ -111,6 +165,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "crossbeam-utils" version = "0.8.1" @@ -143,6 +206,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + [[package]] name = "encoding_rs" version = "0.8.26" @@ -152,6 +221,40 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "flate2" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -246,6 +349,12 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + [[package]] name = "h2" version = "0.2.7" @@ -355,14 +464,22 @@ version = "0.1.0" dependencies = [ "anyhow", "atty", + "base64 0.13.0", + "failure", + "failure_derive", + "flate2", + "libc", + "mailparse", "reqwest", "serde", "serde_json", + "serde_yaml", "slog", "slog-term", "socket2", "tempfile", "unicode-normalization", + "users", ] [[package]] @@ -386,9 +503,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" [[package]] name = "itoa" @@ -427,6 +544,12 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "log" version = "0.4.11" @@ -436,6 +559,17 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "mailparse" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f526fc13a50f46a3689a6f438cb833c59817c898bb40a3954f341ddf74ce1" +dependencies = [ + "base64 0.13.0", + "charset", + "quoted_printable", +] + [[package]] name = "matches" version = "0.1.8" @@ -464,6 +598,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" version = "0.6.22" @@ -535,6 +679,12 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" + [[package]] name = "once_cell" version = "1.5.2" @@ -629,6 +779,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1238256b09923649ec89b08104c4dfe9f6cb2fea734a5db5384e44916d59e9c5" + [[package]] name = "rand" version = "0.7.3" @@ -702,7 +858,7 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb15d6255c792356a0f578d8a645c677904dc02e862bebe2ecc18e0c01b9a0ce" dependencies = [ - "base64", + "base64 0.13.0", "bytes", "encoding_rs", "futures-core", @@ -736,12 +892,18 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" dependencies = [ - "base64", + "base64 0.13.0", "blake2b_simd", "constant_time_eq", "crossbeam-utils", ] +[[package]] +name = "rustc-demangle" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49" + [[package]] name = "ryu" version = "1.0.5" @@ -797,6 +959,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + [[package]] name = "slab" version = "0.4.2" @@ -845,6 +1019,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "tempfile" version = "3.1.0" @@ -1015,6 +1201,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + [[package]] name = "version_check" version = "0.9.2" @@ -1197,3 +1393,12 @@ dependencies = [ "winapi 0.2.8", "winapi-build", ] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index 6b42c2e..3f554d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "illumos-metadata-agent" description = "Cloud metadata bootstrap software for illumos systems" version = "0.1.0" -authors = ["Joshua M. Clulow "] +authors = ["Joshua M. Clulow ", "Till Wegmueller "] edition = "2018" license = "Apache-2.0" repository = "https://github.com/illumos/metadata-agent" @@ -14,11 +14,19 @@ path = "src/main.rs" [dependencies] serde = { version = "1", features = [ "derive" ] } serde_json = "1" +serde_yaml = "0.8" tempfile = "3" anyhow = "1" slog = "2.5" slog-term = "2.5" atty = "0.2" +failure = "0.1" +failure_derive = "0.1" +flate2 = "1.0" +mailparse = "0.13.5" +users = { version="0.11.0", optional=true } +base64 = "0.13.0" +libc = { version="0.2.15", optional=true } # # Force an earlier version of socket2 which does not use the TCP_MAXSEG symbol @@ -39,3 +47,8 @@ unicode-normalization = "0.1, <0.1.14" version = "0.10" default-features = false features = ["blocking", "json"] + +[features] +default = [ "cmd_users" ] +libc_users = ["users", "libc"] +cmd_users = [] \ No newline at end of file diff --git a/Makefile b/Makefile index ac38399..783585a 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ USER=root BIN_GROUP=bin SYS_GROUP=sys -.PHONY: all +.PHONY: all build install # Convenience magic so packagers don't accidentally package debug builds ifdef DESTDIR diff --git a/hack/init_fallback_with_dhcp.sh b/hack/init_fallback_with_dhcp.sh new file mode 100644 index 0000000..14542f3 --- /dev/null +++ b/hack/init_fallback_with_dhcp.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +DLADM="/usr/sbin/dladm" +IPADM="/usr/sbin/ipadm" +GREP="/usr/bin/grep" + +links=$($DLADM show-link -p -o link,class | $GREP phys) + +for link in $links; do + link_name=${link%:*} + link_class=${link#*:} + echo "Trying to enable DHCP on $link_name" + if $IPADM create-addr -T dhcp -1 -t "$link_name/cloud-init-dhcp"; then + break; + fi +done \ No newline at end of file diff --git a/sample_data/mime_message.txt b/sample_data/mime_message.txt new file mode 100644 index 0000000..5933326 --- /dev/null +++ b/sample_data/mime_message.txt @@ -0,0 +1,48 @@ +Content-Type: multipart/mixed; boundary="===============5051528810296096788==" +MIME-Version: 1.0 + +--===============5051528810296096788== +Content-Type: text/x-shellscript; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="userscript.sh" + +IyEvYmluL2Jhc2gKCi4gL2xpYi9zdmMvc2hhcmUvc21mX2luY2x1ZGUuc2gKClVTRVJTQ1JJUFQ9 +L3Zhci9tZXRhZGF0YS91c2Vyc2NyaXB0CgppZiBbWyAteiAiJFNNRl9GTVJJIiBdXTsgdGhlbgoJ +cHJpbnRmICdFUlJPUjogU01GX0ZNUkkgbm90IHNldDsgcnVubmluZyB1bmRlciBTTUY/XG4nID4m +MgoJZXhpdCAiJFNNRl9FWElUX0VSUl9GQVRBTCIKZmkKCiMKIyBDaGVjayB0byBzZWUgaWYgdGhl +IG1ldGFkYXRhIHNlcnZpY2Ugb2J0YWluZWQgYSBtZXRhZGF0YSBzY3JpcHQ6CiMKaWYgW1sgISAt +eCAiJFVTRVJTQ1JJUFQiIF1dOyB0aGVuCgkjCgkjIFRoZXJlIGlzIG5vIHNjcmlwdC4gIERpc2Fi +bGUgdGhpcyBzZXJ2aWNlIGFuZCBzaWduYWwgc3VjY2Vzcy4KCSMKCS91c3Ivc2Jpbi9zdmNhZG0g +ZGlzYWJsZSAiJFNNRl9GTVJJIgoJZXhpdCAiJFNNRl9FWElUX09LIgpmaQoKIwojIFJ1biB0aGUg +c2NyaXB0LgojCmlmICEgIiRVU0VSU0NSSVBUIjsgdGhlbgoJIwoJIyBFeGl0IDEgc28gdGhhdCBT +TUYgbWlnaHQgcmVzdGFydCB1cy4KCSMKCWV4aXQgMQpmaQoKIwojIFRoZSBzY3JpcHQgd2FzIHN1 +Y2Nlc3NmdWwuICBSZW1vdmUgaXQgc28gdGhhdCBpdCBkb2VzIG5vdCBydW4gYWdhaW4uCiMKaWYg +ISAvYmluL3JtIC1mICIkVVNFUlNDUklQVCI7IHRoZW4KCXByaW50ZiAnRVJST1I6IGNvdWxkIG5v +dCByZW1vdmUgdXNlcnNjcmlwdCBmaWxlP1xuJyA+JjIKCWV4aXQgMQpmaQoKIwojIEV2ZXJ5dGhp +bmcgY29tcGxldGVkIHN1Y2Nlc3NmdWxseS4gIERpc2FibGUgdGhpcyBzZXJ2aWNlIGFuZCBzaWdu +YWwgc3VjY2Vzcy4KIwovdXNyL3NiaW4vc3ZjYWRtIGRpc2FibGUgIiRTTUZfRk1SSSIKZXhpdCAi +JFNNRl9FWElUX09LIgo= + +--===============5051528810296096788== +Content-Type: text/cloud-config; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="sample_data/user-data" + +I2Nsb3VkLWNvbmZpZwoKdXNlcnM6CiAgLSBuYW1lOiBrYWJsdWIKICAgIHN1ZG86ICJBTEw9KEFM +TCkgTk9QQVNTV0Q6QUxMIgogICAgc3NoX2F1dGhvcml6ZWRfa2V5czoKICAgICAgLSAic3NoLXJz +YSBBQUFBQjNOemFDMXljMkVBQUFBQkpRQUFBUXNrYWpkYWxkbmFsc2RuYWxrZG5hbGtkZGpmbmRq +a25kaiBjb250YWN0QG9wZW5mbG93bGFicy5jb20iCiAgICBncm91cHM6IFthZG1pbl0KICAgIHBh +c3N3ZDogIiQ2JHJvdW5kcz00MDk2JE1ZZXR1LjY5WiRzZmRmc2Rmc2Rmc2Rmc2Rmc2ZzZGZzZGZz +ZGYiCiAgICBsb2NrX3Bhc3N3ZDogZmFsc2UKICAgIHNoZWxsOiAiL3Vzci9iaW4vYmFzaCIKICAt +IG5hbWU6IGZvb2JhcgogICAgc3VkbzogIkFMTD0oQUxMKSBOT1BBU1NXRDpBTEwiCiAgICBzc2hf +YXV0aG9yaXplZF9rZXlzOgogICAgICAtICJzc2gtcnNhIEFBQUFCM056YUMxeWMyRUFBQUFCSlFB +QUFRc2thamRhbGRuYWxzZG5hbGtkbmFsa2RkamZuZGprbmRqIGNvbnRhY3RAb3BlbmZsb3dsYWJz +LmNvbSIKICAgIGdyb3VwczogW2FkbWluXQogICAgcGFzc3dkOiAiJDYkcm91bmRzPTQwOTYkTVll +dHUuNjlaJHNmZGZzZGZzZGZzZGZzZGZzZnNkZnNkZnNkZiIKICAgIGxvY2tfcGFzc3dkOiBmYWxz +ZQogICAgc2hlbGw6ICIvdXNyL2Jpbi9iYXNoIgoKcGFja2FnZV91cGRhdGU6IHRydWUKcGFja2Fn +ZV91cGdyYWRlOiB0cnVlCgpncm93cGFydDoKICBtb2RlOiBhdXRvCiAgZGV2aWNlczogWycvJ10K + +--===============5051528810296096788==-- + diff --git a/sample_data/network_config_v1/test_bond.yaml b/sample_data/network_config_v1/test_bond.yaml new file mode 100644 index 0000000..1c4cb82 --- /dev/null +++ b/sample_data/network_config_v1/test_bond.yaml @@ -0,0 +1,21 @@ +network: + version: 1 + config: + # Simple network adapter + - type: physical + name: interface0 + mac_address: '00:11:22:33:44:55' + # 10G pair + - type: physical + name: gbe0 + mac_address: cd:11:22:33:44:00 + - type: physical + name: gbe1 + mac_address: cd:11:22:33:44:02 + - type: bond + name: bond0 + bond_interfaces: + - gbe0 + - gbe1 + params: + bond-mode: active-backup \ No newline at end of file diff --git a/sample_data/network_config_v1/test_bonded_vlan.yaml b/sample_data/network_config_v1/test_bonded_vlan.yaml new file mode 100644 index 0000000..1e6d9c9 --- /dev/null +++ b/sample_data/network_config_v1/test_bonded_vlan.yaml @@ -0,0 +1,26 @@ +network: + version: 1 + config: + # 10G pair + - type: physical + name: gbe0 + mac_address: cd:11:22:33:44:00 + - type: physical + name: gbe1 + mac_address: cd:11:22:33:44:02 + # Bond. + - type: bond + name: bond0 + bond_interfaces: + - gbe0 + - gbe1 + params: + bond-mode: 802.3ad + bond-lacp-rate: fast + # A Bond VLAN. + - type: vlan + name: bond0.200 + vlan_link: bond0 + vlan_id: 200 + subnets: + - type: dhcp4 \ No newline at end of file diff --git a/sample_data/network_config_v1/test_bridge.yaml b/sample_data/network_config_v1/test_bridge.yaml new file mode 100644 index 0000000..67ca4a6 --- /dev/null +++ b/sample_data/network_config_v1/test_bridge.yaml @@ -0,0 +1,28 @@ +network: + version: 1 + config: + # Simple network adapter + - type: physical + name: interface0 + mac_address: '00:11:22:33:44:55' + # Second nic with Jumbo frames + - type: physical + name: jumbo0 + mac_address: aa:11:22:33:44:55 + mtu: 9000 + - type: bridge + name: br0 + bridge_interfaces: + - jumbo0 + params: + bridge_ageing: 250 + bridge_bridgeprio: 22 + bridge_fd: 1 + bridge_hello: 1 + bridge_maxage: 10 + bridge_maxwait: 0 + bridge_pathcost: + - jumbo0 75 + bridge_pathprio: + - jumbo0 28 + bridge_stp: 'off' \ No newline at end of file diff --git a/sample_data/network_config_v1/test_multiple_vlan.yaml b/sample_data/network_config_v1/test_multiple_vlan.yaml new file mode 100644 index 0000000..62535aa --- /dev/null +++ b/sample_data/network_config_v1/test_multiple_vlan.yaml @@ -0,0 +1,67 @@ +network: + version: 1 + config: + - id: eth0 + mac_address: d4:be:d9:a8:49:13 + mtu: 1500 + name: eth0 + subnets: + - address: 10.245.168.16/21 + dns_nameservers: + - 10.245.168.2 + gateway: 10.245.168.1 + type: static + type: physical + - id: eth1 + mac_address: d4:be:d9:a8:49:15 + mtu: 1500 + name: eth1 + subnets: + - address: 10.245.188.2/24 + dns_nameservers: [] + type: static + type: physical + - id: eth1.2667 + mtu: 1500 + name: eth1.2667 + subnets: + - address: 10.245.184.2/24 + dns_nameservers: [] + type: static + type: vlan + vlan_id: 2667 + vlan_link: eth1 + - id: eth1.2668 + mtu: 1500 + name: eth1.2668 + subnets: + - address: 10.245.185.1/24 + dns_nameservers: [] + type: static + type: vlan + vlan_id: 2668 + vlan_link: eth1 + - id: eth1.2669 + mtu: 1500 + name: eth1.2669 + subnets: + - address: 10.245.186.1/24 + dns_nameservers: [] + type: static + type: vlan + vlan_id: 2669 + vlan_link: eth1 + - id: eth1.2670 + mtu: 1500 + name: eth1.2670 + subnets: + - address: 10.245.187.2/24 + dns_nameservers: [] + type: static + type: vlan + vlan_id: 2670 + vlan_link: eth1 + - address: [ 10.245.168.2 ] + search: + - dellstack + type: nameserver \ No newline at end of file diff --git a/sample_data/network_config_v1/test_nameserver.yaml b/sample_data/network_config_v1/test_nameserver.yaml new file mode 100644 index 0000000..4c610c4 --- /dev/null +++ b/sample_data/network_config_v1/test_nameserver.yaml @@ -0,0 +1,17 @@ +network: + version: 1 + config: + - type: physical + name: interface0 + mac_address: '00:11:22:33:44:55' + subnets: + - type: static + address: 192.168.23.14/27 + gateway: 192.168.23.1 + - type: nameserver + interface: interface0 # Ties nameserver to interface0 only + address: + - 192.168.23.2 + - 8.8.8.8 + search: + - exemplary \ No newline at end of file diff --git a/sample_data/network_config_v1/test_physical.yaml b/sample_data/network_config_v1/test_physical.yaml new file mode 100644 index 0000000..ed7e479 --- /dev/null +++ b/sample_data/network_config_v1/test_physical.yaml @@ -0,0 +1,7 @@ +network: + version: 1 + config: + - type: physical + name: eth0 + subnets: + - type: dhcp \ No newline at end of file diff --git a/sample_data/network_config_v1/test_physical_2.yaml b/sample_data/network_config_v1/test_physical_2.yaml new file mode 100644 index 0000000..9dc4fc1 --- /dev/null +++ b/sample_data/network_config_v1/test_physical_2.yaml @@ -0,0 +1,19 @@ +network: + version: 1 + config: + # Simple network adapter + - type: physical + name: interface0 + mac_address: '00:11:22:33:44:55' + # Second nic with Jumbo frames + - type: physical + name: jumbo0 + mac_address: aa:11:22:33:44:55 + mtu: 9000 + # 10G pair + - type: physical + name: gbe0 + mac_address: cd:11:22:33:44:00 + - type: physical + name: gbe1 + mac_address: cd:11:22:33:44:02 \ No newline at end of file diff --git a/sample_data/network_config_v1/test_route.yaml b/sample_data/network_config_v1/test_route.yaml new file mode 100644 index 0000000..f730854 --- /dev/null +++ b/sample_data/network_config_v1/test_route.yaml @@ -0,0 +1,14 @@ +network: + version: 1 + config: + - type: physical + name: interface0 + mac_address: '00:11:22:33:44:55' + subnets: + - type: static + address: 192.168.23.14/24 + gateway: 192.168.23.1 + - type: route + destination: 192.168.24.0/24 + gateway: 192.168.24.1 + metric: 3 \ No newline at end of file diff --git a/sample_data/network_config_v1/test_subnet_dhcp.yaml b/sample_data/network_config_v1/test_subnet_dhcp.yaml new file mode 100644 index 0000000..568b06c --- /dev/null +++ b/sample_data/network_config_v1/test_subnet_dhcp.yaml @@ -0,0 +1,8 @@ +network: + version: 1 + config: + - type: physical + name: interface0 + mac_address: '00:11:22:33:44:55' + subnets: + - type: dhcp \ No newline at end of file diff --git a/sample_data/network_config_v1/test_subnet_multiple.yaml b/sample_data/network_config_v1/test_subnet_multiple.yaml new file mode 100644 index 0000000..4df6bc8 --- /dev/null +++ b/sample_data/network_config_v1/test_subnet_multiple.yaml @@ -0,0 +1,16 @@ +network: + version: 1 + config: + - type: physical + name: interface0 + mac_address: '00:11:22:33:44:55' + subnets: + - type: dhcp + - type: static + address: 192.168.23.14/27 + gateway: 192.168.23.1 + dns_nameservers: + - 192.168.23.2 + - 8.8.8.8 + dns_search: + - exemplary \ No newline at end of file diff --git a/sample_data/network_config_v1/test_subnet_static.yaml b/sample_data/network_config_v1/test_subnet_static.yaml new file mode 100644 index 0000000..e34c997 --- /dev/null +++ b/sample_data/network_config_v1/test_subnet_static.yaml @@ -0,0 +1,15 @@ +network: + version: 1 + config: + - type: physical + name: interface0 + mac_address: '00:11:22:33:44:55' + subnets: + - type: static + address: 192.168.23.14/27 + gateway: 192.168.23.1 + dns_nameservers: + - 192.168.23.2 + - 8.8.8.8 + dns_search: + - exemplary.maas \ No newline at end of file diff --git a/sample_data/network_config_v1/test_subnet_with_routes.yaml b/sample_data/network_config_v1/test_subnet_with_routes.yaml new file mode 100644 index 0000000..88d9db5 --- /dev/null +++ b/sample_data/network_config_v1/test_subnet_with_routes.yaml @@ -0,0 +1,18 @@ +network: + version: 1 + config: + - type: physical + name: interface0 + mac_address: '00:11:22:33:44:55' + subnets: + - type: dhcp + - type: static + address: 10.184.225.122 + netmask: 255.255.255.252 + routes: + - gateway: 10.184.225.121 + netmask: 255.240.0.0 + network: 10.176.0.0 + - gateway: 10.184.225.121 + netmask: 255.240.0.0 + network: 10.208.0.0 \ No newline at end of file diff --git a/sample_data/network_config_v1/test_vlan.yaml b/sample_data/network_config_v1/test_vlan.yaml new file mode 100644 index 0000000..09b9f23 --- /dev/null +++ b/sample_data/network_config_v1/test_vlan.yaml @@ -0,0 +1,13 @@ +network: + version: 1 + config: + # Physical interfaces. + - type: physical + name: eth0 + mac_address: c0:d6:9f:2c:e8:80 + # VLAN interface. + - type: vlan + name: eth0.101 + vlan_link: eth0 + vlan_id: 101 + mtu: 1500 \ No newline at end of file diff --git a/sample_data/user-data b/sample_data/user-data new file mode 100644 index 0000000..8b548e6 --- /dev/null +++ b/sample_data/user-data @@ -0,0 +1,26 @@ +#cloud-config + +users: + - name: kablub + sudo: "ALL=(ALL) NOPASSWD:ALL" + ssh_authorized_keys: + - "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQskajdaldnalsdnalkdnalkddjfndjkndj contact@openflowlabs.com" + groups: [admin] + passwd: "$6$rounds=4096$MYetu.69Z$sfdfsdfsdfsdfsdfsfsdfsdfsdf" + lock_passwd: false + shell: "/usr/bin/bash" + - name: foobar + sudo: "ALL=(ALL) NOPASSWD:ALL" + ssh_authorized_keys: + - "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQskajdaldnalsdnalkdnalkddjfndjkndj contact@openflowlabs.com" + groups: [admin] + passwd: "$6$rounds=4096$MYetu.69Z$sfdfsdfsdfsdfsdfsfsdfsdfsdf" + lock_passwd: false + shell: "/usr/bin/bash" + +package_update: true +package_upgrade: true + +growpart: + mode: auto + devices: ['/'] diff --git a/src/common.rs b/src/common.rs index 89c1a6c..b953c00 100644 --- a/src/common.rs +++ b/src/common.rs @@ -7,7 +7,8 @@ use slog::Drain; use std::sync::Mutex; pub use slog::{info, warn, error, debug, trace, o, Logger}; -pub use anyhow::{bail, Result, Context}; +pub use anyhow::{Context}; +pub use failure::{ResultExt, bail, Error, Fail}; /** * Initialise a logger which writes to stdout, and which does the right thing on diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 0000000..96bdb4a --- /dev/null +++ b/src/file.rs @@ -0,0 +1,101 @@ +use crate::common::*; +use std::fs::{DirBuilder, File}; +use std::os::unix::fs::{DirBuilderExt, PermissionsExt}; +use std::io::{ErrorKind, BufReader, BufRead, copy as IOCopy, Read, Write}; + +pub fn ensure_dir(log: &Logger, path: &str) -> Result<(), failure::Error> { + if !exists_dir(path)? { + info!(log, "mkdir {}", path); + DirBuilder::new() + .mode(0o700) + .create(path)?; + } + Ok(()) +} + +pub fn exists_dir(p: &str) -> Result { + let md = match std::fs::metadata(p) { + Ok(md) => md, + Err(e) => match e.kind() { + ErrorKind::NotFound => return Ok(false), + _ => bail!("checking {}: {}", p, e), + }, + }; + + if !md.is_dir() { + bail!("\"{}\" exists but is not a directory", p); + } + + Ok(true) +} + +pub fn exists_file(p: &str) -> Result { + let md = match std::fs::metadata(p) { + Ok(md) => md, + Err(e) => match e.kind() { + ErrorKind::NotFound => return Ok(false), + _ => bail!("checking {}: {}", p, e), + }, + }; + + if !md.is_file() { + bail!("\"{}\" exists but is not a file", p); + } + + Ok(true) +} + + +pub fn read_lines(p: &str) -> Result>, failure::Error> { + Ok(read_file(p)?.map(|data| { + data.lines().map(|a| a.trim().to_string()).collect() + })) +} + +pub fn read_lines_maybe(p: &str) -> Result, failure::Error> { + Ok(match read_lines(p)? { + None => Vec::new(), + Some(l) => l, + }) +} + +pub + +fn read_file(p: &str) -> Result, failure::Error> { + let f = match File::open(p) { + Ok(f) => f, + Err(e) => { + match e.kind() { + std::io::ErrorKind::NotFound => return Ok(None), + _ => bail!("open \"{}\": {}", p, e), + }; + } + }; + let mut r = std::io::BufReader::new(f); + let mut out = String::new(); + r.read_to_string(&mut out)?; + Ok(Some(out)) +} + +pub fn write_file(p: &str, data: &str) -> Result<(), failure::Error> { + let f = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(p)?; + let mut w = std::io::BufWriter::new(f); + w.write_all(data.as_bytes())?; + Ok(()) +} + +pub fn write_lines(log: &Logger, p: &str, lines: &[L]) -> Result<(), failure::Error> + where L: AsRef + std::fmt::Debug +{ + info!(log, "----- WRITE FILE: {} ------ {:#?}", p, lines); + let mut out = String::new(); + for l in lines { + out.push_str(l.as_ref()); + out.push_str("\n"); + } + write_file(p, &out) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 30b6eea..ebb9f44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,47 @@ /* * Copyright 2020 Oxide Computer Company + * Copyright 2021 OpenFlowLabs + * */ -use std::io::Read; -use std::io::Write; -use std::io::ErrorKind; +#[cfg(target_os="illumos")] +use std::os::unix::fs::MetadataExt; +#[cfg(target_os="linux")] +use std::os::linux::fs::MetadataExt; +use std::os::unix::ffi::OsStrExt; + +mod zpool; +mod common; +mod userdata; +mod users; +mod public_keys; +mod file; + use std::collections::HashMap; -use std::path::Path; use std::fs::{self, DirBuilder, File, OpenOptions}; +use std::io::prelude::*; +use std::io::{ErrorKind, BufReader, BufRead, copy as IOCopy, Read, Write}; +use std::net::Ipv4Addr; use std::os::unix::fs::{DirBuilderExt, PermissionsExt}; -use std::process::Command; +use std::path::{Path, PathBuf}; +use std::process::{Command}; +use userdata::read_user_data; +use flate2::read::GzDecoder; +use base64::{decode as base64Decode}; +use libc::{self, gid_t, uid_t}; +use std::ffi::CString; +use std::io::Error as IOError; +use std::io::Result as IOResult; use serde::Deserialize; -use std::net::Ipv4Addr; - -mod zpool; -mod common; use common::*; - +use file::*; +use crate::users::*; +use crate::userdata::networkconfig::{NetworkDataV1Iface, NetworkDataV1Subnet}; +use crate::userdata::UserData; +use crate::userdata::cloudconfig::{UserConfig, WriteFileEncoding, WriteFileData}; +use crate::userdata::multiformat_deserialize::Multiformat; const METADATA_DIR: &str = "/var/metadata"; const STAMP: &str = "/var/metadata/stamp"; @@ -41,6 +64,7 @@ const SVCADM: &str = "/usr/sbin/svcadm"; const SWAPADD: &str = "/sbin/swapadd"; const ZFS: &str = "/sbin/zfs"; const CPIO: &str = "/usr/bin/cpio"; +const PKG: &str = "/usr/bin/pkg"; const FMRI_USERSCRIPT: &str = "svc:/system/illumos/userscript:default"; @@ -52,7 +76,7 @@ struct Smbios { uuid: String, } -fn amazon_metadata_getx(log: &Logger, key: &str) -> Result> { +fn amazon_metadata_getx(log: &Logger, key: &str) -> Result, failure::Error> { let url = format!("http://169.254.169.254/latest/{}", key); let cb = reqwest::blocking::ClientBuilder::new() @@ -80,11 +104,11 @@ fn amazon_metadata_getx(log: &Logger, key: &str) -> Result> { } } -fn amazon_metadata_get(log: &Logger, key: &str) -> Result> { +fn amazon_metadata_get(log: &Logger, key: &str) -> Result, failure::Error> { amazon_metadata_getx(log, &format!("meta-data/{}", key)) } -fn smf_enable(log: &Logger, fmri: &str) -> Result<()> { +fn smf_enable(log: &Logger, fmri: &str) -> Result<(), failure::Error> { info!(log, "exec: svcadm enable {}", fmri); let output = Command::new(SVCADM) .env_clear() @@ -99,7 +123,7 @@ fn smf_enable(log: &Logger, fmri: &str) -> Result<()> { Ok(()) } -fn dhcpinfo(log: &Logger, key: &str) -> Result> { +fn dhcpinfo(log: &Logger, key: &str) -> Result, failure::Error> { info!(log, "exec: dhcpinfo {}", key); let output = Command::new(DHCPINFO) .env_clear() @@ -119,7 +143,7 @@ fn dhcpinfo(log: &Logger, key: &str) -> Result> { } } -fn smbios(log: &Logger) -> Result> { +fn smbios(log: &Logger) -> Result, failure::Error> { info!(log, "exec: smbios -t 1"); let output = Command::new(SMBIOS) .env_clear() @@ -171,7 +195,7 @@ enum Mdata { WrongHypervisor, } -fn mdata_get(log: &Logger, key: &str) -> Result { +fn mdata_get(log: &Logger, key: &str) -> Result { info!(log, "mdata-get \"{}\"...", key); let output = Command::new(MDATA_GET) .env_clear() @@ -203,7 +227,7 @@ fn mdata_get(log: &Logger, key: &str) -> Result { }) } -fn mdata_probe(log: &Logger) -> Result { +fn mdata_probe(log: &Logger) -> Result { /* * Check for an mdata-get(1M) binary on this system: */ @@ -234,59 +258,7 @@ fn mdata_probe(log: &Logger) -> Result { } } -pub fn write_file(p: &str, data: &str) -> Result<()> { - let f = std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(p)?; - let mut w = std::io::BufWriter::new(f); - w.write_all(data.as_bytes())?; - Ok(()) -} - -fn write_lines(log: &Logger, p: &str, lines: &[L]) -> Result<()> - where L: AsRef + std::fmt::Debug -{ - info!(log, "----- WRITE FILE: {} ------ {:#?}", p, lines); - let mut out = String::new(); - for l in lines { - out.push_str(l.as_ref()); - out.push_str("\n"); - } - write_file(p, &out) -} - -fn read_file(p: &str) -> Result> { - let f = match File::open(p) { - Ok(f) => f, - Err(e) => { - match e.kind() { - std::io::ErrorKind::NotFound => return Ok(None), - _ => bail!("open \"{}\": {}", p, e), - }; - } - }; - let mut r = std::io::BufReader::new(f); - let mut out = String::new(); - r.read_to_string(&mut out)?; - Ok(Some(out)) -} - -fn read_lines(p: &str) -> Result>> { - Ok(read_file(p)?.map(|data| { - data.lines().map(|a| a.trim().to_string()).collect() - })) -} - -fn read_lines_maybe(p: &str) -> Result> { - Ok(match read_lines(p)? { - None => Vec::new(), - Some(l) => l, - }) -} - -fn read_json(p: &str) -> Result> +fn read_json(p: &str) -> Result, failure::Error> where for<'de> T: Deserialize<'de> { let s = read_file(p)?; @@ -329,7 +301,7 @@ struct IPv4 { } impl IPv4 { - fn prefix_len(&self) -> Result { + fn prefix_len(&self) -> Result { let nm: Ipv4Addr = self.netmask.parse()?; let bits: u32 = nm.into(); @@ -346,7 +318,7 @@ impl IPv4 { Ok(len) } - fn cidr(&self) -> Result { + fn cidr(&self) -> Result { let prefix_len = self.prefix_len()?; Ok(format!("{}/{}", self.ip_address, prefix_len)) } @@ -368,7 +340,7 @@ struct Interfaces { } #[derive(Debug,Deserialize)] -struct Metadata { +struct DOMetadata { auth_key: String, dns: DNS, droplet_id: u64, @@ -404,7 +376,7 @@ impl SdcNic { * a dictionary as there may be more than one mount entry for a particular mount * point. */ -fn mounts() -> Result> { +fn mounts() -> Result, failure::Error> { let mnttab = read_lines("/etc/mnttab")?.unwrap(); let rows: Vec> = mnttab.iter() .map(|m| { m.split('\t').collect() }) @@ -440,49 +412,7 @@ fn mounts() -> Result> { Ok(out) } -fn exists_dir(p: &str) -> Result { - let md = match std::fs::metadata(p) { - Ok(md) => md, - Err(e) => match e.kind() { - ErrorKind::NotFound => return Ok(false), - _ => bail!("checking {}: {}", p, e), - }, - }; - - if !md.is_dir() { - bail!("\"{}\" exists but is not a directory", p); - } - - Ok(true) -} - -fn ensure_dir(log: &Logger, path: &str) -> Result<()> { - if !exists_dir(path)? { - info!(log, "mkdir {}", path); - DirBuilder::new() - .mode(0o700) - .create(path)?; - } - Ok(()) -} - -fn exists_file(p: &str) -> Result { - let md = match std::fs::metadata(p) { - Ok(md) => md, - Err(e) => match e.kind() { - ErrorKind::NotFound => return Ok(false), - _ => bail!("checking {}: {}", p, e), - }, - }; - - if !md.is_file() { - bail!("\"{}\" exists but is not a file", p); - } - - Ok(true) -} - -fn detect_archive>(rdevpath: P) -> Result> { +fn detect_archive>(rdevpath: P) -> Result, failure::Error> { let mut buf = [0u8; 512]; let mut f = OpenOptions::new() .read(true) @@ -506,7 +436,7 @@ fn detect_archive>(rdevpath: P) -> Result> { } } -fn find_cpio_device(log: &Logger) -> Result> { +fn find_cpio_device(log: &Logger) -> Result, failure::Error> { /* * Use the raw device so that we can read just enough bytes to look for the * cpio magic: @@ -545,7 +475,75 @@ fn find_cpio_device(log: &Logger) -> Result> { } } -fn find_device() -> Result> { +#[derive(Debug, Default, Clone)] +struct CiDataDevice { + fstype: String, + path: String, + label: String, +} + +fn find_cidata_devices(log: &Logger) -> Result>, failure::Error> { + let i = std::fs::read_dir("/dev/dsk")?; + + let mut out: Vec = Vec::new(); + + for ent in i { + let ent = ent?; + + if let Some(name) = ent.file_name().to_str() { + if !name.ends_with("p0") && !name.ends_with("s0") { + continue; + } + } else { + continue; + } + + /* + * Determine which type of file system resides on the device: + */ + let output = Command::new(FSTYP) + .env_clear() + .arg("-a") + .arg(ent.path()) + .output()?; + + if !output.status.success() { + continue; + } + + + let mut dev = CiDataDevice::default(); + dev.path = ent.path().to_str().unwrap().to_string(); + let mut count = 0; + for line_result in BufReader::new(output.stdout.as_slice()).lines() { + if let Ok(line) = line_result { + if count == 0 { + dev.fstype = line.trim().to_lowercase().clone(); + } + if line.contains(":") { + if let Some((key, value)) = line.split_once(":") { + if key.trim().to_lowercase() == "volume_label" { + let label = value.replace("'", "").trim().to_lowercase(); + if label == "cidata" { + dev.label = label; + debug!(log, "found cidata device {} of type {}", &dev.path, &dev.fstype); + out.push(dev.clone()); + } + } + } + } + count += 1; + } + } + } + + match out.len() { + 0 => Ok(None), + _ => Ok(Some(out)), + } +} + +fn find_device() -> Result, failure::Error> { let i = std::fs::read_dir("/dev/dsk")?; let mut out = Vec::new(); @@ -554,7 +552,7 @@ fn find_device() -> Result> { let ent = ent?; if let Some(name) = ent.file_name().to_str() { - if !name.ends_with("p0") { + if !name.ends_with("p0") && !name.ends_with("s0") { continue; } } else { @@ -577,6 +575,9 @@ fn find_device() -> Result> { if s.trim() == "hsfs" { out.push(ent.path()); } + if s.trim() == "pcfs" { + out.push(ent.path()); + } } } @@ -620,7 +621,7 @@ impl Terms { } } -fn parse_net_adm(stdout: Vec) -> Result>> { +fn parse_net_adm(stdout: Vec) -> Result>, failure::Error> { let stdout = String::from_utf8(stdout)?; let mut out = Vec::new(); @@ -648,7 +649,67 @@ fn parse_net_adm(stdout: Vec) -> Result>> { Ok(out) } -fn ipadm_interface_list() -> Result> { +fn netmask_to_cidr(netmask: &Option) -> Result { + match netmask { + None => Ok(24), + Some(mask) => { + let mask_parsed: Ipv4Addr = mask.parse()?; + let octects = mask_parsed.octets(); + if octects[0] != 255 { + match octects[0] { + 128 => Ok(1), + 192 => Ok(2), + 224 => Ok(3), + 240 => Ok(4), + 248 => Ok(5), + 252 => Ok(6), + 254 => Ok(7), + _ => bail!("invalid netmask: {}", mask) + } + } else if octects[1] != 255 { + match octects[1] { + 0 => Ok(8), + 128 => Ok(9), + 192 => Ok(10), + 224 => Ok(11), + 240 => Ok(12), + 248 => Ok(13), + 252 => Ok(14), + 254 => Ok(15), + _ => bail!("invalid netmask: {}", mask) + } + } else if octects[2] != 255 { + match octects[2] { + 0 => Ok(16), + 128 => Ok(17), + 192 => Ok(18), + 224 => Ok(19), + 240 => Ok(20), + 248 => Ok(21), + 252 => Ok(22), + 254 => Ok(23), + _ => bail!("invalid netmask: {}", mask) + } + } else if octects[3] != 255 { + match octects[3] { + 0 => Ok(24), + 128 => Ok(25), + 192 => Ok(26), + 224 => Ok(27), + 240 => Ok(28), + 248 => Ok(29), + 252 => Ok(30), + 254 => Ok(31), + _ => bail!("invalid netmask: {}", mask) + } + } else { + Ok(32) + } + } + } +} + +fn ipadm_interface_list() -> Result, failure::Error> { let output = Command::new(IPADM) .env_clear() .arg("show-if") @@ -673,7 +734,7 @@ struct IpadmAddress { cidr: String, } -fn ipadm_address_list() -> Result> { +fn ipadm_address_list() -> Result, failure::Error> { let output = Command::new(IPADM) .env_clear() .arg("show-addr") @@ -720,7 +781,7 @@ fn mac_sanitise(input: &str) -> String { mac } -fn dladm_ether_list() -> Result> { +fn dladm_ether_list() -> Result, failure::Error> { let output = Command::new(DLADM) .env_clear() .arg("show-ether") @@ -736,7 +797,7 @@ fn dladm_ether_list() -> Result> { Ok(ents.iter().map(|l| l[0].trim().to_string()).collect()) } -fn mac_to_nic(mac: &str) -> Result> { +fn mac_to_nic(mac: &str) -> Result, failure::Error> { let output = Command::new(DLADM) .env_clear() .arg("show-phys") @@ -768,7 +829,7 @@ fn mac_to_nic(mac: &str) -> Result> { } } -fn memsize() -> Result { +fn memsize() -> Result { let output = std::process::Command::new(PRTCONF) .env_clear() .arg("-m") @@ -781,7 +842,7 @@ fn memsize() -> Result { Ok(String::from_utf8(output.stdout)?.trim().parse()?) } -fn create_zvol(name: &str, size_mib: u64) -> Result<()> { +fn create_zvol(name: &str, size_mib: u64) -> Result<(), failure::Error> { let output = std::process::Command::new(ZFS) .env_clear() .arg("create") @@ -796,7 +857,7 @@ fn create_zvol(name: &str, size_mib: u64) -> Result<()> { Ok(()) } -fn exists_zvol(name: &str) -> Result { +fn exists_zvol(name: &str) -> Result { let output = std::process::Command::new(ZFS) .env_clear() .arg("list") @@ -827,7 +888,7 @@ fn exists_zvol(name: &str) -> Result { Ok(false) } -fn swapadd() -> Result<()> { +fn swapadd() -> Result<(), failure::Error> { let output = std::process::Command::new(SWAPADD) .env_clear() .output()?; @@ -839,7 +900,113 @@ fn swapadd() -> Result<()> { Ok(()) } -fn ensure_ipadm_interface(log: &Logger, n: &str) -> Result { +fn ensure_interface_name(log: &Logger, name: &str, mac_addres: &str) -> Result<(), failure::Error> { + // First lets get the nic and check if we need to run. + let nic = mac_to_nic(mac_addres)?; + match nic { + Some(iface) => { + info!(log, "interface {} with mac {} found", &iface, mac_addres); + // Rename the link if the current name is not what we expect + if &iface != name { + info!(log, "renaming link {} -> {}", &iface, name); + let output = Command::new(DLADM) + .env_clear() + .arg("rename-link") + .arg(iface) + .arg(name) + .output()?; + if !output.status.success() { + bail!("dladm rename-link returned an error: {}", output.info()); + } + } + } + None => { + bail!("could not find a nic with mac {}", mac_addres); + } + }; + + Ok(()) +} + +fn ensure_ipadm_subnets_config(log: &Logger, link_name: &str, subnets: &Vec) -> Result<(), failure::Error> { + info!(log, "configuring subnets for link {}", link_name); + for subnet in subnets { + if let Err(e) = ensure_ipadm_single_subnet_config(log, link_name, subnet) { + error!(log, "error while configuring link {}: {}", link_name, e) + } + } + + Ok(()) +} + +fn ensure_ipadm_single_subnet_config(log: &Logger, link_name: &str, subnet: &NetworkDataV1Subnet) -> Result<(), failure::Error> { + match subnet { + NetworkDataV1Subnet::Dhcp4 | NetworkDataV1Subnet::Dhcp => { + ensure_ipv4_interface_dhcp(log, "dhcp4", link_name) + } + NetworkDataV1Subnet::Dhcp6 | NetworkDataV1Subnet::Dhcpv6Stateful => { + bail!("ipv6 is not implemented yet") + } + NetworkDataV1Subnet::Static(conf) => { + if let Some(_) = &conf.netmask { + let address = format!("{}/{}", conf.address, netmask_to_cidr(&conf.netmask)?); + ensure_ipv4_interface(log, "ipv4", None, Some(link_name), &address)?; + } else { + let address = if !conf.address.contains("/") { + format!("{}/24", conf.address) + } else { + conf.address.clone() + }; + ensure_ipv4_interface(log, "ipv4", None, Some(link_name), &address)?; + } + + if let Some(gw) = &conf.gateway { + ensure_ipv4_gateway(log, gw)?; + } + + if let Some(dns_servers) = &conf.dns_nameservers { + ensure_dns_nameservers(log, dns_servers)?; + } + + if let Some(dns_search) = &conf.dns_search { + ensure_dns_search(log, dns_search)?; + } + + if let Some(routes) = &conf.routes { + error!(log, "route configuration not yet wupported, cannot apply {:#?} to link {}", routes, link_name); + } + + Ok(()) + } + NetworkDataV1Subnet::Static6(_) => { + bail!("ipv6 is not implemented yet") + } + NetworkDataV1Subnet::Dhcpv6Stateless => { + bail!("ipv6 is not implemented yet") + } + NetworkDataV1Subnet::SLAAC { .. } => { + bail!("ipv6 is not implemented yet") + } + } +} + +fn ensure_interface_mtu(log: &Logger, name: &str, mtu: &i32) -> Result<(), failure::Error> { + info!(log, "setting mtu of interface {} to {}", name, mtu); + let output = Command::new(DLADM) + .env_clear() + .arg("set-linkprop") + .arg("-p") + .arg(format!("mtu={}", mtu)) + .arg(name) + .output()?; + if !output.status.success() { + bail!("dladm setting mtu failed: {}", output.info()); + } + + Ok(()) +} + +fn ensure_ipadm_interface(log: &Logger, n: &str) -> Result { info!(log, "ENSURE INTERFACE: {}", n); let ifaces = ipadm_interface_list()?; @@ -864,7 +1031,7 @@ fn ensure_ipadm_interface(log: &Logger, n: &str) -> Result { } } -fn ensure_ipv4_gateway(log: &Logger, gateway: &str) -> Result<()> { +fn ensure_ipv4_gateway(log: &Logger, gateway: &str) -> Result<(), failure::Error> { info!(log, "ENSURE IPv4 GATEWAY: {}", gateway); let orig_defrouters = read_lines_maybe(DEFROUTER)?; @@ -898,7 +1065,7 @@ fn ensure_ipv4_gateway(log: &Logger, gateway: &str) -> Result<()> { } fn ensure_ipv4_interface_dhcp(log: &Logger, sfx: &str, n: &str) - -> Result<()> + -> Result<(), failure::Error> { info!(log, "ENSURE IPv4 DHCP INTERFACE: {}", n); @@ -976,22 +1143,31 @@ fn ensure_ipv4_interface_dhcp(log: &Logger, sfx: &str, n: &str) } } -fn ensure_ipv4_interface(log: &Logger, sfx: &str, mac: &str, ipv4: &str) - -> Result<()> +fn ensure_ipv4_interface(log: &Logger, sfx: &str, mac_option: Option<&str>, link_name: Option<&str>, ipv4: &str) + -> Result<(), failure::Error> { - info!(log, "ENSURE IPv4 INTERFACE: {}, {:?}", mac, ipv4); - let n = match mac_to_nic(mac)? { - None => bail!("MAC address {} not found", mac), - Some(n) => n, + let found_nic = if let Some(mac) = mac_option { + match mac_to_nic(mac)? { + None => bail!("MAC address {} not found", mac), + Some(n) => { + info!(log, "MAC address {} is NIC {}", mac, n); + n + }, + } + } else if let Some(name) = link_name { + String::from(name) + } else { + panic!("programmer error: either link_name or mac_address must be passed to this function got none of both"); }; - info!(log, "MAC address {} is NIC {}", mac, n); - ensure_ipadm_interface(log, &n)?; + info!(log, "ENSURE IPv4 INTERFACE: {}, {:?}", found_nic, ipv4); + + ensure_ipadm_interface(log, &found_nic)?; info!(log, "target IP address: {}", ipv4); - let targname = format!("{}/{}", n, sfx); + let targname = format!("{}/{}", found_nic, sfx); info!(log, "target IP name: {}", targname); let addrs = ipadm_address_list()?; @@ -1039,7 +1215,7 @@ fn ensure_ipv4_interface(log: &Logger, sfx: &str, mac: &str, ipv4: &str) } } - info!(log, "ok, interface {} address {} ({}) complete", n, ipv4, sfx); + info!(log, "ok, interface {} address {} ({}) complete", found_nic, ipv4, sfx); Ok(()) } @@ -1058,7 +1234,7 @@ fn main() { } } -fn run(log: &Logger) -> Result<()> { +fn run(log: &Logger) -> Result<(), failure::Error> { /* * This program could be destructive if run in the wrong place. Try to * ensure it has at least been installed as an SMF service: @@ -1107,13 +1283,13 @@ fn run(log: &Logger) -> Result<()> { run_amazon(log)?; return Ok(()); } - ("OmniOS", "OmniOS HVM") => { - info!(log, "hypervisor type: OmniOS BHYVE (from SMBIOS)"); + ("OmniOS", "OmniOS HVM") | ("OpenIndiana", "OpenIndiana HVM") => { + info!(log, "hypervisor type: illumos BHYVE (from SMBIOS)"); /* * Skip networking under OmniOS for now, until we figure out the * appropriate strategy for configuring networking in the guest. */ - run_generic(log, &smbios.uuid, false)?; + run_illumos(log, &smbios.uuid)?; return Ok(()); } ("QEMU", _) => { @@ -1152,7 +1328,7 @@ fn run(log: &Logger) -> Result<()> { Ok(()) } -fn run_generic(log: &Logger, smbios_uuid: &str, network: bool) -> Result<()> { +fn run_generic(log: &Logger, smbios_uuid: &str, network: bool) -> Result<(), failure::Error> { /* * Load our stamp file to see if the Guest UUID has changed. */ @@ -1242,7 +1418,7 @@ fn run_generic(log: &Logger, smbios_uuid: &str, network: bool) -> Result<()> { */ let keys = format!("{}/authorized_keys", UNPACKDIR); if let Some(keys) = read_lines(&keys)? { - phase_pubkeys(log, keys.as_slice())?; + ensure_pubkeys(log, "root",keys.as_slice())?; } /* @@ -1258,7 +1434,227 @@ fn run_generic(log: &Logger, smbios_uuid: &str, network: bool) -> Result<()> { Ok(()) } -fn run_amazon(log: &Logger) -> Result<()> { +#[derive(Default)] +struct SMBIOSDatasource { + uuid: String, + datasource: String, + seed_from: String, + local_hostname: String, +} + +fn parse_smbios_datasource_string(raw_string: &str) -> Result { + let mut ds = SMBIOSDatasource::default(); + for part in raw_string.split(";") { + let key_val: Vec<&str> = part.split("=").collect(); + match key_val[0] { + "ds" => ds.datasource = String::from(key_val[1]), + "i" | "instance-id" => ds.uuid = String::from(key_val[1]), + "s" | "seedfrom" => ds.seed_from = String::from(key_val[1]), + "h" | "local-hostname" => ds.local_hostname = String::from(key_val[1]), + _ => {} + } + } + + Ok(ds) +} + +fn ensure_network_config(log: &Logger, config: &userdata::networkconfig::NetworkConfig) -> Result<(), failure::Error> { + let net_config = match config { + userdata::networkconfig::NetworkConfig::V1(c) => &c.config, + userdata::networkconfig::NetworkConfig::V2(_) => { + error!(log, "Network config v2 not supported yet"); + bail!("Network Config v2 not supported"); + } + }; + + // First configure the Physical Interfaces + for iface_config in net_config { + match iface_config { + NetworkDataV1Iface::Physical { name, mac_address: mac_address_option, mtu: mtu_option, subnets: subnet_option } => { + // First we make sure the Interface is named correctly + // if we have a mac address set. Thus making + // the optional mac address attribute the + // attribute to identify the nic + // not providing a mac address thus results in the name + // being the identifying attribute + if let Some(mac_addr) = mac_address_option { + info!(log, "ensuring nic with mac {} is named {}", mac_addr, name); + ensure_interface_name(log, name, &mac_addr)?; + } + + if let Some(mtu) = mtu_option { + ensure_interface_mtu(log, name, mtu)?; + } + + if let Some(subnets) = subnet_option { + ensure_ipadm_subnets_config(log, name, subnets)?; + } + } + _ => {} + } + } + + // On the Second pass configure the combined ineterfaces (Bond, Bridge, VLAN...) + for iface_config in net_config { + match iface_config { + NetworkDataV1Iface::Bond { .. } => { + bail!("bonds not yet supported") + } + NetworkDataV1Iface::Bridge { .. } => { + bail!("bridges not yet supported") + } + NetworkDataV1Iface::Vlan { .. } => { + bail!("vlans not yet supported") + } + _ => {} + } + } + + //Configure Routes + for iface_config in net_config { + match iface_config { + NetworkDataV1Iface::Route {..} => { + bail!("routes not yet supported") + } + _ => {} + } + } + + //Finaly configure nameservers + for iface_config in net_config { + match iface_config { + NetworkDataV1Iface::Nameserver { address, search, .. } => { + ensure_dns_nameservers(log, address)?; + ensure_dns_search(log, search)?; + } + _ => {} + } + } + + Ok(()) +} + +fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<(), failure::Error> { + /* + * Parse any datasource definition from smbios_uuid field, which by cloud-init standard is + * not just the uuid + */ + let ds = parse_smbios_datasource_string(smbios_raw_string)?; + + /* + * Load our stamp file to see if the Guest UUID has changed. + */ + if let Some([id]) = read_lines(STAMP)?.as_deref() { + if id.trim() == ds.uuid { + info!(log, "this guest has already completed first \ + boot processing, halting"); + return Ok(()); + } else { + info!(log, "guest UUID changed ({} -> {}), reprocessing", + id.trim(), ds.uuid); + } + } + + phase_reguid_zpool(log)?; + + /* + * The meta-data and user-data can be provided via a vfat (pcfs) or iso9660 (hsfs) + * + */ + info!(log, "searching for cidata device with metadata..."); + let devs_opt = find_cidata_devices(log)?; + if let Some(devs) = devs_opt { + for dev in devs { + info!(log, "mounting cidata device {} to {}", dev.path, UNPACKDIR); + ensure_dir(log, UNPACKDIR)?; + let mount = Command::new(MOUNT) + .arg("-F").arg(&dev.fstype) + .arg(&dev.path) + .arg(UNPACKDIR) + .env_clear() + .output()?; + + if !mount.status.success() { + bail!("mount failure: {}", mount.info()); + } + + info!(log, "ok, disk mounted"); + + } + } else { + bail!("could not find a cidata device bailing") + } + + let dir_buf = PathBuf::from(UNPACKDIR); + + let user_data = read_user_data(log, &dir_buf.join("user-data"))?; + + let meta_data_file = File::open(&dir_buf.join("meta-data"))?; + let meta_data = serde_yaml::from_reader::(meta_data_file)?; + + + /* + * Get a system hostname from the metadata, if provided. Make sure to set + * this before engaging DHCP, so that "virsh net-dhcp-leases default" can + * display the hostname in the lease record instead of "unknown". + */ + phase_set_hostname(log, meta_data.get_hostname())?; + + let network_config_result = userdata::networkconfig::parse_network_config( &dir_buf.join("network-config")); + // Apply network config if we have one. + if network_config_result.is_ok() { + ensure_network_config(&log, &network_config_result?)?; + } else { + /* + * If we have no network config try DHCP. Virtio interfaces are + * preferred. + */ + let ifaces = dladm_ether_list()?; + let mut chosen = None; + info!(log, "found these ethernet interfaces: {:?}", ifaces); + /* + * Prefer Virtio devices: + */ + for iface in ifaces.iter() { + if iface.starts_with("vioif") { + chosen = Some(iface.as_str()); + break; + } + } + /* + * Otherwise, use whatever we have: + */ + if chosen.is_none() { + chosen = ifaces.iter().next().map(|x| x.as_str()); + } + + if let Some(chosen) = chosen { + info!(log, "chose interface {}", chosen); + ensure_ipv4_interface_dhcp(log, "dhcp", chosen)?; + } else { + bail!("could not find an appropriate Ethernet interface!"); + } + } + + /* + * User data phase + */ + phase_user_data(log, &user_data)?; + + + for script in user_data.scripts { + /* + * handle the userscripts we have in the user-data: + */ + phase_userscript(log, &script)?; + } + + write_lines(log, STAMP, &[ds.uuid])?; + + Ok(()) +} + +fn run_amazon(log: &Logger) -> Result<(), failure::Error> { /* * Sadly, Amazon has no mechanism for metadata access that does not require * a correctly configured IP interface. In addition, the available NIC @@ -1345,7 +1741,7 @@ fn run_amazon(log: &Logger) -> Result<()> { */ if let Some(pk) = amazon_metadata_get(log, "public-keys/0/openssh-key")? { let pubkeys = vec![pk]; - phase_pubkeys(log, &pubkeys)?; + ensure_pubkeys(log, "root",&pubkeys)?; } else { warn!(log, "no SSH public key?"); } @@ -1365,7 +1761,7 @@ fn run_amazon(log: &Logger) -> Result<()> { Ok(()) } -fn run_smartos(log: &Logger) -> Result<()> { +fn run_smartos(log: &Logger) -> Result<(), failure::Error> { let uuid = if let Mdata::Found(uuid) = mdata_get(log, "sdc:uuid")? { uuid.trim().to_string() } else { @@ -1422,8 +1818,8 @@ fn run_smartos(log: &Logger) -> Result<()> { let sfx = format!("ip{}", i); - if let Err(e) = ensure_ipv4_interface(log, &sfx, &nic.mac, - &ip) + if let Err(e) = ensure_ipv4_interface(log, &sfx, Some(&nic.mac), + None, &ip) { error!(log, "IFACE {}/{} ERROR: {}", nic.interface, sfx, e); } @@ -1445,7 +1841,7 @@ fn run_smartos(log: &Logger) -> Result<()> { if let Mdata::Found(resolvers) = mdata_get(log, "sdc:resolvers")? { let resolvers: Vec = serde_json::from_str(&resolvers)?; - phase_dns(log, &resolvers)?; + ensure_dns_nameservers(log, &resolvers)?; } /* @@ -1456,7 +1852,7 @@ fn run_smartos(log: &Logger) -> Result<()> { .map(|s| s.trim().to_string()) .collect(); - phase_pubkeys(log, &pubkeys)?; + ensure_pubkeys(log, "root",&pubkeys)?; } /* @@ -1472,7 +1868,7 @@ fn run_smartos(log: &Logger) -> Result<()> { Ok(()) } -fn run_digitalocean(log: &Logger) -> Result<()> { +fn run_digitalocean(log: &Logger) -> Result<(), failure::Error> { /* * First, locate and mount the metadata ISO. We need to load the droplet ID * so that we can determine if we have completed first boot processing for @@ -1526,7 +1922,7 @@ fn run_digitalocean(log: &Logger) -> Result<()> { /* * Read metadata from the file system: */ - let md: Option = read_json( + let md: Option = read_json( &format!("{}/digitalocean_meta_data.json", MOUNTPOINT))?; let md = if let Some(md) = md { @@ -1564,8 +1960,8 @@ fn run_digitalocean(log: &Logger) -> Result<()> { continue; } - if let Err(e) = ensure_ipv4_interface(log, "private", &iface.mac, - &iface.ipv4.cidr()?) + if let Err(e) = ensure_ipv4_interface(log, "private", Some(&iface.mac), + None, &iface.ipv4.cidr()?) { /* * Report the error, but drive on in case we can complete other @@ -1580,8 +1976,8 @@ fn run_digitalocean(log: &Logger) -> Result<()> { continue; } - if let Err(e) = ensure_ipv4_interface(log, "public", &iface.mac, - &iface.ipv4.cidr()?) + if let Err(e) = ensure_ipv4_interface(log, "public", Some(&iface.mac), + None, &iface.ipv4.cidr()?) { /* * Report the error, but drive on in case we can complete other @@ -1595,16 +1991,16 @@ fn run_digitalocean(log: &Logger) -> Result<()> { } if let Some(anchor) = &iface.anchor_ipv4 { - if let Err(e) = ensure_ipv4_interface(log, "anchor", &iface.mac, - &anchor.cidr()?) + if let Err(e) = ensure_ipv4_interface(log, "anchor", Some(&iface.mac), + None, &anchor.cidr()?) { error!(log, "ANCHOR IFACE ERROR: {}", e); } } } - phase_dns(log, &md.dns.nameservers)?; - phase_pubkeys(log, md.public_keys.as_slice())?; + ensure_dns_nameservers(log, &md.dns.nameservers)?; + ensure_pubkeys(log, "root",md.public_keys.as_slice())?; /* * Get userscript: @@ -1620,13 +2016,13 @@ fn run_digitalocean(log: &Logger) -> Result<()> { Ok(()) } -fn phase_reguid_zpool(log: &Logger) -> Result<()> { +fn phase_reguid_zpool(log: &Logger) -> Result<(), failure::Error> { info!(log, "regenerate pool guid for rpool"); zpool::zpool_reguid("rpool")?; Ok(()) } -fn phase_expand_zpool(log: &Logger) -> Result<()> { +fn phase_expand_zpool(log: &Logger) -> Result<(), failure::Error> { /* * NOTE: Though it might seem like we could skip directly to using "zpool * online -e ...", there appears to be at least one serious deadlock in this @@ -1655,7 +2051,7 @@ fn phase_expand_zpool(log: &Logger) -> Result<()> { Ok(()) } -fn phase_add_swap(log: &Logger) -> Result<()> { +fn phase_add_swap(log: &Logger) -> Result<(), failure::Error> { /* * Next, add a swap device. Ideally we will have enough room for a swap * file twice the size of physical memory -- but if not, we want to use at @@ -1698,7 +2094,7 @@ fn phase_add_swap(log: &Logger) -> Result<()> { Ok(()) } -fn phase_set_hostname(log: &Logger, hostname: &str) -> Result<()> { +fn phase_set_hostname(log: &Logger, hostname: &str) -> Result<(), failure::Error> { /* * Check nodename: */ @@ -1786,7 +2182,176 @@ fn phase_set_hostname(log: &Logger, hostname: &str) -> Result<()> { Ok(()) } -fn phase_dns(log: &Logger, nameservers: &[String]) -> Result<()> { +fn phase_user_data(log: &Logger, user_data: &UserData) -> Result<(), failure::Error> { + + //First Apply cloud configurations + for cc in user_data.cloud_configs.clone() { + /* + * First Apply the groups + */ + if let Some(groups) = cc.groups { + for group in groups { + ensure_group(group)?; + } + } + + for user in cc.users { + ensure_user(log, &user)?; + } + + if let Some(files) = cc.write_files { + for file in files { + ensure_write_file(log, &file)?; + } + } + + /* + if let Some(ca_certs) = cc.ca_certs { + + } + */ + + if let Some(packages) = cc.packages { + match packages { + Multiformat::String(pkg) => { + ensure_packages(&log, vec![pkg])?; + } + Multiformat::List(pkgs) => { + ensure_packages(&log, pkgs)?; + } + _ => {} + } + } + + /* + * Set general ssh Authorized keys + */ + if let Some(keys) = cc.ssh_authorized_keys { + ensure_pubkeys(log, "root", &keys)?; + } + } + + //Then run the scripts + + Ok(()) +} + +fn ensure_packages(log: &&Logger, pkgs: Vec) -> Result<(), failure::Error> { + info!(log, "installing packages {:#?}", pkgs); + let mut pkg_cmd = Command::new(PKG); + pkg_cmd.env_clear(); + pkg_cmd.arg("install"); + pkg_cmd.arg("-v"); + + for pkg in pkgs { + pkg_cmd.arg(pkg); + } + + let mut child = pkg_cmd.spawn()?; + let status = child.wait()?; + if !status.success() { + bail!("failed package installation see log messages above") + } + + info!(log, "packages installed"); + Ok(()) +} + +fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<(), failure::Error> { + info!(log, "creating file {}", file.path); + let f = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&file.path)?; + let mut w = std::io::BufWriter::new(&f); + + match file.encoding { + WriteFileEncoding::None => { + trace!(log, "writing string data"); + w.write_all(file.content.as_bytes())?; + } + WriteFileEncoding::B64 => { + trace!(log, "writing base64 encoded data"); + w.write_all(base64Decode(&file.content)?.as_slice())?; + } + WriteFileEncoding::Gzip => { + trace!(log, "writing gzip encoded data"); + let mut content_clone = file.content.clone(); + let mut conent_bytes = content_clone.as_str().as_bytes(); + let mut d = GzDecoder::new(BufReader::new(&mut conent_bytes)); + IOCopy(&mut d, &mut w)?; + } + WriteFileEncoding::B64Gzip => { + trace!(log, "writing gzipped base64 encoded data"); + let decoded_content = base64Decode(&file.content)?; + let mut d = GzDecoder::new(decoded_content.as_slice()); + IOCopy(&mut d, &mut w)?; + } + } + + if let Some(mode_string) = &file.permissions { + info!(log, "setting permissions of file {} to {}", file.path, mode_string); + let meta = &f.metadata()?; + let mut perms = meta.permissions(); + perms.set_mode(mode_string.parse::()?); + } + + if let Some(owner) = &file.owner { + info!(log, "setting owner and group of file {} to {}", file.path, owner); + let mut uid: uid_t; + let mut gid: gid_t; + if owner.contains(":") { + if let Some((u, g)) = owner.split_once(":") { + if let Some(user) = users::get_user_by_name(u) { + uid = user.uid(); + } else { + bail!("could not find user {} in system", u) + } + + if let Some(group) = users::get_group_by_name(g) { + gid = group.gid(); + } else { + bail!("could not find group {} in system", g) + } + } else { + bail!("wrong user group string invalid config") + } + } else { + let meta = f.metadata()?; + gid = meta.st_gid(); + if let Some(u) = users::get_user_by_name(&owner) { + uid = u.uid(); + } else { + bail!("could not find user {} in system", &owner) + } + } + chown(&file.path, uid, gid, false)?; + } + + Ok(()) +} + +/// Actually perform the change of owner on a path +fn chown>(path: P, uid: uid_t, gid: gid_t, follow: bool) -> IOResult<()> { + let path = path.as_ref(); + let s = CString::new(path.as_os_str().as_bytes()).unwrap(); + let ret = unsafe { + if follow { + libc::chown(s.as_ptr(), uid, gid) + } else { + libc::lchown(s.as_ptr(), uid, gid) + } + }; + if ret == 0 { + Ok(()) + } else { + Err(IOError::last_os_error()) + } +} + + +fn ensure_dns_nameservers(log: &Logger, nameservers: &[String]) -> Result<(), failure::Error> { /* * DNS Servers: */ @@ -1826,15 +2391,63 @@ fn phase_dns(log: &Logger, nameservers: &[String]) -> Result<()> { Ok(()) } -fn phase_pubkeys(log: &Logger, public_keys: &[String]) -> Result<()> { +fn ensure_dns_search(log: &Logger, dns_search: &[String]) -> Result<(), failure::Error> { + /* + * DNS Servers: + */ + info!(log, "checking DNS search configuration..."); + let lines = read_lines_maybe("/etc/resolv.conf")?; + info!(log, "existing DNS search config lines: {:#?}", &lines); + + let mut dirty = false; + let mut file: Vec = Vec::new(); + + for ns in dns_search.iter() { + let l = format!("search {}", ns); + if !lines.contains(&l) { + info!(log, "ADD DNS Search CONFIG LINE: {}", l); + file.push(l); + dirty = true; + } + } + + for l in &lines { + let ll: Vec<_> = l.splitn(2, ' ').collect(); + if ll.len() == 2 && ll[0] == "search" && + !dns_search.contains(&ll[1].to_string()) + { + info!(log, "REMOVE DNS Search CONFIG LINE: {}", l); + file.push(format!("#{}", l)); + dirty = true; + } else { + file.push(l.to_string()); + } + } + + if dirty { + write_lines(log, "/etc/resolv.conf", file.as_ref())?; + } + + Ok(()) +} + +fn ensure_pubkeys(log: &Logger, user: &str, public_keys: &[String]) -> Result<(), failure::Error> { /* * Manage the public keys: */ - info!(log, "checking SSH public keys..."); + info!(log, "checking SSH public keys for user {}...", user); + + let sshdir = if user == "root" { + format!("/root/.ssh") + } else { + format!("/export/home/{}/.ssh", user) + }; + + ensure_dir(log, &sshdir)?; - ensure_dir(log, "/root/.ssh")?; + let authorized_keys = sshdir + "/authorized_keys"; - let mut file = read_lines_maybe("/root/.ssh/authorized_keys")?; + let mut file = read_lines_maybe(&authorized_keys)?; info!(log, "existing SSH public keys: {:#?}", &file); let mut dirty = false; @@ -1847,13 +2460,13 @@ fn phase_pubkeys(log: &Logger, public_keys: &[String]) -> Result<()> { } if dirty { - write_lines(log, "/root/.ssh/authorized_keys", file.as_ref())?; + write_lines(log, &authorized_keys, file.as_ref())?; } Ok(()) } -fn phase_userscript(log: &Logger, userscript: &str) -> Result<()> { +fn phase_userscript(log: &Logger, userscript: &str) -> Result<(), failure::Error> { /* * If the userscript is basically empty, just ignore it. */ diff --git a/src/public_keys.rs b/src/public_keys.rs new file mode 100644 index 0000000..2630ba7 --- /dev/null +++ b/src/public_keys.rs @@ -0,0 +1,37 @@ +use crate::common::*; +use crate::file::*; + +pub fn ensure_pubkeys(log: &Logger, user: &str, public_keys: &[String]) -> Result<(), failure::Error> { + /* + * Manage the public keys: + */ + info!(log, "checking SSH public keys for user {}...", user); + + let sshdir = if user == "root" { + format!("/root/.ssh") + } else { + format!("/export/home/{}/.ssh", user) + }; + + ensure_dir(log, &sshdir)?; + + let authorized_keys = sshdir + "/authorized_keys"; + + let mut file = read_lines_maybe(&authorized_keys)?; + info!(log, "existing SSH public keys: {:#?}", &file); + + let mut dirty = false; + for key in public_keys.iter() { + if !file.contains(key) { + info!(log, "add SSH public key: {}", key); + file.push(key.to_string()); + dirty = true; + } + } + + if dirty { + write_lines(log, &authorized_keys, file.as_ref())?; + } + + Ok(()) +} \ No newline at end of file diff --git a/src/userdata.rs b/src/userdata.rs new file mode 100644 index 0000000..bdef1a4 --- /dev/null +++ b/src/userdata.rs @@ -0,0 +1,125 @@ +/* + * Copyright 2021 OpenFlowLabs + * + */ +use cloudconfig::CloudConfig; +use std::io::prelude::*; +use flate2::read::GzDecoder; +use std::io::BufReader; +use std::path::PathBuf; +use failure::Error; +use std::fs::File; +use crate::common::*; + +pub mod cloudconfig; +pub mod networkconfig; +pub mod multiformat_deserialize; + +pub fn read_user_data(log: &Logger, path: &PathBuf) -> Result { + // Parse Multipart message from stream + match read_gz_data(log, path) { + Ok(data) => Ok(data), + Err(err) => { + match err.downcast::() { + Ok(uerr) => { + if uerr == UserDataError::NotGzData { + Ok(read_uncompressed(log, path)?) + } else { + Err(uerr)? + } + } + Err(oerr) => Err(oerr) + } + } + } +} + +fn read_gz_data(log: &Logger, path: &PathBuf) -> Result { + let gzreader = GzDecoder::new(File::open(path)?); + if None == gzreader.header() { + return Err(UserDataError::NotGzData)?; + } + + let mut reader = BufReader::new(gzreader); + parse_user_data_multipart_stream::>>(log, &mut reader) +} + +fn read_uncompressed(log: &Logger, path: &PathBuf) -> Result { + let mut reader = BufReader::new(File::open(path)?); + parse_user_data_multipart_stream::>(log, &mut reader) +} + +fn parse_user_data_multipart_stream(log: &Logger, stream: &mut S) -> Result { + + let mut buf = Vec::new(); + stream.read_to_end(&mut buf)?; + + let mail = mailparse::parse_mail(buf.as_slice())?; + let mut data = UserData::default(); + for part in &mail.subparts { + let body = part.get_body()?; + parse_file_part(log, &mut data, part.ctype.mimetype.as_str(), &body)?; + } + + let body = mail.get_body()?; + parse_file_part(log, &mut data, &mail.headers[0].get_key(), &body)?; + + Ok(data) +} + +fn parse_file_part(log: &Logger, d: &mut UserData, mime_type: &str, buf: &str) -> Result<(), Error> { + match mime_type { + "text/cloud-config" | "#cloud-config" => { + let cc = serde_yaml::from_str::(buf)?; + d.cloud_configs.push(cc); + } + "text/x-shellscript" => { + d.scripts.push(buf.into()) + } + _ => { + info!(log, "unsupported mime type {}, skipping", mime_type); + } + } + + Ok(()) +} + +#[derive(Debug, Fail, PartialEq)] +enum UserDataError { + #[fail(display = "format of file {} is not parseable", path)] + InvalidFile{ + path: String, + }, + #[fail(display = "file is not compressed")] + NotGzData, +} + +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct UserData { + pub cloud_configs: Vec, + pub scripts: Vec, +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + use std::str::FromStr; + use crate::userdata::UserData; + use crate::common::init_log; + + #[test] + fn multipart_parse() { + let log = init_log(); + let res = crate::userdata::read_user_data(&log, &PathBuf::from_str("./sample_data/mime_message.txt").unwrap()); + let udata = res.unwrap(); + assert_ne!(udata, UserData::default()); + } + + #[test] + fn userdata_parse() { + let log = init_log(); + let res = crate::userdata::read_user_data(&log, &PathBuf::from_str("./sample_data/user-data").unwrap()); + let udata = res.unwrap(); + assert_ne!(udata, UserData::default()); + } +} diff --git a/src/userdata/cloudconfig.rs b/src/userdata/cloudconfig.rs new file mode 100644 index 0000000..89da868 --- /dev/null +++ b/src/userdata/cloudconfig.rs @@ -0,0 +1,168 @@ +/* + * Copyright 2021 OpenFlowLabs + * + */ +use std::collections::HashMap; +use serde::{Deserialize}; +use crate::userdata::multiformat_deserialize::Multiformat; + +#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct Metadata { + #[serde(rename = "instance-id")] + pub instance_id: String, + #[serde(rename = "instance-type")] + pub instance_type: Option, + #[serde(rename = "network-interfaces")] + pub network_interfaces: Option, + pub hostname: Option, + #[serde(rename = "local-hostname")] + pub local_hostname: Option, + #[serde(rename = "public-hostname")] + pub public_hostname: Option, + pub placement: Option, +} + +impl Metadata { + pub fn get_hostname(&self) -> &str { + if let Some(name) = &self.public_hostname { + return name; + } + + if let Some(name) = &self.hostname { + return name; + } + + if let Some(name) = &self.local_hostname { + return name; + } + + "" + } + + pub fn get_public_hostname(&self) -> &str { + if let Some(name) = &self.public_hostname { + return name; + } + + "" + } +} + +#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct PlacementData { + #[serde(rename = "availability-zone")] + pub availability_zone: Option, + #[serde(rename = "group-name")] + pub group_name: Option, + #[serde(rename = "partition-number")] + pub partition_number: Option, + pub region: Option, +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct CloudConfig { + pub groups: Option>>>>, + pub users: Vec, + pub write_files: Option>, + #[serde(rename = "ca-certs")] + pub ca_certs: Option, + pub bootcmd: Option, + pub runcms: Option, + pub final_message: Option, + pub packages: Option, + pub package_update: Option, + pub phone_home: Option, + pub growpart: Option, + pub ssh_authorized_keys: Option>, + pub ssh_keys: Option>, + pub no_ssh_fingerprints: Option, + pub ssh: Option>, +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct UserConfig { + pub name: String, + #[serde(rename = "expiredate")] + pub expire_date: Option, + pub gecos: Option, + pub homedir: Option, + pub primary_group: Option, + pub groups: Option>, + pub lock_passwd: Option, + pub inactive: Option, + pub passwd: Option, + pub no_create_home: Option, + pub no_user_group: Option, + pub ssh_authorized_keys: Option>, + pub system: Option, + pub shell: Option, +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub enum GrowPartMode { + #[serde(rename = "auto")] + Auto, + #[serde(rename = "growpart")] + Growpart, + #[serde(rename = "off")] + Off +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct GrowPartData { + pub mode: GrowPartMode, + pub devices: Vec, + pub ignore_growroot_disabled: Option, +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub enum PowerStateMode { + #[serde(rename = "poweroff")] + Poweroff, + #[serde(rename = "halt")] + Halt, + #[serde(rename = "reboot")] + Reboot +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct PowerStateData { + pub delay: String, + pub mode: PowerStateMode, + pub message: String, + pub timeout: String, + // TODO figure out how to parse string or list case + pub condition: String, +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct PhoneHomeData { + pub url: String, + pub post: Vec, + pub tries: i32, +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct CaCertsData { + #[serde(rename = "remove-defaults")] + pub remove_defaults: bool, + pub trusted: Vec, +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub enum WriteFileEncoding { + None, + B64, + Gzip, + B64Gzip +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct WriteFileData { + // TODO Figure out how to deserialize + pub encoding: WriteFileEncoding, + pub content: String, + pub owner: Option, + pub path: String, + pub permissions: Option, +} diff --git a/src/userdata/multiformat_deserialize.rs b/src/userdata/multiformat_deserialize.rs new file mode 100644 index 0000000..9cec0ec --- /dev/null +++ b/src/userdata/multiformat_deserialize.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize}; + +#[derive(Debug, Deserialize, Eq, PartialEq, Clone)] +#[serde(untagged)] +pub enum Multiformat { + String(String), + List(Vec), + Integer(i32), +} \ No newline at end of file diff --git a/src/userdata/networkconfig.rs b/src/userdata/networkconfig.rs new file mode 100644 index 0000000..0388dc0 --- /dev/null +++ b/src/userdata/networkconfig.rs @@ -0,0 +1,834 @@ +use std::fs::File; +use std::path::PathBuf; +use std::collections::HashMap; +use serde::{Deserialize}; +use crate::userdata::multiformat_deserialize::Multiformat; + +pub fn parse_network_config(path: &PathBuf) -> Result { + // Try V1 first + let file = File::open(path)?; + let f = serde_yaml::from_reader::(file)?; + Ok(f.network) +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct NetworkConfigFile { + pub network: NetworkConfig, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +#[serde(tag = "version")] +pub enum NetworkConfig { + #[serde(rename = "1")] + V1(NetworkDataV1), + #[serde(rename = "2")] + V2(NetworkDataV2) +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct NetworkDataV1 { + pub config: Vec, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +#[serde(tag = "type")] +pub enum NetworkDataV1Iface { + #[serde(rename = "physical")] + Physical { name: String, mac_address: Option, mtu: Option, subnets: Option> }, + #[serde(rename = "bond")] + Bond { name: String, mac_address: Option, bond_interfaces: Vec, mtu: Option, params: HashMap, subnets: Option> }, + #[serde(rename = "bridge")] + Bridge { name: String, bridge_interfaces: Vec, params: HashMap, subnets: Option> }, + #[serde(rename = "vlan")] + Vlan { name: String, vlan_link: String, vlan_id: i32, mtu: Option, subnets: Option> }, + #[serde(rename = "nameserver")] + Nameserver { address: Vec, search: Vec, interface: Option }, + #[serde(rename = "route")] + Route { destination: String, gateway: String, metric: i32 } +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +#[serde(tag = "type")] +pub enum NetworkDataV1Subnet { + #[serde(rename = "dhcp4")] + Dhcp4, + #[serde(rename = "dhcp")] + Dhcp, + #[serde(rename = "dhcp6")] + Dhcp6, + #[serde(rename = "static")] + Static(StaticSubnetConfig), + #[serde(rename = "static6")] + Static6(StaticSubnetConfig), + #[serde(rename = "ipv6_dhcpv6-stateful")] + Dhcpv6Stateful, + #[serde(rename = "ipv6_dhcpv6-stateless")] + Dhcpv6Stateless, + #[serde(rename = "ipv6_slaac")] + SLAAC { control: Option, gateway: String, dns_nameservers: Vec, dns_search: Vec, routes: Vec } +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct StaticSubnetConfig { + pub address: String, + pub gateway: Option, + pub netmask: Option, + pub control: Option, + pub dns_nameservers: Option>, + pub dns_search: Option>, + pub routes: Option> +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub enum NetworkDataV1SubnetControl { + #[serde(rename = "manual")] + Manual, + #[serde(rename = "auto")] + Auto, + #[serde(rename = "hotplug")] + Hotplug +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct NetworkDataV1SubnetRoute { + pub gateway: String, + pub netmask: String, + pub network: String, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct NetworkDataV2 { + +} + +#[cfg(test)] +mod tests { + use crate::userdata::networkconfig::{parse_network_config, NetworkConfig, NetworkDataV1Iface, NetworkDataV1Subnet}; + use std::path::PathBuf; + use std::str::FromStr; + use crate::userdata::multiformat_deserialize::Multiformat; + + #[test] + fn parse_physical () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_physical.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + for iface in v1.config { + match iface { + NetworkDataV1Iface::Physical { name, .. } => { + assert_eq!(&name, "eth0"); + } + _ => { + panic!("expected physical network config") + } + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_physical_2 () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_physical_2.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, .. } => { + assert_eq!(name.as_str(), "interface0"); + assert_eq!(mac_address, &Some("00:11:22:33:44:55".to_owned())); + } + _ => { + panic!("expected physical network config") + } + } + match &v1.config[1] { + NetworkDataV1Iface::Physical { name, mac_address, mtu, .. } => { + assert_eq!(name.as_str(), "jumbo0"); + assert_eq!(mac_address, &Some("aa:11:22:33:44:55".to_owned())); + assert_eq!(mtu, &Some(9000)); + } + _ => { + panic!("expected physical network config") + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_bond () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_bond.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, .. } => { + assert_eq!(name.as_str(), "interface0"); + assert_eq!(mac_address, &Some("00:11:22:33:44:55".to_owned())); + } + _ => { + panic!("expected physical network config") + } + } + match &v1.config[3] { + NetworkDataV1Iface::Bond { name, bond_interfaces, params, .. } => { + assert_eq!(name.as_str(), "bond0"); + assert_eq!(bond_interfaces[0].as_str(), "gbe0"); + assert_eq!(bond_interfaces[1].as_str(), "gbe1"); + assert_eq!(params["bond-mode"].as_str(), "active-backup"); + } + _ => { + panic!("expected physical network config") + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_bridge () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_bridge.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, .. } => { + assert_eq!(name.as_str(), "interface0"); + assert_eq!(mac_address, &Some("00:11:22:33:44:55".to_owned())); + } + _ => { + panic!("expected physical network config") + } + } + match &v1.config[2] { + NetworkDataV1Iface::Bridge { name, bridge_interfaces, params, .. } => { + assert_eq!(name.as_str(), "br0"); + assert_eq!(bridge_interfaces[0].as_str(), "jumbo0"); + assert_eq!(params["bridge_ageing"], Multiformat::Integer(250)); + assert_eq!(params["bridge_pathcost"], Multiformat::List(["jumbo0 75".to_owned()].to_vec())); + } + _ => { + panic!("expected physical network config") + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_vlan () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_vlan.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, .. } => { + assert_eq!(name.as_str(), "eth0"); + assert_eq!(mac_address, &Some("c0:d6:9f:2c:e8:80".to_owned())); + } + _ => { + panic!("expected physical network config") + } + } + match &v1.config[1] { + NetworkDataV1Iface::Vlan { name, vlan_link, vlan_id, mtu, .. } => { + assert_eq!(name.as_str(), "eth0.101"); + assert_eq!(vlan_link.as_str(), "eth0"); + assert_eq!(vlan_id, &101); + assert_eq!(mtu, &Some(1500)); + } + not => { + panic!("expected vlan network config, got {:?}", not) + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_nameserver () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_nameserver.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, subnets, .. } => { + assert_eq!(name.as_str(), "interface0"); + assert_eq!(mac_address, &Some("00:11:22:33:44:55".to_owned())); + match subnets { + None => { + panic!("expected a subnet config in physical network config, got {:?}", subnets) + } + Some(nets) => { + let s0 = &nets[0]; + match s0 { + NetworkDataV1Subnet::Static(cfg) => { + assert_eq!(cfg.address.as_str(), "192.168.23.14/27"); + assert_eq!(cfg.gateway, Some("192.168.23.1".to_owned())); + } + not => { + panic!("expected static subnet config, got {:?}", not) + } + } + } + } + } + _ => { + panic!("expected physical network config") + } + } + match &v1.config[1] { + NetworkDataV1Iface::Nameserver { address, search, interface } => { + assert_eq!(address[0].as_str(), "192.168.23.2"); + assert_eq!(address[1].as_str(), "8.8.8.8"); + assert_eq!(search[0].as_str(), "exemplary"); + assert_eq!(interface, &Some("interface0".to_owned())) + } + not => { + panic!("expected nameserver network config, got {:?}", not) + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_route () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_route.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, subnets, .. } => { + assert_eq!(name.as_str(), "interface0"); + assert_eq!(mac_address, &Some("00:11:22:33:44:55".to_owned())); + match subnets { + None => { + panic!("expected a subnet config in physical network config, got {:?}", subnets) + } + Some(nets) => { + let s0 = &nets[0]; + match s0 { + NetworkDataV1Subnet::Static(cfg) => { + assert_eq!(cfg.address.as_str(), "192.168.23.14/24"); + assert_eq!(cfg.gateway, Some("192.168.23.1".to_owned())); + } + not => { + panic!("expected static subnet config, got {:?}", not) + } + } + } + } + } + _ => { + panic!("expected physical network config") + } + } + match &v1.config[1] { + NetworkDataV1Iface::Route { destination, gateway, metric } => { + assert_eq!(destination.as_str(), "192.168.24.0/24"); + assert_eq!(gateway.as_str(), "192.168.24.1"); + assert_eq!(metric, &3); + } + not => { + panic!("expected route network config, got {:?}", not) + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_subnet_dhcp () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_subnet_dhcp.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, subnets, .. } => { + assert_eq!(name.as_str(), "interface0"); + assert_eq!(mac_address, &Some("00:11:22:33:44:55".to_owned())); + match subnets { + None => { + panic!("expected a subnet config in physical network config, got {:?}", subnets) + } + Some(nets) => { + let s0 = &nets[0]; + match s0 { + NetworkDataV1Subnet::Dhcp => {} + not => { + panic!("expected static subnet config, got {:?}", not) + } + } + } + } + } + _ => { + panic!("expected physical network config") + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_subnet_static () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_subnet_static.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, subnets, .. } => { + assert_eq!(name.as_str(), "interface0"); + assert_eq!(mac_address, &Some("00:11:22:33:44:55".to_owned())); + match subnets { + None => { + panic!("expected a subnet config in physical network config, got {:?}", subnets) + } + Some(nets) => { + let s0 = &nets[0]; + match s0 { + NetworkDataV1Subnet::Static(cfg) => { + assert_eq!(cfg.address.as_str(), "192.168.23.14/27"); + assert_eq!(cfg.gateway, Some("192.168.23.1".to_owned())); + match &cfg.dns_nameservers { + None => { + panic!("expecting a dns_nameservers key in testfile") + } + Some(nameservers) => { + assert_eq!(nameservers[0].as_str(), "192.168.23.2"); + assert_eq!(nameservers[1].as_str(), "8.8.8.8"); + } + } + match &cfg.dns_search { + None => { + panic!("expecting a dns_search key in testfile") + } + Some(nameservers) => { + assert_eq!(nameservers[0].as_str(), "exemplary.maas"); + } + } + } + not => { + panic!("expected static subnet config, got {:?}", not) + } + } + } + } + } + _ => { + panic!("expected physical network config") + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_subnet_multiple () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_subnet_multiple.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, subnets, .. } => { + assert_eq!(name.as_str(), "interface0"); + assert_eq!(mac_address, &Some("00:11:22:33:44:55".to_owned())); + match subnets { + None => { + panic!("expected a subnet config in physical network config, got {:?}", subnets) + } + Some(nets) => { + match &nets[0] { + NetworkDataV1Subnet::Dhcp => {}, + not => { + panic!("expected static subnet config, got {:?}", not) + } + } + match &nets[1] { + NetworkDataV1Subnet::Static(cfg) => { + assert_eq!(cfg.address.as_str(), "192.168.23.14/27"); + assert_eq!(cfg.gateway, Some("192.168.23.1".to_owned())); + match &cfg.dns_nameservers { + None => { + panic!("expecting a dns_nameservers key in testfile") + } + Some(nameservers) => { + assert_eq!(nameservers[0].as_str(), "192.168.23.2"); + assert_eq!(nameservers[1].as_str(), "8.8.8.8"); + } + } + match &cfg.dns_search { + None => { + panic!("expecting a dns_search key in testfile") + } + Some(nameservers) => { + assert_eq!(nameservers[0].as_str(), "exemplary"); + } + } + } + not => { + panic!("expected static subnet config, got {:?}", not) + } + } + } + } + } + _ => { + panic!("expected physical network config") + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_subnet_with_routes () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_subnet_with_routes.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, subnets, .. } => { + assert_eq!(name.as_str(), "interface0"); + assert_eq!(mac_address, &Some("00:11:22:33:44:55".to_owned())); + match subnets { + None => { + panic!("expected a subnet config in physical network config, got {:?}", subnets) + } + Some(nets) => { + match &nets[0] { + NetworkDataV1Subnet::Dhcp => {}, + not => { + panic!("expected static subnet config, got {:?}", not) + } + } + match &nets[1] { + NetworkDataV1Subnet::Static(cfg) => { + assert_eq!(cfg.address.as_str(), "10.184.225.122"); + assert_eq!(cfg.netmask, Some("255.255.255.252".to_owned())); + match &cfg.routes { + None => { + panic!("expecting a routes key in testfile") + } + Some(routes) => { + assert_eq!(routes[0].gateway.as_str(), "10.184.225.121"); + assert_eq!(routes[0].netmask.as_str(), "255.240.0.0"); + assert_eq!(routes[0].network.as_str(), "10.176.0.0"); + assert_eq!(routes[1].gateway.as_str(), "10.184.225.121"); + assert_eq!(routes[1].netmask.as_str(), "255.240.0.0"); + assert_eq!(routes[1].network.as_str(), "10.208.0.0"); + } + } + } + not => { + panic!("expected static subnet config, got {:?}", not) + } + } + } + } + } + _ => { + panic!("expected physical network config") + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_subnet_bonded_vlan () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_bonded_vlan.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, .. } => { + assert_eq!(name.as_str(), "gbe0"); + assert_eq!(mac_address, &Some("cd:11:22:33:44:00".to_owned())); + } + not => { + panic!("expected physical network config, got: {:?}", not) + } + } + match &v1.config[1] { + NetworkDataV1Iface::Physical { name, mac_address, .. } => { + assert_eq!(name.as_str(), "gbe1"); + assert_eq!(mac_address, &Some("cd:11:22:33:44:02".to_owned())); + } + not => { + panic!("expected physical network config, got: {:?}", not) + } + } + match &v1.config[2] { + NetworkDataV1Iface::Bond { name, bond_interfaces, params, .. } => { + assert_eq!(name.as_str(), "bond0"); + assert_eq!(bond_interfaces[0].as_str(), "gbe0"); + assert_eq!(bond_interfaces[1].as_str(), "gbe1"); + assert_eq!(params["bond-mode"].as_str(), "802.3ad"); + assert_eq!(params["bond-lacp-rate"].as_str(), "fast"); + } + not => { + panic!("expected bond network config, got: {:?}", not) + } + } + match &v1.config[3] { + NetworkDataV1Iface::Vlan { name, vlan_link, vlan_id, subnets, .. } => { + assert_eq!(name.as_str(), "bond0.200"); + assert_eq!(vlan_link.as_str(), "bond0"); + assert_eq!(vlan_id, &200); + match subnets { + None => { + panic!("expected subnet network config for vlan, got: {:?}", &v1.config[3]) + } + Some(nets) => { + match &nets[0] { + NetworkDataV1Subnet::Dhcp4 => {} + not => { + panic!("expected dhcp4 subnet config, got: {:?}", not) + } + } + } + } + } + not => { + panic!("expected vlan network config, got: {:?}", not) + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } + + #[test] + fn parse_multiple_vlan () -> Result<(), failure::Error> { + let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_multiple_vlan.yaml")?)?; + match cfg { + NetworkConfig::V1(v1) => { + match &v1.config[0] { + NetworkDataV1Iface::Physical { name, mac_address, mtu, subnets, .. } => { + assert_eq!(name.as_str(), "eth0"); + assert_eq!(mac_address, &Some("d4:be:d9:a8:49:13".to_owned())); + assert_eq!(mtu, &Some(1500)); + match subnets { + None => { + panic!("expected subnet network config for eth0, got: {:?}", &v1.config[0]) + } + Some(nets) => { + match &nets[0] { + NetworkDataV1Subnet::Static(st0) => { + assert_eq!(st0.address.as_str(), "10.245.168.16/21"); + assert_eq!(st0.gateway, Some("10.245.168.1".to_owned())); + if let Some(dns) = &st0.dns_nameservers { + assert_eq!(dns[0].as_str(), "10.245.168.2") + } + } + not => { + panic!("expected static subnet config, got: {:?}", not) + } + } + } + } + } + not => { + panic!("expected physical network config, got: {:?}", not) + } + } + match &v1.config[1] { + NetworkDataV1Iface::Physical { name, mac_address, mtu, subnets, .. } => { + assert_eq!(name.as_str(), "eth1"); + assert_eq!(mac_address, &Some("d4:be:d9:a8:49:15".to_owned())); + assert_eq!(mtu, &Some(1500)); + match subnets { + None => { + panic!("expected subnet network config for eth1, got: {:?}", &v1.config[1]) + } + Some(nets) => { + match &nets[0] { + NetworkDataV1Subnet::Static(st0) => { + assert_eq!(st0.address.as_str(), "10.245.188.2/24"); + if let Some(dns) = &st0.dns_nameservers { + assert_eq!(dns.len(), 0); + } + } + not => { + panic!("expected static subnet config, got: {:?}", not) + } + } + } + } + } + not => { + panic!("expected physical network config, got: {:?}", not) + } + } + match &v1.config[2] { + NetworkDataV1Iface::Vlan { name, mtu, vlan_id, vlan_link, subnets, .. } => { + assert_eq!(name.as_str(), "eth1.2667"); + assert_eq!(vlan_link.as_str(), "eth1"); + assert_eq!(mtu, &Some(1500)); + assert_eq!(vlan_id, &2667); + match subnets { + None => { + panic!("expected subnet network config for eth1, got: {:?}", &v1.config[2]) + } + Some(nets) => { + match &nets[0] { + NetworkDataV1Subnet::Static(st0) => { + assert_eq!(st0.address.as_str(), "10.245.184.2/24"); + if let Some(dns) = &st0.dns_nameservers { + assert_eq!(dns.len(), 0); + } + } + not => { + panic!("expected static subnet config, got: {:?}", not) + } + } + } + } + } + not => { + panic!("expected physical network config, got: {:?}", not) + } + } + match &v1.config[3] { + NetworkDataV1Iface::Vlan { name, mtu, vlan_id, vlan_link, subnets, .. } => { + assert_eq!(name.as_str(), "eth1.2668"); + assert_eq!(vlan_link.as_str(), "eth1"); + assert_eq!(mtu, &Some(1500)); + assert_eq!(vlan_id, &2668); + match subnets { + None => { + panic!("expected subnet network config for eth1, got: {:?}", &v1.config[3]) + } + Some(nets) => { + match &nets[0] { + NetworkDataV1Subnet::Static(st0) => { + assert_eq!(st0.address.as_str(), "10.245.185.1/24"); + if let Some(dns) = &st0.dns_nameservers { + assert_eq!(dns.len(), 0); + } + } + not => { + panic!("expected static subnet config, got: {:?}", not) + } + } + } + } + } + not => { + panic!("expected physical network config, got: {:?}", not) + } + } + match &v1.config[4] { + NetworkDataV1Iface::Vlan { name, mtu, vlan_id, vlan_link, subnets, .. } => { + assert_eq!(name.as_str(), "eth1.2669"); + assert_eq!(vlan_link.as_str(), "eth1"); + assert_eq!(mtu, &Some(1500)); + assert_eq!(vlan_id, &2669); + match subnets { + None => { + panic!("expected subnet network config for eth1, got: {:?}", &v1.config[4]) + } + Some(nets) => { + match &nets[0] { + NetworkDataV1Subnet::Static(st0) => { + assert_eq!(st0.address.as_str(), "10.245.186.1/24"); + if let Some(dns) = &st0.dns_nameservers { + assert_eq!(dns.len(), 0); + } + } + not => { + panic!("expected static subnet config, got: {:?}", not) + } + } + } + } + } + not => { + panic!("expected physical network config, got: {:?}", not) + } + } + match &v1.config[5] { + NetworkDataV1Iface::Vlan { name, mtu, vlan_id, vlan_link, subnets, .. } => { + assert_eq!(name.as_str(), "eth1.2670"); + assert_eq!(vlan_link.as_str(), "eth1"); + assert_eq!(mtu, &Some(1500)); + assert_eq!(vlan_id, &2670); + match subnets { + None => { + panic!("expected subnet network config for eth1, got: {:?}", &v1.config[5]) + } + Some(nets) => { + match &nets[0] { + NetworkDataV1Subnet::Static(st0) => { + assert_eq!(st0.address.as_str(), "10.245.187.2/24"); + if let Some(dns) = &st0.dns_nameservers { + assert_eq!(dns.len(), 0); + } + } + not => { + panic!("expected static subnet config, got: {:?}", not) + } + } + } + } + } + not => { + panic!("expected physical network config, got: {:?}", not) + } + } + match &v1.config[6] { + NetworkDataV1Iface::Nameserver { address, search, .. } => { + assert_eq!(address[0].as_str(), "10.245.168.2"); + assert_eq!(search[0].as_str(), "dellstack"); + } + not => { + panic!("expected nameserver network config, got: {:?}", not) + } + } + } + NetworkConfig::V2(_) => { + panic!("expected v1 config got v2") + } + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/users.rs b/src/users.rs new file mode 100644 index 0000000..cb45db6 --- /dev/null +++ b/src/users.rs @@ -0,0 +1,25 @@ +use std::collections::HashMap; +use crate::common::*; +use crate::userdata::cloudconfig::UserConfig; + +#[cfg(feature = "libc_users")] +mod libc; +#[cfg(feature = "libc_users")] +use libc::ensure_group as real_ensure_group; +#[cfg(feature = "libc_users")] +use ffi::ensure_user as real_ensure_user; + +#[cfg(feature = "cmd_users")] +mod ffi; +#[cfg(feature = "cmd_users")] +use ffi::ensure_group as real_ensure_group; +#[cfg(feature = "cmd_users")] +use ffi::ensure_user as real_ensure_user; + +pub fn ensure_group(group: HashMap>>) -> Result<(), failure::Error> { + real_ensure_group(group) +} + +pub fn ensure_user(log: &Logger, user: &UserConfig) -> Result<(), failure::Error> { + real_ensure_user(log, user) +} \ No newline at end of file diff --git a/src/users/ffi.rs b/src/users/ffi.rs new file mode 100644 index 0000000..cd4a161 --- /dev/null +++ b/src/users/ffi.rs @@ -0,0 +1,162 @@ +use std::collections::HashMap; +use crate::common::*; +use std::process::Command; +use crate::userdata::cloudconfig::UserConfig; +use std::ffi::{OsStr, CString}; +use std::os::unix::ffi::OsStrExt; +use std::fmt; +use libc::{c_char, uid_t, gid_t, c_int}; + +const GROUPADD: &str = "/usr/sbin/groupadd"; +const USERMOD: &str = "/usr/sbin/usermod"; +const USERADD: &str = "/usr/sbin/useradd"; +const PASSWD: &str = "/usr/bin/passwd"; + +pub fn ensure_group(group: HashMap>>) -> Result<(), failure::Error> { + +} + +pub fn ensure_user(log: &Logger, user: &UserConfig) -> Result<(), failure::Error> { + +} + + + +/// Information about a particular group. +/// +/// For more information, see the [module documentation](index.html). +#[derive(Clone)] +struct Group { + gid: gid_t, + extras: os::GroupExtras, + pub(crate) name_arc: Arc, +} + +impl Group { + + /// Create a new `Group` with the given group ID and name, with the + /// rest of the fields filled in with dummy values. + /// + /// This method does not actually create a new group on the system — it + /// should only be used for comparing groups in tests. + /// + /// # Examples + /// + /// ``` + /// use users::Group; + /// + /// let group = Group::new(102, "database"); + /// ``` + pub fn new + ?Sized>(gid: gid_t, name: &S) -> Self { + let name_arc = Arc::from(name.as_ref()); + let extras = os::GroupExtras::default(); + + Self { gid, name_arc, extras } + } + + /// Returns this group’s ID. + /// + /// # Examples + /// + /// ``` + /// use users::Group; + /// + /// let group = Group::new(102, "database"); + /// assert_eq!(group.gid(), 102); + /// ``` + pub fn gid(&self) -> gid_t { + self.gid + } + + /// Returns this group’s name. + /// + /// # Examples + /// + /// ``` + /// use std::ffi::OsStr; + /// use users::Group; + /// + /// let group = Group::new(102, "database"); + /// assert_eq!(group.name(), OsStr::new("database")); + /// ``` + pub fn name(&self) -> &OsStr { + &*self.name_arc + } +} + +impl fmt::Debug for Group { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if f.alternate() { + f.debug_struct("Group") + .field("gid", &self.gid) + .field("name_arc", &self.name_arc) + .field("extras", &self.extras) + .finish() + } + else { + write!(f, "Group({}, {})", self.gid(), self.name().to_string_lossy()) + } + } +} + +/// Searches for a `Group` with the given group name in the system’s group database. +/// Returns it if one is found, otherwise returns `None`. +/// +/// # libc functions used +/// +/// - [`getgrnam_r`](https://docs.rs/libc/*/libc/fn.getgrnam_r.html) +/// +/// # Examples +/// +/// ``` +/// use users::get_group_by_name; +/// +/// match get_group_by_name("db-access") { +/// Some(group) => println!("Found group #{}", group.gid()), +/// None => println!("Group not found"), +/// } +/// ``` +pub fn get_group_by_name + ?Sized>(groupname: &S) -> Option { + let groupname = match CString::new(groupname.as_ref().as_bytes()) { + Ok(u) => u, + Err(_) => { + // The groupname that was passed in contained a null character, + // which will match no usernames. + return None; + } + }; + + let mut group = unsafe { mem::zeroed::() }; + let mut buf = vec![0; 2048]; + let mut result = ptr::null_mut::(); + + #[cfg(feature = "logging")] + trace!("Running getgrnam_r for group {:?}", groupname.as_ref()); + + loop { + let r = unsafe { + libc::getgrnam_r(groupname.as_ptr(), &mut group, buf.as_mut_ptr(), buf.len(), &mut result) + }; + + if r != libc::ERANGE { + break; + } + + let newsize = buf.len().checked_mul(2)?; + buf.resize(newsize, 0); + } + + if result.is_null() { + // There is no such group, or an error has occurred. + // errno gets set if there’s an error. + return None; + } + + if result != &mut group { + // The result of getgrnam_r should be its input struct. + return None; + } + + let group = unsafe { struct_to_group(result.read()) }; + Some(group) +} \ No newline at end of file diff --git a/src/users/libc.rs b/src/users/libc.rs new file mode 100644 index 0000000..d74b4c7 --- /dev/null +++ b/src/users/libc.rs @@ -0,0 +1,126 @@ +use std::collections::HashMap; +use crate::common::*; +use std::process::Command; +use users::{get_group_by_name, get_user_by_name, get_user_groups}; +use crate::userdata::cloudconfig::UserConfig; +use crate::public_keys::ensure_pubkeys; + +const GROUPADD: &str = "/usr/sbin/groupadd"; +const USERMOD: &str = "/usr/sbin/usermod"; +const USERADD: &str = "/usr/sbin/useradd"; +const PASSWD: &str = "/usr/bin/passwd"; + +pub fn ensure_group(group: HashMap>>) -> Result<(), failure::Error> { + if let Some(group_name) = group.keys().next(){ + let system_group = get_group_by_name(&group_name); + match system_group { + None => { + let output = Command::new(GROUPADD) + .env_clear() + .arg(&group_name) + .output()?; + + if !output.status.success() { + bail!("could not create group {}: {}", group_name, output.info()) + } + } + _ => {} + } + + if let Some(members) = group[group_name].clone() { + for member in members { + let user_option = get_user_by_name(&member); + if let Some(user) = user_option { + let groups = get_user_groups(user.name(), user.primary_group_id()); + + if let Some(grps) = groups { + let mut current_groups: Vec = grps.iter().map(|g| { String::from(g.name().to_string_lossy()) }).collect(); + current_groups.push(group_name.to_string()); + let new_groups_string: String = current_groups.join(","); + let output = Command::new(USERMOD) + .env_clear() + .arg("-G") + .arg(new_groups_string) + .arg(&member) + .output()?; + if !output.status.success() { + bail!("failed to set groups of user {}: {}", member, output.info()) + } + } + } + + } + } + } else { + bail!("no key in groups configuration error: we have {:#?} in the config", group) + } + + + Ok(()) +} + +pub fn ensure_user(log: &Logger, user: &UserConfig) -> Result<(), failure::Error> { + if let None = users::get_user_by_name(&user.name) { + let mut useradd = Command::new(USERADD); + useradd.env_clear(); + if let Some(d) = &user.expire_date { + useradd.arg("-e"); + useradd.arg(d); + } + + if let Some(g) = &user.gecos { + useradd.arg("-c"); + useradd.arg(g); + } + + if let Some(h) = &user.homedir { + useradd.arg("-d"); + useradd.arg(h); + } + + if let Some(g) = &user.primary_group { + useradd.arg("-g"); + useradd.arg(g); + } + + if let Some(gs) = &user.groups { + useradd.arg("-G"); + useradd.arg(gs.join(",")); + } + + if user.system == None && user.no_create_home == None { + useradd.arg("-m"); + useradd.arg("-z"); + } + + if let Some(s) = &user.shell { + useradd.arg("-s"); + useradd.arg(s); + } + + useradd.arg(&user.name); + + let output = useradd.output()?; + + if !output.status.success() { + bail!("failed to run useradd for {}", user.name) + } + } else { + info!(log, "user {} exists skipping", user.name) + } + + if let Some(keys) = &user.ssh_authorized_keys { + ensure_pubkeys(log, &user.name, keys)?; + } + + let output = Command::new(PASSWD) + .env_clear() + .arg("-N") + .arg(&user.name) + .output()?; + if !output.status.success() { + bail!("failed to run passwd for {}", user.name) + } + + Ok(()) +} \ No newline at end of file diff --git a/src/zpool.rs b/src/zpool.rs index b64479d..f49d084 100644 --- a/src/zpool.rs +++ b/src/zpool.rs @@ -8,7 +8,7 @@ use std::io::Write; use super::common::*; pub fn fmthard(log: &Logger, disk: &str, part: &str, tag: &str, flag: &str, - start: u64, size: u64) -> Result<()> + start: u64, size: u64) -> Result<(), failure::Error> { let cmd = format!("{}:{}:{}:{}:{}", part, tag, flag, start, size); let path = format!("/dev/rdsk/{}p0", disk); @@ -28,7 +28,7 @@ pub fn fmthard(log: &Logger, disk: &str, part: &str, tag: &str, flag: &str, Ok(()) } -pub fn zpool_logical_size(pool: &str) -> Result { +pub fn zpool_logical_size(pool: &str) -> Result { /* * It is tempting to use "zpool list" to obtain the pool size, but that size * does not account for parity and overhead in the way that one might @@ -60,7 +60,7 @@ pub fn zpool_logical_size(pool: &str) -> Result { Ok(used.saturating_add(avail) / 1024 / 1024) } -pub fn zpool_expand(pool: &str, disk: &str) -> Result<()> { +pub fn zpool_expand(pool: &str, disk: &str) -> Result<(), failure::Error> { let output = std::process::Command::new("/sbin/zpool") .env_clear() .arg("online") @@ -76,7 +76,7 @@ pub fn zpool_expand(pool: &str, disk: &str) -> Result<()> { Ok(()) } -pub fn zpool_reguid(pool: &str) -> Result<()> { +pub fn zpool_reguid(pool: &str) -> Result<(), failure::Error> { let output = std::process::Command::new("/sbin/zpool") .env_clear() .arg("reguid") @@ -90,7 +90,7 @@ pub fn zpool_reguid(pool: &str) -> Result<()> { Ok(()) } -pub fn zpool_disk() -> Result { +pub fn zpool_disk() -> Result { let pool = "rpool"; let output = std::process::Command::new("/sbin/zpool") .env_clear() @@ -140,7 +140,7 @@ struct Vtoc { partitions: HashMap, } -fn prtvtoc(disk: &str) -> Result { +fn prtvtoc(disk: &str) -> Result { let output = std::process::Command::new("/usr/sbin/prtvtoc") .env_clear() .arg(format!("/dev/dsk/{}", disk)) @@ -215,7 +215,7 @@ fn prtvtoc(disk: &str) -> Result { * enlarged underlying volume. This interface is clunky and unfortunate, but * I'm not currently aware of a better way. */ -pub fn format_expand(log: &Logger, disk: &str) -> Result<()> { +pub fn format_expand(log: &Logger, disk: &str) -> Result<(), failure::Error> { /* * We need a temporary file with a list of commands for format(1M) to * execute using the "-f" option. @@ -246,7 +246,7 @@ pub fn format_expand(log: &Logger, disk: &str) -> Result<()> { * Check to see if this disk requires expansion at the GPT table level using * format(1M). */ -pub fn should_expand(disk: &str) -> Result { +pub fn should_expand(disk: &str) -> Result { let vtoc = prtvtoc(&disk)?; if vtoc.sector_size != 512 { @@ -274,7 +274,7 @@ pub fn should_expand(disk: &str) -> Result { /** * Check to see if the data slice should be grown to fill any unallocated space. */ -pub fn grow_data_partition(log: &Logger, disk: &str) -> Result<()> { +pub fn grow_data_partition(log: &Logger, disk: &str) -> Result<(), failure::Error> { let vtoc = prtvtoc(&disk)?; if vtoc.sector_size != 512 { From 1e7bee995a3100edf14d5cafb8ca5d8e0184768b Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Sun, 30 Jan 2022 14:41:04 -0300 Subject: [PATCH 08/14] Fixing users and group ensuring code --- Cargo.lock | 9 +-- Cargo.toml | 11 +--- src/main.rs | 140 ++++++++++++++++++++++++++++++++++----- src/users.rs | 25 ------- src/users/ffi.rs | 162 ---------------------------------------------- src/users/libc.rs | 126 ------------------------------------ 6 files changed, 131 insertions(+), 342 deletions(-) delete mode 100644 src/users.rs delete mode 100644 src/users/ffi.rs delete mode 100644 src/users/libc.rs diff --git a/Cargo.lock b/Cargo.lock index b5d4809..0696080 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "addr2line" version = "0.14.1" @@ -540,9 +542,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.80" +version = "0.2.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +checksum = "565dbd88872dbe4cc8a46e527f26483c1d1f7afa6b884a3bd6cd893d4f98da74" [[package]] name = "linked-hash-map" @@ -1204,8 +1206,7 @@ dependencies = [ [[package]] name = "users" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +source = "git+https://github.com/Toasterson/rust-users.git?branch=illumos#ab8ccb3422c2693f84c2c99b35f4829a009af731" dependencies = [ "libc", "log", diff --git a/Cargo.toml b/Cargo.toml index 3f554d4..7a55def 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,9 +24,9 @@ failure = "0.1" failure_derive = "0.1" flate2 = "1.0" mailparse = "0.13.5" -users = { version="0.11.0", optional=true } +users = { git="https://github.com/Toasterson/rust-users.git", branch="illumos" } base64 = "0.13.0" -libc = { version="0.2.15", optional=true } +libc = { version="0.2.116" } # # Force an earlier version of socket2 which does not use the TCP_MAXSEG symbol @@ -46,9 +46,4 @@ unicode-normalization = "0.1, <0.1.14" [dependencies.reqwest] version = "0.10" default-features = false -features = ["blocking", "json"] - -[features] -default = [ "cmd_users" ] -libc_users = ["users", "libc"] -cmd_users = [] \ No newline at end of file +features = ["blocking", "json"] \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ebb9f44..0322365 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,12 @@ /* * Copyright 2020 Oxide Computer Company - * Copyright 2021 OpenFlowLabs + * Copyright 2022 OpenFlowLabs * */ -#[cfg(target_os="illumos")] -use std::os::unix::fs::MetadataExt; -#[cfg(target_os="linux")] -use std::os::linux::fs::MetadataExt; -use std::os::unix::ffi::OsStrExt; - mod zpool; mod common; mod userdata; -mod users; mod public_keys; mod file; @@ -28,16 +21,18 @@ use std::process::{Command}; use userdata::read_user_data; use flate2::read::GzDecoder; use base64::{decode as base64Decode}; -use libc::{self, gid_t, uid_t}; -use std::ffi::CString; +use std::ffi::{CString, OsStr, OsString}; use std::io::Error as IOError; use std::io::Result as IOResult; - +use std::os::unix::fs::MetadataExt; +use std::os::unix::ffi::OsStrExt; +use users; +use libc; use serde::Deserialize; +use users::User; use common::*; use file::*; -use crate::users::*; use crate::userdata::networkconfig::{NetworkDataV1Iface, NetworkDataV1Subnet}; use crate::userdata::UserData; use crate::userdata::cloudconfig::{UserConfig, WriteFileEncoding, WriteFileData}; @@ -65,6 +60,9 @@ const SWAPADD: &str = "/sbin/swapadd"; const ZFS: &str = "/sbin/zfs"; const CPIO: &str = "/usr/bin/cpio"; const PKG: &str = "/usr/bin/pkg"; +const USERADD: &str = "/usr/sbin/useradd"; +const GROUPADD: &str = "/usr/sbin/groupadd"; +const USERMOD: &str = "/usr/sbin/usermod"; const FMRI_USERSCRIPT: &str = "svc:/system/illumos/userscript:default"; @@ -2191,7 +2189,7 @@ fn phase_user_data(log: &Logger, user_data: &UserData) -> Result<(), failure::Er */ if let Some(groups) = cc.groups { for group in groups { - ensure_group(group)?; + ensure_group(log, group)?; } } @@ -2299,8 +2297,8 @@ fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<(), failure:: if let Some(owner) = &file.owner { info!(log, "setting owner and group of file {} to {}", file.path, owner); - let mut uid: uid_t; - let mut gid: gid_t; + let mut uid: users::uid_t; + let mut gid: users::gid_t; if owner.contains(":") { if let Some((u, g)) = owner.split_once(":") { if let Some(user) = users::get_user_by_name(u) { @@ -2319,7 +2317,7 @@ fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<(), failure:: } } else { let meta = f.metadata()?; - gid = meta.st_gid(); + gid = meta.gid(); if let Some(u) = users::get_user_by_name(&owner) { uid = u.uid(); } else { @@ -2333,7 +2331,7 @@ fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<(), failure:: } /// Actually perform the change of owner on a path -fn chown>(path: P, uid: uid_t, gid: gid_t, follow: bool) -> IOResult<()> { +fn chown>(path: P, uid: libc::uid_t, gid: libc::gid_t, follow: bool) -> IOResult<()> { let path = path.as_ref(); let s = CString::new(path.as_os_str().as_bytes()).unwrap(); let ret = unsafe { @@ -2466,6 +2464,114 @@ fn ensure_pubkeys(log: &Logger, user: &str, public_keys: &[String]) -> Result<() Ok(()) } +fn ensure_user(log: &Logger, user: &UserConfig) -> Result<(), failure::Error> { + if users::get_user_by_name(&user.name).is_none() { + let mut cmd = Command::new(USERADD); + if let Some(groups) = &user.groups { + cmd.arg("-G").arg(groups.join(",")); + } + + if let Some(expire_date) = &user.expire_date { + cmd.arg("-e").arg(expire_date); + } + + if let Some(gecos) = &user.gecos { + cmd.arg("-c").arg(gecos); + } + + if let Some(home_dir) = &user.homedir { + cmd.arg("-d").arg(home_dir); + } + + if let Some(primary_group) = &user.primary_group { + cmd.arg("-g").arg(primary_group); + } else if let Some(no_user_group) = &user.no_user_group { + if !no_user_group { + let mut ump = HashMap::>>::new(); + ump.insert(user.name.clone(), Some(vec![])); + ensure_group(log, ump)?; + } + } + + if let Some(inactive) = &user.inactive { + cmd.arg("-f").arg(inactive); + } + + if let Some(shell) = &user.shell { + cmd.arg("-s").arg(shell); + } + + cmd.arg(&user.name); + + //TODO lock_passwd + //TODO passwd + //TODO is_system_user + debug!(log, "Running useradd {:?}", cmd); + let output = cmd.output()?; + if !output.status.success() { + bail!("useradd failed for {}: {}", &user.name, output.info()); + } + + } else { + info!(log, "user with name {} exists skipping", &user.name); + } + + if let Some(public_keys) = &user.ssh_authorized_keys { + ensure_pubkeys(log, &user.name, public_keys)?; + } + + Ok(()) +} + +fn ensure_group(log: &Logger, groups: HashMap>>) -> Result<(), failure::Error> { + for (group_name, users_in_group_opt) in groups { + if users::get_group_by_name(&group_name).is_none() { + let mut cmd = Command::new(GROUPADD); + cmd.arg(&group_name); + debug!(log, "Running groupadd {:?}", cmd); + let output = cmd.output()?; + if !output.status.success() { + bail!("groupadd failed for {}: {}", &group_name, output.info()); + } + } else { + info!(log, "group {} exists", group_name) + } + + if let Some(users_in_group) = users_in_group_opt { + for user in users_in_group { + let existing_groups: Vec = if let Some(sys_user) = users::get_user_by_name(&user) { + if let Some(user_groups) = users::get_user_groups(&user, sys_user.primary_group_id()) { + user_groups.iter().map(|g| + g.name().to_os_string().into_string().unwrap_or(String::new()) + ).collect() + } else { + vec![] + } + } else { + vec![] + }; + + let mut user_groups: Vec = existing_groups.iter().filter(|&g| + g.clone() != String::new() + ).map(|g| g.clone()).collect(); + + user_groups.push(group_name.clone()); + let mut cmd = Command::new(USERMOD); + cmd.arg("-G"); + cmd.arg(user_groups.clone().join(",")); + cmd.arg(&user); + debug!(log, "Running usermod {:?}", cmd); + let output = cmd.output()?; + if !output.status.success() { + bail!("usermod failed for {}: {}", &user, output.info()); + } + } + } + } + + Ok(()) +} + fn phase_userscript(log: &Logger, userscript: &str) -> Result<(), failure::Error> { /* * If the userscript is basically empty, just ignore it. diff --git a/src/users.rs b/src/users.rs deleted file mode 100644 index cb45db6..0000000 --- a/src/users.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::collections::HashMap; -use crate::common::*; -use crate::userdata::cloudconfig::UserConfig; - -#[cfg(feature = "libc_users")] -mod libc; -#[cfg(feature = "libc_users")] -use libc::ensure_group as real_ensure_group; -#[cfg(feature = "libc_users")] -use ffi::ensure_user as real_ensure_user; - -#[cfg(feature = "cmd_users")] -mod ffi; -#[cfg(feature = "cmd_users")] -use ffi::ensure_group as real_ensure_group; -#[cfg(feature = "cmd_users")] -use ffi::ensure_user as real_ensure_user; - -pub fn ensure_group(group: HashMap>>) -> Result<(), failure::Error> { - real_ensure_group(group) -} - -pub fn ensure_user(log: &Logger, user: &UserConfig) -> Result<(), failure::Error> { - real_ensure_user(log, user) -} \ No newline at end of file diff --git a/src/users/ffi.rs b/src/users/ffi.rs deleted file mode 100644 index cd4a161..0000000 --- a/src/users/ffi.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::collections::HashMap; -use crate::common::*; -use std::process::Command; -use crate::userdata::cloudconfig::UserConfig; -use std::ffi::{OsStr, CString}; -use std::os::unix::ffi::OsStrExt; -use std::fmt; -use libc::{c_char, uid_t, gid_t, c_int}; - -const GROUPADD: &str = "/usr/sbin/groupadd"; -const USERMOD: &str = "/usr/sbin/usermod"; -const USERADD: &str = "/usr/sbin/useradd"; -const PASSWD: &str = "/usr/bin/passwd"; - -pub fn ensure_group(group: HashMap>>) -> Result<(), failure::Error> { - -} - -pub fn ensure_user(log: &Logger, user: &UserConfig) -> Result<(), failure::Error> { - -} - - - -/// Information about a particular group. -/// -/// For more information, see the [module documentation](index.html). -#[derive(Clone)] -struct Group { - gid: gid_t, - extras: os::GroupExtras, - pub(crate) name_arc: Arc, -} - -impl Group { - - /// Create a new `Group` with the given group ID and name, with the - /// rest of the fields filled in with dummy values. - /// - /// This method does not actually create a new group on the system — it - /// should only be used for comparing groups in tests. - /// - /// # Examples - /// - /// ``` - /// use users::Group; - /// - /// let group = Group::new(102, "database"); - /// ``` - pub fn new + ?Sized>(gid: gid_t, name: &S) -> Self { - let name_arc = Arc::from(name.as_ref()); - let extras = os::GroupExtras::default(); - - Self { gid, name_arc, extras } - } - - /// Returns this group’s ID. - /// - /// # Examples - /// - /// ``` - /// use users::Group; - /// - /// let group = Group::new(102, "database"); - /// assert_eq!(group.gid(), 102); - /// ``` - pub fn gid(&self) -> gid_t { - self.gid - } - - /// Returns this group’s name. - /// - /// # Examples - /// - /// ``` - /// use std::ffi::OsStr; - /// use users::Group; - /// - /// let group = Group::new(102, "database"); - /// assert_eq!(group.name(), OsStr::new("database")); - /// ``` - pub fn name(&self) -> &OsStr { - &*self.name_arc - } -} - -impl fmt::Debug for Group { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if f.alternate() { - f.debug_struct("Group") - .field("gid", &self.gid) - .field("name_arc", &self.name_arc) - .field("extras", &self.extras) - .finish() - } - else { - write!(f, "Group({}, {})", self.gid(), self.name().to_string_lossy()) - } - } -} - -/// Searches for a `Group` with the given group name in the system’s group database. -/// Returns it if one is found, otherwise returns `None`. -/// -/// # libc functions used -/// -/// - [`getgrnam_r`](https://docs.rs/libc/*/libc/fn.getgrnam_r.html) -/// -/// # Examples -/// -/// ``` -/// use users::get_group_by_name; -/// -/// match get_group_by_name("db-access") { -/// Some(group) => println!("Found group #{}", group.gid()), -/// None => println!("Group not found"), -/// } -/// ``` -pub fn get_group_by_name + ?Sized>(groupname: &S) -> Option { - let groupname = match CString::new(groupname.as_ref().as_bytes()) { - Ok(u) => u, - Err(_) => { - // The groupname that was passed in contained a null character, - // which will match no usernames. - return None; - } - }; - - let mut group = unsafe { mem::zeroed::() }; - let mut buf = vec![0; 2048]; - let mut result = ptr::null_mut::(); - - #[cfg(feature = "logging")] - trace!("Running getgrnam_r for group {:?}", groupname.as_ref()); - - loop { - let r = unsafe { - libc::getgrnam_r(groupname.as_ptr(), &mut group, buf.as_mut_ptr(), buf.len(), &mut result) - }; - - if r != libc::ERANGE { - break; - } - - let newsize = buf.len().checked_mul(2)?; - buf.resize(newsize, 0); - } - - if result.is_null() { - // There is no such group, or an error has occurred. - // errno gets set if there’s an error. - return None; - } - - if result != &mut group { - // The result of getgrnam_r should be its input struct. - return None; - } - - let group = unsafe { struct_to_group(result.read()) }; - Some(group) -} \ No newline at end of file diff --git a/src/users/libc.rs b/src/users/libc.rs deleted file mode 100644 index d74b4c7..0000000 --- a/src/users/libc.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::collections::HashMap; -use crate::common::*; -use std::process::Command; -use users::{get_group_by_name, get_user_by_name, get_user_groups}; -use crate::userdata::cloudconfig::UserConfig; -use crate::public_keys::ensure_pubkeys; - -const GROUPADD: &str = "/usr/sbin/groupadd"; -const USERMOD: &str = "/usr/sbin/usermod"; -const USERADD: &str = "/usr/sbin/useradd"; -const PASSWD: &str = "/usr/bin/passwd"; - -pub fn ensure_group(group: HashMap>>) -> Result<(), failure::Error> { - if let Some(group_name) = group.keys().next(){ - let system_group = get_group_by_name(&group_name); - match system_group { - None => { - let output = Command::new(GROUPADD) - .env_clear() - .arg(&group_name) - .output()?; - - if !output.status.success() { - bail!("could not create group {}: {}", group_name, output.info()) - } - } - _ => {} - } - - if let Some(members) = group[group_name].clone() { - for member in members { - let user_option = get_user_by_name(&member); - if let Some(user) = user_option { - let groups = get_user_groups(user.name(), user.primary_group_id()); - - if let Some(grps) = groups { - let mut current_groups: Vec = grps.iter().map(|g| { String::from(g.name().to_string_lossy()) }).collect(); - current_groups.push(group_name.to_string()); - let new_groups_string: String = current_groups.join(","); - let output = Command::new(USERMOD) - .env_clear() - .arg("-G") - .arg(new_groups_string) - .arg(&member) - .output()?; - if !output.status.success() { - bail!("failed to set groups of user {}: {}", member, output.info()) - } - } - } - - } - } - } else { - bail!("no key in groups configuration error: we have {:#?} in the config", group) - } - - - Ok(()) -} - -pub fn ensure_user(log: &Logger, user: &UserConfig) -> Result<(), failure::Error> { - if let None = users::get_user_by_name(&user.name) { - let mut useradd = Command::new(USERADD); - useradd.env_clear(); - if let Some(d) = &user.expire_date { - useradd.arg("-e"); - useradd.arg(d); - } - - if let Some(g) = &user.gecos { - useradd.arg("-c"); - useradd.arg(g); - } - - if let Some(h) = &user.homedir { - useradd.arg("-d"); - useradd.arg(h); - } - - if let Some(g) = &user.primary_group { - useradd.arg("-g"); - useradd.arg(g); - } - - if let Some(gs) = &user.groups { - useradd.arg("-G"); - useradd.arg(gs.join(",")); - } - - if user.system == None && user.no_create_home == None { - useradd.arg("-m"); - useradd.arg("-z"); - } - - if let Some(s) = &user.shell { - useradd.arg("-s"); - useradd.arg(s); - } - - useradd.arg(&user.name); - - let output = useradd.output()?; - - if !output.status.success() { - bail!("failed to run useradd for {}", user.name) - } - } else { - info!(log, "user {} exists skipping", user.name) - } - - if let Some(keys) = &user.ssh_authorized_keys { - ensure_pubkeys(log, &user.name, keys)?; - } - - let output = Command::new(PASSWD) - .env_clear() - .arg("-N") - .arg(&user.name) - .output()?; - if !output.status.success() { - bail!("failed to run passwd for {}", user.name) - } - - Ok(()) -} \ No newline at end of file From 212e8315021e635ebb1c7605c727fbf06ff82463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Wegm=C3=BCller?= Date: Thu, 16 Jun 2022 17:00:57 +0000 Subject: [PATCH 09/14] Add Rust devcontainer to do more work in codespaces --- .devcontainer/Dockerfile | 9 ++++++ .devcontainer/devcontainer.json | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..0299821 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.236.0/containers/rust/.devcontainer/base.Dockerfile + +# [Choice] Debian OS version (use bullseye on local arm64/Apple Silicon): buster, bullseye +ARG VARIANT="buster" +FROM mcr.microsoft.com/vscode/devcontainers/rust:0-${VARIANT} + +# [Optional] Uncomment this section to install additional packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..9aa5cd1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,57 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.236.0/containers/rust +{ + "name": "Rust", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Use the VARIANT arg to pick a Debian OS version: buster, bullseye + // Use bullseye when on local on arm64/Apple Silicon. + "VARIANT": "bullseye" + } + }, + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "lldb.executable": "/usr/bin/lldb", + // VS Code don't watch files under ./target + "files.watcherExclude": { + "**/target/**": true + }, + "rust-analyzer.checkOnSave.command": "clippy" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "vadimcn.vscode-lldb", + "mutantdino.resourcemonitor", + "matklad.rust-analyzer", + "tamasfe.even-better-toml", + "serayuzgur.crates" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "rustc --version", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "git": "os-provided", + "git-lfs": "latest", + "github-cli": "latest" + } +} From 5d5cedf921bd6846ed0f510db8d1fe89b6511d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Wegm=C3=BCller?= Date: Thu, 16 Jun 2022 17:20:23 +0000 Subject: [PATCH 10/14] Make changes according to comments from PR --- Cargo.lock | 98 +++++++----------------------- Cargo.toml | 4 +- src/common.rs | 3 +- src/file.rs | 21 ++++--- src/main.rs | 109 +++++++++++++++++----------------- src/public_keys.rs | 3 +- src/userdata.rs | 19 +++--- src/userdata/networkconfig.rs | 30 +++++----- src/zpool.rs | 19 +++--- 9 files changed, 129 insertions(+), 177 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0696080..c26a10e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "addr2line" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" -dependencies = [ - "gimli", -] - [[package]] name = "adler" version = "1.0.2" @@ -52,20 +43,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "backtrace" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" -dependencies = [ - "addr2line", - "cfg-if 1.0.0", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - [[package]] name = "base64" version = "0.10.1" @@ -223,28 +200,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "failure" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" -dependencies = [ - "backtrace", - "failure_derive", -] - -[[package]] -name = "failure_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "flate2" version = "1.0.20" @@ -351,12 +306,6 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] -[[package]] -name = "gimli" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" - [[package]] name = "h2" version = "0.2.7" @@ -467,8 +416,6 @@ dependencies = [ "anyhow", "atty", "base64 0.13.0", - "failure", - "failure_derive", "flate2", "libc", "mailparse", @@ -480,6 +427,7 @@ dependencies = [ "slog-term", "socket2", "tempfile", + "thiserror", "unicode-normalization", "users", ] @@ -681,12 +629,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" - [[package]] name = "once_cell" version = "1.5.2" @@ -900,12 +842,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rustc-demangle" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49" - [[package]] name = "ryu" version = "1.0.5" @@ -1021,18 +957,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "synstructure" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - [[package]] name = "tempfile" version = "3.1.0" @@ -1057,6 +981,26 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 7a55def..7938735 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ name = "illumos-metadata-agent" description = "Cloud metadata bootstrap software for illumos systems" version = "0.1.0" -authors = ["Joshua M. Clulow ", "Till Wegmueller "] edition = "2018" license = "Apache-2.0" repository = "https://github.com/illumos/metadata-agent" @@ -20,8 +19,7 @@ anyhow = "1" slog = "2.5" slog-term = "2.5" atty = "0.2" -failure = "0.1" -failure_derive = "0.1" +thiserror = "1.0" flate2 = "1.0" mailparse = "0.13.5" users = { git="https://github.com/Toasterson/rust-users.git", branch="illumos" } diff --git a/src/common.rs b/src/common.rs index b953c00..d5c4b7d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -7,8 +7,7 @@ use slog::Drain; use std::sync::Mutex; pub use slog::{info, warn, error, debug, trace, o, Logger}; -pub use anyhow::{Context}; -pub use failure::{ResultExt, bail, Error, Fail}; +pub use anyhow::{Context, bail}; /** * Initialise a logger which writes to stdout, and which does the right thing on diff --git a/src/file.rs b/src/file.rs index 96bdb4a..7813b2d 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,9 +1,14 @@ +/* + * Copyright 2020 Oxide Computer Company + */ + use crate::common::*; use std::fs::{DirBuilder, File}; use std::os::unix::fs::{DirBuilderExt, PermissionsExt}; use std::io::{ErrorKind, BufReader, BufRead, copy as IOCopy, Read, Write}; +use anyhow::{Result}; -pub fn ensure_dir(log: &Logger, path: &str) -> Result<(), failure::Error> { +pub fn ensure_dir(log: &Logger, path: &str) -> Result<()> { if !exists_dir(path)? { info!(log, "mkdir {}", path); DirBuilder::new() @@ -13,7 +18,7 @@ pub fn ensure_dir(log: &Logger, path: &str) -> Result<(), failure::Error> { Ok(()) } -pub fn exists_dir(p: &str) -> Result { +pub fn exists_dir(p: &str) -> Result { let md = match std::fs::metadata(p) { Ok(md) => md, Err(e) => match e.kind() { @@ -29,7 +34,7 @@ pub fn exists_dir(p: &str) -> Result { Ok(true) } -pub fn exists_file(p: &str) -> Result { +pub fn exists_file(p: &str) -> Result { let md = match std::fs::metadata(p) { Ok(md) => md, Err(e) => match e.kind() { @@ -46,13 +51,13 @@ pub fn exists_file(p: &str) -> Result { } -pub fn read_lines(p: &str) -> Result>, failure::Error> { +pub fn read_lines(p: &str) -> Result>> { Ok(read_file(p)?.map(|data| { data.lines().map(|a| a.trim().to_string()).collect() })) } -pub fn read_lines_maybe(p: &str) -> Result, failure::Error> { +pub fn read_lines_maybe(p: &str) -> Result> { Ok(match read_lines(p)? { None => Vec::new(), Some(l) => l, @@ -61,7 +66,7 @@ pub fn read_lines_maybe(p: &str) -> Result, failure::Error> { pub -fn read_file(p: &str) -> Result, failure::Error> { +fn read_file(p: &str) -> Result> { let f = match File::open(p) { Ok(f) => f, Err(e) => { @@ -77,7 +82,7 @@ fn read_file(p: &str) -> Result, failure::Error> { Ok(Some(out)) } -pub fn write_file(p: &str, data: &str) -> Result<(), failure::Error> { +pub fn write_file(p: &str, data: &str) -> Result<()> { let f = std::fs::OpenOptions::new() .write(true) .create(true) @@ -88,7 +93,7 @@ pub fn write_file(p: &str, data: &str) -> Result<(), failure::Error> { Ok(()) } -pub fn write_lines(log: &Logger, p: &str, lines: &[L]) -> Result<(), failure::Error> +pub fn write_lines(log: &Logger, p: &str, lines: &[L]) -> Result<()> where L: AsRef + std::fmt::Debug { info!(log, "----- WRITE FILE: {} ------ {:#?}", p, lines); diff --git a/src/main.rs b/src/main.rs index f39df79..afa6c3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ use users; use libc; use serde::Deserialize; use users::User; +use anyhow::{Result}; use common::*; use file::*; @@ -74,7 +75,7 @@ struct Smbios { uuid: String, } -fn amazon_metadata_getx(log: &Logger, key: &str) -> Result, failure::Error> { +fn amazon_metadata_getx(log: &Logger, key: &str) -> Result> { let url = format!("http://169.254.169.254/latest/{}", key); let cb = reqwest::blocking::ClientBuilder::new() @@ -102,11 +103,11 @@ fn amazon_metadata_getx(log: &Logger, key: &str) -> Result, failu } } -fn amazon_metadata_get(log: &Logger, key: &str) -> Result, failure::Error> { +fn amazon_metadata_get(log: &Logger, key: &str) -> Result> { amazon_metadata_getx(log, &format!("meta-data/{}", key)) } -fn smf_enable(log: &Logger, fmri: &str) -> Result<(), failure::Error> { +fn smf_enable(log: &Logger, fmri: &str) -> Result<()> { info!(log, "exec: svcadm enable {}", fmri); let output = Command::new(SVCADM) .env_clear() @@ -121,7 +122,7 @@ fn smf_enable(log: &Logger, fmri: &str) -> Result<(), failure::Error> { Ok(()) } -fn dhcpinfo(log: &Logger, key: &str) -> Result, failure::Error> { +fn dhcpinfo(log: &Logger, key: &str) -> Result> { info!(log, "exec: dhcpinfo {}", key); let output = Command::new(DHCPINFO) .env_clear() @@ -141,7 +142,7 @@ fn dhcpinfo(log: &Logger, key: &str) -> Result, failure::Error> { } } -fn smbios(log: &Logger) -> Result, failure::Error> { +fn smbios(log: &Logger) -> Result> { info!(log, "exec: smbios -t 1"); let output = Command::new(SMBIOS) .env_clear() @@ -193,7 +194,7 @@ enum Mdata { WrongHypervisor, } -fn mdata_get(log: &Logger, key: &str) -> Result { +fn mdata_get(log: &Logger, key: &str) -> Result { info!(log, "mdata-get \"{}\"...", key); let output = Command::new(MDATA_GET) .env_clear() @@ -225,7 +226,7 @@ fn mdata_get(log: &Logger, key: &str) -> Result { }) } -fn mdata_probe(log: &Logger) -> Result { +fn mdata_probe(log: &Logger) -> Result { /* * Check for an mdata-get(1M) binary on this system: */ @@ -256,7 +257,7 @@ fn mdata_probe(log: &Logger) -> Result { } } -fn read_json(p: &str) -> Result, failure::Error> +fn read_json(p: &str) -> Result> where for<'de> T: Deserialize<'de> { let s = read_file(p)?; @@ -299,7 +300,7 @@ struct IPv4 { } impl IPv4 { - fn prefix_len(&self) -> Result { + fn prefix_len(&self) -> Result { let nm: Ipv4Addr = self.netmask.parse()?; let bits: u32 = nm.into(); @@ -316,7 +317,7 @@ impl IPv4 { Ok(len) } - fn cidr(&self) -> Result { + fn cidr(&self) -> Result { let prefix_len = self.prefix_len()?; Ok(format!("{}/{}", self.ip_address, prefix_len)) } @@ -374,7 +375,7 @@ impl SdcNic { * a dictionary as there may be more than one mount entry for a particular mount * point. */ -fn mounts() -> Result, failure::Error> { +fn mounts() -> Result> { let mnttab = read_lines("/etc/mnttab")?.unwrap(); let rows: Vec> = mnttab.iter() .map(|m| { m.split('\t').collect() }) @@ -410,7 +411,7 @@ fn mounts() -> Result, failure::Error> { Ok(out) } -fn detect_archive>(rdevpath: P) -> Result, failure::Error> { +fn detect_archive>(rdevpath: P) -> Result> { let mut buf = [0u8; 512]; let mut f = OpenOptions::new() .read(true) @@ -434,7 +435,7 @@ fn detect_archive>(rdevpath: P) -> Result, failure } } -fn find_cpio_device(log: &Logger) -> Result, failure::Error> { +fn find_cpio_device(log: &Logger) -> Result> { /* * Use the raw device so that we can read just enough bytes to look for the * cpio magic: @@ -480,7 +481,7 @@ struct CiDataDevice { label: String, } -fn find_cidata_devices(log: &Logger) -> Result>, failure::Error> { +fn find_cidata_devices(log: &Logger) -> Result>> { let i = std::fs::read_dir("/dev/dsk")?; let mut out: Vec = Vec::new(); @@ -541,7 +542,7 @@ fn find_cidata_devices(log: &Logger) -> Result>, failur } } -fn find_device() -> Result, failure::Error> { +fn find_device() -> Result> { let i = std::fs::read_dir("/dev/dsk")?; let mut out = Vec::new(); @@ -619,7 +620,7 @@ impl Terms { } } -fn parse_net_adm(stdout: Vec) -> Result>, failure::Error> { +fn parse_net_adm(stdout: Vec) -> Result>> { let stdout = String::from_utf8(stdout)?; let mut out = Vec::new(); @@ -647,7 +648,7 @@ fn parse_net_adm(stdout: Vec) -> Result>, failure::Error> { Ok(out) } -fn netmask_to_cidr(netmask: &Option) -> Result { +fn netmask_to_cidr(netmask: &Option) -> Result { match netmask { None => Ok(24), Some(mask) => { @@ -707,7 +708,7 @@ fn netmask_to_cidr(netmask: &Option) -> Result { } } -fn ipadm_interface_list() -> Result, failure::Error> { +fn ipadm_interface_list() -> Result> { let output = Command::new(IPADM) .env_clear() .arg("show-if") @@ -732,7 +733,7 @@ struct IpadmAddress { cidr: String, } -fn ipadm_address_list() -> Result, failure::Error> { +fn ipadm_address_list() -> Result> { let output = Command::new(IPADM) .env_clear() .arg("show-addr") @@ -779,7 +780,7 @@ fn mac_sanitise(input: &str) -> String { mac } -fn dladm_ether_list() -> Result, failure::Error> { +fn dladm_ether_list() -> Result> { let output = Command::new(DLADM) .env_clear() .arg("show-ether") @@ -795,7 +796,7 @@ fn dladm_ether_list() -> Result, failure::Error> { Ok(ents.iter().map(|l| l[0].trim().to_string()).collect()) } -fn mac_to_nic(mac: &str) -> Result, failure::Error> { +fn mac_to_nic(mac: &str) -> Result> { let output = Command::new(DLADM) .env_clear() .arg("show-phys") @@ -827,7 +828,7 @@ fn mac_to_nic(mac: &str) -> Result, failure::Error> { } } -fn memsize() -> Result { +fn memsize() -> Result { let output = std::process::Command::new(PRTCONF) .env_clear() .arg("-m") @@ -840,7 +841,7 @@ fn memsize() -> Result { Ok(String::from_utf8(output.stdout)?.trim().parse()?) } -fn create_zvol(name: &str, size_mib: u64) -> Result<(), failure::Error> { +fn create_zvol(name: &str, size_mib: u64) -> Result<()> { let output = std::process::Command::new(ZFS) .env_clear() .arg("create") @@ -855,7 +856,7 @@ fn create_zvol(name: &str, size_mib: u64) -> Result<(), failure::Error> { Ok(()) } -fn exists_zvol(name: &str) -> Result { +fn exists_zvol(name: &str) -> Result { let output = std::process::Command::new(ZFS) .env_clear() .arg("list") @@ -886,7 +887,7 @@ fn exists_zvol(name: &str) -> Result { Ok(false) } -fn swapadd() -> Result<(), failure::Error> { +fn swapadd() -> Result<()> { let output = std::process::Command::new(SWAPADD) .env_clear() .output()?; @@ -898,7 +899,7 @@ fn swapadd() -> Result<(), failure::Error> { Ok(()) } -fn ensure_interface_name(log: &Logger, name: &str, mac_addres: &str) -> Result<(), failure::Error> { +fn ensure_interface_name(log: &Logger, name: &str, mac_addres: &str) -> Result<()> { // First lets get the nic and check if we need to run. let nic = mac_to_nic(mac_addres)?; match nic { @@ -926,7 +927,7 @@ fn ensure_interface_name(log: &Logger, name: &str, mac_addres: &str) -> Result<( Ok(()) } -fn ensure_ipadm_subnets_config(log: &Logger, link_name: &str, subnets: &Vec) -> Result<(), failure::Error> { +fn ensure_ipadm_subnets_config(log: &Logger, link_name: &str, subnets: &Vec) -> Result<()> { info!(log, "configuring subnets for link {}", link_name); for subnet in subnets { if let Err(e) = ensure_ipadm_single_subnet_config(log, link_name, subnet) { @@ -937,7 +938,7 @@ fn ensure_ipadm_subnets_config(log: &Logger, link_name: &str, subnets: &Vec Result<(), failure::Error> { +fn ensure_ipadm_single_subnet_config(log: &Logger, link_name: &str, subnet: &NetworkDataV1Subnet) -> Result<()> { match subnet { NetworkDataV1Subnet::Dhcp4 | NetworkDataV1Subnet::Dhcp => { ensure_ipv4_interface_dhcp(log, "dhcp4", link_name) @@ -988,7 +989,7 @@ fn ensure_ipadm_single_subnet_config(log: &Logger, link_name: &str, subnet: &Net } } -fn ensure_interface_mtu(log: &Logger, name: &str, mtu: &i32) -> Result<(), failure::Error> { +fn ensure_interface_mtu(log: &Logger, name: &str, mtu: &i32) -> Result<()> { info!(log, "setting mtu of interface {} to {}", name, mtu); let output = Command::new(DLADM) .env_clear() @@ -1004,7 +1005,7 @@ fn ensure_interface_mtu(log: &Logger, name: &str, mtu: &i32) -> Result<(), failu Ok(()) } -fn ensure_ipadm_interface(log: &Logger, n: &str) -> Result { +fn ensure_ipadm_interface(log: &Logger, n: &str) -> Result { info!(log, "ENSURE INTERFACE: {}", n); let ifaces = ipadm_interface_list()?; @@ -1029,7 +1030,7 @@ fn ensure_ipadm_interface(log: &Logger, n: &str) -> Result } } -fn ensure_ipv4_gateway(log: &Logger, gateway: &str) -> Result<(), failure::Error> { +fn ensure_ipv4_gateway(log: &Logger, gateway: &str) -> Result<()> { info!(log, "ENSURE IPv4 GATEWAY: {}", gateway); let orig_defrouters = read_lines_maybe(DEFROUTER)?; @@ -1063,7 +1064,7 @@ fn ensure_ipv4_gateway(log: &Logger, gateway: &str) -> Result<(), failure::Error } fn ensure_ipv4_interface_dhcp(log: &Logger, sfx: &str, n: &str) - -> Result<(), failure::Error> + -> Result<()> { info!(log, "ENSURE IPv4 DHCP INTERFACE: {}", n); @@ -1142,7 +1143,7 @@ fn ensure_ipv4_interface_dhcp(log: &Logger, sfx: &str, n: &str) } fn ensure_ipv4_interface(log: &Logger, sfx: &str, mac_option: Option<&str>, link_name: Option<&str>, ipv4: &str) - -> Result<(), failure::Error> + -> Result<()> { let found_nic = if let Some(mac) = mac_option { @@ -1232,7 +1233,7 @@ fn main() { } } -fn run(log: &Logger) -> Result<(), failure::Error> { +fn run(log: &Logger) -> Result<()> { /* * This program could be destructive if run in the wrong place. Try to * ensure it has at least been installed as an SMF service: @@ -1334,7 +1335,7 @@ fn run(log: &Logger) -> Result<(), failure::Error> { Ok(()) } -fn run_generic(log: &Logger, smbios_uuid: &str, network: bool) -> Result<(), failure::Error> { +fn run_generic(log: &Logger, smbios_uuid: &str, network: bool) -> Result<()> { /* * Load our stamp file to see if the Guest UUID has changed. */ @@ -1448,7 +1449,7 @@ struct SMBIOSDatasource { local_hostname: String, } -fn parse_smbios_datasource_string(raw_string: &str) -> Result { +fn parse_smbios_datasource_string(raw_string: &str) -> Result { let mut ds = SMBIOSDatasource::default(); for part in raw_string.split(";") { let key_val: Vec<&str> = part.split("=").collect(); @@ -1464,7 +1465,7 @@ fn parse_smbios_datasource_string(raw_string: &str) -> Result Result<(), failure::Error> { +fn ensure_network_config(log: &Logger, config: &userdata::networkconfig::NetworkConfig) -> Result<()> { let net_config = match config { userdata::networkconfig::NetworkConfig::V1(c) => &c.config, userdata::networkconfig::NetworkConfig::V2(_) => { @@ -1540,7 +1541,7 @@ fn ensure_network_config(log: &Logger, config: &userdata::networkconfig::Network Ok(()) } -fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<(), failure::Error> { +fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<()> { /* * Parse any datasource definition from smbios_uuid field, which by cloud-init standard is * not just the uuid @@ -1660,7 +1661,7 @@ fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<(), failure::Err Ok(()) } -fn run_amazon(log: &Logger) -> Result<(), failure::Error> { +fn run_amazon(log: &Logger) -> Result<()> { /* * Sadly, Amazon has no mechanism for metadata access that does not require * a correctly configured IP interface. In addition, the available NIC @@ -1767,7 +1768,7 @@ fn run_amazon(log: &Logger) -> Result<(), failure::Error> { Ok(()) } -fn run_smartos(log: &Logger) -> Result<(), failure::Error> { +fn run_smartos(log: &Logger) -> Result<()> { let uuid = if let Mdata::Found(uuid) = mdata_get(log, "sdc:uuid")? { uuid.trim().to_string() } else { @@ -1874,7 +1875,7 @@ fn run_smartos(log: &Logger) -> Result<(), failure::Error> { Ok(()) } -fn run_digitalocean(log: &Logger) -> Result<(), failure::Error> { +fn run_digitalocean(log: &Logger) -> Result<()> { /* * First, locate and mount the metadata ISO. We need to load the droplet ID * so that we can determine if we have completed first boot processing for @@ -2022,13 +2023,13 @@ fn run_digitalocean(log: &Logger) -> Result<(), failure::Error> { Ok(()) } -fn phase_reguid_zpool(log: &Logger) -> Result<(), failure::Error> { +fn phase_reguid_zpool(log: &Logger) -> Result<()> { info!(log, "regenerate pool guid for rpool"); zpool::zpool_reguid("rpool")?; Ok(()) } -fn phase_expand_zpool(log: &Logger) -> Result<(), failure::Error> { +fn phase_expand_zpool(log: &Logger) -> Result<()> { /* * NOTE: Though it might seem like we could skip directly to using "zpool * online -e ...", there appears to be at least one serious deadlock in this @@ -2057,7 +2058,7 @@ fn phase_expand_zpool(log: &Logger) -> Result<(), failure::Error> { Ok(()) } -fn phase_add_swap(log: &Logger) -> Result<(), failure::Error> { +fn phase_add_swap(log: &Logger) -> Result<()> { /* * Next, add a swap device. Ideally we will have enough room for a swap * file twice the size of physical memory -- but if not, we want to use at @@ -2100,7 +2101,7 @@ fn phase_add_swap(log: &Logger) -> Result<(), failure::Error> { Ok(()) } -fn phase_set_hostname(log: &Logger, hostname: &str) -> Result<(), failure::Error> { +fn phase_set_hostname(log: &Logger, hostname: &str) -> Result<()> { /* * Check nodename: */ @@ -2188,7 +2189,7 @@ fn phase_set_hostname(log: &Logger, hostname: &str) -> Result<(), failure::Error Ok(()) } -fn phase_user_data(log: &Logger, user_data: &UserData) -> Result<(), failure::Error> { +fn phase_user_data(log: &Logger, user_data: &UserData) -> Result<()> { //First Apply cloud configurations for cc in user_data.cloud_configs.clone() { @@ -2242,7 +2243,7 @@ fn phase_user_data(log: &Logger, user_data: &UserData) -> Result<(), failure::Er Ok(()) } -fn ensure_packages(log: &&Logger, pkgs: Vec) -> Result<(), failure::Error> { +fn ensure_packages(log: &&Logger, pkgs: Vec) -> Result<()> { info!(log, "installing packages {:#?}", pkgs); let mut pkg_cmd = Command::new(PKG); pkg_cmd.env_clear(); @@ -2263,7 +2264,7 @@ fn ensure_packages(log: &&Logger, pkgs: Vec) -> Result<(), failure::Erro Ok(()) } -fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<(), failure::Error> { +fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<()> { info!(log, "creating file {}", file.path); let f = std::fs::OpenOptions::new() .write(true) @@ -2357,7 +2358,7 @@ fn chown>(path: P, uid: libc::uid_t, gid: libc::gid_t, follow: bo } -fn ensure_dns_nameservers(log: &Logger, nameservers: &[String]) -> Result<(), failure::Error> { +fn ensure_dns_nameservers(log: &Logger, nameservers: &[String]) -> Result<()> { /* * DNS Servers: */ @@ -2397,7 +2398,7 @@ fn ensure_dns_nameservers(log: &Logger, nameservers: &[String]) -> Result<(), fa Ok(()) } -fn ensure_dns_search(log: &Logger, dns_search: &[String]) -> Result<(), failure::Error> { +fn ensure_dns_search(log: &Logger, dns_search: &[String]) -> Result<()> { /* * DNS Servers: */ @@ -2437,7 +2438,7 @@ fn ensure_dns_search(log: &Logger, dns_search: &[String]) -> Result<(), failure: Ok(()) } -fn ensure_pubkeys(log: &Logger, user: &str, public_keys: &[String]) -> Result<(), failure::Error> { +fn ensure_pubkeys(log: &Logger, user: &str, public_keys: &[String]) -> Result<()> { /* * Manage the public keys: */ @@ -2472,7 +2473,7 @@ fn ensure_pubkeys(log: &Logger, user: &str, public_keys: &[String]) -> Result<() Ok(()) } -fn ensure_user(log: &Logger, user: &UserConfig) -> Result<(), failure::Error> { +fn ensure_user(log: &Logger, user: &UserConfig) -> Result<()> { if users::get_user_by_name(&user.name).is_none() { let mut cmd = Command::new(USERADD); if let Some(groups) = &user.groups { @@ -2531,7 +2532,7 @@ fn ensure_user(log: &Logger, user: &UserConfig) -> Result<(), failure::Error> { Ok(()) } -fn ensure_group(log: &Logger, groups: HashMap>>) -> Result<(), failure::Error> { +fn ensure_group(log: &Logger, groups: HashMap>>) -> Result<()> { for (group_name, users_in_group_opt) in groups { if users::get_group_by_name(&group_name).is_none() { let mut cmd = Command::new(GROUPADD); @@ -2580,7 +2581,7 @@ fn ensure_group(log: &Logger, groups: HashMap>>) -> R Ok(()) } -fn phase_userscript(log: &Logger, userscript: &str) -> Result<(), failure::Error> { +fn phase_userscript(log: &Logger, userscript: &str) -> Result<()> { /* * If the userscript is basically empty, just ignore it. */ diff --git a/src/public_keys.rs b/src/public_keys.rs index 2630ba7..9b0d26a 100644 --- a/src/public_keys.rs +++ b/src/public_keys.rs @@ -1,7 +1,8 @@ use crate::common::*; use crate::file::*; +use anyhow::{Result}; -pub fn ensure_pubkeys(log: &Logger, user: &str, public_keys: &[String]) -> Result<(), failure::Error> { +pub fn ensure_pubkeys(log: &Logger, user: &str, public_keys: &[String]) -> Result<()> { /* * Manage the public keys: */ diff --git a/src/userdata.rs b/src/userdata.rs index bdef1a4..5b71390 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -7,15 +7,16 @@ use std::io::prelude::*; use flate2::read::GzDecoder; use std::io::BufReader; use std::path::PathBuf; -use failure::Error; use std::fs::File; use crate::common::*; +use anyhow::{Result}; +use thiserror::{Error}; pub mod cloudconfig; pub mod networkconfig; pub mod multiformat_deserialize; -pub fn read_user_data(log: &Logger, path: &PathBuf) -> Result { +pub fn read_user_data(log: &Logger, path: &PathBuf) -> Result { // Parse Multipart message from stream match read_gz_data(log, path) { Ok(data) => Ok(data), @@ -34,7 +35,7 @@ pub fn read_user_data(log: &Logger, path: &PathBuf) -> Result { } } -fn read_gz_data(log: &Logger, path: &PathBuf) -> Result { +fn read_gz_data(log: &Logger, path: &PathBuf) -> Result { let gzreader = GzDecoder::new(File::open(path)?); if None == gzreader.header() { return Err(UserDataError::NotGzData)?; @@ -44,12 +45,12 @@ fn read_gz_data(log: &Logger, path: &PathBuf) -> Result { parse_user_data_multipart_stream::>>(log, &mut reader) } -fn read_uncompressed(log: &Logger, path: &PathBuf) -> Result { +fn read_uncompressed(log: &Logger, path: &PathBuf) -> Result { let mut reader = BufReader::new(File::open(path)?); parse_user_data_multipart_stream::>(log, &mut reader) } -fn parse_user_data_multipart_stream(log: &Logger, stream: &mut S) -> Result { +fn parse_user_data_multipart_stream(log: &Logger, stream: &mut S) -> Result { let mut buf = Vec::new(); stream.read_to_end(&mut buf)?; @@ -67,7 +68,7 @@ fn parse_user_data_multipart_stream(log: &Logger, stream: &mut S) -> Ok(data) } -fn parse_file_part(log: &Logger, d: &mut UserData, mime_type: &str, buf: &str) -> Result<(), Error> { +fn parse_file_part(log: &Logger, d: &mut UserData, mime_type: &str, buf: &str) -> Result<()> { match mime_type { "text/cloud-config" | "#cloud-config" => { let cc = serde_yaml::from_str::(buf)?; @@ -84,13 +85,13 @@ fn parse_file_part(log: &Logger, d: &mut UserData, mime_type: &str, buf: &str) - Ok(()) } -#[derive(Debug, Fail, PartialEq)] +#[derive(Debug, Error, PartialEq)] enum UserDataError { - #[fail(display = "format of file {} is not parseable", path)] + #[error("format of file {} is not parseable", path)] InvalidFile{ path: String, }, - #[fail(display = "file is not compressed")] + #[error("file is not compressed")] NotGzData, } diff --git a/src/userdata/networkconfig.rs b/src/userdata/networkconfig.rs index 0388dc0..3fef1fe 100644 --- a/src/userdata/networkconfig.rs +++ b/src/userdata/networkconfig.rs @@ -3,8 +3,9 @@ use std::path::PathBuf; use std::collections::HashMap; use serde::{Deserialize}; use crate::userdata::multiformat_deserialize::Multiformat; +use anyhow::{Result}; -pub fn parse_network_config(path: &PathBuf) -> Result { +pub fn parse_network_config(path: &PathBuf) -> Result { // Try V1 first let file = File::open(path)?; let f = serde_yaml::from_reader::(file)?; @@ -107,9 +108,10 @@ mod tests { use std::path::PathBuf; use std::str::FromStr; use crate::userdata::multiformat_deserialize::Multiformat; + use anyhow::{Result}; #[test] - fn parse_physical () -> Result<(), failure::Error> { + fn parse_physical () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_physical.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -132,7 +134,7 @@ mod tests { } #[test] - fn parse_physical_2 () -> Result<(), failure::Error> { + fn parse_physical_2 () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_physical_2.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -164,7 +166,7 @@ mod tests { } #[test] - fn parse_bond () -> Result<(), failure::Error> { + fn parse_bond () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_bond.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -197,7 +199,7 @@ mod tests { } #[test] - fn parse_bridge () -> Result<(), failure::Error> { + fn parse_bridge () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_bridge.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -230,7 +232,7 @@ mod tests { } #[test] - fn parse_vlan () -> Result<(), failure::Error> { + fn parse_vlan () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_vlan.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -263,7 +265,7 @@ mod tests { } #[test] - fn parse_nameserver () -> Result<(), failure::Error> { + fn parse_nameserver () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_nameserver.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -313,7 +315,7 @@ mod tests { } #[test] - fn parse_route () -> Result<(), failure::Error> { + fn parse_route () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_route.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -362,7 +364,7 @@ mod tests { } #[test] - fn parse_subnet_dhcp () -> Result<(), failure::Error> { + fn parse_subnet_dhcp () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_subnet_dhcp.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -398,7 +400,7 @@ mod tests { } #[test] - fn parse_subnet_static () -> Result<(), failure::Error> { + fn parse_subnet_static () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_subnet_static.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -454,7 +456,7 @@ mod tests { } #[test] - fn parse_subnet_multiple () -> Result<(), failure::Error> { + fn parse_subnet_multiple () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_subnet_multiple.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -515,7 +517,7 @@ mod tests { } #[test] - fn parse_subnet_with_routes () -> Result<(), failure::Error> { + fn parse_subnet_with_routes () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_subnet_with_routes.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -572,7 +574,7 @@ mod tests { } #[test] - fn parse_subnet_bonded_vlan () -> Result<(), failure::Error> { + fn parse_subnet_bonded_vlan () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_bonded_vlan.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { @@ -638,7 +640,7 @@ mod tests { } #[test] - fn parse_multiple_vlan () -> Result<(), failure::Error> { + fn parse_multiple_vlan () -> Result<()> { let cfg = parse_network_config(&PathBuf::from_str("./sample_data/network_config_v1/test_multiple_vlan.yaml")?)?; match cfg { NetworkConfig::V1(v1) => { diff --git a/src/zpool.rs b/src/zpool.rs index f49d084..993026e 100644 --- a/src/zpool.rs +++ b/src/zpool.rs @@ -4,11 +4,12 @@ use std::collections::HashMap; use std::io::Write; +use anyhow::{Result}; use super::common::*; pub fn fmthard(log: &Logger, disk: &str, part: &str, tag: &str, flag: &str, - start: u64, size: u64) -> Result<(), failure::Error> + start: u64, size: u64) -> Result<()> { let cmd = format!("{}:{}:{}:{}:{}", part, tag, flag, start, size); let path = format!("/dev/rdsk/{}p0", disk); @@ -28,7 +29,7 @@ pub fn fmthard(log: &Logger, disk: &str, part: &str, tag: &str, flag: &str, Ok(()) } -pub fn zpool_logical_size(pool: &str) -> Result { +pub fn zpool_logical_size(pool: &str) -> Result { /* * It is tempting to use "zpool list" to obtain the pool size, but that size * does not account for parity and overhead in the way that one might @@ -60,7 +61,7 @@ pub fn zpool_logical_size(pool: &str) -> Result { Ok(used.saturating_add(avail) / 1024 / 1024) } -pub fn zpool_expand(pool: &str, disk: &str) -> Result<(), failure::Error> { +pub fn zpool_expand(pool: &str, disk: &str) -> Result<()> { let output = std::process::Command::new("/sbin/zpool") .env_clear() .arg("online") @@ -76,7 +77,7 @@ pub fn zpool_expand(pool: &str, disk: &str) -> Result<(), failure::Error> { Ok(()) } -pub fn zpool_reguid(pool: &str) -> Result<(), failure::Error> { +pub fn zpool_reguid(pool: &str) -> Result<()> { let output = std::process::Command::new("/sbin/zpool") .env_clear() .arg("reguid") @@ -90,7 +91,7 @@ pub fn zpool_reguid(pool: &str) -> Result<(), failure::Error> { Ok(()) } -pub fn zpool_disk() -> Result { +pub fn zpool_disk() -> Result { let pool = "rpool"; let output = std::process::Command::new("/sbin/zpool") .env_clear() @@ -140,7 +141,7 @@ struct Vtoc { partitions: HashMap, } -fn prtvtoc(disk: &str) -> Result { +fn prtvtoc(disk: &str) -> Result { let output = std::process::Command::new("/usr/sbin/prtvtoc") .env_clear() .arg(format!("/dev/dsk/{}", disk)) @@ -215,7 +216,7 @@ fn prtvtoc(disk: &str) -> Result { * enlarged underlying volume. This interface is clunky and unfortunate, but * I'm not currently aware of a better way. */ -pub fn format_expand(log: &Logger, disk: &str) -> Result<(), failure::Error> { +pub fn format_expand(log: &Logger, disk: &str) -> Result<()> { /* * We need a temporary file with a list of commands for format(1M) to * execute using the "-f" option. @@ -246,7 +247,7 @@ pub fn format_expand(log: &Logger, disk: &str) -> Result<(), failure::Error> { * Check to see if this disk requires expansion at the GPT table level using * format(1M). */ -pub fn should_expand(disk: &str) -> Result { +pub fn should_expand(disk: &str) -> Result { let vtoc = prtvtoc(&disk)?; if vtoc.sector_size != 512 { @@ -274,7 +275,7 @@ pub fn should_expand(disk: &str) -> Result { /** * Check to see if the data slice should be grown to fill any unallocated space. */ -pub fn grow_data_partition(log: &Logger, disk: &str) -> Result<(), failure::Error> { +pub fn grow_data_partition(log: &Logger, disk: &str) -> Result<()> { let vtoc = prtvtoc(&disk)?; if vtoc.sector_size != 512 { From 7bbfab438013103df036a0c1fcbfcd6da613ed10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Wegm=C3=BCller?= Date: Thu, 16 Jun 2022 17:37:15 +0000 Subject: [PATCH 11/14] Ignore .vscode changes --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 19c012f..2674b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target .idea - +.vscode \ No newline at end of file From b7b0cd64d2216ea2c31e9892926c77dedc44964d Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Mon, 6 Feb 2023 15:29:10 +0100 Subject: [PATCH 12/14] fix cloud-init iso detection --- src/main.rs | 546 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 323 insertions(+), 223 deletions(-) diff --git a/src/main.rs b/src/main.rs index afa6c3f..f7913ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,40 +4,40 @@ * */ -mod zpool; mod common; -mod userdata; -mod public_keys; mod file; +mod public_keys; +mod userdata; +mod zpool; +use anyhow::Result; +use base64::decode as base64Decode; +use flate2::read::GzDecoder; +use libc; +use serde::Deserialize; use std::collections::HashMap; +use std::ffi::{CString, OsStr, OsString}; use std::fs::{self, DirBuilder, File, OpenOptions}; use std::io::prelude::*; -use std::io::{ErrorKind, BufReader, BufRead, copy as IOCopy, Read, Write}; +use std::io::Error as IOError; +use std::io::Result as IOResult; +use std::io::{copy as IOCopy, BufRead, BufReader, ErrorKind, Read, Write}; use std::net::Ipv4Addr; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::MetadataExt; use std::os::unix::fs::{DirBuilderExt, PermissionsExt}; use std::path::{Path, PathBuf}; -use std::process::{Command}; +use std::process::Command; use userdata::read_user_data; -use flate2::read::GzDecoder; -use base64::{decode as base64Decode}; -use std::ffi::{CString, OsStr, OsString}; -use std::io::Error as IOError; -use std::io::Result as IOResult; -use std::os::unix::fs::MetadataExt; -use std::os::unix::ffi::OsStrExt; use users; -use libc; -use serde::Deserialize; use users::User; -use anyhow::{Result}; -use common::*; -use file::*; +use crate::userdata::cloudconfig::{UserConfig, WriteFileData, WriteFileEncoding}; +use crate::userdata::multiformat_deserialize::Multiformat; use crate::userdata::networkconfig::{NetworkDataV1Iface, NetworkDataV1Subnet}; use crate::userdata::UserData; -use crate::userdata::cloudconfig::{UserConfig, WriteFileEncoding, WriteFileData}; -use crate::userdata::multiformat_deserialize::Multiformat; +use common::*; +use file::*; const METADATA_DIR: &str = "/var/metadata"; const STAMP: &str = "/var/metadata/stamp"; @@ -124,10 +124,7 @@ fn smf_enable(log: &Logger, fmri: &str) -> Result<()> { fn dhcpinfo(log: &Logger, key: &str) -> Result> { info!(log, "exec: dhcpinfo {}", key); - let output = Command::new(DHCPINFO) - .env_clear() - .arg(key) - .output()?; + let output = Command::new(DHCPINFO).env_clear().arg(key).output()?; if !output.status.success() { bail!("dhcpinfo {} failed: {}", key, output.info()); @@ -164,7 +161,8 @@ fn smbios(log: &Logger) -> Result> { let mut u = "".to_string(); for l in String::from_utf8(output.stdout)?.lines() { - let t: Vec<_> = l.trim() + let t: Vec<_> = l + .trim() .splitn(2, ':') .map(|s| s.trim().to_string()) .collect(); @@ -184,7 +182,12 @@ fn smbios(log: &Logger) -> Result> { } } - Ok(Some(Smbios { manufacturer: m, product: p, version: v, uuid: u, })) + Ok(Some(Smbios { + manufacturer: m, + product: p, + version: v, + uuid: u, + })) } } @@ -196,10 +199,7 @@ enum Mdata { fn mdata_get(log: &Logger, key: &str) -> Result { info!(log, "mdata-get \"{}\"...", key); - let output = Command::new(MDATA_GET) - .env_clear() - .arg(key) - .output()?; + let output = Command::new(MDATA_GET).env_clear().arg(key).output()?; Ok(match output.status.code() { Some(0) => { @@ -258,12 +258,13 @@ fn mdata_probe(log: &Logger) -> Result { } fn read_json(p: &str) -> Result> -where for<'de> T: Deserialize<'de> +where + for<'de> T: Deserialize<'de>, { let s = read_file(p)?; match s { None => Ok(None), - Some(s) => Ok(serde_json::from_str(&s)?) + Some(s) => Ok(serde_json::from_str(&s)?), } } @@ -282,17 +283,17 @@ struct Mount { time: u64, } -#[derive(Debug,Deserialize)] +#[derive(Debug, Deserialize)] struct DNS { nameservers: Vec, } -#[derive(Debug,Deserialize)] +#[derive(Debug, Deserialize)] struct FloatingIP { active: bool, } -#[derive(Debug,Deserialize)] +#[derive(Debug, Deserialize)] struct IPv4 { ip_address: String, gateway: String, @@ -323,7 +324,7 @@ impl IPv4 { } } -#[derive(Debug,Deserialize)] +#[derive(Debug, Deserialize)] struct Interface { anchor_ipv4: Option, ipv4: IPv4, @@ -332,13 +333,13 @@ struct Interface { type_: String, } -#[derive(Debug,Deserialize)] +#[derive(Debug, Deserialize)] struct Interfaces { public: Option>, private: Option>, } -#[derive(Debug,Deserialize)] +#[derive(Debug, Deserialize)] struct DOMetadata { auth_key: String, dns: DNS, @@ -352,7 +353,7 @@ struct DOMetadata { user_data: Option, } -#[derive(Debug,Deserialize)] +#[derive(Debug, Deserialize)] struct SdcNic { mac: String, interface: String, @@ -377,9 +378,7 @@ impl SdcNic { */ fn mounts() -> Result> { let mnttab = read_lines("/etc/mnttab")?.unwrap(); - let rows: Vec> = mnttab.iter() - .map(|m| { m.split('\t').collect() }) - .collect(); + let rows: Vec> = mnttab.iter().map(|m| m.split('\t').collect()).collect(); assert!(rows.len() >= 5); @@ -461,8 +460,12 @@ fn find_cpio_device(log: &Logger) -> Result> { out.push(ent.path()); } } - Err(e) => warn!(log, "detecting archive on {}: {:?}", - ent.path().display(), e), + Err(e) => warn!( + log, + "detecting archive on {}: {:?}", + ent.path().display(), + e + ), _ => {} } } @@ -510,7 +513,6 @@ fn find_cidata_devices(log: &Logger) -> Result>> { continue; } - let mut dev = CiDataDevice::default(); dev.path = ent.path().to_str().unwrap().to_string(); let mut count = 0; @@ -521,11 +523,16 @@ fn find_cidata_devices(log: &Logger) -> Result>> { } if line.contains(":") { if let Some((key, value)) = line.split_once(":") { - if key.trim().to_lowercase() == "volume_label" { + if key.trim().to_lowercase() == "volume_label" + || key.trim().to_lowercase() == "volume_id" + { let label = value.replace("'", "").trim().to_lowercase(); if label == "cidata" { dev.label = label; - debug!(log, "found cidata device {} of type {}", &dev.path, &dev.fstype); + debug!( + log, + "found cidata device {} of type {}", &dev.path, &dev.fstype + ); out.push(dev.clone()); } } @@ -561,10 +568,7 @@ fn find_device() -> Result> { /* * Determine which type of file system resides on the device: */ - let output = Command::new(FSTYP) - .env_clear() - .arg(ent.path()) - .output()?; + let output = Command::new(FSTYP).env_clear().arg(ent.path()).output()?; if !output.status.success() { continue; @@ -663,7 +667,7 @@ fn netmask_to_cidr(netmask: &Option) -> Result { 248 => Ok(5), 252 => Ok(6), 254 => Ok(7), - _ => bail!("invalid netmask: {}", mask) + _ => bail!("invalid netmask: {}", mask), } } else if octects[1] != 255 { match octects[1] { @@ -675,7 +679,7 @@ fn netmask_to_cidr(netmask: &Option) -> Result { 248 => Ok(13), 252 => Ok(14), 254 => Ok(15), - _ => bail!("invalid netmask: {}", mask) + _ => bail!("invalid netmask: {}", mask), } } else if octects[2] != 255 { match octects[2] { @@ -687,7 +691,7 @@ fn netmask_to_cidr(netmask: &Option) -> Result { 248 => Ok(21), 252 => Ok(22), 254 => Ok(23), - _ => bail!("invalid netmask: {}", mask) + _ => bail!("invalid netmask: {}", mask), } } else if octects[3] != 255 { match octects[3] { @@ -699,7 +703,7 @@ fn netmask_to_cidr(netmask: &Option) -> Result { 248 => Ok(29), 252 => Ok(30), 254 => Ok(31), - _ => bail!("invalid netmask: {}", mask) + _ => bail!("invalid netmask: {}", mask), } } else { Ok(32) @@ -713,7 +717,8 @@ fn ipadm_interface_list() -> Result> { .env_clear() .arg("show-if") .arg("-p") - .arg("-o").arg("ifname") + .arg("-o") + .arg("ifname") .output()?; if !output.status.success() { @@ -738,7 +743,8 @@ fn ipadm_address_list() -> Result> { .env_clear() .arg("show-addr") .arg("-p") - .arg("-o").arg("addrobj,type,state,addr") + .arg("-o") + .arg("addrobj,type,state,addr") .output()?; if !output.status.success() { @@ -747,12 +753,15 @@ fn ipadm_address_list() -> Result> { let ents = parse_net_adm(output.stdout)?; - Ok(ents.iter().map(|ent| IpadmAddress { - name: ent[0].to_string(), - type_: ent[1].to_string(), - state: ent[2].to_string(), - cidr: ent[3].to_string(), - }).collect()) + Ok(ents + .iter() + .map(|ent| IpadmAddress { + name: ent[0].to_string(), + type_: ent[1].to_string(), + state: ent[2].to_string(), + cidr: ent[3].to_string(), + }) + .collect()) } fn mac_sanitise(input: &str) -> String { @@ -785,7 +794,8 @@ fn dladm_ether_list() -> Result> { .env_clear() .arg("show-ether") .arg("-p") - .arg("-o").arg("link") + .arg("-o") + .arg("link") .output()?; if !output.status.success() { @@ -802,7 +812,8 @@ fn mac_to_nic(mac: &str) -> Result> { .arg("show-phys") .arg("-m") .arg("-p") - .arg("-o").arg("link,address") + .arg("-o") + .arg("link,address") .output()?; if !output.status.success() { @@ -845,7 +856,8 @@ fn create_zvol(name: &str, size_mib: u64) -> Result<()> { let output = std::process::Command::new(ZFS) .env_clear() .arg("create") - .arg("-V").arg(format!("{}m", size_mib)) + .arg("-V") + .arg(format!("{}m", size_mib)) .arg(name) .output()?; @@ -861,7 +873,8 @@ fn exists_zvol(name: &str) -> Result { .env_clear() .arg("list") .arg("-Hp") - .arg("-o").arg("name,type") + .arg("-o") + .arg("name,type") .output()?; if !output.status.success() { @@ -888,9 +901,7 @@ fn exists_zvol(name: &str) -> Result { } fn swapadd() -> Result<()> { - let output = std::process::Command::new(SWAPADD) - .env_clear() - .output()?; + let output = std::process::Command::new(SWAPADD).env_clear().output()?; if !output.status.success() { bail!("swapadd failed: {}", output.info()); @@ -927,7 +938,11 @@ fn ensure_interface_name(log: &Logger, name: &str, mac_addres: &str) -> Result<( Ok(()) } -fn ensure_ipadm_subnets_config(log: &Logger, link_name: &str, subnets: &Vec) -> Result<()> { +fn ensure_ipadm_subnets_config( + log: &Logger, + link_name: &str, + subnets: &Vec, +) -> Result<()> { info!(log, "configuring subnets for link {}", link_name); for subnet in subnets { if let Err(e) = ensure_ipadm_single_subnet_config(log, link_name, subnet) { @@ -938,7 +953,11 @@ fn ensure_ipadm_subnets_config(log: &Logger, link_name: &str, subnets: &Vec Result<()> { +fn ensure_ipadm_single_subnet_config( + log: &Logger, + link_name: &str, + subnet: &NetworkDataV1Subnet, +) -> Result<()> { match subnet { NetworkDataV1Subnet::Dhcp4 | NetworkDataV1Subnet::Dhcp => { ensure_ipv4_interface_dhcp(log, "dhcp4", link_name) @@ -972,7 +991,12 @@ fn ensure_ipadm_single_subnet_config(log: &Logger, link_name: &str, subnet: &Net } if let Some(routes) = &conf.routes { - error!(log, "route configuration not yet wupported, cannot apply {:#?} to link {}", routes, link_name); + error!( + log, + "route configuration not yet wupported, cannot apply {:#?} to link {}", + routes, + link_name + ); } Ok(()) @@ -1063,9 +1087,7 @@ fn ensure_ipv4_gateway(log: &Logger, gateway: &str) -> Result<()> { Ok(()) } -fn ensure_ipv4_interface_dhcp(log: &Logger, sfx: &str, n: &str) - -> Result<()> -{ +fn ensure_ipv4_interface_dhcp(log: &Logger, sfx: &str, n: &str) -> Result<()> { info!(log, "ENSURE IPv4 DHCP INTERFACE: {}", n); ensure_ipadm_interface(log, &n)?; @@ -1090,7 +1112,10 @@ fn ensure_ipv4_interface_dhcp(log: &Logger, sfx: &str, n: &str) } if name_found && !address_found { - info!(log, "ipadm address exists but with wrong IP address, deleting"); + info!( + log, + "ipadm address exists but with wrong IP address, deleting" + ); let output = Command::new(IPADM) .env_clear() .arg("delete-addr") @@ -1107,9 +1132,11 @@ fn ensure_ipv4_interface_dhcp(log: &Logger, sfx: &str, n: &str) let output = Command::new(IPADM) .env_clear() .arg("create-addr") - .arg("-T").arg("dhcp") + .arg("-T") + .arg("dhcp") .arg("-1") - .arg("-w").arg("10") + .arg("-w") + .arg("10") .arg(&targname) .output()?; @@ -1129,8 +1156,10 @@ fn ensure_ipv4_interface_dhcp(log: &Logger, sfx: &str, n: &str) if let Some(addr) = addr { if addr.state == "ok" { - info!(log, "ok, interface {} address {} ({}) complete", - n, addr.cidr, sfx); + info!( + log, + "ok, interface {} address {} ({}) complete", n, addr.cidr, sfx + ); return Ok(()); } } else { @@ -1142,17 +1171,20 @@ fn ensure_ipv4_interface_dhcp(log: &Logger, sfx: &str, n: &str) } } -fn ensure_ipv4_interface(log: &Logger, sfx: &str, mac_option: Option<&str>, link_name: Option<&str>, ipv4: &str) - -> Result<()> -{ - +fn ensure_ipv4_interface( + log: &Logger, + sfx: &str, + mac_option: Option<&str>, + link_name: Option<&str>, + ipv4: &str, +) -> Result<()> { let found_nic = if let Some(mac) = mac_option { match mac_to_nic(mac)? { None => bail!("MAC address {} not found", mac), Some(n) => { info!(log, "MAC address {} is NIC {}", mac, n); n - }, + } } } else if let Some(name) = link_name { String::from(name) @@ -1186,7 +1218,10 @@ fn ensure_ipv4_interface(log: &Logger, sfx: &str, mac_option: Option<&str>, link } if name_found && !address_found { - info!(log, "ipadm address exists but with wrong IP address, deleting"); + info!( + log, + "ipadm address exists but with wrong IP address, deleting" + ); let output = Command::new(IPADM) .env_clear() .arg("delete-addr") @@ -1203,18 +1238,27 @@ fn ensure_ipv4_interface(log: &Logger, sfx: &str, mac_option: Option<&str>, link let output = Command::new(IPADM) .env_clear() .arg("create-addr") - .arg("-T").arg("static") - .arg("-a").arg(ipv4) + .arg("-T") + .arg("static") + .arg("-a") + .arg(ipv4) .arg(&targname) .output()?; if !output.status.success() { - bail!("ipadm create-addr {} {}: {}", &targname, ipv4, - output.info()); + bail!( + "ipadm create-addr {} {}: {}", + &targname, + ipv4, + output.info() + ); } } - info!(log, "ok, interface {} address {} ({}) complete", found_nic, ipv4, sfx); + info!( + log, + "ok, interface {} address {} ({}) complete", found_nic, ipv4, sfx + ); Ok(()) } @@ -1250,7 +1294,9 @@ fn run(log: &Logger) -> Result<()> { * First, expand the ZFS pool. We can do this prior to metadata access. */ phase_expand_zpool(log)?; - phase_add_swap(log).map_err(|e| error!(log, "add swap failed: {}", e)).ok(); + phase_add_swap(log) + .map_err(|e| error!(log, "add swap failed: {}", e)) + .ok(); /* * Try first to use SMBIOS information to determine what kind of hypervisor @@ -1284,16 +1330,12 @@ fn run(log: &Logger) -> Result<()> { } ("OmniOS", "OmniOS HVM") | ("OpenIndiana", "OpenIndiana HVM") => { info!(log, "hypervisor type: illumos BHYVE (from SMBIOS)"); - /* - * Skip networking under OmniOS for now, until we figure out the - * appropriate strategy for configuring networking in the guest. - */ run_illumos(log, &smbios.uuid)?; return Ok(()); } ("QEMU", _) => { info!(log, "hypervisor type: Generic QEMU (from SMBIOS)"); - run_generic(log, &smbios.uuid, true)?; + run_illumos(log, &smbios.uuid)?; return Ok(()); } ("VMware, Inc.", "VMware Virtual Platform") => { @@ -1341,12 +1383,19 @@ fn run_generic(log: &Logger, smbios_uuid: &str, network: bool) -> Result<()> { */ if let Some([id]) = read_lines(STAMP)?.as_deref() { if id.trim() == smbios_uuid { - info!(log, "this guest has already completed first \ - boot processing, halting"); + info!( + log, + "this guest has already completed first \ + boot processing, halting" + ); return Ok(()); } else { - info!(log, "guest UUID changed ({} -> {}), reprocessing", - id.trim(), smbios_uuid); + info!( + log, + "guest UUID changed ({} -> {}), reprocessing", + id.trim(), + smbios_uuid + ); } } @@ -1366,7 +1415,8 @@ fn run_generic(log: &Logger, smbios_uuid: &str, network: bool) -> Result<()> { let cpio = Command::new(CPIO) .arg("-i") .arg("-q") - .arg("-I").arg(&dev) + .arg("-I") + .arg(&dev) .current_dir(UNPACKDIR) .env_clear() .output()?; @@ -1425,7 +1475,7 @@ fn run_generic(log: &Logger, smbios_uuid: &str, network: bool) -> Result<()> { */ let keys = format!("{}/authorized_keys", UNPACKDIR); if let Some(keys) = read_lines(&keys)? { - ensure_pubkeys(log, "root",keys.as_slice())?; + ensure_pubkeys(log, "root", keys.as_slice())?; } /* @@ -1465,7 +1515,10 @@ fn parse_smbios_datasource_string(raw_string: &str) -> Result Ok(ds) } -fn ensure_network_config(log: &Logger, config: &userdata::networkconfig::NetworkConfig) -> Result<()> { +fn ensure_network_config( + log: &Logger, + config: &userdata::networkconfig::NetworkConfig, +) -> Result<()> { let net_config = match config { userdata::networkconfig::NetworkConfig::V1(c) => &c.config, userdata::networkconfig::NetworkConfig::V2(_) => { @@ -1477,7 +1530,12 @@ fn ensure_network_config(log: &Logger, config: &userdata::networkconfig::Network // First configure the Physical Interfaces for iface_config in net_config { match iface_config { - NetworkDataV1Iface::Physical { name, mac_address: mac_address_option, mtu: mtu_option, subnets: subnet_option } => { + NetworkDataV1Iface::Physical { + name, + mac_address: mac_address_option, + mtu: mtu_option, + subnets: subnet_option, + } => { // First we make sure the Interface is named correctly // if we have a mac address set. Thus making // the optional mac address attribute the @@ -1520,7 +1578,7 @@ fn ensure_network_config(log: &Logger, config: &userdata::networkconfig::Network //Configure Routes for iface_config in net_config { match iface_config { - NetworkDataV1Iface::Route {..} => { + NetworkDataV1Iface::Route { .. } => { bail!("routes not yet supported") } _ => {} @@ -1530,7 +1588,9 @@ fn ensure_network_config(log: &Logger, config: &userdata::networkconfig::Network //Finaly configure nameservers for iface_config in net_config { match iface_config { - NetworkDataV1Iface::Nameserver { address, search, .. } => { + NetworkDataV1Iface::Nameserver { + address, search, .. + } => { ensure_dns_nameservers(log, address)?; ensure_dns_search(log, search)?; } @@ -1553,12 +1613,19 @@ fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<()> { */ if let Some([id]) = read_lines(STAMP)?.as_deref() { if id.trim() == ds.uuid { - info!(log, "this guest has already completed first \ - boot processing, halting"); + info!( + log, + "this guest has already completed first \ + boot processing, halting" + ); return Ok(()); } else { - info!(log, "guest UUID changed ({} -> {}), reprocessing", - id.trim(), ds.uuid); + info!( + log, + "guest UUID changed ({} -> {}), reprocessing", + id.trim(), + ds.uuid + ); } } @@ -1575,7 +1642,8 @@ fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<()> { info!(log, "mounting cidata device {} to {}", dev.path, UNPACKDIR); ensure_dir(log, UNPACKDIR)?; let mount = Command::new(MOUNT) - .arg("-F").arg(&dev.fstype) + .arg("-F") + .arg(&dev.fstype) .arg(&dev.path) .arg(UNPACKDIR) .env_clear() @@ -1586,7 +1654,6 @@ fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<()> { } info!(log, "ok, disk mounted"); - } } else { bail!("could not find a cidata device bailing") @@ -1597,8 +1664,8 @@ fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<()> { let user_data = read_user_data(log, &dir_buf.join("user-data"))?; let meta_data_file = File::open(&dir_buf.join("meta-data"))?; - let meta_data = serde_yaml::from_reader::(meta_data_file)?; - + let meta_data = + serde_yaml::from_reader::(meta_data_file)?; /* * Get a system hostname from the metadata, if provided. Make sure to set @@ -1607,7 +1674,8 @@ fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<()> { */ phase_set_hostname(log, meta_data.get_hostname())?; - let network_config_result = userdata::networkconfig::parse_network_config( &dir_buf.join("network-config")); + let network_config_result = + userdata::networkconfig::parse_network_config(&dir_buf.join("network-config")); // Apply network config if we have one. if network_config_result.is_ok() { ensure_network_config(&log, &network_config_result?)?; @@ -1648,7 +1716,6 @@ fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<()> { */ phase_user_data(log, &user_data)?; - for script in user_data.scripts { /* * handle the userscripts we have in the user-data: @@ -1719,12 +1786,19 @@ fn run_amazon(log: &Logger) -> Result<()> { */ if let Some([id]) = read_lines(STAMP)?.as_deref() { if id.trim() == instid { - info!(log, "this guest has already completed first \ - boot processing, halting"); + info!( + log, + "this guest has already completed first \ + boot processing, halting" + ); return Ok(()); } else { - info!(log, "guest Instance ID changed ({} -> {}), reprocessing", - id.trim(), instid); + info!( + log, + "guest Instance ID changed ({} -> {}), reprocessing", + id.trim(), + instid + ); } } @@ -1748,7 +1822,7 @@ fn run_amazon(log: &Logger) -> Result<()> { */ if let Some(pk) = amazon_metadata_get(log, "public-keys/0/openssh-key")? { let pubkeys = vec![pk]; - ensure_pubkeys(log, "root",&pubkeys)?; + ensure_pubkeys(log, "root", &pubkeys)?; } else { warn!(log, "no SSH public key?"); } @@ -1758,7 +1832,8 @@ fn run_amazon(log: &Logger) -> Result<()> { */ if let Some(userscript) = amazon_metadata_getx(log, "user-data")? { phase_userscript(log, &userscript) - .map_err(|e| error!(log, "failed to get user-script: {}", e)).ok(); + .map_err(|e| error!(log, "failed to get user-script: {}", e)) + .ok(); } else { info!(log, "no user-data?"); } @@ -1780,12 +1855,19 @@ fn run_smartos(log: &Logger) -> Result<()> { */ if let Some([id]) = read_lines(STAMP)?.as_deref() { if id.trim() == uuid { - info!(log, "this guest has already completed first \ - boot processing, halting"); + info!( + log, + "this guest has already completed first \ + boot processing, halting" + ); return Ok(()); } else { - info!(log, "guest UUID changed ({} -> {}), reprocessing", - id.trim(), uuid); + info!( + log, + "guest UUID changed ({} -> {}), reprocessing", + id.trim(), + uuid + ); } } @@ -1802,7 +1884,9 @@ fn run_smartos(log: &Logger) -> Result<()> { uuid } else { bail!("could not get hostname or alias or UUID for this VM"); - }.trim().to_string(); + } + .trim() + .to_string(); info!(log, "VM node name is \"{}\"", n); phase_set_hostname(log, &n)?; @@ -1818,16 +1902,13 @@ fn run_smartos(log: &Logger) -> Result<()> { /* * XXX handle these. */ - error!(log, "interface {} requires {} support", - nic.interface, ip); + error!(log, "interface {} requires {} support", nic.interface, ip); continue; } let sfx = format!("ip{}", i); - if let Err(e) = ensure_ipv4_interface(log, &sfx, Some(&nic.mac), - None, &ip) - { + if let Err(e) = ensure_ipv4_interface(log, &sfx, Some(&nic.mac), None, &ip) { error!(log, "IFACE {}/{} ERROR: {}", nic.interface, sfx, e); } } @@ -1855,11 +1936,9 @@ fn run_smartos(log: &Logger) -> Result<()> { * Get public keys: */ if let Mdata::Found(pubkeys) = mdata_get(log, "root_authorized_keys")? { - let pubkeys: Vec = pubkeys.lines() - .map(|s| s.trim().to_string()) - .collect(); + let pubkeys: Vec = pubkeys.lines().map(|s| s.trim().to_string()).collect(); - ensure_pubkeys(log, "root",&pubkeys)?; + ensure_pubkeys(log, "root", &pubkeys)?; } /* @@ -1867,7 +1946,8 @@ fn run_smartos(log: &Logger) -> Result<()> { */ if let Mdata::Found(userscript) = mdata_get(log, "user-script")? { phase_userscript(log, &userscript) - .map_err(|e| error!(log, "failed to get user-script: {}", e)).ok(); + .map_err(|e| error!(log, "failed to get user-script: {}", e)) + .ok(); } write_lines(log, STAMP, &[uuid])?; @@ -1882,8 +1962,10 @@ fn run_digitalocean(log: &Logger) -> Result<()> { * this droplet or not. */ let mounts = mounts()?; - let mdmp: Vec<_> = mounts.iter() - .filter(|m| { m.mount_point == MOUNTPOINT }).collect(); + let mdmp: Vec<_> = mounts + .iter() + .filter(|m| m.mount_point == MOUNTPOINT) + .collect(); let do_mount = match mdmp.as_slice() { [] => true, @@ -1914,7 +1996,8 @@ fn run_digitalocean(log: &Logger) -> Result<()> { let output = Command::new(MOUNT) .env_clear() - .arg("-F").arg("hsfs") + .arg("-F") + .arg("hsfs") .arg(dev) .arg(MOUNTPOINT) .output()?; @@ -1929,8 +2012,7 @@ fn run_digitalocean(log: &Logger) -> Result<()> { /* * Read metadata from the file system: */ - let md: Option = read_json( - &format!("{}/digitalocean_meta_data.json", MOUNTPOINT))?; + let md: Option = read_json(&format!("{}/digitalocean_meta_data.json", MOUNTPOINT))?; let md = if let Some(md) = md { md @@ -1947,12 +2029,19 @@ fn run_digitalocean(log: &Logger) -> Result<()> { let expected = md.droplet_id.to_string(); if id.trim() == expected { - info!(log, "this droplet has already completed first \ - boot processing, halting"); + info!( + log, + "this droplet has already completed first \ + boot processing, halting" + ); return Ok(()); } else { - info!(log, "droplet ID changed ({} -> {}), reprocessing", - id.trim(), expected); + info!( + log, + "droplet ID changed ({} -> {}), reprocessing", + id.trim(), + expected + ); } } @@ -1967,8 +2056,8 @@ fn run_digitalocean(log: &Logger) -> Result<()> { continue; } - if let Err(e) = ensure_ipv4_interface(log, "private", Some(&iface.mac), - None, &iface.ipv4.cidr()?) + if let Err(e) = + ensure_ipv4_interface(log, "private", Some(&iface.mac), None, &iface.ipv4.cidr()?) { /* * Report the error, but drive on in case we can complete other @@ -1983,8 +2072,8 @@ fn run_digitalocean(log: &Logger) -> Result<()> { continue; } - if let Err(e) = ensure_ipv4_interface(log, "public", Some(&iface.mac), - None, &iface.ipv4.cidr()?) + if let Err(e) = + ensure_ipv4_interface(log, "public", Some(&iface.mac), None, &iface.ipv4.cidr()?) { /* * Report the error, but drive on in case we can complete other @@ -1998,8 +2087,8 @@ fn run_digitalocean(log: &Logger) -> Result<()> { } if let Some(anchor) = &iface.anchor_ipv4 { - if let Err(e) = ensure_ipv4_interface(log, "anchor", Some(&iface.mac), - None, &anchor.cidr()?) + if let Err(e) = + ensure_ipv4_interface(log, "anchor", Some(&iface.mac), None, &anchor.cidr()?) { error!(log, "ANCHOR IFACE ERROR: {}", e); } @@ -2007,7 +2096,7 @@ fn run_digitalocean(log: &Logger) -> Result<()> { } ensure_dns_nameservers(log, &md.dns.nameservers)?; - ensure_pubkeys(log, "root",md.public_keys.as_slice())?; + ensure_pubkeys(log, "root", md.public_keys.as_slice())?; /* * Get userscript: @@ -2114,21 +2203,17 @@ fn phase_set_hostname(log: &Logger, hostname: &str) -> Result<()> { if write_nodename { info!(log, "WRITE NODENAME \"{}\"", hostname); - let status = Command::new(HOSTNAME) - .env_clear() - .arg(hostname) - .status()?; + let status = Command::new(HOSTNAME).env_clear().arg(hostname).status()?; if !status.success() { - error!(log, "could not set live system hostname"); + error!(log, "could not set live system hostname"); } /* * Write the file after we set the live system hostname, so that if we * are restarted we don't forget to do that part. */ - write_lines(log, "/etc/nodename", &[ hostname ])?; - + write_lines(log, "/etc/nodename", &[hostname])?; } else { info!(log, "NODENAME \"{}\" OK ALREADY", hostname); } @@ -2137,60 +2222,59 @@ fn phase_set_hostname(log: &Logger, hostname: &str) -> Result<()> { * Write /etc/hosts file with new nodename... */ let hosts = read_lines("/etc/inet/hosts")?.unwrap(); - let hostsout: Vec = hosts.iter().map(|l| { - /* - * Split the line into a substantive portion and an optional comment. - */ - let sect: Vec<&str> = l.splitn(2, '#').collect(); - - let mut fore = sect[0].to_string(); - - if !sect[0].trim().is_empty() { + let hostsout: Vec = hosts + .iter() + .map(|l| { /* - * If the line has a substantive portion, split that into an IP - * address and a set of host names: + * Split the line into a substantive portion and an optional comment. */ - let portions: Vec<&str> = sect[0] - .splitn(2, |c| c == ' ' || c == '\t') - .collect(); + let sect: Vec<&str> = l.splitn(2, '#').collect(); - if portions.len() > 1 { + let mut fore = sect[0].to_string(); + + if !sect[0].trim().is_empty() { /* - * Rewrite only the localhost entry, to include the system node - * name. This essentially matches the OmniOS out-of-box file - * contents. + * If the line has a substantive portion, split that into an IP + * address and a set of host names: */ - if portions[0] == "127.0.0.1" || portions[0] == "::1" { - let mut hosts = String::new(); - hosts.push_str(portions[0]); - if portions[0] == "::1" { - hosts.push('\t'); - } - hosts.push_str("\tlocalhost"); - if portions[0] == "127.0.0.1" { - hosts.push_str(" loghost"); - } - hosts.push_str(&format!(" {}.local {}", - hostname, hostname)); + let portions: Vec<&str> = sect[0].splitn(2, |c| c == ' ' || c == '\t').collect(); + + if portions.len() > 1 { + /* + * Rewrite only the localhost entry, to include the system node + * name. This essentially matches the OmniOS out-of-box file + * contents. + */ + if portions[0] == "127.0.0.1" || portions[0] == "::1" { + let mut hosts = String::new(); + hosts.push_str(portions[0]); + if portions[0] == "::1" { + hosts.push('\t'); + } + hosts.push_str("\tlocalhost"); + if portions[0] == "127.0.0.1" { + hosts.push_str(" loghost"); + } + hosts.push_str(&format!(" {}.local {}", hostname, hostname)); - fore = hosts; + fore = hosts; + } } } - } - if sect.len() > 1 { - format!("{}#{}", fore, sect[1]) - } else { - fore - } - }).collect(); + if sect.len() > 1 { + format!("{}#{}", fore, sect[1]) + } else { + fore + } + }) + .collect(); write_lines(log, "/etc/inet/hosts", &hostsout)?; Ok(()) } fn phase_user_data(log: &Logger, user_data: &UserData) -> Result<()> { - //First Apply cloud configurations for cc in user_data.cloud_configs.clone() { /* @@ -2298,14 +2382,20 @@ fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<()> { } if let Some(mode_string) = &file.permissions { - info!(log, "setting permissions of file {} to {}", file.path, mode_string); + info!( + log, + "setting permissions of file {} to {}", file.path, mode_string + ); let meta = &f.metadata()?; let mut perms = meta.permissions(); perms.set_mode(mode_string.parse::()?); } if let Some(owner) = &file.owner { - info!(log, "setting owner and group of file {} to {}", file.path, owner); + info!( + log, + "setting owner and group of file {} to {}", file.path, owner + ); let mut uid: users::uid_t; let mut gid: users::gid_t; if owner.contains(":") { @@ -2340,7 +2430,12 @@ fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<()> { } /// Actually perform the change of owner on a path -fn chown>(path: P, uid: libc::uid_t, gid: libc::gid_t, follow: bool) -> IOResult<()> { +fn chown>( + path: P, + uid: libc::uid_t, + gid: libc::gid_t, + follow: bool, +) -> IOResult<()> { let path = path.as_ref(); let s = CString::new(path.as_os_str().as_bytes()).unwrap(); let ret = unsafe { @@ -2357,7 +2452,6 @@ fn chown>(path: P, uid: libc::uid_t, gid: libc::gid_t, follow: bo } } - fn ensure_dns_nameservers(log: &Logger, nameservers: &[String]) -> Result<()> { /* * DNS Servers: @@ -2380,9 +2474,7 @@ fn ensure_dns_nameservers(log: &Logger, nameservers: &[String]) -> Result<()> { for l in &lines { let ll: Vec<_> = l.splitn(2, ' ').collect(); - if ll.len() == 2 && ll[0] == "nameserver" && - !nameservers.contains(&ll[1].to_string()) - { + if ll.len() == 2 && ll[0] == "nameserver" && !nameservers.contains(&ll[1].to_string()) { info!(log, "REMOVE DNS CONFIG LINE: {}", l); file.push(format!("#{}", l)); dirty = true; @@ -2420,9 +2512,7 @@ fn ensure_dns_search(log: &Logger, dns_search: &[String]) -> Result<()> { for l in &lines { let ll: Vec<_> = l.splitn(2, ' ').collect(); - if ll.len() == 2 && ll[0] == "search" && - !dns_search.contains(&ll[1].to_string()) - { + if ll.len() == 2 && ll[0] == "search" && !dns_search.contains(&ll[1].to_string()) { info!(log, "REMOVE DNS Search CONFIG LINE: {}", l); file.push(format!("#{}", l)); dirty = true; @@ -2520,7 +2610,6 @@ fn ensure_user(log: &Logger, user: &UserConfig) -> Result<()> { if !output.status.success() { bail!("useradd failed for {}: {}", &user.name, output.info()); } - } else { info!(log, "user with name {} exists skipping", &user.name); } @@ -2548,21 +2637,32 @@ fn ensure_group(log: &Logger, groups: HashMap>>) -> R if let Some(users_in_group) = users_in_group_opt { for user in users_in_group { - let existing_groups: Vec = if let Some(sys_user) = users::get_user_by_name(&user) { - if let Some(user_groups) = users::get_user_groups(&user, sys_user.primary_group_id()) { - user_groups.iter().map(|g| - g.name().to_os_string().into_string().unwrap_or(String::new()) - ).collect() + let existing_groups: Vec = + if let Some(sys_user) = users::get_user_by_name(&user) { + if let Some(user_groups) = + users::get_user_groups(&user, sys_user.primary_group_id()) + { + user_groups + .iter() + .map(|g| { + g.name() + .to_os_string() + .into_string() + .unwrap_or(String::new()) + }) + .collect() + } else { + vec![] + } } else { vec![] - } - } else { - vec![] - }; + }; - let mut user_groups: Vec = existing_groups.iter().filter(|&g| - g.clone() != String::new() - ).map(|g| g.clone()).collect(); + let mut user_groups: Vec = existing_groups + .iter() + .filter(|&g| g.clone() != String::new()) + .map(|g| g.clone()) + .collect(); user_groups.push(group_name.clone()); let mut cmd = Command::new(USERMOD); From e0ed740da1fd99324f513e7eae602c8ae0485aea Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Mon, 6 Feb 2023 19:08:01 +0100 Subject: [PATCH 13/14] Fix trying to mount mutiple cidata devices; Fix imports and warings Signed-off-by: Till Wegmueller --- src/file.rs | 24 +++++++---------- src/main.rs | 22 ++++++++------- src/public_keys.rs | 38 -------------------------- src/userdata.rs | 53 +++++++++++++++++-------------------- src/userdata/cloudconfig.rs | 11 ++++---- 5 files changed, 53 insertions(+), 95 deletions(-) delete mode 100644 src/public_keys.rs diff --git a/src/file.rs b/src/file.rs index 7813b2d..0be5c9d 100644 --- a/src/file.rs +++ b/src/file.rs @@ -3,17 +3,15 @@ */ use crate::common::*; +use anyhow::Result; use std::fs::{DirBuilder, File}; -use std::os::unix::fs::{DirBuilderExt, PermissionsExt}; -use std::io::{ErrorKind, BufReader, BufRead, copy as IOCopy, Read, Write}; -use anyhow::{Result}; +use std::io::{ErrorKind, Read, Write}; +use std::os::unix::fs::DirBuilderExt; pub fn ensure_dir(log: &Logger, path: &str) -> Result<()> { if !exists_dir(path)? { info!(log, "mkdir {}", path); - DirBuilder::new() - .mode(0o700) - .create(path)?; + DirBuilder::new().mode(0o700).create(path)?; } Ok(()) } @@ -50,11 +48,8 @@ pub fn exists_file(p: &str) -> Result { Ok(true) } - pub fn read_lines(p: &str) -> Result>> { - Ok(read_file(p)?.map(|data| { - data.lines().map(|a| a.trim().to_string()).collect() - })) + Ok(read_file(p)?.map(|data| data.lines().map(|a| a.trim().to_string()).collect())) } pub fn read_lines_maybe(p: &str) -> Result> { @@ -64,9 +59,7 @@ pub fn read_lines_maybe(p: &str) -> Result> { }) } -pub - -fn read_file(p: &str) -> Result> { +pub fn read_file(p: &str) -> Result> { let f = match File::open(p) { Ok(f) => f, Err(e) => { @@ -94,7 +87,8 @@ pub fn write_file(p: &str, data: &str) -> Result<()> { } pub fn write_lines(log: &Logger, p: &str, lines: &[L]) -> Result<()> - where L: AsRef + std::fmt::Debug +where + L: AsRef + std::fmt::Debug, { info!(log, "----- WRITE FILE: {} ------ {:#?}", p, lines); let mut out = String::new(); @@ -103,4 +97,4 @@ pub fn write_lines(log: &Logger, p: &str, lines: &[L]) -> Result<()> out.push_str("\n"); } write_file(p, &out) -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index f7913ea..28e70a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,6 @@ mod common; mod file; -mod public_keys; mod userdata; mod zpool; @@ -16,21 +15,20 @@ use flate2::read::GzDecoder; use libc; use serde::Deserialize; use std::collections::HashMap; -use std::ffi::{CString, OsStr, OsString}; -use std::fs::{self, DirBuilder, File, OpenOptions}; -use std::io::prelude::*; +use std::ffi::CString; +use std::fs::{self, File, OpenOptions}; + use std::io::Error as IOError; use std::io::Result as IOResult; -use std::io::{copy as IOCopy, BufRead, BufReader, ErrorKind, Read, Write}; +use std::io::{copy as IOCopy, BufRead, BufReader, Read, Write}; use std::net::Ipv4Addr; use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::MetadataExt; -use std::os::unix::fs::{DirBuilderExt, PermissionsExt}; +use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::process::Command; use userdata::read_user_data; use users; -use users::User; use crate::userdata::cloudconfig::{UserConfig, WriteFileData, WriteFileEncoding}; use crate::userdata::multiformat_deserialize::Multiformat; @@ -274,6 +272,7 @@ enum MountOptionValue { Value(String), } +#[allow(dead_code)] #[derive(Debug)] struct Mount { special: String, @@ -288,6 +287,7 @@ struct DNS { nameservers: Vec, } +#[allow(dead_code)] #[derive(Debug, Deserialize)] struct FloatingIP { active: bool, @@ -339,6 +339,7 @@ struct Interfaces { private: Option>, } +#[allow(dead_code)] #[derive(Debug, Deserialize)] struct DOMetadata { auth_key: String, @@ -353,6 +354,7 @@ struct DOMetadata { user_data: Option, } +#[allow(dead_code)] #[derive(Debug, Deserialize)] struct SdcNic { mac: String, @@ -1638,7 +1640,7 @@ fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<()> { info!(log, "searching for cidata device with metadata..."); let devs_opt = find_cidata_devices(log)?; if let Some(devs) = devs_opt { - for dev in devs { + if let Some(dev) = devs.first() { info!(log, "mounting cidata device {} to {}", dev.path, UNPACKDIR); ensure_dir(log, UNPACKDIR)?; let mount = Command::new(MOUNT) @@ -2368,7 +2370,7 @@ fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<()> { } WriteFileEncoding::Gzip => { trace!(log, "writing gzip encoded data"); - let mut content_clone = file.content.clone(); + let content_clone = file.content.clone(); let mut conent_bytes = content_clone.as_str().as_bytes(); let mut d = GzDecoder::new(BufReader::new(&mut conent_bytes)); IOCopy(&mut d, &mut w)?; @@ -2396,7 +2398,9 @@ fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<()> { log, "setting owner and group of file {} to {}", file.path, owner ); + #[allow(unused_mut)] let mut uid: users::uid_t; + #[allow(unused_mut)] let mut gid: users::gid_t; if owner.contains(":") { if let Some((u, g)) = owner.split_once(":") { diff --git a/src/public_keys.rs b/src/public_keys.rs deleted file mode 100644 index 9b0d26a..0000000 --- a/src/public_keys.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::common::*; -use crate::file::*; -use anyhow::{Result}; - -pub fn ensure_pubkeys(log: &Logger, user: &str, public_keys: &[String]) -> Result<()> { - /* - * Manage the public keys: - */ - info!(log, "checking SSH public keys for user {}...", user); - - let sshdir = if user == "root" { - format!("/root/.ssh") - } else { - format!("/export/home/{}/.ssh", user) - }; - - ensure_dir(log, &sshdir)?; - - let authorized_keys = sshdir + "/authorized_keys"; - - let mut file = read_lines_maybe(&authorized_keys)?; - info!(log, "existing SSH public keys: {:#?}", &file); - - let mut dirty = false; - for key in public_keys.iter() { - if !file.contains(key) { - info!(log, "add SSH public key: {}", key); - file.push(key.to_string()); - dirty = true; - } - } - - if dirty { - write_lines(log, &authorized_keys, file.as_ref())?; - } - - Ok(()) -} \ No newline at end of file diff --git a/src/userdata.rs b/src/userdata.rs index 5b71390..db8f08d 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -2,36 +2,34 @@ * Copyright 2021 OpenFlowLabs * */ +use crate::common::*; +use anyhow::Result; use cloudconfig::CloudConfig; -use std::io::prelude::*; use flate2::read::GzDecoder; +use std::fs::File; +use std::io::prelude::*; use std::io::BufReader; use std::path::PathBuf; -use std::fs::File; -use crate::common::*; -use anyhow::{Result}; -use thiserror::{Error}; +use thiserror::Error; pub mod cloudconfig; -pub mod networkconfig; pub mod multiformat_deserialize; +pub mod networkconfig; pub fn read_user_data(log: &Logger, path: &PathBuf) -> Result { // Parse Multipart message from stream match read_gz_data(log, path) { Ok(data) => Ok(data), - Err(err) => { - match err.downcast::() { - Ok(uerr) => { - if uerr == UserDataError::NotGzData { - Ok(read_uncompressed(log, path)?) - } else { - Err(uerr)? - } + Err(err) => match err.downcast::() { + Ok(uerr) => { + if uerr == UserDataError::NotGzData { + Ok(read_uncompressed(log, path)?) + } else { + Err(uerr)? } - Err(oerr) => Err(oerr) } - } + Err(oerr) => Err(oerr), + }, } } @@ -51,7 +49,6 @@ fn read_uncompressed(log: &Logger, path: &PathBuf) -> Result { } fn parse_user_data_multipart_stream(log: &Logger, stream: &mut S) -> Result { - let mut buf = Vec::new(); stream.read_to_end(&mut buf)?; @@ -74,9 +71,7 @@ fn parse_file_part(log: &Logger, d: &mut UserData, mime_type: &str, buf: &str) - let cc = serde_yaml::from_str::(buf)?; d.cloud_configs.push(cc); } - "text/x-shellscript" => { - d.scripts.push(buf.into()) - } + "text/x-shellscript" => d.scripts.push(buf.into()), _ => { info!(log, "unsupported mime type {}, skipping", mime_type); } @@ -87,10 +82,6 @@ fn parse_file_part(log: &Logger, d: &mut UserData, mime_type: &str, buf: &str) - #[derive(Debug, Error, PartialEq)] enum UserDataError { - #[error("format of file {} is not parseable", path)] - InvalidFile{ - path: String, - }, #[error("file is not compressed")] NotGzData, } @@ -103,15 +94,18 @@ pub struct UserData { #[cfg(test)] mod tests { + use crate::common::init_log; + use crate::userdata::UserData; use std::path::PathBuf; use std::str::FromStr; - use crate::userdata::UserData; - use crate::common::init_log; #[test] fn multipart_parse() { let log = init_log(); - let res = crate::userdata::read_user_data(&log, &PathBuf::from_str("./sample_data/mime_message.txt").unwrap()); + let res = crate::userdata::read_user_data( + &log, + &PathBuf::from_str("./sample_data/mime_message.txt").unwrap(), + ); let udata = res.unwrap(); assert_ne!(udata, UserData::default()); } @@ -119,7 +113,10 @@ mod tests { #[test] fn userdata_parse() { let log = init_log(); - let res = crate::userdata::read_user_data(&log, &PathBuf::from_str("./sample_data/user-data").unwrap()); + let res = crate::userdata::read_user_data( + &log, + &PathBuf::from_str("./sample_data/user-data").unwrap(), + ); let udata = res.unwrap(); assert_ne!(udata, UserData::default()); } diff --git a/src/userdata/cloudconfig.rs b/src/userdata/cloudconfig.rs index 89da868..512ea4e 100644 --- a/src/userdata/cloudconfig.rs +++ b/src/userdata/cloudconfig.rs @@ -2,9 +2,9 @@ * Copyright 2021 OpenFlowLabs * */ -use std::collections::HashMap; -use serde::{Deserialize}; use crate::userdata::multiformat_deserialize::Multiformat; +use serde::Deserialize; +use std::collections::HashMap; #[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] pub struct Metadata { @@ -39,6 +39,7 @@ impl Metadata { "" } + #[allow(dead_code)] pub fn get_public_hostname(&self) -> &str { if let Some(name) = &self.public_hostname { return name; @@ -105,7 +106,7 @@ pub enum GrowPartMode { #[serde(rename = "growpart")] Growpart, #[serde(rename = "off")] - Off + Off, } #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] @@ -122,7 +123,7 @@ pub enum PowerStateMode { #[serde(rename = "halt")] Halt, #[serde(rename = "reboot")] - Reboot + Reboot, } #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] @@ -154,7 +155,7 @@ pub enum WriteFileEncoding { None, B64, Gzip, - B64Gzip + B64Gzip, } #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] From f8aa5e9afebe9441c42d2bda57aec361fc3fc2f9 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Tue, 7 Feb 2023 17:07:42 +0100 Subject: [PATCH 14/14] Split user creation and package installation ine apropriate runlevel Signed-off-by: Till Wegmueller --- Cargo.toml | 4 + src/file.rs | 2 +- src/main.rs | 344 +------------------------------ src/user-agent.rs | 503 ++++++++++++++++++++++++++++++++++++++++++++++ useragent.xml | 54 +++++ 5 files changed, 568 insertions(+), 339 deletions(-) create mode 100644 src/user-agent.rs create mode 100644 useragent.xml diff --git a/Cargo.toml b/Cargo.toml index 7938735..65fd011 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,10 @@ repository = "https://github.com/illumos/metadata-agent" name = "metadata" path = "src/main.rs" +[[bin]] +name = "useragent" +path = "src/user-agent.rs" + [dependencies] serde = { version = "1", features = [ "derive" ] } serde_json = "1" diff --git a/src/file.rs b/src/file.rs index 0be5c9d..983b22f 100644 --- a/src/file.rs +++ b/src/file.rs @@ -11,7 +11,7 @@ use std::os::unix::fs::DirBuilderExt; pub fn ensure_dir(log: &Logger, path: &str) -> Result<()> { if !exists_dir(path)? { info!(log, "mkdir {}", path); - DirBuilder::new().mode(0o700).create(path)?; + DirBuilder::new().recursive(true).mode(0o700).create(path)?; } Ok(()) } diff --git a/src/main.rs b/src/main.rs index 28e70a8..8f32f72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,45 +6,30 @@ mod common; mod file; +#[allow(dead_code)] mod userdata; mod zpool; +#[allow(dead_code)] +use crate::userdata::networkconfig::{NetworkDataV1Iface, NetworkDataV1Subnet}; use anyhow::Result; -use base64::decode as base64Decode; -use flate2::read::GzDecoder; -use libc; +use common::*; +use file::*; use serde::Deserialize; use std::collections::HashMap; -use std::ffi::CString; use std::fs::{self, File, OpenOptions}; - -use std::io::Error as IOError; -use std::io::Result as IOResult; -use std::io::{copy as IOCopy, BufRead, BufReader, Read, Write}; +use std::io::{BufRead, BufReader, Read}; use std::net::Ipv4Addr; -use std::os::unix::ffi::OsStrExt; -use std::os::unix::fs::MetadataExt; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::process::Command; -use userdata::read_user_data; -use users; - -use crate::userdata::cloudconfig::{UserConfig, WriteFileData, WriteFileEncoding}; -use crate::userdata::multiformat_deserialize::Multiformat; -use crate::userdata::networkconfig::{NetworkDataV1Iface, NetworkDataV1Subnet}; -use crate::userdata::UserData; -use common::*; -use file::*; const METADATA_DIR: &str = "/var/metadata"; const STAMP: &str = "/var/metadata/stamp"; const USERSCRIPT: &str = "/var/metadata/userscript"; const MOUNTPOINT: &str = "/var/metadata/iso"; const UNPACKDIR: &str = "/var/metadata/files"; - const DEFROUTER: &str = "/etc/defaultrouter"; - const DHCPINFO: &str = "/sbin/dhcpinfo"; const DLADM: &str = "/usr/sbin/dladm"; const FSTYP: &str = "/usr/sbin/fstyp"; @@ -58,11 +43,6 @@ const SVCADM: &str = "/usr/sbin/svcadm"; const SWAPADD: &str = "/sbin/swapadd"; const ZFS: &str = "/sbin/zfs"; const CPIO: &str = "/usr/bin/cpio"; -const PKG: &str = "/usr/bin/pkg"; -const USERADD: &str = "/usr/sbin/useradd"; -const GROUPADD: &str = "/usr/sbin/groupadd"; -const USERMOD: &str = "/usr/sbin/usermod"; - const FMRI_USERSCRIPT: &str = "svc:/system/illumos/userscript:default"; #[derive(Debug)] @@ -1663,8 +1643,6 @@ fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<()> { let dir_buf = PathBuf::from(UNPACKDIR); - let user_data = read_user_data(log, &dir_buf.join("user-data"))?; - let meta_data_file = File::open(&dir_buf.join("meta-data"))?; let meta_data = serde_yaml::from_reader::(meta_data_file)?; @@ -1713,18 +1691,6 @@ fn run_illumos(log: &Logger, smbios_raw_string: &str) -> Result<()> { } } - /* - * User data phase - */ - phase_user_data(log, &user_data)?; - - for script in user_data.scripts { - /* - * handle the userscripts we have in the user-data: - */ - phase_userscript(log, &script)?; - } - write_lines(log, STAMP, &[ds.uuid])?; Ok(()) @@ -2276,186 +2242,6 @@ fn phase_set_hostname(log: &Logger, hostname: &str) -> Result<()> { Ok(()) } -fn phase_user_data(log: &Logger, user_data: &UserData) -> Result<()> { - //First Apply cloud configurations - for cc in user_data.cloud_configs.clone() { - /* - * First Apply the groups - */ - if let Some(groups) = cc.groups { - for group in groups { - ensure_group(log, group)?; - } - } - - for user in cc.users { - ensure_user(log, &user)?; - } - - if let Some(files) = cc.write_files { - for file in files { - ensure_write_file(log, &file)?; - } - } - - /* - if let Some(ca_certs) = cc.ca_certs { - - } - */ - - if let Some(packages) = cc.packages { - match packages { - Multiformat::String(pkg) => { - ensure_packages(&log, vec![pkg])?; - } - Multiformat::List(pkgs) => { - ensure_packages(&log, pkgs)?; - } - _ => {} - } - } - - /* - * Set general ssh Authorized keys - */ - if let Some(keys) = cc.ssh_authorized_keys { - ensure_pubkeys(log, "root", &keys)?; - } - } - - //Then run the scripts - - Ok(()) -} - -fn ensure_packages(log: &&Logger, pkgs: Vec) -> Result<()> { - info!(log, "installing packages {:#?}", pkgs); - let mut pkg_cmd = Command::new(PKG); - pkg_cmd.env_clear(); - pkg_cmd.arg("install"); - pkg_cmd.arg("-v"); - - for pkg in pkgs { - pkg_cmd.arg(pkg); - } - - let mut child = pkg_cmd.spawn()?; - let status = child.wait()?; - if !status.success() { - bail!("failed package installation see log messages above") - } - - info!(log, "packages installed"); - Ok(()) -} - -fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<()> { - info!(log, "creating file {}", file.path); - let f = std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&file.path)?; - let mut w = std::io::BufWriter::new(&f); - - match file.encoding { - WriteFileEncoding::None => { - trace!(log, "writing string data"); - w.write_all(file.content.as_bytes())?; - } - WriteFileEncoding::B64 => { - trace!(log, "writing base64 encoded data"); - w.write_all(base64Decode(&file.content)?.as_slice())?; - } - WriteFileEncoding::Gzip => { - trace!(log, "writing gzip encoded data"); - let content_clone = file.content.clone(); - let mut conent_bytes = content_clone.as_str().as_bytes(); - let mut d = GzDecoder::new(BufReader::new(&mut conent_bytes)); - IOCopy(&mut d, &mut w)?; - } - WriteFileEncoding::B64Gzip => { - trace!(log, "writing gzipped base64 encoded data"); - let decoded_content = base64Decode(&file.content)?; - let mut d = GzDecoder::new(decoded_content.as_slice()); - IOCopy(&mut d, &mut w)?; - } - } - - if let Some(mode_string) = &file.permissions { - info!( - log, - "setting permissions of file {} to {}", file.path, mode_string - ); - let meta = &f.metadata()?; - let mut perms = meta.permissions(); - perms.set_mode(mode_string.parse::()?); - } - - if let Some(owner) = &file.owner { - info!( - log, - "setting owner and group of file {} to {}", file.path, owner - ); - #[allow(unused_mut)] - let mut uid: users::uid_t; - #[allow(unused_mut)] - let mut gid: users::gid_t; - if owner.contains(":") { - if let Some((u, g)) = owner.split_once(":") { - if let Some(user) = users::get_user_by_name(u) { - uid = user.uid(); - } else { - bail!("could not find user {} in system", u) - } - - if let Some(group) = users::get_group_by_name(g) { - gid = group.gid(); - } else { - bail!("could not find group {} in system", g) - } - } else { - bail!("wrong user group string invalid config") - } - } else { - let meta = f.metadata()?; - gid = meta.gid(); - if let Some(u) = users::get_user_by_name(&owner) { - uid = u.uid(); - } else { - bail!("could not find user {} in system", &owner) - } - } - chown(&file.path, uid, gid, false)?; - } - - Ok(()) -} - -/// Actually perform the change of owner on a path -fn chown>( - path: P, - uid: libc::uid_t, - gid: libc::gid_t, - follow: bool, -) -> IOResult<()> { - let path = path.as_ref(); - let s = CString::new(path.as_os_str().as_bytes()).unwrap(); - let ret = unsafe { - if follow { - libc::chown(s.as_ptr(), uid, gid) - } else { - libc::lchown(s.as_ptr(), uid, gid) - } - }; - if ret == 0 { - Ok(()) - } else { - Err(IOError::last_os_error()) - } -} - fn ensure_dns_nameservers(log: &Logger, nameservers: &[String]) -> Result<()> { /* * DNS Servers: @@ -2567,124 +2353,6 @@ fn ensure_pubkeys(log: &Logger, user: &str, public_keys: &[String]) -> Result<() Ok(()) } -fn ensure_user(log: &Logger, user: &UserConfig) -> Result<()> { - if users::get_user_by_name(&user.name).is_none() { - let mut cmd = Command::new(USERADD); - if let Some(groups) = &user.groups { - cmd.arg("-G").arg(groups.join(",")); - } - - if let Some(expire_date) = &user.expire_date { - cmd.arg("-e").arg(expire_date); - } - - if let Some(gecos) = &user.gecos { - cmd.arg("-c").arg(gecos); - } - - if let Some(home_dir) = &user.homedir { - cmd.arg("-d").arg(home_dir); - } - - if let Some(primary_group) = &user.primary_group { - cmd.arg("-g").arg(primary_group); - } else if let Some(no_user_group) = &user.no_user_group { - if !no_user_group { - let mut ump = HashMap::>>::new(); - ump.insert(user.name.clone(), Some(vec![])); - ensure_group(log, ump)?; - } - } - - if let Some(inactive) = &user.inactive { - cmd.arg("-f").arg(inactive); - } - - if let Some(shell) = &user.shell { - cmd.arg("-s").arg(shell); - } - - cmd.arg(&user.name); - - //TODO lock_passwd - //TODO passwd - //TODO is_system_user - debug!(log, "Running useradd {:?}", cmd); - let output = cmd.output()?; - if !output.status.success() { - bail!("useradd failed for {}: {}", &user.name, output.info()); - } - } else { - info!(log, "user with name {} exists skipping", &user.name); - } - - if let Some(public_keys) = &user.ssh_authorized_keys { - ensure_pubkeys(log, &user.name, public_keys)?; - } - - Ok(()) -} - -fn ensure_group(log: &Logger, groups: HashMap>>) -> Result<()> { - for (group_name, users_in_group_opt) in groups { - if users::get_group_by_name(&group_name).is_none() { - let mut cmd = Command::new(GROUPADD); - cmd.arg(&group_name); - debug!(log, "Running groupadd {:?}", cmd); - let output = cmd.output()?; - if !output.status.success() { - bail!("groupadd failed for {}: {}", &group_name, output.info()); - } - } else { - info!(log, "group {} exists", group_name) - } - - if let Some(users_in_group) = users_in_group_opt { - for user in users_in_group { - let existing_groups: Vec = - if let Some(sys_user) = users::get_user_by_name(&user) { - if let Some(user_groups) = - users::get_user_groups(&user, sys_user.primary_group_id()) - { - user_groups - .iter() - .map(|g| { - g.name() - .to_os_string() - .into_string() - .unwrap_or(String::new()) - }) - .collect() - } else { - vec![] - } - } else { - vec![] - }; - - let mut user_groups: Vec = existing_groups - .iter() - .filter(|&g| g.clone() != String::new()) - .map(|g| g.clone()) - .collect(); - - user_groups.push(group_name.clone()); - let mut cmd = Command::new(USERMOD); - cmd.arg("-G"); - cmd.arg(user_groups.clone().join(",")); - cmd.arg(&user); - debug!(log, "Running usermod {:?}", cmd); - let output = cmd.output()?; - if !output.status.success() { - bail!("usermod failed for {}: {}", &user, output.info()); - } - } - } - } - - Ok(()) -} - fn phase_userscript(log: &Logger, userscript: &str) -> Result<()> { /* * If the userscript is basically empty, just ignore it. diff --git a/src/user-agent.rs b/src/user-agent.rs new file mode 100644 index 0000000..b6acf43 --- /dev/null +++ b/src/user-agent.rs @@ -0,0 +1,503 @@ +// Add this as we havent structured the crates very nicely for two binaries +#[allow(dead_code)] +mod common; +// Add this as we havent structured the crates very nicely for two binaries +#[allow(dead_code)] +mod file; +// Add this as we havent structured the crates very nicely for two binaries +#[allow(dead_code)] +mod userdata; + +use anyhow::Result; +use base64::decode as base64Decode; +use common::*; +use flate2::read::GzDecoder; +use libc; +use std::collections::HashMap; +use std::ffi::CString; +use std::fs; +use std::io::Error as IOError; +use std::io::Result as IOResult; +use std::io::{copy as IOCopy, BufReader, Write}; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::MetadataExt; +use std::os::unix::fs::PermissionsExt; + +use file::*; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use userdata::cloudconfig::*; +use userdata::multiformat_deserialize::*; +use userdata::*; + +const UNPACKDIR: &str = "/var/metadata/files"; +const PKG: &str = "/usr/bin/pkg"; +const USERADD: &str = "/usr/sbin/useradd"; +const GROUPADD: &str = "/usr/sbin/groupadd"; +const USERMOD: &str = "/usr/sbin/usermod"; +const USERSCRIPT: &str = "/var/metadata/userscript"; +const FMRI_USERSCRIPT: &str = "svc:/system/illumos/userscript:default"; +const SVCADM: &str = "/usr/sbin/svcadm"; + +fn main() -> Result<()> { + let log = init_log(); + + /* + * This program could be destructive if run in the wrong place. Try to + * ensure it has at least been installed as an SMF service: + */ + if let Some(fmri) = std::env::var_os("SMF_FMRI") { + info!(log, "SMF instance: {}", fmri.to_string_lossy()); + } else { + bail!("SMF_FMRI is not set; running under SMF?"); + } + + let dir_buf = PathBuf::from(UNPACKDIR); + + let user_data_path = dir_buf.join("user-data"); + + if !user_data_path.exists() { + return Ok(()); + } + + let user_data = read_user_data(&log, &user_data_path)?; + + /* + * User data phase + */ + phase_user_data(&log, &user_data)?; + + for script in user_data.scripts { + /* + * handle the userscripts we have in the user-data: + */ + phase_userscript(&log, &script)?; + } + + Ok(()) +} + +fn phase_user_data(log: &Logger, user_data: &UserData) -> Result<()> { + //First Apply cloud configurations + for cc in user_data.cloud_configs.clone() { + /* + * First Apply the groups + */ + if let Some(groups) = cc.groups { + for group in groups { + ensure_group(log, group)?; + } + } + + for user in cc.users { + ensure_user(log, &user)?; + } + + if let Some(files) = cc.write_files { + for file in files { + ensure_write_file(log, &file)?; + } + } + + /* + if let Some(ca_certs) = cc.ca_certs { + + } + */ + + if let Some(packages) = cc.packages { + match packages { + Multiformat::String(pkg) => { + ensure_packages(&log, vec![pkg])?; + } + Multiformat::List(pkgs) => { + ensure_packages(&log, pkgs)?; + } + _ => {} + } + } + + /* + * Set general ssh Authorized keys + */ + if let Some(keys) = cc.ssh_authorized_keys { + ensure_pubkeys(log, "root", &keys)?; + } + } + + //Then run the scripts + + Ok(()) +} + +fn ensure_packages(log: &&Logger, pkgs: Vec) -> Result<()> { + info!(log, "installing packages {:#?}", pkgs); + let mut pkg_cmd = Command::new(PKG); + pkg_cmd.env_clear(); + pkg_cmd.arg("install"); + pkg_cmd.arg("-v"); + + for pkg in pkgs { + pkg_cmd.arg(pkg); + } + + let mut child = pkg_cmd.spawn()?; + let status = child.wait()?; + if !status.success() { + bail!("failed package installation see log messages above") + } + + info!(log, "packages installed"); + Ok(()) +} + +fn ensure_write_file(log: &Logger, file: &WriteFileData) -> Result<()> { + info!(log, "creating file {}", file.path); + let f = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&file.path)?; + let mut w = std::io::BufWriter::new(&f); + + match file.encoding { + WriteFileEncoding::None => { + trace!(log, "writing string data"); + w.write_all(file.content.as_bytes())?; + } + WriteFileEncoding::B64 => { + trace!(log, "writing base64 encoded data"); + w.write_all(base64Decode(&file.content)?.as_slice())?; + } + WriteFileEncoding::Gzip => { + trace!(log, "writing gzip encoded data"); + let content_clone = file.content.clone(); + let mut conent_bytes = content_clone.as_str().as_bytes(); + let mut d = GzDecoder::new(BufReader::new(&mut conent_bytes)); + IOCopy(&mut d, &mut w)?; + } + WriteFileEncoding::B64Gzip => { + trace!(log, "writing gzipped base64 encoded data"); + let decoded_content = base64Decode(&file.content)?; + let mut d = GzDecoder::new(decoded_content.as_slice()); + IOCopy(&mut d, &mut w)?; + } + } + + if let Some(mode_string) = &file.permissions { + info!( + log, + "setting permissions of file {} to {}", file.path, mode_string + ); + let meta = &f.metadata()?; + let mut perms = meta.permissions(); + perms.set_mode(mode_string.parse::()?); + } + + if let Some(owner) = &file.owner { + info!( + log, + "setting owner and group of file {} to {}", file.path, owner + ); + #[allow(unused_mut)] + let mut uid: users::uid_t; + #[allow(unused_mut)] + let mut gid: users::gid_t; + if owner.contains(":") { + if let Some((u, g)) = owner.split_once(":") { + if let Some(user) = users::get_user_by_name(u) { + uid = user.uid(); + } else { + bail!("could not find user {} in system", u) + } + + if let Some(group) = users::get_group_by_name(g) { + gid = group.gid(); + } else { + bail!("could not find group {} in system", g) + } + } else { + bail!("wrong user group string invalid config") + } + } else { + let meta = f.metadata()?; + gid = meta.gid(); + if let Some(u) = users::get_user_by_name(&owner) { + uid = u.uid(); + } else { + bail!("could not find user {} in system", &owner) + } + } + chown(&file.path, uid, gid, false)?; + } + + Ok(()) +} + +/// Actually perform the change of owner on a path +fn chown>( + path: P, + uid: libc::uid_t, + gid: libc::gid_t, + follow: bool, +) -> IOResult<()> { + let path = path.as_ref(); + let s = CString::new(path.as_os_str().as_bytes()).unwrap(); + let ret = unsafe { + if follow { + libc::chown(s.as_ptr(), uid, gid) + } else { + libc::lchown(s.as_ptr(), uid, gid) + } + }; + if ret == 0 { + Ok(()) + } else { + Err(IOError::last_os_error()) + } +} + +fn ensure_pubkeys(log: &Logger, user: &str, public_keys: &[String]) -> Result<()> { + /* + * Manage the public keys: + */ + info!(log, "checking SSH public keys for user {}...", user); + + let sshdir = if user == "root" { + format!("/root/.ssh") + } else { + format!("/export/home/{}/.ssh", user) + }; + + ensure_dir(log, &sshdir)?; + + let authorized_keys = sshdir.clone() + "/authorized_keys"; + + let mut file = read_lines_maybe(&authorized_keys)?; + info!(log, "existing SSH public keys: {:#?}", &file); + + let mut dirty = false; + for key in public_keys.iter() { + if !file.contains(key) { + info!(log, "add SSH public key: {}", key); + file.push(key.to_string()); + dirty = true; + } + } + + if dirty { + write_lines(log, &authorized_keys, file.as_ref())?; + + if let Some(usr) = users::get_user_by_name(&user) { + chown( + Path::new(sshdir.as_str()), + usr.uid(), + usr.primary_group_id(), + false, + )?; + chown( + Path::new(authorized_keys.as_str()), + usr.uid(), + usr.primary_group_id(), + false, + )?; + } + } + + Ok(()) +} + +fn ensure_user(log: &Logger, user: &UserConfig) -> Result<()> { + if users::get_user_by_name(&user.name).is_none() { + let mut cmd = Command::new(USERADD); + if let Some(groups) = &user.groups { + cmd.arg("-G").arg(groups.join(",")); + } + + if let Some(expire_date) = &user.expire_date { + cmd.arg("-e").arg(expire_date); + } + + if let Some(gecos) = &user.gecos { + cmd.arg("-c").arg(gecos); + } + + if let Some(home_dir) = &user.homedir { + cmd.arg("-d").arg(home_dir); + } + + if let Some(primary_group) = &user.primary_group { + cmd.arg("-g").arg(primary_group); + } else if let Some(no_user_group) = &user.no_user_group { + if !no_user_group { + let mut ump = HashMap::>>::new(); + ump.insert(user.name.clone(), Some(vec![])); + ensure_group(log, ump)?; + } + } + + if let Some(inactive) = &user.inactive { + cmd.arg("-f").arg(inactive); + } + + if let Some(shell) = &user.shell { + cmd.arg("-s").arg(shell); + } + + cmd.arg("-m"); + + cmd.arg(&user.name); + + //TODO lock_passwd + //TODO passwd + //TODO is_system_user + debug!(log, "Running useradd {:?}", cmd); + let output = cmd.output()?; + if !output.status.success() { + bail!("useradd failed for {}: {}", &user.name, output.info()); + } + + debug!(log, "Running passwd -N {}", &user.name); + let passwd_out = Command::new("passwd").arg("-N").arg(&user.name).output()?; + if !passwd_out.status.success() { + bail!( + "unlocking user {} failed: {}", + &user.name, + passwd_out.info() + ); + } + } else { + info!(log, "user with name {} exists skipping", &user.name); + } + + if let Some(public_keys) = &user.ssh_authorized_keys { + ensure_pubkeys(log, &user.name, public_keys)?; + } + + Ok(()) +} + +fn ensure_group(log: &Logger, groups: HashMap>>) -> Result<()> { + for (group_name, users_in_group_opt) in groups { + if users::get_group_by_name(&group_name).is_none() { + let mut cmd = Command::new(GROUPADD); + cmd.arg(&group_name); + debug!(log, "Running groupadd {:?}", cmd); + let output = cmd.output()?; + if !output.status.success() { + bail!("groupadd failed for {}: {}", &group_name, output.info()); + } + } else { + info!(log, "group {} exists", group_name) + } + + if let Some(users_in_group) = users_in_group_opt { + for user in users_in_group { + let existing_groups: Vec = + if let Some(sys_user) = users::get_user_by_name(&user) { + if let Some(user_groups) = + users::get_user_groups(&user, sys_user.primary_group_id()) + { + user_groups + .iter() + .map(|g| { + g.name() + .to_os_string() + .into_string() + .unwrap_or(String::new()) + }) + .collect() + } else { + vec![] + } + } else { + vec![] + }; + + let mut user_groups: Vec = existing_groups + .iter() + .filter(|&g| g.clone() != String::new()) + .map(|g| g.clone()) + .collect(); + + user_groups.push(group_name.clone()); + let mut cmd = Command::new(USERMOD); + cmd.arg("-G"); + cmd.arg(user_groups.clone().join(",")); + cmd.arg(&user); + debug!(log, "Running usermod {:?}", cmd); + let output = cmd.output()?; + if !output.status.success() { + bail!("usermod failed for {}: {}", &user, output.info()); + } + } + } + } + + Ok(()) +} + +fn phase_userscript(log: &Logger, userscript: &str) -> Result<()> { + /* + * If the userscript is basically empty, just ignore it. + */ + if userscript.trim().is_empty() { + return Ok(()); + } + + /* + * First check to see if this is a script with an interpreter line that has + * an absolute path; i.e., begins with "#!/". If not, we will assume it is + * in some format we do not understand for now (like the cloud-init format, + * etc). + */ + if !userscript.starts_with("#!/") { + bail!("userscript does not start with an #!/interpreter line"); + } + + let us2; + let filedata = if !userscript.is_empty() && !userscript.ends_with('\n') { + /* + * UNIX text files should end with a newline. + */ + us2 = format!("{}\n", userscript); + &us2 + } else { + userscript + }; + + /* + * Write userscript to a file, ensuring it is root:root 0700. + */ + write_file(USERSCRIPT, filedata)?; + + /* + * Make sure the userscript is executable. + */ + let mut perms = fs::metadata(USERSCRIPT)?.permissions(); + perms.set_mode(0o700); + fs::set_permissions(USERSCRIPT, perms)?; + + /* + * Enable the svc:/system/illumos/userscript:default SMF instance. + */ + smf_enable(log, FMRI_USERSCRIPT)?; + + Ok(()) +} + +fn smf_enable(log: &Logger, fmri: &str) -> Result<()> { + info!(log, "exec: svcadm enable {}", fmri); + let output = Command::new(SVCADM) + .env_clear() + .arg("enable") + .arg(fmri) + .output()?; + + if !output.status.success() { + bail!("svcadm enable {} failed: {}", fmri, output.info()); + } + + Ok(()) +} diff --git a/useragent.xml b/useragent.xml new file mode 100644 index 0000000..4b05acb --- /dev/null +++ b/useragent.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file