# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""
Base class for DHCPv6 tests.  This class just sets up a little bit of plumbing,
like a virtual ethernet device with one end that looks like a real ethernet
device to shill and a DHCPv6 test server on the end that doesn't look like a
real ethernet interface to shill.  Child classes should override test_body()
with the logic of their test.
"""

import logging
import time
import traceback

from autotest_lib.client.bin import test
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib.cros import virtual_ethernet_pair
from autotest_lib.client.cros import dhcpv6_test_server
from autotest_lib.client.cros.networking import shill_proxy

# These are keys that may be used with the DBus dictionary returned from
# Dhcpv6TestBase.get_interface_ipconfig().
DHCPV6_KEY_ADDRESS = 'Address'
DHCPV6_KEY_DELEGATED_PREFIX = 'DelegatedPrefix'
DHCPV6_KEY_DELEGATED_PREFIX_LENGTH = 'DelegatedPrefixLength'
DHCPV6_KEY_NAMESERVERS = 'NameServers'
DHCPV6_KEY_SEARCH_DOMAIN_LIST = 'SearchDomains'

# After DHCPv6 completes, an ipconfig should appear shortly after
IPCONFIG_POLL_COUNT = 5
IPCONFIG_POLL_PERIOD_SECONDS = 1

class Dhcpv6TestBase(test.test):
    """Parent class for tests that work verify DHCPv6 behavior."""
    version = 1

    def get_device(self, interface_name):
        """Finds the corresponding Device object for an interface with
        the name |interface_name|.

        @param interface_name string The name of the interface to check.

        @return DBus interface object representing the associated device.

        """
        return self.shill_proxy.find_object('Device',
                                            {'Name': interface_name})


    def find_ethernet_service(self, interface_name):
        """Finds the corresponding service object for an Ethernet interface.

        @param interface_name string The name of the associated interface

        @return Service object representing the associated service.

        """
        device = self.get_device(interface_name)
        device_path = shill_proxy.ShillProxy.dbus2primitive(device.object_path)
        return self.shill_proxy.find_object('Service', {'Device': device_path})


    def get_interface_ipconfig_objects(self, interface_name):
        """
        Returns a list of dbus object proxies for |interface_name|.
        Returns an empty list if no such interface exists.

        @param interface_name string name of the device to query (e.g., "eth0").

        @return list of objects representing DBus IPConfig RPC endpoints.

        """
        device = self.get_device(interface_name)
        if device is None:
            return []

        device_properties = device.GetProperties(utf8_strings=True)
        proxy = self.shill_proxy

        ipconfig_object = proxy.DBUS_TYPE_IPCONFIG
        return filter(bool,
                      [ proxy.get_dbus_object(ipconfig_object, property_path)
                        for property_path in device_properties['IPConfigs'] ])


    def get_interface_ipconfig(self, interface_name):
        """
        Returns a dictionary containing settings for an |interface_name| set
        via DHCPv6.  Returns None if no such interface or setting bundle on
        that interface can be found in shill.

        @param interface_name string name of the device to query (e.g., "eth0").

        @return dict containing the the properties of the IPConfig stripped
            of DBus meta-data or None.

        """
        dhcp_properties = None
        for ipconfig in self.get_interface_ipconfig_objects(interface_name):
          logging.info('Looking at ipconfig %r', ipconfig)
          ipconfig_properties = ipconfig.GetProperties(utf8_strings=True)
          if 'Method' not in ipconfig_properties:
              logging.info('Found ipconfig object with no method field')
              continue
          if ipconfig_properties['Method'] != 'dhcp6':
              logging.info('Found ipconfig object with method != dhcp6')
              continue
          if dhcp_properties != None:
              raise error.TestFail('Found multiple ipconfig objects '
                                   'with method == dhcp6')
          dhcp_properties = ipconfig_properties
        if dhcp_properties is None:
            logging.info('Did not find IPConfig object with method == dhcp6')
            return None
        logging.info('Got raw dhcp config dbus object: %s.', dhcp_properties)
        return shill_proxy.ShillProxy.dbus2primitive(dhcp_properties)


    def run_once(self):
        self._server = None
        self._server_ip = None
        self._ethernet_pair = None
        self._shill_proxy = shill_proxy.ShillProxy()
        try:
            # TODO(zqiu): enable DHCPv6 for peer interface, either by restarting
            # shill with appropriate command line options or via a new DBUS
            # command.
            self._ethernet_pair = virtual_ethernet_pair.VirtualEthernetPair(
                    interface_ip=None,
                    peer_interface_name='pseudoethernet0',
                    peer_interface_ip=None,
                    interface_ipv6=dhcpv6_test_server.DHCPV6_SERVER_ADDRESS)
            self._ethernet_pair.setup()
            if not self._ethernet_pair.is_healthy:
                raise error.TestFail('Could not create virtual ethernet pair.')
            self._server_ip = self._ethernet_pair.interface_ip
            self._server = dhcpv6_test_server.Dhcpv6TestServer(
                    self._ethernet_pair.interface_name)
            self._server.start()
            self.test_body()
        except (error.TestFail, error.TestNAError):
            # Pass these through without modification.
            raise
        except Exception as e:
            logging.error('Caught exception: %s.', str(e))
            logging.error('Trace: %s', traceback.format_exc())
            raise error.TestFail('Caught exception: %s.' % str(e))
        finally:
            if self._server is not None:
                self._server.stop()
            if self._ethernet_pair is not None:
                self._ethernet_pair.teardown()

    def test_body(self):
        """
        Override this method with the body of your test.  You may safely assume
        that the the properties exposed by DhcpTestBase correctly return
        references to the test apparatus.
        """
        raise error.TestFail('No test body implemented')

    @property
    def server_ip(self):
        """
        Return the IP address of the side of the interface that the DHCPv6 test
        server is bound to.  The server itself is bound the the broadcast
        address on the interface.
        """
        return self._server_ip

    @property
    def server(self):
        """
        Returns a reference to the DHCP test server.  Use this to add handlers
        and run tests.
        """
        return self._server

    @property
    def ethernet_pair(self):
        """
        Returns a reference to the virtual ethernet pair created to run DHCP
        tests on.
        """
        return self._ethernet_pair

    @property
    def shill_proxy(self):
        """
        Returns a the shill proxy instance.
        """
        return self._shill_proxy


    def check_dhcpv6_config(self):
        """
        Compare the DHCPv6 ipconfig with DHCP lease parameters to ensure
        that the DUT attained the correct values.

        """
        # Retrieve DHCPv6 configuration.
        for attempt in range(IPCONFIG_POLL_COUNT):
            dhcpv6_config = self.get_interface_ipconfig(
                    self.ethernet_pair.peer_interface_name)
            # Wait until both IP address and delegated prefix are obtained.
            if (dhcpv6_config is not None and
                dhcpv6_config.get(DHCPV6_KEY_ADDRESS) and
                dhcpv6_config.get(DHCPV6_KEY_DELEGATED_PREFIX)):
                break;
            time.sleep(IPCONFIG_POLL_PERIOD_SECONDS)
        else:
            raise error.TestFail('Failed to retrieve DHCPv6 ipconfig object '
                                 'from shill.')

        # Verify Non-temporary Address prefix.
        address = dhcpv6_config.get(DHCPV6_KEY_ADDRESS)
        actual_prefix = address[:address.index('::')]
        expected_prefix = dhcpv6_test_server.DHCPV6_SERVER_SUBNET_PREFIX[:
                dhcpv6_test_server.DHCPV6_SERVER_SUBNET_PREFIX.index('::')]
        if actual_prefix != expected_prefix:
            raise error.TestFail('Address prefix mismatch: '
                                 'actual %s expected %s.' %
                                 (actual_prefix, expected_prefix))
        # Verify Non-temporary Address suffix.
        actual_suffix = int(address[address.index('::')+2:], 16)
        if (actual_suffix < dhcpv6_test_server.DHCPV6_ADDRESS_RANGE_LOW or
            actual_suffix > dhcpv6_test_server.DHCPV6_ADDRESS_RANGE_HIGH):
            raise error.TestFail('Invalid address suffix: '
                                 'actual %x expected (%x-%x)' %
                                 (actual_suffix,
                                  dhcpv6_test_server.DHCPV6_ADDRESS_RANGE_LOW,
                                  dhcpv6_test_server.DHCPV6_ADDRESS_RANGE_HIGH))

        # Verify delegated prefix.
        delegated_prefix = dhcpv6_config.get(DHCPV6_KEY_DELEGATED_PREFIX)
        for x in range(
                dhcpv6_test_server.DHCPV6_PREFIX_DELEGATION_INDEX_LOW,
                dhcpv6_test_server.DHCPV6_PREFIX_DELEGATION_INDEX_HIGH+1):
            valid_prefix = \
                    dhcpv6_test_server.DHCPV6_PREFIX_DELEGATION_RANGE_FORMAT % x
            if delegated_prefix == valid_prefix:
                break;
        else:
            raise error.TestFail('Invalid delegated prefix: %s' %
                                 (delegated_prefix))
        # Verify delegated prefix length.
        delegated_prefix_length = \
                int(dhcpv6_config.get(DHCPV6_KEY_DELEGATED_PREFIX_LENGTH))
        expected_delegated_prefix_length = \
                dhcpv6_test_server.DHCPV6_PREFIX_DELEGATION_PREFIX_LENGTH
        if delegated_prefix_length != expected_delegated_prefix_length:
            raise error.TestFail('Delegated prefix length mismatch: '
                                 'actual %d expected %d' %
                                 (delegated_prefix_length,
                                  expected_delegated_prefix_length))

        # Verify name servers.
        actual_name_servers = dhcpv6_config.get(DHCPV6_KEY_NAMESERVERS)
        expected_name_servers = \
                dhcpv6_test_server.DHCPV6_NAME_SERVERS.split(',')
        if actual_name_servers != expected_name_servers:
            raise error.TestFail('Name servers mismatch: actual %r expected %r'
                                 % (actual_name_servers, expected_name_servers))
        # Verify domain search.
        actual_domain_search = dhcpv6_config.get(DHCPV6_KEY_SEARCH_DOMAIN_LIST)
        expected_domain_search = \
                dhcpv6_test_server.DHCPV6_DOMAIN_SEARCH.split(',')
        if actual_domain_search != expected_domain_search:
            raise error.TestFail('Domain search list mismatch: '
                                 'actual %r expected %r' %
                                 (actual_domain_search, expected_domain_search))