Managing local groups

Posted by Lars Erik Wik
October 1, 2021

Manually managing groups on a large infrastructure can be a tedious task, and is therefore best suited through automation software like CFEngine. Unfortunately - at time of writing - CFEngine does not have any built-in promise types for managing groups. But fear not; in CFEngine 3.17, custom promise types were introduced. This new exhilarating feature does not only allow for members of our community to make their own custom promise types, but also lets the CFEngine Core developers prototype new future promise types. In this blog post, I’d like to introduce one of our prototypes; the experimental groups promise type.

Getting started

Using CFEngine 3.18 LTS with the default policy set, it is quite easy to get started using the groups promise type. The first step consists of adding the respective promise module and its support library to /var/cfengine/masterfiles/modules/promises/. The files you will need to copy are the following:

  1. groups.py (Promise module)
  2. cfengine.py (Library)

Next we’ll need to tell cf-agent about our custom promise type, where to find it, as well as what interpreter to use in order to execute it. This is typically done through a promise type definition in /var/cfengine/masterfiles/services/init.cf. Open the respective file in your favorite editor, and add the following lines:

promise agent groups
{
  path => "$(sys.workdir)/modules/promises/groups.py";
  interpreter => "/usr/bin/python3";
}

Awesome! We’re all set to start writing some groups promises.

The groups promise type

Groups promises are promises about local groups on a host. They express whether a group shall be present or absent; which users shall be included or excluded as members of the group; as well as what GID the group shall have. These properties are expressed through the attributes policy, members and gid respectively.

In the example bundle below we specify promises about three groups named foo, bar and baz.

bundle agent example
{
  groups:
    "foo"
      policy => "present",
      members => "$(foo.members)";
    "bar"
      members => "$(bar.members)",
      gid => "1234";
    "baz"
      policy => "absent";
}

bundle agent foo
{
  vars:
    "members" string => '{ "include": ["alice", "bob"],
                           "exclude": ["malcom"] }';
}

bundle agent bar
{
  vars:
    "members" string => '{ "only": ["alice"] }';
}

In the first promise statement, we promise that a group named foo shall be present on the host and that its members shall include the users alice and bob, but exclude the user malcom.

Note that in the second promise statement, we do not specify the policy attribute. Still we promise that a group named bar shall be present. This is due to the fact that the policy attribute defaults to "present" if not specified.

Additionally, in the second promise statement, we promise that the members of bar shall only contain the user alice, excluding all others. Please note that the only member-attribute cannot be used in combination with include or exclude.

In the last promise statement, we promise that a group named baz shall be absent from the host, meaning that this groups shall not exist.

By now, you may have noticed that the member attribute takes a JSON string. And you’re probably be wondering why it does not take a member body as illustrated below.

bundle agent example_2
{
  groups:
    "c_experts"
      policy => "present",
      members => c_expert_members;
}

body member c_expert_members
{
  include => { "vpodzime", "olehermanse" };
  exclude => { "larsewi" };
}

The reasoning behind this is that the use of bodies - unfortunately - are still not supported with custom promise types. But this is definitely something we will implement in the near future.

A complete example can be found here. By running this example, you can expect the following output:

$ /var/cfengine/bin/cf-agent -KIf /var/cfengine/masterfiles/services/groups.cf
    info: User promise repaired
    info: User promise repaired
    info: User promise repaired
    info: Created group 'foo'
    info: Added user 'alice' to group 'foo'
    info: Added user 'bob' to group 'foo'
    info: Created group 'bar'
    info: Members of group 'bar' set to only users ['alice']

Managing local groups with CMDB

Combining the groups promise type with CMDB can lead to some powerful use cases.

Lets say you have an issue with one of your hosts. And in order for the developers to debug the host, you would need to grant them temporary access using a local group.

With CMDB you can use Mission Portal to create a temp_members_debug list variable, containing your most blessed developers:

Managing local groups with CMDB

In policy, a promise can add the users from this list as members to the group debug, if and only if the variable is present:

bundle agent add_temp_members_to_debug
{
  groups:
    "debug"
      policy => "present",
      members => '{ "include": ["$(data:variables.temp_members_debug)"] }',
      if => isvariable("data:variables.temp_members_debug");
}

bundle agent __main__
{
  methods:
    "add_temp_members_to_debug";
}

Awesome, right? To expand on this, you could write policy to remove the users when the variable is removed from CMDB. Here is the output from running the example above:

$ cf-agent -KIf /var/cfengine/masterfiles/services/add_temp_members_to_debug.cf
    info: Created group 'debug'
    info: Added user 'vpodzime' to group 'debug'
    info: Added user 'olehermanse' to group 'debug'
    info: Added user 'larsewi' to group 'debug'

Share your thoughts

By now you know all there is to know about the groups promise type. If you choose to use this promise type, or if you have any tips on how we can improve it; please let us know over at GitHub Discussions, we are eager to hear your feedback.