keeping things running

At this point you can install a client unattended. The client will boot but you can't yet control which services will be running on that client. In this section we will configure services which we will use to configure the machine after it has been installed.

Some admins put a lot of effort into their kickstart files, having everything configured in the post section. In our farm we'll keep the kickstart fairly minimal and use other services such as puppet, augeas and func to configure the machine. By doing things this way, we ensure that we can keep systems configured without reinstalling them.

In order to install these services, we need to make our own yum repository that we can invoke at install time. We will do this in the next section.

repo

In order to have your clients install puppet at boot time we need to configure a yum repository for our puppet packages. I have previously posted how to do this as a blog entry here. There are x steps in creating a repo:

making your own repo, a comps.xml a group, using yum groupinstall make your own packages. sign them, key signing. about making packages in an appendix using yum, automatic for workstations, you control the repos.

signing user

The first step in creating your own repository is to make a gpg key for your key signer. I find it best to separate out the key signing completely from myself and have a separate account for signing rpms.
[root@server0 ~]# useradd signer
[root@server0 ~]# passwd signer
Changing password for user signer.
New UNIX password: 
Retype new UNIX password: 
passwd: all authentication tokens updated successfully.
[root@server0 ~]# su - signer
[signer@server0 ~]$ gpg --gen-key
gpg (GnuPG) 1.4.5; Copyright (C) 2006 Free Software Foundation, Inc.
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions. See the file COPYING for details.

Please select what kind of key you want:
   (1) DSA and Elgamal (default)
   (2) DSA (sign only)
   (5) RSA (sign only)
Your selection? 5
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 
Requested keysize is 2048 bits
Please specify how long the key should be valid.
         0 = key does not expire
        = key expires in n days
      w = key expires in n weeks
      m = key expires in n months
      y = key expires in n years
Key is valid for? (0) 10y
Key expires at Sun 21 Jul 2019 04:32:53 PM EDT
Is this correct? (y/N) y

You need a user ID to identify your key; the software constructs the user ID
from the Real Name, Comment and Email Address in this form:
    "Heinrich Heine (Der Dichter) "

Real name: Repository Signer
Email address: signer@example.com
Comment: Example Com
You selected this USER-ID:
    "Repository Signer (Example Com) "

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
You need a Passphrase to protect your secret key.

We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

...+++++
..........+++++
gpg: key 44CB93FD marked as ultimately trusted
public and secret key created and signed.

gpg: checking the trustdb
gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
gpg: next trustdb check due at 2019-07-21
pub   2048R/44CB93FD 2009-07-23 [expires: 2019-07-21]
      Key fingerprint = BD7F E3E1 3403 3F35 5DA7  C8AF CC08 B6BA 44CB 93FD
uid                  Repository Signer (Example Com) 

Note that this key cannot be used for encryption.  You may want to use
the command "--edit-key" to generate a subkey for this purpose.
Next, export the newly created key to a file.
[signer@server0 ~]$ gpg --export -a "Repository Signer (Example Com) " >RPM-GPG-KEY-example.com
Now, import the key into the rpm database to verify everything is working correctly.
[root@server0 install]# rpm -q gpg-pubkey-*
gpg-pubkey-f51839ac-46362566
gpg-pubkey-b2980b13-3c1d0597
[root@server0 install]# rpm --import ~signer/RPM-GPG-KEY-example.com 
[root@server0 install]# rpm -q gpg-pubkey-*
gpg-pubkey-f51839ac-46362566
gpg-pubkey-b2980b13-3c1d0597
gpg-pubkey-44cb93fd-4a68c9c4

signing rpms

RPMs Needed

This is a list of the packages needed
puppet
puppet-server
facter
ruby-shadow
augeas
augeas-libs
ruby-augeas
func
certmaster
smolt
python-ctypes
python-paste
python-simplejson

Now, you'll need to either download the src rpm or binary rpms for the packages you wish to redistribute to your clients (Make sure you can legally redistribute these rpms). For our purposes we will need the func, puppet, facter and augeas rpms. We will download them from the epel repository. Using lftp is one of the quickest ways to find all the right files and download them. First we'll setup the directories for our repository, then we'll start downloading.

[root@server0 ~]# cd /var/www/html/install
[root@server0 install]# mkdir -p Local/SRPMS Local/i386 Local/x86_64
[root@server0 install]# cd Local
[root@server0 Local]# lftp http://download.fedora.redhat.com/pub/epel/5/x86_64/
cd ok, cwd=/pub/epel/5/x86_64                                              
lftp download.fedora.redhat.com:/pub/epel/5/x86_64> get puppet-0.24.8-1.el5.1.noarch.rpm 
554952 bytes transferred                                             
lftp download.fedora.redhat.com:/pub/epel/5/x86_64> get puppet-server-0.24.8-1.el5.1.noarch.rpm 
26918 bytes transferred                                                   
lftp download.fedora.redhat.com:/pub/epel/5/x86_64> get func-0.24-1.el5.noarch.rpm 
282228 bytes transferred                                     
...
lftp download.fedora.redhat.com:/pub/epel/5/x86_64> quit
[root@server0 Local]# ls
augeas-0.5.1-1.el5.x86_64.rpm       puppet-server-0.24.8-1.el5.1.noarch.rpm
augeas-libs-0.5.1-1.el5.x86_64.rpm  ruby-augeas-0.2.0-1.el5.x86_64.rpm
...
python-simplejson-2.0.3-2.el5.x86_64.rpm

At this point we have our rpms downloaded, we need to place them in the appropriate directories, and also sign them (they are signed by the epel repo at this point). We'll sign them first, this requires giving the signer user permission to write in the Local directory and setting the .rpmmacros file in signers home directory to use the correct signing key.

[root@server0 Local]# rpm -K augeas-0.5.1-1.el5.x86_64.rpm 
augeas-0.5.1-1.el5.x86_64.rpm: (SHA1) DSA sha1 md5 (GPG) NOT OK (MISSING KEYS: GPG#217521f6) 
[root@server0 Local]# chown -R signer:signer .
[root@server0 Local]# su - signer
[signer@server0 ~]$ gpg --list-keys
/home/signer/.gnupg/pubring.gpg
-------------------------------
pub   2048R/44CB93FD 2009-07-23 [expires: 2019-07-21]
uid                  Repository Signer (Example Com) 

[signer@server0 ~]$ cat <.rpmmacros
> %_signature gpg
> %_gpg_name Repository Signer (Example Com) 
> EOF
[signer@server0 ~]$ cd /var/www/html/install/Local
[signer@server0 Local]$ rpm --resign *.rpm
Enter pass phrase: 
Pass phrase is good.
augeas-0.5.1-1.el5.x86_64.rpm:
augeas-libs-0.5.1-1.el5.x86_64.rpm:
facter-1.5.5-1.el5.noarch.rpm:
...
[signer@server0 Local]$ rpm -K augeas-0.5.1-1.el5.x86_64.rpm 
augeas-0.5.1-1.el5.x86_64.rpm: rsa sha1 (md5) pgp md5 OK

moving the rpms

Now that the rpms are signed by our key, we can move the rpms to the correct directories. If disk space it not at a premium, just copy the noarch rpms into both the i386 and the x86_64 directories. If you are concerned about usage (like me), hard link the noarch file in i386 to the one in x86_64 (they are the same file anyway).

[signer@server0 Local]$ mv *x86_64.rpm x86_64
[signer@server0 Local]$ for file in *noarch.rpm
> do
>  mv $file x86_64
>  ln x86_64/$file i386
> done

comps.xml (optional)

If you want to be able to install groups of packages from your repository, you'll need to define those groups in a comps.xml file. The syntax of the comps.xml file is fairly straightforward. You define groups, then within a group you define if a package is mandatory, required or optional. For our example, we can create a "Local Client" group that will install puppet, func and augeas.
<comps>
<!--  <meta> -->
<!-- Meta information will go here eventually -->
<!--  </meta> -->
  <group>
    <id>localclient</id>
    <name>Local Client</name>
    <default>true</default>
    <description>Default RPMS from Local Clients</description>
    <uservisible>true</uservisible>
    <packagelist>
      <packagereq type="default">puppet</packagereq>
      <packagereq type="default">ruby-augeas</packagereq>
      <packagereq type="default">facter</packagereq>
      <packagereq type="default">func</packagereq>
    </packagelist>
  </group>
</comps>

createrepo

Now that all the files are in place, we can run createrepo on our repository to generate the files necessary for yum.
[root@server0 install]# yum -y install createrepo
Setting up Install Process
Parsing package install arguments
Resolving Dependencies
--> Running transaction check
---> Package createrepo.noarch 0:0.4.4-2.fc6 set to be updated
--> Finished Dependency Resolution

Dependencies Resolved

========================================================================================================
 Package               Arch              Version                 Repository                        Size
========================================================================================================
Installing:
 createrepo            noarch            0.4.4-2.fc6             Install_Server_Client             37 k

Transaction Summary
========================================================================================================
Install      1 Package(s)         
Update       0 Package(s)         
Remove       0 Package(s)         

Total download size: 37 k
Downloading Packages:
Running rpm_check_debug
Running Transaction Test
Finished Transaction Test
Transaction Test Succeeded
Running Transaction
  Installing     : createrepo                                        [1/1] 

Installed: createrepo.noarch 0:0.4.4-2.fc6
Complete!
[root@server0 install]# cd Local
[root@server0 Local]# mv comps.xml x86_64
[root@server0 Local]# ln x86_64/comps.xml i386
[root@server0 Local]# createrepo -g comps.xml x86_64
9/9 - augeas-0.5.1-1.el5.x86_64.rpm                                             
Saving Primary metadata
Saving file lists metadata
Saving other metadata
[root@server0 Local]# createrepo -g comps.xml i386
5/5 - func-0.24-1.el5.noarch.rpm                                                
Saving Primary metadata
Saving file lists metadata
Saving other metadata
Now that our repo files are in place, we need to create an rpm for our repository so we can install it with kickstart or we can install a repo file and add the key to rpm using kickstart. Creating an rpm is probably cleaner. If you want to test this repo at this point, you can add the following file to /etc/yum.repos.d, and copy the RPM-GPG-KEY-example.com that we created earlier into /etc/pki/rpm-gpg (actually, since we already imported the key with rpm --import, yum shouldn't complain about keys at this point, but copying the file is a good idea).
[root@server0 yum.repos.d]# cp ~signer/RPM-GPG-KEY-example.com /etc/pki/rpm-gpg/
[root@server0 yum.repos.d]# cat local.repo
[Local]
name=Local RPMS $releasever - $basearch
baseurl=file:///var/www/html/install/Local/$basearch
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-example.com
enabled=1
[root@server0 yum.repos.d]# yum search augeas
Local                                                                            | 1.1 kB     00:00     
primary.xml.gz                                                                   | 2.8 kB     00:00     
Local                                                          7/7
=========================================== Matched: augeas ============================================
augeas.x86_64 : A library for changing configuration files
augeas-libs.x86_64 : Libraries for augeas
ruby-augeas.x86_64 : Ruby bindings for Augeas
[root@server0 yum.repos.d]# yum grouplist |grep Client
   Local Client

repository rpm (optional)

If you don't want to build a repository rpm, you can just put the local.repo file into your kickstart file. You will need to import the key using rpm --import also.

A somewhat better way to do it is to make a repository rpm that install the file and the key. To make such an rpm, create a working directory in signer's home directory

[root@server0 ~]# yum -y install rpm-build
[root@server0 ~]# su - signer
[signer@server0 ~]$ mkdir -p src/RPMS src/SPECS src/BUILD src/SRPMS
[signer@server0 ~]$ echo "%_topdir /home/signer/src" >> ~/.rpmmacros
[signer@server0 ~]$ cd src/SPECS
[signer@server0 SPECS]$ cat Example.com-local.spec
Summary: yum Local repository
Name: Example.com-Local
Version: 1
Release: 1
Group: System Environment/Base
License: GPL
BuildRoot: %{_tmppath}/%{name}-root
BuildArch: noarch

%description
This rpm contains the yum Example.com Local repository

%prep

%build

%install
mkdir -p $RPM_BUILD_ROOT/etc/yum.repos.d/
cat > $RPM_BUILD_ROOT/etc/yum.repos.d/local-%{version}-local.repo < $RPM_BUILD_ROOT/etc/pki/rpm-gpg/RPM-GPG-KEY-example.com <
- initial release

[signer@server0 SPECS]$ rpmbuild -ba Example.com-local.spec 
Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.10406
+ umask 022
+ cd /home/signer/src/BUILD
+ exit 0
Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.10406
+ umask 022
+ cd /home/signer/src/BUILD
+ exit 0
Executing(%install): /bin/sh -e /var/tmp/rpm-tmp.10406
+ umask 022
+ cd /home/signer/src/BUILD
+ mkdir -p /var/tmp/Example.com-Local-root/etc/yum.repos.d/
+ cat
+ mkdir -p /var/tmp/Example.com-Local-root/etc/pki/rpm-gpg/
+ cat
+ exit 0
Processing files: Example.com-Local-1-1
Requires(interp): /bin/sh /bin/sh
Requires(rpmlib): rpmlib(CompressedFileNames) <= 3.0.4-1 rpmlib(PayloadFilesHavePrefix) <= 4.0-1
Requires(post): /bin/sh
Requires(postun): /bin/sh
Checking for unpackaged file(s): /usr/lib/rpm/check-files /var/tmp/Example.com-Local-root
Wrote: /home/signer/src/SRPMS/Example.com-Local-1-1.src.rpm
Wrote: /home/signer/src/RPMS/noarch/Example.com-Local-1-1.noarch.rpm
Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.10406
+ umask 022
+ cd /home/signer/src/BUILD
+ rm -rf /var/tmp/Example.com-Local-root
+ exit 0
[signer@server0 SPECS]$ cd ../
[signer@server0 src]$ cp RPMS/noarch/Example.com-Local-1-1.noarch.rpm SRPMS/Example.com-Local-1-1.src.rpm /var/www/html/install/Local
Now we need to move the rpms into the correct directories and rerun createrepo. We should script this at this point. Here is a simple script (modified from the one we use) that checks if rpms are signed and then links them into the appropriate directory. By linking first, we can make multiple links and then remove the copy in the current directory.
[signer@server0 Local]$ mkdir ~/bin
[signer@server0 Local]$ cat ~/bin/update_repo 
#!/bin/sh

COMPS=comps.xml

# determine the architecture of the rpm (noarch x86_64 i386...)
rpm_arch() {
	echo $1 | awk -F'.' '{NF=NF-1; print $NF}'
}

# build a list of rpms to move
for i in $@ *.rpm; do
	if [ -f "$i" ]; then
		case $i in
			*rpm)
				if rpm -K $i | awk '/pgp/ && /OK/ && !/NOT OK/' &>/dev/null; then
					RPMLIST="$RPMLIST $i"
				else
					echo "ERROR: rpm $i is NOT SIGNED"
					exit 1
				fi
				;;
			*)
				echo "ERROR: $i is not an rpm"
				exit 1
				;;
		esac
	else
		if [ "XXX$i" != "XXX*.rpm" ]; then
			echo "ERROR: $i is not a file"
			exit 1
		fi
	fi
done

echo $RPMLIST
if [ -d i386 -a -d x86_64 -a -d SRPMS ]; then
	for i in $RPMLIST; do
		ARCH=`rpm_arch $i`
		case $ARCH in
			src)
				ARCH=SRPMS
				;;
			i386|i486|i586|i686)
				ARCH=i386
				;;
			x86_64)
				;;
			noarch)
				ARCH="i386 x86_64"
				;;
			*)
				ARCH=unknown
				echo "$i unknown architecture"
				;;
		esac
		ERROR=""
		if [ "$ARCH" != "unknown" ]; then
			for DESTARCH in $ARCH
			do
				if [ -e $DESTARCH/$i ]; then
					echo "$i already exists in $DESTARCH"
					ERROR=1
				else
					echo "linking $i into $DESTARCH"
					ln $i $DESTARCH
				fi
			done
			if [ -z "$ERROR" ]; then
				# linking was successful, remove file
				rm -f $i
			else
				echo "ERROR: could not link $i"
			fi
		fi
	done
	echo "Running createrepo now"	
	for ARCH in i386 x86_64
	do
		createrepo -g $COMPS $ARCH
	done
else
	echo "ERROR: required directories not found (i386 x86_64 SRPMS)"
fi
[signer@server0 Local]$ chmod 755 ~/bin/update_repo
[signer@server0 Local]$ ~/bin/update_repo 
ERROR: rpm Example.com-Local-1-1.noarch.rpm is NOT SIGNED
We forgot to sign the rpms we just built. Sign them now.
[signer@server0 Local]$ rpm --addsign *rpm
Enter pass phrase: 
Pass phrase is good.
Example.com-Local-1-1.noarch.rpm:
gpg: WARNING: standard input reopened
gpg: WARNING: standard input reopened
Example.com-Local-1-1.src.rpm:
gpg: WARNING: standard input reopened
gpg: WARNING: standard input reopened
Now try that update again
[signer@server0 Local]$ ~/bin/update_repo 
Example.com-Local-1-1.noarch.rpm Example.com-Local-1-1.src.rpm
linking Example.com-Local-1-1.noarch.rpm into i386
linking Example.com-Local-1-1.noarch.rpm into x86_64
linking Example.com-Local-1-1.src.rpm into SRPMS
Running createrepo now
4/4 - func-0.24-1.el5.noarch.rpm                                                
Saving Primary metadata
Saving file lists metadata
Saving other metadata
8/8 - augeas-0.5.1-1.el5.x86_64.rpm                                             
Saving Primary metadata
Saving file lists metadata
Saving other metadata
Now we have our rpm signed and in our repo and can install it at install time using kickstart.

putting the repo into kickstart

We can add the repository to our kickstart with the repo keyword. We can then specify packages or groups from the repository in the %packages section.

repo --name=Local --baseurl=http://server0.example.com/install/Local/x86_64
We will want our group to be installed as well as our repository rpm, here is how we do that.
@localclient
Now after installation our client will have the following packages installed from our repository
[root@client15 ~]# rpm -q puppet ruby-augeas func
puppet-0.24.8-1.el5.1
ruby-augeas-0.2.0-1.el5
func-0.24-1.el5
Now that we have our extra rpms installed, we can move on to configuring puppet.

puppet

puppet is made by Reductive Labs and released under the GNU Public License. It is a system to manage configuration files and services. It is written in ruby.

There are several products out there that do the same job as puppet, the main competitor is Cfengine. There is also the open source version of Red Hat satellite server spacewalk. All these systems have their various benefits and pitfalls. I find puppet to be simpler to configure and very easy to extend. One possible pitfall is that it is written in Ruby, and I have almost no experience in ruby. That said, it's a very simple language to read to quite easy to follow (unlike PERL).

To configure puppet, you install the puppet-server rpm or build from source. The rpms are available for centos, RHEL (via EPEL) and Fedora. In the previous section we downloaded the puppet rpms from EPEL and installed them in our local repository.

The steps to configure puppet are

puppetmaster

The puppetmaster is installed from the puppet-server rpm. Using yum, you should get the following dependencies installed: augeas-libs; puppet; ruby-augeas; and ruby-shadow (or possibly more). After installing the puppet-server rpm, you can chkconfig puppetmaster on and cd to /etc/puppet
[root@server0 ~]# chkconfig puppetmaster on
[root@server0 ~]# cd /etc/puppet
[root@server0 puppet]# ls
fileserver.conf  manifests  puppet.conf
[root@server0 puppet]# ls manifests
[root@server0 puppet]# 
Both the server and client versions use the /etc/puppet directory. Both are configured from puppet.conf. The default configuration of puppet.conf will be sufficient at this point, but the manifests directory is completely empty. This is possibly the biggest complain with puppet, you start with a blank slate. It is up to you to populate the manifest directory. The only file you will need is site.pp in that directory. The site.pp file names all the other files you wish to include in your manifest.

The puppetmaster listens on port 8140, so we'll need to add that port to our firewall

[root@server0 ~]# iptables -I RH-Firewall-1-INPUT -p tcp -m state --state NEW,ESTABLISHED,RELATED --dport 8140 -j ACCEPT
[root@server0 ~]# iptables-save >/etc/sysconfig/iptables
We cannot start the puppetmaster yet, we have an empty manifests directory, we'll start building the contents of that directory now.

manifests, classes and types

The manifests are files that contain a series of functions, types, classes and control logic to apply a set of actions to nodes. In the manifests you define classes which are a grouping of type instances and functions. Classes are then applied to nodes.

There are many types predefined, for a complete list of all types and their usage, see the documentation. Common types used are:

To create a new class, use the class keyword. As a simple example, we'll create a class which creates a file in root's home directory called hello, with the word "world" in it.

class test {
	file {"/root/hello":
		content => "world",
		mode => 644,
		owner => root,
		group => root
	}		
}
To have this class applied to a node, we need to assign the class to the node with the node and include keywords.
node client15 {
	include test
}
Put these definitions in site.pp and start puppetmaster.
[root@server0 manifests]# cat site.pp
class test {
	file {"/root/hello":
		content => "world",
		mode => 644,
		owner => root,
		group => root
	}		
}

node client15 {
	include test
}
[root@server0 manifests]# service puppetmaster start
Starting puppetmaster:                                     [  OK  ]
Now that we have a minimal site.pp installed, we can configure our client and test the configuration. We'll configure clients from kickstart later, but to test the install at this point, login to client15 and execute puppet manually.
[root@client15 ~]# puppetd --no-daemonize  --server server0.example.com --test --no-splay
info: Creating a new certificate request for client15.example.com
info: Creating a new SSL key at /var/lib/puppet/ssl/private_keys/client15.example.com.pem
warning: peer certificate won't be verified in this SSL session
notice: Did not receive certificate
notice: Set to run 'one time'; exiting with no certificate
Our client successfully connected to the puppetmaster but it failed to retrieve a catalog (what puppet calls the collection of actions to perform on the client). This is because our ssl key was not signed by the puppetmaster. Back on the puppetmaster, we'll sign the key for client15 and then try our test again.

[root@server0 ssl]# puppetca --list
client15.example.com
[root@server0 ssl]# puppetca --sign client15.example.com
Signed client15.example.com
Back on client 15
[root@client15 ~]# puppetd --no-daemonize  --server server0.example.com --test --no-splay
warning: peer certificate won't be verified in this SSL session
notice: Got signed certificate
info: Caching catalog at /var/lib/puppet/localconfig.yaml
notice: Starting catalog run
notice: //Node[client15]/test/File[/root/hello]/content: defined 'content' as '{md5}7d793037a0760186574b0282f2f435e7'
notice: //Node[client15]/test/File[/root/hello]/owner: defined 'owner' as 'root'
notice: //Node[client15]/test/File[/root/hello]/group: defined 'group' as 'root'
notice: //Node[client15]/test/File[/root/hello]/mode: defined 'mode' as '644'
info: Creating state file /var/lib/puppet/state/state.yaml
notice: Finished catalog run in 0.03 seconds
[root@client15 ~]# cat /root/hello
world[root@client15 ~]# 
Now that we have verified that puppet is working, we'll make a proper site.pp file and some useful classes.

site.pp

In our configuration, we will keep node definitions in the file nodes.pp, any puppet functions we define in functions.pp and then any classes we implement in files named for the class they contain. Our initial site will be the following:
# site.pp
import "functions.pp"

# define nodes
import "nodes.pp"

# define classes
import "base.pp"

functions.pp

Function definitions are created with the define keyword. Functions can go in any of the pp files in the manifests directory. I choose to keep them separate for accounting purposes. My current functions.pp only has two functions in it, and they are very simple. The first one is a redefinition of the file type with defaults for my installation.
define remotefile($owner = root, $group= root, $mode, $server = "server0.example.com", $cls="base", $backup = false, $recurse = false) {
	file {
	    $name:
            mode => $mode,
            owner => $owner,
            group => $group,
            backup => $backup,
            source => "puppet://$server/$cls/$name"
	}
}
The name of the function is remotefile, since we imported functions.pp in our site.pp, we can use remotefile in any other file we include from site.pp. The file type in puppet can have a remote location that is given in the source option. Our function defines a default location of puppet://server0.example.com/base/$name where name is the name of the file. We also set the owner and group to root by default. Without using this function, copying an /etc/resolv.conf file from our puppetmaster to a client would look like this:
file {"/etc/resolv.conf":
    mode => 644,
    owner => root,
    group => root,
    backup => false
    source => "puppet://server0.example.com/base/etc/resolv.conf"
}
Using the function we can slim this down to:
remotefile { "/etc/resolv.conf": mode=> 644 }
Next we will define our nodes and a default class for puppetmaster to apply to the nodes.

nodes.pp

Nodes are defined with the node keyword. In a node you call the relevant classes with include class. The special node named default is used to specify which class to apply to all nodes that aren't addressed specifically. Our simple setup will have only the default definition.

node default {
    include base
}

With this defined, we'll create another manifest for the class base.

base.pp

In this class we'll define those changes that we want on all the machines in our organization. We'll start the class with a class definition and include anything we with to define. As a practical example, we'll create an ssh key for the user signer and install that key on any puppet clients that register with our puppetmaster. We'll also write out the puppet configuration file in /etc/sysconfig (should we need to update it at a later date, it will already be under puppet control).

[root@server0 manifests]# su - signer
[signer@server0 ~]$ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/signer/.ssh/id_rsa): 
Created directory '/home/signer/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/signer/.ssh/id_rsa.
Your public key has been saved in /home/signer/.ssh/id_rsa.pub.
The key fingerprint is:
35:b7:84:6d:34:8c:76:9a:8d:7c:3e:4a:e8:c1:1e:fd signer@server0.example.com
[signer@server0 ~]$ cat .ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAr9rnu0jbSPipuZI2umz/v73jeRTjxlX9D7cHSFIaJUUShFSelFUfojkjl4ri4m4qc40icArMa4NMGZ9d3y+ZqMqeIPZVtJKEqkn2E9GJS36N13H75DwVPv4KE2oLR9Zk4T8HovLr50tWbJr5/G6VfwwybR3q6HdJSO7liAKmrJwFokev1fsmiZQX+rADL8XB+gZ/9FsFIi4F4YKsLGQz78CSf/jZ71qNC5Y4HniVQDv6RmZp+koHT6hOPKTuUD/VOWXHxoLc9c6ypkeSMaINvNHDvmUsbp+rNppiZPKnsDFoh3fL4h5pFKJ1DAYjOdnhLKJgwLzmBq7qfYpd/PEw2Q== signer@server0.example.com
[signer@server0 ~]$ exit
[root@server0 manifests]# cat base.pp 
class base {
	remotefile { "/etc/sysconfig/puppet": mode => 644 }
        service { puppet: ensure => true, enable => true, hasrestart => true }
	ssh_authorized_key { "signer":
		ensure => present,
		type => "ssh-rsa",
		key => "AAAAB3NzaC1yc2EAAAABIwAAAQEAr9rnu0jbSPipuZI2umz/v73jeRTjxlX9D7cHSFIaJUUShFSelFUfojkjl4ri4m4qc40icArMa4NMGZ9d3y+ZqMqeIPZVtJKEqkn2E9GJS36N13H75DwVPv4KE2oLR9Zk4T8HovLr50tWbJr5/G6VfwwybR3q6HdJSO7liAKmrJwFokev1fsmiZQX+rADL8XB+gZ/9FsFIi4F4YKsLGQz78CSf/jZ71qNC5Y4HniVQDv6RmZp+koHT6hOPKTuUD/VOWXHxoLc9c6ypkeSMaINvNHDvmUsbp+rNppiZPKnsDFoh3fL4h5pFKJ1DAYjOdnhLKJgwLzmBq7qfYpd/PEw2Q==",
		name => "signer@example.com",
		target => "/root/.ssh/authorized_keys"
	}
}
[root@server0 manifests]# 
Now we have to create /etc/sysconfig/puppet that we referenced in our call to remotefile.
[root@server0 puppet]# pushd /var/lib/puppet/files
/var/lib/puppet/files /etc/puppet
[root@server0 puppet]# mkdir -p files/base/etc/sysconfig
[root@server0 puppet]# mkdir facts
[root@server0 puppet]# cd files/base/etc/sysconfig
[root@server0 sysconfig]# cat <puppet
> PUPPET_SERVER=server0.example.com
> PUPPET_EXTRA_OPTS=--factsync
> EOF
[root@server0 files]# popd
/etc/puppet
Next, we'll configure the puppet fileserver to serve out files that are stored in /var/lib/puppet/files/base

fileserver.conf

We configure the puppet fileserver from fileserver.conf
[root@server0 puppet]# cat fileserver.conf
[base]
	path /var/lib/puppet/files/base
	allow *.example.com
	allow 192.168.0.0/24

[facts]
	path /var/lib/puppet/facts
	allow *.example.com
	allow 192.168.0.0/24
Now we have defined the base and facts shares. The facts share is used by facter, something we'll talk about later, but we might as well configure it's file share while we are here. At this point we can restart the puppetmaster, but when new clients connect, they have to wait for us to manually sign their keys. We'll configure puppet to automatically sign keys using autosign.conf

autosign.conf

autosign.conf is a very simple file that simply lists the objects which will be automatically signed. In our case we can add example.com and the 192.168.0.0/24 subnet for automatic signing

[root@server0 puppet]# cat autosign.conf 
*.example.com
192.168.0.0/24

At this point new clients can connect and (provided they are in example.com or our subnet) they will have their keys automatically signed. They will all be placed in the base class by default. Our next step is to go back to our kickstart and have puppet configured with kickstart.

puppet in kickstart

We'll configure puppet in the %post section of our kickstart. We'll call puppet directly (not as a service) and have it create /etc/sysconfig/puppet for us (since we configured that file in our base class). This is one way to do this, another is to put the puppet configuration into the kickstart file and ensure that puppet is chkconfig'd on from there. The latter has the advantage that if the puppetmaster is unavailable at install time, the machine will still have the correct configuration.

Method 1:

%post
chvt 3
echo "executing post install"

echo hostname for puppet is $HOSTNAME
puppetd --fqdn=$HOSTNAME --test --no-splay --server=server0.example.com --onetime --verbose --factsync
echo "type enter to continue"
read enter_key

chvt 1

After installing your machine should change to virtual terminal 3 (chvt 3) and echo "executing post install" followed by the output from puppet
info: Retrieving facts
info: Caching catalog at /var/lib/puppet/localconfig.yaml
notice: Starting catalog run
notice: //Node[default]/base/Ssh_authorized_key[signer]/ensure: created
1,11c1,2
< # The puppetmaster server
< #PUPPET_SERVER=puppet
< 
< # If you wish to specify the port to connect to do so here
< #PUPPET_PORT=8140
< 
< # Where to log to. Specify syslog to send log messages to the system log.
< #PUPPET_LOG=/var/log/puppet/puppet.log
< 
< # You may specify other parameters to the puppet client here
< #PUPPET_EXTRA_OPTS=--waitforcert=500
---
> PUPPET_SERVER=server0.example.com
> PUPPET_EXTRA_OPTS=--factsync
notice: //Node[default]/base/Remotefile[/etc/sysconfig/puppet]/File[/etc/sysconfig/puppet]/source: replacing from source puppet://server0.example.com/base//etc/sysconfig/puppet with contents {md5}87dd2effcdc742da03df3ab010c03436
notice: //Node[default]/base/Service[puppet]/enable: enable changed 'false' to 'true'
notice: Finished catalog run in 0.23 seconds
type enter to continue

Method 2:

%post
chvt 3
echo "executing post install"

cat >/etc/sysconfig/puppet <<EOF
PUPPET_SERVER=server0.example.com
PUPPET_EXTRA_OPTS=--factsync
EOF
chkconfig puppet on

echo hostname for puppet is $HOSTNAME
puppetd --fqdn=$HOSTNAME --test --no-splay --server=server0.example.com --onetime --verbose --factsync
echo "type enter to continue"
read enter_key

chvt 1
Using this method, if the puppetmaster is down when this client goes to execute puppetd, it will fail but puppet will still be configured and will work when the machine is rebooted. I prefer to use Method 2 just in case.

At this point your clients are installing from scratch and getting configured by puppet automatically from kickstart. The bulk of the work is done, what you now need to do is go through your machine configurations and translate all the changes into puppet. This may take a while, but in the end it is well worth it. There may be some changes that are difficult to translate into puppet, for those hard to make changes that require some advanced sed/awk work, augeas may be the answer. We'll look at that in the next section.

For making changes on the fly (not waiting for puppet clients to check-in) you will need to configure func (or use cluster ssh or something similar). Now, on to augeas.

augeas

Augeas is a redhat project, it is a configuration API. It is a method for manipulating configuration files safely. It may be found at augeas.net.

Augeas manipulates files through a system of lenses. Lenses abstract configuration files into configuration parameters that can be manipulated using get and set commands in augeas. The first example I like to work with is /etc/hosts. First we'll install augeas on client15 then we'll make a new entry in /etc/hosts on client15.

[root@client15 ~]# yum install augeas
Loading "installonlyn" plugin
Loading "rhnplugin" plugin
Setting up Install Process
Setting up repositories
Local                     100% |=========================| 1.1 kB    00:00     
Reading repository metadata in from local files
...
Installed: augeas.x86_64 0:0.5.1-1.el5
Complete!
[root@client15 ~]# cat /etc/hosts
# Do not remove the following line, or various programs
# that require network functionality will fail.
127.0.0.1		localhost.localdomain localhost
::1		localhost6.localdomain6 localhost6
192.168.0.31		client15.example.com client15
[root@client15 ~]# augtool -b
augtool> ls /files/etc/hosts
#comment[1] = Do not remove the following line, or various programs
#comment[2] = that require network functionality will fail.
1/ = (none)
2/ = (none)
3/ = (none)
augtool> ls /files/etc/hosts/1/
ipaddr = 127.0.0.1
canonical = localhost.localdomain
alias = localhost
augtool> ls /files/etc/hosts/2/
ipaddr = ::1
canonical = localhost6.localdomain6
alias = localhost6
augtool> ls /files/etc/hosts/3/
ipaddr = 192.168.0.31
canonical = client15.example.com
alias = client15

augtool> set /files/etc/hosts/4/ipaddr 192.168.0.1
augtool> set /files/etc/hosts/4/canonical server0.example.com
augtool> set /files/etc/hosts/4/alias[1] server0
augtool> set /files/etc/hosts/4/alias[2] puppet.example.com
augtool> save
Saved 1 file(s)
augtool> quit
[root@client15 ~]# cat /etc/hosts
# Do not remove the following line, or various programs
# that require network functionality will fail.
127.0.0.1		localhost.localdomain localhost
::1		localhost6.localdomain6 localhost6
192.168.0.31		client15.example.com client15
192.168.0.1	server0.example.com server0 puppet.example.com
[root@client15 ~]# 

There are several lenses provided with augeas for manipulating different types of configuration files. The best way to work with augeas is to provide yourself a testing area and use the environment variable AUGEAS_ROOT to specify your "sandbox" to play in. It is important that the directory structure remain the same in your sandbox as it would be in /etc, file locations are used by augeas to apply the correct lens.

[root@client15 ~]# mkdir augeas-play
[root@client15 ~]# export AUGEAS_ROOT=/root/augeas-play
[root@client15 ~]# cp /etc/ssh/sshd_config augeas-play/
[root@client15 ~]# augtool -b
augtool> ls /files/sshd_config
augtool> quit
[root@client15 ~]# cd augeas-play/
[root@client15 augeas-play]# mkdir -p etc/ssh
[root@client15 augeas-play]# mv sshd_config etc/ssh/
[root@client15 augeas-play]# augtool -b
augtool> ls /files/etc/ssh/sshd_config
#comment[1] = $OpenBSD: sshd_config,v 1.73 2005/12/06 22:38:28 reyk Exp $
#comment[2] = This is the sshd server system-wide configuration file.  See
#comment[3] = sshd_config(5) for more information.
#comment[4] = This sshd was compiled with PATH=/usr/local/bin:/bin:/usr/bin
#comment[5] = The strategy used for options in the default sshd_config shipped with
#comment[6] = OpenSSH is to specify options with their default value where
#comment[7] = possible, but leave them commented.  Uncommented options change a
#comment[8] = default value.
#comment[9] = Port 22
#comment[10] = Protocol 2,1
Protocol = 2
#comment[11] = AddressFamily any
#comment[12] = ListenAddress 0.0.0.0
...
Subsystem/ = (none)
augtool> quit
After familiarizing yourself with augeas and making some changes to files in the sandbox, it's time to incorporate those changes into puppet using puppet's augeas plugin.

puppet-augeas

Help on using puppet-augeas is available on the puppet-augeas wikipage. After you have worked out the commands you need with augtool, you can rewrite them to work in puppet. From our previous example, we used set a few times to make a new entry in /etc/hosts. In puppet we'll use the augeas type and call set the same way.
augeas{ "server0":
	context => "/files/etc/hosts",
	changes => [
		"set 4/ipaddr 192.168.0.1",
		"set 4/canonical server0.example.com",
		"set 4/alias[1] server0",
		"set 4/alias[2] puppet-augeas.example.com",
	],
}

[root@client15 augeas-play]# puppetd --fqdn=$HOSTNAME --test --no-splay --server=server0.example.com --onetime --verbose --factsync
info: Retrieving facts
info: Caching catalog at /var/lib/puppet/localconfig.yaml
notice: Starting catalog run
notice: //Node[default]/base/Augeas[server0]/returns: executed successfully
notice: Finished catalog run in 1.87 seconds
[root@client15 augeas-play]# cat /etc/hosts
# Do not remove the following line, or various programs
# that require network functionality will fail.
127.0.0.1		localhost.localdomain localhost
::1		localhost6.localdomain6 localhost6
192.168.0.31		client15.example.com client15
192.168.0.1	server0.example.com server0 puppet-augeas.example.com
[root@client15 augeas-play]# 
We replaced the alias puppet.example.com with puppet-augeas.example.com using puppet-augeas. Trying to make this change with a combination of sed or awk would be possible, but with augeas it is much more clear what we are trying to achieve and it is safer also.

For a more concrete example, we'll modify ssh to deny password access. We'll use the puppet nofity system to have ssh restart after we make the change with augeas.
Added to base.pp

service { sshd: ensure => true, enable => true, hasrestart => true }
augeas{ "ssh":
	context => "/files/etc/ssh/sshd_config",
	changes => [
		"set PasswordAuthentication no"
	],
	notify => Service["sshd"]
}
Now when we run puppet again sshd_config will be updated which will cause sshd to be restarted (triggered).
[root@client15 augeas-play]# puppetd --fqdn=$HOSTNAME --test --no-splay --server=server0.example.com --onetime --verbose --factsync
info: Retrieving facts
info: Caching catalog at /var/lib/puppet/localconfig.yaml
notice: Starting catalog run
notice: //Node[default]/base/Augeas[ssh]/returns: executed successfully
info: //Node[default]/base/Augeas[ssh]: Scheduling refresh of Service[sshd]
notice: //Node[default]/base/Service[sshd]: Triggering 'refresh' from 1 dependencies
notice: Finished catalog run in 3.04 seconds
[root@client15 augeas-play]# exit
Connection to client15.example.com closed.
[root@server0 manifests]# ssh root@client15.example.com
Permission denied (publickey,gssapi-with-mic).
Using a combination of kickstart, puppet and puppet-augeas, you can configure just about every change you need to make on a machine. But for those occasions where you need to make the change immediately, there is a solution, func. We'll talk about that next.

func

func is the Fedora Unified Network Controller, it can be found on fedorahosted.org. Func is written in python and can used in python scripts. For some examples of what func can do, look at the home page in the section "examples speak louder than words".

We already downloaded and installed func on our clients using kickstart. We can now install fun on server0 and push the configuration with puppet.

[root@server0 ~]# yum install func
Setting up Install Process
Parsing package install arguments
Resolving Dependencies
--> Running transaction check
---> Package func.noarch 0:0.24-1.el5 set to be updated
--> Processing Dependency: certmaster >= 0.24 for package: func
--> Processing Dependency: python-simplejson for package: func
--> Processing Dependency: pyOpenSSL for package: func
--> Running transaction check
...
Installed: func.noarch 0:0.24-1.el5
Dependency Installed: certmaster.noarch 0:0.24-1.el5 pyOpenSSL.x86_64 0:0.6-1.p24.7.2.2 python-simplejson.x86_64 0:2.0.3-2.el5
Complete!
[root@server0 ~]# chkconfig certmaster on
[root@server0 ~]# service certmaster start
Starting certmaster daemon: 
Certmaster listens on port 51235, we'll need to add that port to our firewall.
[root@server0 ~]# iptables -I RH-Firewall-1-INPUT -p tcp -m state --state NEW,ESTABLISHED,RELATED --dport 51235 -j ACCEPT
[root@server0 ~]# cd /etc/sysconfig
[root@server0 sysconfig]# iptables-save >iptables
Now, let's add the configuration for func to puppet and restart the puppetmaster (since we will need to change site.pp).
[root@server0 etc]# cd /var/lib/puppet/files/base/etc
[root@server0 etc]# mkdir certmaster
[root@server0 etc]# cd certmaster/
[root@server0 certmaster]# vi minion.conf
[root@server0 certmaster]# cat minion.conf
# configuration for minions

[main]
certmaster = server0.example.com
certmaster_port = 51235
log_level = DEBUG
cert_dir = /etc/pki/certmaster
[root@server0 certmaster]# cd /etc/puppet/manifests/
[root@server0 manifests]# vi func.pp
[root@server0 manifests]# cat func.pp
class func {
	remotefile { "/etc/certmaster/minion.conf": mode => 644 }
	service { funcd: ensure => true, enable => true, hasrestart => true }
}
[root@server0 manifests]# vi site.pp
[root@server0 manifests]# cat site.pp
# site.pp
import "functions.pp"

# define nodes
import "nodes.pp"

# define classes
import "base.pp"
import "func.pp"
[root@server0 manifests]# vi nodes.pp 
[root@server0 manifests]# cat nodes.pp
node default {
	include base
	include func
}
[root@server0 manifests]# service puppetmaster restart
Stopping puppetmaster:                                     [  OK  ]
Starting puppetmaster:                                     [  OK  ]
Now on our client, if we restart puppet, we can see the change propagated (we could also wait and the change would be propagated at the next check-in.
[root@client15 ~]# cd /etc/certmaster
[root@client15 certmaster]# puppetd --fqdn=$HOSTNAME --test --no-splay --server=server0.example.com --onetime --verbose --factsync
info: Retrieving facts
info: Caching catalog at /var/lib/puppet/localconfig.yaml
notice: Starting catalog run
4c4
< certmaster = certmaster
---
> certmaster = server0.example.com
8d7
< 
notice: //Node[default]/func/Remotefile[/etc/certmaster/minion.conf]/File[/etc/certmaster/minion.conf]/source: replacing from source puppet://server0.example.com/base//etc/certmaster/minion.conf with contents {md5}7c1ae4564f05e3f9ca50633c89d2a63b
notice: //Node[default]/func/Service[funcd]/ensure: ensure changed 'stopped' to 'running'
notice: Finished catalog run in 2.67 seconds
[root@client15 certmaster]# cat minion.conf 
# configuration for minions

[main]
certmaster = server0.example.com
certmaster_port = 51235
log_level = DEBUG
cert_dir = /etc/pki/certmaster
Now, back on server0 we'll need to sign client15's certificate.
[root@server0 ~]# certmaster-ca --list
client15.example.com
[root@server0 ~]# certmaster-ca --sign client15.example.com
/var/lib/certmaster/certmaster/csrs/client15.example.com.csr signed - cert located at /var/lib/certmaster/certmaster/certs/client15.example.com.cert
[root@server0 ~]# certmaster-ca --list
No certificates to sign
At this point out client has connected to our server and requested a certificate, we then signed that certificate. We can now ask the client to do something.
[root@server0 certmaster]# func client15.example.com call hardware info
{'client15.example.com': ['REMOTE_ERROR',
                          'socket.error',
                          "(113, 'No route to host')",
                          '  File "/usr/lib/python2.4/site-packages/func/overlord/client.py", line 433, in process_server\n    retval = getattr(conn, meth)(*args[:])\n   File "/usr/lib64/python2.4/xmlrpclib.py", line 1096, in __call__\n    return self.__send(self.__name, args)\n   File "/usr/lib64/python2.4/xmlrpclib.py", line 1383, in __request\n    verbose=self.__verbose\n   File "/usr/lib64/python2.4/xmlrpclib.py", line 1129, in request\n    self.send_content(h, request_body)\n   File "/usr/lib64/python2.4/xmlrpclib.py", line 1243, in send_content\n    connection.endheaders()\n   File "/usr/lib64/python2.4/httplib.py", line 804, in endheaders\n    self._send_output()\n   File "/usr/lib64/python2.4/httplib.py", line 685, in _send_output\n    self.send(msg)\n   File "/usr/lib64/python2.4/httplib.py", line 652, in send\n    self.connect()\n   File "/usr/lib/python2.4/site-packages/certmaster/SSLCommon.py", line 106, in connect\n    self.sock.connect((self.host, self.port))\n   File "", line 1, in connect\n']}=
Almost, not quite, we need to open the port on the firewall on the client so we can talk to the funcd running on client15. We'll do this with puppet, there is an iptables module available. We'll just use service and remotefile in this example (We'll also change the default policy from REJECT to DROP)
[root@server0 ~]# cd /var/lib/puppet/files/base/etc/sysconfig
[root@server0 sysconfig]# vi iptables
[root@server0 sysconfig]# cat iptables
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:Example.com-1-INPUT - [0:0]
-A INPUT -j Example.com-1-INPUT
-A FORWARD -j Example.com-1-INPUT
-A Example.com-1-INPUT -i lo -j ACCEPT
-A Example.com-1-INPUT -p icmp --icmp-type any -j ACCEPT
-A Example.com-1-INPUT -p 50 -j ACCEPT
-A Example.com-1-INPUT -p 51 -j ACCEPT
-A Example.com-1-INPUT -p udp --dport 5353 -d 224.0.0.251 -j ACCEPT
-A Example.com-1-INPUT -p udp -m udp --dport 631 -j ACCEPT
-A Example.com-1-INPUT -p tcp -m tcp --dport 631 -j ACCEPT
-A Example.com-1-INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A Example.com-1-INPUT -m state --state NEW -m tcp -p tcp --dport 51234 -j ACCEPT
-A Example.com-1-INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT
-A Example.com-1-INPUT -j DROP
COMMIT
[root@server0 sysconfig]# cd /etc/puppet/manifests
[root@server0 manifests]# vi func.pp
[root@server0 manifests]# cat func.pp
class func {
	remotefile { "/etc/certmaster/minion.conf": mode => 644 }
	service { funcd: ensure => true, enable => true, hasrestart => true }
	remotefile { "/etc/sysconfig/iptables": mode => 644,
		notify => Service["iptables"] }
	service { iptables: ensure => true, enable => true, hasrestart => true }
}
Now, back on client15, we again could wait for the change to be propagated, but we'll just trigger it.
[root@client15 ~]# puppetd --fqdn=$HOSTNAME --test --no-splay --server=server0.example.com --onetime --verbose --factsync
info: Retrieving facts
info: Caching catalog at /var/lib/puppet/localconfig.yaml
notice: Starting catalog run
1,2d0
< # Firewall configuration written by system-config-securitylevel
< # Manual customization of this file is not recommended.
7,19c5,18
...
notice: //Node[default]/func/Service[iptables]/ensure: ensure changed 'stopped' to 'running'
notice: //Node[default]/func/Service[iptables]: Triggering 'refresh' from 2 dependencies
notice: Finished catalog run in 2.55 seconds
[root@client15 ~]# 
Back on server0, we'll try our func call again.
[root@server0 ~]# func client15.example.com call hardware info
{'client15.example.com': {'bogomips': '7187.63',
                          'cpuModel': 'Intel(R) Pentium(R) 4 CPU 3.60GHz',
                          'cpuSpeed': '3590',
                          'cpuVendor': 'GenuineIntel',
                          'defaultRunlevel': '3',
...
                          'systemSwap': '8191',
                          'systemVendor': 'Dell Inc.'}}
At this point you could configure certmaster to automatically sign keys as we did for puppet. Anything we need to do immediately, we can now do in func. If you want to use func for inventory purposes, you'll need to also download a git rpm and install that on server0. If you try to use func-inventory without git, it will work, but changes to the inventory won't be tracked.
[root@server0 ~]# func-inventory
git-core is not installed, so no change tracking is available.
use --no-git or, better, just install it.
[root@server0 ~]# func-inventory --no-git
The inventory files will be stored in /var/lib/func/inventory.