summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Plugins/Packages
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Server/Plugins/Packages')
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Apt.py24
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Collection.py14
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Pkgng.py86
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Source.py44
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Yum.py62
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/__init__.py14
6 files changed, 200 insertions, 44 deletions
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py b/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
index dba56eed2..3d5c68e3f 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
@@ -69,12 +69,11 @@ class AptSource(Source):
else:
return ["%sPackages.gz" % self.rawurl]
- def read_files(self):
+ def read_files(self): # pylint: disable=R0912
bdeps = dict()
+ brecs = dict()
bprov = dict()
- depfnames = ['Depends', 'Pre-Depends']
- if self.recommended:
- depfnames.append('Recommends')
+ self.essentialpkgs = set()
for fname in self.files:
if not self.rawurl:
barch = [x
@@ -86,6 +85,7 @@ class AptSource(Source):
barch = self.arches[0]
if barch not in bdeps:
bdeps[barch] = dict()
+ brecs[barch] = dict()
bprov[barch] = dict()
try:
reader = gzip.GzipFile(fname)
@@ -100,9 +100,10 @@ class AptSource(Source):
pkgname = words[1].strip().rstrip()
self.pkgnames.add(pkgname)
bdeps[barch][pkgname] = []
+ brecs[barch][pkgname] = []
elif words[0] == 'Essential' and self.essential:
self.essentialpkgs.add(pkgname)
- elif words[0] in depfnames:
+ elif words[0] in ['Depends', 'Pre-Depends', 'Recommends']:
vindex = 0
for dep in words[1].split(','):
if '|' in dep:
@@ -113,17 +114,24 @@ class AptSource(Source):
barch,
vindex)
vindex += 1
- bdeps[barch][pkgname].append(dyn_dname)
+
+ if words[0] == 'Recommends':
+ brecs[barch][pkgname].append(dyn_dname)
+ else:
+ bdeps[barch][pkgname].append(dyn_dname)
bprov[barch][dyn_dname] = set(cdeps)
else:
raw_dep = re.sub(r'\(.*\)', '', dep)
raw_dep = raw_dep.rstrip().strip()
- bdeps[barch][pkgname].append(raw_dep)
+ if words[0] == 'Recommends':
+ brecs[barch][pkgname].append(raw_dep)
+ else:
+ bdeps[barch][pkgname].append(raw_dep)
elif words[0] == 'Provides':
for pkg in words[1].split(','):
dname = pkg.rstrip().strip()
if dname not in bprov[barch]:
bprov[barch][dname] = set()
bprov[barch][dname].add(pkgname)
- self.process_files(bdeps, bprov)
+ self.process_files(bdeps, bprov, brecs)
read_files.__doc__ = Source.read_files.__doc__
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py b/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
index 8b20df58a..004e27874 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
@@ -289,7 +289,7 @@ class Collection(list, Debuggable):
return any(source.is_virtual_package(self.metadata, package)
for source in self)
- def get_deps(self, package):
+ def get_deps(self, package, recs=None):
""" Get a list of the dependencies of the given package.
The base implementation simply aggregates the results of
@@ -299,9 +299,14 @@ class Collection(list, Debuggable):
:type package: string
:returns: list of strings, but see :ref:`pkg-objects`
"""
+ recommended = None
+ if recs and package in recs:
+ recommended = recs[package]
+
for source in self:
if source.is_package(self.metadata, package):
- return source.get_deps(self.metadata, package)
+ return source.get_deps(self.metadata, package, recommended)
+
return []
def get_essential(self):
@@ -465,7 +470,8 @@ class Collection(list, Debuggable):
return list(complete.difference(initial))
@track_statistics()
- def complete(self, packagelist): # pylint: disable=R0912,R0914
+ def complete(self, packagelist, # pylint: disable=R0912,R0914
+ recommended=None):
""" Build a complete list of all packages and their dependencies.
:param packagelist: Set of initial packages computed from the
@@ -529,7 +535,7 @@ class Collection(list, Debuggable):
self.debug_log("Packages: handling package requirement %s" %
(current,))
packages.add(current)
- deps = self.get_deps(current)
+ deps = self.get_deps(current, recommended)
newdeps = set(deps).difference(examined)
if newdeps:
self.debug_log("Packages: Package %s added requirements %s"
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Pkgng.py b/src/lib/Bcfg2/Server/Plugins/Packages/Pkgng.py
new file mode 100644
index 000000000..e393cabfe
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Pkgng.py
@@ -0,0 +1,86 @@
+""" pkgng backend for :mod:`Bcfg2.Server.Plugins.Packages` """
+
+import lzma
+import tarfile
+
+try:
+ import json
+ # py2.4 json library is structured differently
+ json.loads # pylint: disable=W0104
+except (ImportError, AttributeError):
+ import simplejson as json
+
+from Bcfg2.Server.Plugins.Packages.Collection import Collection
+from Bcfg2.Server.Plugins.Packages.Source import Source
+
+
+class PkgngCollection(Collection):
+ """ Handle collections of pkgng sources. This is a no-op object
+ that simply inherits from
+ :class:`Bcfg2.Server.Plugins.Packages.Collection.Collection`,
+ overrides nothing, and defers all operations to :class:`PacSource`
+ """
+
+ def __init__(self, metadata, sources, cachepath, basepath, debug=False):
+ # 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
+ # __init__ docstring, minus everything after ``.. -----``,
+ # 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 """
+ Collection.__init__(self, metadata, sources, cachepath, basepath,
+ debug=debug)
+ __init__.__doc__ = Collection.__init__.__doc__.split(".. -----")[0]
+
+
+class PkgngSource(Source):
+ """ Handle pkgng sources """
+
+ #: PkgngSource sets the ``type`` on Package entries to "pkgng"
+ ptype = 'pkgng'
+
+ @property
+ def urls(self):
+ """ A list of URLs to the base metadata file for each
+ repository described by this source. """
+ if not self.rawurl:
+ rv = []
+ for part in self.components:
+ for arch in self.arches:
+ rv.append("%s/freebsd:%s:%s/%s/packagesite.txz" %
+ (self.url, self.version, arch, part))
+ return rv
+ else:
+ return ["%s/packagesite.txz" % self.rawurl]
+
+ def read_files(self):
+ bdeps = dict()
+ for fname in self.files:
+ if not self.rawurl:
+ abi = [x
+ for x in fname.split('@')
+ if x.startswith('freebsd:')][0][8:]
+ barch = ':'.join(abi.split(':')[1:])
+ else:
+ # RawURL entries assume that they only have one <Arch></Arch>
+ # element and that it is the architecture of the source.
+ barch = self.arches[0]
+ if barch not in bdeps:
+ bdeps[barch] = dict()
+ try:
+ tar = tarfile.open(fileobj=lzma.LZMAFile(fname))
+ reader = tar.extractfile('packagesite.yaml')
+ except:
+ self.logger.error("Packages: Failed to read file %s" % fname)
+ raise
+ for line in reader.readlines():
+ if not isinstance(line, str):
+ line = line.decode('utf-8')
+ pkg = json.loads(line)
+ pkgname = pkg['name']
+ self.pkgnames.add(pkgname)
+ if 'deps' in pkg:
+ bdeps[barch][pkgname] = pkg['deps'].keys()
+ self.process_files(bdeps, dict())
+ read_files.__doc__ = Source.read_files.__doc__
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
index 4b6130f72..24db2963d 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
@@ -246,6 +246,10 @@ class Source(Debuggable): # pylint: disable=R0902
#: :class:`Bcfg2.Server.Plugins.Packages.Collection.Collection`
self.provides = dict()
+ #: A dict of ``<package name>`` -> ``<list of recommended
+ #: symbols>``. This will not necessarily be populated.
+ self.recommends = dict()
+
#: The file (or directory) used for this source's cache data
self.cachefile = os.path.join(self.basepath,
"cache-%s" % self.cachekey)
@@ -310,7 +314,7 @@ class Source(Debuggable): # pylint: disable=R0902
:raises: cPickle.UnpicklingError - If the saved data is corrupt """
data = open(self.cachefile, 'rb')
(self.pkgnames, self.deps, self.provides,
- self.essentialpkgs) = cPickle.load(data)
+ self.essentialpkgs, self.recommends) = cPickle.load(data)
def save_state(self):
""" Save state to :attr:`cachefile`. If caching and
@@ -318,7 +322,7 @@ class Source(Debuggable): # pylint: disable=R0902
does not need to be implemented. """
cache = open(self.cachefile, 'wb')
cPickle.dump((self.pkgnames, self.deps, self.provides,
- self.essentialpkgs), cache, 2)
+ self.essentialpkgs, self.recommends), cache, 2)
cache.close()
@track_statistics()
@@ -513,13 +517,14 @@ class Source(Debuggable): # pylint: disable=R0902
as its final step."""
pass
- def process_files(self, dependencies, provides):
+ def process_files(self, dependencies, # pylint: disable=R0912,W0102
+ provides, recommends=dict()):
""" Given dicts of depends and provides generated by
:func:`read_files`, this generates :attr:`deps` and
:attr:`provides` and calls :func:`save_state` to save the
cached data to disk.
- Both arguments are dicts of dicts of lists. Keys are the
+ All arguments are dicts of dicts of lists. Keys are the
arches of packages contained in this source; values are dicts
whose keys are package names and values are lists of either
dependencies for each package the symbols provided by each
@@ -531,14 +536,20 @@ class Source(Debuggable): # pylint: disable=R0902
:param provides: A dict of symbols provided by packages in
this repository.
:type provides: dict; see above.
+ :param recommends: A dict of recommended dependencies
+ found for this source.
+ :type recommends: dict; see above.
"""
self.deps['global'] = dict()
+ self.recommends['global'] = dict()
self.provides['global'] = dict()
for barch in dependencies:
self.deps[barch] = dict()
+ self.recommends[barch] = dict()
self.provides[barch] = dict()
for pkgname in self.pkgnames:
pset = set()
+ rset = set()
for barch in dependencies:
if pkgname not in dependencies[barch]:
dependencies[barch][pkgname] = []
@@ -548,6 +559,18 @@ class Source(Debuggable): # pylint: disable=R0902
else:
for barch in dependencies:
self.deps[barch][pkgname] = dependencies[barch][pkgname]
+
+ for barch in recommends:
+ if pkgname not in recommends[barch]:
+ recommends[barch][pkgname] = []
+ rset.add(tuple(recommends[barch][pkgname]))
+ if len(rset) == 1:
+ self.recommends['global'][pkgname] = rset.pop()
+ else:
+ for barch in recommends:
+ self.recommends[barch][pkgname] = \
+ recommends[barch][pkgname]
+
provided = set()
for bprovided in list(provides.values()):
provided.update(set(bprovided))
@@ -655,17 +678,24 @@ class Source(Debuggable): # pylint: disable=R0902
"""
return ['global'] + [a for a in self.arches if a in metadata.groups]
- def get_deps(self, metadata, package):
+ def get_deps(self, metadata, package, recommended=None):
""" Get a list of the dependencies of the given package.
:param package: The name of the symbol
:type package: string
:returns: list of strings
"""
+ recs = []
+ if ((recommended is None and self.recommended) or
+ (recommended and recommended.lower() == 'true')):
+ for arch in self.get_arches(metadata):
+ if package in self.recommends[arch]:
+ recs.extend(self.recommends[arch][package])
+
for arch in self.get_arches(metadata):
if package in self.deps[arch]:
- return self.deps[arch][package]
- return []
+ recs.extend(self.deps[arch][package])
+ return recs
def get_provides(self, metadata, package):
""" Get a list of all symbols provided by the given package.
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
index b98d3f419..f26ded4c5 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
@@ -63,6 +63,7 @@ import Bcfg2.Server.Plugin
import Bcfg2.Server.FileMonitor
from lockfile import FileLock
from Bcfg2.Utils import Executor
+from distutils.spawn import find_executable # pylint: disable=E0611
# pylint: disable=W0622
from Bcfg2.Compat import StringIO, cPickle, HTTPError, URLError, \
ConfigParser, any
@@ -89,7 +90,9 @@ try:
import yum
try:
import json
- except ImportError:
+ # py2.4 json library is structured differently
+ json.loads # pylint: disable=W0104
+ except (ImportError, AttributeError):
import simplejson as json
HAS_YUM = True
except ImportError:
@@ -340,25 +343,21 @@ class YumCollection(Collection):
@property
def helper(self):
- """ The full path to :file:`bcfg2-yum-helper`. First, we
- check in the config file to see if it has been explicitly
- specified; next we see if it's in $PATH (which we do by making
- a call to it; I wish there was a way to do this without
- forking, but apparently not); finally we check in /usr/sbin,
- the default location. """
+ """The full path to :file:`bcfg2-yum-helper`. First, we check in the
+ config file to see if it has been explicitly specified; next
+ we see if it's in $PATH; finally we default to /usr/sbin, the
+ default location. """
+ # pylint: disable=W0212
if not self._helper:
- # pylint: disable=W0212
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")
- self.cmd.run(['bcfg2-yum-helper'])
- self.__class__._helper = 'bcfg2-yum-helper'
- except OSError:
+ self.debug_log("Checking for bcfg2-yum-helper in $PATH")
+ self.__class__._helper = find_executable('bcfg2-yum-helper')
+ if not self.__class__._helper:
self.__class__._helper = "/usr/sbin/bcfg2-yum-helper"
- # pylint: enable=W0212
- return self._helper
+ return self.__class__._helper
+ # pylint: enable=W0212
@property
def use_yum(self):
@@ -417,6 +416,25 @@ class YumCollection(Collection):
yumconf.write(open(self.cfgfile, 'w'))
+ def get_arch(self):
+ """ If 'arch' for each source is the same, return that arch, otherwise
+ None.
+
+ This helps bcfg2-yum-helper when the client arch is
+ incompatible with the bcfg2 server's arch.
+
+ In case multiple arches are found, punt back to the default behavior.
+ """
+ arches = set()
+ for source in self:
+ for url_map in source.url_map:
+ if url_map['arch'] in self.metadata.groups:
+ arches.add(url_map['arch'])
+ if len(arches) == 1:
+ return arches.pop()
+ else:
+ return None
+
def get_config(self, raw=False): # pylint: disable=W0221
""" Get the yum configuration for this collection.
@@ -839,7 +857,7 @@ class YumCollection(Collection):
return new
@track_statistics()
- def complete(self, packagelist):
+ def complete(self, packagelist, recommended=None):
""" Build a complete list of all packages and their dependencies.
When using the Python yum libraries, this defers to the
@@ -857,7 +875,7 @@ class YumCollection(Collection):
resolved.
"""
if not self.use_yum:
- return Collection.complete(self, packagelist)
+ return Collection.complete(self, packagelist, recommended)
lock = FileLock(os.path.join(self.cachefile, "lock"))
slept = 0
@@ -872,10 +890,12 @@ class YumCollection(Collection):
if packagelist:
try:
- result = self.call_helper(
- "complete",
- dict(packages=list(packagelist),
- groups=list(self.get_relevant_groups())))
+ helper_dict = dict(packages=list(packagelist),
+ groups=list(self.get_relevant_groups()))
+ arch = self.get_arch()
+ if arch is not None:
+ helper_dict['arch'] = arch
+ result = self.call_helper("complete", helper_dict)
except ValueError:
# error reported by call_helper()
return set(), packagelist
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
index 49f64bdf3..d11ac60fe 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
@@ -101,7 +101,8 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
cf=("packages", "backends"), dest="packages_backends",
help="Packages backends to load",
type=Bcfg2.Options.Types.comma_list,
- action=PackagesBackendAction, default=['Yum', 'Apt', 'Pac']),
+ action=PackagesBackendAction,
+ default=['Yum', 'Apt', 'Pac', 'Pkgng']),
Bcfg2.Options.PathOption(
cf=("packages", "cache"), dest="packages_cache",
help="Path to the Packages cache",
@@ -319,8 +320,8 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
structures.append(indep)
@track_statistics()
- def _build_packages(self, metadata, independent, structures,
- collection=None):
+ def _build_packages(self, metadata, independent, # pylint: disable=R0914
+ structures, collection=None):
""" Perform dependency resolution and build the complete list
of packages that need to be included in the specification by
:func:`validate_structures`, based on the initial list of
@@ -357,10 +358,15 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
initial = set()
to_remove = []
groups = []
+ recommended = dict()
+
for struct in structures:
for pkg in struct.xpath('//Package | //BoundPackage'):
if pkg.get("name"):
initial.update(collection.packages_from_entry(pkg))
+
+ if pkg.get("recommended"):
+ recommended[pkg.get("name")] = pkg.get("recommended")
elif pkg.get("group"):
groups.append((pkg.get("group"),
pkg.get("type")))
@@ -399,7 +405,7 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
pcache = Bcfg2.Server.Cache.Cache("Packages", "pkg_sets",
collection.cachekey)
if pkey not in pcache:
- pcache[pkey] = collection.complete(base)
+ pcache[pkey] = collection.complete(base, recommended)
packages, unknown = pcache[pkey]
if unknown:
self.logger.info("Packages: Got %d unknown entries" % len(unknown))