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" + } +} diff --git a/.gitignore b/.gitignore index ea8c4bf..2674b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +.idea +.vscode \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5b92824..c26a10e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,13 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + +[[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 +43,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[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 +81,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 +105,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 +144,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 +185,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 +200,18 @@ dependencies = [ "cfg-if 1.0.0", ] +[[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" @@ -355,14 +415,21 @@ version = "0.1.0" dependencies = [ "anyhow", "atty", + "base64 0.13.0", + "flate2", + "libc", + "mailparse", "reqwest", "serde", "serde_json", + "serde_yaml", "slog", "slog-term", "socket2", "tempfile", + "thiserror", "unicode-normalization", + "users", ] [[package]] @@ -386,9 +453,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" @@ -423,9 +490,15 @@ 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" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "log" @@ -436,6 +509,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 +548,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" @@ -629,6 +723,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 +802,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,7 +836,7 @@ 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", @@ -797,6 +897,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" @@ -869,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" @@ -1015,6 +1147,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "users" +version = "0.11.0" +source = "git+https://github.com/Toasterson/rust-users.git?branch=illumos#ab8ccb3422c2693f84c2c99b35f4829a009af731" +dependencies = [ + "libc", + "log", +] + [[package]] name = "version_check" version = "0.9.2" @@ -1197,3 +1338,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..65fd011 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 "] edition = "2018" license = "Apache-2.0" repository = "https://github.com/illumos/metadata-agent" @@ -11,14 +10,25 @@ 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" +serde_yaml = "0.8" tempfile = "3" anyhow = "1" slog = "2.5" slog-term = "2.5" atty = "0.2" +thiserror = "1.0" +flate2 = "1.0" +mailparse = "0.13.5" +users = { git="https://github.com/Toasterson/rust-users.git", branch="illumos" } +base64 = "0.13.0" +libc = { version="0.2.116" } # # Force an earlier version of socket2 which does not use the TCP_MAXSEG symbol @@ -38,4 +48,4 @@ unicode-normalization = "0.1, <0.1.14" [dependencies.reqwest] version = "0.10" default-features = false -features = ["blocking", "json"] +features = ["blocking", "json"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..783585a --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +MODE=debug +USER=root +BIN_GROUP=bin +SYS_GROUP=sys + +.PHONY: all build install + +# 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 +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 -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 diff --git a/README.md b/README.md index 9b65e0b..2c44e6a 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 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..d5c4b7d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -7,7 +7,7 @@ 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, 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 new file mode 100644 index 0000000..983b22f --- /dev/null +++ b/src/file.rs @@ -0,0 +1,100 @@ +/* + * Copyright 2020 Oxide Computer Company + */ + +use crate::common::*; +use anyhow::Result; +use std::fs::{DirBuilder, File}; +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().recursive(true).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>> { + Ok(read_file(p)?.map(|data| data.lines().map(|a| a.trim().to_string()).collect())) +} + +pub fn read_lines_maybe(p: &str) -> Result> { + Ok(match read_lines(p)? { + None => Vec::new(), + Some(l) => l, + }) +} + +pub 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)) +} + +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(()) +} + +pub 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) +} diff --git a/src/main.rs b/src/main.rs index bfbdce5..8f32f72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,33 +1,35 @@ /* * Copyright 2020 Oxide Computer Company + * Copyright 2022 OpenFlowLabs + * */ -use std::io::Read; -use std::io::Write; -use std::io::ErrorKind; -use std::collections::HashMap; -use std::path::Path; -use std::fs::{self, DirBuilder, File, OpenOptions}; -use std::os::unix::fs::{DirBuilderExt, PermissionsExt}; -use std::process::Command; +mod common; +mod file; +#[allow(dead_code)] +mod userdata; +mod zpool; +#[allow(dead_code)] +use crate::userdata::networkconfig::{NetworkDataV1Iface, NetworkDataV1Subnet}; +use anyhow::Result; +use common::*; +use file::*; use serde::Deserialize; - +use std::collections::HashMap; +use std::fs::{self, File, OpenOptions}; +use std::io::{BufRead, BufReader, Read}; use std::net::Ipv4Addr; - -mod zpool; -mod common; -use common::*; - +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::process::Command; 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"; @@ -41,7 +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 FMRI_USERSCRIPT: &str = "svc:/system/illumos/userscript:default"; #[derive(Debug)] @@ -101,10 +102,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()); @@ -141,7 +139,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(); @@ -161,7 +160,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, + })) } } @@ -173,10 +177,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) => { @@ -234,65 +235,14 @@ 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> -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)?), } } @@ -302,6 +252,7 @@ enum MountOptionValue { Value(String), } +#[allow(dead_code)] #[derive(Debug)] struct Mount { special: String, @@ -311,17 +262,18 @@ struct Mount { time: u64, } -#[derive(Debug,Deserialize)] +#[derive(Debug, Deserialize)] struct DNS { nameservers: Vec, } -#[derive(Debug,Deserialize)] +#[allow(dead_code)] +#[derive(Debug, Deserialize)] struct FloatingIP { active: bool, } -#[derive(Debug,Deserialize)] +#[derive(Debug, Deserialize)] struct IPv4 { ip_address: String, gateway: String, @@ -352,7 +304,7 @@ impl IPv4 { } } -#[derive(Debug,Deserialize)] +#[derive(Debug, Deserialize)] struct Interface { anchor_ipv4: Option, ipv4: IPv4, @@ -361,14 +313,15 @@ struct Interface { type_: String, } -#[derive(Debug,Deserialize)] +#[derive(Debug, Deserialize)] struct Interfaces { public: Option>, private: Option>, } -#[derive(Debug,Deserialize)] -struct Metadata { +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +struct DOMetadata { auth_key: String, dns: DNS, droplet_id: u64, @@ -381,7 +334,8 @@ struct Metadata { user_data: Option, } -#[derive(Debug,Deserialize)] +#[allow(dead_code)] +#[derive(Debug, Deserialize)] struct SdcNic { mac: String, interface: String, @@ -406,9 +360,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); @@ -440,48 +392,6 @@ 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> { let mut buf = [0u8; 512]; let mut f = OpenOptions::new() @@ -532,8 +442,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 + ), _ => {} } } @@ -545,16 +459,23 @@ 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>> { let i = std::fs::read_dir("/dev/dsk")?; - let mut out = Vec::new(); + 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") { + if !name.ends_with("p0") && !name.ends_with("s0") { continue; } } else { @@ -566,6 +487,7 @@ fn find_device() -> Result> { */ let output = Command::new(FSTYP) .env_clear() + .arg("-a") .arg(ent.path()) .output()?; @@ -573,10 +495,74 @@ fn find_device() -> Result> { 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" + || 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 + ); + out.push(dev.clone()); + } + } + } + } + count += 1; + } + } + } + + match out.len() { + 0 => Ok(None), + _ => Ok(Some(out)), + } +} + +fn find_device() -> Result> { + let i = std::fs::read_dir("/dev/dsk")?; + + let mut out = 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(ent.path()).output()?; + + if !output.status.success() { + continue; + } + if let Ok(s) = String::from_utf8(output.stdout) { if s.trim() == "hsfs" { out.push(ent.path()); } + if s.trim() == "pcfs" { + out.push(ent.path()); + } } } @@ -648,12 +634,73 @@ fn parse_net_adm(stdout: Vec) -> Result>> { Ok(out) } +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> { let output = Command::new(IPADM) .env_clear() .arg("show-if") .arg("-p") - .arg("-o").arg("ifname") + .arg("-o") + .arg("ifname") .output()?; if !output.status.success() { @@ -678,7 +725,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() { @@ -687,12 +735,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 { @@ -725,7 +776,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() { @@ -742,7 +794,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() { @@ -785,7 +838,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()?; @@ -801,7 +855,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() { @@ -828,9 +883,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()); @@ -839,6 +892,125 @@ fn swapadd() -> Result<()> { Ok(()) } +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 { + 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<()> { + 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<()> { + 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<()> { + 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); @@ -897,9 +1069,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)?; @@ -924,7 +1094,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") @@ -941,9 +1114,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()?; @@ -963,8 +1138,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 { @@ -976,22 +1153,34 @@ fn ensure_ipv4_interface_dhcp(log: &Logger, sfx: &str, n: &str) } } -fn ensure_ipv4_interface(log: &Logger, sfx: &str, mac: &str, ipv4: &str) - -> Result<()> -{ - info!(log, "ENSURE IPv4 INTERFACE: {}, {:?}", mac, ipv4); - - let n = match mac_to_nic(mac)? { - None => bail!("MAC address {} not found", mac), - Some(n) => n, +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) + } 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()?; @@ -1011,7 +1200,10 @@ fn ensure_ipv4_interface(log: &Logger, sfx: &str, mac: &str, ipv4: &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") @@ -1028,18 +1220,27 @@ fn ensure_ipv4_interface(log: &Logger, sfx: &str, mac: &str, ipv4: &str) 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", n, ipv4, sfx); + info!( + log, + "ok, interface {} address {} ({}) complete", found_nic, ipv4, sfx + ); Ok(()) } @@ -1075,7 +1276,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 @@ -1107,18 +1310,14 @@ fn run(log: &Logger) -> Result<()> { run_amazon(log)?; return Ok(()); } - ("OmniOS", "OmniOS HVM") => { - info!(log, "hypervisor type: OmniOS 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)?; + ("OmniOS", "OmniOS HVM") | ("OpenIndiana", "OpenIndiana HVM") => { + info!(log, "hypervisor type: illumos BHYVE (from SMBIOS)"); + 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") => { @@ -1166,12 +1365,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 + ); } } @@ -1191,7 +1397,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()?; @@ -1250,7 +1457,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())?; } /* @@ -1266,6 +1473,229 @@ fn run_generic(log: &Logger, smbios_uuid: &str, network: bool) -> Result<()> { Ok(()) } +#[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<()> { + 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<()> { + /* + * 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 { + if let Some(dev) = devs.first() { + 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 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!"); + } + } + + write_lines(log, STAMP, &[ds.uuid])?; + + Ok(()) +} + fn run_amazon(log: &Logger) -> Result<()> { /* * Sadly, Amazon has no mechanism for metadata access that does not require @@ -1324,12 +1754,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 + ); } } @@ -1353,7 +1790,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?"); } @@ -1363,7 +1800,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?"); } @@ -1385,12 +1823,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 + ); } } @@ -1407,7 +1852,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)?; @@ -1423,16 +1870,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, &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); } } @@ -1453,18 +1897,16 @@ 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)?; } /* * 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(); - phase_pubkeys(log, &pubkeys)?; + ensure_pubkeys(log, "root", &pubkeys)?; } /* @@ -1472,7 +1914,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])?; @@ -1487,8 +1930,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, @@ -1519,7 +1964,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()?; @@ -1534,8 +1980,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 @@ -1552,12 +1997,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 + ); } } @@ -1572,8 +2024,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 @@ -1588,8 +2040,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 @@ -1603,16 +2055,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: @@ -1719,21 +2171,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); } @@ -1742,59 +2190,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(); + + let mut fore = sect[0].to_string(); - if portions.len() > 1 { + 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(); - fore = hosts; + 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; + } } } - } - 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_dns(log: &Logger, nameservers: &[String]) -> Result<()> { +fn ensure_dns_nameservers(log: &Logger, nameservers: &[String]) -> Result<()> { /* * DNS Servers: */ @@ -1816,9 +2264,7 @@ fn phase_dns(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; @@ -1834,15 +2280,61 @@ 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<()> { + /* + * 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<()> { /* * 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; @@ -1855,7 +2347,7 @@ 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(()) 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/src/userdata.rs b/src/userdata.rs new file mode 100644 index 0000000..db8f08d --- /dev/null +++ b/src/userdata.rs @@ -0,0 +1,123 @@ +/* + * Copyright 2021 OpenFlowLabs + * + */ +use crate::common::*; +use anyhow::Result; +use cloudconfig::CloudConfig; +use flate2::read::GzDecoder; +use std::fs::File; +use std::io::prelude::*; +use std::io::BufReader; +use std::path::PathBuf; +use thiserror::Error; + +pub mod cloudconfig; +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(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<()> { + 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, Error, PartialEq)] +enum UserDataError { + #[error("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 crate::common::init_log; + use crate::userdata::UserData; + use std::path::PathBuf; + use std::str::FromStr; + + #[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..512ea4e --- /dev/null +++ b/src/userdata/cloudconfig.rs @@ -0,0 +1,169 @@ +/* + * Copyright 2021 OpenFlowLabs + * + */ +use crate::userdata::multiformat_deserialize::Multiformat; +use serde::Deserialize; +use std::collections::HashMap; + +#[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; + } + + "" + } + + #[allow(dead_code)] + 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..3fef1fe --- /dev/null +++ b/src/userdata/networkconfig.rs @@ -0,0 +1,836 @@ +use std::fs::File; +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 { + // 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; + use anyhow::{Result}; + + #[test] + 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) => { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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<()> { + 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/zpool.rs b/src/zpool.rs index b64479d..993026e 100644 --- a/src/zpool.rs +++ b/src/zpool.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::io::Write; +use anyhow::{Result}; use super::common::*; 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