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, &notify.clone());
                    host.write().unwrap().notify(play_count, &notify.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
openSUSE Build Service is sponsored by