diff --git a/Cargo.lock b/Cargo.lock
index d36ae478..1cd0b5d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -912,6 +912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [
"clap_builder",
+ "clap_derive",
]
[[package]]
@@ -926,6 +927,18 @@ dependencies = [
"strsim",
]
+[[package]]
+name = "clap_derive"
+version = "4.5.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
[[package]]
name = "clap_lex"
version = "0.7.7"
@@ -968,7 +981,7 @@ version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1608,6 +1621,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+[[package]]
+name = "erased-serde"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
[[package]]
name = "errno"
version = "0.3.14"
@@ -1615,7 +1639,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2657,7 +2681,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2796,7 +2820,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "642883fdc81cf2da15ee8183fa1d2c7da452414dd41541a0f3e1428069345447"
dependencies = [
"scopeguard",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2939,6 +2963,7 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
name = "libmodcore"
version = "0.1.0"
dependencies = [
+ "clap",
"colored",
"indexmap 2.13.0",
"libsysinspect",
@@ -2947,6 +2972,8 @@ dependencies = [
"serde_json",
"serde_yaml",
"shlex",
+ "strum 0.27.2",
+ "strum_macros 0.27.2",
"textwrap",
]
@@ -2972,6 +2999,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
+ "textwrap",
"tokio",
]
@@ -3226,6 +3254,42 @@ dependencies = [
"hashbrown 0.15.5",
]
+[[package]]
+name = "lua-runtime"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "clap",
+ "libmodcore",
+ "libsysinspect",
+ "log",
+ "mlua",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "lua-src"
+version = "548.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdc4e1aff422ad5f08cffb4719603dcdbc2be2307f4c1510d7aab74b7fa88ca8"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "luajit-src"
+version = "210.6.4+e17ee83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a0ceb2a395ffa403a863adcf365e82cc8d8338ac7f5f949b9df5ca3de251e1"
+dependencies = [
+ "cc",
+ "which",
+]
+
[[package]]
name = "lz4_flex"
version = "0.12.0"
@@ -3435,6 +3499,38 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "mlua"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "935ac67539907efcd7198137eb7358e052555f77fe1b2916600a2249351f2b33"
+dependencies = [
+ "bstr",
+ "either",
+ "erased-serde",
+ "libc",
+ "mlua-sys",
+ "num-traits",
+ "parking_lot 0.12.5",
+ "rustc-hash 2.1.1",
+ "rustversion",
+ "serde",
+ "serde-value",
+]
+
+[[package]]
+name = "mlua-sys"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c968af21bf6b19fc9ca8e7b85ee16f86e4c9e3d0591de101a5608086bda0ad8"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "lua-src",
+ "luajit-src",
+ "pkg-config",
+]
+
[[package]]
name = "mt19937"
version = "3.1.0"
@@ -3568,7 +3664,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -3877,6 +3973,15 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978aa494585d3ca4ad74929863093e87cac9790d81fe7aba2b3dc2890643a0fc"
+[[package]]
+name = "ordered-float"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "page_size"
version = "0.6.0"
@@ -4447,7 +4552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
dependencies = [
"heck",
- "itertools 0.12.1",
+ "itertools 0.14.0",
"log",
"multimap",
"once_cell",
@@ -4467,7 +4572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
dependencies = [
"anyhow",
- "itertools 0.12.1",
+ "itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.114",
@@ -5197,7 +5302,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5263,7 +5368,7 @@ dependencies = [
"security-framework 3.5.1",
"security-framework-sys",
"webpki-root-certs",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5815,6 +5920,16 @@ dependencies = [
"serde_derive",
]
+[[package]]
+name = "serde-value"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
+dependencies = [
+ "ordered-float",
+ "serde",
+]
+
[[package]]
name = "serde_core"
version = "1.0.228"
@@ -6509,7 +6624,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.3",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -7036,6 +7151,12 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
[[package]]
name = "typenum"
version = "1.19.0"
@@ -7602,7 +7723,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 22b03e88..221a8924 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,6 +31,7 @@ openssl = { version = "0.10.75", features = ["vendored"] }
resolver = "2"
members = [
"modules/sys/*",
+ "modules/runtime/*",
"libsysinspect",
"libeventreg",
"sysmaster",
diff --git a/Makefile b/Makefile
index 4a2b07f4..5a047766 100644
--- a/Makefile
+++ b/Makefile
@@ -40,11 +40,14 @@ define move_bin
mkdir -p $$dir/sys; \
rm -rf $$dir/fs; \
mkdir -p $$dir/fs; \
+ rm -rf $$dir/runtime; \
+ mkdir -p $$dir/runtime; \
mv $$dir/proc $$dir/sys/; \
mv $$dir/net $$dir/sys/; \
mv $$dir/run $$dir/sys/; \
mv $$dir/ssrun $$dir/sys/; \
- mv $$dir/file $$dir/fs/;
+ mv $$dir/file $$dir/fs/; \
+ mv $$dir/lua-runtime $$dir/runtime/;
endef
setup:
diff --git a/docs/conf.py b/docs/conf.py
index 1a07c728..1347bd29 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,8 +1,8 @@
project = "Sysinspect"
-copyright = "2024, Bo Maryniuk"
+copyright = "2026, Bo Maryniuk"
author = "Bo Maryniuk"
version = "0.4.0"
-release = "Alpha"
+# release = "Alpha"
extensions = [
"myst_parser",
diff --git a/docs/global_config.rst b/docs/global_config.rst
index 3b3d3bc9..593d53e4 100644
--- a/docs/global_config.rst
+++ b/docs/global_config.rst
@@ -557,6 +557,23 @@ Example configuration for the Sysinspect Minion:
master.ip: 192.168.2.31
master.port: 4200
+``log.forward``
+##################
+
+ Type: **boolean**
+
+ Forward logs from actions and modules to the main sysinspect log, landing them in the main log file.
+ If disabled, logs from actions and modules will not be forwarded to the main sysinspect log but are kept
+ within their own context inside the returned data and will travel across the whole network back to the master.
+
+ Thus, disabling this feature on a large cluster might inflate your network traffic so much that your network
+ admin will start believe in ghosts and aliens.
+
+ .. warning::
+
+ Disable this option only if you really know what you are doing.
+
+ Default is ``true``
Layout of ``/etc/sysinspect``
-----------------------------
diff --git a/docs/index.rst b/docs/index.rst
index 258dd73d..6c63ff2e 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -45,6 +45,7 @@ welcome—see the section on contributing for how to get involved.
tutorial/cfgmgmt_tutor
tutorial/action_chain_tutor
tutorial/module_management
+ tutorial/lua_modules_tutor
Licence
diff --git a/docs/moddescr/overview.rst b/docs/moddescr/overview.rst
index d3e46eac..2b67024b 100644
--- a/docs/moddescr/overview.rst
+++ b/docs/moddescr/overview.rst
@@ -36,10 +36,10 @@ programs etc. However there are few rules that needs to be complied:
Refer to the detailed communication protocol documentation in chapter :ref:`commproto`.
-Available modules
------------------
+Control Modules
+---------------
-Below is a list of available modules and their documentation:
+Below is a list of available control modules and their documentation:
.. toctree::
:maxdepth: 1
@@ -48,4 +48,23 @@ Below is a list of available modules and their documentation:
sys_net
sys_run
sys_ssrun
- fs_file
\ No newline at end of file
+ fs_file
+
+Runtime Modules
+----------------
+
+Runtime modules are the same control modules as usual, but they are simply additionally
+running specific targets they are meant to run. For example, WASM runtime, Python runtime, Lua etc.
+These modules are enabling certain types of user-written modules to be executed
+inside Sysinspect ecosystem.
+
+This is done for the reason of security, isolation, portability and customisation.
+For example, there are cases where no Python interpreter is needed at all, so user
+can simply remove that module from the Minion in a whole.
+
+Below is a list of available runtime modules and their documentation:
+
+.. toctree::
+ :maxdepth: 1
+
+ runtime_lua
\ No newline at end of file
diff --git a/docs/moddescr/runtime_lua.rst b/docs/moddescr/runtime_lua.rst
new file mode 100644
index 00000000..9b29c3ee
--- /dev/null
+++ b/docs/moddescr/runtime_lua.rst
@@ -0,0 +1,80 @@
+``runtime.lua``
+===============
+
+.. note::
+
+ This document describes ``runtime.lua`` module usage.
+
+Lua runtime
+-----------
+
+``runtime.lua`` is the SysInspect Lua runtime module. It enables you to execute Lua scripts as SysInspect
+modules, which makes it convenient to extend inspections with small, self-contained checks written in Lua.
+
+At a high level, the runtime:
+
+* discovers Lua scripts in the configured scripts directory,
+* executes a selected script (module) by name,
+* optionally forwards keyword arguments to the script as global variables,
+* can expose logging and native library loading depending on the configured options.
+
+Script lookup and naming
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+When you set the required keyword argument ``rt.mod``, SysInspect searches for a Lua file with that module
+name in the predefined, configured scripts directory.
+
+The runtime treats the script as an entry point. Keep the script focused on one task and prefer importing
+shared helpers from the dependency directory described below.
+
+Directory layout (Lua 5.4)
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To keep module scripts and their dependencies predictable, install files into the following locations
+under ``${SYSINSPECT_SHARELIB_ROOT}``:
+
+1. **Main scripts (modules)**
+
+ Install entry-point scripts into::
+
+ lib/runtime/lua54/
+
+2. **Dependency libraries**
+
+ Install Lua libraries required by your scripts into::
+
+ lib/runtime/lua54/site-lua/
+
+This separation keeps your callable modules easy to list and prevents helper libraries from being treated
+as top-level SysInspect modules.
+
+Options
+-------
+
+``rt.list``
+ List available Lua scripts that can be called as modules.
+
+``rt.logs``
+ Enable logging from Lua scripts into SysInspect logs. Use this for diagnostics and traceability.
+
+``rt.native``
+ Enable loading of native Lua libraries (C modules). Use with caution, since native modules run with the
+ same privileges as the SysInspect process and can widen the runtime's attack surface.
+
+Keyword arguments
+-----------------
+
+``rt.mod`` (type: string, required)
+ The name of the Lua script to execute. The runtime looks it up in the configured scripts directory.
+
+``[ANY]`` (type: string)
+ Additional keyword arguments forwarded to the executed script. These values are made available to the
+ script as global variables.
+
+Practical notes
+---------------
+
+* Keep argument names unambiguous, since they become globals in the script.
+* Prefer passing strings and parsing them in Lua when you need structured values.
+* Use ``rt.logs`` while developing scripts, then disable it if you want quieter operation.
+
diff --git a/docs/tutorial/lua_modules_tutor.rst b/docs/tutorial/lua_modules_tutor.rst
new file mode 100644
index 00000000..0c2f573d
--- /dev/null
+++ b/docs/tutorial/lua_modules_tutor.rst
@@ -0,0 +1,270 @@
+.. raw:: html
+
+
+
+.. role:: u
+ :class: underlined
+
+.. role:: bi
+ :class: bolditalic
+
+.. _lua_tutorial:
+
+Using Lua Modules
+=================
+
+.. note::
+ This tutorial shows how to run Lua code via Sysinspect by installing a Lua runtime module and
+ publishing Lua scripts as modules. It assumes you have a working Sysinspect installation and basic
+ familiarity with Sysinspect concepts such as modules, models, actions, and entities.
+
+Prerequisites
+-----------------
+
+Before you begin, ensure you have the following prerequisites:
+
+- A working installation of Sysinspect.
+- Basic Lua knowledge (functions, modules, returning tables).
+- Access to the SysMaster node (the node hosting the package repository).
+- Permission to register modules and to sync the cluster.
+
+Installing Lua Runtime
+----------------------
+
+Sysinspect runs scripts through *runtime modules*. A runtime module is a normal Sysinspect module
+that ships an interpreter (Lua in this case) plus any runtime glue needed to execute your scripts.
+
+To run Lua scripts, Sysinspect must have a Lua runtime module available in the repository.
+
+Build the runtime
+^^^^^^^^^^^^^^^^^
+
+If you build Sysinspect from source, build the project from the top-level directory:
+
+.. code-block:: bash
+
+ make
+
+After a successful build, the Lua runtime binary is typically located here:
+
+.. code-block:: bash
+
+ $SRC/target/release/runtime/lua-runtime
+
+(Use ``target/debug/...`` if you built a debug configuration.)
+
+Register the runtime
+^^^^^^^^^^^^^^^^^^^^
+
+Register the runtime on the SysMaster so it becomes available in the module repository.
+Replace the path with the actual path to your built binary:
+
+.. code-block:: bash
+
+ sysinspect module -A \
+ --path /path/to/your/target/release/runtime/lua-runtime \
+ --name "runtime.lua-runtime" \
+ --descr "Lua runtime"
+
+What this does:
+
+1. ``--path`` points to the runtime binary you built.
+2. ``--name`` is the module name you will reference from models.
+3. ``--descr`` is a human-readable description (optional).
+
+This adds the runtime into SysMaster's package repository. You can then verify the module is registered
+by listing all available modules:
+
+.. code-block:: bash
+
+ sysinspect module -L
+
+After registering the module, the cluster **needs to be synced** so all nodes receive the new module metadata:
+
+.. code-block:: bash
+
+ sysinspect --sync
+
+Once the runtime is registered and the cluster is synced, you can publish Lua scripts that will run
+through that runtime.
+
+
+Installing Lua Modules
+----------------------
+
+Sysinspect can ship plain script files as modules. For Lua, the current packaging style is:
+
+* You upload a directory tree (usually named ``lib``).
+* Sysinspect preserves the directory structure.
+* Those scripts are treated as a library attached to the Lua runtime.
+
+Directory layout
+^^^^^^^^^^^^^^^^
+
+Assume your local working directory contains a ``lib`` directory with Lua scripts under a
+runtime-specific path, for example:
+
+.. code-block:: text
+
+ lib/
+ runtime/
+ lua54/
+ hello.lua
+ reader.lua
+ caller.lua
+ site-lua/
+ mathx/
+ init.lua
+ extra.lua
+
+The important part is: upload the *directory* (``./lib``), not individual files, so the runtime
+sees the same module paths on all nodes.
+
+To maintain your set of Lua scripts, you can create your own directory tree like the one above.
+Make changes you need and then re-upload the entire ``./lib`` directory to update the scripts
+in the repository. It will overwrite existing scripts with the same paths.
+
+
+Publish the scripts
+^^^^^^^^^^^^^^^^^^^
+
+Example scripts are provided in the ``modules/runtime/lua-runtime/examples`` directory of the Sysinspect
+source tree. You can freely modify or extend these scripts for your own use. Assuming you navigated to that
+directory, mentioned above, and have a ``./lib`` directory, publish the scripts like this:
+
+1. Add the library tree to the repository (run this from the directory that contains ``./lib``):
+
+ .. code-block:: bash
+
+ sysinspect module -A --path ./lib -l
+
+ The ``-l`` flag tells Sysinspect you are adding a library directory.
+
+ .. important::
+
+ Take a look into the structure of the ``./lib`` directory before uploading.
+ All runtime modules are typically going to ``runtime/`` subdirectories.
+ All dependencies are typically under ``site-`` subdirectories.
+
+ Always upload the *directory* (``./lib``), not individual files, so the runtime
+ sees the same module paths on all nodes.
+
+2. Sync the cluster so all nodes receive the new module metadata:
+
+ .. code-block:: bash
+
+ sysinspect --sync
+
+3. Verify that the scripts are visible:
+
+ .. code-block:: bash
+
+ sysinspect module -Ll
+
+ Example output (yours might be different):
+
+ .. code-block:: text
+
+ Type Name OS Arch SHA256
+ ──────────────────────────────────────────────────────────────────────────
+ script runtime/lua54/caller.lua Any noarch 7aff...d8c5
+ script runtime/lua54/hello.lua Any noarch 22ce...f2e1
+ script runtime/lua54/reader.lua Any noarch 8ce3...0135
+ script runtime/lua54/site-lua/mathx/extra.lua Any noarch 92ce...79e3
+ script runtime/lua54/site-lua/mathx/init.lua Any noarch f636...f314
+
+Once you've done this, the Lua scripts are available to be called from models.
+There are three modules in the example set:
+- ``hello.lua``: A demonstration of using packages and modules (``site-lua/mathx`` in this case).
+- ``reader.lua``: Reads the ``VERSION`` field from ``/etc/os-release`` and returns it.
+- ``caller.lua``: Demonstrates calling an external program from Lua and capturing its output.
+
+This set of examples although minimal, is more than enough to produce simple powerful
+configuration management modules, capable of being extended for any use case.
+
+Calling a Lua module from a model
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In current implementation, you :bi:`do not call Lua modules directly` from actions. To execute these Lua
+modules, you reference the runtime module (``runtime.lua-runtime``) from an action. Your action passes
+arguments telling the runtime which Lua module to invoke.
+
+Example model snippet:
+
+.. code-block:: yaml
+
+ entities:
+ - foo
+
+ actions:
+ my-example:
+ descr: Read VERSION from /etc/os-release via Lua
+ module: runtime.lua-runtime
+ bind:
+ - foo
+ state:
+ $:
+ opts:
+ # Enable logging from Lua scripts to SysInspect logs
+ # Logs will be included in the action result under 'logs' namespace
+ - rt.logs
+ args:
+ rt.mod: reader
+
+The confusing part here is the ``rt.mod`` argument under ``opts.args``. All arguments with the ``rt.*``
+prefix are special runtime arguments that the Lua runtime module understands and are **not** passed to
+the Lua script. Any other arguments (without the ``rt.*`` prefix) are directly passed to the Lua script
+as normal arguments.
+
+.. important::
+
+ The ``rt.*`` arguments are runtime-specific. Different runtimes may have different
+ ``rt.*`` arguments. To know them, run the embedded runtime manpage command. In this case,
+ if you have a standard installation, run this command on the **SysMinion** node:
+
+ .. code-block:: bash
+
+ /usr/lib/sysinspect/runtime/lua-runtime --man
+
+In this example, ``mod: reader`` means you would run the Lua module implemented by ``reader.lua``.
+What *exactly* the Lua runtime expects for module naming depends on the runtime implementation,
+but the intent is: keep your script name stable and call it by module name.
+
+Run the action
+^^^^^^^^^^^^^^
+
+Execute the action against a node (minion) like this:
+
+.. code-block:: bash
+
+ sysinspect yourmodel/foo yourminion
+
+Where:
+
+* ``yourmodel`` is the model directory/name.
+* ``foo`` is the bound entity instance.
+* ``yourminion`` is the hostname.
+
+To target all nodes, you can use ``*``. Once executed, you can also check the action result
+by invokindg the Sysinspect terminal UI, used for merely checking if the results came back correctly:
+
+.. code-block:: bash
+
+ sysinspect --ui
+
+Troubleshooting
+^^^^^^^^^^^^^^^
+
+* If the runtime is missing, confirm ``runtime.lua-runtime`` appears in ``sysinspect module -L``.
+* If scripts are missing, confirm you uploaded ``./lib`` (the directory) and re-ran
+ ``sysinspect --sync``.
+* If module imports fail, verify your ``lib/runtime/lua54/...`` layout matches what the runtime
+ expects, and that any ``site-lua`` modules are located under the same tree.
diff --git a/libmodcore/Cargo.toml b/libmodcore/Cargo.toml
index 758997b3..bb744f1c 100644
--- a/libmodcore/Cargo.toml
+++ b/libmodcore/Cargo.toml
@@ -13,3 +13,6 @@ serde_yaml = "0.9.34"
shlex = "1.3.0"
textwrap = "0.16.2"
libsysinspect = { path = "../libsysinspect" }
+clap = { version = "4.5.53", features = ["derive"] }
+strum = "0.27.2"
+strum_macros = "0.27.2"
diff --git a/libmodcore/src/lib.rs b/libmodcore/src/lib.rs
index e7d7e98c..cf79368b 100644
--- a/libmodcore/src/lib.rs
+++ b/libmodcore/src/lib.rs
@@ -1,8 +1,12 @@
use indexmap::IndexMap;
use shlex::Shlex;
+pub mod manrndr;
+pub mod modcli;
pub mod modinit;
pub mod response;
+pub mod rtdocschema;
+pub mod rtspec;
pub mod runtime;
pub mod tpl;
diff --git a/libmodcore/src/manrndr.rs b/libmodcore/src/manrndr.rs
new file mode 100644
index 00000000..b6f2f48b
--- /dev/null
+++ b/libmodcore/src/manrndr.rs
@@ -0,0 +1,230 @@
+use colored::Colorize;
+use serde::Deserialize;
+use serde::de::{Deserializer, Error};
+use serde_json::Value::{self, Array, Null, Object};
+
+#[derive(Debug, Deserialize)]
+pub struct ModuleDoc {
+ pub name: String,
+
+ /// Optional metadata
+ #[serde(default)]
+ pub version: Option,
+ #[serde(default)]
+ pub author: Option,
+
+ /// Required per your validator
+ pub description: String,
+
+ /// Optional sections
+ #[serde(default, deserialize_with = "unvec")]
+ pub arguments: Vec,
+
+ #[serde(default, deserialize_with = "unvec")]
+ pub options: Vec,
+
+ #[serde(default, deserialize_with = "unvec")]
+ pub examples: Vec,
+
+ #[serde(default)]
+ pub returns: Option,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct DocParam {
+ pub name: String,
+
+ /// optional in your validator
+ #[serde(rename = "type", default)]
+ pub ty: Option,
+
+ #[serde(default)]
+ pub required: bool,
+
+ /// Lua doc uses "description"
+ #[serde(default)]
+ pub description: String,
+
+ /// Not in your current schema, but keep compatibility if someone adds it
+ #[serde(default)]
+ pub default: Option,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct DocOption {
+ pub name: String,
+
+ #[serde(default)]
+ pub description: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct DocExample {
+ pub code: String,
+
+ #[serde(default)]
+ pub description: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct DocReturns {
+ #[serde(default)]
+ pub description: String,
+
+ /// Can be anything JSON-ish
+ #[serde(default)]
+ pub sample: Option,
+}
+
+fn unvec<'de, D, T>(deserializer: D) -> Result, D::Error>
+where
+ D: Deserializer<'de>,
+ T: Deserialize<'de>,
+{
+ match Value::deserialize(deserializer)? {
+ Array(arr) => {
+ let mut out = Vec::with_capacity(arr.len());
+ for item in arr {
+ out.push(T::deserialize(item).map_err(Error::custom)?);
+ }
+ Ok(out)
+ }
+ Object(obj) if obj.is_empty() => Ok(vec![]),
+ Null => Ok(vec![]),
+ other => Err(Error::custom(format!("expected array ([]) or empty object ({{}}), got {other}"))),
+ }
+}
+
+pub fn print_mod_manual(doc_val: Value) {
+ let doc: ModuleDoc = match serde_json::from_value(doc_val) {
+ Ok(d) => d,
+ Err(err) => {
+ eprintln!("Failed to parse module doc: {err}");
+ return;
+ }
+ };
+
+ // ---- Header ----
+ println!("{}:\n {}\n", "Name".bright_yellow(), doc.name.yellow());
+
+ if doc.version.is_some() || doc.author.is_some() {
+ println!("{}:", "Meta".bright_yellow());
+ if let Some(v) = &doc.version {
+ println!(" {} {}", "Version:".bright_white(), v.white());
+ }
+ if let Some(a) = &doc.author {
+ println!(" {} {}", "Author:".bright_white(), a.white());
+ }
+ println!();
+ }
+
+ println!("{}:", "Description".bright_yellow());
+ for line in doc.description.trim().lines() {
+ println!(" {}", line.yellow());
+ }
+ println!();
+
+ // ---- Arguments ----
+ if !doc.arguments.is_empty() {
+ println!("{}", "Keyword arguments:".bright_yellow());
+ print_args(&doc.arguments);
+ }
+
+ // ---- Options ----
+ if !doc.options.is_empty() {
+ println!("{}", "Options:".bright_yellow());
+ print_options(&doc.options);
+ }
+
+ // ---- Examples ----
+ if !doc.examples.is_empty() {
+ println!("{}", "Usage examples:".bright_yellow());
+ for (i, ex) in doc.examples.iter().enumerate() {
+ if !ex.description.trim().is_empty() {
+ print!(" {} ", ((i + 1).to_string() + ".").yellow());
+ for (j, line) in ex.description.trim().lines().enumerate() {
+ println!("{}{}", if j == 0 { "" } else { " " }, line.yellow());
+ }
+ } else {
+ println!(" Example {}", (i + 1).to_string().yellow());
+ }
+ for line in ex.code.trim().lines() {
+ println!(" {}", line.bright_white());
+ }
+ println!();
+ }
+ }
+
+ // ---- Returns ----
+ if let Some(ret) = &doc.returns {
+ println!("{}", "Returns:".bright_yellow());
+
+ if !ret.description.trim().is_empty() {
+ for line in ret.description.trim().lines() {
+ println!(" {}", line.yellow());
+ }
+ }
+
+ if let Some(sample) = &ret.sample {
+ match serde_json::to_string_pretty(sample) {
+ Ok(s) => {
+ for line in s.trim().lines() {
+ println!(" {}", line.bright_white());
+ }
+ }
+ Err(_) => println!(" {}", "".bright_red()),
+ }
+ }
+
+ println!();
+ }
+}
+
+fn print_args(params: &[DocParam]) {
+ let name_width = params.iter().map(|p| p.name.len()).max().unwrap_or(0);
+
+ for p in params {
+ // Build metadata like: "type: string, required" or "type: string, default: foo"
+ let mut meta = String::new();
+
+ if let Some(ty) = &p.ty {
+ meta.push_str(&format!("type: {}", ty));
+ } else {
+ meta.push_str("type: ");
+ }
+
+ if p.required {
+ meta.push_str(", required");
+ } else if let Some(def) = &p.default {
+ meta.push_str(&format!(", default: {}", def));
+ }
+
+ let meta_col = meta.magenta();
+
+ println!(" {:width$} ({})", p.name.bright_green().bold(), meta_col, width = name_width);
+
+ if !p.description.trim().is_empty() {
+ for line in p.description.lines() {
+ println!(" {}", line.bright_white());
+ }
+ }
+
+ println!();
+ }
+}
+
+fn print_options(opts: &[DocOption]) {
+ let name_width = opts.iter().map(|o| o.name.len()).max().unwrap_or(0);
+
+ for o in opts {
+ println!(" {:width$}", o.name.bright_green().bold(), width = name_width);
+
+ if !o.description.trim().is_empty() {
+ for line in o.description.lines() {
+ println!(" {}", line.bright_white());
+ }
+ }
+
+ println!();
+ }
+}
diff --git a/libmodcore/src/modcli.rs b/libmodcore/src/modcli.rs
new file mode 100644
index 00000000..1160bf69
--- /dev/null
+++ b/libmodcore/src/modcli.rs
@@ -0,0 +1,95 @@
+use clap::{
+ Parser,
+ builder::{
+ Styles,
+ styling::{AnsiColor, Style},
+ },
+};
+pub static SHARELIB: &str = "/usr/share/sysinspect/lib/{}"; // Add runtime ID
+
+pub fn monokai_style() -> Styles {
+ Styles::styled()
+ // section headers: "USAGE", "OPTIONS"
+ .header(
+ Style::new()
+ .fg_color(Some(AnsiColor::Yellow.into()))
+ .bold(),
+ )
+ // the "Usage:" line content
+ .usage(
+ Style::new()
+ .fg_color(Some(AnsiColor::Magenta.into()))
+ .bold(),
+ )
+ // flags and literals: `-m`, `--man`, `--help-on`
+ .literal(
+ Style::new()
+ .fg_color(Some(AnsiColor::Green.into())),
+ )
+ // metavars / value names: ``, ``
+ .placeholder(
+ Style::new()
+ .fg_color(Some(AnsiColor::BrightYellow.into())), // faux-orange
+ )
+}
+
+#[derive(Parser, Debug)]
+#[command(
+ name = env!("CARGO_PKG_NAME"),
+ version = env!("CARGO_PKG_VERSION"),
+ about = env!("CARGO_PKG_DESCRIPTION"),
+ color = clap::ColorChoice::Always,
+ styles = monokai_style(),
+ override_usage = format!("{} [OPTIONS] < ", env!("CARGO_PKG_NAME"))
+)]
+/// CLI definition
+pub struct ModuleCli {
+ /// Show this runtime module operational manual
+ #[arg(short = 'm', long = "man", alias = "manual")]
+ man: bool,
+
+ /// List available runtime modules
+ #[arg(short = 'l', long = "list-modules", alias = "list-plugins")]
+ modules: bool,
+
+ /// Path where runtime modules are located. Default: /usr/share/sysinspect/lib/{runtime-id}
+ #[arg(short = 's', long = "sharelib", value_name = "PATH")]
+ sharelib: Option,
+
+ /// Show operational manual for specific module
+ #[arg(short = 'i', long = "info", value_name = "MODULE")]
+ help_on: Option,
+}
+
+/// CLI definition implementation
+impl ModuleCli {
+ /// Get sharelib path
+ /// # Returns
+ /// Sharelib path
+ pub fn get_sharelib(&self) -> String {
+ self.sharelib.clone().unwrap_or_else(|| SHARELIB.to_string())
+ }
+
+ /// Is manual requested
+ /// # Returns
+ /// `true` if manual requested
+ /// `false` otherwise
+ pub fn is_manual(&self) -> bool {
+ self.man
+ }
+
+ /// Is list plugins requested
+ /// # Returns
+ /// `true` if list plugins requested
+ /// `false` otherwise
+ pub fn is_list_modules(&self) -> bool {
+ self.modules
+ }
+
+ /// Get help on specific module
+ /// # Returns
+ /// Help on module string
+ pub fn get_help_on(&self) -> String {
+ self.help_on.clone().unwrap_or_default()
+ }
+}
diff --git a/libmodcore/src/modinit.rs b/libmodcore/src/modinit.rs
index ac214e92..066f5222 100644
--- a/libmodcore/src/modinit.rs
+++ b/libmodcore/src/modinit.rs
@@ -1,6 +1,9 @@
use colored::Colorize;
use indexmap::IndexMap;
-use libsysinspect::util::dataconv;
+use libsysinspect::util::{
+ dataconv,
+ tty::{indent_block, render_markup},
+};
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -109,6 +112,7 @@ pub struct ModInterface {
options: Vec,
arguments: Vec,
examples: Vec,
+ manpage: Option, // Optional full manpage text with additional details, if needed
// Map of flags/args to output data structure
returns: IndexMap,
@@ -187,13 +191,17 @@ impl ModInterface {
let dsc_title = "Description:".bright_yellow();
let ex_title = "Usage examples:".bright_yellow();
let ex_code = self.examples.iter().map(|e| e.format()).collect::>().join("\n");
+ let manpage = match &self.manpage {
+ Some(m) => format!("\n\n{}", indent_block(&render_markup(m), " ")),
+ None => "".to_string(),
+ };
format!(
"{}, {} (Author: {})
{dsc_title}
- {}
+ {}{}
{}
@@ -204,6 +212,7 @@ impl ModInterface {
self.version.green().bold(),
self.author,
fill(&self.description, Options::new(H_WIDTH).subsequent_indent(" ")).yellow(),
+ manpage,
args(self),
ex_code,
returns(self),
diff --git a/libmodcore/src/rtdocschema.rs b/libmodcore/src/rtdocschema.rs
new file mode 100644
index 00000000..f8cd80fa
--- /dev/null
+++ b/libmodcore/src/rtdocschema.rs
@@ -0,0 +1,140 @@
+use crate::rtspec::{RuntimeModuleDocPrefix, RuntimeModuleDocumentation, RuntimeSpec};
+use libsysinspect::SysinspectError;
+use serde_json::Value as JsonValue;
+
+fn expect_string<'a>(v: &'a JsonValue, path: &str) -> Result<&'a str, SysinspectError> {
+ v.as_str().ok_or_else(|| SysinspectError::ModuleError(format!("{path} must be a string")))
+}
+
+fn expect_bool(v: &JsonValue, path: &str) -> Result {
+ v.as_bool().ok_or_else(|| SysinspectError::ModuleError(format!("{path} must be a boolean")))
+}
+
+fn expect_array<'a>(v: &'a JsonValue, path: &str) -> Result<&'a Vec, SysinspectError> {
+ v.as_array().ok_or_else(|| SysinspectError::ModuleError(format!("{path} must be an array")))
+}
+
+fn expect_object<'a>(v: &'a JsonValue, path: &str) -> Result<&'a serde_json::Map, SysinspectError> {
+ v.as_object().ok_or_else(|| SysinspectError::ModuleError(format!("{path} must be an object")))
+}
+
+fn is_empty_object(v: &JsonValue) -> bool {
+ v.as_object().map(|m| m.is_empty()).unwrap_or(false)
+}
+
+fn validate_obj_list_field(
+ doc_obj: &serde_json::Map, field: &str, item_validator: impl Fn(usize, &JsonValue) -> Result<(), SysinspectError>,
+) -> Result<(), SysinspectError> {
+ let Some(v) = doc_obj.get(field) else {
+ return Ok(()); // not there -> muted
+ };
+
+ if is_empty_object(v) {
+ return Ok(()); // {} -> muted
+ }
+
+ let arr = expect_array(v, &format!("{}.{}", RuntimeSpec::DocumentationFunction, field))?;
+ for (i, item) in arr.iter().enumerate() {
+ item_validator(i, item)?;
+ }
+ Ok(())
+}
+
+pub fn validate_module_doc(doc: &JsonValue) -> Result<(), SysinspectError> {
+ let obj = expect_object(doc, &RuntimeSpec::DocumentationFunction.to_string())?;
+ let p = RuntimeModuleDocPrefix::new(&RuntimeSpec::DocumentationFunction);
+
+ // required
+ let name = obj
+ .get(&RuntimeModuleDocumentation::Name.to_string())
+ .ok_or_else(|| SysinspectError::ModuleError(format!("{} is required", p.field(&RuntimeModuleDocumentation::Name))))?;
+ let _ = expect_string(name, &p.field(&RuntimeModuleDocumentation::Name))?;
+
+ let desc = obj
+ .get(&RuntimeModuleDocumentation::Description.to_string())
+ .ok_or_else(|| SysinspectError::ModuleError(format!("{} is required", p.field(&RuntimeModuleDocumentation::Description))))?;
+ let _ = expect_string(desc, &p.field(&RuntimeModuleDocumentation::Description))?;
+
+ // optional strings
+ if let Some(v) = obj.get(&RuntimeModuleDocumentation::Version.to_string()) {
+ let _ = expect_string(v, &p.field(&RuntimeModuleDocumentation::Version))?;
+ }
+ if let Some(v) = obj.get(&RuntimeModuleDocumentation::Author.to_string()) {
+ let _ = expect_string(v, &p.field(&RuntimeModuleDocumentation::Author))?;
+ }
+
+ // arguments: array of objects (muted if missing or {})
+ validate_obj_list_field(obj, &RuntimeModuleDocumentation::Arguments.to_string(), |i, item| {
+ let p = format!("{}.{}[{i}]", RuntimeSpec::DocumentationFunction, RuntimeModuleDocumentation::Arguments);
+ let aobj = expect_object(item, &p)?;
+
+ let n = aobj
+ .get(&RuntimeModuleDocumentation::Name.to_string())
+ .ok_or_else(|| SysinspectError::ModuleError(format!("{p}.{} is required", RuntimeModuleDocumentation::Name)))?;
+ let _ = expect_string(n, &format!("{p}.{}", RuntimeModuleDocumentation::Name))?;
+
+ if let Some(t) = aobj.get(&RuntimeModuleDocumentation::Type.to_string()) {
+ let _ = expect_string(t, &format!("{p}.{}", RuntimeModuleDocumentation::Type))?;
+ }
+ if let Some(r) = aobj.get(&RuntimeModuleDocumentation::Required.to_string()) {
+ let _ = expect_bool(r, &format!("{p}.{}", RuntimeModuleDocumentation::Required))?;
+ }
+ if let Some(d) = aobj.get(&RuntimeModuleDocumentation::Description.to_string()) {
+ let _ = expect_string(d, &format!("{p}.{}", RuntimeModuleDocumentation::Description))?;
+ }
+ Ok(())
+ })?;
+
+ // options: array of objects (muted if missing or {})
+ validate_obj_list_field(obj, &RuntimeModuleDocumentation::Options.to_string(), |i, item| {
+ let p = format!("{}.{}[{i}]", RuntimeSpec::DocumentationFunction, RuntimeModuleDocumentation::Options);
+ let oobj = expect_object(item, &p)?;
+
+ let n = oobj
+ .get(&RuntimeModuleDocumentation::Name.to_string())
+ .ok_or_else(|| SysinspectError::ModuleError(format!("{p}.{} is required", RuntimeModuleDocumentation::Name)))?;
+ let _ = expect_string(n, &format!("{p}.{}", RuntimeModuleDocumentation::Name))?;
+
+ if let Some(d) = oobj.get(&RuntimeModuleDocumentation::Description.to_string()) {
+ let _ = expect_string(d, &format!("{p}.{}", RuntimeModuleDocumentation::Description))?;
+ }
+ Ok(())
+ })?;
+
+ // examples: array of objects (muted if missing or {})
+ validate_obj_list_field(obj, &RuntimeModuleDocumentation::Examples.to_string(), |i, item| {
+ let p = format!("{}.{}[{i}]", RuntimeSpec::DocumentationFunction, RuntimeModuleDocumentation::Examples);
+ let eobj = expect_object(item, &p)?;
+
+ let code = eobj
+ .get(&RuntimeModuleDocumentation::Code.to_string())
+ .ok_or_else(|| SysinspectError::ModuleError(format!("{p}.{} is required", RuntimeModuleDocumentation::Code)))?;
+ let _ = expect_string(code, &format!("{p}.{}", RuntimeModuleDocumentation::Code))?;
+
+ if let Some(d) = eobj.get(&RuntimeModuleDocumentation::Description.to_string()) {
+ let _ = expect_string(d, &format!("{p}.{}", RuntimeModuleDocumentation::Description))?;
+ }
+ Ok(())
+ })?;
+
+ // returns: object (optional). If present but {} -> muted too.
+ if let Some(ret) = obj.get(&RuntimeModuleDocumentation::Returns.to_string())
+ && !is_empty_object(ret)
+ {
+ let robj = expect_object(ret, &format!("{}.{}", RuntimeSpec::DocumentationFunction, RuntimeModuleDocumentation::Returns))?;
+ if let Some(d) = robj.get(&RuntimeModuleDocumentation::Description.to_string()) {
+ let _ = expect_string(
+ d,
+ &format!(
+ "{}.{}.{}",
+ RuntimeSpec::DocumentationFunction,
+ RuntimeModuleDocumentation::Returns,
+ RuntimeModuleDocumentation::Description
+ ),
+ )?;
+ }
+ // sample: any JSON type ok
+ }
+
+ Ok(())
+}
diff --git a/libmodcore/src/rtspec.rs b/libmodcore/src/rtspec.rs
new file mode 100644
index 00000000..2547ed20
--- /dev/null
+++ b/libmodcore/src/rtspec.rs
@@ -0,0 +1,114 @@
+use strum_macros::{Display, EnumString};
+
+/// RuntimeParams enum
+/// Holds runtime parameter names
+#[derive(Debug, Display, EnumString)]
+pub enum RuntimeParams {
+ #[strum(serialize = "rt.mod")]
+ ModuleName,
+
+ #[strum(serialize = "rt.man")]
+ ModuleManual,
+
+ #[strum(serialize = "rt.")]
+ RtPrefix,
+}
+
+/// RuntimeSpec enum
+/// Holds runtime specification field names
+#[derive(Debug, Display, EnumString)]
+pub enum RuntimeSpec {
+ #[strum(serialize = "run")]
+ MainEntryFunction,
+
+ #[strum(serialize = "doc")]
+ DocumentationFunction,
+
+ /// Logs section field for returning logs from module
+ /// within the returned data object.
+ #[strum(serialize = "__sysinspect-module-logs")]
+ LogsSectionField,
+
+ #[strum(serialize = "data")]
+ DataSectionField,
+}
+
+/// RuntimeModuleDocumentation enum
+/// Holds documentation field names
+#[derive(Debug, Display, EnumString)]
+pub enum RuntimeModuleDocumentation {
+ #[strum(serialize = "name")]
+ Name,
+
+ #[strum(serialize = "version")]
+ Version,
+
+ #[strum(serialize = "description")]
+ Description,
+
+ #[strum(serialize = "author")]
+ Author,
+
+ #[strum(serialize = "license")]
+ License,
+
+ #[strum(serialize = "arguments")]
+ Arguments,
+
+ #[strum(serialize = "options")]
+ Options,
+
+ #[strum(serialize = "examples")]
+ Examples,
+
+ #[strum(serialize = "returns")]
+ Returns,
+
+ #[strum(serialize = "code")]
+ Code,
+
+ #[strum(serialize = "required")]
+ Required,
+
+ #[strum(serialize = "type")]
+ Type,
+}
+
+/// RuntimeModuleDocPrefix struct
+/// Holds prefix for runtime module documentation fields
+/// # Fields
+/// * `prefix` - Prefix string
+pub struct RuntimeModuleDocPrefix {
+ pub prefix: String,
+}
+
+/// RuntimeModuleDocPrefix implementation
+impl RuntimeModuleDocPrefix {
+ /// Create new RuntimeModuleDocPrefix
+ /// # Arguments
+ /// * `prefix` - RuntimeSpec enumeration
+ /// # Returns
+ /// RuntimeModuleDocPrefix instance
+ pub fn new(prefix: &RuntimeSpec) -> Self {
+ Self { prefix: prefix.to_string() }
+ }
+
+ /// Get full field name with prefix
+ /// # Arguments
+ /// * `field` - Field enumeration
+ /// # Returns
+ /// Full field name string
+ pub fn field(&self, field: &RuntimeModuleDocumentation) -> String {
+ format!("{}.{}", self.prefix, field)
+ }
+}
+
+/// Get runtime modules path
+/// # Arguments
+/// * `runtime_id` - Runtime identifier
+/// * `sharelib` - Optional sharelib base path (without `$PATH/lib` part)
+/// # Returns
+/// Runtime modules path
+pub fn runtime_modules_path(runtime_id: &str, sharelib: Option<&str>) -> String {
+ format!("/{}/lib/{runtime_id}", sharelib.unwrap_or("/usr/share/sysinspect"))
+}
diff --git a/libmodcore/src/runtime.rs b/libmodcore/src/runtime.rs
index 15982271..8c581808 100644
--- a/libmodcore/src/runtime.rs
+++ b/libmodcore/src/runtime.rs
@@ -1,12 +1,15 @@
+use crate::rtspec::RuntimeParams;
+
use super::response::ModResponse;
use indexmap::IndexMap;
+use libsysinspect::cfg::mmconf::DEFAULT_MODULES_SHARELIB;
use libsysinspect::util;
use serde::{Deserialize, Serialize};
use std::io::Error;
use std::io::{self, Read};
/// ArgValue is a type converter from input JSON to the internal types
-#[derive(Serialize, Deserialize, Debug, Clone, Default)]
+#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
pub struct ArgValue(serde_json::Value);
impl ArgValue {
@@ -79,26 +82,68 @@ impl ModRequest {
self.quiet.unwrap_or(false).to_owned()
}
+ pub fn options_all(&self) -> Vec {
+ self.options.to_owned().unwrap_or_default()
+ }
+
/// Get param options
pub fn options(&self) -> Vec {
- self.options.to_owned().unwrap_or_default()
+ let mut out = Vec::new();
+ for av in self.options.to_owned().unwrap_or_default() {
+ if let Some(s) = av.as_string()
+ && !s.starts_with(&RuntimeParams::RtPrefix.to_string())
+ {
+ out.push(av);
+ }
+ }
+ out
+ }
+
+ /// Check if an option is present
+ pub fn has_option(&self, opt: &str) -> bool {
+ for av in self.options_all() {
+ if av.as_string().unwrap_or_default().eq(opt) {
+ return true;
+ }
+ }
+ false
}
pub fn config(&self) -> IndexMap {
- self.config.clone().unwrap_or_default()
+ // Inject sharelib path if not defined
+ // Modules not supposed to take explicit care where is their shared library located,
+ // but simply read the configuration. For example, runtimes need to know where to find their
+ // modules.
+ let mut config = self.config.clone().unwrap_or_default();
+ if config.get("path.sharelib").is_none() {
+ config.insert("path.sharelib".to_string(), ArgValue(serde_json::Value::String(DEFAULT_MODULES_SHARELIB.to_string())));
+ }
+ config
}
- /// Get all param args
- pub fn args(&self) -> IndexMap {
+ /// Get all param args including runtime-specific ones (those starting with "rt.")
+ pub fn args_all(&self) -> IndexMap {
self.arguments.clone().unwrap_or_default()
}
+ /// Get all param args without runtime-specific ones (those starting with "rt.")
+ pub fn args(&self) -> IndexMap {
+ let mut target_args = IndexMap::new();
+ for (k, v) in self.arguments.clone().unwrap_or_default() {
+ if !k.starts_with(&RuntimeParams::RtPrefix.to_string()) {
+ target_args.insert(k, v);
+ }
+ }
+ target_args
+ }
+
/// Get arg
pub fn get_arg(&self, kw: &str) -> Option {
if let Some(a) = &self.arguments
- && let Some(a) = a.get(kw) {
- return Some(a.clone());
- };
+ && let Some(a) = a.get(kw)
+ {
+ return Some(a.clone());
+ };
None
}
diff --git a/libmodpak/Cargo.toml b/libmodpak/Cargo.toml
index 068b8065..b79a25a5 100644
--- a/libmodpak/Cargo.toml
+++ b/libmodpak/Cargo.toml
@@ -23,3 +23,4 @@ cruet = "0.15.0"
prettytable = "0.10.0"
glob = "0.3.3"
regex = "1.12.2"
+textwrap = { version = "0.16.2", features = ["hyphenation"] }
diff --git a/libmodpak/src/lib.rs b/libmodpak/src/lib.rs
index 80750076..10511673 100644
--- a/libmodpak/src/lib.rs
+++ b/libmodpak/src/lib.rs
@@ -7,11 +7,12 @@ use libsysinspect::cfg::mmconf::{CFG_AUTOSYNC_FAST, CFG_AUTOSYNC_SHALLOW, DEFAUL
use libsysinspect::{SysinspectError, cfg::mmconf::DEFAULT_MODULES_DIR};
use mpk::{ModAttrs, ModPakMetadata, ModPakRepoIndex};
use once_cell::sync::Lazy;
-use prettytable::Table;
use prettytable::format::{FormatBuilder, LinePosition, LineSeparator};
+use prettytable::{Cell, Row, Table, format};
use std::os::unix::fs::PermissionsExt;
use std::sync::Arc;
use std::{collections::HashMap, fs, path::PathBuf};
+use textwrap::{Options, wrap};
use tokio::sync::Mutex;
pub mod mpk;
@@ -471,44 +472,40 @@ impl SysInspectModPak {
println!("{} {}", padded.bright_yellow(), descr.white());
}
}
- /// Prints a table of modules with their attributes.
- fn print_table(modules: &IndexMap, verbose: bool) {
- let mw = modules.keys().map(|s| s.len()).max().unwrap_or(0);
- let kw = "descr".len().max("type".len());
- let mut mods: Vec<_> = modules.iter().collect();
- mods.sort_by_key(|(name, _)| *name);
-
- for (mname, attrs) in mods {
- let mut m_attrs = [("descr", attrs.descr()), ("type", attrs.mod_type())];
- m_attrs.sort_by_key(|(k, _)| *k);
- if let Some((first_key, first_value)) = m_attrs.first() {
- println!(" {:kw$}: {}", mname.bright_white().bold(), first_key.yellow(), first_value, mw = mw, kw = kw,);
- for (k, v) in m_attrs.iter().skip(1) {
- println!(" {:kw$}: {}", "", k.yellow(), v, mw = mw, kw = kw,);
- }
- // Print additional data, if any
- if verbose {
- if let Some(opts) = attrs.opts() {
- println!("{}", " Options:".yellow());
- for opt in opts {
- Self::print_kv(opt.name(), opt.required(), &opt.description(), 15);
- }
- println!()
- }
+ fn print_table(modules: &IndexMap, _verbose: bool) {
+ let mut t = Table::new();
+ t.set_format(*format::consts::FORMAT_CLEAN);
+ /*
+ t.set_titles(Row::new(vec![
+ Cell::new(" "),
+ Cell::new(&"Name".bright_yellow().bold().to_string()),
+ Cell::new(&"Description".bright_white().bold().to_string()),
+ ]));
+ */
- if let Some(args) = attrs.args() {
- println!("{}", " Arguments:".yellow());
- for arg in args {
- Self::print_kv(arg.name(), arg.required(), &arg.description(), 15);
- }
- }
- }
- } else {
- println!(" {mname: = modules.iter().collect();
+ entries.sort_by(|(a, _), (b, _)| a.cmp(b));
+
+ let wrap_opts = Options::new(50);
+ for (modname, modattr) in entries {
+ let lines = wrap(modattr.descr(), &wrap_opts);
+ if lines.is_empty() {
+ t.add_row(Row::new(vec![Cell::new(" "), Cell::new(&modname.bright_yellow().to_string()), Cell::new("")]));
+ continue;
+ }
+
+ t.add_row(Row::new(vec![Cell::new(" "), Cell::new(&modname.bright_yellow().to_string()), Cell::new(&lines[0])]));
+ for l in lines.iter().skip(1) {
+ t.add_row(Row::new(vec![Cell::new(""), Cell::new(""), Cell::new(l)]));
+ }
+ if lines.len() > 1 {
+ t.add_row(Row::new(vec![Cell::new(""), Cell::new(""), Cell::new("")]));
}
- println!();
}
+
+ t.printstd();
}
/// Lists all libraries in the repository.
diff --git a/libsysinspect/src/cfg/mmconf.rs b/libsysinspect/src/cfg/mmconf.rs
index 31244b5c..9f167c3a 100644
--- a/libsysinspect/src/cfg/mmconf.rs
+++ b/libsysinspect/src/cfg/mmconf.rs
@@ -382,6 +382,11 @@ pub struct MinionConfig {
#[serde(skip_serializing_if = "Option::is_none")]
log_err: Option,
+ // Forward logs from actions and modules to the main sysinspect log
+ #[serde(rename = "log.forward")]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ log_forward: Option,
+
// Pidfile
#[serde(rename = "pidfile")]
#[serde(skip_serializing_if = "Option::is_none")]
@@ -488,6 +493,12 @@ impl MinionConfig {
PathBuf::from(format!("/run/user/{}/sysminion.pid", unsafe { libc::getuid() }))
}
+ /// Forward logs from actions and modules to the main sysinspect log
+ /// Default: true
+ pub fn forward_logs(&self) -> bool {
+ self.log_forward.unwrap_or(true)
+ }
+
/// Return main logfile in daemon mode
pub fn logfile_std(&self) -> PathBuf {
if let Some(lfn) = &self.log_main {
diff --git a/libsysinspect/src/inspector.rs b/libsysinspect/src/inspector.rs
index ef355ea1..cb6ae23b 100644
--- a/libsysinspect/src/inspector.rs
+++ b/libsysinspect/src/inspector.rs
@@ -155,7 +155,7 @@ impl SysInspectRunner {
match self.action_allowed(&ac) {
Ok(is_allowed) => {
if is_allowed {
- match ac.run() {
+ match ac.run(Self::minion_cfg().clone().forward_logs()) {
Ok(response) => {
let response = response.unwrap_or(ActionResponse::default());
self.update_cstr_eval(&response);
diff --git a/libsysinspect/src/intp/actions.rs b/libsysinspect/src/intp/actions.rs
index 07ebbd6e..047ac51c 100644
--- a/libsysinspect/src/intp/actions.rs
+++ b/libsysinspect/src/intp/actions.rs
@@ -4,7 +4,7 @@ use super::{
functions,
inspector::SysInspector,
};
-use crate::{SysinspectError, util::dataconv};
+use crate::{SysinspectError, logger::log_forward, util::dataconv};
use colored::Colorize;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
@@ -17,7 +17,7 @@ pub struct ModArgs {
options: Option>,
#[serde(alias = "args")]
- arguments: Option>,
+ arguments: Option>,
#[serde(alias = "ctx")]
context: Option>, // Context variables definition for Jinja templates. Used only for model documentation.
@@ -28,7 +28,7 @@ pub struct ModArgs {
impl ModArgs {
/// Return args
- pub fn args(&self) -> IndexMap {
+ pub fn args(&self) -> IndexMap {
if let Some(args) = &self.arguments {
return args.to_owned();
}
@@ -133,13 +133,60 @@ impl Action {
}
/// Run action
- pub fn run(&self) -> Result