Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
No build reason found for standard:aarch64
home:ojkastl_buildservice:Branch_systemsmanagement_jetporch
jetporch
jetporch-0.0.1.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File jetporch-0.0.1.obscpio of Package jetporch
07070100000000000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001700000000jetporch-0.0.1/.github07070100000001000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000002600000000jetporch-0.0.1/.github/ISSUE_TEMPLATE07070100000002000081A400000000000000000000000165135CC10000017D000000000000000000000000000000000000003700000000jetporch-0.0.1/.github/ISSUE_TEMPLATE/01_bug_report.md--- name: Create Bug Report about: No one likes bugs title: '' labels: Bug Report assignees: '' --- ### What Happened? <!-- explain details --> ### Version <-- paste the version/date info from 'jetp --version' --> ### Steps to Reproduce 1. 2. 3. <!-- optional: sharing playbook content on gist.github.com may be helpful --> ### Additional Information <!-- optional --> 07070100000003000081A400000000000000000000000165135CC10000012E000000000000000000000000000000000000003800000000jetporch-0.0.1/.github/ISSUE_TEMPLATE/02_docs_report.md--- name: Create Documentation Report about: Report errors in documentation title: '' labels: Bug Report assignees: '' --- ### Explain the problem <!--- what is wrong with the docs? --> ### URL <!-- paste the URL of the page(s) being discussed --> ### Additional information <!-- optional --> 07070100000004000081A400000000000000000000000165135CC10000025E000000000000000000000000000000000000003100000000jetporch-0.0.1/.github/ISSUE_TEMPLATE/config.ymlblank_issues_enabled: false contact_links: - name: Get Help url: https://www.jetporch.com/community/discord-chat about: Discord Chat - name: Ask Questions url: https://www.jetporch.com/community/discord-chat about: Discord Chat - name: Make A Feature Request url: https://www.jetporch.com/community/discord-chat about: Discord Chat - name: Discuss Development or Feature Ideas url: https://www.jetporch.com/community/discord-chat about: Discord Chat - name: Contributor Guide url: https://www.jetporch.com/community/contributing about: Required reading 07070100000005000081A400000000000000000000000165135CC10000001B000000000000000000000000000000000000001A00000000jetporch-0.0.1/.gitignore/src/cli/version.rs target 07070100000006000081A400000000000000000000000165135CC10000002D000000000000000000000000000000000000001D00000000jetporch-0.0.1/.rustfmt.tomldisable_all_formatting: true ignore = ["/"] 07070100000007000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001700000000jetporch-0.0.1/.vscode07070100000008000081A400000000000000000000000165135CC100000049000000000000000000000000000000000000002500000000jetporch-0.0.1/.vscode/settings.json{ "files.exclude": { "target*": true, ".vscode" : true, } } 07070100000009000081A400000000000000000000000165135CC100005BBA000000000000000000000000000000000000001A00000000jetporch-0.0.1/Cargo.lock# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "coolor" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af4d7a805ca0d92f8c61a31c809d4323fdaa939b0b440e544d21db7797c5aaad" dependencies = [ "crossterm", ] [[package]] name = "cpufeatures" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] [[package]] name = "crossbeam" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" dependencies = [ "cfg-if", "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", "crossbeam-queue", "crossbeam-utils", ] [[package]] name = "crossbeam-channel" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-queue" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] [[package]] name = "crossterm" version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" dependencies = [ "bitflags", "crossterm_winapi", "libc", "mio", "parking_lot 0.12.1", "signal-hook", "signal-hook-mio", "winapi", ] [[package]] name = "crossterm_winapi" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "guid-create" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523933167d2fe3685898d235d6b84c578b2414c446f865a0c405977ef0345980" dependencies = [ "rand", "winapi", ] [[package]] name = "handlebars" version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c39b3bc2a8f715298032cf5087e58573809374b08160aa7d750582bdb82d2683" dependencies = [ "log", "pest", "pest_derive", "serde", "serde_json", "thiserror", ] [[package]] name = "hashbrown" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" [[package]] name = "indexmap" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "inline_colorization" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fe264857e08559df9a1dfde1a43388129c9629fe4db630ded669a8c59e887a1" [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jetp" version = "0.1.0" dependencies = [ "guid-create", "handlebars", "inline_colorization", "once_cell", "rayon", "rs_sha512", "serde", "serde_derive", "serde_json", "serde_yaml", "ssh2", "termimad", ] [[package]] name = "libc" version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libssh2-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", ] [[package]] name = "libz-sys" version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "lock_api" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memoffset" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "minimad" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "277639f0198568f70f8fe4ab88a52a67c96bca12f27ba5c17a76acdcb8b45834" dependencies = [ "once_cell", ] [[package]] name = "mio" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", "wasi", "windows-sys", ] [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl-sys" version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "parking_lot" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", "parking_lot_core 0.8.6", ] [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core 0.9.8", ] [[package]] name = "parking_lot_core" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ "cfg-if", "instant", "libc", "redox_syscall 0.2.16", "smallvec", "winapi", ] [[package]] name = "parking_lot_core" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", "redox_syscall 0.3.5", "smallvec", "windows-targets", ] [[package]] name = "pest" version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33" dependencies = [ "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bee7be22ce7918f641a33f08e3f43388c7656772244e2bbb2477f44cc9021a" dependencies = [ "pest", "pest_generator", ] [[package]] name = "pest_generator" version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1511785c5e98d79a05e8a6bc34b4ac2168a0e3e92161862030ad84daa223141" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", "syn", ] [[package]] name = "pest_meta" version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42f0394d3123e33353ca5e1e89092e533d2cc490389f2bd6131c43c634ebc5f" dependencies = [ "once_cell", "pest", "sha2", ] [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rayon" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags", ] [[package]] name = "rs_hasher_ctx" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a45ae5cc6246fa2666253289d6495e1fb3d125fb83842ff56b747a3b662e28e" dependencies = [ "rs_internal_hasher", "rs_internal_state", "rs_n_bit_words", ] [[package]] name = "rs_internal_hasher" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19754b7c13d7fb92e995b1f6330918466e134ba7c3f55bf805c72e6a9727c426" dependencies = [ "rs_internal_state", "rs_n_bit_words", ] [[package]] name = "rs_internal_state" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "214a4e27fec5b651d615675874c6a829496cc2aa66e5f1b184ab05cb39fd3625" dependencies = [ "rs_n_bit_words", ] [[package]] name = "rs_n_bit_words" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bc1bbb4c2a60f76b331e6ba70b5065e210fa6e72fc966c2d488736755d89cb6" [[package]] name = "rs_sha512" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bb3ee2bcf2e0bd2ead2504c3b67d1fb34ae978a2014febc011f82fcbe58d56" dependencies = [ "rs_hasher_ctx", "rs_internal_hasher", "rs_internal_state", "rs_n_bit_words", ] [[package]] name = "ryu" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_yaml" version = "0.9.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" dependencies = [ "indexmap", "itoa", "ryu", "serde", "unsafe-libyaml", ] [[package]] name = "sha2" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-mio" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "smallvec" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "ssh2" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7fe461910559f6d5604c3731d00d2aafc4a83d1665922e280f42f9a168d5455" dependencies = [ "bitflags", "libc", "libssh2-sys", "parking_lot 0.11.2", ] [[package]] name = "syn" version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "termimad" version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfab44b4bc17601cf226cce31c87462a4a5bd5d325948c8ebbc9e715660a1287" dependencies = [ "coolor", "crossbeam", "crossterm", "minimad", "thiserror", "unicode-width", ] [[package]] name = "thiserror" version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unsafe-libyaml" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 0707010000000A000081A400000000000000000000000165135CC1000001B9000000000000000000000000000000000000001A00000000jetporch-0.0.1/Cargo.toml[package] name = "jetp" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] once_cell="1.18.0" ssh2="0.9.4" serde_derive= "=1.0.171" serde= { version = "1.0.171", features = ["derive"] } serde_yaml="0.9.25" serde_json="1.0.105" termimad="0.20" inline_colorization="0.1.5" rayon="1.7.0" handlebars="4.3.7" rs_sha512="0.1.3" guid-create="0.3.1" 0707010000000B000081A400000000000000000000000165135CC10000024D000000000000000000000000000000000000001800000000jetporch-0.0.1/Makefileall: bin loc: loc bin: sh ./version.sh # RUSTFLAGS='-C target-feature=+crt-static' cargo build --release # --target x86_64-unknown-linux-gnu cargo build --release # --target x86_64-unknown-linux-gnu m1: SDKROOT=`xcrun -sdk macosx --show-sdk-path` MACOSX_DEPLOYMENT_TARGET=13.3 cargo build --target=aarch64-apple-darwin test: clean bin chmod +x target/release/jetp #./target/release/jetp --mode ssh ./target/release/jetp ssh --playbook /tmp/foo --inventory /tmp/foo clean: rm -rf ./target run: cargo run # ./target/release/hello-rust contributors: git shortlog -sne --all 0707010000000C000081A400000000000000000000000165135CC100000336000000000000000000000000000000000000001900000000jetporch-0.0.1/README.md# JetPorch - the Jet Enterprise Professional Orchestrator Jet is a general-purpose, community-driven IT automation platform for configuration management, deployment, orchestration, patching, and arbitrary task execution workflows. Jet was created and is led by [Michael DeHaan](mailto:michael@michaeldehaan.net). # ALL DOCUMENTATION https://www.jetporch.com/ # ANNOUNCEMENTS & BLOG Sign up for free emails at https://jetporch.substack.com/ # INSTALLATION See https://www.jetporch.com/basics/installing-from-source # LICENSE Jetporch is GPLv3 licensed and collectively copyrighted by all project contributors. # CONTRIBUTING Please read https://www.jetporch.com/community/contributing first # Help, Questions, Ideas, Discussion? Please join Discord here: https://www.jetporch.com/community/discord-chat 0707010000000D000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001300000000jetporch-0.0.1/src0707010000000E000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001700000000jetporch-0.0.1/src/cli0707010000000F000081A400000000000000000000000165135CC100000329000000000000000000000000000000000000001E00000000jetporch-0.0.1/src/cli/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. pub mod parser; pub mod show; pub mod playbooks; pub mod version;07070100000010000081A400000000000000000000000165135CC100006960000000000000000000000000000000000000002100000000jetporch-0.0.1/src/cli/parser.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. // we don't use any parsing libraries here because they are a bit too automagical // this may change later. use std::env; use std::fs; use std::vec::Vec; use std::path::PathBuf; use std::sync::{Arc,RwLock}; use crate::util::io::directory_as_string; use crate::util::yaml::blend_variables; use crate::inventory::loading::convert_json_vars; use crate::util::io::jet_file_open; use crate::util::yaml::show_yaml_error_in_context; use crate::cli::version::{GIT_VERSION,GIT_BRANCH,BUILD_TIME}; use std::path::Path; use std::io; // the CLI parser struct values hold various values calculated when calling parse() on // the struct pub struct CliParser { pub playbook_paths: Arc<RwLock<Vec<PathBuf>>>, pub inventory_paths: Arc<RwLock<Vec<PathBuf>>>, pub role_paths: Arc<RwLock<Vec<PathBuf>>>, pub limit_groups: Vec<String>, pub limit_hosts: Vec<String>, pub inventory_set: bool, pub playbook_set: bool, pub mode: u32, pub needs_help: bool, pub needs_version: bool, pub show_hosts: Vec<String>, pub show_groups: Vec<String>, pub batch_size: Option<usize>, pub default_user: String, pub sudo: Option<String>, pub default_port: i64, pub threads: usize, pub verbosity: u32, pub tags: Option<Vec<String>>, pub allow_localhost_delegation: bool, pub extra_vars: serde_yaml::Value, pub forward_agent: bool, pub login_password: Option<String>, } // subcommands are usually required // FIXME: convert this to an enum pub const CLI_MODE_UNSET: u32 = 0; pub const CLI_MODE_SYNTAX: u32 = 1; pub const CLI_MODE_LOCAL: u32 = 2; pub const CLI_MODE_CHECK_LOCAL: u32 = 3; pub const CLI_MODE_SSH: u32 = 4; pub const CLI_MODE_CHECK_SSH: u32 = 5; pub const CLI_MODE_SHOW: u32 = 6; pub const CLI_MODE_SIMULATE: u32 = 7; fn is_cli_mode_valid(value: &String) -> bool { match cli_mode_from_string(value) { Ok(_) => true, Err(_) => false, } } fn cli_mode_from_string(s: &String) -> Result<u32, String> { return match s.as_str() { "local" => Ok(CLI_MODE_LOCAL), "check-local" => Ok(CLI_MODE_CHECK_LOCAL), "ssh" => Ok(CLI_MODE_SSH), "check-ssh" => Ok(CLI_MODE_CHECK_SSH), "__simulate" => Ok(CLI_MODE_SIMULATE), "show-inventory" => Ok(CLI_MODE_SHOW), _ => Err(format!("invalid mode: {}", s)) } } // all the supported flags const ARGUMENT_VERSION: &str = "--version"; const ARGUMENT_INVENTORY: & str = "--inventory"; const ARGUMENT_INVENTORY_SHORT: &str = "-i"; const ARGUMENT_PLAYBOOK: &str = "--playbook"; const ARGUMENT_PLAYBOOK_SHORT: &str = "-p"; const ARGUMENT_ROLES: &str = "--roles"; const ARGUMENT_ROLES_SHORT: &str = "-r"; const ARGUMENT_SHOW_GROUPS: &str = "--show-groups"; const ARGUMENT_SHOW_HOSTS: &str = "--show-hosts"; const ARGUMENT_LIMIT_GROUPS: &str = "--limit-groups"; const ARGUMENT_LIMIT_HOSTS: &str = "--limit-hosts"; const ARGUMENT_HELP: &str = "--help"; const ARGUMENT_PORT: &str = "--port"; const ARGUMENT_USER: &str = "--user"; const ARGUMENT_USER_SHORT: &str = "-u"; const ARGUMENT_SUDO: &str = "--sudo"; const ARGUMENT_TAGS: &str = "--tags"; const ARGUMENT_ALLOW_LOCALHOST: &str = "--allow-localhost-delegation"; const ARGUMENT_FORWARD_AGENT: &str = "--forward-agent"; const ARGUMENT_THREADS: &str = "--threads"; const ARGUMENT_THREADS_SHORT: &str = "-t"; const ARGUMENT_BATCH_SIZE: &str = "--batch-size"; const ARGUMENT_VERBOSE: &str = "-v"; const ARGUMENT_VERBOSER: &str = "-vv"; const ARGUMENT_VERBOSEST: &str = "-vvv"; const ARGUMENT_EXTRA_VARS: &str = "--extra-vars"; const ARGUMENT_ASK_LOGIN_PASSWORD: &str = "--ask-login-password"; const ARGUMENT_EXTRA_VARS_SHORT: &str = "-e"; // output from --version fn show_version() { let header_table = format!("|-|:-\n\ |jetp | http://www.jetporch.com/\n\ | | (C) Michael DeHaan + contributors, 2023\n\ | |\n\ | build | {}@{}\n\ | | {}\n\ | --- | ---\n\ | | usage: jetp <MODE> [flags]\n\ |-|-", GIT_VERSION, GIT_BRANCH, BUILD_TIME); println!(""); crate::util::terminal::markdown_print(&String::from(header_table)); println!(""); } // output from --help fn show_help() { show_version(); let mode_table = "|:-|:-|:-\n\ | *Category* | *Mode* | *Description*\n\ | --- | --- | ---\n\ | utility: |\n\ | | show-inventory | displays inventory, specify --show-groups group1:group2 or --show-hosts host1:host2\n\ | |\n\ | --- | --- | ---\n\ | local machine management: |\n\ | | check-local| looks for configuration differences on the local machine\n\ | |\n\ | | local| manages only the local machine\n\ | |\n\ | --- | --- | ---\n\ | remote machine management: |\n\ | | check-ssh | looks for configuration differences over SSH\n\ | |\n\ | | ssh| manages multiple machines over SSH\n\ |-|-"; crate::util::terminal::markdown_print(&String::from(mode_table)); println!(""); let flags_table = "|:-|:-|\n\ | *Category* | *Flags* |*Description*\n\ | --- | ---\n\ | Basics:\n\ | | -p, --playbook path1:path2| specifies automation content\n\ | |\n\ | | -i, --inventory path1:path2| (required for ssh only) specifies which systems to manage\n\ | |\n\ | | -r, --roles path1:path2| adds additional role search paths. Also uses $JET_ROLES_PATH\n\ | |\n\ | --- | ---\n\ | SSH options:\n\ | | --ask-login-password | prompt for the login password on standard input\n\ | |\n\ | | --batch-size N| fully configure this many hosts before moving to the next batch\n\ | |\n\ | | --forward-agent | enables SSH agent forwarding but only on specific tasks (ex: git)\n\ | |\n\ | | --limit-groups group1:group2 | further limits scope for playbook runs\n\ | |\n\ | | --limit-hosts host1 | further limits scope for playbook runs\n\ | |\n\ | | --port N | use this default port instead of $JET_SSH_PORT or 22\n\ | |\n\ | | -t, --threads N| how many parallel threads to use. Alternatively set $JET_THREADS\n\ | |\n\ | | -u, --user username | use this default username instead of $JET_SSH_USER or $USER\n\ | |\n\ | --- | ---\n\ | Misc options:\n\ | | --allow-localhost-delegation | signs off on variable sourcing risks and enables localhost actions with delegate_to\n\ | |\n\ | | -e, --extra-vars @filename | injects extra variables into the playbook runtime context from a YAML file, or quoted JSON\n\ | |\n\ | | --sudo username | sudo to this user by default for all tasks\n\ | |\n\ | | --tags tag1:tag2 | only run tasks or roles with one of these tags\n\ | |\n\ | | -v -vv -vvv| ever increasing verbosity\n\ | |\n\ |-|"; crate::util::terminal::markdown_print(&String::from(flags_table)); println!(""); } impl CliParser { // construct a parser with empty result values that will be filled in once parsed. pub fn new() -> Self { let p = CliParser { playbook_paths: Arc::new(RwLock::new(Vec::new())), inventory_paths: Arc::new(RwLock::new(Vec::new())), role_paths: Arc::new(RwLock::new(Vec::new())), needs_help: false, needs_version: false, mode: CLI_MODE_UNSET, show_hosts: Vec::new(), show_groups: Vec::new(), batch_size: None, default_user: match env::var("JET_SSH_USER") { Ok(x) => { println!("$JET_SSH_USER: {}", x); x }, Err(_) => match env::var("USER") { Ok(y) => y, Err(_) => String::from("root") } }, sudo: None, default_port: match env::var("JET_SSH_PORT") { Ok(x) => match x.parse::<i64>() { Ok(i) => { println!("$JET_SSH_PORT: {}", i); i }, Err(_) => { println!("environment variable JET_SSH_PORT has an invalid value, ignoring: {}", x); 22 } }, Err(_) => 22 }, threads: match env::var("JET_THREADS") { Ok(x) => match x.parse::<usize>() { Ok(i) => i, Err(_) => { println!("environment variable JET_THREADS has an invalid value, ignoring: {}", x); 20 } }, Err(_) => 20 }, inventory_set: false, playbook_set: false, verbosity: 0, limit_groups: Vec::new(), limit_hosts: Vec::new(), tags: None, allow_localhost_delegation: false, extra_vars: serde_yaml::Value::Mapping(serde_yaml::Mapping::new()), forward_agent: false, login_password: None }; return p; } pub fn show_help(&self) { show_help(); } pub fn show_version(&self) { show_version(); } // actual CLI parsing happens here pub fn parse(&mut self) -> Result<(), String> { let mut arg_count: usize = 0; let mut next_is_value = false; // we go through each CLI arg in a loop, certain arguments take // parameters and others do not. let args: Vec<String> = env::args().collect(); 'each_argument: for argument in &args { let argument_str = argument.as_str(); arg_count = arg_count + 1; match arg_count { // the program name doesn't matter 1 => continue 'each_argument, // the second argument is the subcommand name 2 => { // we should accept --help anywhere, but this is special // handling as with --help we don't need a subcommand if argument == ARGUMENT_HELP { self.needs_help = true; return Ok(()) } if argument == ARGUMENT_VERSION { self.needs_version = true; return Ok(()); } // if it's not --help, then the second argument is the // required 'mode' parameter let _result = self.store_mode(argument)?; continue 'each_argument; }, // for the rest of the arguments we need to pay attention to whether // we are reading a flag or a value, which alternate _ => { if next_is_value == false { // if we expect a flag... // the --help argument requires special handling as it has no // following value if argument_str == ARGUMENT_HELP { self.needs_help = true; return Ok(()) } if argument_str == ARGUMENT_VERSION { self.needs_version = true; return Ok(()) } let result = match argument_str { ARGUMENT_PLAYBOOK => self.append_playbook(&args[arg_count]), ARGUMENT_PLAYBOOK_SHORT => self.append_playbook(&args[arg_count]), ARGUMENT_ROLES => self.append_roles(&args[arg_count]), ARGUMENT_ROLES_SHORT => self.append_roles(&args[arg_count]), ARGUMENT_INVENTORY => self.append_inventory(&args[arg_count]), ARGUMENT_INVENTORY_SHORT => self.append_inventory(&args[arg_count]), ARGUMENT_SUDO => self.store_sudo(&args[arg_count]), ARGUMENT_TAGS => self.store_tags(&args[arg_count]), ARGUMENT_USER => self.store_default_user(&args[arg_count]), ARGUMENT_USER_SHORT => self.store_default_user(&args[arg_count]), ARGUMENT_SHOW_GROUPS => self.store_show_groups(&args[arg_count]), ARGUMENT_SHOW_HOSTS => self.store_show_hosts(&args[arg_count]), ARGUMENT_LIMIT_GROUPS => self.store_limit_groups(&args[arg_count]), ARGUMENT_LIMIT_HOSTS => self.store_limit_hosts(&args[arg_count]), ARGUMENT_BATCH_SIZE => self.store_batch_size(&args[arg_count]), ARGUMENT_THREADS => self.store_threads(&args[arg_count]), ARGUMENT_THREADS_SHORT => self.store_threads(&args[arg_count]), ARGUMENT_PORT => self.store_port(&args[arg_count]), ARGUMENT_ALLOW_LOCALHOST => self.store_allow_localhost_delegation(), ARGUMENT_FORWARD_AGENT => self.store_forward_agent(), ARGUMENT_VERBOSE => self.increase_verbosity(1), ARGUMENT_VERBOSER => self.increase_verbosity(2), ARGUMENT_VERBOSEST => self.increase_verbosity(3), ARGUMENT_EXTRA_VARS => self.store_extra_vars(&args[arg_count]), ARGUMENT_EXTRA_VARS_SHORT => self.store_extra_vars(&args[arg_count]), ARGUMENT_ASK_LOGIN_PASSWORD => self.store_login_password(), _ => Err(format!("invalid flag: {}", argument_str)), }; if result.is_err() { return result; } if argument_str.eq(ARGUMENT_VERBOSE) || argument_str.eq(ARGUMENT_VERBOSER) || argument_str.eq(ARGUMENT_VERBOSEST) || argument_str.eq(ARGUMENT_ALLOW_LOCALHOST) || argument_str.eq(ARGUMENT_FORWARD_AGENT) || argument_str.eq(ARGUMENT_ASK_LOGIN_PASSWORD) { // these do not take arguments } else { next_is_value = true; } } else { next_is_value = false; continue 'each_argument; } } // end argument numbers 3-N } } // make adjustments based on modes match self.mode { CLI_MODE_LOCAL => { self.threads = 1 }, CLI_MODE_CHECK_LOCAL => { self.threads = 1 }, CLI_MODE_SYNTAX => { self.threads = 1 }, CLI_MODE_SHOW => { self.threads = 1 }, CLI_MODE_UNSET => { self.needs_help = true; }, _ => {} } if self.playbook_set { self.add_role_paths_from_environment()?; self.add_implicit_role_paths()?; } Ok(()) } fn store_mode(&mut self, value: &String) -> Result<(), String> { if is_cli_mode_valid(value) { self.mode = cli_mode_from_string(value).unwrap(); return Ok(()); } return Err(format!("jetp mode ({}) is not valid, see --help", value)) } fn append_playbook(&mut self, value: &String) -> Result<(), String> { self.playbook_set = true; match parse_paths(&String::from("-p/--playbook"), value) { Ok(paths) => { for p in paths.iter() { if p.is_file() { let full = std::fs::canonicalize(p.as_path()).unwrap(); self.playbook_paths.write().unwrap().push(full.to_path_buf()); } else { return Err(format!("playbook file missing: {:?}", p)); } } }, Err(err_msg) => return Err(format!("--{} {}", ARGUMENT_PLAYBOOK, err_msg)), } return Ok(()); } fn append_roles(&mut self, value: &String) -> Result<(), String> { // FIXME: TODO: also load from environment at JET_ROLES_PATH match parse_paths(&String::from("-r/--roles"), value) { Ok(paths) => { for p in paths.iter() { if p.is_dir() { let full = std::fs::canonicalize(p.as_path()).unwrap(); self.role_paths.write().unwrap().push(full.to_path_buf()); } else { return Err(format!("roles directory not found: {:?}", p)); } } }, Err(err_msg) => return Err(format!("--{} {}", ARGUMENT_ROLES, err_msg)), } return Ok(()); } fn append_inventory(&mut self, value: &String) -> Result<(), String> { self.inventory_set = true; if self.mode == CLI_MODE_LOCAL || self.mode == CLI_MODE_CHECK_LOCAL { return Err(format!("--inventory cannot be specified for local modes")); } match parse_paths(&String::from("-i/--inventory"),value) { Ok(paths) => { for p in paths.iter() { self.inventory_paths.write().unwrap().push(p.clone()); } } Err(err_msg) => return Err(format!("--{} {}", ARGUMENT_INVENTORY, err_msg)), } return Ok(()); } fn store_show_groups(&mut self, value: &String) -> Result<(), String> { match split_string(value) { Ok(values) => { self.show_groups = values; }, Err(err_msg) => return Err(format!("--{} {}", ARGUMENT_SHOW_GROUPS, err_msg)), } return Ok(()); } fn store_show_hosts(&mut self, value: &String) -> Result<(), String> { match split_string(value) { Ok(values) => { self.show_hosts = values; }, Err(err_msg) => return Err(format!("--{} {}", ARGUMENT_SHOW_HOSTS, err_msg)), } return Ok(()); } fn store_limit_groups(&mut self, value: &String) -> Result<(), String> { match split_string(value) { Ok(values) => { self.limit_groups = values; }, Err(err_msg) => return Err(format!("--{} {}", ARGUMENT_LIMIT_GROUPS, err_msg)), } return Ok(()); } fn store_limit_hosts(&mut self, value: &String) -> Result<(), String> { match split_string(value) { Ok(values) => { self.limit_hosts = values; }, Err(err_msg) => return Err(format!("--{} {}", ARGUMENT_LIMIT_HOSTS, err_msg)), } return Ok(()); } fn store_tags(&mut self, value: &String) -> Result<(), String> { match split_string(value) { Ok(values) => { self.tags = Some(values); }, Err(err_msg) => return Err(format!("--{} {}", ARGUMENT_TAGS, err_msg)), } return Ok(()); } fn store_sudo(&mut self, value: &String) -> Result<(), String> { self.sudo = Some(value.clone()); return Ok(()); } fn store_default_user(&mut self, value: &String) -> Result<(), String> { self.default_user = value.clone(); return Ok(()); } fn store_batch_size(&mut self, value: &String) -> Result<(), String> { if self.batch_size.is_some() { return Err(format!("{} has been specified already", ARGUMENT_BATCH_SIZE)); } match value.parse::<usize>() { Ok(n) => { self.batch_size = Some(n); return Ok(()); }, Err(_e) => { return Err(format!("{}: invalid value",ARGUMENT_BATCH_SIZE)); } } } fn store_threads(&mut self, value: &String) -> Result<(), String> { match value.parse::<usize>() { Ok(n) => { self.threads = n; return Ok(()); } Err(_e) => { return Err(format!("{}: invalid value", ARGUMENT_THREADS)); } } } fn store_port(&mut self, value: &String) -> Result<(), String> { match value.parse::<i64>() { Ok(n) => { self.default_port = n; return Ok(()); } Err(_e) => { return Err(format!("{}: invalid value", ARGUMENT_PORT)); } } } fn store_allow_localhost_delegation(&mut self) -> Result<(), String> { self.allow_localhost_delegation = true; Ok(()) } fn increase_verbosity(&mut self, amount: u32) -> Result<(), String> { self.verbosity = self.verbosity + amount; return Ok(()) } fn add_implicit_role_paths(&mut self) -> Result<(), String> { let paths = self.playbook_paths.read().unwrap(); for pb in paths.iter() { let dirname = directory_as_string(pb.as_path()); let mut pathbuf = PathBuf::new(); pathbuf.push(dirname); pathbuf.push("roles"); if pathbuf.is_dir() { let full = fs::canonicalize(pathbuf.as_path()).unwrap(); self.role_paths.write().unwrap().push(full.to_path_buf()); } else { // ignore as there does not need to be a roles/ dir alongside playbooks } } return Ok(()); } fn add_role_paths_from_environment(&mut self) -> Result<(), String> { let env_roles_path = env::var("JET_ROLES_PATH"); if env_roles_path.is_ok() { match parse_paths(&String::from("$JET_ROLES_PATH"), &env_roles_path.unwrap()) { Ok(paths) => { for p in paths.iter() { if p.is_dir() { let full = fs::canonicalize(p.as_path()).unwrap(); self.role_paths.write().unwrap().push(full.to_path_buf()); } } }, Err(y) => return Err(y) }; } return Ok(()); } fn store_extra_vars(&mut self, value: &String) -> Result<(), String> { if value.starts_with("@") { // input is a filename where the data is YAML let rest_of_path = value.replace("@",""); let path = Path::new(&rest_of_path); if ! path.is_file() { return Err(format!("--extra-vars parameter with @ expects a file: {}", rest_of_path)) } let extra_file = jet_file_open(path)?; let parsed: Result<serde_yaml::Mapping, serde_yaml::Error> = serde_yaml::from_reader(extra_file); if parsed.is_err() { show_yaml_error_in_context(&parsed.unwrap_err(), &path); return Err(format!("edit the file and try again?")); } blend_variables(&mut self.extra_vars, serde_yaml::Value::Mapping(parsed.unwrap())); } else { // input is inline JSON (as YAML wouldn't make sense with the newlines) let parsed: Result<serde_json::Value, serde_json::Error> = serde_json::from_str(value); let actual = match parsed { Ok(x) => x, Err(y) => { return Err(format!("inline json is not valid: {}", y)) } }; let serde_map = convert_json_vars(&actual); blend_variables(&mut self.extra_vars, serde_yaml::Value::Mapping(serde_map)); } return Ok(()); } fn store_forward_agent(&mut self) -> Result<(), String>{ self.forward_agent = true; return Ok(()); } fn store_login_password(&mut self) -> Result<(), String>{ let mut value = String::new(); println!("enter login password:"); match io::stdin().read_line(&mut value) { Ok(_) => { self.login_password = Some(String::from(value.trim())); println!("GOT IT!: ({:?})", self.login_password.clone()) } Err(e) => return Err(format!("failure reading input: {}", e)) } return Ok(()); } } fn split_string(value: &String) -> Result<Vec<String>, String> { return Ok(value.split(":").map(|x| String::from(x)).collect()); } // accept paths eliminated by ":" and return a list of paths, provided they exist fn parse_paths(from: &String, value: &String) -> Result<Vec<PathBuf>, String> { let string_paths = value.split(":"); let mut results = Vec::new(); for string_path in string_paths { let mut path_buf = PathBuf::new(); path_buf.push(string_path); if path_buf.exists() { results.push(path_buf) } else { return Err(format!("path ({}) specified by ({}) does not exist", string_path, from)); } } return Ok(results); } 07070100000011000081A400000000000000000000000165135CC100001227000000000000000000000000000000000000002400000000jetporch-0.0.1/src/cli/playbooks.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::cli::parser::CliParser; use crate::connection::ssh::SshFactory; use crate::connection::local::LocalFactory; use crate::connection::no::NoFactory; use crate::playbooks::traversal::{playbook_traversal,RunState}; use crate::playbooks::context::PlaybookContext; use crate::playbooks::visitor::PlaybookVisitor; use crate::inventory::inventory::Inventory; use std::sync::{Arc,RwLock}; // code behind *most* playbook related CLI commands, launched from main.rs // FIXME: the original plan was for visitors to be able to override more behavior // than just check mode, but so far we are *not* using it. We can // probably move to merging visitor with // context, and using the CheckMode enum when constructing context, // eliminating some weird interactions // when sometimes visitor calls context and in other times outside code // calls context. struct CheckVisitor {} impl CheckVisitor { pub fn new() -> Self { Self {} } } impl PlaybookVisitor for CheckVisitor { fn is_check_mode(&self) -> bool { return true; } } struct LiveVisitor {} impl LiveVisitor { pub fn new() -> Self { Self {} } } impl PlaybookVisitor for LiveVisitor { fn is_check_mode(&self) -> bool { return false; } } enum CheckMode { Yes, No } enum ConnectionMode { Ssh, Local, Simulate } pub fn playbook_ssh(inventory: &Arc<RwLock<Inventory>>, parser: &CliParser) -> i32 { return playbook(inventory, parser, CheckMode::No, ConnectionMode::Ssh); } pub fn playbook_check_ssh(inventory: &Arc<RwLock<Inventory>>, parser: &CliParser) -> i32 { return playbook(inventory, parser, CheckMode::Yes, ConnectionMode::Ssh); } pub fn playbook_local(inventory: &Arc<RwLock<Inventory>>, parser: &CliParser) -> i32 { return playbook(inventory, parser, CheckMode::No, ConnectionMode::Local); } pub fn playbook_check_local(inventory: &Arc<RwLock<Inventory>>, parser: &CliParser) -> i32 { return playbook(inventory, parser, CheckMode::Yes, ConnectionMode::Local); } pub fn playbook_simulate(inventory: &Arc<RwLock<Inventory>>, parser: &CliParser) -> i32 { return playbook(inventory, parser, CheckMode::No, ConnectionMode::Simulate); } fn playbook(inventory: &Arc<RwLock<Inventory>>, parser: &CliParser, check_mode: CheckMode, connection_mode: ConnectionMode) -> i32 { let run_state = Arc::new(RunState { // every object gets an inventory, though with local modes it's empty. inventory: Arc::clone(inventory), playbook_paths: Arc::clone(&parser.playbook_paths), role_paths: Arc::clone(&parser.role_paths), limit_hosts: parser.limit_hosts.clone(), limit_groups: parser.limit_groups.clone(), batch_size: parser.batch_size.clone(), // the context is constructed with an instance of the parser instead of having a back-reference // to run-state. Context should mostly *not* get parameters from the parser unless they // are going to appear in variables. context: Arc::new(RwLock::new(PlaybookContext::new(parser))), visitor: match check_mode { CheckMode::Yes => Arc::new(RwLock::new(CheckVisitor::new())), CheckMode::No => Arc::new(RwLock::new(LiveVisitor::new())), }, connection_factory: match connection_mode { ConnectionMode::Ssh => Arc::new(RwLock::new(SshFactory::new(inventory, parser.forward_agent, parser.login_password.clone()))), ConnectionMode::Local => Arc::new(RwLock::new(LocalFactory::new(inventory))), ConnectionMode::Simulate => Arc::new(RwLock::new(NoFactory::new())) }, tags: parser.tags.clone(), allow_localhost_delegation: parser.allow_localhost_delegation }); return match playbook_traversal(&run_state) { Ok(_) => run_state.visitor.read().unwrap().get_exit_status(&run_state.context), Err(s) => { println!("{}", s); 1 } }; } 07070100000012000081A400000000000000000000000165135CC100001609000000000000000000000000000000000000001F00000000jetporch-0.0.1/src/cli/show.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::util::terminal::{two_column_table, captioned_display}; use std::sync::Arc; use std::sync::RwLock; use crate::inventory::inventory::Inventory; // cli support for the show-inventory subcommand fn string_slice(values: &Vec<String>) -> String { // if there are too many values the output of various group/host lists in the tables // stops being useful. we may want to have some flag where we don't show the // nice tables for this, though right now they really don't exist if values.len() > 500 { let tmp = values[0..499].to_vec(); return format!("{}, ...", tmp.join(", ")); } return values.join(", "); } // ============================================================================================================== // PUBLIC API // ============================================================================================================== // jetp show --inventory <path> --hosts host1:host2 pub fn show_inventory_host(inventory: &Arc<RwLock<Inventory>>, host_name: &String) -> Result<(),String> { let inventory = inventory.read().expect("inventory read"); if !inventory.has_host(&host_name.clone()) { return Err(format!("no such host: {}", host_name.clone())); } let binding = inventory.get_host(&host_name.clone()); let host = binding.read().unwrap(); println!("Host: {}", host_name); println!(" "); let mut parents : Vec<String> = host.get_group_names(); let mut ancestors : Vec<String> = host.get_ancestor_group_names(); let blended_variables = host.get_blended_variables_yaml()?; parents.sort(); ancestors.sort(); let ancestor_string = string_slice(&ancestors); let parents_string = string_slice(&parents); let host_elements : Vec<(String,String)> = vec![ (String::from("Ancestor Groups"), ancestor_string), (String::from("Direct Groups"), parents_string), ]; two_column_table(&String::from("Host Report:"), &String::from(""), &host_elements); println!(""); captioned_display(&String::from("Variables"), &blended_variables); println!(""); return Ok(()); } // jetp show --inventory <path> # implicit --group all // jetp show --inventory <path> --groups group1:group2 pub fn show_inventory_group(inventory: &Arc<RwLock<Inventory>>, group_name: &String) -> Result<(),String> { let inventory = inventory.read().expect("inventory read"); if !inventory.has_group(&group_name.clone()) { return Err(format!("no such group: {}", group_name)); } let binding = inventory.get_group(&group_name.clone()); let group = binding.read().unwrap(); println!("Group: {}", group_name); println!(""); let mut descendants : Vec<String> = group.get_descendant_group_names(); let mut children : Vec<String> = group.get_subgroup_names(); let mut ancestors : Vec<String> = group.get_ancestor_group_names(); let mut parents : Vec<String> = group.get_parent_group_names(); let mut descendant_hosts : Vec<String> = group.get_descendant_host_names(); let mut child_hosts : Vec<String> = group.get_direct_host_names(); descendants.sort(); children.sort(); ancestors.sort(); parents.sort(); descendant_hosts.sort(); child_hosts.sort(); let blended_variables = group.get_blended_variables_yaml()?; let descendant_hosts_count = String::from(format!("{}", descendant_hosts.len())); let child_hosts_count = String::from(format!("{}", child_hosts.len())); // TODO: add a method that "..."'s these strings if too long - just use for hosts let descendants_string = string_slice(&descendants); let children_string = string_slice(&children); let ancestors_string = string_slice(&ancestors); let parents_string = string_slice(&parents); let descendant_hosts_string = string_slice(&descendant_hosts); let child_hosts_string = string_slice(&child_hosts); let group_elements : Vec<(String,String)> = vec![ (String::from("All Descendants"), descendants_string), (String::from("Children"), children_string), (String::from("All Ancestors"), ancestors_string), (String::from("Parents"), parents_string) ]; let host_elements : Vec<(String, String)> = vec![ (format!("All Ancestors ({})",descendant_hosts_count), descendant_hosts_string), (format!("Children ({})", child_hosts_count), child_hosts_string), ]; two_column_table(&String::from("Group Report:"), &String::from(""), &group_elements); println!(""); two_column_table(&String::from("Host Report:"), &String::from(""), &host_elements); println!(""); captioned_display(&String::from("Variables"), &blended_variables); println!(""); return Ok(()); } 07070100000013000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001E00000000jetporch-0.0.1/src/connection07070100000014000081A400000000000000000000000165135CC100000757000000000000000000000000000000000000002700000000jetporch-0.0.1/src/connection/cache.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::connection::connection::{Connection}; use crate::inventory::hosts::Host; use std::sync::Arc; use std::sync::Mutex; use std::sync::RwLock; use std::collections::HashMap; pub struct ConnectionCache { connections: HashMap<String, Arc<Mutex<dyn Connection>>> } impl ConnectionCache { pub fn new() -> Self { Self { connections: HashMap::new() } } pub fn add_connection(&mut self, host:&Arc<RwLock<Host>>, connection: &Arc<Mutex<dyn Connection>>) { let host2 = host.read().expect("host read"); self.connections.insert(host2.name.clone(), Arc::clone(connection)); } pub fn has_connection(&self, host: &Arc<RwLock<Host>>) -> bool { let host2 = host.read().expect("host read"); return self.connections.contains_key(&host2.name.clone()); } pub fn get_connection(&self, host: &Arc<RwLock<Host>>) -> Arc<Mutex<dyn Connection>> { let host2 = host.read().expect("host read"); return Arc::clone(self.connections.get(&host2.name.clone()).unwrap()); } pub fn clear(&mut self) { self.connections.clear(); } }07070100000015000081A400000000000000000000000165135CC100000554000000000000000000000000000000000000002900000000jetporch-0.0.1/src/connection/command.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::sync::Arc; use crate::tasks::response::TaskResponse; // details useful for working with commands // not much here, see handle/remote.rs for more #[derive(Clone,Debug)] pub struct CommandResult { pub cmd: String, pub out: String, pub rc: i32 } #[derive(Debug,Copy,Clone,PartialEq)] pub enum Forward { Yes, No } pub fn cmd_info(info: &Arc<TaskResponse>) -> (i32, String) { assert!(info.command_result.is_some(), "called cmd_info on a response that is not a command result"); let result = info.command_result.as_ref().as_ref().unwrap(); return (result.rc, result.out.clone()); }07070100000016000081A400000000000000000000000165135CC1000006B1000000000000000000000000000000000000002C00000000jetporch-0.0.1/src/connection/connection.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::request::TaskRequest; use crate::tasks::response::TaskResponse; use crate::handle::response::Response; use std::sync::Arc; use std::marker::{Send,Sync}; use std::path::Path; use crate::connection::command::Forward; // the connection trait that serves as the base for SshConnection, LocalConnection, and NoConnection pub trait Connection : Send + Sync { fn connect(&mut self) -> Result<(),String>; // FIXME: add error return objects fn write_data(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, data: &String, remote_path: &String) -> Result<(),Arc<TaskResponse>>; fn copy_file(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, src: &Path, dest: &String) -> Result<(), Arc<TaskResponse>>; fn whoami(&self) -> Result<String,String>; fn run_command(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, cmd: &String, forward: Forward) -> Result<Arc<TaskResponse>,Arc<TaskResponse>>; }07070100000017000081A400000000000000000000000165135CC10000055B000000000000000000000000000000000000002900000000jetporch-0.0.1/src/connection/factory.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::connection::connection::Connection; use crate::playbooks::context::PlaybookContext; use crate::inventory::hosts::Host; use std::sync::Arc; use std::sync::Mutex; use std::sync::RwLock; use std::marker::{Send,Sync}; // the factory trait that serves as the base for SshFactory, LocalFactory, and NoFactory pub trait ConnectionFactory : Send + Sync { fn get_connection(&self, context: &Arc<RwLock<PlaybookContext>>, host: &Arc<RwLock<Host>>) -> Result<Arc<Mutex<dyn Connection>>, String>; fn get_local_connection(&self, context: &Arc<RwLock<PlaybookContext>>) -> Result<Arc<Mutex<dyn Connection>>, String>; } 07070100000018000081A400000000000000000000000165135CC1000020D3000000000000000000000000000000000000002700000000jetporch-0.0.1/src/connection/local.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::connection::connection::Connection; use crate::connection::command::CommandResult; use crate::playbooks::context::PlaybookContext; use crate::connection::factory::ConnectionFactory; use crate::connection::command::Forward; use crate::inventory::hosts::Host; use crate::handle::response::Response; use crate::tasks::{TaskRequest,TaskResponse}; use std::sync::Arc; use std::sync::Mutex; use std::sync::RwLock; use std::process::Command; use crate::Inventory; use crate::util::io::jet_file_open; use std::fs::File; use std::path::Path; use std::io::Write; use std::env; // implementation for both the local connection factory and local connections #[allow(dead_code)] pub struct LocalFactory { local_connection: Arc<Mutex<dyn Connection>>, inventory: Arc<RwLock<Inventory>> } impl LocalFactory { pub fn new(inventory: &Arc<RwLock<Inventory>>) -> Self { // we require a localhost to be in the inventory and immediately construct a connection to it let host = inventory.read().expect("inventory read").get_host(&String::from("localhost")); let mut lc = LocalConnection::new(&Arc::clone(&host)); lc.connect().expect("connection ok"); Self { inventory: Arc::clone(&inventory), local_connection: Arc::new(Mutex::new(lc)) } } } impl ConnectionFactory for LocalFactory { fn get_connection(&self, _context: &Arc<RwLock<PlaybookContext>>, _host: &Arc<RwLock<Host>>) -> Result<Arc<Mutex<dyn Connection>>,String> { // rather than producing new connections, this always returns a clone of the already established local connection from the constructor let conn : Arc<Mutex<dyn Connection>> = Arc::clone(&self.local_connection); return Ok(conn); } fn get_local_connection(&self, _context: &Arc<RwLock<PlaybookContext>>) -> Result<Arc<Mutex<dyn Connection>>, String> { let conn : Arc<Mutex<dyn Connection>> = Arc::clone(&self.local_connection); return Ok(conn); } } pub struct LocalConnection { host: Arc<RwLock<Host>>, } impl LocalConnection { pub fn new(host: &Arc<RwLock<Host>>) -> Self { Self { host: Arc::clone(&host) } } fn trim_newlines(&self, s: &mut String) { if s.ends_with('\n') { s.pop(); if s.ends_with('\r') { s.pop(); } } } } impl Connection for LocalConnection { fn whoami(&self) -> Result<String,String> { // get the currently logged in user. let user_result = env::var("USER"); return match user_result { Ok(x) => Ok(x), Err(y) => Err(format!("environment variable $USER: {y}")) }; } fn connect(&mut self) -> Result<(),String> { // upon connection make sure the localhost detection routine runs let result = detect_os(&self.host); if result.is_ok() { return Ok(()); } else { let (_rc, out) = result.unwrap_err(); return Err(out); } } fn run_command(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, cmd: &String, _forward: Forward) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let mut base = Command::new("sh"); let command = base.arg("-c").arg(cmd).arg("2>&1"); match command.output() { Ok(x) => { match x.status.code() { Some(rc) => { let mut out = convert_out(&x.stdout,&x.stderr); self.trim_newlines(&mut out); return Ok(response.command_ok(request,&Arc::new(Some(CommandResult { cmd: cmd.clone(), out: out.clone(), rc: rc })))); }, None => { return Err(response.command_failed(request, &Arc::new(Some(CommandResult { cmd: cmd.clone(), out: String::from(""), rc: 418 })))); } } }, Err(_x) => { return Err(response.command_failed(request, &Arc::new(Some(CommandResult { cmd: cmd.clone(), out: String::from(""), rc: 404 })))); } }; } fn copy_file(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, src: &Path, remote_path: &String) -> Result<(), Arc<TaskResponse>> { // FIXME: this (temporary) implementation currently loads the file contents into memory which we do not want // copy the files with system calls instead. let remote_path2 = Path::new(remote_path); let result = std::fs::copy(src, &remote_path2); return match result { Ok(_x) => Ok(()), Err(e) => { return Err(response.is_failed(&request, &format!("copy failed: {:?}", e))) } } } fn write_data(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, data: &String, remote_path: &String) -> Result<(),Arc<TaskResponse>> { let path = Path::new(&remote_path); if path.exists() { let mut file = match jet_file_open(path) { Ok(x) => x, Err(y) => return Err(response.is_failed(&request, &format!("failed to open: {}: {:?}", remote_path, y))) }; let write_result = write!(file, "{}", data); match write_result { Ok(_) => {}, Err(y) => return Err(response.is_failed(&request, &format!("failed to write: {}: {:?}", remote_path, y))) }; } else { let mut file = match File::create(&path) { Ok(x) => x, Err(y) => return Err(response.is_failed(&request, &format!("failed to create: {}: {:?}", remote_path, y))) }; let write_result = write!(file, "{}", data); match write_result { Ok(_) => {}, Err(y) => return Err(response.is_failed(&request, &format!("failed to write: {}: {:?}", remote_path, y))) }; } return Ok(()); } } pub fn convert_out(output: &Vec<u8>, err: &Vec<u8>) -> String { // output from the Rust command class can contain junk bytes, here we mostly don't try to solve this yet // and will basically fail if output contains junk. This may be dealt with later. let mut base = match std::str::from_utf8(output) { Ok(val) => val.to_string(), Err(_) => String::from("invalid UTF-8 characters in response"), }; let rest = match std::str::from_utf8(err) { Ok(val) => val.to_string(), Err(_) => String::from("invalid UTF-8 characters in response"), }; base.push_str("\n"); base.push_str(&rest); return base.trim().to_string(); } fn detect_os(host: &Arc<RwLock<Host>>) -> Result<(),(i32, String)> { // upon connection we run uname -a on connect to check the OS type. let mut base = Command::new("uname"); let command = base.arg("-a"); return match command.output() { Ok(x) => match x.status.code() { Some(0) => { let out = convert_out(&x.stdout,&x.stderr); { match host.write().unwrap().set_os_info(&out) { Ok(_) => { }, Err(_) => { return Err((500, String::from("failed to set OS info"))); } } } Ok(()) } Some(status) => Err((status, convert_out(&x.stdout, &x.stderr))), _ => Err((418, String::from("uname -a failed without status code"))) }, Err(_x) => Err((418, String::from("uname -a failed without status code"))) } } 07070100000019000081A400000000000000000000000165135CC100000354000000000000000000000000000000000000002500000000jetporch-0.0.1/src/connection/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. pub mod connection; pub mod factory; pub mod ssh; pub mod local; pub mod no; pub mod command; pub mod cache;0707010000001A000081A400000000000000000000000165135CC100000DE3000000000000000000000000000000000000002400000000jetporch-0.0.1/src/connection/no.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::connection::connection::{Connection}; use crate::connection::factory::ConnectionFactory; use crate::playbooks::context::PlaybookContext; use crate::inventory::hosts::{Host,HostOSType}; use crate::tasks::request::TaskRequest; use crate::tasks::response::TaskResponse; use crate::handle::response::Response; use crate::connection::command::CommandResult; use crate::connection::command::Forward; use std::sync::Arc; use std::sync::Mutex; use std::sync::RwLock; use std::path::Path; // the noconnection and nofactory are not really used in normal execution of jet, but are around in the "__simulate" hidden // suboption, as this is occasionally useful for testing certain jet internals. This is not meant for serious work. pub struct NoFactory {} impl NoFactory { pub fn new() -> Self { Self {} } } impl ConnectionFactory for NoFactory { fn get_connection(&self, _context: &Arc<RwLock<PlaybookContext>>, host: &Arc<RwLock<Host>>) -> Result<Arc<Mutex<dyn Connection>>,String> { // we just pretend everything is Linux for now host.write().unwrap().os_type = Some(HostOSType::Linux); let conn : Arc<Mutex<dyn Connection>> = Arc::new(Mutex::new(NoConnection::new())); return Ok(conn); } fn get_local_connection(&self, _context: &Arc<RwLock<PlaybookContext>>) -> Result<Arc<Mutex<dyn Connection>>, String> { let conn : Arc<Mutex<dyn Connection>> = Arc::new(Mutex::new(NoConnection::new())); return Ok(conn); } } pub struct NoConnection { } impl NoConnection { pub fn new() -> Self { Self { } } } impl Connection for NoConnection { fn whoami(&self) -> Result<String,String> { // we don't really bother with saying what username is connected return Ok(String::from("root")); } fn connect(&mut self) -> Result<(),String> { // all connections are imaginary so there's nothing to do return Ok(()); } fn run_command(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, cmd: &String, _forward: Forward) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { // all commands return junk output pretending they were successful return Ok(response.command_ok(request,&Arc::new(Some(CommandResult { cmd: cmd.clone(), out: String::from("__simulated__"), rc: 0 })))); } fn write_data(&self, _response: &Arc<Response>, _request: &Arc<TaskRequest>, _data: &String, _remote_path: &String) -> Result<(),Arc<TaskResponse>>{ // no data is transferred, we just pretend things were successful return Ok(()); } fn copy_file(&self, _response: &Arc<Response>, _request: &Arc<TaskRequest>, _src: &Path, _dest: &String) -> Result<(), Arc<TaskResponse>> { // no data is transferred, as per above return Ok(()); } }0707010000001B000081A400000000000000000000000165135CC100003FBD000000000000000000000000000000000000002500000000jetporch-0.0.1/src/connection/ssh.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::connection::connection::Connection; use crate::connection::command::CommandResult; use crate::connection::factory::ConnectionFactory; use crate::playbooks::context::PlaybookContext; use crate::connection::local::LocalFactory; use crate::tasks::*; use crate::inventory::hosts::Host; use crate::Inventory; use crate::handle::response::Response; use crate::connection::command::Forward; use crate::connection::local::convert_out; use std::process::Command; use std::sync::{Arc,Mutex,RwLock}; use ssh2::Session; use std::io::{Read,Write}; use std::net::TcpStream; use std::path::Path; use std::time::Duration; use std::net::ToSocketAddrs; use std::fs::File; // implementation for both Ssh Connections and the Ssh Connection factory pub struct SshFactory { local_factory: LocalFactory, localhost: Arc<RwLock<Host>>, forward_agent: bool, login_password: Option<String> } impl SshFactory { pub fn new(inventory: &Arc<RwLock<Inventory>>, forward_agent: bool, login_password: Option<String>) -> Self { // we create a local connection factory for localhost rather than establishing local connections with SSH Self { localhost : inventory.read().expect("inventory read").get_host(&String::from("localhost")), local_factory: LocalFactory::new(inventory), forward_agent, login_password } } } impl ConnectionFactory for SshFactory { fn get_local_connection(&self, context: &Arc<RwLock<PlaybookContext>>) -> Result<Arc<Mutex<dyn Connection>>, String> { return Ok(self.local_factory.get_connection(context, &self.localhost)?); } fn get_connection(&self, context: &Arc<RwLock<PlaybookContext>>, host:&Arc<RwLock<Host>>) -> Result<Arc<Mutex<dyn Connection>>, String> { let ctx = context.read().expect("context read"); let hostname1 = host.read().expect("host read").name.clone(); if hostname1.eq("localhost") { // if we are asked for a connection to localhost because it's in a group, we'll be called here // instead of from get_local_connecton, so have to return the local connection versus assuming SSH let conn : Arc<Mutex<dyn Connection>> = self.local_factory.get_connection(context, &self.localhost)?; return Ok(conn); } { // SSH connections are kept open between tasks generally but cleared at many strategic points during playbook traversal // between plays, in between batches, etc. let cache = ctx.connection_cache.read().unwrap(); if cache.has_connection(host) { let conn = cache.get_connection(host); return Ok(conn); } } // how we connect to a host depends on some settings of the play (ssh_port, ssh_user), the CLI (--user) and // possibly magic variables on the host. The context contains all of this logic. let (hostname2, user, port) = ctx.get_ssh_connection_details(host); if hostname2.eq("localhost") { // jet_ssh_hostname was set to localhost, which doesn't make a lot of sense but could happen in testing // contrived playbooks when we don't want a lot of real remote hosts let conn : Arc<Mutex<dyn Connection>> = self.local_factory.get_connection(context, &self.localhost)?; return Ok(conn); } // actually connect here let mut conn = SshConnection::new(Arc::clone(&host), &user, port, self.forward_agent, self.login_password.clone()); return match conn.connect() { Ok(_) => { let conn2 : Arc<Mutex<dyn Connection>> = Arc::new(Mutex::new(conn)); ctx.connection_cache.write().expect("connection cache write").add_connection( &Arc::clone(&host), &Arc::clone(&conn2)); Ok(conn2) }, Err(x) => { Err(x) } } } } pub struct SshConnection { pub host: Arc<RwLock<Host>>, pub username: String, pub port: i64, pub session: Option<Session>, pub forward_agent: bool, pub login_password: Option<String> } impl SshConnection { pub fn new(host: Arc<RwLock<Host>>, username: &String, port: i64, forward_agent: bool, login_password: Option<String>) -> Self { Self { host: Arc::clone(&host), username: username.clone(), port, session: None, forward_agent, login_password } } } impl Connection for SshConnection { fn whoami(&self) -> Result<String,String> { // if asked who we are logged in as, it is the user we have connected with // sudoers info is on top of that, and this logic is expressed in remote.rs return Ok(self.username.clone()); } fn connect(&mut self) -> Result<(), String> { if self.session.is_some() { // don't re-connect if we are already connected (the code might not try this anyway?) return Ok(()); } // derived from docs at https://docs.rs/ssh2/latest/ssh2/ let session = match Session::new() { Ok(x) => x, Err(_y) => { return Err(String::from("failed to attach to session")); } }; let mut agent = match session.agent() { Ok(x) => x, Err(_y) => { return Err(String::from("failed to acquire SSH-agent")); } }; // Connect the agent match agent.connect() { Ok(_x) => {}, Err(_y) => { return Err(String::from("failed to connect to SSH-agent")) }} // currently we don't do anything with listing the identities in SSH agent. It might be helpful to provide a nice error // if none were detected //agent.list_identities().unwrap(); //for identity in agent.identities().unwrap() { // println!("{}", identity.comment()); // let _pubkey = identity.blob(); //} // Connect to the local SSH server - need to get socketaddrs first in order to use Duration for timeout let seconds = Duration::from_secs(10); assert!(!self.host.read().expect("host read").name.eq("localhost")); let connect_str = format!("{host}:{port}", host=self.host.read().expect("host read").name, port=self.port.to_string()); // connect with timeout requires SocketAddr objects instead of just connection strings let addrs_iter = connect_str.as_str().to_socket_addrs(); // check for errors let mut addrs_iter2 = match addrs_iter { Err(_x) => { return Err(String::from("unable to resolve")); }, Ok(y) => y }; let addr = addrs_iter2.next(); if ! addr.is_some() { return Err(String::from("unable to resolve(2)")); } // actually connect (finally) here let tcp = match TcpStream::connect_timeout(&addr.unwrap(), seconds) { Ok(x) => x, _ => { return Err(format!("SSH connection attempt failed for {}:{}", self.host.read().expect("host read").name, self.port)); } }; // new session & handshake let mut sess = match Session::new() { Ok(x) => x, _ => { return Err(String::from("SSH session failed")); } }; sess.set_tcp_stream(tcp); match sess.handshake() { Ok(_) => {}, _ => { return Err(String::from("SSH handshake failed")); } } ; //let identities = agent.identities(); if self.login_password.is_some() { match sess.userauth_password(&self.username.clone(), self.login_password.clone().unwrap().as_str()) { Ok(_) => {}, Err(x) => { return Err(format!("SSH password authentication failed for user {}: {}", self.username, x)); } } } else { // try to authenticate with the identities in the agent match sess.userauth_agent(&self.username) { Ok(_) => {}, Err(x) => { return Err(format!("SSH agent authentication failed for user {}: {}", self.username, x)); } }; } if !(sess.authenticated()) { return Err("failed to authenticate".to_string()); }; // OS detection -- always run uname -a on first connect so we know the OS type, which will allow the command library and facts // module to work correctly. self.session = Some(sess); let uname_result = self.run_command_low_level(&String::from("uname -a")); match uname_result { Ok((_rc,out)) => { { match self.host.write().unwrap().set_os_info(&out.clone()) { Ok(_x) => {}, Err(_y) => return Err(format!("failed to set OS info")) } } //match result2 { Ok(_) => {}, Err(s) => { return Err(s.to_string()) } } }, Err((rc,out)) => return Err(format!("uname -a command failed: rc={}, out={}", rc,out)) } return Ok(()); } fn run_command(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, cmd: &String, forward: Forward) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let result = match forward { Forward::Yes => match self.forward_agent { false => self.run_command_low_level(cmd), true => self.run_command_with_ssh_a(cmd) }, Forward::No => self.run_command_low_level(cmd) }; match result { Ok((rc,s)) => { // note that non-zero return codes are "ok" to the connection plugin, handle elsewhere! return Ok(response.command_ok(request, &Arc::new(Some(CommandResult { cmd: cmd.clone(), out: s.clone(), rc: rc })))); }, Err((rc,s)) => { return Err(response.command_failed(request, &Arc::new(Some(CommandResult { cmd: cmd.clone(), out: s.clone(), rc: rc })))); } } } fn write_data(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, data: &String, remote_path: &String) -> Result<(),Arc<TaskResponse>> { // SFTP writing does not allow root to overwrite files root does not own, and does not support sudo. // as such this is a pretty low level write (as is copy_file) and logic around tempfiles and permissions is handled in remote.rs // write_data writes a string and is really meant for small files like the template module. Large files should use copy_file instead. let session = self.session.as_ref().expect("session not established"); let sftp_result = session.sftp(); let sftp = match sftp_result { Ok(x) => x, Err(y) => { return Err(response.is_failed(request, &format!("sftp connection failed: {y}"))); } }; let sftp_path = Path::new(&remote_path); let fh_result = sftp.create(sftp_path); let mut fh = match fh_result { Ok(x) => x, Err(y) => { return Err(response.is_failed(request, &format!("sftp open failed: {y}"))) } }; let bytes = data.as_bytes(); match fh.write_all(bytes) { Ok(_x) => {}, Err(y) => { return Err(response.is_failed(request, &format!("sftp write failed: {y}"))); } } return Ok(()); } fn copy_file(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, src: &Path, remote_path: &String) -> Result<(), Arc<TaskResponse>> { // this is a streaming copy that should be fine with large files. let src_open_result = File::open(src); let mut src = match src_open_result { Ok(x) => x, Err(y) => { return Err(response.is_failed(request, &format!("failed to open source file: {y}"))); } }; let session = self.session.as_ref().expect("session not established"); let sftp_result = session.sftp(); let sftp = match sftp_result { Ok(x) => x, Err(y) => { return Err(response.is_failed(request, &format!("sftp connection failed: {y}"))); } }; let sftp_path = Path::new(&remote_path); let fh_result = sftp.create(sftp_path); let mut fh = match fh_result { Ok(x) => x, Err(y) => { return Err(response.is_failed(request, &format!("sftp write failed (1): {y}"))) } }; let chunk_size = 64536; loop { let mut chunk = Vec::with_capacity(chunk_size); let mut taken = std::io::Read::by_ref(&mut src).take(chunk_size as u64); let take_result = taken.read_to_end(&mut chunk); let n = match take_result { Ok(x) => x, Err(y) => { return Err(response.is_failed(request, &format!("failed during file transfer: {y}"))); } }; if n == 0 { break; } match fh.write(&chunk) { Err(y) => { return Err(response.is_failed(request, &format!("sftp write failed: {y}"))); } _ => {}, } } return Ok(()); } } impl SshConnection { fn trim_newlines(&self, s: &mut String) { if s.ends_with('\n') { s.pop(); if s.ends_with('\r') { s.pop(); } } } fn run_command_low_level(&self, cmd: &String) -> Result<(i32,String),(i32,String)> { // FIXME: catch the rare possibility this unwrap fails and return a nice error? let session = self.session.as_ref().unwrap(); let mut channel = match session.channel_session() { Ok(x) => x, Err(y) => { return Err((500, format!("channel session failed: {:?}", y))); } }; let actual_cmd = format!("{} 2>&1", cmd); match channel.exec(&actual_cmd) { Ok(_x) => {}, Err(y) => { return Err((500,y.to_string())) } }; let mut s = String::new(); match channel.read_to_string(&mut s) { Ok(_x) => {}, Err(y) => { return Err((500,y.to_string())) } }; // BOOKMARK: add sudo password prompt (configurable) support here (and below) let _w = channel.wait_close(); let exit_status = match channel.exit_status() { Ok(x) => x, Err(y) => { return Err((500,y.to_string())) } }; self.trim_newlines(&mut s); return Ok((exit_status, s.clone())); } fn run_command_with_ssh_a(&self, cmd: &String) -> Result<(i32,String),(i32,String)> { // this is annoying but libssh2 agent support is not really working, so if we need to SSH -A we need to invoke // SSHd directly, which we need to for example with git clones. we will likely use this again // for fanout support. let mut base = Command::new("ssh"); let hostname = &self.host.read().unwrap().name; let port = format!("{}", self.port); let cmd2 = format!("{} 2>&1", cmd); let command = base.arg(hostname).arg("-p").arg(port).arg("-l").arg(self.username.clone()).arg("-A").arg(cmd2); match command.output() { Ok(x) => { match x.status.code() { Some(rc) => { let mut out = convert_out(&x.stdout,&x.stderr); self.trim_newlines(&mut out); return Ok((rc, out.clone())) }, None => { return Ok((418, String::from(""))) } } }, Err(_x) => { return Err((404, String::from(""))) } }; } } 0707010000001C000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001A00000000jetporch-0.0.1/src/handle0707010000001D000081A400000000000000000000000165135CC100001241000000000000000000000000000000000000002400000000jetporch-0.0.1/src/handle/handle.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::sync::{Arc,Mutex,RwLock}; use crate::connection::connection::Connection; use crate::tasks::request::TaskRequest; use crate::inventory::hosts::Host; use crate::playbooks::traversal::RunState; use crate::handle::local::Local; use crate::handle::remote::Remote; use crate::handle::template::Template; use crate::handle::response::Response; // task handles are given to modules to give them shortcuts to work with the jet system // actual functionality is mostly provided via TaskRequest/TaskResponse and such, the handles // are mostly module authors don't need to think about how things work as much. This is // especially true for the finite state machine that executes tasks. // whether commands should treat non-zero returns as errors #[derive(Eq,Hash,PartialEq,Clone,Copy,Debug)] pub enum CheckRc { Checked, Unchecked } pub struct TaskHandle { pub run_state: Arc<RunState>, _connection: Arc<Mutex<dyn Connection>>, pub host: Arc<RwLock<Host>>, pub local: Arc<Local>, pub remote: Arc<Remote>, pub response: Arc<Response>, pub template: Arc<Template>, } impl TaskHandle { pub fn new(run_state_handle: Arc<RunState>, connection_handle: Arc<Mutex<dyn Connection>>, host_handle: Arc<RwLock<Host>>) -> Self { // since we can't really have back-references (thanks Rust?) we pass to each namespace what we need of the others // thankfully, no circular references seem to be required :) // response contains namespaced shortcuts for returning results from module calls let response = Arc::new(Response::new( Arc::clone(&run_state_handle), Arc::clone(&host_handle) )); // template contains various functions around templating strings, and is most commonly seen in processing module // input parameters as well as directly used in modules like template. It's also used in a few places inside // the engine itself. let template = Arc::new(Template::new( Arc::clone(&run_state_handle), Arc::clone(&host_handle), Arc::clone(&response) )); // remote contains code for interacting with the host being configured. The host could actually be 'localhost', but it's usually // a machine different from the control machine. this could be called "configuration_target" instead but that would be more typing let remote = Arc::new(Remote::new( Arc::clone(&run_state_handle), Arc::clone(&connection_handle), Arc::clone(&host_handle), Arc::clone(&template), Arc::clone(&response) )); // local contains code that is related to looking at the control machine. Even in local configuration modes, functions here are // not used to configure the actual system, those would be from remote. this could be thought of as 'control-machine-side-module-support'. let local = Arc::new(Local::new( Arc::clone(&run_state_handle), Arc::clone(&host_handle), Arc::clone(&response) )); // the handle itself allows access to all of the above namespaces and also has a reference to the host being configured. // run_state itself is a bit of a pseudo-global and contains quite a few more parameters, see playbook/traversal.rs for // what it contains. return Self { run_state: Arc::clone(&run_state_handle), _connection: Arc::clone(&connection_handle), host: Arc::clone(&host_handle), remote: Arc::clone(&remote), local: Arc::clone(&local), response: Arc::clone(&response), template: Arc::clone(&template), }; } pub fn debug(&self, _request: &Arc<TaskRequest>, message: &String) { self.run_state.visitor.read().unwrap().debug_host(&self.host, message); } }0707010000001E000081A400000000000000000000000165135CC1000017C2000000000000000000000000000000000000002300000000jetporch-0.0.1/src/handle/local.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::sync::{Arc,RwLock}; use std::path::Path; use crate::connection::command::cmd_info; use crate::tasks::{TaskRequest,TaskRequestType,TaskResponse}; use crate::inventory::hosts::Host; use crate::playbooks::traversal::RunState; use crate::tasks::cmd_library::screen_general_input_loose; use crate::handle::handle::CheckRc; use crate::handle::response::Response; use crate::connection::command::Forward; // local contains code that always executes on the control machine, whether in SSH mode or 'local' execution // mode. The code that refers to the machine being configured is always in 'remote.rs', whether in SSH // mode or using a local connection also! pub struct Local { run_state: Arc<RunState>, _host: Arc<RwLock<Host>>, response: Arc<Response>, } impl Local { pub fn new(run_state_handle: Arc<RunState>, host_handle: Arc<RwLock<Host>>, response:Arc<Response>) -> Self { Self { run_state: run_state_handle, _host: host_handle, response: response } } pub fn get_localhost(&self) -> Arc<RwLock<Host>> { let inventory = self.run_state.inventory.read().unwrap(); return inventory.get_host(&String::from("localhost")); } fn unwrap_string_result(&self, request: &Arc<TaskRequest>, str_result: &Result<String,String>) -> Result<String, Arc<TaskResponse>> { return match str_result { Ok(x) => Ok(x.clone()), Err(y) => Err(self.response.is_failed(request, &y.clone())) }; } // runs a shell command. These can only be executed in the query stage as we don't want anything done to actually configure // a machine in local.rs. fn run(&self, request: &Arc<TaskRequest>, cmd: &String, check_rc: CheckRc) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { assert!(request.request_type == TaskRequestType::Query, "local commands can only be run in query stage (was: {:?})", request.request_type); // apply basic screening of the entire shell command, more filtering should already be done by cmd_library match screen_general_input_loose(&cmd) { Ok(_x) => {}, Err(y) => return Err(self.response.is_failed(request, &y.clone())) } let ctx = &self.run_state.context; let local_result = self.run_state.connection_factory.read().unwrap().get_local_connection(&ctx); let local_conn = match local_result { Ok(x) => x, Err(y) => { return Err(self.response.is_failed(request, &y.clone())) } }; let result = local_conn.lock().unwrap().run_command(&self.response, request, cmd, Forward::No); if check_rc == CheckRc::Checked { if result.is_ok() { let ok_result = result.as_ref().unwrap(); let cmd_result = ok_result.command_result.as_ref().as_ref().unwrap(); if cmd_result.rc != 0 { return Err(self.response.command_failed(request, &Arc::new(Some(cmd_result.clone())))); } } } return result; } pub fn read_file(&self, request: &Arc<TaskRequest>, path: &Path) -> Result<String, Arc<TaskResponse>> { return match crate::util::io::read_local_file(path) { Ok(s) => Ok(s), Err(x) => Err(self.response.is_failed(request, &x.clone())) }; } fn internal_sha512(&self, request: &Arc<TaskRequest>, path: &String) -> Result<String,Arc<TaskResponse>> { let localhost = self.get_localhost(); let os_type = localhost.read().unwrap().os_type.expect("unable to detect host OS type"); let get_cmd_result = crate::tasks::cmd_library::get_sha512_command(os_type, path); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; let result = self.run(request, &cmd, CheckRc::Unchecked)?; let (rc, out) = cmd_info(&result); match rc { // we can all unwrap because all possible string lists will have at least 1 element 0 => { let value = out.split_whitespace().nth(0).unwrap().to_string(); return Ok(value); }, 127 => { // file not found return Ok(String::from("")) }, _ => { return Err(self.response.is_failed(request, &format!("checksum failed: {}. {}", path, out))); } }; } pub fn get_sha512(&self, request: &Arc<TaskRequest>, path: &Path, use_cache: bool) -> Result<String,Arc<TaskResponse>> { let path2 = format!("{}", path.display()); let localhost = self.get_localhost(); if use_cache { let ctx = self.run_state.context.read().unwrap(); let task_id = ctx.get_task_count(); let mut localhost2 = localhost.write().unwrap(); let cached = localhost2.get_checksum_cache(task_id, &path2); if cached.is_some() { return Ok(cached.unwrap()); } } // this is a little weird. let value = self.internal_sha512(request, &path2)?; if use_cache { let mut localhost2 = localhost.write().unwrap(); localhost2.set_checksum_cache(&path2, &value); } return Ok(value); } }0707010000001F000081A400000000000000000000000165135CC10000033A000000000000000000000000000000000000002100000000jetporch-0.0.1/src/handle/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. pub mod handle; pub mod local; pub mod remote; pub mod response; pub mod template;07070100000020000081A400000000000000000000000165135CC10000572D000000000000000000000000000000000000002400000000jetporch-0.0.1/src/handle/remote.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::sync::{Arc,Mutex,RwLock}; use std::path::Path; use crate::connection::connection::Connection; use crate::connection::command::cmd_info; use crate::tasks::request::{TaskRequest, TaskRequestType}; use crate::tasks::response::TaskResponse; use crate::inventory::hosts::{Host,HostOSType}; use crate::playbooks::traversal::RunState; use crate::tasks::fields::Field; use crate::tasks::FileAttributesEvaluated; use crate::connection::command::Forward; use crate::tasks::cmd_library::screen_general_input_loose; use crate::handle::handle::CheckRc; use crate::handle::template::Safety; use crate::handle::response::Response; use crate::handle::template::Template; use crate::tasks::files::Recurse; use std::path::PathBuf; // contains all code that eventually reaches out and touches systems to be configured. // this includes the local system (somewhat confusingly) in 'local' mode, and of course // SSH-based remotes. 'Remote' should be thought of as 'for the system being configured' // as opposed to from the perspective of the control machine. pub struct Remote { run_state: Arc<RunState>, connection: Arc<Mutex<dyn Connection>>, host: Arc<RwLock<Host>>, template: Arc<Template>, response: Arc<Response> } #[derive(Debug,Copy,Clone,PartialEq)] pub enum UseSudo { Yes, No } impl Remote { pub fn new( run_state: Arc<RunState>, connection: Arc<Mutex<dyn Connection>>, host: Arc<RwLock<Host>>, template: Arc<Template>, response: Arc<Response>) -> Self { Self { run_state, connection, host, template, response, } } fn unwrap_string_result(&self, request: &Arc<TaskRequest>, str_result: &Result<String,String>) -> Result<String, Arc<TaskResponse>> { return match str_result { Ok(x) => Ok(x.clone()), Err(y) => { return Err(self.response.is_failed(request, &y.clone())); } }; } // who is the remote user? pub fn get_whoami(&self) -> Result<String,String> { return self.connection.lock().unwrap().whoami(); } // various files need to store things in tmp locations, mainly because SFTP does not support sudo or give the root // user the ability to replace unowned files pub fn make_temp_path(&self, who: &String, request: &Arc<TaskRequest>) -> Result<(PathBuf, PathBuf), Arc<TaskResponse>> { let mut pb = PathBuf::new(); let tmpdir = match who.eq("root") { false => match self.host.read().unwrap().os_type { Some(HostOSType::MacOS) => format!("/Users/{}/.jet/tmp", who), _ => format!("/home/{}/.jet/tmp", who), } true => String::from("/root/.jet/tmp") }; pb.push(tmpdir); let mut pb2 = pb.clone(); let guid = self.run_state.context.read().unwrap().get_guid(); pb2.push(guid.as_str()); let create_tmp_dir = format!("mkdir -p '{}'", pb.display()); self.run_no_sudo(request, &create_tmp_dir, CheckRc::Checked)?; return Ok((pb.clone(), pb2.clone())); } // wrappers around running CLI commands pub fn run(&self, request: &Arc<TaskRequest>, cmd: &String, check_rc: CheckRc) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { return self.internal_run(request, cmd, Safety::Safe, check_rc, UseSudo::Yes, Forward::No); } pub fn run_forwardable(&self, request: &Arc<TaskRequest>, cmd: &String, check_rc: CheckRc) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { return self.internal_run(request, cmd, Safety::Safe, check_rc, UseSudo::Yes, Forward::Yes); } pub fn run_no_sudo(&self, request: &Arc<TaskRequest>, cmd: &String, check_rc: CheckRc) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { return self.internal_run(request, cmd, Safety::Safe, check_rc, UseSudo::No, Forward::No); } // the unsafe version of this doesn't check the shell string for possible shell variable injections, the most obvious and basic being ";" // usage of unsafe requires a special keyword in the 'shell' module for instance, or that no variables are present in the cmd parameter. pub fn run_unsafe(&self, request: &Arc<TaskRequest>, cmd: &String, check_rc: CheckRc) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { return self.internal_run(request, cmd, Safety::Unsafe, check_rc, UseSudo::Yes, Forward::No); } fn internal_run(&self, request: &Arc<TaskRequest>, cmd: &String, safe: Safety, check_rc: CheckRc, use_sudo: UseSudo, forward: Forward) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { assert!(request.request_type != TaskRequestType::Validate, "commands cannot be run in validate stage"); // apply basic screening of the entire shell command, more filtering should already be done by cmd_library // for parameterized calls that use that if safe == Safety::Safe { // check for invalid shell parameters match screen_general_input_loose(&cmd) { Ok(_x) => {}, Err(y) => return Err(self.response.is_failed(request, &y.clone())) } } // use the sudo template to choose a new command to execute if specified. // this doesn't need to be sudo specifically, it's really a generic concept that can wrap a command with another tool let cmd_out = match use_sudo { UseSudo::Yes => match self.template.add_sudo_details(request, &cmd) { Ok(x) => x, Err(y) => { return Err(self.response.is_failed(request, &format!("failure constructing sudo command: {}", y))); } }, UseSudo::No => cmd.clone() }; self.response.get_visitor().read().expect("read visitor").on_command_run(&self.response.get_context(), &Arc::clone(&self.host), &cmd); let result = self.connection.lock().unwrap().run_command(&self.response, request, &cmd_out, forward); // if requested, turn non-zero return codes into errors if check_rc == CheckRc::Checked && result.is_ok() { let ok_result = result.as_ref().unwrap(); let cmd_result = ok_result.command_result.as_ref().as_ref().unwrap(); if cmd_result.rc != 0 { return Err(self.response.command_failed(request, &Arc::new(Some(cmd_result.clone())))); } } return result; } // the OS type of a host is set on connection by automatically running a discovery command pub fn get_os_type(&self) -> HostOSType { let os_type = self.host.read().unwrap().os_type; if os_type.is_none() { panic!("failed to detect OS type for {}, bailing out", self.host.read().unwrap().name); } return os_type.unwrap(); } // when we need to write a file we need to place it in a particular temp location and then move it fn get_transfer_location(&self, request: &Arc<TaskRequest>, _path: &String) -> Result<(Option<PathBuf>, Option<PathBuf>), Arc<TaskResponse>> { let whoami = match self.get_whoami() { Ok(x) => x, Err(y) => { return Err(self.response.is_failed(request, &format!("cannot determine current user: {}", y))) } }; let (p1,f1) = self.make_temp_path(&whoami, request)?; return Ok((Some(p1.clone()), Some(f1.clone()))) } // supporting code for file transfer using temp files fn get_effective_filename(&self, temp_dir: Option<PathBuf>, temp_path: Option<PathBuf>, path: &String) -> String { let result = match temp_dir.is_some() { true => { let t = temp_path.as_ref().unwrap(); t.clone().into_os_string().into_string().unwrap() }, false => path.clone() }; return result; } // more supporting code for file transfer using temp files fn conditionally_move_back(&self, request: &Arc<TaskRequest>, temp_dir: Option<PathBuf>, temp_path: Option<PathBuf>, desired_path: &String) -> Result<(), Arc<TaskResponse>> { if temp_dir.is_some() { let move_to_correct_location = format!("mv '{}' '{}'", temp_path.as_ref().unwrap().display(), desired_path); let delete_tmp_location = format!("rm '{}'", temp_path.as_ref().unwrap().display()); let result = self.run(request, &move_to_correct_location, CheckRc::Checked); if result.is_err() { let _ = self.run(request, &delete_tmp_location, CheckRc::Unchecked); return Err(result.unwrap_err()); } } Ok(()) } // writes a string (for example, from a template) to a remote file location pub fn write_data<G>(&self, request: &Arc<TaskRequest>, data: &String, path: &String, mut before_complete: G) -> Result<(), Arc<TaskResponse>> where G: FnMut(&String) -> Result<(), Arc<TaskResponse>> { let (temp_dir, temp_path) = self.get_transfer_location(request, path)?; let real_path = self.get_effective_filename(temp_dir.clone(), temp_path.clone(), path); /* will be either temp_path or path */ self.response.get_visitor().read().expect("read visitor").on_before_transfer(&self.response.get_context(), &Arc::clone(&self.host), &real_path); let xfer_result = self.connection.lock().unwrap().write_data(&self.response, request, data, &real_path)?; before_complete(&real_path.clone())?; self.conditionally_move_back(request, temp_dir.clone(), temp_path.clone(), path)?; return Ok(xfer_result); } // copies a file to a remote location pub fn copy_file<G>(&self, request: &Arc<TaskRequest>, src: &Path, dest: &String, mut before_complete: G) -> Result<(), Arc<TaskResponse>> where G: FnMut(&String) -> Result<(), Arc<TaskResponse>> { let (temp_dir, temp_path) = self.get_transfer_location(request, dest)?; let real_path = self.get_effective_filename(temp_dir.clone(), temp_path.clone(), dest); /* will be either temp_path or path */ self.response.get_visitor().read().expect("read visitor").on_before_transfer(&self.response.get_context(), &Arc::clone(&self.host), &real_path); let xfer_result = self.connection.lock().unwrap().copy_file(&self.response, &request, src, &real_path)?; before_complete(&real_path.clone())?; self.conditionally_move_back(request, temp_dir.clone(), temp_path.clone(), dest)?; return Ok(xfer_result); } // gets the octal string mode of a remote file pub fn get_mode(&self, request: &Arc<TaskRequest>, path: &String) -> Result<Option<String>,Arc<TaskResponse>> { let get_cmd_result = crate::tasks::cmd_library::get_mode_command(self.get_os_type(), path); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; let result = self.run(request, &cmd, CheckRc::Unchecked)?; let (rc, out) = cmd_info(&result); return match rc { // we can all unwrap because all possible string lists will have at least 1 element 0 => Ok(Some(out.split_whitespace().nth(0).unwrap().to_string())), _ => Ok(None), } } // is a remote path a file? pub fn get_is_file(&self, request: &Arc<TaskRequest>, path: &String) -> Result<bool,Arc<TaskResponse>> { return match self.get_is_directory(request, path) { Ok(true) => Ok(false), Ok(false) => Ok(true), Err(x) => Err(x) }; } // is a remote path a directory? pub fn get_is_directory(&self, request: &Arc<TaskRequest>, path: &String) -> Result<bool,Arc<TaskResponse>> { let get_cmd_result = crate::tasks::cmd_library::get_is_directory_command(self.get_os_type(), path); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; let result = self.run(request, &cmd, CheckRc::Checked)?; let (_rc, out) = cmd_info(&result); // so far this assumes reliable ls -ld output across all supported operating systems, this may change // in wich case we may need to consider os_type here if out.starts_with("d") { return Ok(true); } return Ok(false); } pub fn touch_file(&self, request: &Arc<TaskRequest>, path: &String) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let get_cmd_result = crate::tasks::cmd_library::get_touch_command(self.get_os_type(), path); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; return self.run(request, &cmd, CheckRc::Checked); } pub fn create_directory(&self, request: &Arc<TaskRequest>, path: &String) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let get_cmd_result = crate::tasks::cmd_library::get_create_directory_command(self.get_os_type(), path); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; return self.run(request, &cmd, CheckRc::Checked); } pub fn delete_file(&self, request: &Arc<TaskRequest>, path: &String) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let get_cmd_result = crate::tasks::cmd_library::get_delete_file_command(self.get_os_type(), path); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; return self.run(request, &cmd, CheckRc::Checked); } pub fn delete_directory(&self, request: &Arc<TaskRequest>, path: &String, recurse: Recurse) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let get_cmd_result = crate::tasks::cmd_library::get_delete_directory_command(self.get_os_type(), path, recurse); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; if path.eq("/") { return Err(self.response.is_failed(request, &String::from("accidental removal of / blocked by safeguard"))); } return self.run(request, &cmd, CheckRc::Checked); } // return the (owner,group) tuple for a remote file. If the command fails this will instead return None // so consider running get_mode first. See the various file modules for examples. pub fn get_ownership(&self, request: &Arc<TaskRequest>, path: &String) -> Result<Option<(String,String)>,Arc<TaskResponse>> { let get_cmd_result = crate::tasks::cmd_library::get_ownership_command(self.get_os_type(), path); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; let result = self.run(request, &cmd, CheckRc::Unchecked)?; let (rc, out) = cmd_info(&result); match rc { 0 => {}, _ => { return Ok(None); }, } let mut split = out.split_whitespace(); let owner = match split.nth(2) { Some(x) => x, None => { return Err(self.response.is_failed(request, &format!("unexpected output format from {}: {}", cmd, out))); } }; // this is a progressive iterator, hence 0 and not 3 for nth() below! let group = match split.nth(0) { Some(x) => x, None => { return Err(self.response.is_failed(request, &format!("unexpected output format from {}: {}", cmd, out))); } }; return Ok(Some((owner.to_string(),group.to_string()))); } pub fn set_owner(&self, request: &Arc<TaskRequest>, remote_path: &String, owner: &String, recurse: Recurse) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let get_cmd_result = crate::tasks::cmd_library::set_owner_command(self.get_os_type(), remote_path, owner, recurse); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; return self.run(request,&cmd,CheckRc::Checked); } pub fn set_group(&self, request: &Arc<TaskRequest>, remote_path: &String, group: &String, recurse: Recurse) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let get_cmd_result = crate::tasks::cmd_library::set_group_command(self.get_os_type(), remote_path, group, recurse); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; return self.run(request,&cmd,CheckRc::Checked); } pub fn set_mode(&self, request: &Arc<TaskRequest>, remote_path: &String, mode: &String, recurse: Recurse) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let get_cmd_result = crate::tasks::cmd_library::set_mode_command(self.get_os_type(), remote_path, mode, recurse); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; return self.run(request,&cmd,CheckRc::Checked); } pub fn get_sha512(&self, request: &Arc<TaskRequest>, path: &String) -> Result<String,Arc<TaskResponse>> { return self.internal_sha512(request, path); } // right now we assume there's a good way to run SHA-512 preinstalled on all platforms. fn internal_sha512(&self, request: &Arc<TaskRequest>, path: &String) -> Result<String,Arc<TaskResponse>> { let os_type = self.get_os_type(); let get_cmd_result = crate::tasks::cmd_library::get_sha512_command(os_type, path); let cmd = self.unwrap_string_result(&request, &get_cmd_result)?; let result = self.run(request, &cmd, CheckRc::Unchecked)?; let (rc, out) = cmd_info(&result); match rc { // we can all unwrap because all possible string lists will have at least 1 element 0 => { let value = out.split_whitespace().nth(0).unwrap().to_string(); return Ok(value); }, 127 => { // file not found return Ok(String::from("")) }, _ => { return Err(self.response.is_failed(request, &format!("checksum failed: {}. {}", path, out))); } }; } // supporting code for any tasks that has an 'attributes' member, see 'template' for one example of usage // TODO: add SELinux pub fn query_common_file_attributes(&self, request: &Arc<TaskRequest>, remote_path: &String, attributes_in: &Option<FileAttributesEvaluated>, changes: &mut Vec<Field>, recurse: Recurse) -> Result<Option<String>,Arc<TaskResponse>> { let remote_mode = self.get_mode(request, remote_path)?; if remote_mode.is_none() { changes.push(Field::Content); return Ok(None); } if attributes_in.is_some() && recurse == Recurse::Yes { changes.push(Field::Owner); changes.push(Field::Group); changes.push(Field::Mode); return Ok(remote_mode); } if attributes_in.is_some() { let attributes = attributes_in.as_ref().unwrap(); let owner_result = self.get_ownership(request, remote_path)?; if owner_result.is_none() { return Err(self.response.is_failed(request, &String::from("file was deleted unexpectedly mid-operation"))); } let (remote_owner, remote_group) = owner_result.unwrap(); if attributes.owner.is_some() && ! remote_owner.eq(attributes.owner.as_ref().unwrap()) { changes.push(Field::Owner); } if attributes.group.is_some() && ! remote_group.eq(attributes.group.as_ref().unwrap()) { changes.push(Field::Group); } if attributes.mode.is_some() && ! remote_mode.as_ref().unwrap().eq(attributes.mode.as_ref().unwrap()) { changes.push(Field::Mode); } } return Ok(remote_mode); } // supporting code for workign with files that have configurable attributes. See above + also // modules like template. // TODO: add SELinux pub fn process_common_file_attributes(&self, request: &Arc<TaskRequest>, remote_path: &String, attributes_in: &Option<FileAttributesEvaluated>, changes: &Vec<Field>, recurse: Recurse) -> Result<(),Arc<TaskResponse>> { if attributes_in.is_none() { return Ok(()); } let attributes = attributes_in.as_ref().unwrap(); for change in changes.iter() { match change { Field::Owner => { assert!(attributes.owner.is_some(), "owner is set"); self.set_owner(request, remote_path, &attributes.owner.as_ref().unwrap(), recurse)?; }, Field::Group => { assert!(attributes.group.is_some(), "owner is set"); self.set_group(request, remote_path, &attributes.group.as_ref().unwrap(), recurse)?; }, Field::Mode => { assert!(attributes.mode.is_some(), "owner is set"); self.set_mode(request, remote_path, &attributes.mode.as_ref().unwrap(), recurse)?; }, _ => {} } } return Ok(()); } // see above comments about file attributes features. pub fn process_all_common_file_attributes(&self, request: &Arc<TaskRequest>, remote_path: &String, attributes_in: &Option<FileAttributesEvaluated>, recurse: Recurse) -> Result<(),Arc<TaskResponse>> { let all = Field::all_file_attributes(); return self.process_common_file_attributes(request, remote_path, attributes_in, &all, recurse); } }07070100000021000081A400000000000000000000000165135CC100002AFF000000000000000000000000000000000000002600000000jetporch-0.0.1/src/handle/response.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::sync::Arc; use crate::tasks::request::{TaskRequest, TaskRequestType}; use crate::tasks::response::{TaskStatus, TaskResponse}; use crate::inventory::hosts::Host; use crate::playbooks::traversal::RunState; use crate::tasks::fields::Field; use crate::connection::command::CommandResult; use crate::playbooks::context::PlaybookContext; use crate::playbooks::visitor::PlaybookVisitor; use std::sync::RwLock; // response mostly contains shortcuts for returning objects that are appropriate for module returns // and also errors, in various instances. Using response ensures the errors are (mostly) constructed // correctly and the code is easier to change than if every module constructed returns // manually. So use the response functions! pub struct Response { run_state: Arc<RunState>, host: Arc<RwLock<Host>>, } impl Response { pub fn new(run_state_handle: Arc<RunState>, host_handle: Arc<RwLock<Host>>) -> Self { Self { run_state: run_state_handle, host: host_handle, } } pub fn get_context(&self) -> Arc<RwLock<PlaybookContext>> { return Arc::clone(&self.run_state.context); } pub fn get_visitor(&self) -> Arc<RwLock<dyn PlaybookVisitor>> { return Arc::clone(&self.run_state.visitor); } pub fn is_failed(&self, _request: &Arc<TaskRequest>, msg: &String) -> Arc<TaskResponse> { return Arc::new(TaskResponse { status: TaskStatus::Failed, changes: Vec::new(), msg: Some(msg.clone()), command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None) }); } pub fn not_supported(&self, request: &Arc<TaskRequest>) -> Arc<TaskResponse> { // modules should return this on any request legs they don't support... though they should also never // be called against those legs if the Query leg is written correctly! return self.is_failed(request, &String::from("not supported")); } pub fn command_failed(&self, _request: &Arc<TaskRequest>, result: &Arc<Option<CommandResult>>) -> Arc<TaskResponse> { // used internally by run functions in remote.rs when commands fail, suitable for use as a final module response self.get_visitor().read().expect("read visitor").on_command_failed(&self.get_context(), &Arc::clone(&self.host), &Arc::clone(result)); return Arc::new(TaskResponse { status: TaskStatus::Failed, changes: Vec::new(), msg: Some(String::from("command failed")), command_result: Arc::clone(&result), with: Arc::new(None), and: Arc::new(None) }); } pub fn command_ok(&self, _request: &Arc<TaskRequest>, result: &Arc<Option<CommandResult>>) -> Arc<TaskResponse> { // used internally by run functions in remote.rs when commands succeed, suitable for use as a final module response self.get_visitor().read().expect("read visitor").on_command_ok(&self.get_context(), &Arc::clone(&self.host), &Arc::clone(result)); return Arc::new(TaskResponse { status: TaskStatus::IsExecuted, changes: Vec::new(), msg: None, command_result: Arc::clone(&result), with: Arc::new(None), and: Arc::new(None) }); } pub fn is_skipped(&self, request: &Arc<TaskRequest>) -> Arc<TaskResponse> { // returned by playbook traversal code when skipping over a task due to a condition not being met or other factors assert!(request.request_type == TaskRequestType::Validate, "is_skipped response can only be returned for a validation request"); return Arc::new(TaskResponse { status: TaskStatus::IsSkipped, changes: Vec::new(), msg: None, command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None) }); } pub fn is_matched(&self, request: &Arc<TaskRequest>, ) -> Arc<TaskResponse> { // returned by a query function when the resource is matched exactly and no operations are neccessary to // run to configure the remote assert!(request.request_type == TaskRequestType::Query || request.request_type == TaskRequestType::Validate, "is_matched response can only be returned for a query request, was {:?}", request.request_type); return Arc::new(TaskResponse { status: TaskStatus::IsMatched, changes: Vec::new(), msg: None, command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None) }); } pub fn is_created(&self, request: &Arc<TaskRequest>) -> Arc<TaskResponse> { // the only successful result to return from a Create leg. assert!(request.request_type == TaskRequestType::Create, "is_executed response can only be returned for a creation request"); return Arc::new(TaskResponse { status: TaskStatus::IsCreated, changes: Vec::new(), msg: None, command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None) }); } // see also command_ok for shortcuts, as used in the shell module. pub fn is_executed(&self, request: &Arc<TaskRequest>) -> Arc<TaskResponse> { // a valid response from an execute leg that is not command based or runs multiple commands assert!(request.request_type == TaskRequestType::Execute, "is_executed response can only be returned for a creation request"); return Arc::new(TaskResponse { status: TaskStatus::IsExecuted, changes: Vec::new(), msg: None, command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None) }); } pub fn is_removed(&self, request: &Arc<TaskRequest>) -> Arc<TaskResponse> { // the only appropriate response from a removal request assert!(request.request_type == TaskRequestType::Remove, "is_removed response can only be returned for a remove request"); return Arc::new(TaskResponse { status: TaskStatus::IsRemoved, changes: Vec::new(), msg: None, command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None) }); } pub fn is_passive(&self, request: &Arc<TaskRequest>) -> Arc<TaskResponse> { // the only appropriate response from a passive module, for example, echo assert!(request.request_type == TaskRequestType::Passive || request.request_type == TaskRequestType::Execute, "is_passive response can only be returned for a passive or execute request"); return Arc::new(TaskResponse { status: TaskStatus::IsPassive, changes: Vec::new(), msg: None, command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None) }); } pub fn is_modified(&self, request: &Arc<TaskRequest>, changes: Vec<Field>) -> Arc<TaskResponse> { // the only appropriate response from a modification leg, note that changes must be passed in and should come from fields.rs assert!(request.request_type == TaskRequestType::Modify, "is_modified response can only be returned for a modification request"); return Arc::new(TaskResponse { status: TaskStatus::IsModified, changes: changes, msg: None, command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None) }); } pub fn needs_creation(&self, request: &Arc<TaskRequest>) -> Arc<TaskResponse> { // a response from a query function that requests invocation of the create leg. assert!(request.request_type == TaskRequestType::Query, "needs_creation response can only be returned for a query request"); return Arc::new(TaskResponse { status: TaskStatus::NeedsCreation, changes: Vec::new(), msg: None, command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None), }); } pub fn needs_modification(&self, request: &Arc<TaskRequest>, changes: &Vec<Field>) -> Arc<TaskResponse> { // a response from a query function that requests invocation of the modify leg. assert!(request.request_type == TaskRequestType::Query, "needs_modification response can only be returned for a query request"); assert!(!changes.is_empty(), "changes must not be empty"); return Arc::new(TaskResponse { status: TaskStatus::NeedsModification, changes: changes.clone(), msg: None, command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None) }); } pub fn needs_removal(&self, request: &Arc<TaskRequest>) -> Arc<TaskResponse> { // a response from a query function that requests invocation of the removal leg. assert!(request.request_type == TaskRequestType::Query, "needs_removal response can only be returned for a query request"); return Arc::new(TaskResponse { status: TaskStatus::NeedsRemoval, changes: Vec::new(), msg: None, command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None) }); } pub fn needs_execution(&self, request: &Arc<TaskRequest>) -> Arc<TaskResponse> { // a response from a query function that requests invocation of the execute leg. // modules that use 'execute' should generally not have legs for creation, removal, or modification assert!(request.request_type == TaskRequestType::Query, "needs_execution response can only be returned for a query request"); return Arc::new(TaskResponse { status: TaskStatus::NeedsExecution, changes: Vec::new(), msg: None, command_result: Arc::new(None), with: Arc::new(None),and: Arc::new(None) }); } pub fn needs_passive(&self, request: &Arc<TaskRequest>) -> Arc<TaskResponse> { // this is the response that passive modules use to exit the query leg assert!(request.request_type == TaskRequestType::Query, "needs_passive response can only be returned for a query request"); return Arc::new(TaskResponse { status: TaskStatus::NeedsPassive, changes: Vec::new(), msg: None, command_result: Arc::new(None), with: Arc::new(None), and: Arc::new(None) }); } }07070100000022000081A400000000000000000000000165135CC100004D76000000000000000000000000000000000000002600000000jetporch-0.0.1/src/handle/template.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::sync::{Arc,RwLock}; use std::path::PathBuf; use crate::tasks::request::TaskRequest; use crate::tasks::response::TaskResponse; use crate::inventory::hosts::Host; use crate::playbooks::traversal::RunState; use crate::playbooks::context::PlaybookContext; use crate::tasks::cmd_library::{screen_path,screen_general_input_strict}; use crate::handle::response::Response; use crate::playbooks::templar::{Templar,TemplateMode}; // template contains support code for all variable evaluation in the playbook language, as well as // support for the template module, and ALSO the code to validate and process module arguments to make // sure they are the right type. // // because module arguments come in as strings, we evaluate templates here and then see if they can // be parsed as their desired types. // when blend target must be specified, it is either the template module or *not*. // the only real difference (at the moment) is that the template module is allowed access // to environment variables which are prefixed as ENV_foo. The environment mechanism is how // we work with secret manager tools. See the website secrets documentation for details #[derive(Eq,Hash,PartialEq,Clone,Copy,Debug)] pub enum BlendTarget { NotTemplateModule, TemplateModule, } // where used, safe means screening commands or arguments for unexpected shell characters // that could lead to command escapes. Because a command is marked unsafe does not mean // it is actually unsafe, it just means that it is not checked. A command using // variables from untrusted sources may actually be unsafe, for instance, the shell // module when used with 'unsafe: true'. Though if no variables are used, it would // be quite safe. #[derive(Eq,Hash,PartialEq,Clone,Copy,Debug)] pub enum Safety { Safe, Unsafe } pub struct Template { run_state: Arc<RunState>, host: Arc<RwLock<Host>>, response: Arc<Response>, detached_templar: Templar } impl Template { // templating is always done in reference to a specific host, so that we can mix in host specific variables // the response is in the constructor as need it to return errors that are passed upwards from // functions below. pub fn new(run_state: Arc<RunState>, host: Arc<RwLock<Host>>, response:Arc<Response>) -> Self { Self { run_state, host, response, detached_templar: Templar::new() } } pub fn get_context(&self) -> Arc<RwLock<PlaybookContext>> { return Arc::clone(&self.run_state.context); } fn unwrap_string_result(&self, request: &Arc<TaskRequest>, str_result: &Result<String,String>) -> Result<String, Arc<TaskResponse>> { return match str_result { Ok(x) => Ok(x.clone()), Err(y) => { return Err(self.response.is_failed(request, &y.clone())); } }; } fn template_unsafe_internal(&self, request: &Arc<TaskRequest>, tm: TemplateMode, _field: &String, template: &String, blend_target: BlendTarget) -> Result<String,Arc<TaskResponse>> { let result = self.run_state.context.read().unwrap().render_template(template, &self.host, blend_target, tm); if result.is_ok() { let result_ok = result.as_ref().unwrap(); if result_ok.eq("") { return Err(self.response.is_failed(request, &format!("evaluated to empty string"))); } } let result2 = self.unwrap_string_result(request, &result)?; return Ok(result2); } pub fn string_for_template_module_use_only(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &String) -> Result<String,Arc<TaskResponse>> { // this is the version of templating that gives access to secret variables, we don't allow them elsewhere as they would be easy to leak to CI/CD/build output/logs // and the contents to templates are not shown to anything return self.template_unsafe_internal(request, tm, field, template, BlendTarget::TemplateModule); } pub fn string_unsafe_for_shell(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &String) -> Result<String,Arc<TaskResponse>> { // indicates templating a string that will not without further processing, be passed to a shell command return self.template_unsafe_internal(request, tm, field, template, BlendTarget::NotTemplateModule); } // FIXME: this code is possibly a bit redundant - perhaps calling methods can use the public function and this can be eliminated fn string_option_unsafe(&self, request: &Arc<TaskRequest>, tm: TemplateMode,field: &String, template: &Option<String>) -> Result<Option<String>,Arc<TaskResponse>> { // templates a string that is not allowed to be used in shell commands and may contain special characters if template.is_none() { return Ok(None); } let result = self.string(request, tm, field, &template.as_ref().unwrap()); return match result { Ok(x) => Ok(Some(x)), Err(y) => { Err(self.response.is_failed(request, &format!("field ({}) template error: {:?}", field, y))) } }; } pub fn string_option_unsafe_for_shell(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &Option<String>) -> Result<Option<String>,Arc<TaskResponse>> { // indicates templating a string that will not without further processing, be passed to a shell command return match template.is_none() { true => Ok(None), false => Ok(Some(self.template_unsafe_internal(request, tm, field, &template.as_ref().unwrap(), BlendTarget::NotTemplateModule)?)) } } pub fn string(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &String) -> Result<String,Arc<TaskResponse>> { // templates a required string parameter - the simplest of argument processing, this requires no casting to other types let result = self.string_unsafe_for_shell(request, tm, field, template); return match result { Ok(x) => match screen_general_input_strict(&x) { Ok(y) => Ok(y), Err(z) => { return Err(self.response.is_failed(request, &format!("field {}, {}", field, z))) } }, Err(y) => Err(y) }; } pub fn string_no_spaces(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &String) -> Result<String,Arc<TaskResponse>> { // same as self.string above, this version also does not allow spaces in the resulting string let value = self.string(request, tm, field, template)?; if self.has_spaces(&value) { return Err(self.response.is_failed(request, &format!("field ({}): spaces are not allowed", field))) } return Ok(value.clone()); } pub fn string_option_no_spaces(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &Option<String>) -> Result<Option<String>,Arc<TaskResponse>> { // this is a version of string_no_spaces that allows the value to be optional let prelim = self.string_option(request, tm, field, template)?; if prelim.is_some() { let value = prelim.as_ref().unwrap(); if self.has_spaces(&value) { return Err(self.response.is_failed(request, &format!("field ({}): spaces are not allowed", field))) } } return Ok(prelim.clone()); } pub fn string_option_default(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &Option<String>, default: &String) -> Result<String,Arc<TaskResponse>> { // this is a version of string_no_spaces that allows the value to be optional let prelim = self.string_option(request, tm, field, template)?; match prelim { Some(x) => Ok(x.clone()), None => Ok(default.clone()) } } pub fn string_option_trim(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &Option<String>) -> Result<Option<String>,Arc<TaskResponse>> { // for processing parameters that take optional strings, but make sure to remove any extra surrounding whitespace // YAML should do this anyway so it's mostly overkill but may prevent some rare errors from inventory variable sources let prelim = self.string_option(request, tm, field, template)?; if prelim.is_some() { return Ok(Some(prelim.unwrap().trim().to_string())); } return Ok(None); } pub fn no_template_string_option_trim(&self, input: &Option<String>) -> Option<String> { // takes a string option and uses it verbatim, for parameters that do not allow variables in them if input.is_some() { let value = input.as_ref().unwrap(); return Some(value.trim().to_string()); } return None; } pub fn path(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &String) -> Result<String,Arc<TaskResponse>> { // templates a string and makes sure the output looks like a valid path let result = self.run_state.context.read().unwrap().render_template(template, &self.host, BlendTarget::NotTemplateModule, tm); let result2 = self.unwrap_string_result(request, &result)?; return match screen_path(&result2) { Ok(x) => Ok(x), Err(y) => { return Err(self.response.is_failed(request, &format!("{}, for field {}", y, field))) } } } pub fn string_option(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &Option<String>) -> Result<Option<String>,Arc<TaskResponse>> { // templates an optional string let result = self.string_option_unsafe(request, tm, field, template); return match result { Ok(x1) => match x1 { Some(x) => match screen_general_input_strict(&x) { Ok(y) => Ok(Some(y)), Err(z) => { return Err(self.response.is_failed(request, &format!("field {}, {}", field, z))) } }, None => Ok(None) }, Err(y) => Err(y) }; } #[allow(dead_code)] pub fn integer(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &String)-> Result<u64,Arc<TaskResponse>> { // templates a required value that must resolve to an integer if tm == TemplateMode::Off { return Ok(0); } let st = self.string(request, tm, field, template)?; let num = st.parse::<u64>(); return match num { Ok(num) => Ok(num), Err(_err) => Err(self.response.is_failed(request, &format!("field ({}) value is not an integer: {}", field, st))) } } pub fn integer_option(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &Option<String>, default: u64) -> Result<u64,Arc<TaskResponse>> { // templates an optional value that must resolve to an integer if tm == TemplateMode::Off { return Ok(0); } if template.is_none() { return Ok(default); } let st = self.string(request, tm, field, &template.as_ref().unwrap())?; let num = st.parse::<u64>(); // FIXME: these can use map_err return match num { Ok(num) => Ok(num), Err(_err) => Err(self.response.is_failed(request, &format!("field ({}) value is not an integer: {}", field, st))) } } #[allow(dead_code)] pub fn boolean(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &String) -> Result<bool,Arc<TaskResponse>> { // templates a required value that must resolve to a boolean // where possible, consider using boolean_option_default_true/false instead // jet mostly favors booleans defaulting to false, but it doesn't always make sense if tm == TemplateMode::Off { return Ok(true); } let st = self.string(request, tm, field, template)?; let x = st.parse::<bool>(); return match x { Ok(x) => Ok(x), Err(_err) => Err(self.response.is_failed(request, &format!("field ({}) value is not a boolean: {}", field, st))) } } #[allow(dead_code)] pub fn boolean_option_default_true(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &Option<String>)-> Result<bool,Arc<TaskResponse>>{ // templates an optional value that resolves to a boolean, if omitted, assume the answer is true return self.internal_boolean_option(request, tm, field, template, true); } pub fn boolean_option_default_false(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &Option<String>)-> Result<bool,Arc<TaskResponse>>{ // templates an optional value that resolves to a boolean, if omitted, assume the answer is false return self.internal_boolean_option(request, tm, field, template, false); } fn internal_boolean_option(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &Option<String>, default: bool)-> Result<bool,Arc<TaskResponse>>{ // supporting code for boolean parsing above if tm == TemplateMode::Off { return Ok(false); } if template.is_none() { return Ok(default); } let st = self.string(request, tm, field, &template.as_ref().unwrap())?; let x = st.parse::<bool>(); return match x { Ok(x) => Ok(x), Err(_err) => Err(self.response.is_failed(request, &format!("field ({}) value is not a boolean: {}", field, st))) } } pub fn boolean_option_default_none(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, template: &Option<String>)-> Result<Option<bool>,Arc<TaskResponse>>{ // supports an optional boolean value that does not default to true or false - effectively making the option a trinary value where None is "no preference" if tm == TemplateMode::Off { return Ok(None); } if template.is_none() { return Ok(None); } let st = self.string(request, tm, field, &template.as_ref().unwrap())?; let x = st.parse::<bool>(); return match x { Ok(x) => Ok(Some(x)), Err(_err) => Err(self.response.is_failed(request, &format!("field ({}) value is not a boolean: {}", field, st))) } } pub fn test_condition(&self, request: &Arc<TaskRequest>, tm: TemplateMode, expr: &String) -> Result<bool, Arc<TaskResponse>> { // used to evaluate in-language conditionals throughout the program. if tm == TemplateMode::Off { return Ok(false); } let result = self.get_context().read().unwrap().test_condition(expr, &self.host, tm); return match result { Ok(x) => Ok(x), Err(y) => Err(self.response.is_failed(request, &y)) } } pub fn test_condition_with_extra_data(&self, request: &Arc<TaskRequest>, tm: TemplateMode, expr: &String, _host: &Arc<RwLock<Host>>, vars_input: serde_yaml::Mapping) -> Result<bool,Arc<TaskResponse>> { // same as test_condition but mixes in some temporary data that is not stored elsewhere for future template evaluation if tm == TemplateMode::Off { return Ok(false); } let result = self.get_context().read().unwrap().test_condition_with_extra_data(expr, &self.host, vars_input, tm); return match result { Ok(x) => Ok(x), Err(y) => Err(self.response.is_failed(request, &y)) } } pub fn find_template_path(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, str_path: &String) -> Result<PathBuf, Arc<TaskResponse>> { // templates a string and then looks for the resulting file in the logical templates/ locations (if not an absolute path) // raises errors if the source files are not found return self.find_sub_path(&String::from("templates"), request, tm, field, str_path); } pub fn find_file_path(&self, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, str_path: &String) -> Result<PathBuf, Arc<TaskResponse>> { // simialr to find_template_path, this one assumes a 'files/' directory for relative paths. return self.find_sub_path(&String::from("files"), request, tm, field, str_path); } fn find_sub_path(&self, prefix: &String, request: &Arc<TaskRequest>, tm: TemplateMode, field: &String, str_path: &String) -> Result<PathBuf, Arc<TaskResponse>> { // supporting code for find_template_path and find_file_path if tm == TemplateMode::Off { return Ok(PathBuf::new()); } let prelim = match screen_path(str_path) { Ok(x) => x, Err(y) => { return Err(self.response.is_failed(request, &format!("{}, for field: {}", y, field))) } }; let mut path = PathBuf::new(); path.push(prelim); if path.is_absolute() { if path.is_file() { return Ok(path); } else { return Err(self.response.is_failed(request, &format!("field ({}): no such file: {}", field, str_path))); } } else { let mut path2 = PathBuf::new(); path2.push(prefix); path2.push(str_path); if path2.is_file() { return Ok(path2); } else { return Err(self.response.is_failed(request, &format!("field ({}): no such file: {}", field, str_path))); } } } fn has_spaces(&self, input: &String) -> bool { let found = input.find(' '); return found.is_some(); } pub fn add_sudo_details(&self, request: &TaskRequest, cmd: &str) -> Result<String, String> { // this is used by remote.rs to modify any command, inserting the results of evaluating the configured sudo_template // instead of the original command. only specific variables are allowed in the sudo template as opposed // to all the variables in jet's current host context. if ! request.is_sudoing() { return Ok(cmd.to_owned()); } let details = request.sudo_details.as_ref().unwrap(); let user = details.user.as_ref().unwrap().clone(); let sudo_template = details.template.clone(); let mut data = serde_yaml::Mapping::new(); data.insert(serde_yaml::Value::String(String::from("jet_sudo_user")), serde_yaml::Value::String(user.clone())); data.insert(serde_yaml::Value::String(String::from("jet_command")), serde_yaml::Value::String(cmd.to_string())); let result = self.detached_templar.render(&sudo_template, data, TemplateMode::Strict)?; return Ok(result) } }07070100000023000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001D00000000jetporch-0.0.1/src/inventory07070100000024000081A400000000000000000000000165135CC100001E70000000000000000000000000000000000000002700000000jetporch-0.0.1/src/inventory/groups.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::collections::HashMap; use crate::util::yaml::blend_variables; use std::sync::Arc; use crate::inventory::hosts::Host; use std::sync::RwLock; use serde_yaml; pub struct Group { pub name : String, pub subgroups : HashMap<String, Arc<RwLock<Self>>>, pub parents : HashMap<String, Arc<RwLock<Self>>>, pub hosts : HashMap<String, Arc<RwLock<Host>>>, pub variables : serde_yaml::Mapping, dyn_variables : serde_yaml::Value, } impl Group { pub fn new(name: &String) -> Self { Self { name : name.clone(), subgroups : HashMap::new(), parents : HashMap::new(), hosts : HashMap::new(), variables : serde_yaml::Mapping::new(), dyn_variables: serde_yaml::Value::from(serde_yaml::Mapping::new()), } } pub fn add_subgroup(&mut self, name: &String, subgroup: Arc<RwLock<Group>>) { assert!(!name.eq(&self.name)); self.subgroups.insert( name.clone(), Arc::clone(&subgroup) ); } pub fn add_host(&mut self, name: &String, host: Arc<RwLock<Host>>) { self.hosts.insert( name.clone(), Arc::clone(&host) ); } pub fn add_parent(&mut self, name: &String, parent: Arc<RwLock<Group>>) { assert!(!name.eq(&self.name)); self.parents.insert( name.clone(), Arc::clone(&parent) ); } pub fn get_ancestor_groups(&self, depth_limit: usize) -> HashMap<String, Arc<RwLock<Group>>> { let mut results : HashMap<String, Arc<RwLock<Group>>> = HashMap::new(); for (k,v) in self.parents.iter() { results.insert(k.clone(), Arc::clone(v)); if depth_limit > 0 { for (k2,v2) in v.read().expect("group read").get_ancestor_groups(depth_limit-1) { results.insert(k2.clone(),Arc::clone(&v2)); } } } return results } pub fn get_ancestor_group_names(&self) -> Vec<String> { return self.get_ancestor_groups(10usize).iter().map(|(k,_v)| k.clone()).collect(); } pub fn get_descendant_groups(&self, depth_limit: usize) -> HashMap<String, Arc<RwLock<Group>>> { let mut results : HashMap<String, Arc<RwLock<Group>>> = HashMap::new(); for (k,v) in self.subgroups.iter() { if results.contains_key(&k.clone()) { continue; } if depth_limit > 0 { for (k2,v2) in v.read().expect("group read").get_descendant_groups(depth_limit-1).iter() { results.insert( k2.clone(), Arc::clone(&v2) ); } } results.insert( k.clone(), Arc::clone(&v) ); } return results } pub fn get_descendant_group_names(&self) -> Vec<String> { return self.get_descendant_groups(10usize).iter().map(|(k,_v)| k.clone()).collect(); } pub fn get_parent_groups(&self) -> HashMap<String, Arc<RwLock<Group>>> { let mut results : HashMap<String, Arc<RwLock<Group>>> = HashMap::new(); for (k,v) in self.parents.iter() { results.insert( k.clone(), Arc::clone(&v) ); } return results } pub fn get_parent_group_names(&self) -> Vec<String> { return self.get_parent_groups().iter().map(|(k,_v)| k.clone()).collect(); } pub fn get_subgroups(&self) -> HashMap<String, Arc<RwLock<Group>>> { let mut results : HashMap<String, Arc<RwLock<Group>>> = HashMap::new(); for (k,v) in self.subgroups.iter() { results.insert( k.clone(), Arc::clone(&v) ); } return results } pub fn get_subgroup_names(&self) -> Vec<String> { return self.get_subgroups().iter().map(|(k,_v)| k.clone()).collect(); } pub fn get_direct_hosts(&self) -> HashMap<String, Arc<RwLock<Host>>> { let mut results : HashMap<String, Arc<RwLock<Host>>> = HashMap::new(); for (k,v) in self.hosts.iter() { results.insert( k.clone(), Arc::clone(&v) ); } return results } pub fn get_direct_host_names(&self) -> Vec<String> { return self.get_direct_hosts().iter().map(|(k,_v)| k.clone()).collect(); } pub fn get_descendant_hosts(&self) -> HashMap<String, Arc<RwLock<Host>>> { let mut results : HashMap<String, Arc<RwLock<Host>>> = HashMap::new(); let children = self.get_direct_hosts(); for (k,v) in children { results.insert(k.clone(), Arc::clone(&v)); } let groups = self.get_descendant_groups(20usize); for (_k,v) in groups.iter() { let hosts = v.read().unwrap().get_direct_hosts(); for (k2,v2) in hosts.iter() { results.insert(k2.clone(), Arc::clone(&v2)); } } return results } pub fn get_descendant_host_names(&self) -> Vec<String> { return self.get_descendant_hosts().iter().map(|(k,_v)| k.clone()).collect(); } pub fn get_variables(&self) -> serde_yaml::Mapping { return self.variables.clone(); } pub fn set_variables(&mut self, variables: serde_yaml::Mapping) { self.variables = variables.clone(); } pub fn update_variables(&mut self, mapping: serde_yaml::Mapping) { let map = mapping.clone(); blend_variables(&mut self.dyn_variables, serde_yaml::Value::Mapping(map)); } pub fn get_blended_variables(&self) -> serde_yaml::Mapping { let mut blended : serde_yaml::Value = serde_yaml::Value::from(serde_yaml::Mapping::new()); let ancestors = self.get_ancestor_groups(20); for (_k,v) in ancestors.iter() { let theirs : serde_yaml::Value = serde_yaml::Value::from(v.read().expect("group read").get_variables()); blend_variables(&mut blended, theirs); } blend_variables(&mut blended, self.dyn_variables.clone()); let mine = serde_yaml::Value::from(self.get_variables()); blend_variables(&mut blended, mine); return match blended { serde_yaml::Value::Mapping(x) => x, _ => panic!("get_blended_variables produced a non-mapping (1)") } } pub fn get_variables_yaml(&self) -> Result<String,String> { let result = serde_yaml::to_string(&self.get_variables()); return match result { Ok(x) => Ok(x), Err(_y) => Err(String::from("error loading variables")) } } pub fn get_blended_variables_yaml(&self) -> Result<String,String> { let result = serde_yaml::to_string(&self.get_blended_variables()); return match result { Ok(x) => Ok(x), Err(_y) => Err(String::from("error loading blended variables")) } } } 07070100000025000081A400000000000000000000000165135CC100001F2A000000000000000000000000000000000000002600000000jetporch-0.0.1/src/inventory/hosts.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::collections::HashMap; use crate::util::yaml::blend_variables; use std::sync::Arc; use crate::inventory::groups::Group; use std::sync::RwLock; use std::collections::HashSet; use serde_yaml; #[derive(Clone,Copy,Debug)] pub enum HostOSType { Linux, MacOS, } #[derive(Clone,Copy,Debug)] pub enum PackagePreference { // other package systems are supported but no other OSes are 'fuzzy' between distro families (yet) // so we don't need to specify them here (yet) Dnf, Yum, } pub struct Host { pub name : String, pub groups : HashMap<String, Arc<RwLock<Group>>>, pub variables : serde_yaml::Mapping, pub os_type : Option<HostOSType>, checksum_cache : HashMap<String,String>, checksum_cache_task_id : usize, facts : serde_yaml::Value, dyn_variables : serde_yaml::Value, pub package_preference : Option<PackagePreference>, notified_handlers : HashMap<usize, HashSet<String>> } impl Host { pub fn new(name: &String) -> Self { Self { name: name.clone(), variables : serde_yaml::Mapping::new(), groups: HashMap::new(), os_type: None, checksum_cache: HashMap::new(), checksum_cache_task_id: 0, facts: serde_yaml::Value::from(serde_yaml::Mapping::new()), dyn_variables: serde_yaml::Value::from(serde_yaml::Mapping::new()), notified_handlers: HashMap::new(), package_preference: None } } pub fn notify(&mut self, play_number: usize, signal: &String) { if ! self.notified_handlers.contains_key(&play_number) { self.notified_handlers.insert(play_number, HashSet::new()); } let entry = self.notified_handlers.get_mut(&play_number).unwrap(); entry.insert(signal.clone()); } pub fn is_notified(&self, play_number: usize, signal: &String) -> bool { let entry = self.notified_handlers.get(&play_number); if entry.is_none() { return false; } else { return entry.unwrap().contains(&signal.clone()); } } pub fn set_checksum_cache(&mut self, path: &String, checksum: &String) { self.checksum_cache.insert(path.clone(), checksum.clone()); } pub fn get_checksum_cache(&mut self, task_id: usize, path: &String) -> Option<String> { if task_id > self.checksum_cache_task_id { self.checksum_cache_task_id = task_id; self.checksum_cache.clear(); } if self.checksum_cache.contains_key(path) { let result = self.checksum_cache.get(path).unwrap(); return Some(result.clone()); } else { return None; } } // used by connection class on initial connect pub fn set_os_info(&mut self, uname_output: &String) -> Result<(),String> { if uname_output.starts_with("Linux") { self.os_type = Some(HostOSType::Linux); } else if uname_output.starts_with("Darwin") { self.os_type = Some(HostOSType::MacOS); } else { return Err(format!("OS Type could not be detected from uname -a: {}", uname_output)); } return Ok(()); } // ============================================================================================================== // PUBLIC API - most code can use this // ============================================================================================================== pub fn get_groups(&self) -> HashMap<String, Arc<RwLock<Group>>> { let mut results : HashMap<String, Arc<RwLock<Group>>> = HashMap::new(); for (k,v) in self.groups.iter() { results.insert(k.clone(), Arc::clone(&v)); } return results; } pub fn has_group(&self, group_name: &String) -> bool { for (k,_v) in self.groups.iter() { if k == group_name { return true; } } return false; } pub fn get_group_names(&self) -> Vec<String> { return self.get_groups().iter().map(|(k,_v)| k.clone()).collect(); } pub fn add_group(&mut self, name: &String, group: Arc<RwLock<Group>>) { self.groups.insert(name.clone(), Arc::clone(&group)); } pub fn get_ancestor_groups(&self, depth_limit: usize) -> HashMap<String, Arc<RwLock<Group>>> { let mut results : HashMap<String, Arc<RwLock<Group>>> = HashMap::new(); for (k,v) in self.get_groups().into_iter() { results.insert(k, Arc::clone(&v)); for (k2,v2) in v.read().expect("group read").get_ancestor_groups(depth_limit).into_iter() { results.insert(k2, Arc::clone(&v2)); } } return results; } pub fn get_ancestor_group_names(&self) -> Vec<String> { return self.get_ancestor_groups(20usize).iter().map(|(k,_v)| k.clone()).collect(); } pub fn get_variables(&self) -> serde_yaml::Mapping { return self.variables.clone(); } pub fn set_variables(&mut self, variables: serde_yaml::Mapping) { self.variables = variables.clone(); } pub fn update_variables(&mut self, mapping: serde_yaml::Mapping) { let map = mapping.clone(); blend_variables(&mut self.dyn_variables, serde_yaml::Value::Mapping(map)); } pub fn get_blended_variables(&self) -> serde_yaml::Mapping { let mut blended : serde_yaml::Value = serde_yaml::Value::from(serde_yaml::Mapping::new()); let ancestors = self.get_ancestor_groups(20); for (_k,v) in ancestors.iter() { let theirs : serde_yaml::Value = serde_yaml::Value::from(v.read().unwrap().get_variables()); blend_variables(&mut blended, theirs); } blend_variables(&mut blended, self.dyn_variables.clone()); let mine = serde_yaml::Value::from(self.get_variables()); blend_variables(&mut blended, mine); blend_variables(&mut blended, self.facts.clone()); return match blended { serde_yaml::Value::Mapping(x) => x, _ => panic!("get_blended_variables produced a non-mapping (1)") } } pub fn update_facts(&mut self, mapping: &Arc<RwLock<serde_yaml::Mapping>>) { let map = mapping.read().unwrap().clone(); blend_variables(&mut self.facts, serde_yaml::Value::Mapping(map)); } pub fn update_facts2(&mut self, mapping: serde_yaml::Mapping) { blend_variables(&mut self.facts, serde_yaml::Value::Mapping(mapping)); } pub fn get_variables_yaml(&self) -> Result<String, String> { let result = serde_yaml::to_string(&self.get_variables()); return match result { Ok(x) => Ok(x), Err(_y) => Err(String::from("error loading variables")) } } pub fn get_blended_variables_yaml(&self) -> Result<String,String> { let result = serde_yaml::to_string(&self.get_blended_variables()); return match result { Ok(x) => Ok(x), Err(_y) => Err(String::from("error loading blended variables")) } } } 07070100000026000081A400000000000000000000000165135CC10000164D000000000000000000000000000000000000002A00000000jetporch-0.0.1/src/inventory/inventory.rs use std::collections::{HashMap}; use std::sync::Arc; use crate::inventory::hosts::Host; use crate::inventory::groups::Group; use std::sync::RwLock; pub struct Inventory { pub groups : HashMap<String, Arc<RwLock<Group>>>, pub hosts : HashMap<String, Arc<RwLock<Host>>>, // SSH inventory is not required to have a localhost in it but needs the object // regardless, this is returned if it is not in inventory so we always get the same // object. backup_localhost: Arc<RwLock<Host>> } impl Inventory { pub fn new() -> Self { Self { groups : HashMap::new(), hosts : HashMap::new(), backup_localhost: Arc::new(RwLock::new(Host::new(&String::from("localhost")))) } } pub fn has_group(&self, group_name: &String) -> bool { return self.groups.contains_key(&group_name.clone()); } pub fn get_group(&self, group_name: &String) -> Arc<RwLock<Group>> { let arc = self.groups.get(group_name).unwrap(); return Arc::clone(&arc); } pub fn has_host(&self, host_name: &String) -> bool { return self.hosts.contains_key(host_name); } pub fn get_host(&self, host_name: &String) -> Arc<RwLock<Host>> { // an explicit fetch of a host is sometimes performed by the connection plugin // which does not bother with the has_host check. If localhost is not in inventory // we don't need any variables from it. if self.has_host(host_name) { let host = self.hosts.get(host_name).unwrap(); return Arc::clone(&host); } else if host_name.eq("localhost") { return Arc::clone(&self.backup_localhost); } else { panic!("internal error: code should call has_host before get_host"); } } // ============================================================================================================== // PACKAGE API (for use by loading.rs only) // ============================================================================================================== pub fn store_subgroup(&mut self, group_name: &String, subgroup_name: &String) { if self.has_group(group_name) { self.create_group(group_name); } if !self.has_group(subgroup_name) { self.create_group(subgroup_name); } self.associate_subgroup(group_name, subgroup_name); } pub fn store_group_variables(&mut self, group_name: &String, mapping: serde_yaml::Mapping) { let group = self.get_group(group_name); group.write().expect("group write").set_variables(mapping); } pub fn store_group(&mut self, group: &String) { self.create_group(&group.clone()); } pub fn associate_host(&mut self, group_name: &String, host_name: &String, host: Arc<RwLock<Host>>) { if !self.has_host(&host_name) { panic!("host does not exist"); } if !self.has_group(&group_name) { self.create_group(group_name); } let group_obj = self.get_group(group_name); // FIXME: these add method should all take strings, not all are consistent yet? group_obj.write().unwrap().add_host(&host_name.clone(), host); self.associate_host_to_group(&group_name.clone(), &host_name.clone()); } pub fn associate_host_to_group(&self, group_name: &String, host_name: &String) { let host = self.get_host(host_name); let group = self.get_group(group_name); host.write().expect("host write").add_group(group_name, Arc::clone(&group)); group.write().expect("group write").add_host(host_name, Arc::clone(&host)); } pub fn store_host_variables(&mut self, host_name: &String, mapping: serde_yaml::Mapping) { let host = self.get_host(host_name); host.write().unwrap().set_variables(mapping); } pub fn create_host(&mut self, host_name: &String) { assert!(!self.has_host(host_name)); self.hosts.insert(host_name.clone(), Arc::new(RwLock::new(Host::new(&host_name.clone())))); } pub fn store_host(&mut self, group_name: &String, host_name: &String) { if !(self.has_host(&host_name)) { self.create_host(&host_name); } let host = self.get_host(host_name); self.associate_host(group_name, host_name, Arc::clone(&host)); } // ============================================================================================================== // PRIVATE INTERNALS // ============================================================================================================== fn create_group(&mut self, group_name: &String) { if self.has_group(group_name) { return; } self.groups.insert(group_name.clone(), Arc::new(RwLock::new(Group::new(&group_name.clone())))); if !group_name.eq(&String::from("all")) { self.associate_subgroup(&String::from("all"), &group_name); } } fn associate_subgroup(&mut self, group_name: &String, subgroup_name: &String) { if !self.has_group(&group_name.clone()) { self.create_group(&group_name.clone()); } if !self.has_group(&subgroup_name.clone()) { self.create_group(&subgroup_name.clone()); } { let group = self.get_group(group_name); let subgroup = self.get_group(subgroup_name); group.write().expect("group write").add_subgroup(subgroup_name, Arc::clone(&subgroup)); } { let group = self.get_group(group_name); let subgroup = self.get_group(subgroup_name); subgroup.write().expect("subgroup write").add_parent(group_name, Arc::clone(&group)); } } }07070100000027000081A400000000000000000000000165135CC100002C94000000000000000000000000000000000000002800000000jetporch-0.0.1/src/inventory/loading.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::path::{Path,PathBuf}; use Vec; use serde::Deserialize; use crate::util::io::{path_walk,jet_file_open,path_basename_as_string,is_executable}; use crate::util::yaml::show_yaml_error_in_context; use crate::inventory::inventory::Inventory; use std::sync::Arc; use std::sync::RwLock; use serde_json; use std::collections::HashMap; use std::process::Command; use crate::connection::local::convert_out; use crate::util::io::directory_as_string; // ============================================================================================================== // YAML SPEC // ============================================================================================================== // for groups/<groupname> inventory files //#[derive(Debug, PartialEq, Deserialize)] #[derive(Debug,Deserialize)] #[serde(deny_unknown_fields)] pub struct YamlGroup { hosts : Option<Vec<String>>, subgroups : Option<Vec<String>>, } #[derive(Debug,Deserialize)] #[serde(deny_unknown_fields)] pub enum DynamicInventoryJson { Entry(HashMap<String, DynamicInventoryJsonEntry>) } /* groups named _meta are not real groups */ #[derive(Debug,Deserialize)] #[serde(deny_unknown_fields)] pub struct DynamicInventoryJsonEntry { hostvars : Option<HashMap<String, serde_json::Value>>, /* if supplied, hosts is not supplied */ vars : Option<HashMap<String, serde_json::Value>>, children : Option<Vec<String>>, hosts : Option<Vec<String>> } // ============================================================================================================== // PUBLIC API // ============================================================================================================== pub fn load_inventory(inventory: &Arc<RwLock<Inventory>>, inventory_paths: Arc<RwLock<Vec<PathBuf>>>) -> Result<(), String> { { let mut inv_obj = inventory.write().unwrap(); inv_obj.store_group(&String::from("all")); } for inventory_path_buf in inventory_paths.read().unwrap().iter() { let inventory_path = inventory_path_buf.as_path(); if inventory_path.is_dir() { let groups_pathbuf = inventory_path_buf.join("groups"); let groups_path = groups_pathbuf.as_path(); if groups_path.exists() && groups_path.is_dir() { load_on_disk_inventory_tree(inventory, true, &inventory_path)?; } else { return Err(format!("missing groups/ in --inventory path parameter ({})", inventory_path.display())) } } else { if is_executable(&inventory_path) { load_dynamic_inventory(inventory, &inventory_path)?; let dirname = directory_as_string(&inventory_path); let dir = Path::new(&dirname); load_on_disk_inventory_tree(inventory, false, &dir)?; } else { return Err(format!("non-directory path to --inventory ({}) is not executable", inventory_path.display())) } } } return Ok(()) } // ============================================================================================================== // PRIVATE INTERNALS // ============================================================================================================== // loads an entire on-disk inventory tree structure (groups/, group_vars/, host_vars/) fn load_on_disk_inventory_tree(inventory: &Arc<RwLock<Inventory>>, include_groups: bool, path: &Path) -> Result<(), String> { let path_buf = PathBuf::from(path); let group_vars_pathbuf = path_buf.join("group_vars"); let host_vars_pathbuf = path_buf.join("host_vars"); let groups_path = path_buf.join("groups"); let group_vars_path = group_vars_pathbuf.as_path(); let host_vars_path = host_vars_pathbuf.as_path(); if include_groups { load_groups_directory(inventory, &groups_path)?; } if group_vars_path.exists() { load_vars_directory(inventory, &group_vars_path, true)?; } if host_vars_path.exists() { load_vars_directory(inventory, &host_vars_path, false)?; } return Ok(()) } // for inventory/groups/* files fn load_groups_directory(inventory: &Arc<RwLock<Inventory>>, path: &Path) -> Result<(), String> { path_walk(path, |groups_file_path| { let group_name = path_basename_as_string(&groups_file_path).clone(); let groups_file = jet_file_open(&groups_file_path)?; let groups_file_parse_result: Result<YamlGroup, serde_yaml::Error> = serde_yaml::from_reader(groups_file); if groups_file_parse_result.is_err() { show_yaml_error_in_context(&groups_file_parse_result.unwrap_err(), &groups_file_path); return Err(format!("edit the file and try again?")); } let yaml_result = groups_file_parse_result.unwrap(); add_group_file_contents_to_inventory(inventory, group_name.clone(), &yaml_result); Ok(()) })?; Ok(()) } // for inventory/groups/* files fn add_group_file_contents_to_inventory(inventory: &Arc<RwLock<Inventory>>, group_name: String, yaml_group: &YamlGroup) { let mut inventory = inventory.write().unwrap(); let hosts = &yaml_group.hosts; if hosts.is_some() { let hosts = hosts.as_ref().unwrap(); for hostname in hosts { inventory.store_host(&group_name.clone(), &hostname.clone()); } } let subgroups = &yaml_group.subgroups; if subgroups.is_some() { let subgroups = subgroups.as_ref().unwrap(); for subgroupname in subgroups { // FIXME: we should not panic here, but do something better if !group_name.eq(subgroupname) { inventory.store_subgroup(&group_name.clone(), &subgroupname.clone()); } } } } // this is used by both on-disk and dynamic inventory sources to load group/ and vars/ directories fn load_vars_directory(inventory: &Arc<RwLock<Inventory>>, path: &Path, is_group: bool) -> Result<(), String> { let inv = inventory.write().unwrap(); path_walk(path, |vars_path| { let base_name = path_basename_as_string(&vars_path).clone(); // FIXME: warning and continue instead? match is_group { true => { if !inv.has_group(&base_name.clone()) { return Ok(()); } } false => { if !inv.has_host(&base_name.clone()) { return Ok(()); } } } let file = jet_file_open(&vars_path)?; let file_parse_result: Result<serde_yaml::Mapping, serde_yaml::Error> = serde_yaml::from_reader(file); if file_parse_result.is_err() { show_yaml_error_in_context(&file_parse_result.unwrap_err(), &vars_path); return Err(format!("edit the file and try again?")); } let yaml_result = file_parse_result.unwrap(); // serialize the vars again just to make them easier to store/output elsewhere // this will also remove any comments and shorten things up //let yaml_string = &serde_yaml::to_string(&yaml_result).unwrap(); match is_group { true => { let group = inv.get_group(&base_name.clone()); group.write().unwrap().set_variables(yaml_result); } false => { let host = inv.get_host(&base_name); host.write().unwrap().set_variables(yaml_result); } } Ok(()) })?; Ok(()) } // TODO: implement fn load_dynamic_inventory(inv: &Arc<RwLock<Inventory>>, path: &Path) -> Result<(), String> { let mut inventory = inv.write().unwrap(); let mut command = Command::new(format!("{}", path.display())); let output = match command.output() { Ok(x) => { match x.status.code() { Some(_rc) => convert_out(&x.stdout,&x.stderr), None => { return Err(format!("unable to get status code from process: {}", path.display())) } } }, Err(_x) => { return Err(format!("inventory script failed: {}", path.display())); } }; let file_parse_result: Result<HashMap<String, DynamicInventoryJsonEntry>, serde_json::Error> = serde_json::from_str(&output); if file_parse_result.is_err() { return Err(format!("error parsing dynamic inventory source: {:?}: {:?}", path.display(), &file_parse_result.unwrap_err())); } let json_result = file_parse_result.unwrap(); for (possible_group_name, entry) in json_result.iter() { let group_name = match possible_group_name.eq("_meta") { true => String::from("all"), false => possible_group_name.clone(), }; if group_name.starts_with("_") { continue; } inventory.store_group(&group_name); let group = inventory.get_group(&group_name); if entry.hostvars.is_some() { let hostvars = entry.hostvars.as_ref().unwrap(); for (host_name, values) in hostvars.iter() { inventory.store_host(&group_name, &host_name); let host = inventory.get_host(&host_name); let vars = convert_json_vars(&values); let mut hst = host.write().unwrap(); hst.update_variables(vars); } } if entry.hosts.is_some() { let hosts = entry.hosts.as_ref().unwrap(); for host_name in hosts.iter() { inventory.store_host(&group_name, &host_name); } } if entry.children.as_ref().is_some() { let subgroups = entry.children.as_ref().unwrap(); for subgroup_name in subgroups.iter() { inventory.store_subgroup(&group_name, &subgroup_name); } } if entry.vars.as_ref().is_some() { let vars = entry.vars.as_ref().unwrap(); for (_key, values) in vars.iter() { let vars = convert_json_vars(&values); let mut grp = group.write().unwrap(); grp.update_variables(vars); } } } Ok(()) } // TODO: this is used in the parser also, move to utils/ pub fn convert_json_vars(input: &serde_json::Value) -> serde_yaml::Mapping { let json = input.to_string(); let file_parse_result: Result<serde_yaml::Mapping, serde_yaml::Error> = serde_yaml::from_str(&json); match file_parse_result { Ok(parsed) => return parsed.clone(), Err(y) => panic!("unable to load JSON back to YAML, this shouldn't happen: {}", y) } } 07070100000028000081A400000000000000000000000165135CC10000032B000000000000000000000000000000000000002400000000jetporch-0.0.1/src/inventory/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. pub mod groups; pub mod hosts; pub mod loading; pub mod inventory; 07070100000029000081A400000000000000000000000165135CC1000010D7000000000000000000000000000000000000001B00000000jetporch-0.0.1/src/main.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. mod cli; mod inventory; mod util; mod playbooks; mod registry; mod connection; mod modules; mod tasks; mod handle; use crate::util::io::{quit}; use crate::inventory::inventory::Inventory; use crate::inventory::loading::{load_inventory}; use crate::cli::show::{show_inventory_group,show_inventory_host}; use crate::cli::parser::{CliParser}; use crate::cli::playbooks::{playbook_ssh,playbook_local,playbook_check_ssh,playbook_check_local,playbook_simulate}; // FIXME: check modes coming use std::sync::{Arc,RwLock}; use std::process; fn main() { match liftoff() { Err(e) => quit(&e), _ => {} } } fn liftoff() -> Result<(),String> { let mut cli_parser = CliParser::new(); cli_parser.parse()?; // jetp --help was given, or no arguments if cli_parser.needs_help { cli_parser.show_help(); return Ok(()); } if cli_parser.needs_version { cli_parser.show_version(); return Ok(()); } let inventory : Arc<RwLock<Inventory>> = Arc::new(RwLock::new(Inventory::new())); match cli_parser.mode { cli::parser::CLI_MODE_SSH | cli::parser::CLI_MODE_CHECK_SSH | cli::parser::CLI_MODE_SHOW | cli::parser::CLI_MODE_SIMULATE => { load_inventory(&inventory, Arc::clone(&cli_parser.inventory_paths))?; if ! cli_parser.inventory_set { return Err(String::from("--inventory is required")); } if inventory.read().expect("inventory read").hosts.len() == 0 { return Err(String::from("no hosts found in --inventory")); } }, _ => { inventory.write().expect("inventory write").store_host(&String::from("all"), &String::from("localhost")); } }; match cli_parser.mode { cli::parser::CLI_MODE_SHOW => {}, _ => { if ! cli_parser.playbook_set { return Err(String::from("--playbook is required")); } } }; if cli_parser.threads > 1 { rayon::ThreadPoolBuilder::new().num_threads(cli_parser.threads).build_global().expect("build global"); }; let exit_status = match cli_parser.mode { cli::parser::CLI_MODE_SHOW => match handle_show(&inventory, &cli_parser) { Ok(_) => 0, Err(s) => { println!("{}", s); 1 } } cli::parser::CLI_MODE_SSH => playbook_ssh(&inventory, &cli_parser), cli::parser::CLI_MODE_CHECK_SSH => playbook_check_ssh(&inventory, &cli_parser), cli::parser::CLI_MODE_LOCAL => playbook_local(&inventory, &cli_parser), cli::parser::CLI_MODE_CHECK_LOCAL => playbook_check_local(&inventory, &cli_parser), cli::parser::CLI_MODE_SIMULATE => playbook_simulate(&inventory, &cli_parser), _ => { println!("invalid CLI mode"); 1 } }; if exit_status != 0 { process::exit(exit_status); } return Ok(()); } pub fn handle_show(inventory: &Arc<RwLock<Inventory>>, parser: &CliParser) -> Result<(), String> { // jetp show -i inventory // jetp show -i inventory --groups g1:g2 // jetp show -i inventory --hosts h1:h2 if parser.show_groups.is_empty() && parser.show_hosts.is_empty() { show_inventory_group(inventory, &String::from("all"))?; } for group_name in parser.show_groups.iter() { show_inventory_group(inventory, &group_name.clone())?; } for host_name in parser.show_hosts.iter() { show_inventory_host(inventory, &host_name.clone())?; } return Ok(()); } 0707010000002A000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001B00000000jetporch-0.0.1/src/modules0707010000002B000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000002400000000jetporch-0.0.1/src/modules/commands0707010000002C000081A400000000000000000000000165135CC100000324000000000000000000000000000000000000002B00000000jetporch-0.0.1/src/modules/commands/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. /** ADD MODULES HERE, KEEP ALPHABETIZED **/ pub mod shell; 0707010000002D000081A400000000000000000000000165135CC10000194A000000000000000000000000000000000000002D00000000jetporch-0.0.1/src/modules/commands/shell.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; use crate::connection::command::cmd_info; //#[allow(unused_imports)] use serde::{Deserialize}; use std::sync::{Arc,RwLock}; use crate::inventory::hosts::Host; const MODULE: &str = "Shell"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct ShellTask { pub name: Option<String>, pub cmd: String, pub save: Option<String>, pub failed_when: Option<String>, pub changed_when: Option<String>, #[serde(rename = "unsafe")] pub unsafe_: Option<String>, /* FIXME: can use r#unsafe instead */ pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput>, } struct ShellAction { pub cmd: String, pub save: Option<String>, pub failed_when: Option<String>, pub changed_when: Option<String>, pub unsafe_: bool, } impl IsTask for ShellTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(ShellAction { unsafe_: { if self.cmd.find("{{").is_none() { // allow all the fancy shell characters unless variables are used, in which case // do a bit of extra filtering unless users turn it off. true } else { handle.template.boolean_option_default_false(&request, tm, &String::from("unsafe"), &self.unsafe_)? } }, cmd: handle.template.string_unsafe_for_shell(&request, tm, &String::from("cmd"), &self.cmd)?, save: handle.template.string_option_no_spaces(&request, tm, &String::from("save"), &self.save)?, failed_when: handle.template.string_option_unsafe_for_shell(&request, tm, &String::from("failed_when"), &self.failed_when)?, changed_when: handle.template.string_option_unsafe_for_shell(&request, tm, &String::from("changed_when"), &self.changed_when)?, }), with: Arc::new(PreLogicInput::template(&handle, &request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(&handle, &request, tm, &self.and)?), } ); } } impl IsAction for ShellAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { return Ok(handle.response.needs_execution(&request)); }, TaskRequestType::Execute => { let task_result : Arc<TaskResponse>; if self.unsafe_ { task_result = handle.remote.run_unsafe(&request, &self.cmd.clone(), CheckRc::Unchecked)?; } else { task_result = handle.remote.run(&request, &self.cmd.clone(), CheckRc::Unchecked)?; } let (rc, out) = cmd_info(&task_result); let map_data = build_results_map(rc, &out); let should_fail = match self.failed_when.is_none() { true => match rc { 0 => false, _ => true }, false => { let condition = self.failed_when.as_ref().unwrap(); handle.template.test_condition_with_extra_data(request, TemplateMode::Strict, condition, &handle.host, map_data.clone())? } }; let should_mark_changed = match self.changed_when.is_none() { true => true, false => { let condition = self.changed_when.as_ref().unwrap(); handle.template.test_condition_with_extra_data(request, TemplateMode::Strict, condition, &handle.host, map_data.clone())? } }; if self.save.is_some() { save_results(&handle.host, self.save.as_ref().unwrap(), map_data); } return match should_fail { true => Err(handle.response.command_failed(request, &Arc::clone(&task_result.command_result))), false => match should_mark_changed { true => Ok(task_result), false => Ok(handle.response.is_passive(request)) } }; }, _ => { return Err(handle.response.not_supported(&request)); } } } } fn build_results_map(rc: i32, out: &String) -> serde_yaml::Mapping { let mut result = serde_yaml::Mapping::new(); let num : serde_yaml::Value = serde_yaml::from_str(&format!("{}", rc)).unwrap(); result.insert(serde_yaml::Value::String(String::from("rc")), num); //result.insert(serde_yaml::Value::String(String::from("rc")), serde_yaml::Value::String(format!("{}", rc))); result.insert(serde_yaml::Value::String(String::from("out")), serde_yaml::Value::String(out.clone())); return result; } fn save_results(host: &Arc<RwLock<Host>>, key: &String, map_data: serde_yaml::Mapping) { let mut result = serde_yaml::Mapping::new(); result.insert(serde_yaml::Value::String(key.clone()), serde_yaml::Value::Mapping(map_data.clone())); host.write().unwrap().update_variables(result); }0707010000002E000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000002300000000jetporch-0.0.1/src/modules/control0707010000002F000081A400000000000000000000000165135CC100001514000000000000000000000000000000000000002D00000000jetporch-0.0.1/src/modules/control/assert.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; //#[allow(unused_imports)] use serde::Deserialize; use std::sync::Arc; const MODULE: &str = "assert"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct AssertTask { pub name: Option<String>, pub msg: Option<String>, pub r#true: Option<String>, pub r#false: Option<String>, pub all_true: Option<Vec<String>>, pub all_false: Option<Vec<String>>, pub some_true: Option<Vec<String>>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } #[allow(dead_code)] struct AssertAction { pub name: String, pub msg: Option<String>, pub r#true: bool, pub r#false: bool, pub all_true: Vec<bool>, pub all_false: Vec<bool>, pub some_true: Vec<bool> } impl IsTask for AssertTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(AssertAction { name: self.name.clone().unwrap_or(String::from(MODULE)), msg: handle.template.string_option_unsafe_for_shell(request, tm, &String::from("msg"), &self.msg)?, r#true: match self.r#true.is_some() { true => handle.template.test_condition(request, tm, &self.r#true.as_ref().unwrap())?, false => true }, r#false: match self.r#false.is_some() { true => handle.template.test_condition(request, tm, &self.r#false.as_ref().unwrap())?, false => false }, all_true: match self.all_true.is_some() { true => eval_list(handle, request, tm, self.all_true.as_ref().unwrap())?, false => vec![true] }, all_false: match self.all_false.is_some() { true => eval_list(handle, request, tm, self.all_false.as_ref().unwrap())?, false => vec![false] }, some_true: match self.some_true.is_some() { true => eval_list(handle, request, tm, self.some_true.as_ref().unwrap())?, false => vec![true] } }), with: Arc::new(PreLogicInput::template(handle, request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(handle, request, tm, &self.and)?), } ); } } fn eval_list(handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode, list: &Vec<String>) -> Result<Vec<bool>,Arc<TaskResponse>> { let mut results : Vec<bool> = Vec::new(); for item in list.iter() { results.push(handle.template.test_condition(request, tm, item)?); } return Ok(results); } impl IsAction for AssertAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { return Ok(handle.response.needs_passive(request)); }, TaskRequestType::Passive => { let mut fail = false; if self.r#true == false { fail = true; } else if self.r#false == true { fail = true; } else if self.all_true.contains(&false) { fail = true; } else if self.all_false.contains(&true) { fail = true; } else if ! self.some_true.contains(&true) { fail = true; } if fail { if self.msg.is_some() { return Err(handle.response.is_failed(request, &format!("assertion failed: {}", self.msg.as_ref().unwrap()))); } else { return Err(handle.response.is_failed(request, &format!("assertion failed"))); } } return Ok(handle.response.is_passive(request)); }, _ => { return Err(handle.response.not_supported(request)); } } } }07070100000030000081A400000000000000000000000165135CC100000E1C000000000000000000000000000000000000002C00000000jetporch-0.0.1/src/modules/control/debug.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; use crate::handle::template::BlendTarget; //#[allow(unused_imports)] use serde::Deserialize; use std::sync::Arc; const MODULE: &str = "debug"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct DebugTask { pub name: Option<String>, pub variables: Option<Vec<String>>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } #[allow(dead_code)] struct DebugAction { pub name: String, pub variables: Option<Vec<String>>, } impl IsTask for DebugTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(DebugAction { name: self.name.clone().unwrap_or(String::from(MODULE)), variables: self.variables.clone() }), with: Arc::new(PreLogicInput::template(handle, request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(handle, request, tm, &self.and)?), } ); } } impl IsAction for DebugAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { return Ok(handle.response.needs_passive(request)); }, TaskRequestType::Passive => { let mut map : serde_yaml::Mapping = serde_yaml::Mapping::new(); let no_vars = self.variables.is_none(); let blended = handle.run_state.context.read().unwrap().get_complete_blended_variables(&handle.host, BlendTarget::NotTemplateModule); for (k,v) in blended.iter() { let k2 : String = match k { serde_yaml::Value::String(s) => s.clone(), _ => { panic!("invalid key in mapping"); } }; if no_vars || self.variables.as_ref().unwrap().contains(&k2) { if ! k2.eq(&String::from("item")) { map.insert(k.clone(), v.clone()); } } } let msg = serde_yaml::to_string(&map).unwrap(); let msg2 = format!("\n{}\n", msg); handle.debug(request, &msg2); return Ok(handle.response.is_passive(request)); }, _ => { return Err(handle.response.not_supported(request)); } } } }07070100000031000081A400000000000000000000000165135CC100000A6D000000000000000000000000000000000000002B00000000jetporch-0.0.1/src/modules/control/echo.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; //#[allow(unused_imports)] use serde::Deserialize; use std::sync::Arc; const MODULE: &str = "echo"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct EchoTask { pub name: Option<String>, pub msg: String, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } #[allow(dead_code)] struct EchoAction { pub name: String, pub msg: String, } impl IsTask for EchoTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(EchoAction { name: self.name.clone().unwrap_or(String::from(MODULE)), msg: handle.template.string_unsafe_for_shell(request, tm, &String::from("msg"), &self.msg)?, }), with: Arc::new(PreLogicInput::template(handle, request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(handle, request, tm, &self.and)?), } ); } } impl IsAction for EchoAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { return Ok(handle.response.needs_passive(request)); }, TaskRequestType::Passive => { handle.debug(&request, &self.msg); return Ok(handle.response.is_passive(request)); }, _ => { return Err(handle.response.not_supported(request)); } } } }07070100000032000081A400000000000000000000000165135CC10000170E000000000000000000000000000000000000002C00000000jetporch-0.0.1/src/modules/control/facts.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; use crate::inventory::hosts::{HostOSType}; //#[allow(unused_imports)] use serde::Deserialize; use std::sync::{Arc,RwLock}; const MODULE: &str = "facts"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct FactsTask { pub name: Option<String>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } struct FactsAction { } impl IsTask for FactsTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(FactsAction { }), with: Arc::new(PreLogicInput::template(handle, request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(handle, request, tm, &self.and)?), } ); } } impl IsAction for FactsAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { return Ok(handle.response.needs_passive(request)); }, TaskRequestType::Passive => { self.do_facts(handle, request)?; return Ok(handle.response.is_passive(request)); }, _ => { return Err(handle.response.not_supported(request)); } } } } impl FactsAction { fn do_facts(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<(), Arc<TaskResponse>> { let os_type = handle.host.read().unwrap().os_type; let facts = Arc::new(RwLock::new(serde_yaml::Mapping::new())); match os_type { Some(HostOSType::Linux) => { self.do_linux_facts(handle, request, &facts)?; }, Some(HostOSType::MacOS) => { self.do_mac_facts(handle, request, &facts)?; } None => { return Err(handle.response.is_failed(request, &String::from("facts not implemented for OS Type"))); } }; handle.host.write().unwrap().update_facts(&facts); return Ok(()); } fn insert_string(&self, mapping: &Arc<RwLock<serde_yaml::Mapping>>, key: &String, value: &String) { mapping.write().unwrap().insert(serde_yaml::Value::String(key.clone()), serde_yaml::Value::String(value.clone())); } fn do_mac_facts(&self, _handle: &Arc<TaskHandle>, _request: &Arc<TaskRequest>, mapping: &Arc<RwLock<serde_yaml::Mapping>>) -> Result<(), Arc<TaskResponse>> { // sets jet_os_type=MacOS self.insert_string(mapping, &String::from("jet_os_type"), &String::from("MacOS")); self.insert_string(mapping, &String::from("jet_os_flavor"), &String::from("OSX")); return Ok(()); } fn do_linux_facts(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, mapping: &Arc<RwLock<serde_yaml::Mapping>>) -> Result<(), Arc<TaskResponse>> { // sets jet_os_type=Linux self.insert_string(mapping, &String::from("jet_os_type"), &String::from("Linux")); // and more facts... self.do_linux_os_release(handle, request, mapping)?; return Ok(()); } fn do_linux_os_release(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, mapping: &Arc<RwLock<serde_yaml::Mapping>>) -> Result<(), Arc<TaskResponse>> { // makes a lot of variables from everything in /etc/os-release with a jet_os_release prefix such as: // jet_os_release_id="rocky" // jet_os_release_platform_id="platform:el9" // jet_os_release_id_like="rhel centos fedora" // not all keys are available on all platforms // more facts will be added from other sources later, some may be conditional based on distro let cmd = String::from("cat /etc/os-release"); let result = handle.remote.run(request, &cmd, CheckRc::Checked)?; let (_rc, out) = cmd_info(&result); for line in out.lines() { let mut tokens = line.split("="); let key = tokens.nth(0); let value = tokens.nth(0); if key.is_some() && value.is_some() { let mut k1 = key.unwrap().trim().to_string(); k1.make_ascii_lowercase(); let v1 = value.unwrap().trim().to_string().replace("\"",""); self.insert_string(mapping, &format!("jet_os_release_{}", k1.to_string()), &v1.clone()); if k1.eq("id_like") { if v1.find("rhel").is_some() { self.insert_string(mapping, &String::from("jet_os_flavor"), &String::from("EL")); } else if v1.find("debian").is_some() { self.insert_string(mapping, &String::from("jet_os_flavor"), &String::from("Debian")) } } } } return Ok(()); } } 07070100000033000081A400000000000000000000000165135CC100000B19000000000000000000000000000000000000002B00000000jetporch-0.0.1/src/modules/control/fail.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; //#[allow(unused_imports)] use serde::Deserialize; use std::sync::Arc; const MODULE: &str = "fail"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct FailTask { pub name: Option<String>, pub msg: Option<String>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } #[allow(dead_code)] struct FailAction { pub name: String, pub msg: Option<String>, } impl IsTask for FailTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(FailAction { name: self.name.clone().unwrap_or(String::from(MODULE)), msg: handle.template.string_option_unsafe_for_shell(request, tm, &String::from("msg"), &self.msg)?, }), with: Arc::new(PreLogicInput::template(handle, request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(handle, request, tm, &self.and)?), } ); } } impl IsAction for FailAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { return Ok(handle.response.needs_passive(request)); }, TaskRequestType::Passive => { let msg = match self.msg.is_some() { true => self.msg.as_ref().unwrap().clone(), false => String::from("fail invoked") }; return Err(handle.response.is_failed(request, &msg)); }, _ => { return Err(handle.response.not_supported(request)); } } } }07070100000034000081A400000000000000000000000165135CC10000036B000000000000000000000000000000000000002A00000000jetporch-0.0.1/src/modules/control/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. /** ADD MODULES HERE, KEEP ALPHABETIZED **/ pub mod assert; pub mod debug; pub mod echo; pub mod fail; pub mod facts; pub mod set;07070100000035000081A400000000000000000000000165135CC100000DCD000000000000000000000000000000000000002A00000000jetporch-0.0.1/src/modules/control/set.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; //#[allow(unused_imports)] use serde::{Deserialize}; use std::sync::{Arc}; const MODULE: &str = "Set"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct SetTask { pub name: Option<String>, pub vars: Option<serde_yaml::Mapping>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput>, } struct SetAction { pub vars: Option<serde_yaml::Mapping>, } impl IsTask for SetTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(SetAction { vars: self.vars.clone() /* templating will happen below */ }), with: Arc::new(PreLogicInput::template(&handle, &request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(&handle, &request, tm, &self.and)?), } ); } } impl IsAction for SetAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { return Ok(handle.response.needs_passive(&request)); }, TaskRequestType::Passive => { /* so far this only templates top level strings, which is probably sufficient, rather than strings found in deeper levels */ let mut mapping = serde_yaml::Mapping::new(); if self.vars.as_ref().is_some() { for (k,v) in self.vars.as_ref().unwrap().iter() { if v.is_string() { let ks = v.as_str().unwrap().to_string(); let vs = v.as_str().unwrap().to_string(); let templated = handle.template.string_unsafe_for_shell(request, TemplateMode::Strict, &ks.clone(), &vs)?; mapping.insert(k.clone(), serde_yaml::Value::String(templated)); } else { mapping.insert(k.clone(), v.clone()); } } } handle.host.write().unwrap().update_variables(mapping); return Ok(handle.response.is_passive(&request)); } _ => { return Err(handle.response.not_supported(request)); } } } } 07070100000036000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000002100000000jetporch-0.0.1/src/modules/files07070100000037000081A400000000000000000000000165135CC1000013F6000000000000000000000000000000000000002900000000jetporch-0.0.1/src/modules/files/copy.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; use crate::tasks::fields::Field; use std::path::{PathBuf}; //#[allow(unused_imports)] use serde::{Deserialize}; use std::sync::Arc; use std::vec::Vec; use crate::tasks::files::Recurse; const MODULE: &str = "copy"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct CopyTask { pub name: Option<String>, pub src: String, pub dest: String, pub attributes: Option<FileAttributesInput>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } struct CopyAction { pub src: PathBuf, pub dest: String, pub attributes: Option<FileAttributesEvaluated>, } impl IsTask for CopyTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { let src = handle.template.string(&request, tm, &String::from("src"), &self.src)?; return Ok( EvaluatedTask { action: Arc::new(CopyAction { src: handle.template.find_file_path(request, tm, &String::from("src"), &src)?, dest: handle.template.path(&request, tm, &String::from("dest"), &self.dest)?, attributes: FileAttributesInput::template(&handle, &request, tm, &self.attributes)? }), with: Arc::new(PreLogicInput::template(&handle, &request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(&handle, &request, tm, &self.and)?), } ); } } impl IsAction for CopyAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { let mut changes : Vec<Field> = Vec::new(); let remote_mode = handle.remote.query_common_file_attributes(request, &self.dest, &self.attributes, &mut changes, Recurse::No)?; if remote_mode.is_none() { return Ok(handle.response.needs_creation(request)); } // this query leg is (at least originally) the same as the template module query except these two lines // to calculate the checksum differently let src_path = self.src.as_path(); let local_512 = handle.local.get_sha512(request, &src_path, true)?; let remote_512 = handle.remote.get_sha512(request, &self.dest)?; if ! remote_512.eq(&local_512) { changes.push(Field::Content); } if ! changes.is_empty() { return Ok(handle.response.needs_modification(request, &changes)); } return Ok(handle.response.is_matched(request)); }, TaskRequestType::Create => { self.do_copy(handle, request, None)?; return Ok(handle.response.is_created(request)); }, TaskRequestType::Modify => { if request.changes.contains(&Field::Content) { self.do_copy(handle, request, Some(request.changes.clone()))?; } else { handle.remote.process_common_file_attributes(request, &self.dest, &self.attributes, &request.changes, Recurse::No)?; } return Ok(handle.response.is_modified(request, request.changes.clone())); }, _ => { return Err(handle.response.not_supported(request)); } } } } impl CopyAction { pub fn do_copy(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, _changes: Option<Vec<Field>>) -> Result<(), Arc<TaskResponse>> { handle.remote.copy_file(request, &self.src, &self.dest, |f| { /* after save */ match handle.remote.process_all_common_file_attributes(request, &f, &self.attributes, Recurse::No) { Ok(_x) => Ok(()), Err(y) => Err(y) } })?; return Ok(()); } } 07070100000038000081A400000000000000000000000165135CC1000013C5000000000000000000000000000000000000002E00000000jetporch-0.0.1/src/modules/files/directory.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; use crate::tasks::fields::Field; use crate::tasks::files::Recurse; //#[allow(unused_imports)] use serde::{Deserialize}; use std::sync::Arc; use std::vec::Vec; const MODULE: &str = "directory"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct DirectoryTask { pub name: Option<String>, pub path: String, pub remove: Option<String>, pub recurse: Option<String>, pub attributes: Option<FileAttributesInput>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } struct DirectoryAction { pub path: String, pub remove: bool, pub recurse: Recurse, pub attributes: Option<FileAttributesEvaluated>, } impl IsTask for DirectoryTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { let recurse = match handle.template.boolean_option_default_false(&request, tm, &String::from("recurse"), &self.recurse)? { true => Recurse::Yes, false => Recurse::No }; return Ok( EvaluatedTask { action: Arc::new(DirectoryAction { remove: handle.template.boolean_option_default_false(&request, tm, &String::from("remove"), &self.remove)?, recurse: recurse, path: handle.template.path(&request, tm, &String::from("path"), &self.path)?, attributes: FileAttributesInput::template(&handle, &request, tm, &self.attributes)? }), with: Arc::new(PreLogicInput::template(&handle, &request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(&handle, &request, tm, &self.and)?), } ); } } impl IsAction for DirectoryAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { let mut changes : Vec<Field> = Vec::new(); let remote_mode = handle.remote.query_common_file_attributes(request, &self.path, &self.attributes, &mut changes, self.recurse)?; if remote_mode.is_none() { if self.remove { return Ok(handle.response.is_matched(request)); } else { return Ok(handle.response.needs_creation(request)); } } else { let is_file = handle.remote.get_is_file(request, &self.path)?; if is_file { return Err(handle.response.is_failed(request, &format!("{} is not a directory", self.path))); } else if self.remove { return Ok(handle.response.needs_removal(request)); } else if changes.is_empty() { return Ok(handle.response.is_matched(request)); } else { return Ok(handle.response.needs_modification(request, &changes)); } } }, TaskRequestType::Create => { handle.remote.create_directory(request, &self.path)?; handle.remote.process_all_common_file_attributes(request, &self.path, &self.attributes, self.recurse)?; return Ok(handle.response.is_created(request)); }, TaskRequestType::Modify => { handle.remote.process_common_file_attributes(request, &self.path, &self.attributes, &request.changes, self.recurse)?; return Ok(handle.response.is_modified(request, request.changes.clone())); }, TaskRequestType::Remove => { handle.remote.delete_directory(request, &self.path, self.recurse)?; return Ok(handle.response.is_removed(request)) } // no passive or execute leg _ => { return Err(handle.response.not_supported(request)); } } } }07070100000039000081A400000000000000000000000165135CC100001258000000000000000000000000000000000000002900000000jetporch-0.0.1/src/modules/files/file.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; use crate::tasks::fields::Field; //#[allow(unused_imports)] use serde::{Deserialize}; use std::sync::Arc; use std::vec::Vec; use crate::tasks::files::Recurse; const MODULE: &str = "file"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct FileTask { pub name: Option<String>, pub path: String, pub remove: Option<String>, pub attributes: Option<FileAttributesInput>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } struct FileAction { pub path: String, pub remove: bool, pub attributes: Option<FileAttributesEvaluated>, } impl IsTask for FileTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(FileAction { remove: handle.template.boolean_option_default_false(&request, tm, &String::from("remove"), &self.remove)?, path: handle.template.path(&request, tm, &String::from("path"), &self.path)?, attributes: FileAttributesInput::template(&handle, &request, tm, &self.attributes)? }), with: Arc::new(PreLogicInput::template(&handle, &request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(&handle, &request, tm, &self.and)?), } ); } } impl IsAction for FileAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { let mut changes : Vec<Field> = Vec::new(); let remote_mode = handle.remote.query_common_file_attributes(request, &self.path, &self.attributes, &mut changes, Recurse::No)?; if remote_mode.is_none() { if self.remove { return Ok(handle.response.is_matched(request)); } else { return Ok(handle.response.needs_creation(request)); } } else { let is_dir = handle.remote.get_is_directory(request, &self.path)?; if is_dir { return Err(handle.response.is_failed(request, &format!("{} is a directory", self.path))); } else if self.remove { return Ok(handle.response.needs_removal(request)); } else if changes.is_empty() { return Ok(handle.response.is_matched(request)); } else { return Ok(handle.response.needs_modification(request, &changes)); } } }, TaskRequestType::Create => { handle.remote.touch_file(request, &self.path)?; handle.remote.process_all_common_file_attributes(request, &self.path, &self.attributes, Recurse::No)?; return Ok(handle.response.is_created(request)); }, TaskRequestType::Modify => { handle.remote.process_common_file_attributes(request, &self.path, &self.attributes, &request.changes, Recurse::No)?; return Ok(handle.response.is_modified(request, request.changes.clone())); }, TaskRequestType::Remove => { handle.remote.delete_file(request, &self.path)?; return Ok(handle.response.is_removed(request)) } // no passive or execute leg _ => { return Err(handle.response.not_supported(request)); } } } } 0707010000003A000081A400000000000000000000000165135CC100002E4B000000000000000000000000000000000000002800000000jetporch-0.0.1/src/modules/files/git.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; use crate::tasks::fields::Field; //#[allow(unused_imports)] use serde::{Deserialize}; use std::sync::Arc; use std::vec::Vec; use crate::tasks::files::Recurse; use std::collections::HashMap; const MODULE: &str = "git"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct GitTask { pub name: Option<String>, pub repo: String, pub path: String, pub branch: Option<String>, pub ssh_options: Option<HashMap<String,String>>, pub accept_keys: Option<String>, pub update: Option<String>, pub attributes: Option<FileAttributesInput>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } struct GitAction { pub repo: String, pub path: String, pub branch: String, pub ssh_options: Vec<String>, pub accept_keys: bool, pub update: bool, pub attributes: Option<FileAttributesEvaluated>, } impl IsTask for GitTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(GitAction { repo: handle.template.string(&request, tm, &String::from("repo"), &self.repo)?, path: handle.template.path(&request, tm, &String::from("path"), &self.path)?, branch: handle.template.string_option_default(&request, tm, &String::from("branch"), &self.branch, &String::from("main"))?, accept_keys: handle.template.boolean_option_default_true(&request, tm, &String::from("accept_keys"), &self.accept_keys)?, update: handle.template.boolean_option_default_true(&request, tm, &String::from("update"), &self.update)?, attributes: FileAttributesInput::template(&handle, &request, tm, &self.attributes)?, ssh_options: { let mut options : Vec<String> = Vec::new(); match &self.ssh_options { Some(input_options) => { for (k,v) in input_options.iter() { options.push(format!("-o {}={}", k, v)) } }, _ => {} }; options.push(String::from("-o BatchMode=Yes")); options } }), with: Arc::new(PreLogicInput::template(&handle, &request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(&handle, &request, tm, &self.and)?), } ); } } impl IsAction for GitAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { let mut changes : Vec<Field> = Vec::new(); // see if the remote directory exists let remote_mode = handle.remote.query_common_file_attributes(request, &self.path, &self.attributes, &mut changes, Recurse::Yes)?; match remote_mode { // the directory does not exist, need to make everything happen None => Ok(handle.response.needs_creation(request)), // the directory does exist, but the .git directory might not, or it might need to change versions/branches // so more checking needed... _ => { let git_path = match self.path.ends_with("/") { // could have used pathbuf, but ... anyway ... true => format!("{}{}", self.path, String::from(".git")), false => format!("{}/{}", self.path, String::from(".git")), }; match handle.remote.get_mode(request, &git_path)? { // the repo does not exist, so do everything None => Ok(handle.response.needs_creation(request)), // the repo does exist, see what needs to change depending on parameters // minor FIXME: this module does not currently deal with repo URLs changing // when a git directory has already been checked out at a given location _ => { let local_version = self.get_local_version(handle, request)?; if local_version.is_none() { changes.push(Field::Version); } else { let remote_version = self.get_remote_version(handle, request)?; let local_branch = self.get_local_branch(handle, request)?; if self.update && (! remote_version.eq(&local_version.unwrap())) { changes.push(Field::Version); } if ! local_branch.eq(&self.branch) { changes.push(Field::Branch); } } if changes.len() > 0 { Ok(handle.response.needs_modification(request, &changes)) } else { Ok(handle.response.is_matched(request)) } } } } } } TaskRequestType::Create => { handle.remote.create_directory(request, &self.path)?; handle.remote.process_all_common_file_attributes(request, &self.path, &self.attributes, Recurse::Yes)?; self.clone(handle, request)?; self.switch_branch(handle, request)?; return Ok(handle.response.is_created(request)); }, TaskRequestType::Modify => { handle.remote.process_common_file_attributes(request, &self.path, &self.attributes, &request.changes, Recurse::Yes)?; if request.changes.contains(&Field::Branch) || request.changes.contains(&Field::Version) { self.pull(handle,request)?; } if request.changes.contains(&Field::Branch) { self.switch_branch(handle, request)?; } return Ok(handle.response.is_modified(request, request.changes.clone())); }, // no passive or execute leg _ => { return Err(handle.response.not_supported(request)); } } } } impl GitAction { // BOOKMARK: fleshing this all out... fn is_ssh_repo(&self) -> bool { let result = self.repo.find("@").is_some() || self.repo.find("ssh://").is_some(); return result; } fn get_ssh_options_string(&self) -> String { let options = self.ssh_options.join(" "); if self.path.starts_with("http") { // http or https:// passwords are intentionally not supported, use a key instead, see docs return String::from("GIT_TERMINAL_PROMPT=0"); } else { let accept_keys = match self.accept_keys { true => String::from(" -o StrictHostKeyChecking=accept-new"), false => String::from("") }; return format!("GIT_SSH_COMMAND=\"ssh {}{}\" GIT_TERMINAL_PROMPT=0", options, accept_keys); } } fn get_local_version(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Option<String>, Arc<TaskResponse>> { let cmd = format!("git -C {} rev-parse HEAD", self.path); let result = handle.remote.run_unsafe(request, &cmd, CheckRc::Unchecked)?; let (rc, out) = cmd_info(&result); if rc == 0 { return Ok(Some(out.replace("\n",""))); } else { return Ok(None); } } fn get_remote_version(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<String, Arc<TaskResponse>> { let ssh_options = self.get_ssh_options_string(); let cmd = format!("{} git ls-remote {} | head -n 1 | cut -f 1", ssh_options, self.repo); let result = match self.is_ssh_repo() { true => handle.remote.run_forwardable(request, &cmd, CheckRc::Checked)?, false => handle.remote.run_unsafe(&request, &cmd, CheckRc::Checked)? }; let (_rc, out) = cmd_info(&result); return Ok(out); } fn pull(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<(), Arc<TaskResponse>> { let ssh_options = self.get_ssh_options_string(); let cmd = format!("{} git -C {} pull", ssh_options, self.path); match self.is_ssh_repo() { true => handle.remote.run_forwardable(request, &cmd, CheckRc::Checked)?, false => handle.remote.run_unsafe(&request, &cmd, CheckRc::Checked)? }; return Ok(()); } fn get_local_branch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<String, Arc<TaskResponse>> { let cmd = format!("git -C {} rev-parse --abbrev-ref HEAD", self.path); let result = handle.remote.run_unsafe(request, &cmd, CheckRc::Checked)?; let (_rc, out) = cmd_info(&result); return Ok(out); } fn clone(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<(),Arc<TaskResponse>> { let ssh_options = self.get_ssh_options_string(); handle.remote.create_directory(request, &self.path)?; let cmd = format!("{} git clone {} {}", ssh_options, self.repo, self.path); match self.is_ssh_repo() { true => handle.remote.run_forwardable(request, &cmd, CheckRc::Checked)?, false => handle.remote.run_unsafe(&request, &cmd, CheckRc::Checked)? }; return Ok(()); } fn switch_branch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<(), Arc<TaskResponse>> { let cmd = format!("git -C {} switch {}", self.path, self.branch); handle.remote.run_unsafe(request, &cmd, CheckRc::Checked)?; return Ok(()); } } // TODO: agent forwarding flag used by SSH connections // + make stuff work // + testing ssh and http repos without passwords // branch changes // etc0707010000003B000081A400000000000000000000000165135CC100000362000000000000000000000000000000000000002800000000jetporch-0.0.1/src/modules/files/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. /** ADD MODULES HERE, KEEP ALPHABETIZED **/ pub mod copy; pub mod directory; pub mod file; pub mod git; pub mod template;0707010000003C000081A400000000000000000000000165135CC1000014B7000000000000000000000000000000000000002D00000000jetporch-0.0.1/src/modules/files/template.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::TaskHandle; use crate::tasks::checksum::sha512; use crate::tasks::fields::Field; use std::path::{PathBuf}; //#[allow(unused_imports)] use serde::{Deserialize}; use std::sync::Arc; use std::vec::Vec; use crate::tasks::files::Recurse; const MODULE: &str = "template"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct TemplateTask { pub name: Option<String>, pub src: String, pub dest: String, pub attributes: Option<FileAttributesInput>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } struct TemplateAction { pub src: PathBuf, pub dest: String, pub attributes: Option<FileAttributesEvaluated>, } impl IsTask for TemplateTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { let src = handle.template.string(&request, tm, &String::from("src"), &self.src)?; return Ok( EvaluatedTask { action: Arc::new(TemplateAction { src: handle.template.find_template_path(request, tm, &String::from("src"), &src)?, dest: handle.template.path(&request, tm, &String::from("dest"), &self.dest)?, attributes: FileAttributesInput::template(&handle, &request, tm, &self.attributes)? }), with: Arc::new(PreLogicInput::template(&handle, &request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(&handle, &request, tm, &self.and)?), } ); } } impl IsAction for TemplateAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { let mut changes : Vec<Field> = Vec::new(); let remote_mode = handle.remote.query_common_file_attributes(request, &self.dest, &self.attributes, &mut changes, Recurse::No)?; if remote_mode.is_none() { return Ok(handle.response.needs_creation(request)); } let data = self.do_template(handle, request, false, None)?; let local_512 = sha512(&data); let remote_512 = handle.remote.get_sha512(request, &self.dest)?; if ! remote_512.eq(&local_512) { changes.push(Field::Content); } if ! changes.is_empty() { return Ok(handle.response.needs_modification(request, &changes)); } return Ok(handle.response.is_matched(request)); }, TaskRequestType::Create => { self.do_template(handle, request, true, None)?; return Ok(handle.response.is_created(request)); } TaskRequestType::Modify => { if request.changes.contains(&Field::Content) { self.do_template(handle, request, true, Some(request.changes.clone()))?; } else { handle.remote.process_common_file_attributes(request, &self.dest, &self.attributes, &request.changes, Recurse::No)?; } return Ok(handle.response.is_modified(request, request.changes.clone())); } _ => { return Err(handle.response.not_supported(request)); } } } } impl TemplateAction { pub fn do_template(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, write: bool, _changes: Option<Vec<Field>>) -> Result<String, Arc<TaskResponse>> { let template_contents = handle.local.read_file(&request, &self.src)?; let data = handle.template.string_for_template_module_use_only(&request, TemplateMode::Strict, &String::from("src"), &template_contents)?; if write { handle.remote.write_data(&request, &data, &self.dest, |f| { /* after save */ match handle.remote.process_all_common_file_attributes(request, &f, &self.attributes, Recurse::No) { Ok(_x) => Ok(()), Err(y) => Err(y) } })?; } return Ok(data); } } 0707010000003D000081A400000000000000000000000165135CC100000375000000000000000000000000000000000000002200000000jetporch-0.0.1/src/modules/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. /** ADD MODULE CATEGORIES HERE, KEEP ALPHABETIZED **/ pub mod commands; pub mod control; pub mod files; pub mod packages; pub mod services; 0707010000003E000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000002400000000jetporch-0.0.1/src/modules/packages0707010000003F000081A400000000000000000000000165135CC100001E62000000000000000000000000000000000000002B00000000jetporch-0.0.1/src/modules/packages/apt.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::{TaskHandle,CheckRc}; use crate::tasks::fields::Field; //#[allow(unused_imports)] use serde::{Deserialize}; use std::sync::Arc; use std::vec::Vec; const MODULE: &str = "apt"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct AptTask { pub name: Option<String>, pub package: String, pub version: Option<String>, pub update: Option<String>, pub remove: Option<String>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } struct AptAction { pub package: String, pub version: Option<String>, pub update: bool, pub remove: bool, } #[derive(Clone,PartialEq,Debug)] struct PackageDetails { name: String, version: String, } impl IsTask for AptTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(AptAction { package: handle.template.string_no_spaces(request, tm, &String::from("package"), &self.package)?, version: handle.template.string_option_no_spaces(&request, tm, &String::from("version"), &self.version)?, update: handle.template.boolean_option_default_false(&request, tm, &String::from("update"), &self.update)?, remove: handle.template.boolean_option_default_false(&request, tm, &String::from("remove"), &self.remove)? }), with: Arc::new(PreLogicInput::template(&handle, &request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(&handle, &request, tm, &self.and)?) } ); } } impl IsAction for AptAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { // FIXME: ALL of this query logic is shared between dnf and apt, but it is likely other package managers // will diverge. Still, consider a common function. let mut changes : Vec<Field> = Vec::new(); let package_details = self.get_package_details(handle, request)?; if package_details.is_some() { // package is installed if self.remove { return Ok(handle.response.needs_removal(request)); } let pkg = package_details.unwrap(); if self.update { changes.push(Field::Version); } else if self.version.is_some() { let specified_version = self.version.as_ref().unwrap(); if ! pkg.version.eq(specified_version) { changes.push(Field::Version); } } if changes.len() > 0 { return Ok(handle.response.needs_modification(request, &changes)); } else { return Ok(handle.response.is_matched(request)); } } else { // package is not installed return match self.remove { true => Ok(handle.response.is_matched(request)), false => Ok(handle.response.needs_creation(request)) } } }, TaskRequestType::Create => { self.install_package(handle, request)?; return Ok(handle.response.is_created(request)); } TaskRequestType::Modify => { if request.changes.contains(&Field::Version) { self.update_package(handle, request)?; } return Ok(handle.response.is_modified(request, request.changes.clone())); } TaskRequestType::Remove => { self.remove_package(handle, request)?; return Ok(handle.response.is_removed(request)); } _ => { return Err(handle.response.not_supported(request)); } } } } impl AptAction { pub fn get_package_details(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Option<PackageDetails>,Arc<TaskResponse>> { let cmd = format!("dpkg-query -W '{}'", self.package); let result = handle.remote.run(request, &cmd, CheckRc::Unchecked); if result.is_ok() { let (rc,out) = cmd_info(&result.unwrap()); if rc == 0 { let details = self.parse_package_details(handle, &out.clone())?; return Ok(details); } else { return Ok(None); } } else { return Err(result.unwrap()); } } pub fn parse_package_details(&self, _handle: &Arc<TaskHandle>, out: &String) -> Result<Option<PackageDetails>,Arc<TaskResponse>> { let mut tokens = out.split("\t"); let version = tokens.nth(1); if version.is_some() { return Ok(Some(PackageDetails { name: self.package.clone(), version: version.unwrap().trim().to_string() })); } else { // shouldn't occur with rc=0, still don't want to call panic. return Ok(None); } } pub fn install_package(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>,Arc<TaskResponse>>{ let cmd = match self.version.is_none() { true => format!("DEBIAN_FRONTEND=noninteractive apt-get install '{}' -qq", self.package), false => format!("DEBIAN_FRONTEND=noninteractive apt-get install '{}={}' -qq", self.package, self.version.as_ref().unwrap()) }; return handle.remote.run(request, &cmd, CheckRc::Checked); } pub fn update_package(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>,Arc<TaskResponse>>{ let cmd = match self.version.is_none() { true => format!("DEBIAN_FRONTEND=noninteractive apt-get install '{}' --only-upgrade -qq", self.package), false => format!("DEBIAN_FRONTEND=noninteractive apt-get install '{}={}' --only-upgrade -qq", self.package, self.version.as_ref().unwrap()) }; return handle.remote.run(request, &cmd, CheckRc::Checked); } pub fn remove_package(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>,Arc<TaskResponse>>{ let cmd = format!("DEBIAN_FRONTEND=noninteractive apt-get remove '{}' -qq", self.package); return handle.remote.run(request, &cmd, CheckRc::Checked); } } 07070100000040000081A400000000000000000000000165135CC100000332000000000000000000000000000000000000002B00000000jetporch-0.0.1/src/modules/packages/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. /** ADD MODULES HERE, KEEP ALPHABETIZED **/ pub mod apt; pub mod yum_dnf;07070100000041000081A400000000000000000000000165135CC10000258B000000000000000000000000000000000000002F00000000jetporch-0.0.1/src/modules/packages/yum_dnf.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::{TaskHandle,CheckRc}; use crate::tasks::fields::Field; use crate::inventory::hosts::PackagePreference; //#[allow(unused_imports)] use serde::{Deserialize}; use std::sync::Arc; use std::vec::Vec; const MODULE: &str = "yum_dnf"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct YumDnfTask { pub name: Option<String>, pub package: String, pub version: Option<String>, pub update: Option<String>, pub remove: Option<String>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } struct YumDnfAction { pub package: String, pub version: Option<String>, pub update: bool, pub remove: bool, } #[derive(Clone,PartialEq,Debug)] struct PackageDetails { name: String, version: String, } impl IsTask for YumDnfTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(YumDnfAction { package: handle.template.string_no_spaces(request, tm, &String::from("package"), &self.package)?, version: handle.template.string_option_no_spaces(&request, tm, &String::from("version"), &self.version)?, update: handle.template.boolean_option_default_false(&request, tm, &String::from("update"), &self.update)?, remove: handle.template.boolean_option_default_false(&request, tm, &String::from("remove"), &self.remove)? }), with: Arc::new(PreLogicInput::template(&handle, &request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(&handle, &request, tm, &self.and)?), } ); } } impl IsAction for YumDnfAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { // FIXME: ALL of this query logic is shared between dnf and apt, but it is likely other package managers // will diverge. Still, consider a common function. let mut changes : Vec<Field> = Vec::new(); self.set_package_preference(handle, request)?; let package_details = self.get_package_details(handle, request)?; if package_details.is_some() { // package is installed if self.remove { return Ok(handle.response.needs_removal(request)); } let pkg = package_details.unwrap(); if self.update { changes.push(Field::Version); } else if self.version.is_some() { let specified_version = self.version.as_ref().unwrap(); if ! pkg.version.eq(specified_version) { changes.push(Field::Version); } } if changes.len() > 0 { return Ok(handle.response.needs_modification(request, &changes)); } else { return Ok(handle.response.is_matched(request)); } } else { // package is not installed return match self.remove { true => Ok(handle.response.is_matched(request)), false => Ok(handle.response.needs_creation(request)) } } }, TaskRequestType::Create => { self.install_package(handle, request)?; return Ok(handle.response.is_created(request)); } TaskRequestType::Modify => { if request.changes.contains(&Field::Version) { self.update_package(handle, request)?; } return Ok(handle.response.is_modified(request, request.changes.clone())); } TaskRequestType::Remove => { self.remove_package(handle, request)?; return Ok(handle.response.is_removed(request)); } _ => { return Err(handle.response.not_supported(request)); } } } } impl YumDnfAction { pub fn set_package_preference(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<(),Arc<TaskResponse>> { if handle.host.read().unwrap().package_preference.is_some() { return Ok(()); } match handle.remote.get_mode(request, &String::from("/usr/bin/dnf"))? { Some(_) => { handle.host.write().unwrap().package_preference = Some(PackagePreference::Dnf); } None => match handle.remote.get_mode(request, &String::from("/usr/bin/yum"))? { Some(_) => { handle.host.write().unwrap().package_preference = Some(PackagePreference::Yum); } None => { return Err(handle.response.is_failed(request, &String::from("neither dnf nor yum detected"))); } } } Ok(()) } pub fn get_package_preference(&self, handle: &Arc<TaskHandle>) -> Option<PackagePreference> { handle.host.read().unwrap().package_preference } pub fn get_package_manager(&self, handle: &Arc<TaskHandle>) -> String { match self.get_package_preference(handle) { Some(PackagePreference::Yum) => String::from("yum"), Some(PackagePreference::Dnf) => String::from("dnf"), _ => { panic!("internal error, package preference not set correctly"); } } } pub fn get_package_details(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Option<PackageDetails>,Arc<TaskResponse>> { let which = self.get_package_manager(handle); let cmd = match self.version.is_none() { true => format!("{} info {}", which, self.package), false => format!("{} info {}-{}", which, self.package, self.version.as_ref().unwrap()) }; let result = handle.remote.run(request, &cmd, CheckRc::Unchecked)?; let (_rc,out) = cmd_info(&result); let details = self.parse_package_details(&out.clone())?; return Ok(details); } pub fn parse_package_details(&self, out: &String) -> Result<Option<PackageDetails>,Arc<TaskResponse>> { let mut name: Option<String> = None; let mut version: Option<String> = None; for line in out.lines() { if line.starts_with("Available") { return Ok(None); } let mut tokens = line.split(":"); let key = tokens.nth(0); let value = tokens.nth(0); if key.is_some() && value.is_some() { let key2 = key.unwrap().trim(); let value2 = value.unwrap().trim(); if key2.eq("Name") { name = Some(value2.to_string()); } if key2.eq("Version") { version = Some(value2.to_string()); break; } } } if name.is_some() && version.is_some() { return Ok(Some(PackageDetails { name: name.unwrap().clone(), version: version.unwrap().clone() })); } else { return Ok(None); } } pub fn install_package(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>,Arc<TaskResponse>>{ let which = self.get_package_manager(handle); let cmd = match self.version.is_none() { true => format!("{} install '{}' -y", which, self.package), false => format!("{}f install '{}-{}' -y", which, self.package, self.version.as_ref().unwrap()) }; return handle.remote.run(request, &cmd, CheckRc::Checked); } pub fn update_package(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>,Arc<TaskResponse>>{ let which = self.get_package_manager(handle); let cmd = format!("{} update '{}' -y", which, self.package); return handle.remote.run(request, &cmd, CheckRc::Checked); } pub fn remove_package(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>,Arc<TaskResponse>>{ let which = self.get_package_manager(handle); let cmd = format!("{} remove '{}' -y", which, self.package); return handle.remote.run(request, &cmd, CheckRc::Checked); } } 07070100000042000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000002400000000jetporch-0.0.1/src/modules/services07070100000043000081A400000000000000000000000165135CC100000328000000000000000000000000000000000000002B00000000jetporch-0.0.1/src/modules/services/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. /** ADD MODULES HERE, KEEP ALPHABETIZED **/ pub mod sd_service;07070100000044000081A400000000000000000000000165135CC100001FD1000000000000000000000000000000000000003200000000jetporch-0.0.1/src/modules/services/sd_service.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::tasks::*; use crate::handle::handle::{TaskHandle,CheckRc}; use crate::tasks::fields::Field; //#[allow(unused_imports)] use serde::{Deserialize}; use std::sync::Arc; use std::vec::Vec; const MODULE: &str = "sd_service"; #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct SystemdServiceTask { pub name: Option<String>, pub service: String, pub enabled: Option<String>, pub started: Option<String>, pub restart: Option<String>, pub with: Option<PreLogicInput>, pub and: Option<PostLogicInput> } struct SystemdServiceAction { pub service: String, pub enabled: Option<bool>, pub started: Option<bool>, pub restart: bool, } #[derive(Clone,PartialEq,Debug)] struct ServiceDetails { enabled: bool, started: bool, } impl IsTask for SystemdServiceTask { fn get_module(&self) -> String { String::from(MODULE) } fn get_name(&self) -> Option<String> { self.name.clone() } fn get_with(&self) -> Option<PreLogicInput> { self.with.clone() } fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { return Ok( EvaluatedTask { action: Arc::new(SystemdServiceAction { service: handle.template.string_no_spaces(request, tm, &String::from("service"), &self.service)?, enabled: handle.template.boolean_option_default_none(&request, tm, &String::from("enabled"), &self.enabled)?, started: handle.template.boolean_option_default_none(&request, tm, &String::from("started"), &self.started)?, restart: handle.template.boolean_option_default_false(&request, tm, &String::from("restart"), &self.restart)? }), with: Arc::new(PreLogicInput::template(&handle, &request, tm, &self.with)?), and: Arc::new(PostLogicInput::template(&handle, &request, tm, &self.and)?) } ); } } impl IsAction for SystemdServiceAction { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>> { match request.request_type { TaskRequestType::Query => { let mut changes : Vec<Field> = Vec::new(); let actual = self.get_service_details(handle, request)?; match (actual.enabled, self.enabled) { (true, Some(false)) => { changes.push(Field::Disable); }, (false, Some(true)) => { changes.push(Field::Enable); }, _ => {} }; match (actual.started, self.started, self.restart) { (_, Some(false), true) => { return Err(handle.response.is_failed(request, &String::from("started:false and restart:true conflict"))); }, (true, Some(true), true) => { changes.push(Field::Restart); }, (true, None, true) => { changes.push(Field::Restart); /* a little weird, but we know what you mean */ }, (false, None, true) => { changes.push(Field::Start); /* a little weird, but we know what you mean */ }, (false, Some(true), _) => { changes.push(Field::Start); }, (true, Some(false), false) => { changes.push(Field::Stop); }, _ => { }, }; if changes.len() > 0 { return Ok(handle.response.needs_modification(request, &changes)); } else { return Ok(handle.response.is_matched(request)); } }, TaskRequestType::Modify => { if request.changes.contains(&Field::Start) { self.do_start(handle, request)?; } else if request.changes.contains(&Field::Stop) { self.do_stop(handle, request)?; } else if request.changes.contains(&Field::Restart) { self.do_restart(handle, request)?; } if request.changes.contains(&Field::Enable) { self.do_enable(handle, request)?; } else if request.changes.contains(&Field::Disable) { self.do_disable(handle, request)?; } return Ok(handle.response.is_modified(request, request.changes.clone())); } _ => { return Err(handle.response.not_supported(request)); } } } } impl SystemdServiceAction { pub fn get_service_details(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<ServiceDetails,Arc<TaskResponse>> { let is_enabled : bool; let is_active : bool; let is_enabled_cmd = format!("systemctl is-enabled '{}'", self.service); let is_active_cmd = format!("systemctl is-active '{}'", self.service); let result = handle.remote.run(request, &is_enabled_cmd, CheckRc::Unchecked)?; let (_rc,out) = cmd_info(&result); if out.find("disabled").is_some() || out.find("deactivating").is_some() { is_enabled = false; } else if out.find("enabled").is_some() || out.find("alias").is_some() { is_enabled = true; } else { return Err(handle.response.is_failed(request, &format!("systemctl enablement status unexpected for service({}): ({})", self.service, out))); } let result2 = handle.remote.run(request, &is_active_cmd, CheckRc::Unchecked)?; let (_rc2,out2) = cmd_info(&result2); if out2.find("inactive").is_some() { is_active = false; } else if out2.find("active").is_some() { is_active = true; } else { return Err(handle.response.is_failed(request, &format!("systemctl activity status unexpected for service({}): {}", self.service, out2))); } return Ok(ServiceDetails { enabled: is_enabled, started: is_active, }); } pub fn do_start(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let cmd = format!("systemctl start '{}'", self.service); return handle.remote.run(request, &cmd, CheckRc::Checked); } pub fn do_stop(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let cmd = format!("systemctl stop '{}'", self.service); return handle.remote.run(request, &cmd, CheckRc::Checked); } pub fn do_enable(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let cmd = format!("systemctl enable '{}'", self.service); return handle.remote.run(request, &cmd, CheckRc::Checked); } pub fn do_disable(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let cmd = format!("systemctl disable '{}'", self.service); return handle.remote.run(request, &cmd, CheckRc::Checked); } pub fn do_restart(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let cmd = format!("systemctl restart '{}'", self.service); return handle.remote.run(request, &cmd, CheckRc::Checked); } } 07070100000045000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001D00000000jetporch-0.0.1/src/playbooks07070100000046000081A400000000000000000000000165135CC100004DDF000000000000000000000000000000000000002800000000jetporch-0.0.1/src/playbooks/context.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::util::io::{path_as_string,directory_as_string}; use crate::playbooks::language::{Play,Role,RoleInvocation}; use std::path::PathBuf; use std::collections::HashMap; use crate::inventory::hosts::Host; use std::sync::{Arc,RwLock}; use crate::connection::cache::ConnectionCache; use crate::registry::list::Task; use crate::util::yaml::blend_variables; use crate::playbooks::templar::{Templar,TemplateMode}; use crate::cli::parser::CliParser; use crate::handle::template::BlendTarget; use std::ops::Deref; use std::env; use guid_create::GUID; // the playbook traversal state, and a little bit more than that. // the playbook context keeps track of where we are in a playbook // execution and various results/stats along the way. pub struct PlaybookContext { pub verbosity: u32, pub playbook_path: Option<String>, pub playbook_directory: Option<String>, pub play: Option<String>, pub role: Option<Role>, pub role_path: Option<String>, pub play_count: usize, pub role_count: usize, pub task_count: usize, pub task: Option<String>, seen_hosts: HashMap<String, Arc<RwLock<Host>>>, targetted_hosts: HashMap<String, Arc<RwLock<Host>>>, failed_hosts: HashMap<String, Arc<RwLock<Host>>>, attempted_count_for_host: HashMap<String, usize>, adjusted_count_for_host: HashMap<String, usize>, created_count_for_host: HashMap<String, usize>, removed_count_for_host: HashMap<String, usize>, modified_count_for_host: HashMap<String, usize>, executed_count_for_host: HashMap<String, usize>, passive_count_for_host: HashMap<String, usize>, matched_count_for_host: HashMap<String, usize>, skipped_count_for_host: HashMap<String, usize>, failed_count_for_host: HashMap<String, usize>, // TODO: some of these don't need to be pub. pub failed_tasks: usize, pub defaults_storage: RwLock<serde_yaml::Mapping>, pub vars_storage: RwLock<serde_yaml::Mapping>, pub role_defaults_storage: RwLock<serde_yaml::Mapping>, pub role_vars_storage: RwLock<serde_yaml::Mapping>, pub env_storage: RwLock<serde_yaml::Mapping>, pub connection_cache: RwLock<ConnectionCache>, pub templar: RwLock<Templar>, pub ssh_user: String, pub ssh_port: i64, pub sudo: Option<String>, extra_vars: serde_yaml::Value, } impl PlaybookContext { pub fn new(parser: &CliParser) -> Self { let mut s = Self { verbosity: parser.verbosity, playbook_path: None, playbook_directory: None, failed_tasks: 0, play: None, role: None, task: None, play_count : 0, role_count : 0, task_count : 0, seen_hosts: HashMap::new(), targetted_hosts: HashMap::new(), failed_hosts: HashMap::new(), role_path: None, adjusted_count_for_host: HashMap::new(), attempted_count_for_host: HashMap::new(), created_count_for_host: HashMap::new(), removed_count_for_host: HashMap::new(), modified_count_for_host: HashMap::new(), executed_count_for_host: HashMap::new(), passive_count_for_host: HashMap::new(), matched_count_for_host: HashMap::new(), failed_count_for_host: HashMap::new(), skipped_count_for_host: HashMap::new(), connection_cache: RwLock::new(ConnectionCache::new()), templar: RwLock::new(Templar::new()), defaults_storage: RwLock::new(serde_yaml::Mapping::new()), vars_storage: RwLock::new(serde_yaml::Mapping::new()), role_vars_storage: RwLock::new(serde_yaml::Mapping::new()), role_defaults_storage: RwLock::new(serde_yaml::Mapping::new()), env_storage: RwLock::new(serde_yaml::Mapping::new()), ssh_user: parser.default_user.clone(), ssh_port: parser.default_port, sudo: parser.sudo.clone(), extra_vars: parser.extra_vars.clone(), }; s.load_environment(); return s; } // the remaining hosts in a play are those that have not failed yet // other functions remove these hosts from the list. pub fn get_remaining_hosts(&self) -> HashMap<String, Arc<RwLock<Host>>> { let mut results : HashMap<String, Arc<RwLock<Host>>> = HashMap::new(); for (k,v) in self.targetted_hosts.iter() { results.insert(k.clone(), Arc::clone(&v)); } return results; } // SSH details are set in traversal and may come from the playbook or // or CLI options. These values are not guaranteed to be used as magic // variables could still exist in inventory for particular hosts pub fn set_ssh_user(&mut self, ssh_user: &String) { self.ssh_user = ssh_user.clone(); } pub fn set_ssh_port(&mut self, ssh_port: i64) { self.ssh_port = ssh_port; } // used in traversal to tell the context what the current set of possible // hosts is. pub fn set_targetted_hosts(&mut self, hosts: &Vec<Arc<RwLock<Host>>>) { self.targetted_hosts.clear(); for host in hosts.iter() { let hostname = host.read().unwrap().name.clone(); match self.failed_hosts.contains_key(&hostname) { true => {}, false => { self.seen_hosts.insert(hostname.clone(), Arc::clone(&host)); self.targetted_hosts.insert(hostname.clone(), Arc::clone(&host)); } } } } // called when a host returns an unacceptable final response. removes // the host from the targetted pool for the play. when no hosts // remain the entire play will fail. pub fn fail_host(&mut self, host: &Arc<RwLock<Host>>) { let host2 = host.read().unwrap(); let hostname = host2.name.clone(); self.failed_tasks = self.failed_tasks + 1; self.targetted_hosts.remove(&hostname); self.failed_hosts.insert(hostname.clone(), Arc::clone(&host)); } pub fn set_playbook_path(&mut self, path: &PathBuf) { self.playbook_path = Some(path_as_string(&path)); self.playbook_directory = Some(directory_as_string(&path)); } pub fn set_task(&mut self, task: &Task) { self.task = Some(task.get_display_name()); } pub fn set_play(&mut self, play: &Play) { self.play = Some(play.name.clone()); self.play_count = self.play_count + 1; } pub fn get_play_name(&self) -> String { return match &self.play { Some(x) => x.clone(), None => panic!("attempting to read a play name before plays have been evaluated") } } pub fn set_role(&mut self, role: &Role, invocation: &RoleInvocation, role_path: &String) { self.role = Some(role.clone()); self.role_path = Some(role_path.clone()); if role.defaults.is_some() { *self.role_defaults_storage.write().unwrap() = role.defaults.as_ref().unwrap().clone(); } if invocation.vars.is_some() { *self.role_vars_storage.write().unwrap() = invocation.vars.as_ref().unwrap().clone(); } } pub fn unset_role(&mut self) { self.role = None; self.role_path = None; self.role_defaults_storage.write().unwrap().clear(); self.role_vars_storage.write().unwrap().clear(); } // template functions need to access all the variables about a host taking variable precendence rules into effect // to get a dictionary of variables to use in template expressions pub fn get_complete_blended_variables(&self, host: &Arc<RwLock<Host>>, blend_target: BlendTarget) -> serde_yaml::Mapping { let blended = self.get_complete_blended_variables_as_value(host, blend_target); return match blended { serde_yaml::Value::Mapping(x) => x, _ => panic!("unexpected, get_blended_variables produced a non-mapping (3)") }; } pub fn get_complete_blended_variables_as_value(&self, host: &Arc<RwLock<Host>>, blend_target: BlendTarget) -> serde_yaml::Value { let mut blended = serde_yaml::Value::from(serde_yaml::Mapping::new()); let src1 = self.defaults_storage.read().unwrap(); let src1a = src1.deref(); blend_variables(&mut blended, serde_yaml::Value::Mapping(src1a.clone())); let src1r = self.role_defaults_storage.read().unwrap(); let src1ar = src1r.deref(); blend_variables(&mut blended, serde_yaml::Value::Mapping(src1ar.clone())); let src2 = host.read().unwrap().get_blended_variables(); blend_variables(&mut blended, serde_yaml::Value::Mapping(src2)); let src3 = self.vars_storage.read().unwrap(); let src3a = src3.deref(); blend_variables(&mut blended, serde_yaml::Value::Mapping(src3a.clone())); let src3r = self.role_vars_storage.read().unwrap(); let src3ar = src3r.deref(); blend_variables(&mut blended, serde_yaml::Value::Mapping(src3ar.clone())); blend_variables(&mut blended, self.extra_vars.clone()); match blend_target { BlendTarget::NotTemplateModule => { }, BlendTarget::TemplateModule => { // for security reasons env vars from security tools like 'op run' are only exposed to the template module // to prevent accidental leakage into logs and history let src4 = self.env_storage.read().unwrap(); let src4a = src4.deref(); blend_variables(&mut blended, serde_yaml::Value::Mapping(src4a.clone())); } }; return blended; } // template code is not used here directly, but in handle/template.rs, which passes back through here, since // only the context knows all the variables from the playbook traversal to fill in and how to blend // variables in the correct order. pub fn render_template(&self, template: &String, host: &Arc<RwLock<Host>>, blend_target: BlendTarget, template_mode: TemplateMode) -> Result<String,String> { let vars = self.get_complete_blended_variables(host, blend_target); return self.templar.read().unwrap().render(template, vars, template_mode); } // testing conditions for truthiness works much like templating strings pub fn test_condition(&self, expr: &String, host: &Arc<RwLock<Host>>, tm: TemplateMode) -> Result<bool,String> { let vars = self.get_complete_blended_variables(host, BlendTarget::NotTemplateModule); return self.templar.read().unwrap().test_condition(expr, vars, tm); } // a version of template evaluation that allows some additional variables, for example from a module pub fn test_condition_with_extra_data(&self, expr: &String, host: &Arc<RwLock<Host>>, vars_input: serde_yaml::Mapping, tm: TemplateMode) -> Result<bool,String> { let mut vars = self.get_complete_blended_variables_as_value(host, BlendTarget::NotTemplateModule); blend_variables(&mut vars, serde_yaml::Value::Mapping(vars_input)); return match vars { serde_yaml::Value::Mapping(x) => self.templar.read().unwrap().test_condition(expr, x, tm), _ => { panic!("impossible input to test_condition"); } }; } // when a host needs to connect over SSH it asks this function - we can use some settings configured // already on the context or check some variables in inventory. pub fn get_ssh_connection_details(&self, host: &Arc<RwLock<Host>>) -> (String,String,i64) { let vars = self.get_complete_blended_variables(host,BlendTarget::NotTemplateModule); let host2 = host.read().unwrap(); let remote_hostname = match vars.contains_key(&String::from("jet_ssh_hostname")) { true => match vars.get(&String::from("jet_ssh_hostname")).unwrap().as_str() { Some(x) => String::from(x), None => host2.name.clone() }, false => host2.name.clone() }; let remote_user = match vars.contains_key(&String::from("jet_ssh_user")) { true => match vars.get(&String::from("jet_ssh_user")).unwrap().as_str() { Some(x) => String::from(x), None => self.ssh_user.clone() }, false => self.ssh_user.clone() }; let remote_port = match vars.contains_key(&String::from("jet_ssh_port")) { true => match vars.get(&String::from("jet_ssh_port")).unwrap().as_i64() { Some(x) => { x }, None => { self.ssh_port } }, false => { self.ssh_port } }; return (remote_hostname, remote_user, remote_port) } // loads environment variables into the context, adding an "ENV_foo" prefix // to each environment variable "foo". These variables will only be made available // to the template module since we use them for secret management features. pub fn load_environment(&mut self) { let mut my_env = self.env_storage.write().unwrap(); // some common environment variables that may occur are not useful for playbooks // or they have no need to share that with other hosts let do_not_load = vec![ "OLDPWD", "PWD", "SHLVL", "SSH_AUTH_SOCK", "SSH_AGENT_PID", "TERM_SESSION_ID", "XPC_FLAGS", "XPC_SERVICE_NAME", "_" ]; for (k,v) in env::vars() { if ! do_not_load.contains(&k.as_str()) { my_env.insert(serde_yaml::Value::String(format!("ENV_{k}")) , serde_yaml::Value::String(v)); } } } // various functions in Jet make use of GUIDs, for example for temp file locations pub fn get_guid(&self) -> String { return GUID::rand().to_string(); } // ================================================================================== // STATISTICS pub fn get_role_count(&self) -> usize { return self.role_count; } pub fn increment_role_count(&mut self) { self.role_count = self.role_count + 1; } pub fn get_task_count(&self) -> usize { return self.task_count; } pub fn increment_task_count(&mut self) { self.task_count = self.task_count + 1; } pub fn increment_attempted_for_host(&mut self, host: &String) { *self.attempted_count_for_host.entry(host.clone()).or_insert(0) += 1; } pub fn increment_created_for_host(&mut self, host: &String) { *self.created_count_for_host.entry(host.clone()).or_insert(0) += 1; *self.adjusted_count_for_host.entry(host.clone()).or_insert(0) += 1; } pub fn increment_removed_for_host(&mut self, host: &String) { *self.removed_count_for_host.entry(host.clone()).or_insert(0) += 1; *self.adjusted_count_for_host.entry(host.clone()).or_insert(0) += 1; } pub fn increment_modified_for_host(&mut self, host: &String) { *self.modified_count_for_host.entry(host.clone()).or_insert(0) += 1; *self.adjusted_count_for_host.entry(host.clone()).or_insert(0) += 1; } pub fn increment_executed_for_host(&mut self, host: &String) { *self.executed_count_for_host.entry(host.clone()).or_insert(0) += 1; *self.adjusted_count_for_host.entry(host.clone()).or_insert(0) += 1; } pub fn increment_failed_for_host(&mut self, host: &String) { *self.failed_count_for_host.entry(host.clone()).or_insert(0) += 1; } pub fn increment_passive_for_host(&mut self, host: &String) { *self.passive_count_for_host.entry(host.clone()).or_insert(0) += 1; } pub fn increment_matched_for_host(&mut self, host: &String) { *self.matched_count_for_host.entry(host.clone()).or_insert(0) += 1; } pub fn increment_skipped_for_host(&mut self, host: &String) { *self.skipped_count_for_host.entry(host.clone()).or_insert(0) += 1; } pub fn get_total_attempted_count(&self) -> usize { return self.attempted_count_for_host.values().fold(0, |ttl, &x| ttl + x); } pub fn get_total_creation_count(&self) -> usize { return self.created_count_for_host.values().fold(0, |ttl, &x| ttl + x); } pub fn get_total_modified_count(&self) -> usize{ return self.modified_count_for_host.values().fold(0, |ttl, &x| ttl + x); } pub fn get_total_removal_count(&self) -> usize{ return self.removed_count_for_host.values().fold(0, |ttl, &x| ttl + x); } pub fn get_total_executions_count(&self) -> usize { return self.executed_count_for_host.values().fold(0, |ttl, &x| ttl + x); } pub fn get_total_failed_count(&self) -> usize{ return self.failed_count_for_host.values().fold(0, |ttl, &x| ttl + x); } pub fn get_total_adjusted_count(&self) -> usize { return self.adjusted_count_for_host.values().fold(0, |ttl, &x| ttl + x); } pub fn get_total_passive_count(&self) -> usize { return self.passive_count_for_host.values().fold(0, |ttl, &x| ttl + x); } pub fn get_total_matched_count(&self) -> usize { return self.matched_count_for_host.values().fold(0, |ttl, &x| ttl + x); } pub fn get_total_skipped_count(&self) -> usize { return self.skipped_count_for_host.values().fold(0, |ttl, &x| ttl + x); } pub fn get_hosts_creation_count(&self) -> usize { return self.created_count_for_host.keys().len(); } pub fn get_hosts_modified_count(&self) -> usize { return self.modified_count_for_host.keys().len(); } pub fn get_hosts_removal_count(&self) -> usize { return self.removed_count_for_host.keys().len(); } pub fn get_hosts_executions_count(&self) -> usize { return self.executed_count_for_host.keys().len(); } pub fn get_hosts_passive_count(&self) -> usize { return self.passive_count_for_host.keys().len(); } pub fn get_hosts_matched_count(&self) -> usize { return self.matched_count_for_host.keys().len(); } pub fn get_hosts_skipped_count(&self) -> usize { return self.skipped_count_for_host.keys().len(); } pub fn get_hosts_failed_count(&self) -> usize { return self.failed_count_for_host.keys().len(); } pub fn get_hosts_adjusted_count(&self) -> usize { return self.adjusted_count_for_host.keys().len(); } pub fn get_hosts_seen_count(&self) -> usize { return self.seen_hosts.keys().len(); } }07070100000047000081A400000000000000000000000165135CC100000754000000000000000000000000000000000000002900000000jetporch-0.0.1/src/playbooks/language.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use serde::{Deserialize}; use crate::registry::list::Task; // all the playbook language YAML structures! #[derive(Debug,Deserialize)] #[serde(deny_unknown_fields)] pub struct Play { pub name : String, pub groups : Vec<String>, pub roles : Option<Vec<RoleInvocation>>, pub defaults: Option<serde_yaml::Mapping>, pub vars : Option<serde_yaml::Mapping>, pub vars_files: Option<Vec<String>>, pub sudo: Option<String>, pub sudo_template: Option<String>, pub ssh_user : Option<String>, pub ssh_port : Option<i64>, pub tasks : Option<Vec<Task>>, pub handlers : Option<Vec<Task>>, pub batch_size : Option<usize>, } #[derive(Debug,Deserialize,Clone)] #[serde(deny_unknown_fields)] pub struct Role { pub name: String, pub defaults: Option<serde_yaml::Mapping>, pub tasks: Option<Vec<String>>, pub handlers: Option<Vec<String>> } #[derive(Debug,Deserialize)] #[serde(deny_unknown_fields)] pub struct RoleInvocation { pub role: String, pub vars: Option<serde_yaml::Mapping>, pub tags: Option<Vec<String>> } // for Task/module definitions see registry/list.rs 07070100000048000081A400000000000000000000000165135CC100000351000000000000000000000000000000000000002400000000jetporch-0.0.1/src/playbooks/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. pub mod language; pub mod context; pub mod visitor; pub mod traversal; pub mod templar; pub mod task_fsm;07070100000049000081A400000000000000000000000165135CC10000587F000000000000000000000000000000000000002900000000jetporch-0.0.1/src/playbooks/task_fsm.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::registry::list::Task; use crate::connection::connection::Connection; use crate::handle::handle::TaskHandle; use crate::playbooks::traversal::RunState; use crate::inventory::hosts::Host; use crate::playbooks::traversal::HandlerMode; use crate::playbooks::language::Play; use crate::tasks::request::SudoDetails; use crate::tasks::*; use crate::handle::template::BlendTarget; use crate::playbooks::templar::TemplateMode; use crate::tasks::logic::template_items; use std::sync::{Arc,RwLock,Mutex}; use std::collections::HashMap; use rayon::prelude::*; use std::{thread, time}; // this module contains the guts of running tasks inside per-host threads // while the actual core finite state machine is not terribly complicated // various logical constructs in the language tend to cause lots of exceptions // // FIXME: this will be gradually refactored over time pub fn fsm_run_task(run_state: &Arc<RunState>, play: &Play, task: &Task, are_handlers: HandlerMode) -> Result<(), String> { // if running in check mode various functions will short circuit early let check = run_state.visitor.read().unwrap().is_check_mode(); // the hosts to configure are not those specified in the batch but the subset of those that have not yet failed let hosts : HashMap<String, Arc<RwLock<Host>>> = run_state.context.read().unwrap().get_remaining_hosts(); let mut host_objects : Vec<Arc<RwLock<Host>>> = Vec::new(); for (_,v) in hosts { host_objects.push(Arc::clone(&v)); } // use rayon to process hosts in different threads let _total : i64 = host_objects.par_iter().map(|host| { // get the connection to each host, which should be left open until the play ends let connection_result = run_state.connection_factory.read().unwrap().get_connection(&run_state.context, &host); match connection_result { Ok(_) => { let connection = connection_result.unwrap(); run_state.visitor.read().unwrap().on_host_task_start(&run_state.context, &host); // the actual task is invoked here let task_response = run_task_on_host(&run_state,connection,&host,play,task,are_handlers); match task_response { Ok(x) => { match check { // output slightly differs in check vs non-check modes false => run_state.visitor.read().unwrap().on_host_task_ok(&run_state.context, &x, &host), true => run_state.visitor.read().unwrap().on_host_task_check_ok(&run_state.context, &x, &host) } } Err(x) => { // hosts with task failures are removed from the pool run_state.context.write().unwrap().fail_host(&host); run_state.visitor.read().unwrap().on_host_task_failed(&run_state.context, &x, &host); }, } }, Err(x) => { // hosts with connection failures are removed from the pool run_state.visitor.read().unwrap().debug_host(&host, &x); run_state.context.write().unwrap().fail_host(&host); run_state.visitor.read().unwrap().on_host_connect_failed(&run_state.context, &host); } } // rayon needs some math to add up, hence the 1. It seems to short-circuit without some work to do. return 1; }).sum(); return Ok(()); } fn get_actual_connection(run_state: &Arc<RunState>, host: &Arc<RwLock<Host>>, task: &Task, input_connection: Arc<Mutex<dyn Connection>>) -> Result<(Option<String>,Arc<Mutex<dyn Connection>>), String> { // usually the connection we already have is the one we will use, but this is not the case for using the delegate_to feature // this is a bit complex... return match task.get_with() { // if the task has a with section then the task might be delegated Some(task_with) => match task_with.delegate_to { // we have found the delegate_to keyword Some(pre_delegate) => { // we need to store the variable 'delegate_host' into the host's facts storage so it can be used in module parameters. let hn = host.read().unwrap().name.clone(); let mut mapping = serde_yaml::Mapping::new(); mapping.insert(serde_yaml::Value::String(String::from("delegate_host")), serde_yaml::Value::String(hn.clone())); host.write().unwrap().update_facts2(mapping); // the delegate_to parameter could be a variable let delegate = run_state.context.read().unwrap().render_template(&pre_delegate, host, BlendTarget::NotTemplateModule, TemplateMode::Strict)?; if delegate.eq(&hn.clone()) { // delegating to the same host will deadlock since the connection is wrapped in a mutex, // so just return the original connection if that is requested return Ok((None, input_connection)) } else if delegate.eq(&String::from("localhost")) { // localhost delegation has some security implications (see docs) so require a CLI flag for access if run_state.allow_localhost_delegation { return Ok((Some(delegate.clone()), run_state.connection_factory.read().unwrap().get_local_connection(&run_state.context)?)) } else { return Err(format!("localhost delegation has potential security implementations, pass --allow-localhost-delegation to sign off")); } } else { // with some pre-checks out of the way, allow delegation to the host if it's in inventory let has_host = run_state.inventory.read().unwrap().has_host(&delegate); if ! has_host { return Err(format!("cannot delegate to a host not found in inventory: {}", delegate)); } let host = run_state.inventory.read().unwrap().get_host(&delegate); return Ok((Some(delegate.clone()), run_state.connection_factory.read().unwrap().get_connection(&run_state.context, &host)?)); } }, // there was no delegate keyword, use the original connection None => Ok((None, input_connection)) }, // there was no 'with' block, use teh original connection None => Ok((None, input_connection)) }; } fn run_task_on_host( run_state: &Arc<RunState>, input_connection: Arc<Mutex<dyn Connection>>, host: &Arc<RwLock<Host>>, play: &Play, task: &Task, are_handlers: HandlerMode) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { // to run a task we must first validate the object, which renders the YAML inputs into versions where the program // has applied more pre-processing let validate = TaskRequest::validate(); // consider the use of the delegate_to keyword, if provided let gac_result = get_actual_connection(run_state, host, task, Arc::clone(&input_connection)); let (delegated, connection, handle) = match gac_result { // construct the TaskHandle if the original connection is to be used Ok((None, ref conn)) => (None, conn, Arc::new(TaskHandle::new(Arc::clone(run_state), Arc::clone(conn), Arc::clone(host)))), // construct the TaskHandle if a delegate connection is to be used Ok((Some(delegate), ref conn)) => (Some(delegate.clone()), conn, Arc::new(TaskHandle::new(Arc::clone(run_state), Arc::clone(conn), Arc::clone(host)))), // something went wrong when processing delegates, create a throw-away handle just so we can use the response functions Err(msg) => { let tmp_handle = Arc::new(TaskHandle::new(Arc::clone(run_state), Arc::clone(&input_connection), Arc::clone(host))); return Err(tmp_handle.response.is_failed(&validate, &msg)); } }; // if we are delegating, tell the user if delegated.is_some() { run_state.visitor.read().unwrap().on_host_delegate(host, &delegated.unwrap()); } // process the YAML inputs of the task and turn them into something we can use // initially we run this in 'template off' mode which returns basically junk // but allows us to get the 'items' data off the collection. let evaluated = task.evaluate(&handle, &validate, TemplateMode::Off)?; // see if we are iterating over a list of items or not let items_input = match evaluated.with.is_some() { true => &evaluated.with.as_ref().as_ref().unwrap().items, false => &None }; // mapping to store the 'item' variable when using 'with_items' let mut mapping = serde_yaml::Mapping::new(); // storing the last result of the items loop so we always have something to return // if a failure occurs it will be returned immediately let mut last : Option<Result<Arc<TaskResponse>,Arc<TaskResponse>>> = None; // even if we are not iterating over a list of items, make a list of one item to simplify the logic let evaluated_items = template_items(&handle, &validate, TemplateMode::Strict, &items_input)?; // walking over each item or just the single task if 'with_items' was not used for item in evaluated_items.iter() { // store the 'items' variable for use in module parameters mapping.insert(serde_yaml::Value::String(String::from("item")), item.clone()); host.write().unwrap().update_facts2(mapping.clone()); // re-evaluate the task, allowing the 'items' to be plugged in. let evaluated = task.evaluate(&handle, &validate, TemplateMode::Strict)?; // see if there is any retry or delay logic in the task let mut retries = match evaluated.and.as_ref().is_some() { false => 0, true => evaluated.and.as_ref().as_ref().unwrap().retry }; let delay = match evaluated.and.as_ref().is_some() { false => 1, true => evaluated.and.as_ref().as_ref().unwrap().delay }; // run the task as many times as defined by retry logic loop { // here we finally call the actual task, everything around this is just support // for delegation, loops, and retries! match run_task_on_host_inner(run_state, &connection, host, play, task, are_handlers, &handle, &validate, &evaluated) { Err(e) => match retries { // retries are used up 0 => { return Err(e); }, // we have retries left _ => { retries = retries - 1; run_state.visitor.read().unwrap().on_host_task_retry(&run_state.context, host, retries, delay); if delay > 0 { let duration = time::Duration::from_secs(delay); thread::sleep(duration); } } }, Ok(x) => { last = Some(Ok(x)); break } } } } // looping over a list of no items should be impossible unless someone passed in a variable that was // an empty list if last.is_some() { return last.unwrap(); } else { return Err(handle.response.is_failed(&validate, &String::from("with/items contained no entries"))); } } // the "on this host" method body from _task fn run_task_on_host_inner( run_state: &Arc<RunState>, _connection: &Arc<Mutex<dyn Connection>>, host: &Arc<RwLock<Host>>, play: &Play, _task: &Task, are_handlers: HandlerMode, handle: &Arc<TaskHandle>, validate: &Arc<TaskRequest>, evaluated: &EvaluatedTask) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> { let play_count = run_state.context.read().unwrap().play_count; let modify_mode = ! run_state.visitor.read().unwrap().is_check_mode(); // access any pre and post-task modifier logic let action = &evaluated.action; let pre_logic = &evaluated.with; let post_logic = &evaluated.and; // get the sudo settings from the play if available, if not see if they were set from the CLI let mut sudo : Option<String> = match play.sudo.is_some() { true => play.sudo.clone(), // minor FIXME: parameters like this are usually set on the run_state false => run_state.context.read().unwrap().sudo.clone() }; // see if the sudo template is configured, if not use the most basic default let sudo_template = match &play.sudo_template { None => String::from("/usr/bin/sudo -u '{{jet_sudo_user}}' {{jet_command}}"), Some(x) => x.clone() }; // is 'with' provided? if pre_logic.is_some() { let logic = pre_logic.as_ref().as_ref().unwrap(); let my_host = host.read().unwrap(); if are_handlers == HandlerMode::Handlers { // if we are running handlers at the moment, skip any un-notified handlers if ! my_host.is_notified(play_count, &logic.subscribe.as_ref().unwrap().clone()) { return Ok(handle.response.is_skipped(&Arc::clone(&validate))); } } // if a condition was provided and it was false, skip the task // lack of a condition provides a 'true' condition, hence no use of option processing here if ! logic.condition { return Ok(handle.response.is_skipped(&Arc::clone(&validate))); } // if sudo was requested on the specific task override any sudo computations above if logic.sudo.is_some() { sudo = Some(logic.sudo.as_ref().unwrap().clone()); } } let sudo_details = SudoDetails { user : sudo.clone(), template : sudo_template.clone() }; // we're about to get to the task finite state machine guts. // this looks like overkill but there's a lot of extra checking to make sure modules // don't return the wrong states, even when returning an error, to prevent // unpredictability in the program let query = TaskRequest::query(&sudo_details); // invoke the resource and see what actions it thinks need to be performed let qrc = action.dispatch(&handle, &query); // in check mode we short-circuit evaluation early, except for passive modules // like 'facts' if run_state.visitor.read().unwrap().is_check_mode() { match qrc { Ok(ref qrc_ok) => match qrc_ok.status { TaskStatus::NeedsPassive => { /* allow modules like !facts or set to execute */ }, _ => { return qrc; } }, _ => {} } } // with the query completed, what action to perform next depends on the query results let prelim_result : Result<Arc<TaskResponse>,Arc<TaskResponse>> = match qrc { Ok(ref qrc_ok) => match qrc_ok.status { // matched indicates we don't need to do anything TaskStatus::IsMatched => { Ok(handle.response.is_matched(&Arc::clone(&query))) }, TaskStatus::NeedsCreation => match modify_mode { true => { let req = TaskRequest::create(&sudo_details); let crc = action.dispatch(&handle, &req); match crc { Ok(ref crc_ok) => match crc_ok.status { TaskStatus::IsCreated => crc, // these are all module coding errors, should they occur, and cannot happen in normal operation _ => { panic!("module internal fsm state invalid (on create): {:?}", crc); } }, Err(ref crc_err) => match crc_err.status { TaskStatus::Failed => crc, _ => { panic!("module internal fsm state invalid (on create), {:?}", crc); } } } }, false => Ok(handle.response.is_created(&Arc::clone(&query))) }, TaskStatus::NeedsRemoval => match modify_mode { true => { let req = TaskRequest::remove(&sudo_details); let rrc = action.dispatch(&handle, &req); match rrc { Ok(ref rrc_ok) => match rrc_ok.status { TaskStatus::IsRemoved => rrc, _ => { panic!("module internal fsm state invalid (on remove): {:?}", rrc); } }, Err(ref rrc_err) => match rrc_err.status { TaskStatus::Failed => rrc, _ => { panic!("module internal fsm state invalid (on remove): {:?}", rrc); } } } }, false => Ok(handle.response.is_removed(&Arc::clone(&query))), }, TaskStatus::NeedsModification => match modify_mode { true => { let req = TaskRequest::modify(&sudo_details, qrc_ok.changes.clone()); let mrc = action.dispatch(&handle, &req); match mrc { Ok(ref mrc_ok) => match mrc_ok.status { TaskStatus::IsModified => mrc, _ => { panic!("module internal fsm state invalid (on modify): {:?}", mrc); } } Err(ref mrc_err) => match mrc_err.status { TaskStatus::Failed => mrc, _ => { panic!("module internal fsm state invalid (on modify): {:?}", mrc); } } } }, false => Ok(handle.response.is_modified(&Arc::clone(&query), qrc_ok.changes.clone())) }, TaskStatus::NeedsExecution => match modify_mode { true => { let req = TaskRequest::execute(&sudo_details); let erc = action.dispatch(&handle, &req); match erc { Ok(ref erc_ok) => match erc_ok.status { TaskStatus::IsExecuted => erc, TaskStatus::IsPassive => erc, _ => { panic!("module internal fsm state invalid (on execute): {:?}", erc); } } Err(ref erc_err) => match erc_err.status { TaskStatus::Failed => erc, _ => { panic!("module internal fsm state invalid (on execute): {:?}", erc); } } } }, false => Ok(handle.response.is_executed(&Arc::clone(&query))) }, TaskStatus::NeedsPassive => { let req = TaskRequest::passive(&sudo_details); let prc = action.dispatch(&handle, &req); match prc { Ok(ref prc_ok) => match prc_ok.status { TaskStatus::IsPassive => prc, _ => { panic!("module internal fsm state invalid (on passive): {:?}", prc); } } Err(ref prc_err) => match prc_err.status { TaskStatus::Failed => prc, _ => { panic!("module internal fsm state invalid (on passive): {:?}", prc); } } } }, // these panic states should never really happen unless there is a module coding error // it is unacceptable for a module to deliberately panic, they should // always return a TaskResponse. TaskStatus::Failed => { panic!("module returned failure inside an Ok(): {:?}", qrc); }, _ => { panic!("module internal fsm state unknown (on query): {:?}", qrc); } }, Err(x) => match x.status { TaskStatus::Failed => Err(x), _ => { panic!("module returned a non-failure code inside an Err: {:?}", x); } } }; // now that we've got a result, whether we use that result depends // on whether ignore_errors was set. let result = match prelim_result { Ok(x) => Ok(x), Err(y) => { if post_logic.is_some() { let logic = post_logic.as_ref().as_ref().unwrap(); match logic.ignore_errors { true => Ok(y), false => Err(y) } } else { Err(y) } } }; // if and/notify is present, notify handlers when changed actions are seen if result.is_ok() && post_logic.is_some() { let logic = post_logic.as_ref().as_ref().unwrap(); if are_handlers == HandlerMode::NormalTasks && result.is_ok() && logic.notify.is_some() { let notify = logic.notify.as_ref().unwrap().clone(); let status = &result.as_ref().unwrap().status; match status { TaskStatus::IsCreated | TaskStatus::IsModified | TaskStatus::IsRemoved | TaskStatus::IsExecuted => { run_state.visitor.read().unwrap().on_notify_handler(host, ¬ify.clone()); host.write().unwrap().notify(play_count, ¬ify.clone()); }, _ => { } } } } // ok, we're done, whew return result; } 0707010000004A000081A400000000000000000000000165135CC100000E33000000000000000000000000000000000000002800000000jetporch-0.0.1/src/playbooks/templar.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use serde_yaml; use once_cell::sync::Lazy; use handlebars::{Handlebars,RenderError}; // templar contains low-level wrapping around handlebars. // this is not used directly when evaluating templates and template // expressions, for this, see handle/template.rs static HANDLEBARS: Lazy<Handlebars> = Lazy::new(|| { let mut hb = Handlebars::new(); // very important: we are not plugging variables into HTML, turn escaping off hb.register_escape_fn(handlebars::no_escape); hb.set_strict_mode(true); return hb; }); // 'off' mode is used in a bit of a weird traversal/engine // situation where we need to get access to some task parameters // before templates are evaluated. You will notice there is no way // to evaluate templates in unstrict mode. This is by design. #[derive(PartialEq,Copy,Clone,Debug)] pub enum TemplateMode { Strict, Off } pub struct Templar { } impl Templar { pub fn new() -> Self { return Self { }; } // evaluate a string pub fn render(&self, template: &String, data: serde_yaml::Mapping, template_mode: TemplateMode) -> Result<String, String> { let result : Result<String, RenderError> = match template_mode { TemplateMode::Strict => HANDLEBARS.render_template(template, &data), /* this is only used to get back the raw 'items' collection inside the task FSM */ TemplateMode::Off => Ok(String::from("empty")) }; return match result { Ok(x) => { Ok(x) }, Err(y) => { Err(format!("Template error: {}", y.desc)) } } } // used for with/cond and also in the shell module pub fn test_condition(&self, expr: &String, data: serde_yaml::Mapping, template_mode: TemplateMode) -> Result<bool, String> { if template_mode == TemplateMode::Off { /* this is only used to get back the raw 'items' collection inside the task FSM */ return Ok(true); } // embed the expression in an if statement as a way to evaluate it for truth let template = format!("{{{{#if {expr} }}}}true{{{{ else }}}}false{{{{/if}}}}"); let result = self.render(&template, data, TemplateMode::Strict); match result { Ok(x) => { if x.as_str().eq("true") { return Ok(true); } else { return Ok(false); } }, Err(y) => { if y.find("Couldn't read parameter").is_some() { return Err(format!("failed to parse conditional: {}: one or more parameters may be undefined", expr)) } else { return Err(format!("failed to parse conditional: {}: {}", expr, y)) } } }; } } 0707010000004B000081A400000000000000000000000165135CC10000585E000000000000000000000000000000000000002A00000000jetporch-0.0.1/src/playbooks/traversal.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::playbooks::language::Play; use crate::playbooks::visitor::PlaybookVisitor; use crate::playbooks::context::PlaybookContext; use crate::playbooks::language::{Role,RoleInvocation}; use crate::connection::factory::ConnectionFactory; use crate::registry::list::Task; use crate::playbooks::task_fsm::fsm_run_task; use crate::inventory::inventory::Inventory; use crate::inventory::hosts::Host; use crate::util::io::{jet_file_open,directory_as_string}; use crate::util::yaml::{blend_variables,show_yaml_error_in_context}; use std::path::PathBuf; use std::collections::HashMap; use std::sync::{Arc,RwLock}; use std::path::Path; use std::env; // this module contains the start of everything related to playbook evaluation // various functions work differntly if we are evaluating handlers or not #[derive(PartialEq,Copy,Debug,Clone)] pub enum HandlerMode { NormalTasks, Handlers } // the run state is a quasi-global that can be used to access all // import 'objects' related to playbook evaluation pub struct RunState { pub inventory: Arc<RwLock<Inventory>>, pub playbook_paths: Arc<RwLock<Vec<PathBuf>>>, pub role_paths: Arc<RwLock<Vec<PathBuf>>>, pub limit_hosts: Vec<String>, pub limit_groups: Vec<String>, pub batch_size: Option<usize>, pub context: Arc<RwLock<PlaybookContext>>, pub visitor: Arc<RwLock<dyn PlaybookVisitor>>, pub connection_factory: Arc<RwLock<dyn ConnectionFactory>>, pub tags: Option<Vec<String>>, pub allow_localhost_delegation: bool } // this is the top end traversal function that is called from cli/playbooks.rs pub fn playbook_traversal(run_state: &Arc<RunState>) -> Result<(), String> { // it's possible to specify multiple playbooks seperated by colons on the command line for playbook_path in run_state.playbook_paths.read().unwrap().iter() { { // let the context object know what playbook we're currently running // braces are to avoid a deadlock let mut ctx = run_state.context.write().unwrap(); ctx.set_playbook_path(playbook_path); } run_state.visitor.read().unwrap().on_playbook_start(&run_state.context); // parse the playbook file let playbook_file = jet_file_open(&playbook_path)?; let parsed: Result<Vec<Play>, serde_yaml::Error> = serde_yaml::from_reader(playbook_file); if parsed.is_err() { show_yaml_error_in_context(&parsed.unwrap_err(), &playbook_path); return Err(format!("edit the file and try again?")); } // chdir in the playbook directory let p1 = env::current_dir().expect("could not get current directory"); let previous = p1.as_path(); let pbdirname = directory_as_string(playbook_path); let pbdir = Path::new(&pbdirname); if pbdirname.eq(&String::from("")) { } else { env::set_current_dir(&pbdir).expect("could not chdir into playbook directory"); } // walk each play in the playbook let plays: Vec<Play> = parsed.unwrap(); for play in plays.iter() { match handle_play(&run_state, play) { Ok(_) => {}, Err(s) => { return Err(s); } } // disconnect from all hosts between plays run_state.context.read().unwrap().connection_cache.write().unwrap().clear(); } // disconnect from all hosts between playbooks run_state.context.read().unwrap().connection_cache.write().unwrap().clear(); // switch back to the original directory env::set_current_dir(&previous).expect("could not restore previous directory"); } // disconnect from all hosts and exit. run_state.context.read().unwrap().connection_cache.write().unwrap().clear(); run_state.visitor.read().unwrap().on_exit(&run_state.context); return Ok(()) } fn handle_play(run_state: &Arc<RunState>, play: &Play) -> Result<(), String> { { // the connection logic will try to determine what SSH hosts and ports // to use by looking at various variables, if there are any CLI // or play settings for these, feed them into the context so these // functions can know what to do when called let mut ctx = run_state.context.write().unwrap(); ctx.set_play(play); if play.ssh_user.is_some() { ctx.set_ssh_user(&play.ssh_user.as_ref().unwrap()); } if play.ssh_port.is_some() { ctx.set_ssh_port(play.ssh_port.unwrap()); } ctx.unset_role(); } run_state.visitor.read().unwrap().on_play_start(&run_state.context); // make sure all hosts are valid and we have some hosts to talk to validate_groups(run_state, play)?; let hosts = get_play_hosts(run_state, play); validate_hosts(run_state, play, &hosts)?; load_vars_into_context(run_state, play)?; // support for serialization if using push configuration // means we may not configure hosts all at once but may take // several passes to do a smaller number of them let (_batch_size, batch_count, batches) = get_host_batches(run_state, play, hosts); let mut failed: bool = false; let mut failure_message: String = String::new(); // process each batch task/handlers seperately for batch_num in 0..batch_count { if failed { break; } let hosts = batches.get(&batch_num).unwrap(); run_state.visitor.read().unwrap().on_batch(batch_num, batch_count, hosts.len()); match handle_batch(run_state, play, hosts) { Ok(_) => {}, Err(s) => { failed = true; failure_message.clear(); failure_message.push_str(&s.clone()); } } // disconect from hosts between batches, one of the reasons we may be using // this is we have a very large number of machines to manage run_state.context.read().unwrap().connection_cache.write().unwrap().clear(); } // we're done, generate our summary/report & output regardless of failures run_state.visitor.read().unwrap().on_play_stop(&run_state.context, failed); if failed { return Err(failure_message.clone()); } else { return Ok(()) } } fn handle_batch(run_state: &Arc<RunState>, play: &Play, hosts: &Vec<Arc<RwLock<Host>>>) -> Result<(), String> { // assign the batch { let mut ctx = run_state.context.write().unwrap(); ctx.set_targetted_hosts(&hosts); } // handle role tasks if play.roles.is_some() { let roles = play.roles.as_ref().unwrap(); for invocation in roles.iter() { process_role(run_state, &play, &invocation, HandlerMode::NormalTasks)?; } } { let mut ctx = run_state.context.write().unwrap(); ctx.unset_role(); } // handle loose play tasks if play.tasks.is_some() { let tasks = play.tasks.as_ref().unwrap(); for task in tasks.iter() { process_task(run_state, &play, &task, HandlerMode::NormalTasks, None)?; } } // handle role handlers if play.roles.is_some() { let roles = play.roles.as_ref().unwrap(); for invocation in roles.iter() { process_role(run_state, &play, &invocation, HandlerMode::Handlers)?; } } { let mut ctx = run_state.context.write().unwrap(); ctx.unset_role(); } // handle loose play handlers if play.handlers.is_some() { let handlers = play.handlers.as_ref().unwrap(); for handler in handlers { process_task(run_state, &play, &handler, HandlerMode::Handlers, None)?; } } return Ok(()) } fn check_tags(run_state: &Arc<RunState>, task: &Task, role_invocation: Option<&RoleInvocation>) -> bool { // a given task may have tags associated from either the current role or directly on the task // if the CLI --tags argument was used, we will skip the task if those tags don't match or // if the tags are ommitted match &run_state.tags { Some(cli_tags) => { // CLI tags were specified match task.get_with() { // a with section was present Some(task_with) => match task_with.tags { // tags are applied to the task Some(task_tags) => { for x in task_tags.iter() { if cli_tags.contains(&x) { return true; } } }, // no tags None => {} }, None => {} }; match role_invocation { // the role invocation has tags applied Some(role_invoke) => match &role_invoke.tags { Some(role_tags) => { for x in role_tags.iter() { if cli_tags.contains(&x) { return true; } } }, None => {} }, None => {} }; } // no CLI tags so run the task None => { return true; } } // we didn't match any tags, so don't run the task return false; } fn process_task(run_state: &Arc<RunState>, play: &Play, task: &Task, are_handlers: HandlerMode, role_invocation: Option<&RoleInvocation>) -> Result<(), String> { // this function is the final wrapper before fsm_run_task, the low-level finite state machine around task execution that is wrapped // by rayon, for multi-threaded execution with our thread worker pool. let hosts : HashMap<String, Arc<RwLock<Host>>> = run_state.context.read().unwrap().get_remaining_hosts(); if hosts.len() == 0 { return Err(String::from("no hosts remaining")) } // we will run tasks with the FSM only if not skipped by tags let should_run = check_tags(run_state, task, role_invocation); if should_run { run_state.context.write().unwrap().set_task(&task); run_state.visitor.read().unwrap().on_task_start(&run_state.context, are_handlers); run_state.context.write().unwrap().increment_task_count(); fsm_run_task(run_state, play, task, are_handlers)?; } return Ok(()); } fn process_role(run_state: &Arc<RunState>, play: &Play, invocation: &RoleInvocation, are_handlers: HandlerMode) -> Result<(), String> { // traversal code for roles. This is called twice, once for normal tasks and again when processing handler tasks. // we traverse roles by seeing the 'invocation' in the playbook, which is different from the definition. // the definition involves all of the role files in the role directory let role_name = invocation.role.clone(); // can we find a role directory in the configured role paths? let (role, role_path) = find_role(run_state, &play, role_name.clone())?; { // we're good. let mut ctx = run_state.context.write().unwrap(); let str_path = directory_as_string(&role_path); ctx.set_role(&role, invocation, &str_path); if are_handlers == HandlerMode::NormalTasks { ctx.increment_role_count(); } } run_state.visitor.read().unwrap().on_role_start(&run_state.context); // roles contain two list of files to include, which one we're processing now // depends on whether we are in handler mode or not let files = match are_handlers { HandlerMode::NormalTasks => role.tasks, HandlerMode::Handlers => role.handlers }; // the file sections are optional... if files.is_some() { // prepare to chdir into the role, this makes operating on template and file paths easier let p1 = env::current_dir().expect("could not get current directory"); let previous = p1.as_path(); match env::set_current_dir(&role_path) { Ok(_) => {}, Err(s) => { return Err(format!("could not chdir into role directory {:?}, {}", role_path, s)) } } // for each task file path that is mentioned for task_file in files.unwrap().iter() { // find the likely path location, which is organized into subdirectories for relative paths let task_buf = match task_file.starts_with("/") { true => { Path::new(task_file).to_path_buf() } false => { let mut pb = PathBuf::new(); pb.push(role_path.clone()); match are_handlers { HandlerMode::NormalTasks => { pb.push("tasks"); }, HandlerMode::Handlers => { pb.push("handlers"); }, }; pb.push(task_file); pb } }; // parse the YAML file let task_fh = jet_file_open(&task_buf.as_path())?; let parsed: Result<Vec<Task>, serde_yaml::Error> = serde_yaml::from_reader(task_fh); if parsed.is_err() { show_yaml_error_in_context(&parsed.unwrap_err(), &task_buf.as_path()); return Err(format!("edit the file and try again?")); } let tasks = parsed.unwrap(); for task in tasks.iter() { // process all tasks in the YAML file, this is the same function used // for processing loose tasks outside of roles process_task(run_state, &play, &task, are_handlers, Some(invocation))?; } } // we're done with the role so flip back to the previous directory match env::set_current_dir(&previous) { Ok(_) => {}, Err(s) => { return Err(format!("could not restore previous directory after role evaluation: {:?}, {}", previous, s)) } } } run_state.visitor.read().unwrap().on_role_stop(&run_state.context); return Ok(()) } fn get_host_batches(run_state: &Arc<RunState>, play: &Play, hosts: Vec<Arc<RwLock<Host>>>) -> (usize, usize, HashMap<usize, Vec<Arc<RwLock<Host>>>>) { // the --batch-size CLI parameter can be used to split a large amount of possible hosts // into smaller subsets, where the playbook will pass over them in multiple waves // this can also be set on the play let batch_size = match play.batch_size { Some(x) => x, None => match run_state.batch_size { Some(y) => y, None => hosts.len() } }; // do some integer division math to see many batches we need let host_count = hosts.len(); let batch_count = match host_count { 0 => 1, _ => { let mut count = host_count / batch_size; let remainder = host_count % batch_size; if remainder > 0 { count = count + 1 } count } }; // sort the hosts so the batches seem consistent when doing successive playbook executions let mut hosts_list : Vec<Arc<RwLock<Host>>> = hosts.iter().map(|v| Arc::clone(&v)).collect(); hosts_list.sort_by(|b, a| a.read().unwrap().name.partial_cmp(&b.read().unwrap().name).unwrap()); // put the hosts into ththe assigned batches let mut results : HashMap<usize, Vec<Arc<RwLock<Host>>>> = HashMap::new(); for batch_num in 0..batch_count { let mut batch : Vec<Arc<RwLock<Host>>> = Vec::new(); for _host_ct in 0..batch_size { let host = hosts_list.pop(); if host.is_some() { batch.push(host.unwrap()); } else { break; } } results.insert(batch_num, batch); } return (batch_size, batch_count, results); } fn get_play_hosts(run_state: &Arc<RunState>,play: &Play) -> Vec<Arc<RwLock<Host>>> { // the hosts we want to talk to are the ones specified in the play but may // be further constrained by the parameters --limit-hosts and limit--groups // from the CLI. let groups = &play.groups; let mut results : HashMap<String, Arc<RwLock<Host>>> = HashMap::new(); let has_group_limits = match run_state.limit_groups.len() { 0 => false, _ => true }; let has_host_limits = match run_state.limit_hosts.len() { 0 => false, _ => true }; for group in groups.iter() { // for each mentioned group get all the hosts in that group and any subgroups let group_object = run_state.inventory.read().unwrap().get_group(&group.clone()); let hosts = group_object.read().unwrap().get_descendant_hosts(); for (k,v) in hosts.iter() { // only add the host to the play if it agrees with the limits // or no limits are specified if has_host_limits && ! run_state.limit_hosts.contains(k) { continue; } if has_group_limits { let mut ok = false; for group_name in run_state.limit_groups.iter() { if v.read().unwrap().has_group(group_name) { ok = true; break; } } if ok { results.insert(k.clone(), Arc::clone(&v)); } } else { results.insert(k.clone(), Arc::clone(&v)); } } } return results.iter().map(|(_k,v)| Arc::clone(&v)).collect(); } fn validate_groups(run_state: &Arc<RunState>, play: &Play) -> Result<(), String> { // groups on the play can't mention any groups that aren't in inventory let groups = &play.groups; let inv = run_state.inventory.read().unwrap(); for group_name in groups.iter() { if !inv.has_group(&group_name.clone()) { return Err(format!("at least one referenced group ({}) is not found in inventory", group_name)); } } return Ok(()); } fn validate_hosts(_run_state: &Arc<RunState>, _play: &Play, hosts: &Vec<Arc<RwLock<Host>>>) -> Result<(), String> { // once hosts are selected we need to select more than one host, if the groups were all // empty, don't try to run the playbook if hosts.is_empty() { return Err(String::from("no hosts selected by groups in play")); } return Ok(()); } fn load_vars_into_context(run_state: &Arc<RunState>, play: &Play) -> Result<(), String> { // the context object is fairly pervasive throughout the running of the program // and is (eventually) the gateway that template requests pass through, since // it holds on to losts of play and role variables. This function loads // a lot of the variables into the context ensuring proper variable precedence let ctx = run_state.context.write().unwrap(); let mut ctx_vars_storage = serde_yaml::Value::from(serde_yaml::Mapping::new()); let mut ctx_defaults_storage = serde_yaml::Value::from(serde_yaml::Mapping::new()); if play.vars.is_some() { // vars are inline variables that are loaded at maximum precedence let vars = play.vars.as_ref().unwrap(); blend_variables(&mut ctx_vars_storage, serde_yaml::Value::Mapping(vars.clone())); } if play.vars_files.is_some() { // vars_files are paths to YAML files that are loaded at maximum precedence let vars_files = play.vars_files.as_ref().unwrap(); for pathname in vars_files { let path = Path::new(&pathname); let vars_file = jet_file_open(&path)?; let parsed: Result<serde_yaml::Mapping, serde_yaml::Error> = serde_yaml::from_reader(vars_file); if parsed.is_err() { show_yaml_error_in_context(&parsed.unwrap_err(), &path); return Err(format!("edit the file and try again?")); } blend_variables(&mut ctx_vars_storage, serde_yaml::Value::Mapping(parsed.unwrap())); } } if play.defaults.is_some() { // defaults works like 'vars' but has the lowest precedence let defaults = play.defaults.as_ref().unwrap(); blend_variables(&mut ctx_defaults_storage, serde_yaml::Value::Mapping(defaults.clone())); } // these match expressions are just used to 'de-enum' the serde values so we can write to them match ctx_vars_storage { serde_yaml::Value::Mapping(x) => { *ctx.vars_storage.write().unwrap() = x }, _ => panic!("unexpected, get_blended_variables produced a non-mapping (1)") } match ctx_defaults_storage { serde_yaml::Value::Mapping(x) => { *ctx.defaults_storage.write().unwrap() = x }, _ => panic!("unexpected, get_blended_variables produced a non-mapping (1)") } return Ok(()); } fn find_role(run_state: &Arc<RunState>, _play: &Play, role_name: String) -> Result<(Role,PathBuf), String> { // when we need to find a role we look for it in the configured role paths for path_buf in run_state.role_paths.read().unwrap().iter() { let mut pb = path_buf.clone(); pb.push(role_name.clone()); let mut pb2 = pb.clone(); pb2.push("role.yml"); // a role.yml file must exist in a directory once we find a directory with a matching // name if pb2.exists() { let path = pb2.as_path(); let role_file = jet_file_open(&path)?; // deserialize the role file and make sure it is valid before returning let parsed: Result<Role, serde_yaml::Error> = serde_yaml::from_reader(role_file); if parsed.is_err() { show_yaml_error_in_context(&parsed.unwrap_err(), &path); return Err(format!("edit the file and try again?")); } let role = parsed.unwrap(); return Ok((role,pb)); } } return Err(format!("role not found: {}", role_name)); } 0707010000004C000081A400000000000000000000000165135CC1000040C8000000000000000000000000000000000000002800000000jetporch-0.0.1/src/playbooks/visitor.rs // Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::playbooks::context::PlaybookContext; use std::sync::Arc; use crate::tasks::*; use std::sync::RwLock; use crate::inventory::hosts::Host; use inline_colorization::{color_red,color_blue,color_green,color_cyan,color_reset,color_yellow}; use std::marker::{Send,Sync}; use crate::connection::command::CommandResult; use crate::playbooks::traversal::HandlerMode; // visitor contains various functions that are called from all over the program // to send feedback to the user. Eventually this object will also take // care of logging requirements (TODO) pub trait PlaybookVisitor : Send + Sync { fn banner(&self) { println!("----------------------------------------------------------"); } fn debug(&self, message: &String) { println!("{color_cyan} ..... (debug) : {}{color_reset}", message); } // used by the echo module fn debug_host(&self, host: &Arc<RwLock<Host>>, message: &String) { println!("{color_cyan} ..... {} : {}{color_reset}", host.read().unwrap().name, message); } // a version of debug that locks with a mutex to prevent the output from being interlaced fn debug_lines(&self, context: &Arc<RwLock<PlaybookContext>>, host: &Arc<RwLock<Host>>, messages: &Vec<String>) { let _lock = context.write().unwrap(); for message in messages.iter() { self.debug_host(host, &message); } } fn on_playbook_start(&self, context: &Arc<RwLock<PlaybookContext>>) { let ctx = context.read().unwrap(); let path = ctx.playbook_path.as_ref().unwrap(); self.banner(); println!("> playbook start: {}", path) } fn on_play_start(&self, context: &Arc<RwLock<PlaybookContext>>) { let play = &context.read().unwrap().play; self.banner(); println!("> play: {}", play.as_ref().unwrap()); } fn on_role_start(&self, _context: &Arc<RwLock<PlaybookContext>>) { } fn on_role_stop(&self, _context: &Arc<RwLock<PlaybookContext>>) { } fn on_play_stop(&self, context: &Arc<RwLock<PlaybookContext>>, failed: bool) { // failed occurs if *ALL* hosts in a play have failed let ctx = context.read().unwrap(); let play_name = ctx.get_play_name(); if ! failed { self.banner(); println!("> play complete: {}", play_name); } else { self.banner(); println!("{color_red}> play failed: {}{color_reset}", play_name); } } fn on_exit(&self, context: &Arc<RwLock<PlaybookContext>>) { println!("----------------------------------------------------------"); println!(""); show_playbook_summary(context); } fn on_task_start(&self, context: &Arc<RwLock<PlaybookContext>>, is_handler: HandlerMode) { let context = context.read().unwrap(); let task = context.task.as_ref().unwrap(); let role = &context.role; let what = match is_handler { HandlerMode::NormalTasks => String::from("task"), HandlerMode::Handlers => String::from("handler") }; self.banner(); if role.is_none() { println!("> begin {}: {}", what, task); } else { println!("> ({}) begin {}: {}", role.as_ref().unwrap().name, what, task); } } fn on_batch(&self, batch_num: usize, batch_count: usize, batch_size: usize) { self.banner(); println!("> batch {}/{}, {} hosts", batch_num+1, batch_count, batch_size); } fn on_task_stop(&self, _context: &Arc<RwLock<PlaybookContext>>) { } fn on_host_task_start(&self, _context: &Arc<RwLock<PlaybookContext>>, host: &Arc<RwLock<Host>>) { let host2 = host.read().unwrap(); println!("… {} => running", host2.name); } fn on_notify_handler(&self, host: &Arc<RwLock<Host>>, which_handler: &String) { let host2 = host.read().unwrap(); println!("… {} => notified: {}", host2.name, which_handler); } fn on_host_delegate(&self, host: &Arc<RwLock<Host>>, delegated: &String) { let host2 = host.read().unwrap(); println!("{color_blue}✓ {} => delegating to: {}{color_reset}", &host2.name, delegated.clone()); } fn on_host_task_ok(&self, context: &Arc<RwLock<PlaybookContext>>, task_response: &Arc<TaskResponse>, host: &Arc<RwLock<Host>>) { let host2 = host.read().unwrap(); let mut context = context.write().unwrap(); context.increment_attempted_for_host(&host2.name); match &task_response.status { TaskStatus::IsCreated => { println!("{color_blue}✓ {} => created{color_reset}", &host2.name); context.increment_created_for_host(&host2.name); }, TaskStatus::IsRemoved => { println!("{color_blue}✓ {} => removed{color_reset}", &host2.name); context.increment_removed_for_host(&host2.name); }, TaskStatus::IsModified => { let changes2 : Vec<String> = task_response.changes.iter().map(|x| { format!("{:?}", x) }).collect(); let change_str = changes2.join(","); println!("{color_blue}✓ {} => modified ({}){color_reset}", &host2.name, change_str); context.increment_modified_for_host(&host2.name); }, TaskStatus::IsExecuted => { println!("{color_blue}✓ {} => complete{color_reset}", &host2.name); context.increment_executed_for_host(&host2.name); }, TaskStatus::IsPassive => { // println!("{color_green}! host: {} => ok (no effect) {color_reset}", &host2.name); context.increment_passive_for_host(&host2.name); } TaskStatus::IsMatched => { println!("{color_green}✓ {} => matched {color_reset}", &host2.name); context.increment_matched_for_host(&host2.name); } TaskStatus::IsSkipped => { println!("{color_yellow}✓ {} => skipped {color_reset}", &host2.name); context.increment_skipped_for_host(&host2.name); } TaskStatus::Failed => { println!("{color_yellow}✓ {} => failed (ignored){color_reset}", &host2.name); } _ => { panic!("on host {}, invalid final task return status, FSM should have rejected: {:?}", host2.name, task_response); } } } // the check mode version of on_host_task_ok - different possible states, slightly different output fn on_host_task_check_ok(&self, context: &Arc<RwLock<PlaybookContext>>, task_response: &Arc<TaskResponse>, host: &Arc<RwLock<Host>>) { let host2 = host.read().unwrap(); let mut context = context.write().unwrap(); context.increment_attempted_for_host(&host2.name); match &task_response.status { TaskStatus::NeedsCreation => { println!("{color_blue}✓ {} => would create{color_reset}", &host2.name); context.increment_created_for_host(&host2.name); }, TaskStatus::NeedsRemoval => { println!("{color_blue}✓ {} => would remove{color_reset}", &host2.name); context.increment_removed_for_host(&host2.name); }, TaskStatus::NeedsModification => { let changes2 : Vec<String> = task_response.changes.iter().map(|x| { format!("{:?}", x) }).collect(); let change_str = changes2.join(","); println!("{color_blue}✓ {} => would modify ({}) {color_reset}", &host2.name, change_str); context.increment_modified_for_host(&host2.name); }, TaskStatus::NeedsExecution => { println!("{color_blue}✓ {} => would run{color_reset}", &host2.name); context.increment_executed_for_host(&host2.name); }, TaskStatus::IsPassive => { context.increment_passive_for_host(&host2.name); } TaskStatus::IsMatched => { println!("{color_green}✓ {} => matched {color_reset}", &host2.name); context.increment_matched_for_host(&host2.name); } TaskStatus::IsSkipped => { println!("{color_yellow}✓ {} => skipped {color_reset}", &host2.name); context.increment_skipped_for_host(&host2.name); } TaskStatus::Failed => { println!("{color_yellow}✓ {} => failed (ignored){color_reset}", &host2.name); } _ => { panic!("on host {}, invalid check-mode final task return status, FSM should have rejected: {:?}", host2.name, task_response); } } } fn on_host_task_retry(&self, _context: &Arc<RwLock<PlaybookContext>>,host: &Arc<RwLock<Host>>, retries: u64, delay: u64) { let host2 = host.read().unwrap(); println!("{color_blue}! {} => retrying ({} retries left) in {} seconds{color_reset}",host2.name,retries,delay); } fn on_host_task_failed(&self, context: &Arc<RwLock<PlaybookContext>>, task_response: &Arc<TaskResponse>, host: &Arc<RwLock<Host>>) { let host2 = host.read().unwrap(); if task_response.msg.is_some() { let msg = &task_response.msg; if task_response.command_result.is_some() { { let cmd_result = task_response.command_result.as_ref().as_ref().unwrap(); let _lock = context.write().unwrap(); println!("{color_red}! {} => failed", host2.name); println!(" cmd: {}", cmd_result.cmd); println!(" out: {}", cmd_result.out); println!(" rc: {}{color_reset}", cmd_result.rc); } } else { println!("{color_red}! error: {}: {}{color_reset}", host2.name, msg.as_ref().unwrap()); } } else { println!("{color_red}! host failed: {}, {color_reset}", host2.name); } context.write().unwrap().increment_failed_for_host(&host2.name); } fn on_host_connect_failed(&self, context: &Arc<RwLock<PlaybookContext>>, host: &Arc<RwLock<Host>>) { let host2 = host.read().unwrap(); context.write().unwrap().increment_failed_for_host(&host2.name); println!("{color_red}! connection failed to host: {}{color_reset}", host2.name); } fn get_exit_status(&self, context: &Arc<RwLock<PlaybookContext>>) -> i32 { let failed_hosts = context.read().unwrap().get_hosts_failed_count(); return match failed_hosts { 0 => 0, _ => 1 }; } fn on_before_transfer(&self, context: &Arc<RwLock<PlaybookContext>>, host: &Arc<RwLock<Host>>, path: &String) { let host2 = host.read().unwrap(); if context.read().unwrap().verbosity > 0 { println!("{color_blue}! {} => transferring to: {}", host2.name, &path.clone()); } } fn on_command_run(&self, context: &Arc<RwLock<PlaybookContext>>, host: &Arc<RwLock<Host>>, cmd: &String) { let host2 = host.read().unwrap(); if context.read().unwrap().verbosity > 0 { println!("{color_blue}! {} => exec: {}", host2.name, &cmd.clone()); } } fn on_command_ok(&self, context: &Arc<RwLock<PlaybookContext>>, host: &Arc<RwLock<Host>>, result: &Arc<Option<CommandResult>>,) { let host2 = host.read().unwrap(); let cmd_result = result.as_ref().as_ref().expect("missing command result"); if context.read().unwrap().verbosity > 2 { let _ctx2 = context.write().unwrap(); // lock for multi-line output println!("{color_blue}! {} ... command ok", host2.name); println!(" cmd: {}", cmd_result.cmd); println!(" out: {}", cmd_result.out.clone()); println!(" rc: {}{color_reset}", cmd_result.rc); } } fn on_command_failed(&self, context: &Arc<RwLock<PlaybookContext>>, host: &Arc<RwLock<Host>>, result: &Arc<Option<CommandResult>>,) { let host2 = host.read().expect("context read"); let cmd_result = result.as_ref().as_ref().expect("missing command result"); if context.read().unwrap().verbosity > 2 { let _ctx2 = context.write().unwrap(); // lock for multi-line output println!("{color_red}! {} ... command failed", host2.name); println!(" cmd: {}", cmd_result.cmd); println!(" out: {}", cmd_result.out.clone()); println!(" rc: {}{color_reset}", cmd_result.rc); } } fn is_check_mode(&self) -> bool; } pub fn show_playbook_summary(context: &Arc<RwLock<PlaybookContext>>) { let ctx = context.read().unwrap(); let seen_hosts = ctx.get_hosts_seen_count(); let role_ct = ctx.get_role_count(); let task_ct = ctx.get_task_count(); let action_ct = ctx.get_total_attempted_count(); let created_ct = ctx.get_total_creation_count(); let created_hosts = ctx.get_hosts_creation_count(); let modified_ct = ctx.get_total_modified_count(); let modified_hosts = ctx.get_hosts_modified_count(); let removed_ct = ctx.get_total_removal_count(); let removed_hosts = ctx.get_hosts_removal_count(); let executed_ct = ctx.get_total_executions_count(); let executed_hosts = ctx.get_hosts_executions_count(); let passive_ct = ctx.get_total_passive_count(); let passive_hosts = ctx.get_hosts_passive_count(); let matched_ct = ctx.get_total_matched_count(); let matched_hosts = ctx.get_hosts_matched_count(); let skipped_ct = ctx.get_total_skipped_count(); let skipped_hosts = ctx.get_hosts_skipped_count(); let adjusted_ct = ctx.get_total_adjusted_count(); let adjusted_hosts = ctx.get_hosts_adjusted_count(); let unchanged_hosts = seen_hosts - adjusted_hosts; let unchanged_ct = action_ct - adjusted_ct; let failed_ct = ctx.get_total_failed_count(); let failed_hosts = ctx.get_hosts_failed_count(); let summary = match failed_hosts { 0 => match adjusted_hosts { 0 => String::from(format!("{color_green}(✓) Perfect. All hosts matched policy.{color_reset}")), _ => String::from(format!("{color_blue}(✓) Actions were applied.{color_reset}")), }, _ => String::from(format!("{color_red}(X) Failures have occured.{color_reset}")), }; let mode_table = format!("|:-|:-|:-|\n\ | Results | Items | Hosts \n\ | --- | --- | --- |\n\ | Roles | {role_ct} | |\n\ | Tasks | {task_ct} | {seen_hosts}|\n\ | --- | --- | --- |\n\ | Matched | {matched_ct} | {matched_hosts}\n\ | Created | {created_ct} | {created_hosts}\n\ | Modified | {modified_ct} | {modified_hosts}\n\ | Removed | {removed_ct} | {removed_hosts}\n\ | Executed | {executed_ct} | {executed_hosts}\n\ | Passive | {passive_ct} | {passive_hosts}\n\ | Skipped | {skipped_ct} | {skipped_hosts}\n\ | --- | --- | ---\n\ | Unchanged | {unchanged_ct} | {unchanged_hosts}\n\ | Changed | {adjusted_ct} | {adjusted_hosts}\n\ | Failed | {failed_ct} | {failed_hosts}\n\ |-|-|-"); crate::util::terminal::markdown_print(&mode_table); println!("{}", format!("\n{summary}")); println!(""); } 0707010000004D000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001C00000000jetporch-0.0.1/src/registry0707010000004E000081A400000000000000000000000165135CC100001A79000000000000000000000000000000000000002400000000jetporch-0.0.1/src/registry/list.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use serde::Deserialize; use crate::tasks::*; use std::sync::Arc; // note: there is some repetition in this module that we would rather not have // however, it comes from a conflict between polymorphic dispatch macros + traits // and a lack of data-inheritance in structs. please ignore it the best you can // and this may be improved later. If there was no Enum, we could have // polymorphic dispatch, but traversal would lose a lot of serde benefits. // ADD NEW MODULES HERE, KEEP ALPHABETIZED BY SECTION // commands use crate::modules::commands::shell::ShellTask; // control use crate::modules::control::assert::AssertTask; use crate::modules::control::debug::DebugTask; use crate::modules::control::echo::EchoTask; use crate::modules::control::fail::FailTask; use crate::modules::control::facts::FactsTask; use crate::modules::control::set::SetTask; // files use crate::modules::files::copy::CopyTask; use crate::modules::files::directory::DirectoryTask; use crate::modules::files::file::FileTask; use crate::modules::files::git::GitTask; use crate::modules::files::template::TemplateTask; // packages use crate::modules::packages::apt::AptTask; use crate::modules::packages::yum_dnf::YumDnfTask; // services use crate::modules::services::sd_service::SystemdServiceTask; #[allow(non_camel_case_types)] #[derive(Deserialize,Debug)] #[serde(rename_all="lowercase")] pub enum Task { // ADD NEW MODULES HERE, KEEP ALPHABETIZED BY NAME Apt(AptTask), Assert(AssertTask), Copy(CopyTask), Debug(DebugTask), Dnf(YumDnfTask), Directory(DirectoryTask), Echo(EchoTask), Fail(FailTask), Facts(FactsTask), File(FileTask), Git(GitTask), Sd_Service(SystemdServiceTask), Set(SetTask), Shell(ShellTask), Template(TemplateTask), Yum(YumDnfTask), } impl Task { pub fn get_module(&self) -> String { return match self { Task::Apt(x) => x.get_module(), Task::Assert(x) => x.get_module(), Task::Copy(x) => x.get_module(), Task::Debug(x) => x.get_module(), Task::Dnf(x) => x.get_module(), Task::Directory(x) => x.get_module(), Task::Echo(x) => x.get_module(), Task::Facts(x) => x.get_module(), Task::Fail(x) => x.get_module(), Task::File(x) => x.get_module(), Task::Git(x) => x.get_module(), Task::Sd_Service(x) => x.get_module(), Task::Set(x) => x.get_module(), Task::Shell(x) => x.get_module(), Task::Template(x) => x.get_module(), Task::Yum(x) => x.get_module(), }; } pub fn get_name(&self) -> Option<String> { return match self { Task::Apt(x) => x.get_name(), Task::Assert(x) => x.get_name(), Task::Copy(x) => x.get_name(), Task::Debug(x) => x.get_name(), Task::Dnf(x) => x.get_name(), Task::Directory(x) => x.get_name(), Task::Echo(x) => x.get_name(), Task::Facts(x) => x.get_name(), Task::Fail(x) => x.get_name(), Task::File(x) => x.get_name(), Task::Git(x) => x.get_name(), Task::Sd_Service(x) => x.get_name(), Task::Set(x) => x.get_name(), Task::Shell(x) => x.get_name(), Task::Template(x) => x.get_name(), Task::Yum(x) => x.get_name(), }; } pub fn get_with(&self) -> Option<PreLogicInput> { return match self { Task::Apt(x) => x.get_with(), Task::Assert(x) => x.get_with(), Task::Copy(x) => x.get_with(), Task::Debug(x) => x.get_with(), Task::Dnf(x) => x.get_with(), Task::Directory(x) => x.get_with(), Task::Echo(x) => x.get_with(), Task::Facts(x) => x.get_with(), Task::Fail(x) => x.get_with(), Task::File(x) => x.get_with(), Task::Git(x) => x.get_with(), Task::Sd_Service(x) => x.get_with(), Task::Set(x) => x.get_with(), Task::Shell(x) => x.get_with(), Task::Template(x) => x.get_with(), Task::Yum(x) => x.get_with(), }; } pub fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>> { // ADD NEW MODULES HERE, KEEP ALPHABETIZED BY NAME return match self { Task::Apt(x) => x.evaluate(handle, request, tm), Task::Assert(x) => x.evaluate(handle, request, tm), Task::Copy(x) => x.evaluate(handle, request, tm), Task::Debug(x) => x.evaluate(handle, request, tm), Task::Dnf(x) => x.evaluate(handle, request, tm), Task::Directory(x) => x.evaluate(handle, request, tm), Task::Echo(x) => x.evaluate(handle, request, tm), Task::Fail(x) => x.evaluate(handle, request, tm), Task::Facts(x) => x.evaluate(handle, request, tm), Task::File(x) => x.evaluate(handle, request, tm), Task::Git(x) => x.evaluate(handle, request, tm), Task::Sd_Service(x) => x.evaluate(handle, request, tm), Task::Set(x) => x.evaluate(handle, request, tm), Task::Shell(x) => x.evaluate(handle, request, tm), Task::Template(x) => x.evaluate(handle, request, tm), Task::Yum(x) => x.evaluate(handle, request, tm), }; } // ==== END MODULE REGISTRY CONFIG ==== pub fn get_display_name(&self) -> String { return match self.get_name() { Some(x) => x, _ => self.get_module() } } } 0707010000004F000081A400000000000000000000000165135CC1000002F7000000000000000000000000000000000000002300000000jetporch-0.0.1/src/registry/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. pub mod list; 07070100000050000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001900000000jetporch-0.0.1/src/tasks07070100000051000081A400000000000000000000000165135CC10000048B000000000000000000000000000000000000002500000000jetporch-0.0.1/src/tasks/checksum.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use rs_sha512::{Sha512State,HasherContext}; use std::hash::BuildHasher; use std::hash::Hasher; pub fn sha512(data: &String) -> String { let mut sha512hasher = Sha512State::default().build_hasher(); let bytes = data.as_bytes(); sha512hasher.write(bytes); let _u64result = sha512hasher.finish(); let bytes_result = HasherContext::finish(&mut sha512hasher); return format!("{bytes_result:02x}") } 07070100000052000081A400000000000000000000000165135CC100001AF5000000000000000000000000000000000000002800000000jetporch-0.0.1/src/tasks/cmd_library.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. // this is here to prevent typos in module code between Query & Modify // match legs. use crate::inventory::hosts::HostOSType; use crate::tasks::FileAttributesInput; use crate::tasks::files::Recurse; // **IMPORTANT** // // all commands are responsible for screening their inputs within this file // it is **NOT** permissible to leave this up to the caller. Err on the side // of over-filtering! // // most filtering should occur in the module() evaluate code by choosing // the right template functions. // // any argument that allows spaces (such as paths) should be the *last* // command in any command sequence. pub fn screen_path(path: &String) -> Result<String,String> { // NOTE: this only checks paths used in commands let path2 = path.trim().to_string(); let path3 = screen_general_input_strict(&path2)?; return Ok(path3.to_string()); } // this filtering is applied to all shell arguments in the command library below (if not, it's an error) // but is automatically also applied to all template calls not marked _unsafe in the evaluate() stages // of modules. We run everything twice to prevent module coding errors. pub fn screen_general_input_strict(input: &String) -> Result<String,String> { let input2 = input.trim(); let bad = vec![ ";", "{", "}", "(", ")", "<", ">", "&", "*", "|", "=", "?", "[", "]", "$", "%", "+", "`"]; for invalid in bad.iter() { if input2.find(invalid).is_some() { return Err(format!("illegal characters found: {} ('{}')", input2, invalid.to_string())); } } return Ok(input2.to_string()); } // a slightly lighter version of checking, that allows = signs and such // this is applied across all commands executed by the system, not just per-parameter checks // unless run_unsafe is used internally. It is assumed that all inputs going into this command // (parameters) are already sufficiently screened for things that can break shell commands and arguments // are already quoted. pub fn screen_general_input_loose(input: &String) -> Result<String,String> { let input2 = input.trim(); let bad = vec![ ";", "<", ">", "&", "*", "?", "{", "}", "[", "]", "$", "`"]; for invalid in bad.iter() { if input2.find(invalid).is_some() { return Err(format!("illegal characters detected: {} ('{}')", input2, invalid.to_string())); } } return Ok(input2.to_string()); } // require that octal inputs be ... octal pub fn screen_mode(mode: &String) -> Result<String,String> { if FileAttributesInput::is_octal_string(&mode) { return Ok(mode.clone()); } else { return Err(format!("not an octal string: {}", mode)); } } pub fn get_mode_command(os_type: HostOSType, untrusted_path: &String) -> Result<String,String> { let path = screen_path(untrusted_path)?; return match os_type { HostOSType::Linux => Ok(format!("stat --format '%a' '{}'", path)), HostOSType::MacOS => Ok(format!("stat -f '%A' '{}'", path)), } } pub fn get_sha512_command(os_type: HostOSType, untrusted_path: &String) -> Result<String,String> { let path = screen_path(untrusted_path)?; return match os_type { HostOSType::Linux => Ok(format!("sha512sum '{}'", path)), HostOSType::MacOS => Ok(format!("shasum -b -a 512 '{}'", path)), } } pub fn get_ownership_command(_os_type: HostOSType, untrusted_path: &String) -> Result<String,String> { let path = screen_path(untrusted_path)?; return Ok(format!("ls -ld '{}'", path)); } pub fn get_is_directory_command(_os_type: HostOSType, untrusted_path: &String) -> Result<String,String> { let path = screen_path(untrusted_path)?; return Ok(format!("ls -ld '{}'", path)); } pub fn get_touch_command(_os_type: HostOSType, untrusted_path: &String) -> Result<String,String> { let path = screen_path(untrusted_path)?; return Ok(format!("touch '{}'", path)); } pub fn get_create_directory_command(_os_type: HostOSType, untrusted_path: &String) -> Result<String,String> { let path = screen_path(untrusted_path)?; return Ok(format!("mkdir -p '{}'", path)); } pub fn get_delete_file_command(_os_type: HostOSType, untrusted_path: &String) -> Result<String,String> { let path = screen_path(untrusted_path)?; return Ok(format!("rm -f '{}'", path)); } pub fn get_delete_directory_command(_os_type: HostOSType, untrusted_path: &String, recurse: Recurse) -> Result<String,String> { let path = screen_path(untrusted_path)?; match recurse { Recurse::No => { return Ok(format!("rm -d '{}'", path)); }, Recurse::Yes => { return Ok(format!("rm -rf '{}'", path)); } } } pub fn set_owner_command(_os_type: HostOSType, untrusted_path: &String, untrusted_owner: &String, recurse: Recurse) -> Result<String,String> { let path = screen_path(untrusted_path)?; let owner = screen_general_input_strict(untrusted_owner)?; match recurse { Recurse::No => { return Ok(format!("chown '{}' '{}'", owner, path)); }, Recurse::Yes => { return Ok(format!("chown -R '{}' '{}'", owner, path)); } } } pub fn set_group_command(_os_type: HostOSType, untrusted_path: &String, untrusted_group: &String, recurse: Recurse) -> Result<String,String> { let path = screen_path(untrusted_path)?; let group = screen_general_input_strict(untrusted_group)?; match recurse { Recurse::No => { return Ok(format!("chgrp '{}' '{}'", group, path)); }, Recurse::Yes => { return Ok(format!("chgrp -R '{}' '{}'", group, path)); } } } pub fn set_mode_command(_os_type: HostOSType, untrusted_path: &String, untrusted_mode: &String, recurse: Recurse) -> Result<String,String> { // mode generally does not have to be screened but someone could call a command directly without going through FileAttributes // so let's be thorough. let path = screen_path(untrusted_path)?; let mode = screen_mode(untrusted_mode)?; match recurse { Recurse::No => { return Ok(format!("chmod '{}' '{}'", mode, path)); }, Recurse::Yes => { return Ok(format!("chmod -R '{}' '{}'", mode, path)); } } } 07070100000053000081A400000000000000000000000165135CC1000006FD000000000000000000000000000000000000002300000000jetporch-0.0.1/src/tasks/common.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::handle::handle::TaskHandle; use crate::tasks::request::TaskRequest; use crate::tasks::response::TaskResponse; use crate::tasks::logic::{PreLogicInput,PreLogicEvaluated,PostLogicEvaluated}; use std::sync::Arc; use crate::tasks::TemplateMode; pub struct EvaluatedTask { pub action: Arc<dyn IsAction>, pub with: Arc<Option<PreLogicEvaluated>>, pub and: Arc<Option<PostLogicEvaluated>> } pub trait IsTask : Send + Sync { fn get_module(&self) -> String; fn get_name(&self) -> Option<String>; fn get_with(&self) -> Option<PreLogicInput>; fn evaluate(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode) -> Result<EvaluatedTask, Arc<TaskResponse>>; fn get_display_name(&self) -> String { return match self.get_name() { Some(x) => x, _ => self.get_module() } } } pub trait IsAction : Send + Sync { fn dispatch(&self, handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>) -> Result<Arc<TaskResponse>, Arc<TaskResponse>>; } 07070100000054000081A400000000000000000000000165135CC100000536000000000000000000000000000000000000002300000000jetporch-0.0.1/src/tasks/fields.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::vec::Vec; // this is to prevent typos in module code between Query & Modify // match legs vs using strings // KEEP THESE ALPHABETIZED #[derive(Eq,Hash,PartialEq,Clone,Copy,Debug)] pub enum Field { Branch, Content, Disable, Enable, Group, Mode, Owner, Restart, Start, Stop, Version, } impl Field { pub fn all_file_attributes() -> Vec<Field> { let mut result : Vec<Field> = Vec::new(); result.push(Field::Owner); result.push(Field::Group); result.push(Field::Mode); return result; } } 07070100000055000081A400000000000000000000000165135CC10000188B000000000000000000000000000000000000002200000000jetporch-0.0.1/src/tasks/files.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::handle::handle::TaskHandle; use crate::tasks::request::TaskRequest; use crate::tasks::response::TaskResponse; use crate::tasks::TemplateMode; use std::sync::Arc; use serde::Deserialize; // this is storage behind all 'and' and 'with' statements in the program, which // are mostly implemented in task_fsm #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct FileAttributesInput { pub owner: Option<String>, pub group: Option<String>, pub mode: Option<String> } #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct FileAttributesEvaluated { pub owner: Option<String>, pub group: Option<String>, pub mode: Option<String> } #[derive(Deserialize,Debug,Copy,Clone,PartialEq)] pub enum Recurse { No, Yes } impl FileAttributesInput { // given an octal string, like 0o755 or 755, return the numeric value pub fn is_octal_string(mode: &String) -> bool { let octal_no_prefix = str::replace(&mode, "0o", ""); // this error should be screened out by template() below already but return types are important. return match i32::from_str_radix(&octal_no_prefix, 8) { Ok(_x) => true, Err(_y) => false } } // given an octal string, like 0o755 or 755, return the numeric value /* fn octal_string_to_number(response: &Arc<Response>, request: &Arc<TaskRequest>, mode: &String) -> Result<i32,Arc<TaskResponse>> { let octal_no_prefix = str::replace(&mode, "0o", ""); // this error should be screened out by template() below already but return types are important. return match i32::from_str_radix(&octal_no_prefix, 8) { Ok(x) => Ok(x), Err(y) => { return Err(response.is_failed(&request, &format!("invalid octal value extracted from mode, was {}, {:?}", octal_no_prefix,y))); } } } */ // template **all** the fields in FileAttributesInput fields, checking values and returning errors as needed pub fn template(handle: &TaskHandle, request: &Arc<TaskRequest>, tm: TemplateMode, input: &Option<Self>) -> Result<Option<FileAttributesEvaluated>,Arc<TaskResponse>> { if tm == TemplateMode::Off { return Ok(None); } if input.is_none() { return Ok(None); } let input2 = input.as_ref().unwrap(); let final_mode_value : Option<String>; // owner & group is easy but mode is complex // makes sure mode is octal and not accidentally enter decimal or hex or leave off the octal prefix // as the input field is a YAML string unwanted conversion shouldn't happen but we want to be strict with other tools // that might read the file and encourage users to use YAML-spec required input here even though YAML isn't doing // the evaluation. if input2.mode.is_some() { let mode_input = input2.mode.as_ref().unwrap(); let templated_mode_string = handle.template.string(request, tm, &String::from("mode"), &mode_input)?; if ! templated_mode_string.starts_with("0o") { return Err(handle.response.is_failed(request, &String::from( format!("(a) field (mode) must have an octal-prefixed value of form 0o755, was {}", templated_mode_string) ))); } let octal_no_prefix = str::replace(&templated_mode_string, "0o", ""); // we may have gotten an 0oExampleJunkString which is still not neccessarily valid - so check if it's a number // and return the value with the 0o stripped off, for easier use elsewhere let decimal_mode = i32::from_str_radix(&octal_no_prefix, 8); match decimal_mode { Ok(_x) => { final_mode_value = Some(octal_no_prefix); }, Err(_y) => { return Err(handle.response.is_failed(request, &String::from( format!("(b) field (mode) must have an octal-prefixed value of form 0o755, was {}", templated_mode_string) ))); } }; } else { // mode was left off in the automation content final_mode_value = None; } return Ok(Some(FileAttributesEvaluated { owner: handle.template.string_option_no_spaces(request, tm, &String::from("owner"), &input2.owner)?, group: handle.template.string_option_no_spaces(request, tm, &String::from("group"), &input2.group)?, mode: final_mode_value, })); } } impl FileAttributesEvaluated { // if the action has an evaluated Attributes section, the mode will be stored as an octal string like "777", but we need // an integer for some internal APIs like the SSH connection put requests. /* pub fn get_numeric_mode(response: &Arc<Response>, request: &Arc<TaskRequest>, this: &Option<Self>) -> Result<Option<i32>, Arc<TaskResponse>> { return match this.is_some() { true => { let mode = &this.as_ref().unwrap().mode; match mode { Some(x) => { let value = FileAttributesInput::octal_string_to_number(response, &request, &x)?; return Ok(Some(value)); }, None => Ok(None) } }, false => Ok(None), }; } */ }07070100000056000081A400000000000000000000000165135CC100001865000000000000000000000000000000000000002200000000jetporch-0.0.1/src/tasks/logic.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use crate::handle::handle::TaskHandle; use crate::tasks::request::TaskRequest; use std::sync::Arc; use crate::tasks::response::TaskResponse; use serde::Deserialize; use crate::handle::template::BlendTarget; use crate::playbooks::templar::TemplateMode; // this is storage behind all 'and' and 'with' statements in the program, which // are mostly implemented in task_fsm #[derive(Deserialize,Debug,Clone)] #[serde(deny_unknown_fields)] pub struct PreLogicInput { pub condition: Option<String>, pub subscribe: Option<String>, pub sudo: Option<String>, pub items: Option<ItemsInput>, pub tags: Option<Vec<String>>, pub delegate_to: Option<String> } #[derive(Deserialize,Debug,Clone)] #[serde(untagged)] pub enum ItemsInput { ItemsString(String), ItemsList(Vec<String>), } #[derive(Debug)] pub struct PreLogicEvaluated { pub condition: bool, pub subscribe: Option<String>, pub sudo: Option<String>, pub items: Option<ItemsInput>, pub tags: Option<Vec<String>> } #[derive(Deserialize,Debug)] #[serde(deny_unknown_fields)] pub struct PostLogicInput { pub notify: Option<String>, pub ignore_errors: Option<String>, pub retry: Option<String>, pub delay: Option<String> } #[derive(Debug)] pub struct PostLogicEvaluated { pub notify: Option<String>, pub ignore_errors: bool, pub retry: u64, pub delay: u64, } impl PreLogicInput { pub fn template(handle: &TaskHandle, request: &Arc<TaskRequest>, tm: TemplateMode, input: &Option<Self>) -> Result<Option<PreLogicEvaluated>,Arc<TaskResponse>> { if input.is_none() { return Ok(None); } let input2 = input.as_ref().unwrap(); return Ok(Some(PreLogicEvaluated { condition: match &input2.condition { Some(cond2) => handle.template.test_condition(request, tm, cond2)?, None => true }, sudo: handle.template.string_option_no_spaces(request, tm, &String::from("sudo"), &input2.sudo)?, subscribe: handle.template.no_template_string_option_trim(&input2.subscribe), items: input2.items.clone(), tags: input2.tags.clone() })); } } impl PostLogicInput { pub fn template(handle: &TaskHandle, request: &Arc<TaskRequest>, tm: TemplateMode, input: &Option<Self>) -> Result<Option<PostLogicEvaluated>,Arc<TaskResponse>> { if input.is_none() { return Ok(None); } let input2 = input.as_ref().unwrap(); return Ok(Some(PostLogicEvaluated { notify: handle.template.string_option_trim(request, tm, &String::from("notify"), &input2.notify)?, // unsafe here means the options cannot be sent to the shell, which they are not. delay: handle.template.integer_option(request, tm, &String::from("delay"), &input2.delay, 1)?, ignore_errors: handle.template.boolean_option_default_false(request, tm, &String::from("ignore_errors"), &input2.ignore_errors)?, retry: handle.template.integer_option(request, tm, &String::from("retry"), &input2.retry, 0)?, })); } } /* this is called from the task_fsm, not above */ pub fn template_items(handle: &Arc<TaskHandle>, request: &Arc<TaskRequest>, tm: TemplateMode, items_input: &Option<ItemsInput>) -> Result<Vec<serde_yaml::Value>, Arc<TaskResponse>> { return match items_input { None => Ok(empty_items_vector()), // with/items: varname Some(ItemsInput::ItemsString(x)) => { let blended = handle.run_state.context.read().unwrap().get_complete_blended_variables( &handle.host, BlendTarget::NotTemplateModule ); match blended.contains_key(&x) { true => { let value : serde_yaml::Value = blended.get(&x).unwrap().clone(); match value { serde_yaml::Value::Sequence(vs) => template_serde_sequence(handle, request, tm, vs), _ => { return Err(handle.response.is_failed(request, &format!("with/items variable did not resolve to a list"))); } } }, false => { return Err(handle.response.is_failed(request, &format!("variable not found for items: {}", x))) } } }, Some(ItemsInput::ItemsList(x)) => { let mut output : Vec<serde_yaml::Value> = Vec::new(); for item in x.iter() { output.push(serde_yaml::Value::String(handle.template.string(request, tm, &String::from("items"), item)?)); } Ok(output) } } } pub fn empty_items_vector() -> Vec<serde_yaml::Value> { return vec![serde_yaml::Value::Bool(true)]; } pub fn template_serde_sequence( handle: &TaskHandle, request: &Arc<TaskRequest>, tm: TemplateMode, vs: serde_yaml::Sequence) -> Result<Vec<serde_yaml::Value>,Arc<TaskResponse>> { let mut output : Vec<serde_yaml::Value> = Vec::new(); for seq_item in vs.iter() { match seq_item { serde_yaml::Value::String(x) => { output.push(serde_yaml::Value::String(handle.template.string(request, tm, &String::from("items"), x)?)) }, x => { output.push(x.clone()) } } } return Ok(output); } 07070100000057000081A400000000000000000000000165135CC100000590000000000000000000000000000000000000002000000000jetporch-0.0.1/src/tasks/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. pub mod request; pub mod response; pub mod common; pub mod logic; pub mod files; pub mod fields; pub mod cmd_library; pub mod checksum; pub use crate::connection::command::cmd_info; pub use crate::tasks::common::{IsTask,IsAction,EvaluatedTask}; pub use crate::tasks::logic::{PreLogicInput,PreLogicEvaluated,PostLogicInput,PostLogicEvaluated}; pub use crate::handle::handle::{TaskHandle,CheckRc}; pub use crate::tasks::response::{TaskResponse,TaskStatus}; pub use crate::tasks::request::{TaskRequestType,TaskRequest}; pub use crate::tasks::files::{FileAttributesInput,FileAttributesEvaluated}; pub use crate::tasks::fields::Field; pub use crate::playbooks::templar::TemplateMode; 07070100000058000081A400000000000000000000000165135CC100000F2B000000000000000000000000000000000000002400000000jetporch-0.0.1/src/tasks/request.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. //use std::collections::HashMap; use std::sync::Arc; use crate::tasks::fields::Field; use std::vec::Vec; // task requests are objects given to modules (and the task FSM) that // describe what questions we are asking of them. In the case of // modifications, this includes the list (map) of parameters to change // as returned by the query request #[derive(Debug,PartialEq)] pub enum TaskRequestType { Validate, Query, Create, Remove, Modify, Execute, Passive, } #[derive(Debug)] pub struct TaskRequest { pub request_type: TaskRequestType, pub changes: Vec<Field>, pub sudo_details: Option<SudoDetails> } #[derive(Debug,PartialEq,Clone)] pub struct SudoDetails { pub user: Option<String>, pub template: String } // most of the various methods in task requests are constructors for different TaskRequest type variants // as used by task_fsm.rs. impl TaskRequest { pub fn validate() -> Arc<Self> { return Arc::new( Self { request_type: TaskRequestType::Validate, changes: Vec::new(), sudo_details: None } ) } pub fn query(sudo_details: &SudoDetails) -> Arc<Self> { return Arc::new( Self { request_type: TaskRequestType::Query, changes: Vec::new(), sudo_details: Some(sudo_details.clone()) } ) } pub fn create(sudo_details: &SudoDetails) -> Arc<Self> { return Arc::new( Self { request_type: TaskRequestType::Create, changes: Vec::new(), sudo_details: Some(sudo_details.clone()) } ) } pub fn remove(sudo_details: &SudoDetails) -> Arc<Self> { return Arc::new( Self { request_type: TaskRequestType::Remove, changes: Vec::new(), sudo_details: Some(sudo_details.clone()) } ) } pub fn modify(sudo_details: &SudoDetails, changes: Vec<Field>) -> Arc<Self> { return Arc::new( Self { request_type: TaskRequestType::Modify, changes: changes, sudo_details: Some(sudo_details.clone()) } ) } pub fn execute(sudo_details: &SudoDetails) -> Arc<Self> { return Arc::new( Self { request_type: TaskRequestType::Execute, changes: Vec::new(), sudo_details: Some(sudo_details.clone()) } ) } pub fn passive(sudo_details: &SudoDetails) -> Arc<Self> { return Arc::new( Self { request_type: TaskRequestType::Passive, changes: Vec::new(), sudo_details: Some(sudo_details.clone()) } ) } pub fn is_sudoing(&self) -> bool { let sudo_details = &self.sudo_details; if sudo_details.is_none() || sudo_details.as_ref().unwrap().user.is_none() { return false } return true; } }07070100000059000081A400000000000000000000000165135CC1000006A2000000000000000000000000000000000000002500000000jetporch-0.0.1/src/tasks/response.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::sync::Arc; //use std::collections::HashMap; use crate::connection::command::CommandResult; use crate::tasks::logic::{PreLogicEvaluated,PostLogicEvaluated}; use crate::tasks::fields::Field; use std::vec::Vec; // task responses are returns from module calls - they are not // created directly but by helper functions in handle.rs, see // the various modules for examples/usage #[derive(Debug,PartialEq)] pub enum TaskStatus { IsCreated, IsRemoved, IsModified, IsExecuted, IsPassive, IsMatched, IsSkipped, NeedsCreation, NeedsRemoval, NeedsModification, NeedsExecution, NeedsPassive, Failed } #[derive(Debug)] pub struct TaskResponse { pub status: TaskStatus, pub changes: Vec<Field>, pub msg: Option<String>, pub command_result: Arc<Option<CommandResult>>, pub with: Arc<Option<PreLogicEvaluated>>, pub and: Arc<Option<PostLogicEvaluated>> } //impl TaskResponse { //}0707010000005A000041ED00000000000000000000000265135CC100000000000000000000000000000000000000000000001800000000jetporch-0.0.1/src/util0707010000005B000081A400000000000000000000000165135CC100000C60000000000000000000000000000000000000001E00000000jetporch-0.0.1/src/util/io.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::fs; use std::path::{Path}; use std::fs::ReadDir; use std::os::unix::fs::PermissionsExt; use std::process; use std::io::Read; // read a directory as per the normal rust way, but map any errors to strings pub fn jet_read_dir(path: &Path) -> Result<ReadDir, String> { return fs::read_dir(path).map_err( |_x| format!("failed to read directory: {}", path.display()) ) } // call fn on each path in a subdirectory of the original path, each step is allowed // to return an error to stop the walking. pub fn path_walk<F>(path: &Path, mut with_each_path: F) -> Result<(), String> where F: FnMut(&Path) -> Result<(), String> { let read_result = jet_read_dir(path); for entry in read_result.unwrap() { with_each_path(&entry.unwrap().path())?; } Ok(()) } // open a file per the normal rust way, but map any errors to strings pub fn jet_file_open(path: &Path) -> Result<std::fs::File, String> { return std::fs::File::open(path).map_err( |_x| format!("unable to open file: {}", path.display()) ); } pub fn read_local_file(path: &Path) -> Result<String,String> { let mut file = jet_file_open(path)?; let mut buffer = String::new(); let read_result = file.read_to_string(&mut buffer); match read_result { Ok(_) => {}, Err(x) => { return Err(format!("unable to read file: {}, {:?}", path.display(), x)); } }; return Ok(buffer.clone()); } // get the last part of the file ignoring the directory part pub fn path_basename_as_string(path: &Path) -> String { return path.file_name().unwrap().to_str().unwrap().to_string(); } // get the last part of the file ignoring the directory part pub fn path_as_string(path: &Path) -> String { return path.to_str().unwrap().to_string(); } pub fn directory_as_string(path: &Path) -> String { return path.parent().unwrap().to_str().unwrap().to_string(); } pub fn quit(s: &String) { // quit with a message - don't use this except in main.rs! println!("{}", s); process::exit(0x01) } pub fn is_executable(path: &Path) -> bool { let metadata = match fs::metadata(path) { Ok(x) => x, Err(_) => return false, }; let permissions = metadata.permissions(); if ! metadata.is_file() { return false; } let mode_bits = permissions.mode() & 0o111; if mode_bits == 0 { return false; } return true; }0707010000005C000081A400000000000000000000000165135CC100000314000000000000000000000000000000000000001F00000000jetporch-0.0.1/src/util/mod.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. pub mod io; pub mod yaml; pub mod terminal; 0707010000005D000081A400000000000000000000000165135CC1000006B9000000000000000000000000000000000000002400000000jetporch-0.0.1/src/util/terminal.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. pub fn markdown_print(markdown: &String) { termimad::print_text(markdown); } pub fn banner(msg: &String) { let markdown = String::from(format!("|:-|\n\ |{}|\n\ |-", msg)); markdown_print(&markdown); } pub fn two_column_table(header_a: &String, header_b: &String, elements: &Vec<(String,String)>) { let mut buffer = String::from("|:-|:-\n"); buffer.push_str( &String::from(format!("|{}|{}\n", header_a, header_b)) ); for (a,b) in elements.iter() { buffer.push_str(&String::from("|-|-\n")); buffer.push_str( &String::from(format!("|{}|{}\n", a, b)) ); } buffer.push_str(&String::from("|-|-\n")); markdown_print(&buffer); } pub fn captioned_display(caption: &String, body: &String) { banner(caption); println!(""); for line in body.lines() { println!(" {}", line); } println!(""); }0707010000005E000081A400000000000000000000000165135CC100001346000000000000000000000000000000000000002000000000jetporch-0.0.1/src/util/yaml.rs// Jetporch // Copyright (C) 2023 - Michael DeHaan <michael@michaeldehaan.net> + contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // long with this program. If not, see <http://www.gnu.org/licenses/>. use std::path::Path; use std::fs::read_to_string; use crate::util::terminal::banner; const YAML_ERROR_SHOW_LINES:usize = 10; const YAML_ERROR_WIDTH:usize = 180; // things will wrap in terminal anyway // ============================================================================================================== // PUBLIC API // ============================================================================================================== pub fn show_yaml_error_in_context(yaml_error: &serde_yaml::Error, path: &Path) { println!(""); let location = yaml_error.location(); let mut yaml_error_str = String::from(format!("{}", yaml_error)); // FIXME: make a utility function for this and also use it in show.rs yaml_error_str.truncate(YAML_ERROR_WIDTH); if yaml_error_str.len() > YAML_ERROR_WIDTH - 3 { yaml_error_str.push_str("..."); } if location.is_none() { let markdown_table = format!("|:-|\n\ |Error reading YAML file: {}|\n\ |{}|\n\ |-", path.display(), yaml_error_str); crate::util::terminal::markdown_print(&markdown_table); return; } // get the line/column info out of the location object let location = location.unwrap(); let error_line = location.line(); let error_column = location.column(); let lines: Vec<String> = read_to_string(path).unwrap().lines().map(String::from).collect(); let line_count = lines.len(); banner(&format!("Error reading YAML file: {}, {}", path.display(), yaml_error_str).to_string()); //if error_line < YAML_ERROR_SHOW_LINES { // show_start = 1; // } let show_start: usize; let mut show_stop : usize = error_line + YAML_ERROR_SHOW_LINES; if error_line < YAML_ERROR_SHOW_LINES { show_start = 0; } else { show_start = error_line - YAML_ERROR_SHOW_LINES; } if show_stop > line_count { show_stop = line_count; } println!(""); let mut count: usize = 0; for line in lines.iter() { count = count + 1; if count >= show_start && count <= show_stop { if count == error_line { println!(" {count:5}:{error_column:5} | >>> | {}", line); } else { println!(" {count:5} | | {}", line); } } } println!(""); } pub fn blend_variables(a: &mut serde_yaml::Value, b: serde_yaml::Value) { /* saving these notes as useful for template code probably println!("~"); if a.is_mapping() { println!("A: I'm a mapping!"); } else if a.is_string() { println!("A: I'm a string!"); } else if a.is_null() { println!("A: I'm null") } else if a.is_sequence() { println!("A: I'm sequence"); } else { println!("A: I'm something else!"); } if b.is_mapping() { println!("B: I'm a mapping!"); } else if b.is_string() { println!("B: I'm a string!"); } else if b.is_null() { println!("B: I'm null"); } else if a.is_sequence() { println!("B: I'm sequence"); } else { println!("B: I'm something else!"); } */ match (a, b) { (_a @ &mut serde_yaml::Value::Mapping(_), serde_yaml::Value::Null) => { }, (a @ &mut serde_yaml::Value::Mapping(_), serde_yaml::Value::Mapping(b)) => { let a = a.as_mapping_mut().unwrap(); for (k, v) in b { if v.is_sequence() && a.contains_key(&k) && a[&k].is_sequence() { let mut _b = a.get(&k).unwrap().as_sequence().unwrap().to_owned(); _b.append(&mut v.as_sequence().unwrap().to_owned()); a[&k] = serde_yaml::Value::from(_b); continue; } if !a.contains_key(&k) { a.insert(k.to_owned(), v.to_owned()); } else { blend_variables(&mut a[&k], v); } } } (a, b) => { *a = b }, } } 0707010000005F000081A400000000000000000000000165135CC100000176000000000000000000000000000000000000001A00000000jetporch-0.0.1/version.sh#!/bin/sh head=`git rev-parse HEAD` branch=`git rev-parse --abbrev-ref HEAD` date=`date` echo "// auto generated by version.sh script" > src/cli/version.rs echo "pub const GIT_VERSION: &str = \"$head\";" >> src/cli/version.rs echo "pub const GIT_BRANCH: &str = \"$branch\";" >> src/cli/version.rs echo "pub const BUILD_TIME: &str = \"$date\";" >> src/cli/version.rs 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!875 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor