summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Client
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Client')
-rw-r--r--src/lib/Bcfg2/Client/Proxy.py104
-rw-r--r--src/lib/Bcfg2/Client/Tools/APK.py4
-rw-r--r--src/lib/Bcfg2/Client/Tools/Action.py2
-rw-r--r--src/lib/Bcfg2/Client/Tools/FreeBSDInit.py140
-rw-r--r--src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py2
-rw-r--r--src/lib/Bcfg2/Client/Tools/IPS.py2
-rw-r--r--src/lib/Bcfg2/Client/Tools/MacPorts.py8
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py296
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/File.py44
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/__init__.py7
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/base.py116
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIXUsers.py5
-rw-r--r--src/lib/Bcfg2/Client/Tools/Pacman.py7
-rw-r--r--src/lib/Bcfg2/Client/Tools/Pkgng.py226
-rw-r--r--src/lib/Bcfg2/Client/Tools/Portage.py12
-rw-r--r--src/lib/Bcfg2/Client/Tools/SMF.py2
-rw-r--r--src/lib/Bcfg2/Client/Tools/Systemd.py2
-rw-r--r--src/lib/Bcfg2/Client/Tools/VCS.py5
-rw-r--r--src/lib/Bcfg2/Client/Tools/YUM.py55
-rw-r--r--src/lib/Bcfg2/Client/XML.py22
-rw-r--r--src/lib/Bcfg2/Client/__init__.py43
21 files changed, 878 insertions, 226 deletions
diff --git a/src/lib/Bcfg2/Client/Proxy.py b/src/lib/Bcfg2/Client/Proxy.py
index a464d6a40..679b4c52b 100644
--- a/src/lib/Bcfg2/Client/Proxy.py
+++ b/src/lib/Bcfg2/Client/Proxy.py
@@ -12,13 +12,9 @@ from Bcfg2.Compat import httplib, xmlrpclib, urlparse, quote_plus
# M2Crypto instead.
try:
import ssl
- SSL_LIB = 'py26_ssl'
SSL_ERROR = ssl.SSLError
except ImportError:
- from M2Crypto import SSL
- import M2Crypto.SSL.Checker
- SSL_LIB = 'm2crypto'
- SSL_ERROR = SSL.SSLError
+ raise Exception("No SSL module support")
version = sys.version_info[:2]
@@ -123,7 +119,7 @@ class SSLHTTPConnection(httplib.HTTPConnection):
"""
def __init__(self, host, port=None, strict=None, timeout=90, key=None,
- cert=None, ca=None, scns=None, protocol='xmlrpc/ssl'):
+ cert=None, ca=None, scns=None, protocol='xmlrpc/tlsv1'):
"""Initializes the `httplib.HTTPConnection` object and stores security
parameters
@@ -148,15 +144,15 @@ class SSLHTTPConnection(httplib.HTTPConnection):
specify the same file as `cert` if using a file that
contains both. See
http://docs.python.org/library/ssl.html#ssl-certificates
- for details. Required if using xmlrpc/ssl with client
- certificate authentication.
+ for details. Required if using client certificate
+ authentication.
cert : string, optional
The file system path to the local endpoint's SSL
certificate. May specify the same file as `cert` if using
a file that contains both. See
http://docs.python.org/library/ssl.html#ssl-certificates
- for details. Required if using xmlrpc/ssl with client
- certificate authentication.
+ for details. Required if using client certificate
+ authentication.
ca : string, optional
The file system path to a set of concatenated certificate
authority certs, which are used to validate certificates
@@ -187,15 +183,6 @@ class SSLHTTPConnection(httplib.HTTPConnection):
self.timeout = timeout
def connect(self):
- """Initiates a connection using previously set attributes."""
- if SSL_LIB == 'py26_ssl':
- self._connect_py26ssl()
- elif SSL_LIB == 'm2crypto':
- self._connect_m2crypto()
- else:
- raise Exception("No SSL module support")
-
- def _connect_py26ssl(self):
"""Initiates a connection using the ssl module."""
# check for IPv6
hostip = socket.getaddrinfo(self.host,
@@ -242,60 +229,11 @@ class SSLHTTPConnection(httplib.HTTPConnection):
raise CertificateError(scn)
self.sock.closeSocket = True
- def _connect_m2crypto(self):
- """Initiates a connection using the M2Crypto module."""
-
- if self.protocol == 'xmlrpc/ssl':
- ctx = SSL.Context('sslv23')
- elif self.protocol == 'xmlrpc/tlsv1':
- ctx = SSL.Context('tlsv1')
- else:
- self.logger.error("Unknown protocol %s" % (self.protocol))
- raise Exception("unknown protocol %s" % self.protocol)
-
- if self.ca:
- # Use the certificate authority to validate the cert
- # presented by the server
- ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert,
- depth=9)
- if ctx.load_verify_locations(self.ca) != 1:
- raise Exception('No CA certs')
- else:
- self.logger.warning("No ca is specified. Cannot authenticate the "
- "server with SSL.")
-
- if self.cert and self.key:
- # A cert/key is defined, use them to support client
- # authentication to the server
- ctx.load_cert(self.cert, self.key)
- elif self.cert:
- self.logger.warning("SSL cert specfied, but no key. Cannot "
- "authenticate this client with SSL.")
- elif self.key:
- self.logger.warning("SSL key specfied, but no cert. Cannot "
- "authenticate this client with SSL.")
-
- self.sock = SSL.Connection(ctx)
- if re.match('\\d+\\.\\d+\\.\\d+\\.\\d+', self.host):
- # host is ip address
- try:
- hostname = socket.gethostbyaddr(self.host)[0]
- except:
- # fall back to ip address
- hostname = self.host
- else:
- hostname = self.host
- try:
- self.sock.connect((hostname, self.port))
- # automatically checks cert matches host
- except M2Crypto.SSL.Checker.WrongHost:
- wr = sys.exc_info()[1]
- raise CertificateError(wr)
-
class XMLRPCTransport(xmlrpclib.Transport):
def __init__(self, key=None, cert=None, ca=None,
- scns=None, use_datetime=0, timeout=90):
+ scns=None, use_datetime=0, timeout=90,
+ protocol='xmlrpc/tlsv1'):
if hasattr(xmlrpclib.Transport, '__init__'):
xmlrpclib.Transport.__init__(self, use_datetime)
self.key = key
@@ -303,6 +241,7 @@ class XMLRPCTransport(xmlrpclib.Transport):
self.ca = ca
self.scns = scns
self.timeout = timeout
+ self.protocol = protocol
def make_connection(self, host):
host, self._extra_headers = self.get_host_info(host)[0:2]
@@ -311,7 +250,8 @@ class XMLRPCTransport(xmlrpclib.Transport):
cert=self.cert,
ca=self.ca,
scns=self.scns,
- timeout=self.timeout)
+ timeout=self.timeout,
+ protocol=self.protocol)
def request(self, host, handler, request_body, verbose=0):
"""Send request to server and return response."""
@@ -354,9 +294,15 @@ class ComponentProxy(xmlrpclib.ServerProxy):
"""Constructs proxies to components. """
options = [
- Bcfg2.Options.Common.location, Bcfg2.Options.Common.ssl_key,
- Bcfg2.Options.Common.ssl_cert, Bcfg2.Options.Common.ssl_ca,
+ Bcfg2.Options.Common.location, Bcfg2.Options.Common.ssl_ca,
Bcfg2.Options.Common.password, Bcfg2.Options.Common.client_timeout,
+ Bcfg2.Options.Common.protocol,
+ Bcfg2.Options.PathOption(
+ '--ssl-key', cf=('communication', 'key'), dest="key",
+ help='Path to SSL key'),
+ Bcfg2.Options.PathOption(
+ cf=('communication', 'certificate'), dest="cert",
+ help='Path to SSL certificate'),
Bcfg2.Options.Option(
"-u", "--user", default="root", cf=('communication', 'user'),
help='The user to provide for authentication'),
@@ -386,10 +332,12 @@ class ComponentProxy(xmlrpclib.ServerProxy):
path)
else:
url = Bcfg2.Options.setup.server
- ssl_trans = XMLRPCTransport(Bcfg2.Options.setup.key,
- Bcfg2.Options.setup.cert,
- Bcfg2.Options.setup.ca,
- Bcfg2.Options.setup.ssl_cns,
- Bcfg2.Options.setup.client_timeout)
+ ssl_trans = XMLRPCTransport(
+ key=Bcfg2.Options.setup.key,
+ cert=Bcfg2.Options.setup.cert,
+ ca=Bcfg2.Options.setup.ca,
+ scns=Bcfg2.Options.setup.ssl_cns,
+ timeout=Bcfg2.Options.setup.client_timeout,
+ protocol=Bcfg2.Options.setup.protocol)
xmlrpclib.ServerProxy.__init__(self, url,
allow_none=True, transport=ssl_trans)
diff --git a/src/lib/Bcfg2/Client/Tools/APK.py b/src/lib/Bcfg2/Client/Tools/APK.py
index 457197c28..7313f6fcc 100644
--- a/src/lib/Bcfg2/Client/Tools/APK.py
+++ b/src/lib/Bcfg2/Client/Tools/APK.py
@@ -25,7 +25,7 @@ class APK(Bcfg2.Client.Tools.PkgTool):
def VerifyPackage(self, entry, _):
"""Verify Package status for entry."""
- if not 'version' in entry.attrib:
+ if 'version' not in entry.attrib:
self.logger.info("Cannot verify unversioned package %s" %
entry.attrib['name'])
return False
@@ -33,7 +33,7 @@ class APK(Bcfg2.Client.Tools.PkgTool):
if entry.attrib['name'] in self.installed:
if entry.attrib['version'] in \
['auto', self.installed[entry.attrib['name']]]:
- #FIXME: Does APK have any sort of verification mechanism?
+ # FIXME: Does APK have any sort of verification mechanism?
return True
else:
self.logger.info(" pkg %s at version %s, not %s" %
diff --git a/src/lib/Bcfg2/Client/Tools/Action.py b/src/lib/Bcfg2/Client/Tools/Action.py
index 5549b1717..dedc50d89 100644
--- a/src/lib/Bcfg2/Client/Tools/Action.py
+++ b/src/lib/Bcfg2/Client/Tools/Action.py
@@ -36,7 +36,7 @@ class Action(Bcfg2.Client.Tools.Tool):
shell = True
shell_string = '(in shell) '
- if not Bcfg2.Options.setup.dryrun:
+ if not Bcfg2.Options.setup.dry_run:
if Bcfg2.Options.setup.interactive:
prompt = ('Run Action %s%s, %s: (y/N): ' %
(shell_string, entry.get('name'),
diff --git a/src/lib/Bcfg2/Client/Tools/FreeBSDInit.py b/src/lib/Bcfg2/Client/Tools/FreeBSDInit.py
index 2ab64f86d..24bc4cf36 100644
--- a/src/lib/Bcfg2/Client/Tools/FreeBSDInit.py
+++ b/src/lib/Bcfg2/Client/Tools/FreeBSDInit.py
@@ -1,27 +1,143 @@
"""FreeBSD Init Support for Bcfg2."""
-__revision__ = '$Rev$'
-
-# TODO
-# - hardcoded path to ports rc.d
-# - doesn't know about /etc/rc.d/
import os
+import re
+import Bcfg2.Options
import Bcfg2.Client.Tools
class FreeBSDInit(Bcfg2.Client.Tools.SvcTool):
"""FreeBSD service support for Bcfg2."""
name = 'FreeBSDInit'
+ __execs__ = ['/usr/sbin/service', '/usr/sbin/sysrc']
__handles__ = [('Service', 'freebsd')]
__req__ = {'Service': ['name', 'status']}
+ rcvar_re = re.compile(r'^(?P<var>[a-z_]+_enable)="[A-Z]+"$')
- def __init__(self, config):
- Bcfg2.Client.Tools.SvcTool.__init__(self, config)
- if os.uname()[0] != 'FreeBSD':
- raise Bcfg2.Client.Tools.ToolInstantiationError
+ def get_svc_command(self, service, action):
+ return '/usr/sbin/service %s %s' % (service.get('name'), action)
- def VerifyService(self, entry, _):
+ def verify_bootstatus(self, entry, bootstatus):
+ """Verify bootstatus for entry."""
+ cmd = self.get_svc_command(entry, 'enabled')
+ current_bootstatus = bool(self.cmd.run(cmd))
+
+ if bootstatus == 'off':
+ if current_bootstatus:
+ entry.set('current_bootstatus', 'on')
+ return False
+ return True
+ elif not current_bootstatus:
+ entry.set('current_bootstatus', 'off')
+ return False
return True
- def get_svc_command(self, service, action):
- return "/usr/local/etc/rc.d/%s %s" % (service.get('name'), action)
+ def check_service(self, entry):
+ # use 'onestatus' to enable status reporting for disabled services
+ cmd = self.get_svc_command(entry, 'onestatus')
+ return bool(self.cmd.run(cmd))
+
+ def stop_service(self, service):
+ # use 'onestop' to enable stopping of disabled services
+ self.logger.debug('Stopping service %s' % service.get('name'))
+ return self.cmd.run(self.get_svc_command(service, 'onestop'))
+
+
+ def VerifyService(self, entry, _):
+ """Verify Service status for entry."""
+ entry.set('target_status', entry.get('status')) # for reporting
+ bootstatus = self.get_bootstatus(entry)
+ if bootstatus is None:
+ return True
+ current_bootstatus = self.verify_bootstatus(entry, bootstatus)
+
+ if entry.get('status') == 'ignore':
+ # 'ignore' should verify
+ current_svcstatus = True
+ svcstatus = True
+ else:
+ svcstatus = self.check_service(entry)
+ if entry.get('status') == 'on':
+ if svcstatus:
+ current_svcstatus = True
+ else:
+ current_svcstatus = False
+ elif entry.get('status') == 'off':
+ if svcstatus:
+ current_svcstatus = False
+ else:
+ current_svcstatus = True
+
+ if svcstatus:
+ entry.set('current_status', 'on')
+ else:
+ entry.set('current_status', 'off')
+
+ return current_bootstatus and current_svcstatus
+
+ def InstallService(self, entry):
+ """Install Service entry."""
+ self.logger.info("Installing Service %s" % (entry.get('name')))
+ bootstatus = self.get_bootstatus(entry)
+
+ # check if service exists
+ all_services_cmd = '/usr/sbin/service -l'
+ all_services = self.cmd.run(all_services_cmd).stdout.splitlines()
+ if entry.get('name') not in all_services:
+ self.logger.debug("Service %s does not exist" % entry.get('name'))
+ return False
+
+ # get rcvar for service
+ vars = set()
+ rcvar_cmd = self.get_svc_command(entry, 'rcvar')
+ for line in self.cmd.run(rcvar_cmd).stdout.splitlines():
+ match = self.rcvar_re.match(line)
+ if match:
+ vars.add(match.group('var'))
+
+ if bootstatus is not None:
+ bootcmdrv = True
+ sysrcstatus = None
+ if bootstatus == 'on':
+ sysrcstatus = 'YES'
+ elif bootstatus == 'off':
+ sysrcstatus = 'NO'
+ if sysrcstatus is not None:
+ for var in vars:
+ if not self.cmd.run('/usr/sbin/sysrc %s="%s"' % (var, sysrcstatus)):
+ bootcmdrv = False
+ break
+
+ if Bcfg2.Options.setup.service_mode == 'disabled':
+ # 'disabled' means we don't attempt to modify running svcs
+ return bootcmdrv
+ buildmode = Bcfg2.Options.setup.service_mode == 'build'
+ if (entry.get('status') == 'on' and not buildmode) and \
+ entry.get('current_status') == 'off':
+ svccmdrv = self.start_service(entry)
+ elif (entry.get('status') == 'off' or buildmode) and \
+ entry.get('current_status') == 'on':
+ svccmdrv = self.stop_service(entry)
+ else:
+ svccmdrv = True # ignore status attribute
+ return bootcmdrv and svccmdrv
+ else:
+ # when bootstatus is 'None', status == 'ignore'
+ return True
+
+ def FindExtra(self):
+ """Find Extra FreeBSD Service entries."""
+ specified = [entry.get('name') for entry in self.getSupportedEntries()]
+ extra = set()
+ for path in self.cmd.run("/usr/sbin/service -e").stdout.splitlines():
+ name = os.path.basename(path)
+ if name not in specified:
+ extra.add(name)
+ return [Bcfg2.Client.XML.Element('Service', name=name, type='freebsd')
+ for name in list(extra)]
+
+ def Remove(self, _):
+ """Remove extra service entries."""
+ # Extra service removal is nonsensical
+ # Extra services need to be reflected in the config
+ return
diff --git a/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py b/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py
index 31925fa3c..22cf802cf 100644
--- a/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py
+++ b/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py
@@ -29,7 +29,7 @@ class FreeBSDPackage(Bcfg2.Client.Tools.PkgTool):
self.installed[name] = version
def VerifyPackage(self, entry, _):
- if not 'version' in entry.attrib:
+ if 'version' not in entry.attrib:
self.logger.info("Cannot verify unversioned package %s" %
entry.attrib['name'])
return False
diff --git a/src/lib/Bcfg2/Client/Tools/IPS.py b/src/lib/Bcfg2/Client/Tools/IPS.py
index c998ff083..0f82b1bc1 100644
--- a/src/lib/Bcfg2/Client/Tools/IPS.py
+++ b/src/lib/Bcfg2/Client/Tools/IPS.py
@@ -37,7 +37,7 @@ class IPS(Bcfg2.Client.Tools.PkgTool):
def VerifyPackage(self, entry, _):
"""Verify package for entry."""
pname = entry.get('name')
- if not 'version' in entry.attrib:
+ if 'version' not in entry.attrib:
self.logger.info("Cannot verify unversioned package %s" % (pname))
return False
if pname not in self.installed:
diff --git a/src/lib/Bcfg2/Client/Tools/MacPorts.py b/src/lib/Bcfg2/Client/Tools/MacPorts.py
index 265171a5a..1e9847c42 100644
--- a/src/lib/Bcfg2/Client/Tools/MacPorts.py
+++ b/src/lib/Bcfg2/Client/Tools/MacPorts.py
@@ -31,16 +31,16 @@ class MacPorts(Bcfg2.Client.Tools.PkgTool):
def VerifyPackage(self, entry, _):
"""Verify Package status for entry."""
- if not 'version' in entry.attrib:
+ if 'version' not in entry.attrib:
self.logger.info("Cannot verify unversioned package %s" %
entry.attrib['name'])
return False
if entry.attrib['name'] in self.installed:
if (self.installed[entry.attrib['name']] == entry.attrib['version']
- or entry.attrib['version'] == 'any'):
- #FIXME: We should be able to check this once
- # http://trac.macports.org/ticket/15709 is implemented
+ or entry.attrib['version'] == 'any'):
+ # FIXME: We should be able to check this once
+ # http://trac.macports.org/ticket/15709 is implemented
return True
else:
self.logger.info(" %s: Wrong version installed. "
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py
new file mode 100644
index 000000000..fc4e16904
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py
@@ -0,0 +1,296 @@
+""" Augeas driver """
+
+import sys
+import Bcfg2.Client.XML
+from augeas import Augeas
+from Bcfg2.Client.Tools.POSIX.base import POSIXTool
+from Bcfg2.Client.Tools.POSIX.File import POSIXFile
+
+
+class AugeasCommand(object):
+ """ Base class for all Augeas command objects """
+
+ def __init__(self, command, augeas_obj, logger):
+ self._augeas = augeas_obj
+ self.command = command
+ self.entry = self.command.getparent()
+ self.logger = logger
+
+ def get_path(self, attr="path"):
+ """ Get a fully qualified path from the name of the parent entry and
+ the path given in this command tag.
+
+ @param attr: The attribute to get the relative path from
+ @type attr: string
+ @returns: string - the fully qualified Augeas path
+
+ """
+ return "/files/%s/%s" % (self.entry.get("name").strip("/"),
+ self.command.get(attr).lstrip("/"))
+
+ def _exists(self, path):
+ """ Return True if a path exists in Augeas, False otherwise.
+
+ Note that a False return can mean many things: A file that
+ doesn't exist, a node within the file that doesn't exist, no
+ lens to parse the file, etc. """
+ return len(self._augeas.match(path)) > 1
+
+ def _verify_exists(self, path=None):
+ """ Verify that the given path exists, with friendly debug
+ logging.
+
+ @param path: The path to verify existence of. Defaults to the
+ result of
+ :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`.
+ @type path: string
+ @returns: bool - Whether or not the path exists
+ """
+ if path is None:
+ path = self.get_path()
+ self.logger.debug("Augeas: Verifying that '%s' exists" % path)
+ return self._exists(path)
+
+ def _verify_not_exists(self, path=None):
+ """ Verify that the given path does not exist, with friendly
+ debug logging.
+
+ @param path: The path to verify existence of. Defaults to the
+ result of
+ :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`.
+ @type path: string
+ @returns: bool - Whether or not the path does not exist.
+ (I.e., True if it does not exist, False if it does
+ exist.)
+ """
+ if path is None:
+ path = self.get_path()
+ self.logger.debug("Augeas: Verifying that '%s' does not exist" % path)
+ return not self._exists(path)
+
+ def _verify_set(self, expected, path=None):
+ """ Verify that the given path is set to the given value, with
+ friendly debug logging.
+
+ @param expected: The expected value of the node.
+ @param path: The path to verify existence of. Defaults to the
+ result of
+ :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`.
+ @type path: string
+ @returns: bool - Whether or not the path matches the expected value.
+
+ """
+ if path is None:
+ path = self.get_path()
+ self.logger.debug("Augeas: Verifying '%s' == '%s'" % (path, expected))
+ actual = self._augeas.get(path)
+ if actual == expected:
+ return True
+ else:
+ self.logger.debug("Augeas: '%s' failed verification: '%s' != '%s'"
+ % (path, actual, expected))
+ return False
+
+ def __str__(self):
+ return Bcfg2.Client.XML.tostring(self.command)
+
+ def verify(self):
+ """ Verify that the command has been applied. """
+ raise NotImplementedError
+
+ def install(self):
+ """ Run the command. """
+ raise NotImplementedError
+
+
+class Remove(AugeasCommand):
+ """ Augeas ``rm`` command """
+ def verify(self):
+ return self._verify_not_exists()
+
+ def install(self):
+ self.logger.debug("Augeas: Removing %s" % self.get_path())
+ return self._augeas.remove(self.get_path())
+
+
+class Move(AugeasCommand):
+ """ Augeas ``move`` command """
+ def __init__(self, command, augeas_obj, logger):
+ AugeasCommand.__init__(self, command, augeas_obj, logger)
+ self.source = self.get_path("source")
+ self.dest = self.get_path("destination")
+
+ def verify(self):
+ return (self._verify_not_exists(self.source),
+ self._verify_exists(self.dest))
+
+ def install(self):
+ self.logger.debug("Augeas: Moving %s to %s" % (self.source, self.dest))
+ return self._augeas.move(self.source, self.dest)
+
+
+class Set(AugeasCommand):
+ """ Augeas ``set`` command """
+ def __init__(self, command, augeas_obj, logger):
+ AugeasCommand.__init__(self, command, augeas_obj, logger)
+ self.value = self.command.get("value")
+
+ def verify(self):
+ return self._verify_set(self.value)
+
+ def install(self):
+ self.logger.debug("Augeas: Setting %s to %s" % (self.get_path(),
+ self.value))
+ return self._augeas.set(self.get_path(), self.value)
+
+
+class Clear(Set):
+ """ Augeas ``clear`` command """
+ def __init__(self, command, augeas_obj, logger):
+ Set.__init__(self, command, augeas_obj, logger)
+ self.value = None
+
+
+class SetMulti(AugeasCommand):
+ """ Augeas ``setm`` command """
+ def __init__(self, command, augeas_obj, logger):
+ AugeasCommand.__init__(self, command, augeas_obj, logger)
+ self.sub = self.command.get("sub")
+ self.value = self.command.get("value")
+ self.base = self.get_path("base")
+
+ def verify(self):
+ return all(self._verify_set(self.value,
+ path="%s/%s" % (path, self.sub))
+ for path in self._augeas.match(self.base))
+
+ def install(self):
+ return self._augeas.setm(self.base, self.sub, self.value)
+
+
+class Insert(AugeasCommand):
+ """ Augeas ``ins`` command """
+ def __init__(self, command, augeas_obj, logger):
+ AugeasCommand.__init__(self, command, augeas_obj, logger)
+ self.label = self.command.get("label")
+ self.where = self.command.get("where", "before")
+ self.before = self.where == "before"
+
+ def verify(self):
+ return self._verify_exists("%s/../%s" % (self.get_path(), self.label))
+
+ def install(self):
+ self.logger.debug("Augeas: Inserting new %s %s %s" %
+ (self.label, self.where, self.get_path()))
+ return self._augeas.insert(self.get_path(), self.label, self.before)
+
+
+class POSIXAugeas(POSIXTool):
+ """ Handle <Path type='augeas'...> entries. See
+ :ref:`client-tools-augeas`. """
+ __req__ = ['name', 'mode', 'owner', 'group']
+
+ def __init__(self, config):
+ POSIXTool.__init__(self, config)
+ self._augeas = dict()
+ # file tool for setting initial values of files that don't
+ # exist
+ self.filetool = POSIXFile(config)
+
+ def get_augeas(self, entry):
+ """ Get an augeas object for the given entry. """
+ if entry.get("name") not in self._augeas:
+ aug = Augeas()
+ if entry.get("lens"):
+ self.logger.debug("Augeas: Adding %s to include path for %s" %
+ (entry.get("name"), entry.get("lens")))
+ incl = "/augeas/load/%s/incl" % entry.get("lens")
+ ilen = len(aug.match(incl))
+ if ilen == 0:
+ self.logger.error("Augeas: Lens %s does not exist" %
+ entry.get("lens"))
+ else:
+ aug.set("%s[%s]" % (incl, ilen + 1), entry.get("name"))
+ aug.load()
+ self._augeas[entry.get("name")] = aug
+ return self._augeas[entry.get("name")]
+
+ def fully_specified(self, entry):
+ return len(entry.getchildren()) != 0
+
+ def get_commands(self, entry):
+ """ Get a list of commands to verify or install.
+
+ @param entry: The entry to get commands from.
+ @type entry: lxml.etree._Element
+ @param unverified: Only get commands that failed verification.
+ @type unverified: bool
+ @returns: list of
+ :class:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand`
+ objects representing the commands.
+ """
+ rv = []
+ for cmd in entry.iterchildren():
+ if cmd.tag == "Initial":
+ continue
+ if cmd.tag in globals():
+ rv.append(globals()[cmd.tag](cmd, self.get_augeas(entry),
+ self.logger))
+ else:
+ err = "Augeas: Unknown command %s in %s" % (cmd.tag,
+ entry.get("name"))
+ self.logger.error(err)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''), err]))
+ return rv
+
+ def verify(self, entry, modlist):
+ rv = True
+ for cmd in self.get_commands(entry):
+ try:
+ if not cmd.verify():
+ err = "Augeas: Command has not been applied to %s: %s" % \
+ (entry.get("name"), cmd)
+ self.logger.debug(err)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''),
+ err]))
+ rv = False
+ cmd.command.set("verified", "false")
+ else:
+ cmd.command.set("verified", "true")
+ except: # pylint: disable=W0702
+ err = "Augeas: Unexpected error verifying %s: %s: %s" % \
+ (entry.get("name"), cmd, sys.exc_info()[1])
+ self.logger.error(err)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''), err]))
+ rv = False
+ cmd.command.set("verified", "false")
+ return POSIXTool.verify(self, entry, modlist) and rv
+
+ def install(self, entry):
+ rv = True
+ if entry.get("current_exists", "true") == "false":
+ initial = entry.find("Initial")
+ if initial is not None:
+ self.logger.debug("Augeas: Setting initial data for %s" %
+ entry.get("name"))
+ file_entry = Bcfg2.Client.XML.Element("Path",
+ **dict(entry.attrib))
+ file_entry.text = initial.text
+ self.filetool.install(file_entry)
+ # re-parse the file
+ self.get_augeas(entry).load()
+ for cmd in self.get_commands(entry):
+ try:
+ cmd.install()
+ except: # pylint: disable=W0702
+ self.logger.error(
+ "Failure running Augeas command on %s: %s: %s" %
+ (entry.get("name"), cmd, sys.exc_info()[1]))
+ rv = False
+ try:
+ self.get_augeas(entry).save()
+ except: # pylint: disable=W0702
+ self.logger.error("Failure saving Augeas changes to %s: %s" %
+ (entry.get("name"), sys.exc_info()[1]))
+ rv = False
+ return POSIXTool.install(self, entry) and rv
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/File.py b/src/lib/Bcfg2/Client/Tools/POSIX/File.py
index d7a70e202..0452ea258 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/File.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/File.py
@@ -3,7 +3,6 @@
import os
import sys
import stat
-import time
import difflib
import tempfile
import Bcfg2.Options
@@ -189,12 +188,11 @@ class POSIXFile(POSIXTool):
prompt.append('Binary file, no printable diff')
attrs['current_bfile'] = b64encode(content)
else:
+ diff = self._diff(content, self._get_data(entry)[0],
+ filename=entry.get("name"))
if interactive:
- diff = self._diff(content, self._get_data(entry)[0],
- difflib.unified_diff,
- filename=entry.get("name"))
if diff:
- udiff = '\n'.join(l.rstrip('\n') for l in diff)
+ udiff = '\n'.join(diff)
if hasattr(udiff, "decode"):
udiff = udiff.decode(Bcfg2.Options.setup.encoding)
try:
@@ -209,8 +207,6 @@ class POSIXFile(POSIXTool):
prompt.append("Diff took too long to compute, no "
"printable diff")
if not sensitive:
- diff = self._diff(content, self._get_data(entry)[0],
- difflib.ndiff, filename=entry.get("name"))
if diff:
attrs["current_bdiff"] = b64encode("\n".join(diff))
else:
@@ -221,28 +217,12 @@ class POSIXFile(POSIXTool):
for attr, val in attrs.items():
entry.set(attr, val)
- def _diff(self, content1, content2, difffunc, filename=None):
- """ Return a diff of the two strings, as produced by difffunc.
- warns after 5 seconds and times out after 30 seconds. """
- rv = []
- start = time.time()
- longtime = False
- for diffline in difffunc(content1.split('\n'),
- content2.split('\n')):
- now = time.time()
- rv.append(diffline)
- if now - start > 5 and not longtime:
- if filename:
- self.logger.info("POSIX: Diff of %s taking a long time" %
- filename)
- else:
- self.logger.info("POSIX: Diff taking a long time")
- longtime = True
- elif now - start > 30:
- if filename:
- self.logger.error("POSIX: Diff of %s took too long; "
- "giving up" % filename)
- else:
- self.logger.error("POSIX: Diff took too long; giving up")
- return False
- return rv
+ def _diff(self, content1, content2, filename=None):
+ """ Return a unified diff of the two strings """
+
+ fromfile = "%s (on disk)" % filename if filename else ""
+ tofile = "%s (from bcfg2)" % filename if filename else ""
+ return difflib.unified_diff(content1.split('\n'),
+ content2.split('\n'),
+ fromfile=fromfile,
+ tofile=tofile)
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py b/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py
index 13b45a759..c27c7559d 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py
@@ -58,8 +58,11 @@ class POSIX(Bcfg2.Client.Tools.Tool):
mname = submodule[1].rsplit('.', 1)[-1]
if mname == 'base':
continue
- module = getattr(__import__(submodule[1]).Client.Tools.POSIX,
- mname)
+ try:
+ module = getattr(__import__(submodule[1]).Client.Tools.POSIX,
+ mname)
+ except ImportError:
+ continue
hdlr = getattr(module, "POSIX" + mname)
if POSIXTool in hdlr.__mro__:
# figure out what entry type this handler handles
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/base.py b/src/lib/Bcfg2/Client/Tools/POSIX/base.py
index 712620206..8895eaae1 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/base.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/base.py
@@ -217,18 +217,13 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
acl.delete_entry(aclentry)
if os.path.isdir(path):
defacl = posix1e.ACL(filedef=path)
- if not defacl.valid():
- # when a default ACL is queried on a directory that
- # has no default ACL entries at all, you get an empty
- # ACL, which is not valid. in this circumstance, we
- # just copy the access ACL to get a base valid ACL
- # that we can add things to.
- defacl = posix1e.ACL(acl=acl)
- else:
- for aclentry in defacl:
- if aclentry.tag_type in [posix1e.ACL_USER,
- posix1e.ACL_GROUP]:
- defacl.delete_entry(aclentry)
+ for aclentry in defacl:
+ if aclentry.tag_type in [posix1e.ACL_USER,
+ posix1e.ACL_USER_OBJ,
+ posix1e.ACL_GROUP,
+ posix1e.ACL_GROUP_OBJ,
+ posix1e.ACL_OTHER]:
+ defacl.delete_entry(aclentry)
else:
defacl = None
@@ -254,10 +249,16 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
try:
if scope == posix1e.ACL_USER:
scopename = "user"
- aclentry.qualifier = self._norm_uid(qualifier)
+ if qualifier:
+ aclentry.qualifier = self._norm_uid(qualifier)
+ else:
+ aclentry.tag_type = posix1e.ACL_USER_OBJ
elif scope == posix1e.ACL_GROUP:
scopename = "group"
- aclentry.qualifier = self._norm_gid(qualifier)
+ if qualifier:
+ aclentry.qualifier = self._norm_gid(qualifier)
+ else:
+ aclentry.tag_type = posix1e.ACL_GROUP_OBJ
except (OSError, KeyError):
err = sys.exc_info()[1]
self.logger.error("POSIX: Could not resolve %s %s: %s" %
@@ -358,7 +359,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
try:
# single octal digit
rv = int(perms)
- if rv > 0 and rv < 8:
+ if rv >= 0 and rv < 8:
return rv
else:
self.logger.error("POSIX: Permissions digit out of range in "
@@ -388,13 +389,17 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
""" Get a string representation of the given ACL. aclkey must
be a tuple of (<acl type>, <acl scope>, <qualifier>) """
atype, scope, qualifier = aclkey
+ if not qualifier:
+ qualifier = ''
acl_str = []
if atype == 'default':
acl_str.append(atype)
- if scope == posix1e.ACL_USER:
+ if scope == posix1e.ACL_USER or scope == posix1e.ACL_USER_OBJ:
acl_str.append("user")
- elif scope == posix1e.ACL_GROUP:
+ elif scope == posix1e.ACL_GROUP or scope == posix1e.ACL_GROUP_OBJ:
acl_str.append("group")
+ elif scope == posix1e.ACL_OTHER:
+ acl_str.append("other")
acl_str.append(qualifier)
acl_str.append(self._acl_perm2string(perms))
return ":".join(acl_str)
@@ -414,7 +419,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
""" Get data on the existing state of <path> -- e.g., whether
or not it exists, owner, group, permissions, etc. """
try:
- ondisk = os.stat(path)
+ ondisk = os.lstat(path)
except OSError:
self.logger.debug("POSIX: %s does not exist" % path)
return (False, None, None, None, None, None)
@@ -451,7 +456,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
if HAS_SELINUX:
try:
- secontext = selinux.getfilecon(path)[1].split(":")[2]
+ secontext = selinux.lgetfilecon(path)[1].split(":")[2]
except (OSError, KeyError):
err = sys.exc_info()[1]
self.logger.debug("POSIX: Could not get current SELinux "
@@ -460,7 +465,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
else:
secontext = None
- if HAS_ACLS:
+ if HAS_ACLS and not stat.S_ISLNK(ondisk[stat.ST_MODE]):
acls = self._list_file_acls(path)
else:
acls = None
@@ -562,9 +567,17 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
wanted = dict()
for acl in entry.findall("ACL"):
if acl.get("scope") == "user":
- scope = posix1e.ACL_USER
+ if acl.get("user"):
+ scope = posix1e.ACL_USER
+ else:
+ scope = posix1e.ACL_USER_OBJ
elif acl.get("scope") == "group":
- scope = posix1e.ACL_GROUP
+ if acl.get("group"):
+ scope = posix1e.ACL_GROUP
+ else:
+ scope = posix1e.ACL_GROUP_OBJ
+ elif acl.get("scope") == "other":
+ scope = posix1e.ACL_OTHER
else:
self.logger.error("POSIX: Unknown ACL scope %s" %
acl.get("scope"))
@@ -573,7 +586,10 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
self.logger.error("POSIX: No permissions set for ACL: %s" %
Bcfg2.Client.XML.tostring(acl))
continue
- wanted[(acl.get("type"), scope, acl.get(acl.get("scope")))] = \
+ qual = acl.get(acl.get("scope"))
+ if not qual:
+ qual = ''
+ wanted[(acl.get("type"), scope, qual)] = \
self._norm_acl_perms(acl.get('perms'))
return wanted
@@ -587,11 +603,12 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
""" Given an ACL object, process it appropriately and add
it to the return value """
try:
+ qual = ''
if acl.tag_type == posix1e.ACL_USER:
qual = pwd.getpwuid(acl.qualifier)[0]
elif acl.tag_type == posix1e.ACL_GROUP:
qual = grp.getgrgid(acl.qualifier)[0]
- else:
+ elif atype == "access" or acl.tag_type == posix1e.ACL_MASK:
return
except (OSError, KeyError):
err = sys.exc_info()[1]
@@ -621,9 +638,38 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
_process_acl(acl, "default")
return existing
- def _verify_acls(self, entry, path=None):
+ def _verify_acls(self, entry, path=None): # pylint: disable=R0912
""" verify POSIX ACLs on the given entry. return True if all
ACLS are correct, false otherwise """
+ def _verify_acl(aclkey, perms):
+ """ Given ACL data, process it appropriately and add it to
+ missing or wrong lists if appropriate """
+ if aclkey not in existing:
+ missing.append(self._acl2string(aclkey, perms))
+ elif existing[aclkey] != perms:
+ wrong.append((self._acl2string(aclkey, perms),
+ self._acl2string(aclkey, existing[aclkey])))
+ if path == entry.get("name"):
+ atype, scope, qual = aclkey
+ aclentry = Bcfg2.Client.XML.Element("ACL", type=atype,
+ perms=str(perms))
+ if (scope == posix1e.ACL_USER or
+ scope == posix1e.ACL_USER_OBJ):
+ aclentry.set("scope", "user")
+ elif (scope == posix1e.ACL_GROUP or
+ scope == posix1e.ACL_GROUP_OBJ):
+ aclentry.set("scope", "group")
+ elif scope == posix1e.ACL_OTHER:
+ aclentry.set("scope", "other")
+ else:
+ self.logger.debug("POSIX: Unknown ACL scope %s on %s" %
+ (scope, path))
+ return
+
+ if scope != posix1e.ACL_OTHER:
+ aclentry.set(aclentry.get("scope"), qual)
+ entry.append(aclentry)
+
if not HAS_ACLS:
if entry.findall("ACL"):
self.logger.debug("POSIX: ACLs listed for %s but no pylibacl "
@@ -644,25 +690,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
extra = []
wrong = []
for aclkey, perms in wanted.items():
- if aclkey not in existing:
- missing.append(self._acl2string(aclkey, perms))
- elif existing[aclkey] != perms:
- wrong.append((self._acl2string(aclkey, perms),
- self._acl2string(aclkey, existing[aclkey])))
- if path == entry.get("name"):
- atype, scope, qual = aclkey
- aclentry = Bcfg2.Client.XML.Element("ACL", type=atype,
- perms=str(perms))
- if scope == posix1e.ACL_USER:
- aclentry.set("scope", "user")
- elif scope == posix1e.ACL_GROUP:
- aclentry.set("scope", "group")
- else:
- self.logger.debug("POSIX: Unknown ACL scope %s on %s" %
- (scope, path))
- continue
- aclentry.set(aclentry.get("scope"), qual)
- entry.append(aclentry)
+ _verify_acl(aclkey, perms)
for aclkey, perms in existing.items():
if aclkey not in wanted:
diff --git a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
index 58a3bbdfc..a7fcb6709 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
@@ -79,7 +79,7 @@ class POSIXUsers(Bcfg2.Client.Tools.Tool):
defined, and the uid/gid is in that whitelist; or b) no
whitelist is defined, and the uid/gid is not in the
blacklist. """
- if self._whitelist[tag] is None:
+ if not self._whitelist[tag]:
return eid not in self._blacklist[tag]
else:
return eid in self._whitelist[tag]
@@ -160,7 +160,8 @@ class POSIXUsers(Bcfg2.Client.Tools.Tool):
""" Get a list of supplmentary groups that the user in the
given entry is a member of """
return [g for g in self.existing['POSIXGroup'].values()
- if entry.get("name") in g[3] and g[0] != entry.get("group")]
+ if entry.get("name") in g[3] and g[0] != entry.get("group")
+ and self._in_managed_range('POSIXGroup', g[2])]
def VerifyPOSIXUser(self, entry, _):
""" Verify a POSIXUser entry """
diff --git a/src/lib/Bcfg2/Client/Tools/Pacman.py b/src/lib/Bcfg2/Client/Tools/Pacman.py
index 2ab9b7403..b82b905e7 100644
--- a/src/lib/Bcfg2/Client/Tools/Pacman.py
+++ b/src/lib/Bcfg2/Client/Tools/Pacman.py
@@ -19,7 +19,6 @@ class Pacman(Bcfg2.Client.Tools.PkgTool):
for pkg in self.cmd.run("/usr/bin/pacman -Q").stdout.splitlines():
pkgname = pkg.split(' ')[0].strip()
version = pkg.split(' ')[1].strip()
- #self.logger.info(" pkgname: %s, version: %s" % (pkgname, version))
self.installed[pkgname] = version
def VerifyPackage(self, entry, _):
@@ -28,7 +27,7 @@ class Pacman(Bcfg2.Client.Tools.PkgTool):
self.logger.info("VerifyPackage: %s : %s" % (entry.get('name'),
entry.get('version')))
- if not 'version' in entry.attrib:
+ if 'version' not in entry.attrib:
self.logger.info("Cannot verify unversioned package %s" %
entry.attrib['name'])
return False
@@ -38,8 +37,8 @@ class Pacman(Bcfg2.Client.Tools.PkgTool):
return True
elif self.installed[entry.attrib['name']] == \
entry.attrib['version']:
- #FIXME: need to figure out if pacman
- # allows you to verify packages
+ # FIXME: need to figure out if pacman
+ # allows you to verify packages
return True
else:
entry.set('current_version', self.installed[entry.get('name')])
diff --git a/src/lib/Bcfg2/Client/Tools/Pkgng.py b/src/lib/Bcfg2/Client/Tools/Pkgng.py
new file mode 100644
index 000000000..cd70d662d
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/Pkgng.py
@@ -0,0 +1,226 @@
+"""This is the Bcfg2 support for pkg."""
+
+import os
+import Bcfg2.Options
+import Bcfg2.Client.Tools
+
+
+class Pkgng(Bcfg2.Client.Tools.Tool):
+ """Support for pkgng packages on FreeBSD."""
+
+ options = Bcfg2.Client.Tools.Tool.options + [
+ Bcfg2.Options.PathOption(
+ cf=('Pkgng', 'path'),
+ default='/usr/sbin/pkg', dest='pkg_path',
+ help='Pkgng tool path')]
+
+ name = 'Pkgng'
+ __execs__ = []
+ __handles__ = [('Package', 'pkgng'), ('Path', 'ignore')]
+ __req__ = {'Package': ['name', 'version'], 'Path': ['type']}
+
+ def __init__(self, config):
+ Bcfg2.Client.Tools.Tool.__init__(self, config)
+
+ self.pkg = Bcfg2.Options.setup.pkg_path
+ self.__execs__ = [self.pkg]
+
+ self.pkgcmd = self.pkg + ' install -fy'
+ if not Bcfg2.Options.setup.debug:
+ self.pkgcmd += ' -q'
+ self.pkgcmd += ' %s'
+
+ self.ignores = [entry.get('name') for struct in config
+ for entry in struct
+ if entry.tag == 'Path' and
+ entry.get('type') == 'ignore']
+
+ self.__important__ = self.__important__ + \
+ [entry.get('name') for struct in config
+ for entry in struct
+ if (entry.tag == 'Path' and
+ entry.get('name').startswith('/etc/pkg/'))]
+ self.nonexistent = [entry.get('name') for struct in config
+ for entry in struct if entry.tag == 'Path'
+ and entry.get('type') == 'nonexistent']
+ self.actions = {}
+ self.pkg_cache = {}
+
+ try:
+ self._load_pkg_cache()
+ except OSError:
+ raise Bcfg2.Client.Tools.ToolInstantiationError
+
+ def _load_pkg_cache(self):
+ """Cache the version of all currently installed packages."""
+ self.pkg_cache = {}
+ output = self.cmd.run([self.pkg, 'query', '-a', '%n %v']).stdout
+ for line in output.splitlines():
+ parts = line.split(' ')
+ name = ' '.join(parts[:-1])
+ self.pkg_cache[name] = parts[-1]
+
+ def FindExtra(self):
+ """Find extra packages."""
+ packages = [entry.get('name') for entry in self.getSupportedEntries()]
+ extras = [(name, value) for (name, value) in self.pkg_cache.items()
+ if name not in packages]
+ return [Bcfg2.Client.XML.Element('Package', name=name,
+ type='pkgng', version=version)
+ for (name, version) in extras]
+
+ def VerifyChecksums(self, entry, modlist):
+ """Verify the checksum of the files, owned by a package."""
+ output = self.cmd.run([self.pkg, 'check', '-s',
+ entry.get('name')]).stdout.splitlines()
+ files = []
+ for item in output:
+ if "checksum mismatch" in item:
+ files.append(item.split()[-1])
+ elif "No such file or directory" in item:
+ continue
+ else:
+ self.logger.error("Got Unsupported pattern %s "
+ "from pkg check" % item)
+
+ files = list(set(files) - set(self.ignores))
+ # We check if there is file in the checksum to do
+ if files:
+ # if files are found there we try to be sure our modlist is sane
+ # with erroneous symlinks
+ modlist = [os.path.realpath(filename) for filename in modlist]
+ bad = [filename for filename in files if filename not in modlist]
+ if bad:
+ self.logger.debug("It is suggested that you either manage "
+ "these files, revert the changes, or ignore "
+ "false failures:")
+ self.logger.info("Package %s failed validation. Bad files "
+ "are:" % entry.get('name'))
+ self.logger.info(bad)
+ entry.set('qtext',
+ "Reinstall Package %s-%s to fix failing files? "
+ "(y/N) " % (entry.get('name'), entry.get('version')))
+ return False
+ return True
+
+ def _get_candidate_versions(self, name):
+ """
+ Get versions of the specified package name available for
+ installation from the configured remote repositories.
+ """
+ output = self.cmd.run([self.pkg, 'search', '-Qversion', '-q',
+ '-Sname', '-e', name]).stdout.splitlines()
+ versions = []
+ for line in output:
+ versions.append(line)
+
+ if len(versions) == 0:
+ return None
+
+ return sorted(versions)
+
+ def VerifyPackage(self, entry, modlist, checksums=True):
+ """Verify package for entry."""
+ if 'version' not in entry.attrib:
+ self.logger.info("Cannot verify unversioned package %s" %
+ (entry.attrib['name']))
+ return False
+
+ pkgname = entry.get('name')
+ if pkgname not in self.pkg_cache:
+ self.logger.info("Package %s not installed" % (entry.get('name')))
+ entry.set('current_exists', 'false')
+ return False
+
+ installed_version = self.pkg_cache[pkgname]
+ candidate_versions = self._get_candidate_versions(pkgname)
+ if candidate_versions is not None:
+ candidate_version = candidate_versions[0]
+ else:
+ self.logger.error("Package %s is installed but no candidate"
+ "version was found." % (entry.get('name')))
+ return False
+
+ if entry.get('version').startswith('auto'):
+ desired_version = candidate_version
+ entry.set('version', "auto: %s" % desired_version)
+ elif entry.get('version').startswith('any'):
+ desired_version = installed_version
+ entry.set('version', "any: %s" % desired_version)
+ else:
+ desired_version = entry.get('version')
+
+ if desired_version != installed_version:
+ entry.set('current_version', installed_version)
+ entry.set('qtext', "Modify Package %s (%s -> %s)? (y/N) " %
+ (entry.get('name'), entry.get('current_version'),
+ desired_version))
+ return False
+ else:
+ # version matches
+ if (not Bcfg2.Options.setup.quick and
+ entry.get('verify', 'true') == 'true'
+ and checksums):
+ pkgsums = self.VerifyChecksums(entry, modlist)
+ return pkgsums
+ return True
+
+ def Remove(self, packages):
+ """Deal with extra configuration detected."""
+ pkgnames = " ".join([pkg.get('name') for pkg in packages])
+ if len(packages) > 0:
+ self.logger.info('Removing packages:')
+ self.logger.info(pkgnames)
+ self.cmd.run([self.pkg, 'delete', '-y', pkgnames])
+ self._load_pkg_cache()
+ self.modified += packages
+ self.extra = self.FindExtra()
+
+ def Install(self, packages):
+ ipkgs = []
+ bad_pkgs = []
+ for pkg in packages:
+ versions = self._get_candidate_versions(pkg.get('name'))
+ if versions is None:
+ self.logger.error("pkg has no information about package %s" %
+ (pkg.get('name')))
+ continue
+
+ if pkg.get('version').startswith('auto') or \
+ pkg.get('version').startswith('any'):
+ ipkgs.append("%s-%s" % (pkg.get('name'), versions[0]))
+ continue
+
+ if pkg.get('version') in versions:
+ ipkgs.append("%s-%s" % (pkg.get('name'), pkg.get('version')))
+ continue
+ else:
+ self.logger.error("Package %s: desired version %s not in %s" %
+ (pkg.get('name'), pkg.get('version'),
+ versions))
+ bad_pkgs.append(pkg.get('name'))
+
+ if bad_pkgs:
+ self.logger.error("Cannot find correct versions of packages:")
+ self.logger.error(bad_pkgs)
+ if not ipkgs:
+ return
+ if not self.cmd.run(self.pkgcmd % (" ".join(ipkgs))):
+ self.logger.error("pkg command failed")
+ self._load_pkg_cache()
+ self.extra = self.FindExtra()
+ mark = []
+ states = dict()
+ for package in packages:
+ states[package] = self.VerifyPackage(package, [], checksums=False)
+ if states[package]:
+ self.modified.append(package)
+ if package.get('origin') == 'Packages':
+ mark.append(package.get('name'))
+ if mark:
+ self.cmd.run([self.pkg, 'set', '-A1', '-y'] + mark)
+ return states
+
+ def VerifyPath(self, _entry, _):
+ """Do nothing here since we only verify Path type=ignore."""
+ return True
diff --git a/src/lib/Bcfg2/Client/Tools/Portage.py b/src/lib/Bcfg2/Client/Tools/Portage.py
index a61ede820..5c092f46b 100644
--- a/src/lib/Bcfg2/Client/Tools/Portage.py
+++ b/src/lib/Bcfg2/Client/Tools/Portage.py
@@ -50,7 +50,7 @@ class Portage(Bcfg2.Client.Tools.PkgTool):
def VerifyPackage(self, entry, modlist):
"""Verify package for entry."""
- if not 'version' in entry.attrib:
+ if 'version' not in entry.attrib:
self.logger.info("Cannot verify unversioned package %s" %
(entry.get('name')))
return False
@@ -68,11 +68,11 @@ class Portage(Bcfg2.Client.Tools.PkgTool):
if ('verify' not in entry.attrib or
entry.get('verify').lower() == 'true'):
- # Check the package if:
- # - Not running in quick mode
- # - No verify option is specified in the literal configuration
- # OR
- # - Verify option is specified and is true
+ # Check the package if:
+ # - Not running in quick mode
+ # - No verify option is specified in the literal configuration
+ # OR
+ # - Verify option is specified and is true
self.logger.debug('Running equery check on %s' %
entry.get('name'))
diff --git a/src/lib/Bcfg2/Client/Tools/SMF.py b/src/lib/Bcfg2/Client/Tools/SMF.py
index 8b23a4a37..1a580d8a5 100644
--- a/src/lib/Bcfg2/Client/Tools/SMF.py
+++ b/src/lib/Bcfg2/Client/Tools/SMF.py
@@ -25,7 +25,7 @@ class SMF(Bcfg2.Client.Tools.SvcTool):
def GetFMRI(self, entry):
"""Perform FMRI resolution for service."""
- if not 'FMRI' in entry.attrib:
+ if 'FMRI' not in entry.attrib:
rv = self.cmd.run(["/usr/bin/svcs", "-H", "-o", "FMRI",
entry.get('name')])
if rv.success:
diff --git a/src/lib/Bcfg2/Client/Tools/Systemd.py b/src/lib/Bcfg2/Client/Tools/Systemd.py
index 20a172d3d..027d91c71 100644
--- a/src/lib/Bcfg2/Client/Tools/Systemd.py
+++ b/src/lib/Bcfg2/Client/Tools/Systemd.py
@@ -13,8 +13,6 @@ class Systemd(Bcfg2.Client.Tools.SvcTool):
__handles__ = [('Service', 'systemd')]
__req__ = {'Service': ['name', 'status']}
- conflicts = ['Chkconfig']
-
def get_svc_command(self, service, action):
return "/bin/systemctl %s %s.service" % (action, service.get('name'))
diff --git a/src/lib/Bcfg2/Client/Tools/VCS.py b/src/lib/Bcfg2/Client/Tools/VCS.py
index 4e8ac76a4..449503b55 100644
--- a/src/lib/Bcfg2/Client/Tools/VCS.py
+++ b/src/lib/Bcfg2/Client/Tools/VCS.py
@@ -165,12 +165,13 @@ class VCS(Bcfg2.Client.Tools.Tool):
def Verifysvn(self, entry, _):
"""Verify svn repositories"""
+ # pylint: disable=E1101
headrev = pysvn.Revision(pysvn.opt_revision_kind.head)
+ # pylint: enable=E1101
client = pysvn.Client()
try:
cur_rev = str(client.info(entry.get('name')).revision.number)
- server = client.info2(entry.get('sourceurl'),
- headrev,
+ server = client.info2(entry.get('sourceurl'), headrev,
recurse=False)
if server:
server_rev = str(server[0][1].rev.number)
diff --git a/src/lib/Bcfg2/Client/Tools/YUM.py b/src/lib/Bcfg2/Client/Tools/YUM.py
index 0b38044d4..86048cb0b 100644
--- a/src/lib/Bcfg2/Client/Tools/YUM.py
+++ b/src/lib/Bcfg2/Client/Tools/YUM.py
@@ -632,34 +632,38 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
package_fail = True
stat['version_fail'] = True
# Just chose the first pkg for the error message
+ current_pkg = all_pkg_objs[0]
if virt_pkg:
provides = \
- [p for p in all_pkg_objs[0].provides
+ [p for p in current_pkg.provides
if p[0] == entry.get("name")][0]
- entry.set('current_version', "%s:%s-%s" % provides[2])
+ current_evr = provides[2]
self.logger.info(
" %s: Wrong version installed. "
"Want %s, but %s provides %s" %
(entry.get("name"),
nevra2string(nevra),
- nevra2string(all_pkg_objs[0]),
+ nevra2string(current_pkg),
yum.misc.prco_tuple_to_string(provides)))
else:
- entry.set('current_version', "%s:%s-%s.%s" %
- (all_pkg_objs[0].epoch,
- all_pkg_objs[0].version,
- all_pkg_objs[0].release,
- all_pkg_objs[0].arch))
+ current_evr = (current_pkg.epoch,
+ current_pkg.version,
+ current_pkg.release)
self.logger.info(" %s: Wrong version installed. "
"Want %s, but have %s" %
(entry.get("name"),
nevra2string(nevra),
- nevra2string(all_pkg_objs[0])))
- entry.set('version', "%s:%s-%s.%s" %
- (nevra.get('epoch', 'any'),
- nevra.get('version', 'any'),
- nevra.get('release', 'any'),
- nevra.get('arch', 'any')))
+ nevra2string(current_pkg)))
+ wanted_evr = (nevra.get('epoch', 'any'),
+ nevra.get('version', 'any'),
+ nevra.get('release', 'any'))
+ entry.set('current_version', "%s:%s-%s" % current_evr)
+ entry.set('version', "%s:%s-%s" % wanted_evr)
+ if yum.compareEVR(current_evr, wanted_evr) == 1:
+ entry.set("package_fail_action", "downgrade")
+ else:
+ entry.set("package_fail_action", "update")
+
qtext_versions.append("U(%s)" % str(all_pkg_objs[0]))
continue
@@ -910,7 +914,7 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
cleanup()
- def Install(self, packages): # pylint: disable=R0912,R0914
+ def Install(self, packages): # pylint: disable=R0912,R0914,R0915
""" Try and fix everything that Yum.VerifyPackages() found
wrong for each Package Entry. This can result in individual
RPMs being installed (for the first time), deleted, downgraded
@@ -932,6 +936,7 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
install_pkgs = []
gpg_keys = []
upgrade_pkgs = []
+ downgrade_pkgs = []
reinstall_pkgs = []
def queue_pkg(pkg, inst, queue):
@@ -971,11 +976,14 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
continue
status = self.instance_status[inst]
if (not status.get('installed', False) and
- Bcfg2.Options.setup.yum_install_missing):
+ Bcfg2.Options.setup.yum_install_missing):
queue_pkg(pkg, inst, install_pkgs)
elif (status.get('version_fail', False) and
Bcfg2.Options.setup.yum_fix_version):
- queue_pkg(pkg, inst, upgrade_pkgs)
+ if pkg.get("package_fail_action") == "downgrade":
+ queue_pkg(pkg, inst, downgrade_pkgs)
+ else:
+ queue_pkg(pkg, inst, upgrade_pkgs)
elif (status.get('verify_fail', False) and
Bcfg2.Options.setup.yum_reinstall_broken):
queue_pkg(pkg, inst, reinstall_pkgs)
@@ -1039,6 +1047,19 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
self.logger.error("Error upgrading package %s: %s" %
(pkg_arg, yume))
+ if len(downgrade_pkgs) > 0:
+ self.logger.info("Attempting to downgrade packages")
+
+ for inst in downgrade_pkgs:
+ pkg_arg = self.instance_status[inst].get('pkg').get('name')
+ self.logger.debug("Downgrading %s" % pkg_arg)
+ try:
+ self.yumbase.downgrade(**build_yname(pkg_arg, inst))
+ except yum.Errors.YumBaseError:
+ yume = sys.exc_info()[1]
+ self.logger.error("Error downgrading package %s: %s" %
+ (pkg_arg, yume))
+
if len(reinstall_pkgs) > 0:
self.logger.info("Attempting to reinstall packages")
for inst in reinstall_pkgs:
diff --git a/src/lib/Bcfg2/Client/XML.py b/src/lib/Bcfg2/Client/XML.py
index 91d4ac5c6..4ba06abae 100644
--- a/src/lib/Bcfg2/Client/XML.py
+++ b/src/lib/Bcfg2/Client/XML.py
@@ -5,9 +5,29 @@
# pylint: disable=E0611,W0611,W0613,C0103
try:
- from lxml.etree import Element, SubElement, XML, tostring
+ from lxml.etree import Element, SubElement, tostring, XMLParser
from lxml.etree import XMLSyntaxError as ParseError
+ from lxml.etree import XML as _XML
+ from Bcfg2.Compat import wraps
driver = 'lxml'
+
+ # libxml2 2.9.0+ doesn't parse 10M+ documents by default:
+ # https://mail.gnome.org/archives/commits-list/2012-August/msg00645.html
+ try:
+ _parser = XMLParser(huge_tree=True)
+ except TypeError:
+ _parser = XMLParser()
+
+ @wraps(_XML)
+ def XML(val, **kwargs):
+ """ unicode strings w/encoding declaration are not supported in
+ recent lxml.etree, so we try to read XML, and if it fails we try
+ encoding the string. """
+ kwargs.setdefault('parser', _parser)
+ try:
+ return _XML(val, **kwargs)
+ except ValueError:
+ return _XML(val.encode(), **kwargs)
except ImportError:
# lxml not available
from xml.parsers.expat import ExpatError as ParseError
diff --git a/src/lib/Bcfg2/Client/__init__.py b/src/lib/Bcfg2/Client/__init__.py
index 2461c1316..073aa7694 100644
--- a/src/lib/Bcfg2/Client/__init__.py
+++ b/src/lib/Bcfg2/Client/__init__.py
@@ -69,8 +69,8 @@ def prompt(msg):
except UnicodeEncodeError:
ans = input(msg.encode('utf-8'))
return ans in ['y', 'Y']
- except EOFError:
- # handle ^C on rhel-based platforms
+ except (EOFError, KeyboardInterrupt):
+ # handle ^C
raise SystemExit(1)
except:
print("Error while reading input: %s" % sys.exc_info()[1])
@@ -113,10 +113,10 @@ class Client(object):
help='Force removal of additional configuration items')),
Bcfg2.Options.ExclusiveOptionGroup(
Bcfg2.Options.PathOption(
- '-f', '--file', type=argparse.FileType('r'),
+ '-f', '--file', type=argparse.FileType('rb'),
help='Configure from a file rather than querying the server'),
Bcfg2.Options.PathOption(
- '-c', '--cache', type=argparse.FileType('w'),
+ '-c', '--cache', type=argparse.FileType('wb'),
help='Store the configuration in a file')),
Bcfg2.Options.BooleanOption(
'--exit-on-probe-failure', default=True,
@@ -144,7 +144,10 @@ class Client(object):
Bcfg2.Options.BooleanOption(
"-e", "--show-extra", help='Enable extra entry output'),
Bcfg2.Options.BooleanOption(
- "-k", "--kevlar", help='Run in bulletproof mode')]
+ "-k", "--kevlar", help='Run in bulletproof mode'),
+ Bcfg2.Options.BooleanOption(
+ "-i", "--only-important",
+ help='Only configure the important entries')]
def __init__(self):
self.config = None
@@ -403,7 +406,7 @@ class Client(object):
self.config = newconfig
if not Bcfg2.Options.setup.no_lock:
- #check lock here
+ # check lock here
try:
lockfile = open(Bcfg2.Options.setup.lockfile, 'w')
if locked(lockfile.fileno()):
@@ -559,11 +562,13 @@ class Client(object):
if x not in b_to_rem]
# take care of important entries first
- if not Bcfg2.Options.setup.dry_run:
+ if (not Bcfg2.Options.setup.dry_run or
+ Bcfg2.Options.setup.only_important):
+ important_installs = set()
for parent in self.config.findall(".//Path/.."):
name = parent.get("name")
- if (name and (name in Bcfg2.Options.setup.only_bundles or
- name not in Bcfg2.Options.setup.except_bundles)):
+ if not name or (name in Bcfg2.Options.setup.except_bundles and
+ name not in Bcfg2.Options.setup.only_bundles):
continue
for cfile in parent.findall("./Path"):
if (cfile.get('name') not in self.__important__ or
@@ -574,6 +579,9 @@ class Client(object):
if t.handlesEntry(cfile) and t.canVerify(cfile)]
if not tools:
continue
+ if Bcfg2.Options.setup.dry_run:
+ important_installs.add(cfile)
+ continue
if (Bcfg2.Options.setup.interactive and not
self.promptFilter("Install %s: %s? (y/N):",
[cfile])):
@@ -589,6 +597,11 @@ class Client(object):
cfile.set('qtext', '')
if tools[0].VerifyPath(cfile, []):
self.whitelist.remove(cfile)
+ if Bcfg2.Options.setup.dry_run and len(important_installs) > 0:
+ self.logger.info("In dryrun mode: "
+ "suppressing entry installation for:")
+ self.logger.info(["%s:%s" % (e.tag, e.get('name'))
+ for e in important_installs])
def Inventory(self):
"""
@@ -845,11 +858,13 @@ class Client(object):
self.times['inventory'] = time.time()
self.CondDisplayState('initial')
self.InstallImportant()
- self.Decide()
- self.Install()
- self.times['install'] = time.time()
- self.Remove()
- self.times['remove'] = time.time()
+ if not Bcfg2.Options.setup.only_important:
+ self.Decide()
+ self.Install()
+ self.times['install'] = time.time()
+ self.Remove()
+ self.times['remove'] = time.time()
+
if self.modified:
self.ReInventory()
self.times['reinventory'] = time.time()