summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2')
-rw-r--r--src/lib/Bcfg2/Client/Frame.py22
-rw-r--r--src/lib/Bcfg2/Client/Tools/Chkconfig.py96
-rw-r--r--src/lib/Bcfg2/Client/Tools/DebInit.py103
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/Directory.py6
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIXUsers.py6
-rw-r--r--src/lib/Bcfg2/Client/Tools/Portage.py8
-rw-r--r--src/lib/Bcfg2/Client/Tools/RcUpdate.py108
-rw-r--r--src/lib/Bcfg2/Client/Tools/Systemd.py2
-rw-r--r--src/lib/Bcfg2/Client/Tools/__init__.py16
-rw-r--r--src/lib/Bcfg2/Client/__init__.py2
-rw-r--r--src/lib/Bcfg2/Compat.py29
-rw-r--r--src/lib/Bcfg2/Options.py21
-rw-r--r--src/lib/Bcfg2/Proxy.py9
-rw-r--r--src/lib/Bcfg2/Reporting/Storage/DjangoORM.py3
-rw-r--r--src/lib/Bcfg2/Reporting/models.py58
-rw-r--r--src/lib/Bcfg2/SSLServer.py11
-rw-r--r--src/lib/Bcfg2/Server/Admin/Client.py13
-rw-r--r--src/lib/Bcfg2/Server/Admin/Compare.py3
-rw-r--r--src/lib/Bcfg2/Server/Admin/Minestruct.py15
-rw-r--r--src/lib/Bcfg2/Server/Admin/Pull.py5
-rw-r--r--src/lib/Bcfg2/Server/Admin/Reports.py23
-rw-r--r--src/lib/Bcfg2/Server/Admin/Syncdb.py9
-rw-r--r--src/lib/Bcfg2/Server/Admin/Xcmd.py17
-rw-r--r--src/lib/Bcfg2/Server/Admin/__init__.py1
-rw-r--r--src/lib/Bcfg2/Server/BuiltinCore.py2
-rw-r--r--src/lib/Bcfg2/Server/Core.py12
-rw-r--r--src/lib/Bcfg2/Server/Lint/Comments.py123
-rwxr-xr-xsrc/lib/Bcfg2/Server/Lint/Genshi.py17
-rw-r--r--src/lib/Bcfg2/Server/Lint/GroupNames.py28
-rw-r--r--src/lib/Bcfg2/Server/Lint/InfoXML.py14
-rw-r--r--src/lib/Bcfg2/Server/Lint/MergeFiles.py2
-rw-r--r--src/lib/Bcfg2/Server/Lint/RequiredAttrs.py28
-rw-r--r--src/lib/Bcfg2/Server/Lint/Validate.py58
-rw-r--r--src/lib/Bcfg2/Server/Lint/__init__.py180
-rw-r--r--src/lib/Bcfg2/Server/MultiprocessingCore.py204
-rw-r--r--src/lib/Bcfg2/Server/Plugin/helpers.py18
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Bundler.py13
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py87
-rw-r--r--src/lib/Bcfg2/Server/Plugins/FileProbes.py6
-rw-r--r--src/lib/Bcfg2/Server/Plugins/GroupPatterns.py7
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Metadata.py90
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Apt.py5
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Source.py4
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Yum.py36
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/__init__.py9
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Pkgmgr.py16
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Probes.py14
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SSHbase.py5
-rw-r--r--src/lib/Bcfg2/Server/Plugins/TemplateHelper.py19
-rw-r--r--src/lib/Bcfg2/Utils.py2
50 files changed, 1159 insertions, 426 deletions
diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py
index 850e58d9d..6bef77081 100644
--- a/src/lib/Bcfg2/Client/Frame.py
+++ b/src/lib/Bcfg2/Client/Frame.py
@@ -6,15 +6,7 @@ import fnmatch
import logging
import Bcfg2.Client.Tools
from Bcfg2.Client import prompt
-from Bcfg2.Compat import any, all, cmp # pylint: disable=W0622
-
-
-def cmpent(ent1, ent2):
- """Sort entries."""
- if ent1.tag != ent2.tag:
- return cmp(ent1.tag, ent2.tag)
- else:
- return cmp(ent1.get('name'), ent2.get('name'))
+from Bcfg2.Compat import any, all # pylint: disable=W0622
def matches_entry(entryspec, entry):
@@ -105,8 +97,8 @@ class Frame(object):
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)
+ self.logger.info("Loaded experimental tool drivers:")
+ self.logger.info(experimental)
# find entries not handled by any tools
self.unhandled = [entry for struct in config
@@ -155,7 +147,7 @@ class Frame(object):
def promptFilter(self, msg, entries):
"""Filter a supplied list based on user input."""
ret = []
- entries.sort(cmpent)
+ entries.sort(key=lambda e: e.tag + ":" + e.get('name'))
for entry in entries[:]:
if entry in self.unhandled:
# don't prompt for entries that can't be installed
@@ -418,10 +410,12 @@ class Frame(object):
# prune out unspecified bundles when running with -b
continue
if bundle in mbundles:
- self.logger.debug("Bundle %s was modified" % bundle)
+ self.logger.debug("Bundle %s was modified" %
+ bundle.get('name'))
func = "BundleUpdated"
else:
- self.logger.debug("Bundle %s was not modified" % bundle)
+ self.logger.debug("Bundle %s was not modified" %
+ bundle.get('name'))
func = "BundleNotUpdated"
for tool in self.tools:
try:
diff --git a/src/lib/Bcfg2/Client/Tools/Chkconfig.py b/src/lib/Bcfg2/Client/Tools/Chkconfig.py
index ec7f462b3..256c28255 100644
--- a/src/lib/Bcfg2/Client/Tools/Chkconfig.py
+++ b/src/lib/Bcfg2/Client/Tools/Chkconfig.py
@@ -19,25 +19,22 @@ class Chkconfig(Bcfg2.Client.Tools.SvcTool):
def get_svc_command(self, service, action):
return "/sbin/service %s %s" % (service.get('name'), action)
- def VerifyService(self, entry, _):
- """Verify Service status for entry."""
- if entry.get('status') == 'ignore':
- return True
-
+ def verify_bootstatus(self, entry, bootstatus):
+ """Verify bootstatus for entry."""
rv = self.cmd.run("/sbin/chkconfig --list %s " % entry.get('name'))
if rv.success:
srvdata = rv.stdout.splitlines()[0].split()
else:
# service not installed
- entry.set('current_status', 'off')
+ entry.set('current_bootstatus', 'service not installed')
return False
if len(srvdata) == 2:
# This is an xinetd service
- if entry.get('status') == srvdata[1]:
+ if bootstatus == srvdata[1]:
return True
else:
- entry.set('current_status', srvdata[1])
+ entry.set('current_bootstatus', srvdata[1])
return False
try:
@@ -46,37 +43,74 @@ class Chkconfig(Bcfg2.Client.Tools.SvcTool):
except IndexError:
onlevels = []
- pstatus = self.check_service(entry)
- if entry.get('status') == 'on':
- status = (len(onlevels) > 0 and pstatus)
+ if bootstatus == 'on':
+ current_bootstatus = (len(onlevels) > 0)
else:
- status = (len(onlevels) == 0 and not pstatus)
+ current_bootstatus = (len(onlevels) == 0)
+ return current_bootstatus
+
+ def VerifyService(self, entry, _):
+ """Verify Service status for entry."""
+ entry.set('target_status', entry.get('status')) # for reporting
+ bootstatus = self.get_bootstatus(entry)
+ if bootstatus is None:
+ return True
+ current_bootstatus = self.verify_bootstatus(entry, bootstatus)
- if not status:
+ if entry.get('status') == 'ignore':
+ # 'ignore' should verify
+ current_svcstatus = True
+ else:
+ svcstatus = self.check_service(entry)
if entry.get('status') == 'on':
- entry.set('current_status', 'off')
- else:
- entry.set('current_status', 'on')
- return status
+ if svcstatus:
+ current_svcstatus = True
+ else:
+ current_svcstatus = False
+ elif entry.get('status') == 'off':
+ if svcstatus:
+ current_svcstatus = False
+ else:
+ current_svcstatus = True
+
+ if svcstatus:
+ entry.set('current_status', 'on')
+ else:
+ entry.set('current_status', 'off')
+
+ return current_bootstatus and current_svcstatus
def InstallService(self, entry):
"""Install Service entry."""
- rcmd = "/sbin/chkconfig %s %s"
- self.cmd.run("/sbin/chkconfig --add %s" % (entry.attrib['name']))
+ self.cmd.run("/sbin/chkconfig --add %s" % (entry.get('name')))
self.logger.info("Installing Service %s" % (entry.get('name')))
- rv = True
- if entry.get('status') == 'off':
- rv &= self.cmd.run((rcmd + " --level 0123456") %
- (entry.get('name'),
- entry.get('status'))).success
- if entry.get("current_status") == "on":
- rv &= self.stop_service(entry).success
+ bootstatus = entry.get('bootstatus')
+ if bootstatus is not None:
+ if bootstatus == 'on':
+ # make sure service is enabled on boot
+ bootcmd = '/sbin/chkconfig %s %s --level 0123456' % \
+ (entry.get('name'), entry.get('bootstatus'))
+ elif bootstatus == 'off':
+ # make sure service is disabled on boot
+ bootcmd = '/sbin/chkconfig %s %s' % (entry.get('name'),
+ entry.get('bootstatus'))
+ bootcmdrv = self.cmd.run(bootcmd).success
+ if self.setup['servicemode'] == 'disabled':
+ # 'disabled' means we don't attempt to modify running svcs
+ return bootcmdrv
+ buildmode = self.setup['servicemode'] == 'build'
+ if (entry.get('status') == 'on' and not buildmode) and \
+ entry.get('current_status') == 'off':
+ svccmdrv = self.start_service(entry)
+ elif (entry.get('status') == 'off' or buildmode) and \
+ entry.get('current_status') == 'on':
+ svccmdrv = self.stop_service(entry)
+ else:
+ svccmdrv = True # ignore status attribute
+ return bootcmdrv and svccmdrv
else:
- rv &= self.cmd.run(rcmd % (entry.get('name'),
- entry.get('status'))).success
- if entry.get("current_status") == "off":
- rv &= self.start_service(entry).success
- return rv
+ # when bootstatus is 'None', status == 'ignore'
+ return True
def FindExtra(self):
"""Locate extra chkconfig Services."""
diff --git a/src/lib/Bcfg2/Client/Tools/DebInit.py b/src/lib/Bcfg2/Client/Tools/DebInit.py
index d916b1662..761c51db7 100644
--- a/src/lib/Bcfg2/Client/Tools/DebInit.py
+++ b/src/lib/Bcfg2/Client/Tools/DebInit.py
@@ -18,13 +18,11 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
svcre = \
re.compile(r'/etc/.*/(?P<action>[SK])(?P<sequence>\d+)(?P<name>\S+)')
- # implement entry (Verify|Install) ops
- def VerifyService(self, entry, _):
- """Verify Service status for entry."""
-
- if entry.get('status') == 'ignore':
- return True
+ def get_svc_command(self, service, action):
+ return '/usr/sbin/invoke-rc.d %s %s' % (service.get('name'), action)
+ def verify_bootstatus(self, entry, bootstatus):
+ """Verify bootstatus for entry."""
rawfiles = glob.glob("/etc/rc*.d/[SK]*%s" % (entry.get('name')))
files = []
@@ -54,9 +52,9 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
continue
if match.group('name') == entry.get('name'):
files.append(filename)
- if entry.get('status') == 'off':
+ if bootstatus == 'off':
if files:
- entry.set('current_status', 'on')
+ entry.set('current_bootstatus', 'on')
return False
else:
return True
@@ -72,12 +70,47 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
return False
return True
else:
- entry.set('current_status', 'off')
+ entry.set('current_bootstatus', 'off')
return False
+ def VerifyService(self, entry, _):
+ """Verify Service status for entry."""
+ entry.set('target_status', entry.get('status')) # for reporting
+ bootstatus = self.get_bootstatus(entry)
+ if bootstatus is None:
+ return True
+ current_bootstatus = self.verify_bootstatus(entry, bootstatus)
+
+ if entry.get('status') == 'ignore':
+ # 'ignore' should verify
+ current_svcstatus = True
+ svcstatus = True
+ else:
+ svcstatus = self.check_service(entry)
+ if entry.get('status') == 'on':
+ if svcstatus:
+ current_svcstatus = True
+ else:
+ current_svcstatus = False
+ elif entry.get('status') == 'off':
+ if svcstatus:
+ current_svcstatus = False
+ else:
+ current_svcstatus = True
+
+ if svcstatus:
+ entry.set('current_status', 'on')
+ else:
+ entry.set('current_status', 'off')
+
+ return current_bootstatus and current_svcstatus
+
def InstallService(self, entry):
- """Install Service for entry."""
+ """Install Service entry."""
self.logger.info("Installing Service %s" % (entry.get('name')))
+ bootstatus = entry.get('bootstatus')
+
+ # check if init script exists
try:
os.stat('/etc/init.d/%s' % entry.get('name'))
except OSError:
@@ -85,20 +118,41 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
entry.get('name'))
return False
- if entry.get('status') == 'off':
- self.cmd.run("/usr/sbin/invoke-rc.d %s stop" % (entry.get('name')))
- return self.cmd.run("/usr/sbin/update-rc.d -f %s remove" %
- entry.get('name')).success
+ if bootstatus is not None:
+ seqcmdrv = True
+ if bootstatus == 'on':
+ # make sure service is enabled on boot
+ bootcmd = '/usr/sbin/update-rc.d %s defaults' % \
+ entry.get('name')
+ if entry.get('sequence'):
+ seqcmd = '/usr/sbin/update-rc.d -f %s remove' % \
+ entry.get('name')
+ seqcmdrv = self.cmd.run(seqcmd)
+ start_sequence = int(entry.get('sequence'))
+ kill_sequence = 100 - start_sequence
+ bootcmd = '%s %d %d' % (bootcmd, start_sequence,
+ kill_sequence)
+ elif bootstatus == 'off':
+ # make sure service is disabled on boot
+ bootcmd = '/usr/sbin/update-rc.d -f %s remove' % \
+ entry.get('name')
+ bootcmdrv = self.cmd.run(bootcmd)
+ if self.setup['servicemode'] == 'disabled':
+ # 'disabled' means we don't attempt to modify running svcs
+ return bootcmdrv and seqcmdrv
+ buildmode = self.setup['servicemode'] == 'build'
+ if (entry.get('status') == 'on' and not buildmode) and \
+ entry.get('current_status') == 'off':
+ svccmdrv = self.start_service(entry)
+ elif (entry.get('status') == 'off' or buildmode) and \
+ entry.get('current_status') == 'on':
+ svccmdrv = self.stop_service(entry)
+ else:
+ svccmdrv = True # ignore status attribute
+ return bootcmdrv and svccmdrv and seqcmdrv
else:
- command = "/usr/sbin/update-rc.d %s defaults" % (entry.get('name'))
- if entry.get('sequence'):
- if not self.cmd.run("/usr/sbin/update-rc.d -f %s remove" %
- entry.get('name')).success:
- return False
- start_sequence = int(entry.get('sequence'))
- kill_sequence = 100 - start_sequence
- command = "%s %d %d" % (command, start_sequence, kill_sequence)
- return self.cmd.run(command).success
+ # when bootstatus is 'None', status == 'ignore'
+ return True
def FindExtra(self):
"""Find Extra Debian Service entries."""
@@ -116,6 +170,3 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
# Extra service removal is nonsensical
# Extra services need to be reflected in the config
return
-
- def get_svc_command(self, service, action):
- return '/usr/sbin/invoke-rc.d %s %s' % (service.get('name'), action)
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Directory.py b/src/lib/Bcfg2/Client/Tools/POSIX/Directory.py
index 9d0fe05e0..675a4461a 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/Directory.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/Directory.py
@@ -36,14 +36,14 @@ class POSIXDirectory(POSIXTool):
self.logger.info("POSIX: " + msg)
entry.set('qtext', entry.get('qtext', '') + '\n' + msg)
for extra in extras:
- Bcfg2.Client.XML.SubElement(entry, 'Prune', path=extra)
+ Bcfg2.Client.XML.SubElement(entry, 'Prune', name=extra)
except OSError:
prune = True
return POSIXTool.verify(self, entry, modlist) and prune
def install(self, entry):
- """Install device entries."""
+ """Install directory entries."""
fmode = self._exists(entry)
if fmode and not stat.S_ISDIR(fmode[stat.ST_MODE]):
@@ -67,7 +67,7 @@ class POSIXDirectory(POSIXTool):
if entry.get('prune', 'false') == 'true':
for pent in entry.findall('Prune'):
- pname = pent.get('path')
+ pname = pent.get('name')
try:
self.logger.debug("POSIX: Removing %s" % pname)
self._remove(pent)
diff --git a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
index 99ed3c7d9..8226392f9 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
@@ -154,7 +154,8 @@ class POSIXUsers(Bcfg2.Client.Tools.Tool):
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")]
+ expected = [e.get("group", e.text).strip()
+ for e in entry.findall("MemberOf")]
if set(expected) != set(actual):
entry.set('qtext',
"\n".join([entry.get('qtext', '')] +
@@ -252,7 +253,8 @@ class POSIXUsers(Bcfg2.Client.Tools.Tool):
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")]
+ extras = [e.get("group", e.text).strip()
+ for e in entry.findall("MemberOf")]
if extras:
cmd.extend(['-G', ",".join(extras)])
cmd.extend(['-d', entry.get('home')])
diff --git a/src/lib/Bcfg2/Client/Tools/Portage.py b/src/lib/Bcfg2/Client/Tools/Portage.py
index 17e7755a9..2d8b66ce5 100644
--- a/src/lib/Bcfg2/Client/Tools/Portage.py
+++ b/src/lib/Bcfg2/Client/Tools/Portage.py
@@ -74,10 +74,10 @@ class Portage(Bcfg2.Client.Tools.PkgTool):
self.logger.debug('Running equery check on %s' %
entry.get('name'))
- for line in self.cmd.run(["/usr/bin/equery", "-N", "check",
- '=%s-%s' %
- (entry.get('name'),
- version)]).stdout.splitlines():
+ for line in self.cmd.run(
+ ["/usr/bin/equery", "-N", "check",
+ '=%s-%s' % (entry.get('name'),
+ entry.get('version'))]).stdout.splitlines():
if '!!!' in line and line.split()[1] not in modlist:
return False
diff --git a/src/lib/Bcfg2/Client/Tools/RcUpdate.py b/src/lib/Bcfg2/Client/Tools/RcUpdate.py
index 4b78581f7..8e9626521 100644
--- a/src/lib/Bcfg2/Client/Tools/RcUpdate.py
+++ b/src/lib/Bcfg2/Client/Tools/RcUpdate.py
@@ -21,21 +21,38 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool):
'-s']).stdout.splitlines()
if 'started' in line]
+ def get_default_svcs(self):
+ """Return a list of services in the 'default' runlevel."""
+ return [line.split()[0]
+ for line in self.cmd.run(['/sbin/rc-update',
+ 'show']).stdout.splitlines()
+ if 'default' in line]
+
+ def verify_bootstatus(self, entry, bootstatus):
+ """Verify bootstatus for entry."""
+ # get a list of all started services
+ allsrv = self.get_default_svcs()
+ # set current_bootstatus attribute
+ if entry.get('name') in allsrv:
+ entry.set('current_bootstatus', 'on')
+ else:
+ entry.set('current_bootstatus', 'off')
+ if bootstatus == 'on':
+ return entry.get('name') in allsrv
+ else:
+ return entry.get('name') not in allsrv
+
def VerifyService(self, entry, _):
"""
Verify Service status for entry.
Assumes we run in the "default" runlevel.
"""
- if entry.get('status') == 'ignore':
+ entry.set('target_status', entry.get('status')) # for reporting
+ bootstatus = self.get_bootstatus(entry)
+ if bootstatus is None:
return True
-
- # get a list of all started services
- allsrv = self.get_enabled_svcs()
-
- # check if service is enabled
- result = self.cmd.run(["/sbin/rc-update", "show", "default"]).stdout
- is_enabled = entry.get("name") in result
+ current_bootstatus = self.verify_bootstatus(entry, bootstatus)
# check if init script exists
try:
@@ -45,39 +62,58 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool):
entry.get('name'))
return False
- # check if service is enabled
- is_running = entry.get('name') in allsrv
-
- if entry.get('status') == 'on' and not (is_enabled and is_running):
- entry.set('current_status', 'off')
- return False
-
- elif entry.get('status') == 'off' and (is_enabled or is_running):
+ if entry.get('status') == 'ignore':
+ # 'ignore' should verify
+ current_svcstatus = True
+ svcstatus = True
+ else:
+ svcstatus = self.check_service(entry)
+ if entry.get('status') == 'on':
+ if svcstatus:
+ current_svcstatus = True
+ else:
+ current_svcstatus = False
+ elif entry.get('status') == 'off':
+ if svcstatus:
+ current_svcstatus = False
+ else:
+ current_svcstatus = True
+
+ if svcstatus:
entry.set('current_status', 'on')
- return False
+ else:
+ entry.set('current_status', 'off')
- return True
+ return current_bootstatus and current_svcstatus
def InstallService(self, entry):
- """
- Install Service entry
-
- """
+ """Install Service entry."""
self.logger.info('Installing Service %s' % entry.get('name'))
- if entry.get('status') == 'on':
- if entry.get('current_status') == 'off':
- self.start_service(entry)
- # make sure it's enabled
- cmd = '/sbin/rc-update add %s default'
- return self.cmd.run(cmd % entry.get('name')).success
- elif entry.get('status') == 'off':
- if entry.get('current_status') == 'on':
- self.stop_service(entry)
- # make sure it's disabled
- cmd = '/sbin/rc-update del %s default'
- return self.cmd.run(cmd % entry.get('name')).success
-
- return False
+ bootstatus = entry.get('bootstatus')
+ if bootstatus is not None:
+ if bootstatus == 'on':
+ # make sure service is enabled on boot
+ bootcmd = '/sbin/rc-update add %s default'
+ elif bootstatus == 'off':
+ # make sure service is disabled on boot
+ bootcmd = '/sbin/rc-update del %s default'
+ bootcmdrv = self.cmd.run(bootcmd % entry.get('name')).success
+ if self.setup['servicemode'] == 'disabled':
+ # 'disabled' means we don't attempt to modify running svcs
+ return bootcmdrv
+ buildmode = self.setup['servicemode'] == 'build'
+ if (entry.get('status') == 'on' and not buildmode) and \
+ entry.get('current_status') == 'off':
+ svccmdrv = self.start_service(entry)
+ elif (entry.get('status') == 'off' or buildmode) and \
+ entry.get('current_status') == 'on':
+ svccmdrv = self.stop_service(entry)
+ else:
+ svccmdrv = True # ignore status attribute
+ return bootcmdrv and svccmdrv
+ else:
+ # when bootstatus is 'None', status == 'ignore'
+ return True
def FindExtra(self):
"""Locate extra rc-update services."""
diff --git a/src/lib/Bcfg2/Client/Tools/Systemd.py b/src/lib/Bcfg2/Client/Tools/Systemd.py
index 027d91c71..20a172d3d 100644
--- a/src/lib/Bcfg2/Client/Tools/Systemd.py
+++ b/src/lib/Bcfg2/Client/Tools/Systemd.py
@@ -13,6 +13,8 @@ class Systemd(Bcfg2.Client.Tools.SvcTool):
__handles__ = [('Service', 'systemd')]
__req__ = {'Service': ['name', 'status']}
+ conflicts = ['Chkconfig']
+
def get_svc_command(self, service, action):
return "/bin/systemctl %s %s.service" % (action, service.get('name'))
diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py
index c5a5ee4d6..11fe55bd6 100644
--- a/src/lib/Bcfg2/Client/Tools/__init__.py
+++ b/src/lib/Bcfg2/Client/Tools/__init__.py
@@ -519,6 +519,22 @@ class SvcTool(Tool):
"""
return '/etc/init.d/%s %s' % (service.get('name'), action)
+ def get_bootstatus(self, service):
+ """ Return the bootstatus attribute if it exists.
+
+ :param service: The service entry
+ :type service: lxml.etree._Element
+ :returns: string or None - Value of bootstatus if it exists. If
+ bootstatus is unspecified and status is not *ignore*,
+ return value of status. If bootstatus is unspecified
+ and status is *ignore*, return None.
+ """
+ if service.get('bootstatus') is not None:
+ return service.get('bootstatus')
+ elif service.get('status') != 'ignore':
+ return service.get('status')
+ return None
+
def start_service(self, service):
""" Start a service.
diff --git a/src/lib/Bcfg2/Client/__init__.py b/src/lib/Bcfg2/Client/__init__.py
index e40ef750b..3bc261f2f 100644
--- a/src/lib/Bcfg2/Client/__init__.py
+++ b/src/lib/Bcfg2/Client/__init__.py
@@ -19,7 +19,7 @@ def prompt(msg):
while len(select.select([sys.stdin.fileno()], [], [], 0.0)[0]) > 0:
os.read(sys.stdin.fileno(), 4096)
try:
- ans = input(msg.encode(sys.stdout.encoding, 'replace'))
+ ans = input(msg)
return ans in ['y', 'Y']
except EOFError:
# python 2.4.3 on CentOS doesn't like ^C for some reason
diff --git a/src/lib/Bcfg2/Compat.py b/src/lib/Bcfg2/Compat.py
index 4e9239e26..d034c0777 100644
--- a/src/lib/Bcfg2/Compat.py
+++ b/src/lib/Bcfg2/Compat.py
@@ -89,11 +89,28 @@ def u_str(string, encoding=None):
else:
return unicode(string)
+try:
+ from functools import wraps
+except ImportError:
+ def wraps(wrapped): # pylint: disable=W0613
+ """ implementation of functools.wraps() for python 2.4 """
+ return lambda f: f
+
+
# base64 compat
if sys.hexversion >= 0x03000000:
from base64 import b64encode as _b64encode, b64decode as _b64decode
- b64encode = lambda s: _b64encode(s.encode('UTF-8')).decode('UTF-8')
- b64decode = lambda s: _b64decode(s.encode('UTF-8')).decode('UTF-8')
+
+ @wraps(_b64encode)
+ def b64encode(val, **kwargs): # pylint: disable=C0111
+ try:
+ return _b64encode(val, **kwargs)
+ except TypeError:
+ return _b64encode(val.encode('UTF-8'), **kwargs).decode('UTF-8')
+
+ @wraps(_b64decode)
+ def b64decode(val, **kwargs): # pylint: disable=C0111
+ return _b64decode(val.encode('UTF-8'), **kwargs).decode('UTF-8')
else:
from base64 import b64encode, b64decode
@@ -242,14 +259,6 @@ except ImportError:
from md5 import md5
-try:
- from functools import wraps
-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
diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py
index 55f799a29..243c4ed2a 100644
--- a/src/lib/Bcfg2/Options.py
+++ b/src/lib/Bcfg2/Options.py
@@ -535,6 +535,11 @@ SERVER_FAM_IGNORE = \
'SCCS', '.svn', '4913', '.gitignore'],
cf=('server', 'ignore_files'),
cook=list_split)
+SERVER_FAM_BLOCK = \
+ Option('FAM blocks on startup until all events are processed',
+ default=False,
+ cook=get_bool,
+ cf=('server', 'fam_blocking'))
SERVER_LISTEN_ALL = \
Option('Listen on all interfaces',
default=False,
@@ -574,7 +579,7 @@ SERVER_PASSWORD = \
SERVER_PROTOCOL = \
Option('Server Protocol',
default='xmlrpc/ssl',
- cf=('communication', 'procotol'))
+ cf=('communication', 'protocol'))
SERVER_BACKEND = \
Option('Server Backend',
default='best',
@@ -604,6 +609,16 @@ SERVER_AUTHENTICATION = \
default='cert+password',
odesc='{cert|bootstrap|cert+password}',
cf=('communication', 'authentication'))
+SERVER_CHILDREN = \
+ Option('Spawn this number of children for the multiprocessing core. '
+ 'By default spawns children equivalent to the number of processors '
+ 'in the machine.',
+ default=None,
+ cmd='--children',
+ odesc='<children>',
+ cf=('server', 'children'),
+ cook=get_int,
+ long_arg=True)
# database options
DB_ENGINE = \
@@ -1166,6 +1181,7 @@ SERVER_COMMON_OPTIONS = dict(repo=SERVER_REPOSITORY,
password=SERVER_PASSWORD,
filemonitor=SERVER_FILEMONITOR,
ignore=SERVER_FAM_IGNORE,
+ fam_blocking=SERVER_FAM_BLOCK,
location=SERVER_LOCATION,
key=SERVER_KEY,
cert=SERVER_CERT,
@@ -1176,7 +1192,8 @@ SERVER_COMMON_OPTIONS = dict(repo=SERVER_REPOSITORY,
vcs_root=SERVER_VCS_ROOT,
authentication=SERVER_AUTHENTICATION,
perflog=LOG_PERFORMANCE,
- perflog_interval=PERFLOG_INTERVAL)
+ perflog_interval=PERFLOG_INTERVAL,
+ children=SERVER_CHILDREN)
CRYPT_OPTIONS = dict(encrypt=ENCRYPT,
decrypt=DECRYPT,
diff --git a/src/lib/Bcfg2/Proxy.py b/src/lib/Bcfg2/Proxy.py
index b2b9fcc2e..f6db66a93 100644
--- a/src/lib/Bcfg2/Proxy.py
+++ b/src/lib/Bcfg2/Proxy.py
@@ -24,6 +24,7 @@ from Bcfg2.Compat import httplib, xmlrpclib, urlparse, quote_plus
version = sys.version_info[:2]
has_py26 = version >= (2, 6)
+has_py32 = version >= (3, 2)
__all__ = ["ComponentProxy",
"RetryMethod",
@@ -173,8 +174,12 @@ class SSLHTTPConnection(httplib.HTTPConnection):
"""
if not has_py26:
httplib.HTTPConnection.__init__(self, host, port, strict)
- else:
+ elif not has_py32:
httplib.HTTPConnection.__init__(self, host, port, strict, timeout)
+ else:
+ # the strict parameter is deprecated.
+ # HTTP 0.9-style "Simple Responses" are not supported anymore.
+ httplib.HTTPConnection.__init__(self, host, port, timeout=timeout)
self.key = key
self.cert = cert
self.ca = ca
@@ -309,7 +314,7 @@ class XMLRPCTransport(xmlrpclib.Transport):
errcode = response.status
errmsg = response.reason
headers = response.msg
- except (socket.error, SSL_ERROR):
+ except (socket.error, SSL_ERROR, httplib.BadStatusLine):
err = sys.exc_info()[1]
raise ProxyError(xmlrpclib.ProtocolError(host + handler,
408,
diff --git a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py
index 3b2c0ccfa..aea5e9d4b 100644
--- a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py
+++ b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py
@@ -197,7 +197,8 @@ class DjangoORM(StorageBase):
def _import_Service(self, entry, state):
return self._import_default(entry, state,
defaults=dict(status='',
- current_status=''),
+ current_status='',
+ target_status=''),
mapping=dict(status='target_status'))
def _import_SEBoolean(self, entry, state):
diff --git a/src/lib/Bcfg2/Reporting/models.py b/src/lib/Bcfg2/Reporting/models.py
index e63c180a8..598e1c6ec 100644
--- a/src/lib/Bcfg2/Reporting/models.py
+++ b/src/lib/Bcfg2/Reporting/models.py
@@ -3,7 +3,7 @@ import sys
from django.core.exceptions import ImproperlyConfigured
try:
- from django.db import models
+ from django.db import models, backend, connection
except ImproperlyConfigured:
e = sys.exc_info()[1]
print("Reports: unable to import django models: %s" % e)
@@ -26,6 +26,8 @@ TYPE_CHOICES = (
(TYPE_EXTRA, 'Extra'),
)
+_our_backend = None
+
def convert_entry_type_to_id(type_name):
"""Convert a entry type to its entry id"""
@@ -49,6 +51,22 @@ def hash_entry(entry_dict):
return hash(cPickle.dumps(dataset))
+def _quote(value):
+ """
+ Quote a string to use as a table name or column
+
+ Newer versions and various drivers require an argument
+ https://code.djangoproject.com/ticket/13630
+ """
+ global _our_backend
+ if not _our_backend:
+ try:
+ _our_backend = backend.DatabaseOperations(connection)
+ except TypeError:
+ _our_backend = backend.DatabaseOperations()
+ return _our_backend.quote_name(value)
+
+
class Client(models.Model):
"""Object representing every client we have seen stats for."""
creation = models.DateTimeField(auto_now_add=True)
@@ -77,16 +95,20 @@ class InteractionManager(models.Manager):
cursor = connection.cursor()
cfilter = "expiration is null"
- sql = 'select ri.id, x.client_id from (select client_id, MAX(timestamp) ' + \
- 'as timer from Reporting_interaction'
+ sql = 'select ri.id, x.client_id from ' + \
+ '(select client_id, MAX(timestamp) as timer from ' + \
+ _quote('Reporting_interaction')
if maxdate:
if not isinstance(maxdate, datetime):
raise ValueError('Expected a datetime object')
sql = sql + " where timestamp <= '%s' " % maxdate
cfilter = "(expiration is null or expiration > '%s') and creation <= '%s'" % (maxdate, maxdate)
- sql = sql + ' GROUP BY client_id) x, Reporting_interaction ri where ' + \
- 'ri.client_id = x.client_id AND ri.timestamp = x.timer'
- sql = sql + " and x.client_id in (select id from Reporting_client where %s)" % cfilter
+ sql = sql + ' GROUP BY client_id) x, ' + \
+ _quote('Reporting_interaction') + \
+ ' ri where ri.client_id = x.client_id AND' + \
+ ' ri.timestamp = x.timer and x.client_id in' + \
+ ' (select id from %s where %s)' % \
+ (_quote('Reporting_client'), cfilter)
try:
cursor.execute(sql)
return [item[0] for item in cursor.fetchall()]
@@ -95,7 +117,6 @@ class InteractionManager(models.Manager):
pass
return []
-
def recent(self, maxdate=None):
"""
Returns the most recent interactions for clients as of a date
@@ -115,7 +136,7 @@ class Interaction(models.Model):
timestamp = models.DateTimeField(db_index=True) # Timestamp for this record
state = models.CharField(max_length=32) # good/bad/modified/etc
repo_rev_code = models.CharField(max_length=64) # repo revision at time of interaction
- server = models.CharField(max_length=256) # Name of the server used for the interaction
+ server = models.CharField(max_length=256) # server used for interaction
good_count = models.IntegerField() # of good config-items
total_count = models.IntegerField() # of total config-items
bad_count = models.IntegerField(default=0)
@@ -211,18 +232,24 @@ class Interaction(models.Model):
def bad(self):
rv = []
for entry in self.entry_types:
+ if entry == 'failures':
+ continue
rv.extend(getattr(self, entry).filter(state=TYPE_BAD))
return rv
def modified(self):
rv = []
for entry in self.entry_types:
+ if entry == 'failures':
+ continue
rv.extend(getattr(self, entry).filter(state=TYPE_MODIFIED))
return rv
def extra(self):
rv = []
for entry in self.entry_types:
+ if entry == 'failures':
+ continue
rv.extend(getattr(self, entry).filter(state=TYPE_EXTRA))
return rv
@@ -234,7 +261,8 @@ class Interaction(models.Model):
class Performance(models.Model):
"""Object representing performance data for any interaction."""
- interaction = models.ForeignKey(Interaction, related_name="performance_items")
+ interaction = models.ForeignKey(Interaction,
+ related_name="performance_items")
metric = models.CharField(max_length=128)
value = models.DecimalField(max_digits=32, decimal_places=16)
@@ -267,11 +295,11 @@ class Group(models.Model):
class Meta:
ordering = ('name',)
-
@staticmethod
def prune_orphans():
'''Prune unused groups'''
- Group.objects.filter(interaction__isnull=True, group__isnull=True).delete()
+ Group.objects.filter(interaction__isnull=True,
+ group__isnull=True).delete()
class Bundle(models.Model):
@@ -289,11 +317,11 @@ class Bundle(models.Model):
class Meta:
ordering = ('name',)
-
@staticmethod
def prune_orphans():
'''Prune unused bundles'''
- Bundle.objects.filter(interaction__isnull=True, group__isnull=True).delete()
+ Bundle.objects.filter(interaction__isnull=True,
+ group__isnull=True).delete()
# new interaction models
@@ -396,7 +424,7 @@ class BaseEntry(models.Model):
def prune_orphans(cls):
'''Remove unused entries'''
# yeat another sqlite hack
- cls_orphans = [x['id'] \
+ cls_orphans = [x['id']
for x in cls.objects.filter(interaction__isnull=True).values("id")]
i = 0
while i < len(cls_orphans):
@@ -671,7 +699,7 @@ class PathEntry(SuccessEntry):
acls = models.ManyToManyField(FileAcl)
detail_type = models.IntegerField(default=0,
- choices=DETAIL_CHOICES)
+ choices=DETAIL_CHOICES)
details = models.TextField(default='')
ENTRY_TYPE = r"Path"
diff --git a/src/lib/Bcfg2/SSLServer.py b/src/lib/Bcfg2/SSLServer.py
index 990bcd512..316c2f86c 100644
--- a/src/lib/Bcfg2/SSLServer.py
+++ b/src/lib/Bcfg2/SSLServer.py
@@ -281,7 +281,10 @@ class XMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
raise
except socket.error:
err = sys.exc_info()[1]
- if err[0] == 32:
+ if isinstance(err, socket.timeout):
+ self.logger.warning("Connection timed out for %s" %
+ self.client_address[0])
+ elif err[0] == 32:
self.logger.warning("Connection dropped from %s" %
self.client_address[0])
elif err[0] == 104:
@@ -337,7 +340,7 @@ class XMLRPCServer(SocketServer.ThreadingMixIn, SSLServer,
:param register: Presence should be reported to service-location
:type register: bool
:param allow_none: Allow None values in XML-RPC
- :type allow_non: bool
+ :type allow_none: bool
:param encoding: Encoding to use for XML-RPC
"""
@@ -414,7 +417,9 @@ class XMLRPCServer(SocketServer.ThreadingMixIn, SSLServer,
def serve_forever(self):
"""Serve single requests until (self.serve == False)."""
self.serve = True
- self.task_thread = threading.Thread(target=self._tasks_thread)
+ self.task_thread = \
+ threading.Thread(name="%sThread" % self.__class__.__name__,
+ target=self._tasks_thread)
self.task_thread.start()
self.logger.info("serve_forever() [start]")
signal.signal(signal.SIGINT, self._handle_shutdown_signal)
diff --git a/src/lib/Bcfg2/Server/Admin/Client.py b/src/lib/Bcfg2/Server/Admin/Client.py
index b7916fab9..187ccfd71 100644
--- a/src/lib/Bcfg2/Server/Admin/Client.py
+++ b/src/lib/Bcfg2/Server/Admin/Client.py
@@ -8,6 +8,7 @@ from Bcfg2.Server.Plugin import MetadataConsistencyError
class Client(Bcfg2.Server.Admin.MetadataCore):
""" Create, delete, or list client entries """
__usage__ = "[options] [add|del|list] [attr=val]"
+ __plugin_whitelist__ = ["Metadata"]
def __call__(self, args):
if len(args) == 0:
@@ -17,19 +18,15 @@ class Client(Bcfg2.Server.Admin.MetadataCore):
try:
self.metadata.add_client(args[1])
except MetadataConsistencyError:
- err = sys.exc_info()[1]
- print("Error in adding client: %s" % err)
- raise SystemExit(1)
+ self.errExit("Error in adding client: %s" % sys.exc_info()[1])
elif args[0] in ['delete', 'remove', 'del', 'rm']:
try:
self.metadata.remove_client(args[1])
except MetadataConsistencyError:
- err = sys.exc_info()[1]
- print("Error in deleting client: %s" % err)
- raise SystemExit(1)
+ self.errExit("Error in deleting client: %s" %
+ sys.exc_info()[1])
elif args[0] in ['list', 'ls']:
for client in self.metadata.list_clients():
print(client)
else:
- print("No command specified")
- raise SystemExit(1)
+ self.errExit("No command specified")
diff --git a/src/lib/Bcfg2/Server/Admin/Compare.py b/src/lib/Bcfg2/Server/Admin/Compare.py
index c56dd0a8f..e3648a6d0 100644
--- a/src/lib/Bcfg2/Server/Admin/Compare.py
+++ b/src/lib/Bcfg2/Server/Admin/Compare.py
@@ -145,5 +145,4 @@ class Compare(Bcfg2.Server.Admin.Mode):
(old, new) = args
return self.compareSpecifications(new, old)
except IndexError:
- print(self.__call__.__doc__)
- raise SystemExit(1)
+ self.errExit(self.__call__.__doc__)
diff --git a/src/lib/Bcfg2/Server/Admin/Minestruct.py b/src/lib/Bcfg2/Server/Admin/Minestruct.py
index 93e42305c..37ca74894 100644
--- a/src/lib/Bcfg2/Server/Admin/Minestruct.py
+++ b/src/lib/Bcfg2/Server/Admin/Minestruct.py
@@ -20,9 +20,8 @@ class Minestruct(Bcfg2.Server.Admin.StructureMode):
"Please see bcfg2-admin minestruct help for usage.")
try:
(opts, args) = getopt.getopt(args, 'f:g:h')
- except:
- self.log.error(self.__doc__)
- raise SystemExit(1)
+ except getopt.GetoptError:
+ self.errExit(self.__doc__)
client = args[0]
output = sys.stdout
@@ -33,8 +32,7 @@ class Minestruct(Bcfg2.Server.Admin.StructureMode):
try:
output = open(optarg, 'w')
except IOError:
- self.log.error("Failed to open file: %s" % (optarg))
- raise SystemExit(1)
+ self.errExit("Failed to open file: %s" % (optarg))
elif opt == '-g':
groups = optarg.split(':')
@@ -43,10 +41,9 @@ class Minestruct(Bcfg2.Server.Admin.StructureMode):
for source in self.bcore.plugins_by_type(PullSource):
for item in source.GetExtra(client):
extra.add(item)
- except:
- self.log.error("Failed to find extra entry info for client %s" %
- client)
- raise SystemExit(1)
+ except: # pylint: disable=W0702
+ self.errExit("Failed to find extra entry info for client %s" %
+ client)
root = lxml.etree.Element("Base")
self.log.info("Found %d extra entries" % (len(extra)))
add_point = root
diff --git a/src/lib/Bcfg2/Server/Admin/Pull.py b/src/lib/Bcfg2/Server/Admin/Pull.py
index 8001425df..459fcec65 100644
--- a/src/lib/Bcfg2/Server/Admin/Pull.py
+++ b/src/lib/Bcfg2/Server/Admin/Pull.py
@@ -32,9 +32,8 @@ class Pull(Bcfg2.Server.Admin.MetadataCore):
use_stdin = False
try:
opts, gargs = getopt.getopt(args, 'vfIs')
- except:
- print(self.__doc__)
- raise SystemExit(1)
+ except getopt.GetoptError:
+ self.errExit(self.__doc__)
for opt in opts:
if opt[0] == '-v':
self.log = True
diff --git a/src/lib/Bcfg2/Server/Admin/Reports.py b/src/lib/Bcfg2/Server/Admin/Reports.py
index 6e313e84b..849df8025 100644
--- a/src/lib/Bcfg2/Server/Admin/Reports.py
+++ b/src/lib/Bcfg2/Server/Admin/Reports.py
@@ -79,8 +79,7 @@ class Reports(Bcfg2.Server.Admin.Mode):
def __call__(self, args):
if len(args) == 0 or args[0] == '-h':
- print(self.__usage__)
- raise SystemExit(0)
+ self.errExit(self.__usage__)
# FIXME - dry run
@@ -101,9 +100,7 @@ class Reports(Bcfg2.Server.Admin.Mode):
management.call_command("syncdb", verbosity=vrb)
management.call_command("migrate", verbosity=vrb)
except:
- print("Update failed: %s" %
- traceback.format_exc().splitlines()[-1])
- raise SystemExit(1)
+ self.errExit("Update failed: %s" % sys.exc_info()[1])
elif args[0] == 'purge':
expired = False
client = None
@@ -124,22 +121,20 @@ class Reports(Bcfg2.Server.Admin.Mode):
maxdate = datetime.datetime.now() - \
datetime.timedelta(days=int(args[i + 1]))
except:
- self.log.error("Invalid number of days: %s" %
- args[i + 1])
- raise SystemExit(-1)
+ self.errExit("Invalid number of days: %s" %
+ args[i + 1])
i = i + 1
elif args[i] == '--expired':
expired = True
i = i + 1
if expired:
if state:
- self.log.error("--state is not valid with --expired")
- raise SystemExit(-1)
+ self.errExit("--state is not valid with --expired")
self.purge_expired(maxdate)
else:
self.purge(client, maxdate, state)
else:
- print("Unknown command: %s" % args[0])
+ self.errExit("Unknown command: %s" % args[0])
@transaction.commit_on_success
def scrub(self):
@@ -155,8 +150,7 @@ class Reports(Bcfg2.Server.Admin.Mode):
(start_count - cls.objects.count(), cls.__class__.__name__))
except:
print("Failed to prune %s: %s" %
- (cls.__class__.__name__,
- traceback.format_exc().splitlines()[-1]))
+ (cls.__class__.__name__, sys.exc_info()[1]))
def django_command_proxy(self, command):
'''Call a django command'''
@@ -180,8 +174,7 @@ class Reports(Bcfg2.Server.Admin.Mode):
cobj = Client.objects.get(name=client)
ipurge = ipurge.filter(client=cobj)
except Client.DoesNotExist:
- self.log.error("Client %s not in database" % client)
- raise SystemExit(-1)
+ self.errExit("Client %s not in database" % client)
self.log.debug("Filtering by client: %s" % client)
if maxdate:
diff --git a/src/lib/Bcfg2/Server/Admin/Syncdb.py b/src/lib/Bcfg2/Server/Admin/Syncdb.py
index 4ba840b86..53cfd1bec 100644
--- a/src/lib/Bcfg2/Server/Admin/Syncdb.py
+++ b/src/lib/Bcfg2/Server/Admin/Syncdb.py
@@ -22,10 +22,7 @@ class Syncdb(Bcfg2.Server.Admin.Mode):
call_command("syncdb", interactive=False, verbosity=0)
self._database_available = True
except ImproperlyConfigured:
- err = sys.exc_info()[1]
- self.log.error("Django configuration problem: %s" % err)
- raise SystemExit(1)
+ self.errExit("Django configuration problem: %s" %
+ sys.exc_info()[1])
except:
- err = sys.exc_info()[1]
- self.log.error("Database update failed: %s" % err)
- raise SystemExit(1)
+ self.errExit("Database update failed: %s" % sys.exc_info()[1])
diff --git a/src/lib/Bcfg2/Server/Admin/Xcmd.py b/src/lib/Bcfg2/Server/Admin/Xcmd.py
index be556bed4..036129a1b 100644
--- a/src/lib/Bcfg2/Server/Admin/Xcmd.py
+++ b/src/lib/Bcfg2/Server/Admin/Xcmd.py
@@ -4,7 +4,6 @@ import sys
import Bcfg2.Options
import Bcfg2.Proxy
import Bcfg2.Server.Admin
-from Bcfg2.Compat import xmlrpclib
class Xcmd(Bcfg2.Server.Admin.Mode):
@@ -31,27 +30,15 @@ class Xcmd(Bcfg2.Server.Admin.Mode):
ca=setup['ca'],
timeout=setup['timeout'])
if len(setup['args']) == 0:
- print("Usage: xcmd <xmlrpc method> <optional arguments>")
- return
+ self.errExit("Usage: xcmd <xmlrpc method> <optional arguments>")
cmd = setup['args'][0]
args = ()
if len(setup['args']) > 1:
args = tuple(setup['args'][1:])
try:
data = getattr(proxy, cmd)(*args)
- except xmlrpclib.Fault:
- flt = sys.exc_info()[1]
- if flt.faultCode == 7:
- print("Unknown method %s" % cmd)
- return
- elif flt.faultCode == 20:
- return
- else:
- raise
except Bcfg2.Proxy.ProxyError:
- err = sys.exc_info()[1]
- print("Proxy Error: %s" % err)
- return
+ self.errExit("Proxy Error: %s" % sys.exc_info()[1])
if data is not None:
print(data)
diff --git a/src/lib/Bcfg2/Server/Admin/__init__.py b/src/lib/Bcfg2/Server/Admin/__init__.py
index 7bba05eb3..8f12a940e 100644
--- a/src/lib/Bcfg2/Server/Admin/__init__.py
+++ b/src/lib/Bcfg2/Server/Admin/__init__.py
@@ -128,6 +128,7 @@ class MetadataCore(Mode):
except Bcfg2.Server.Core.CoreInitError:
msg = sys.exc_info()[1]
self.errExit("Core load failed: %s" % msg)
+ self.bcore.load_plugins()
self.bcore.fam.handle_event_set()
self.metadata = self.bcore.metadata
diff --git a/src/lib/Bcfg2/Server/BuiltinCore.py b/src/lib/Bcfg2/Server/BuiltinCore.py
index c3302f1d0..e69a92b64 100644
--- a/src/lib/Bcfg2/Server/BuiltinCore.py
+++ b/src/lib/Bcfg2/Server/BuiltinCore.py
@@ -117,11 +117,11 @@ class Core(BaseCore):
self.logger.error("Server startup failed: %s" % err)
self.context.close()
return False
- self.server.register_instance(self)
return True
def _block(self):
""" Enter the blocking infinite loop. """
+ self.server.register_instance(self)
try:
self.server.serve_forever()
finally:
diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py
index 59d67e566..6e0d38418 100644
--- a/src/lib/Bcfg2/Server/Core.py
+++ b/src/lib/Bcfg2/Server/Core.py
@@ -301,6 +301,7 @@ class BaseCore(object):
self.logger.info("Performance statistics: "
"%s min=%.06f, max=%.06f, average=%.06f, "
"count=%d" % ((name, ) + stats))
+ self.logger.debug("Performance logging thread terminated")
def _file_monitor_thread(self):
""" The thread that runs the
@@ -321,6 +322,7 @@ class BaseCore(object):
except:
continue
self._update_vcs_revision()
+ self.logger.debug("File monitor thread terminated")
@track_statistics()
def _update_vcs_revision(self):
@@ -440,8 +442,10 @@ class BaseCore(object):
if not self.terminate.isSet():
self.terminate.set()
self.fam.shutdown()
+ self.logger.debug("FAM shut down")
for plugin in list(self.plugins.values()):
plugin.shutdown()
+ self.logger.debug("All plugins shut down")
@property
def metadata_cache_mode(self):
@@ -776,7 +780,13 @@ class BaseCore(object):
self.shutdown()
raise
- self.set_debug(None, self.debug_flag)
+ if self.setup['fam_blocking']:
+ time.sleep(1)
+ while self.fam.pending() != 0:
+ time.sleep(1)
+
+ if self.debug_flag:
+ self.set_debug(None, self.debug_flag)
self._block()
def _daemonize(self):
diff --git a/src/lib/Bcfg2/Server/Lint/Comments.py b/src/lib/Bcfg2/Server/Lint/Comments.py
index 8bfb76461..7c3b2d9cc 100644
--- a/src/lib/Bcfg2/Server/Lint/Comments.py
+++ b/src/lib/Bcfg2/Server/Lint/Comments.py
@@ -1,8 +1,9 @@
-""" check files for various required comments """
+""" Check files for various required comments. """
import os
import lxml.etree
import Bcfg2.Server.Lint
+from Bcfg2.Server import XI_NAMESPACE
from Bcfg2.Server.Plugins.Cfg.CfgPlaintextGenerator \
import CfgPlaintextGenerator
from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator
@@ -11,7 +12,10 @@ from Bcfg2.Server.Plugins.Cfg.CfgInfoXML import CfgInfoXML
class Comments(Bcfg2.Server.Lint.ServerPlugin):
- """ check files for various required headers """
+ """ The Comments lint plugin checks files for header comments that
+ give information about the files. For instance, you can require
+ SVN keywords in a comment, or require the name of the maintainer
+ of a Genshi template, and so on. """
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
self.config_cache = {}
@@ -27,21 +31,43 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
def Errors(cls):
return {"unexpanded-keywords": "warning",
"keywords-not-found": "warning",
- "comments-not-found": "warning"}
+ "comments-not-found": "warning",
+ "broken-xinclude-chain": "warning"}
def required_keywords(self, rtype):
- """ given a file type, fetch the list of required VCS keywords
- from the bcfg2-lint config """
+ """ Given a file type, fetch the list of required VCS keywords
+ from the bcfg2-lint config. Valid file types are documented
+ in :manpage:`bcfg2-lint.conf(5)`.
+
+ :param rtype: The file type
+ :type rtype: string
+ :returns: list - the required items
+ """
return self.required_items(rtype, "keyword")
def required_comments(self, rtype):
- """ given a file type, fetch the list of required comments
- from the bcfg2-lint config """
+ """ Given a file type, fetch the list of required comments
+ from the bcfg2-lint config. Valid file types are documented
+ in :manpage:`bcfg2-lint.conf(5)`.
+
+ :param rtype: The file type
+ :type rtype: string
+ :returns: list - the required items
+ """
return self.required_items(rtype, "comment")
def required_items(self, rtype, itype):
- """ given a file type and item type (comment or keyword),
- fetch the list of required items from the bcfg2-lint config """
+ """ Given a file type and item type (``comment`` or
+ ``keyword``), fetch the list of required items from the
+ bcfg2-lint config. Valid file types are documented in
+ :manpage:`bcfg2-lint.conf(5)`.
+
+ :param rtype: The file type
+ :type rtype: string
+ :param itype: The item type (``comment`` or ``keyword``)
+ :type itype: string
+ :returns: list - the required items
+ """
if itype not in self.config_cache:
self.config_cache[itype] = {}
@@ -62,7 +88,7 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
return self.config_cache[itype][rtype]
def check_bundles(self):
- """ check bundle files for required headers """
+ """ Check bundle files for required comments. """
if 'Bundler' in self.core.plugins:
for bundle in self.core.plugins['Bundler'].entries.values():
xdata = None
@@ -78,15 +104,41 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
self.check_xml(bundle.name, xdata, rtype)
def check_properties(self):
- """ check properties files for required headers """
+ """ Check Properties files for required comments. """
if 'Properties' in self.core.plugins:
props = self.core.plugins['Properties']
- for propfile, pdata in props.store.entries.items():
+ for propfile, pdata in props.entries.items():
if os.path.splitext(propfile)[1] == ".xml":
self.check_xml(pdata.name, pdata.xdata, 'properties')
+ def has_all_xincludes(self, mfile):
+ """ Return True if :attr:`Bcfg2.Server.Lint.Plugin.files`
+ includes all XIncludes listed in the specified metadata type,
+ false otherwise. In other words, this returns True if
+ bcfg2-lint is dealing with complete metadata.
+
+ :param mfile: The metadata file ("clients.xml" or
+ "groups.xml") to check for XIncludes
+ :type mfile: string
+ :returns: bool
+ """
+ if self.files is None:
+ return True
+ else:
+ path = os.path.join(self.metadata.data, mfile)
+ if path in self.files:
+ xdata = lxml.etree.parse(path)
+ for el in xdata.findall('./%sinclude' % XI_NAMESPACE):
+ if not self.has_all_xincludes(el.get('href')):
+ self.LintError("broken-xinclude-chain",
+ "Broken XInclude chain: could not "
+ "include %s" % path)
+ return False
+
+ return True
+
def check_metadata(self):
- """ check metadata files for required headers """
+ """ Check Metadata files for required comments. """
if self.has_all_xincludes("groups.xml"):
self.check_xml(os.path.join(self.metadata.data, "groups.xml"),
self.metadata.groups_xml.data,
@@ -97,7 +149,8 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
"metadata")
def check_cfg(self):
- """ check Cfg files and info.xml files for required headers """
+ """ Check Cfg files and ``info.xml`` files for required
+ comments. """
if 'Cfg' in self.core.plugins:
for entryset in self.core.plugins['Cfg'].entries.values():
for entry in entryset.entries.values():
@@ -117,29 +170,57 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
self.check_plaintext(entry.name, entry.data, rtype)
def check_probes(self):
- """ check probes for required headers """
+ """ Check Probes for required comments """
if 'Probes' in self.core.plugins:
for probe in self.core.plugins['Probes'].probes.entries.values():
self.check_plaintext(probe.name, probe.data, "probes")
def check_xml(self, filename, xdata, rtype):
- """ check generic XML files for required headers """
+ """ Generic check to check an XML file for required comments.
+
+ :param filename: The filename
+ :type filename: string
+ :param xdata: The file data
+ :type xdata: lxml.etree._Element
+ :param rtype: The type of file. Available types are
+ documented in :manpage:`bcfg2-lint.conf(5)`.
+ :type rtype: string
+ """
self.check_lines(filename,
[str(el)
for el in xdata.getiterator(lxml.etree.Comment)],
rtype)
def check_plaintext(self, filename, data, rtype):
- """ check generic plaintext files for required headers """
+ """ Generic check to check a plain text file for required
+ comments.
+
+ :param filename: The filename
+ :type filename: string
+ :param data: The file data
+ :type data: string
+ :param rtype: The type of file. Available types are
+ documented in :manpage:`bcfg2-lint.conf(5)`.
+ :type rtype: string
+ """
self.check_lines(filename, data.splitlines(), rtype)
def check_lines(self, filename, lines, rtype):
- """ generic header check for a set of lines """
+ """ Generic header check for a set of lines.
+
+ :param filename: The filename
+ :type filename: string
+ :param lines: The data to check
+ :type lines: list of strings
+ :param rtype: The type of file. Available types are
+ documented in :manpage:`bcfg2-lint.conf(5)`.
+ :type rtype: string
+ """
if self.HandlesFile(filename):
# found is trivalent:
- # False == not found
- # None == found but not expanded
- # True == found and expanded
+ # False == keyword not found
+ # None == keyword found but not expanded
+ # True == keyword found and expanded
found = dict((k, False) for k in self.required_keywords(rtype))
for line in lines:
diff --git a/src/lib/Bcfg2/Server/Lint/Genshi.py b/src/lib/Bcfg2/Server/Lint/Genshi.py
index c045c2ca2..1ecb6da42 100755
--- a/src/lib/Bcfg2/Server/Lint/Genshi.py
+++ b/src/lib/Bcfg2/Server/Lint/Genshi.py
@@ -1,4 +1,4 @@
-""" Check Genshi templates for syntax errors """
+""" Check Genshi templates for syntax errors. """
import sys
import Bcfg2.Server.Lint
@@ -9,10 +9,9 @@ from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator
class Genshi(Bcfg2.Server.Lint.ServerPlugin):
- """ Check Genshi templates for syntax errors """
+ """ Check Genshi templates for syntax errors. """
def Run(self):
- """ run plugin """
if 'Cfg' in self.core.plugins:
self.check_cfg()
if 'TGenshi' in self.core.plugins:
@@ -25,7 +24,7 @@ class Genshi(Bcfg2.Server.Lint.ServerPlugin):
return {"genshi-syntax-error": "error"}
def check_cfg(self):
- """ Check genshi templates in Cfg for syntax errors """
+ """ Check genshi templates in Cfg for syntax errors. """
for entryset in self.core.plugins['Cfg'].entries.values():
for entry in entryset.entries.values():
if (self.HandlesFile(entry.name) and
@@ -38,9 +37,15 @@ class Genshi(Bcfg2.Server.Lint.ServerPlugin):
err = sys.exc_info()[1]
self.LintError("genshi-syntax-error",
"Genshi syntax error: %s" % err)
+ except:
+ etype, err = sys.exc_info()[:2]
+ self.LintError(
+ "genshi-syntax-error",
+ "Unexpected Genshi error on %s: %s: %s" %
+ (entry.name, etype.__name__, err))
def check_tgenshi(self):
- """ Check templates in TGenshi for syntax errors """
+ """ Check templates in TGenshi for syntax errors. """
loader = TemplateLoader()
for eset in self.core.plugins['TGenshi'].entries.values():
@@ -54,7 +59,7 @@ class Genshi(Bcfg2.Server.Lint.ServerPlugin):
"Genshi syntax error: %s" % err)
def check_bundler(self):
- """ Check templates in Bundler for syntax errors """
+ """ Check templates in Bundler for syntax errors. """
loader = TemplateLoader()
for entry in self.core.plugins['Bundler'].entries.values():
diff --git a/src/lib/Bcfg2/Server/Lint/GroupNames.py b/src/lib/Bcfg2/Server/Lint/GroupNames.py
index 52e42aa7b..b180083d5 100644
--- a/src/lib/Bcfg2/Server/Lint/GroupNames.py
+++ b/src/lib/Bcfg2/Server/Lint/GroupNames.py
@@ -1,4 +1,4 @@
-""" ensure that all named groups are valid group names """
+""" Ensure that all named groups are valid group names. """
import os
import re
@@ -11,8 +11,15 @@ except ImportError:
class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
- """ ensure that all named groups are valid group names """
+ """ Ensure that all named groups are valid group names. """
+
+ #: A string regex that matches only valid group names. Currently,
+ #: a group name is considered valid if it contains only
+ #: non-whitespace characters.
pattern = r'\S+$'
+
+ #: A compiled regex for
+ #: :attr:`Bcfg2.Server.Lint.GroupNames.GroupNames.pattern`
valid = re.compile(r'^' + pattern)
def Run(self):
@@ -31,7 +38,7 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
return {"invalid-group-name": "error"}
def check_rules(self):
- """ Check groups used in the Rules plugin for validity """
+ """ Check groups used in the Rules plugin for validity. """
for rules in self.core.plugins['Rules'].entries.values():
if not self.HandlesFile(rules.name):
continue
@@ -40,7 +47,7 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
os.path.join(self.config['repo'], rules.name))
def check_bundles(self):
- """ Check groups used in the Bundler plugin for validity """
+ """ Check groups used in the Bundler plugin for validity. """
for bundle in self.core.plugins['Bundler'].entries.values():
if (self.HandlesFile(bundle.name) and
(not HAS_GENSHI or
@@ -50,7 +57,7 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
def check_metadata(self):
""" Check groups used or declared in the Metadata plugin for
- validity """
+ validity. """
self.check_entries(self.metadata.groups_xml.xdata.xpath("//Group"),
os.path.join(self.config['repo'],
self.metadata.groups_xml.name))
@@ -68,7 +75,7 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
def check_cfg(self):
""" Check groups used in group-specific files in the Cfg
- plugin for validity """
+ plugin for validity. """
for root, _, files in os.walk(self.core.plugins['Cfg'].data):
for fname in files:
basename = os.path.basename(root)
@@ -81,7 +88,14 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
def check_entries(self, entries, fname):
""" Check a generic list of XML entries for <Group> tags with
- invalid name attributes """
+ invalid name attributes.
+
+ :param entries: A list of XML <Group> tags whose ``name``
+ attributes will be validated.
+ :type entries: list of lxml.etree._Element
+ :param fname: The filename the entry list came from
+ :type fname: string
+ """
for grp in entries:
if not self.valid.search(grp.get("name")):
self.LintError("invalid-group-name",
diff --git a/src/lib/Bcfg2/Server/Lint/InfoXML.py b/src/lib/Bcfg2/Server/Lint/InfoXML.py
index e34f387ff..95657317e 100644
--- a/src/lib/Bcfg2/Server/Lint/InfoXML.py
+++ b/src/lib/Bcfg2/Server/Lint/InfoXML.py
@@ -1,4 +1,4 @@
-""" ensure that all config files have an info.xml file"""
+""" Ensure that all config files have a valid info.xml file. """
import os
import Bcfg2.Options
@@ -8,7 +8,14 @@ from Bcfg2.Server.Plugins.Cfg.CfgLegacyInfo import CfgLegacyInfo
class InfoXML(Bcfg2.Server.Lint.ServerPlugin):
- """ ensure that all config files have an info.xml file"""
+ """ Ensure that all config files have a valid info.xml file. This
+ plugin can check for:
+
+ * Missing ``info.xml`` files;
+ * Use of deprecated ``info``/``:info`` files;
+ * Paranoid mode disabled in an ``info.xml`` file;
+ * Required attributes missing from ``info.xml``
+ """
def Run(self):
if 'Cfg' not in self.core.plugins:
return
@@ -40,11 +47,10 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin):
return {"no-infoxml": "warning",
"deprecated-info-file": "warning",
"paranoid-false": "warning",
- "broken-xinclude-chain": "warning",
"required-infoxml-attrs-missing": "error"}
def check_infoxml(self, fname, xdata):
- """ verify that info.xml contains everything it should """
+ """ Verify that info.xml contains everything it should. """
for info in xdata.getroottree().findall("//Info"):
required = []
if "required_attrs" in self.config:
diff --git a/src/lib/Bcfg2/Server/Lint/MergeFiles.py b/src/lib/Bcfg2/Server/Lint/MergeFiles.py
index 44d02c2ff..2419c3d43 100644
--- a/src/lib/Bcfg2/Server/Lint/MergeFiles.py
+++ b/src/lib/Bcfg2/Server/Lint/MergeFiles.py
@@ -57,7 +57,7 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin):
else:
threshold = 0.75
rv = []
- elist = entries.items()
+ elist = list(entries.items())
while elist:
result = self._find_similar(elist.pop(0), copy.copy(elist),
threshold)
diff --git a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
index 40ff71dbd..6ffdd33a0 100644
--- a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
+++ b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
@@ -1,5 +1,5 @@
-""" verify attributes for configuration entries that cannot be
-verified with an XML schema alone"""
+""" Verify attributes for configuration entries that cannot be
+verified with an XML schema alone. """
import os
import re
@@ -15,7 +15,8 @@ except ImportError:
HAS_GENSHI = False
-# format verifying functions
+# format verifying functions. TODO: These should be moved into XML
+# schemas where possible.
def is_filename(val):
""" Return True if val is a string describing a valid full path
"""
@@ -53,8 +54,8 @@ def is_device_mode(val):
class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
- """ verify attributes for configuration entries that cannot be
- verified with an XML schema alone """
+ """ Verify attributes for configuration entries that cannot be
+ verified with an XML schema alone. """
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
self.required_attrs = dict(
@@ -115,8 +116,7 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
SEInterface={None: dict(name=None, selinuxtype=is_selinux_type)},
SEPermissive={None: dict(name=is_selinux_type)},
POSIXGroup={None: dict(name=is_username)},
- POSIXUser={None: dict(name=is_username)},
- MemberOf={None: dict(__text__=is_username)})
+ POSIXUser={None: dict(name=is_username)})
def Run(self):
self.check_packages()
@@ -136,7 +136,8 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
"extra-attrs": "warning"}
def check_packages(self):
- """ check package sources for Source entries with missing attrs """
+ """ Check Packages sources for Source entries with missing
+ attributes. """
if 'Packages' not in self.core.plugins:
return
@@ -176,7 +177,8 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
rules.name))
def check_bundles(self):
- """ check bundles for BoundPath entries with missing attrs """
+ """ Check bundles for BoundPath entries with missing
+ attrs. """
if 'Bundler' not in self.core.plugins:
return
@@ -195,7 +197,13 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
self.check_entry(path, bundle.name)
def check_entry(self, entry, filename):
- """ generic entry check """
+ """ Generic entry check.
+
+ :param entry: The XML entry to check for missing attributes.
+ :type entry: lxml.etree._Element
+ :param filename: The filename the entry came from
+ :type filename: string
+ """
if self.HandlesFile(filename):
name = entry.get('name')
tag = entry.tag
diff --git a/src/lib/Bcfg2/Server/Lint/Validate.py b/src/lib/Bcfg2/Server/Lint/Validate.py
index ae7c75804..09f3f3d25 100644
--- a/src/lib/Bcfg2/Server/Lint/Validate.py
+++ b/src/lib/Bcfg2/Server/Lint/Validate.py
@@ -1,4 +1,5 @@
-""" Ensure that the repo validates """
+""" Ensure that all XML files in the Bcfg2 repository validate
+according to their respective schemas. """
import os
import sys
@@ -10,10 +11,19 @@ import Bcfg2.Server.Lint
class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
- """ Ensure that the repo validates """
+ """ Ensure that all XML files in the Bcfg2 repository validate
+ according to their respective schemas. """
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerlessPlugin.__init__(self, *args, **kwargs)
+
+ #: A dict of <file glob>: <schema file> that maps files in the
+ #: Bcfg2 specification to their schemas. The globs are
+ #: extended :mod:`fnmatch` globs that also support ``**``,
+ #: which matches any number of any characters, including
+ #: forward slashes. The schema files are relative to the
+ #: schema directory, which can be controlled by the
+ #: ``bcfg2-lint --schema`` option.
self.filesets = \
{"Metadata/groups.xml": "metadata.xsd",
"Metadata/clients.xml": "clients.xsd",
@@ -76,7 +86,7 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
"input-output-error": "error"}
def check_properties(self):
- """ check Properties files against their schemas """
+ """ Check Properties files against their schemas. """
for filename in self.filelists['props']:
schemafile = "%s.xsd" % os.path.splitext(filename)[0]
if os.path.exists(schemafile):
@@ -90,7 +100,11 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
def parse(self, filename):
""" Parse an XML file, raising the appropriate LintErrors if
it can't be parsed or read. Return the
- lxml.etree._ElementTree parsed from the file. """
+ lxml.etree._ElementTree parsed from the file.
+
+ :param filename: The full path to the file to parse
+ :type filename: string
+ :returns: lxml.etree._ElementTree - the parsed data"""
try:
return lxml.etree.parse(filename)
except SyntaxError:
@@ -106,8 +120,20 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
return False
def validate(self, filename, schemafile, schema=None):
- """validate a file against the given lxml.etree.Schema.
- return True on success, False on failure """
+ """ Validate a file against the given schema.
+
+ :param filename: The full path to the file to validate
+ :type filename: string
+ :param schemafile: The full path to the schema file to
+ validate against
+ :type schemafile: string
+ :param schema: The loaded schema to validate against. This
+ can be used to avoid parsing a single schema
+ file for every file that needs to be validate
+ against it.
+ :type schema: lxml.etree.Schema
+ :returns: bool - True if the file validates, false otherwise
+ """
if schema is None:
# if no schema object was provided, instantiate one
schema = self._load_schema(schemafile)
@@ -121,6 +147,9 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
cmd.extend(["--noout", "--schema", schemafile, filename])
lint = Popen(cmd, stdout=PIPE, stderr=STDOUT)
output = lint.communicate()[0]
+ # py3k fix
+ if not isinstance(output, str):
+ output = output.decode('utf-8')
if lint.wait():
self.LintError("xml-failed-to-verify",
"%s fails to verify:\n%s" % (filename, output))
@@ -128,7 +157,14 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
return True
def get_filelists(self):
- """ get lists of different kinds of files to validate """
+ """ Get lists of different kinds of files to validate. This
+ doesn't return anything, but it sets
+ :attr:`Bcfg2.Server.Lint.Validate.Validate.filelists` to a
+ dict whose keys are path globs given in
+ :attr:`Bcfg2.Server.Lint.Validate.Validate.filesets` and whose
+ values are lists of the full paths to all files in the Bcfg2
+ repository (or given with ``bcfg2-lint --stdin``) that match
+ the glob."""
if self.files is not None:
listfiles = lambda p: fnmatch.filter(self.files,
os.path.join('*', p))
@@ -155,7 +191,13 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
self.filelists['props'] = listfiles("Properties/*.xml")
def _load_schema(self, filename):
- """ load an XML schema document, returning the Schema object """
+ """ Load an XML schema document, returning the Schema object
+ and raising appropriate lint errors on failure.
+
+ :param filename: The full path to the schema file to load.
+ :type filename: string
+ :returns: lxml.etree.Schema - The loaded schema data
+ """
try:
return lxml.etree.XMLSchema(lxml.etree.parse(filename))
except IOError:
diff --git a/src/lib/Bcfg2/Server/Lint/__init__.py b/src/lib/Bcfg2/Server/Lint/__init__.py
index 11afdd75d..28644263f 100644
--- a/src/lib/Bcfg2/Server/Lint/__init__.py
+++ b/src/lib/Bcfg2/Server/Lint/__init__.py
@@ -9,10 +9,9 @@ import lxml.etree
import fcntl
import termios
import struct
-from Bcfg2.Server import XI_NAMESPACE
from Bcfg2.Compat import walk_packages
-__all__ = [m[1] for m in walk_packages(path=__path__)]
+plugins = [m[1] for m in walk_packages(path=__path__)] # pylint: disable=C0103
def _ioctl_GWINSZ(fd): # pylint: disable=C0103
@@ -45,30 +44,56 @@ def get_termsize():
class Plugin(object):
- """ base class for ServerlessPlugin and ServerPlugin """
+ """ Base class for all bcfg2-lint plugins """
def __init__(self, config, errorhandler=None, files=None):
+ """
+ :param config: A :mod:`Bcfg2.Options` setup dict
+ :type config: dict
+ :param errorhandler: A :class:`Bcfg2.Server.Lint.ErrorHandler`
+ that will be used to handle lint errors.
+ If one is not provided, a new one will be
+ instantiated.
+ :type errorhandler: Bcfg2.Server.Lint.ErrorHandler
+ :param files: A list of files to run bcfg2-lint against. (See
+ the bcfg2-lint ``--stdin`` option.)
+ :type files: list of strings
+ """
+
+ #: The list of files that bcfg2-lint should be run against
self.files = files
+
+ #: The Bcfg2.Options setup dict
self.config = config
+
self.logger = logging.getLogger('bcfg2-lint')
if errorhandler is None:
+ #: The error handler
self.errorhandler = ErrorHandler()
else:
self.errorhandler = errorhandler
self.errorhandler.RegisterErrors(self.Errors())
def Run(self):
- """ run the plugin. must be overloaded by child classes """
- pass
+ """ Run the plugin. Must be overloaded by child classes. """
+ raise NotImplementedError
@classmethod
def Errors(cls):
- """ returns a dict of errors the plugin supplies. must be
- overloaded by child classes """
+ """ Returns a dict of errors the plugin supplies, in a format
+ suitable for passing to
+ :func:`Bcfg2.Server.Lint.ErrorHandler.RegisterErrors`.
+
+ Must be overloaded by child classes.
+
+ :returns: dict
+ """
+ raise NotImplementedError
def HandlesFile(self, fname):
- """ returns true if the given file should be handled by the
- plugin according to the files list, false otherwise """
+ """ Returns True if the given file should be handled by the
+ plugin according to :attr:`Bcfg2.Server.Lint.Plugin.files`,
+ False otherwise. """
return (self.files is None or
fname in self.files or
os.path.join(self.config['repo'], fname) in self.files or
@@ -77,12 +102,27 @@ class Plugin(object):
fname)) in self.files)
def LintError(self, err, msg):
- """ record an error in the lint process """
+ """ Raise an error from the lint process.
+
+ :param err: The name of the error being raised. This name
+ must be a key in the dict returned by
+ :func:`Bcfg2.Server.Lint.Plugin.Errors`.
+ :type err: string
+ :param msg: The freeform message to display to the end user.
+ :type msg: string
+ """
self.errorhandler.dispatch(err, msg)
def RenderXML(self, element, keep_text=False):
- """render an XML element for error output -- line number
- prefixed, no children"""
+ """ Render an XML element for error output. This prefixes the
+ line number and removes children for nicer display.
+
+ :param element: The element to render
+ :type element: lxml.etree._Element
+ :param keep_text: Do not discard text content from the element
+ for display
+ :type keep_text: boolean
+ """
xml = None
if len(element) or element.text:
el = copy(element)
@@ -100,11 +140,18 @@ class Plugin(object):
return " line %s: %s" % (element.sourceline, xml)
-class ErrorHandler (object):
- """ a class to handle errors for bcfg2-lint plugins """
+class ErrorHandler(object):
+ """ A class to handle errors for bcfg2-lint plugins """
- def __init__(self, config=None):
+ def __init__(self, errors=None):
+ """
+ :param config: An initial dict of errors to register
+ :type config: dict
+ """
+ #: The number of errors passed to this error handler
self.errors = 0
+
+ #: The number of warnings passed to this error handler
self.warnings = 0
self.logger = logging.getLogger('bcfg2-lint')
@@ -114,17 +161,25 @@ class ErrorHandler (object):
twrap = textwrap.TextWrapper(initial_indent=" ",
subsequent_indent=" ",
width=termsize[0])
+ #: A function to wrap text to the width of the terminal
self._wrapper = twrap.wrap
else:
self._wrapper = lambda s: [s]
+ #: A dict of registered errors
self.errortypes = dict()
- if config is not None:
- self.RegisterErrors(dict(config.items()))
+ if errors is not None:
+ self.RegisterErrors(dict(errors.items()))
def RegisterErrors(self, errors):
- """ Register a dict of errors (name: default level) that a
- plugin may raise """
+ """ Register a dict of errors that a plugin may raise. The
+ keys of the dict are short strings that describe each error;
+ the values are the default error handling for that error
+ ("error", "warning", or "silent").
+
+ :param errors: The error dict
+ :type errors: dict
+ """
for err, action in errors.items():
if err not in self.errortypes:
if "warn" in action:
@@ -135,7 +190,16 @@ class ErrorHandler (object):
self.errortypes[err] = self.debug
def dispatch(self, err, msg):
- """ Dispatch an error to the correct handler """
+ """ Dispatch an error to the correct handler.
+
+ :param err: The name of the error being raised. This name
+ must be a key in
+ :attr:`Bcfg2.Server.Lint.ErrorHandler.errortypes`,
+ the dict of registered errors.
+ :type err: string
+ :param msg: The freeform message to display to the end user.
+ :type msg: string
+ """
if err in self.errortypes:
self.errortypes[err](msg)
self.logger.debug(" (%s)" % err)
@@ -145,22 +209,34 @@ class ErrorHandler (object):
self.logger.warning("Unknown error %s" % err)
def error(self, msg):
- """ log an error condition """
+ """ Log an error condition.
+
+ :param msg: The freeform message to display to the end user.
+ :type msg: string
+ """
self.errors += 1
self._log(msg, self.logger.error, prefix="ERROR: ")
def warn(self, msg):
- """ log a warning condition """
+ """ Log a warning condition.
+
+ :param msg: The freeform message to display to the end user.
+ :type msg: string
+ """
self.warnings += 1
self._log(msg, self.logger.warning, prefix="WARNING: ")
def debug(self, msg):
- """ log a silent/debug condition """
+ """ Log a silent/debug condition.
+
+ :param msg: The freeform message to display to the end user.
+ :type msg: string
+ """
self._log(msg, self.logger.debug)
def _log(self, msg, logfunc, prefix=""):
""" Generic log function that logs a message with the given
- function after wrapping it for the terminal width """
+ function after wrapping it for the terminal width. """
# a message may itself consist of multiple lines. wrap() will
# elide them all into a single paragraph, which we don't want.
# so we split the message into its paragraphs and wrap each
@@ -180,37 +256,37 @@ class ErrorHandler (object):
logfunc(line)
-class ServerlessPlugin (Plugin):
- """ base class for plugins that are run before the server starts
- up (i.e., plugins that check things that may prevent the server
- from starting up) """
+class ServerlessPlugin(Plugin): # pylint: disable=W0223
+ """ Base class for bcfg2-lint plugins that are run before the
+ server starts up (i.e., plugins that check things that may prevent
+ the server from starting up). """
pass
-class ServerPlugin (Plugin):
- """ base class for plugins that check things that require the
- running Bcfg2 server """
- def __init__(self, core, config, **kwargs):
- Plugin.__init__(self, config, **kwargs)
+class ServerPlugin(Plugin): # pylint: disable=W0223
+ """ Base class for bcfg2-lint plugins that check things that
+ require the running Bcfg2 server. """
+
+ def __init__(self, core, config, errorhandler=None, files=None):
+ """
+ :param core: The Bcfg2 server core
+ :type core: Bcfg2.Server.Core.BaseCore
+ :param config: A :mod:`Bcfg2.Options` setup dict
+ :type config: dict
+ :param errorhandler: A :class:`Bcfg2.Server.Lint.ErrorHandler`
+ that will be used to handle lint errors.
+ If one is not provided, a new one will be
+ instantiated.
+ :type errorhandler: Bcfg2.Server.Lint.ErrorHandler
+ :param files: A list of files to run bcfg2-lint against. (See
+ the bcfg2-lint ``--stdin`` option.)
+ :type files: list of strings
+ """
+ Plugin.__init__(self, config, errorhandler=errorhandler, files=files)
+
+ #: The server core
self.core = core
self.logger = self.core.logger
- self.metadata = self.core.metadata
- self.errorhandler.RegisterErrors({"broken-xinclude-chain": "warning"})
- def has_all_xincludes(self, mfile):
- """ return true if self.files includes all XIncludes listed in
- the specified metadata type, false otherwise"""
- if self.files is None:
- return True
- else:
- path = os.path.join(self.metadata.data, mfile)
- if path in self.files:
- xdata = lxml.etree.parse(path)
- for el in xdata.findall('./%sinclude' % XI_NAMESPACE):
- if not self.has_all_xincludes(el.get('href')):
- self.LintError("broken-xinclude-chain",
- "Broken XInclude chain: could not "
- "include %s" % path)
- return False
-
- return True
+ #: The metadata plugin
+ self.metadata = self.core.metadata
diff --git a/src/lib/Bcfg2/Server/MultiprocessingCore.py b/src/lib/Bcfg2/Server/MultiprocessingCore.py
new file mode 100644
index 000000000..81fba7092
--- /dev/null
+++ b/src/lib/Bcfg2/Server/MultiprocessingCore.py
@@ -0,0 +1,204 @@
+""" The multiprocessing server core is a reimplementation of the
+:mod:`Bcfg2.Server.BuiltinCore` that uses the Python
+:mod:`multiprocessing` library to offload work to multiple child
+processes. As such, it requires Python 2.6+.
+"""
+
+import threading
+import lxml.etree
+import multiprocessing
+from Bcfg2.Compat import Queue
+from Bcfg2.Server.Core import BaseCore, exposed
+from Bcfg2.Server.BuiltinCore import Core as BuiltinCore
+
+
+class DualEvent(object):
+ """ DualEvent is a clone of :class:`threading.Event` that
+ internally implements both :class:`threading.Event` and
+ :class:`multiprocessing.Event`. """
+
+ def __init__(self, threading_event=None, multiprocessing_event=None):
+ self._threading_event = threading_event or threading.Event()
+ self._multiproc_event = multiprocessing_event or \
+ multiprocessing.Event()
+ if threading_event or multiprocessing_event:
+ # initialize internal flag to false, regardless of the
+ # state of either object passed in
+ self.clear()
+
+ def is_set(self):
+ """ Return true if and only if the internal flag is true. """
+ return self._threading_event.is_set()
+
+ isSet = is_set
+
+ def set(self):
+ """ Set the internal flag to true. """
+ self._threading_event.set()
+ self._multiproc_event.set()
+
+ def clear(self):
+ """ Reset the internal flag to false. """
+ self._threading_event.clear()
+ self._multiproc_event.clear()
+
+ def wait(self, timeout=None):
+ """ Block until the internal flag is true, or until the
+ optional timeout occurs. """
+ return self._threading_event.wait(timeout=timeout)
+
+
+class ChildCore(BaseCore):
+ """ A child process for :class:`Bcfg2.MultiprocessingCore.Core`.
+ This core builds configurations from a given
+ :class:`multiprocessing.Pipe`. Note that this is a full-fledged
+ server core; the only input it gets from the parent process is the
+ hostnames of clients to render. All other state comes from the
+ FAM. However, this core only is used to render configs; it doesn't
+ handle anything else (authentication, probes, etc.) because those
+ are all much faster. There's no reason that it couldn't handle
+ those, though, if the pipe communication "protocol" were made more
+ robust. """
+
+ #: How long to wait while polling for new clients to build. This
+ #: doesn't affect the speed with which a client is built, but
+ #: setting it too high will result in longer shutdown times, since
+ #: we only check for the termination event from the main process
+ #: every ``poll_wait`` seconds.
+ poll_wait = 5.0
+
+ def __init__(self, setup, pipe, terminate):
+ """
+ :param setup: A Bcfg2 options dict
+ :type setup: Bcfg2.Options.OptionParser
+ :param pipe: The pipe to which client hostnames are added for
+ ChildCore objects to build configurations, and to
+ which client configurations are added after
+ having been built by ChildCore objects.
+ :type pipe: multiprocessing.Pipe
+ :param terminate: An event that flags ChildCore objects to shut
+ themselves down.
+ :type terminate: multiprocessing.Event
+ """
+ BaseCore.__init__(self, setup)
+
+ #: The pipe to which client hostnames are added for ChildCore
+ #: objects to build configurations, and to which client
+ #: configurations are added after having been built by
+ #: ChildCore objects.
+ self.pipe = pipe
+
+ #: The :class:`multiprocessing.Event` that will be monitored
+ #: to determine when this child should shut down.
+ self.terminate = terminate
+
+ def _daemonize(self):
+ return True
+
+ def _run(self):
+ return True
+
+ def _block(self):
+ while not self.terminate.isSet():
+ try:
+ if self.pipe.poll(self.poll_wait):
+ if not self.metadata.use_database:
+ # handle FAM events, in case (for instance) the
+ # client has just been added to clients.xml, or a
+ # profile has just been asserted. but really, you
+ # should be using the metadata database if you're
+ # using this core.
+ self.fam.handle_events_in_interval(0.1)
+ client = self.pipe.recv()
+ self.logger.debug("Building configuration for %s" % client)
+ config = \
+ lxml.etree.tostring(self.BuildConfiguration(client))
+ self.logger.debug("Returning configuration for %s to main "
+ "process" % client)
+ self.pipe.send(config)
+ self.logger.debug("Returned configuration for %s to main "
+ "process" % client)
+ except KeyboardInterrupt:
+ break
+ self.shutdown()
+
+
+class Core(BuiltinCore):
+ """ A multiprocessing core that delegates building the actual
+ client configurations to
+ :class:`Bcfg2.Server.MultiprocessingCore.ChildCore` objects. The
+ parent process doesn't build any children itself; all calls to
+ :func:`GetConfig` are delegated to children. All other calls are
+ handled by the parent process. """
+
+ #: How long to wait for a child process to shut down cleanly
+ #: before it is terminated.
+ shutdown_timeout = 10.0
+
+ def __init__(self, setup):
+ BuiltinCore.__init__(self, setup)
+ if setup['children'] is None:
+ setup['children'] = multiprocessing.cpu_count()
+
+ #: A dict of child name -> one end of the
+ #: :class:`multiprocessing.Pipe` object used to communicate
+ #: with that child. (The child is given the other end of the
+ #: Pipe.)
+ self.pipes = dict()
+
+ #: A queue that keeps track of which children are available to
+ #: render a configuration. A child is popped from the queue
+ #: when it starts to render a config, then it's pushed back on
+ #: when it's done. This lets us use a blocking call to
+ #: :func:`Queue.Queue.get` when waiting for an available
+ #: child.
+ self.available_children = Queue(maxsize=self.setup['children'])
+
+ # sigh. multiprocessing was added in py2.6, which is when the
+ # camelCase methods for threading objects were deprecated in
+ # favor of the Pythonic under_score methods. So
+ # multiprocessing.Event *only* has is_set(), while
+ # threading.Event has *both* isSet() and is_set(). In order
+ # to make the core work with Python 2.4+, and with both
+ # multiprocessing and threading Event objects, we just
+ # monkeypatch self.terminate to have isSet().
+ self.terminate = DualEvent(threading_event=self.terminate)
+
+ def _run(self):
+ for cnum in range(self.setup['children']):
+ name = "Child-%s" % cnum
+ (mainpipe, childpipe) = multiprocessing.Pipe()
+ self.pipes[name] = mainpipe
+ self.logger.debug("Starting child %s" % name)
+ childcore = ChildCore(self.setup, childpipe, self.terminate)
+ child = multiprocessing.Process(target=childcore.run, name=name)
+ child.start()
+ self.logger.debug("Child %s started with PID %s" % (name,
+ child.pid))
+ self.available_children.put(name)
+ return BuiltinCore._run(self)
+
+ def shutdown(self):
+ BuiltinCore.shutdown(self)
+ for child in multiprocessing.active_children():
+ self.logger.debug("Shutting down child %s" % child.name)
+ child.join(self.shutdown_timeout)
+ if child.is_alive():
+ self.logger.error("Waited %s seconds to shut down %s, "
+ "terminating" % (self.shutdown_timeout,
+ child.name))
+ child.terminate()
+ else:
+ self.logger.debug("Child %s shut down" % child.name)
+ self.logger.debug("All children shut down")
+
+ @exposed
+ def GetConfig(self, address):
+ client = self.resolve_client(address)[0]
+ childname = self.available_children.get()
+ self.logger.debug("Building configuration on child %s" % childname)
+ pipe = self.pipes[childname]
+ pipe.send(client)
+ config = pipe.recv()
+ self.available_children.put_nowait(childname)
+ return config
diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py
index b14968d77..81dc1d736 100644
--- a/src/lib/Bcfg2/Server/Plugin/helpers.py
+++ b/src/lib/Bcfg2/Server/Plugin/helpers.py
@@ -41,15 +41,15 @@ del DEFAULT_FILE_METADATA['configfile']
LOGGER = logging.getLogger(__name__)
#: a compiled regular expression for parsing info and :info files
-INFO_REGEX = re.compile(r'owner:(\s)*(?P<owner>\S+)|' +
- r'group:(\s)*(?P<group>\S+)|' +
- r'mode:(\s)*(?P<mode>\w+)|' +
- r'secontext:(\s)*(?P<secontext>\S+)|' +
- r'paranoid:(\s)*(?P<paranoid>\S+)|' +
- r'sensitive:(\s)*(?P<sensitive>\S+)|' +
- r'encoding:(\s)*(?P<encoding>\S+)|' +
- r'important:(\s)*(?P<important>\S+)|' +
- r'mtime:(\s)*(?P<mtime>\w+)|')
+INFO_REGEX = re.compile(r'owner:\s*(?P<owner>\S+)|' +
+ r'group:\s*(?P<group>\S+)|' +
+ r'mode:\s*(?P<mode>\w+)|' +
+ r'secontext:\s*(?P<secontext>\S+)|' +
+ r'paranoid:\s*(?P<paranoid>\S+)|' +
+ r'sensitive:\s*(?P<sensitive>\S+)|' +
+ r'encoding:\s*(?P<encoding>\S+)|' +
+ r'important:\s*(?P<important>\S+)|' +
+ r'mtime:\s*(?P<mtime>\w+)')
def bind_info(entry, metadata, infoxml=None, default=DEFAULT_FILE_METADATA):
diff --git a/src/lib/Bcfg2/Server/Plugins/Bundler.py b/src/lib/Bcfg2/Server/Plugins/Bundler.py
index 5c5e3da0c..eef176cca 100644
--- a/src/lib/Bcfg2/Server/Plugins/Bundler.py
+++ b/src/lib/Bcfg2/Server/Plugins/Bundler.py
@@ -144,10 +144,10 @@ class Bundler(Bcfg2.Server.Plugin.Plugin,
class BundlerLint(Bcfg2.Server.Lint.ServerPlugin):
- """ Perform various bundle checks """
+ """ Perform various :ref:`Bundler
+ <server-plugins-structures-bundler-index>` checks. """
def Run(self):
- """ run plugin """
self.missing_bundles()
for bundle in self.core.plugins['Bundler'].entries.values():
if (self.HandlesFile(bundle.name) and
@@ -161,7 +161,8 @@ class BundlerLint(Bcfg2.Server.Lint.ServerPlugin):
"inconsistent-bundle-name": "warning"}
def missing_bundles(self):
- """ find bundles listed in Metadata but not implemented in Bundler """
+ """ Find bundles listed in Metadata but not implemented in
+ Bundler. """
if self.files is None:
# when given a list of files on stdin, this check is
# useless, so skip it
@@ -180,7 +181,11 @@ class BundlerLint(Bcfg2.Server.Lint.ServerPlugin):
bundle)
def bundle_names(self, bundle):
- """ verify bundle name attribute matches filename """
+ """ Verify bundle name attribute matches filename.
+
+ :param bundle: The bundle to verify
+ :type bundle: Bcfg2.Server.Plugins.Bundler.BundleFile
+ """
try:
xdata = lxml.etree.XML(bundle.data)
except AttributeError:
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
index ffe93c25b..842202a9c 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
@@ -10,6 +10,7 @@ import lxml.etree
import Bcfg2.Options
import Bcfg2.Server.Plugin
import Bcfg2.Server.Lint
+from itertools import chain
from Bcfg2.Server.Plugin import PluginExecutionError
# pylint: disable=W0622
from Bcfg2.Compat import u_str, unicode, b64encode, walk_packages, \
@@ -35,6 +36,24 @@ SETUP = None
#: facility for passing it otherwise.
CFG = None
+_HANDLERS = []
+
+
+def handlers():
+ """ A list of Cfg handler classes. Loading the handlers must
+ be done at run-time, not at compile-time, or it causes a
+ circular import and Bad Things Happen."""
+ if not _HANDLERS:
+ for submodule in walk_packages(path=__path__, prefix=__name__ + "."):
+ mname = submodule[1].rsplit('.', 1)[-1]
+ module = getattr(__import__(submodule[1]).Server.Plugins.Cfg,
+ mname)
+ hdlr = getattr(module, mname)
+ if issubclass(hdlr, CfgBaseFileMatcher):
+ _HANDLERS.append(hdlr)
+ _HANDLERS.sort(key=operator.attrgetter("__priority__"))
+ return _HANDLERS
+
class CfgBaseFileMatcher(Bcfg2.Server.Plugin.SpecificData,
Bcfg2.Server.Plugin.Debuggable):
@@ -459,7 +478,6 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
entry_type, encoding)
Bcfg2.Server.Plugin.Debuggable.__init__(self)
self.specific = None
- self._handlers = None
__init__.__doc__ = Bcfg2.Server.Plugin.EntrySet.__doc__
def set_debug(self, debug):
@@ -468,24 +486,6 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
entry.set_debug(debug)
return rv
- @property
- def handlers(self):
- """ A list of Cfg handler classes. Loading the handlers must
- be done at run-time, not at compile-time, or it causes a
- circular import and Bad Things Happen."""
- if self._handlers is None:
- self._handlers = []
- for submodule in walk_packages(path=__path__,
- prefix=__name__ + "."):
- mname = submodule[1].rsplit('.', 1)[-1]
- module = getattr(__import__(submodule[1]).Server.Plugins.Cfg,
- mname)
- hdlr = getattr(module, mname)
- if CfgBaseFileMatcher in hdlr.__mro__:
- self._handlers.append(hdlr)
- self._handlers.sort(key=operator.attrgetter("__priority__"))
- return self._handlers
-
def handle_event(self, event):
""" Dispatch a FAM event to :func:`entry_init` or the
appropriate child handler object.
@@ -502,7 +502,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
# process a bogus changed event like a created
return
- for hdlr in self.handlers:
+ for hdlr in handlers():
if hdlr.handles(event, basename=self.path):
if action == 'changed':
# warn about a bogus 'changed' event, but
@@ -520,7 +520,9 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
return
elif hdlr.ignore(event, basename=self.path):
return
- elif action == 'changed':
+ # we only get here if event.filename in self.entries, so handle
+ # created event like changed
+ elif action == 'changed' or action == 'created':
self.entries[event.filename].handle_event(event)
return
elif action == 'deleted':
@@ -886,12 +888,15 @@ class CfgLint(Bcfg2.Server.Lint.ServerPlugin):
for basename, entry in list(self.core.plugins['Cfg'].entries.items()):
self.check_delta(basename, entry)
self.check_pubkey(basename, entry)
+ self.check_missing_files()
@classmethod
def Errors(cls):
return {"cat-file-used": "warning",
"diff-file-used": "warning",
- "no-pubkey-xml": "warning"}
+ "no-pubkey-xml": "warning",
+ "unknown-cfg-files": "error",
+ "extra-cfg-files": "error"}
def check_delta(self, basename, entry):
""" check that no .cat or .diff files are in use """
@@ -925,3 +930,41 @@ class CfgLint(Bcfg2.Server.Lint.ServerPlugin):
self.LintError("no-pubkey-xml",
"%s has no corresponding pubkey.xml at %s" %
(basename, pubkey))
+
+ def check_missing_files(self):
+ """ check that all files on the filesystem are known to Cfg """
+ cfg = self.core.plugins['Cfg']
+
+ # first, collect ignore patterns from handlers
+ ignore = []
+ for hdlr in handlers():
+ ignore.extend(hdlr.__ignore__)
+
+ # next, get a list of all non-ignored files on the filesystem
+ all_files = set()
+ for root, _, files in os.walk(cfg.data):
+ all_files.update(os.path.join(root, fname)
+ for fname in files
+ if not any(fname.endswith("." + i)
+ for i in ignore))
+
+ # next, get a list of all files known to Cfg
+ cfg_files = set()
+ for root, eset in cfg.entries.items():
+ cfg_files.update(os.path.join(cfg.data, root.lstrip("/"), fname)
+ for fname in eset.entries.keys())
+
+ # finally, compare the two
+ unknown_files = all_files - cfg_files
+ extra_files = cfg_files - all_files
+ if unknown_files:
+ self.LintError(
+ "unknown-cfg-files",
+ "Files on the filesystem could not be understood by Cfg: %s" %
+ "; ".join(unknown_files))
+ if extra_files:
+ self.LintError(
+ "extra-cfg-files",
+ "Cfg has entries for files that do not exist on the "
+ "filesystem: %s\nThis is probably a bug." %
+ "; ".join(extra_files))
diff --git a/src/lib/Bcfg2/Server/Plugins/FileProbes.py b/src/lib/Bcfg2/Server/Plugins/FileProbes.py
index d816192aa..8e074118f 100644
--- a/src/lib/Bcfg2/Server/Plugins/FileProbes.py
+++ b/src/lib/Bcfg2/Server/Plugins/FileProbes.py
@@ -24,7 +24,11 @@ import sys
import pwd
import grp
import Bcfg2.Client.XML
-from Bcfg2.Compat import b64encode, oct_mode
+try:
+ from Bcfg2.Compat import b64encode, oct_mode
+except ImportError:
+ from base64 import b64encode
+ oct_mode = oct
path = "%s"
diff --git a/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py b/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
index 8d1e50526..09685d972 100644
--- a/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
+++ b/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
@@ -129,7 +129,12 @@ class GroupPatterns(Bcfg2.Server.Plugin.Plugin,
class GroupPatternsLint(Bcfg2.Server.Lint.ServerPlugin):
- """ bcfg2-lint plugin for GroupPatterns """
+ """ ``bcfg2-lint`` plugin to check all given :ref:`GroupPatterns
+ <server-plugins-grouping-grouppatterns>` patterns for validity.
+ This is simply done by trying to create a
+ :class:`Bcfg2.Server.Plugins.GroupPatterns.PatternMap` object for
+ each pattern, and catching exceptions and presenting them as
+ ``bcfg2-lint`` errors."""
def Run(self):
cfg = self.core.plugins['GroupPatterns'].config
diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py
index bdf3b87fe..3b8361c76 100644
--- a/src/lib/Bcfg2/Server/Plugins/Metadata.py
+++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py
@@ -557,6 +557,12 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
open(os.path.join(repo, cls.name, fname),
"w").write(kwargs[aname])
+ @property
+ def use_database(self):
+ """ Expose self._use_db publicly for use in
+ :class:`Bcfg2.Server.MultiprocessingCore.ChildCore` """
+ return self._use_db
+
def _handle_file(self, fname):
""" set up the necessary magic for handling a metadata file
(clients.xml or groups.xml, e.g.) """
@@ -945,7 +951,8 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
self.debug_log("Client %s set as nonexistent group %s"
% (client, group))
- def set_profile(self, client, profile, addresspair):
+ def set_profile(self, client, profile, # pylint: disable=W0221
+ addresspair, require_public=True):
"""Set group parameter for provided client."""
self.logger.info("Asserting client %s profile to %s" % (client,
profile))
@@ -957,7 +964,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
self.logger.error(msg)
raise Bcfg2.Server.Plugin.MetadataConsistencyError(msg)
group = self.groups[profile]
- if not group.is_public:
+ if require_public and not group.is_public:
msg = "Cannot set client %s to private group %s" % (client,
profile)
self.logger.error(msg)
@@ -1128,7 +1135,8 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
pgroup = self.default
if pgroup:
- self.set_profile(client, pgroup, (None, None))
+ self.set_profile(client, pgroup, (None, None),
+ require_public=False)
profile = _add_group(pgroup)
else:
msg = "Cannot add new client %s; no default group set" % client
@@ -1477,7 +1485,16 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
- """ bcfg2-lint plugin for Metadata """
+ """ ``bcfg2-lint`` plugin for :ref:`Metadata
+ <server-plugins-grouping-metadata>`. This checks for several things:
+
+ * ``<Client>`` tags nested inside other ``<Client>`` tags;
+ * Deprecated options (like ``location="floating"``);
+ * Profiles that don't exist, or that aren't profile groups;
+ * Groups or clients that are defined multiple times;
+ * Multiple default groups or a default group that isn't a profile
+ group.
+ """
def Run(self):
self.nested_clients()
@@ -1500,8 +1517,8 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
"default-is-not-profile": "error"}
def deprecated_options(self):
- """ check for the location='floating' option, which has been
- deprecated in favor of floating='true' """
+ """ Check for the ``location='floating'`` option, which has
+ been deprecated in favor of ``floating='true'``. """
if not hasattr(self.metadata, "clients_xml"):
# using metadata database
return
@@ -1519,8 +1536,8 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
(loc, floating, self.RenderXML(el)))
def nested_clients(self):
- """ check for a Client tag inside a Client tag, which doesn't
- make any sense """
+ """ Check for a ``<Client/>`` tag inside a ``<Client/>`` tag,
+ which is either redundant or will never match. """
groupdata = self.metadata.groups_xml.xdata
for el in groupdata.xpath("//Client//Client"):
self.LintError("nested-client-tags",
@@ -1528,8 +1545,8 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
(el.get("name"), self.RenderXML(el)))
def bogus_profiles(self):
- """ check for clients that have profiles that are either not
- flagged as public groups in groups.xml, or don't exist """
+ """ Check for clients that have profiles that are either not
+ flagged as profile groups in ``groups.xml``, or don't exist. """
if not hasattr(self.metadata, "clients_xml"):
# using metadata database
return
@@ -1547,20 +1564,8 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
(profile, client.get("name"), profile,
self.RenderXML(client)))
- def duplicate_groups(self):
- """ check for groups that are defined twice. We count a group
- tag as a definition if it a) has profile or public set; or b)
- has any children. """
- self.duplicate_entries(
- self.metadata.groups_xml.xdata.xpath("//Groups/Group") +
- self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group"),
- "group",
- include=lambda g: (g.get("profile") or
- g.get("public") or
- g.getchildren()))
-
def duplicate_default_groups(self):
- """ check for multiple default groups """
+ """ Check for multiple default groups. """
defaults = []
for grp in self.metadata.groups_xml.xdata.xpath("//Groups/Group") + \
self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group"):
@@ -1572,7 +1577,7 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
"\n".join(defaults))
def duplicate_clients(self):
- """ check for clients that are defined twice. """
+ """ Check for clients that are defined more than once. """
if not hasattr(self.metadata, "clients_xml"):
# using metadata database
return
@@ -1580,17 +1585,34 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
self.metadata.clients_xml.xdata.xpath("//Client"),
"client")
- def duplicate_entries(self, allentries, etype, include=None):
- """ generic duplicate entry finder """
- if include is None:
- include = lambda e: True
+ def duplicate_groups(self):
+ """ Check for groups that are defined more than once. We
+ count a group tag as a definition if it a) has profile or
+ public set; or b) has any children."""
+ allgroups = [
+ g
+ for g in self.metadata.groups_xml.xdata.xpath("//Groups/Group") +
+ self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group")
+ if g.get("profile") or g.get("public") or g.getchildren()]
+ self.duplicate_entries(allgroups, "group")
+
+ def duplicate_entries(self, allentries, etype):
+ """ Generic duplicate entry finder.
+
+ :param allentries: A list of all entries to check for
+ duplicates.
+ :type allentries: list of lxml.etree._Element
+ :param etype: The entry type. This will be used to determine
+ the error name (``duplicate-<etype>``) and for
+ display to the end user.
+ :type etype: string
+ """
entries = dict()
for el in allentries:
- if include(el):
- if el.get("name") in entries:
- entries[el.get("name")].append(self.RenderXML(el))
- else:
- entries[el.get("name")] = [self.RenderXML(el)]
+ if el.get("name") in entries:
+ entries[el.get("name")].append(self.RenderXML(el))
+ else:
+ entries[el.get("name")] = [self.RenderXML(el)]
for ename, els in entries.items():
if len(els) > 1:
self.LintError("duplicate-%s" % etype,
@@ -1598,7 +1620,7 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
(etype.title(), ename, "\n".join(els)))
def default_is_profile(self):
- """ ensure that the default group is a profile group """
+ """ Ensure that the default group is a profile group. """
if (self.metadata.default and
not self.metadata.groups[self.metadata.default].is_profile):
xdata = \
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py b/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
index bc2928fa6..a82a183d8 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
@@ -40,6 +40,11 @@ class AptCollection(Collection):
else:
lines.append("deb %s %s %s" % (source.url, source.version,
" ".join(source.components)))
+ if source.debsrc:
+ lines.append("deb-src %s %s %s" %
+ (source.url,
+ source.version,
+ " ".join(source.components)))
lines.append("")
return "\n".join(lines)
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
index 7ba374dd3..22073493c 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
@@ -158,6 +158,10 @@ class Source(Bcfg2.Server.Plugin.Debuggable): # pylint: disable=R0902
#: this source
self.whitelist = [item.text for item in xsource.findall('Whitelist')]
+ #: Whether or not to include deb-src lines in the generated APT
+ #: configuration
+ self.debsrc = xsource.get('debsrc', 'false') == 'true'
+
#: A dict of repository options that will be included in the
#: configuration generated on the server side (if such is
#: applicable; most backends do not generate any sort of
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
index 7438c633b..20b2c9500 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
@@ -675,7 +675,10 @@ class YumCollection(Collection):
gdicts.append(dict(group=group, type=ptype))
if self.use_yum:
- return self.call_helper("get_groups", inputdata=gdicts)
+ try:
+ return self.call_helper("get_groups", inputdata=gdicts)
+ except ValueError:
+ return dict()
else:
pkgs = dict()
for gdict in gdicts:
@@ -838,12 +841,13 @@ class YumCollection(Collection):
return Collection.complete(self, packagelist)
if packagelist:
- result = \
- self.call_helper("complete",
- dict(packages=list(packagelist),
- groups=list(self.get_relevant_groups())))
- if not result:
- # some sort of error, reported by call_helper()
+ try:
+ result = self.call_helper(
+ "complete",
+ dict(packages=list(packagelist),
+ groups=list(self.get_relevant_groups())))
+ except ValueError:
+ # error reported by call_helper()
return set(), packagelist
# json doesn't understand sets or tuples, so we get back a
# lists of lists (packages) and a list of unicode strings
@@ -905,7 +909,7 @@ class YumCollection(Collection):
err = sys.exc_info()[1]
self.logger.error("Packages: error reading bcfg2-yum-helper "
"output: %s" % err)
- return None
+ raise
def setup_data(self, force_update=False):
""" Do any collection-level data setup tasks. This is called
@@ -931,13 +935,21 @@ class YumCollection(Collection):
if force_update:
# we call this twice: one to clean up data from the old
# config, and once to clean up data from the new config
- self.call_helper("clean")
+ try:
+ self.call_helper("clean")
+ except ValueError:
+ # error reported by call_helper
+ pass
os.unlink(self.cfgfile)
self.write_config()
if force_update:
- self.call_helper("clean")
+ try:
+ self.call_helper("clean")
+ except ValueError:
+ # error reported by call_helper
+ pass
class YumSource(Source):
@@ -1120,9 +1132,9 @@ class YumSource(Source):
self.packages['global'] = copy.deepcopy(sdata.pop())
except IndexError:
self.logger.error("Packages: No packages in repo")
+ self.packages['global'] = set()
while sdata:
- self.packages['global'] = \
- self.packages['global'].intersection(sdata.pop())
+ self.packages['global'].update(sdata.pop())
for key in self.packages:
if key == 'global':
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
index efbca28cd..f82b8a392 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
@@ -9,7 +9,7 @@ import shutil
import lxml.etree
import Bcfg2.Logger
import Bcfg2.Server.Plugin
-from Bcfg2.Compat import ConfigParser, urlopen, HTTPError
+from Bcfg2.Compat import ConfigParser, urlopen, HTTPError, URLError
from Bcfg2.Server.Plugins.Packages.Collection import Collection, \
get_collection_class
from Bcfg2.Server.Plugins.Packages.PackagesSources import PackagesSources
@@ -459,7 +459,7 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
try:
open(localfile, 'w').write(urlopen(key).read())
keys.append(key)
- except HTTPError:
+ except (URLError, HTTPError):
err = sys.exc_info()[1]
self.logger.error("Packages: Error downloading %s: %s"
% (key, err))
@@ -527,8 +527,9 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
collection = cclass(metadata, relevant, self.cachepath, self.data,
self.core.fam, debug=self.debug_flag)
ckey = collection.cachekey
- self.clients[metadata.hostname] = ckey
- self.collections[ckey] = collection
+ if cclass != Collection:
+ self.clients[metadata.hostname] = ckey
+ self.collections[ckey] = collection
return collection
def get_additional_data(self, metadata):
diff --git a/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py b/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py
index 7dac907e1..a1dcb575f 100644
--- a/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py
+++ b/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py
@@ -177,7 +177,10 @@ class Pkgmgr(Bcfg2.Server.Plugin.PrioDir):
class PkgmgrLint(Bcfg2.Server.Lint.ServerlessPlugin):
- """ find duplicate Pkgmgr entries with the same priority """
+ """ Find duplicate :ref:`Pkgmgr
+ <server-plugins-generators-pkgmgr>` entries with the same
+ priority. """
+
def Run(self):
pset = set()
for pfile in glob.glob(os.path.join(self.config['repo'], 'Pkgmgr',
@@ -202,12 +205,13 @@ class PkgmgrLint(Bcfg2.Server.Lint.ServerlessPlugin):
# check if package is already listed with same
# priority, type, grp
if ptuple in pset:
- self.LintError("duplicate-package",
- "Duplicate Package %s, priority:%s, type:%s" %
- (pkg.get('name'), priority, ptype))
+ self.LintError(
+ "duplicate-package",
+ "Duplicate Package %s, priority:%s, type:%s" %
+ (pkg.get('name'), priority, ptype))
else:
pset.add(ptuple)
-
+
@classmethod
def Errors(cls):
- return {"duplicate-packages":"error"}
+ return {"duplicate-packages": "error"}
diff --git a/src/lib/Bcfg2/Server/Plugins/Probes.py b/src/lib/Bcfg2/Server/Plugins/Probes.py
index 309b96475..f8baddb4b 100644
--- a/src/lib/Bcfg2/Server/Plugins/Probes.py
+++ b/src/lib/Bcfg2/Server/Plugins/Probes.py
@@ -63,7 +63,7 @@ class ProbeData(str): # pylint: disable=E0012,R0924
.json, and .yaml properties to provide convenient ways to use
ProbeData objects as XML, JSON, or YAML data """
def __new__(cls, data):
- return str.__new__(cls, data.encode('utf-8'))
+ return str.__new__(cls, data)
def __init__(self, data): # pylint: disable=W0613
str.__init__(self)
@@ -222,15 +222,9 @@ class Probes(Bcfg2.Server.Plugin.Probing,
lxml.etree.SubElement(top, 'Client', name=client,
timestamp=str(int(probedata.timestamp)))
for probe in sorted(probedata):
- try:
- lxml.etree.SubElement(
- ctag, 'Probe', name=probe,
- value=str(
- self.probedata[client][probe]).decode('utf-8'))
- except AttributeError:
- lxml.etree.SubElement(
- ctag, 'Probe', name=probe,
- value=str(self.probedata[client][probe]))
+ lxml.etree.SubElement(
+ ctag, 'Probe', name=probe,
+ value=self.probedata[client][probe])
for group in sorted(self.cgroups[client]):
lxml.etree.SubElement(ctag, "Group", name=group)
try:
diff --git a/src/lib/Bcfg2/Server/Plugins/SSHbase.py b/src/lib/Bcfg2/Server/Plugins/SSHbase.py
index fc07a90e9..d8b3104b7 100644
--- a/src/lib/Bcfg2/Server/Plugins/SSHbase.py
+++ b/src/lib/Bcfg2/Server/Plugins/SSHbase.py
@@ -172,7 +172,7 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin,
for name in names[cmeta.hostname]:
newnames.add(name.split('.')[0])
try:
- newips.add(self.get_ipcache_entry(name)[0])
+ newips.update(self.get_ipcache_entry(name)[0])
except: # pylint: disable=W0702
continue
names[cmeta.hostname].update(newnames)
@@ -288,7 +288,8 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin,
else:
# need to add entry
try:
- ipaddr = socket.gethostbyname(client)
+ ipaddr = set([info[4][0]
+ for info in socket.getaddrinfo(client, None)])
self.ipcache[client] = (ipaddr, client)
return (ipaddr, client)
except socket.gaierror:
diff --git a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
index 7dd15f7b5..fcd73bae2 100644
--- a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
+++ b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
@@ -97,7 +97,18 @@ class TemplateHelper(Bcfg2.Server.Plugin.Plugin,
class TemplateHelperLint(Bcfg2.Server.Lint.ServerPlugin):
- """ find duplicate Pkgmgr entries with the same priority """
+ """ ``bcfg2-lint`` plugin to ensure that all :ref:`TemplateHelper
+ <server-plugins-connectors-templatehelper>` modules are valid.
+ This can check for:
+
+ * A TemplateHelper module that cannot be imported due to syntax or
+ other compile-time errors;
+ * A TemplateHelper module that does not have an ``__export__``
+ attribute, or whose ``__export__`` is not a list;
+ * Bogus symbols listed in ``__export__``, including symbols that
+ don't exist, that are reserved, or that start with underscores.
+ """
+
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
self.reserved_keywords = dir(HelperModule("foo.py"))
@@ -108,7 +119,11 @@ class TemplateHelperLint(Bcfg2.Server.Lint.ServerPlugin):
self.check_helper(helper.name)
def check_helper(self, helper):
- """ check a helper module for export errors """
+ """ Check a single helper module.
+
+ :param helper: The filename of the helper module
+ :type helper: string
+ """
module_name = MODULE_RE.search(helper).group(1)
try:
diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py
index 581445bf4..1c2dceed2 100644
--- a/src/lib/Bcfg2/Utils.py
+++ b/src/lib/Bcfg2/Utils.py
@@ -2,6 +2,7 @@
used by both client and server. Stuff that doesn't fit anywhere
else. """
+import shlex
import fcntl
import logging
import threading
@@ -218,6 +219,7 @@ class Executor(object):
"""
if isinstance(command, str):
cmdstr = command
+ command = shlex.split(cmdstr)
else:
cmdstr = " ".join(command)
self.logger.debug("Running: %s" % cmdstr)