# Copyright (c) 2012 OpenStack Foundation
# All Rights Reserved.
#
#    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.

"""Tests for compute resource tracking."""

import copy
import datetime
import uuid

import mock
from oslo_config import cfg
from oslo_serialization import jsonutils
from oslo_utils import timeutils
import six

from nova.compute.monitors import base as monitor_base
from nova.compute import resource_tracker
from nova.compute import resources
from nova.compute import task_states
from nova.compute import vm_states
from nova import context
from nova import exception
from nova import objects
from nova.objects import base as obj_base
from nova.objects import fields
from nova.objects import pci_device_pool
from nova import rpc
from nova import test
from nova.tests.unit.pci import fakes as pci_fakes
from nova.tests import uuidsentinel
from nova.virt import driver


FAKE_VIRT_MEMORY_MB = 5
FAKE_VIRT_MEMORY_OVERHEAD = 1
FAKE_VIRT_MEMORY_WITH_OVERHEAD = (
        FAKE_VIRT_MEMORY_MB + FAKE_VIRT_MEMORY_OVERHEAD)
FAKE_VIRT_NUMA_TOPOLOGY = objects.NUMATopology(
        cells=[objects.NUMACell(id=0, cpuset=set([1, 2]), memory=3072,
                                cpu_usage=0, memory_usage=0, mempages=[],
                                siblings=[], pinned_cpus=set([])),
               objects.NUMACell(id=1, cpuset=set([3, 4]), memory=3072,
                                cpu_usage=0, memory_usage=0, mempages=[],
                                siblings=[], pinned_cpus=set([]))])
FAKE_VIRT_NUMA_TOPOLOGY_OVERHEAD = objects.NUMATopologyLimits(
    cpu_allocation_ratio=2, ram_allocation_ratio=2)

ROOT_GB = 5
EPHEMERAL_GB = 1
FAKE_VIRT_LOCAL_GB = ROOT_GB + EPHEMERAL_GB
FAKE_VIRT_VCPUS = 1
FAKE_VIRT_STATS = {'virt_stat': 10}
FAKE_VIRT_STATS_COERCED = {'virt_stat': '10'}
FAKE_VIRT_STATS_JSON = jsonutils.dumps(FAKE_VIRT_STATS)
RESOURCE_NAMES = ['vcpu']
CONF = cfg.CONF


class UnsupportedVirtDriver(driver.ComputeDriver):
    """Pretend version of a lame virt driver."""

    def __init__(self):
        super(UnsupportedVirtDriver, self).__init__(None)

    def get_host_ip_addr(self):
        return '127.0.0.1'

    def get_available_resource(self, nodename):
        # no support for getting resource usage info
        return {}


class FakeVirtDriver(driver.ComputeDriver):

    def __init__(self, pci_support=False, stats=None,
                 numa_topology=FAKE_VIRT_NUMA_TOPOLOGY):
        super(FakeVirtDriver, self).__init__(None)
        self.memory_mb = FAKE_VIRT_MEMORY_MB
        self.local_gb = FAKE_VIRT_LOCAL_GB
        self.vcpus = FAKE_VIRT_VCPUS
        self.numa_topology = numa_topology

        self.memory_mb_used = 0
        self.local_gb_used = 0
        self.pci_support = pci_support
        self.pci_devices = [
            {
                'label': 'label_8086_0443',
                'dev_type': fields.PciDeviceType.SRIOV_VF,
                'compute_node_id': 1,
                'address': '0000:00:01.1',
                'product_id': '0443',
                'vendor_id': '8086',
                'status': 'available',
                'extra_k1': 'v1',
                'numa_node': 1,
                'parent_addr': '0000:00:01.0',
            },
            {
                'label': 'label_8086_0443',
                'dev_type': fields.PciDeviceType.SRIOV_VF,
                'compute_node_id': 1,
                'address': '0000:00:01.2',
                'product_id': '0443',
                'vendor_id': '8086',
                'status': 'available',
                'extra_k1': 'v1',
                'numa_node': 1,
                'parent_addr': '0000:00:01.0',
            },
            {
                'label': 'label_8086_0443',
                'dev_type': fields.PciDeviceType.SRIOV_PF,
                'compute_node_id': 1,
                'address': '0000:00:01.0',
                'product_id': '0443',
                'vendor_id': '8086',
                'status': 'available',
                'extra_k1': 'v1',
                'numa_node': 1,
            },
            {
                'label': 'label_8086_0123',
                'dev_type': 'type-PCI',
                'compute_node_id': 1,
                'address': '0000:00:01.0',
                'product_id': '0123',
                'vendor_id': '8086',
                'status': 'available',
                'extra_k1': 'v1',
                'numa_node': 1,
            },
            {
                'label': 'label_8086_7891',
                'dev_type': fields.PciDeviceType.SRIOV_VF,
                'compute_node_id': 1,
                'address': '0000:00:01.0',
                'product_id': '7891',
                'vendor_id': '8086',
                'status': 'available',
                'extra_k1': 'v1',
                'numa_node': None,
                'parent_addr': '0000:08:01.0',
            },
        ] if self.pci_support else []
        self.pci_stats = [
            {
                'count': 2,
                'vendor_id': '8086',
                'product_id': '0443',
                'numa_node': 1,
                'dev_type': fields.PciDeviceType.SRIOV_VF
            },
            {
                'count': 1,
                'vendor_id': '8086',
                'product_id': '0443',
                'numa_node': 1,
                'dev_type': fields.PciDeviceType.SRIOV_PF
            },
            {
                'count': 1,
                'vendor_id': '8086',
                'product_id': '7891',
                'numa_node': None,
                'dev_type': fields.PciDeviceType.SRIOV_VF
            },
        ] if self.pci_support else []
        if stats is not None:
            self.stats = stats

    def get_host_ip_addr(self):
        return '127.0.0.1'

    def get_available_resource(self, nodename):
        d = {
            'vcpus': self.vcpus,
            'memory_mb': self.memory_mb,
            'local_gb': self.local_gb,
            'vcpus_used': 0,
            'memory_mb_used': self.memory_mb_used,
            'local_gb_used': self.local_gb_used,
            'hypervisor_type': 'fake',
            'hypervisor_version': 0,
            'hypervisor_hostname': 'fakehost',
            'cpu_info': '',
            'numa_topology': (
                self.numa_topology._to_json() if self.numa_topology else None),
        }
        if self.pci_support:
            d['pci_passthrough_devices'] = jsonutils.dumps(self.pci_devices)
        if hasattr(self, 'stats'):
            d['stats'] = self.stats
        return d

    def estimate_instance_overhead(self, instance_info):
        instance_info['memory_mb']  # make sure memory value is present
        overhead = {
            'memory_mb': FAKE_VIRT_MEMORY_OVERHEAD
        }
        return overhead  # just return a constant value for testing


class BaseTestCase(test.TestCase):

    @mock.patch('stevedore.enabled.EnabledExtensionManager')
    def setUp(self, _mock_ext_mgr):
        super(BaseTestCase, self).setUp()

        self.flags(reserved_host_disk_mb=0,
                   reserved_host_memory_mb=0)

        self.context = context.get_admin_context()

        self._set_pci_passthrough_whitelist()
        self.flags(use_local=True, group='conductor')
        self.conductor = self.start_service('conductor',
                                            manager=CONF.conductor.manager)

        self._instances = {}
        self._instance_types = {}

        self.stubs.Set(objects.InstanceList, 'get_by_host_and_node',
                       self._fake_instance_get_by_host_and_node)
        self.stubs.Set(self.conductor.db,
                       'flavor_get', self._fake_flavor_get)

        self.host = 'fakehost'
        self.compute = self._create_compute_node()
        self.updated = False
        self.deleted = False
        self.update_call_count = 0

    def _set_pci_passthrough_whitelist(self):
        self.flags(pci_passthrough_whitelist=[
            '{"vendor_id": "8086", "product_id": "0443"}',
            '{"vendor_id": "8086", "product_id": "7891"}'])

    def _create_compute_node(self, values=None):
        # This creates a db representation of a compute_node.
        compute = {
            "id": 1,
            "uuid": uuidsentinel.fake_compute_node,
            "service_id": 1,
            "host": "fakehost",
            "vcpus": 1,
            "memory_mb": 1,
            "local_gb": 1,
            "vcpus_used": 1,
            "memory_mb_used": 1,
            "local_gb_used": 1,
            "free_ram_mb": 1,
            "free_disk_gb": 1,
            "current_workload": 1,
            "running_vms": 0,
            "cpu_info": None,
            "numa_topology": None,
            "stats": '{"num_instances": "1"}',
            "hypervisor_hostname": "fakenode",
            'hypervisor_version': 1,
            'hypervisor_type': 'fake-hyp',
            'disk_available_least': None,
            'host_ip': None,
            'metrics': None,
            'created_at': None,
            'updated_at': None,
            'deleted_at': None,
            'deleted': False,
            'cpu_allocation_ratio': None,
            'ram_allocation_ratio': None,
            'disk_allocation_ratio': None,
        }
        if values:
            compute.update(values)
        return compute

    def _create_compute_node_obj(self, context):
        # Use the db representation of a compute node returned
        # by _create_compute_node() to create an equivalent compute
        # node object.
        compute = self._create_compute_node()
        compute_obj = objects.ComputeNode()
        compute_obj = objects.ComputeNode._from_db_object(
            context, compute_obj, compute)
        return compute_obj

    def _create_service(self, host="fakehost", compute=None):
        if compute:
            compute = [compute]

        service = {
            "id": 1,
            "host": host,
            "binary": "nova-compute",
            "topic": "compute",
            "compute_node": compute,
            "report_count": 0,
            'disabled': False,
            'disabled_reason': None,
            'created_at': None,
            'updated_at': None,
            'deleted_at': None,
            'deleted': False,
            'last_seen_up': None,
            'forced_down': False,
            'version': 0,
        }
        return service

    def _fake_instance_obj(self, stash=True, flavor=None, **kwargs):

        # Default to an instance ready to resize to or from the same
        # instance_type
        flavor = flavor or self._fake_flavor_create()
        if not isinstance(flavor, objects.Flavor):
            flavor = objects.Flavor(**flavor)

        instance_uuid = str(uuid.uuid1())
        instance = objects.Instance(context=self.context, uuid=instance_uuid,
                                    flavor=flavor)
        instance.update({
            'vm_state': vm_states.RESIZED,
            'task_state': None,
            'ephemeral_key_uuid': None,
            'os_type': 'Linux',
            'project_id': '123456',
            'host': None,
            'node': None,
            'instance_type_id': flavor['id'],
            'memory_mb': flavor['memory_mb'],
            'vcpus': flavor['vcpus'],
            'root_gb': flavor['root_gb'],
            'ephemeral_gb': flavor['ephemeral_gb'],
            'launched_on': None,
            'system_metadata': {},
            'availability_zone': None,
            'vm_mode': None,
            'reservation_id': None,
            'display_name': None,
            'default_swap_device': None,
            'power_state': None,
            'access_ip_v6': None,
            'access_ip_v4': None,
            'key_name': None,
            'updated_at': None,
            'cell_name': None,
            'locked': None,
            'locked_by': None,
            'launch_index': None,
            'architecture': None,
            'auto_disk_config': None,
            'terminated_at': None,
            'ramdisk_id': None,
            'user_data': None,
            'cleaned': None,
            'deleted_at': None,
            'id': 333,
            'disable_terminate': None,
            'hostname': None,
            'display_description': None,
            'key_data': None,
            'deleted': None,
            'default_ephemeral_device': None,
            'progress': None,
            'launched_at': None,
            'config_drive': None,
            'kernel_id': None,
            'user_id': None,
            'shutdown_terminate': None,
            'created_at': None,