In this module you will learn about
|
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.
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.
“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.”
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.
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.
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.
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.
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.
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"
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.
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.
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.
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.
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.
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
|
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.
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.
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.
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.
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
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 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 $*
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.
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.
--no-modules
)
Ignore modules in actionsequence.
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
|
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
forcereplyto
returnvars
returnclasses
sendclasses
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.
# # 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\"" }
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)" }
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.
Collecting user ssh keys for adding to an authorized key file (e.g. for delegating root login) can be automated.
#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 }
# 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 }
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
|
# # 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
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 }
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)." }
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.
|
Table of Contents