PICA: Perl Installation and Configuration Agent
Miguel Armas del Río and Esteban Manchado Velázquez
At the ULPGC's (Universidad de Las Palmas de Gran Canaria,
University of Las Palmas of Gran Canaria) Network Division, we administer
several servers that run critical network services such as DNS,
DHCP, network monitoring, etc. Because these services are critical,
we run a number of scripts on every server to check sanity and try
to fix basic error situations.
We needed a way to distribute all these scripts and the important
services' configuration files from a centralized location with
little differences to adapt them to each host. We also needed a
way to register any change on the configuration files, to be able
to detect when a particular error was introduced, and who did it,
and we wanted to centralize all network incident notifications and
alarm management.
To meet all these needs, we developed PICA (http://pica.ulpgc.es).
With PICA, we have a central repository of configuration files and
alarm scripts. This repository is managed using CVS, so we can recover
old versions, see change logs, and let various admins work concurrently.
Actually, every sys admin has a local copy of the working tree,
and CVS does all the dirty work.
In this scenario, PICA is used to distribute the configuration
files and alarm scripts to the various servers. PICA uses SSH to
establish secure connections to the remote servers, which is very
convenient, since we were already using SSH with RSA authentication
to access all remote servers. The alarm scripts send incident notifications
and service status reports to our central NetSaint (http://www.netsaint.org/)
server using asynchronous checks (see NetSaint documentation). If
a critical error is detected, an alarm is also sent via email and
as a SMS message to the sys admin mobile phone.
Approach
From the beginning, flexibility and security were important to
us. For that reason, we based PICA on SSH and, as the name already
suggests, Perl. We also liked the idea of easing the common case
and letting the user solve his own problems, if possible.
Now, you may be thinking "but there's PIKT" (http://pikt.uchicago.edu/pikt/).
Sure. We used it for almost a year, but we never really loved it.
It had too many features we didn't need. We didn't like
the RPC approach it used (fewer open ports means fewer security
problems). We already had SSH running on all servers, and we wanted
it to be the only way to access some of them. We also had SSH's
RSA authentication to log in securely to all servers without typing
passwords. So why not use it in the configuration tool for secure
encrypted connections to the remote servers?
We also didn't need PIKT's own scripting language (pikt).
We didn't particularly like learning yet another script language.
Sure, it had a lot of useful features like variable persistence,
but we could also do that with various Perl modules. Perl is a much
more complete programming language and allows us to do virtually
anything we need. So, although we know PIKT is a great tool, and
we like it's philosophy, it wasn't the solution for us.
PICA understands basically two concepts: objects and hosts. The
purpose of PICA is to manage objects (files) in different hosts
(computers). To achieve this in a simple manner, PICA lets us define
object groups and host groups, so a collection of objects can be
installed, deleted, listed, executed, or compared in a collection
of hosts (just in a single process). Since PICA knows the hosts
we are administering, it can also be used to execute standard UNIX
commands in a given group of hosts.
Moreover, PICA includes the PICA Framework for Integrated Alarms
(PIFIA), which is a collection of files and conventions to manage
alarms. In this context, alarms are little programs installed in
your machines that are executed periodically to check the sanity
of your servers.
PICA Components
PICA is built from several components. There is a central executable,
pica, a Perl preprocessor, and an alarm manager and Perl
library. The central executable reads the supplied command-line
arguments and does the job. The Perl preprocessor is used for every
configuration and non-verbatim distribution file before processing
(obviously). The alarm manager is used to set up and install alarms,
and comes with a handy Perl library. The latter is the most primitive
component, and will evolve a lot.
The preprocessor component is used on every PICA configuration
file. That means the files are terribly dynamic, as the preprocessor
is completely Perl-based. The recognized directives are:
- #perl/#lrep -- For executing completely arbitrary
Perl code. Anything returned by this code will be substituted
for the original code.
- #if/#elsif/#else/#fi -- For conditionals, from
parenthesized arbitrary Perl conditions.
- <# ... #> -- Inline #perl/#lrep environment
(for one-liners).
Thus, we can select portions of code based on simple conditionals,
build parts of the configuration or distribution files on the fly,
and just about anything you can think of.
The PICA central executable begins by preprocessing and reading
hosts.conf. Here, the program finds out about the machines,
with their attributes, variables, and such. Then, and once for each
host, PICA preprocess and reads objects.conf. Obviously,
as the file is preprocessed once for each machine, the result can
be different, because the preprocessor namespace is, in general,
distinct. That way, we can define different distribution files,
or in different ways, for each machine. We can:
1. Make the host definition depend on command-line variables.
2. Make the objects definition depend on command-line and host
variables.
3. Make the contents of the objects depend on almost everything.
Understanding this is very important to understand PICA behavior.
Once you get the hang of it, you will know how to make PICA do what
you want.
The last PICA component, the alarm manager, is basically just
design ideas, and is discussed further in the sidebar ("PICA
Framework for Integrated Alarms (PIFIA)".
PICA Usage
To start using PICA, we create the config files where we specify
which "objects" (files and alarms) we want to distribute,
and which hosts will be administered using PICA.
The Hosts File
The file hosts.conf is used to configure the hosts that
should be administered using PICA. We can organize the hosts in
different, possibly overlapped, groups that can be later used in
conditions, so we could install a given file only in hosts belonging
to a given group. We can also define variables for a given host
or group and use them later. This way we can specify different values
for a variable depending on the group we are working on.
For each group or host, we can define attributes and local variables.
Attributes are object properties or general preferences for PICA.
Thus, they are limited to several that are already defined (like
paths for different programs, uid and gid to set in distributed
files, etc.). On the other hand, local variables are user-defined
identifiers with scalar values associated. They are substituted
in the Perl preprocessor expressions. Note that every host should
be "declared" for PICA to know about it. This is to prevent
big mistakes.
A simple example of the hosts.conf file is shown in Listing
1. For each host we can specify the attribute "fqdn",
which is the name PICA will use to create the secure connection.
If we don't specify it, PICA will use the name used to define
the host object. This attribute is useful if you have hosts in different
domains and don't want to use the FQDN for the host definition.
The Objects File
The objects.conf file defines all the objects that will
be distributed using PICA, and depends on the current host. In general,
you can include or generate dynamically parts of it, depending on
evaluations of previously defined variables. These, at this point,
will be:
1. Variables defined in the hosts.conf "defaults"
environment.
2. Variables defined in the objects.conf "defaults"
environment.
3. Variables defined in any of the groups to which the current
host belongs.
4. Variables defined in the current host.
The latter, of course, have preference in case of multiple definition.
We also can use the internal variable $picahost to get the
name of the current host. Because of that, the possibilities explode
here: we can write entire Perl scripts (or in other languages, as
long as we call them from Perl) to dynamically generate configuration
files, depending on defined variables, the day of the week, etc.
And, here resides the power of PICA.
There are two types of objects in PICA: files and alarms. Depending
on the object type, they will have some mandatory and some optional
attributes:
Mandatory Attributes
So urce -- Where to read the file source from. If this is a relative
path, $picasrc is prepended.
Pr iority -- Determines the priority of the alarm. It also determines
where the alarm will be installed in the remote host. ($picaalrm/$priority)
So urce -- Where to read the alarm code from. If this is a
relative path, $picasrc is prepended.
Additional Attributes
- uid -- uid to set in the installed file. Default: 0
- gid -- gid to set in the installed file. Default: 0
- verbatim -- If set to 1, the object will be installed without
preprocessing it. It is useful to install binary files, and files
that could include preprocessor directives that we don't
want to parse. Default: 0
- perms -- File permissions to set in the installed file.
Default: 644
- path -- Where to install the file in the remote host. If
it isn't specified, use the same as "source" attribute.
This attribute is only used in file objects.
For any of these attributes, we can define default values in the "defaults"
environment (see example).
A simple objects.conf file is shown in Listing 2. There
is a defaults section that can be seen from any object, and we can
define variables that can only be seen by the object they are defined
in.
Any time PICA operates with an object, it will build a namespace
containing all the variables seen by that object in the host it
is working on. Of course, the most specific definition has precedence.
That is, if we define a variable in an object, and that object belongs
to a group where the same variable is also defined, the value used
is the one given in the object definition. Besides these variables,
we always have $picahost and $picaobject, which hold
the current host and object we are processing.
Command-Line Syntax
Once we have built our hosts and objects definitions, we can start
using PICA. Basically, PICA always does "something" on
a list of objects on each of the given hosts. As the the help option
(-h) states, PICA has the following command-line syntax:
PICA -- Perl Installation and Configuration Agent, Version:
v0.0.1
Usage: pica -[ixtflh] [-d] [-v] +|-F objects +|-D defines +|-H
hosts
-i -- Install objects
-x -- Execute object/command
-t -- Delete object
-f -- Diff object
-l -- List objects
-h -- Shows this help
-d -- Debug. Don't install/delete things, just testing
+|-F -- Build object list
+|-D -- Build defines list
+|-H -- Build hosts list
\The command line has three different mandatory parts:
1. The command -- This is the first group of options, and
it determines what we want to do.
2. The object arithmetic -- Determines what objects we want
to operate on. The object list is built using +F/-F to add/delete
objects.
3. The hosts arithmetic -- Determines what hosts we want to
operate on. The hosts list is built using +H/-H to add/delete
hosts.
Object and Host Arithmetic
The object and host list on which we want to operate is built
with +F/-F and +H/-H. With +H/-H we add/delete
hosts or groups to the hosts list in the same order they are entered.
For example, the expression:
+H dnsservers solaris -H deimos
will result in the host list "fobos mercurio sar",
since PICA will add the members in groups "dnsservers" and
"solaris" and delete host "deimos".
The object list is built the same way using +F/-F, but
the object arithmetic is evaluated for every host. This is because
the objects available for every host can be different since we can
use conditionals in the objects file. For example, in the objects
file in Listing 2, all documentation objects will only be seen by
members of the group "doc". In both lists, the implicit
group "all" can be used to reference all defined objects
and hosts.
We always need at least one host and object, so options +F
and +H are mandatory. If after doing the host/object arithmetic
PICA gets an empty list (either objects or groups), it gives an
error message and aborts execution.
We can also use +D/-D to build an optional list of definitions
that can be used to preprocess files and as variables in object
definitions, as if they were defined in the "defaults"
environment of the hosts.conf file. Actually, there is a
small but very important difference; because they are defined before
reading the hosts.conf file, these are the only variables
that can be used to conditionally preprocess this file.
PICA Commands
PICA currently supports five different internal commands, but
more could be added in the near future since we are still adding
new features. With each of these commands, we can use the -v
option for verbosity and -d for debug. Option -d gives
a lot of output. It doesn't really do anything, but just prints
what it would do.
All these commands are executed using SSH connections to the remote
hosts. Right now PICA doesn't have any access control system,
and will probably never have one. We like SSH, and it gives us everything
we need to access remote servers securely.
Note that if you try PICA, be sure to properly configure the RSA
authentication, or be ready to type a lot of passwords. One nice
trick is to distribute the SSH's RSA authentication files using
PICA as explained in one of the real-life examples described later.
Install (-i)
The install (-i) command will install the given objects
in each of the given hosts. The install command first generates
the objects it will install in a local dir ($picaroot/tmp/$picahost),
and then install them in the remote host.
PICA supports three different method for remote file installation:
- SSH -- This is the default method. It installs each file
using an SSH connection. This is the default method because it
only needs SSH, but it's painfully slow because it makes
a different connection for each file.
- tar -- This method uses tar over a SSH connection
to transfer all files using the same SSH connection. Right now,
this is the recommended method.
- rsync -- This method is still under development,
because we have found some problems with the way rsync
handles directory permissions and symlinks. It will be the recommended
method once we fix this problem, because it's the fastest.
Don't use it right now.
To change the installation method, set the attribute "method"
in the default definition of file objects.conf. To use methods
tar and rsync, you must also set the binary path to
this utils with attributes "tar" and "rsync".
The following command:
pica -iv +F NTP -F step-tickers +H all -H deimos
will install all objects in the group NTP, except "step-tickers",
in all servers except "deimos".
Execute (-x)
The execute command will execute the given list of commands in
each of the given hosts. If PICA finds an object with the given
name, it will read its path attribute and use it for remote execution.
This way we don't have to remember the location of the script.
If it doesn't find an object with that name, PICA assumes it's
a UNIX command and tries to run it.
For example, the command:
pica -xv +F DNSChkUrgent +H servers
will execute /usr/lib/pica/alarms/Urgent/DNSChkUrgent in all
servers, since the object DNSChkUrgent exists and PICA can build the
remote path using the object's attributes. On the other hand,
the command:
pica -xv +F "ndc reload" +H dnsservers
will run the command ndc reload in each member of the "dnsservers"
group. Since PICA can't find an object named "ndc",
it assumes it's a UNIX command and tries to execute.
List (-l)
This command lists the given objects. It basically executes ls
-l for every object's path. It can be used to see whether
an object is installed and whether the uid/gid and file permissions
for that object are correct.
Delete (-t)
This command deletes the given objects in each of the given hosts.
Diff (-f)
The diff command finds differences between the object that should
be installed in a host, and the one really installed. It basically
generates the object for that host, and makes a diff -u between
this object and the one installed in the remote host. It's
very useful to see whether a host has the latest version of an object.
Future Commands
As mentioned previously, we may add new commands to PICA in the
near future. One of the functionalities that still needs work is
the alarm management, and we will add new commands to enable, disable,
and list installed alarms in a remote host. For more information
about the current alarm functionality, please see the sidebar.
Real-Life Examples
Two Files from the Same Source
Let's begin with an example for distributing two files from
the same source. At the Network Division, we run the university's
primary nameservers. But we also give support to people that run
secondary servers. In one of our primary servers, we also wanted
to publish the configuration file needed to run a secondary server.
We wanted to use the same source to keep both files consistent.
This apparently simple task has created a lot of problems with other
tools we have used. With PICA, the solution is simple. In the case
of the masters, we have to install one version for production (the
master server file) and one for documentation (the slave DNS server
file). Thus, we defined, in hosts.conf, the following:
hostgroup dnsservers {
members { fobos, deimos, mercurio, ulpnet, ulpnet2 }
}
hostgroup dnsmaster {
members { ulpnet, ulpnet2 }
}
hostgroup doc {
members { ulpnet, ulpnet2 }
}
This way we can differentiate between documentation, DNS master, and
DNS slave servers. In objects.conf, we can define the relevant
files as:
#if (ingroup('dnsservers'))
group DNS {
file named.conf {
path = '/etc/named.conf';
source = "DNS/named_conf.cfg";
}
## Documentation for this Service
# if (ingroup('doc'))
# ...
file named.conf.sample {
path = '<#$docdir#>/Servicios/DNS/named.conf.sample';
source = 'DNS/named_conf.cfg';
}
# fi
}
#fi
The only thing left to do is define the named.conf file contents.
The trick is checking the name of the object: if it ends with .sample,
it's a slave sample file; it not, then it's a master file.
# ...
zone "ulpgc.es" {
#if ((ingroup('dnsmaster')) && ($picaobject !~ /\.sample$/))
type master;
file "mydb.db";
also-notify {
# ...
};
#else
type slave;
file "mydb.db.bak";
masters {
# ...
};
#fi
};
# ...
This shows the power of using arbitrary Perl code in PICA conditions.
Installing RSA Authentication Files within PICA
When you start administering a new server with PICA, you should
first configure SSH's RSA authentication, to be able to access
that server without typing any password. This task can be simplified
by distributing the needed files using PICA. We use SSHv2, so we
will assume this version of SSH. First of all, every sys admin needs
a private/public key pair. Let's say our public keys are in
SSHv2 format in the files sysadm1.pub and sysadm2.pub.
We will add the following entries to the objects.conf file:
# SSH RSA authentication files
group RSAAuth {
# SSHv2 authorization file
file ssh_auth {
path = '/root/.ssh2/authorization';
source = "SSH/authorization.cfg";
}
file sysadm1.pub {
path = '/root/.ssh2/sysadm1.pub';
source = "SSH/sysadm1.pub";
}
file sysadm2.pub {
path = '/root/.ssh2/sysadm2.pub';
source = "SSH/sysadm2.pub";
}
}
Different versions of SSH (SSHv2 or SSHv1) can be used in different
hosts using conditionals in the previous entries. This is left as
an exercise for the reader.
We could even generate the authorization file on the fly with
the needed Key entries with the following code snippet:
#perl
my @return;
# Get key files reading group members and skipping 'ssh_auth'
my @keys=grep(/\.pub$/,members('SSHAuth'));
foreach my $key (@keys) {
push @return,"Key $key\n";
}
# Return the array (will be printed)
@return;
#lrep
This code will generate one "Key file.pub" entry for each
public key file we define in the group, thus allowing access to the
server with that key. This is really outside the scope of this article,
but is a good example of what can be done with the #perl/#lrep
environment. With this configuration, after adding the new host to
the hosts.conf file, you could run the command:
pica -iv +F SSHAuth +H new_server
You will then have to type the server's password. After installing
this files, both sys admins will be able to access the server without
typing any password (assuming they are running ssh-agent).
Execution of Perl Code within Configuration Files
Here at Network Division, we have a very handy script to restart
critical services via the Web, just in case SSH is down on a given
server. Of course, it has a secret password to allow access only
to authorized administrators. Before PICA, we had to manually create
the password's MD5 hash using the "crypt" Perl function,
and include the hash in the source file. Now, we can use something
as elegant as this:
$passwd='<# crypt('secret-key-that-you-wont-ever-figure-out','salt') #>';
This way, just before sending the data, the command is executed and
the encrypted key is put automagically into the distributed file.
This saves time and work, and is a perfect example of the immense
power of PICA (and Perl, of course).
Conclusion
PICA is a great tool to ease the administration of large installations.
It allows the sys admin to keep all the important configuration
files and alarm scripts centralized. These files are created for
each host using the same source file, and we can include arbitrary
conditions depending on the target host, file name, and user-defined
variables.
We are successfully using PICA in an environment where we have
all the important configuration files centralized and distributed
using PICA. We also distribute a number of alarms that check important
conditions and notify them to our NetSaint (http://www.netsaint.org)
monitoring server.
Note that if you are considering using PICA, it's still a
work in progress. We have been working on it for only a month, and
although it's very usable now, it still needs work. Specifically,
the alarm framework is very basic and will probably be completely
rewritten. In fact, when you read this article, a lot of things
will probably have changed.
Miguel Armas is a 29-year-old Network and System Administrator
at the University of Las Palmas' Network Division. He has also
worked as a network and system consultant for five years. When not
working, he enjoys sailing, reading, and messing with Linux. He
can be reached at kuko@ulpgc.es.
Esteban Manchado is a 23-year-old Network and System Administrator
at the University of Las Palmas' Network Division. In his spare
time, he downloads MP3s, studies linguistics, listens to music,
and picon-scratches Elvis Crespo songs. He can be reached at zoso@ulpgc.es.
|