summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Plugins/Packages
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2013-08-08 13:35:20 -0400
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2013-08-08 13:35:20 -0400
commit73c61dd6c0c464f06637db750484417bc8bbb6de (patch)
tree01381b324b6e0a44ebe49660752f31a4abca648a /src/lib/Bcfg2/Server/Plugins/Packages
parente1e194a573b3803fa7f45a646bbb36b2f164a3e1 (diff)
parentcd14868d4db8eaa7e9421e1d5fe8653294ac1e38 (diff)
downloadbcfg2-73c61dd6c0c464f06637db750484417bc8bbb6de.tar.gz
bcfg2-73c61dd6c0c464f06637db750484417bc8bbb6de.tar.bz2
bcfg2-73c61dd6c0c464f06637db750484417bc8bbb6de.zip
Merge branch 'options-rewrite'
Conflicts: src/lib/Bcfg2/Client/Frame.py src/lib/Bcfg2/Options.py src/lib/Bcfg2/Server/Admin/Init.py src/lib/Bcfg2/Server/Admin/Xcmd.py src/lib/Bcfg2/Server/BuiltinCore.py src/lib/Bcfg2/Server/Core.py src/lib/Bcfg2/Server/MultiprocessingCore.py src/lib/Bcfg2/Server/Plugin/base.py src/lib/Bcfg2/Server/Plugin/helpers.py src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py src/lib/Bcfg2/Server/Plugins/Packages/Yum.py src/lib/Bcfg2/Server/Plugins/Packages/__init__.py src/lib/Bcfg2/Server/SSLServer.py src/lib/Bcfg2/Utils.py src/lib/Bcfg2/settings.py src/sbin/bcfg2-crypt src/sbin/bcfg2-info src/sbin/bcfg2-lint src/sbin/bcfg2-test src/sbin/bcfg2-yum-helper tools/bcfg2-profile-templates.py
Diffstat (limited to 'src/lib/Bcfg2/Server/Plugins/Packages')
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Collection.py45
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py26
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Source.py9
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Yum.py84
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/YumHelper.py384
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/__init__.py109
6 files changed, 508 insertions, 149 deletions
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py b/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
index 4d05f9d97..0df8624f6 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
@@ -73,20 +73,17 @@ The Collection Module
---------------------
"""
-import sys
import copy
-import logging
import lxml.etree
+import Bcfg2.Options
import Bcfg2.Server.Plugin
-from Bcfg2.Server.FileMonitor import get_fam
-from Bcfg2.Options import get_option_parser
+from Bcfg2.Logger import Debuggable
from Bcfg2.Compat import any, md5 # pylint: disable=W0622
+from Bcfg2.Server.FileMonitor import get_fam
from Bcfg2.Server.Statistics import track_statistics
-LOGGER = logging.getLogger(__name__)
-
-class Collection(list, Bcfg2.Server.Plugin.Debuggable):
+class Collection(list, Debuggable):
""" ``Collection`` objects represent the set of
:class:`Bcfg2.Server.Plugins.Packages.Source` objects that apply
to a given client, and can be used to query all software
@@ -119,15 +116,14 @@ class Collection(list, Bcfg2.Server.Plugin.Debuggable):
.. -----
.. autoattribute:: __package_groups__
"""
- Bcfg2.Server.Plugin.Debuggable.__init__(self)
+ Debuggable.__init__(self)
list.__init__(self, sources)
- self.debug_flag = debug
+ self.debug_flag = self.debug_flag or debug
self.metadata = metadata
self.basepath = basepath
self.cachepath = cachepath
self.virt_pkgs = dict()
self.fam = get_fam()
- self.setup = get_option_parser()
try:
self.ptype = sources[0].ptype
@@ -447,9 +443,7 @@ class Collection(list, Bcfg2.Server.Plugin.Debuggable):
"""
for pkg in pkglist:
lxml.etree.SubElement(entry, 'BoundPackage', name=pkg,
- version=self.setup.cfp.get("packages",
- "version",
- default="auto"),
+ version=Bcfg2.Options.setup.packages_version,
type=self.ptype, origin='Packages')
def get_new_packages(self, initial, complete):
@@ -601,22 +595,9 @@ def get_collection_class(source_type):
:type source_type: string
:returns: type - the Collection subclass that should be used to
instantiate an object to contain sources of the given type. """
- modname = "Bcfg2.Server.Plugins.Packages.%s" % source_type.title()
- try:
- module = sys.modules[modname]
- except KeyError:
- try:
- module = __import__(modname).Server.Plugins.Packages
- except ImportError:
- msg = "Packages: Unknown source type %s" % source_type
- LOGGER.error(msg)
- raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
-
- try:
- cclass = getattr(module, source_type.title() + "Collection")
- except AttributeError:
- msg = "Packages: No collection class found for %s sources" % \
- source_type
- LOGGER.error(msg)
- raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
- return cclass
+ cls = None
+ for mod in Bcfg2.Options.setup.packages_backends:
+ if mod.__name__.endswith(".%s" % source_type.title()):
+ return getattr(mod, "%sCollection" % source_type.title())
+ raise Bcfg2.Server.Plugin.PluginExecutionError(
+ "Packages: No collection class found for %s sources" % source_type)
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py b/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
index 9ff2d53a0..1af046ec0 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
@@ -4,14 +4,12 @@
import os
import sys
import Bcfg2.Server.Plugin
-from Bcfg2.Options import get_option_parser
from Bcfg2.Server.Statistics import track_statistics
from Bcfg2.Server.Plugins.Packages.Source import SourceInitError
# pylint: disable=E0012,R0924
-class PackagesSources(Bcfg2.Server.Plugin.StructFile,
- Bcfg2.Server.Plugin.Debuggable):
+class PackagesSources(Bcfg2.Server.Plugin.StructFile):
""" PackagesSources handles parsing of the
:mod:`Bcfg2.Server.Plugins.Packages` ``sources.xml`` file, and the
creation of the appropriate
@@ -37,7 +35,6 @@ class PackagesSources(Bcfg2.Server.Plugin.StructFile,
:raises: :class:`Bcfg2.Server.Plugin.exceptions.PluginInitError` -
If ``sources.xml`` cannot be read
"""
- Bcfg2.Server.Plugin.Debuggable.__init__(self)
Bcfg2.Server.Plugin.StructFile.__init__(self, filename,
should_monitor=True)
@@ -54,8 +51,6 @@ class PackagesSources(Bcfg2.Server.Plugin.StructFile,
err = sys.exc_info()[1]
self.logger.error("Could not create Packages cache at %s: %s" %
(self.cachepath, err))
- #: The Bcfg2 options dict
- self.setup = get_option_parser()
#: The :class:`Bcfg2.Server.Plugins.Packages.Packages` that
#: instantiated this ``PackagesSources`` object
@@ -69,10 +64,9 @@ class PackagesSources(Bcfg2.Server.Plugin.StructFile,
self.parsed = set()
def set_debug(self, debug):
- Bcfg2.Server.Plugin.Debuggable.set_debug(self, debug)
+ Bcfg2.Server.Plugin.StructFile.set_debug(self, debug)
for source in self.entries:
source.set_debug(debug)
- set_debug.__doc__ = Bcfg2.Server.Plugin.Plugin.set_debug.__doc__
def HandleEvent(self, event=None):
""" HandleEvent is called whenever the FAM registers an event.
@@ -138,15 +132,13 @@ class PackagesSources(Bcfg2.Server.Plugin.StructFile,
xsource.get("url"))))
return None
- try:
- module = getattr(__import__("Bcfg2.Server.Plugins.Packages.%s" %
- stype.title()).Server.Plugins.Packages,
- stype.title())
- cls = getattr(module, "%sSource" % stype.title())
- except (ImportError, AttributeError):
- err = sys.exc_info()[1]
- self.logger.error("Packages: Unknown source type %s (%s)" % (stype,
- err))
+ cls = None
+ for mod in Bcfg2.Options.setup.packages_backends:
+ if mod.__name__.endswith(".%s" % stype.title()):
+ cls = getattr(mod, "%sSource" % stype.title())
+ break
+ else:
+ self.logger.error("Packages: Unknown source type %s" % stype)
return None
try:
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
index 767ac13ac..e1659dbb3 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
@@ -50,7 +50,7 @@ import os
import re
import sys
import Bcfg2.Server.Plugin
-from Bcfg2.Options import get_option_parser
+from Bcfg2.Logger import Debuggable
from Bcfg2.Compat import HTTPError, HTTPBasicAuthHandler, \
HTTPPasswordMgrWithDefaultRealm, install_opener, build_opener, urlopen, \
cPickle, md5
@@ -93,7 +93,7 @@ class SourceInitError(Exception):
REPO_RE = re.compile(r'(?:pulp/repos/|/RPMS\.|/)([^/]+)/?$')
-class Source(Bcfg2.Server.Plugin.Debuggable): # pylint: disable=R0902
+class Source(Debuggable): # pylint: disable=R0902
""" ``Source`` objects represent a single <Source> tag in
``sources.xml``. Note that a single Source tag can itself
describe multiple repositories (if it uses the "url" attribute
@@ -121,7 +121,7 @@ class Source(Bcfg2.Server.Plugin.Debuggable): # pylint: disable=R0902
:type source: lxml.etree._Element
:raises: :class:`Bcfg2.Server.Plugins.Packages.Source.SourceInitError`
"""
- Bcfg2.Server.Plugin.Debuggable.__init__(self)
+ Debuggable.__init__(self)
#: The base filesystem path under which cache data for this
#: source should be stored
@@ -130,9 +130,6 @@ class Source(Bcfg2.Server.Plugin.Debuggable): # pylint: disable=R0902
#: The XML tag that describes this source
self.xsource = xsource
- #: A Bcfg2 options dict
- self.setup = get_option_parser()
-
#: A set of package names that are deemed "essential" by this
#: source
self.essentialpkgs = set()
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
index 75dab3f76..0d49473c6 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
@@ -59,12 +59,10 @@ import errno
import socket
import logging
import lxml.etree
-from lockfile import FileLock
-
-import Bcfg2.Server.FileMonitor
import Bcfg2.Server.Plugin
+import Bcfg2.Server.FileMonitor
+from lockfile import FileLock
from Bcfg2.Utils import Executor
-from Bcfg2.Options import get_option_parser
# pylint: disable=W0622
from Bcfg2.Compat import StringIO, cPickle, HTTPError, URLError, \
ConfigParser, any
@@ -109,6 +107,30 @@ PULPSERVER = None
PULPCONFIG = None
+options = [
+ Bcfg2.Options.PathOption(
+ cf=("packages:yum", "helper"), dest="yum_helper",
+ help="Path to the bcfg2-yum-helper executable"),
+ Bcfg2.Options.BooleanOption(
+ cf=("packages:yum", "use_yum_libraries"),
+ help="Use Python yum libraries"),
+ Bcfg2.Options.PathOption(
+ cf=("packages:yum", "gpg_keypath"), default="/etc/pki/rpm-gpg",
+ help="GPG key path on the client"),
+ Bcfg2.Options.Option(
+ cf=("packages:yum", "*"), dest="yum_options",
+ help="Other yum options to include in generated yum configs")]
+if HAS_PULP:
+ options.append(
+ Bcfg2.Options.Option(
+ cf=("packages:pulp", "username"), dest="pulp_username",
+ help="Username for Pulp authentication"))
+ options.append(
+ Bcfg2.Options.Option(
+ cf=("packages:pulp", "password"), dest="pulp_password",
+ help="Password for Pulp authentication"))
+
+
def _setup_pulp():
""" Connect to a Pulp server and pass authentication credentials.
This only needs to be called once, but multiple calls won't hurt
@@ -124,20 +146,6 @@ def _setup_pulp():
raise Bcfg2.Server.Plugin.PluginInitError(msg)
if PULPSERVER is None:
- setup = get_option_parser()
- try:
- username = setup.cfp.get("packages:pulp", "username")
- password = setup.cfp.get("packages:pulp", "password")
- except ConfigParser.NoSectionError:
- msg = "Packages: No [pulp] section found in bcfg2.conf"
- LOGGER.error(msg)
- raise Bcfg2.Server.Plugin.PluginInitError(msg)
- except ConfigParser.NoOptionError:
- msg = "Packages: Required option not found in bcfg2.conf: %s" % \
- sys.exc_info()[1]
- LOGGER.error(msg)
- raise Bcfg2.Server.Plugin.PluginInitError(msg)
-
PULPCONFIG = ConsumerConfig()
serveropts = PULPCONFIG.server
@@ -145,7 +153,9 @@ def _setup_pulp():
int(serveropts['port']),
serveropts['scheme'],
serveropts['path'])
- PULPSERVER.set_basic_auth_credentials(username, password)
+ PULPSERVER.set_basic_auth_credentials(
+ Bcfg2.Options.setup.pulp_username,
+ Bcfg2.Options.setup.pulp_password)
server.set_active_server(PULPSERVER)
return PULPSERVER
@@ -357,10 +367,8 @@ class YumCollection(Collection):
the default location. """
if not self._helper:
# pylint: disable=W0212
- try:
- self.__class__._helper = self.setup.cfp.get("packages:yum",
- "helper")
- except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
+ self.__class__._helper = Bcfg2.Options.setup.yum_helper
+ if not self.__class__._helper:
# first see if bcfg2-yum-helper is in PATH
try:
self.debug_log("Checking for bcfg2-yum-helper in $PATH")
@@ -375,9 +383,7 @@ class YumCollection(Collection):
def use_yum(self):
""" True if we should use the yum Python libraries, False
otherwise """
- return HAS_YUM and self.setup.cfp.getboolean("packages:yum",
- "use_yum_libraries",
- default=False)
+ return HAS_YUM and Bcfg2.Options.setup.use_yum_libraries
@property
def has_pulp_sources(self):
@@ -413,15 +419,15 @@ class YumCollection(Collection):
debuglevel="0",
sslverify="0",
reposdir="/dev/null")
- if self.setup['debug']:
+ if Bcfg2.Options.setup.debug:
mainopts['debuglevel'] = "5"
- elif self.setup['verbose']:
+ elif Bcfg2.Options.setup.verbose:
mainopts['debuglevel'] = "2"
try:
- for opt in self.setup.cfp.options("packages:yum"):
+ for opt, val in Bcfg2.Options.setup.yum_options.items():
if opt not in self.option_blacklist:
- mainopts[opt] = self.setup.cfp.get("packages:yum", opt)
+ mainopts[opt] = val
except ConfigParser.NoSectionError:
pass
@@ -543,8 +549,7 @@ class YumCollection(Collection):
for key in needkeys:
# figure out the path of the key on the client
- keydir = self.setup.cfp.get("global", "gpg_keypath",
- default="/etc/pki/rpm-gpg")
+ keydir = Bcfg2.Options.setup.gpg_keypath
remotekey = os.path.join(keydir, os.path.basename(key))
localkey = os.path.join(self.keypath, os.path.basename(key))
kdata = open(localkey).read()
@@ -763,8 +768,7 @@ class YumCollection(Collection):
""" Given a package tuple, return a dict of attributes
suitable for applying to either a Package or an Instance
tag """
- attrs = dict(version=self.setup.cfp.get("packages", "version",
- default="auto"))
+ attrs = dict(version=Bcfg2.Options.setup.packages_version)
if attrs['version'] == 'any' or not isinstance(pkgtup, tuple):
return attrs
@@ -923,14 +927,10 @@ class YumCollection(Collection):
``bcfg2-yum-helper`` command.
"""
cmd = [self.helper, "-c", self.cfgfile]
- if self.setup['verbose']:
+ if Bcfg2.Options.setup.verbose:
cmd.append("-v")
if self.debug_flag:
- if not self.setup['verbose']:
- # ensure that running in debug gets -vv, even if
- # verbose is not enabled
- cmd.append("-v")
- cmd.append("-v")
+ cmd.append("-d")
cmd.append(command)
self.debug_log("Packages: running %s" % " ".join(cmd))
@@ -1053,9 +1053,7 @@ class YumSource(Source):
def use_yum(self):
""" True if we should use the yum Python libraries, False
otherwise """
- return HAS_YUM and self.setup.cfp.getboolean("packages:yum",
- "use_yum_libraries",
- default=False)
+ return HAS_YUM and Bcfg2.Options.setup.use_yum_libraries
def save_state(self):
""" If using the builtin yum parser, save state to
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/YumHelper.py b/src/lib/Bcfg2/Server/Plugins/Packages/YumHelper.py
new file mode 100644
index 000000000..32db0b32d
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/YumHelper.py
@@ -0,0 +1,384 @@
+""" Libraries for bcfg2-yum-helper plugin, used if yum library support
+is enabled. The yum libs have horrific memory leaks, so apparently
+the right way to get around that in long-running processes it to have
+a short-lived helper. No, seriously -- check out the yum-updatesd
+code. It's pure madness. """
+
+import os
+import sys
+import yum
+import logging
+import Bcfg2.Options
+import Bcfg2.Logger
+from Bcfg2.Compat import wraps
+from lockfile import FileLock, LockTimeout
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+
+def pkg_to_tuple(package):
+ """ json doesn't distinguish between tuples and lists, but yum
+ does, so we convert a package in list format to one in tuple
+ format """
+ if isinstance(package, list):
+ return tuple(package)
+ else:
+ return package
+
+
+def pkgtup_to_string(package):
+ """ given a package tuple, return a human-readable string
+ describing the package """
+ if package[3] in ['auto', 'any']:
+ return package[0]
+
+ rv = [package[0], "-"]
+ if package[2]:
+ rv.extend([package[2], ':'])
+ rv.extend([package[3], '-', package[4]])
+ if package[1]:
+ rv.extend(['.', package[1]])
+ return ''.join(str(e) for e in rv)
+
+
+class YumHelper(object):
+ """ Yum helper base object """
+
+ def __init__(self, cfgfile, verbose=1):
+ self.cfgfile = cfgfile
+ self.yumbase = yum.YumBase()
+ # pylint: disable=E1121,W0212
+ try:
+ self.yumbase.preconf.debuglevel = verbose
+ self.yumbase.preconf.fn = cfgfile
+ self.yumbase._getConfig()
+ except AttributeError:
+ self.yumbase._getConfig(cfgfile, debuglevel=verbose)
+ # pylint: enable=E1121,W0212
+ self.logger = logging.getLogger(self.__class__.__name__)
+
+
+class DepSolver(YumHelper):
+ """ Yum dependency solver. This is used for operations that only
+ read from the yum cache, and thus operates in cacheonly mode. """
+
+ def __init__(self, cfgfile, verbose=1):
+ YumHelper.__init__(self, cfgfile, verbose=verbose)
+ # internally, yum uses an integer, not a boolean, for conf.cache
+ self.yumbase.conf.cache = 1
+ self._groups = None
+
+ def get_groups(self):
+ """ getter for the groups property """
+ if self._groups is not None:
+ return self._groups
+ else:
+ return ["noarch"]
+
+ def set_groups(self, groups):
+ """ setter for the groups property """
+ self._groups = set(groups).union(["noarch"])
+
+ groups = property(get_groups, set_groups)
+
+ def get_package_object(self, pkgtup, silent=False):
+ """ given a package tuple, get a yum package object """
+ try:
+ matches = yum.packageSack.packagesNewestByName(
+ self.yumbase.pkgSack.searchPkgTuple(pkgtup))
+ except yum.Errors.PackageSackError:
+ if not silent:
+ self.logger.warning("Package '%s' not found" %
+ self.get_package_name(pkgtup))
+ matches = []
+ except yum.Errors.RepoError:
+ err = sys.exc_info()[1]
+ self.logger.error("Temporary failure loading metadata for %s: %s" %
+ (self.get_package_name(pkgtup), err))
+ matches = []
+
+ pkgs = self._filter_arch(matches)
+ if pkgs:
+ return pkgs[0]
+ else:
+ return None
+
+ def get_group(self, group, ptype="default"):
+ """ Resolve a package group name into a list of packages """
+ if group.startswith("@"):
+ group = group[1:]
+
+ try:
+ if self.yumbase.comps.has_group(group):
+ group = self.yumbase.comps.return_group(group)
+ else:
+ self.logger.error("%s is not a valid group" % group)
+ return []
+ except yum.Errors.GroupsError:
+ err = sys.exc_info()[1]
+ self.logger.warning(err)
+ return []
+
+ if ptype == "default":
+ return [p
+ for p, d in list(group.default_packages.items())
+ if d]
+ elif ptype == "mandatory":
+ return [p
+ for p, m in list(group.mandatory_packages.items())
+ if m]
+ elif ptype == "optional" or ptype == "all":
+ return group.packages
+ else:
+ self.logger.warning("Unknown group package type '%s'" % ptype)
+ return []
+
+ def _filter_arch(self, packages):
+ """ filter packages in the given list that do not have an
+ architecture in the list of groups for this client """
+ matching = []
+ for pkg in packages:
+ if pkg.arch in self.groups:
+ matching.append(pkg)
+ else:
+ self.logger.debug("%s has non-matching architecture (%s)" %
+ (pkg, pkg.arch))
+ if matching:
+ return matching
+ else:
+ # no packages match architecture; we'll assume that the
+ # user knows what s/he is doing and this is a multiarch
+ # box.
+ return packages
+
+ def get_package_name(self, package):
+ """ get the name of a package or virtual package from the
+ internal representation used by this Collection class """
+ if isinstance(package, tuple):
+ if len(package) == 3:
+ return yum.misc.prco_tuple_to_string(package)
+ else:
+ return pkgtup_to_string(package)
+ else:
+ return str(package)
+
+ def complete(self, packagelist):
+ """ resolve dependencies and generate a complete package list
+ from the given list of initial packages """
+ packages = set()
+ unknown = set()
+ for pkg in packagelist:
+ if isinstance(pkg, tuple):
+ pkgtup = pkg
+ else:
+ pkgtup = (pkg, None, None, None, None)
+ pkgobj = self.get_package_object(pkgtup)
+ if not pkgobj:
+ self.logger.debug("Unknown package %s" %
+ self.get_package_name(pkg))
+ unknown.add(pkg)
+ else:
+ if self.yumbase.tsInfo.exists(pkgtup=pkgobj.pkgtup):
+ self.logger.debug("%s added to transaction multiple times"
+ % pkgobj)
+ else:
+ self.logger.debug("Adding %s to transaction" % pkgobj)
+ self.yumbase.tsInfo.addInstall(pkgobj)
+ self.yumbase.resolveDeps()
+
+ for txmbr in self.yumbase.tsInfo:
+ packages.add(txmbr.pkgtup)
+ return list(packages), list(unknown)
+
+
+def acquire_lock(func):
+ """ decorator for CacheManager methods that gets and release a
+ lock while the method runs """
+ @wraps(func)
+ def inner(self, *args, **kwargs):
+ """ Get and release a lock while running the function this
+ wraps. """
+ self.logger.debug("Acquiring lock at %s" % self.lockfile)
+ while not self.lock.i_am_locking():
+ try:
+ self.lock.acquire(timeout=60) # wait up to 60 seconds
+ except LockTimeout:
+ self.lock.break_lock()
+ self.lock.acquire()
+ try:
+ func(self, *args, **kwargs)
+ finally:
+ self.lock.release()
+ self.logger.debug("Released lock at %s" % self.lockfile)
+
+ return inner
+
+
+class CacheManager(YumHelper):
+ """ Yum cache manager. Unlike :class:`DepSolver`, this can write
+ to the yum cache, and so is used for operations that muck with the
+ cache. (Technically, :func:`CacheManager.clean_cache` could be in
+ either DepSolver or CacheManager, but for consistency I've put it
+ here.) """
+
+ def __init__(self, cfgfile, verbose=1):
+ YumHelper.__init__(self, cfgfile, verbose=verbose)
+ self.lockfile = \
+ os.path.join(os.path.dirname(self.yumbase.conf.config_file_path),
+ "lock")
+ self.lock = FileLock(self.lockfile)
+
+ @acquire_lock
+ def clean_cache(self):
+ """ clean the yum cache """
+ for mdtype in ["Headers", "Packages", "Sqlite", "Metadata",
+ "ExpireCache"]:
+ # for reasons that are entirely obvious, all of the yum
+ # API clean* methods return a tuple of 0 (zero, always
+ # zero) and a list containing a single message about how
+ # many files were deleted. so useful. thanks, yum.
+ msg = getattr(self.yumbase, "clean%s" % mdtype)()[1][0]
+ if not msg.startswith("0 "):
+ self.logger.info(msg)
+
+ @acquire_lock
+ def populate_cache(self):
+ """ populate the yum cache """
+ for repo in self.yumbase.repos.findRepos('*'):
+ repo.metadata_expire = 0
+ repo.mdpolicy = "group:all"
+ self.yumbase.doRepoSetup()
+ self.yumbase.repos.doSetup()
+ for repo in self.yumbase.repos.listEnabled():
+ # this populates the cache as a side effect
+ repo.repoXML # pylint: disable=W0104
+ try:
+ repo.getGroups()
+ except yum.Errors.RepoMDError:
+ pass # this repo has no groups
+ self.yumbase.repos.populateSack(mdtype='metadata', cacheonly=1)
+ self.yumbase.repos.populateSack(mdtype='filelists', cacheonly=1)
+ self.yumbase.repos.populateSack(mdtype='otherdata', cacheonly=1)
+ # this does something with the groups cache as a side effect
+ self.yumbase.comps # pylint: disable=W0104
+
+
+class HelperSubcommand(Bcfg2.Options.Subcommand):
+ # the value to JSON encode and print out if the command fails
+ fallback = None
+
+ # whether or not this command accepts input on stdin
+ accept_input = True
+
+ def __init__(self):
+ Bcfg2.Options.Subcommand.__init__(self)
+ self.verbosity = 0
+ if Bcfg2.Options.setup.debug:
+ self.verbosity = 5
+ elif Bcfg2.Options.setup.verbose:
+ self.verbosity = 1
+
+ def run(self, setup):
+ try:
+ data = json.loads(sys.stdin.read())
+ except: # pylint: disable=W0702
+ self.logger.error("Unexpected error decoding JSON input: %s" %
+ sys.exc_info()[1])
+ print(json.dumps(self.fallback))
+ return 2
+
+ try:
+ print(json.dumps(self._run(setup, data)))
+ except: # pylint: disable=W0702
+ self.logger.error("Unexpected error running %s: %s" %
+ self.__class__.__name__.lower(),
+ sys.exc_info()[1], exc_info=1)
+ print(json.dumps(self.fallback))
+ return 2
+ return 0
+
+ def _run(self, setup, data):
+ raise NotImplementedError
+
+
+class DepSolverSubcommand(HelperSubcommand):
+ def __init__(self):
+ HelperSubcommand.__init__(self)
+ self.depsolver = DepSolver(Bcfg2.Options.setup.yum_config,
+ self.verbosity)
+
+
+class CacheManagerSubcommand(HelperSubcommand):
+ fallback = False
+ accept_input = False
+
+ def __init__(self):
+ HelperSubcommand.__init__(self)
+ self.cachemgr = CacheManager(Bcfg2.Options.setup.yum_config,
+ self.verbosity)
+
+
+class Clean(CacheManagerSubcommand):
+ def _run(self, setup, data): # pylint: disable=W0613
+ self.cachemgr.clean_cache()
+ return True
+
+
+class MakeCache(CacheManagerSubcommand):
+ def _run(self, setup, data): # pylint: disable=W0613
+ self.cachemgr.populate_cache()
+ return True
+
+
+class Complete(DepSolverSubcommand):
+ fallback = dict(packages=[], unknown=[])
+
+ def _run(self, _, data):
+ self.depsolver.groups = data['groups']
+ self.fallback['unknown'] = data['packages']
+ (packages, unknown) = self.depsolver.complete(
+ [pkg_to_tuple(p) for p in data['packages']])
+ return dict(packages=list(packages), unknown=list(unknown))
+
+
+class GetGroups(DepSolverSubcommand):
+ def _run(self, _, data):
+ rv = dict()
+ for gdata in data:
+ if "type" in gdata:
+ packages = self.depsolver.get_group(gdata['group'],
+ ptype=gdata['type'])
+ else:
+ packages = self.depsolver.get_group(gdata['group'])
+ rv[gdata['group']] = list(packages)
+ return rv
+
+
+Get_Groups = GetGroups
+
+
+class CLI(Bcfg2.Options.CommandRegistry):
+ options = [
+ Bcfg2.Options.PathOption(
+ "-c", "--yum-config", help="Yum config file"),
+ Bcfg2.Options.PositionalArgument(
+ "command", help="Yum helper command",
+ choices=['clean', 'complete', 'get_groups'])]
+
+ def __init__(self):
+ Bcfg2.Options.CommandRegistry.__init__(self)
+ Bcfg2.Options.register_commands(self.__class__, globals().values(),
+ parent=HelperSubcommand)
+ parser = Bcfg2.Options.get_parser("Bcfg2 yum helper",
+ components=[self])
+ parser.parse()
+ self.logger = logging.getLogger(parser.prog)
+
+ def run(self):
+ if not os.path.exists(Bcfg2.Options.setup.yum_config):
+ self.logger.error("Config file %s not found" %
+ Bcfg2.Options.setup.yum_config)
+ return 1
+ return self.runcommand()
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
index 20a75c678..e6240f39a 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
@@ -7,21 +7,30 @@ import sys
import glob
import shutil
import lxml.etree
-import Bcfg2.Logger
+import Bcfg2.Options
import Bcfg2.Server.Plugin
-from Bcfg2.Compat import ConfigParser, urlopen, HTTPError, URLError, \
- MutableMapping
+from Bcfg2.Compat import urlopen, HTTPError, URLError, MutableMapping
from Bcfg2.Server.Plugins.Packages.Collection import Collection, \
get_collection_class
from Bcfg2.Server.Plugins.Packages.PackagesSources import PackagesSources
from Bcfg2.Server.Statistics import track_statistics
-#: The default path for generated yum configs
-YUM_CONFIG_DEFAULT = "/etc/yum.repos.d/bcfg2.repo"
-#: The default path for generated apt configs
-APT_CONFIG_DEFAULT = \
- "/etc/apt/sources.list.d/bcfg2-packages-generated-sources.list"
+def packages_boolean(value):
+ """ For historical reasons, the Packages booleans 'resolver' and
+ 'metadata' both accept "enabled" in addition to the normal boolean
+ values. """
+ if value == 'disabled':
+ return False
+ elif value == 'enabled':
+ return True
+ else:
+ return value
+
+
+class PackagesBackendAction(Bcfg2.Options.ComponentAction):
+ bases = ['Bcfg2.Server.Plugins.Packages']
+ module = True
class OnDemandDict(MutableMapping):
@@ -86,6 +95,37 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
.. private-include: _build_packages"""
+ options = [
+ Bcfg2.Options.Option(
+ cf=("packages", "backends"), dest="packages_backends",
+ help="Packages backends to load",
+ type=Bcfg2.Options.Types.comma_list,
+ action=PackagesBackendAction, default=['Yum', 'Apt', 'Pac']),
+ Bcfg2.Options.PathOption(
+ cf=("packages", "cache"), dest="packages_cache",
+ help="Path to the Packages cache",
+ default='<repository>/Packages/cache'),
+ Bcfg2.Options.Option(
+ cf=("packages", "resolver"), dest="packages_resolver",
+ help="Disable the Packages resolver",
+ type=packages_boolean, default=True),
+ Bcfg2.Options.Option(
+ cf=("packages", "metadata"), dest="packages_metadata",
+ help="Disable all Packages metadata processing",
+ type=packages_boolean, default=True),
+ Bcfg2.Options.Option(
+ cf=("packages", "version"), dest="packages_version",
+ help="Set default Package entry version", default="auto",
+ choices=["auto", "any"]),
+ Bcfg2.Options.PathOption(
+ cf=("packages", "yum_config"),
+ help="The default path for generated yum configs",
+ default="/etc/yum.repos.d/bcfg2.repo"),
+ Bcfg2.Options.PathOption(
+ cf=("packages", "apt_config"),
+ help="The default path for generated apt configs",
+ default="/etc/apt/sources.list.d/bcfg2-packages-generated-sources.list")]
+
#: Packages is an alternative to
#: :mod:`Bcfg2.Server.Plugins.Pkgmgr` and conflicts with it.
conflicts = ['Pkgmgr']
@@ -108,9 +148,7 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
#: Packages does a potentially tremendous amount of on-disk
#: caching. ``cachepath`` holds the base directory to where
#: data should be cached.
- self.cachepath = \
- self.core.setup.cfp.get("packages", "cache",
- default=os.path.join(self.data, 'cache'))
+ self.cachepath = Bcfg2.Options.setup.packages_cache
#: Where Packages should store downloaded GPG key files
self.keypath = os.path.join(self.cachepath, 'keys')
@@ -186,40 +224,17 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
:attr:`disableMetaData`) implies disabling the resolver.
This property cannot be set. """
- if self.disableMetaData:
- # disabling metadata without disabling the resolver Breaks
- # Things
- return True
- try:
- return not self.core.setup.cfp.getboolean("packages", "resolver")
- except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
- return False
- except ValueError:
- # for historical reasons we also accept "enabled" and
- # "disabled", which are not handled according to the
- # Python docs but appear to be handled properly by
- # ConfigParser in at least some versions
- return self.core.setup.cfp.get(
- "packages",
- "resolver",
- default="enabled").lower() == "disabled"
+ # disabling metadata without disabling the resolver Breaks
+ # Things
+ return not Bcfg2.Options.setup.packages_metadata or \
+ not Bcfg2.Options.setup.packages_resolver
@property
def disableMetaData(self):
""" Report whether or not metadata processing is enabled.
This property cannot be set. """
- try:
- return not self.core.setup.cfp.getboolean("packages", "resolver")
- except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
- return False
- except ValueError:
- # for historical reasons we also accept "enabled" and
- # "disabled"
- return self.core.setup.cfp.get(
- "packages",
- "metadata",
- default="enabled").lower() == "disabled"
+ return not Bcfg2.Options.setup.packages_metadata
def create_config(self, entry, metadata):
""" Create yum/apt config for the specified client.
@@ -268,9 +283,7 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
"""
if entry.tag == 'Package':
collection = self.get_collection(metadata)
- entry.set('version', self.core.setup.cfp.get("packages",
- "version",
- default="auto"))
+ entry.set('version', Bcfg2.Options.setup.packages_version)
entry.set('type', collection.ptype)
elif entry.tag == 'Path':
self.create_config(entry, metadata)
@@ -299,14 +312,8 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
return True
elif entry.tag == 'Path':
# managed entries for yum/apt configs
- if (entry.get("name") ==
- self.core.setup.cfp.get("packages",
- "yum_config",
- default=YUM_CONFIG_DEFAULT) or
- entry.get("name") ==
- self.core.setup.cfp.get("packages",
- "apt_config",
- default=APT_CONFIG_DEFAULT)):
+ if entry.get("name") in [Bcfg2.Options.setup.apt_config,
+ Bcfg2.Options.setup.yum_config]:
return True
return False
@@ -339,7 +346,7 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
:returns: None
"""
collection = self.get_collection(metadata)
- indep = lxml.etree.Element('Independent')
+ indep = lxml.etree.Element('Independent', name=self.__class__.__name__)
self._build_packages(metadata, indep, structures,
collection=collection)
collection.build_extra_structures(indep)