summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Server')
-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/Init.py4
-rw-r--r--src/lib/Bcfg2/Server/Admin/Minestruct.py18
-rw-r--r--src/lib/Bcfg2/Server/Admin/Perf.py11
-rw-r--r--src/lib/Bcfg2/Server/Admin/Pull.py17
-rw-r--r--src/lib/Bcfg2/Server/Admin/Reports.py23
-rw-r--r--src/lib/Bcfg2/Server/Admin/Syncdb.py10
-rw-r--r--src/lib/Bcfg2/Server/Admin/Xcmd.py22
-rw-r--r--src/lib/Bcfg2/Server/Admin/__init__.py32
-rw-r--r--src/lib/Bcfg2/Server/BuiltinCore.py16
-rw-r--r--src/lib/Bcfg2/Server/Core.py255
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/Fam.py2
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/Inotify.py8
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/Pseudo.py2
-rw-r--r--src/lib/Bcfg2/Server/Lint/Comments.py132
-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.py53
-rw-r--r--src/lib/Bcfg2/Server/Lint/Validate.py80
-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/base.py20
-rw-r--r--src/lib/Bcfg2/Server/Plugin/helpers.py187
-rw-r--r--src/lib/Bcfg2/Server/Plugin/interfaces.py19
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Base.py9
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Bundler.py41
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py8
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py128
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cvs.py6
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Darcs.py6
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Decisions.py13
-rw-r--r--src/lib/Bcfg2/Server/Plugins/FileProbes.py16
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Fossil.py10
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Git.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugins/GroupLogic.py47
-rw-r--r--src/lib/Bcfg2/Server/Plugins/GroupPatterns.py18
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Metadata.py231
-rw-r--r--src/lib/Bcfg2/Server/Plugins/NagiosGen.py24
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Ohai.py31
-rw-r--r--src/lib/Bcfg2/Server/Plugins/POSIXCompat.py4
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Apt.py17
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py14
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Source.py14
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Yum.py75
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/__init__.py39
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Pkgmgr.py16
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Probes.py60
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Properties.py42
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Reporting.py7
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SSHbase.py12
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SSLCA.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugins/ServiceCompat.py6
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Svn.py39
-rw-r--r--src/lib/Bcfg2/Server/Plugins/TemplateHelper.py23
-rw-r--r--src/lib/Bcfg2/Server/Plugins/__init__.py33
-rw-r--r--src/lib/Bcfg2/Server/__init__.py5
60 files changed, 1545 insertions, 827 deletions
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/Init.py b/src/lib/Bcfg2/Server/Admin/Init.py
index 4b8d65597..6175d8ed0 100644
--- a/src/lib/Bcfg2/Server/Admin/Init.py
+++ b/src/lib/Bcfg2/Server/Admin/Init.py
@@ -227,8 +227,8 @@ class Init(Bcfg2.Server.Admin.Mode):
def _prompt_password(self):
"""Ask for a password or generate one if none is provided."""
newpassword = getpass.getpass(
- "Input password used for communication verification "
- "(without echoing; leave blank for a random): ").strip()
+ "Input password used for communication verification "
+ "(without echoing; leave blank for a random): ").strip()
if len(newpassword) != 0:
self.data['password'] = newpassword
diff --git a/src/lib/Bcfg2/Server/Admin/Minestruct.py b/src/lib/Bcfg2/Server/Admin/Minestruct.py
index 6d0dab106..37ca74894 100644
--- a/src/lib/Bcfg2/Server/Admin/Minestruct.py
+++ b/src/lib/Bcfg2/Server/Admin/Minestruct.py
@@ -3,6 +3,7 @@ import getopt
import lxml.etree
import sys
import Bcfg2.Server.Admin
+from Bcfg2.Server.Plugin import PullSource
class Minestruct(Bcfg2.Server.Admin.StructureMode):
@@ -19,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
@@ -32,20 +32,18 @@ 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(':')
try:
extra = set()
- for source in self.bcore.pull_sources:
+ 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/Perf.py b/src/lib/Bcfg2/Server/Admin/Perf.py
index 86eb6810d..f6bc22959 100644
--- a/src/lib/Bcfg2/Server/Admin/Perf.py
+++ b/src/lib/Bcfg2/Server/Admin/Perf.py
@@ -18,8 +18,7 @@ class Perf(Bcfg2.Server.Admin.Mode):
'password': Bcfg2.Options.SERVER_PASSWORD,
'server': Bcfg2.Options.SERVER_LOCATION,
'user': Bcfg2.Options.CLIENT_USER,
- 'timeout': Bcfg2.Options.CLIENT_TIMEOUT,
- }
+ 'timeout': Bcfg2.Options.CLIENT_TIMEOUT}
setup = Bcfg2.Options.OptionParser(optinfo)
setup.parse(sys.argv[1:])
proxy = Bcfg2.Proxy.ComponentProxy(setup['server'],
@@ -31,8 +30,8 @@ class Perf(Bcfg2.Server.Admin.Mode):
timeout=setup['timeout'])
data = proxy.get_statistics()
for key in sorted(data.keys()):
- output.append((key, ) +
- tuple(["%.06f" % item
- for item in data[key][:-1]] + \
- [data[key][-1]]))
+ output.append(
+ (key, ) +
+ tuple(["%.06f" % item
+ for item in data[key][:-1]] + [data[key][-1]]))
self.print_table(output)
diff --git a/src/lib/Bcfg2/Server/Admin/Pull.py b/src/lib/Bcfg2/Server/Admin/Pull.py
index 130e85b67..459fcec65 100644
--- a/src/lib/Bcfg2/Server/Admin/Pull.py
+++ b/src/lib/Bcfg2/Server/Admin/Pull.py
@@ -6,6 +6,7 @@ import sys
import getopt
import select
import Bcfg2.Server.Admin
+from Bcfg2.Server.Plugin import PullSource, Generator
from Bcfg2.Compat import input # pylint: disable=W0622
@@ -31,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
@@ -62,13 +62,14 @@ class Pull(Bcfg2.Server.Admin.MetadataCore):
given client/entry from statistics.
"""
new_entry = {'type': etype, 'name': ename}
- for plugin in self.bcore.pull_sources:
+ pull_sources = self.bcore.plugins_by_type(PullSource)
+ for plugin in pull_sources:
try:
(owner, group, mode, contents) = \
- plugin.GetCurrentEntry(client, etype, ename)
+ plugin.GetCurrentEntry(client, etype, ename)
break
except Bcfg2.Server.Plugin.PluginExecutionError:
- if plugin == self.bcore.pull_sources[-1]:
+ if plugin == pull_sources[-1]:
print("Pull Source failure; could not fetch current state")
raise SystemExit(1)
@@ -121,8 +122,8 @@ class Pull(Bcfg2.Server.Admin.MetadataCore):
meta = self.bcore.build_metadata(client)
# Find appropriate plugin in bcore
- glist = [gen for gen in self.bcore.generators if
- ename in gen.Entries.get(etype, {})]
+ glist = [gen for gen in self.bcore.plugins_by_type(Generator)
+ if ename in gen.Entries.get(etype, {})]
if len(glist) != 1:
self.errExit("Got wrong numbers of matching generators for entry:"
"%s" % ([g.name for g in glist]))
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..eb417966d 100644
--- a/src/lib/Bcfg2/Server/Admin/Syncdb.py
+++ b/src/lib/Bcfg2/Server/Admin/Syncdb.py
@@ -3,6 +3,7 @@ import Bcfg2.settings
import Bcfg2.Options
import Bcfg2.Server.Admin
import Bcfg2.Server.models
+from django.core.exceptions import ImproperlyConfigured
from django.core.management import setup_environ, call_command
@@ -22,10 +23,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 79eeebc7c..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):
@@ -19,8 +18,7 @@ class Xcmd(Bcfg2.Server.Admin.Mode):
'key': Bcfg2.Options.SERVER_KEY,
'certificate': Bcfg2.Options.CLIENT_CERT,
'ca': Bcfg2.Options.CLIENT_CA,
- 'timeout': Bcfg2.Options.CLIENT_TIMEOUT,
- }
+ 'timeout': Bcfg2.Options.CLIENT_TIMEOUT}
setup = Bcfg2.Options.OptionParser(optinfo)
setup.parse(args)
Bcfg2.Proxy.RetryMethod.max_retries = 1
@@ -32,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 != None:
+ 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 19175533f..8f12a940e 100644
--- a/src/lib/Bcfg2/Server/Admin/__init__.py
+++ b/src/lib/Bcfg2/Server/Admin/__init__.py
@@ -1,31 +1,14 @@
""" Base classes for admin modes """
-__all__ = [
- 'Backup',
- 'Bundle',
- 'Client',
- 'Compare',
- 'Group',
- 'Init',
- 'Minestruct',
- 'Perf',
- 'Pull',
- 'Query',
- 'Reports',
- 'Snapshots',
- 'Syncdb',
- 'Tidy',
- 'Viz',
- 'Xcmd'
- ]
-
import re
import sys
import logging
import lxml.etree
import Bcfg2.Server.Core
import Bcfg2.Options
-from Bcfg2.Compat import ConfigParser
+from Bcfg2.Compat import ConfigParser, walk_packages
+
+__all__ = [m[1] for m in walk_packages(path=__path__)]
class Mode(object):
@@ -105,15 +88,15 @@ class Mode(object):
# Calculate column widths (longest item in each column
# plus padding on both sides)
cols = list(zip(*rows))
- col_widths = [max([len(str(item)) + 2 * padding for \
- item in col]) for col in cols]
+ col_widths = [max([len(str(item)) + 2 * padding
+ for item in col]) for col in cols]
borderline = vdelim.join([w * hdelim for w in col_widths])
# Print out the table
print(borderline)
for row in rows:
- print(vdelim.join([justify(str(item), width) for \
- (item, width) in zip(row, col_widths)]))
+ print(vdelim.join([justify(str(item), width)
+ for (item, width) in zip(row, col_widths)]))
if hdr:
print(borderline)
hdr = False
@@ -145,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 4d7453840..e69a92b64 100644
--- a/src/lib/Bcfg2/Server/BuiltinCore.py
+++ b/src/lib/Bcfg2/Server/BuiltinCore.py
@@ -9,12 +9,12 @@ from Bcfg2.Server.Core import BaseCore, NoExposedMethod
from Bcfg2.Compat import xmlrpclib, urlparse
from Bcfg2.SSLServer import XMLRPCServer
-from lockfile import LockFailed
+from lockfile import LockFailed, LockTimeout
# pylint: disable=E0611
try:
- from daemon.pidfile import PIDLockFile
+ from daemon.pidfile import TimeoutPIDLockFile
except ImportError:
- from daemon.pidlockfile import PIDLockFile
+ from daemon.pidlockfile import TimeoutPIDLockFile
# pylint: enable=E0611
@@ -33,7 +33,8 @@ class Core(BaseCore):
gid=self.setup['daemon_gid'],
umask=int(self.setup['umask'], 8))
if self.setup['daemon']:
- daemon_args['pidfile'] = PIDLockFile(self.setup['daemon'])
+ daemon_args['pidfile'] = TimeoutPIDLockFile(self.setup['daemon'],
+ acquire_timeout=5)
#: The :class:`daemon.DaemonContext` used to drop
#: privileges, write the PID file (with :class:`PidFile`),
#: and daemonize this core.
@@ -89,6 +90,11 @@ class Core(BaseCore):
err = sys.exc_info()[1]
self.logger.error("Failed to daemonize %s: %s" % (self.name, err))
return False
+ except LockTimeout:
+ err = sys.exc_info()[1]
+ self.logger.error("Failed to daemonize %s: Failed to acquire lock "
+ "on %s" % (self.name, self.setup['daemon']))
+ return False
def _run(self):
""" Create :attr:`server` to start the server listening. """
@@ -111,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 382f11e50..ecd68e1e4 100644
--- a/src/lib/Bcfg2/Server/Core.py
+++ b/src/lib/Bcfg2/Server/Core.py
@@ -2,14 +2,14 @@
implementations inherit from. """
import os
-import sys
-import time
+import pwd
import atexit
-import select
-import signal
import logging
-import inspect
+import select
+import sys
import threading
+import time
+import inspect
import lxml.etree
import Bcfg2.settings
import Bcfg2.Server
@@ -19,8 +19,9 @@ from Bcfg2.Cache import Cache
import Bcfg2.Statistics
from itertools import chain
from Bcfg2.Compat import xmlrpclib # pylint: disable=W0622
-from Bcfg2.Server.Plugin import PluginInitError, PluginExecutionError, \
- track_statistics
+from Bcfg2.Server.Plugin.exceptions import * # pylint: disable=W0401,W0614
+from Bcfg2.Server.Plugin.interfaces import * # pylint: disable=W0401,W0614
+from Bcfg2.Server.Plugin import track_statistics
try:
import psyco
@@ -96,6 +97,7 @@ class BaseCore(object):
.. automethod:: _block
.. -----
.. automethod:: _file_monitor_thread
+ .. automethod:: _perflog_thread
"""
#: The Bcfg2 repository directory
self.datastore = setup['repo']
@@ -174,6 +176,9 @@ class BaseCore(object):
#: the first one loaded wins.
self.plugin_blacklist = {}
+ #: The Metadata plugin
+ self.metadata = None
+
#: Revision of the Bcfg2 specification. This will be sent to
#: the client in the configuration, and can be set by a
#: :class:`Bcfg2.Server.Plugin.interfaces.Version` plugin.
@@ -235,88 +240,21 @@ class BaseCore(object):
self.logger.error("Failed to set ownership of database "
"at %s: %s" % (db_settings['NAME'], err))
- if '' in setup['plugins']:
- setup['plugins'].remove('')
-
- for plugin in setup['plugins']:
- if not plugin in self.plugins:
- self.init_plugin(plugin)
- # Remove blacklisted plugins
- for plugin, blacklist in list(self.plugin_blacklist.items()):
- if len(blacklist) > 0:
- self.logger.error("The following plugins conflict with %s;"
- "Unloading %s" % (plugin, blacklist))
- for plug in blacklist:
- del self.plugins[plug]
-
- # Log experimental plugins
- expl = [plug for plug in list(self.plugins.values())
- if plug.experimental]
- if expl:
- self.logger.info("Loading experimental plugin(s): %s" %
- (" ".join([x.name for x in expl])))
- self.logger.info("NOTE: Interfaces subject to change")
-
- # Log deprecated plugins
- depr = [plug for plug in list(self.plugins.values())
- if plug.deprecated]
- if depr:
- self.logger.info("Loading deprecated plugin(s): %s" %
- (" ".join([x.name for x in depr])))
-
- # Find the metadata plugin and set self.metadata
- mlist = self.plugins_by_type(Bcfg2.Server.Plugin.Metadata)
- if len(mlist) >= 1:
- #: The Metadata plugin
- self.metadata = mlist[0]
- if len(mlist) > 1:
- self.logger.error("Multiple Metadata plugins loaded; "
- "using %s" % self.metadata)
- else:
- self.logger.error("No Metadata plugin loaded; "
- "failed to instantiate Core")
- raise CoreInitError("No Metadata Plugin")
-
- #: The list of plugins that handle
- #: :class:`Bcfg2.Server.Plugin.interfaces.Statistics`
- self.statistics = self.plugins_by_type(Bcfg2.Server.Plugin.Statistics)
-
- #: The list of plugins that implement the
- #: :class:`Bcfg2.Server.Plugin.interfaces.PullSource`
- #: interface
- self.pull_sources = \
- self.plugins_by_type(Bcfg2.Server.Plugin.PullSource)
-
- #: The list of
- #: :class:`Bcfg2.Server.Plugin.interfaces.Generator` plugins
- self.generators = self.plugins_by_type(Bcfg2.Server.Plugin.Generator)
-
- #: The list of plugins that handle
- #: :class:`Bcfg2.Server.Plugin.interfaces.Structure`
- #: generation
- self.structures = self.plugins_by_type(Bcfg2.Server.Plugin.Structure)
-
- #: The list of plugins that implement the
- #: :class:`Bcfg2.Server.Plugin.interfaces.Connector` interface
- self.connectors = self.plugins_by_type(Bcfg2.Server.Plugin.Connector)
-
#: The CA that signed the server cert
self.ca = setup['ca']
- def hdlr(sig, frame): # pylint: disable=W0613
- """ Handle SIGINT/Ctrl-C by shutting down the core and exiting
- properly. """
- self.shutdown()
- os._exit(1) # pylint: disable=W0212
-
- signal.signal(signal.SIGINT, hdlr)
-
#: The FAM :class:`threading.Thread`,
#: :func:`_file_monitor_thread`
self.fam_thread = \
threading.Thread(name="%sFAMThread" % setup['filemonitor'],
target=self._file_monitor_thread)
+ self.perflog_thread = None
+ if self.setup['perflog']:
+ self.perflog_thread = \
+ threading.Thread(name="PerformanceLoggingThread",
+ target=self._perflog_thread)
+
#: A :func:`threading.Lock` for use by
#: :func:`Bcfg2.Server.FileMonitor.FileMonitor.handle_event_set`
self.lock = threading.Lock()
@@ -325,10 +263,6 @@ class BaseCore(object):
#: metadata
self.metadata_cache = Cache()
- if self.debug_flag:
- # enable debugging on everything else.
- self.plugins[plugin].set_debug(self.debug_flag)
-
def plugins_by_type(self, base_cls):
""" Return a list of loaded plugins that match the passed type.
@@ -349,11 +283,24 @@ class BaseCore(object):
if isinstance(plugin, base_cls)],
key=lambda p: (p.sort_order, p.name))
+ def _perflog_thread(self):
+ """ The thread that periodically logs performance statistics
+ to syslog. """
+ self.logger.debug("Performance logging thread starting")
+ while not self.terminate.isSet():
+ self.terminate.wait(self.setup['perflog_interval'])
+ for name, stats in self.get_statistics(None).items():
+ 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
:class:`Bcfg2.Server.FileMonitor.FileMonitor`. This also
queries :class:`Bcfg2.Server.Plugin.interfaces.Version`
plugins for the current revision of the Bcfg2 repo. """
+ self.logger.debug("File monitor thread starting")
famfd = self.fam.fileno()
terminate = self.terminate
while not terminate.isSet():
@@ -367,12 +314,13 @@ class BaseCore(object):
except:
continue
self._update_vcs_revision()
+ self.logger.debug("File monitor thread terminated")
@track_statistics()
def _update_vcs_revision(self):
""" Update the revision of the current configuration on-disk
from the VCS plugin """
- for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Version):
+ for plugin in self.plugins_by_type(Version):
try:
newrev = plugin.get_revision()
if newrev != self.revision:
@@ -384,6 +332,58 @@ class BaseCore(object):
(plugin.name, sys.exc_info()[1]))
self.revision = '-1'
+ def load_plugins(self):
+ """ Load all plugins, setting
+ :attr:`Bcfg2.Server.Core.BaseCore.plugins` and
+ :attr:`Bcfg2.Server.Core.BaseCore.metadata` as side effects.
+ This does not start plugin threads; that is done later, in
+ :func:`Bcfg2.Server.Core.BaseCore.run` """
+ while '' in self.setup['plugins']:
+ self.setup['plugins'].remove('')
+
+ for plugin in self.setup['plugins']:
+ if not plugin in self.plugins:
+ self.init_plugin(plugin)
+
+ # Remove blacklisted plugins
+ for plugin, blacklist in list(self.plugin_blacklist.items()):
+ if len(blacklist) > 0:
+ self.logger.error("The following plugins conflict with %s;"
+ "Unloading %s" % (plugin, blacklist))
+ for plug in blacklist:
+ del self.plugins[plug]
+
+ # Log experimental plugins
+ expl = [plug for plug in list(self.plugins.values())
+ if plug.experimental]
+ if expl:
+ self.logger.info("Loading experimental plugin(s): %s" %
+ (" ".join([x.name for x in expl])))
+ self.logger.info("NOTE: Interfaces subject to change")
+
+ # Log deprecated plugins
+ depr = [plug for plug in list(self.plugins.values())
+ if plug.deprecated]
+ if depr:
+ self.logger.info("Loading deprecated plugin(s): %s" %
+ (" ".join([x.name for x in depr])))
+
+ # Find the metadata plugin and set self.metadata
+ mlist = self.plugins_by_type(Metadata)
+ if len(mlist) >= 1:
+ self.metadata = mlist[0]
+ if len(mlist) > 1:
+ self.logger.error("Multiple Metadata plugins loaded; using %s"
+ % self.metadata)
+ else:
+ self.logger.error("No Metadata plugin loaded; "
+ "failed to instantiate Core")
+ raise CoreInitError("No Metadata Plugin")
+
+ if self.debug_flag:
+ # enable debugging on plugins
+ self.plugins[plugin].set_debug(self.debug_flag)
+
def init_plugin(self, plugin):
""" Import and instantiate a single plugin. The plugin is
stored to :attr:`plugins`.
@@ -397,7 +397,7 @@ class BaseCore(object):
self.logger.debug("Loading plugin %s" % plugin)
try:
mod = getattr(__import__("Bcfg2.Server.Plugins.%s" %
- (plugin)).Server.Plugins, plugin)
+ (plugin)).Server.Plugins, plugin)
except ImportError:
try:
mod = __import__(plugin, globals(), locals(),
@@ -420,6 +420,10 @@ class BaseCore(object):
except PluginInitError:
self.logger.error("Failed to instantiate plugin %s" % plugin,
exc_info=1)
+ except OSError:
+ err = sys.exc_info()[1]
+ self.logger.error("Failed to add a file monitor while "
+ "instantiating plugin %s: %s" % (plugin, err))
except:
self.logger.error("Unexpected instantiation failure for plugin %s"
% plugin, exc_info=1)
@@ -430,8 +434,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):
@@ -464,8 +470,7 @@ class BaseCore(object):
metadata.hostname))
start = time.time()
try:
- for plugin in \
- self.plugins_by_type(Bcfg2.Server.Plugin.ClientRunHooks):
+ for plugin in self.plugins_by_type(ClientRunHooks):
try:
getattr(plugin, hook)(metadata)
except AttributeError:
@@ -496,11 +501,10 @@ class BaseCore(object):
:type data: list of lxml.etree._Element objects
"""
self.logger.debug("Validating structures for %s" % metadata.hostname)
- for plugin in \
- self.plugins_by_type(Bcfg2.Server.Plugin.StructureValidator):
+ for plugin in self.plugins_by_type(StructureValidator):
try:
plugin.validate_structures(metadata, data)
- except Bcfg2.Server.Plugin.ValidationError:
+ except ValidationError:
err = sys.exc_info()[1]
self.logger.error("Plugin %s structure validation failed: %s" %
(plugin.name, err))
@@ -523,10 +527,10 @@ class BaseCore(object):
:type data: list of lxml.etree._Element objects
"""
self.logger.debug("Validating goals for %s" % metadata.hostname)
- for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.GoalValidator):
+ for plugin in self.plugins_by_type(GoalValidator):
try:
plugin.validate_goals(metadata, data)
- except Bcfg2.Server.Plugin.ValidationError:
+ except ValidationError:
err = sys.exc_info()[1]
self.logger.error("Plugin %s goal validation failed: %s" %
(plugin.name, err.message))
@@ -544,8 +548,9 @@ class BaseCore(object):
:returns: list of :class:`lxml.etree._Element` objects
"""
self.logger.debug("Getting structures for %s" % metadata.hostname)
- structures = list(chain(*[struct.BuildStructures(metadata)
- for struct in self.structures]))
+ structures = list(
+ chain(*[struct.BuildStructures(metadata)
+ for struct in self.plugins_by_type(Structure)]))
sbundles = [b.get('name') for b in structures if b.tag == 'Bundle']
missing = [b for b in metadata.bundles if b not in sbundles]
if missing:
@@ -630,8 +635,9 @@ class BaseCore(object):
self.logger.error("Falling back to %s:%s" %
(entry.tag, entry.get('name')))
- glist = [gen for gen in self.generators if
- entry.get('name') in gen.Entries.get(entry.tag, {})]
+ generators = self.plugins_by_type(Generator)
+ glist = [gen for gen in generators
+ if entry.get('name') in gen.Entries.get(entry.tag, {})]
if len(glist) == 1:
return glist[0].Entries[entry.tag][entry.get('name')](entry,
metadata)
@@ -639,8 +645,8 @@ class BaseCore(object):
generators = ", ".join([gen.name for gen in glist])
self.logger.error("%s %s served by multiple generators: %s" %
(entry.tag, entry.get('name'), generators))
- g2list = [gen for gen in self.generators if
- gen.HandlesEntry(entry, metadata)]
+ g2list = [gen for gen in generators
+ if gen.HandlesEntry(entry, metadata)]
try:
if len(g2list) == 1:
return g2list[0].HandleEntry(entry, metadata)
@@ -667,7 +673,7 @@ class BaseCore(object):
revision=self.revision)
try:
meta = self.build_metadata(client)
- except Bcfg2.Server.Plugin.MetadataConsistencyError:
+ except MetadataConsistencyError:
self.logger.error("Metadata consistency error for client %s" %
client)
return lxml.etree.Element("error", type='metadata error')
@@ -714,7 +720,8 @@ class BaseCore(object):
:type event: Bcfg2.Server.FileMonitor.Event
"""
if event.filename != self.cfile:
- print("Got event for unknown file: %s" % event.filename)
+ self.logger.error("Got event for unknown file: %s" %
+ event.filename)
return
if event.code2str() == 'deleted':
return
@@ -743,6 +750,11 @@ class BaseCore(object):
os.chmod(piddir, 493) # 0775
if not self._daemonize():
return False
+
+ # rewrite $HOME. pulp stores its auth creds in ~/.pulp, so
+ # this is necessary to make that work when privileges are
+ # dropped
+ os.environ['HOME'] = pwd.getpwuid(self.setup['daemon_uid'])[5]
else:
os.umask(int(self.setup['umask'], 8))
@@ -751,17 +763,27 @@ class BaseCore(object):
return False
try:
+ self.load_plugins()
+
self.fam.start()
self.fam_thread.start()
self.fam.AddMonitor(self.cfile, self)
+ if self.perflog_thread is not None:
+ self.perflog_thread.start()
- for plug in self.plugins_by_type(Bcfg2.Server.Plugin.Threaded):
+ for plug in self.plugins_by_type(Threaded):
plug.start_threads()
except:
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):
@@ -792,7 +814,7 @@ class BaseCore(object):
"""
self.logger.debug("Getting decision list for %s" % metadata.hostname)
result = []
- for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Decision):
+ for plugin in self.plugins_by_type(Decision):
try:
result.extend(plugin.GetDecisions(metadata, mode))
except:
@@ -811,7 +833,7 @@ class BaseCore(object):
"""
if not hasattr(self, 'metadata'):
# some threads start before metadata is even loaded
- raise Bcfg2.Server.Plugin.MetadataRuntimeError
+ raise MetadataRuntimeError("Metadata not loaded yet")
if self.metadata_cache_mode == 'initial':
# the Metadata plugin handles loading the cached data if
# we're only caching the initial metadata object
@@ -821,10 +843,11 @@ class BaseCore(object):
if not imd:
self.logger.debug("Building metadata for %s" % client_name)
imd = self.metadata.get_initial_metadata(client_name)
- for conn in self.connectors:
+ connectors = self.plugins_by_type(Connector)
+ for conn in connectors:
grps = conn.get_additional_groups(imd)
self.metadata.merge_additional_groups(imd, grps)
- for conn in self.connectors:
+ for conn in connectors:
data = conn.get_additional_data(imd)
self.metadata.merge_additional_data(imd, conn.name, data)
imd.query.by_name = self.build_metadata
@@ -845,7 +868,7 @@ class BaseCore(object):
meta = self.build_metadata(client_name)
state = statistics.find(".//Statistics")
if state.get('version') >= '2.0':
- for plugin in self.statistics:
+ for plugin in self.plugins_by_type(Statistics):
try:
plugin.process_statistics(meta, statistics)
except:
@@ -887,11 +910,11 @@ class BaseCore(object):
meta = self.build_metadata(client)
else:
meta = None
- except Bcfg2.Server.Plugin.MetadataConsistencyError:
+ except MetadataConsistencyError:
err = sys.exc_info()[1]
self.critical_error("Client metadata resolution error for %s: %s" %
(address[0], err))
- except Bcfg2.Server.Plugin.MetadataRuntimeError:
+ except MetadataRuntimeError:
err = sys.exc_info()[1]
self.critical_error('Metadata system runtime failure for %s: %s' %
(address[0], err))
@@ -985,8 +1008,7 @@ class BaseCore(object):
version))
try:
self.metadata.set_version(client, version)
- except (Bcfg2.Server.Plugin.MetadataConsistencyError,
- Bcfg2.Server.Plugin.MetadataRuntimeError):
+ except (MetadataConsistencyError, MetadataRuntimeError):
err = sys.exc_info()[1]
self.critical_error("Unable to set version for %s: %s" %
(client, err))
@@ -1006,7 +1028,7 @@ class BaseCore(object):
client, metadata = self.resolve_client(address, cleanup_cache=True)
self.logger.debug("Getting probes for %s" % client)
try:
- for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Probing):
+ for plugin in self.plugins_by_type(Probing):
for probe in plugin.GetProbes(metadata):
resp.append(probe)
return lxml.etree.tostring(resp,
@@ -1076,11 +1098,10 @@ class BaseCore(object):
self.logger.debug("%s sets its profile to %s" % (client, profile))
try:
self.metadata.set_profile(client, profile, address)
- except (Bcfg2.Server.Plugin.MetadataConsistencyError,
- Bcfg2.Server.Plugin.MetadataRuntimeError):
+ except (MetadataConsistencyError, MetadataRuntimeError):
err = sys.exc_info()[1]
self.critical_error("Unable to assert profile for %s: %s" %
- (client, err))
+ (client, err))
return True
@exposed
@@ -1099,7 +1120,7 @@ class BaseCore(object):
config = self.BuildConfiguration(client)
return lxml.etree.tostring(config,
xml_declaration=False).decode('UTF-8')
- except Bcfg2.Server.Plugin.MetadataConsistencyError:
+ except MetadataConsistencyError:
self.critical_error("Metadata consistency failure for %s" % client)
@exposed
diff --git a/src/lib/Bcfg2/Server/FileMonitor/Fam.py b/src/lib/Bcfg2/Server/FileMonitor/Fam.py
index 253bb2801..09d41038e 100644
--- a/src/lib/Bcfg2/Server/FileMonitor/Fam.py
+++ b/src/lib/Bcfg2/Server/FileMonitor/Fam.py
@@ -51,7 +51,7 @@ class Fam(FileMonitor):
else:
handle = self.filemonitor.monitorFile(path, None)
self.handles[handle.requestID()] = handle
- if obj != None:
+ if obj is not None:
self.users[handle.requestID()] = obj
return handle.requestID()
AddMonitor.__doc__ = FileMonitor.AddMonitor.__doc__
diff --git a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py
index cdd52dbb9..2cdf27ed8 100644
--- a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py
+++ b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py
@@ -3,14 +3,11 @@ support. """
import os
import errno
-import logging
import pyinotify
from Bcfg2.Compat import reduce # pylint: disable=W0622
from Bcfg2.Server.FileMonitor import Event
from Bcfg2.Server.FileMonitor.Pseudo import Pseudo
-LOGGER = logging.getLogger(__name__)
-
class Inotify(Pseudo, pyinotify.ProcessEvent):
""" File monitor backend with `inotify
@@ -123,8 +120,9 @@ class Inotify(Pseudo, pyinotify.ProcessEvent):
try:
watch = self.watchmgr.watches[ievent.wd]
except KeyError:
- LOGGER.error("Error handling event %s for %s: Watch %s not found" %
- (action, ievent.pathname, ievent.wd))
+ self.logger.error("Error handling event %s for %s: "
+ "Watch %s not found" %
+ (action, ievent.pathname, ievent.wd))
return
# FAM-style file monitors return the full path to the parent
# directory that is being watched, relative paths to anything
diff --git a/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py
index 24cd099d0..b1e1adab7 100644
--- a/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py
+++ b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py
@@ -24,6 +24,6 @@ class Pseudo(FileMonitor):
self.events.append(Event(handleID, fname, 'exists'))
self.events.append(Event(handleID, path, 'endExist'))
- if obj != None:
+ if obj is not None:
self.handles[handleID] = obj
return handleID
diff --git a/src/lib/Bcfg2/Server/Lint/Comments.py b/src/lib/Bcfg2/Server/Lint/Comments.py
index 8bfb76461..f028e225e 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,26 +104,54 @@ 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,
"metadata")
- if self.has_all_xincludes("clients.xml"):
- self.check_xml(os.path.join(self.metadata.data, "clients.xml"),
- self.metadata.clients_xml.data,
- "metadata")
+ if hasattr(self.metadata, "clients_xml"):
+ if self.has_all_xincludes("clients.xml"):
+ self.check_xml(os.path.join(self.metadata.data, "clients.xml"),
+ self.metadata.clients_xml.data,
+ "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 +171,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 2a10da417..e49779a10 100644
--- a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
+++ b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
@@ -1,13 +1,13 @@
-""" 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
import lxml.etree
import Bcfg2.Server.Lint
-import Bcfg2.Client.Tools.POSIX
import Bcfg2.Client.Tools.VCS
from Bcfg2.Server.Plugins.Packages import Apt, Yum
+from Bcfg2.Client.Tools.POSIX.base import device_map
try:
from Bcfg2.Server.Plugins.Bundler import BundleTemplateFile
HAS_GENSHI = True
@@ -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,16 +54,16 @@ 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(
Path=dict(
- device=dict(name=is_filename, owner=is_username,
+ device=dict(name=is_filename,
+ owner=is_username,
group=is_username,
- dev_type=lambda v: \
- v in Bcfg2.Client.Tools.POSIX.base.device_map),
+ dev_type=lambda v: v in device_map),
directory=dict(name=is_filename, owner=is_username,
group=is_username, mode=is_octal_mode),
file=dict(name=is_filename, owner=is_username,
@@ -75,7 +76,7 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
permissions=dict(name=is_filename, owner=is_username,
group=is_username, mode=is_octal_mode),
vcs=dict(vcstype=lambda v: (v != 'Path' and
- hasattr(Bcfg2.Client.Tools.VCS,
+ hasattr(Bcfg2.Client.Tools.VCS.VCS,
"Install%s" % v)),
revision=None, sourceurl=None)),
Service={"__any__": dict(name=None),
@@ -87,21 +88,21 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
command=None)},
ACL=dict(
default=dict(scope=lambda v: v in ['user', 'group'],
- perms=lambda v: re.match('^([0-7]|[rwx\-]{0,3}',
+ perms=lambda v: re.match(r'^([0-7]|[rwx\-]{0,3}',
v)),
access=dict(scope=lambda v: v in ['user', 'group'],
- perms=lambda v: re.match('^([0-7]|[rwx\-]{0,3}',
+ perms=lambda v: re.match(r'^([0-7]|[rwx\-]{0,3}',
v)),
- mask=dict(perms=lambda v: re.match('^([0-7]|[rwx\-]{0,3}',
+ mask=dict(perms=lambda v: re.match(r'^([0-7]|[rwx\-]{0,3}',
v))),
Package={"__any__": dict(name=None)},
SEBoolean={None: dict(name=None,
value=lambda v: v in ['on', 'off'])},
SEModule={None: dict(name=None, __text__=None)},
- SEPort={None:
- dict(name=lambda v: re.match(r'^\d+(-\d+)?/(tcp|udp)',
- v),
- selinuxtype=is_selinux_type)},
+ SEPort={
+ None: dict(name=lambda v: re.match(r'^\d+(-\d+)?/(tcp|udp)',
+ v),
+ selinuxtype=is_selinux_type)},
SEFcontext={None: dict(name=None, selinuxtype=is_selinux_type)},
SENode={None: dict(name=lambda v: "/" in v,
selinuxtype=is_selinux_type,
@@ -115,9 +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()
@@ -137,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
@@ -177,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
@@ -196,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 37bc230d1..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",
@@ -40,7 +50,8 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
"NagiosGen/config.xml": "nagiosgen.xsd",
"FileProbes/config.xml": "fileprobes.xsd",
"SSLCA/**/cert.xml": "sslca-cert.xsd",
- "SSLCA/**/key.xml": "sslca-key.xsd"
+ "SSLCA/**/key.xml": "sslca-key.xsd",
+ "GroupLogic/groups.xml": "grouplogic.xsd"
}
self.filelists = {}
@@ -75,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):
@@ -83,17 +94,19 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
else:
self.LintError("properties-schema-not-found",
"No schema found for %s" % filename)
+ # ensure that it at least parses
+ self.parse(filename)
- def validate(self, filename, schemafile, schema=None):
- """validate a file against the given lxml.etree.Schema.
- return True on success, False on failure """
- if schema is None:
- # if no schema object was provided, instantiate one
- schema = self._load_schema(schemafile)
- if not schema:
- return False
+ 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.
+
+ :param filename: The full path to the file to parse
+ :type filename: string
+ :returns: lxml.etree._ElementTree - the parsed data"""
try:
- datafile = lxml.etree.parse(filename)
+ return lxml.etree.parse(filename)
except SyntaxError:
lint = Popen(["xmllint", filename], stdout=PIPE, stderr=STDOUT)
self.LintError("xml-failed-to-parse",
@@ -106,6 +119,27 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
"Failed to open file %s" % filename)
return False
+ def validate(self, filename, schemafile, schema=None):
+ """ 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)
+ if not schema:
+ return False
+ datafile = self.parse(filename)
if not schema.validate(datafile):
cmd = ["xmllint"]
if self.files is None:
@@ -113,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))
@@ -120,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))
@@ -147,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/base.py b/src/lib/Bcfg2/Server/Plugin/base.py
index f7bc08717..c825a57b5 100644
--- a/src/lib/Bcfg2/Server/Plugin/base.py
+++ b/src/lib/Bcfg2/Server/Plugin/base.py
@@ -87,6 +87,10 @@ class Plugin(Debuggable):
#: alphabetically by their name.
sort_order = 500
+ #: Whether or not to automatically create a data directory for
+ #: this plugin
+ create = True
+
#: List of names of methods to be exposed as XML-RPC functions
__rmi__ = Debuggable.__rmi__
@@ -97,15 +101,21 @@ class Plugin(Debuggable):
:param datastore: The path to the Bcfg2 repository on the
filesystem
:type datastore: string
- :raises: :class:`Bcfg2.Server.Plugin.exceptions.PluginInitError`
+ :raises: :exc:`OSError` if adding a file monitor failed;
+ :class:`Bcfg2.Server.Plugin.exceptions.PluginInitError`
+ on other errors
.. autoattribute:: Bcfg2.Server.Plugin.base.Debuggable.__rmi__
"""
+ Debuggable.__init__(self, name=self.name)
self.Entries = {}
self.core = core
self.data = os.path.join(datastore, self.name)
+ if self.create and not os.path.exists(self.data):
+ self.logger.warning("%s: %s does not exist, creating" %
+ (self.name, self.data))
+ os.makedirs(self.data)
self.running = True
- Debuggable.__init__(self, name=self.name)
@classmethod
def init_repo(cls, repo):
@@ -125,5 +135,11 @@ class Plugin(Debuggable):
self.debug_log("Shutting down %s plugin" % self.name)
self.running = False
+ def set_debug(self, debug):
+ for entry in self.Entries.values():
+ if isinstance(entry, Debuggable):
+ entry.set_debug(debug)
+ return Debuggable.set_debug(self, debug)
+
def __str__(self):
return "%s Plugin" % self.__class__.__name__
diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py
index 0b81077a3..81dc1d736 100644
--- a/src/lib/Bcfg2/Server/Plugin/helpers.py
+++ b/src/lib/Bcfg2/Server/Plugin/helpers.py
@@ -25,15 +25,15 @@ except ImportError:
HAS_DJANGO = False
#: A dict containing default metadata for Path entries from bcfg2.conf
-DEFAULT_FILE_METADATA = Bcfg2.Options.OptionParser(dict(
- configfile=Bcfg2.Options.CFILE,
- owner=Bcfg2.Options.MDATA_OWNER,
- group=Bcfg2.Options.MDATA_GROUP,
- mode=Bcfg2.Options.MDATA_MODE,
- secontext=Bcfg2.Options.MDATA_SECONTEXT,
- important=Bcfg2.Options.MDATA_IMPORTANT,
- paranoid=Bcfg2.Options.MDATA_PARANOID,
- sensitive=Bcfg2.Options.MDATA_SENSITIVE))
+DEFAULT_FILE_METADATA = Bcfg2.Options.OptionParser(
+ dict(configfile=Bcfg2.Options.CFILE,
+ owner=Bcfg2.Options.MDATA_OWNER,
+ group=Bcfg2.Options.MDATA_GROUP,
+ mode=Bcfg2.Options.MDATA_MODE,
+ secontext=Bcfg2.Options.MDATA_SECONTEXT,
+ important=Bcfg2.Options.MDATA_IMPORTANT,
+ paranoid=Bcfg2.Options.MDATA_PARANOID,
+ sensitive=Bcfg2.Options.MDATA_SENSITIVE))
DEFAULT_FILE_METADATA.parse([Bcfg2.Options.CFILE.cmd, Bcfg2.Options.CFILE])
del DEFAULT_FILE_METADATA['args']
del DEFAULT_FILE_METADATA['configfile']
@@ -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('owner:(\s)*(?P<owner>\S+)|' +
- 'group:(\s)*(?P<group>\S+)|' +
- 'mode:(\s)*(?P<mode>\w+)|' +
- 'secontext:(\s)*(?P<secontext>\S+)|' +
- 'paranoid:(\s)*(?P<paranoid>\S+)|' +
- 'sensitive:(\s)*(?P<sensitive>\S+)|' +
- 'encoding:(\s)*(?P<encoding>\S+)|' +
- 'important:(\s)*(?P<important>\S+)|' +
- '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):
@@ -193,7 +193,7 @@ class PluginDatabaseModel(object):
app_label = "Server"
-class FileBacked(object):
+class FileBacked(Debuggable):
""" This object caches file data in memory. FileBacked objects are
principally meant to be used as a part of
:class:`Bcfg2.Server.Plugin.helpers.DirectoryBacked`. """
@@ -206,7 +206,7 @@ class FileBacked(object):
changes
:type fam: Bcfg2.Server.FileMonitor.FileMonitor
"""
- object.__init__(self)
+ Debuggable.__init__(self)
#: A string containing the raw data in this file
self.data = ''
@@ -231,10 +231,10 @@ class FileBacked(object):
self.Index()
except IOError:
err = sys.exc_info()[1]
- LOGGER.error("Failed to read file %s: %s" % (self.name, err))
+ self.logger.error("Failed to read file %s: %s" % (self.name, err))
except:
err = sys.exc_info()[1]
- LOGGER.error("Failed to parse file %s: %s" % (self.name, err))
+ self.logger.error("Failed to parse file %s: %s" % (self.name, err))
def Index(self):
""" Index() is called by :func:`HandleEvent` every time the
@@ -246,7 +246,7 @@ class FileBacked(object):
return "%s: %s" % (self.__class__.__name__, self.name)
-class DirectoryBacked(object):
+class DirectoryBacked(Debuggable):
""" DirectoryBacked objects represent a directory that contains
files, represented by objects of the type listed in
:attr:`__child__`, and other directories recursively. It monitors
@@ -280,7 +280,7 @@ class DirectoryBacked(object):
.. -----
.. autoattribute:: __child__
"""
- object.__init__(self)
+ Debuggable.__init__(self)
self.data = os.path.normpath(data)
self.fam = fam
@@ -299,11 +299,29 @@ class DirectoryBacked(object):
self.handles = {}
# Monitor everything in the plugin's directory
+ if not os.path.exists(self.data):
+ self.logger.warning("%s does not exist, creating" % self.data)
+ os.makedirs(self.data)
self.add_directory_monitor('')
+ def set_debug(self, debug):
+ for entry in self.entries.values():
+ if isinstance(entry, Debuggable):
+ entry.set_debug(debug)
+ return Debuggable.set_debug(self, debug)
+
def __getitem__(self, key):
return self.entries[key]
+ def __len__(self):
+ return len(self.entries)
+
+ def __delitem__(self, key):
+ del self.entries[key]
+
+ def __setitem__(self, key, val):
+ self.entries[key] = val
+
def __iter__(self):
return iter(list(self.entries.items()))
@@ -320,7 +338,7 @@ class DirectoryBacked(object):
dirpathname = os.path.join(self.data, relative)
if relative not in self.handles.values():
if not os.path.isdir(dirpathname):
- LOGGER.error("%s is not a directory" % dirpathname)
+ self.logger.error("%s is not a directory" % dirpathname)
return
reqid = self.fam.AddMonitor(dirpathname, self)
self.handles[reqid] = relative
@@ -365,8 +383,8 @@ class DirectoryBacked(object):
return
if event.requestID not in self.handles:
- LOGGER.warn("Got %s event with unknown handle (%s) for %s" %
- (action, event.requestID, event.filename))
+ self.logger.warn("Got %s event with unknown handle (%s) for %s" %
+ (action, event.requestID, event.filename))
return
# Clean up path names
@@ -376,7 +394,7 @@ class DirectoryBacked(object):
event.filename = event.filename[len(self.data) + 1:]
if self.ignore and self.ignore.search(event.filename):
- LOGGER.debug("Ignoring event %s" % event.filename)
+ self.logger.debug("Ignoring event %s" % event.filename)
return
# Calculate the absolute and relative paths this event refers to
@@ -411,19 +429,20 @@ class DirectoryBacked(object):
# class doesn't support canceling, so at least let
# the user know that a restart might be a good
# idea.
- LOGGER.warn("Directory properties for %s changed, please "
- " consider restarting the server" % abspath)
+ self.logger.warn("Directory properties for %s changed, "
+ "please consider restarting the server" %
+ abspath)
else:
# Got a "changed" event for a directory that we
# didn't know about. Go ahead and treat it like a
# "created" event, but log a warning, because this
# is unexpected.
- LOGGER.warn("Got %s event for unexpected dir %s" %
- (action, abspath))
+ self.logger.warn("Got %s event for unexpected dir %s" %
+ (action, abspath))
self.add_directory_monitor(relpath)
else:
- LOGGER.warn("Got unknown dir event %s %s %s" %
- (event.requestID, event.code2str(), abspath))
+ self.logger.warn("Got unknown dir event %s %s %s" %
+ (event.requestID, event.code2str(), abspath))
elif self.patterns.search(event.filename):
if action in ['exists', 'created']:
self.add_entry(relpath, event)
@@ -435,16 +454,15 @@ class DirectoryBacked(object):
# know about. Go ahead and treat it like a
# "created" event, but log a warning, because this
# is unexpected.
- LOGGER.warn("Got %s event for unexpected file %s" %
- (action,
- abspath))
+ self.logger.warn("Got %s event for unexpected file %s" %
+ (action, abspath))
self.add_entry(relpath, event)
else:
- LOGGER.warn("Got unknown file event %s %s %s" %
- (event.requestID, event.code2str(), abspath))
+ self.logger.warn("Got unknown file event %s %s %s" %
+ (event.requestID, event.code2str(), abspath))
else:
- LOGGER.warn("Could not process filename %s; ignoring" %
- event.filename)
+ self.logger.warn("Could not process filename %s; ignoring" %
+ event.filename)
class XMLFileBacked(FileBacked):
@@ -459,7 +477,11 @@ class XMLFileBacked(FileBacked):
#: behavior, set ``__identifier__`` to ``None``.
__identifier__ = 'name'
- def __init__(self, filename, fam=None, should_monitor=False):
+ #: If ``create`` is set, then it overrides the ``create`` argument
+ #: to the constructor.
+ create = None
+
+ def __init__(self, filename, fam=None, should_monitor=False, create=None):
"""
:param filename: The full path to the file to cache and monitor
:type filename: string
@@ -474,6 +496,13 @@ class XMLFileBacked(FileBacked):
:class:`Bcfg2.Server.Plugin.helpers.XMLDirectoryBacked`
object).
:type should_monitor: bool
+ :param create: Create the file if it doesn't exist.
+ ``create`` can be either an
+ :class:`lxml.etree._Element` object, which will
+ be used as initial content, or a string, which
+ will be used as the name of the (empty) tag
+ that will be the initial content of the file.
+ :type create: lxml.etree._Element or string
.. -----
.. autoattribute:: __identifier__
@@ -497,6 +526,21 @@ class XMLFileBacked(FileBacked):
#: "Extra" files included in this file by XInclude.
self.extras = []
+ #: Extra FAM monitors set by this object for files included by
+ #: XInclude.
+ self.extra_monitors = []
+
+ if ((create is not None or self.create not in [None, False]) and
+ not os.path.exists(self.name)):
+ toptag = create or self.create
+ self.logger.warning("%s does not exist, creating" % self.name)
+ if hasattr(toptag, "getroottree"):
+ el = toptag
+ else:
+ el = lxml.etree.Element(toptag)
+ el.getroottree().write(self.name, xml_declaration=False,
+ pretty_print=True)
+
#: Whether or not to monitor this file for changes.
self.should_monitor = should_monitor
if fam and should_monitor:
@@ -528,17 +572,19 @@ class XMLFileBacked(FileBacked):
if not extras:
msg = "%s: %s does not exist, skipping" % (self.name, name)
if el.findall('./%sfallback' % Bcfg2.Server.XI_NAMESPACE):
- LOGGER.debug(msg)
+ self.logger.debug(msg)
else:
- LOGGER.warning(msg)
+ self.logger.warning(msg)
parent = el.getparent()
parent.remove(el)
for extra in extras:
if extra != self.name and extra not in self.extras:
- self.add_monitor(extra)
+ self.extras.append(extra)
lxml.etree.SubElement(parent, xinclude, href=extra)
self._follow_xincludes(fname=extra)
+ if extra not in self.extra_monitors:
+ self.add_monitor(extra)
def Index(self):
self.xdata = lxml.etree.XML(self.data, base_url=self.name,
@@ -550,7 +596,8 @@ class XMLFileBacked(FileBacked):
self.xdata.getroottree().xinclude()
except lxml.etree.XIncludeError:
err = sys.exc_info()[1]
- LOGGER.error("XInclude failed on %s: %s" % (self.name, err))
+ self.logger.error("XInclude failed on %s: %s" % (self.name,
+ err))
self.entries = self.xdata.getchildren()
if self.__identifier__ is not None:
@@ -566,7 +613,7 @@ class XMLFileBacked(FileBacked):
:type fpath: string
:returns: None
"""
- self.extras.append(fpath)
+ self.extra_monitors.append(fpath)
if self.fam and self.should_monitor:
self.fam.AddMonitor(fpath, self)
@@ -755,14 +802,14 @@ class InfoNode (INode):
Client="lambda m, e: '%(name)s' == m.hostname and predicate(m, e)",
Group="lambda m, e: '%(name)s' in m.groups and predicate(m, e)",
Path="lambda m, e: ('%(name)s' == e.get('name') or " +
- "'%(name)s' == e.get('realname')) and " +
- "predicate(m, e)")
+ "'%(name)s' == e.get('realname')) and " +
+ "predicate(m, e)")
nraw = dict(
Client="lambda m, e: '%(name)s' != m.hostname and predicate(m, e)",
Group="lambda m, e: '%(name)s' not in m.groups and predicate(m, e)",
Path="lambda m, e: '%(name)s' != e.get('name') and " +
- "'%(name)s' != e.get('realname') and " +
- "predicate(m, e)")
+ "'%(name)s' != e.get('realname') and " +
+ "predicate(m, e)")
containers = ['Group', 'Client', 'Path']
@@ -776,8 +823,8 @@ class XMLSrc(XMLFileBacked):
__cacheobj__ = dict
__priority_required__ = True
- def __init__(self, filename, fam=None, should_monitor=False):
- XMLFileBacked.__init__(self, filename, fam, should_monitor)
+ def __init__(self, filename, fam=None, should_monitor=False, create=None):
+ XMLFileBacked.__init__(self, filename, fam, should_monitor, create)
self.items = {}
self.cache = None
self.pnode = None
@@ -789,7 +836,7 @@ class XMLSrc(XMLFileBacked):
data = open(self.name).read()
except IOError:
msg = "Failed to read file %s: %s" % (self.name, sys.exc_info()[1])
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
self.items = {}
try:
@@ -797,7 +844,7 @@ class XMLSrc(XMLFileBacked):
except lxml.etree.XMLSyntaxError:
msg = "Failed to parse file %s: %s" % (self.name,
sys.exc_info()[1])
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
self.pnode = self.__node__(xdata, self.items)
self.cache = None
@@ -807,7 +854,7 @@ class XMLSrc(XMLFileBacked):
if self.__priority_required__:
msg = "Got bogus priority %s for file %s" % \
(xdata.get('priority'), self.name)
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
del xdata, data
@@ -817,8 +864,8 @@ class XMLSrc(XMLFileBacked):
if self.cache is None or self.cache[0] != metadata:
cache = (metadata, self.__cacheobj__())
if self.pnode is None:
- LOGGER.error("Cache method called early for %s; "
- "forcing data load" % self.name)
+ self.logger.error("Cache method called early for %s; "
+ "forcing data load" % self.name)
self.HandleEvent()
return
self.pnode.Match(metadata, cache[1])
@@ -842,7 +889,7 @@ class XMLDirectoryBacked(DirectoryBacked):
#: Only track and include files whose names (not paths) match this
#: compiled regex.
- patterns = re.compile('^.*\.xml$')
+ patterns = re.compile(r'^.*\.xml$')
#: The type of child objects to create for files contained within
#: the directory that is tracked. Default is
@@ -1111,7 +1158,7 @@ class EntrySet(Debuggable):
#: file is encountered that does not match the ``basename``
#: argument passed to the constructor or ``ignore``, then a
#: warning will be produced.
- ignore = re.compile("^(\.#.*|.*~|\\..*\\.(sw[px])|.*\\.genshi_include)$")
+ ignore = re.compile(r'^(\.#.*|.*~|\..*\.(sw[px])|.*\.genshi_include)$')
# The ``basename`` argument passed to the constructor will be
#: processed as a string that contains a regular expression (i.e.,
@@ -1174,8 +1221,8 @@ class EntrySet(Debuggable):
base_pat = basename
else:
base_pat = re.escape(basename)
- pattern = '(.*/)?%s(\.((H_(?P<hostname>\S+))|' % base_pat
- pattern += '(G(?P<prio>\d+)_(?P<group>\S+))))?$'
+ pattern = r'(.*/)?' + base_pat + \
+ r'(\.((H_(?P<hostname>\S+))|(G(?P<prio>\d+)_(?P<group>\S+))))?$'
#: ``specific`` is a regular expression that is used to
#: determine the specificity of a file in this entry set. It
@@ -1254,8 +1301,8 @@ class EntrySet(Debuggable):
self.entry_init(event)
else:
if event.filename not in self.entries:
- LOGGER.warning("Got %s event for unknown file %s" %
- (action, event.filename))
+ self.logger.warning("Got %s event for unknown file %s" %
+ (action, event.filename))
if action == 'changed':
# received a bogus changed event; warn, but treat
# it like a created event
@@ -1291,7 +1338,7 @@ class EntrySet(Debuggable):
entry_type = self.entry_type
if event.filename in self.entries:
- LOGGER.warn("Got duplicate add for %s" % event.filename)
+ self.logger.warn("Got duplicate add for %s" % event.filename)
else:
fpath = os.path.join(self.path, event.filename)
try:
@@ -1299,8 +1346,8 @@ class EntrySet(Debuggable):
specific=specific)
except SpecificityError:
if not self.ignore.match(event.filename):
- LOGGER.error("Could not process filename %s; ignoring" %
- fpath)
+ self.logger.error("Could not process filename %s; ignoring"
+ % fpath)
return
self.entries[event.filename] = entry_type(fpath, spec,
self.encoding)
@@ -1365,8 +1412,8 @@ class EntrySet(Debuggable):
for line in open(fpath).readlines():
match = INFO_REGEX.match(line)
if not match:
- LOGGER.warning("Failed to match line in %s: %s" % (fpath,
- line))
+ self.logger.warning("Failed to match line in %s: %s" %
+ (fpath, line))
continue
else:
mgd = match.groupdict()
@@ -1450,8 +1497,6 @@ class GroupSpool(Plugin, Generator):
def __init__(self, core, datastore):
Plugin.__init__(self, core, datastore)
Generator.__init__(self)
- if self.data[-1] == '/':
- self.data = self.data[:-1]
#: See :class:`Bcfg2.Server.Plugins.interfaces.Generator` for
#: details on the Entries attribute.
diff --git a/src/lib/Bcfg2/Server/Plugin/interfaces.py b/src/lib/Bcfg2/Server/Plugin/interfaces.py
index cb996b1ca..222b94fe3 100644
--- a/src/lib/Bcfg2/Server/Plugin/interfaces.py
+++ b/src/lib/Bcfg2/Server/Plugin/interfaces.py
@@ -286,6 +286,8 @@ class Statistics(Plugin):
you should avoid using Statistics and use
:class:`ThreadedStatistics` instead."""
+ create = False
+
def process_statistics(self, client, xdata):
""" Process the given XML statistics data for the specified
client.
@@ -337,12 +339,11 @@ class ThreadedStatistics(Statistics, Threaded, threading.Thread):
pending_data = []
try:
while not self.work_queue.empty():
- (metadata, data) = self.work_queue.get_nowait()
- pending_data.append(
- (metadata.hostname,
- lxml.etree.tostring(
- data,
- xml_declaration=False).decode("UTF-8")))
+ (metadata, xdata) = self.work_queue.get_nowait()
+ data = \
+ lxml.etree.tostring(xdata,
+ xml_declaration=False).decode("UTF-8")
+ pending_data.append((metadata.hostname, data))
except Empty:
pass
@@ -409,7 +410,7 @@ class ThreadedStatistics(Statistics, Threaded, threading.Thread):
def run(self):
if not self._load():
return
- while not self.terminate.isSet() and self.work_queue != None:
+ while not self.terminate.isSet() and self.work_queue is not None:
try:
(client, xdata) = self.work_queue.get(block=True, timeout=2)
except Empty:
@@ -419,7 +420,7 @@ class ThreadedStatistics(Statistics, Threaded, threading.Thread):
self.logger.error("ThreadedStatistics: %s" % err)
continue
self.handle_statistic(client, xdata)
- if self.work_queue != None and not self.work_queue.empty():
+ if self.work_queue is not None and not self.work_queue.empty():
self._save()
def process_statistics(self, metadata, data):
@@ -527,6 +528,8 @@ class GoalValidator(object):
class Version(Plugin):
""" Version plugins interact with various version control systems. """
+ create = False
+
#: The path to the VCS metadata file or directory, relative to the
#: base of the Bcfg2 repository. E.g., for Subversion this would
#: be ".svn"
diff --git a/src/lib/Bcfg2/Server/Plugins/Base.py b/src/lib/Bcfg2/Server/Plugins/Base.py
index d662da60a..a18204d60 100644
--- a/src/lib/Bcfg2/Server/Plugins/Base.py
+++ b/src/lib/Bcfg2/Server/Plugins/Base.py
@@ -20,13 +20,8 @@ class Base(Bcfg2.Server.Plugin.Plugin,
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Structure.__init__(self)
- try:
- Bcfg2.Server.Plugin.XMLDirectoryBacked.__init__(self,
- self.data,
- self.core.fam)
- except OSError:
- self.logger.error("Failed to load Base repository")
- raise Bcfg2.Server.Plugin.PluginInitError
+ Bcfg2.Server.Plugin.XMLDirectoryBacked.__init__(self, self.data,
+ self.core.fam)
def BuildStructures(self, metadata):
"""Build structures for client described by metadata."""
diff --git a/src/lib/Bcfg2/Server/Plugins/Bundler.py b/src/lib/Bcfg2/Server/Plugins/Bundler.py
index 7030c1574..eef176cca 100644
--- a/src/lib/Bcfg2/Server/Plugins/Bundler.py
+++ b/src/lib/Bcfg2/Server/Plugins/Bundler.py
@@ -13,7 +13,7 @@ import Bcfg2.Server.Lint
try:
import genshi.template.base
- import Bcfg2.Server.Plugins.TGenshi
+ from Bcfg2.Server.Plugins.TGenshi import removecomment, TemplateFile
HAS_GENSHI = True
except ImportError:
HAS_GENSHI = False
@@ -34,14 +34,12 @@ class BundleFile(Bcfg2.Server.Plugin.StructFile):
if HAS_GENSHI:
- class BundleTemplateFile(Bcfg2.Server.Plugins.TGenshi.TemplateFile,
+ class BundleTemplateFile(TemplateFile,
Bcfg2.Server.Plugin.StructFile):
""" Representation of a Genshi-templated bundle XML file """
def __init__(self, name, specific, encoding):
- Bcfg2.Server.Plugins.TGenshi.TemplateFile.__init__(self, name,
- specific,
- encoding)
+ TemplateFile.__init__(self, name, specific, encoding)
Bcfg2.Server.Plugin.StructFile.__init__(self, name)
self.logger = logging.getLogger(name)
@@ -52,9 +50,9 @@ if HAS_GENSHI:
msg = "No parsed template information for %s" % self.name
self.logger.error(msg)
raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
- stream = self.template.generate(metadata=metadata,
- repo=SETUP['repo']).filter(
- Bcfg2.Server.Plugins.TGenshi.removecomment)
+ stream = self.template.generate(
+ metadata=metadata,
+ repo=SETUP['repo']).filter(removecomment)
data = lxml.etree.XML(stream.render('xml',
strip_whitespace=False),
parser=Bcfg2.Server.XMLParser)
@@ -85,23 +83,15 @@ class Bundler(Bcfg2.Server.Plugin.Plugin,
""" The bundler creates dependent clauses based on the
bundle/translation scheme from Bcfg1. """
__author__ = 'bcfg-dev@mcs.anl.gov'
- patterns = re.compile('^(?P<name>.*)\.(xml|genshi)$')
+ patterns = re.compile(r'^(?P<name>.*)\.(xml|genshi)$')
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Structure.__init__(self)
self.encoding = core.setup['encoding']
self.__child__ = self.template_dispatch
- try:
- Bcfg2.Server.Plugin.XMLDirectoryBacked.__init__(self,
- self.data,
- self.core.fam)
- except OSError:
- err = sys.exc_info()[1]
- msg = "Failed to load Bundle repository %s: %s" % (self.data, err)
- self.logger.error(msg)
- raise Bcfg2.Server.Plugin.PluginInitError(msg)
-
+ Bcfg2.Server.Plugin.XMLDirectoryBacked.__init__(self, self.data,
+ self.core.fam)
global SETUP
SETUP = core.setup
@@ -154,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
@@ -171,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
@@ -190,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/CfgGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
index c2e5afbad..83a5c1165 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
@@ -90,7 +90,7 @@ class CfgGenshiGenerator(CfgGenerator):
#: exception in a Genshi template so we can provide a decent error
#: message that actually tells the end user where an error
#: occurred.
- pyerror_re = re.compile('<\w+ u?[\'"](.*?)\s*\.\.\.[\'"]>')
+ pyerror_re = re.compile(r'<\w+ u?[\'"](.*?)\s*\.\.\.[\'"]>')
def __init__(self, fname, spec, encoding):
CfgGenerator.__init__(self, fname, spec, encoding)
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py
index aaeb65cd6..c7b62f352 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py
@@ -48,9 +48,8 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile):
if (HAS_CRYPTO and
SETUP.cfp.has_section("sshkeys") and
SETUP.cfp.has_option("sshkeys", "passphrase")):
- return Bcfg2.Encryption.get_passphrases(SETUP)[SETUP.cfp.get(
- "sshkeys",
- "passphrase")]
+ return Bcfg2.Encryption.get_passphrases(SETUP)[
+ SETUP.cfp.get("sshkeys", "passphrase")]
return None
def handle_event(self, event):
@@ -70,7 +69,7 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile):
the given client metadata, and may be obtained by
doing ``self.XMLMatch(metadata)``
:type spec: lxml.etree._Element
- :returns: None
+ :returns: string - The filename of the private key
"""
if spec is None:
spec = self.XMLMatch(metadata)
@@ -141,7 +140,6 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile):
if spec is None:
spec = self.XMLMatch(metadata)
category = spec.get("category", self.category)
- print("category=%s" % category)
if category is None:
per_host_default = "true"
else:
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
index ec3ba222c..154cd5e63 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
@@ -35,6 +35,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):
@@ -87,7 +105,7 @@ class CfgBaseFileMatcher(Bcfg2.Server.Plugin.SpecificData,
Bcfg2.Server.Plugin.Debuggable.__init__(self)
self.encoding = encoding
__init__.__doc__ = Bcfg2.Server.Plugin.SpecificData.__init__.__doc__ + \
-"""
+ """
.. -----
.. autoattribute:: CfgBaseFileMatcher.__basenames__
.. autoattribute:: CfgBaseFileMatcher.__extensions__
@@ -111,12 +129,12 @@ class CfgBaseFileMatcher(Bcfg2.Server.Plugin.SpecificData,
components = ['^(?P<basename>%s)' % '|'.join(re.escape(b)
for b in basenames)]
if cls.__specific__:
- components.append('(|\\.H_(?P<hostname>\S+?)|' +
- '\.G(?P<prio>\d+)_(?P<group>\S+?))')
+ components.append(r'(|\.H_(?P<hostname>\S+?)|' +
+ r'\.G(?P<prio>\d+)_(?P<group>\S+?))')
if cls.__extensions__:
- components.append('\\.(?P<extension>%s)' %
- '|'.join(cls.__extensions__))
- components.append('$')
+ components.append(r'\.(?P<extension>%s)' %
+ r'|'.join(cls.__extensions__))
+ components.append(r'$')
return re.compile("".join(components))
@classmethod
@@ -459,7 +477,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 +485,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 +501,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 +519,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':
@@ -580,10 +581,18 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
def bind_entry(self, entry, metadata):
self.bind_info_to_entry(entry, metadata)
- data = self._generate_data(entry, metadata)
-
- for fltr in self.get_handlers(metadata, CfgFilter):
- data = fltr.modify_data(entry, metadata, data)
+ data, generator = self._generate_data(entry, metadata)
+
+ if generator is not None:
+ # apply no filters if the data was created by a CfgCreator
+ for fltr in self.get_handlers(metadata, CfgFilter):
+ if fltr.specific <= generator.specific:
+ # only apply filters that are as specific or more
+ # specific than the generator used for this entry.
+ # Note that specificity comparison is backwards in
+ # this sense, since it's designed to sort from
+ # most specific to least specific.
+ data = fltr.modify_data(entry, metadata, data)
if SETUP['validate']:
try:
@@ -599,6 +608,8 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
else:
try:
if not isinstance(data, unicode):
+ if not isinstance(data, str):
+ data = data.decode('utf-8')
data = u_str(data, self.encoding)
except UnicodeDecodeError:
msg = "Failed to decode %s: %s" % (entry.get('name'),
@@ -690,7 +701,9 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
:type entry: lxml.etree._Element
:param metadata: The client metadata to generate data for
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :returns: string - the data for the entry
+ :returns: tuple of (string, generator) - the data for the
+ entry and the generator used to generate it (or
+ None, if data was created)
"""
try:
generator = self.best_matching(metadata,
@@ -699,7 +712,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
except PluginExecutionError:
# if no creators or generators exist, _create_data()
# raises an appropriate exception
- return self._create_data(entry, metadata)
+ return (self._create_data(entry, metadata), None)
if entry.get('mode').lower() == 'inherit':
# use on-disk permissions
@@ -709,7 +722,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
entry.set('mode',
oct_mode(stat.S_IMODE(os.stat(fname).st_mode)))
try:
- return generator.get_data(entry, metadata)
+ return (generator.get_data(entry, metadata), generator)
except:
msg = "Cfg: Error rendering %s: %s" % (entry.get("name"),
sys.exc_info()[1])
@@ -758,8 +771,8 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
pass
if not rv or not rv[0].hostname:
- rv.append(Bcfg2.Server.Plugin.Specificity(
- hostname=metadata.hostname))
+ rv.append(
+ Bcfg2.Server.Plugin.Specificity(hostname=metadata.hostname))
return rv
def build_filename(self, specific):
@@ -884,12 +897,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 """
@@ -923,3 +939,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/Cvs.py b/src/lib/Bcfg2/Server/Plugins/Cvs.py
index ba1559a1a..22cacaa76 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cvs.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cvs.py
@@ -20,9 +20,9 @@ class Cvs(Bcfg2.Server.Plugin.Version):
"""Read cvs revision information for the Bcfg2 repository."""
try:
data = Popen("env LC_ALL=C cvs log",
- shell=True,
- cwd=self.vcs_root,
- stdout=PIPE).stdout.readlines()
+ shell=True,
+ cwd=self.vcs_root,
+ stdout=PIPE).stdout.readlines()
return data[3].strip('\n')
except IndexError:
msg = "Failed to read CVS log"
diff --git a/src/lib/Bcfg2/Server/Plugins/Darcs.py b/src/lib/Bcfg2/Server/Plugins/Darcs.py
index 0033e00f3..b4abafb0e 100644
--- a/src/lib/Bcfg2/Server/Plugins/Darcs.py
+++ b/src/lib/Bcfg2/Server/Plugins/Darcs.py
@@ -20,9 +20,9 @@ class Darcs(Bcfg2.Server.Plugin.Version):
"""Read Darcs changeset information for the Bcfg2 repository."""
try:
data = Popen("env LC_ALL=C darcs changes",
- shell=True,
- cwd=self.vcs_root,
- stdout=PIPE).stdout.readlines()
+ shell=True,
+ cwd=self.vcs_root,
+ stdout=PIPE).stdout.readlines()
revision = data[0].strip('\n')
except:
msg = "Failed to read darcs repository"
diff --git a/src/lib/Bcfg2/Server/Plugins/Decisions.py b/src/lib/Bcfg2/Server/Plugins/Decisions.py
index eae18fdfe..66f299bc9 100644
--- a/src/lib/Bcfg2/Server/Plugins/Decisions.py
+++ b/src/lib/Bcfg2/Server/Plugins/Decisions.py
@@ -2,7 +2,6 @@
blacklist certain entries. """
import os
-import sys
import lxml.etree
import Bcfg2.Server.Plugin
@@ -40,18 +39,10 @@ class Decisions(Bcfg2.Server.Plugin.EntrySet,
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Decision.__init__(self)
-
Bcfg2.Server.Plugin.EntrySet.__init__(self, '(white|black)list',
- self.data,
- DecisionFile,
+ self.data, DecisionFile,
core.setup['encoding'])
- try:
- core.fam.AddMonitor(self.data, self)
- except OSError:
- err = sys.exc_info()[1]
- msg = 'Adding filemonitor for %s failed: %s' % (self.data, err)
- self.logger.error(msg)
- raise Bcfg2.Server.Plugin.PluginInitError(msg)
+ core.fam.AddMonitor(self.data, self)
def HandleEvent(self, event):
""" Handle events on Decision files by passing them off to
diff --git a/src/lib/Bcfg2/Server/Plugins/FileProbes.py b/src/lib/Bcfg2/Server/Plugins/FileProbes.py
index 5ec0d7280..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"
@@ -67,7 +71,8 @@ class FileProbes(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.StructFile(os.path.join(self.data,
'config.xml'),
fam=core.fam,
- should_monitor=True)
+ should_monitor=True,
+ create=self.name)
self.entries = dict()
self.probes = dict()
@@ -225,11 +230,8 @@ class FileProbes(Bcfg2.Server.Plugin.Plugin,
root = lxml.etree.Element("FileInfo")
root.append(info)
try:
- open(infoxml,
- "w").write(
- lxml.etree.tostring(root,
- xml_declaration=False,
- pretty_print=True).decode('UTF-8'))
+ root.getroottree().write(infoxml, xml_declaration=False,
+ pretty_print=True)
except IOError:
err = sys.exc_info()[1]
self.logger.error("Could not write %s: %s" % (infoxml, err))
diff --git a/src/lib/Bcfg2/Server/Plugins/Fossil.py b/src/lib/Bcfg2/Server/Plugins/Fossil.py
index f6735df12..6165ac651 100644
--- a/src/lib/Bcfg2/Server/Plugins/Fossil.py
+++ b/src/lib/Bcfg2/Server/Plugins/Fossil.py
@@ -20,11 +20,11 @@ class Fossil(Bcfg2.Server.Plugin.Version):
"""Read fossil revision information for the Bcfg2 repository."""
try:
data = Popen("env LC_ALL=C fossil info",
- shell=True,
- cwd=self.vcs_root,
- stdout=PIPE).stdout.readlines()
- revline = [line.split(': ')[1].strip() for line in data if \
- line.split(': ')[0].strip() == 'checkout'][-1]
+ shell=True,
+ cwd=self.vcs_root,
+ stdout=PIPE).stdout.readlines()
+ revline = [line.split(': ')[1].strip() for line in data
+ if line.split(': ')[0].strip() == 'checkout'][-1]
return revline.split(' ')[0]
except IndexError:
msg = "Failed to read fossil info"
diff --git a/src/lib/Bcfg2/Server/Plugins/Git.py b/src/lib/Bcfg2/Server/Plugins/Git.py
index c8362db41..44971aba7 100644
--- a/src/lib/Bcfg2/Server/Plugins/Git.py
+++ b/src/lib/Bcfg2/Server/Plugins/Git.py
@@ -44,7 +44,7 @@ class Git(Version):
else:
cmd = ["git", "--git-dir", self.vcs_path,
"--work-tree", self.vcs_root, "rev-parse", "HEAD"]
- self.debug_log("Git: Running cmd")
+ self.debug_log("Git: Running %s" % cmd)
proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
rv, err = proc.communicate()
if proc.wait():
diff --git a/src/lib/Bcfg2/Server/Plugins/GroupLogic.py b/src/lib/Bcfg2/Server/Plugins/GroupLogic.py
new file mode 100644
index 000000000..810b273af
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/GroupLogic.py
@@ -0,0 +1,47 @@
+""" GroupLogic is a connector plugin that lets you use an XML Genshi
+template to dynamically set additional groups for clients. """
+
+import os
+import lxml.etree
+import Bcfg2.Server.Plugin
+try:
+ from Bcfg2.Server.Plugins.Bundler import BundleTemplateFile
+except ImportError:
+ # BundleTemplateFile missing means that genshi is missing. we
+ # import genshi to get the _real_ error
+ import genshi # pylint: disable=W0611
+
+
+class GroupLogicConfig(BundleTemplateFile):
+ """ Representation of the GroupLogic groups.xml file """
+ create = lxml.etree.Element("GroupLogic",
+ nsmap=dict(py="http://genshi.edgewall.org/"))
+
+ def __init__(self, name, fam):
+ BundleTemplateFile.__init__(self, name,
+ Bcfg2.Server.Plugin.Specificity(), None)
+ self.fam = fam
+ self.should_monitor = True
+ self.fam.AddMonitor(self.name, self)
+
+ def _match(self, item, metadata):
+ if item.tag == 'Group' and not len(item.getchildren()):
+ return [item]
+ return BundleTemplateFile._match(self, item, metadata)
+
+
+class GroupLogic(Bcfg2.Server.Plugin.Plugin,
+ Bcfg2.Server.Plugin.Connector):
+ """ GroupLogic is a connector plugin that lets you use an XML
+ Genshi template to dynamically set additional groups for
+ clients. """
+
+ def __init__(self, core, datastore):
+ Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
+ Bcfg2.Server.Plugin.Connector.__init__(self)
+ self.config = GroupLogicConfig(os.path.join(self.data, "groups.xml"),
+ core.fam)
+
+ def get_additional_groups(self, metadata):
+ return [el.get("name")
+ for el in self.config.get_xml_value(metadata).findall("Group")]
diff --git a/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py b/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
index 5716a134f..09685d972 100644
--- a/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
+++ b/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
@@ -3,7 +3,6 @@
import os
import re
import sys
-import logging
import Bcfg2.Server.Lint
import Bcfg2.Server.Plugin
from Bcfg2.Utils import PackedDigitRange
@@ -16,16 +15,16 @@ class PatternMap(object):
self.pattern = pattern
self.rangestr = rangestr
self.groups = groups
- if pattern != None:
+ if pattern is not None:
self.re = re.compile(pattern)
self.process = self.process_re
- elif rangestr != None:
+ elif rangestr is not None:
if '\\' in rangestr:
raise Exception("Backslashes are not allowed in NameRanges")
range_finder = r'\[\[[\d\-,]+\]\]'
self.process = self.process_range
- self.re = re.compile('^' + re.sub(range_finder, '(\d+)',
- rangestr))
+ self.re = re.compile(r'^' + re.sub(range_finder, r'(\d+)',
+ rangestr))
dmatcher = re.compile(re.sub(range_finder,
r'\[\[([\d\-,]+)\]\]',
rangestr))
@@ -67,6 +66,7 @@ class PatternMap(object):
class PatternFile(Bcfg2.Server.Plugin.XMLFileBacked):
""" representation of GroupPatterns config.xml """
__identifier__ = None
+ create = 'GroupPatterns'
def __init__(self, filename, core=None):
try:
@@ -77,7 +77,6 @@ class PatternFile(Bcfg2.Server.Plugin.XMLFileBacked):
should_monitor=True)
self.core = core
self.patterns = []
- self.logger = logging.getLogger(self.__class__.__name__)
def Index(self):
Bcfg2.Server.Plugin.XMLFileBacked.Index(self)
@@ -130,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 8fb3a0998..4ed3dede5 100644
--- a/src/lib/Bcfg2/Server/Plugins/Metadata.py
+++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py
@@ -40,6 +40,8 @@ if HAS_DJANGO:
""" dict-like object to make it easier to access client bcfg2
versions from the database """
+ create = False
+
def __getitem__(self, key):
try:
return MetadataClientModel.objects.get(hostname=key).version
@@ -75,6 +77,7 @@ if HAS_DJANGO:
yield client.hostname
def keys(self):
+ """ Get keys for the mapping """
return [c.hostname for c in MetadataClientModel.objects.all()]
def __contains__(self, key):
@@ -94,9 +97,11 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked):
# then we immediately set should_monitor to the proper value,
# so that XInclude'd files get properly watched
fpath = os.path.join(metadata.data, basefile)
+ toptag = os.path.splitext(basefile)[0].title()
Bcfg2.Server.Plugin.XMLFileBacked.__init__(self, fpath,
fam=metadata.core.fam,
- should_monitor=False)
+ should_monitor=False,
+ create=toptag)
self.should_monitor = watch_clients
self.metadata = metadata
self.basefile = basefile
@@ -326,6 +331,11 @@ class ClientMetadata(object):
return grp
return ''
+ def __repr__(self):
+ return "%s(%s, profile=%s, groups=%s)" % (self.__class__.__name__,
+ self.hostname,
+ self.profile, self.groups)
+
class MetadataQuery(object):
""" This class provides query methods for the metadata of all
@@ -439,7 +449,7 @@ class MetadataQuery(object):
return [self.by_name(name) for name in self.all_clients()]
-class MetadataGroup(tuple):
+class MetadataGroup(tuple): # pylint: disable=E0012,R0924
""" representation of a metadata group. basically just a named tuple """
# pylint: disable=R0913,W0613
@@ -549,6 +559,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.) """
@@ -595,7 +611,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
def _add_xdata(self, config, tag, name, attribs=None, alias=False):
""" Generic method to add XML data (group, client, etc.) """
node = self._search_xdata(tag, name, config.xdata, alias=alias)
- if node != None:
+ if node is not None:
raise Bcfg2.Server.Plugin.MetadataConsistencyError("%s \"%s\" "
"already exists"
% (tag, name))
@@ -655,7 +671,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
def _update_xdata(self, config, tag, name, attribs, alias=False):
""" Generic method to modify XML data (group, client, etc.) """
node = self._search_xdata(tag, name, config.xdata, alias=alias)
- if node == None:
+ if node is None:
self.logger.error("%s \"%s\" does not exist" % (tag, name))
raise Bcfg2.Server.Plugin.MetadataConsistencyError
xdict = config.find_xml_for_xpath('.//%s[@name="%s"]' %
@@ -672,7 +688,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
"""Update a groups attributes."""
if self._use_db:
msg = "Metadata does not support updating groups with " + \
- "use_database enabled"
+ "use_database enabled"
self.logger.error(msg)
raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
else:
@@ -700,7 +716,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
def _remove_xdata(self, config, tag, name):
""" Generic method to remove XML data (group, client, etc.) """
node = self._search_xdata(tag, name, config.xdata)
- if node == None:
+ if node is None:
self.logger.error("%s \"%s\" does not exist" % (tag, name))
raise Bcfg2.Server.Plugin.MetadataConsistencyError
xdict = config.find_xml_for_xpath('.//%s[@name="%s"]' %
@@ -936,16 +952,12 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
if group not in self.groups:
self.debug_log("Client %s set as nonexistent group %s"
% (client, group))
- for gname, ginfo in list(self.groups.items()):
- for group in ginfo.groups:
- if group not in self.groups:
- self.debug_log("Group %s set as nonexistent group %s" %
- (gname, 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))
+ self.logger.info("Asserting client %s profile to %s" % (client,
+ profile))
if False in list(self.states.values()):
raise Bcfg2.Server.Plugin.MetadataRuntimeError("Metadata has not "
"been read yet")
@@ -954,7 +966,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)
@@ -996,19 +1008,18 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
self.clients_xml.write()
def set_version(self, client, version):
- """Set group parameter for provided client."""
- if client in self.clients:
- if client not in self.versions or version != self.versions[client]:
- self.logger.info("Setting client %s version to %s" %
- (client, version))
- if not self._use_db:
- self.update_client(client, dict(version=version))
- self.clients_xml.write()
- self.versions[client] = version
- else:
- msg = "Cannot set version on non-existent client %s" % client
- self.logger.error(msg)
- raise Bcfg2.Server.Plugin.MetadataConsistencyError(msg)
+ """Set version for provided client."""
+ if client not in self.clients:
+ # this creates the client as a side effect
+ self.get_initial_metadata(client)
+
+ if client not in self.versions or version != self.versions[client]:
+ self.logger.info("Setting client %s version to %s" % (client,
+ version))
+ if not self._use_db:
+ self.update_client(client, dict(version=version))
+ self.clients_xml.write()
+ self.versions[client] = version
def resolve_client(self, addresspair, cleanup_cache=False):
"""Lookup address locally or in DNS to get a hostname."""
@@ -1085,7 +1096,6 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
raise Bcfg2.Server.Plugin.MetadataRuntimeError("Metadata has not "
"been read yet")
client = client.lower()
-
if client in self.core.metadata_cache:
return self.core.metadata_cache[client]
@@ -1096,6 +1106,29 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
categories = dict()
profile = None
+ def _add_group(grpname):
+ """ Add a group to the set of groups for this client.
+ Handles setting categories and category suppression.
+ Returns the new profile for the client (which might be
+ unchanged). """
+ groups.add(grpname)
+ if grpname in self.groups:
+ group = self.groups[grpname]
+ category = group.category
+ if category:
+ if category in categories:
+ self.logger.warning("%s: Group %s suppressed by "
+ "category %s; %s already a member "
+ "of %s" %
+ (self.name, grpname, category,
+ client, categories[category]))
+ return
+ categories[category] = grpname
+ if not profile and group.is_profile:
+ return grpname
+ else:
+ return profile
+
if client not in self.clients:
pgroup = None
if client in self.clientgroups:
@@ -1104,42 +1137,30 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
pgroup = self.default
if pgroup:
- self.set_profile(client, pgroup, (None, None))
- groups.add(pgroup)
- category = self.groups[pgroup].category
- if category:
- categories[category] = pgroup
- if (pgroup in self.groups and self.groups[pgroup].is_profile):
- profile = pgroup
+ 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
self.logger.error(msg)
raise Bcfg2.Server.Plugin.MetadataConsistencyError(msg)
- if client in self.clientgroups:
- for cgroup in self.clientgroups[client]:
- if cgroup in groups:
- continue
- if cgroup not in self.groups:
- self.groups[cgroup] = MetadataGroup(cgroup)
- category = self.groups[cgroup].category
- if category and category in categories:
- self.logger.warning("%s: Group %s suppressed by "
- "category %s; %s already a member "
- "of %s" %
- (self.name, cgroup, category,
- client, categories[category]))
- continue
- if category:
- categories[category] = cgroup
- groups.add(cgroup)
- # favor client groups for setting profile
- if not profile and self.groups[cgroup].is_profile:
- profile = cgroup
+ for cgroup in self.clientgroups.get(client, []):
+ if cgroup in groups:
+ continue
+ if cgroup not in self.groups:
+ self.groups[cgroup] = MetadataGroup(cgroup)
+ profile = _add_group(cgroup)
groups, categories = self._merge_groups(client, groups,
categories=categories)
+ if len(groups) == 0 and self.default:
+ # no initial groups; add the default profile
+ profile = _add_group(self.default)
+ groups, categories = self._merge_groups(client, groups,
+ categories=categories)
+
bundles = set()
for group in groups:
try:
@@ -1466,7 +1487,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()
@@ -1475,6 +1505,7 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
self.duplicate_groups()
self.duplicate_default_groups()
self.duplicate_clients()
+ self.default_is_profile()
@classmethod
def Errors(cls):
@@ -1484,11 +1515,15 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
"non-profile-set-as-profile": "error",
"duplicate-group": "error",
"duplicate-client": "error",
- "multiple-default-groups": "error"}
+ "multiple-default-groups": "error",
+ "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
clientdata = self.metadata.clients_xml.xdata
for el in clientdata.xpath("//Client"):
loc = el.get("location")
@@ -1503,8 +1538,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",
@@ -1512,8 +1547,11 @@ 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
for client in self.metadata.clients_xml.xdata.findall('.//Client'):
profile = client.get("profile")
if profile not in self.metadata.groups:
@@ -1528,20 +1566,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"):
@@ -1553,24 +1579,55 @@ 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
self.duplicate_entries(
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,
"%s %s is defined multiple times:\n%s" %
(etype.title(), ename, "\n".join(els)))
+
+ def default_is_profile(self):
+ """ Ensure that the default group is a profile group. """
+ if (self.metadata.default and
+ not self.metadata.groups[self.metadata.default].is_profile):
+ xdata = \
+ self.metadata.groups_xml.xdata.xpath("//Group[@name='%s']" %
+ self.metadata.default)[0]
+ self.LintError("default-is-not-profile",
+ "Default group is not a profile group:\n%s" %
+ self.RenderXML(xdata))
diff --git a/src/lib/Bcfg2/Server/Plugins/NagiosGen.py b/src/lib/Bcfg2/Server/Plugins/NagiosGen.py
index c39bd4c42..466665382 100644
--- a/src/lib/Bcfg2/Server/Plugins/NagiosGen.py
+++ b/src/lib/Bcfg2/Server/Plugins/NagiosGen.py
@@ -5,26 +5,9 @@ import re
import sys
import glob
import socket
-import logging
import Bcfg2.Server
import Bcfg2.Server.Plugin
-LOGGER = logging.getLogger(__name__)
-
-
-class NagiosGenConfig(Bcfg2.Server.Plugin.StructFile):
- """ NagiosGen config file handler """
-
- def __init__(self, filename, fam):
- # create config.xml if missing
- if not os.path.exists(filename):
- LOGGER.warning("NagiosGen: %s missing. "
- "Creating empty one for you." % filename)
- open(filename, "w").write("<NagiosGen></NagiosGen>")
-
- Bcfg2.Server.Plugin.StructFile.__init__(self, filename, fam=fam,
- should_monitor=True)
-
class NagiosGen(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.Generator):
@@ -36,8 +19,11 @@ class NagiosGen(Bcfg2.Server.Plugin.Plugin,
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Generator.__init__(self)
- self.config = NagiosGenConfig(os.path.join(self.data, 'config.xml'),
- core.fam)
+ self.config = \
+ Bcfg2.Server.Plugin.StructFile(os.path.join(self.data,
+ 'config.xml'),
+ core.fam, should_monitor=True,
+ create=self.name)
self.Entries = {'Path':
{'/etc/nagiosgen.status': self.createhostconfig,
'/etc/nagios/nagiosgen.cfg': self.createserverconfig}}
diff --git a/src/lib/Bcfg2/Server/Plugins/Ohai.py b/src/lib/Bcfg2/Server/Plugins/Ohai.py
index ebc03197e..1ec3cbd60 100644
--- a/src/lib/Bcfg2/Server/Plugins/Ohai.py
+++ b/src/lib/Bcfg2/Server/Plugins/Ohai.py
@@ -2,8 +2,10 @@
operating system using ohai
(http://wiki.opscode.com/display/chef/Ohai) """
-import lxml.etree
import os
+import sys
+import glob
+import lxml.etree
import Bcfg2.Server.Plugin
try:
@@ -31,22 +33,39 @@ class OhaiCache(object):
self.dirname = dirname
self.cache = dict()
+ def hostpath(self, host):
+ """ Get the path to the file that contains Ohai data for the
+ given host """
+ return os.path.join(self.dirname, "%s.json" % host)
+
def __setitem__(self, item, value):
- if value == None:
+ if value is None:
# simply return if the client returned nothing
return
self.cache[item] = json.loads(value)
- open("%s/%s.json" % (self.dirname, item), 'w').write(value)
+ open(self.hostpath(item), 'w').write(value)
def __getitem__(self, item):
if item not in self.cache:
try:
- data = open("%s/%s.json" % (self.dirname, item)).read()
+ data = open(self.hostpath(item)).read()
except:
raise KeyError(item)
self.cache[item] = json.loads(data)
return self.cache[item]
+ def __delitem__(self, item):
+ if item in self.cache:
+ del self.cache[item]
+ try:
+ os.unlink(self.hostpath(item))
+ except:
+ raise IndexError("Could not unlink %s: %s" % (self.hostpath(item),
+ sys.exc_info()[1]))
+
+ def __len__(self):
+ return len(glob.glob(self.hostpath('*')))
+
def __iter__(self):
data = list(self.cache.keys())
data.extend([x[:-5] for x in os.listdir(self.dirname)])
@@ -69,10 +88,6 @@ class Ohai(Bcfg2.Server.Plugin.Plugin,
self.probe = lxml.etree.Element('probe', name='Ohai', source='Ohai',
interpreter='/bin/sh')
self.probe.text = PROBECODE
- try:
- os.stat(self.data)
- except OSError:
- os.makedirs(self.data)
self.cache = OhaiCache(self.data)
def GetProbes(self, _):
diff --git a/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py b/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py
index 490ee6f20..71128d64c 100644
--- a/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py
+++ b/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py
@@ -9,13 +9,15 @@ class POSIXCompat(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.GoalValidator):
"""POSIXCompat is a goal validator plugin for POSIX entries."""
+ create = False
+
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.GoalValidator.__init__(self)
def validate_goals(self, metadata, goals):
"""Verify that we are generating correct old POSIX entries."""
- if metadata.version_info and metadata.version_info > (1, 3, 0, '', 0):
+ if metadata.version_info and metadata.version_info >= (1, 3, 0, '', 0):
# do not care about a client that is _any_ 1.3.0 release
# (including prereleases and RCs)
return
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py b/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
index 27f493677..a82a183d8 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
@@ -30,8 +30,8 @@ class AptCollection(Collection):
""" Get an APT configuration file (i.e., ``sources.list``).
:returns: string """
- lines = ["# This config was generated automatically by the Bcfg2 " \
- "Packages plugin", '']
+ lines = ["# This config was generated automatically by the Bcfg2 "
+ "Packages plugin", '']
for source in self:
if source.rawurl:
@@ -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)
@@ -93,6 +98,8 @@ class AptSource(Source):
self.logger.error("Packages: Failed to read file %s" % fname)
raise
for line in reader.readlines():
+ if not isinstance(line, str):
+ line = line.decode('utf-8')
words = str(line.strip()).split(':', 1)
if words[0] == 'Package':
pkgname = words[1].strip().rstrip()
@@ -104,8 +111,8 @@ class AptSource(Source):
vindex = 0
for dep in words[1].split(','):
if '|' in dep:
- cdeps = [re.sub('\s+', '',
- re.sub('\(.*\)', '', cdep))
+ cdeps = [re.sub(r'\s+', '',
+ re.sub(r'\(.*\)', '', cdep))
for cdep in dep.split('|')]
dyn_dname = "choice-%s-%s-%s" % (pkgname,
barch,
@@ -114,7 +121,7 @@ class AptSource(Source):
bdeps[barch][pkgname].append(dyn_dname)
bprov[barch][dyn_dname] = set(cdeps)
else:
- raw_dep = re.sub('\(.*\)', '', dep)
+ raw_dep = re.sub(r'\(.*\)', '', dep)
raw_dep = raw_dep.rstrip().strip()
bdeps[barch][pkgname].append(raw_dep)
elif words[0] == 'Provides':
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py b/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
index 2735e389a..332f0bbab 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
@@ -7,6 +7,7 @@ import Bcfg2.Server.Plugin
from Bcfg2.Server.Plugins.Packages.Source import SourceInitError
+# pylint: disable=E0012,R0924
class PackagesSources(Bcfg2.Server.Plugin.StructFile,
Bcfg2.Server.Plugin.Debuggable):
""" PackagesSources handles parsing of the
@@ -16,6 +17,7 @@ class PackagesSources(Bcfg2.Server.Plugin.StructFile,
each ``Source`` tag. """
__identifier__ = None
+ create = "Sources"
def __init__(self, filename, cachepath, fam, packages, setup):
"""
@@ -39,14 +41,8 @@ class PackagesSources(Bcfg2.Server.Plugin.StructFile,
If ``sources.xml`` cannot be read
"""
Bcfg2.Server.Plugin.Debuggable.__init__(self)
- try:
- Bcfg2.Server.Plugin.StructFile.__init__(self, filename, fam=fam,
- should_monitor=True)
- except OSError:
- err = sys.exc_info()[1]
- msg = "Packages: Failed to read configuration file: %s" % err
- self.logger.error(msg)
- raise Bcfg2.Server.Plugin.PluginInitError(msg)
+ Bcfg2.Server.Plugin.StructFile.__init__(self, filename, fam=fam,
+ should_monitor=True)
#: The full path to the directory where
#: :class:`Bcfg2.Server.Plugins.Packages.Source.Source` data
@@ -129,7 +125,7 @@ class PackagesSources(Bcfg2.Server.Plugin.StructFile,
""" Create a
:class:`Bcfg2.Server.Plugins.Packages.Source.Source` subclass
object from XML representation of a source in ``sources.xml``.
- ``source_from-xml`` determines the appropriate subclass of
+ ``source_from_xml`` determines the appropriate subclass of
``Source`` to instantiate according to the ``type`` attribute
of the ``Source`` tag.
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
index 985405e65..22073493c 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
@@ -52,8 +52,8 @@ import re
import sys
import Bcfg2.Server.Plugin
from Bcfg2.Compat import HTTPError, HTTPBasicAuthHandler, \
- HTTPPasswordMgrWithDefaultRealm, install_opener, build_opener, \
- urlopen, cPickle, md5
+ HTTPPasswordMgrWithDefaultRealm, install_opener, build_opener, urlopen, \
+ cPickle, md5
def fetch_url(url):
@@ -65,7 +65,7 @@ def fetch_url(url):
:raises: URLError - Failure fetching URL
:returns: string - the content of the page at the given URL """
if '@' in url:
- mobj = re.match('(\w+://)([^:]+):([^@]+)@(.*)$', url)
+ mobj = re.match(r'(\w+://)([^:]+):([^@]+)@(.*)$', url)
if not mobj:
raise ValueError("Invalid URL")
user = mobj.group(2)
@@ -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
@@ -315,7 +319,7 @@ class Source(Bcfg2.Server.Plugin.Debuggable): # pylint: disable=R0902
:raises: OSError - If the saved data cannot be read
:raises: cPickle.UnpicklingError - If the saved data is corrupt """
- data = open(self.cachefile)
+ data = open(self.cachefile, 'rb')
(self.pkgnames, self.deps, self.provides,
self.essentialpkgs) = cPickle.load(data)
@@ -615,7 +619,7 @@ class Source(Bcfg2.Server.Plugin.Debuggable): # pylint: disable=R0902
self.logger.info("Packages: Updating %s" % url)
fname = self.escape_url(url)
try:
- open(fname, 'w').write(fetch_url(url))
+ open(fname, 'wb').write(fetch_url(url))
except ValueError:
self.logger.error("Packages: Bad url string %s" % url)
raise
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
index 6b8ed1f7d..4608bcca5 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
@@ -66,7 +66,7 @@ from Bcfg2.Compat import StringIO, cPickle, HTTPError, URLError, \
# pylint: enable=W0622
from Bcfg2.Server.Plugins.Packages.Collection import Collection
from Bcfg2.Server.Plugins.Packages.Source import SourceInitError, Source, \
- fetch_url
+ fetch_url
LOGGER = logging.getLogger(__name__)
@@ -281,7 +281,7 @@ class YumCollection(Collection):
#: Define a unique cache file for this collection to use
#: for cached yum metadata
self.cachefile = os.path.join(self.cachepath,
- "cache-%s" % self.cachekey)
+ "cache-%s" % self.cachekey)
if not os.path.exists(self.cachefile):
os.mkdir(self.cachefile)
@@ -422,7 +422,7 @@ class YumCollection(Collection):
config.add_section(reponame)
added = True
except ConfigParser.DuplicateSectionError:
- match = re.search("-(\d+)", reponame)
+ match = re.search(r'-(\d+)', reponame)
if match:
rid = int(match.group(1)) + 1
else:
@@ -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
@@ -874,11 +878,16 @@ class YumCollection(Collection):
``bcfg2-yum-helper`` command.
"""
cmd = [self.helper, "-c", self.cfgfile]
- verbose = self.debug_flag or self.setup['verbose']
- if verbose:
+ if self.setup['verbose']:
+ cmd.append("-v")
+ if self.debug_flag:
+ if not self.setup['verbose']:
+ # ensure that running in debug gets -vv, even if
+ # verbose is not enabled
+ cmd.append("-v")
cmd.append("-v")
cmd.append(command)
- self.debug_log("Packages: running %s" % " ".join(cmd), flag=verbose)
+ self.debug_log("Packages: running %s" % " ".join(cmd))
try:
helper = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
except OSError:
@@ -893,19 +902,27 @@ class YumCollection(Collection):
else:
(stdout, stderr) = helper.communicate()
rv = helper.wait()
+ errlines = stderr.splitlines()
if rv:
+ if not errlines:
+ errlines.append("No error output")
self.logger.error("Packages: error running bcfg2-yum-helper "
- "(returned %d): %s" % (rv, stderr))
- else:
+ "(returned %d): %s" % (rv, errlines[0]))
+ for line in errlines[1:]:
+ self.logger.error("Packages: %s" % line)
+ elif errlines:
self.debug_log("Packages: debug info from bcfg2-yum-helper: %s" %
- stderr, flag=verbose)
+ errlines[0])
+ for line in errlines[1:]:
+ self.debug_log("Packages: %s" % line)
+
try:
return json.loads(stdout)
except ValueError:
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 +948,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 +1145,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':
@@ -1169,7 +1194,7 @@ class YumSource(Source):
if entry.get('name').startswith('/'):
self.needed_paths.add(entry.get('name'))
pro = pdata.find(RP + 'provides')
- if pro != None:
+ if pro is not None:
for entry in pro.getchildren():
prov = entry.get('name')
if prov not in self.provides[arch]:
@@ -1185,9 +1210,9 @@ class YumSource(Source):
try:
groupid = group.xpath('id')[0].text
self.yumgroups[groupid] = {'mandatory': list(),
- 'default': list(),
- 'optional': list(),
- 'conditional': list()}
+ 'default': list(),
+ 'optional': list(),
+ 'conditional': list()}
except IndexError:
continue
try:
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
index c3eadc6bb..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
@@ -18,7 +18,8 @@ from Bcfg2.Server.Plugins.Packages.PackagesSources import PackagesSources
YUM_CONFIG_DEFAULT = "/etc/yum.repos.d/bcfg2.repo"
#: The default path for generated apt configs
-APT_CONFIG_DEFAULT = "/etc/apt/sources.d/bcfg2"
+APT_CONFIG_DEFAULT = \
+ "/etc/apt/sources.list.d/bcfg2-packages-generated-sources.list"
class Packages(Bcfg2.Server.Plugin.Plugin,
@@ -184,6 +185,14 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
for (key, value) in list(attrib.items()):
entry.attrib.__setitem__(key, value)
+ def get_config(self, metadata):
+ """ Get yum/apt config, as a string, for the specified client.
+
+ :param metadata: The client to create the config for.
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ """
+ return self.get_collection(metadata).get_config()
+
def HandleEntry(self, entry, metadata):
""" Bind configuration entries. ``HandleEntry`` handles
entries two different ways:
@@ -239,14 +248,14 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
return True
elif entry.tag == 'Path':
# managed entries for yum/apt configs
- if (entry.get("name") == \
- self.core.setup.cfp.get("packages",
- "yum_config",
- default=YUM_CONFIG_DEFAULT) or
- entry.get("name") == \
- self.core.setup.cfp.get("packages",
- "apt_config",
- default=APT_CONFIG_DEFAULT)):
+ if (entry.get("name") ==
+ self.core.setup.cfp.get("packages",
+ "yum_config",
+ default=YUM_CONFIG_DEFAULT) or
+ entry.get("name") ==
+ self.core.setup.cfp.get("packages",
+ "apt_config",
+ default=APT_CONFIG_DEFAULT)):
return True
return False
@@ -450,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))
@@ -518,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):
@@ -536,7 +546,8 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
:return: dict of lists of ``url_map`` data
"""
collection = self.get_collection(metadata)
- return dict(sources=collection.get_additional_data())
+ return dict(sources=collection.get_additional_data(),
+ get_config=self.get_config)
def end_client_run(self, metadata):
""" Hook to clear the cache for this client in
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 f106b75a4..0974184b4 100644
--- a/src/lib/Bcfg2/Server/Plugins/Probes.py
+++ b/src/lib/Bcfg2/Server/Plugins/Probes.py
@@ -12,6 +12,7 @@ import Bcfg2.Server.Plugin
try:
from django.db import models
+ from django.core.exceptions import MultipleObjectsReturned
HAS_DJANGO = True
class ProbesDataModel(models.Model,
@@ -58,7 +59,7 @@ class ClientProbeDataSet(dict):
dict.__init__(self, *args, **kwargs)
-class ProbeData(str):
+class ProbeData(str): # pylint: disable=E0012,R0924
""" a ProbeData object emulates a str object, but also has .xdata,
.json, and .yaml properties to provide convenient ways to use
ProbeData objects as XML, JSON, or YAML data """
@@ -111,15 +112,15 @@ class ProbeData(str):
class ProbeSet(Bcfg2.Server.Plugin.EntrySet):
""" Handle universal and group- and host-specific probe files """
- ignore = re.compile("^(\.#.*|.*~|\\..*\\.(tmp|sw[px])|probed\\.xml)$")
+ ignore = re.compile(r'^(\.#.*|.*~|\..*\.(tmp|sw[px])|probed\.xml)$')
probename = \
- re.compile("(.*/)?(?P<basename>\S+?)(\.(?P<mode>(?:G\d\d)|H)_\S+)?$")
- bangline = re.compile('^#!\s*(?P<interpreter>.*)$')
+ re.compile(r'(.*/)?(?P<basename>\S+?)(\.(?P<mode>(?:G\d\d)|H)_\S+)?$')
+ bangline = re.compile(r'^#!\s*(?P<interpreter>.*)$')
basename_is_regex = True
def __init__(self, path, fam, encoding, plugin_name):
self.plugin_name = plugin_name
- Bcfg2.Server.Plugin.EntrySet.__init__(self, '[0-9A-Za-z_\-]+', path,
+ Bcfg2.Server.Plugin.EntrySet.__init__(self, r'[0-9A-Za-z_\-]+', path,
Bcfg2.Server.Plugin.SpecificData,
encoding)
fam.AddMonitor(path, self)
@@ -153,7 +154,20 @@ class ProbeSet(Bcfg2.Server.Plugin.EntrySet):
probe = lxml.etree.Element('probe')
probe.set('name', os.path.basename(name))
probe.set('source', self.plugin_name)
- probe.text = entry.data
+ if (metadata.version_info and
+ metadata.version_info > (1, 3, 1, '', 0)):
+ try:
+ probe.text = entry.data.decode('utf-8')
+ except AttributeError:
+ probe.text = entry.data
+ else:
+ try:
+ probe.text = entry.data
+ except: # pylint: disable=W0702
+ self.logger.error("Client unable to handle unicode "
+ "probes. Skipping %s" %
+ probe.get('name'))
+ continue
match = self.bangline.match(entry.data.split('\n')[0])
if match:
probe.set('interpreter', match.group('interpreter'))
@@ -209,15 +223,15 @@ class Probes(Bcfg2.Server.Plugin.Probing,
lxml.etree.SubElement(top, 'Client', name=client,
timestamp=str(int(probedata.timestamp)))
for probe in sorted(probedata):
- 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:
- datafile = open(os.path.join(self.data, 'probed.xml'), 'w')
- datafile.write(lxml.etree.tostring(
- top, xml_declaration=False,
- pretty_print='true').decode('UTF-8'))
+ top.getroottree().write(os.path.join(self.data, 'probed.xml'),
+ xml_declaration=False,
+ pretty_print='true')
except IOError:
err = sys.exc_info()[1]
self.logger.error("Failed to write probed.xml: %s" % err)
@@ -232,21 +246,25 @@ class Probes(Bcfg2.Server.Plugin.Probing,
if pdata.data != data:
pdata.data = data
pdata.save()
+
ProbesDataModel.objects.filter(
hostname=client.hostname).exclude(
- probe__in=self.probedata[client.hostname]).delete()
+ probe__in=self.probedata[client.hostname]).delete()
for group in self.cgroups[client.hostname]:
try:
- ProbesGroupsModel.objects.get(hostname=client.hostname,
- group=group)
- except ProbesGroupsModel.DoesNotExist:
- grp = ProbesGroupsModel(hostname=client.hostname,
- group=group)
- grp.save()
+ ProbesGroupsModel.objects.get_or_create(
+ hostname=client.hostname,
+ group=group)
+ except MultipleObjectsReturned:
+ ProbesGroupsModel.objects.filter(hostname=client.hostname,
+ group=group).delete()
+ ProbesGroupsModel.objects.get_or_create(
+ hostname=client.hostname,
+ group=group)
ProbesGroupsModel.objects.filter(
hostname=client.hostname).exclude(
- group__in=self.cgroups[client.hostname]).delete()
+ group__in=self.cgroups[client.hostname]).delete()
def load_data(self):
""" Load probe data from the appropriate backend (probed.xml
@@ -320,7 +338,7 @@ class Probes(Bcfg2.Server.Plugin.Probing,
def ReceiveDataItem(self, client, data, cgroups, cprobedata):
"""Receive probe results pertaining to client."""
- if data.text == None:
+ if data.text is None:
self.logger.info("Got null response to probe %s from %s" %
(data.get('name'), client.hostname))
cprobedata[data.get('name')] = ProbeData('')
diff --git a/src/lib/Bcfg2/Server/Plugins/Properties.py b/src/lib/Bcfg2/Server/Plugins/Properties.py
index 3ebad40e3..e97f66675 100644
--- a/src/lib/Bcfg2/Server/Plugins/Properties.py
+++ b/src/lib/Bcfg2/Server/Plugins/Properties.py
@@ -266,8 +266,13 @@ class XMLPropertyFile(Bcfg2.Server.Plugin.StructFile, PropertyFile):
return repr(self.xdata)
-class PropDirectoryBacked(Bcfg2.Server.Plugin.DirectoryBacked):
- """ A collection of properties files. """
+class Properties(Bcfg2.Server.Plugin.Plugin,
+ Bcfg2.Server.Plugin.Connector,
+ Bcfg2.Server.Plugin.DirectoryBacked):
+ """ The properties plugin maps property files into client metadata
+ instances. """
+
+ #: Extensions that are understood by Properties.
extensions = ["xml"]
if HAS_JSON:
extensions.append("json")
@@ -284,14 +289,18 @@ class PropDirectoryBacked(Bcfg2.Server.Plugin.DirectoryBacked):
#: Ignore XML schema (``.xsd``) files
ignore = re.compile(r'.*\.xsd$')
- def __init__(self, data, fam):
- Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, data, fam)
+ def __init__(self, core, datastore):
+ global SETUP # pylint: disable=W0603
+ Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
+ Bcfg2.Server.Plugin.Connector.__init__(self)
+ Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data, core.fam)
+ SETUP = core.setup
#: Instead of creating children of this object with a static
#: object, we use :func:`property_dispatcher` to create a
#: child of the appropriate subclass of :class:`PropertyFile`
self.__child__ = self.property_dispatcher
- __init__.__doc__ = Bcfg2.Server.Plugin.DirectoryBacked.__init__.__doc__
+ __init__.__doc__ = Bcfg2.Server.Plugin.Plugin.__init__.__doc__
def property_dispatcher(self, fname, fam):
""" Dispatch an event on a Properties file to the
@@ -314,30 +323,9 @@ class PropDirectoryBacked(Bcfg2.Server.Plugin.DirectoryBacked):
raise Bcfg2.Server.Plugin.PluginExecutionError(
"Properties: Unknown extension %s" % fname)
-
-class Properties(Bcfg2.Server.Plugin.Plugin,
- Bcfg2.Server.Plugin.Connector):
- """ The properties plugin maps property files into client metadata
- instances. """
-
- def __init__(self, core, datastore):
- global SETUP # pylint: disable=W0603
- Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
- Bcfg2.Server.Plugin.Connector.__init__(self)
- SETUP = core.setup
- try:
- self.store = PropDirectoryBacked(self.data, core.fam)
- except OSError:
- err = sys.exc_info()[1]
- self.logger.error("Error while creating Properties store: %s" %
- err)
- raise Bcfg2.Server.Plugin.PluginInitError
-
- __init__.__doc__ = Bcfg2.Server.Plugin.Plugin.__init__.__doc__
-
def get_additional_data(self, metadata):
rv = dict()
- for fname, pfile in self.store.entries.items():
+ for fname, pfile in self.entries.items():
rv[fname] = pfile.get_additional_data(metadata)
return rv
get_additional_data.__doc__ = \
diff --git a/src/lib/Bcfg2/Server/Plugins/Reporting.py b/src/lib/Bcfg2/Server/Plugins/Reporting.py
index a6dc2c1ef..3354763d4 100644
--- a/src/lib/Bcfg2/Server/Plugins/Reporting.py
+++ b/src/lib/Bcfg2/Server/Plugins/Reporting.py
@@ -92,10 +92,11 @@ class Reporting(Statistics, Threaded, PullSource, Debuggable):
# try 3 times to store the data
for i in [1, 2, 3]:
try:
- self.transport.store(client.hostname, cdata,
- lxml.etree.tostring(
+ self.transport.store(
+ client.hostname, cdata,
+ lxml.etree.tostring(
stats,
- xml_declaration=False).decode('UTF-8'))
+ xml_declaration=False))
self.debug_log("%s: Queued statistics data for %s" %
(self.__class__.__name__, client.hostname))
return
diff --git a/src/lib/Bcfg2/Server/Plugins/SSHbase.py b/src/lib/Bcfg2/Server/Plugins/SSHbase.py
index c7db67301..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)
@@ -201,10 +201,11 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin,
if specific.hostname and specific.hostname in names:
hostnames = names[specific.hostname]
elif specific.group:
- hostnames = list(chain(
+ hostnames = list(
+ chain(
*[names[cmeta.hostname]
- for cmeta in \
- mquery.by_groups([specific.group])]))
+ for cmeta in
+ mquery.by_groups([specific.group])]))
elif specific.all:
# a generic key for all hosts? really?
hostnames = list(chain(*list(names.values())))
@@ -287,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/SSLCA.py b/src/lib/Bcfg2/Server/Plugins/SSLCA.py
index 7d00201da..f111ffc60 100644
--- a/src/lib/Bcfg2/Server/Plugins/SSLCA.py
+++ b/src/lib/Bcfg2/Server/Plugins/SSLCA.py
@@ -68,7 +68,7 @@ class SSLCACertSpec(SSLCAXMLSpec):
def get_spec(self, metadata):
rv = SSLCAXMLSpec.get_spec(self, metadata)
rv['subjectaltname'] = [e.text for e in self.Match(metadata)
- if e.tag == "SubjectAltName"]
+ if e.tag == "subjectAltName"]
return rv
diff --git a/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py b/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py
index 0aea439f9..41e6bf8b5 100644
--- a/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py
+++ b/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py
@@ -6,7 +6,9 @@ import Bcfg2.Server.Plugin
class ServiceCompat(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.GoalValidator):
""" Use old-style service modes for older clients """
- name = 'ServiceCompat'
+
+ create = False
+
__author__ = 'bcfg-dev@mcs.anl.gov'
mode_map = {('true', 'true'): 'default',
('interactive', 'true'): 'interactive_only',
@@ -14,7 +16,7 @@ class ServiceCompat(Bcfg2.Server.Plugin.Plugin,
def validate_goals(self, metadata, config):
""" Apply defaults """
- if metadata.version_info and metadata.version_info > (1, 3, 0, '', 0):
+ if metadata.version_info and metadata.version_info >= (1, 3, 0, '', 0):
# do not care about a client that is _any_ 1.3.0 release
# (including prereleases and RCs)
return
diff --git a/src/lib/Bcfg2/Server/Plugins/Svn.py b/src/lib/Bcfg2/Server/Plugins/Svn.py
index 51f44c52d..240fd7f89 100644
--- a/src/lib/Bcfg2/Server/Plugins/Svn.py
+++ b/src/lib/Bcfg2/Server/Plugins/Svn.py
@@ -59,9 +59,48 @@ class Svn(Bcfg2.Server.Plugin.Version):
self.client.callback_conflict_resolver = \
self.get_conflict_resolver(choice)
+ try:
+ if self.core.setup.cfp.get(
+ "svn",
+ "always_trust").lower() == "true":
+ self.client.callback_ssl_server_trust_prompt = \
+ self.ssl_server_trust_prompt
+ except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+ self.logger.debug("Svn: Using subversion cache for SSL "
+ "certificate trust")
+
+ try:
+ if (self.core.setup.cfp.get("svn", "user") and
+ self.core.setup.cfp.get("svn", "password")):
+ self.client.callback_get_login = \
+ self.get_login
+ except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+ self.logger.info("Svn: Using subversion cache for "
+ "password-based authetication")
+
self.logger.debug("Svn: Initialized svn plugin with SVN directory %s" %
self.vcs_path)
+ # pylint: disable=W0613
+ def get_login(self, realm, username, may_save):
+ """ PySvn callback to get credentials for HTTP basic authentication """
+ self.logger.debug("Svn: Logging in with username: %s" %
+ self.core.setup.cfp.get("svn", "user"))
+ return True, \
+ self.core.setup.cfp.get("svn", "user"), \
+ self.core.setup.cfp.get("svn", "password"), \
+ False
+ # pylint: enable=W0613
+
+ def ssl_server_trust_prompt(self, trust_dict):
+ """ PySvn callback to always trust SSL certificates from SVN server """
+ self.logger.debug("Svn: Trusting SSL certificate from %s, "
+ "issued by %s for realm %s" %
+ (trust_dict['hostname'],
+ trust_dict['issuer_dname'],
+ trust_dict['realm']))
+ return True, trust_dict['failures'], False
+
def get_conflict_resolver(self, choice):
""" Get a PySvn conflict resolution callback """
def callback(conflict_description):
diff --git a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
index ea7454e11..db7370f01 100644
--- a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
+++ b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
@@ -82,7 +82,7 @@ class TemplateHelper(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.DirectoryBacked):
""" A plugin to provide helper classes and functions to templates """
__author__ = 'chris.a.st.pierre@gmail.com'
- ignore = re.compile("^(\.#.*|.*~|\\..*\\.(sw[px])|.*\.py[co])$")
+ ignore = re.compile(r'^(\.#.*|.*~|\..*\.(sw[px])|.*\.py[co])$')
patterns = MODULE_RE
__child__ = HelperModule
@@ -97,18 +97,33 @@ 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"))
def Run(self):
for helper in self.core.plugins['TemplateHelper'].entries.values():
- if self.HandlesFile(helper):
+ if self.HandlesFile(helper.name):
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/Server/Plugins/__init__.py b/src/lib/Bcfg2/Server/Plugins/__init__.py
index b33eeba28..ad51cf368 100644
--- a/src/lib/Bcfg2/Server/Plugins/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/__init__.py
@@ -1,32 +1,5 @@
"""Imports for Bcfg2.Server.Plugins."""
-__all__ = [
- 'Account',
- 'Base',
- 'Bundler',
- 'Bzr',
- 'Cfg',
- 'Cvs',
- 'Darcs',
- 'Decisions',
- 'Fossil',
- 'Git',
- 'GroupPatterns',
- 'Hg',
- 'Hostbase',
- 'Metadata',
- 'NagiosGen',
- 'Ohai',
- 'Packages',
- 'Properties',
- 'Probes',
- 'Pkgmgr',
- 'Rules',
- 'SSHbase',
- 'Snapshots',
- 'Statistics',
- 'Svn',
- 'TCheetah',
- 'Trigger',
- 'TGenshi',
- ]
+from Bcfg2.Compat import walk_packages
+
+__all__ = [m[1] for m in walk_packages(path=__path__)]
diff --git a/src/lib/Bcfg2/Server/__init__.py b/src/lib/Bcfg2/Server/__init__.py
index 3eb300a98..0678e4579 100644
--- a/src/lib/Bcfg2/Server/__init__.py
+++ b/src/lib/Bcfg2/Server/__init__.py
@@ -1,10 +1,9 @@
"""This is the set of modules for Bcfg2.Server."""
import lxml.etree
+from Bcfg2.Compat import walk_packages
-__all__ = ["Admin", "Core", "FileMonitor", "Plugin", "Plugins",
- "Hostbase", "Reports", "Snapshots", "XMLParser",
- "XI", "XI_NAMESPACE"]
+__all__ = [m[1] for m in walk_packages(path=__path__)]
XI = 'http://www.w3.org/2001/XInclude'
XI_NAMESPACE = '{%s}' % XI