# -*- coding: utf-8 -*-
# Copyright (C) 2014 the2nd <the2nd@otpme.org>
import os

try:
    if os.environ['OTPME_DEBUG_MODULE_LOADING'] == "True":
        msg = _("Loading module: {module}")
        msg = msg.format(module=__name__)
        print(msg)
except:
    pass

from otpme.lib import oid
from otpme.lib import config
from otpme.lib import locking
from otpme.lib import filetools
from otpme.lib import nsswitch
from otpme.lib.pidfile import pidfile_handler

from otpme.lib.exceptions import *

LOCK_TYPE = "nsscache"
UPDATE_EXT = "update"
REMOVE_EXT = "remove"

locking.register_lock_type(LOCK_TYPE, module=__file__)

valid_actions = {
                'update' : UPDATE_EXT,
                'remove' : REMOVE_EXT,
                }

# nsscache files we link to /etc/ on realm join.
NSSCACHE_FILES = [
            'group.cache',
            'group.cache.ixgid',
            'group.cache.ixname',
            'passwd.cache',
            'passwd.cache.ixname',
            'passwd.cache.ixuid',
            'shadow.cache',
            'shadow.cache.ixname',
            'shadow.cache.ixuid',
            ]

# Object types that will be added to nsscache.
NSSCACHE_ADD_ORDER = [ 'user', 'group' ]
NSSCACHE_OBJECT_TYPES = {
                        'user' : {
                                'realm_uniq' : True,
                                },
                        'group' : {
                                'realm_uniq' : True,
                                },
                        'role' : {
                                'realm_uniq' : False,
                                },
                        }

def get_last_synced_revision():
    if not os.path.exists(config.nsscache_sync_file):
        return
    # Get logger.
    logger = config.logger
    try:
        last_synced_revision = filetools.read_file(config.nsscache_sync_file)
    except Exception as e:
        log_msg = _("Failed to read last synced revision: {e}", log=True)[1]
        log_msg = log_msg.format(e=e)
        logger.warning(log_msg)
        return
    try:
        last_synced_revision = float(last_synced_revision)
    except Exception as e:
        log_msg = _("Invalid revision format, should be float: {e}", log=True)[1]
        log_msg = log_msg.format(e=e)
        logger.warning(log_msg)
        return
    return last_synced_revision

def set_last_synced_revision(last_synced_revision):
    # Get logger.
    logger = config.logger
    try:
        filetools.create_file(path=config.nsscache_sync_file,
                            content=str(last_synced_revision))
    except Exception as e:
        log_msg = _("Failed to read last synced revision: {e}", log=True)[1]
        log_msg = log_msg.format(e=e)
        logger.warning(log_msg)
        return
    return last_synced_revision

def gen_cachefile_path(object_id, action):
    """ Generate cache file path. """
    if action not in valid_actions:
        msg = _("Invalid action: {action}")
        msg = msg.format(action=action)
        raise OTPmeException(msg)
    path = f"{config.nsscache_objects_dir}/{object_id.full_oid.replace('/', '+')}.{action}"
    return path

def update_sync_map(lock=None, syncing=False):
    """
    Update checksum in sync map to indicate that local nsscache is up-to-date.
    """
    from otpme.lib import backend
    from otpme.lib.protocols.server.sync1 import add_sync_list_checksum
    # Get logger.
    logger = config.logger

    if lock is None:
        lock = locking.OTPmeFakeLock(lock_type=LOCK_TYPE, lock_id="fake")

    all_sites = backend.search(object_type="site",
                                attribute="uuid",
                                value="*",
                                realm=config.realm,
                                return_type="instance")
    # Get users to skip.
    skip_users = list(config.get_internal_objects("user"))
    # Get object types to sync.
    try:
        host_type = config.host_data['type']
    except:
        return
    object_types = config.get_sync_object_types(host_type)

    for x in all_sites:
        # Skip disabled sites.
        if not x.enabled:
            continue
        # Set "syncing" status.
        if syncing:
            try:
                response = add_sync_list_checksum(realm=x.realm,
                                            site=x.name,
                                            peer_realm=config.realm,
                                            peer_site=config.site,
                                            skip_admin=False,
                                            skip_users=skip_users,
                                            object_types=object_types,
                                            checksum=config.SYNCING_STATUS_STRING)
                exception = None
            except OTPmeException as e:
                exception = str(e)
            if exception:
                log_msg = exception
                logger.warning(log_msg)
            else:
                log_msg = response
                logger.debug(log_msg)
            return

        # Get sync list checksum.
        sync_list, \
        sync_list_checksum = backend.get_sync_list(realm=x.realm,
                                                    site=x.name,
                                                    skip_admin=False,
                                                    skip_users=skip_users,
                                                    object_types=object_types)
        # Add sync list checksum to sync map.
        try:
            response = add_sync_list_checksum(realm=x.realm,
                                        site=x.name,
                                        peer_realm=config.realm,
                                        peer_site=config.site,
                                        skip_admin=False,
                                        skip_users=skip_users,
                                        object_types=object_types,
                                        checksum=sync_list_checksum)
            exception = None
        except OTPmeException as e:
            exception = str(e)
        if exception:
            log_msg = exception
            logger.warning(log_msg)
        else:
            log_msg = response
            logger.debug(log_msg)

def update_object(object_id, action):
    """ Add object ID to nsscache spool directory. """
    # Get logger.
    logger = config.logger
    if action not in valid_actions:
        msg = _("Invalid action: {action}")
        msg = msg.format(action=action)
        raise OTPmeException(msg)
    # Skip internal users.
    if object_id.object_type == "user":
        internal_users = config.get_internal_objects("user")
        if object_id.name in internal_users:
            return
    # Get file paths.
    update_file = gen_cachefile_path(object_id, UPDATE_EXT)
    remove_file = gen_cachefile_path(object_id, REMOVE_EXT)
    if action == "update":
        cache_file = update_file
        obsolete_file = remove_file
    elif action == "remove":
        cache_file = remove_file
        obsolete_file = update_file

    if os.path.exists(obsolete_file):
        log_msg = _("Removing obsolete spool file: {obsolete_file}", log=True)[1]
        log_msg = log_msg.format(obsolete_file=obsolete_file)
        logger.debug(log_msg)
        try:
            filetools.delete(obsolete_file)
        except FileNotFoundError:
            pass
        except Exception as e:
            log_msg = _("Failed to remove nsscache file: {obsolete_file}: {e}", log=True)[1]
            log_msg = log_msg.format(obsolete_file=obsolete_file, e=e)
            logger.critical(log_msg)
    # Write file.
    filetools.touch(path=cache_file,
                    user=config.user,
                    group=config.group,
                    mode=0o660)

@pidfile_handler(config.nsscache_pidfile, proc_name="nsscache")
def update(realm, site, resync=False, cache_resync=False, lock=None):
    """ Update nsscache cache files. """
    from otpme.lib import backend
    from otpme.lib.third_party.nss_cache.maps import group
    from otpme.lib.third_party.nss_cache.maps import passwd
    from otpme.lib.third_party.nss_cache.maps import shadow
    from otpme.lib.third_party.nss_cache.caches import files
    from otpme.lib.daemon.clusterd import cluster_nsscache_sync
    # Get logger.
    logger = config.logger

    log_msg = _("Starting sync of nsscache: {realm}/{site}", log=True)[1]
    log_msg = log_msg.format(realm=realm, site=site)
    logger.info(log_msg)

    if lock is None:
        lock = locking.OTPmeFakeLock(lock_type=LOCK_TYPE, lock_id="fake")

    # Get current data revision.
    data_revision = config.get_data_revision()

    # Set "syncing" status.
    update_sync_map(lock=lock, syncing=True)

    nsscache_config = {'dir': config.nsscache_dir}
    user_cache = files.FilesPasswdMapHandler(nsscache_config)
    shadow_cache = files.FilesShadowMapHandler(nsscache_config)
    group_cache = files.FilesGroupMapHandler(nsscache_config)
    nss_cache_files = filetools.list_dir(config.nsscache_objects_dir)

    # Object types we support.
    object_types = NSSCACHE_ADD_ORDER

    # Nsscache caches we need.
    nsscache_caches = {
                            'user'      : user_cache,
                            'group'     : group_cache,
                            'shadow'    : shadow_cache,
                    }

    # Nsscache methods used to create new maps.
    nsscache_map_methods = {
                            'user'      : passwd.PasswdMap,
                            'shadow'    : shadow.ShadowMap,
                            'group'     : group.GroupMap,
                        }

    # Will hold current nsscache map entry objects (used to do incremental
    # updates)
    nsscache_current_entries = {
                            'user'  : {},
                            'group' : {},
                    }

    # Map entries for new/updated objects we got from last OTPme sync process.
    nsscache_update_entries = {
                            'user'  : {},
                            'group' : {},
                        }

    # OTPme object names of deleted objects we have to remove from nsscache.
    nsscache_remove_names = {
                            'user'  : [],
                            'group' : [],
                        }

    # Will hold the new/merged entries we will write to nsscache.
    nsscache_new_entries = {
                            'user'  : [],
                            'group' : [],
                        }

    updated_objects = {}
    removed_objects = {}
    files_to_remove = {}

    update_members = True
    if config.host_type == "host":
        update_members = False
    else:
        if not config.use_api:
            if not config.master_node:
                update_members = False
            if config.master_failover:
                update_members = False
            if not config.cluster_status:
                update_members = False

    if update_members:
        # Get roles/groups to update members of.
        counter = 0
        update_roles = []
        update_groups = []
        update_objects = len(nss_cache_files)
        for f in nss_cache_files:
            file_path = os.path.join(config.nsscache_objects_dir, f)
            x_oid = ".".join(f.split(".")[:-1]).replace("+", "/")
            x_oid = oid.get(x_oid)
            if x_oid.realm != realm:
                continue
            if x_oid.site != site:
                continue

            counter += 1
            log_msg = _("Reading nsscache ({counter}/{update_objects}): {x_oid}", log=True)[1]
            log_msg = log_msg.format(counter=counter, update_objects=update_objects, x_oid=x_oid)
            logger.debug(log_msg)

            x_object = backend.get_object(x_oid)
            if not x_object:
                continue

            # FIXME: do we need this lines?
            #if x_object.type == "user":
            #    update_roles += x_object.get_roles(return_type="instance")
            #    update_groups += x_object.get_groups(return_type="instance")

            if x_object.type == "role":
                if x_object.site != config.site:
                    continue
                update_roles.append(x_object)
                try:
                    filetools.delete(file_path)
                except Exception as e:
                    log_msg = _("Failed to remove nsscache file: {file_path}: {e}", log=True)[1]
                    log_msg = log_msg.format(file_path=file_path, e=e)
                    logger.critical(log_msg)

            if x_object.type == "group":
                if x_object.site != config.site:
                    continue
                update_groups.append(x_object)

        # Update group members from role members.
        updated_groups = []
        if config.host_type == "node":
            for role in set(sorted(update_roles)):
                if role.site != config.site:
                    continue
                log_msg = _("Updating group members from role: {role_oid}", log=True)[1]
                log_msg = log_msg.format(role_oid=role.oid)
                logger.info(log_msg)
                callback = config.get_callback()
                try:
                    updated_groups += role.update_extensions("update_members",
                                                            callback=callback)[1]
                except Exception as e:
                    log_msg = _("Failed to update role members: {role_oid}: {e}", log=True)[1]
                    log_msg = log_msg.format(role_oid=role.oid, e=e)
                    logger.critical(log_msg)
                finally:
                    update_objects = False
                    if config.master_node:
                        if config.cluster_status:
                            if not config.master_failover:
                                update_objects = True
                    if config.use_api:
                        update_objects = True
                    if update_objects:
                        callback.write_modified_objects()
                        callback.release_cache_locks()

            # Update group members (but not those processed by role updates above).
            for _group in set(sorted(update_groups)):
                if _group.site != config.site:
                    continue
                if _group.oid in updated_groups:
                    continue
                callback = config.get_callback()
                try:
                    _group.update_extensions("update_members",
                                            callback=callback)
                except Exception as e:
                    log_msg = _("Failed to update group members: {group_oid}: {e}", log=True)[1]
                    log_msg = log_msg.format(group_oid=_group.oid, e=e)
                    logger.critical(log_msg)
                finally:
                    update_objects = False
                    if config.master_node:
                        if config.cluster_status:
                            if not config.master_failover:
                                update_objects = True
                    if config.use_api:
                        update_objects = True
                    if update_objects:
                        callback.write_modified_objects()
                        callback.release_cache_locks()
                        updated_groups.append(_group.oid)

            for x in updated_groups:
                update_file = gen_cachefile_path(x, UPDATE_EXT)
                update_file = os.path.basename(update_file)
                nss_cache_files.append(update_file)

    if cache_resync:
        for object_type in object_types:
            # If object type has a realm wide namespace we have to search
            # objects of all sites.
            if NSSCACHE_OBJECT_TYPES[object_type]['realm_uniq']:
                site = None
            else:
                site = config.site

            search_attrs = {
                            'uuid'      : {
                                        'value'     : '*',
                                        },
                            'template'  : {
                                        'value'     : False,
                                        },
                            }
            result = backend.search(realm=config.realm,
                                    site=site,
                                    object_type=object_type,
                                    attributes=search_attrs,
                                    return_type="full_oid")
            if result:
                try:
                    x_updated_objects = updated_objects[object_type]
                except:
                    x_updated_objects = []
                x_updated_objects += result
                updated_objects[object_type] = x_updated_objects
    else:
        # No spool dir, no updates ;)
        if not os.path.exists(config.nsscache_objects_dir):
            update_sync_map(lock=lock)
            log_msg = _("No nsscache updates found.", log=True)[1]
            logger.info(log_msg)
            # Mark current revision as synced.
            if realm == config.realm:
                if site == config.site:
                    set_last_synced_revision(data_revision)
            return None

        for f in set(nss_cache_files):
            action = f.split(".")[-1]
            if action not in valid_actions:
                continue
            full_oid = ".".join(f.split(".")[:-1]).replace("+", "/")
            object_id = oid.get(full_oid)
            object_type = object_id.object_type
            if object_id.realm != realm:
                continue
            if object_id.site != site:
                continue

            file_path = os.path.join(config.nsscache_objects_dir, f)
            try:
                files_to_remove[file_path] = os.path.getmtime(file_path)
            except FileNotFoundError:
                continue

            if object_type == "role":
                try:
                    filetools.delete(file_path)
                except Exception as e:
                    log_msg = _("Failed to remove nsscache file: {file_path}: {e}", log=True)[1]
                    log_msg = log_msg.format(file_path=file_path, e=e)
                    logger.warning(log_msg)
                continue

            if action == "update":
                try:
                    x_updated_objects = updated_objects[object_type]
                except:
                    x_updated_objects = []
                x_updated_objects.append(full_oid)
                updated_objects[object_type] = x_updated_objects

            if action == "remove":
                try:
                    x_removed_objects = removed_objects[object_type]
                except:
                    x_removed_objects = []
                x_removed_objects.append(object_id)
                removed_objects[object_type] = x_removed_objects

    if len(updated_objects) + len(removed_objects) == 0:
        update_sync_map(lock=lock)
        log_msg = _("No nsscache updates found.", log=True)[1]
        logger.info(log_msg)
        # Mark current revision as synced.
        if realm == config.realm:
            if site == config.site:
                set_last_synced_revision(data_revision)
        return None

    object_count = 0
    object_attributes = {}
    for object_type in updated_objects:
        object_oids = updated_objects[object_type]
        if object_type == "user":
            return_attrs = [
                            'name',
                            'full_oid',
                            'extension',
                            'ldif:cn',
                            'ldif:uidNumber',
                            'ldif:gidNumber',
                            'ldif:loginShell',
                            'ldif:homeDirectory',
                            ]
        if object_type == "group":
            return_attrs = [
                            'name',
                            'full_oid',
                            'extension',
                            'ldif:gidNumber',
                            'ldif:memberUid',
                            ]
        # Get objects.
        result = backend.search(object_type=object_type,
                                attribute="full_oid",
                                values=object_oids,
                                return_attributes=return_attrs)
        object_attributes[object_type] = result
        object_count += len(result)

    counter = 0
    for object_type in object_attributes:
        object_attrs = object_attributes[object_type]
        # Create map entries for new/updated objects.
        for uuid in object_attrs:
            counter += 1
            try:
                extensions = object_attrs[uuid]['extension']
            except:
                extensions = []
            if "posix" not in extensions:
                continue
            # Get OID and type.
            full_oid = object_attrs[uuid]['full_oid']
            object_id = oid.get(full_oid)
            object_type = object_id.object_type
            # Skip unsupported objects.
            if not object_type in object_types:
                continue
            # Skip objects from other sites which do not have a realm wide uniq
            # name.
            if not NSSCACHE_OBJECT_TYPES[object_type]['realm_uniq']:
                object_site = object_id.site
                if object_site != config.site:
                    continue
            # Get object name.
            object_name = object_attrs[uuid]['name']
            log_msg = _("Processing nsscache ({counter}/{object_count}): {object_id}", log=True)[1]
            log_msg = log_msg.format(counter=counter, object_count=object_count, object_id=object_id)
            logger.debug(log_msg)
            shadow_map_entry = None
            if object_type == "user":
                # Skip users that already exist in another NSS source.
                nss_source = nsswitch.user_exists(object_name, skip_sources=['cache'])
                if nss_source:
                    log_msg = ("Skipping nsscache export, user already "
                               "exists in NSS source '%s': %s"
                               % (nss_source, object_name))
                    logger.warning(log_msg)
                    continue
                try:
                    cn = object_attrs[uuid]['ldif:cn'][0]
                except:
                    cn = ''
                try:
                    uidnumber = object_attrs[uuid]['ldif:uidNumber'][0]
                except:
                    log_msg = _("Cannot create nsscache map: Object is missing uidNumber: {full_oid}", log=True)[1]
                    log_msg = log_msg.format(full_oid=full_oid)
                    logger.warning(log_msg)
                    continue
                try:
                    gidnumber = object_attrs[uuid]['ldif:gidNumber'][0]
                except:
                    log_msg = _("Cannot create nsscache map: Object is missing gidNumber: {full_oid}", log=True)[1]
                    log_msg = log_msg.format(full_oid=full_oid)
                    logger.warning(log_msg)
                    continue
                try:
                    homedir = object_attrs[uuid]['ldif:homeDirectory'][0]
                except:
                    log_msg = _("Cannot create nsscache map: Object is missing homeDirectory: {full_oid}", log=True)[1]
                    log_msg = log_msg.format(full_oid=full_oid)
                    logger.warning(log_msg)
                    continue
                try:
                    loginshell = object_attrs[uuid]['ldif:loginShell'][0]
                except:
                    log_msg = _("Cannot create nsscache map: Object is missing loginShell: {full_oid}", log=True)[1]
                    log_msg = log_msg.format(full_oid=full_oid)
                    logger.warning(log_msg)
                    continue
                # Gen passwd entry.
                map_entry = passwd.PasswdMapEntry()
                map_entry.name = object_name
                map_entry.passwd = 'x'
                map_entry.uid = uidnumber
                map_entry.gid = gidnumber
                map_entry.gecos = cn
                map_entry.dir = homedir
                map_entry.shell = loginshell
                # Gen shadow entry.
                shadow_map_entry = shadow.ShadowMapEntry()
                shadow_map_entry.name = object_name
                shadow_map_entry.passwd = "*"
                #shadow_map_entry.lstchg = int(nss_entry[2])
                #shadow_map_entry.min = int(nss_entry[3])
                #shadow_map_entry.max = int(nss_entry[4])
                #shadow_map_entry.warn = int(nss_entry[5])
                #shadow_map_entry.inact = int(nss_entry[6])
                #shadow_map_entry.expire = int(nss_entry[7])
                #shadow_map_entry.flag = int(nss_entry[8])

            if object_type == "group":
                # Skip groups that already exist in another NSS source.
                nss_source = nsswitch.group_exists(object_name, skip_sources=['cache'])
                if nss_source:
                    log_msg = ("Skipping nsscache export, group already "
                               "exists in NSS source '%s': %s"
                               % (nss_source, object_name))
                    logger.warning(log_msg)
                    continue
                try:
                    gidnumber = object_attrs[uuid]['ldif:gidNumber'][0]
                except Exception as e:
                    log_msg = _("Cannot create nsscache map: Object is missing gidNumber: {full_oid}", log=True)[1]
                    log_msg = log_msg.format(full_oid=full_oid)
                    logger.warning(log_msg)
                    continue
                try:
                    group_members = object_attrs[uuid]['ldif:memberUid']
                except:
                    group_members = None
                # Gen group entry.
                map_entry = group.GroupMapEntry()
                map_entry.name = object_name
                map_entry.passwd = 'x'
                map_entry.gid = gidnumber
                if group_members is not None:
                    # Make sure root is not in any OTPme group.
                    try:
                        group_members.remove(config.admin_user_name)
                    except ValueError:
                        pass
                    map_entry.members = group_members
            nsscache_update_entries[object_type][map_entry.name] = (map_entry, shadow_map_entry)

    # Counters for log message.
    nsscache_adds = 0
    nsscache_updates = 0
    nsscache_removes = 0

    if resync or cache_resync:
        for object_type in object_types:
            for name in sorted(nsscache_update_entries[object_type]):
                log_msg = _("Adding {object_type} to nsscache: {name}", log=True)[1]
                log_msg = log_msg.format(object_type=object_type, name=name)
                logger.debug(log_msg)
                entry = nsscache_update_entries[object_type][name]
                nsscache_new_entries[object_type].append(entry)
                nsscache_adds += 1
    else:
        # Get object names of removed objects.
        del_order = reversed(config.object_add_order)
        for object_type in del_order:
            try:
                oids = removed_objects[object_type]
            except:
                continue
            for object_id in oids:
                object_type = object_id.object_type
                # Skip objects from other sites which do not have a realm wide uniq
                # name.
                if not NSSCACHE_OBJECT_TYPES[object_type]['realm_uniq']:
                    object_site = object_id.site
                    if object_site != config.site:
                        continue
                object_name = object_id.name
                nsscache_remove_names[object_type].append(object_name)

        # Get current map entries.
        for object_type in object_types:
            current_map = nsscache_caches[object_type].GetMap()
            for entry in current_map:
                nsscache_current_entries[object_type][entry.name] = (entry,)

        for object_type in object_types:
            update_needed = False
            # Handle removed entries.
            for name in sorted(nsscache_remove_names[object_type]):
                if name not in nsscache_current_entries[object_type]:
                    continue
                update_needed = True
                log_msg = _("Removing {object_type} from nsscache: {name}", log=True)[1]
                log_msg = log_msg.format(object_type=object_type, name=name)
                logger.debug(log_msg)
                nsscache_removes += 1
                nsscache_current_entries[object_type].pop(name)

            # Remove non existing users from updated groups.
            if config.host_type == "host":
                if object_type == "group":
                    for group_name in nsscache_update_entries[object_type]:
                        entry = nsscache_update_entries[object_type][group_name][0]
                        local_users = backend.search(object_type="user",
                                                    attribute="uuid",
                                                    value="*",
                                                    return_type="name")
                        for x_uid in list(entry.members):
                            if x_uid in local_users:
                                continue
                            entry.members.remove(x_uid)

            # Handle new/updated entries.
            for name in sorted(nsscache_update_entries[object_type]):
                x_entries = nsscache_update_entries[object_type][name]
                x_entry = x_entries[0]
                try:
                    current_entry = nsscache_current_entries[object_type][x_entry.name][0]
                except KeyError:
                    current_entry = None
                if current_entry == x_entry:
                    log_msg = _("No update required for {object_type}: {name}", log=True)[1]
                    log_msg = log_msg.format(object_type=object_type, name=name)
                    logger.debug(log_msg)
                    continue
                update_needed = True
                if name in nsscache_current_entries[object_type]:
                    log_msg = _("Updating {object_type} in nsscache: {name}", log=True)[1]
                    log_msg = log_msg.format(object_type=object_type, name=name)
                    logger.debug(log_msg)
                    nsscache_updates += 1
                else:
                    log_msg = _("Adding {object_type} to nsscache: {name}", log=True)[1]
                    log_msg = log_msg.format(object_type=object_type, name=name)
                    logger.debug(log_msg)
                    nsscache_adds += 1
                nsscache_current_entries[object_type][x_entry.name] = x_entries

            if update_needed:
                # We need merged entries in nsscache_new_entries.
                for name in nsscache_current_entries[object_type]:
                    entry = nsscache_current_entries[object_type][name][0]
                    nsscache_new_entries[object_type].append((entry,))

    # Actually create new/merged maps and write them to nsscache.
    for object_type in object_types:
        entries = nsscache_new_entries[object_type]
        cache = nsscache_caches[object_type]
        # Remove file if no objects exist for this cache.
        if len(entries) == 0:
            if resync or cache_resync:
                cache_file = cache.GetCacheFilename()
                if os.path.exists(cache_file):
                    try:
                        filetools.delete(cache_file)
                    except Exception as e:
                        log_msg = _("Failed to remove nsscache file: {cache_file}: {e}", log=True)[1]
                        log_msg = log_msg.format(cache_file=cache_file, e=e)
                        logger.critical(log_msg)
            continue
        map_entries = []
        shadow_entries = []
        for x in entries:
            map_entry = x[0]
            try:
                shadow_entry = x[1]
            except IndexError:
                shadow_entry = None
            map_entries.append(map_entry)
            if shadow_entry:
                shadow_entries.append(shadow_entry)
        map_method = nsscache_map_methods[object_type]
        new_map = map_method(map_entries)
        log_msg = _("Writing nsscache: {object_type}", log=True)[1]
        log_msg = log_msg.format(object_type=object_type)
        logger.debug(log_msg)
        cache.Write(new_map)
        cache.WriteIndex()
        cache._Commit()
        if shadow_entries:
            map_method = nsscache_map_methods['shadow']
            new_map = map_method(shadow_entries)
            shadow_cache = nsscache_caches['shadow']
            log_msg = _("Writing nsscache (shadow): {object_type}", log=True)[1]
            log_msg = log_msg.format(object_type=object_type)
            logger.debug(log_msg)
            shadow_cache.Write(new_map)
            shadow_cache.WriteIndex()
            shadow_cache._Commit()

    # Set cache file ownership.
    for x in os.listdir(config.nsscache_dir):
        cache_file = os.path.join(config.nsscache_dir, x)
        filetools.set_fs_ownership(path=cache_file,
                                user=config.user,
                                group=config.group)

    if nsscache_adds > 0 or nsscache_updates > 0 or nsscache_removes > 0:
        cluster_nsscache = True
        log_msg = _("Updated nsscache: adds: {nsscache_adds} updates: {nsscache_updates} removes: {nsscache_removes}", log=True)[1]
        log_msg = log_msg.format(nsscache_adds=nsscache_adds, nsscache_updates=nsscache_updates, nsscache_removes=nsscache_removes)
        logger.info(log_msg)
    else:
        cluster_nsscache = False
        log_msg = _("No nsscache update needed.", log=True)[1]
        logger.info(log_msg)

    # Inform hostd that we are in sync again.
    update_sync_map(lock=lock)

    # Remove cache files.
    for file_path in files_to_remove:
        if not os.path.exists(file_path):
            continue
        old_mtime = files_to_remove[file_path]
        try:
            new_mtime = os.path.getmtime(file_path)
        except FileNotFoundError:
            continue
        if new_mtime != old_mtime:
            continue
        try:
            filetools.delete(file_path)
        except Exception as e:
            log_msg = _("Failed to remove nsscache file: {file_path}: {e}", log=True)[1]
            log_msg = log_msg.format(file_path=file_path, e=e)
            logger.critical(log_msg)

    if config.host_data['type'] == "node":
        if not config.master_node:
            cluster_nsscache = False
        if cluster_nsscache:
            cluster_nsscache_sync()

    # Mark current revision as synced.
    if realm == config.realm:
        if site == config.site:
            set_last_synced_revision(data_revision)

    return True

def enable():
    """ Create OTPme nsscache symlinks. """
    # Get logger.
    logger = config.logger
    nsscache_links = {}
    for x in NSSCACHE_FILES:
        link_src = f"{config.nsscache_dir}/{x}"
        link_dst = f"/etc/{x}"
        if os.path.exists(link_dst) \
        or os.path.islink(link_dst):
            curr_src = os.path.realpath(link_dst)
            if curr_src != link_src:
                msg = _("Cannot enable OTPme nsscache configuration: File exists: {link_dst}")
                msg = msg.format(link_dst=link_dst)
                raise OTPmeException(msg)
        else:
            nsscache_links[link_src] = link_dst

    for link_src in nsscache_links:
        link_dst = nsscache_links[link_src]
        log_msg = _("Creating symlink: {link_src} -> {link_dst}", log=True)[1]
        log_msg = log_msg.format(link_src=link_src, link_dst=link_dst)
        logger.debug(log_msg)
        os.symlink(link_src, link_dst)

def disable():
    """ Remove OTPme nsscache symlinks. """
    # Get logger.
    logger = config.logger
    for x in NSSCACHE_FILES:
        link_src = f"{config.nsscache_dir}/{x}"
        link_dst = f"/etc/{x}"
        if not os.path.islink(link_dst):
            continue
        curr_src = os.path.realpath(link_dst)
        if curr_src != link_src:
            continue
        log_msg = _("Removing symlink: {link_src} -> {link_dst}", log=True)[1]
        log_msg = log_msg.format(link_src=link_src, link_dst=link_dst)
        logger.debug(log_msg)
        os.remove(link_dst)
