CFEngine provides the services promise type to manage the state of a given service. By default, bundle agent standard_services is used for the service_method. 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. On sysvinit (non-systemd) hosts, 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
McAfee agent service status

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

1
2
3
4
  echo McAfee agent service is not running.
  echo McAfee common services is not running.
  echo McAfee compat service is not running.
  return 0; # Return zero means service OK/Running per the LSB
Mock /etc/init.d/ma

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
  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
  }
bundle agent mcafee_services_handler in example_custom_services.cf

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

1
2
3
4
5
6
7
8
  body service_method macafee
  {
      linux::
          service_bundle => mcafee_services_handler(
                                                   $(this.promiser), # The services promiser
                                                   $(this.service_policy) # The value of service_policy
        );
  }
body service_method macafee in example_custom_services.cf

And then you could specify use the services promise

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  bundle agent example_custom_services
  {
      services:
        "ma"
          #service_policy => "inactive", # you get to choose the state
          service_policy => "active", # you get to choose the state
          service_method => macafee,
          classes => results( "namespace", "macafee" );

        "my custom service"
          service_policy => "kablewy", # you get to choose the state
          service_method => macafee,
          classes => results( "namespace", "kablewy" );


  } 
  bundle agent __main__
  {
       methods: "example_custom_services"; 
  }
bundle agent example_custom_services in example_custom_services.cf

Execute the policy to see it in action:

1
  ./example_custom_services.cf -KI
   error: Policy failed validation with command '"/home/nickanderson/.cfagent/bin/cf-promises" -c "./example_custom_services.cf"'
   error: CFEngine was not able to get confirmation of promises from cf-promises, so going to failsafe
   error: CFEngine failsafe.cf: /home/nickanderson/.cfagent/inputs /home/nickanderson/.cfagent/inputs/failsafe.cf
    info: Unable to find host '$(sys.policy_hub)' service '$(sys.policy_hub_port)' (Servname not supported for ai_socktype)
    info: No server is responding on port: $(sys.policy_hub_port)
    info: Unable to establish connection to '$(sys.policy_hub)'
   error: No suitable server found
    info: Promise belongs to bundle 'failsafe_cfe_internal_update' in file '/home/nickanderson/.cfagent/inputs/failsafe.cf' near line 114
    info: Unable to find host '$(sys.policy_hub)' service '$(sys.policy_hub_port)' (Servname not supported for ai_socktype)
    info: No server is responding on port: $(sys.policy_hub_port)
    info: Unable to establish connection to '$(sys.policy_hub)'
   error: No suitable server found
    info: Promise belongs to bundle 'failsafe_cfe_internal_update' in file '/home/nickanderson/.cfagent/inputs/failsafe.cf' near line 123
    info: Unable to find host '$(sys.policy_hub)' service '$(sys.policy_hub_port)' (Servname not supported for ai_socktype)
    info: No server is responding on port: $(sys.policy_hub_port)
    info: Unable to establish connection to '$(sys.policy_hub)'
   error: No suitable server found
    info: Promise belongs to bundle 'failsafe_cfe_internal_update' in file '/home/nickanderson/.cfagent/inputs/failsafe.cf' near line 137
    info: Comment is 'If we failed to fetch policy we try again using
                    the legacy default in case we are fetching policy
                    from a hub that is not serving mastefiles via a
                    shortcut.'
   error: Method 'failsafe_cfe_internal_update' failed in some repairs
    info: Executing 'no timeout' ... '"/home/nickanderson/.cfagent/bin/cf-agent" -f /home/nickanderson/.cfagent/inputs/update.cf --define failsafe_mode'
  notice: Q: ".../cf-agent" -f /":    error: No suitable server found
Q: ".../cf-agent" -f /":    error: No suitable server found
Q: ".../cf-agent" -f /":    error: No suitable server found
Q: ".../cf-agent" -f /":    error: Method 'cfe_internal_update_policy_cpv' failed in some repairs
    info: Last 4 quoted lines were generated by promiser '"/home/nickanderson/.cfagent/bin/cf-agent" -f /home/nickanderson/.cfagent/inputs/update.cf --define failsafe_mode'
    info: Completed execution of '"/home/nickanderson/.cfagent/bin/cf-agent" -f /home/nickanderson/.cfagent/inputs/update.cf --define failsafe_mode'
R: Built-in failsafe policy triggered
Running the policy

Download the full policy