Feature Friday #26: Groups custom promise type

Posted by Nick Anderson
September 6, 2024

There’s a users promise type for managing local users. However, did you know there is also a custom one for managing local groups?

You might have seen it mentioned in the CFEngine Build announcement, the blog post on Managing local groups, or in the announcement supporting custom bodies post. But let’s take another look. The easiest way to integrate the groups custom promise type is by using cfbs, simply cfbs add promise-type-groups in your project. Next, we need some policy that leverages the groups promise type. Let’s create groups.cf in the projects root directory and add it to the project with cfbs add ./groups.cf, selecting the option to add the groups bundle to the bundlesequence.

groups.cf
bundle agent groups
{
  vars:
    "characters" slist => { "yoda", "obi-wan", "luke", "dooku", "anakin" };

  users:
    "$(characters)" policy => "present";

  groups:
    "jedi"
      policy => "present",
      members => jedi;

    "sith"
      policy => "present",
      members => sith;
}
body members jedi
{
  include => { "yoda", "obi-wan", "luke" };
  exclude => { "dooku", "anakin" };
}
body members sith
{
  exclude => { "yoda", "obi-wan", "luke" };
  include => { "dooku", "anakin" };
}

After running cfbs build and cfbs install, we can see it in action when we run the policy:

command
cf-agent -KIf update.cf && cf-agent -KI
output
<snip>
    info: Created user 'yoda'
    info: Created user 'obi-wan'
    info: Created user 'luke'
    info: Created user 'dooku'
    info: Created user 'anakin'
    info: Created group 'jedi'
    info: Added user 'yoda' to group 'jedi'
    info: Added user 'obi-wan' to group 'jedi'
    info: Added user 'luke' to group 'jedi'
    info: Created group 'sith'
    info: Added user 'dooku' to group 'sith'
    info: Added user 'anakin' to group 'sith'
<snip>

It created the groups and users as expected. And if we add anakin to the jedi group manually, we see that it gets removed on the subsequent execution:

[root@hub cfbs]# usermod --groups jedi --append anakin
[root@hub cfbs]# getent group jedi
jedi:x:1006:yoda,obi-wan,luke,anakin
[root@hub cfbs]# cf-agent -KI
    info: Removed user 'anakin' from group 'jedi'
[root@hub cfbs]# getent group jedi
jedi:x:1006:yoda,obi-wan,luke

Unfortunately, bodies in custom promise types cannot currently accept a list directly as a parameter, so if you try to abstract the membership like this:

bundle agent groups
{
  vars:
    "jedi" slist => { "yoda", "obi-wan", "luke" };
    "sith" slist => { "dooku", "anakin" };
    "characters" slist => { @(jedi), @(sith) };

  users:
    "$(characters)" policy => "present";

  groups:
    "jedi"
      policy => "present",
      members => members( @(jedi), @(sith) ) ;

    "sith"
      policy => "present",
      members => members( @(sith), @(jedi) ) ;
}
body members members(include, exclude)
{
  include => { "@(include)" };
  exclude => { "@(exclude)" };
}

It won’t work, you will see errors about undefined variables:

error: groups promise with promiser 'jedi' has unresolved/unexpanded variables
error: groups promise with promiser 'sith' has unresolved/unexpanded variables
error: groups promise with promiser 'jedi' has unresolved/unexpanded variables
error: groups promise with promiser 'sith' has unresolved/unexpanded variables

But that can be worked around by simply passing the name of the variable to the members’ body and expanding it there like this:

bundle agent groups
{
  vars:
    "jedi" slist => { "yoda", "obi-wan", "luke" };
    "sith" slist => { "dooku", "anakin" };
    "characters" slist => { @(jedi), @(sith) };

  users:
    "$(characters)" policy => "present";

  groups:
    "jedi"
      policy => "present",
      members => members( "groups.jedi", "groups.sith" ) ;

    "sith"
      policy => "present",
      members => members( "groups.sith", "groups.jedi" ) ;
}
body members members(include, exclude)
{
  include => { "@($(include))" };
  exclude => { "@($(exclude))" };
}

Happy Friday! 🎉