From b89a5a812f016d5152633a1ad172eb9ff905d13a Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 17:44:49 +0200 Subject: [PATCH 1/6] Added policy-dispatcher experimental module Co-authored-by: Claude Fable 5 Signed-off-by: Ole Herman Schumacher Elgesem --- cfbs.json | 42 ++++++++++++++ management/policy-dispatcher/README.md | 40 +++++++++++++ management/policy-dispatcher/def.json | 19 +++++++ management/policy-dispatcher/main.cf | 79 ++++++++++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 management/policy-dispatcher/README.md create mode 100644 management/policy-dispatcher/def.json create mode 100644 management/policy-dispatcher/main.cf diff --git a/cfbs.json b/cfbs.json index 2892337..9864d8b 100644 --- a/cfbs.json +++ b/cfbs.json @@ -397,6 +397,48 @@ } ] }, + "policy-dispatcher": { + "description": "Policy dispatcher for saving policy snippets to files and running them with cf-agent", + "subdirectory": "management/policy-dispatcher", + "steps": [ + "copy main.cf services/cfbs/modules/policy-dispatcher/main.cf", + "policy_files services/cfbs/modules/policy-dispatcher/main.cf", + "bundles policy_dispatcher:main", + "input ./input.json def.json" + ], + "input": [ + { + "type": "list", + "variable": "policy_files_to_run", + "namespace": "policy_dispatcher", + "bundle": "main", + "label": "Policy", + "subtype": [ + { + "key": "policy", + "type": "string", + "label": "Policy", + "question": "Policy snippet to save and run (must contain a 'main' bundle)" + }, + { + "key": "condition", + "type": "string", + "label": "Condition", + "question": "Condition for when to run", + "default": "any" + }, + { + "key": "ifelapsed", + "type": "string", + "label": "ifelapsed", + "question": "Number of minutes between promise assessments", + "default": "5" + } + ], + "while": "Do you want to specify more policy snippets to be run?" + } + ] + }, "promise-type-ansible": { "description": "Promise type to manage systemd services.", "subdirectory": "promise-types/ansible", diff --git a/management/policy-dispatcher/README.md b/management/policy-dispatcher/README.md new file mode 100644 index 0000000..6d03d23 --- /dev/null +++ b/management/policy-dispatcher/README.md @@ -0,0 +1,40 @@ +This module enables the running of CFEngine policy snippets based on input from the Mission-Portal's build page or `cfbs input`. + +Each policy snippet is saved to a file in `$(sys.statedir)` and then run with `cf-agent --no-lock --file `, based on the `condition`-variable. + +**Note:** +The policy snippets are run with root-privilege (uid=0). +Each snippet must be a complete, standalone policy file, containing a `main` bundle (this is what `cf-agent` runs by default). + +--- + +**Usage:** + +* `policy` - The policy snippet to save and run. Must contain a `main` bundle. + +* `condition` - Condition for running. Use a class expression (e.g., `linux|bsd`). Defaults to `"any"` + +* `ifelapsed` - Number of minutes between assessments. Defaults to 5 minutes. + +E.g. +```json +... +{ +"policy": "bundle agent main\n{\n reports:\n \"Hello World\";\n}\n", +"condition": "linux", +"ifelapsed": "5" +}, +... +``` +Would report "Hello World" on every linux device with 5 minute intervals. + +--- + +## Contribute + +Feel free to open pull requests to expand this documentation, add features or fix problems. +You can also pick up an existing task or file an issue in [our bug tracker](https://tracker.mender.io/issues/). + +## License + +This software is licensed under the MIT License. See LICENSE in the root of the repository for the full license text. diff --git a/management/policy-dispatcher/def.json b/management/policy-dispatcher/def.json new file mode 100644 index 0000000..81a59cf --- /dev/null +++ b/management/policy-dispatcher/def.json @@ -0,0 +1,19 @@ +{ + "variables": { + "policy_dispatcher:main.policy_files_to_run": { + "value": [ + { + "policy": "bundle agent main\n{\n reports:\n \"Hello World\";\n}\n", + "condition": "any", + "ifelapsed": "5" + }, + { + "policy": "bundle agent main\n{\n files:\n \"/tmp/policy-dispatcher-example.txt\"\n create => \"true\",\n content => \"This file is managed by the policy-dispatcher module\n\";\n}\n", + "condition": "linux", + "ifelapsed": "60" + } + ], + "comment": "Example data generated by 'cfbs input' / Mission Portal build page" + } + } +} diff --git a/management/policy-dispatcher/main.cf b/management/policy-dispatcher/main.cf new file mode 100644 index 0000000..b4ec9c8 --- /dev/null +++ b/management/policy-dispatcher/main.cf @@ -0,0 +1,79 @@ +body file control +{ + namespace => "policy_dispatcher"; +} + +bundle agent main +{ + classes: + "enabled" expression => isvariable("policy_files_to_run"); + "run_$(i)" expression => "$(_condition[$(i)])"; + + vars: + enabled:: + "i" slist => getindices(policy_files_to_run); + + "_policy[$(i)]" + string => "$(policy_files_to_run[$(i)][policy])", + if => isvariable("policy_files_to_run[$(i)][policy]"); + + "_condition[$(i)]" + string => ifelse( + not(strcmp("$(policy_files_to_run[$(i)][condition])", "")), + "$(policy_files_to_run[$(i)][condition])", + "any" + ); + + "_ifelapsed[$(i)]" + string => ifelse( + not(strcmp("$(policy_files_to_run[$(i)][ifelapsed])", "")), + "$(policy_files_to_run[$(i)][ifelapsed])", + "5" + ); + + "_file[$(i)]" string => "$(sys.statedir)/policy-dispatcher-$(i).cf"; + + files: + enabled:: + "$(_file[$(i)])" + create => "true", + content => "$(_policy[$(i)])", + perms => mo("600", "root"), + if => "run_$(i)"; + + reports: + enabled:: + "Policy [$(i)]: $(_file[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; + + !enabled:: + "Policy-dispatcher: policy_files_to_run variable not found"; + + commands: + enabled:: + "$(sys.cf_agent) --no-lock --file $(_file[$(i)])" + if => "run_$(i)", + action => ifelapsed("$(_ifelapsed[$(i)])"); +} + +body perms mo(mode, owner) +{ + mode => "$(mode)"; + owners => { "$(owner)" }; + rxdirs => "false"; +} + +body action ifelapsed(x) +{ + ifelapsed => "$(x)"; +} + +body file control +{ + namespace => "default"; +} + +bundle agent __main__ +{ + methods: + "policy_dispatcher:main"; +} From bcecc2a9076f3962a51554dbf9f508a43f51f4ff Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 17:46:10 +0200 Subject: [PATCH 2/6] cfengine format with the newest version Signed-off-by: Ole Herman Schumacher Elgesem --- management/manage-fwupd/main.cf | 4 ++++ management/package-method-winget/package-method-winget.cf | 2 ++ 2 files changed, 6 insertions(+) diff --git a/management/manage-fwupd/main.cf b/management/manage-fwupd/main.cf index f62cc43..5304c72 100644 --- a/management/manage-fwupd/main.cf +++ b/management/manage-fwupd/main.cf @@ -133,6 +133,7 @@ bundle agent main "fwupd-refresh.timer" service_policy => "enabled", comment => "Ensure the fwupd metadata refresh timer is enabled so the LVFS firmware catalog stays current"; + # --- Stale marker cleanup ---------------------------------------- files: linux:: @@ -140,6 +141,7 @@ bundle agent main delete => default:tidy, if => not("_update_applied"), comment => "Remove stale marker from a previous boot cycle"; + # --- Apply firmware updates -------------------------------------- commands: linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: @@ -151,6 +153,7 @@ bundle agent main if => and("_device_allowed_$(_dev_idx)", not("_update_applied")), classes => default:results("bundle", "manage_fwupd_update_$(_dev_idx)"), comment => "Apply firmware update to allowed device $(_dev_name[$(_dev_idx)])"; + # --- Post-update marker and reboot -------------------------------- files: linux.default:have_fwupdmgr.manage_fwupd:apply_updates:: @@ -184,6 +187,7 @@ bundle agent main }, handle => "manage_fwupd_reboot_after_update_fallback", comment => "Fallback reboot for non-systemd systems (shutdown -r +1)"; + # --- Inventory -------------------------------------------------- vars: linux.default:have_fwupdmgr:: diff --git a/management/package-method-winget/package-method-winget.cf b/management/package-method-winget/package-method-winget.cf index c393c31..6fccf5b 100644 --- a/management/package-method-winget/package-method-winget.cf +++ b/management/package-method-winget/package-method-winget.cf @@ -6,6 +6,7 @@ body package_method winget package_name_convention => "$(name)"; package_delete_convention => "$(name)"; package_installed_regex => ".*"; + # Note that package_list_name_regex does not allow for commas inside of package names as parsing that from ConvertTo-Csv below would be too complex # an example of output of the package_list_command below is # @@ -14,6 +15,7 @@ body package_method winget # so the matches below simply grab the thing before or after the comma separating Id and InstalledVersion fields. package_list_name_regex => '^"([^"]*)",.*'; package_list_version_regex => '.*,"([^"]*)".*'; + # Here we use the Get-WinGetPackage Cmdlet because it is easier to produce easily parsed information that way package_list_command => "$(sys.winsysdir)\\WindowsPowerShell\\v1.0\\powershell.exe -Command \"Get-WinGetPackage | Select Id,InstalledVersion | ConvertTo-Csv "; package_delete_command => "$(sys.winsysdir)\\WindowsPowerShell\\v1.0\\powershell.exe -Command \"Uninstall-WinGetPackage "; From fa920014040a2a6db0117abe9b03fc9a4b99d32e Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 19:24:01 +0200 Subject: [PATCH 3/6] policy-dispatcher: Changed to use folder in statedir Signed-off-by: Ole Herman Schumacher Elgesem --- management/policy-dispatcher/main.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/policy-dispatcher/main.cf b/management/policy-dispatcher/main.cf index b4ec9c8..4756ddb 100644 --- a/management/policy-dispatcher/main.cf +++ b/management/policy-dispatcher/main.cf @@ -31,7 +31,7 @@ bundle agent main "5" ); - "_file[$(i)]" string => "$(sys.statedir)/policy-dispatcher-$(i).cf"; + "_file[$(i)]" string => "$(sys.statedir)/policy-dispatcher/$(i).cf"; files: enabled:: From 754da517dde5d1da16c6295337bb721808767ea2 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 19:31:18 +0200 Subject: [PATCH 4/6] policy-dispatcher: Improved README Signed-off-by: Ole Herman Schumacher Elgesem --- management/policy-dispatcher/README.md | 31 ++++++++++---------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/management/policy-dispatcher/README.md b/management/policy-dispatcher/README.md index 6d03d23..b3b7285 100644 --- a/management/policy-dispatcher/README.md +++ b/management/policy-dispatcher/README.md @@ -1,34 +1,27 @@ -This module enables the running of CFEngine policy snippets based on input from the Mission-Portal's build page or `cfbs input`. +This module enables the running of CFEngine policy snippets based on input from Build in Mission Portal or `cfbs input`. -Each policy snippet is saved to a file in `$(sys.statedir)` and then run with `cf-agent --no-lock --file `, based on the `condition`-variable. +Each policy snippet is saved to a file in `/var/cfengine/state/policy-dispatcher/.cf` and then run with `cf-agent --no-lock --file `, based on the `condition`-variable. -**Note:** -The policy snippets are run with root-privilege (uid=0). +**Note:** The policy snippets are run with root-privilege (uid=0). Each snippet must be a complete, standalone policy file, containing a `main` bundle (this is what `cf-agent` runs by default). ---- - **Usage:** -* `policy` - The policy snippet to save and run. Must contain a `main` bundle. - -* `condition` - Condition for running. Use a class expression (e.g., `linux|bsd`). Defaults to `"any"` - -* `ifelapsed` - Number of minutes between assessments. Defaults to 5 minutes. +- `policy` - The policy snippet to save and run. Must contain a `main` bundle. +- `condition` - Condition for running. Use a class expression (e.g., `linux|bsd`). Defaults to `"any"` +- `ifelapsed` - Number of minutes between assessments. Defaults to 5 minutes. E.g. + ```json -... { -"policy": "bundle agent main\n{\n reports:\n \"Hello World\";\n}\n", -"condition": "linux", -"ifelapsed": "5" -}, -... + "policy": "bundle agent main { reports: \"Hello World\"; } ", + "condition": "linux", + "ifelapsed": "5" +} ``` -Would report "Hello World" on every linux device with 5 minute intervals. ---- +Would report "Hello World" on every linux device with 5 minute intervals. ## Contribute From cd99668537fded29b2e7b1917fb4eea0a5e6277f Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 19:34:28 +0200 Subject: [PATCH 5/6] policy-dispatcher: Small cleanups and improvements Signed-off-by: Ole Herman Schumacher Elgesem --- management/policy-dispatcher/main.cf | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/management/policy-dispatcher/main.cf b/management/policy-dispatcher/main.cf index 4756ddb..cccf9e4 100644 --- a/management/policy-dispatcher/main.cf +++ b/management/policy-dispatcher/main.cf @@ -7,13 +7,12 @@ bundle agent main { classes: "enabled" expression => isvariable("policy_files_to_run"); - "run_$(i)" expression => "$(_condition[$(i)])"; vars: enabled:: "i" slist => getindices(policy_files_to_run); - "_policy[$(i)]" + "_policy_file[$(i)]" string => "$(policy_files_to_run[$(i)][policy])", if => isvariable("policy_files_to_run[$(i)][policy]"); @@ -37,21 +36,21 @@ bundle agent main enabled:: "$(_file[$(i)])" create => "true", - content => "$(_policy[$(i)])", + content => "$(_policy_file[$(i)])", perms => mo("600", "root"), - if => "run_$(i)"; + if => "$(_condition[$(i)])"; reports: - enabled:: - "Policy [$(i)]: $(_file[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; + enabled.DEBUG:: + "Policy file [$(i)]: $(_file[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; - !enabled:: - "Policy-dispatcher: policy_files_to_run variable not found"; + !enabled.DEBUG:: + "policy-dispatcher: policy_files_to_run variable not found"; commands: enabled:: "$(sys.cf_agent) --no-lock --file $(_file[$(i)])" - if => "run_$(i)", + if => "$(_condition[$(i)])", action => ifelapsed("$(_ifelapsed[$(i)])"); } From 80d8e71e98fea79379023cfb49d5dbc3ab8668e6 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 16 Jul 2026 19:55:24 +0200 Subject: [PATCH 6/6] policy-dispatcher: Clean up leftover files Signed-off-by: Ole Herman Schumacher Elgesem --- management/policy-dispatcher/main.cf | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/management/policy-dispatcher/main.cf b/management/policy-dispatcher/main.cf index cccf9e4..c606ca4 100644 --- a/management/policy-dispatcher/main.cf +++ b/management/policy-dispatcher/main.cf @@ -32,6 +32,14 @@ bundle agent main "_file[$(i)]" string => "$(sys.statedir)/policy-dispatcher/$(i).cf"; + "_expected_files" + slist => maplist("$(sys.statedir)/policy-dispatcher/$(this).cf", @(i)); + + "_actual_files" + slist => findfiles("$(sys.statedir)/policy-dispatcher/*.cf"); + + "_unexpected_files" slist => difference(_actual_files, _expected_files); + files: enabled:: "$(_file[$(i)])" @@ -40,6 +48,8 @@ bundle agent main perms => mo("600", "root"), if => "$(_condition[$(i)])"; + "$(_unexpected_files)" delete => tidy; + reports: enabled.DEBUG:: "Policy file [$(i)]: $(_file[$(i)]), condition: $(_condition[$(i)]), ifelapsed: $(_ifelapsed[$(i)])"; @@ -54,6 +64,14 @@ bundle agent main action => ifelapsed("$(_ifelapsed[$(i)])"); } +body delete tidy +# @brief Delete the file and remove empty directories +# and links to directories +{ + dirlinks => "delete"; + rmdirs => "true"; +} + body perms mo(mode, owner) { mode => "$(mode)";