How to best NOT run a command

75 views
Skip to first unread message

Michael Dahlberg

unread,
Feb 8, 2024, 5:02:08 PMFeb 8
to help-cfengine
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.  

First, I know, the logic of the class statement is wrong.  As it is written, the agent will only run the authselect select​ command when SSSD is already present.
However, it seems as if there might be a better way to write this, I'm just not sure what it might be.  Any suggestions are welcome.

Thanks,
Mike

Michael Dahlberg

Nick Anderson

unread,
Feb 8, 2024, 6:58:32 PMFeb 8
to recurs...@gmail.com, help-c...@googlegroups.com

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.

Michael Dahlberg

unread,
Feb 9, 2024, 8:32:45 AMFeb 9
to help-cfengine
Nick:

Thank you very much.  This is exactly what I needed.

Two quick questions:  
(1) We're using 3.18 and it looks as if it uses `ifelse` rather than `if`.  Is that correct?
(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

Thanks again,
Mike

Nick Anderson

unread,
Feb 9, 2024, 1:54:13 PMFeb 9
to recurs...@gmail.com, help-cfengine

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
Message has been deleted

Michael Dahlberg

unread,
Feb 13, 2024, 10:45:09 AMFeb 13
to Nick Anderson, help-cfengine
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.

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?

Thanks again,
Mike

Nick Anderson

unread,
Feb 13, 2024, 11:46:11 AMFeb 13
to recurs...@gmail.com, help-cfengine

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.

Nick Anderson

unread,
Feb 13, 2024, 11:53:40 AMFeb 13
to help-cfengine
oh, it seems that authselect list-features profile_id doesn't list enabled features, it lists features that could be enabled. So, seems you would need to parse the currently enabled features out from `authselect current` instead.

Nick Anderson

unread,
Feb 13, 2024, 12:03:56 PMFeb 13
to help-cfengine
Couldn't resist looking a bit closer ...

seems authselect current -r lists profile with enabled features space separated.

```
[root@hub ~]# authselect current -r
sssd
[root@hub ~]# authselect current
Profile ID: sssd
Enabled features: None
[root@hub ~]# authselect enable-feature without-nullok
Make sure that SSSD service is configured and enabled. See SSSD documentation for more information.

[root@hub ~]# authselect current
Profile ID: sssd
Enabled features:
- without-nullok
[root@hub ~]# authselect current -r
sssd without-nullok
[root@hub ~]#

```
Reply all
Reply to author
Forward
0 new messages