#! /usr/bin/python
#
# Copyright 2011-2014 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Refer to the README and COPYING files for full details of the license
#
import argparse
import logging
import logging.config
import os

from vdsm.config import config
from vdsm import netinfo
from vdsm.constants import P_VDSM_RUN
from vdsm.netconfpersistence import KernelConfig, BaseConfig

# Ifcfg persistence restoration
from network.configurators import ifcfg

# Unified persistence restoration
from network.api import setupNetworks
from network import configurators
from vdsm.netconfpersistence import RunningConfig, PersistentConfig
import pkgutil

_NETS_RESTORED_MARK = os.path.join(P_VDSM_RUN, 'nets_restored')


def ifcfg_restoration():
    configWriter = ifcfg.ConfigWriter()
    configWriter.restorePersistentBackup()


def unified_restoration():
    """
    Builds a setupNetworks command from the persistent configuration to set it
    as running configuration.
    """
    runningConfig = RunningConfig()
    removeNetworks = {}
    removeBonds = {}
    for network in runningConfig.networks:
        removeNetworks[network] = {'remove': True}
    for bond in runningConfig.bonds:
        removeBonds[bond] = {'remove': True}
    logging.debug('Removing all networks (%s) and bonds (%s) in running '
                  'config.', removeNetworks, removeBonds)
    setupNetworks(removeNetworks, removeBonds, connectivityCheck=False,
                  _inRollback=True)

    # Restore non-VDSM network devices (BZ#1188251)
    configWriter = ifcfg.ConfigWriter()
    configWriter.restorePersistentBackup()

    persistent_config = PersistentConfig()
    available_config = _filter_available(persistent_config)
    changed_config = _filter_changed_nets_bonds(available_config)
    nets = changed_config.networks
    bonds = changed_config.bonds
    _convert_to_blocking_dhcp(nets)
    logging.debug('Calling setupNetworks with networks (%s) and bond (%s).',
                  nets, bonds)
    setupNetworks(nets, bonds, connectivityCheck=False, _inRollback=True)


def _convert_to_blocking_dhcp(networks):
    """
    This function changes DHCP configuration, if present, to be blocking.

    This is done right before restoring the network configuration, and forces
    the configurator to wait for an IP address to be configured on the devices
    before restoration is completed. This prevents VDSM to possibly report
    missing IP address on interfaces that had been restored right before it was
    started.
    """
    for net, net_attr in networks.iteritems():
        if net_attr.get('bootproto') == 'dhcp':
            net_attr['blockingdhcp'] = True


def _filter_available(persistent_config):
    """Returns only nets and bonds that can be configured with the devices
    present in the system"""
    available_nets, available_bonds = {}, {}
    available_nics = netinfo.nics()
    for bond, attrs in persistent_config.bonds.iteritems():
        available_bond_nics = [nic for nic in attrs['nics'] if
                               nic in available_nics]
        if available_bond_nics:
            available_bonds[bond] = attrs.copy()
            available_bonds[bond]['nics'] = available_bond_nics

    for net, attrs in persistent_config.networks.iteritems():
        bond = attrs.get('bonding')
        if bond is not None:
            if bond not in persistent_config.bonds:
                logging.error('Bond "%s" is not configured. '
                              'Network "%s" will not be '
                              'configured as a consequence', bond, net)
            elif bond not in available_bonds:
                logging.error('Some of the nics required by bond "%s" (%s) '
                              'are missing. Network "%s" will not be '
                              'configured as a consequence', bond,
                              persistent_config.bonds[bond]['nics'], net)
            else:
                available_nets[net] = attrs
            continue  # Regardless of availability, the net is processed

        nic = attrs.get('nic')
        if nic is not None:
            if nic not in available_nics:
                logging.error('Nic "%s" required by network %s is missing. '
                              'The network will not be configured', nic, net)
            else:
                available_nets[net] = attrs
            continue  # Regardless of availability, the net is processed

        # Bridge-only nics
        available_nets[net] = attrs
    return BaseConfig(available_nets, available_bonds)


def _filter_changed_nets_bonds(persistent_config):
    """filter-out unchanged networks and bond, so that we are left only with
    changes that must be applied"""

    kernel_config = KernelConfig(netinfo.NetInfo())
    normalized_config = kernel_config.normalize(persistent_config)

    changed_bonds_names = _find_changed_or_missing(normalized_config.bonds,
                                                   kernel_config.bonds)
    changed_nets_names = _find_changed_or_missing(normalized_config.networks,
                                                  kernel_config.networks)
    changed_nets = dict((net, persistent_config.networks[net])
                        for net in changed_nets_names)
    changed_bonds = dict((bond, persistent_config.bonds[bond])
                         for bond in changed_bonds_names)

    return BaseConfig(changed_nets, changed_bonds)


def _find_changed_or_missing(persisted, current):
    changed_or_missing = []
    for name, persisted_attrs in persisted.iteritems():
        current_attrs = current.get(name)
        if current_attrs != persisted_attrs:
            logging.info("%s is different or missing from persistent "
                         "configuration. current: %s, persisted: %s",
                         name, current_attrs, persisted_attrs)
            changed_or_missing.append(name)
        else:
            logging.info("%s was not changed since last time it was persisted,"
                         " skipping restoration.", name)
    return changed_or_missing


def _get_all_configurators():
    """Returns the class objects of all the configurators in the netconf pkg"""
    prefix = configurators.__name__ + '.'
    for importer, moduleName, isPackage in pkgutil.iter_modules(
            configurators.__path__, prefix):
        __import__(moduleName, fromlist="_")

    for cls in configurators.Configurator.__subclasses__():
        yield cls


def _nets_already_restored(nets_restored_mark):
    return os.path.exists(nets_restored_mark)


def touch_file(file_path):
    with open(file_path, 'a'):
        os.utime(file_path, None)


def restore(args):
    if not args.force and _nets_already_restored(_NETS_RESTORED_MARK):
        logging.info('networks already restored. doing nothing.')
        return

    if config.get('vars', 'net_persistence') == 'unified':
        unified_restoration()
    else:
        ifcfg_restoration()

    touch_file(_NETS_RESTORED_MARK)


if __name__ == '__main__':
    try:
        logging.config.fileConfig('/etc/vdsm/svdsm.logger.conf',
                                  disable_existing_loggers=False)
    except:
        logging.basicConfig(filename='/dev/stdout', filemode='w+',
                            level=logging.DEBUG)
        logging.error('Could not init proper logging', exc_info=True)

    restore_help = ("Restores the network configuration from vdsm configured "
                    "network system persistence.\n"
                    "Restoration will delete any trace of network system "
                    "persistence except the vdsm internal persistent network "
                    "configuration. In order to avoid this use --no-flush.")
    parser = argparse.ArgumentParser(description=restore_help)

    force_option_help = ("Restore networks even if the " + _NETS_RESTORED_MARK
                         + " mark exists. The mark is created upon a previous "
                           "successful restore")
    parser.add_argument('--force', action='store_true', default=False,
                        help=force_option_help)

    args = parser.parse_args()
    restore(args)
