wglass/lighthouse

View on GitHub
lighthouse/haproxy/config.py

Summary

Maintainability
A
45 mins
Test Coverage
import collections
import datetime
import logging

import six

from .stanzas.section import Section
from .stanzas.meta import MetaFrontendStanza
from .stanzas.frontend import FrontendStanza
from .stanzas.backend import BackendStanza
from .stanzas.peers import PeersStanza


logger = logging.getLogger(__name__)


class HAProxyConfig(object):
    """
    Class for generating HAProxy config file content.

    Requires global and defaults stanzas to be passed, can optionally take
    a `stats_stanza` for enabling a stats portal.
    """

    def __init__(
            self,
            global_stanza, defaults_stanza,
            proxy_stanzas=None, stats_stanza=None, meta_clusters=None,
            bind_address=None
    ):
        self.global_stanza = global_stanza
        self.defaults_stanza = defaults_stanza
        self.proxy_stanzas = proxy_stanzas or []
        self.stats_stanza = stats_stanza
        self.meta_clusters = meta_clusters or {}
        self.bind_address = bind_address

    def generate(self, clusters, version=None):
        """
        Generates HAProxy config file content based on a given list of
        clusters.
        """
        now = datetime.datetime.now()

        sections = [
            Section(
                "Auto-generated by Lighthouse (%s)" % now.strftime("%c"),
                self.global_stanza,
                self.defaults_stanza
            )
        ]

        meta_stanzas = [
            MetaFrontendStanza(
                name, self.meta_clusters[name]["port"],
                self.meta_clusters[name].get("frontend", []), members,
                self.bind_address
            )
            for name, members
            in six.iteritems(self.get_meta_clusters(clusters))
        ]
        frontend_stanzas = [
            FrontendStanza(cluster, self.bind_address)
            for cluster in clusters
            if "port" in cluster.haproxy
        ]
        backend_stanzas = [BackendStanza(cluster) for cluster in clusters]

        if version and version >= (1, 5, 0):
            peers_stanzas = [PeersStanza(cluster) for cluster in clusters]
        else:
            peers_stanzas = []

        sections.extend([
            Section("Frontend stanzas for ACL meta clusters", *meta_stanzas),
            Section("Per-cluster frontend definitions", *frontend_stanzas),
            Section("Per-cluster backend definitions", *backend_stanzas),
            Section("Per-cluster peer listings", *peers_stanzas),
            Section("Individual proxy definitions", *self.proxy_stanzas),
        ])
        if self.stats_stanza:
            sections.append(
                Section("Listener for stats web interface", self.stats_stanza)
            )

        return "\n\n\n".join([str(section) for section in sections]) + "\n"

    def get_meta_clusters(self, clusters):
        """
        Returns a dictionary keyed off of meta cluster names, where the values
        are lists of clusters associated with the meta cluster name.

        If a meta cluster name doesn't have a port defined in the
        `meta_cluster_ports` attribute an error is given and the meta cluster
        is removed from the mapping.
        """
        meta_clusters = collections.defaultdict(list)

        for cluster in clusters:
            if not cluster.meta_cluster:
                continue
            meta_clusters[cluster.meta_cluster].append(cluster)

        unconfigured_meta_clusters = [
            name for name in meta_clusters.keys()
            if name not in self.meta_clusters
        ]

        for name in unconfigured_meta_clusters:
            logger.error("Meta cluster %s not configured!")
            del meta_clusters[name]

        return meta_clusters