Writing custom service_methods

Posted by Nick Anderson
June 17, 2019

This post has been re-published with permission.

CFEngine provides the services promise type to manage the state of a given service. services type promises are an abstraction of agent bundles, they can be used to declare the desired state for a collection of things identified by a name. Most commonly services type promises are used to manage standard operating system services though they can be used for abstracting other logical states. By default, bundle agent standard_services is used for the service_method in promises that specify no specific service_method.

Why is CFEngine always or never issuing the command to start or stop a service each agent run?

For sysvinit (non-systemd) hosts, the standard_services bundle uses the status command and interprets the return codes according to the Linux Standard Base init script actions. Unfortunately some init scripts do not follow the standards. If the status command returns zero when a service is not running, cfengine will issue the commands necessary to stop the service every time the agent is run. Similarly, if the status command returns zero when the service is not running, CFEngine will never issue the commands necessary to start a service.

How can I handle a service status that does not comport with the standard?

One way to deal with misbehaving services is to implement a custom service_method that better understands the specifics for a given service.

For example, the MacAfee Agent status command simply emits the current status for it’s services and does not use the expected return codes.

[root@host ]# /etc/init.d/ma status
McAfee agent service is not running.
McAfee common services is not running.
McAfee compat service is not running.
[root@host ]# echo $?
0

First, for testing, create a mock init script that matches the behavior seen in the output above.

Listing 1: Mock /etc/init.d/ma

case $1 in
  status)
      echo McAfee agent service is not running.
      echo McAfee common services is not running.
      echo McAfee compat service is not running.
      ;;
  start)
      echo Starting McAfee agent service.
      echo Starting McAfee common services.
      echo Starting McAfee compat service.
      ;;
  stop)
      echo Stoping McAfee agent service.
      echo Stoping McAfee common services.
      echo Stoping McAfee compat service.
      ;;
  *)
      echo "Unknnown action '$1'. Expecting status|start|stop."
      ;;
  esac
return 0; # Return zero means service OK/Running per the LSB

Next you can write a custom bundle to implement services based on the McAfee behavior.

Listing 2: bundle agent mcafee_services_handler in example_custom_services.cf

bundle agent mcafee_services_handler( service_name, desired_state )
# @brief Works around init script not exiting with the appropriate return code for status
# (per Linux Standard Base init script actions)
{
  vars:

      "valid_states" slist => { "active", "inactive" };
      "init_script" string => "/etc/init.d/ma";

      "ma_status"
        string => execresult( "$(init_script) status", noshell );

  classes:

      # Define a class for the desired state if the state is one we recognize
      "desired_$(desired_state)"
        expression => strcmp( $(desired_state), $(valid_states) );

      # It's nice to know if someone is using the policy incorrectly
      "invalid_desired_state"
        not => some( $(desired_state), @(valid_states) );

      "at_least_one_component_not_running"
        expression => regcmp( '.*not running.*', $(ma_status) ),
        comment => "If any component is not running, then we consider the whole service not running";

      "some_component_running"
        expression => regcmp( '.*is running.*', $(ma_status) );

      "all_components_running"
        expression => not( regcmp( '.*not running.*', $(ma_status) ) ),
        comment => "If no component is not running, then we consider the service running";

  commands:
    desired_active.at_least_one_component_not_running::
      "$(init_script) start";

    desired_inactive.some_component_running::
      "$(init_script) stop";

  reports:
      "Something is wrong, I only expect to be used with 'ma' aka 'MacAfee Agent' but I am being used with '$(this.service_name)'"
        if => not( strcmp( $(service_name), 'ma' ) );

@if minimum_version(3.11)
# This debug message levarages the with attribute to avoid building a
# intermediary variable (joined string) that was only useful in a single report
      "Invalid service state. Selected '$(desired_state)'. Valid states: '$(with)'"
        with => join( ", ", @(valid_states) ),
        if => "invalid_desired_state";
@endif
}

Then you can expose it as a service_method attribute value by defining a service_method body.

Listing 3: body service_method macafee in example_custom_services.cf

body service_method macafee
{
    linux::
        service_bundle => mcafee_services_handler(
                                                 $(this.promiser), # The services promiser
                                                 $(this.service_policy) # The value of service_policy
      );
}

And then you could specify use the services promise

Listing 4: bundle agent example_custom_services in example_custom_services.cf

bundle agent example_custom_services
{
    services:
      "ma"
        service_policy => "active", # you get to choose the word describing the state
        service_method => macafee;

      "my custom service"
        service_policy => "kablewy",
        service_method => macafee;


}
bundle agent __main__
{
     methods: "example_custom_services";
}

Execute the policy to see it in action:

Listing 5: Running the policy

cf-agent -KIf /tmp/example_custom_services.cf

Reviewing the output we can see that the service was correctly seen to not be running, so the start command was issued.

    info: Executing 'no timeout' ... '/etc/init.d/ma start'
  notice: Q: ".../init.d/ma star": Starting McAfee agent service.
Q: ".../init.d/ma star": Starting McAfee common services.
Q: ".../init.d/ma star": Starting McAfee compat service.
    info: Last 3 quoted lines were generated by promiser '/etc/init.d/ma start'
    info: Completed execution of '/etc/init.d/ma start'
R: Something is wrong, I only expect to be used with 'ma' aka 'MacAfee Agent' but I am being used with 'my custom service'
R: Invalid service state. Selected 'kablewy'. Valid states: 'active, inactive'