summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJason Kincl <kincljc@ornl.gov>2012-12-04 08:14:33 -0500
committerJason Kincl <kincljc@ornl.gov>2012-12-04 08:14:33 -0500
commit09a45d745269a419b0c5da0664912e061dc8e5d3 (patch)
treec5c0af33093087f10f0caf5ce021aa4cb0b4a879 /src
parent648c8f6e313e684d5fadc1fdbc97e08d83eb2b16 (diff)
parentf35c38e87eafffb497338b9273fe84f284a41dcf (diff)
downloadbcfg2-09a45d745269a419b0c5da0664912e061dc8e5d3.tar.gz
bcfg2-09a45d745269a419b0c5da0664912e061dc8e5d3.tar.bz2
bcfg2-09a45d745269a419b0c5da0664912e061dc8e5d3.zip
Merge remote branch 'upstream/master' into jasons-hacking
Diffstat (limited to 'src')
-rw-r--r--src/lib/Bcfg2/Client/Client.py4
-rw-r--r--src/lib/Bcfg2/Client/Frame.py11
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/File.py4
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/base.py12
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIXUsers.py300
-rw-r--r--src/lib/Bcfg2/Client/Tools/SELinux.py121
-rw-r--r--src/lib/Bcfg2/Client/Tools/__init__.py1
-rw-r--r--src/lib/Bcfg2/Compat.py15
-rw-r--r--src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html2
-rw-r--r--src/lib/Bcfg2/Reporting/views.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py65
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py5
-rw-r--r--src/lib/Bcfg2/Server/Plugins/FileProbes.py6
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SEModules.py6
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SSLCA.py491
-rw-r--r--src/lib/Bcfg2/version.py4
16 files changed, 731 insertions, 318 deletions
diff --git a/src/lib/Bcfg2/Client/Client.py b/src/lib/Bcfg2/Client/Client.py
index f197a9074..45e0b64e6 100644
--- a/src/lib/Bcfg2/Client/Client.py
+++ b/src/lib/Bcfg2/Client/Client.py
@@ -56,8 +56,8 @@ class Client(object):
self.logger.error("Service removal is nonsensical; "
"removed services will only be disabled")
if (self.setup['remove'] and
- self.setup['remove'].lower() not in ['all', 'services',
- 'packages']):
+ self.setup['remove'].lower() not in ['all', 'services', 'packages',
+ 'users']):
self.logger.error("Got unknown argument %s for -r" %
self.setup['remove'])
if self.setup["file"] and self.setup["cache"]:
diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py
index 53180ab68..4f3ff1820 100644
--- a/src/lib/Bcfg2/Client/Frame.py
+++ b/src/lib/Bcfg2/Client/Frame.py
@@ -105,6 +105,10 @@ class Frame(object):
if deprecated:
self.logger.warning("Loaded deprecated tool drivers:")
self.logger.warning(deprecated)
+ experimental = [tool.name for tool in self.tools if tool.experimental]
+ if experimental:
+ self.logger.warning("Loaded experimental tool drivers:")
+ self.logger.warning(experimental)
# find entries not handled by any tools
self.unhandled = [entry for struct in config
@@ -281,12 +285,15 @@ class Frame(object):
if self.setup['remove']:
if self.setup['remove'] == 'all':
self.removal = self.extra
- elif self.setup['remove'] in ['services', 'Services']:
+ elif self.setup['remove'].lower() == 'services':
self.removal = [entry for entry in self.extra
if entry.tag == 'Service']
- elif self.setup['remove'] in ['packages', 'Packages']:
+ elif self.setup['remove'].lower() == 'packages':
self.removal = [entry for entry in self.extra
if entry.tag == 'Package']
+ elif self.setup['remove'].lower() == 'users':
+ self.removal = [entry for entry in self.extra
+ if entry.tag in ['POSIXUser', 'POSIXGroup']]
candidates = [entry for entry in self.states
if not self.states[entry]]
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/File.py b/src/lib/Bcfg2/Client/Tools/POSIX/File.py
index 5842c4e1f..9b95d2234 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/File.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/File.py
@@ -188,6 +188,10 @@ class POSIXFile(POSIXTool):
prompt.append(udiff)
except UnicodeEncodeError:
prompt.append("Could not encode diff")
+ elif entry.get("empty", "true"):
+ # the file doesn't exist on disk, but there's no
+ # expected content
+ prompt.append("%s does not exist" % entry.get("name"))
else:
prompt.append("Diff took too long to compute, no "
"printable diff")
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/base.py b/src/lib/Bcfg2/Client/Tools/POSIX/base.py
index 6388f6731..b867fa3d8 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/base.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/base.py
@@ -9,6 +9,7 @@ import copy
import shutil
import Bcfg2.Client.Tools
import Bcfg2.Client.XML
+from Bcfg2.Compat import oct_mode
try:
import selinux
@@ -128,7 +129,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
wanted_mode |= device_map[entry.get('dev_type')]
try:
self.logger.debug("POSIX: Setting mode on %s to %s" %
- (path, oct(wanted_mode)))
+ (path, oct_mode(wanted_mode)))
os.chmod(path, wanted_mode)
except (OSError, KeyError):
self.logger.error('POSIX: Failed to change mode on %s' %
@@ -436,7 +437,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
group = None
try:
- mode = oct(ondisk[stat.ST_MODE])[-4:]
+ mode = oct_mode(ondisk[stat.ST_MODE])[-4:]
except (OSError, KeyError, TypeError):
err = sys.exc_info()[1]
self.logger.debug("POSIX: Could not get current permissions of "
@@ -507,7 +508,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
(path, attrib['current_group'], entry.get('group')))
if (wanted_mode and
- oct(int(attrib['current_mode'], 8)) != oct(wanted_mode)):
+ oct_mode(int(attrib['current_mode'], 8)) != oct_mode(wanted_mode)):
errors.append("Permissions for path %s are incorrect. "
"Current permissions are %s but should be %s" %
(path, attrib['current_mode'], entry.get('mode')))
@@ -708,10 +709,11 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
for i in range(0, 3):
if newmode & (6 * pow(8, i)):
newmode |= 1 * pow(8, i)
- tmpentry.set('mode', oct(newmode))
+ tmpentry.set('mode', oct_mode(newmode))
for acl in tmpentry.findall('ACL'):
acl.set('perms',
- oct(self._norm_acl_perms(acl.get('perms')) | ACL_MAP['x']))
+ oct_mode(self._norm_acl_perms(acl.get('perms')) | \
+ ACL_MAP['x']))
for cpath in created:
rv &= self._set_perms(tmpentry, path=cpath)
return rv
diff --git a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
new file mode 100644
index 000000000..78734f5c2
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
@@ -0,0 +1,300 @@
+""" A tool to handle creating users and groups with useradd/mod/del
+and groupadd/mod/del """
+
+import sys
+import pwd
+import grp
+import Bcfg2.Client.XML
+import subprocess
+import Bcfg2.Client.Tools
+
+
+class ExecutionError(Exception):
+ """ Raised when running an external command fails """
+
+ def __init__(self, msg, retval=None):
+ Exception.__init__(self, msg)
+ self.retval = retval
+
+ def __str__(self):
+ return "%s (rv: %s)" % (Exception.__str__(self),
+ self.retval)
+
+
+class Executor(object):
+ """ A better version of Bcfg2.Client.Tool.Executor, which captures
+ stderr, raises exceptions on error, and doesn't use the shell to
+ execute by default """
+
+ def __init__(self, logger):
+ self.logger = logger
+ self.stdout = None
+ self.stderr = None
+ self.retval = None
+
+ def run(self, command, inputdata=None, shell=False):
+ """ Run a command, given as a list, optionally giving it the
+ specified input data """
+ self.logger.debug("Running: %s" % " ".join(command))
+ proc = subprocess.Popen(command, shell=shell, bufsize=16384,
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, close_fds=True)
+ if inputdata:
+ for line in inputdata.splitlines():
+ self.logger.debug('> %s' % line)
+ (self.stdout, self.stderr) = proc.communicate(inputdata)
+ else:
+ (self.stdout, self.stderr) = proc.communicate()
+ for line in self.stdout.splitlines(): # pylint: disable=E1103
+ self.logger.debug('< %s' % line)
+ self.retval = proc.wait()
+ if self.retval == 0:
+ for line in self.stderr.splitlines(): # pylint: disable=E1103
+ self.logger.warning(line)
+ return True
+ else:
+ raise ExecutionError(self.stderr, self.retval)
+
+
+class POSIXUsers(Bcfg2.Client.Tools.Tool):
+ """ A tool to handle creating users and groups with
+ useradd/mod/del and groupadd/mod/del """
+ __execs__ = ['/usr/sbin/useradd', '/usr/sbin/usermod', '/usr/sbin/userdel',
+ '/usr/sbin/groupadd', '/usr/sbin/groupmod',
+ '/usr/sbin/groupdel']
+ __handles__ = [('POSIXUser', None),
+ ('POSIXGroup', None)]
+ __req__ = dict(POSIXUser=['name'],
+ POSIXGroup=['name'])
+ experimental = True
+
+ # A mapping of XML entry attributes to the indexes of
+ # corresponding values in the get*ent data structures
+ attr_mapping = dict(POSIXUser=dict(name=0, uid=2, gecos=4, home=5,
+ shell=6),
+ POSIXGroup=dict(name=0, gid=2))
+
+ def __init__(self, logger, setup, config):
+ Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config)
+ self.set_defaults = dict(POSIXUser=self.populate_user_entry,
+ POSIXGroup=lambda g: g)
+ self.cmd = Executor(logger)
+ self._existing = None
+
+ @property
+ def existing(self):
+ """ Get a dict of existing users and groups """
+ if self._existing is None:
+ self._existing = dict(POSIXUser=dict([(u[0], u)
+ for u in pwd.getpwall()]),
+ POSIXGroup=dict([(g[0], g)
+ for g in grp.getgrall()]))
+ return self._existing
+
+ def Inventory(self, states, structures=None):
+ if not structures:
+ structures = self.config.getchildren()
+ # we calculate a list of all POSIXUser and POSIXGroup entries,
+ # and then add POSIXGroup entries that are required to create
+ # the primary group for each user to the structures. this is
+ # sneaky and possibly evil, but it works great.
+ groups = []
+ for struct in structures:
+ groups.extend([e.get("name")
+ for e in struct.findall("POSIXGroup")])
+ for struct in structures:
+ for entry in struct.findall("POSIXUser"):
+ group = self.set_defaults[entry.tag](entry).get('group')
+ if group and group not in groups:
+ self.logger.debug("POSIXUsers: Adding POSIXGroup entry "
+ "'%s' for user '%s'" %
+ (group, entry.get("name")))
+ struct.append(Bcfg2.Client.XML.Element("POSIXGroup",
+ name=group))
+ return Bcfg2.Client.Tools.Tool.Inventory(self, states, structures)
+
+ def FindExtra(self):
+ extra = []
+ for handles in self.__handles__:
+ tag = handles[0]
+ specified = []
+ for entry in self.getSupportedEntries():
+ if entry.tag == tag:
+ specified.append(entry.get("name"))
+ extra.extend([Bcfg2.Client.XML.Element(tag, name=e)
+ for e in self.existing[tag].keys()
+ if e not in specified])
+ return extra
+
+ def populate_user_entry(self, entry):
+ """ Given a POSIXUser entry, set all of the 'missing' attributes
+ with their defaults """
+ defaults = dict(group=entry.get('name'),
+ gecos=entry.get('name'),
+ shell='/bin/bash')
+ if entry.get('name') == 'root':
+ defaults['home'] = '/root'
+ else:
+ defaults['home'] = '/home/%s' % entry.get('name')
+ for key, val in defaults.items():
+ if entry.get(key) is None:
+ entry.set(key, val)
+ if entry.get('group') in self.existing['POSIXGroup']:
+ entry.set('gid',
+ str(self.existing['POSIXGroup'][entry.get('group')][2]))
+ return entry
+
+ def user_supplementary_groups(self, entry):
+ """ Get a list of supplmentary groups that the user in the
+ given entry is a member of """
+ return [g for g in self.existing['POSIXGroup'].values()
+ if entry.get("name") in g[3] and g[0] != entry.get("group")]
+
+ def VerifyPOSIXUser(self, entry, _):
+ """ Verify a POSIXUser entry """
+ rv = self._verify(self.populate_user_entry(entry))
+ if entry.get("current_exists", "true") == "true":
+ # verify supplemental groups
+ actual = [g[0] for g in self.user_supplementary_groups(entry)]
+ expected = [e.text for e in entry.findall("MemberOf")]
+ if set(expected) != set(actual):
+ entry.set('qtext',
+ "\n".join([entry.get('qtext', '')] +
+ ["%s %s has incorrect supplemental group "
+ "membership. Currently: %s. Should be: %s"
+ % (entry.tag, entry.get("name"),
+ actual, expected)]))
+ rv = False
+ if self.setup['interactive'] and not rv:
+ entry.set('qtext',
+ '%s\nInstall %s %s: (y/N) ' %
+ (entry.get('qtext', ''), entry.tag, entry.get('name')))
+ return rv
+
+ def VerifyPOSIXGroup(self, entry, _):
+ """ Verify a POSIXGroup entry """
+ rv = self._verify(entry)
+ if self.setup['interactive'] and not rv:
+ entry.set('qtext',
+ '%s\nInstall %s %s: (y/N) ' %
+ (entry.get('qtext', ''), entry.tag, entry.get('name')))
+ return rv
+
+ def _verify(self, entry):
+ """ Perform most of the actual work of verification """
+ errors = []
+ if entry.get("name") not in self.existing[entry.tag]:
+ entry.set('current_exists', 'false')
+ errors.append("%s %s does not exist" % (entry.tag,
+ entry.get("name")))
+ else:
+ for attr, idx in self.attr_mapping[entry.tag].items():
+ val = str(self.existing[entry.tag][entry.get("name")][idx])
+ entry.set("current_%s" % attr, val)
+ if attr in ["uid", "gid"]:
+ if entry.get(attr) is None:
+ # no uid/gid specified, so we let the tool
+ # automatically determine one -- i.e., it always
+ # verifies
+ continue
+ if val != entry.get(attr):
+ errors.append("%s for %s %s is incorrect. Current %s is "
+ "%s, but should be %s" %
+ (attr.title(), entry.tag, entry.get("name"),
+ attr, entry.get(attr), val))
+
+ if errors:
+ for error in errors:
+ self.logger.debug("%s: %s" % (self.name, error))
+ entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors))
+ return len(errors) == 0
+
+ def Install(self, entries, states):
+ for entry in entries:
+ # install groups first, so that all groups exist for
+ # users that might need them
+ if entry.tag == 'POSIXGroup':
+ states[entry] = self._install(entry)
+ for entry in entries:
+ if entry.tag == 'POSIXUser':
+ states[entry] = self._install(entry)
+ self._existing = None
+
+ def _install(self, entry):
+ """ add or modify a user or group using the appropriate command """
+ if entry.get("name") not in self.existing[entry.tag]:
+ action = "add"
+ else:
+ action = "mod"
+ try:
+ self.cmd.run(self._get_cmd(action,
+ self.set_defaults[entry.tag](entry)))
+ self.modified.append(entry)
+ return True
+ except ExecutionError:
+ self.logger.error("POSIXUsers: Error creating %s %s: %s" %
+ (entry.tag, entry.get("name"),
+ sys.exc_info()[1]))
+ return False
+
+ def _get_cmd(self, action, entry):
+ """ Get a command to perform the appropriate action (add, mod,
+ del) on the given entry. The command is always the same; we
+ set all attributes on a given user or group when modifying it
+ rather than checking which ones need to be changed. This
+ makes things fail as a unit (e.g., if a user is logged in, you
+ can't change its home dir, but you could change its GECOS, but
+ the whole operation fails), but it also makes this function a
+ lot, lot easier and simpler."""
+ cmd = ["/usr/sbin/%s%s" % (entry.tag[5:].lower(), action)]
+ if action != 'del':
+ if entry.tag == 'POSIXGroup':
+ if entry.get('gid'):
+ cmd.extend(['-g', entry.get('gid')])
+ elif entry.tag == 'POSIXUser':
+ cmd.append('-m')
+ if entry.get('uid'):
+ cmd.extend(['-u', entry.get('uid')])
+ cmd.extend(['-g', entry.get('group')])
+ extras = [e.text for e in entry.findall("MemberOf")]
+ if extras:
+ cmd.extend(['-G', ",".join(extras)])
+ cmd.extend(['-d', entry.get('home')])
+ cmd.extend(['-s', entry.get('shell')])
+ cmd.extend(['-c', entry.get('gecos')])
+ cmd.append(entry.get('name'))
+ return cmd
+
+ def Remove(self, entries):
+ for entry in entries:
+ # remove users first, so that all users have been removed
+ # from groups before we remove them
+ if entry.tag == 'POSIXUser':
+ self._remove(entry)
+ for entry in entries:
+ if entry.tag == 'POSIXGroup':
+ try:
+ grp.getgrnam(entry.get("name"))
+ self._remove(entry)
+ except KeyError:
+ # at least some versions of userdel automatically
+ # remove the primary group for a user if the group
+ # name is the same as the username, and no other
+ # users are in the group
+ self.logger.info("POSIXUsers: Group %s does not exist. "
+ "It may have already been removed when "
+ "its users were deleted" %
+ entry.get("name"))
+ self._existing = None
+ self.extra = self.FindExtra()
+
+ def _remove(self, entry):
+ """ Remove an entry """
+ try:
+ self.cmd.run(self._get_cmd("del", entry))
+ return True
+ except ExecutionError:
+ self.logger.error("POSIXUsers: Error deleting %s %s: %s" %
+ (entry.tag, entry.get("name"),
+ sys.exc_info()[1]))
+ return False
diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py
index fc47883c9..6bd728114 100644
--- a/src/lib/Bcfg2/Client/Tools/SELinux.py
+++ b/src/lib/Bcfg2/Client/Tools/SELinux.py
@@ -58,36 +58,48 @@ def netmask_itoa(netmask, proto="ipv4"):
class SELinux(Bcfg2.Client.Tools.Tool):
""" SELinux entry support """
name = 'SELinux'
- __handles__ = [('SELinux', 'boolean'),
- ('SELinux', 'port'),
- ('SELinux', 'fcontext'),
- ('SELinux', 'node'),
- ('SELinux', 'login'),
- ('SELinux', 'user'),
- ('SELinux', 'interface'),
- ('SELinux', 'permissive'),
- ('SELinux', 'module')]
- __req__ = dict(SELinux=dict(boolean=['name', 'value'],
- module=['name'],
- port=['name', 'selinuxtype'],
- fcontext=['name', 'selinuxtype'],
- node=['name', 'selinuxtype', 'proto'],
- login=['name', 'selinuxuser'],
- user=['name', 'roles', 'prefix'],
- interface=['name', 'selinuxtype'],
- permissive=['name']))
+ __handles__ = [('SEBoolean', None),
+ ('SEFcontext', None),
+ ('SEInterface', None),
+ ('SELogin', None),
+ ('SEModule', None),
+ ('SENode', None),
+ ('SEPermissive', None),
+ ('SEPort', None),
+ ('SEUser', None)]
+ __req__ = dict(SEBoolean=['name', 'value'],
+ SEFcontext=['name', 'selinuxtype'],
+ SEInterface=['name', 'selinuxtype'],
+ SELogin=['name', 'selinuxuser'],
+ SEModule=['name'],
+ SENode=['name', 'selinuxtype', 'proto'],
+ SEPermissive=['name'],
+ SEPort=['name', 'selinuxtype'],
+ SEUser=['name', 'roles', 'prefix'])
def __init__(self, logger, setup, config):
Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config)
self.handlers = {}
- for handles in self.__handles__:
- etype = handles[1]
+ for handler in self.__handles__:
+ etype = handler[0]
self.handlers[etype] = \
globals()["SELinux%sHandler" % etype.title()](self, logger,
setup, config)
self.txn = False
self.post_txn_queue = []
+ def __getattr__(self, attr):
+ if attr.startswith("VerifySE"):
+ return self.GenericSEVerify
+ elif attr.startswith("InstallSE"):
+ return self.GenericSEInstall
+ # there's no need for an else here, because python checks for
+ # an attribute in the "normal" ways first. i.e., if self.txn
+ # is used, __getattr__() is never called because txn exists as
+ # a "normal" attribute of this object. See
+ # http://docs.python.org/2/reference/datamodel.html#object.__getattr__
+ # for details
+
def BundleUpdated(self, _, states):
for handler in self.handlers.values():
handler.BundleUpdated(states)
@@ -100,12 +112,12 @@ class SELinux(Bcfg2.Client.Tools.Tool):
def canInstall(self, entry):
return (Bcfg2.Client.Tools.Tool.canInstall(self, entry) and
- self.handlers[entry.get('type')].canInstall(entry))
+ self.handlers[entry.tag].canInstall(entry))
def primarykey(self, entry):
""" return a string that should be unique amongst all entries
in the specification """
- return self.handlers[entry.get('type')].primarykey(entry)
+ return self.handlers[entry.tag].primarykey(entry)
def Install(self, entries, states):
# start a transaction
@@ -125,32 +137,32 @@ class SELinux(Bcfg2.Client.Tools.Tool):
for func, arg, kwargs in self.post_txn_queue:
states[arg] = func(*arg, **kwargs)
- def InstallSELinux(self, entry):
- """Dispatch install to the proper method according to type"""
- return self.handlers[entry.get('type')].Install(entry)
+ def GenericSEInstall(self, entry):
+ """Dispatch install to the proper method according to entry tag"""
+ return self.handlers[entry.tag].Install(entry)
- def VerifySELinux(self, entry, _):
- """Dispatch verify to the proper method according to type"""
- rv = self.handlers[entry.get('type')].Verify(entry)
+ def GenericSEVerify(self, entry, _):
+ """Dispatch verify to the proper method according to entry tag"""
+ rv = self.handlers[entry.tag].Verify(entry)
if entry.get('qtext') and self.setup['interactive']:
entry.set('qtext',
- '%s\nInstall SELinux %s %s: (y/N) ' %
+ '%s\nInstall %s: (y/N) ' %
(entry.get('qtext'),
- entry.get('type'),
- self.handlers[entry.get('type')].tostring(entry)))
+ self.handlers[entry.tag].tostring(entry)))
return rv
def Remove(self, entries):
- """Dispatch verify to the proper removal method according to type"""
+ """Dispatch verify to the proper removal
+ method according to entry tag"""
# sort by type
types = list()
for entry in entries:
- if entry.get('type') not in types:
- types.append(entry.get('type'))
+ if entry.tag not in types:
+ types.append(entry.tag)
for etype in types:
self.handlers[etype].Remove([e for e in entries
- if e.get('type') == etype])
+ if e.tag == etype])
class SELinuxEntryHandler(object):
@@ -253,8 +265,7 @@ class SELinuxEntryHandler(object):
def key2entry(self, key):
""" Generate an XML entry from an SELinux record key """
attrs = self._key2attrs(key)
- attrs["type"] = self.etype
- return Bcfg2.Client.XML.Element("SELinux", **attrs)
+ return Bcfg2.Client.XML.Element(self.etype, **attrs)
def _args(self, entry, method):
""" Get the argument list for invoking _modify or _add, or
@@ -279,7 +290,7 @@ class SELinuxEntryHandler(object):
""" return a string that should be unique amongst all entries
in the specification. some entry types are not universally
disambiguated by tag:type:name alone """
- return ":".join([entry.tag, entry.get("type"), entry.get("name")])
+ return ":".join([entry.tag, entry.get("name")])
def exists(self, entry):
""" return True if the entry already exists in the record list """
@@ -303,8 +314,8 @@ class SELinuxEntryHandler(object):
continue
if current_attrs[attr] != desired_attrs[attr]:
entry.set('current_%s' % attr, current_attrs[attr])
- errors.append("SELinux %s %s has wrong %s: %s, should be %s" %
- (self.etype, self.tostring(entry), attr,
+ errors.append("%s %s has wrong %s: %s, should be %s" %
+ (entry.tag, entry.get('name'), attr,
current_attrs[attr], desired_attrs[attr]))
if errors:
@@ -331,8 +342,8 @@ class SELinuxEntryHandler(object):
return True
except ValueError:
err = sys.exc_info()[1]
- self.logger.debug("Failed to %s SELinux %s %s: %s" %
- (method, self.etype, self.tostring(entry), err))
+ self.logger.info("Failed to %s SELinux %s %s: %s" %
+ (method, self.etype, self.tostring(entry), err))
return False
def Remove(self, entries):
@@ -365,7 +376,7 @@ class SELinuxEntryHandler(object):
pass
-class SELinuxBooleanHandler(SELinuxEntryHandler):
+class SELinuxSebooleanHandler(SELinuxEntryHandler):
""" handle SELinux boolean entries """
etype = "boolean"
value_format = ("value",)
@@ -414,7 +425,7 @@ class SELinuxBooleanHandler(SELinuxEntryHandler):
SELinuxEntryHandler.canInstall(self, entry))
-class SELinuxPortHandler(SELinuxEntryHandler):
+class SELinuxSeportHandler(SELinuxEntryHandler):
""" handle SELinux port entries """
etype = "port"
value_format = ('selinuxtype', None)
@@ -486,7 +497,7 @@ class SELinuxPortHandler(SELinuxEntryHandler):
return tuple(entry.get("name").split("/"))
-class SELinuxFcontextHandler(SELinuxEntryHandler):
+class SELinuxSefcontextHandler(SELinuxEntryHandler):
""" handle SELinux file context entries """
etype = "fcontext"
@@ -556,11 +567,11 @@ class SELinuxFcontextHandler(SELinuxEntryHandler):
'', '')
def primarykey(self, entry):
- return ":".join([entry.tag, entry.get("type"), entry.get("name"),
+ return ":".join([entry.tag, entry.get("name"),
entry.get("filetype", "all")])
-class SELinuxNodeHandler(SELinuxEntryHandler):
+class SELinuxSenodeHandler(SELinuxEntryHandler):
""" handle SELinux node entries """
etype = "node"
@@ -592,7 +603,7 @@ class SELinuxNodeHandler(SELinuxEntryHandler):
entry.get("selinuxtype"))
-class SELinuxLoginHandler(SELinuxEntryHandler):
+class SELinuxSeloginHandler(SELinuxEntryHandler):
""" handle SELinux login entries """
etype = "login"
@@ -603,7 +614,7 @@ class SELinuxLoginHandler(SELinuxEntryHandler):
return (entry.get("name"), entry.get("selinuxuser"), "")
-class SELinuxUserHandler(SELinuxEntryHandler):
+class SELinuxSeuserHandler(SELinuxEntryHandler):
""" handle SELinux user entries """
etype = "user"
@@ -652,7 +663,7 @@ class SELinuxUserHandler(SELinuxEntryHandler):
return tuple(rv)
-class SELinuxInterfaceHandler(SELinuxEntryHandler):
+class SELinuxSeinterfaceHandler(SELinuxEntryHandler):
""" handle SELinux interface entries """
etype = "interface"
@@ -663,7 +674,7 @@ class SELinuxInterfaceHandler(SELinuxEntryHandler):
return (entry.get("name"), '', entry.get("selinuxtype"))
-class SELinuxPermissiveHandler(SELinuxEntryHandler):
+class SELinuxSepermissiveHandler(SELinuxEntryHandler):
""" handle SELinux permissive domain entries """
etype = "permissive"
@@ -695,7 +706,7 @@ class SELinuxPermissiveHandler(SELinuxEntryHandler):
return (entry.get("name"),)
-class SELinuxModuleHandler(SELinuxEntryHandler):
+class SELinuxSemoduleHandler(SELinuxEntryHandler):
""" handle SELinux module entries """
etype = "module"
@@ -808,10 +819,9 @@ class SELinuxModuleHandler(SELinuxEntryHandler):
def Install(self, entry, _=None):
if not self.filetool.install(self._pathentry(entry)):
return False
- if hasattr(self, 'records'):
+ if hasattr(seobject, 'moduleRecords'):
# if seobject has the moduleRecords attribute, install the
# module using the seobject library
- self.records # pylint: disable=W0104
return self._install_seobject(entry)
else:
# seobject doesn't have the moduleRecords attribute, so
@@ -891,8 +901,7 @@ class SELinuxModuleHandler(SELinuxEntryHandler):
def FindExtra(self):
specified = [self._key(e)
- for e in self.tool.getSupportedEntries()
- if e.get("type") == self.etype]
+ for e in self.tool.getSupportedEntries()]
rv = []
for module in self._all_records_from_filesystem().keys():
if module not in specified:
diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py
index 927b25ba8..d5f55759f 100644
--- a/src/lib/Bcfg2/Client/Tools/__init__.py
+++ b/src/lib/Bcfg2/Client/Tools/__init__.py
@@ -61,6 +61,7 @@ class Tool(object):
__req__ = {}
__important__ = []
deprecated = False
+ experimental = False
def __init__(self, logger, setup, config):
self.setup = setup
diff --git a/src/lib/Bcfg2/Compat.py b/src/lib/Bcfg2/Compat.py
index 23f7ef784..b0f0ef5cf 100644
--- a/src/lib/Bcfg2/Compat.py
+++ b/src/lib/Bcfg2/Compat.py
@@ -245,3 +245,18 @@ except ImportError:
def wraps(wrapped): # pylint: disable=W0613
""" implementation of functools.wraps() for python 2.4 """
return lambda f: f
+
+
+def oct_mode(mode):
+ """ Convert a decimal number describing a POSIX permissions mode
+ to a string giving the octal mode. In Python 2, this is a synonym
+ for :func:`oct`, but in Python 3 the octal format has changed to
+ ``0o000``, which cannot be used as an octal permissions mode, so
+ we need to strip the 'o' from the output. I.e., this function
+ acts like the Python 2 :func:`oct` regardless of what version of
+ Python is in use.
+
+ :param mode: The decimal mode to convert to octal
+ :type mode: int
+ :returns: string """
+ return oct(mode).replace('o', '')
diff --git a/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html b/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
index 06c99d899..fd9a545ce 100644
--- a/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
+++ b/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
@@ -5,8 +5,8 @@
{% block pagebanner %}Clients - Detailed View{% endblock %}
{% block content %}
-<div class='client_list_box'>
{% filter_navigator %}
+<div class='client_list_box'>
{% if entry_list %}
<table cellpadding="3">
<tr id='table_list_header' class='listview'>
diff --git a/src/lib/Bcfg2/Reporting/views.py b/src/lib/Bcfg2/Reporting/views.py
index 7dc216bd4..8ab3f8e59 100644
--- a/src/lib/Bcfg2/Reporting/views.py
+++ b/src/lib/Bcfg2/Reporting/views.py
@@ -276,7 +276,7 @@ def client_index(request, timestamp=None, **kwargs):
"""
list = _handle_filters(Interaction.objects.recent(timestamp), **kwargs).\
- select_related().order_by("client__name").all()
+ select_related('client').order_by("client__name").all()
return render_to_response('clients/index.html',
{'inter_list': list,
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
index 3a78b4847..73550cd9d 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
@@ -11,8 +11,39 @@ from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP
try:
import genshi.core
from genshi.template import TemplateLoader, NewTextTemplate
- from genshi.template.eval import UndefinedError
+ from genshi.template.eval import UndefinedError, Suite
+ #: True if Genshi libraries are available
HAS_GENSHI = True
+
+ def _genshi_removes_blank_lines():
+ """ Genshi 0.5 uses the Python :mod:`compiler` package to
+ compile genshi snippets to AST. Genshi 0.6 uses some bespoke
+ magic, because compiler has been deprecated.
+ :func:`compiler.parse` produces an AST that removes all excess
+ whitespace (e.g., blank lines), while
+ :func:`genshi.template.astutil.parse` does not. In order to
+ determine which actual line of code an error occurs on, we
+ need to know which is in use and how it treats blank lines.
+ I've beat my head against this for hours and the best/only way
+ I can find is to compile some genshi code with an error and
+ see which line it's on."""
+ code = """d = dict()
+
+d['a']"""
+ try:
+ Suite(code).execute(dict())
+ except KeyError:
+ line = traceback.extract_tb(sys.exc_info()[2])[-1][1]
+ if line == 2:
+ return True
+ else:
+ return False
+
+ #: True if Genshi removes all blank lines from a code block before
+ #: executing it; False indicates that Genshi only removes leading
+ #: and trailing blank lines. See
+ #: :func:`_genshi_removes_blank_lines` for an explanation of this.
+ GENSHI_REMOVES_BLANK_LINES = _genshi_removes_blank_lines()
except ImportError:
TemplateLoader = None # pylint: disable=C0103
HAS_GENSHI = False
@@ -111,7 +142,17 @@ class CfgGenshiGenerator(CfgGenerator):
# the traceback is just the beginning of the block.
err = exc[1]
stack = traceback.extract_tb(exc[2])
- lineno, func = stack[-1][1:3]
+
+ # find the right frame of the stack
+ for frame in reversed(stack):
+ if frame[0] == self.name:
+ lineno, func = frame[1:3]
+ break
+ else:
+ # couldn't even find the stack frame, wtf.
+ raise PluginExecutionError("%s: %s" %
+ (err.__class__.__name__, err))
+
execs = [contents
for etype, contents, _ in self.template.stream
if etype == self.template.EXEC]
@@ -129,18 +170,20 @@ class CfgGenshiGenerator(CfgGenerator):
# else, no EXEC blocks -- WTF?
if contents:
# we now have the bogus block, but we need to get the
- # offending line. To get there, we do (line number
- # given in the exception) - (firstlineno from the
- # internal genshi code object of the snippet) + 1 =
- # (line number of the line with an error within the
- # block, with all multiple line breaks elided to a
- # single line break)
- real_lineno = lineno - contents.code.co_firstlineno
- src = re.sub(r'\n\n+', '\n', contents.source).splitlines()
+ # offending line. To get there, we do (line number given
+ # in the exception) - (firstlineno from the internal
+ # genshi code object of the snippet) = (line number of the
+ # line with an error within the block, with blank lines
+ # removed as appropriate for
+ # :attr:`GENSHI_REMOVES_BLANK_LINES`)
+ code = contents.source.strip().splitlines()
+ if GENSHI_REMOVES_BLANK_LINES:
+ code = [l for l in code if l.strip()]
try:
+ line = code[lineno - contents.code.co_firstlineno]
raise PluginExecutionError("%s: %s at '%s'" %
(err.__class__.__name__, err,
- src[real_lineno]))
+ line))
except IndexError:
raise PluginExecutionError("%s: %s" %
(err.__class__.__name__, err))
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
index db6810e7c..f8712213e 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
@@ -11,7 +11,8 @@ import Bcfg2.Options
import Bcfg2.Server.Plugin
import Bcfg2.Server.Lint
# pylint: disable=W0622
-from Bcfg2.Compat import u_str, unicode, b64encode, walk_packages, any
+from Bcfg2.Compat import u_str, unicode, b64encode, walk_packages, any, \
+ oct_mode
# pylint: enable=W0622
LOGGER = logging.getLogger(__name__)
@@ -538,7 +539,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet):
entry.get("name"))
fname = os.path.join(self.path, generator.name)
entry.set('mode',
- str(oct(stat.S_IMODE(os.stat(fname).st_mode))))
+ oct_mode(stat.S_IMODE(os.stat(fname).st_mode)))
try:
return generator.get_data(entry, metadata)
except:
diff --git a/src/lib/Bcfg2/Server/Plugins/FileProbes.py b/src/lib/Bcfg2/Server/Plugins/FileProbes.py
index 8bd1d3504..5ec0d7280 100644
--- a/src/lib/Bcfg2/Server/Plugins/FileProbes.py
+++ b/src/lib/Bcfg2/Server/Plugins/FileProbes.py
@@ -24,7 +24,7 @@ import sys
import pwd
import grp
import Bcfg2.Client.XML
-from Bcfg2.Compat import b64encode
+from Bcfg2.Compat import b64encode, oct_mode
path = "%s"
@@ -41,7 +41,7 @@ data = Bcfg2.Client.XML.Element("ProbedFileData",
name=path,
owner=pwd.getpwuid(stat[4])[0],
group=grp.getgrgid(stat[5])[0],
- mode=oct(stat[0] & 4095))
+ mode=oct_mode(stat[0] & 4095))
try:
data.text = b64encode(open(path).read())
except:
@@ -101,7 +101,7 @@ class FileProbes(Bcfg2.Server.Plugin.Plugin,
for data in datalist:
if data.text is None:
- self.logger.error("Got null response to %s file probe from %s"
+ self.logger.error("Got null response to %s file probe from %s"
% (data.get('name'), metadata.hostname))
else:
try:
diff --git a/src/lib/Bcfg2/Server/Plugins/SEModules.py b/src/lib/Bcfg2/Server/Plugins/SEModules.py
index 3edfb72a3..fa47f9496 100644
--- a/src/lib/Bcfg2/Server/Plugins/SEModules.py
+++ b/src/lib/Bcfg2/Server/Plugins/SEModules.py
@@ -40,8 +40,8 @@ class SEModules(Bcfg2.Server.Plugin.GroupSpool):
#: objects as its EntrySet children.
es_child_cls = SEModuleData
- #: SEModules manages ``SELinux`` entries
- entry_type = 'SELinux'
+ #: SEModules manages ``SEModule`` entries
+ entry_type = 'SEModule'
#: The SEModules plugin is experimental
experimental = True
@@ -68,7 +68,7 @@ class SEModules(Bcfg2.Server.Plugin.GroupSpool):
return name.lstrip("/")
def HandlesEntry(self, entry, metadata):
- if entry.tag in self.Entries and entry.get('type') == 'module':
+ if entry.tag in self.Entries:
return self._get_module_filename(entry) in self.Entries[entry.tag]
return Bcfg2.Server.Plugin.GroupSpool.HandlesEntry(self, entry,
metadata)
diff --git a/src/lib/Bcfg2/Server/Plugins/SSLCA.py b/src/lib/Bcfg2/Server/Plugins/SSLCA.py
index b3a49c047..f83c04e87 100644
--- a/src/lib/Bcfg2/Server/Plugins/SSLCA.py
+++ b/src/lib/Bcfg2/Server/Plugins/SSLCA.py
@@ -3,253 +3,164 @@ certificates and their keys. """
import os
import sys
-import Bcfg2.Server.Plugin
-import Bcfg2.Options
-import lxml.etree
+import logging
import tempfile
+import lxml.etree
from subprocess import Popen, PIPE, STDOUT
-from Bcfg2.Compat import ConfigParser, md5
+import Bcfg2.Options
+import Bcfg2.Server.Plugin
+from Bcfg2.Compat import ConfigParser
from Bcfg2.Server.Plugin import PluginExecutionError
+LOGGER = logging.getLogger(__name__)
-class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
- """ The SSLCA generator handles the creation and management of ssl
- certificates and their keys. """
- __author__ = 'g.hagger@gmail.com'
- __child__ = Bcfg2.Server.Plugin.FileBacked
- key_specs = {}
- cert_specs = {}
- CAs = {}
- def __init__(self, core, datastore):
- Bcfg2.Server.Plugin.GroupSpool.__init__(self, core, datastore)
- self.infoxml = dict()
+class SSLCAXMLSpec(Bcfg2.Server.Plugin.StructFile):
+ """ Base class to handle key.xml and cert.xml """
+ attrs = dict()
+ tag = None
+
+ def get_spec(self, metadata):
+ """ Get a specification for the type of object described by
+ this SSLCA XML file for the given client metadata object """
+ entries = [e for e in self.Match(metadata) if e.tag == self.tag]
+ if len(entries) == 0:
+ raise PluginExecutionError("No matching %s entry found for %s "
+ "in %s" % (self.tag,
+ metadata.hostname,
+ self.name))
+ elif len(entries) > 1:
+ LOGGER.warning("More than one matching %s entry found for %s in "
+ "%s; using first match" % (self.tag,
+ metadata.hostname,
+ self.name))
+ rv = dict()
+ for attr, default in self.attrs.items():
+ val = entries[0].get(attr.lower(), default)
+ if default in ['true', 'false']:
+ rv[attr] = val == 'true'
+ else:
+ rv[attr] = val
+ return rv
+
+
+class SSLCAKeySpec(SSLCAXMLSpec):
+ """ Handle key.xml files """
+ attrs = dict(bits='2048', type='rsa')
+ tag = 'Key'
- def HandleEvent(self, event=None):
- """
- Updates which files this plugin handles based upon filesystem events.
- Allows configuration items to be added/removed without server restarts.
- """
- action = event.code2str()
- if event.filename[0] == '/':
- return
- epath = "".join([self.data, self.handles[event.requestID],
- event.filename])
- if os.path.isdir(epath):
- ident = self.handles[event.requestID] + event.filename
- else:
- ident = self.handles[event.requestID][:-1]
- fname = os.path.join(ident, event.filename)
+class SSLCACertSpec(SSLCAXMLSpec):
+ """ Handle cert.xml files """
+ attrs = dict(ca='default',
+ format='pem',
+ key=None,
+ days='365',
+ C=None,
+ L=None,
+ ST=None,
+ OU=None,
+ O=None,
+ emailAddress=None,
+ append_chain='false')
+ tag = 'Cert'
- if event.filename.endswith('.xml'):
+ def get_spec(self, metadata):
+ rv = SSLCAXMLSpec.get_spec(self, metadata)
+ rv['subjectaltname'] = [e.text for e in self.Match(metadata)
+ if e.tag == "SubjectAltName"]
+ return rv
+
+
+class SSLCADataFile(Bcfg2.Server.Plugin.SpecificData):
+ """ Handle key and cert files """
+ def bind_entry(self, entry, _):
+ """ Bind the data in the file to the given abstract entry """
+ entry.text = self.data
+ return entry
+
+
+class SSLCAEntrySet(Bcfg2.Server.Plugin.EntrySet):
+ """ Entry set to handle SSLCA entries and XML files """
+ def __init__(self, _, path, entry_type, encoding, parent=None):
+ Bcfg2.Server.Plugin.EntrySet.__init__(self, os.path.basename(path),
+ path, entry_type, encoding)
+ self.parent = parent
+ self.key = None
+ self.cert = None
+
+ def handle_event(self, event):
+ action = event.code2str()
+ fpath = os.path.join(self.path, event.filename)
+
+ if event.filename == 'key.xml':
+ if action in ['exists', 'created', 'changed']:
+ self.key = SSLCAKeySpec(fpath)
+ self.key.HandleEvent(event)
+ elif event.filename == 'cert.xml':
if action in ['exists', 'created', 'changed']:
- if event.filename.endswith('key.xml'):
- key_spec = lxml.etree.parse(epath,
- parser=Bcfg2.Server.XMLParser
- ).find('Key')
- self.key_specs[ident] = {
- 'bits': key_spec.get('bits', '2048'),
- 'type': key_spec.get('type', 'rsa')
- }
- self.Entries['Path'][ident] = self.get_key
- elif event.filename.endswith('cert.xml'):
- cert_spec = lxml.etree.parse(epath,
- parser=Bcfg2.Server.XMLParser
- ).find('Cert')
- ca = cert_spec.get('ca', 'default')
- self.cert_specs[ident] = {
- 'ca': ca,
- 'format': cert_spec.get('format', 'pem'),
- 'key': cert_spec.get('key'),
- 'days': cert_spec.get('days', '365'),
- 'C': cert_spec.get('c'),
- 'L': cert_spec.get('l'),
- 'ST': cert_spec.get('st'),
- 'OU': cert_spec.get('ou'),
- 'O': cert_spec.get('o'),
- 'emailAddress': cert_spec.get('emailaddress'),
- 'append_chain':
- cert_spec.get('append_chain',
- 'false').lower() == 'true',
- }
- self.CAs[ca] = dict(self.core.setup.cfp.items('sslca_%s' %
- ca))
- self.Entries['Path'][ident] = self.get_cert
- elif event.filename.endswith("info.xml"):
- self.infoxml[ident] = Bcfg2.Server.Plugin.InfoXML(epath)
- self.infoxml[ident].HandleEvent(event)
- if action == 'deleted':
- if ident in self.Entries['Path']:
- del self.Entries['Path'][ident]
+ self.cert = SSLCACertSpec(fpath)
+ self.cert.HandleEvent(event)
else:
- if action in ['exists', 'created']:
- if os.path.isdir(epath):
- self.AddDirectoryMonitor(epath[len(self.data):])
- if ident not in self.entries and os.path.isfile(epath):
- self.entries[fname] = self.__child__(epath)
- self.entries[fname].HandleEvent(event)
- if action == 'changed':
- self.entries[fname].HandleEvent(event)
- elif action == 'deleted':
- if fname in self.entries:
- del self.entries[fname]
- else:
- self.entries[fname].HandleEvent(event)
-
- def get_key(self, entry, metadata):
+ Bcfg2.Server.Plugin.EntrySet.handle_event(self, event)
+
+ def build_key(self, entry, metadata):
"""
either grabs a prexisting key hostfile, or triggers the generation
of a new key if one doesn't exist.
"""
- # check if we already have a hostfile, or need to generate a new key
# TODO: verify key fits the specs
- path = entry.get('name')
- filename = os.path.join(path, "%s.H_%s" % (os.path.basename(path),
- metadata.hostname))
- if filename not in list(self.entries.keys()):
- self.logger.info("SSLCA: Generating new key %s" % filename)
- key = self.build_key(entry)
- open(self.data + filename, 'w').write(key)
- entry.text = key
- self.entries[filename] = self.__child__(self.data + filename)
- self.entries[filename].HandleEvent()
- else:
- entry.text = self.entries[filename].data
-
- entry.set("type", "file")
- if path in self.infoxml:
- Bcfg2.Server.Plugin.bind_info(entry, metadata,
- infoxml=self.infoxml[path])
- else:
- Bcfg2.Server.Plugin.bind_info(entry, metadata)
-
- def build_key(self, entry):
- """ generates a new key according the the specification """
- ktype = self.key_specs[entry.get('name')]['type']
- bits = self.key_specs[entry.get('name')]['bits']
+ filename = "%s.H_%s" % (os.path.basename(entry.get('name')),
+ metadata.hostname)
+ self.logger.info("SSLCA: Generating new key %s" % filename)
+ key_spec = self.key.get_spec(metadata)
+ ktype = key_spec['type']
+ bits = key_spec['bits']
if ktype == 'rsa':
cmd = ["openssl", "genrsa", bits]
elif ktype == 'dsa':
cmd = ["openssl", "dsaparam", "-noout", "-genkey", bits]
self.debug_log("SSLCA: Generating new key: %s" % " ".join(cmd))
- return Popen(cmd, stdout=PIPE).stdout.read()
-
- def get_cert(self, entry, metadata):
- """
- either grabs a prexisting cert hostfile, or triggers the generation
- of a new cert if one doesn't exist.
- """
- path = entry.get('name')
- filename = os.path.join(path, "%s.H_%s" % (os.path.basename(path),
- metadata.hostname))
-
- # first - ensure we have a key to work with
- key = self.cert_specs[entry.get('name')].get('key')
- key_filename = os.path.join(key, "%s.H_%s" % (os.path.basename(key),
- metadata.hostname))
- if key_filename not in self.entries:
- el = lxml.etree.Element('Path')
- el.set('name', key)
- self.core.Bind(el, metadata)
-
- # check if we have a valid hostfile
- if (filename in self.entries.keys() and
- self.verify_cert(filename, key_filename, entry)):
- entry.text = self.entries[filename].data
- else:
- self.logger.info("SSLCA: Generating new cert %s" % filename)
- cert = self.build_cert(key_filename, entry, metadata)
- open(self.data + filename, 'w').write(cert)
- self.entries[filename] = self.__child__(self.data + filename)
- self.entries[filename].HandleEvent()
- entry.text = cert
-
- entry.set("type", "file")
- if path in self.infoxml:
- Bcfg2.Server.Plugin.bind_info(entry, metadata,
- infoxml=self.infoxml[path])
- else:
- Bcfg2.Server.Plugin.bind_info(entry, metadata)
-
- def verify_cert(self, filename, key_filename, entry):
- """ Perform certification verification against the CA and
- against the key """
- ca = self.CAs[self.cert_specs[entry.get('name')]['ca']]
- do_verify = ca.get('chaincert')
- if do_verify:
- return (self.verify_cert_against_ca(filename, entry) and
- self.verify_cert_against_key(filename, key_filename))
- return True
-
- def verify_cert_against_ca(self, filename, entry):
- """
- check that a certificate validates against the ca cert,
- and that it has not expired.
- """
- ca = self.CAs[self.cert_specs[entry.get('name')]['ca']]
- chaincert = ca.get('chaincert')
- cert = self.data + filename
- cmd = ["openssl", "verify"]
- is_root = ca.get('root_ca', "false").lower() == 'true'
- if is_root:
- cmd.append("-CAfile")
- else:
- # verifying based on an intermediate cert
- cmd.extend(["-purpose", "sslserver", "-untrusted"])
- cmd.extend([chaincert, cert])
- self.debug_log("SSLCA: Verifying %s against CA: %s" %
- (entry.get("name"), " ".join(cmd)))
- res = Popen(cmd, stdout=PIPE, stderr=STDOUT).stdout.read()
- if res == cert + ": OK\n":
- self.debug_log("SSLCA: %s verified successfully against CA" %
- entry.get("name"))
- return True
- self.logger.warning("SSLCA: %s failed verification against CA: %s" %
- (entry.get("name"), res))
- return False
-
- def verify_cert_against_key(self, filename, key_filename):
- """
- check that a certificate validates against its private key.
- """
- cert = self.data + filename
- key = self.data + key_filename
- cert_md5 = \
- md5(Popen(["openssl", "x509", "-noout", "-modulus", "-in", cert],
- stdout=PIPE,
- stderr=STDOUT).stdout.read().strip()).hexdigest()
- key_md5 = \
- md5(Popen(["openssl", "rsa", "-noout", "-modulus", "-in", key],
- stdout=PIPE,
- stderr=STDOUT).stdout.read().strip()).hexdigest()
- if cert_md5 == key_md5:
- self.debug_log("SSLCA: %s verified successfully against key %s" %
- (filename, key_filename))
- return True
- self.logger.warning("SSLCA: %s failed verification against key %s" %
- (filename, key_filename))
- return False
+ proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ key, err = proc.communicate()
+ if proc.wait():
+ raise PluginExecutionError("SSLCA: Failed to generate key %s for "
+ "%s: %s" % (entry.get("name"),
+ metadata.hostname, err))
+ open(os.path.join(self.path, filename), 'w').write(key)
+ return key
- def build_cert(self, key_filename, entry, metadata):
- """
- creates a new certificate according to the specification
- """
+ def build_cert(self, entry, metadata, keyfile):
+ """ generate a new cert """
+ filename = "%s.H_%s" % (os.path.basename(entry.get('name')),
+ metadata.hostname)
+ self.logger.info("SSLCA: Generating new cert %s" % filename)
+ cert_spec = self.cert.get_spec(metadata)
+ ca = self.parent.get_ca(cert_spec['ca'])
req_config = None
req = None
try:
- req_config = self.build_req_config(entry, metadata)
- req = self.build_request(key_filename, req_config, entry)
- ca = self.cert_specs[entry.get('name')]['ca']
- ca_config = self.CAs[ca]['config']
- days = self.cert_specs[entry.get('name')]['days']
- passphrase = self.CAs[ca].get('passphrase')
- cmd = ["openssl", "ca", "-config", ca_config, "-in", req,
+ req_config = self.build_req_config(metadata)
+ req = self.build_request(keyfile, req_config, metadata)
+ days = cert_spec['days']
+ cmd = ["openssl", "ca", "-config", ca['config'], "-in", req,
"-days", days, "-batch"]
+ passphrase = ca.get('passphrase')
if passphrase:
cmd.extend(["-passin", "pass:%s" % passphrase])
+
+ def _scrub_pass(arg):
+ """ helper to scrub the passphrase from the
+ argument list """
+ if arg.startswith("pass:"):
+ return "pass:******"
+ else:
+ return arg
+ else:
+ _scrub_pass = lambda a: a
+
self.debug_log("SSLCA: Generating new certificate: %s" %
- " ".join(cmd))
+ " ".join(_scrub_pass(a) for a in cmd))
proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
(cert, err) = proc.communicate()
if proc.wait():
@@ -266,12 +177,13 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
except OSError:
self.logger.error("SSLCA: Failed to unlink temporary files: %s"
% sys.exc_info()[1])
- if (self.cert_specs[entry.get('name')]['append_chain'] and
- self.CAs[ca]['chaincert']):
- cert += open(self.CAs[ca]['chaincert']).read()
+ if cert_spec['append_chain'] and 'chaincert' in ca:
+ cert += open(self.parent.get_ca(ca)['chaincert']).read()
+
+ open(os.path.join(self.path, filename), 'w').write(cert)
return cert
- def build_req_config(self, entry, metadata):
+ def build_req_config(self, metadata):
"""
generates a temporary openssl configuration file that is
used to generate the required certificate request
@@ -298,16 +210,17 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
cfp.add_section(section)
for key in defaults[section]:
cfp.set(section, key, defaults[section][key])
+ cert_spec = self.cert.get_spec(metadata)
altnamenum = 1
- altnames = list(metadata.aliases)
+ altnames = cert_spec['subjectaltname']
+ altnames.extend(list(metadata.aliases))
altnames.append(metadata.hostname)
for altname in altnames:
cfp.set('alt_names', 'DNS.' + str(altnamenum), altname)
altnamenum += 1
for item in ['C', 'L', 'ST', 'O', 'OU', 'emailAddress']:
- if self.cert_specs[entry.get('name')][item]:
- cfp.set('req_distinguished_name', item,
- self.cert_specs[entry.get('name')][item])
+ if cert_spec[item]:
+ cfp.set('req_distinguished_name', item, cert_spec[item])
cfp.set('req_distinguished_name', 'CN', metadata.hostname)
self.debug_log("SSLCA: Writing temporary request config to %s" % fname)
try:
@@ -317,16 +230,15 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
"config file: %s" % sys.exc_info()[1])
return fname
- def build_request(self, key_filename, req_config, entry):
+ def build_request(self, keyfile, req_config, metadata):
"""
creates the certificate request
"""
fd, req = tempfile.mkstemp()
os.close(fd)
- days = self.cert_specs[entry.get('name')]['days']
- key = self.data + key_filename
+ days = self.cert.get_spec(metadata)['days']
cmd = ["openssl", "req", "-new", "-config", req_config,
- "-days", days, "-key", key, "-text", "-out", req]
+ "-days", days, "-key", keyfile, "-text", "-out", req]
self.debug_log("SSLCA: Generating new CSR: %s" % " ".join(cmd))
proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
err = proc.communicate()[1]
@@ -334,3 +246,122 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
raise PluginExecutionError("SSLCA: Failed to generate CSR: %s" %
err)
return req
+
+ def verify_cert(self, filename, keyfile, entry, metadata):
+ """ Perform certification verification against the CA and
+ against the key """
+ ca = self.parent.get_ca(self.cert.get_spec(metadata)['ca'])
+ do_verify = ca.get('chaincert')
+ if do_verify:
+ return (self.verify_cert_against_ca(filename, entry, metadata) and
+ self.verify_cert_against_key(filename, keyfile))
+ return True
+
+ def verify_cert_against_ca(self, filename, entry, metadata):
+ """
+ check that a certificate validates against the ca cert,
+ and that it has not expired.
+ """
+ ca = self.parent.get_ca(self.cert.get_spec(metadata)['ca'])
+ chaincert = ca.get('chaincert')
+ cert = os.path.join(self.path, filename)
+ cmd = ["openssl", "verify"]
+ is_root = ca.get('root_ca', "false").lower() == 'true'
+ if is_root:
+ cmd.append("-CAfile")
+ else:
+ # verifying based on an intermediate cert
+ cmd.extend(["-purpose", "sslserver", "-untrusted"])
+ cmd.extend([chaincert, cert])
+ self.debug_log("SSLCA: Verifying %s against CA: %s" %
+ (entry.get("name"), " ".join(cmd)))
+ res = Popen(cmd, stdout=PIPE, stderr=STDOUT).stdout.read()
+ if res == cert + ": OK\n":
+ self.debug_log("SSLCA: %s verified successfully against CA" %
+ entry.get("name"))
+ return True
+ self.logger.warning("SSLCA: %s failed verification against CA: %s" %
+ (entry.get("name"), res))
+ return False
+
+ def verify_cert_against_key(self, filename, keyfile):
+ """
+ check that a certificate validates against its private key.
+ """
+ def _modulus(fname, ftype="x509"):
+ """ get the modulus from the given file """
+ cmd = ["openssl", ftype, "-noout", "-modulus", "-in", fname]
+ self.debug_log("SSLCA: Getting modulus of %s for verification: %s"
+ % (fname, " ".join(cmd)))
+ proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ rv, err = proc.communicate()
+ if proc.wait():
+ self.logger.warning("SSLCA: Failed to get modulus of %s: %s" %
+ (fname, err))
+ return rv.strip() # pylint: disable=E1103
+
+ certfile = os.path.join(self.path, filename)
+ cert = _modulus(certfile)
+ key = _modulus(keyfile, ftype="rsa")
+ if cert == key:
+ self.debug_log("SSLCA: %s verified successfully against key %s" %
+ (filename, keyfile))
+ return True
+ self.logger.warning("SSLCA: %s failed verification against key %s" %
+ (filename, keyfile))
+ return False
+
+ def bind_entry(self, entry, metadata):
+ if self.key:
+ self.bind_info_to_entry(entry, metadata)
+ try:
+ return self.best_matching(metadata).bind_entry(entry, metadata)
+ except PluginExecutionError:
+ entry.text = self.build_key(entry, metadata)
+ return entry
+ elif self.cert:
+ key = self.cert.get_spec(metadata)['key']
+ cleanup_keyfile = False
+ try:
+ keyfile = self.parent.entries[key].best_matching(metadata).name
+ except PluginExecutionError:
+ cleanup_keyfile = True
+ # create a temp file with the key in it
+ fd, keyfile = tempfile.mkstemp()
+ os.chmod(keyfile, 384) # 0600
+ el = lxml.etree.Element('Path', name=key)
+ self.parent.core.Bind(el, metadata)
+ os.fdopen(fd, 'w').write(el.text)
+
+ try:
+ self.bind_info_to_entry(entry, metadata)
+ try:
+ best = self.best_matching(metadata)
+ if self.verify_cert(best.name, keyfile, entry, metadata):
+ return best.bind_entry(entry, metadata)
+ except PluginExecutionError:
+ pass
+ # if we get here, it's because either a) there was no best
+ # matching entry; or b) the existing cert did not verify
+ entry.text = self.build_cert(entry, metadata, keyfile)
+ return entry
+ finally:
+ if cleanup_keyfile:
+ try:
+ os.unlink(keyfile)
+ except OSError:
+ err = sys.exc_info()[1]
+ self.logger.error("SSLCA: Failed to unlink temporary "
+ "key %s: %s" % (keyfile, err))
+
+
+class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
+ """ The SSLCA generator handles the creation and management of ssl
+ certificates and their keys. """
+ __author__ = 'g.hagger@gmail.com'
+ es_cls = lambda self, *args: SSLCAEntrySet(*args, parent=self)
+ es_child_cls = SSLCADataFile
+
+ def get_ca(self, name):
+ """ get a dict describing a CA from the config file """
+ return dict(self.core.setup.cfp.items("sslca_%s" % name))
diff --git a/src/lib/Bcfg2/version.py b/src/lib/Bcfg2/version.py
index b4ac47769..f83863cce 100644
--- a/src/lib/Bcfg2/version.py
+++ b/src/lib/Bcfg2/version.py
@@ -29,8 +29,8 @@ class Bcfg2VersionInfo(tuple):
tuple(self)
def __repr__(self):
- return "(major=%s, minor=%s, micro=%s, releaselevel=%s, serial=%s)" % \
- tuple(self)
+ return "%s(major=%s, minor=%s, micro=%s, releaselevel=%s, serial=%s)" \
+ % ((self.__class__.__name__,) + tuple(self))
def _release_cmp(self, rel1, rel2): # pylint: disable=R0911
""" compare two release numbers """