# Copyright (C) 2017, 2018 Cumulus Networks, Inc. all rights reserved
#
# 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; version 2.
#
# 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.
#
# https://www.gnu.org/licenses/gpl-2.0-standalone.html
#
# Author:
#       Julien Fortin, julien@cumulusnetworks.com
#
# sysfs -- contains all sysfs related operation
#

import os
import glob

try:
    from ifupdown2.lib.io import IO
    from ifupdown2.lib.base_objects import Requirements

    from ifupdown2.ifupdown.utils import utils

    from ifupdown2.nlmanager.nlpacket import Link
except (ImportError, ModuleNotFoundError):
    from lib.io import IO
    from lib.base_objects import Requirements

    from ifupdown.utils import utils

    from nlmanager.nlpacket import Link


class __Sysfs(IO, Requirements):

    __bond_netlink_to_sysfs_attr_map = {
        Link.IFLA_BOND_MODE: "mode",
        Link.IFLA_BOND_MIIMON: "miimon",
        Link.IFLA_BOND_USE_CARRIER: "use_carrier",
        Link.IFLA_BOND_AD_LACP_RATE: "lacp_rate",
        Link.IFLA_BOND_XMIT_HASH_POLICY: "xmit_hash_policy",
        Link.IFLA_BOND_MIN_LINKS: "min_links",
        Link.IFLA_BOND_NUM_PEER_NOTIF: "num_grat_arp",
        Link.IFLA_BOND_AD_ACTOR_SYSTEM: "ad_actor_system",
        Link.IFLA_BOND_AD_ACTOR_SYS_PRIO: "ad_actor_sys_prio",
        Link.IFLA_BOND_AD_LACP_BYPASS: "lacp_bypass",
        Link.IFLA_BOND_UPDELAY: "updelay",
        Link.IFLA_BOND_DOWNDELAY: "downdelay",
        Link.IFLA_BOND_PRIMARY: "primary",
    }

    def __init__(self):
        IO.__init__(self)
        Requirements.__init__(self)

        # Temporary work around to solve the circular dependency with nlcache.
        # Once nlcache is created it will populate sysfs.cache
        self.cache = None

        # if bridge utils is not installed overrrides specific functions to
        # avoid constantly checking bridge_utils_is_installed
        if not Requirements.bridge_utils_is_installed:
            self.bridge_get_mcqv4src = self.bridge_get_mcqv4src_dry_run

    @staticmethod
    def link_get_uppers(ifname):
        try:
            uppers = glob.glob("/sys/class/net/%s/upper_*" % ifname)
            if not uppers:
                return []
            return [os.path.basename(u)[6:] for u in uppers]
        except Exception:
            return []

    @staticmethod
    def link_get_lowers(ifname):
        try:
            lowers = glob.glob("/sys/class/net/%s/lower_*" % ifname)
            if not lowers:
                return []
            return [os.path.basename(l)[6:] for l in lowers]
        except Exception:
            return []

    def link_is_up(self, ifname):
        """
        Read sysfs operstate file
        """
        return "up" == self.read_file_oneline("/sys/class/net/%s/operstate" % ifname)

    def get_link_address(self, ifname):
        """
        Read MAC hardware address from sysfs
        """
        return self.read_file_oneline("/sys/class/net/%s/address" % ifname)

    #
    # MTU
    #

    def link_get_mtu(self, ifname):
        return int(self.read_file_oneline("/sys/class/net/%s/mtu" % ifname) or 0)

    def link_set_mtu(self, ifname, mtu_str, mtu_int):
        if self.cache.get_link_mtu(ifname) != mtu_int:
            if self.write_to_file('/sys/class/net/%s/mtu' % ifname, mtu_str):
                self.cache.override_link_mtu(ifname, mtu_int)

    def link_set_mtu_dry_run(self, ifname, mtu_str, mtu_int):
        # we can remove the cache check in DRYRUN mode
        self.write_to_file('/sys/class/net/%s/mtu' % ifname, mtu_str)

    #
    # ALIAS
    #

    def link_set_alias(self, ifname, alias):
        cached_alias = self.cache.get_link_alias(ifname)

        if cached_alias == alias:
            return

        if not alias:
            alias = "\n"

        if self.write_to_file("/sys/class/net/%s/ifalias" % ifname, alias):
            pass # self.cache.override_link_mtu(ifname, mtu_int)

    def link_set_alias_dry_run(self, ifname, alias):
        # we can remove the cache check in DRYRUN mode
        if not alias:
            alias = ""
        self.write_to_file("/sys/class/net/%s/ifalias" % ifname, alias)

    ############################################################################
    # BRIDGE
    ############################################################################

    def bridge_port_pvids_get(self, bridge_port_name):
        return self.read_file_oneline("/sys/class/net/%s/brport/pvid" % bridge_port_name)

    def bridge_get_stp(self, bridge):
        stp_state_path = "/sys/class/net/%s/bridge/stp_state" % bridge

        if not os.path.exists(stp_state_path):
            return "error"

        stp_state = self.read_file_oneline(stp_state_path)

        if not stp_state:
            return "error"

        try:
            stp_state_int = int(stp_state)
            return "yes" if stp_state_int > 0 else "no"
        except Exception:
            return "unknown"

    def bridge_get_mcqv4src(self, bridge):
        mcqv4src = {}
        try:
            filename = "/sys/class/net/%s/bridge/multicast_v4_queriers" % bridge
            if os.path.exists(filename):
                for line in self.read_file(filename) or []:
                    vlan_id, ip = line.split('=')
                    mcqv4src[vlan_id] = ip.strip()
            return mcqv4src
        except Exception as e:
            self.logger.info("%s showmcqv4src: skipping unsupported command" % utils.brctl_cmd)
            self.bridge_get_mcqv4src = self.bridge_get_mcqv4src_dry_run
            return {}

    @staticmethod
    def bridge_get_mcqv4src_dry_run(bridge):
        return {}

    ############################################################################
    # BOND
    ############################################################################

    def bond_remove_slave(self, bond_name, slave_name):
        if self.cache.is_link_enslaved_to(slave_name, bond_name):
            if self.write_to_file("/sys/class/net/%s/bonding/slaves" % bond_name, "-%s" % slave_name):
                # success we can manually update our cache to make sure we stay up-to-date
                self.cache.override_cache_unslave_link(slave=slave_name, master=bond_name)

    def bond_remove_slave_dry_run(self, bond_name, slave_name):
        self.write_to_file("/sys/class/net/%s/bonding/slaves" % bond_name, "-%s" % slave_name)

    ###

    def bond_create(self, bond_name):
        if self.cache.bond_exists(bond_name):
            return
        self.write_to_file("/sys/class/net/bonding_masters", "+%s" % bond_name)

    def bond_create_dry_run(self, bond_name):
        self.write_to_file("/sys/class/net/bonding_masters", "+%s" % bond_name)

    ###

    def bond_set_attrs_nl(self, bond_name, ifla_info_data):
        """
        bond_set_attrs_nl doesn't need a _dry_run handler because each
        entry in ifla_info_data was checked against the cache already.
        Here write_to_file already has a dry_run handler.
        :param bond_name:
        :param ifla_info_data:
        :return:
        """
        bond_attr_name = 'None'  # for log purpose (in case an exception raised)

        for nl_attr, value in list(ifla_info_data.items()):
            try:
                bond_attr_name = self.__bond_netlink_to_sysfs_attr_map.get(nl_attr)

                if bond_attr_name is None:
                    self.logger.warning(
                        "%s: sysfs configuration: unknown bond attribute %s (value %s)"
                        % (bond_name, nl_attr, value)
                    )
                    continue

                file_path = "/sys/class/net/%s/bonding/%s" % (bond_name, bond_attr_name)
                if os.path.exists(file_path):
                    self.write_to_file(file_path, str(value))
            except Exception as e:
                self.logger.warning("%s: %s %s: %s" % (bond_name, bond_attr_name, value, str(e)))

    ############################################################################
    # /proc/sys/ipv6/conf
    ############################################################################

    def get_ipv6_conf_disable_ipv6(self, ifname):
        return int(self.read_file_oneline("/proc/sys/net/ipv6/conf/%s/disable_ipv6" % ifname) or 0)


Sysfs = __Sysfs()
