Solaris Features: Service Management Facility - Part 4: Developing for SMF

Okay, now you know how to do basic tasks. But how to use SMF for your your own applications. I will use openvpn as an example. A good source for this program is blastwave. Please install the package tun and openvpn I want to show a running example, thus we have to do some work. It will be just a simple static shared key configuration, as this is a SMF tutorial, not one for OpenVPN. We will use theoden and gandalf again. gandalf will be the server. theoden the client.

10.211.55.201 gandalf
10.211.55.200 theoden

Preparing the server

Okay … this is just a short configuration for an working OpenVPN server.

# mkdir /etc/openvpn
# cd /etc/openvpn
# openvpn --genkey --secret static.key
# openvpn --dev tun --ifconfig 192.16.1.1 172.16.1.2 --secret static.key --daemon
# scp static.key jmoekamp@10.211.55.200:/tmp/static.key

Now just leave this terminal this way.

Preparing the client

Okay, we need some basic configurations to get the client side of OpenVPN working, even when it´s unter control of the SMF ;)

# mkdir /etc/openvpn
# mv /tmp/static.key /etc/openvpn
# cd /etc/openvpn/
# ls -l
total 2
-rw-------   1 jmoekamp other        636 Feb 27 16:11 static.key
# chown -R root:root /etc/openvpn
# chmod 600 static.key

Before working with SMF itself

At first i remove this stinking init.d links. We don´t need them anymore:

# rm /etc/rc0.d/K16openvpn
# rm /etc/rc1.d/K16openvpn
# rm /etc/rc2.d/S60openvpn
# rm /etc/rcS.d/K16openvpn

Okay, and now let´s hack the startup script….? Wrong! SMF can do many task for you, but this needs careful planing. You should answer yourself some questions:

  1. What variables make a generic description of a service to a specific server?
  2. How do i start the process? How do i stop them? How can i force the process to reload it´s config?
  3. Which services are my dependencies? Which services depend on my new service?
  4. How should the service react in the case of a failed dependency?
  5. What should happen in the case of a failure in the new service. Okay, let´s answer this questions for our OpenVPN service. The variables for our OpenVPN client are the hostname of the remote hosts and the local and remote IP of the VPN tunnel . Besides of this the filename of the secret key and the tunnel device may differ, thus it would be nice to keep them configurable. Starting openvpn is easy. We have just to start the openvpn daemon with some command line parameters. We stop the service by killing the process. And a refresh is done via stopping and starting the service. We clearly need the networking to use a VPN service. But networking isn´t just bringing up the networking cards. You need the name services for example. So make things easier, the service don´t check for every networking service to be up and running. We just define an dependency for the “network” milestone. As it make no sense to connect to a server without a network it looks like a sensible choice to stop the service in case of a failed networking. Furthermore it seems a good choice to restart the service when the networking configuration has changed. Perhaps we modified the configuration of the name services and the name of the OpenVPN server resolves to a different IP. What should happen in the case of a exiting OpenVPN daemon? Of course it should started again. Okay, now we can start with coding the scripts and xml files.

The Manifest

Okay, as i wrote before, the manifest is the source of the configuration of a service. Thus we have to write such a manifest. The manifest is a XML file. Okay, at first we obey the gods of XML and do some definitions:

# cat openvpn.xml<br />
&lt;?xml version="1.0"?&gt;<br />
&lt;!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1"&gt;<br />
&lt;service_bundle type='manifest' name='openvpn'&gt;

Okay, at first we habve to name the service:

&lt;service<br />
name='application/network/openvpn'<br />
type='service'<br />
version='1'&gt;

In this example, we define some simple dependencies. As i wrote before: Without networking a VPN is quite useless, thus the OpenVPN service depends on the reached milestone.

&lt;dependency 
name='network' 
grouping='require_all' 
restart_on='none' 
type='service'&gt; 
&lt;service_fmri value='svc:/milestone/network:default' /&gt; 
&lt;/dependency&gt;

In this part of the manifest we define the exec method to start the service. We use a script to start the service. The %m is a variable. It will be substituted with the name of the called action. In this example it would be expanded to /lib/svc/method/openvpn start.

<exec_method 
type='method' 
name='start' 
exec='/lib/svc/method/openvpn %m' 
timeout_seconds='2' />

Okay, we can stop OpenVPN simply by sending a SIGTERM signal to it. Thus we can use a automagical exec method. In case you use the :kill SMF will kill all processes in the actual contract of the service.

<exec_method
type='method'
name='stop'
exec=':kill'
timeout_seconds='2'>
</exec_method>

Okay, thus far we´ve only define the service. Let´s define a service. We call the instance theoden2gandalf for obvious names. The service should run with root privileges. After this we define the properties of this service instance like the remote host or the file with the secret keys.

<instance name='theoden2gandalf' enabled='false'>
<method_context> 
<method_credential user='root' group='root' /> 
</method_context>
<property_group name='openvpn' type='application'> 
<propval name='remotehost' type='astring' value='gandalf' /> 
<propval name='secret' type='astring' value='/etc/openvpn/static.key' />
<propval name='tunnel_local_ip' type='astring' value='172.16.1.2' />
<propval name='tunnel_remote_ip' type='astring' value='172.16.1.1' />
<propval name='tunneldevice' type='astring' value='tun' />
</property_group> 
</instance>

At the end we add some further metadata to this service:

<stability value='Evolving' />
<template> 
<common_name> 
<loctext xml:lang='C'>OpenVPN</loctext> 
</common_name> 
<documentation> 
<manpage title='openvpn' section='1' /> 
<doc_link name='openvpn.org' uri='http://openvpn.org' /> 
</documentation> 
</template> 
</service> 
</service_bundle>

I saved this xml file to my homedirectory under /export/home/jmoekamp/openvpn.xml.

The exec method´s script

General considerations

Okay, we referenced a script in the exec method. This script is really similar to a normal init.d script. But there are some important differences. As there´s no parallel startup of services in init.d most scripts for this system bringup method tend to return as quickly as possible. We have to change this behaviour. Scripts for transient or standalone services should only return in the case of the successful execution of the complete script or when we´ve terminated the process. For services under control of the contract mechanism the script should at least wait until the processes of the service generate some meaningful error messages, but they have to exit, as SMF would consider the service startup as failed, when the script doesn´t return after There are some general tips:

Implementing a exec method script

We store configuration properties in the Service Component repository. It would be nice to use them for configuration. Thus we have to access them. Here comes the svcprop command to help:

# svcprop -p openvpn/remotehost svc:/application/network/openvpn:theoden2gandalf
gandalf

With a little bit of shell scripting we can use this properties to use them for starting our processes.

#!/bin/sh

. /lib/svc/share/smf_include.sh

getproparg() { 
val=`svcprop -p $1 $SMF_FMRI` 
[ -n "$val" ] && echo $val 
} 

if [ -z "$SMF_FMRI" ]; then 
echo "SMF framework variables are not initialized." 
exit $SMF_EXIT_ERR 
fi

OPENVPNBIN='/opt/csw/sbin/openvpn'
REMOTEHOST=`getproparg openvpn/remotehost`
SECRET=`getproparg openvpn/secret`
TUN_LOCAL=`getproparg openvpn/tunnel_local_ip`
TUN_REMOTE=`getproparg openvpn/tunnel_remote_ip`
DEVICETYPE=`getproparg openvpn/tunneldevice`

if [ -z "$REMOTEHOST" ]; then 
echo "openvpn/remotehost property not set" 
exit $SMF_EXIT_ERR_CONFIG 
fi

if [ -z "$SECRET" ]; then 
echo "openvpn/secret property not set" 
exit $SMF_EXIT_ERR_CONFIG 
fi

if [ -z "$TUN_LOCAL" ]; then 
echo "openvpn/tunnel_local_ip property not set" 
exit $SMF_EXIT_ERR_CONFIG 
fi

if [ -z "$TUN_REMOTE" ]; then 
echo "openvpn/tunnel_remote_ip property not set" 
exit $SMF_EXIT_ERR_CONFIG 
fi

if [ -z "$DEVICETYPE" ]; then 
echo "openvpn/tunneldevice property not set" 
exit $SMF_EXIT_ERR_CONFIG 
fi

case "$1" in 
'start') 
$OPENVPNBIN --daemon --remote $REMOTEHOST --secret $SECRET --ifconfig $TUN_LOCAL $TUN_REMOTE --dev $DEVICETYPE 
;; 

'stop') 
echo "not implemented"
;; 

'refresh') 
echo "not implemented"
;; 

*) 
echo $"Usage: $0 {start|refresh}" 
exit 1 
;; 

esac 
exit $SMF_EXIT_OK

I saved this script to my homedirectory under /export/home/jmoekamp/openvpn.

Installation of the new Service

Okay, copy the script to /lib/svc/method/:

# cp openvpn /lib/svc/method
# chmod +x /lib/svc/method/openvpn

After this step you have to import the manifest into the Service Configuration Repository:

# svccfg validate /export/home/jmoekamp/openvpn.xml
# svccfg import /home/jmoekamp/openvpn.xml

Testing it

Let´s test our brand new service:

# ping 172.16.1.2<br />
^C

The OpenVPN service isn´t enabled. Thus there is no tunnel. The ping doesn´t get through.
Now we enable the service and test it again.

# svcadm enable  openvpn:theoden2gandalf<br />
# ping 172.16.1.2<br />
172.16.1.2 is alive

Voila … SMF has started our brand new service. When we look into the list of services, we will find it:

# svcs openvpn:theoden2gandalf<br />
STATE          STIME    FMRI<br />
online         18:39:15 svc:/application/network/openvpn:theoden2gandalf

When we look into the process table, we will finde the according process:

# /usr/ucb/ps -auxwww | grep "openvpn" | grep -v "grep"<br />
root      1588  0.0  0.5 4488 1488 ?        S 18:39:15  0:00 /opt/csw/sbin/openvpn --daemon --remote gandalf --secret /etc/openvpn/static.key --ifconfig 172.16.1.2 172.16.1.1 --dev tun

Okay, we doesn´t need the tunnel any longer after a few day,thus we disable it:

# svcadm disable openvpn:theoden2gandalf<br />
# /usr/ucb/ps -auxwww | grep "openvpn" | grep -v "grep"<br />
# 

No process left.