try:
from configparser import ConfigParser, NoOptionError, NoSectionError
except ImportError:
from ConfigParser import ConfigParser, NoOptionError, NoSectionError
import errno
from functools import wraps
import glob
import logging
import os
import shlex
import time
import warnings
from string import Template
from subprocess import Popen, PIPE
try:
from subprocess import DEVNULL
except ImportError:
DEVNULL = open(os.devnull, 'r')
try:
basestring
except NameError:
basestring = str
LOGGER = logging.getLogger(__name__)
class GroupSourceError(Exception):
def __init__(self, message, group_source):
Exception.__init__(self, message)
self.group_source = group_source
class GroupSourceNoUpcall(GroupSourceError):
class GroupSourceQueryFailed(GroupSourceError):
class GroupResolverError(Exception):
class GroupResolverSourceError(GroupResolverError):
class GroupResolverIllegalCharError(GroupResolverError):
class GroupResolverConfigError(GroupResolverError):
_DEFAULT_CACHE_TIME = 3600
class GroupSource(object):
def __init__(self, name, groups=None, allgroups=None):
self.name = name
self.groups = groups or {} self.allgroups = allgroups
self.has_reverse = False
def resolv_map(self, group):
return self.groups.get(group, '')
def resolv_list(self):
return list(self.groups)
def resolv_all(self):
if self.allgroups is None:
raise GroupSourceNoUpcall("All groups info not available", self)
return self.allgroups
def resolv_reverse(self, node):
raise GroupSourceNoUpcall("Not implemented", self)
class FileGroupSource(GroupSource):
def __init__(self, name, loader):
self.name = name
self.loader = loader
self.has_reverse = False
@property
def groups(self):
return self.loader.groups(self.name)
@property
def allgroups(self):
return self.groups.get('all')
class UpcallGroupSource(GroupSource):
def __init__(self, name, map_upcall=None, all_upcall=None,
list_upcall=None, reverse_upcall=None, cfgdir=None,
cache_time=None, mapall_upcall=None):
GroupSource.__init__(self, name)
self.verbosity = 0 self.cfgdir = cfgdir
self.logger = logging.getLogger(__name__)
self.upcalls = {}
if map_upcall:
self.upcalls['map'] = map_upcall
if all_upcall:
self.upcalls['all'] = all_upcall
if list_upcall:
self.upcalls['list'] = list_upcall
if reverse_upcall:
self.upcalls['reverse'] = reverse_upcall
self.has_reverse = True
if mapall_upcall:
self.upcalls['mapall'] = mapall_upcall
if cache_time is None:
self.cache_time = _DEFAULT_CACHE_TIME
else:
self.cache_time = cache_time
self._cache = {}
self.clear_cache()
def clear_cache(self):
self._cache = {
'map': {},
'reverse': {}
}
def _upcall_read(self, cmdtpl, args=dict()):
cmdline = Template(self.upcalls[cmdtpl]).safe_substitute(args)
self.logger.debug("EXEC '%s'", cmdline)
proc = Popen(cmdline, stdin=DEVNULL, stdout=PIPE, shell=True,
cwd=self.cfgdir, universal_newlines=True)
output = proc.communicate()[0].strip()
self.logger.debug("READ '%s'", output)
if proc.returncode != 0:
self.logger.debug("ERROR '%s' returned %d", cmdline,
proc.returncode)
raise GroupSourceQueryFailed(cmdline, self)
return output
def _upcall_cache(self, upcall, cache, key, **args):
if key in cache and cache[key][1] < time.time():
self.logger.debug("PURGE EXPIRED (%d)'%s'", cache[key][1], key)
del cache[key]
if key not in cache:
if not self.upcalls.get(upcall):
raise GroupSourceNoUpcall(upcall, self)
cache_expiry = time.time() + self.cache_time
args['CFGDIR'] = self.cfgdir
args['SOURCE'] = self.name
cache[key] = (self._upcall_read(upcall, args), cache_expiry)
return cache[key][0]
def _populate_from_mapall(self):
if 'mapall' not in self.upcalls:
return
cached = self._cache.get('mapall')
if cached is not None and cached[1] >= time.time():
return
content = self._upcall_cache('mapall', self._cache, 'mapall')
cache_expiry = self._cache['mapall'][1]
new_map = {}
try:
for line in content.splitlines():
line = line.strip()
if not line:
continue
group, sep, nodes = line.partition(':')
group = group.strip()
if not sep or not group or len(group.split()) > 1:
raise GroupSourceQueryFailed(
"mapall: invalid line %r (expected 'group: nodes')"
% line, self)
nodes = nodes.strip()
if group in new_map:
nodes = ','.join(n for n in (new_map[group][0], nodes) if n)
new_map[group] = (nodes, cache_expiry)
except GroupSourceQueryFailed:
del self._cache['mapall'] raise
self._cache['map'] = new_map
self._cache['list'] = (' '.join(new_map), cache_expiry)
def resolv_map(self, group):
self._populate_from_mapall()
if 'map' not in self.upcalls and 'mapall' in self.upcalls:
return self._cache['map'].get(group, ('',))[0]
return self._upcall_cache('map', self._cache['map'], group, GROUP=group)
def resolv_list(self):
self._populate_from_mapall()
if 'list' not in self.upcalls and 'mapall' in self.upcalls:
return self._cache['list'][0]
return self._upcall_cache('list', self._cache, 'list')
def resolv_all(self):
return self._upcall_cache('all', self._cache, 'all')
def resolv_reverse(self, node):
node_str = str(node)
return self._upcall_cache('reverse', self._cache['reverse'], node_str,
NODE=node_str)
class YAMLGroupLoader(object):
def __init__(self, filename, cache_time=None):
if cache_time is None:
self.cache_time = _DEFAULT_CACHE_TIME
else:
self.cache_time = cache_time
self.cache_expiry = 0
self.filename = filename
self.sources = {}
self._groups = {}
self._load()
def _load(self):
with open(self.filename) as yamlfile:
try:
import yaml
sources = yaml.safe_load(yamlfile)
except ImportError as exc:
msg = "Disable autodir or install PyYAML!"
raise GroupResolverConfigError("%s (%s)" % (str(exc), msg))
except yaml.YAMLError as exc:
raise GroupResolverConfigError("%s: %s" % (self.filename, exc))
if not isinstance(sources, dict):
fmt = "%s: invalid content (base is not a dict)"
raise GroupResolverConfigError(fmt % self.filename)
first = not self.sources
for srcname, groups in sources.items():
if not isinstance(srcname, basestring):
fmt = '%s: group source %s not a string (add quotes?)'
raise GroupResolverConfigError(fmt % (self.filename, srcname))
if not isinstance(groups, dict):
fmt = "%s: invalid content (group source '%s' is not a dict)"
raise GroupResolverConfigError(fmt % (self.filename, srcname))
for grp, grpnodes in groups.items():
if not isinstance(grp, basestring):
fmt = '%s: %s: group name %s not a string (add quotes?)'
raise GroupResolverConfigError(fmt % (self.filename,
srcname, grp))
if grpnodes is None:
groups[grp] = ''
if first:
self._groups[srcname] = groups
self.sources[srcname] = FileGroupSource(srcname, self)
elif srcname in self.sources:
self._groups[srcname] = groups
self.cache_expiry = time.time() + self.cache_time
def __iter__(self):
return iter(self.sources.values())
def groups(self, sourcename):
if self.cache_expiry < time.time():
self._load()
return self._groups[sourcename]
class GroupResolver(object):
def __init__(self, default_source=None, illegal_chars=None):
self._sources = {}
self._default_source = default_source
self._initialized = False
self.illegal_chars = illegal_chars or set()
def _late_init(self):
if self._default_source:
self._sources[self._default_source.name] = self._default_source
self._initialized = True
def init(func):
@wraps(func)
def wrapper(self, *args):
if not self._initialized:
self._late_init()
return func(self, *args)
return wrapper
@init
def set_verbosity(self, value):
warnings.warn("set_verbosity() is deprecated; use logging instead",
DeprecationWarning, stacklevel=2)
for source in self._sources.values():
source.verbosity = value
@init
def add_source(self, group_source):
if group_source.name in self._sources:
raise ValueError("GroupSource '%s': name collision" % \
group_source.name)
self._sources[group_source.name] = group_source
@init
def sources(self):
srcs = list(self._sources)
if srcs and srcs[0] is not self._default_source:
srcs.remove(self._default_source.name)
srcs.insert(0, self._default_source.name)
return srcs
@init
def _get_default_source_name(self):
if self._default_source is None:
return None
return self._default_source.name
@init
def _set_default_source_name(self, sourcename):
try:
self._default_source = self._sources[sourcename]
except KeyError:
raise GroupResolverSourceError(sourcename)
default_source_name = property(_get_default_source_name,
_set_default_source_name)
def _list_nodes(self, source, what, *args):
result = []
assert source
raw = getattr(source, 'resolv_%s' % what)(*args)
if isinstance(raw, list):
raw = ','.join(raw)
for line in raw.splitlines():
[result.append(x) for x in line.strip().split()]
return result
def _list_groups(self, source, what, *args):
result = []
assert source
raw = getattr(source, 'resolv_%s' % what)(*args)
try:
grpiter = raw.splitlines()
except AttributeError:
grpiter = raw
for line in grpiter:
for grpstr in line.strip().split():
if self.illegal_chars.intersection(grpstr):
errmsg = ' '.join(self.illegal_chars.intersection(grpstr))
raise GroupResolverIllegalCharError(errmsg)
result.append(grpstr)
return result
@init
def _source(self, namespace):
if not namespace:
source = self._default_source
else:
source = self._sources.get(namespace)
if not source:
raise GroupResolverSourceError(namespace or "<default>")
return source
def group_nodes(self, group, namespace=None):
source = self._source(namespace)
return self._list_nodes(source, 'map', group)
def all_nodes(self, namespace=None):
source = self._source(namespace)
return self._list_nodes(source, 'all')
def grouplist(self, namespace=None):
source = self._source(namespace)
return self._list_groups(source, 'list')
def has_node_groups(self, namespace=None):
try:
return self._source(namespace).has_reverse
except GroupResolverSourceError:
return False
def node_groups(self, node, namespace=None):
source = self._source(namespace)
return self._list_groups(source, 'reverse', node)
class GroupResolverConfig(GroupResolver):
SECTION_MAIN = 'Main'
def __init__(self, filenames, illegal_chars=None):
GroupResolver.__init__(self, illegal_chars=illegal_chars)
self.filenames = filenames
self.config = None
def _late_init(self):
GroupResolver._late_init(self)
self.config = ConfigParser()
parsed = self.config.read(self.filenames)
if parsed:
self._parse_config(os.path.dirname(parsed[-1]))
def _parse_config(self, cfg_dirname):
try:
if self.config.has_option(self.SECTION_MAIN, 'groupsdir'):
opt_confdir = 'groupsdir'
else:
opt_confdir = 'confdir'
loaded_confdirs = set()
confdirstr = self.config.get(self.SECTION_MAIN, opt_confdir)
for confdir in shlex.split(confdirstr):
confdir = Template(confdir).safe_substitute(CFGDIR=cfg_dirname)
confdir = os.path.normpath(confdir)
if confdir in loaded_confdirs:
continue loaded_confdirs.add(confdir)
if not os.path.isdir(confdir):
if not os.path.exists(confdir):
continue
raise GroupResolverConfigError("Defined confdir %s is not"
" a directory" % confdir)
for groupsfn in sorted(glob.glob('%s/*.conf' % confdir)):
grpcfg = ConfigParser()
grpcfg.read(groupsfn) self._sources_from_cfg(grpcfg, confdir)
except (NoSectionError, NoOptionError):
pass
try:
loaded_autodirs = set()
autodirstr = self.config.get(self.SECTION_MAIN, 'autodir')
for autodir in shlex.split(autodirstr):
autodir = Template(autodir).safe_substitute(CFGDIR=cfg_dirname)
autodir = os.path.normpath(autodir)
if autodir in loaded_autodirs:
continue loaded_autodirs.add(autodir)
if not os.path.isdir(autodir):
if not os.path.exists(autodir):
continue
raise GroupResolverConfigError("Defined autodir %s is not"
" a directory" % autodir)
for autosfn in sorted(glob.glob('%s/*.yaml' % autodir)):
try:
self._sources_from_yaml(autosfn)
except IOError as exc: if exc.errno in (errno.EACCES, errno.EPERM):
LOGGER.debug(exc)
continue
except (NoSectionError, NoOptionError):
pass
self._sources_from_cfg(self.config, cfg_dirname)
try:
def_sourcename = self.config.get('Main', 'default')
self.default_source_name = def_sourcename
except (NoSectionError, NoOptionError):
pass
except GroupResolverSourceError:
if def_sourcename: fmt = 'Default group source not found: "%s"'
raise GroupResolverConfigError(fmt % self.config.get('Main',
'default'))
if not self.default_source_name and self._sources:
self.default_source_name = list(self._sources)[0]
def _sources_from_cfg(self, cfg, cfgdir):
try:
for section in cfg.sections():
for srcname in section.split(','):
if srcname != self.SECTION_MAIN:
if not cfg.has_option(section, 'map') and \
not cfg.has_option(section, 'mapall'):
raise GroupResolverConfigError(
"No option 'map' or 'mapall' in section: %r"
% section)
map_upcall = mapall_upcall = None
all_upcall = list_upcall = reverse_upcall = ctime = None
if cfg.has_option(section, 'map'):
map_upcall = cfg.get(section, 'map', raw=True)
if cfg.has_option(section, 'mapall'):
mapall_upcall = cfg.get(section, 'mapall',
raw=True)
if cfg.has_option(section, 'all'):
all_upcall = cfg.get(section, 'all', raw=True)
if cfg.has_option(section, 'list'):
list_upcall = cfg.get(section, 'list', raw=True)
if cfg.has_option(section, 'reverse'):
reverse_upcall = cfg.get(section, 'reverse',
raw=True)
if cfg.has_option(section, 'cache_time'):
ctime = float(cfg.get(section, 'cache_time',
raw=True))
self.add_source(UpcallGroupSource(
srcname, map_upcall, all_upcall, list_upcall,
reverse_upcall, cfgdir, ctime,
mapall_upcall=mapall_upcall))
except (NoSectionError, NoOptionError, ValueError) as exc:
raise GroupResolverConfigError(str(exc))
def _sources_from_yaml(self, filepath):
for source in YAMLGroupLoader(filepath):
self.add_source(source)