class ConfFiles(object):
    """
    Encapsulation of a set of configuration files.
    """

    def __init__(self,
                 host_and_role,
                 environment_loader,
                 data_loader):
        """
        A set of templated configuration files.

        :param host_and_role: An instance of
                              :class:`confab.definitions.HostAndRoleDefinition`
        :param environment_loader: An environment loader (e.g.
                                   :class:`confab.loaders.FileSystemEnvironmentLoader`)
        :param data_load: An instance DataLoader

        The environment loader must return a Jinja2 environment with an
        underlying loader that supports :meth:`jinja2.Environment.list_templates`.
        On init, ``ConfFiles`` will load all :term:`templates<template>` in the
        :term:`environment` for the specified :term:`host` and :term:`role`
        (including any role components).
        """
        self.conffiles = []
        self.host = host_and_role.host
        self.role = host_and_role.role
        self.environment = host_and_role.environment

        # default base path for generated and remotes directories
        self.directory = host_and_role.environmentdef.directory or os.getcwd()

        for component in host_and_role.components():
            debug("Processing: {}".format(component.name))

            data = data_loader(component)
            environment = environment_loader(component.name)
            jinja_filters.register(environment)

            for template_name in environment.list_templates(filter_func=options.filter_func):
                debug("Adding template: {}".format(template_name))

                self.conffiles.append(ConfFile(environment.get_template(template_name),
                                               data,
                                               component))

        if not self.conffiles:
            warn("No conffiles found for '{role}' on '{host}' in environment '{environment}'"
                 .format(role=self.role,
                         host=self.host,
                         environment=self.environment))

    def _get_host_generated_dir(self, directory):
        return join(directory or self.directory,
                    options.get_generated_dir(),
                    self.host)

    def _get_host_remotes_dir(self, directory):
        return join(directory or self.directory,
                    options.get_remotes_dir(),
                    self.host)

    def generate(self, directory=None):
        """
        Write all configuration files to ``generated_dir``.
        """
        host_generated_dir = self._get_host_generated_dir(directory)

        _clear_dir(host_generated_dir)
        _ensure_dir(host_generated_dir)

        for conffile in self.conffiles:
            conffile.generate(host_generated_dir)

    def pull(self, directory=None):
        """
        Pull remote versions of files into ``remotes_dir``.
        """
        host_remotes_dir = self._get_host_remotes_dir(directory)

        for conffile in self.conffiles:
            conffile.pull(host_remotes_dir)

    def diff(self, directory=None):
        """
        Show diffs for all configuration files.
        """
        host_generated_dir = self._get_host_generated_dir(directory)
        host_remotes_dir = self._get_host_remotes_dir(directory)

        for conffile in self.conffiles:
            conffile.pull(host_remotes_dir)

        for conffile in self.conffiles:
            conffile.generate(host_generated_dir)

        for conffile in self.conffiles:
            conffile.diff(host_generated_dir, host_remotes_dir).show()

    def push(self, directory=None):
        """
        Push configuration files that have changes, given user confirmation.
        """
        host_generated_dir = self._get_host_generated_dir(directory)
        host_remotes_dir = self._get_host_remotes_dir(directory)

        for conffile in self.conffiles:
            conffile.pull(host_remotes_dir)

        for conffile in self.conffiles:
            conffile.generate(host_generated_dir)

        has_diff = lambda conffile: conffile.diff(host_generated_dir, host_remotes_dir, True)
        with_diffs = filter(has_diff, self.conffiles)

        if not with_diffs:
            print(magenta('No configuration files to push for {host}'
                          .format(host=self.host)))
            return

        print(magenta('The following configuration files have changed for {host}:'
                      .format(host=self.host)))
        print
        for conffile in with_diffs:
            print(magenta('\t' + conffile.remote))

        if options.assume_yes or confirm('Push configuration files to {host}?'
                                         .format(host=self.host),
                                         default=False):
            for conffile in with_diffs:
                conffile.push(host_generated_dir) class Settings(object):
    """
    Collection of :term:`environment`, :term:`role`, and :term:`component` definitions.

    :param directory: optional configuration directory

    Supports convenient iteration. For example::

        # Iterate over all hosts and roles
        for host_and_role in settings.for_env("env").all():
            print host_and_role

        # Iterate over selected hosts and/or roles
        for host_and_role in settings.for_env("env").with_hosts("host1", "host2").with_roles("role1").all():
            print host_and_role

        # Iterate over all hosts and roles, then all components
        for host_and_role in settings.for_env("dev").all():
            environment, host, role = host_and_role
            for component in host_and_role.components():
                print component

        # Iterate over components
        for component in settings.for_env("dev").components():
            environment, host, role, name = component
            print component
    """

    KEYS = ["environmentdefs", "roledefs", "componentdefs"]

    def __init__(self, directory=None):
        self.directory = directory
        self.environmentdefs = {}
        self.roledefs = {}
        self.componentdefs = {}

    @classmethod
    def load_from_module(cls, settings_path=None):
        """
        Load settings from a Python module.

        :param settings_path: path to settings module. a full path or directory name.
                              module name defaults to ``settings``. directory defaults
                              to the current working directory.
        """
        if settings_path:
            if settings_path.endswith(".py"):
                dir_name, module = os.path.split(settings_path)
                module_name, _ = os.path.splitext(module)
            else:
                dir_name, module_name = settings_path, None
        else:
            dir_name, module_name = os.getcwd(), None

        settings_ = Settings(dir_name)
        try:
            module = _import(module_name or 'settings', dir_name)
        except ImportError as e:
            raise Exception("Unable to load {settings}: {error}"
                            .format(settings=os.path.join(dir_name, module_name or "settings.py"),
                                    error=e))

        for key in Settings.KEYS:
            setattr(settings_, key, getattr(module, key, {}))
        return settings_

    @classmethod
    def load_from_dict(cls, dct):
        """
        Load settings from a Python dictionary.
        """
        settings = Settings()
        for key in Settings.KEYS:
            setattr(settings, key, dct.get(key, {}))
        return settings

    def for_env(self, environment):
        """
        Obtain a specific :term:`environment` definition.

        Raises exceptions if the environment is not known
        """
        if not environment:
            raise ValueError("Environment was not specified")
        if environment not in self.environmentdefs:
            raise KeyError("Environment '{}' is not defined".format(environment))
        if not self.environmentdefs[environment]:
            warn("Environment '{}' does not have any hosts defined.".format(environment))
        for host in self.environmentdefs[environment]:
            if not self._roles_for_host(host):
                raise Exception("Host '{}' does not have any configured roles".format(host))
        return EnvironmentDefinition(self, environment)

    def all(self):
        """
        Iterate through all valid enviornments.
        """
        for environment in self.environmentdefs:
            yield self.for_env(environment)

    def _roles_for_host(self, host):
        """
        Compute complete list of roles for a host.
        """
        return [role for (role, hosts) in self.roledefs.iteritems() if host in hosts] def generate_tasks(settings_path=None):
    """
    Autogenerate `env` tasks for all defined environments.
    """
    def create_task(settings, environment):
        def select_environment(*roles):
            if hasattr(env, "environmentdef") and env.environmentdef.name != environment:
                abort("Environment already defined as '{}'".format(env.environmentdef.name))

            # Do not select hosts here.
            #
            # If `fab` is run with multiple hosts, this task will be run multiple
            # times, overwriting the value of "env.environmentdef". By not selecting hosts
            # here, we ensure that the same environmentdef will be loaded each
            # time. See also confab.iter:iter_conffiles
            env.environmentdef = settings.for_env(environment).with_roles(*roles)
        return select_environment

    settings = Settings.load_from_module(settings_path)

    for environment in settings.environmentdefs.iterkeys():
        _add_task(environment,
                  create_task(settings, environment),
                  "Set environment to '{environment}'".format(environment=environment))
    return settings class FileSystemEnvironmentLoader(object):
    """Loads Jinja2 environments from directories."""

    def __init__(self, *directories):
        self.directories = directories

    def __call__(self, subdir):
        """
        Load a Jinja2 Environment for a template sub-directory.
        """
        paths = map(lambda d: join(d, subdir), self.directories)
        template_path = next(iter(filter(exists, paths)), None)

        if template_path is None:
            debug("Using EmptyLoader for {}; no such directory".format(subdir))
            return Environment(loader=EmptyLoader())

        debug("Creating ConfabFileSystemLoader for {}".format(template_path))
        return Environment(loader=ConfabFileSystemLoader(template_path),
                           undefined=StrictUndefined) class PackageEnvironmentLoader(object):
    """Loads Jinja2 environments from python packages."""

    def __init__(self, package_name, templates_path='templates'):
        self.package_name = package_name
        self.templates_path = templates_path

    def __call__(self, subdir):
        """
        Load a Jinja2 Environment for a template sub-directory.
        """
        package_path = join(self.templates_path, subdir)

        provider = get_provider(self.package_name)
        if not provider.resource_isdir(package_path):
            debug("Using EmptyLoader for {}; no such directory".format(package_path))
            return Environment(loader=EmptyLoader())

        debug("Creating PackageLoader for {}".format(package_path))
        return Environment(loader=PackageLoader(self.package_name, package_path),
                           undefined=StrictUndefined) def add_jinja_filter(filter):
    """
    Add a custom jinja filter.
    """
    jinja_filters.add_filter(filter) def remove_jinja_filter(filter):
    """
    Remove a custom jinja filter.
    """
    return jinja_filters.remove_filter(filter) class JinjaFilters(object):
    """
    Context manager for Jinja filters.
    """
    def __init__(self, *filters):
        self._filters = filters

    def __enter__(self):
        for filter in self._filters:
            add_jinja_filter(filter)

    def __exit__(self, type, value, traceback):
        for filter in self._filters:
            remove_jinja_filter(filter) class DataLoader(object):
    """
    Load and merge configuration data.

    Configuration data is loaded from python files by type,
    where type is defined to include defaults, per-role values,
    per-component values, per-environment values and per-host values.

    Configuration data also includes the current environment
    and host string values under a ``confab`` key.
    """

    ALL = ['default', 'component', 'role', 'environment', 'host']

    def __init__(self, data_dirs, data_modules=ALL, ignore_hooks=False):
        """
        Create a data loader for the given data directories.

        :param data_dirs: list of data directories or a single data directory path.
        :param data_modules: list of modules to load in the order to load them.
        """
        self.data_dirs = data_dirs if isinstance(data_dirs, list) else [data_dirs]
        self.data_modules = set(data_modules)
        self._ignore_hooks = ignore_hooks

    def __call__(self, componentdef):
        """
        Load the data for the current configuration.

        :param component: a component definition.
        """
        def load_modules():
            for scope, module_name in self._list_modules(componentdef):
                yield import_configuration(module_name, *self.data_dirs, scope=scope)

                if not self._ignore_hooks:
                    for hook in hooks.for_scope(scope):
                        if hook.filter(componentdef):
                            yield hook(module_name)

        confab_data = dict(confab=dict(environment=componentdef.environment,
                                       host=componentdef.host,
                                       role=componentdef.role,
                                       component=componentdef.name))

        return merge(confab_data, *load_modules())

    def _list_modules(self, componentdef):
        """
        Get the list of modules to load.
        """
        module_names = [
            ('default', 'default'),
            ('component', componentdef.name),
            ('role', componentdef.role if componentdef.role != componentdef.name else None),
            ('environment', componentdef.environment),
            ('host', componentdef.host)
        ]

        return [(key, name) for key, name in module_names
                if key in self.data_modules and name is not None] class Hook(object):
    """
    Class representation of a hook.
    * hook_func to call
    * scope in which to call it
    * filter_func to determine whether it should be called for specific component
    """
    def __init__(self, hook_func, filter_func=None):
        self._hook_func = hook_func
        self._filter_func = filter_func
        self._data = None

    def __call__(self, module_name):
        if self._data is None:
            self._data = self._hook_func(module_name)
        return self._data

    def filter(self, componentdef):
        if self._filter_func is None:
            return True
        return self._filter_func(componentdef) def add_data_hook(scope, hook):
    hooks.add_hook(scope, hook) def remove_data_hook(scope, hook):
    return hooks.remove_hook(scope, hook) @task
def assume_yes():
    """
    Set the option to ``assume_yes`` in other tasks.
    """
    options.assume_yes = True class Options(object):
    """
    Context manager to temporarily set options.
    """

    def __init__(self, **kwargs):
        self.kwargs = kwargs
        self.previous = {}

    def __enter__(self):
        for (k, v) in self.kwargs.iteritems():
            self.previous[k] = options.get(k)
            options[k] = v
        return self

    def __exit__(self, exc_type, value, traceback):
        for k in self.kwargs.keys():
            options[k] = self.previous[k] def iter_hosts_and_roles():
    """
    Iterate over all hosts and roles in the configured environment.
    """
    environmentdef = _get_environmentdef()

    for host_and_role in environmentdef.all():
        # fabric needs the host if we're calling from main()
        with this_hostname(host_and_role.host):
            yield host_and_role def iter_hosts():
    """
    Iterate over all hosts in the configured environment.
    """
    environmentdef = _get_environmentdef()

    for host in environmentdef.hosts():
        # fabric needs the host if we're calling from main()
        with this_hostname(host.host):
            yield host def iter_conffiles(directory=None):
    """
    Generate :class:`~confab.conffiles.ConfFiles` objects for each
    ``host_and_role`` in an :term:`environment`.

    Uses the default :class:`~confab.loaders.FileSystemEnvironmentLoader` and
    :class:`~confab.data.DataLoader`.

    :param directory: Path to templates and data directories.
    """
    for host_and_role in iter_hosts_and_roles():
        yield make_conffiles(host_and_role, directory) def make_conffiles(host_and_role, directory=None):
    """
    Create a :class:`~confab.conffiles.ConfFiles` object for a
    ``host_and_role`` in an :term:`environment`.

    Uses the default :class:`~confab.loaders.FileSystemEnvironmentLoader` and
    :class:`~confab.data.DataLoader`.

    :param directory: Path to templates and data directories.
    """
    directories = [directory or options.get_base_dir()]
    directories.extend(iter_extension_paths())

    # Construct directories
    templates_dirs = map(lambda dir: join(dir, options.get_templates_dir()), directories)
    assert_exists(*templates_dirs)
    data_dirs = map(lambda dir: join(dir, options.get_data_dir()), directories)
    assert_exists(*data_dirs)

    return ConfFiles(host_and_role,
                     FileSystemEnvironmentLoader(*templates_dirs),
                     DataLoader(data_dirs)) @contextmanager
def this_hostname(hostname):
    """
    Context manager that uses the current SSH confg to switch Fabric to a specific hostname.

    Updates hostname and port.
    """
    host_config = ssh_config(hostname)

    host_string = hostname
    port = host_config.get("port", env.default_port)

    with settings(host_string=host_string,
                  port=port):
        yield @task
@with_validation
def diff(directory=None):
    """
    Show configuration file diffs.
    """
    for conffiles in iter_conffiles(directory):
        status("Computing template diffs for '{environment}' and '{role}'",
               environment=conffiles.environment,
               role=conffiles.role)

        conffiles.diff(directory) @task
@with_validation
def generate(directory=None):
    """
    Generate configuration files.
    """
    for conffiles in iter_conffiles(directory):
        status("Generating templates for '{environment}' and '{role}'",
               environment=conffiles.environment,
               role=conffiles.role)

        conffiles.generate() @task
@with_validation
def pull(directory=None):
    """
    Pull remote configuration files.
    """
    for conffiles in iter_conffiles(directory):
        status("Pulling remote templates for '{environment}' and '{role}'",
               environment=conffiles.environment,
               role=conffiles.role)

        conffiles.pull() @task
@with_validation
def push(directory=None):
    """
    Push configuration files.
    """
    for conffiles in iter_conffiles(directory):
        status("Pushing templates for '{environment}' and '{role}'",
               environment=conffiles.environment,
               role=conffiles.role)

        conffiles.push()
from confab.conffiles import ConfFiles
from confab.definitions import Settings
from confab.autotasks import generate_tasks
from confab.loaders import FileSystemEnvironmentLoader, PackageEnvironmentLoader
from confab.jinja_filters import add_jinja_filter, remove_jinja_filter, JinjaFilters
from confab.data import DataLoader
from confab.hooks import Hook, add_data_hook, remove_data_hook
from confab.options import assume_yes, Options
from confab.iter import (iter_hosts_and_roles,
                         iter_hosts,
                         iter_conffiles,
                         make_conffiles,
                         this_hostname)
from confab.diff import diff
from confab.generate import generate
from confab.pull import pull
from confab.push import push
"""
Non-init module for doing convenient * imports from.
"""

# core

# settings

# environment tasks

# jinja2 environment loading

# jinja2 filters

# data loading

# hooks

# options

# iterations

# fabric tasks

__ignore__ = [
    assume_yes,
    generate_tasks,
    ConfFiles,
    FileSystemEnvironmentLoader,
    PackageEnvironmentLoader,
    DataLoader,
    Hook,
    add_data_hook,
    remove_data_hook,
    diff,
    generate,
    pull,
    push,
    Options,
    Settings,
    iter_hosts_and_roles,
    iter_hosts,
    iter_conffiles,
    make_conffiles,
    this_hostname,

    add_jinja_filter,