# Copyright (C) 2016, Red Hat, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

from json import decoder as json_decoder

from oslo_log import log as logging
from oslo_serialization import jsonutils as json

from tempest.api.compute import base
from tempest.common import utils
from tempest.common.utils.linux import remote_client
from tempest.common import waiters
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
from tempest.lib import exceptions


CONF = config.CONF

LOG = logging.getLogger(__name__)


class DeviceTaggingBase(base.BaseV2ComputeTest):

    @classmethod
    def skip_checks(cls):
        super(DeviceTaggingBase, cls).skip_checks()
        if not CONF.service_available.neutron:
            raise cls.skipException('Neutron is required')
        if not CONF.validation.run_validation:
            raise cls.skipException('Validation must be enabled')
        if (not CONF.compute_feature_enabled.config_drive and
                not CONF.compute_feature_enabled.metadata_service):
            raise cls.skipException('One of metadata or config drive must be '
                                    'enabled')

    @classmethod
    def setup_clients(cls):
        super(DeviceTaggingBase, cls).setup_clients()
        cls.networks_client = cls.os_primary.networks_client
        cls.ports_client = cls.os_primary.ports_client
        cls.subnets_client = cls.os_primary.subnets_client
        cls.interfaces_client = cls.os_primary.interfaces_client

    @classmethod
    def setup_credentials(cls):
        cls.set_network_resources(network=True, subnet=True, router=True,
                                  dhcp=True)
        super(DeviceTaggingBase, cls).setup_credentials()

    def verify_metadata_from_api(self, server, ssh_client, verify_method):
        md_url = 'http://169.254.169.254/openstack/latest/meta_data.json'
        LOG.info('Attempting to verify tagged devices in server %s via '
                 'the metadata service: %s', server['id'], md_url)

        def get_and_verify_metadata():
            try:
                ssh_client.exec_command('curl -V')
            except exceptions.SSHExecCommandFailed:
                if not CONF.compute_feature_enabled.config_drive:
                    raise self.skipException('curl not found in guest '
                                             'and config drive is '
                                             'disabled')
                LOG.warning('curl was not found in the guest, device '
                            'tagging metadata was not checked in the '
                            'metadata API')
                return True
            cmd = 'curl %s' % md_url
            md_json = ssh_client.exec_command(cmd)
            return verify_method(md_json)
        # NOTE(gmann) Keep refreshing the metadata info until the metadata
        # cache is refreshed. For safer side, we will go with wait loop of
        # build_interval till build_timeout. verify_method() above will return
        # True if all metadata verification is done as expected.
        if not test_utils.call_until_true(get_and_verify_metadata,
                                          CONF.compute.build_timeout,
                                          CONF.compute.build_interval):
            raise exceptions.TimeoutException('Timeout while verifying '
                                              'metadata on server.')

    def verify_metadata_on_config_drive(self, server, ssh_client,
                                        verify_method):
        LOG.info('Attempting to verify tagged devices in server %s via '
                 'the config drive.', server['id'])
        ssh_client.mount_config_drive()
        cmd_md = 'sudo cat /mnt/openstack/latest/meta_data.json'
        md_json = ssh_client.exec_command(cmd_md)
        verify_method(md_json)
        ssh_client.unmount_config_drive()


class TaggedBootDevicesTest(DeviceTaggingBase):

    min_microversion = '2.32'
    # NOTE(mriedem): max_version looks odd but it's actually correct. Due to a
    # bug in the 2.32 microversion, tags on block devices only worked with the
    # 2.32 microversion specifically. And tags on networks only worked between
    # 2.32 and 2.36 inclusive; the 2.37 microversion broke tags for networks.
    max_microversion = '2.32'

    def verify_device_metadata(self, md_json):
        try:
            md_dict = json.loads(md_json)
        except (json_decoder.JSONDecodeError, TypeError):
            return False

        for d in md_dict['devices']:
            if d['type'] == 'nic':
                if d['mac'] == self.port1['mac_address']:
                    self.assertEqual(d['tags'], ['port-1'])
                if d['mac'] == self.port2['mac_address']:
                    self.assertEqual(d['tags'], ['port-2'])
                if d['mac'] == self.net_2_100_mac:
                    self.assertEqual(d['tags'], ['net-2-100'])
                if d['mac'] == self.net_2_200_mac:
                    self.assertEqual(d['tags'], ['net-2-200'])

        # A hypervisor may present multiple paths to a tagged disk, so
        # there may be duplicated tags in the metadata, use set() to
        # remove duplicated tags.
        # Some hypervisors might report devices with no tags as well.
        found_devices = [d['tags'][0] for d in md_dict['devices']
                         if d.get('tags')]
        try:
            self.assertEqual(set(found_devices), set(['port-1', 'port-2',
                                                      'net-1', 'net-2-100',
                                                      'net-2-200', 'boot',
                                                      'other']))
            return True
        except Exception:
            return False

    # NOTE(mriedem): This is really more like a scenario test and is slow so
    # it's marked as such.
    @decorators.attr(type='slow')
    @decorators.idempotent_id('a2e65a6c-66f1-4442-aaa8-498c31778d96')
    @utils.services('network', 'volume', 'image')
    def test_tagged_boot_devices(self):
        # Create volumes
        # The create_volume methods waits for the volumes to be available and
        # the base class will clean them up on tearDown.
        boot_volume = self.create_volume(CONF.compute.image_ref)
        other_volume = self.create_volume()
        untagged_volume = self.create_volume()

        # Create networks
        net1 = self.networks_client.create_network(
            name=data_utils.rand_name('device-tagging-net1'))['network']
        self.addCleanup(self.networks_client.delete_network, net1['id'])

        net2 = self.networks_client.create_network(
            name=data_utils.rand_name('device-tagging-net2'))['network']
        self.addCleanup(self.networks_client.delete_network, net2['id'])

        # Create subnets
        subnet1 = self.subnets_client.create_subnet(
            network_id=net1['id'],
            cidr='10.1.1.0/24',
            ip_version=4)['subnet']
        self.addCleanup(self.subnets_client.delete_subnet, subnet1['id'])

        subnet2 = self.subnets_client.create_subnet(
            network_id=net2['id'],
            cidr='10.2.2.0/24',
            ip_version=4)['subnet']
        self.addCleanup(self.subnets_client.delete_subnet, subnet2['id'])

        # Create ports
        self.port1 = self.ports_client.create_port(
            network_id=net1['id'],
            name=data_utils.rand_name(self.__class__.__name__),
            fixed_ips=[{'subnet_id': subnet1['id']}])['port']
        self.addCleanup(self.ports_client.delete_port, self.port1['id'])

        self.port2 = self.ports_client.create_port(
            network_id=net1['id'],
            name=data_utils.rand_name(self.__class__.__name__),
            fixed_ips=[{'subnet_id': subnet1['id']}])['port']
        self.addCleanup(self.ports_client.delete_port, self.port2['id'])

        # Create server
        config_drive_enabled = CONF.compute_feature_enabled.config_drive
        validation_resources = self.get_test_validation_resources(
            self.os_primary)

        server = self.create_test_server(
            validatable=True,
            wait_until='ACTIVE',
            validation_resources=validation_resources,
            config_drive=config_drive_enabled,
            name=data_utils.rand_name('device-tagging-server'),
            networks=[
                # Validation network for ssh
                {
                    'uuid': self.get_tenant_network()['id']
                },
                # Different tags for different ports
                {
                    'port': self.port1['id'],
                    'tag': 'port-1'
                },
                {
                    'port': self.port2['id'],
                    'tag': 'port-2'
                },
                # Two nics on same net, one tagged one not
                {
                    'uuid': net1['id'],
                    'tag': 'net-1'
                },
                {
                    'uuid': net1['id']
                },
                # Two nics on same net, different IP
                {
                    'uuid': net2['id'],
                    'fixed_ip': '10.2.2.100',
                    'tag': 'net-2-100'
                },
                {
                    'uuid': net2['id'],
                    'fixed_ip': '10.2.2.200',
                    'tag': 'net-2-200'
                }
            ],
            block_device_mapping_v2=[
                # Boot volume
                {
                    'uuid': boot_volume['id'],
                    'source_type': 'volume',
                    'destination_type': 'volume',
                    'boot_index': 0,
                    'tag': 'boot'
                },
                # Other volume
                {
                    'uuid': other_volume['id'],
                    'source_type': 'volume',
                    'destination_type': 'volume',
                    'boot_index': 1,
                    'tag': 'other'
                },
                # Untagged volume
                {
                    'uuid': untagged_volume['id'],
                    'source_type': 'volume',
                    'destination_type': 'volume',
                    'boot_index': 2
                }
            ])

        self.addCleanup(self.delete_server, server['id'])

        server = self.servers_client.show_server(server['id'])['server']
        ssh_client = remote_client.RemoteClient(
            self.get_server_ip(server, validation_resources),
            CONF.validation.image_ssh_user,
            pkey=validation_resources['keypair']['private_key'],
            server=server,
            servers_client=self.servers_client)

        # Find the MAC addresses of our fixed IPs
        self.net_2_100_mac = None
        self.net_2_200_mac = None
        ifaces = self.interfaces_client.list_interfaces(server['id'])
        for iface in ifaces['interfaceAttachments']:
            if 'fixed_ips' in iface:
                for ip in iface['fixed_ips']:
                    if ip['ip_address'] == '10.2.2.100':
                        self.net_2_100_mac = iface['mac_addr']
                    if ip['ip_address'] == '10.2.2.200':
                        self.net_2_200_mac = iface['mac_addr']
        # Make sure we have the MACs we need, there's no reason for some to be
        # missing
        self.assertTrue(self.net_2_100_mac)
        self.assertTrue(self.net_2_200_mac)

        # Verify metadata from metadata API
        if CONF.compute_feature_enabled.metadata_service:
            self.verify_metadata_from_api(server, ssh_client,
                                          self.verify_device_metadata)

        # Verify metadata on config drive
        if CONF.compute_feature_enabled.config_drive:
            self.verify_metadata_on_config_drive(server, ssh_client,
                                                 self.verify_device_metadata)


class TaggedBootDevicesTest_v242(TaggedBootDevicesTest):
    min_microversion = '2.42'
    max_microversion = 'latest'


class TaggedAttachmentsTest(DeviceTaggingBase):

    min_microversion = '2.49'
    max_microversion = 'latest'

    @classmethod
    def skip_checks(cls):
        super(TaggedAttachmentsTest, cls).skip_checks()
        if not CONF.compute_feature_enabled.metadata_service:
            raise cls.skipException('Metadata API must be enabled')

    def verify_device_metadata(self, md_json):
        try:
            md_dict = json.loads(md_json)
        except (json_decoder.JSONDecodeError, TypeError):
            return False

        found_devices = [d['tags'][0] for d in md_dict['devices']
                         if d.get('tags')]
        try:
            self.assertItemsEqual(found_devices, ['nic-tag', 'volume-tag'])
            return True
        except Exception:
            return False

    def verify_empty_devices(self, md_json):
        md_dict = json.loads(md_json)
        try:
            self.assertEmpty(md_dict['devices'])
            return True
        except AssertionError:
            LOG.debug("Related bug 1775947. Devices dict is not empty: %s",
                      md_dict['devices'])
            return False

    @decorators.idempotent_id('3e41c782-2a89-4922-a9d2-9a188c4e7c7c')
    @utils.services('network', 'volume', 'image')
    def test_tagged_attachment(self):
        # Create network
        net = self.networks_client.create_network(
            name=data_utils.rand_name(
                'tagged-attachments-test-net'))['network']
        self.addCleanup(self.networks_client.delete_network, net['id'])

        # Create subnet
        subnet = self.subnets_client.create_subnet(
            network_id=net['id'],
            cidr='10.10.10.0/24',
            ip_version=4)['subnet']
        self.addCleanup(self.subnets_client.delete_subnet, subnet['id'])

        # Create volume
        volume = self.create_volume()

        # Boot test server
        config_drive_enabled = CONF.compute_feature_enabled.config_drive
        validation_resources = self.get_test_validation_resources(
            self.os_primary)

        server = self.create_test_server(
            validatable=True,
            validation_resources=validation_resources,
            config_drive=config_drive_enabled,
            name=data_utils.rand_name('device-tagging-server'),
            networks=[{'uuid': self.get_tenant_network()['id']}],
            wait_until='ACTIVE')
        self.addCleanup(self.delete_server, server['id'])

        # NOTE(mgoddard): Get detailed server to ensure addresses are present
        # in fixed IP case.
        server = self.servers_client.show_server(server['id'])['server']

        # Attach tagged nic and volume
        interface = self.interfaces_client.create_interface(
            server['id'], net_id=net['id'],
            tag='nic-tag')['interfaceAttachment']
        self.attach_volume(server, volume, tag='volume-tag')

        ssh_client = remote_client.RemoteClient(
            self.get_server_ip(server, validation_resources),
            CONF.validation.image_ssh_user,
            pkey=validation_resources['keypair']['private_key'],
            server=server,
            servers_client=self.servers_client)

        self.verify_metadata_from_api(server, ssh_client,
                                      self.verify_device_metadata)

        # Detach tagged nic and volume
        self.servers_client.detach_volume(server['id'], volume['id'])
        waiters.wait_for_volume_resource_status(self.volumes_client,
                                                volume['id'], 'available')
        self.interfaces_client.delete_interface(server['id'],
                                                interface['port_id'])
        waiters.wait_for_interface_detach(self.interfaces_client,
                                          server['id'],
                                          interface['port_id'])
        # FIXME(mriedem): The assertion that the tagged devices are removed
        # from the metadata for the server is being skipped until bug 1775947
        # is fixed.
        # self.verify_metadata_from_api(server, ssh_client,
        #                               self.verify_empty_devices)
