summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Plugins/Cfg
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Server/Plugins/Cfg')
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py29
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgCatFilter.py28
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py11
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgDiffFilter.py35
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py12
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py20
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgExternalCommandVerifier.py26
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py119
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgInfoXML.py16
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py46
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py162
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py11
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCACertCreator.py255
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCAKeyCreator.py36
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py426
15 files changed, 577 insertions, 655 deletions
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py
index 41d5588e4..7792d7e5c 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py
@@ -3,8 +3,9 @@ based on an XML specification of which SSH keypairs should granted
access. """
import lxml.etree
+import Bcfg2.Options
from Bcfg2.Server.Plugin import StructFile, PluginExecutionError
-from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP, CFG
+from Bcfg2.Server.Plugins.Cfg import CfgGenerator, get_cfg
from Bcfg2.Server.Plugins.Metadata import ClientMetadata
@@ -20,26 +21,13 @@ class CfgAuthorizedKeysGenerator(CfgGenerator, StructFile):
#: Handle authorized keys XML files
__basenames__ = ['authorizedkeys.xml', 'authorized_keys.xml']
- #: This handler is experimental, in part because it depends upon
- #: the (experimental) CfgPrivateKeyCreator handler
- experimental = True
-
def __init__(self, fname):
- CfgGenerator.__init__(self, fname, None, None)
+ CfgGenerator.__init__(self, fname, None)
StructFile.__init__(self, fname)
self.cache = dict()
- self.core = CFG.core
+ self.core = get_cfg().core
__init__.__doc__ = CfgGenerator.__init__.__doc__
- @property
- def category(self):
- """ The name of the metadata category that generated keys are
- specific to """
- if (SETUP.cfp.has_section("sshkeys") and
- SETUP.cfp.has_option("sshkeys", "category")):
- return SETUP.cfp.get("sshkeys", "category")
- return None
-
def handle_event(self, event):
CfgGenerator.handle_event(self, event)
StructFile.HandleEvent(self, event)
@@ -51,12 +39,6 @@ class CfgAuthorizedKeysGenerator(CfgGenerator, StructFile):
rv = []
for allow in spec.findall("Allow"):
options = []
- if allow.find("Params") is not None:
- self.logger.warning("Use of <Params> in authorized_keys.xml "
- "is deprecated; use <Option> instead")
- options.extend("=".join(p)
- for p in allow.find("Params").attrib.items())
-
for opt in allow.findall("Option"):
if opt.get("value"):
options.append("%s=%s" % (opt.get("name"),
@@ -68,7 +50,8 @@ class CfgAuthorizedKeysGenerator(CfgGenerator, StructFile):
if pubkey_name:
host = allow.get("host")
group = allow.get("group")
- category = allow.get("category", self.category)
+ category = allow.get("category",
+ Bcfg2.Options.setup.sshkeys_category)
if host:
key_md = self.core.build_metadata(host)
elif group:
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCatFilter.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCatFilter.py
deleted file mode 100644
index 49a5a85b3..000000000
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCatFilter.py
+++ /dev/null
@@ -1,28 +0,0 @@
-""" Handle .cat files, which append lines to and remove lines from
-plaintext files """
-
-from Bcfg2.Server.Plugins.Cfg import CfgFilter
-
-
-class CfgCatFilter(CfgFilter):
- """ CfgCatFilter appends lines to and remove lines from plaintext
- :ref:`server-plugins-generators-Cfg` files"""
-
- #: Handle .cat files
- __extensions__ = ['cat']
-
- #: .cat files are deprecated
- deprecated = True
-
- def modify_data(self, entry, metadata, data):
- datalines = data.strip().split('\n')
- for line in self.data.split('\n'):
- if not line:
- continue
- if line.startswith('+'):
- datalines.append(line[1:])
- elif line.startswith('-'):
- if line[1:] in datalines:
- datalines.remove(line[1:])
- return "\n".join(datalines) + "\n"
- modify_data.__doc__ = CfgFilter.modify_data.__doc__
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py
index 724164cf5..476dc1fc6 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py
@@ -2,8 +2,9 @@
<http://www.cheetahtemplate.org/>`_ templating system to generate
:ref:`server-plugins-generators-cfg` files. """
+import Bcfg2.Options
from Bcfg2.Server.Plugin import PluginExecutionError
-from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP
+from Bcfg2.Server.Plugins.Cfg import CfgGenerator
try:
from Cheetah.Template import Template
@@ -27,19 +28,19 @@ class CfgCheetahGenerator(CfgGenerator):
#: :class:`Cheetah.Template.Template` compiler settings
settings = dict(useStackFrames=False)
- def __init__(self, fname, spec, encoding):
- CfgGenerator.__init__(self, fname, spec, encoding)
+ def __init__(self, fname, spec):
+ CfgGenerator.__init__(self, fname, spec)
if not HAS_CHEETAH:
raise PluginExecutionError("Cheetah is not available")
__init__.__doc__ = CfgGenerator.__init__.__doc__
def get_data(self, entry, metadata):
- template = Template(self.data.decode(self.encoding),
+ template = Template(self.data.decode(Bcfg2.Options.setup.encoding),
compilerSettings=self.settings)
template.metadata = metadata
template.name = entry.get('realname', entry.get('name'))
template.path = entry.get('realname', entry.get('name'))
template.source_path = self.name
- template.repo = SETUP['repo']
+ template.repo = Bcfg2.Options.setup.repository
return template.respond()
get_data.__doc__ = CfgGenerator.get_data.__doc__
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgDiffFilter.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgDiffFilter.py
deleted file mode 100644
index da506a195..000000000
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgDiffFilter.py
+++ /dev/null
@@ -1,35 +0,0 @@
-""" Handle .diff files, which apply diffs to plaintext files """
-
-import os
-import tempfile
-from Bcfg2.Server.Plugin import PluginExecutionError
-from subprocess import Popen, PIPE
-from Bcfg2.Server.Plugins.Cfg import CfgFilter
-
-
-class CfgDiffFilter(CfgFilter):
- """ CfgDiffFilter applies diffs to plaintext
- :ref:`server-plugins-generators-Cfg` files """
-
- #: Handle .diff files
- __extensions__ = ['diff']
-
- #: .diff files are deprecated
- deprecated = True
-
- def modify_data(self, entry, metadata, data):
- basehandle, basename = tempfile.mkstemp()
- open(basename, 'w').write(data)
- os.close(basehandle)
-
- cmd = ["patch", "-u", "-f", basename]
- patch = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- stderr = patch.communicate(input=self.data)[1]
- ret = patch.wait()
- output = open(basename, 'r').read()
- os.unlink(basename)
- if ret != 0:
- raise PluginExecutionError("Error applying diff %s: %s" %
- (self.name, stderr))
- return output
- modify_data.__doc__ = CfgFilter.modify_data.__doc__
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py
index 3b4703ddb..e2a2f696a 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py
@@ -2,10 +2,9 @@
:ref:`server-plugins-generators-cfg` files on the server. """
from Bcfg2.Server.Plugin import PluginExecutionError
-from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP
+from Bcfg2.Server.Plugins.Cfg import CfgGenerator
try:
- from Bcfg2.Encryption import bruteforce_decrypt, EVPError, \
- get_algorithm
+ from Bcfg2.Server.Encryption import bruteforce_decrypt, EVPError
HAS_CRYPTO = True
except ImportError:
HAS_CRYPTO = False
@@ -22,8 +21,8 @@ class CfgEncryptedGenerator(CfgGenerator):
#: .genshi.crypt and .cheetah.crypt files
__priority__ = 50
- def __init__(self, fname, spec, encoding):
- CfgGenerator.__init__(self, fname, spec, encoding)
+ def __init__(self, fname, spec):
+ CfgGenerator.__init__(self, fname, spec)
if not HAS_CRYPTO:
raise PluginExecutionError("M2Crypto is not available")
__init__.__doc__ = CfgGenerator.__init__.__doc__
@@ -34,8 +33,7 @@ class CfgEncryptedGenerator(CfgGenerator):
return
# todo: let the user specify a passphrase by name
try:
- self.data = bruteforce_decrypt(self.data, setup=SETUP,
- algorithm=get_algorithm(SETUP))
+ self.data = bruteforce_decrypt(self.data)
except EVPError:
raise PluginExecutionError("Failed to decrypt %s" % self.name)
handle_event.__doc__ = CfgGenerator.handle_event.__doc__
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py
index 130652aef..f69ab8e5f 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py
@@ -1,32 +1,24 @@
""" Handle encrypted Genshi templates (.crypt.genshi or .genshi.crypt
files) """
+from genshi.template import TemplateLoader
from Bcfg2.Compat import StringIO
from Bcfg2.Server.Plugin import PluginExecutionError
-from Bcfg2.Server.Plugins.Cfg import SETUP
from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator
try:
- from Bcfg2.Encryption import bruteforce_decrypt, get_algorithm
+ from Bcfg2.Server.Encryption import bruteforce_decrypt
HAS_CRYPTO = True
except ImportError:
HAS_CRYPTO = False
-try:
- from genshi.template import TemplateLoader
-except ImportError:
- # CfgGenshiGenerator will raise errors if genshi doesn't exist
- TemplateLoader = object # pylint: disable=C0103
-
class EncryptedTemplateLoader(TemplateLoader):
""" Subclass :class:`genshi.template.TemplateLoader` to decrypt
the data on the fly as it's read in using
- :func:`Bcfg2.Encryption.bruteforce_decrypt` """
+ :func:`Bcfg2.Server.Encryption.bruteforce_decrypt` """
def _instantiate(self, cls, fileobj, filepath, filename, encoding=None):
- plaintext = \
- StringIO(bruteforce_decrypt(fileobj.read(),
- algorithm=get_algorithm(SETUP)))
+ plaintext = StringIO(bruteforce_decrypt(fileobj.read()))
return TemplateLoader._instantiate(self, cls, plaintext, filepath,
filename, encoding=encoding)
@@ -45,7 +37,7 @@ class CfgEncryptedGenshiGenerator(CfgGenshiGenerator):
#: when it's read in
__loader_cls__ = EncryptedTemplateLoader
- def __init__(self, fname, spec, encoding):
- CfgGenshiGenerator.__init__(self, fname, spec, encoding)
+ def __init__(self, fname, spec):
+ CfgGenshiGenerator.__init__(self, fname, spec)
if not HAS_CRYPTO:
raise PluginExecutionError("M2Crypto is not available")
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgExternalCommandVerifier.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgExternalCommandVerifier.py
index 313e53ee9..953473a12 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgExternalCommandVerifier.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgExternalCommandVerifier.py
@@ -3,8 +3,8 @@
import os
import sys
import shlex
+from Bcfg2.Utils import Executor
from Bcfg2.Server.Plugin import PluginExecutionError
-from subprocess import Popen, PIPE
from Bcfg2.Server.Plugins.Cfg import CfgVerifier, CfgVerificationError
@@ -15,27 +15,19 @@ class CfgExternalCommandVerifier(CfgVerifier):
#: Handle :file:`:test` files
__basenames__ = [':test']
- def __init__(self, name, specific, encoding):
- CfgVerifier.__init__(self, name, specific, encoding)
+ def __init__(self, name, specific):
+ CfgVerifier.__init__(self, name, specific)
self.cmd = []
+ self.exc = Executor(timeout=30)
__init__.__doc__ = CfgVerifier.__init__.__doc__
def verify_entry(self, entry, metadata, data):
try:
- proc = Popen(self.cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- out, err = proc.communicate(input=data)
- rv = proc.wait()
- if rv != 0:
- # pylint: disable=E1103
- raise CfgVerificationError(err.strip() or out.strip() or
- "Non-zero return value %s" % rv)
- # pylint: enable=E1103
- except CfgVerificationError:
- raise
- except:
- err = sys.exc_info()[1]
- raise CfgVerificationError("Error running external command "
- "verifier: %s" % err)
+ result = self.exc.run(self.cmd, inputdata=data)
+ if not result.success:
+ raise CfgVerificationError(result.error)
+ except OSError:
+ raise CfgVerificationError(sys.exc_info()[1])
verify_entry.__doc__ = CfgVerifier.verify_entry.__doc__
def handle_event(self, event):
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
index 83a5c1165..7ba8c4491 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
@@ -5,63 +5,41 @@
import re
import sys
import traceback
-from Bcfg2.Server.Plugin import PluginExecutionError
-from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP
-
-try:
- import genshi.core
- from genshi.template import TemplateLoader, NewTextTemplate
- from genshi.template.eval import UndefinedError, Suite
- #: True if Genshi libraries are available
- HAS_GENSHI = True
-
- def _genshi_removes_blank_lines():
- """ Genshi 0.5 uses the Python :mod:`compiler` package to
- compile genshi snippets to AST. Genshi 0.6 uses some bespoke
- magic, because compiler has been deprecated.
- :func:`compiler.parse` produces an AST that removes all excess
- whitespace (e.g., blank lines), while
- :func:`genshi.template.astutil.parse` does not. In order to
- determine which actual line of code an error occurs on, we
- need to know which is in use and how it treats blank lines.
- I've beat my head against this for hours and the best/only way
- I can find is to compile some genshi code with an error and
- see which line it's on."""
- code = """d = dict()
-
+import Bcfg2.Options
+from Bcfg2.Server.Plugin import PluginExecutionError, removecomment
+from Bcfg2.Server.Plugins.Cfg import CfgGenerator
+from genshi.template import TemplateLoader, NewTextTemplate
+from genshi.template.eval import UndefinedError, Suite
+
+
+def _genshi_removes_blank_lines():
+ """ Genshi 0.5 uses the Python :mod:`compiler` package to
+ compile genshi snippets to AST. Genshi 0.6 uses some bespoke
+ magic, because compiler has been deprecated.
+ :func:`compiler.parse` produces an AST that removes all excess
+ whitespace (e.g., blank lines), while
+ :func:`genshi.template.astutil.parse` does not. In order to
+ determine which actual line of code an error occurs on, we
+ need to know which is in use and how it treats blank lines.
+ I've beat my head against this for hours and the best/only way
+ I can find is to compile some genshi code with an error and
+ see which line it's on."""
+ code = """d = dict()
d['a']"""
- try:
- Suite(code).execute(dict())
- except KeyError:
- line = traceback.extract_tb(sys.exc_info()[2])[-1][1]
- if line == 2:
- return True
- else:
- return False
-
- #: True if Genshi removes all blank lines from a code block before
- #: executing it; False indicates that Genshi only removes leading
- #: and trailing blank lines. See
- #: :func:`_genshi_removes_blank_lines` for an explanation of this.
- GENSHI_REMOVES_BLANK_LINES = _genshi_removes_blank_lines()
-except ImportError:
- TemplateLoader = None # pylint: disable=C0103
- HAS_GENSHI = False
-
-
-def removecomment(stream):
- """ A Genshi filter that removes comments from the stream. This
- function is a generator.
-
- :param stream: The Genshi stream to remove comments from
- :type stream: genshi.core.Stream
- :returns: tuple of ``(kind, data, pos)``, as when iterating
- through a Genshi stream
- """
- for kind, data, pos in stream:
- if kind is genshi.core.COMMENT:
- continue
- yield kind, data, pos
+ try:
+ Suite(code).execute(dict())
+ except KeyError:
+ line = traceback.extract_tb(sys.exc_info()[2])[-1][1]
+ if line == 2:
+ return True
+ else:
+ return False
+
+#: True if Genshi removes all blank lines from a code block before
+#: executing it; False indicates that Genshi only removes leading
+#: and trailing blank lines. See
+#: :func:`_genshi_removes_blank_lines` for an explanation of this.
+GENSHI_REMOVES_BLANK_LINES = _genshi_removes_blank_lines()
class CfgGenshiGenerator(CfgGenerator):
@@ -92,10 +70,8 @@ class CfgGenshiGenerator(CfgGenerator):
#: occurred.
pyerror_re = re.compile(r'<\w+ u?[\'"](.*?)\s*\.\.\.[\'"]>')
- def __init__(self, fname, spec, encoding):
- CfgGenerator.__init__(self, fname, spec, encoding)
- if not HAS_GENSHI:
- raise PluginExecutionError("Genshi is not available")
+ def __init__(self, fname, spec):
+ CfgGenerator.__init__(self, fname, spec)
self.template = None
self.loader = self.__loader_cls__(max_cache_size=0)
__init__.__doc__ = CfgGenerator.__init__.__doc__
@@ -106,18 +82,20 @@ class CfgGenshiGenerator(CfgGenerator):
self.name)
fname = entry.get('realname', entry.get('name'))
- stream = \
- self.template.generate(name=fname,
- metadata=metadata,
- path=self.name,
- source_path=self.name,
- repo=SETUP['repo']).filter(removecomment)
+ stream = self.template.generate(
+ name=fname,
+ metadata=metadata,
+ path=self.name,
+ source_path=self.name,
+ repo=Bcfg2.Options.setup.repository).filter(removecomment)
try:
try:
- return stream.render('text', encoding=self.encoding,
+ return stream.render('text',
+ encoding=Bcfg2.Options.setup.encoding,
strip_whitespace=False)
except TypeError:
- return stream.render('text', encoding=self.encoding)
+ return stream.render('text',
+ encoding=Bcfg2.Options.setup.encoding)
except UndefinedError:
# a failure in a genshi expression _other_ than %{ python ... %}
err = sys.exc_info()[1]
@@ -196,8 +174,9 @@ class CfgGenshiGenerator(CfgGenerator):
def handle_event(self, event):
CfgGenerator.handle_event(self, event)
try:
- self.template = self.loader.load(self.name, cls=NewTextTemplate,
- encoding=self.encoding)
+ self.template = \
+ self.loader.load(self.name, cls=NewTextTemplate,
+ encoding=Bcfg2.Options.setup.encoding)
except:
raise PluginExecutionError("Failed to load template: %s" %
sys.exc_info()[1])
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgInfoXML.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgInfoXML.py
index 3b6fc8fa0..886b3993b 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgInfoXML.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgInfoXML.py
@@ -1,6 +1,6 @@
""" Handle info.xml files """
-from Bcfg2.Server.Plugin import PluginExecutionError, InfoXML
+from Bcfg2.Server.Plugin import InfoXML
from Bcfg2.Server.Plugins.Cfg import CfgInfo
@@ -17,21 +17,9 @@ class CfgInfoXML(CfgInfo):
__init__.__doc__ = CfgInfo.__init__.__doc__
def bind_info_to_entry(self, entry, metadata):
- mdata = dict()
- self.infoxml.pnode.Match(metadata, mdata, entry=entry)
- if 'Info' not in mdata:
- raise PluginExecutionError("Failed to set metadata for file %s" %
- entry.get('name'))
- self._set_info(entry, mdata['Info'][None])
+ self.infoxml.BindEntry(entry, metadata)
bind_info_to_entry.__doc__ = CfgInfo.bind_info_to_entry.__doc__
def handle_event(self, event):
self.infoxml.HandleEvent()
handle_event.__doc__ = CfgInfo.handle_event.__doc__
-
- def _set_info(self, entry, info):
- CfgInfo._set_info(self, entry, info)
- if '__children__' in info:
- for child in info['__children__']:
- entry.append(child)
- _set_info.__doc__ = CfgInfo._set_info.__doc__
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py
deleted file mode 100644
index 5122d9aa1..000000000
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py
+++ /dev/null
@@ -1,46 +0,0 @@
-""" Handle info and :info files """
-
-import Bcfg2.Server.Plugin
-from Bcfg2.Server.Plugins.Cfg import CfgInfo
-
-
-class CfgLegacyInfo(CfgInfo):
- """ CfgLegacyInfo handles :file:`info` and :file:`:info` files for
- :ref:`server-plugins-generators-cfg` """
-
- #: Handle :file:`info` and :file:`:info`
- __basenames__ = ['info', ':info']
-
- #: CfgLegacyInfo is deprecated. Use
- #: :class:`Bcfg2.Server.Plugins.Cfg.CfgInfoXML.CfgInfoXML` instead.
- deprecated = True
-
- def __init__(self, path):
- CfgInfo.__init__(self, path)
- self.path = path
-
- #: The set of info metadata stored in the file
- self.metadata = None
- __init__.__doc__ = CfgInfo.__init__.__doc__
-
- def bind_info_to_entry(self, entry, metadata):
- self._set_info(entry, self.metadata)
- bind_info_to_entry.__doc__ = CfgInfo.bind_info_to_entry.__doc__
-
- def handle_event(self, event):
- if event.code2str() == 'deleted':
- return
- self.metadata = dict()
- for line in open(self.path).readlines():
- match = Bcfg2.Server.Plugin.INFO_REGEX.match(line)
- if not match:
- self.logger.warning("Failed to parse line in %s: %s" %
- (event.filename, line))
- continue
- else:
- for key, value in list(match.groupdict().items()):
- if value:
- self.metadata[key] = value
- if ('mode' in self.metadata and len(self.metadata['mode']) == 3):
- self.metadata['mode'] = "0%s" % self.metadata['mode']
- handle_event.__doc__ = CfgInfo.handle_event.__doc__
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py
index e890fdecb..e9698f526 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py
@@ -3,18 +3,13 @@
import os
import shutil
import tempfile
-import subprocess
-from Bcfg2.Server.Plugin import PluginExecutionError, StructFile
-from Bcfg2.Server.Plugins.Cfg import CfgCreator, CfgCreationError, SETUP
+import Bcfg2.Options
+from Bcfg2.Utils import Executor
+from Bcfg2.Server.Plugins.Cfg import XMLCfgCreator, CfgCreationError
from Bcfg2.Server.Plugins.Cfg.CfgPublicKeyCreator import CfgPublicKeyCreator
-try:
- import Bcfg2.Encryption
- HAS_CRYPTO = True
-except ImportError:
- HAS_CRYPTO = False
-class CfgPrivateKeyCreator(CfgCreator, StructFile):
+class CfgPrivateKeyCreator(XMLCfgCreator):
"""The CfgPrivateKeyCreator creates SSH keys on the fly. """
#: Different configurations for different clients/groups can be
@@ -24,38 +19,22 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile):
#: Handle XML specifications of private keys
__basenames__ = ['privkey.xml']
- def __init__(self, fname):
- CfgCreator.__init__(self, fname)
- StructFile.__init__(self, fname)
+ cfg_section = "sshkeys"
+ options = [
+ Bcfg2.Options.Option(
+ cf=("sshkeys", "category"), dest="sshkeys_category",
+ help="Metadata category that generated SSH keys are specific to"),
+ Bcfg2.Options.Option(
+ cf=("sshkeys", "passphrase"), dest="sshkeys_passphrase",
+ help="Passphrase used to encrypt generated SSH private keys")]
+ def __init__(self, fname):
+ XMLCfgCreator.__init__(self, fname)
pubkey_path = os.path.dirname(self.name) + ".pub"
pubkey_name = os.path.join(pubkey_path, os.path.basename(pubkey_path))
self.pubkey_creator = CfgPublicKeyCreator(pubkey_name)
- __init__.__doc__ = CfgCreator.__init__.__doc__
-
- @property
- def category(self):
- """ The name of the metadata category that generated keys are
- specific to """
- if (SETUP.cfp.has_section("sshkeys") and
- SETUP.cfp.has_option("sshkeys", "category")):
- return SETUP.cfp.get("sshkeys", "category")
- return None
-
- @property
- def passphrase(self):
- """ The passphrase used to encrypt private keys """
- 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 None
-
- def handle_event(self, event):
- CfgCreator.handle_event(self, event)
- StructFile.HandleEvent(self, event)
- handle_event.__doc__ = CfgCreator.handle_event.__doc__
+ self.cmd = Executor()
+ __init__.__doc__ = XMLCfgCreator.__init__.__doc__
def _gen_keypair(self, metadata, spec=None):
""" Generate a keypair according to the given client medata
@@ -102,62 +81,22 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile):
log_cmd.append("''")
self.debug_log("Cfg: Generating new SSH key pair: %s" %
" ".join(log_cmd))
- proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- err = proc.communicate()[1]
- if proc.wait():
+ result = self.cmd.run(cmd)
+ if not result.success:
raise CfgCreationError("Cfg: Failed to generate SSH key pair "
"at %s for %s: %s" %
- (filename, metadata.hostname, err))
- elif err:
+ (filename, metadata.hostname,
+ result.error))
+ elif result.stderr:
self.logger.warning("Cfg: Generated SSH key pair at %s for %s "
"with errors: %s" % (filename,
metadata.hostname,
- err))
+ result.stderr))
return filename
except:
shutil.rmtree(tempdir)
raise
- def get_specificity(self, metadata, spec=None):
- """ Get config settings for key generation specificity
- (per-host or per-group).
-
- :param metadata: The client metadata to create data for
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :param spec: The key specification to follow when creating the
- keys. This should be an XML document that only
- contains key specification data that applies to
- the given client metadata, and may be obtained by
- doing ``self.XMLMatch(metadata)``
- :type spec: lxml.etree._Element
- :returns: dict - A dict of specificity arguments suitable for
- passing to
- :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.write_data`
- or
- :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.get_filename`
- """
- if spec is None:
- spec = self.XMLMatch(metadata)
- category = spec.get("category", self.category)
- if category is None:
- per_host_default = "true"
- else:
- per_host_default = "false"
- per_host = spec.get("perhost", per_host_default).lower() == "true"
-
- specificity = dict(host=metadata.hostname)
- if category and not per_host:
- group = metadata.group_in_category(category)
- if group:
- specificity = dict(group=group,
- prio=int(spec.get("priority", 50)))
- else:
- self.logger.info("Cfg: %s has no group in category %s, "
- "creating host-specific key" %
- (metadata.hostname, category))
- return specificity
-
# pylint: disable=W0221
def create_data(self, entry, metadata):
""" Create data for the given entry on the given client
@@ -170,7 +109,7 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile):
:returns: string - The private key data
"""
spec = self.XMLMatch(metadata)
- specificity = self.get_specificity(metadata, spec)
+ specificity = self.get_specificity(metadata)
filename = self._gen_keypair(metadata, spec)
try:
@@ -184,63 +123,8 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile):
# encrypt the private key, write to the proper place, and
# return it
privkey = open(filename).read()
- if HAS_CRYPTO and self.passphrase:
- self.debug_log("Cfg: Encrypting key data at %s" % filename)
- privkey = Bcfg2.Encryption.ssl_encrypt(
- privkey,
- self.passphrase,
- algorithm=Bcfg2.Encryption.get_algorithm(SETUP))
- specificity['ext'] = '.crypt'
-
self.write_data(privkey, **specificity)
return privkey
finally:
shutil.rmtree(os.path.dirname(filename))
# pylint: enable=W0221
-
- def Index(self):
- StructFile.Index(self)
- if HAS_CRYPTO:
- strict = self.xdata.get(
- "decrypt",
- SETUP.cfp.get(Bcfg2.Encryption.CFG_SECTION, "decrypt",
- default="strict")) == "strict"
- for el in self.xdata.xpath("//*[@encrypted]"):
- try:
- el.text = self._decrypt(el).encode('ascii',
- 'xmlcharrefreplace')
- except UnicodeDecodeError:
- self.logger.info("Cfg: Decrypted %s to gibberish, skipping"
- % el.tag)
- except Bcfg2.Encryption.EVPError:
- msg = "Cfg: Failed to decrypt %s element in %s" % \
- (el.tag, self.name)
- if strict:
- raise PluginExecutionError(msg)
- else:
- self.logger.info(msg)
- Index.__doc__ = StructFile.Index.__doc__
-
- def _decrypt(self, element):
- """ Decrypt a single encrypted element """
- if not element.text or not element.text.strip():
- return
- passes = Bcfg2.Encryption.get_passphrases(SETUP)
- try:
- passphrase = passes[element.get("encrypted")]
- try:
- return Bcfg2.Encryption.ssl_decrypt(
- element.text,
- passphrase,
- algorithm=Bcfg2.Encryption.get_algorithm(SETUP))
- except Bcfg2.Encryption.EVPError:
- # error is raised below
- pass
- except KeyError:
- # bruteforce_decrypt raises an EVPError with a sensible
- # error message, so we just let it propagate up the stack
- return Bcfg2.Encryption.bruteforce_decrypt(
- element.text,
- passphrases=passes.values(),
- algorithm=Bcfg2.Encryption.get_algorithm(SETUP))
- raise Bcfg2.Encryption.EVPError("Failed to decrypt")
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py
index 4bd8690ed..3f2d1030b 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py
@@ -8,7 +8,7 @@ import tempfile
import lxml.etree
from Bcfg2.Utils import Executor
from Bcfg2.Server.Plugin import StructFile, PluginExecutionError
-from Bcfg2.Server.Plugins.Cfg import CfgCreator, CfgCreationError, CFG
+from Bcfg2.Server.Plugins.Cfg import CfgCreator, CfgCreationError, get_cfg
class CfgPublicKeyCreator(CfgCreator, StructFile):
@@ -21,17 +21,20 @@ class CfgPublicKeyCreator(CfgCreator, StructFile):
creation of a keypair when a public key is created. """
#: Different configurations for different clients/groups can be
- #: handled with Client and Group tags within privkey.xml
+ #: handled with Client and Group tags within pubkey.xml
__specific__ = False
#: Handle XML specifications of private keys
__basenames__ = ['pubkey.xml']
+ #: No text content on any tags, so encryption support disabled
+ encryption = False
+
def __init__(self, fname):
CfgCreator.__init__(self, fname)
StructFile.__init__(self, fname)
- self.cfg = CFG
- self.core = CFG.core
+ self.cfg = get_cfg()
+ self.core = self.cfg.core
self.cmd = Executor()
def create_data(self, entry, metadata):
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCACertCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCACertCreator.py
new file mode 100644
index 000000000..92fcc4cd8
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCACertCreator.py
@@ -0,0 +1,255 @@
+""" Cfg creator that creates SSL certs """
+
+import os
+import sys
+import tempfile
+import lxml.etree
+import Bcfg2.Options
+from Bcfg2.Utils import Executor
+from Bcfg2.Compat import ConfigParser
+from Bcfg2.Server.FileMonitor import get_fam
+from Bcfg2.Server.Plugin import PluginExecutionError
+from Bcfg2.Server.Plugins.Cfg import CfgCreationError, XMLCfgCreator, \
+ CfgCreator, CfgVerifier, CfgVerificationError, get_cfg
+
+
+class CfgSSLCACertCreator(XMLCfgCreator, CfgVerifier):
+ """ This class acts as both a Cfg creator that creates SSL certs,
+ and as a Cfg verifier that verifies SSL certs. """
+
+ #: Different configurations for different clients/groups can be
+ #: handled with Client and Group tags within pubkey.xml
+ __specific__ = False
+
+ #: Handle XML specifications of private keys
+ __basenames__ = ['sslcert.xml']
+
+ cfg_section = "sslca"
+ options = [
+ Bcfg2.Options.Option(
+ cf=("sslca", "category"), dest="sslca_category",
+ help="Metadata category that generated SSL keys are specific to"),
+ Bcfg2.Options.Option(
+ cf=("sslca", "passphrase"), dest="sslca_passphrase",
+ help="Passphrase used to encrypt generated SSL keys"),
+ Bcfg2.Options.WildcardSectionGroup(
+ Bcfg2.Options.PathOption(
+ cf=("sslca_*", "config"),
+ help="Path to the openssl config for the CA"),
+ Bcfg2.Options.Option(
+ cf=("sslca_*", "passphrase"),
+ help="Passphrase for the CA private key"),
+ Bcfg2.Options.PathOption(
+ cf=("sslca_*", "chaincert"),
+ help="Path to the SSL chaining certificate for verification"),
+ Bcfg2.Options.BooleanOption(
+ cf=("sslca_*", "root_ca"),
+ help="Whether or not <chaincert> is a root CA (as opposed to "
+ "an intermediate cert"),
+ prefix="")]
+
+ def __init__(self, fname):
+ XMLCfgCreator.__init__(self, fname)
+ CfgVerifier.__init__(self, fname, None)
+ self.cmd = Executor()
+ self.cfg = get_cfg()
+
+ def build_req_config(self, metadata):
+ """ Generates a temporary openssl configuration file that is
+ used to generate the required certificate request. """
+ fd, fname = tempfile.mkstemp()
+ cfp = ConfigParser.ConfigParser({})
+ cfp.optionxform = str
+ defaults = dict(
+ req=dict(
+ default_md='sha1',
+ distinguished_name='req_distinguished_name',
+ req_extensions='v3_req',
+ x509_extensions='v3_req',
+ prompt='no'),
+ req_distinguished_name=dict(),
+ v3_req=dict(subjectAltName='@alt_names'),
+ alt_names=dict())
+ for section in list(defaults.keys()):
+ cfp.add_section(section)
+ for key in defaults[section]:
+ cfp.set(section, key, defaults[section][key])
+ spec = self.XMLMatch(metadata)
+ cert = spec.find("Cert")
+ altnamenum = 1
+ altnames = spec.findall('subjectAltName')
+ altnames.extend(list(metadata.aliases))
+ altnames.append(metadata.hostname)
+ for altname in altnames:
+ cfp.set('alt_names', 'DNS.' + str(altnamenum), altname)
+ altnamenum += 1
+ for item in ['C', 'L', 'ST', 'O', 'OU', 'emailAddress']:
+ if cert.get(item):
+ cfp.set('req_distinguished_name', item, cert.get(item))
+ cfp.set('req_distinguished_name', 'CN', metadata.hostname)
+ self.debug_log("Cfg: Writing temporary CSR config to %s" % fname)
+ try:
+ cfp.write(os.fdopen(fd, 'w'))
+ except IOError:
+ raise CfgCreationError("Cfg: Failed to write temporary CSR config "
+ "file: %s" % sys.exc_info()[1])
+ return fname
+
+ def build_request(self, keyfile, metadata):
+ """ Create the certificate request """
+ req_config = self.build_req_config(metadata)
+ try:
+ fd, req = tempfile.mkstemp()
+ os.close(fd)
+ cert = self.XMLMatch(metadata).find("Cert")
+ days = cert.get("days", "365")
+ cmd = ["openssl", "req", "-new", "-config", req_config,
+ "-days", days, "-key", keyfile, "-text", "-out", req]
+ result = self.cmd.run(cmd)
+ if not result.success:
+ raise CfgCreationError("Failed to generate CSR: %s" %
+ result.error)
+ return req
+ finally:
+ try:
+ os.unlink(req_config)
+ except OSError:
+ self.logger.error("Cfg: Failed to unlink temporary CSR "
+ "config: %s" % sys.exc_info()[1])
+
+ def get_ca(self, name):
+ """ get a dict describing a CA from the config file """
+ rv = dict()
+ prefix = "sslca_%s_" % name
+ for attr in dir(Bcfg2.Options.setup):
+ if attr.startswith(prefix):
+ rv[attr[len(prefix):]] = getattr(Bcfg2.Options.setup, attr)
+ return rv
+
+ def create_data(self, entry, metadata):
+ """ generate a new cert """
+ self.logger.info("Cfg: Generating new SSL cert for %s" % self.name)
+ cert = self.XMLMatch(metadata).find("Cert")
+ ca = self.get_ca(cert.get('ca', 'default'))
+ req = self.build_request(self._get_keyfile(cert, metadata), metadata)
+ try:
+ days = cert.get('days', '365')
+ cmd = ["openssl", "ca", "-config", ca['config'], "-in", req,
+ "-days", days, "-batch"]
+ passphrase = ca.get('passphrase')
+ if passphrase:
+ cmd.extend(["-passin", "pass:%s" % passphrase])
+ result = self.cmd.run(cmd)
+ if not result.success:
+ raise CfgCreationError("Failed to generate cert: %s" %
+ result.error)
+ except KeyError:
+ raise CfgCreationError("Cfg: [sslca_%s] section has no 'config' "
+ "option" % cert.get('ca', 'default'))
+ finally:
+ try:
+ os.unlink(req)
+ except OSError:
+ self.logger.error("Cfg: Failed to unlink temporary CSR: %s " %
+ sys.exc_info()[1])
+ data = result.stdout
+ if cert.get('append_chain') and 'chaincert' in ca:
+ data += open(ca['chaincert']).read()
+
+ self.write_data(data, **self.get_specificity(metadata))
+ return data
+
+ def verify_entry(self, entry, metadata, data):
+ fd, fname = tempfile.mkstemp()
+ self.debug_log("Cfg: Writing SSL cert %s to temporary file %s for "
+ "verification" % (entry.get("name"), fname))
+ os.fdopen(fd, 'w').write(data)
+ cert = self.XMLMatch(metadata).find("Cert")
+ ca = self.get_ca(cert.get('ca', 'default'))
+ try:
+ if ca.get('chaincert'):
+ self.verify_cert_against_ca(fname, entry, metadata)
+ self.verify_cert_against_key(fname,
+ self._get_keyfile(cert, metadata))
+ finally:
+ os.unlink(fname)
+
+ def _get_keyfile(self, cert, metadata):
+ """ Given a <Cert/> element and client metadata, return the
+ full path to the file on the filesystem that the key lives in."""
+ keypath = cert.get("key")
+ eset = self.cfg.entries[keypath]
+ try:
+ return eset.best_matching(metadata).name
+ except PluginExecutionError:
+ # SSL key needs to be created
+ try:
+ creator = eset.best_matching(metadata,
+ eset.get_handlers(metadata,
+ CfgCreator))
+ except PluginExecutionError:
+ raise CfgCreationError("Cfg: No SSL key or key creator "
+ "defined for %s" % keypath)
+
+ keyentry = lxml.etree.Element("Path", name=keypath)
+ creator.create_data(keyentry, metadata)
+
+ tries = 0
+ while True:
+ if tries >= 10:
+ raise CfgCreationError("Cfg: Timed out waiting for event "
+ "on SSL key at %s" % keypath)
+ get_fam().handle_events_in_interval(1)
+ try:
+ return eset.best_matching(metadata).name
+ except PluginExecutionError:
+ tries += 1
+ continue
+
+ def verify_cert_against_ca(self, filename, entry, metadata):
+ """
+ check that a certificate validates against the ca cert,
+ and that it has not expired.
+ """
+ cert = self.XMLMatch(metadata).find("Cert")
+ ca = self.get_ca(cert.get("ca", "default"))
+ chaincert = ca.get('chaincert')
+ cmd = ["openssl", "verify"]
+ is_root = ca.get('root_ca', "false").lower() == 'true'
+ if is_root:
+ cmd.append("-CAfile")
+ else:
+ # verifying based on an intermediate cert
+ cmd.extend(["-purpose", "sslserver", "-untrusted"])
+ cmd.extend([chaincert, filename])
+ self.debug_log("Cfg: Verifying %s against CA" % entry.get("name"))
+ result = self.cmd.run(cmd)
+ if result.stdout == cert + ": OK\n":
+ self.debug_log("Cfg: %s verified successfully against CA" %
+ entry.get("name"))
+ else:
+ raise CfgVerificationError("%s failed verification against CA: %s"
+ % (entry.get("name"), result.error))
+
+ def _get_modulus(self, fname, ftype="x509"):
+ """ get the modulus from the given file """
+ cmd = ["openssl", ftype, "-noout", "-modulus", "-in", fname]
+ self.debug_log("Cfg: Getting modulus of %s for verification: %s" %
+ (fname, " ".join(cmd)))
+ result = self.cmd.run(cmd)
+ if not result.success:
+ raise CfgVerificationError("Failed to get modulus of %s: %s" %
+ (fname, result.error))
+ return result.stdout.strip()
+
+ def verify_cert_against_key(self, filename, keyfile):
+ """ check that a certificate validates against its private
+ key. """
+ cert = self._get_modulus(filename)
+ key = self._get_modulus(keyfile, ftype="rsa")
+ if cert == key:
+ self.debug_log("Cfg: %s verified successfully against key %s" %
+ (filename, keyfile))
+ else:
+ raise CfgVerificationError("%s failed verification against key %s"
+ % (filename, keyfile))
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCAKeyCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCAKeyCreator.py
new file mode 100644
index 000000000..a158302be
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCAKeyCreator.py
@@ -0,0 +1,36 @@
+""" Cfg creator that creates SSL keys """
+
+from Bcfg2.Utils import Executor
+from Bcfg2.Server.Plugins.Cfg import CfgCreationError, XMLCfgCreator
+
+
+class CfgSSLCAKeyCreator(XMLCfgCreator):
+ """ Cfg creator that creates SSL keys """
+
+ #: Different configurations for different clients/groups can be
+ #: handled with Client and Group tags within sslkey.xml
+ __specific__ = False
+
+ __basenames__ = ["sslkey.xml"]
+
+ cfg_section = "sslca"
+
+ def create_data(self, entry, metadata):
+ self.logger.info("Cfg: Generating new SSL key for %s" % self.name)
+ spec = self.XMLMatch(metadata)
+ key = spec.find("Key")
+ if not key:
+ key = dict()
+ ktype = key.get('type', 'rsa')
+ bits = key.get('bits', '2048')
+ if ktype == 'rsa':
+ cmd = ["openssl", "genrsa", bits]
+ elif ktype == 'dsa':
+ cmd = ["openssl", "dsaparam", "-noout", "-genkey", bits]
+ result = Executor().run(cmd)
+ if not result.success:
+ raise CfgCreationError("Failed to generate key %s for %s: %s" %
+ (self.name, metadata.hostname,
+ result.error))
+ self.write_data(result.stdout, **self.get_specificity(metadata))
+ return result.stdout
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
index c6e2d0acb..632a5c3bf 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
@@ -3,60 +3,39 @@
import re
import os
import sys
-import stat
import errno
import operator
import lxml.etree
import Bcfg2.Options
import Bcfg2.Server.Plugin
-import Bcfg2.Server.Lint
-from fnmatch import fnmatch
from Bcfg2.Server.Plugin import PluginExecutionError
# pylint: disable=W0622
-from Bcfg2.Compat import u_str, unicode, b64encode, walk_packages, \
- any, oct_mode
+from Bcfg2.Compat import u_str, unicode, b64encode, any, walk_packages
# pylint: enable=W0622
-#: SETUP contains a reference to the
-#: :class:`Bcfg2.Options.OptionParser` created by the Bcfg2 core for
-#: parsing command-line and config file options.
-#: :class:`Bcfg2.Server.Plugins.Cfg.Cfg` stores it in a module global
-#: so that the handler objects can access it, because there is no other
-#: facility for passing a setup object from a
-#: :class:`Bcfg2.Server.Plugin.helpers.GroupSpool` to its
-#: :class:`Bcfg2.Server.Plugin.helpers.EntrySet` objects and thence to
-#: the EntrySet children.
-SETUP = None
-
-#: CFG is a reference to the :class:`Bcfg2.Server.Plugins.Cfg.Cfg`
-#: plugin object created by the Bcfg2 core. This is provided so that
-#: the handler objects can access it as necessary, since the existing
-#: :class:`Bcfg2.Server.Plugin.helpers.GroupSpool` and
-#: :class:`Bcfg2.Server.Plugin.helpers.EntrySet` classes have no
-#: 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):
+try:
+ import Bcfg2.Server.Encryption
+ HAS_CRYPTO = True
+except ImportError:
+ HAS_CRYPTO = False
+
+_handlers = [m[1] # pylint: disable=C0103
+ for m in walk_packages(path=__path__)]
+
+_CFG = None
+
+
+def get_cfg():
+ """ Get the :class:`Bcfg2.Server.Plugins.Cfg.Cfg` plugin object
+ created by the Bcfg2 core. This is provided so that the handler
+ objects can access it as necessary, since the existing
+ :class:`Bcfg2.Server.Plugin.helpers.GroupSpool` and
+ :class:`Bcfg2.Server.Plugin.helpers.EntrySet` classes have no
+ facility for passing it otherwise."""
+ return _CFG
+
+
+class CfgBaseFileMatcher(Bcfg2.Server.Plugin.SpecificData):
""" .. currentmodule:: Bcfg2.Server.Plugins.Cfg
CfgBaseFileMatcher is the parent class for all Cfg handler
@@ -100,13 +79,10 @@ class CfgBaseFileMatcher(Bcfg2.Server.Plugin.SpecificData,
#: Flag to indicate an experimental handler.
experimental = False
- def __init__(self, name, specific, encoding):
+ def __init__(self, name, specific):
if not self.__specific__ and not specific:
specific = Bcfg2.Server.Plugin.Specificity(all=True)
- Bcfg2.Server.Plugin.SpecificData.__init__(self, name, specific,
- encoding)
- Bcfg2.Server.Plugin.Debuggable.__init__(self)
- self.encoding = encoding
+ Bcfg2.Server.Plugin.SpecificData.__init__(self, name, specific)
__init__.__doc__ = Bcfg2.Server.Plugin.SpecificData.__init__.__doc__ + \
"""
.. -----
@@ -197,7 +173,7 @@ class CfgGenerator(CfgBaseFileMatcher):
client. See :class:`Bcfg2.Server.Plugin.helpers.EntrySet` for more
details on how the best handler is chosen."""
- def __init__(self, name, specific, encoding):
+ def __init__(self, name, specific):
# we define an __init__ that just calls the parent __init__,
# so that we can set the docstring on __init__ to something
# different from the parent __init__ -- namely, the parent
@@ -205,7 +181,7 @@ class CfgGenerator(CfgBaseFileMatcher):
# which we use to delineate the actual docs from the
# .. autoattribute hacks we have to do to get private
# attributes included in sphinx 1.0 """
- CfgBaseFileMatcher.__init__(self, name, specific, encoding)
+ CfgBaseFileMatcher.__init__(self, name, specific)
__init__.__doc__ = CfgBaseFileMatcher.__init__.__doc__.split(".. -----")[0]
def get_data(self, entry, metadata): # pylint: disable=W0613
@@ -225,9 +201,9 @@ class CfgFilter(CfgBaseFileMatcher):
""" CfgFilters modify the initial content of a file after it has
been generated by a :class:`Bcfg2.Server.Plugins.Cfg.CfgGenerator`. """
- def __init__(self, name, specific, encoding):
+ def __init__(self, name, specific):
# see comment on CfgGenerator.__init__ above
- CfgBaseFileMatcher.__init__(self, name, specific, encoding)
+ CfgBaseFileMatcher.__init__(self, name, specific)
__init__.__doc__ = CfgBaseFileMatcher.__init__.__doc__.split(".. -----")[0]
def modify_data(self, entry, metadata, data):
@@ -249,10 +225,7 @@ class CfgFilter(CfgBaseFileMatcher):
class CfgInfo(CfgBaseFileMatcher):
""" CfgInfo handlers provide metadata (owner, group, paranoid,
- etc.) for a file entry.
-
- .. private-include: _set_info
- """
+ etc.) for a file entry. """
#: Whether or not the files handled by this handler are permitted
#: to have specificity indicators in their filenames -- e.g.,
@@ -268,7 +241,7 @@ class CfgInfo(CfgBaseFileMatcher):
.. -----
.. autoattribute:: Bcfg2.Server.Plugins.Cfg.CfgInfo.__specific__
"""
- CfgBaseFileMatcher.__init__(self, fname, None, None)
+ CfgBaseFileMatcher.__init__(self, fname, None)
def bind_info_to_entry(self, entry, metadata):
""" Assign the appropriate attributes to the entry, modifying
@@ -282,20 +255,6 @@ class CfgInfo(CfgBaseFileMatcher):
"""
raise NotImplementedError
- def _set_info(self, entry, info):
- """ Helper function to assign a dict of info attributes to an
- entry object. ``entry`` is modified in-place.
-
- :param entry: The abstract entry to bind the info to
- :type entry: lxml.etree._Element
- :param info: A dict of attribute: value pairs
- :type info: dict
- :returns: None
- """
- for key, value in list(info.items()):
- if not key.startswith("__"):
- entry.attrib[key] = value
-
class CfgVerifier(CfgBaseFileMatcher):
""" CfgVerifier handlers validate entry data once it has been
@@ -305,9 +264,9 @@ class CfgVerifier(CfgBaseFileMatcher):
etc.), or both.
"""
- def __init__(self, name, specific, encoding):
+ def __init__(self, name, specific):
# see comment on CfgGenerator.__init__ above
- CfgBaseFileMatcher.__init__(self, name, specific, encoding)
+ CfgBaseFileMatcher.__init__(self, name, specific)
__init__.__doc__ = CfgBaseFileMatcher.__init__.__doc__.split(".. -----")[0]
def verify_entry(self, entry, metadata, data):
@@ -338,18 +297,15 @@ class CfgCreator(CfgBaseFileMatcher):
#: file, and are thus not specific
__specific__ = False
- #: The CfgCreator interface is experimental at this time
- experimental = True
-
def __init__(self, fname):
"""
:param name: The full path to the file
:type name: string
.. -----
- .. autoattribute:: Bcfg2.Server.Plugins.Cfg.CfgCreator.__specific__
+ .. autoattribute:: Bcfg2.Server.Plugins.Cfg.CfgInfo.__specific__
"""
- CfgBaseFileMatcher.__init__(self, fname, None, None)
+ CfgBaseFileMatcher.__init__(self, fname, None)
def create_data(self, entry, metadata):
""" Create new data for the given entry and write it to disk
@@ -369,7 +325,9 @@ class CfgCreator(CfgBaseFileMatcher):
``host`` is given, it will be host-specific. It will be
group-specific if ``group`` and ``prio`` are given. If
neither ``host`` nor ``group`` is given, the filename will be
- non-specific.
+ non-specific. In general, this will be called as::
+
+ self.get_filename(**self.get_specificity(metadata))
:param host: The file applies to the given host
:type host: bool
@@ -400,6 +358,9 @@ class CfgCreator(CfgBaseFileMatcher):
written as a host-specific file, or as a group-specific file
if ``group`` and ``prio`` are given. If neither ``host`` nor
``group`` is given, it will be written as a non-specific file.
+ In general, this will be called as::
+
+ self.write_data(data, **self.get_specificity(metadata))
:param data: The data to write
:type data: string
@@ -419,7 +380,7 @@ class CfgCreator(CfgBaseFileMatcher):
:raises: :exc:`Bcfg2.Server.Plugins.Cfg.CfgCreationError`
"""
fileloc = self.get_filename(host=host, group=group, prio=prio, ext=ext)
- self.debug_log("%s: Writing new file %s" % (self.name, fileloc))
+ self.debug_log("Cfg: Writing new file %s" % fileloc)
try:
os.makedirs(os.path.dirname(fileloc))
except OSError:
@@ -435,6 +396,95 @@ class CfgCreator(CfgBaseFileMatcher):
raise CfgCreationError("Could not write %s: %s" % (fileloc, err))
+class XMLCfgCreator(CfgCreator, # pylint: disable=W0223
+ Bcfg2.Server.Plugin.StructFile):
+ """ A CfgCreator that uses XML to describe how data should be
+ generated. """
+
+ #: Whether or not the created data from this class can be
+ #: encrypted
+ encryptable = True
+
+ #: Encryption and creation settings can be stored in bcfg2.conf,
+ #: either under the [cfg] section, or under the named section.
+ cfg_section = None
+
+ def __init__(self, name):
+ CfgCreator.__init__(self, name)
+ Bcfg2.Server.Plugin.StructFile.__init__(self, name)
+
+ def handle_event(self, event):
+ CfgCreator.handle_event(self, event)
+ Bcfg2.Server.Plugin.StructFile.HandleEvent(self, event)
+
+ @property
+ def passphrase(self):
+ """ The passphrase used to encrypt created data """
+ if self.cfg_section:
+ localopt = "%s_passphrase" % self.cfg_section
+ passphrase = getattr(Bcfg2.Options.setup, localopt,
+ Bcfg2.Options.setup.cfg_passphrase)
+ else:
+ passphrase = Bcfg2.Options.setup.cfg_passphrase
+ if passphrase is None:
+ return None
+ try:
+ return Bcfg2.Options.setup.passphrases[passphrase]
+ except KeyError:
+ raise CfgCreationError("%s: No such passphrase: %s" %
+ (self.__class__.__name__, passphrase))
+
+ @property
+ def category(self):
+ """ The category to which created data is specific """
+ if self.cfg_section:
+ localopt = "%s_category" % self.cfg_section
+ return getattr(Bcfg2.Options.setup, localopt,
+ Bcfg2.Options.setup.cfg_category)
+ else:
+ return Bcfg2.Options.setup.cfg_category
+
+ def write_data(self, data, host=None, group=None, prio=0, ext=''):
+ if HAS_CRYPTO and self.encryptable and self.passphrase:
+ self.debug_log("Cfg: Encrypting created data")
+ data = Bcfg2.Server.Encryption.ssl_encrypt(data, self.passphrase)
+ ext = '.crypt'
+ CfgCreator.write_data(self, data, host=host, group=group, prio=prio,
+ ext=ext)
+
+ def get_specificity(self, metadata):
+ """ Get config settings for key generation specificity
+ (per-host or per-group).
+
+ :param metadata: The client metadata to create data for
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :returns: dict - A dict of specificity arguments suitable for
+ passing to
+ :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.write_data`
+ or
+ :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.get_filename`
+ """
+ category = self.xdata.get("category", self.category)
+ if category is None:
+ per_host_default = "true"
+ else:
+ per_host_default = "false"
+ per_host = self.xdata.get("perhost",
+ per_host_default).lower() == "true"
+
+ specificity = dict(host=metadata.hostname)
+ if category and not per_host:
+ group = metadata.group_in_category(category)
+ if group:
+ specificity = dict(group=group,
+ prio=int(self.xdata.get("priority", 50)))
+ else:
+ self.logger.info("Cfg: %s has no group in category %s, "
+ "creating host-specific data" %
+ (metadata.hostname, category))
+ return specificity
+
+
class CfgVerificationError(Exception):
""" Raised by
:func:`Bcfg2.Server.Plugins.Cfg.CfgVerifier.verify_entry` when an
@@ -453,37 +503,27 @@ class CfgDefaultInfo(CfgInfo):
""" :class:`Bcfg2.Server.Plugins.Cfg.Cfg` handler that supplies a
default set of file metadata """
- def __init__(self, defaults):
+ def __init__(self):
CfgInfo.__init__(self, '')
- self.defaults = defaults
__init__.__doc__ = CfgInfo.__init__.__doc__.split(".. -----")[0]
- def bind_info_to_entry(self, entry, metadata):
- self._set_info(entry, self.defaults)
+ def bind_info_to_entry(self, entry, _):
+ for key, value in Bcfg2.Server.Plugin.default_path_metadata().items():
+ entry.attrib[key] = value
bind_info_to_entry.__doc__ = CfgInfo.bind_info_to_entry.__doc__
-#: A :class:`CfgDefaultInfo` object instantiated with
-#: :attr:`Bcfg2.Server.Plugin.helper.DEFAULT_FILE_METADATA` as its
-#: default metadata. This is used to set a default file metadata set
-#: on an entry before a "real" :class:`CfgInfo` handler applies its
-#: metadata to the entry.
-DEFAULT_INFO = CfgDefaultInfo(Bcfg2.Server.Plugin.DEFAULT_FILE_METADATA)
-
-class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
- Bcfg2.Server.Plugin.Debuggable):
+class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet):
""" Handle a collection of host- and group-specific Cfg files with
multiple different Cfg handlers in a single directory. """
- def __init__(self, basename, path, entry_type, encoding):
- Bcfg2.Server.Plugin.EntrySet.__init__(self, basename, path,
- entry_type, encoding)
- Bcfg2.Server.Plugin.Debuggable.__init__(self)
+ def __init__(self, basename, path, entry_type):
+ Bcfg2.Server.Plugin.EntrySet.__init__(self, basename, path, entry_type)
self.specific = None
__init__.__doc__ = Bcfg2.Server.Plugin.EntrySet.__doc__
def set_debug(self, debug):
- rv = Bcfg2.Server.Plugin.Debuggable.set_debug(self, debug)
+ rv = Bcfg2.Server.Plugin.EntrySet.set_debug(self, debug)
for entry in self.entries.values():
entry.set_debug(debug)
return rv
@@ -504,7 +544,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
# process a bogus changed event like a created
return
- for hdlr in handlers():
+ for hdlr in Bcfg2.Options.setup.cfg_handlers:
if hdlr.handles(event, basename=self.path):
if action == 'changed':
# warn about a bogus 'changed' event, but
@@ -597,7 +637,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
# most specific to least specific.
data = fltr.modify_data(entry, metadata, data)
- if SETUP['validate']:
+ if Bcfg2.Options.setup.cfg_validation:
try:
self._validate_data(entry, metadata, data)
except CfgVerificationError:
@@ -613,7 +653,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
if not isinstance(data, unicode):
if not isinstance(data, str):
data = data.decode('utf-8')
- data = u_str(data, self.encoding)
+ data = u_str(data, Bcfg2.Options.setup.encoding)
except UnicodeDecodeError:
msg = "Failed to decode %s: %s" % (entry.get('name'),
sys.exc_info()[1])
@@ -652,7 +692,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
rv = []
for ent in self.entries.values():
if (isinstance(ent, handler_type) and
- (not ent.__specific__ or ent.specific.matches(metadata))):
+ (not ent.__specific__ or ent.specific.matches(metadata))):
rv.append(ent)
return rv
@@ -668,7 +708,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
:returns: None
"""
info_handlers = self.get_handlers(metadata, CfgInfo)
- DEFAULT_INFO.bind_info_to_entry(entry, metadata)
+ CfgDefaultInfo().bind_info_to_entry(entry, metadata)
if len(info_handlers) > 1:
self.logger.error("More than one info supplier found for %s: %s" %
(entry.get("name"), info_handlers))
@@ -717,13 +757,6 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
# raises an appropriate exception
return (self._create_data(entry, metadata), None)
- if entry.get('mode').lower() == 'inherit':
- # use on-disk permissions
- self.logger.warning("Cfg: %s: Use of mode='inherit' is deprecated"
- % entry.get("name"))
- fname = os.path.join(self.path, generator.name)
- entry.set('mode',
- oct_mode(stat.S_IMODE(os.stat(fname).st_mode)))
try:
return (generator.get_data(entry, metadata), generator)
except:
@@ -801,10 +834,10 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
self.logger.error(msg)
raise PluginExecutionError(msg)
try:
- etext = new_entry['text'].encode(self.encoding)
+ etext = new_entry['text'].encode(Bcfg2.Options.setup.encoding)
except:
msg = "Cfg: Cannot encode content of %s as %s" % \
- (name, self.encoding)
+ (name, Bcfg2.Options.setup.encoding)
self.logger.error(msg)
raise PluginExecutionError(msg)
open(name, 'w').write(etext)
@@ -812,13 +845,6 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
badattr = [attr for attr in ['owner', 'group', 'mode']
if attr in new_entry]
if badattr:
- # check for info files and inform user of their removal
- for ifile in ['info', ':info']:
- info = os.path.join(self.path, ifile)
- if os.path.exists(info):
- self.logger.info("Removing %s and replacing with info.xml"
- % info)
- os.remove(info)
metadata_updates = {}
metadata_updates.update(self.metadata)
for attr in badattr:
@@ -836,6 +862,11 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
flag=log)
+class CfgHandlerAction(Bcfg2.Options.ComponentAction):
+ """ Option parser action to load Cfg handlers """
+ bases = ['Bcfg2.Server.Plugins.Cfg']
+
+
class Cfg(Bcfg2.Server.Plugin.GroupSpool,
Bcfg2.Server.Plugin.PullTarget):
""" The Cfg plugin provides a repository to describe configuration
@@ -847,17 +878,30 @@ class Cfg(Bcfg2.Server.Plugin.GroupSpool,
es_cls = CfgEntrySet
es_child_cls = Bcfg2.Server.Plugin.SpecificData
+ options = Bcfg2.Server.Plugin.GroupSpool.options + [
+ Bcfg2.Options.BooleanOption(
+ '--cfg-validation', cf=('cfg', 'validation'), default=True,
+ help='Run validation on Cfg files'),
+ Bcfg2.Options.Option(
+ cf=('cfg', 'category'), dest="cfg_category",
+ help='The default name of the metadata category that created data '
+ 'is specific to'),
+ Bcfg2.Options.Option(
+ cf=('cfg', 'passphrase'), dest="cfg_passphrase",
+ help='The default passphrase name used to encrypt created data'),
+ Bcfg2.Options.Option(
+ cf=("cfg", "handlers"), dest="cfg_handlers",
+ help="Cfg handlers to load",
+ type=Bcfg2.Options.Types.comma_list, action=CfgHandlerAction,
+ default=_handlers)]
+
def __init__(self, core, datastore):
- global SETUP, CFG # pylint: disable=W0603
+ global _CFG # pylint: disable=W0603
Bcfg2.Server.Plugin.GroupSpool.__init__(self, core, datastore)
Bcfg2.Server.Plugin.PullTarget.__init__(self)
-
- CFG = self
-
- SETUP = core.setup
- if 'validate' not in SETUP:
- SETUP.add_option('validate', Bcfg2.Options.CFG_VALIDATION)
- SETUP.reparse()
+ Bcfg2.Options.setup.cfg_handlers.sort(
+ key=operator.attrgetter("__priority__"))
+ _CFG = self
__init__.__doc__ = Bcfg2.Server.Plugin.GroupSpool.__init__.__doc__
def has_generator(self, entry, metadata):
@@ -891,127 +935,3 @@ class Cfg(Bcfg2.Server.Plugin.GroupSpool,
log)
AcceptPullData.__doc__ = \
Bcfg2.Server.Plugin.PullTarget.AcceptPullData.__doc__
-
-
-class CfgLint(Bcfg2.Server.Lint.ServerPlugin):
- """ warn about usage of .cat and .diff files """
-
- def Run(self):
- 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()
- self.check_conflicting_handlers()
-
- @classmethod
- def Errors(cls):
- return {"cat-file-used": "warning",
- "diff-file-used": "warning",
- "no-pubkey-xml": "warning",
- "unknown-cfg-files": "error",
- "extra-cfg-files": "error",
- "multiple-global-handlers": "error"}
-
- def check_delta(self, basename, entry):
- """ check that no .cat or .diff files are in use """
- for fname, handler in entry.entries.items():
- path = handler.name
- if self.HandlesFile(path) and isinstance(handler, CfgFilter):
- extension = fname.split(".")[-1]
- if extension in ["cat", "diff"]:
- self.LintError("%s-file-used" % extension,
- "%s file used on %s: %s" % (extension,
- basename,
- fname))
-
- def check_pubkey(self, basename, entry):
- """ check that privkey.xml files have corresponding pubkey.xml
- files """
- if "privkey.xml" not in entry.entries:
- return
- privkey = entry.entries["privkey.xml"]
- if not self.HandlesFile(privkey.name):
- return
-
- pubkey = basename + ".pub"
- if pubkey not in self.core.plugins['Cfg'].entries:
- self.LintError("no-pubkey-xml",
- "%s has no corresponding pubkey.xml at %s" %
- (basename, pubkey))
- else:
- pubset = self.core.plugins['Cfg'].entries[pubkey]
- if "pubkey.xml" not in pubset.entries:
- self.LintError("no-pubkey-xml",
- "%s has no corresponding pubkey.xml at %s" %
- (basename, pubkey))
-
- def _list_path_components(self, path):
- """ Get a list of all components of a path. E.g.,
- ``self._list_path_components("/foo/bar/foobaz")`` would return
- ``["foo", "bar", "foo", "baz"]``. The list is not guaranteed
- to be in order."""
- rv = []
- remaining, component = os.path.split(path)
- while component != '':
- rv.append(component)
- remaining, component = os.path.split(remaining)
- return rv
-
- def check_conflicting_handlers(self):
- """ Check that a single entryset doesn't have multiple
- non-specific (i.e., 'all') handlers. """
- cfg = self.core.plugins['Cfg']
- for eset in cfg.entries.values():
- alls = [e for e in eset.entries.values()
- if (e.specific.all and
- issubclass(e.__class__, CfgGenerator))]
- if len(alls) > 1:
- self.LintError("multiple-global-handlers",
- "%s has multiple global handlers: %s" %
- (eset.path, ", ".join(os.path.basename(e.name)
- for e in alls)))
-
- 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 = set()
- for hdlr in handlers():
- ignore.update(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):
- for fname in files:
- fpath = os.path.join(root, fname)
- # check against the handler ignore patterns and the
- # global FAM ignore list
- if (not any(fname.endswith("." + i) for i in ignore) and
- not any(fnmatch(fpath, p)
- for p in self.config['ignore']) and
- not any(fnmatch(c, p)
- for p in self.config['ignore']
- for c in self._list_path_components(fpath))):
- all_files.add(fpath)
-
- # 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))