#! /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 glob
import logging
import logging.config
import os
import re
import time
import errno

from vdsm.config import config
from vdsm import ipwrapper
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')
_ALL_DEVICES_UP_TIMEOUT = 5


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}
    if removeNetworks or removeBonds:
        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)

    _verify_all_devices_are_up(list(_owned_ifcfg_files()))

    _update_running_config(persistent_config)

    _wait_for_for_all_devices_up(
        available_config.networks.keys() + available_config.bonds.keys())
    changed_config = _filter_changed_nets_bonds(available_config)
    nets = changed_config.networks
    bonds = changed_config.bonds
    _convert_to_blocking_dhcp(nets)
    if nets or bonds:
        logging.debug('Calling setupNetworks with networks (%s) and bond (%s).',
                      nets, bonds)
        setupNetworks(nets, bonds, connectivityCheck=False, _inRollback=True)


def _verify_all_devices_are_up(owned_ifcfg_files):
    """REQUIRED_FOR upgrade from 4.16<=vdsm<=4.16.20
    Were ifcfg files were created with ONBOOT=no.
    """
    for ifcfg_file in owned_ifcfg_files:
        _upgrade_onboot(ifcfg_file)
    ifcfg.start_devices(owned_ifcfg_files)


def _upgrade_onboot(ifcfg_file):
    with open(ifcfg_file) as f:
        old_content = f.read()
    new_content = re.sub('ONBOOT=no', 'ONBOOT=yes', old_content)
    if new_content != old_content:
        logging.debug("updating %s to ONBOOT=yes", ifcfg_file)
        with open(ifcfg_file, 'w') as f:
            f.write(new_content)


def _update_running_config(persistent_config):
    """We must recreate RunningConfig so that following setSafeNetworkConfig
    will persist a valid configuration.
    """
    running_config = RunningConfig()
    for net, net_attr in persistent_config.networks.iteritems():
        running_config.setNetwork(net, net_attr)
    for bonding, bonding_attr in persistent_config.bonds.iteritems():
        running_config.setBonding(bonding, bonding_attr)
    running_config.save()


def _owned_ifcfg_files():
    for fpath in glob.iglob(netinfo.NET_CONF_DIR + '/*'):
        if not os.path.isfile(fpath):
            continue

        with open(fpath) as f:
            content = f.read()
        if _owned_ifcfg_content(content):
            yield fpath


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 _wait_for_for_all_devices_up(links):
    timeout = time.time() + _ALL_DEVICES_UP_TIMEOUT
    down_links = _get_links_with_state_down(links)

    # TODO: use netlink monitor here might be more elegant (not available in
    # TODO: 3.5)
    while down_links and time.time() < timeout:
        logging.debug("waiting for %s to be up.", down_links)
        time.sleep(1)
        down_links = _get_links_with_state_down(links)

    if down_links:
        logging.warning("Not all devices are up. VDSM might restore them "
                        "although they were not changed since they were "
                        "persisted.")
    else:
        logging.debug("All devices are up.")


def _get_links_with_state_down(links):
    def oper_up(link):
        return bool(link.flags & 1 << 6)

    return set(l.name for l in ipwrapper.getLinks() if
               l.name in links and
               _owned_ifcfg(l.name) and
               _onboot_ifcfg(l.name) and
               not oper_up(l))


def _owned_ifcfg(link_name):
    return _ifcfg_predicate(link_name, _owned_ifcfg_content)


def _onboot_ifcfg(link_name):
    predicate = lambda content: any(
        line == 'ONBOOT=yes' for line in content.splitlines())
    return _ifcfg_predicate(link_name, predicate)


def _owned_ifcfg_content(content):
    return content.startswith(
        '# Generated by VDSM version') or content.startswith(
        '# automatically generated by vdsm')


def _ifcfg_predicate(link_name, predicate):
    try:
        with open(netinfo.NET_CONF_PREF + link_name) as conf:
            content = conf.read()
    except IOError as ioe:
        if ioe.errno == errno.ENOENT:
            return False
        else:
            raise
    else:
        return predicate(content)


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)
