#!/usr/bin/env python """Create a host package for bootstrapping a puppet host.""" import os import glob import time import shutil import OpenSSL import tarfile import commands import optparse import tempfile ssldir = '/etc/puppet/ssl' defaults = { 'destssldir': ssldir, 'domain': '', 'force': False, 'force_cert': False, 'force_tarball': False, 'force_package': False, 'release': '1', 'rpmdir': os.environ['HOME'], 'ssldir': ssldir, 'template': '%s/template.spec' % ssldir, } del ssldir status, domain = commands.getstatusoutput('dnsdomainname') if status == 0: defaults['domain'] = domain del status package_types = ['deb', 'rpm'] class PuppetHostError(StandardError): pass class PuppetHost(object): def __init__(self, hostname = '', opts = defaults): """Puppet host class""" if not hostname: raise PuppetHostError('A hostname must be provided') if '.' not in hostname and domain: hostname = '.'.join([hostname, domain]) hostname = hostname.lower() self.hostname = hostname self.opts = opts self.debfile = '' self.rpmfile = '' self.tarfile = '' self._version = '' def gencert(self): """Generate a puppet certificate""" cert = self.files['cert'] ssldir = self.opts['ssldir'] if os.path.exists(cert) and not self.opts['force_cert']: raise PuppetHostError('%s exists, not overwriting' % cert) cmd = 'puppetca --generate --ssldir %s %s' % (ssldir, self.hostname) status, output = commands.getstatusoutput(cmd) if status: raise PuppetHostError('Error generating cert for %s: %s' % (self.hostname, output)) def package(self, packages = ['rpm']): """Create packages in requested formats""" for package in packages: if package not in package_types or not hasattr(self, package): raise PuppetHostError('Bogus package type: %s' % package) try: getattr(self, package)() except Exception, error: print error continue def deb(self): """Create a .deb package""" raise NotImplementedError('FIXME if you need debian packages.') def tar(self, path = ''): self._check_files() if not path: path = tempfile.mkdtemp('', 'puppet-host-') name = 'puppet-%s-%s' % (self.hostname, self.version) tarball = '%s/%s.tar.gz' % (path, name) if os.path.exists(tarball) and not self.opts['force_tarball']: raise PuppetHostError('%s exists, not overwriting' % tarball) tar = tarfile.open(tarball, 'w:gz') tar.dereference = True for f in sorted(self.files.values()): arcname = '%s%s' % (name, f.replace(self.opts['ssldir'], '')) tar.add(f, arcname) tar.close() self.tarfile = tarball def rpm(self): """Create a .rpm package""" rpmdir = self.opts['rpmdir'] tmpdir = tempfile.mkdtemp('', 'puppet-host-') if not self.tarfile or not os.path.exists(self.tarfile): try: self.tar(tmpdir) except: self._cleanup(tmpdir) raise specfile = '%s/puppet-%s.spec' % (tmpdir, self.hostname) text = open(self.opts['template']).read() text = text.replace('__HOSTNAME__', self.hostname) text = text.replace('__VERSION__', self.version) text = text.replace('__RELEASE__', self.opts['release']) text = text.replace('__SSLDIR__', self.opts['destssldir']) spec = open(specfile, 'w') spec.write(text) spec.close() build_name_fmt = '%{N}-%{V}-%{R}.%{ARCH}.rpm' rpmfilecmd = 'rpm -q --qf "%s/%s" --specfile %s' % ( rpmdir, build_name_fmt, specfile) status, rpm = commands.getstatusoutput(rpmfilecmd) if status: self._cleanup(tmpdir) raise PuppetHostError('Failed to get rpm filename:\n%s' % rpm) if os.path.exists(rpm) and not self.opts['force_package']: self._cleanup(tmpdir) raise PuppetHostError('%s exists, not overwriting' % rpm) rpmbuild = 'rpmbuild -bb' rpmbuild += ' --define "_build_name_fmt %s"' % build_name_fmt for i in ['_builddir', '_sourcedir', '_specdir', '_srcrpmdir']: rpmbuild += ' --define "%s %s"' % (i, tmpdir) rpmbuild += ' --define "_rpmdir %s" %s' % (rpmdir, specfile) status, output = commands.getstatusoutput(rpmbuild) if status or not os.path.exists(rpm): self._cleanup(tmpdir) raise PuppetHostError('Error building rpm:\n', output) self._cleanup(tmpdir) self.rpmfile = rpm def _check_files(self): """Check for the files we care about""" self.missing = [f for f in self.files.values() if not os.path.exists(f)] if self.missing: error = 'The following required files are missing:\n' for f in self.missing: error += '\t%s' % f raise PuppetHostError(error) def _cleanup(self, dir): if os.path.isdir(dir): shutil.rmtree(dir) @property def files(self): hostname = self.hostname ssldir = self.opts['ssldir'] return { 'crl': '%s/ca/ca_crl.pem' % ssldir, 'cacert': '%s/certs/ca.pem' % ssldir, 'cert': '%s/certs/%s.pem' % (ssldir, hostname), 'private': '%s/private_keys/%s.pem' % (ssldir, hostname), } @property def version(self): """Get the date from a certificate""" if self._version: return self._version self._check_files() cert = self.files['cert'] try: x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(cert).read()) self._version = x509.get_notBefore()[:8] except: # pyOpenSSL < 0.7 lacks load_certificate()... do it the hard way format = '%Y%m%d' cmd = 'openssl x509 -in %s -text -noout' % cert status, output = commands.getstatusoutput(cmd) if status == 0: date = [l.split(':', 1)[1].lstrip() for l in output.split('\n') if 'Not Before:' in l][0] fmt = '%b %d %H:%M:%S %Y %Z' self._version = time.strftime(format, time.strptime(date, fmt)) else: self._version = time.strftime(format) return self._version def _main(): usage = '%prog [options] hostname [hostname ...]' parser = optparse.OptionParser(usage=usage) parser.defaults = defaults parser.add_option('-a', '--all', dest='allcerts', action='store_true', help='Create packages for all signed certificates') parser.add_option('-d', '--domain', dest='domain', help='Domain append to non fqdn hostnames [%default]') parser.add_option('-f', '--force', dest='force', action='store_true', help='Overwrite existing certs, tarballs, and packages') parser.add_option('--force-cert', dest='force_cert', action='store_true', help='Overwrite existing certs') parser.add_option('--force-tarball', dest='force_tarball', action='store_true', help='Overwrite existing tarballs') parser.add_option('--force-package', dest='force_package', action='store_true', help='Overwrite existing packages') parser.add_option('-r', '--rpmdir', dest='rpmdir', metavar='dir', help='Directory where packages are stored [%default]') parser.add_option('--release', dest='release', metavar='num', help='Package release number [%default]') parser.add_option('-s', '--ssldir', dest='ssldir', metavar='dir', help='Directory where ssl certs are stored [%default]') # FIXME improve the help string parser.add_option('-S', '--dest-ssldir', dest='destssldir', metavar='dir', help='Directory where ssl certs are packaged') parser.add_option('-t', '--template', dest='template', metavar='file', help='RPM spec file template [%default]') opts, args = parser.parse_args() if opts.force: opts.force_cert = True opts.force_tarball = True opts.force_package = True opts.rpmdir = os.path.abspath(opts.rpmdir) opts.ssldir = os.path.abspath(opts.ssldir) if not opts.destssldir: opts.destssldir = opts.ssldir if opts.allcerts: args = [] for cert in glob.glob('%s/ca/signed/*.pem' % opts.ssldir): basename = os.path.basename(cert) # we need the private key as well as the cert if os.path.exists('%s/private_keys/%s' % (opts.ssldir, basename)): args.append(os.path.splitext(basename)[0]) args.sort() if not args: raise SystemExit(parser.print_usage()) for d in [opts.rpmdir, opts.ssldir]: if not os.path.isdir(d): raise SystemExit('%s does not exist (or is not a directory)' % d) if not os.path.isfile(opts.template): raise SystemExit('Template file (%s) does not exist' % opts.template) for hostname in args: host = PuppetHost(hostname, opts.__dict__) if not os.path.exists(host.files['cert']) or opts.force_cert: try: host.gencert() except PuppetHostError, error: print error continue try: host.package() except Exception, error: print error continue if __name__ == '__main__': try: _main() except KeyboardInterrupt: raise SystemExit(1)