APPENDIX B

Writing cfengine Modules

Cfengine automatically sets a large number of classes at runtime based on attributes of the system. These are classes based on the IP address of the system, the operating system (e.g., linux or solaris), the date and time, and many other attributes. Many predefined cfengine classes are shown and explained in Chapter 4.

Cfengine modules are designed for the definition of custom classes. Modules allow you to write code to extend cfengine, so that it can detect new situations and site-specific conditions. We say "designed for" because it's possible to use modules to implement system changes as well. We'll focus on what modules are designed for and then briefly touch on other uses. We'll explain the requirements for using modules and then show you how to create a simple module to get you started. Once you know how to create and use a module, you'll be able to build on the example in your own environment.

Requirements for Using Modules

Before we discuss modules in any detail, we'll lay out the requirements for using them:

  • Modules must be placed in the directory defined by the variable moduledirectory in cfagent.conf (or a file imported from cfagent.conf).
  • Each module file must have a name following the convention module:mymodule.
  • cfagent will execute only modules
    • Owned by root (or the user running cfagent)
    • Placed in the special directory
    • That follow the naming convention
  • Modules may be written in any language supported on the system, and they can output anything you deem appropriate. The important things about module output follow:
    • Lines that begin with a + sign are interpreted as classes to be defined.
    • Lines that begin with a - sign are interpreted as classes to be undefined.
    • Lines starting with = are interpreted as variables to be defined.
  • Any other lines of output are also printed by cfagent, so modules should generally be silent.

Defining Custom Classes Without Modules

Classes are used by cfengine to determine the appropriate actions to take, if any. During the development of our example environment used throughout this book, we only needed classes based on simple tests. For example, the following shellcommands section will only be run if the niagara_t1_proc class is set:

classes:
   sunos_sun4v::
      niagara_t1_proc =
        ( "/usr/platform/sun4v/sbin/prtdiag | grep UltraSPARC-T1 >/dev/null" )

shellcommands:
   niagara_t1_proc::
      "/bin/echo hello world"

Sun hardware classified as sun4v has the processor class that we're looking for but not all systems of that class run a particular CPU called the Niagara T1 processor. In the classes section, we ran the prtdiag command and piped the output into the grep command looking for the string "UltraSPARC-T1". Running the prtdiag and grep commands enabled us to find the sun4v systems that are running the Niagara T1 processor and then to set the niagara_t1_proc class.

This very simple example of setting a custom class is well suited to the classes section because of its simplicity. At some point, you may need to set classes based on much more complex criteria. If you can write code in any language supported on your systems, you can write a cfengine module to set your custom classes. We will use Bourne shell scripting for our example module.

Creating Your First cfengine Module

Making use of a module to set the niagara_t1_proc class is a good way to get familiar with creating your own modules. We will implement this simple module in our example environment.

First, we created a module called module:detect_niagara, with these contents:

1. #!/bin/sh
2. PATH=/bin:/usr/bin
3.
4. if /usr/platform/sun4v/sbin/prtdiag | grep UltraSPARC-T1 >/dev/null
5. then
6.        THREAD='/usr/platform/sun4v/sbin/prtdiag | grep UltraSPARC-T1
7.        | wc -l | sed 's/^[ ]*//' '
8.        echo "+niagara_t1_proc"
9.        echo "=num_cores=$THREAD"
10. fi

This script is very simple. It executes a grep command against the output of the prtdiag command on line 4, and if a match is found, three things happen:

  1. On line 6, the prtdiag command is run again—this time to capture the total number of CPU threads present on the system's processor—using the wc command. The sed command in the pipeline removes any leading whitespace placed in the output by the wc command.
  2. The niagara_t1_proc class is set using an echo statement on lines 7 and 8, so now, the cfagent process running this module will have the class defined.
  3. A variable named num_cores will be passed back to the cfagent process running the module, with the value set to the number of threads on the system from line 6.

We placed the file in the PROD/inputs/modules directory, which should exist if you followed along with this book (this relative path convention has also been used throughout this book; the full path on the cfengine master in our example environment is /var/lib/cfengine2/masterfiles/PROD/inputs/modules). If not, you may need to create the directory. We added this line to PROD/inputs/control/cf.control_cfagent_conf so our module could be found by cfagent at runtime (make sure that it applies to the any class):

moduledirectory         = ( $(client_cfinput)/modules )

We then created a task at PROD/inputs/tasks/misc/cf.detect_niagara_proc with these contents:

control:
    sunos_sun4v::
        addinstallable    = ( niagara_t1_proc )
        actionsequence = ( module:detect_niagara )

shellcommands:
    sunos_sun4v.niagara_t1_proc::
        "/bin/echo hello world - I have a Niagara proc with $(num_cores) threads"

We needed the addinstallable line so that cfengine knew that a custom class might be defined. We set the actionsequence to include this module on any hosts running the sun4v architecture—recall that modules are always called via the cfengine actionsequence. We run the command in the shellcommands section when a host is a sun4v system and when the niagara_t1_proc class is set. When the command is run, the variable containing the number of threads on the processor is returned.

To put the task into use, we added it to the PROD/inputs/hostgroups/cf.any file with this entry:

tasks/misc/cf.detect_niagara_proc

Check the files into Subversion, and check them out onto your cfengine master, if applicable and if you've followed along with the book to this point (if you're not sure what we mean, don't worry about it). We don't need take any extra measures in our example environment to set the ownership or permissions on the files in our modules directory, because the update.conf in our example environment copies all files inside the PROD/inputs directory with root (user and group) ownership and other permissions completely absent (file permission mode 700 and owned by root:root). If you haven't followed along with this book, here's the pertinent section from update.conf:

copy:
                $(master_cfinput)/
                                        dest=$(workdir)/inputs/
                                        r=inf
                                        mode=700
                                        type=checksum
                                        ignore=RCS
                                        ignore=.svn
                                        owner=root
                                        group=root
                                        ignore=*,v
                                        purge=true
                                        server=$(policyhost)
                                        trustkey=true
                                        encrypt=true

Our modules directory is under the inputs directory (which is copied via update.conf), and this copy action recursively copies all files and directories beneath the inputs directory. The variables used aren't pertinent to this section. What's important is that the module files are owned by root, since we only run cfengine as root at our example site.

On systems running a Niagara processor (such as a Sun T2000 system), you'll see output from cfagent like this:

cfengine:sunbox:/bin/echo hello: hello world - I have a Niagara proc with 32 threads

This simple example puts all the pieces in place for you to successfully use cfengine modules. You can use it to build a much more complicated module that sets classes and variables that you can then use to take actions in a cfengine task file (as we did with the echo command in the shellcommands section of cf.detect_niagara_proc).

Using modules, you can extend cfengine in ways never imagined by the author of cfengine.

Using Modules in Place of shellcommands

Cfengine provides the shellcommands section so you have an easy way to perform custom actions. The commands defined in a shellcommands section can be standard operating system utilities or custom scripts. Cfengine makes every attempt to be as generic as possible, and it directly supports only the most basic system administration actions (e.g., file copies, permission fixes, link creation, file editing, etc).

Nothing prevents the code in a module from making changes on a system. The entire list of classes defined by cfengine on the host is passed to scripts or programs run by shellcommands as well as to scripts or programs run as a module, so there is no technical barrier to using a module instead of a shellcommands section.

We don't like to use modules this way, because they weren't designed to replace shellcommands. We think that some sites choose to use modules in place of shellcommands since it's easy to automate the copy of the modules directory and use that as a single location for cfengine-specific scripts. In our example environment, we automated the copy of an administrative script directory, so we have an easy location to place sitewide scripts for execution by administrators, cfengine, or both.

Modules are sometimes recommended on Internet mailing lists when the quotes used in shellcommands actions get too complicated for the cfengine parser, resulting in errors. Consider a shellcommands section such as this:

shellcommands:
    debian.i686::
        "/bin/grep 'foo bar' /etc/foobar | /bin/sed 's/^[ ]*//'  
           | /usr/bin/mail -s"file contents from 'hostname'" [email protected]"

This code is difficult to read and needs some escaping of double quotes, and sometimes, cfengine can get confused when parsing commands like this. However, we don't consider this a reason to put the commands in a cfengine module. Instead, use a shell script, in a location such as our example site's /opt/admin-scripts directory.

We could create PROD/repl/admin-scripts/mail-foobar with these contents:

#!/bin/sh
PATH=/bin:/usr/bin

if [ -f /etc/foobar ]
then
    /bin/grep 'foo bar' /etc/foobar | /bin/sed 's/^[ ]*//'  |
      /usr/bin/mail -s"file contents from 'hostname'" [email protected]
fi

We could then create a new shellcommands section like this:

shellcommands:
        debian.i686::
                "/opt/admin-scripts/mail-foobar"

Now, the cfengine configuration is easy to read, and the script can be a simple shell script with no special escaping rules, making it easy to read too. You can also easily add extra niceties to the shell script, like a PATH statement and a test for the /etc/foobar file's existence before running grep against it. At our example site, the contents of PROD/repl/admin-scripts on our cfengine master are copied to /opt/admin-scripts on all hosts, so once we place a script into the central directory, the copy is automatic.

We definitely recommend using shell scripts—not modules—for complicated shellcommands sections.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.132.107