Feature Friday #37: Decisions based on arbitrary semantic versions

Posted by Nick Anderson
November 22, 2024

Ever need to make a decision based on the version of something? The version_compare() function might be useful for you.1

Over time, software changes and features are added and removed. Sometimes, we need to make a decision based on versions. For example, the Include directive in ssh_config was introduced in OpenSSH 7.3.2 Let’s take a look at how we could possibly use it.

This example illustrates the basic use of version_compare():

/tmp/feature-friday-37-0.cf
bundle agent __main__
{
  vars:
    "version_1" string => "7.3.0";
    "version_2" string => "7.2.0";
    "comparisons"
      slist => { "=",  "==", "!=", ">", "<", ">=", "<=" };

  reports:
    "$(version_1) is $(comparisons) $(version_2)"
      if => version_compare( "$(version_1)", "$(comparisons)", "$(version_2)" );
}

The example above compares version_1 against version_2 using each of the supported comparison operators, and it emits a report when the comparison evaluates to true. Running the example, we see the expected reports:

command
cf-agent -Kf /tmp/feature-friday-37-0.cf
output
R: 7.3.0 is != 7.2.0
R: 7.3.0 is > 7.2.0
R: 7.3.0 is >= 7.2.0

For something a bit more realistic, let’s extend the example to test against the currently installed version of openssh-client:

/tmp/feature-friday-37-1.cf
bundle agent __main__
{
  vars:
    "installed_version_data"
      data => packagesmatching("openssh-client", ".*", ".*", ".*");
    "ssh_conf[HashKnownHosts]"
      string => "yes";
    "ssh_conf[Include]"
      string => "/etc/ssh/ssh_config.d/*.conf",
      if => version_compare(
        # Debian version numbers may contain a leading epoch:string that is unrelated to the upstream package version
        # Here, if it exists we strip it.
        regex_replace( "$(installed_version_data[0][version])","^\d+:(.*)", "$1", "" ),
        ">=",
        "7.3.0"
      );

  reports:
    "ssh_conf for openssh-client '$(installed_version_data[0][version])' should include: $(with)"
      with => join( ", ", getindices( ssh_conf ) );
}

Remember that version_compare() compares semantic versions. But not all versions you encounter are semantic. For example, Debian package versions may include a leading epoch followed by a colon, resulting in a version that looks like this 1:8.9p1-3ubuntu0.6. In that case, you must extract the “real” version you want to compare, which we did in the example using regex_replace(). Here ssh-conf[Include] is defined as /etc/ssh/ssh_config.d/*.conf if the version of the installed package matching openssh-client with the epoch stripped if it’s present is greater than or equal to 7.3.0.

"ssh_conf[Include]"
  string => "/etc/ssh/ssh_config.d/*.conf",
  if => version_compare(
    # Debian version numbers may contain a leading epoch: string that is unrelated to the upstream package version
    # Here, if it exists we strip it.
    regex_replace( "$(installed_version_data[0][version])", "^\d+:(.*)", "$1", "" ),
    ">=",
    "7.3.0"
  );

Running the policy we can see that configuration for both HashKnownHosts and Include should be included:

command
cf-agent -Kf /tmp/feature-friday-37-1.cf
output
R: ssh_conf for openssh-client '1:8.9p1-3ubuntu0.6' should include: HashKnownHosts, Include

If you want to play around with this you can simply define a data container matching the results of the packagesmatching() function call you expect. For example, here we can see the result if the version of the package installed was 6.9p1-3ubuntu0.6:

/tmp/feature-friday-37-2.cf
bundle agent __main__
{
  vars:
      # "installed_version_data" data => packagesmatching( "openssh-client",
      #                                                    ".*", ".*", ".*" );
    "installed_version_data" data => '[
  {
    "arch": "amd64",
    "method": "apt_get",
    "name": "openssh-client",
    "version": "6.9p1-3ubuntu0.6"
  }
]';

    "ssh_conf[HashKnownHosts]"
      string => "yes";

    "ssh_conf[Include]"
      string => "/etc/ssh/ssh_config.d/*.conf",
      if => version_compare(
        # Debian version numbers may contain a leading epoch: string that is unrelated to the upstream package version
        # Here, if it exists we strip it.
        regex_replace( "$(installed_version_data[0][version])", "^\d+:(.*)", "$1", "" ),
        ">=",
        "7.3.0"
      );

  reports:
    "ssh_conf for openssh-client '$(installed_version_data[0][version])' should include: $(with)"
      with => join( ", ", getindices( ssh_conf ) );
}

Executing the policy we can see that only configuration for HashKnownHosts should be included since 6.9p1-3ubuntu0.6 is not greater than or equal to 7.3.0.

command
cf-agent -Kf /tmp/feature-friday-37-2.cf
output
R: ssh_conf for openssh-client '6.9p1-3ubuntu0.6' should include: HashKnownHosts

Happy Friday! 🎉


  1. The version_compare() function was introduced in CFEngine 3.23.0, find the documentation for it here: https://docs.cfengine.com/docs/3.23/reference-functions-version_compare.html ↩︎

  2. The OpenSSH 7.3 release notes indicate addition of Include directive for ssh_config files: https://www.openssh.com/txt/release-7.3 ↩︎