I have a bundle that I need to troubleshoot and I'm hoping there is a better way of writing this.
What I have:
classes: "has_sssd_authselect" expression => returrnszero("/usr/bin/authselect current -r | grep sssd > /dev/null 2>&1","useshell");
commands: has_sssd_authselect:: "/usr/bin/authselect select sssd –force"
Basically, I want to run authselect current -r every time the agent runs but only run authselect select sssd –force when SSSD is NOT present.
Hi Mike,
If this is the only place that you are using that constraint then I think that it might be slightly better to constrain the commands promise using if
rather than define a class via a classes promise that uses returnszero()
. It's less to read and it would bypass the extra executions of functions in classes and vars promises that occurs during pre evaluation.
bundle agent __main__ { commands: "/usr/bin/authselect select sssd --force" if => not( returnzero( "/usr/bin/authselect current -r | grep sssd > /dev/null 2>&1", "useshell" ) ); }
But, if you are using that constraint for multiple promises then I think it reads better to define a class and then use that class to guard multiple promises like in your snippet.
Maybe that authselect current
info is otherwise useful. If that's the case, then it might make more sense to inventory the output from authselect parsing it into variables that you could reference from other places in policy or for some kind of reporting.
Two quick questions:
I guess I might not be capable of providing "quick" answers ….
(1) We're using 3.18 and it looks as if it uses `ifelse` rather than `if`. Is that correct?
No, ifelse()
is a way to set a variable based on a series of conditions.
bundle agent __main__ { vars: # using ifelse() you can do this: "X" string => ifelse( "windows", "The 'windows' class is defined", strcmp( "$(sys.class)", "linux" ), "The variable 'sys.class' expands to 'linux'", "None of the prior conditions are true"); # Instead of this: windows:: "Y" string => "windows"; any:: "Y" string => "The variable 'sys.class' expands to 'linux'", if => strcmp( "$(sys.class)", "linux" ); # Look, here is if constraining this single promise "Y" string => "None of the prior conditions are true, Y hasn't been defined yet", if => not( isvariable( "Y" )); reports: "X = $(X)"; "Y = $(Y)"; }
R: X = The variable 'sys.class' expands to 'linux' R: Y = The variable 'sys.class' expands to 'linux'
(2) In the "Agent is In", you and Craig usually do a test run of the bundles from the command line. Can you do that just by $ cf-agent name-of-bundle
There are a few ways, one might fit you better than the other.
I don't know if you have heard of Emacs org-mode, but that is where I spend most of my time and I usually prototype policy there where I can simply execute the block and see the result. Now, ob-cfengine3
doesn't know how to work with tramp (for executing on remote hosts), so I mostly use it for simple prototypes and examples. Someday maybe I will be able to run the policy from a block of code on a remote host.
I talked about that a bit in Episode 12 of The Agent is In, Spacemacs for CFEngine. https://www.youtube.com/watch?v=rlJiIdhHYUY
To your question, if you want to run a specific bundle name you use the -b
or --bundlesequence
parameter.
For example, I have /tmp/my-policy.cf
, and it's got a couple of bundles in it:
bundle agent my_bundle { reports: "Hello from $(this.bundle)"; } bundle agent my_other_bundle { reports: "Goodbye from $(this.bundle)"; }
I can use -f
or --file
to select that policy file as the entry (overriding the default of $(sys.inputdir)/promises.cf
:
cf-agent -Kf /tmp/my-policy.cf --bundlesequence my_bundle
R: Hello from my_bundle
I can specify multiple bundles by separating them with a comma:
cf-agent -Kf /tmp/my-policy.cf --bundlesequence my_bundle,my_other_bundle
R: Hello from my_bundle R: Goodbye from my_other_bundle
If I don't specify a policy file, then I am running the default policy entry. For example, here I run the host_info_report
bundle from the MPF:
cf-agent -K --bundlesequence host_info_report cat /var/cfengine/reports/host_info_report.txt
R: Host info report generated and available at '/var/cfengine/reports/host_info_report.txt' # Host Information Generated: Fri Feb 9 18:42:56 2024 ## Identity Fully Qualified Hostname: hub.example.com Host ID: SHA=d3f7672d6fabbf1d217bc4176bac3d87a3bc8f7fa280b27115f96ca476b36d30 ## CFEngine Version: CFEngine Enterprise 3.21.4 Last Agent Run: 2024-02-09 18:19:38 UTC Policy Release ID: 824d55f0f260a79987b85f58e04bad9077ac6c85 Policy Last Updated: 2024-02-09 17:23:41 UTC Bootstrapped to: 192.168.56.2 ## OS Architecture: x86_64 Os: linux Release: 3.10.0-1160.105.1.el7.x86_64 Flavor: centos_7 Version: #1 SMP Thu Dec 7 15:39:45 UTC 2023 Uptime: 85 minutes ## Hardware No. CPUs: 2 Total Memory: 1837.61 MB Total Swap: 2048.00 MB Free Memory: 228.32 MB Free Swap: 2047.49 MB ## Network ### Interfaces * eth0: IPv4 10.0.2.15 * eth0: up broadcast running multicast * eth1: IPv4 10.0.2.15 * eth1: up broadcast running multicast * lo: IPv4 127.0.0.1 * lo: up loopback running ### IPv4 TCP Ports listening * 25 * 5432 * 22 ## Policy Information about the policy set on this host. ### Inventory #### Variables tagged for inventory { "default:cfe_autorun_inventory_disk.free": "97.00", "default:cfe_autorun_inventory_dmidecode.dmi[bios-vendor]": "innotek GmbH", "default:cfe_autorun_inventory_dmidecode.dmi[bios-version]": "VirtualBox", "default:cfe_autorun_inventory_dmidecode.dmi[system-manufacturer]": "innotek GmbH", "default:cfe_autorun_inventory_dmidecode.dmi[system-product-name]": "VirtualBox", "default:cfe_autorun_inventory_dmidecode.dmi[system-serial-number]": "0", "default:cfe_autorun_inventory_dmidecode.dmi[system-uuid]": "0EEB2A73-9E20-8946-B4E1-6AF38B7DEEDA", "default:cfe_autorun_inventory_dmidecode.total_physical_memory_MB": "0", "default:cfe_autorun_inventory_ip_addresses.ipv4[10.0.2.15]": "10.0.2.15", "default:cfe_autorun_inventory_ip_addresses.ipv4[192.168.56.2]": "192.168.56.2", "default:cfe_autorun_inventory_listening_ports.ports": [ "22", "25", "80", "443", "5308", "5432" ], "default:cfe_autorun_inventory_memory.total": "1837.61", "default:cfe_autorun_inventory_policy_servers._policy_servers": [ "192.168.56.2" ], "default:cfe_autorun_inventory_policy_servers._primary_policy_server": "192.168.56.2", "default:cfe_autorun_inventory_timezone.gmt_offset": "+0000", "default:cfe_autorun_inventory_timezone.timezone": "UTC", "default:def.control_server_allowusers": [], "default:def.mpf_admit_cf_runagent_shell_selected": [ "192.168.56.2" ], "default:inventory_any.id": "824d55f0f260a79987b85f58e04bad9077ac6c85", "default:inventory_any.policy_version": "CFEngine Promises.cf 3.21.4", "default:inventory_os.description": "CentOS 7", "default:sys.arch": "x86_64", "default:sys.cf_version": "3.21.4", "default:sys.class": "linux", "default:sys.cpus": "2", "default:sys.flavor": "centos_7", "default:sys.fqhost": "hub.example.com", "default:sys.hardware_addresses": [ "08:00:27:64:b1:d3", "08:00:27:7b:b7:66" ], "default:sys.host": "hub.example.com", "default:sys.interfaces": [ "eth0", "eth1" ], "default:sys.ipv4": "10.0.2.15", "default:sys.key_digest": "SHA=d3f7672d6fabbf1d217bc4176bac3d87a3bc8f7fa280b27115f96ca476b36d30", "default:sys.os_release": { "ANSI_COLOR": "0;31", "BUG_REPORT_URL": "https://bugs.centos.org/", "CENTOS_MANTISBT_PROJECT": "CentOS-7", "CENTOS_MANTISBT_PROJECT_VERSION": "7", "CPE_NAME": "cpe:/o:centos:centos:7", "HOME_URL": "https://www.centos.org/", "ID": "centos", "ID_LIKE": "rhel fedora", "NAME": "CentOS Linux", "PRETTY_NAME": "CentOS Linux 7 (Core)", "REDHAT_SUPPORT_PRODUCT": "centos", "REDHAT_SUPPORT_PRODUCT_VERSION": "7", "VERSION": "7 (Core)", "VERSION_ID": "7" }, "default:sys.release": "3.10.0-1160.105.1.el7.x86_64", "default:sys.uptime": "85", "default:sys.uqhost": "hub" } ### Enterprise maintanance bundles available cfengine_enterprise_federation:entry default:cfe_internal_clear_last_seen_hosts_logs default:cfe_internal_enterprise_policy_analyzer default:cfe_internal_exported_report_location default:cfe_internal_refresh_events_table default:cfe_internal_refresh_hosts_view default:cfe_internal_refresh_inventory_view default:cfe_internal_update_health_failures
There is also the library __main__
bundle. If bundle agent __main__
is found in the policy entry ($(sys.policy_entry_filename)
) then it is understood as bundle agent main
and main
is the default bundle sequence. So, this can be handy for various things, running partial policy manually or testing.
So, if I have /tmp/example-library-main.cf
bundle agent __main__ { methods: "bundle1"; } bundle agent bundle1 { reports: "Hello from $(this.bundle)"; }
When I run it as the policy entry bundle1
gets run without having to specify it.
cf-agent -Kf /tmp/example-library-main.cf
R: Hello from bundle1
The nice thing is that you can have one bundle agent __main__
per file and still not get duplicate definition of bundle errors.
Let's say I also have /tmp/example-library-main2.cf
which includes /tmp/example-library-main2.cf
as an additional policy file to parse.
body file control { inputs => { "example-library-main.cf" }; } bundle agent __main__ { methods: "bundle2"; } bundle agent bundle2 { reports: "Hello from $(this.bundle)"; }
echo "Running with /tmp/example-library-main2.cf as entry" cf-agent -Kf /tmp/example-library-main2.cf echo "Running with /tmp/example-library-main.cf as entry" cf-agent -Kf /tmp/example-library-main.cf
Running with /tmp/example-library-main2.cf as entry R: Hello from bundle2 Running with /tmp/example-library-main.cf as entry R: Hello from bundle1
Nick:
This is very helpful. I watched that Agent-is-In episode three times this weekend and worked on setting up my own cfengine installation. I've been working with Org-Mode since we talked about it a couple of months ago.
Great, org-mode is fantastic, and paired with ob-cfengine3 at least I find it super great for prototyping small policy.
My real question that I wanted to ask was about the structure of the `if` statement since I wanted to do something like this:
bundle agent main { vars: "el_authselect_features" slist => { "with-custom-group", "with-custom-passwd", "with-mkhomedir", "with-sudo", "with-files-domain", "without-nullok" };
commands: "/usr/bin/authselect select sssd –force" AND "/usr/bin/authselect enable-feature $(el_authselect_features)" if => not( returnzero( "/usr/bin/authselect current -r | grep sssd > /dev/null 2>&1", "useshell" ) ); }
I was thinking how to use `canonify`, but I'm not sure that works in this case. Is what I'm trying to do possible?
It's not clear to me what you are looking for here.
Is it that you want to run authselect select sssd --force && authselect enable-feature with-custom-group with-custom-password with-mkhomedir with-sudo with-files-domain without-nullok
or you want to run authselect select sssd --force && authselect enable-feature with-custom-group && authselect enable-feature with-custom-password && authselect enable-feature with-mkhomedir && authselect enable-feature with-sudo && authselect enable-feature with-files-domain && authselect enable-feature without-nullok
?
There are few ways to run multiple commands in a single promise. If that is what you want generally you put the multiple commands into a script and run the script as a commands promise. If you need to communicate more back to the agent then that script could output in the variables and classes module protocol format. Alternatively if you execute the commands promise in a shell you can use &&
or ;
to separate the individual commands within the single statement.
Also, what happens if after you get into your desired state an intern fed Gizmo after midnight, then Spike comes along and runs authselect disable-feature with-custom-group
?
Your condition (authselect current -r | grep sssd
) will pass, and no commands will be run. Just glancing here at some random doc result on authselect
I see that authselect current
returns a list of enabled features:
$ authselect current Profile ID: sssd Enabled features: - with-sudo - with-mkhomedir - with-smartcard
It seems you can also get enabled features for a given profile with authselect list-features profile_id
.
# authselect list-features sssd with-custom-automount with-custom-group with-custom-netgroup with-custom-passwd with-custom-services with-faillock with-files-access-provider with-fingerprint with-mkhomedir with-pam-u2f with-pam-u2f-2fa with-pamaccess with-silent-lastlog with-smartcard with-smartcard-lock-on-removal with-smartcard-required with-sudo without-nullok without-pam-u2f-nouserok
You could use difference()
to figure out which desired features are not enabled so that you can enable the proper ones, you could also use difference()
to figure out which enabled features are not explicitly desired (maybe you want to disable those).
bundle agent __main__ { vars: "current_features_enabled" # slist => string_split( "/usr/bin/authselect list-features", "\n", inf ); slist => { "with-sudo", "with-mkhomedir", "with-smartcard" }; "desired_features_enabled"
slist => { "with-custom-group", "with-custom-passwd", "with-mkhomedir", "with-sudo", "with-files-domain"
, "without-nullok", }; "desired_features_missing" slist => difference( "desired_features_enabled", "current_features_enabled" ); "enabled_features_not_explicitly_desired" slist => difference( "current_features_enabled","desired_features_enabled" ); reports: "Missing desired feature: $(desired_features_missing)"; "/usr/bin/authselect enable-feature $(desired_features_missing)"; "Extra features enabled: $(with)" with => join( ", ", enabled_features_not_explicitly_desired ); "/usr/bin/authselect disable-feature $(enabled_features_not_explicitly_desired)?"; }
R: Missing desired feature: with-custom-group R: Missing desired feature: with-custom-passwd R: Missing desired feature: with-files-domain R: Missing desired feature: without-nullok R: /usr/bin/authselect enable-feature with-custom-group R: /usr/bin/authselect enable-feature with-custom-passwd R: /usr/bin/authselect enable-feature with-files-domain R: /usr/bin/authselect enable-feature without-nullok R: Extra features enabled: with-smartcard R: /usr/bin/authselect disable-feature with-smartcard?
So, that might result in some policy like this (no warranty, untested!):
bundle agent authselect_sssd { methods: "authselect_profile"; "authselect_features"; } bundle agent authselect_profile {
commands: "/usr/bin/authselect select sssd --force"
if => not( returnszero( "/usr/bin/authselect current -r | grep sssd > /dev/null 2>&1", "useshell" ) ); } bundle agent authselect_features { vars: "current_profile" string => execresult( "/usr/bin/authselect current -r", noshell); "current_features_enabled" slist => string_split( "/usr/bin/authselect list-features", "\n", inf ); "desired_features_enabled"
slist => { "with-custom-group", "with-custom-passwd", "with-mkhomedir", "with-sudo", "with-files-domain"
, "without-nullok", }; "desired_features_missing" slist => difference( "desired_features_enabled", "current_features_enabled" ); "enabled_features_not_explicitly_desired" slist => difference( "current_features_enabled","desired_features_enabled" ); commands: # Run authselect enable-feature for each feature that is not currently enabled if the current profile is sssd "/usr/bin/authselect enable-feature $(desired_features_enabled)" if => and( strcmp( "$(current_profile)", "sssd" ), isgreaterthan( length( desired_features_missing ), 0 ) ); # Alternatively I think you could use some() matching .* "/usr/bin/authselect disable-feature $(enabled_features_not_explicitly_desired)" if => and( strcmp( "$(current_profile)", "sssd" ), isgreaterthan( length( enabled_features_not_explicitly_desired ), 0 ) ); }
This will result in a fair number of commands being executed during each policy execution, if that turns out to be more overhead than desired for some reason then you could consider caching the result of the probing commands.