Manage sets of packages in puppet

Not long ago, I posted an article on managing sets of packages using encoded lists in puppet here. Recently I made an update to the logic to provide a more generic way of managing package sets. Using the following puppet module code, you can manage an entire system of packages using a single-file list.

Here is a simple example. You want to make a "profile" of sorts of a system to enforce on other machines. You can create a package list by running the following on a redhat system:

rpm -qa > /root/package_list.txt

This would result in a very long list of RPM packages, similar to the following:

tzdata-2012f-1.el6.noarch
cronie-anacron-1.4.4-7.el6.i686
fuse-sshfs-2.4-1.el6.i686
tmpwatch-2.9.16-4.el6.i686
libuuid-2.17.2-12.7.el6.i686
isomd5sum-1.0.6-1.el6.i686
...
...
...

Having run the above command, you should now have a file that describes every single package installed on your machine. In theory, you should be able to take this list and apply it to other machines to create an identical software stack.

Using this module, you could write a puppet manifest like the following that would make this possible:

apply_package_list('/root/package_list.txt')

And with that, you would have effectively created a package resource for each package in your list.

If you'd also like to remove packages that shouldn't be installed, this module can examine every package already installed on your system, and compare them against the list you fed in, creating an ensure => absent on any package not mentioned in the list. Just pass one extra option to enable this purging feature:

apply_package_list('/root/package_list.txt', 'purge')

This works especially well using puppet apply locally, like so:

puppet apply -e 'apply_package_list("/root/package_list.txt")'
puppet apply -e 'apply_package_list("/root/package_list.txt", "purge")'

I would advise you to run puppet with the "--noop" argument first before applying a package list all will-nilly.

Also noteworthy is that you can choose to specify a package version and architecture, or not to grab the latest each time. Check the documentation within the module for more information on this.

# Dynamically create package resources from list files
#
# @author    Ryan Uber <ryan@blankbmx.com>
# @license   Apache License, Version 2.0
# @category  functions
# @package   apply_package_list
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module Puppet::Parser::Functions
  newfunction(:apply_package_list, :type => :statement, :doc =>
    "Dynamcially creates a package resource for each element in a list, and
    ensures that each version is enforced. Any package not defined in your
    package list will automatically generate a package resource with an
    ensure => absent, giving you the power to define exactly what you want
    on your system in one list and enforce it using Puppet.

    Basic Syntax:
    apply_package_list('/path/to/list/file', '[purge=nopurge]')

    The purge option determines whether or not to purge packages that do not
    appear in your package set. It defaults to nopurge. Set it to 'purge' to
    enable pruning, or omit it to skip the purging stage.

    You need to pass in the path of a file that contains a newline-delimited
    list of package names to apply. For each package in this list, an ensure
    will be dynamically created to latest. This allows you to either place
    an exact package version in (along with the name), or simply the package
    name if you want the latest from your mirror. Example follows:

      kernel-2.6.32-220.4.1.el6.x86_64
      grub-0.97-75.el6.x86_64
      vim-enhanced

    This would ensure that kernel and grub matched the versions specified, and
    the latest vim-enhanced from your configured mirrors.

    An easy way to create a package list for an entire system is using RPM
    directly. Examples follow.

    With versions:
    rpm -qa > /path/to/list/file

    Without versions (to get latest):
    rpm -qa --qf='%{name}\\n' > /path/to/list/file

    Limitations
    1) Inability to pass plain package names (without version/arch/release etc) with the "purge" option
    2) Only RPM-based operating systems are supported at this time.") do |args|

    if args.length == 0
      raise Puppet::ParseError, "No package list specified during apply_package_list()"
    end

    file  = args[0]
    purge = args.length > 1 ? args[1] : 'nopurge'

    if not File.exists?(file)
      raise Puppet::ParseError, "File '#{file}' not found during apply_package_list()"
    end

    packages = File.read(file).split("\n")

    if not ['purge', 'nopurge'].include?(purge)
      raise Puppet::ParseError, "Invalid argument '#{purge}' during apply_package_list()"
    end

    os = lookupvar('osfamily')
    if not ['RedHat'].include?(os)
      raise Puppet::ParseError, "Unsupported operating system detected during apply_package_list()"
    end

    allowed = Array.new

    packages.each do |package|
      catalog.add_resource Puppet::Type.type(:package).hash2resource(
        {:name => package, :ensure => "latest"}
      )
      allowed << package
    end

    if purge == 'purge'
      installed = %x(rpm -qa).split("\n") if os == 'RedHat'
      if installed == nil
        raise Puppet::ParseError, "Could not query local package database during apply_package_list()"
      end
      installed.each do |package|
        # In RHEL, GPG keys show up in this output, so skip them (we really don't want
        # to uninstall imported GPG keys)
        next if os == 'RedHat' and package.start_with?('gpg-pubkey')
        if not allowed.include?(package)
          catalog.add_resource Puppet::Type.type(:package).hash2resource(
            {:name => package, :ensure => "absent"}
          )
        end
      end
    end

  end
end