Source code for spicerack.confctl
"""Confctl module to abstract Conftool."""
import logging
import re
from conftool import configuration, kvobject, loader
from conftool.drivers import BackendError
from spicerack.exceptions import SpicerackError
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
[docs]class Confctl:
"""Confctl class to abstract conftool operations."""
def __init__(self, config='/etc/conftool/config.yaml', schema='/etc/conftool/schema.yaml', dry_run=True):
"""Initialize the instance.
Arguments:
config (str, optional): the path to the configuration file to load.
schema (str, optional): the path to the Conftool schema to load.
dry_run (bool, optional): whether this is a DRY-RUN.
"""
self._dry_run = dry_run
self._schema = loader.Schema.from_file(schema)
kvobject.KVObject.setup(configuration.get(config))
[docs] def entity(self, entity_name):
"""Get the Conftool specific entity class.
Arguments:
entity_name (str): the name of the entiryself.
Returns:
spicerack.confctl.ConftoolEntity: and entity-specific class to perform Conftool operations.
"""
return ConftoolEntity(self._schema.entities[entity_name], dry_run=self._dry_run)
[docs]class ConftoolEntity:
"""ConftoolEntity class to perform operations on a specific Conftool entity."""
def __init__(self, entity, dry_run=True):
"""Initialize the instance.
Arguments:
entity (conftool.kvobject.Entity): an instance of Conftool entity.
dry_run (bool, optional): whether this is a DRY-RUN.
"""
self._entity = entity
self._dry_run = dry_run
def _select(self, tags):
"""Generator that yields the selected objects based on the provided tags.
Arguments:
tags (dict): dictionary with tag: expression pairs of Conftool selectors.
Yields:
conftool.kvobject.Entity: the selected object.
Raises:
spicerack.confctl.ConfctlError: if not match is found.
"""
selectors = {}
for tag, expr in tags.items():
selectors[tag] = re.compile('^{}$'.format(expr))
obj = None
for obj in self._entity.query(selectors):
yield obj
if obj is None:
raise ConfctlError('No match found')
[docs] def update(self, changed, **tags):
"""Updates the value of conftool objects corresponding to the selection done with tags.
Arguments:
changed (dict): the new values to set for the selected objects.
**tags: arbitrary Conftool tags as keyword arguments.
Raises:
spicerack.confctl.ConfctlError: on etcd or Conftool errors.
Examples:
>>> confctl.update({'pooled': False}, service='appservers-.*', name='eqiad')
"""
logger.debug('Updating conftool matching tags: %s', tags)
if self._dry_run:
message_prefix = 'Skipping conftool update on dry-run mode'
else:
message_prefix = 'Updating conftool'
for obj in self._select(tags):
logger.debug('%s: %s -> %s', message_prefix, obj, changed)
if self._dry_run:
continue
try:
obj.update(changed)
except BackendError as e:
raise ConfctlError('Error writing to etcd') from e
except Exception as e:
raise ConfctlError('Generic error in conftool') from e
[docs] def get(self, **tags):
"""Generator that yields conftool objects corresponding to the selection.
Arguments:
**tags: arbitrary Conftool tags as keyword arguments.
Yields:
conftool.kvobject.Entity: the selected object.
"""
for obj in self._select(tags):
logger.debug('Selected conftool object: %s', obj)
yield obj
[docs] def set_and_verify(self, key, value, **tags):
"""Set and verify a single Conftool value.
Arguments:
key (str): the key in Conftool to modify.
value (mixed): the value to set.
**tags: arbitrary Conftool tags as keyword arguments.
Raises:
spicerack.confctl.ConfctlError: on etcd or Conftool errors or failing to verify the changes.
"""
logger.info('Setting %s=%s for tags: %s', key, value, tags)
self.update({key: value}, **tags)
for obj in self.get(**tags): # Verify the changes were applied
new = getattr(obj, key)
if new != value and not self._dry_run:
raise ConfctlError("Conftool key {key} has value '{new}', expecting '{value}' for tags: {tags}".format(
key=key, new=new, value=value, tags=tags))