summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTodd Zullinger <tmz@pobox.com>2008-11-25 11:02:33 -0500
committerTodd Zullinger <tmz@pobox.com>2008-11-25 17:39:29 -0500
commite1de241f356a87ff8ca99aafba99f5bbcd5d5323 (patch)
tree56ea677b71adb735eb5cb54a53a66338a7a9464e
downloadpuppet-host-package-e1de241f356a87ff8ca99aafba99f5bbcd5d5323.zip
puppet-host-package-e1de241f356a87ff8ca99aafba99f5bbcd5d5323.tar.gz
puppet-host-package-e1de241f356a87ff8ca99aafba99f5bbcd5d5323.tar.xz
Initial commit for puppet host package tool
-rw-r--r--.gitignore10
-rwxr-xr-xpuppet-host-package80
-rw-r--r--puppethost.py200
-rw-r--r--template.spec53
4 files changed, 343 insertions, 0 deletions
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 <tmz@pobox.com>
+- Initial template for puppet client package