Original image by bre pattis

Introduction

In the previous post in this series, we showed how it was possible to take a single package and republish it, such that other packages could overwrite a default configuration file.

The example we used was the Squid web proxy, allowing configuration packages to overwrite /etc/squid/squid.conf with new contents.

There was a limitation using that approach: only one package could contribute to that configuration at a time, replacing the entire shipped configuration.

Recall, that we define self-assembly in Chapter 1 of the IPS Developer Guide as:

Any collection of installed software on a system should be able to build itself into a working configuration when that system is booted, by the time the packaging operation completes, or at software runtime.

In this post, we’ll cover a more advanced case than last time: true self-assembly, where the configuration can be delivered by multiple add-on packages, if necessary. In particular, we’ll continue to talk about Squid, a package that isn’t normally capable of self-assembly, and will show how we fix that.

How does self-assembly work?

The main premise with self-assembly, is that configuration for an application must be built from a composed view of all fragments of the entire configuration that are present on the system. That can be done either by the application itself, in which case nothing else is required on the part of the application packager, or it can be done with an add-on service to assemble the entire configuration file from the delivered fragments.

When a new package delivers another fragment of the configuration, then the application must have its configuration rebuilt to include that fragment.

Similarly, when a fragment is removed from the system, again, the application must have its configuration rebuilt from the remaining fragments on the system.

A good example of self-assembly is in the Solaris package for pkg:/web/server/apache-22. Solaris ships a default httpd.conf file that has an Include directive that references /etc/apache2/2.2/conf.d.

Packages can deliver a new file to that directory, and use a refresh_fmri actuator causing the system to automatically to refresh the Apache instance
either after a pkg install operation has completed, or after a
pkg remove operation has completed, causing the webserver to rebuild its configuration.

The reason behind self-assembly, is to replace postinstall, preinstall, preremove, postremove and class action scripts, needed by other packaging systems. Install-time scripting was a common source of errors during application packaging because the scripting had to work in multiple scenarios.

For example, scripts had to correctly run

  • against alternate image roots, perhaps running on a system that didn’t have
    the necessary tools support to correctly run the script
  • within the confines of a LiveCD environment
  • when making edits to an offline zone

With IPS, we eliminated those forms of install-time scripting, concentrating on an atomic set of actions (discussed in Chapter 3 of the IPS Developer Guide) that performed common packaging tasks, and allowing for actuators (discussed in Chapter 9 of the IPS Developer Guide) to run during packaging operations.

Actuators enable self-assembly to work on live systems by restarting or refreshing the necessary SMF services. Since the same SMF services they point to run during boot as well, we don’t need to do anything when performing operations on alternate images: the next time the image is booted, our self-assembly is completed.

Making Squid self-assembly aware

As in the previous post, we will start by downloading and modifying our Squid package.

This time, we intend to remove the etc/squid/squid.conf file entirely – our self-assembly service will be constructing this file instead for us. Recall that
Squid delivers some of its configuration files with the following actions:

file 7d8f133b331e7460fbbdca593bff31446f8a3bad path=etc/squid/squid.conf \
    owner=root group=webservd mode=0644 \
    chash=272ed7f686ce409a121f427a5b0bf75aed0e2095 \
    original_name=SUNWsquid:etc/squid/squid.conf pkg.csize=1414 pkg.size=3409 \
    preserve=renamenew
file 7d8f133b331e7460fbbdca593bff31446f8a3bad \
    path=etc/squid/squid.conf.default owner=root group=bin mode=0444 \
    chash=272ed7f686ce409a121f427a5b0bf75aed0e2095 pkg.csize=1414 pkg.size=3409
file 971681745b21a3d88481dbadeea6ce7f87b0070a \
    path=etc/squid/squid.conf.documented owner=root group=bin mode=0444 \
    chash=b9662e497184c97fff50b1c249a6e153c51432e1 pkg.csize=60605 \
    pkg.size=200255

Since squid.conf.default is already shipped and is identical to the
squid.conf file that is also delivered, we can use that for the basis of our self-assembly of the squid.conf configuration file.

We download a copy of the package with the following command:

$ pkgrecv -s http://pkg.oracle.com/solaris/release --raw -d squid-proto squid@3.1.8,5.11-0.175.0.0.0.2.537

which pulls the content into the squid-proto directory.

We’ll use a series of pkgmogrify(1) transforms to edit the package contents, similar to the ones we used in the previous post. We will remove the file action that delivers squid.conf using a drop transform operation, and will also deliver a new directory, etc/squid/conf.d. Here is the transform file that accomplishes that:

<transform set name=pkg.fmri -> edit value pkg://[^/]+/ pkg://mypublisher/>
<transform file path=etc/squid/squid.conf$ -> drop>
dir path=etc/squid/conf.d owner=root group=bin mode=0755

We can create a new manifest using this transform using pkgmogrify(1):

$ pkgmogrify squid\-assembly.mog \
    squid-proto/web%2Fproxy%2Fsquid/3.1.8%2C5.11-0.175.0.0.0.2.537%3A20111019T121425Z/manifest \
    > squid-assembly.mf

A self-assembly SMF service

In order for self-assembly to happen during packaging operations, we need to use an actuator discussed in Chapter 9 of the IPS Developer Guide.

The actuator is a special tag on any IPS action that points to an SMF service. The SMF service is made up of two components:

  • The SMF manifest
  • The SMF method script

This self-assembly SMF service is going to be responsible for building the contents of /etc/squid/squid.conf. We’ll talk about each component in the following section:

SMF manifest

This is what the SMF manifest of our self-assembly service looks like:

<?xml version="1.0"?>
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">

<service_bundle type='manifest' name='Squid:self-assembly'>

<service
    name='config/network/http/squid-assembly'
    type='service'
    version='1'>

    <single_instance />

    <dependency
        name='fs-local'
        grouping='require_all'
        restart_on='none'
        type='service'>
            <service_fmri value='svc:/system/filesystem/local:default' />
    </dependency>

    <dependent
            name='squid_self-assembly-complete'
            grouping='optional_all'
            restart_on='none'>
            <service_fmri value='svc:/milestone/self-assembly-complete' />
    </dependent>
    <instance enabled='true' name='default'>
            <exec_method
                type='method'
                name='start'
                exec='/lib/svc/method/squid-self-assembly'
                timeout_seconds='30'/>

            <exec_method
                type='method'
                name='stop'
                exec=':true'
                timeout_seconds='0'/>

            <property_group name='startd' type='framework'>
                <propval name='duration' type='astring' value='transient' />
            </property_group>
    </instance>
</service>
</service_bundle>

This defines a service instance that we intend to use whenever we deliver new configuration file fragments to the system.

For that to happen, any configuration file fragment added or removed must include a restart_fmri actuator.

For example, a package might deliver a configuration file fragment:

file path=etc/squid/squid.conf/myconfig.conf owner=root group=bin mode=0644 \
    restart_fmri=svc:/config/network/http/squid-assembly:default \
    restart_fmri=svc:/network/http/squid:default

The other vital thing needed, is an SMF dependency on the SMF service delivered by the Squid package. We need to add this, so that the Squid application will only be able to start once our self-assembly service has finished producing our configuration file.

First, we’ll create a proto area for the files we’re going to add to our Squid package, and copy the default SMF manifest:

$ mkdir -p squid-assembly-proto/lib/svc/manifest/network
$ cp /lib/svc/manifest/network/http\-squid.xml squid-assembly-proto/lib/svc/manifest/network

Next, we edit the http-squid.xml SMF manifest, adding the following:

<!--
  Wait for the Squid self-assembly service to complete
-->
<dependency name='squid-assembly'
    grouping='require_all'
    restart_on='none'
    type='service'>
    <service_fmri
        value='svc:/config/network/http/squid-assembly:default'/>
</dependency>

Now that we’ve done this, our next step, is writing the method script for our self-assembly service.

The SMF method script

We need to write a script, such that when it is run, we end up with /etc/squid.conf containing all changes, as defined in all configuration fragments installed on the system.

This step can be as simple or complex as you’d like it to be – essentially we’re performing postinstall scripting here, but on our terms: we know exactly the environment the script is running in – that of a booted OS where our package is installed (defined by the depend actions that accompany the package)

Here is a sample script, written in Python (as short as I could make it, so there’s very little error checking involved here) which takes squid.conf.default copies it to squid.conf, then applies a series of edits to it.

We’ll save the script as /lib/svc/method/squid-self-assembly.

#!/usr/bin/python2.6
import shutil
import os
import re
import logging

# define the paths we'll work with
MASTER="/etc/squid/squid.conf.default"
CONF_FILE="/etc/squid/squid.conf"
CONF_DIR="/etc/squid/conf.d/"

# verbose logging for now
logging.basicConfig(level=logging.DEBUG)

def apply_edits(fragment):
        """Takes edit operations in the path "fragment", and applies
        them to CONF_FILE in order. The syntax of our config file is
        intentionally basic. We support the following operations:

        # lines that start with a hash are comments
        add <line to add to the config file>
        remove <regular expression to remove>
        """

        squid_config = file(CONF_FILE).readlines()
        squid_config = "".join(squid_config)

        # read our list of operations
        operations = open(fragment, "r").readlines()
        operations = [line.rstrip() for line in operations]
        for op in operations:
                if op.startswith("add"):
                        addition = op[len("add") + 1:]
                        logging.debug("adding line %s" % addition)
                        squid_config += "\n" + addition
                elif op.startswith("remove"):
                        exp = op[len("remove") + 1:]
                        squid_config = re.sub(exp, "", squid_config)
                        logging.debug("removing expression %s" % exp)
                elif op.startswith("#"):
                        pass

        conf = open(CONF_FILE, "w")
        conf.write(squid_config + "\n")
        conf.close()

# first, remove any existing configuration
if os.path.exists(CONF_FILE):
       os.unlink(CONF_FILE)

# now copy the master template file in, on
# which all edits are based
shutil.copy(MASTER, CONF_FILE)
os.chmod(CONF_FILE, 0644)

fragments = []
# now iterate through the contents of /etc/squid/conf.d
# looking for configuration fragments, and apply the changes
# find in a defined order.   We do not look in subdirectories.
for dirpath, dirnames, filenames in os.walk("/etc/squid/conf.d/"):
        fragments = sorted(filenames)
        break

for fragment in fragments:
        logging.debug("Applying edits from %s" % fragment)
        apply_edits(os.path.join(CONF_DIR, fragment))

Testing the self-assembly script

We can now test the self-assembly script. For the most part, this testing can be done outside the confines of the pkg(1) command – we simply need to ensure
that our self-assembly script runs properly.

First, we’ll check that the squid.conf file isn’t present, run the script, then determine that the contents are the same as squid.conf.default

# ls /etc/squid/squid.conf
/etc/squid/squid.conf: No such file or directory
# /lib/svc/method/squid-self-assembly
# digest -a sha1 /etc/squid/squid.conf.default /etc/squid/squid.conf
(/etc/squid/squid.conf.default) = 7d8f133b331e7460fbbdca593bff31446f8a3bad
(/etc/squid/squid.conf) = 7d8f133b331e7460fbbdca593bff31446f8a3bad
#

Next, we’ll try a simple configuration fragment:

# cat > /etc/squid/conf.d/change_http_port.conf
# The default configuration uses port 3128, our organisation uses 8080
# We'll remove that default, add a comment, and add a http_port directive
remove # Squid normally listens to port 3128
remove http_port 3128
add # Our organisation requires Squid to operate on port 8080
add http_port 8080
^D

Then we’ll test the self-assembly script again:

# /lib/svc/method/squid-self-assembly
DEBUG:root:  --- applying edits from change_http_port.conf   ---
DEBUG:root:removing expression # Squid normally listens to port 3128
DEBUG:root:removing expression http_port 3128
DEBUG:root:adding line # Our organisation requires Squid to operate on port 8080
DEBUG:root:adding line http_port 8080

We can verify that the changes have been made:

# grep "port 8080" /etc/squid/squid.conf
# Our organisation requires Squid to operate on port 8080
http_port 8080

Now, we’ll add another configuration fragment:

# cat > /etc/squid/conf.d/connect_ports.conf
# We want to allow users to connect to gmail and irc
# over our proxy server.
add # We need to allow access to gmail and irc
add acl Connect_ports port 5222     # gmail chat
add acl Connect_ports port 6667     # irc chat
add http_access allow CONNECT Connect_ports
^D

and see what happens when we run the self-assembly script:

# /lib/svc/method/squid-self-assembly
DEBUG:root:  --- applying edits from change_http_port.conf   ---
DEBUG:root:removing expression # Squid normally listens to port 3128
DEBUG:root:removing expression http_port 3128
DEBUG:root:adding line # Our organisation requires Squid to operate on port 8080
DEBUG:root:adding line http_port 8080
DEBUG:root:  --- applying edits from connect_ports.conf   ---
DEBUG:root:adding line # We need to allow access to gmail and irc
DEBUG:root:adding line acl Connect_ports port 5222     # gmail chat
DEBUG:root:adding line acl Connect_ports port 6667     # irc chat
DEBUG:root:adding line http_access allow CONNECT Connect_ports

Again, we can verify that the edits have been made correctly:

# grep "port 8080" /etc/squid/squid.conf
# Our organisation requires Squid to operate on port 8080
http_port 8080
# egrep gmail\|irc /etc/squid/squid.conf
# We need to allow access to gmail and irc
acl Connect_ports port 5222     # gmail chat
acl Connect_ports port 6667     # irc chat

And finally, we can see what happens if we remove one of our fragments:

# rm /etc/squid/conf.d/connect_ports.conf
# /lib/svc/method/squid-self-assembly
DEBUG:root:  --- applying edits from change_http_port.conf   ---
DEBUG:root:removing expression # Squid normally listens to port 3128
DEBUG:root:removing expression http_port 3128
DEBUG:root:adding line # Our organisation requires Squid to operate on port 8080
DEBUG:root:adding line http_port 8080
#
# grep "port 8080" /etc/squid/squid.conf
# Our organisation requires Squid to operate on port 8080
http_port 8080
# egrep gmail\|irc /etc/squid/squid.conf
#

As expected, the configuration file no longer contains the directives configured by connect_ports.conf, since that was removed from the system, but still
contains the changes from change_http_port.conf

Delivering the SMF service

The bulk of the hard work has been done now – to recap:

  • we have modified the Squid package to drop the shipped squid.conf file
  • we have an SMF service that can perform self assembly, generating
    squid.conf files from installed fragments on the system
  • we have added a dependency to the Squid SMF service on our self-assembly SMF service

All that remains, is to ensure that the self-assembly service gets included in
the Squid package.

For that, we’ll add a few more lines to the pkgmogrify(1) transform that we talked about earlier, so that it looks like:

<transform set name=pkg.fmri -> edit value pkg://[^/]+/ pkg://mypublisher/>
<transform file path=etc/squid/squid.conf$ -> drop>
dir path=etc/squid/conf.d owner=root group=bin mode=0755
file path=lib/svc/method/squid/squid-self-assembly group=bin mode=0555 owner=root
file path=lib/svc/manifest/network/http-squid-assembly.xml group=sys \
    mode=0444 owner=root restart_fmri=svc:/system/manifest-import:default

Now we can transform our original Squid package, and publish it to our repository:

$ pkgmogrify squid-assembly.mog \
    squid-proto/web%2Fproxy%2Fsquid/3.1.8%2C5.11-0.175.0.0.0.2.537%3A20111019T121425Z/manifest \
    > squid\-assembly.mf
$ pkgsend -s myrepository publish -d squid-assembly-proto \
    -d squid-proto/web%2Fproxy%2Fsquid/3.1.8%2C5.11-0.175.0.0.0.2.537%3A20111019T121425Z \
    squid\-assembly.mf
WARNING: Omitting signature action 'signature 2ce2688faa049abe9d5dceeeabc4b17e7b72e792
.
.
pkg://mypublisher/web/proxy/squid@3.1.8,5.11-0.175.0.0.0.2.537:20111108T201820Z
PUBLISHED
$

Installing that package, we discover a svc:/config/network/http/squid-assembly service, and verify that when we drop unpackaged files into /etc/squid/conf.d, and restart the self-assembly service, we see what we expect:

# more /var/svc/log/config-network-http-squid-assembly\:default.log
[ Nov  8 12:19:50 Enabled. ]
[ Nov  8 12:19:50 Rereading configuration. ]
[ Nov  8 12:19:50 Executing start method ("/lib/svc/method/squid-self-assembly"). ]
[ Nov  8 12:19:50 Method "start" exited with status 0. ]
[ Nov  8 12:23:42 Stopping because service restarting. ]
[ Nov  8 12:23:42 Executing stop method (null). ]
[ Nov  8 12:23:42 Executing start method ("/lib/svc/method/squid-self-assembly"). ]
DEBUG:root:  --- applying edits from change_port.conf   ---
DEBUG:root:removing expression # Squid normally listens to port 3128
DEBUG:root:removing expression http_port 3128
DEBUG:root:adding line # Our organisation requires Squid to operate on port 8080
DEBUG:root:adding line http_port 8080
[ Nov  8 12:23:42 Method "start" exited with status 0. ]

We have verified that Squid is performing self-assembly perfectly.

Delivering new configuration fragments

Now that we have a service that’s capable of performing self-assembly, we need to know how to deliver configuration fragments in new packages.

This is simply a case of delivering config files to /etc/squid/conf.d, and applying the correct actuator tags to the manifest.

An example manifest that delivers this would be:

set name=pkg.fmri value=pkg:/config/web/proxy/squid-configuration@2.0
set name=pkg.summary value="Our organisations squid configurations"
file path=etc/squid/conf.d/change_http.conf owner=root group=bin mode=0644 \
    restart_fmri=svc:/config/network/http/squid-assembly:default \
    restart_fmri=svc:/network/http:squid

When we publish, then install this manifest, we see:

# pkg install squid-configuration@2
           Packages to install:  1
       Create boot environment: No
Create backup boot environment: No
            Services to change:  2

DOWNLOAD                                  PKGS       FILES    XFER (MB)
Completed                                  1/1         1/1      0.0/0.0

PHASE                                        ACTIONS
Install Phase                                    3/3

PHASE                                          ITEMS
Package State Update Phase                       1/1
Image State Update Phase                         2/2

We can quickly verify that the Squid configuration has changed:

$ curl localhost:8080 | grep squid/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2904  100  2904    0     0  1633k      0 --:--:-- --:--:-- --:--:-- 2835k
<p>Generated Tue, 08 Nov 2011 23:00:27 GMT by tcx2250-13 (squid/3.1.8)</p>

And we can backout the configuration by removing the package, and again check that the Squid configuration has changed:

# curl localhost:8080 | grep squid
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
curl: (7) couldn't connect to host
# curl localhost:3128 | grep squid/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3140  100  3140    0     0  1779k      0 --:--:-- --:--:-- --:--:-- 3066k
<p>Generated Tue, 08 Nov 2011 23:03:37 GMT by tcx2250-13 (squid/3.1.8)</p>

We won’t go into details here, but clearly, multiple packages could deliver
configuration fragments at the same time, and they would all contribute to the
configuration of our service.

Conclusion

This has been a pretty fast example of the self-assembly idiom, but we hope this has been useful, and shows complex scripting operations can be performed in IPS.

There may more work to do to make the Squid application fully self-assembly aware – we’ve only covered the main configuration file and have’t looked at whether we also want to allow the other files in /etc/squid to participate in self-assembly. If we did want to do that, it would be a case of ensuring that:

  • we ship a master template for each configuration file
  • modify our self-assembly SMF service to copy each template into place
  • ensure our script can perform edits on that file

Of course, there’s other ways in which a self-assembly service could perform edits – we could use SMF to deliver properties to the service, which are then accessed by a self-assembly script, and placed into a configuration file, but perhaps that’s an example for another day.