A Detailed Look at Creating a Component

Introduction

An LCFG component is a utility (usually a shell or perl script) that manages a subsystem by monitoring configuration resources within a machine's profile. For example, the fstab component manages disk layout by using resources in the profile to partition disks, create and mount filesystems, generate and update the fstab file, etc. Components are also used to control system daemons in a flexible and responsive way - for example, restarting or kicking a daemon if its configuration file changes.

On DICE machines, components live in the /usr/lib/lcfg/components directory, and are manipulated (stopped, started) by the "om" command. Note that a component may be said to be "running" even if there is not an active daemon associated with it. Running components are flagged by "component.run" files in the /var/lcfg/tmp directory.

A component consists of a set of files used to generate the run-time script, and there are LCFG tools and templates available to create these scripts and associated files.

The basic progression is from script to component to RPM: a script is created from scratch (or using a template), and then incorporated into a component framework by extracting configurable information into separate files, these files are then built into an RPM, which is distributed and installed as required. Resources are incorporated into the profile or other header files, and these are used by the relevant component to configure and control the appropriate daemon or subsystem on a per-machine basis.

How to Write a Component

First, catch your hare - or, in this case, decide on the actions you want your component to perform.

Note that if the new component is to perform a function that was already catered for in some other way (rc or init script, ad-hoc invocation, cron job, etc) then some integration of the new component will be required and some additional questions will need to be considered:

Certain basic functions (called methods) are inherited from a global component, ngeneric, offering ways of starting, stopping, and configuring the daemon or subsystem. However, it should be possible to create a script and run it as a normal shell (or perl) process:
./component start
- but in practice, this is not ideal (and is actively discouraged) because the environment in which the component runs may not match the environment in which it will normally be run (root permissions, available resources, etc).

For a more detailed discussion of writing a component, see the LCFG documentation.

How to Build a Component From Scratch

Once you have a working script (LCFG supports either Shell or Perl scripts) this needs to be converted into a form for use by buildtools. The absolute minimum for a (shell-scripted) component is just:

#!/bin/bash
. /usr/lib/lcfg/components/ngeneric
Dispatch "$@"
To get even this running successfully, additional steps are required, as there is no integration into the LCFG environment

In order to convert a standalone script into a standard-form LCFG component, various transformations are required, resulting in various constituent files:

Note that the buildtools Makefile has rules to generate a file from the abstracted .cin version.

Using ngeneric

When building a component - whether using a template or from scratch - the basic structure is supplied by the ngeneric component. This provides the building blocks which are used by all other components, and creates a uniform operating environment with a fixed set of basic methods (which other components will assume to exist, so it really does need to be included).

ngeneric provides a set of wrapper-methods, and defaults for the standard methods. For each standard method there is a wrapper-method, Method_methodname:

For example, the Start method is called from within Method_Start(), and is initially defined to do nothing (when called, it just returns true). The wrapper method, Method_Start(), does basic initialisation and housekeeping - and then invokes the Start method. If the new component has re-defined the Start method, then this code will get run instead. Any component must use ngeneric, and must use the Dispatch method to handle arguments.

Starting From Scratch With ngeneric

Consider the following simple, uncommented script, "basic":

#!/bin/bash

 . /usr/lib/lcfg/components/ngeneric

Dispatch "$@"
- we can abstract from this certain values, and add some comments. These values & comments can then be included in the config.mk file:
COMP=basic
NAME=lcfg-$(COMP)
DESCR=A Simple LCFG component
V=1.1.1
R=1
SCHEMA=1
VERSION=$(V)
GROUP=LCFG
AUTHOR=Simple Simon <simple.simon@pieman.com>
PLATFORMS=Solaris9, Fedora3, Fedora5, Fedora6
ORGANIZATION=University of Edinburgh

CONFIGDIR=$(LCFGCONF)/$(COMP)

MANDIR=$(LCFGMAN)/man$(MANSECT)

DATE=23/05/07 17:28
- and the script can then be generalised (conventionally named as <component.cin>, which in this example becomes "basic.cin"):
#!@SHELL@							
##########################################################	
#								
# Simple LCFG Component					
#								
# @AUTHOR@							
# Version @VERSION@ : @DATE@				
#								
# @MSG@							
#								
##########################################################	
								
@TESTSHELLV@ . @LCFGCOMP@/ngeneric				
								
##########################################################	
# Dispatch methods						
##########################################################	

Dispatch "$@"
- the two files, basic.cin and config.mk, are used together to recreate the (original) basic script.

Note that the @TESTSHELLV@ variable is only used during testing (to define local, test directories of system locations) and when a non-develop version is built, the variable is not set.

To automate the process, a generic makefile can be used (copied from one of the dice-example, lcfg-example or lcfg-skeleton components), which should contain no component-specific references - it just includes the buildtools.mk file, and provides some basic configure and install targets).

A documentation page can also be constructed, using the (slightly modified) .pod.cin file from one of the sample components above as a template.

Once these files have been created, they need to be included in a CVS repository (the DICE buildtools do make some assumptions, and one of them is that it can access files via CVS). After this has been done (using CVS import) the configure process should work, creating the script "basic", plus documentation (basic.pod & lcfg-basic.8), and the variable-substitution file, config.sh:

% touch TIMESTAMP
% make configure
>>>> configuring basic ...
>>>> configuring basic.pod ...
>>>> creating lcfg-basic.8 ...
% ls -1t
basic
basic.pod
config.sh
lcfg-basic.8
TIMESTAMP
CVS
config.mk
basic.cin
basic.pod.cin
Makefile
%
The next step is to build this into an RPM which can be submitted and then installed on other clients via their profile (or included header file). This can be done using "make rpm", but at this stage we're still testing/developing, and so should use "make devrpm". Trying to use "make rpm" without generating a new release first means that no tags will have been assigned - and there will be a subsequent error:
% make rpm
>>>> packing distribution ...
cvs [export aborted]: no such tag lcfg_basic_1_1_1
make: *** [pack] Error 1
% 
Since we're still testing, we should use "make devrpm". Note, however, that make objects to the non-existence of a spec file (the location of which is determined by evaluating "rpm --eval %_specdir" in buildtools.mk).
% make devrpm
>>>> packing development distribution
make[1]: Entering directory `~/linux/BUILD/lcfg-basic-1.1.1_dev'
make[1]: Leaving directory `~/linux/BUILD/lcfg-basic-1.1.1_dev'
make[1]: Entering directory `~/linux/BUILD/lcfg-basic-1.1.1_dev'
make[1]: Nothing to be done for `devprep'.
make[1]: Leaving directory `~/linux/BUILD/lcfg-basic-1.1.1_dev'
>>>> generating development specfile ...
make[1]: Entering directory `~/linux/BUILD/lcfg-basic-1.1.1_dev'
make[1]: Leaving directory `~/linux/BUILD/lcfg-basic-1.1.1_dev'
/bin/bash: specfile: No such file or directory
>>>> building development rpm ...
error: Name field must be present in package: (main package)
error: Version field must be present in package: (main package)
error: Release field must be present in package: (main package)
error: Summary field must be present in package: (main package)
error: Group field must be present in package: (main package)
error: License field must be present in package: (main package)
make: *** [devrpm] Error 1
%
A spec file can be created using the version in lcfg-example, but this cannot be used as-is, because it contains component-specific entries.

As we're only constructing a simple component, only a few changes are necessary:

Using this spec file, the building of the test RPM gets a little further, but make now objects to the lack of a "template" file. This can be created by copying "template.cin" from lcfg-example, and editing as required:
% cat template.cin
###############################################################
#
# Basic LCFG Component Template File
#
# @AUTHOR@
# Version @VERSION@ : @DATE@
#
# @MSG@
#
###############################################################

server = <%server%>
% 
Using this template file, the building of the test RPM gets a little further, but make now objects to the lack of a "component.def" file. This can be created by using the .def.cin file from lcfg-example, and modifying as required:
% cat ../lcfg-example/example.def.cin
/*
 * LCFG example component : default resources
 *
 * @AUTHOR@
 * Version: @VERSION@ @DATE@ (Schema @SCHEMA@)
 *
 * @MSG@
 *
 */

#include "ngeneric-1.def"
#include "om-1.def"

schema @SCHEMA@
server foo.bar.com
template @LCFGDATA@/@COMP@/template
configfile @CONFIGDIR@/config
%
Also, at this point, added two README files (referred to in the %files section of the spec file). These are not absolutely essential, although their use is recommended, but - in a full component - should provide information for the build.

How to Build a Component Using a Template

A sample component is provided by the lcfg-example RPM (but see also the dice-example and lcfg-skeleton RPMs). This supplies:
% rpm -ql lcfg-example-1.2.5
/usr/lib/lcfg/components/example
/usr/lib/lcfg/conf/example
/usr/lib/lcfg/conf/example/template
/usr/lib/lcfg/defaults/client/example-1.def
/usr/lib/lcfg/doc/pod/example.pod
/usr/share/doc/lcfg-example-1.2.5
/usr/share/doc/lcfg-example-1.2.5/ChangeLog
/usr/share/doc/lcfg-example-1.2.5/Makefile
/usr/share/doc/lcfg-example-1.2.5/README
/usr/share/doc/lcfg-example-1.2.5/README.BUILD
/usr/share/doc/lcfg-example-1.2.5/config.mk
/usr/share/doc/lcfg-example-1.2.5/example.cin
/usr/share/doc/lcfg-example-1.2.5/example.pod
/usr/share/doc/lcfg-example-1.2.5/specfile
/usr/share/man/man8/lcfg-example.8.gz
/var/lcfg/conf/example
% 
To create a new component, simple, for testing, we can rename a copy of the lcfg-example directory to lcfg-simple:
% cvs co lcfg-example
cvs checkout: Updating lcfg-example
U lcfg-example/ChangeLog
U lcfg-example/Makefile
U lcfg-example/README
U lcfg-example/README.BUILD
U lcfg-example/config.mk
U lcfg-example/example.cin
U lcfg-example/example.pod
U lcfg-example/specfile
% mv lcfg-example lcfg-simple
% mv lcfg-simple/example.cin lcfg-simple/simple.cin
% mv lcfg-simple/example.pod lcfg-simple/simple.pod
%
and edit the simple.cin file. In this example, the component-specific variable names in the sxprof command are amended, and the default start method is redefined to just give simple run-time information:
Start()
{   Info "Starting my component"
	Info "My arguments are $*"
	Info "My server resource is $LCFG foo server"
	Info "The verbose flag is $_VERBOSE"
}
Other files that need to be edited are:

config.mk variable substitution file

simple.pod man-page documentation

specfile RPM build file

README general info (if appropriate)

The config.mk file needs to be edited, to supply revised values for COMP, DESCR, & TARFILE (Version, Revision, and Schema definitions might also need to be changed to match whatever component is being created - although not in this example).

The simple.pod file needs to be edited to reflect documentation for the new component.

The specfile file needs to be edited to reflect the RPM build constraints. In this example, "%description" and "%doc" sections are modified. Other build parameters are generated by variable substitution from config.sh.

Using the edited files, the Makefile target "configure" generates the component script from the .cin file, using a variable substitution file (config.sh) created by the Makefile-included /usr/include/buildtools.mk file.

% make configure
>>>> configuring simple ...
>>>> creating lcfg-simple.8 ...
%
A manpage is similarly created (from a .pod file), also by the included /usr/include/buildtools.mk).

Once the configuration is completed, the Makefile target "install" moves the generated files into place:

% make install
echo installing ...
mkdir -p /usr/lib/lcfg/components
mkdir -p /usr/lib/lcfg/doc/pod
mkdir -p /usr/lib/lcfg/defaults/server
mkdir -p /usr/lib/lcfg/defaults/client
mkdir -p /usr/share/man/man8
mkdir -p /usr/lib/lcfg/conf/simple
mkdir -p /var/lcfg/conf/simple
install -m 0555 simple /usr/lib/lcfg/components/simple
install -m 0555 template /usr/lib/lcfg/conf/simple/template
install -m 0444 lcfg-simple.8 \
    /usr/share/man/man8/lcfg-simple.8
install -m 0444 simple.pod /usr/lib/lcfg/doc/pod/simple.pod
install -m 0444 simple.def \
    /usr/lib/lcfg/defaults/server/simple-1.def
install -m 0444 simple.def \
    /usr/lib/lcfg/defaults/client/simple-1.def
% 

Writing New Methods

The above should enable the creation of a new component, using modified versions of default methods (start, stop, configure, etc) which may be sufficient. If a completely separate action is required, a new method should be written and added to the component.cin file. This new method needs both a method definition and a Method_method definition. Consider the following simple example:
######################################################################
Method_HelloWorld() {
######################################################################

  HelloWorld
}

export -f Method_HelloWorld

######################################################################
HelloWorld() {
######################################################################

  # simplistic new method

  Info "Hello World!"
}
- note that this is not sufficient, however. It is also necessary to add the new method name to the list of allowable methods for that component, which is held in the component.om_methods resource. To do this, an entry needs to be made in the component.def.cin file (and hence component.def):
!om_methods     mADD(helloworld)
- and, since the mutation (mutex) syntax is not available to the component, this needs to be explicitly added to component.def.cin too:
#include        "mutate.h"
(note that any changes to the defaults file will require the copy on the LCFG master server to be updated - usually done by re-installing the defaults RPM).

This should be sufficient to add the new method to the component:

% qxprof basic.om_methods
om_methods=configure start stop restart run logrotate monitor \
    reset unlock status helloworld
% 
- which can then be invoked in the usual way:
% om basic helloworld
[INFO] basic: Hello World!
[OK] basic: helloworld
%

Installing A Component

Having created our component, we now have the two required testing/develop RPMs - one for the component installation, and one for the server defaults file:
% rpm -qpl ~/linux/RPMS/noarch/lcfg-basic-defaults-s1-1.1.1_dev-3.noarch.rpm
/usr/lib/lcfg/defaults/server/basic-1.def
%

% rpm -qpl ~/linux/RPMS/noarch/lcfg-basic-1.1.1_dev-3.noarch.rpm
/usr/lib/lcfg/components/basic
/usr/lib/lcfg/conf/basic
/usr/lib/lcfg/conf/basic/template
/usr/lib/lcfg/defaults/client/basic-1.def
/usr/lib/lcfg/doc/pod/basic.pod
/usr/share/doc/lcfg-basic-1.1.1_dev
/usr/share/doc/lcfg-basic-1.1.1_dev/README
/usr/share/doc/lcfg-basic-1.1.1_dev/README.BUILD
/usr/share/man/man8/lcfg-basic.8.gz
/var/lcfg/conf/basic
% 
When the testing version is complete, a new CVS release can be generated (which will create tags on the server) and "make rpm" can be used to create the distribution RPMs. Note that only distribution RPMs should be submitted (those built with "make rpm", and that development RPMs (those built with "make devrpm" and having "_dev" in the name) should not be submitted.

While testing, the make target "devinst" (which bumps-up the RPM release number) can be used to build and install the component RPM. Note that - even for testing - the defaults file needs to be installed on the LCFG master server (currently pointed to by the lcfg-master DNS alias).

% ssh lcfg-master
tobermory% nsu
tobermory# rpm -ivh ~/linux/RPMS/noarch/lcfg-basic-defaults-s1-1.1.1_dev-3.noarch.rpm
Preparing...                ########################################### [100%]
   1:lcfg-basic-defaults-s1 ########################################### [100%]
tobermory# ls -o /usr/lib/lcfg/defaults/server/basic-1.def
-r--r--r-- 1 root 349 May 30 13:03 /usr/lib/lcfg/defaults/server/basic-1.def
tobermory# 
While testing, the component and version resources need to be added to the profile (once testing is completed, they can be moved to the appropriate header file):
!profile.components     mADD(basic)     /* test component       */
!profile.version_basic  mSET(1)
Once the RPMs are submitted and installed, the component RPM name should be added to the profile.packages resource usually via rpmcfg, but possibly by mADDing it via a machine's profile (if, for example, in the final stages of testing). Note that profile.packages is a "logical" resource, and is not accessible via qxprof - it is constructed from RPM package lists in the lcfg/core/packages section of the SVN repository.

Once installed, the component should be invoked via "om":

% om basic start
[OK] basic: start
% ls -o /var/lcfg/tmp/basic.run 
-rw-r--r-- 1 root 0 May 31 08:48 /var/lcfg/tmp/basic.run
% 

% om basic stop
[OK] basic: stop
% 
Note that version numbers (when using "make release") are significant (see DICE Guidelines document for more details), and should consist of three components (X.Y.Z), which are:

Resources