Modularization in cfengine


Next: , Previous: (dir), Up: (dir)

CFEngine-Modularization

COMPLETE TABLE OF CONTENTS

Summary of contents

In this module you will learn about

  • How to split up a configuration into multiple parts
  • How to delegate responsibility for different parts of a configuration.
  • How to extend cfengine's functionality with careful scripting.
  • How to think about role-based access control using cfengine.


Next: , Previous: Top, Up: Top

1 Modularization

Modularization is a way of organizing your policy into parts that can be maintained separately. It is about:

It is also about organizing the system you are managing into modular parts and matching the management of the system to the organization of the policy.


Next: , Previous: Modularization, Up: Modularization

1.1 Reminder about classes

Planning the organization of file resources is one of the many aspects of managing (coping) with systems. The files themselves form a system, and we usually maintain the files by a different process than the one by which we maintain system resources.

In cfengine 3 there are additional, more sophisticated possibilities.


Next: , Previous: Reminder about classes, Up: Modularization

1.2 User experiences on organizing policy

“I manage a very large cfengine site, and as of lately with many different sysengs editing the cf.classes class definition file, its starting to become somewhat unruly. I'm going to also need a better way to manage the editing of this file (it is already in SVN).”

“We use a team-based approach, with a set of common classes (managed by a core team), and other teams managing classes files (and other cf files) for their own servers, each with their own version control repos. This allows for some separation of influence. Parsing problems etc., other than in the core files, will only affect a teams systems, not all systems. Still as Brendan says, some education and cooperation is needed, so there are not class naming or action conflicts for example, between team and core managed files.”

“This is the "old way" we were defining classes:

     web = ( web01_dev web01_qa web01_stg web01_sd web02_sd
     web03_sd web04_sd web05_sd web06_sd web07_sd web08_sd web09_sd
     web10_sd web02_qa )

This is the new way:

         web = ( ClassMatch(^web[0-9]+_.*) )

See... much cleaner! The biggest advantage is if we add any new hosts, we don't have to come back and manually add them to the class. As long as they adhere to the naming convention the appropriate class will get defined.”

“That's the same thing we do, though we have a combination of regexp's directly in cfengine as well as regexps in modules to catch some of the stranger conditions (prepmodule, since we import based on them).

Hostnames are often the single most important configuration setting at sites that use cfengine - it certainly is at ours. Using hostname conventions most of us on this list probably have cfengine make hundreds or even thousands of different changes to hosts (total changes, not every time it runs, think of it more like all the changes that would be made when you image a host with only a bare OS).

So at our site - going from an in-house rdist-based config management framework to cfengine meant setting up only new hosts using cfengine, and basically leaving older hosts alone (except for core OS things like local accounts, resolv.conf, etc). So if web1 and web2 are already setup as web servers, but when we setup web3 and web4 using cfengine we have hosts that might not be exactly the same, but all of them sharing a common hostname convention, and therefore a shared class/group in cfengine.

It mostly gets messy with things like deploying packages - the huge list of packages goes only to web3 and web4, but packages that are needed after web3 and web4 are deployed go to all hosts, meaning you either have to setup a new group NOT based on the convention, but now listing hosts manually, or you start adding excludes like "web_servers.!(web1|web2)::". Either way is much messier than what we intended by defining groups based on hostname conventions.

The solution? What I learned the hard way, is that you should feel free to invent a new hostname convention when you migrate over to configuring your hosts entirely from cfengine. Call your new hosts www1 and www2, or something else. Now you have a very clean way to tell the old from the new, and at worst you now have two classes to define in your packages or files or copy or whatever actions. You can still take full advantage of regular expression matching to group hosts together. When you have new hosts that aren't exactly the same, don't name them the same. It starts out not that ugly, but degrades into ugly configs rather quickly.”


Next: , Previous: User experiences on organizing policy, Up: Modularization

1.3 Object orientation

Object orientation (OO) is a paradigm that has achieved some dominance in computer science. It includes concepts like:

Classes in OO are data-types that lie in a hierarchy or tree structure. OO-classes are usually mutually exclusive, i.e. you must be either in one class or a other – classes cannot overlap. Classes in OO are designed based on requirements or inherited from libraries that others have designed.

Classes in cfengine are not compound data-types, they are labels for the characteristics of hosts. Hosts are classified by a number of identifiers, and these are called classs. CFEngine classes form a possibly overlapping collections of environmental attributes; they are observed (not planned) from the context which cfengine runs.

What OO and cfengine classes share in common is the tacit assumption that classes model a particular context in which certain rules or methods apply.

OO classes are used to divide and conquer programming tasks, by the separation of concerns. CFEngine classes are used to specify patches of the resource landscape in which certain promises are made. Different classes of machine can make different promises.

A key feature of OO relationships which is apparently useful in configuration management is inheritance: the ability to set defaults that apply in a given set of contexts (a base class), and extend the basic defaults in a context dependent way. We speak of child-classes or derived classes, and imagine that they inherit attributes from their parents. Inheritance actually means two separate things:

All management paradigms require you to create a `model' for your system. A model is just an abstract plan.


Next: , Previous: Object orientation, Up: Object orientation

1.3.1 Inheritance

Inheritance is a mechanism for defining default characteristics which apply for a broad set of contexts, which can also be used partially or completely in a specific subset.

In OO you create the parent class first – i.e. you make the default case first. This works well if you are designing from scratch and trying to make everything basically the same, with only a few exceptions. It can be less successful if you are trying to make sense of an existing system which did not take into account the model

Let's look at some examples of class inheritance in pseudocode.

     class Unix  # Base class
       begin
         # inside the Unix container
         set base_resource to "default value"
       end

     class Solaris extends_or_inherits Unix
       begin
         # inside the Solaris container, which is inside the Unix container
         set solar_resource to "special value"
       end

     class Freebsd overrides Unix
       begin
         # inside the Freebsd container, which exclusively replaces Unix
         set resource1 to "override value"
       end

In cfengine, you are not encouraged especially to think in these terms. Nevertheless we can reproduce some aspects of this. In cfengine, we would implement the above pseudo-code something like this.

         * Extend or inherit base class:

             # Using files as containers

             import:

              baseclass::           # parent context

                cf.basedefaults

              baseclass.solaris::   # child 1 of parent

                cf.base-solaris

              baseclass.linux::     # child 2 of parent

                cf.base-linux

By writing baseclass.solaris, for instance, it looks as though solaris is a sub-class of a parent class Unix. CFEngine couldn't care less about this observation and there is no automatic connection between the classes. However, it might be helpful to think in these terms.

The advantages of this kind of construction are:

The disadvantages of this kind of construction are:

Unlike OO, the child classes baseclass.solaris and baseclass.linux do not have to be mutually exclusive. In this case they are, but we could have classes domain1.host1, domain2.host1 in which host1 is the same host. This is because most real networks are not usually cleanly separated in an OO way.


Next: , Previous: Inheritance, Up: Object orientation

1.3.2 Overriding

We sometimes want sub-classes to override locally the decisions made in a parent class. Overriding means replacing part of base class. Overriding/replacement is not transparently checked and handled in cfengine as it is in OO languages. CFEngine forces you to make it explicit. Consider the following examples, where italicized "rules" are pseudo-code:

      include:

             baseclass::

                "base_policy.cf"

Later if we make a rule

             baseclass.solaris::

               "solaris_policy.cf"

this will overlap with the old rule and could lead to an inconsistency if the files contain rules for the same things. To make the rulses mutually exclusive we must write:

             baseclass.!solaris::

               "base_policy.cf"

             baseclass.solaris::

                "solaris_policy.cf"

or if there are multiple "subclasses":

             classes:

               subclasses = ( solaris linux darwin hpux )

             rule:

              baseclass.!subclasses::

                "common_policy.cf"

              baseclass.solaris::

                "solaris_policy.cf"

              baseclass.linux::

                "linux_policy.cf"

You could do the same thing with departments, domains, or any other organizational separation, whether physical or virtual. This is the power of cfengine's flexible class model.


Previous: Overriding, Up: Object orientation

1.3.3 Overriding a policy file

A simple way to override a policy is this.

      control:

        override = ( FileExists(/var/cfengine/inputs/overridepolicy.cf) )

      import:

        override::

          "overridepolicy.cf"

        !override::

          "defaultpolicy.cf"

SECURITY: You must decide whether this approach is secure enough for your site. Consider who is allowed to write the override script. As long as the override file is placed in the secure cfengine inputs directory, it can only be generated by authorized persons. Note that the `parent' policy voluntarily adopts the override policy. An override policy cannot force its way into the configuration.


Next: , Previous: Object orientation, Up: Modularization

1.4 Organizing the files into classes

There are no real containers for these bundles of definitions in cfengine as classes are only labels – they do not have the concept of mutual exclusion. OO languages often ask you to group everything for a given class in a single block:

     // Not cfengine code

     class myclass
        {
        # everything to do with myclass
        }

CFEngine does not force you to do this. If, on the other hand, you want to organize in this fashion, then the simplest thing is to put the different mutually exclusive classes into separate files using the import example above.

The problem with distinct OO containers The basic assumption of OO is that classes are a mutually exclusive breakdown of a context into different types. And this is where classic OO thinking runs into trouble. The world is rarely this simple – and this is why cfengine allows you to make any kind of class combination, and does not force a grouping of rules for a given class into a single container. The fact of the matter is that most real-world objects fall between two chairs, or wear two hats at the same time. Classifications are not mutually exclusive. They overlap.

This is the strength of cfengine's classification model. The trouble with great power is that is can lead to a mess. You therefore need a policy for organizing your files.

CFEngine 3.0 adds a new level of container which is called a promise bundle which greatly extends the flexibility of containers. It also has parameterized templates for promise values which can be class-context dependent.


Next: , Previous: Organizing the files into classes, Up: Modularization

1.5 Aspect orientation

Aspect orientation is about `cross-cutting concerns', or code that you want to apply in every class, or a set of classes. For instance, you might want to add auditing code to very different, unrelated classes and have the same code for all cases. This cannot be done in a strict OO scheme.

Aspect orientation is an artifice required in strict object orientation, because we have been `too quick' (in some sense) in separating issues into exclusive containers that share nothing. So we have to trick common-code back in at multiple places in the class hierarchy. C++ has the mechanism for making `friends', for example.

In cfengine there is no problem at all here, because cfengine classes are not hierarchical. The class model allows any kind of overlapping set to be a class. If we want to break out of a specific class and address all the classes, we just change context:

       # Start in a special context

       baseclass.subclass::

         "some specialist rules..."

       # Cancel the specificity

       any::

         "This rule cuts across all other classes"

       # Now sub-region

       linux::

         "This rule cuts across only the sub-classes of linux"


Next: , Previous: Aspect orientation, Up: Modularization

1.6 Delegation

Cfengine's principle model is that of voluntary cooperation. Because pushing data onto others is not allowed, delegation is a matter of voluntary acceptance of contributions from many sources. Just as overriding is voluntary, delegation also cannot be forced. A single cfengine instance must voluntarily be willing to accept input from delegates and unify them.

CFEngine has no meta-access control mechanism which can decide who may write policy rules, but we can select from which sources rules are collected.

To create such a mechanism, there would have to be a monitor which could identify users, and an authority mechanism that would disallow certain users to write rules of certain types about certain objects on certain hosts. Clearly it is possible to create such a system, but it would be both technically difficult, very cumbersome to use and would add a whole new level of complexity to policy and potential error to the configuration process.

To keep matters as simple as possible, cfengine avoids this and proposes a different approach. Promise theory allows us to model the security implications of this and its bow-tie structure (see figure below). A simple method of delegating is the following.

  1. Delegate responsibility for different issues to admin teams 1,2,3, etc.
  2. Make each of these teams responsible for version control of their own configuration rules.
  3. Make an intermediate agent responsible for collating and vetting the rules, checking for irregularities and conflicts. This agent must promise to disallow rules by one team that are the responsibility of another team. The agent could be a layer of software, but a cheaper and more manageable solution is the make this another group of one or more humans.
  4. Make the resulting collated configuration version controlled. Publish approved promises for all hosts to download from a trusted source.

A review procedure for policy promises is a good solution if you want to delegate responsibility for different parts of a policy to different sources. Human judgement is irreplaceable, and tools can be added to make conflicts easier to detect.

Promise theory (the theory which have been developed in order to understand cfengine's voluntary cooperation approach) underlines that, if a host of computing device accepts policy from any source, then it is alone and entirely responsible for this decision. The ultimate responsibility for the published version policy is the vetting agent. This creates a shallow hierarchy, but there is no reason why this formal body could not be comprised of representatives from the multiple teams.

Delegation's bow-tie funnel


Next: , Previous: Delegation, Up: Modularization

1.7 Role based access control

Fom delegation we essentially have role-based access control. Role based access control means allowing one or more users (who define a group) to have the `right' or authorization to perform one or more tasks (which define a role).

The feature that enables role based access control to take place is the ability to measure identity of users and computers using public-private key authentication. Thus all of the usual warnings about public keys, and trust apply here.

The voluntary cooperation model is a strong security model. No server is obliged to do anything an external party asks of it.

In cfengine 2, role based access control is limited. We can define different roles by using classes.

Create a new class that cannot be defined without an explicit instruction to switch it on.

Granting access to cfengine on a local system is not possible without special operating-system mechanisms. If you have a privileged account, you can do anything you want. If you don't have a privileged account, you cannot run cfengine with privilege (we'll disregard sudo here).

     cfrun serverhost -- -- -Dmy_role

     cfrun serverhost --  -- linux -Dmy_role

The ability to connect to the server is moderated by user-key access control. Individual users are authenaticated by their public keys. In the cfservd.conf configuration file

     control:

            AllowUsers = ( mark systemuser )

In future releases of cfengine 3, roles access mechanisms will be extended to allow you to accept only certain classes from certain users.


Next: , Previous: Role based access control, Up: Modularization

1.8 Methodology to organize systems

Although cfengine offers tools of sufficient generality to cope with any kind of organizational model, certain experiences are worth capturing to avoid falling into traps.

One of the classic questions of organization is whether to build a structure from the top down or from the bottom up. Top down thinking, it turns out, has several classic pitfalls.


Next: , Previous: Methodology to organize systems, Up: Methodology to organize systems

1.8.1 Top down

In top down analysis, you start by setting requirements and breaking them down by subdividing the problem into smaller and smaller pieces. This works only if the distance between your intentions and the actual low level operations is short.

The pitfall of top down analysis is to fall into a kind of Zeno's paradox of endless subdivisions – constantly taking classes and subdividing them into sub-classes in a process that never seems to end. Often what one finds is that, after this process, items that we want to belong together have been separated into quite different classes, and this causes problems, as described above under the heading of aspect orientation. Short of reconnecting them with artificial relationships like `friend' (adding further to the complexity), there is no way to capture those similarities.

A top-down analysis will tend to lead to too many small categories that are artificially disconnected. This can lead to logical and practical problems.


Previous: Top down, Up: Methodology to organize systems

1.8.2 Bottom up

In bottom up analysis, we have a methodology that fits promise theory very well. Every component in a system has certain properties, which it promises. A higher level observer can then look down on these and group similar items into categories. This process of aggregation tends to be greedy so we end up with fewer categories than we get with a zealous de-construction. A bottom up analysis will naturally lead to a few broad categories, in which you learn to live with a natural level of approximation.

We recommend bottom up thinking because


Previous: Methodology to organize systems, Up: Modularization

1.9 Modularization self-test questions

  1. How can I separate a configuration into many parts?
  2. Can I make object oriented configurations?
  3. How can I override one file with another?
  4. How can I delegate responsibility for parts of the configuration to various individuals?
  5. What does role based access control mean for cfengine.


Next: , Previous: Modularization, Up: Top

2 CFEngine plugin modules


Next: , Previous: CFEngine plugin modules, Up: CFEngine plugin modules

2.1 Why cfengine modules?

CFEngine provides flexible tools for fundamental issues in system management. It does not offer a solution to every need. For that reason there are simple ways of extending it.

Modules are not meant for large-scale programming tasks. They were intended mainly to allow users to detect and define additional classes. But of course, the only real limitation is your imagination.


Next: , Previous: Why cfengine modules?, Up: CFEngine plugin modules

2.2 Modules can define classes

Normally cfengine's ability to detect the system's condition is limited to what it is able to determine while excuting predefined actions. Classes may be switched on as a result of actions cfengine takes to correct a problem. To increase the flexibility of cfengine, a mechanism has been introduced in version 1.5 which allows you to include a module of your own making in order to define or undefine a number of classes. The syntax

       module:mytests

       "module:mytests arg1 arg2 .."

declares a user defined module which can potentially set the classes class1 etc. Classes returned by the module must be declared so that cfengine knows to pay attention to rules which use these classes when parsing; this is done using AddInstallable. If arguments are passed to the module, the whole string must be quoted like a shellcommand. See Writing plugin modules. Whether or not these classes become set or not depends on the behaviour of your module. The classes continue to apply for all actions which occur after the module's execution. The module must be owned by the user executing cfengine or root (for security reasons), it must be named module:module-name and must lie in a special directory, See moduledirectory.

CFEngine communicates with itself and its environment by passing messages in the form of classes. When a class becomes switched on or off, cfengine's program effectively becomes modified. There are several ways in which you can switch on and off classes. Learning these fully will take some time, and only then will you harness the full power of cfengine.


Next: , Previous: Modules can define classes, Up: CFEngine plugin modules

2.3 Other reasons for modules

Some people use modules through laziness, to avoid having to “think cfengine”. Others use them legitimately as a way to extend the functionality of cfengine. We recommend the latter.


Previous: Other reasons for modules, Up: Other reasons for modules

2.3.1 The moduledirectory

        moduledirectory  = ( directory for plugin modules )

This is the directory where cfengine will look for plug-in modules for the actionsequence. Plugin modules may be used to activate classes using special algorithms.

This variable defaults to /var/cfengine/modules for privileged users and to $HOME)/.cfengine/modules for non-privileged users.


Next: , Previous: Other reasons for modules, Up: CFEngine plugin modules

2.4 Storing modules and methods

For security cfengine only executes modules that are authorized by being in a special protected directory.

When methods were introduced, it was decided not to increase the number of special places but rather to put methods together with modules in the modules directory.

Thus modules and methods are normally kept together in a separate directory than inputs files are kept in, because they require a directory with special authorizations whe executing. This is good practice As long as the update.conf places the master versions in the correct location (usually /var/cfengine/modules) on the local host, all will be okay.

You should not try to copy files directly from a version controlled repository, as you might end up sending out an incomplete or partially tested version of the files to all your hosts.

     # Example update.conf

     control:

        master_cfinput  = ( /usr/local/masterfiles/cfengine/inputs )
        workdir         = ( /var/cfengine )

     copy:

        # Copy from bullet 2 to bullet 3

          $(master_cfinput)            dest=$(workdir)/inputs
                                       r=inf
                                       mode=700
                                       type=binary
                                       exclude=*.lst
                                       exclude=*~
                                       exclude=#*
                                       server=$(policyhost)
                                       trustkey=true

          $(master_modules)            dest=$(workdir)/modules
                                       r=inf
                                       mode=700
                                       type=binary
                                       exclude=*.lst
                                       exclude=*~
                                       exclude=#*
                                       server=$(policyhost)
                                       trustkey=true


Next: , Previous: Storing modules and methods, Up: CFEngine plugin modules

2.5 Writing plugin modules

Modules must lie in a special directory defined by the variable moduledirectory. They must have a name of the form module:mymodule and they must follow a simple protocol.

Cfagent will only execute a module which is owned either by root or the user who is running cfagent, if it lies in the special directory and has the special name.

A plug-in module may be written in any language, it can return any output you like, but lines which begin with a ‘+’ sign are treated as classes to be defined (like -D), while lines which begin with a ‘-’ sign are treated as classes to be undefined (like -N). Lines starting with ‘=’ are variables/macros to be defined. Any other lines of output are cited by cfagent, so you should normally make your module completely silent. Here is an example module written in perl. First we define the module in the cfagent program:

      control:

        moduledirectory = ( /local/cfagent/modules )

        actionsequence = (
                         files
                         module:myplugin
                         "module:argplugin arg1 arg2"
                         copy
                         )
      ...
        AddInstallables = ( specialclass )

      classes:

        ok = ( PrepModule("module:other",noargs) )

Note that the class definitions for all action-sequence modules must also be defined in as AddInstallables to declare the classes before using them in the cfagent configuration, or else those actions will be ignored.


Next: , Previous: Writing plugin modules, Up: Writing plugin modules

2.5.1 The plugin itself

Next we write the plugin itself.

     #!/usr/bin/perl
     #
     # module:myplugin
     #

       # lots of computation....

     if (special-condition)
        {
        print "+specialclass";
        }

Modules inherit the environment variables from cfagent and accept arguments, just as a regular shellcommand does.

     #!/bin/sh
     #
     # module:myplugin
     #

     /bin/echo $*


Previous: The plugin itself, Up: Writing plugin modules

2.5.2 Using the class environment in plugins

Cfagent defines the classes as an environment variable so that programs have access to these. E.g. try the following module:

     #!/usr/bin/perl

     print "Decoding $ENV{CFALLCLASSES}\n";

     @allclasses = split (":","$ENV{CFALLCLASSES}");

     while ($c=shift(@allclasses))
       {
       $classes{$c} = 1;
       print "$c is set\n";
       }

Modules can define macros in cfagent by outputting strings of the form

     =variablename=value

When the $(allclasses) variable becomes too large to manipulate conveniently, you can access the complete list of currently defined classes in the file /var/cfengine/state/allclasses.


Next: , Previous: Writing plugin modules, Up: CFEngine plugin modules

2.6 Preparatory Modules

The problem with actionsequence modules is that they are executed after parsing has finished. But what if we want the outcome of a module to influence the outcome of the decisions in the cfagent.conf? For this we use preparatory modules. These prepare the system for parsing. You can use these to set classes, e.g. to include certain files:

     classes:

       result = ( PrepModule("module:setmoreclasses","$(host)") )

       # sets class myclass

     import:

       myclass::

          "my_special_stuff.cf"

PrepModule is treated as a special function. You should place it in cfagent.conf right at the start of the configuration. The syntax of the command is as follows:

     classes:

       class = ( PrepModule(module,arg1 arg2...) )

True if the named module exists and can be executed. The module is assumed to follow the standard programming interface for modules (see Writing plugin modules in tutorial). Unlike actionsequence modules, these modules are evaluated immediately on parsing. Note that the module should be specified relative to the authorized module directory.


Next: , Previous: Preparatory modules, Up: CFEngine plugin modules

2.7 Options related to modules


Next: , Previous: Options related to modules, Up: CFEngine plugin modules

2.8 Examples of modules

Get a list of names from a database. This example uses MySQL, but this could be LDAP or some other source at your option.

     #!/bin/sh

     # Get a list of names from a MYSQL database

     list=`echo "use userdb; select name from users where department='mydepartment'" | mysql`
     echo `echo $list | sed s/\ /:/g`

     # outputs a ':' separated list


Previous: Examples of modules, Up: CFEngine plugin modules

2.9 Module self-test questions

  1. What language must you use to write modules
  2. What is the difference between an actionsequence module and a prep-module?
  3. How do you get a module to set a class?
  4. How do you get a module to set a variable?
  5. Can a module return a list?
  6. How can you access cfengine's classes in your module?


Previous: CFEngine plugin modules, Up: Top

3 CFEngine Methods

From version 2.1.0, cfagent provided for the execution of closed functions or "methods". Methods are modular cfengine sub-programs. They are written in cfengine's own language.

Methods allow you to call an independent cfengine program, pass it arguments and classes, and collect the results for use in your main program. It thus introduces parent-child semantics into cfengine "imports". If a single method fails, other methods can still be executed, so it also adds a level of encapsulation.

A method is more than an import however. (Import is analagous to a C #include, while a method is like a C function.) Communication is peer to peer, by mutual consent. There is no "method server" that executes methods on remote hosts. Hosts exchange information by invitation only. This is an unreliable service (in the sense of UDP).

The order of method exeuction is not guaranteed. This results from the decoupling between client request and service provision.

       methods:

          class::

           function_name(parameters or none)

           action=filename

           sendclasses=comma separated class list

           returnvars=comma separated variable list or void
           returnclasses=comma separated class list

           server=ip-host/localhost/none

           forcereplyto=ip address

           owner=setuid
           group=setgid
           chdir=cd for child
           chroot=sandbox directory

Most of these functions will be familiar from other cfengine commands. Some special ones are noted below:

action
The name of the method file that should be defined in the modules directory of the server host.
forcereplyto
Sometimes nameservice problems (especially with remote devices) can lead to confusion about where a method should be sent. The caller can therefore declare to the server which address it wants the reply to be marked for.
returnvars
Returns the values of the variables to the parent process. This acts as an access control list for variable names transmitted by the child process. The names returned by the child must match this list.
returnclasses
Returns the classes to the parent process, if and only if they are defined at the end of the current method. This acts as an access control list for class names transmitted by the child process. The names returned by the child must match this list.
sendclasses
Transmits the current status of the named classes to the child process. In other words, if the listed classes are defined, they become defined in the child process, else they remain undefined. The class may still be defined in the child by independent local definitions.

If the server is set to localhost, the method will be evaluated on the local machine. If the server is set of none, the method will not be executed at all. The function arguments may not be empty, but a null value can be transmitted with a dummy value, e.g. Function(null) or function(void). Here is an example method call.

      # cfagent.conf

     control:

        actionsequence = ( methods )

     #################################################

     methods:

      any::

        SimpleMethod(null)

           action=cf.simple
           returnvars=null
           returnclasses=null
           server=localhost


With method file (located in the ModulesDirectory),

      # cf.simple

       control:

         MethodName       = ( SimpleMethod )
         MethodParameters = ( null )
         actionsequence   = ( timezone )


       classes:

          dummy = ( any )

     ####################################################

     alerts:

      dummy::

       "This simple method does nothing"

       ReturnVariables(void)
       ReturnClasses(void)

On executing this example, the output is:

     nexus$ ./cfagent -f ./cftest
     cfengine:myhost:SimpleMethod: cfengine:nexus: This simple method does nothing

If the server name is a wildcard, e.g. * then this acts as a multicast or broadcast.


Next: , Previous: CFEngine Methods, Up: CFEngine Methods

3.1 Local method examples


Next: , Previous: local method examples, Up: local method examples

3.1.1 Setting up users

     #
     # The calling cfagent.conf file
     #

     control:

      actionsequence = (  shellcommands methods )

      Split = ( ";" )

     classes:

      ok = ( PrepModule("module:getusers","") )

     shellcommands:

      "/bin/echo got $(user)"

     methods:

       FixUser("$(user)")
        action=fixuser.cf
        server=localhost

This uses a module to get a list of users (e.g. from a database), and the calls a method.

     #!/usr/bin/perl
     #
     # The method code
     #

     print "=user=mark;fred;sally\n";

     #
     # The method code
     #

     control:

       MethodName = ( FixUser )
       MethodParameters = ( user )

       actionsequence = ( directories editfiles )

     directories:

       /home/$(user)/.ssh mode=700

     editfiles:


      { /home/$(user)/.bashrc

      AppendIfNoSuchLine "alias passwd=\"echo Do not change your password here\""
      }


Next: , Previous: Setting up users, Up: local method examples

3.1.2 User passwords

Extract a restricted subset of password and shadow files from a master source for other hosts. Suppose you have a group of machines where you only want a limited number of users to be able to log on. Suppose that the master password and shadow files are located at some server, then we begin by copying them to a private location <pre>

     control:

        actionsequence = ( module:getuserlistfile copy editfiles )

        srcserver     = ( master.domain.tla )
        realsrcpasswd = ( /etc/passwdsrc )
        realsrcshadow = ( /etc/shadowsrc )

        realpasswd = ( /tmp/passwd )
        realshadow = ( /tmp/shadow )

        temppasswd = ( /tmp/workfile1 )
        tempshadow = ( /tmp/workfile2 )

        # Module generates this list from db?
        listfile = ( /masterdir/userlist )

        editfilesize = ( 0 )

     copy:

        # First make a copy of the complete passwd file as tmp

        $(realsrcpasswd) dest=$(temppasswd) mode=600 server=$(srcserver)
        $(realsrcshadow) dest=$(tempshadow) mode=600 server=$(srcserver)

     #######################################################

     editfiles:

     specialhosts.do:: # Add special users

       { $(temppasswd)

       # $(listfile) contains a list of users whom we want to
       # have accounts on this subset of machines
       # So get rid of all the accounts that are not in our
       # special list

       DeleteLinesNotStartingFileItems "$(listfile)"
       }

       { $(tempshadow)

       # Same for the shadow file....

       DeleteLinesNotStartingFileItems "$(listfile)"
       }


       { $(realpasswd)

       # Add the restricted list to the password file, if the user does
       # not already exist there...

       DeleteLinesStartingFileItems "$(listfile)"
       AppendIfNoSuchLinesFromFile  "$(temppasswd)"
       }

       { $(realshadow)

       DeleteLinesStartingFileItems "$(listfile)"
       AppendIfNoSuchLinesFromFile  "$(tempshadow)"
       }

      #######################################################

     specialhosts.undo:: # Remove special users

       { $(realpasswd)

       DeleteLinesStartingFileItems "$(listfile)"
       }

       { $(realshadow)

       DeleteLinesStartingFileItems "$(listfile)"
       }


Previous: User passwords, Up: local method examples

3.1.3 Tar package installation

The following example collects the tar file, unpacks it, configures and compiles it, then tidies its files.

     ####################################################
     #
     # This is a cfengine file that calls a method.
     # It should be in the usual place for cfinputs
     #
     ####################################################

     control:

        actionsequence = ( methods )

     #####################################################

     methods:

        InstallTar(cfengine-2.1.0b7,/local/gnu)

           action=cf.install
           returnvars=null
           returnclasses=null
           server=localhost

We must install the method in the trusted modules directory (normally /var/cfengine/modules, or ~/.cfagent/modules for non-privileged users i.e. WORKDIR/modules).

     ####################################################
     #
     # This is an example method file, that needs to be
     # in the module directory /var/cfengine/modules
     # since this is the trusted directory
     #
     # e.g. InstallFromTar(cfengine-2.2.8,/usr/local/gnu)
     #
     ####################################################

     control:


      MethodName       = ( InstallTar )
      MethodParameters = ( filename gnuprefix )

      path = ( /usr/local/gnu/bin )


      TrustedWorkDir = ( /tmp )

      TrustedSources = ( /iu/nexus/ud/mark/tmp )
      TrustedSourceServer = ( localhost )

      actionsequence = ( copy editfiles shellcommands tidy )

     ####################################################

     classes:

       Force = ( any )

     ####################################################

     copy:

      $(TrustedSources)/$(filename).tar.gz

         dest=$(TrustedWorkDir)/$(filename).tar.gz
         server=$(TrustedSourceServer)

     ####################################################

     shellcommands:

      "$(path)/tar zxf $(filename).tar.gz"

          chdir=$(TrustedWorkDir)

      "$(TrustedWorkDir)/$(filename)/configure --prefix=$(gnuprefix)"

         chdir=$(TrustedWorkDir)/$(filename)
         define=okay

      okay::

      "$(path)/make"

          chdir=$(TrustedWorkDir)/$(filename)

     ####################################################

     tidy:

       $(TrustedWorkDir) pattern=$(filename) r=inf rmdirs=true age=0


     ####################################################

     alerts:

      Force::

       ReturnVariables(none)
       ReturnClasses(success)

A more complex example is given below:

       GetAnalysis("${parent1}",param2,ReadFile("/etc/passwd",300))

         # The name of the method that is in modulesdir

         action=cf.methodtest

         # The variables that we get back should be called these names
         # with method name prefix

         returnvars=a,b,c,d

         # This is an access list for returned classes. Classes will
         # only be handed back if they are included here

         returnclasses=define1,define2,class1

         # The host(s) that should execute the method

         server=localhost

         # Only localhost can decide these - not a remote caller
         #    owner=25
         #    group=root
         #    chdir=/var/cfengine
         #    chroot=/tmp

Here the function being called is the cfengine program cf.methodtest. It is passed three arguments: the contents of variable parent1, the literal string "param2" and the first 300 bytes of the file /etc/passwd. On return, if the method gets executed, the values will be placed in the four variables:

     $(GetAnalysis.a) $(GetAnalysis.b) $(GetAnalysis.c) $(GetAnalysis.d)

If the classes define1 etc, are returned by the method, then we set them also in the main program as

     GetAnalysis_define::

In other words, the class name is also prefixed with the method name to distinguish it. (returnclasses works like an access control list for setting classes, deciding whether or not the main script should accept the results from the child method.). The remaining options are as those for executing shell commands, and apply only on the host that executes the function.

Both the client and server hosts must have a copy of the same method declaration. The client should have a non-empty server= declaration. The server side should have no server= declaration unless it is sending the request on recursively to other hosts. At present only requests to localhost are allowed, so only there is automatic access to the rule.

The cfagent file that contains the method code must have the following declarations:

       control:

         MethodName       = ( identifier )
         MethodParameters = ( spaced list of recipient variables or files )

        # ....

       alerts:

        # Return variables are alerts to parent

         ReturnVariables(comma separated list of variables or functions or void)
         ReturnClasses(comma separated list of classes)

e.g.

     control:

      MethodName       = ( GetAnalysis )
      MethodParameters = ( value1 value2 /tmp/file1 )

      # ....

     alerts:

       # Return variables are alerts to parent

       ReturnVariables("${var1}","${var2}","var3",literal_value)
       ReturnClasses(class1,class2)

The parameters transmitted by the parent are read into the formal parameters value1, value2 and the the file excerpt is placed in the temporary file /tmp/file1.

The return classes are passed in their current state to the parent; i.e. if class1 is defined then it is offered to the parent, but if it is not defined in the method, it is not passed on. The parent can then choose to accept or ignore the value.


Next: , Previous: local method examples, Up: CFEngine Methods

3.2 Method example: SSH key distribution

Collecting user ssh keys for adding to an authorized key file (e.g. for delegating root login) can be automated.


Next: , Previous: method examples, Up: method examples

3.2.1 From an authorized cache directory to different locations

From an authorized directory to users
     #This master configuration calls a `cf.copykey' method

     control:

        actionsequence = ( methods )

        user  = ( user1:user2 )       # list of users on the servers
        myip  = ( host1:host2  )      # servers with the public keys

     methods:

       CopyKey($(user),$(myip))

         action=cf.copykey

This assumes that we have collected together authorized keys into a known directory on a list of hosts, and named them with the name of the user. The method copies these keys from all the remote hosts and adds these to the authorized key files of the named users.

     # file cf.copykey
     # this method should be in modules directory

      control:

        actionsequence = ( copy editfiles )

        domain = ( example.org )

        MethodName       = ( CopyKey )
        MethodParameters = ( username ip )

        user        = ( $(username) )
        sourcehost  = ( $(ip) )

       # put username.pub in a known place

       path  = ( /master/cfengine/inputs/$(username).pub )
       cfdir = ( /master/cfengine/ssh )

     # pull public key to the local host

     copy:

      # Collect from directories where public keys have been
      # placed and leave them at dest

        $(cfdir)/id_dsa.pub

              dest=$(path)
              server=$(sourcehost)

     editfiles:

       { /home/$(user)/.ssh/authorized_keys

       # Must use BeginGroup to make convergent
       # or use EmptyEntireFilePlease to reset

       BeginGroupIfNoLineContaining "$(user)"
         InsertFile "$(path)"
       EndGroup
       }


Previous: From an authorized cache directory to different locations, Up: method examples

3.2.2 Allow ssh root login

From authorized users to root
     # Master configuration

     control:

       actionsequence  = ( methods )
       authuserlist    = ( user1:user2:user3:user4 )

     methods:

      CopyKey("$(userlist)")
         action=cf.sshkey

For each user, we add the ssh key into their

     #This is a module & should be in the modules directory

     control:

       actionsequence = ( editfiles  )

        MethodName = ( CopyKey )
        MethodParameters = ( user )

        source = ( "/home/$(user)/id_dsa.pub" )

     editfiles:

       { /root/.ssh/authorized_keys

       AutoCreate

       BeginGroupIfNoLineContaining "$(user)"
         InsertFile "$(source)"
       EndGroup
       }


Next: , Previous: method examples, Up: CFEngine Methods

3.3 Method example: DNS server setup

This shows three files: a calling script and two method files. On three flavours of Linux the script creates and edits the necessary files to set up a domain.

EXERCISE: rewrite this example to simplify the input using InsertFile and ExpandVariables, etc.

     #
     # WORKDIR/inputs/dnscall.cf
     #

     control:

      actionsequence = ( methods )

      hostname = ( oslo:accra:london:zurich )
      ip       = ( 192.168.189.136:192.168.189.129:192.168.189.138:192.168.189.133 )
      hostid   = ( 136:129:138:133 )

      testprefix     = ( /home/mark/tmp/DNSTEST )
      AddInstallable = ( EditDNS_created )

     methods:

      EditDNS("mycfengine.com","mycentos","189.168.192",8600,20212008,68000,3000,2000,1000,"192.168.189.134",134,"localhost","$(testprefix)")
        action=dns.conf
       #

      PopulateDNS("mycfengine.com","$(hostname)","$(ip)","$(hostid)","$(testprefix)")
         action=dnspopulate.conf


Next: , Previous: Method example DNS server setup, Up: Method example DNS server setup

3.3.1 EditDNS

     control:

      actionsequence = ( files editfiles )

      MethodName = ( EditDNS )

      #dname - domain name - e.g mycfengine.com
      #dnshost - the host name of the dns server - e.g localhost
      #revnetworkip - reversing ip  - 10.168.192
      #ttl - time to live leave for the dns -e.g 8600
      #serial - serial number  - e.g 20202008
      #refresh - refresh time  -e.g  1000
      #retry - retry time      -e.g 1000
      #expire - dns expire time - e.g 2000
      #min1 - minimum time       - e.g 1000
      #hostid - of the dns server -e.g  2
      #ip - of the dns server     - e.g 192.168.10.2
      #zonenameserver - the name of the server you want to use for your name server- e.g localhost

      MethodParameters = ( dname dnshost revnetworkip ttl serial refresh retry expire min1 ip hostid zonenameserver prefix )


     ###################################################################################################
     # Debian
     ###################################################################################################

     files:

      debian::

        $(prefix)/etc/bind/zones/$(dname)     mode=644 action=touch
        $(prefix)/etc/bind/zones/$(dname).rev mode=644 action=touch
        $(prefix)/etc/named.conf              mode=644 action=touch

     editfiles:

      debian::

       { $(prefix)/etc/bind/named.conf

         AppendIfNoSuchLine

         #begin cfline
         "zone \"$(dname)\" {
         type master;
         file \"/etc/bind/zones/$(dname)\";
         };

         zone \"$(revnetworkip).in-addr.arpa\" {
         type master;
         file \"/etc/bind/zones/$(dname).rev\";
         };
         "
         #end cfline
       }


       { $(prefix)/etc/bind/zones/$(dname)

       AppendIfNoSuchLine

         #begin cfline
         "$(dollar)TTL $(ttl)				; - lifetime for cached mappings
            IN      SOA     $(dname). root.$(dname). (
                             $(serial)        ; serial - the zone file version
                             $(refresh)           ; refresh - slaves check for update this often
                             $(retry)            ; retry - retry a failed update after this long
                             $(expire)          ; expire - discard zone data if master down this long
                             $(min1)            ; minimum - cache lifetime for negative answers
     )


                            	IN      NS      $(zonenameserver).		;the zone use nameserver localhost

     $(dnshost).$(dname).  	IN      A       $(ip)              ; specify IP address to the domain name
         "
         #end cfline
       }

       #

       { $(prefix)/etc/bind/zones/$(dname).rev

        AppendIfNoSuchLine

         #begin cfline
         "$(dollar)TTL $(ttl)
            IN      SOA     $(dname). root.$(dname). (
                              $(serial)        ; serial - the zone file version
                             $(refresh)           ; refresh - slaves check for update this often
                             $(retry)            ; retry - retry a failed update after this long
                             $(expire)          ; expire - discard zone data if master down this long
                             $(min1)            ; minimum - cache lifetime for negative answers

     )

                                    IN      NS      $(zonenameserver).		;the zone use nameserver localhost

     $(hostid)                             IN      PTR     $(dnshost).$(dname).
         "
         #end cfline
        }


     #touch domain files suse

     ###################################################################################################
     # SuSE
     ###################################################################################################

     files:

      SuSE::

       $(prefix)/var/lib/named/master/$(dname)     mode=644  action=touch
       $(prefix)/var/lib/named/master/$(dname).rev mode=644  action=touch
       $(prefix)/etc/named.conf                    mode=644  action=touch

     editfiles:

      SuSE::

       { $(prefix)/etc/named.conf

       AutoCreate
       AppendIfNoSuchLine

         #begin cfline
         "
         zone \"$(dname)\" in {
         type master;
         file \"/var/lib/named/master/$(dname)\";
         };

         zone \"$(revnetworkip).in-addr.arpa\" in {
         type master;
         file \"/var/lib/named/master/$(dname).rev\";
         };
         "
         #end cfline
       }

      ################################################################################

      { $(prefix)/var/lib/named/master/$(dname)

      AutoCreate
      AppendIfNoSuchLine

         #begin cfline
        "$(dollar)TTL $(ttl)				; - lifetime for cached mappings
            IN      SOA     $(dname). root.$(dname). (
                             $(serial)        ; serial - the zone file version
                             $(refresh)           ; refresh - slaves check for update this often
                             $(retry)            ; retry - retry a failed update after this long
                             $(expire)          ; expire - discard zone data if master down this long
                             $(min1)            ; minimum - cache lifetime for negative answers
     )


                            	IN      NS      $(zonenameserver).		;the zone use nameserver localhost

     $(dnshost).$(dname).  	IN      A       $(ip)              ; specify IP address to the domain name
         "
         #end cfline
       }

      ################################################################################

       { $(prefix)/var/lib/named/master/$(dname).rev

       AutoCreate
       AppendIfNoSuchLine

         #begin cfline
         "$(dollar)TTL $(ttl)
            IN      SOA     $(dname). root.$(dname). (
                              $(serial)        ; serial - the zone file version
                             $(refresh)           ; refresh - slaves check for update this often
                             $(retry)            ; retry - retry a failed update after this long
                             $(expire)          ; expire - discard zone data if master down this long
                             $(min1)            ; minimum - cache lifetime for negative answers

     )

                                    IN      NS      $(zonenameserver).		;the zone use nameserver localhost

     $(hostid)                             IN      PTR     $(dnshost).$(dname)."
         #end cfline
       }


     ###################################################################################################
     # Redhat
     ###################################################################################################

     files:

      redhat::

        $(prefix)/var/named/zones/$(dname)     mode=644 action=touch
        $(prefix)/var/named/zones/$(dname).rev mode=644 action=touch
        $(prefix)/etc/named.conf               mode=644 action=touch

     editfiles:

      redhat::

       { $(prefix)/etc/named.conf

       AutoCreate
       AppendIfNoSuchLine

         #begin cfline
         "zone \"$(dname)\" {
         type master;
         file \"/var/named/zones/$(dname)\";
         };

         zone \"$(revnetworkip).in-addr.arpa\" {
         type master;
         file \"/var/named/zones/$(dname).rev\";
         };
        "
         #end cfline

       }

     ################################################################################

       { $(prefix)/var/named/zones/$(dname)

       AutoCreate
       AppendIfNoSuchLine

         #begin cfline
         "$(dollar)TTL $(ttl)				; - lifetime for cached mappings
            IN      SOA     $(dname). root.$(dname). (
                             $(serial)        ; serial - the zone file version
                             $(refresh)           ; refresh - slaves check for update this often
                             $(retry)            ; retry - retry a failed update after this long
                             $(expire)          ; expire - discard zone data if master down this long
                             $(min1)            ; minimum - cache lifetime for negative answers
     )


                            	IN      NS      $(zonenameserver).		;the zone use nameserver localhost

     $(dnshost).$(dname).  	IN      A       $(ip)              ; specify IP address to the domain name
         "
         #end cfline
       }


      ################################################################################

       { $(prefix)/var/named/zones/$(dname).rev

       AutoCreate
       AppendIfNoSuchLine

         #begin cfline
         "$(dollar)TTL $(ttl)
            IN      SOA     $(dname). root.$(dname). (
                              $(serial)        ; serial - the zone file version
                             $(refresh)           ; refresh - slaves check for update this often
                             $(retry)            ; retry - retry a failed update after this long
                             $(expire)          ; expire - discard zone data if master down this long
                             $(min1)            ; minimum - cache lifetime for negative answers

     )

                                    IN      NS      $(zonenameserver).		;the zone use nameserver localhost

     $(hostid)                             IN      PTR     $(dnshost).$(dname).
         "
         #end cfline

       }



Previous: EditDNS, Up: Method example DNS server setup

3.3.2 PopulateDNS

     control:

      actionsequence = ( editfiles )

      MethodName = ( PopulateDNS )

      #dname - domain name - mycfengine.com
      #hostname -the host name or machine - e.g eternity
      #ip - the list of ip address - e.g. 192.168.10.3,192.168.10.4
      #hostip - the host ip - e.g. 3,4

      MethodParameters = ( dname hostname ip hostip prefix )

     #############################################################################################

     editfiles:

      debian::

       { $(prefix)/etc/bind/zones/$(dname)

       AutoCreate
       AppendIfNoSuchLine "$(hostname).$(dname).  	IN      A       $(ip)"
       }

       #

       { $(prefix)/etc/bind/zones/$(dname).rev

       AutoCreate
       AppendIfNoSuchLine "$(hostip)                   IN      PTR     $(hostname).$(dname)."
       }

     #############################################################################################

      SuSE::

       { $(prefix)/var/lib/named/master/$(dname)

       AutoCreate
       AppendIfNoSuchLine "$(hostname).$(dname).  	IN      A       $(ip)"
       }

       { $(prefix)/var/lib/named/master/$(dname).rev

       AutoCreate
       AppendIfNoSuchLine "$(hostip)                   IN      PTR     $(hostname).$(dname)."
       }

     ###############################################################################################

      redhat::

       { $(prefix)/var/named/zones/$(dname)

       AutoCreate
       AppendIfNoSuchLine "$(hostname).$(dname).  	IN      A       $(ip)"
       }

       { $(prefix)/var/named/zones/$(dname).rev

       AutoCreate
       AppendIfNoSuchLine "$(hostip)                 IN      PTR     $(hostname).$(dname)."
       }



Next: , Previous: Method example DNS server setup, Up: CFEngine Methods

3.4 Remote method examples

Methods can also be scheduled for execution on remote hosts.

Remote method execution is the same as local method execution except for some additional requirements. A list of collaborating peers must be added to the control section of update.conf. In order the the requests to be collected automatically, you must have copy in the action sequence of the update.conf file.

     control:

      MethodPeers = ( hostname list ) # Must have copy in actionsequence

This list tells the agent which remote hosts to collaborate with, i.e. whom should we contact to look for work that we have promised to perform? In order the the requests to be collected automatically, you must have copy in the action sequence of the update.conf file.

For example, to make two hosts collaborate:

     methods:

      host1|host2::

       MethodTest("my test!")

       action=cf.methodtest
       server=host2.iu.hio.no
       returnclasses=null
       returnvars=retval
       ifelapsed=120

Note that an important aspect of remote method invocation is that there is only voluntary cooperation between the parties. A reply bundle from a finished method can collected from a server by the client many times, causing the classes and variables associated with it to be defined at regular intervals, controlled by the ifelapsed time. To avoid multiple actions, you should lock methods or their follow-up actions with long ifelapsed times. This is a fundamental `feature' of voluntary cooperation: each party must take responsibilty for the sense of what it receives from the other. This feature will not be to everyone's taste, and it is unconventional. However, voluntary cooperation provides a way of collaborating without trust in a framework that forces us to confront the security issues directly. As such, it is a successful experiment.


Previous: remote method examples, Up: CFEngine Methods

3.5 Method self-test questions

  1. What language can I write methods in?
  2. Where do I put the method code?
  3. How do I send a variable to a method?
  4. How do I send a class to a method?
  5. How do I get variables and classes back from methods?

Table of Contents