"""
This file stores persistent metadata for the Bcfg2 Configuration Repository.
"""

__revision__ = '$Revision$'

import copy
import fcntl
import lxml.etree
import os
import socket
import sys
import time
import Bcfg2.Server
import Bcfg2.Server.FileMonitor
import Bcfg2.Server.Plugin


def locked(fd):
    """Aquire a lock on a file"""
    try:
        fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except IOError:
        return True
    return False


class MetadataConsistencyError(Exception):
    """This error gets raised when metadata is internally inconsistent."""
    pass


class MetadataRuntimeError(Exception):
    """This error is raised when the metadata engine
    is called prior to reading enough data.
    """
    pass


class XMLMetadataConfig(object):
    """Handles xml config files and all XInclude statements"""
    def __init__(self, metadata, watch_clients, basefile):
        self.metadata = metadata
        self.basefile = basefile
        self.should_monitor = watch_clients
        self.extras = []
        self.data = None
        self.basedata = None
        self.basedir = metadata.data
        self.logger = metadata.logger
        self.pseudo_monitor = isinstance(metadata.core.fam,
                                         Bcfg2.Server.FileMonitor.Pseudo)

    @property
    def xdata(self):
        if not self.data:
            raise MetadataRuntimeError
        return self.data

    @property
    def base_xdata(self):
        if not self.basedata:
            raise MetadataRuntimeError
        return self.basedata

    def add_monitor(self, fname):
        """Add a fam monitor for an included file"""
        if self.should_monitor:
            self.metadata.core.fam.AddMonitor("%s/%s" % (self.basedir, fname),
                                              self.metadata)
            self.extras.append(fname)

    def load_xml(self):
        """Load changes from XML"""
        try:
            xdata = lxml.etree.parse(os.path.join(self.basedir, self.basefile),
                                     parser=Bcfg2.Server.XMLParser)
        except lxml.etree.XMLSyntaxError:
            self.logger.error('Failed to parse %s' % (self.basefile))
            return
        self.basedata = copy.copy(xdata)
        included = [ent.get('href') for ent in \
                    xdata.findall('./{http://www.w3.org/2001/XInclude}include')]
        if included:
            for name in included:
                if name not in self.extras:
                    self.add_monitor(name)
            try:
                xdata.xinclude()
            except lxml.etree.XIncludeError:
                self.logger.error("Failed to process XInclude for file %s" % self.basefile)
        self.data = xdata

    def write(self):
        """Write changes to xml back to disk."""
        self.write_xml("%s/%s" % (self.basedir, self.basefile),
                       self.basedata)

    def write_xml(self, fname, xmltree):
        """Write changes to xml back to disk."""
        tmpfile = "%s.new" % fname
        try:
            datafile = open("%s" % tmpfile, 'w')
        except IOError:
            e = sys.exc_info()[1]
            self.logger.error("Failed to write %s: %s" % (tmpfile, e))
            raise MetadataRuntimeError
        # prep data
        dataroot = xmltree.getroot()
        newcontents = lxml.etree.tostring(dataroot, pretty_print=True)

        fd = datafile.fileno()
        while locked(fd) == True:
            pass
        try:
            datafile.write(newcontents)
        except:
            fcntl.lockf(fd, fcntl.LOCK_UN)
            self.logger.error("Metadata: Failed to write new xml data to %s" % tmpfile, exc_info=1)
            os.unlink("%s" % tmpfile)
            raise MetadataRuntimeError
        datafile.close()

        # check if clients.xml is a symlink
        xmlfile = "%s" % fname
        if os.path.islink(xmlfile):
            xmlfile = os.readlink(xmlfile)

        try:
            os.rename("%s" % tmpfile, xmlfile)
        except:
            self.logger.error("Metadata: Failed to rename %s" % tmpfile)
            raise MetadataRuntimeError

    def find_xml_for_xpath(self, xpath):
        """Find and load xml data containing the xpath query"""
        if self.pseudo_monitor:
            # Reload xml if we don't have a real monitor
            self.load_xml()
        cli = self.basedata.xpath(xpath)
        if len(cli) > 0:
            return {'filename': "%s/%s" % (self.basedir, self.basefile),
                    'xmltree': self.basedata,
                    'xquery': cli}
        else:
            """Try to find the data in included files"""
            for included in self.extras:
                try:
                    xdata = lxml.etree.parse(os.path.join(self.basedir,
                                                          included),
                                             parser=Bcfg2.Server.XMLParser)
                    cli = xdata.xpath(xpath)
                    if len(cli) > 0:
                        return {'filename': os.path.join(self.basedir,
                                                         included),
                                'xmltree': xdata,
                                'xquery': cli}
                except lxml.etree.XMLSyntaxError:
                    self.logger.error('Failed to parse %s' % (included))
        return {}

    def HandleEvent(self, event):
        """Handle fam events"""
        filename = event.filename.split('/')[-1]
        if filename in self.extras:
            if event.code2str() == 'exists':
                return False
        elif filename != self.basefile:
            return False
        if event.code2str() == 'endExist':
            return False
        self.load_xml()
        return True


class ClientMetadata(object):
    """This object contains client metadata."""
    def __init__(self, client, profile, groups, bundles,
                 aliases, addresses, categories, uuid, password, query):
        self.hostname = client
        self.profile = profile
        self.bundles = bundles
        self.aliases = aliases
        self.addresses = addresses
        self.groups = groups
        self.categories = categories
        self.uuid = uuid
        self.password = password
        self.connectors = []
        self.query = query

    def inGroup(self, group):
        """Test to see if client is a member of group."""
        return group in self.groups

    def group_in_category(self, category):
        for grp in self.query.all_groups_in_category(category):
            if grp in self.groups:
                return grp
        return ''


class MetadataQuery(object):
    def __init__(self, by_name, get_clients, by_groups, by_profiles, all_groups, all_groups_in_category):
        # resolver is set later
        self.by_name = by_name
        self.names_by_groups = by_groups
        self.names_by_profiles = by_profiles
        self.all_clients = get_clients
        self.all_groups = all_groups
        self.all_groups_in_category = all_groups_in_category

    def by_groups(self, groups):
        return [self.by_name(name) for name in self.names_by_groups(groups)]

    def by_profiles(self, profiles):
        return [self.by_name(name) for name in self.names_by_profiles(profiles)]

    def all(self):
        return [self.by_name(name) for name in self.all_clients()]


class Metadata(Bcfg2.Server.Plugin.Plugin,
               Bcfg2.Server.Plugin.Metadata,
               Bcfg2.Server.Plugin.Statistics):
    """This class contains data for bcfg2 server metadata."""
    __version__ = '$Id$'
    __author__ = 'bcfg-dev@mcs.anl.gov'
    name = "Metadata"
    sort_order = 500

    def __init__(self, core, datastore, watch_clients=True):
        Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
        Bcfg2.Server.Plugin.Metadata.__init__(self)
        Bcfg2.Server.Plugin.Statistics.__init__(self)
        if watch_clients:
            try:
                core.fam.AddMonitor("%s/%s" % (self.data, "groups.xml"), self)
                core.fam.AddMonitor("%s/%s" % (self.data, "clients.xml"), self)
            except:
                print("Unable to add file monitor for groups.xml or clients.xml")
                raise Bcfg2.Server.Plugin.PluginInitError

        self.clients_xml = XMLMetadataConfig(self, watch_clients, 'clients.xml')
        self.groups_xml = XMLMetadataConfig(self, watch_clients, 'groups.xml')
        self.states = {}
        if watch_clients:
            self.states = {"groups.xml": False,
                           "clients.xml": False}
        self.addresses = {}
        self.auth = dict()
        self.clients = {}
        self.aliases = {}
        self.groups = {}
        self.cgroups = {}
        self.public = []
        self.private = []
        self.profiles = []
        self.categories = {}
        self.bad_clients = {}
        self.uuid = {}
        self.secure = []
        self.floating = []
        self.passwords = {}
        self.session_cache = {}
        self.default = None
        self.pdirty = False
        self.extra = {'groups.xml': [],
                      'clients.xml': []}
        self.password = core.password
        self.query = MetadataQuery(core.build_metadata,
                                   lambda: list(self.clients.keys()),
                                   self.get_client_names_by_groups,
                                   self.get_client_names_by_profiles,
                                   self.get_all_group_names,
                                   self.get_all_groups_in_category)

    @classmethod
    def init_repo(cls, repo, groups, os_selection, clients):
        path = '%s/%s' % (repo, cls.name)
        os.makedirs(path)
        open("%s/Metadata/groups.xml" %
             repo, "w").write(groups % os_selection)
        open("%s/Metadata/clients.xml" %
             repo, "w").write(clients % socket.getfqdn())

    def get_groups(self):
        '''return groups xml tree'''
        groups_tree = lxml.etree.parse(self.data + "/groups.xml",
                                       parser=Bcfg2.Server.XMLParser)
        root = groups_tree.getroot()
        return root

    def search_group(self, group_name, tree):
        """Find a group."""
        for node in tree.findall("//Group"):
            if node.get("name") == group_name:
                return node
            for child in node:
                if child.tag == "Alias" and child.attrib["name"] == group_name:
                    return node
        return None

    def add_group(self, group_name, attribs):
        """Add group to groups.xml."""

        node = self.search_group(group_name, self.groups_xml.xdata)
        if node != None:
            self.logger.error("Group \"%s\" already exists" % (group_name))
            raise MetadataConsistencyError

        element = lxml.etree.SubElement(self.groups_xml.base_xdata.getroot(),
                                      "Group", name=group_name)
        for key, val in list(attribs.items()):
            element.set(key, val)
        self.groups_xml.write()

    def update_group(self, group_name, attribs):
        """Update a groups attributes."""
        node = self.search_group(group_name, self.groups_xml.xdata)
        if node == None:
            self.logger.error("Group \"%s\" does not exist" % (group_name))
            raise MetadataConsistencyError
        xdict = self.groups_xml.find_xml_for_xpath('.//Group[@name="%s"]' % (node.get('name')))
        if not xdict:
            self.logger.error("Unexpected error finding group")
            raise MetadataConsistencyError

        for key, val in list(attribs.items()):
            xdict['xquery'][0].set(key, val)
        self.groups_xml.write_xml(xdict['filename'], xdict['xmltree'])

    def remove_group(self, group_name):
        """Remove a group."""
        node = self.search_group(group_name, self.groups_xml.xdata)
        if node == None:
            self.logger.error("Group \"%s\" does not exist" % (group_name))
            raise MetadataConsistencyError
        xdict = self.groups_xml.find_xml_for_xpath('.//Group[@name="%s"]' % (node.get('name')))
        if not xdict:
            self.logger.error("Unexpected error finding group")
            raise MetadataConsistencyError
        xdict['xquery'][0].getparent().remove(xdict['xquery'][0])
        self.groups_xml.write_xml(xdict['filename'], xdict['xmltree'])

    def add_bundle(self, bundle_name):
        """Add bundle to groups.xml."""
        tree = lxml.etree.parse(self.data + "/groups.xml",
                                parser=Bcfg2.Server.XMLParser)
        root = tree.getroot()
        element = lxml.etree.Element("Bundle", name=bundle_name)
        node = self.search_group(bundle_name, tree)
        if node != None:
            self.logger.error("Bundle \"%s\" already exists" % (bundle_name))
            raise MetadataConsistencyError
        root.append(element)
        group_tree = open(self.data + "/groups.xml", "w")
        fd = group_tree.fileno()
        while True:
            try:
                fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            except IOError:
                continue
            else:
                break
        tree.write(group_tree)
        fcntl.lockf(fd, fcntl.LOCK_UN)
        group_tree.close()

    def remove_bundle(self, bundle_name):
        """Remove a bundle."""
        tree = lxml.etree.parse(self.data + "/groups.xml",
                                parser=Bcfg2.Server.XMLParser)
        root = tree.getroot()
        node = self.search_group(bundle_name, tree)
        if node == None:
            self.logger.error("Bundle \"%s\" not found" % (bundle_name))
            raise MetadataConsistencyError
        root.remove(node)
        group_tree = open(self.data + "/groups.xml", "w")
        fd = group_tree.fileno()
        while True:
            try:
                fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            except IOError:
                continue
            else:
                break
        tree.write(group_tree)
        fcntl.lockf(fd, fcntl.LOCK_UN)
        group_tree.close()

    def search_client(self, client_name, tree):
        """Find a client."""
        for node in tree.findall("//Client"):
            if node.get("name") == client_name:
                return node
            for child in node:
                if child.tag == "Alias" and child.attrib["name"] == client_name:
                    return node
        return None

    def add_client(self, client_name, attribs):
        """Add client to clients.xml."""
        node = self.search_client(client_name, self.clients_xml.xdata)
        if node != None:
            self.logger.error("Client \"%s\" already exists" % (client_name))
            raise MetadataConsistencyError

        element = lxml.etree.SubElement(self.clients_xml.base_xdata.getroot(),
                                      "Client", name=client_name)
        for key, val in list(attribs.items()):
            element.set(key, val)
        self.clients_xml.write()

    def update_client(self, client_name, attribs):
        """Update a clients attributes."""
        node = self.search_client(client_name, self.clients_xml.xdata)
        if node == None:
            self.logger.error("Client \"%s\" does not exist" % (client_name))
            raise MetadataConsistencyError

        xdict = self.clients_xml.find_xml_for_xpath('.//Client[@name="%s"]' % (node.get('name')))
        if not xdict:
            self.logger.error("Unexpected error finding client")
            raise MetadataConsistencyError

        node = xdict['xquery'][0]
        [node.set(key, value) for key, value in list(attribs.items())]
        self.clients_xml.write_xml(xdict['filename'], xdict['xmltree'])

    def HandleEvent(self, event):
        """Handle update events for data files."""
        if self.clients_xml.HandleEvent(event):
            xdata = self.clients_xml.xdata
            self.clients = {}
            self.aliases = {}
            self.raliases = {}
            self.bad_clients = {}
            self.secure = []
            self.floating = []
            self.addresses = {}
            self.raddresses = {}
            for client in xdata.findall('.//Client'):
                clname = client.get('name').lower()
                if 'address' in client.attrib:
                    caddr = client.get('address')
                    if caddr in self.addresses:
                        self.addresses[caddr].append(clname)
                    else:
                        self.addresses[caddr] = [clname]
                    if clname not in self.raddresses:
                        self.raddresses[clname] = set()
                    self.raddresses[clname].add(caddr)
                if 'auth' in client.attrib:
                    self.auth[client.get('name')] = client.get('auth',
                                                               'cert+password')
                if 'uuid' in client.attrib:
                    self.uuid[client.get('uuid')] = clname
                if client.get('secure', 'false') == 'true':
                    self.secure.append(clname)
                if client.get('location', 'fixed') == 'floating':
                    self.floating.append(clname)
                if 'password' in client.attrib:
                    self.passwords[clname] = client.get('password')
                for alias in [alias for alias in client.findall('Alias')\
                              if 'address' in alias.attrib]:
                    if alias.get('address') in self.addresses:
                        self.addresses[alias.get('address')].append(clname)
                    else:
                        self.addresses[alias.get('address')] = [clname]
                    if clname not in self.raddresses:
                        self.raddresses[clname] = set()
                    self.raddresses[clname].add(alias.get('address'))
                self.clients.update({clname: client.get('profile')})
                [self.aliases.update({alias.get('name'): clname}) \
                 for alias in client.findall('Alias')]
                self.raliases[clname] = set()
                [self.raliases[clname].add(alias.get('name')) for alias \
                 in client.findall('Alias')]
            self.states['clients.xml'] = True
        elif self.groups_xml.HandleEvent(event):
            xdata = self.groups_xml.xdata
            self.public = []
            self.private = []
            self.profiles = []
            self.groups = {}
            grouptmp = {}
            self.categories = {}
            groupseen = list()
            for group in xdata.xpath('//Groups/Group'):
                if group.get('name') not in groupseen:
                    groupseen.append(group.get('name'))
                else:
                    self.logger.error("Metadata: Group %s defined multiply" % (group.get('name')))
                grouptmp[group.get('name')] = tuple([[item.get('name') for item in group.findall(spec)]
                                                     for spec in ['./Bundle', './Group']])
                grouptmp[group.get('name')][1].append(group.get('name'))
                if group.get('default', 'false') == 'true':
                    self.default = group.get('name')
                if group.get('profile', 'false') == 'true':
                    self.profiles.append(group.get('name'))
                if group.get('public', 'false') == 'true':
                    self.public.append(group.get('name'))
                elif group.get('public', 'true') == 'false':
                    self.private.append(group.get('name'))
                if 'category' in group.attrib:
                    self.categories[group.get('name')] = group.get('category')
            for group in grouptmp:
                # self.groups[group] => (bundles, groups, categories)
                self.groups[group] = (set(), set(), {})
                tocheck = [group]
                group_cat = self.groups[group][2]
                while tocheck:
                    now = tocheck.pop()
                    self.groups[group][1].add(now)
                    if now in grouptmp:
                        (bundles, groups) = grouptmp[now]
                        for ggg in [ggg for ggg in groups if ggg not in self.groups[group][1]]:
                            if ggg not in self.categories or \
                                   self.categories[ggg] not in self.groups[group][2]:
                                self.groups[group][1].add(ggg)
                                tocheck.append(ggg)
                                if ggg in self.categories:
                                    group_cat[self.categories[ggg]] = ggg
                            elif ggg in self.categories:
                                self.logger.info("Group %s: %s cat-suppressed %s" % \
                                                 (group,
                                                  group_cat[self.categories[ggg]],
                                                  ggg))
                        [self.groups[group][0].add(bund) for bund in bundles]
            self.states['groups.xml'] = True
        if False not in list(self.states.values()):
            # check that all client groups are real and complete
            real = list(self.groups.keys())
            for client in list(self.clients.keys()):
                if self.clients[client] not in self.profiles:
                    self.logger.error("Client %s set as nonexistent or incomplete group %s" \
                                      % (client, self.clients[client]))
                    self.logger.error("Removing client mapping for %s" % (client))
                    self.bad_clients[client] = self.clients[client]
                    del self.clients[client]
            for bclient in list(self.bad_clients.keys()):
                if self.bad_clients[bclient] in self.profiles:
                    self.logger.info("Restored profile mapping for client %s" % bclient)
                    self.clients[bclient] = self.bad_clients[bclient]
                    del self.bad_clients[bclient]

    def set_profile(self, client, profile, addresspair):
        """Set group parameter for provided client."""
        self.logger.info("Asserting client %s profile to %s" % (client, profile))
        if False in list(self.states.values()):
            raise MetadataRuntimeError
        if profile not in self.public:
            self.logger.error("Failed to set client %s to private group %s" % (client, profile))
            raise MetadataConsistencyError
        if client in self.clients:
            self.logger.info("Changing %s group from %s to %s" % (client, self.clients[client], profile))
            xdict = self.clients_xml.find_xml_for_xpath('.//Client[@name="%s"]' % (client))
            if not xdict:
                self.logger.error("Metadata: Unable to update profile for client %s.  Use of Xinclude?" % client)
                raise MetadataConsistencyError
            xdict['xquery'][0].set('profile', profile)
            self.clients_xml.write_xml(xdict['filename'], xdict['xmltree'])
        else:
            self.logger.info("Creating new client: %s, profile %s" % \
                             (client, profile))
            if addresspair in self.session_cache:
                # we are working with a uuid'd client
                lxml.etree.SubElement(self.clients_xml.base_xdata.getroot(),
                                      'Client',
                                      name=self.session_cache[addresspair][1],
                                      uuid=client, profile=profile,
                                      address=addresspair[0])
            else:
                lxml.etree.SubElement(self.clients_xml.base_xdata.getroot(),
                                      'Client', name=client,
                                      profile=profile)
        self.clients[client] = profile
        self.clients_xml.write()

    def resolve_client(self, addresspair, cleanup_cache=False):
        """Lookup address locally or in DNS to get a hostname."""
        if addresspair in self.session_cache:
            # client _was_ cached, so there can be some expired entries
            # we need to clean them up to avoid potentially infinite memory swell
            cache_ttl = 90
            if cleanup_cache:
                # remove entries for this client's IP address with _any_ port numbers
                # - perhaps a priority queue could be faster?
                curtime = time.time()
                for addrpair in self.session_cache.keys():
                     if addresspair[0] == addrpair[0]:
                         (stamp, _) = self.session_cache[addrpair]
                         if curtime - stamp > cache_ttl:
                             del self.session_cache[addrpair]
            # return the cached data
            (stamp, uuid) = self.session_cache[addresspair]
            if time.time() - stamp < cache_ttl:
                return self.session_cache[addresspair][1]
        address = addresspair[0]
        if address in self.addresses:
            if len(self.addresses[address]) != 1:
                self.logger.error("Address %s has multiple reverse assignments; a uuid must be used" % (address))
                raise MetadataConsistencyError
            return self.addresses[address][0]
        try:
            cname = socket.gethostbyaddr(address)[0].lower()
            if cname in self.aliases:
                return self.aliases[cname]
            return cname
        except socket.herror:
            warning = "address resolution error for %s" % (address)
            self.logger.warning(warning)
            raise MetadataConsistencyError

    def get_initial_metadata(self, client):
        """Return the metadata for a given client."""
        if False in list(self.states.values()):
            raise MetadataRuntimeError
        client = client.lower()
        if client in self.aliases:
            client = self.aliases[client]
        if client in self.clients:
            profile = self.clients[client]
            (bundles, groups, categories) = self.groups[profile]
        else:
            if self.default == None:
                self.logger.error("Cannot set group for client %s; no default group set" % (client))
                raise MetadataConsistencyError
            self.set_profile(client, self.default, (None, None))
            profile = self.default
            [bundles, groups, categories] = self.groups[self.default]
        aliases = self.raliases.get(client, set())
        addresses = self.raddresses.get(client, set())
        newgroups = set(groups)
        newbundles = set(bundles)
        newcategories = {}
        newcategories.update(categories)
        if client in self.passwords:
            password = self.passwords[client]
        else:
            password = None
        uuids = [item for item, value in list(self.uuid.items()) if value == client]
        if uuids:
            uuid = uuids[0]
        else:
            uuid = None
        for group in self.cgroups.get(client, []):
            if group in self.groups:
                nbundles, ngroups, ncategories = self.groups[group]
            else:
                nbundles, ngroups, ncategories = ([], [group], {})
            [newbundles.add(b) for b in nbundles if b not in newbundles]
            [newgroups.add(g) for g in ngroups if g not in newgroups]
            newcategories.update(ncategories)
        return ClientMetadata(client, profile, newgroups, newbundles, aliases,
                              addresses, newcategories, uuid, password, self.query)

    def get_all_group_names(self):
        all_groups = set()
        [all_groups.update(g[1]) for g in list(self.groups.values())]
        return all_groups

    def get_all_groups_in_category(self, category):
        all_groups = set()
        [all_groups.add(g) for g in self.categories \
                if self.categories[g] == category]
        return all_groups

    def get_client_names_by_profiles(self, profiles):
        return [client for client, profile in list(self.clients.items()) \
                if profile in profiles]

    def get_client_names_by_groups(self, groups):
        mdata = [self.core.build_metadata(client)
                 for client in list(self.clients.keys())]
        return [md.hostname for md in mdata if md.groups.issuperset(groups)]

    def merge_additional_groups(self, imd, groups):
        for group in groups:
            if group in self.categories and \
                   self.categories[group] in imd.categories:
                continue
            nb, ng, _ = self.groups.get(group, (list(), [group], dict()))
            for b in nb:
                if b not in imd.bundles:
                    imd.bundles.add(b)
            for g in ng:
                if g not in imd.groups:
                    if g in self.categories and \
                       self.categories[g] in imd.categories:
                        continue
                    if g in self.private:
                        self.logger.error("Refusing to add dynamic membership in private group %s for client %s" % (g, imd.hostname))
                        continue
                    imd.groups.add(g)
                    if g in self.categories:
                        imd.categories[self.categories[g]] = g

    def merge_additional_data(self, imd, source, data):
        if not hasattr(imd, source):
            setattr(imd, source, data)
            imd.connectors.append(source)

    def validate_client_address(self, client, addresspair):
        """Check address against client."""
        address = addresspair[0]
        if client in self.floating:
            self.debug_log("Client %s is floating" % client)
            return True
        if address in self.addresses:
            if client in self.addresses[address]:
                self.debug_log("Client %s matches address %s" % (client, address))
                return True
            else:
                self.logger.error("Got request for non-float client %s from %s" \
                                  % (client, address))
                return False
        resolved = self.resolve_client(addresspair)
        if resolved.lower() == client.lower():
            return True
        else:
            self.logger.error("Got request for %s from incorrect address %s" \
                              % (client, address))
            self.logger.error("Resolved to %s" % resolved)
            return False

    def AuthenticateConnection(self, cert, user, password, address):
        """This function checks auth creds."""
        if cert:
            id_method = 'cert'
            certinfo = dict([x[0] for x in cert['subject']])
            # look at cert.cN
            client = certinfo['commonName']
            self.debug_log("Got cN %s; using as client name" % client)
            auth_type = self.auth.get(client, 'cert+password')
        elif user == 'root':
            id_method = 'address'
            try:
                client = self.resolve_client(address)
            except MetadataConsistencyError:
                self.logger.error("Client %s failed to resolve; metadata problem" % (address[0]))
                return False
        else:
            id_method = 'uuid'
            # user maps to client
            if user not in self.uuid:
                client = user
                self.uuid[user] = user
            else:
                client = self.uuid[user]

        # we have the client name
        self.debug_log("Authenticating client %s" % client)

        # next we validate the address
        if id_method == 'uuid':
            addr_is_valid = True
        else:
            addr_is_valid = self.validate_client_address(client, address)

        if not addr_is_valid:
            return False

        if id_method == 'cert' and auth_type != 'cert+password':
            # remember the cert-derived client name for this connection
            if client in self.floating:
                self.session_cache[address] = (time.time(), client)
            # we are done if cert+password not required
            return True

        if client not in self.passwords:
            if client in self.secure:
                self.logger.error("Client %s in secure mode but has no password" % (address[0]))
                return False
            if password != self.password:
                self.logger.error("Client %s used incorrect global password" % (address[0]))
                return False
        if client not in self.secure:
            if client in self.passwords:
                plist = [self.password, self.passwords[client]]
            else:
                plist = [self.password]
            if password not in plist:
                self.logger.error("Client %s failed to use either allowed password" % \
                                  (address[0]))
                return False
        else:
            # client in secure mode and has a client password
            if password != self.passwords[client]:
                self.logger.error("Client %s failed to use client password in secure mode" % \
                                  (address[0]))
                return False
        # populate the session cache
        if user.decode('utf-8') != 'root':
            self.session_cache[address] = (time.time(), client)
        return True

    def process_statistics(self, meta, _):
        """Hook into statistics interface to toggle clients in bootstrap mode."""
        client = meta.hostname
        if client in self.auth and self.auth[client] == 'bootstrap':
            self.logger.info("Asserting client %s auth mode to cert" % client)
            xdict = self.clients_xml.find_xml_for_xpath('.//Client[@name="%s"]' % (client))
            if not xdict:
                self.logger.error("Metadata: Unable to update profile for client %s.  Use of Xinclude?" % client)
                raise MetadataConsistencyError
            xdict['xquery'][0].set('auth', 'cert')
            self.clients_xml.write_xml(xdict['filename'], xdict['xmltree'])

    def viz(self, hosts, bundles, key, only_client, colors):
        """Admin mode viz support."""
        if only_client:
            clientmeta = self.core.build_metadata(only_client)

        def include_client(client):
            return not only_client or client != only_client

        def include_bundle(bundle):
            return not only_client or bundle in clientmeta.bundles

        def include_group(group):
            return not only_client or group in clientmeta.groups

        groups_tree = lxml.etree.parse(self.data + "/groups.xml",
                                       parser=Bcfg2.Server.XMLParser)
        try:
            groups_tree.xinclude()
        except lxml.etree.XIncludeError:
            self.logger.error("Failed to process XInclude for file %s: %s" %
                              (dest, sys.exc_info()[1]))
        groups = groups_tree.getroot()
        categories = {'default': 'grey83'}
        viz_str = ""
        egroups = groups.findall("Group") + groups.findall('.//Groups/Group')
        for group in egroups:
            if not group.get('category') in categories:
                categories[group.get('category')] = colors.pop()
            group.set('color', categories[group.get('category')])
        if None in categories:
            del categories[None]
        if hosts:
            instances = {}
            clients = self.clients
            for client, profile in list(clients.items()):
                if include_client(client):
                    continue
                if profile in instances:
                    instances[profile].append(client)
                else:
                    instances[profile] = [client]
            for profile, clist in list(instances.items()):
                clist.sort()
                viz_str += '''\t"%s-instances" [ label="%s", shape="record" ];\n''' \
                    % (profile, '|'.join(clist))
                viz_str += '''\t"%s-instances" -> "group-%s";\n''' \
                    % (profile, profile)
        if bundles:
            bundles = []
            [bundles.append(bund.get('name')) \
                 for bund in groups.findall('.//Bundle') \
                 if bund.get('name') not in bundles \
                     and include_bundle(bund.get('name'))]
            bundles.sort()
            for bundle in bundles:
                viz_str += '''\t"bundle-%s" [ label="%s", shape="septagon"];\n''' \
                    % (bundle, bundle)
        gseen = []
        for group in egroups:
            if group.get('profile', 'false') == 'true':
                style = "filled, bold"
            else:
                style = "filled"
            gseen.append(group.get('name'))
            if include_group(group.get('name')):
                viz_str += '\t"group-%s" [label="%s", style="%s", fillcolor=%s];\n' % \
                           (group.get('name'), group.get('name'), style, group.get('color'))
                if bundles:
                    for bundle in group.findall('Bundle'):
                        viz_str += '\t"group-%s" -> "bundle-%s";\n' % \
                                   (group.get('name'), bundle.get('name'))
        gfmt = '\t"group-%s" [label="%s", style="filled", fillcolor="grey83"];\n'
        for group in egroups:
            for parent in group.findall('Group'):
                if parent.get('name') not in gseen and include_group(parent.get('name')):
                    viz_str += gfmt % (parent.get('name'), parent.get('name'))
                    gseen.append(parent.get("name"))
                if include_group(group.get('name')):
                    viz_str += '\t"group-%s" -> "group-%s" ;\n' % \
                               (group.get('name'), parent.get('name'))
        if key:
            for category in categories:
                viz_str += '''\t"''' + category + '''" [label="''' + category + \
                    '''", shape="record", style="filled", fillcolor=''' + \
                    categories[category] + '''];\n'''
        return viz_str
