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