From e1de241f356a87ff8ca99aafba99f5bbcd5d5323 Mon Sep 17 00:00:00 2001 From: Todd Zullinger Date: Tue, 25 Nov 2008 11:02:33 -0500 Subject: Initial commit for puppet host package tool --- .gitignore | 10 +++ puppet-host-package | 80 +++++++++++++++++++++ puppethost.py | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++ template.spec | 53 ++++++++++++++ 4 files changed, 343 insertions(+) create mode 100644 .gitignore create mode 100755 puppet-host-package create mode 100644 puppethost.py create mode 100644 template.spec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1110214 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# git-ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +*.py[co] +*.sw[po] +*~ + +/TODO diff --git a/puppet-host-package b/puppet-host-package new file mode 100755 index 0000000..1914dc3 --- /dev/null +++ b/puppet-host-package @@ -0,0 +1,80 @@ +#!/usr/bin/env python +"""Create a host package for bootstrapping a puppet client.""" + +import os +import glob +import optparse +import puppethost + +usage = '%prog [options] hostname [hostname ...]' +parser = optparse.OptionParser(usage=usage) +parser.defaults = puppethost.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/or 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: + client = puppethost.PuppetHost(hostname, opts.__dict__) + + cert = client.files['cert'] + + if not os.path.exists(cert) or opts.force_cert: + try: client.gencert() + except puppethost.PuppetHostError, error: + print error + continue + + try: client.package() + except Exception, error: + print error + continue diff --git a/puppethost.py b/puppethost.py new file mode 100644 index 0000000..46be2c0 --- /dev/null +++ b/puppethost.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +"""Create a host package for bootstrapping a puppet client.""" + +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, +} + +status, domain = commands.getstatusoutput('dnsdomainname') +if status == 0: + defaults['domain'] = domain + +package_types = ['deb', 'rpm'] + +class PuppetHostError(StandardError): + pass + +class PuppetHost(object): + def __init__(self, hostname = '', opts = defaults): + """Puppet client 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, types = ['rpm']): + """Create packages in requested formats""" + for type in types: + if type not in package_types or not hasattr(self, type): + raise PuppetHostError('Bogus package type: %s' % type) + try: getattr(self, type)() + except Exception, error: + print error + continue + + def deb(self): + """Create a .deb package""" + raise NotImplementedError('FIXME if you need debian packages.') + + def tar(self, dir = '', keep = False): + self._check_files() + + if not dir: + dir = tempfile.mkdtemp('', 'puppet-client-') + + name = 'puppet-%s-%s' % (self.hostname, self.version) + tarball = '%s/%s.tar.gz' % (dir, 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-client-') + + 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.files + 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 diff --git a/template.spec b/template.spec new file mode 100644 index 0000000..85b10dd --- /dev/null +++ b/template.spec @@ -0,0 +1,53 @@ +%define hostname __HOSTNAME__ +%define version __VERSION__ +%define release __RELEASE__ +%define ssldir __SSLDIR__ + +Name: puppet-%{hostname} +Version: %{version} +Release: %{release} +Summary: Puppet SSL certificate files for %{hostname} + +Group: Applications/System +License: Public Domain +Source0: puppet-%{hostname}-%{version}.tar.gz +BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) + +BuildArch: noarch +Requires: puppet + +%description +Puppet SSL Keys for %{hostname}. + +%prep +%setup -q + + +%build +# nothing to build + + +%install +rm -rf %{buildroot} +mkdir -p %{buildroot}%{ssldir}/{private,public_keys} +cp -a * %{buildroot}%{ssldir} + +# set modes - puppet resets these on each run, so there's no point in trying to +# tighten them up. :/ +chmod 0771 %{buildroot}%{ssldir} +chmod 0750 %{buildroot}%{ssldir}/private* +chmod 0600 %{buildroot}%{ssldir}/private_keys/%{hostname}.pem + + +%clean +rm -rf %{buildroot} + + +%files +%defattr(-,root,root,-) +%{ssldir} + + +%changelog +* Thu Nov 20 2008 Todd Zullinger +- Initial template for puppet client package -- cgit