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'