#    Copyright 2013 IBM Corp.
#
#    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.

import copy

import mock
import netaddr
from oslo_serialization import jsonutils
from oslo_utils import timeutils
from oslo_versionedobjects import base as ovo_base
from oslo_versionedobjects import exception as ovo_exc

from nova import db
from nova import exception
from nova import objects
from nova.objects import base
from nova.objects import compute_node
from nova.objects import hv_spec
from nova.objects import service
from nova.tests.unit import fake_pci_device_pools
from nova.tests.unit.objects import test_objects
from nova.tests import uuidsentinel

NOW = timeutils.utcnow().replace(microsecond=0)
fake_stats = {'num_foo': '10'}
fake_stats_db_format = jsonutils.dumps(fake_stats)
# host_ip is coerced from a string to an IPAddress
# but needs to be converted to a string for the database format
fake_host_ip = '127.0.0.1'
fake_numa_topology = objects.NUMATopology(
        cells=[objects.NUMACell(id=0, cpuset=set([1, 2]), memory=512,
                                cpu_usage=0, memory_usage=0,
                                mempages=[], pinned_cpus=set([]),
                                siblings=[]),
               objects.NUMACell(id=1, cpuset=set([3, 4]), memory=512,
                                cpu_usage=0, memory_usage=0,
                                mempages=[], pinned_cpus=set([]),
                                siblings=[])])
fake_numa_topology_db_format = fake_numa_topology._to_json()
fake_supported_instances = [('x86_64', 'kvm', 'hvm')]
fake_hv_spec = hv_spec.HVSpec(arch=fake_supported_instances[0][0],
                              hv_type=fake_supported_instances[0][1],
                              vm_mode=fake_supported_instances[0][2])
fake_supported_hv_specs = [fake_hv_spec]
# for backward compatibility, each supported instance object
# is stored as a list in the database
fake_supported_hv_specs_db_format = jsonutils.dumps([fake_hv_spec.to_list()])
fake_pci = jsonutils.dumps(fake_pci_device_pools.fake_pool_list_primitive)
fake_compute_node = {
    'created_at': NOW,
    'updated_at': None,
    'deleted_at': None,
    'deleted': False,
    'id': 123,
    'uuid': uuidsentinel.fake_compute_node,
    'service_id': None,
    'host': 'fake',
    'vcpus': 4,
    'memory_mb': 4096,
    'local_gb': 1024,
    'vcpus_used': 2,
    'memory_mb_used': 2048,
    'local_gb_used': 512,
    'hypervisor_type': 'Hyper-Dan-VM-ware',
    'hypervisor_version': 1001,
    'hypervisor_hostname': 'vm.danplanet.com',
    'free_ram_mb': 1024,
    'free_disk_gb': 256,
    'current_workload': 100,
    'running_vms': 2013,
    'cpu_info': 'Schmintel i786',
    'disk_available_least': 256,
    'metrics': '',
    'stats': fake_stats_db_format,
    'host_ip': fake_host_ip,
    'numa_topology': fake_numa_topology_db_format,
    'supported_instances': fake_supported_hv_specs_db_format,
    'pci_stats': fake_pci,
    'cpu_allocation_ratio': 16.0,
    'ram_allocation_ratio': 1.5,
    'disk_allocation_ratio': 1.0,
    }
# FIXME(sbauza) : For compatibility checking, to be removed once we are sure
# that all computes are running latest DB version with host field in it.
fake_old_compute_node = fake_compute_node.copy()
del fake_old_compute_node['host']
# resources are passed from the virt drivers and copied into the compute_node
fake_resources = {
    'vcpus': 2,
    'memory_mb': 1024,
    'local_gb': 10,
    'cpu_info': 'fake-info',
    'vcpus_used': 1,
    'memory_mb_used': 512,
    'local_gb_used': 4,
    'numa_topology': fake_numa_topology_db_format,
    'hypervisor_type': 'fake-type',
    'hypervisor_version': 1,
    'hypervisor_hostname': 'fake-host',
    'disk_available_least': 256,
    'host_ip': fake_host_ip,
    'supported_instances': fake_supported_instances
}
fake_compute_with_resources = objects.ComputeNode(
    vcpus=fake_resources['vcpus'],
    memory_mb=fake_resources['memory_mb'],
    local_gb=fake_resources['local_gb'],
    cpu_info=fake_resources['cpu_info'],
    vcpus_used=fake_resources['vcpus_used'],
    memory_mb_used=fake_resources['memory_mb_used'],
    local_gb_used =fake_resources['local_gb_used'],
    numa_topology=fake_resources['numa_topology'],
    hypervisor_type=fake_resources['hypervisor_type'],
    hypervisor_version=fake_resources['hypervisor_version'],
    hypervisor_hostname=fake_resources['hypervisor_hostname'],
    disk_available_least=fake_resources['disk_available_least'],
    host_ip=netaddr.IPAddress(fake_resources['host_ip']),
    supported_hv_specs=fake_supported_hv_specs,
)


class _TestComputeNodeObject(object):
    def supported_hv_specs_comparator(self, expected, obj_val):
        obj_val = [inst.to_list() for inst in obj_val]
        self.assertJsonEqual(expected, obj_val)

    def pci_device_pools_comparator(self, expected, obj_val):
        if obj_val is not None:
            obj_val = obj_val.obj_to_primitive()
            self.assertJsonEqual(expected, obj_val)
        else:
            self.assertEqual(expected, obj_val)

    def comparators(self):
        return {'stats': self.assertJsonEqual,
                'host_ip': self.str_comparator,
                'supported_hv_specs': self.supported_hv_specs_comparator,
                'pci_device_pools': self.pci_device_pools_comparator,
                }

    def subs(self):
        return {'supported_hv_specs': 'supported_instances',
                'pci_device_pools': 'pci_stats'}

    def test_get_by_id(self):
        self.mox.StubOutWithMock(db, 'compute_node_get')
        db.compute_node_get(self.context, 123).AndReturn(fake_compute_node)
        self.mox.ReplayAll()
        compute = compute_node.ComputeNode.get_by_id(self.context, 123)
        self.compare_obj(compute, fake_compute_node,
                         subs=self.subs(),
                         comparators=self.comparators())
        self.assertNotIn('uuid', compute.obj_what_changed())

    @mock.patch.object(objects.Service, 'get_by_id')
    @mock.patch.object(db, 'compute_node_get')
    def test_get_by_id_with_host_field_not_in_db(self, mock_cn_get,
                                                 mock_obj_svc_get):
        fake_compute_node_with_svc_id = fake_compute_node.copy()
        fake_compute_node_with_svc_id['service_id'] = 123
        fake_compute_node_with_no_host = fake_compute_node_with_svc_id.copy()
        host = fake_compute_node_with_no_host.pop('host')
        fake_service = service.Service(id=123)
        fake_service.host = host

        mock_cn_get.return_value = fake_compute_node_with_no_host
        mock_obj_svc_get.return_value = fake_service

        compute = compute_node.ComputeNode.get_by_id(self.context, 123)
        self.compare_obj(compute, fake_compute_node_with_svc_id,
                         subs=self.subs(),
                         comparators=self.comparators())

    def test_get_by_service_id(self):
        self.mox.StubOutWithMock(db, 'compute_nodes_get_by_service_id')
        db.compute_nodes_get_by_service_id(self.context, 456).AndReturn(
            [fake_compute_node])
        self.mox.ReplayAll()
        compute = compute_node.ComputeNode.get_by_service_id(self.context, 456)
        self.compare_obj(compute, fake_compute_node,
                         subs=self.subs(),
                         comparators=self.comparators())

    @mock.patch.object(db, 'compute_node_get_by_host_and_nodename')
    def test_get_by_host_and_nodename(self, cn_get_by_h_and_n):
        cn_get_by_h_and_n.return_value = fake_compute_node

        compute = compute_node.ComputeNode.get_by_host_and_nodename(
            self.context, 'fake', 'vm.danplanet.com')
        self.compare_obj(compute, fake_compute_node,
                         subs=self.subs(),
                         comparators=self.comparators())

    @mock.patch('nova.db.compute_node_get_all_by_host')
    def test_get_first_node_by_host_for_old_compat(
            self, cn_get_all_by_host):
        another_node = fake_compute_node.copy()
        another_node['hypervisor_hostname'] = 'neverland'
        cn_get_all_by_host.return_value = [fake_compute_node, another_node]

        compute = (
            compute_node.ComputeNode.get_first_node_by_host_for_old_compat(
                self.context, 'fake')
        )
        self.compare_obj(compute, fake_compute_node,
                         subs=self.subs(),
                         comparators=self.comparators())

    @mock.patch('nova.objects.ComputeNodeList.get_all_by_host')
    def test_get_first_node_by_host_for_old_compat_not_found(
            self, cn_get_all_by_host):
        cn_get_all_by_host.side_effect = exception.ComputeHostNotFound(
            host='fake')

        self.assertRaises(
            exception.ComputeHostNotFound,
            compute_node.ComputeNode.get_first_node_by_host_for_old_compat,
            self.context, 'fake')

    @mock.patch('nova.db.compute_node_get', return_value=fake_compute_node)
    def test_create(self, mock_get):
        self.mox.StubOutWithMock(db, 'compute_node_create')
        db.compute_node_create(
            self.context,
            {
                'service_id': 456,
                'stats': fake_stats_db_format,
                'host_ip': fake_host_ip,
                'supported_instances': fake_supported_hv_specs_db_format,
                'uuid': uuidsentinel.fake_compute_node,
            }).AndReturn(fake_compute_node)
        self.mox.ReplayAll()
        compute = compute_node.ComputeNode(context=self.context)
        compute.service_id = 456
        compute.uuid = uuidsentinel.fake_compute_node
        compute.stats = fake_stats
        # NOTE (pmurray): host_ip is coerced to an IPAddress
        compute.host_ip = fake_host_ip
        compute.supported_hv_specs = fake_supported_hv_specs
        with mock.patch('oslo_utils.uuidutils.generate_uuid') as mock_gu:
            compute.create()
            self.assertFalse(mock_gu.called)
        self.compare_obj(compute, fake_compute_node,
                         subs=self.subs(),
                         comparators=self.comparators())

    @mock.patch('nova.db.compute_node_create')
    @mock.patch('oslo_utils.uuidutils.generate_uuid')
    @mock.patch('nova.db.compute_node_get', return_value=fake_compute_node)
    def test_create_allocates_uuid(self, mock_get, mock_gu, mock_create):
        mock_create.return_value = fake_compute_node
        mock_gu.return_value = fake_compute_node['uuid']
        obj = objects.ComputeNode(context=self.context)
        obj.create()
        mock_gu.assert_called_once_with()
        mock_create.assert_called_once_with(
            self.context, {'uuid': fake_compute_node['uuid']})

    @mock.patch('nova.db.compute_node_get', return_value=fake_compute_node)
    def test_recreate_fails(self, mock_get):
        self.mox.StubOutWithMock(db, 'compute_node_create')
        db.compute_node_create(
            self.context, {'service_id': 456,
                           'uuid': uuidsentinel.fake_compute_node}).AndReturn(
            fake_compute_node)
        self.mox.ReplayAll()
        compute = compute_node.ComputeNode(context=self.context)
        compute.service_id = 456
        compute.uuid = uuidsentinel.fake_compute_node
        compute.create()
        self.assertRaises(exception.ObjectActionError, compute.create)

    def test_save(self):
        self.mox.StubOutWithMock(db, 'compute_node_update')
        db.compute_node_update(
            self.context, 123,
            {
                'vcpus_used': 3,
                'stats': fake_stats_db_format,
                'host_ip': fake_host_ip,
                'supported_instances': fake_supported_hv_specs_db_format,
                'uuid': uuidsentinel.fake_compute_node,
            }).AndReturn(fake_compute_node)
        self.mox.ReplayAll()
        compute = compute_node.ComputeNode(context=self.context)
        compute.id = 123
        compute.vcpus_used = 3
        compute.stats = fake_stats
        compute.uuid = uuidsentinel.fake_compute_node
        # NOTE (pmurray): host_ip is coerced to an IPAddress
        compute.host_ip = fake_host_ip
        compute.supported_hv_specs = fake_supported_hv_specs
        compute.save()
        self.compare_obj(compute, fake_compute_node,
                         subs=self.subs(),
                         comparators=self.comparators())

    @mock.patch('nova.db.compute_node_update')
    def test_save_pci_device_pools_empty(self, mock_update):
        fake_pci = jsonutils.dumps(
            objects.PciDevicePoolList(objects=[]).obj_to_primitive())
        compute_dict = fake_compute_node.copy()
        compute_dict['pci_stats'] = fake_pci
        mock_update.return_value = compute_dict

        compute = compute_node.ComputeNode(context=self.context)
        compute.id = 123
        compute.pci_device_pools = objects.PciDevicePoolList(objects=[])
        compute.save()
        self.compare_obj(compute, compute_dict,
                         subs=self.subs(),
                         comparators=self.comparators())

        mock_update.assert_called_once_with(
            self.context, 123, {'pci_stats': fake_pci})

    @mock.patch('nova.db.compute_node_update')
    def test_save_pci_device_pools_null(self, mock_update):
        compute_dict = fake_compute_node.copy()
        compute_dict['pci_stats'] = None
        mock_update.return_value = compute_dict

        compute = compute_node.ComputeNode(context=self.context)
        compute.id = 123
        compute.pci_device_pools = None
        compute.save()
        self.compare_obj(compute, compute_dict,
                         subs=self.subs(),
                         comparators=self.comparators())

        mock_update.assert_called_once_with(
            self.context, 123, {'pci_stats': None})

    @mock.patch.object(db, 'compute_node_create',
                       return_value=fake_compute_node)
    @mock.patch.object(db, 'compute_node_get',
                       return_value=fake_compute_node)
    def test_set_id_failure(self, mock_get, db_mock):
        compute = compute_node.ComputeNode(context=self.context,
                                           uuid=fake_compute_node['uuid'])
        compute.create()
        self.assertRaises(ovo_exc.ReadOnlyFieldError, setattr,
                          compute, 'id', 124)

    def test_destroy(self):
        self.mox.StubOutWithMock(db, 'compute_node_delete')
        db.compute_node_delete(self.context, 123)
        self.mox.ReplayAll()
        compute = compute_node.ComputeNode(context=self.context)
        compute.id = 123
        compute.destroy()

    def test_get_all(self):
        self.mox.StubOutWithMock(db, 'compute_node_get_all')
        db.compute_node_get_all(self.context).AndReturn([fake_compute_node])
        self.mox.ReplayAll()
        computes = compute_node.ComputeNodeList.get_all(self.context)
        self.assertEqual(1, len(computes))
        self.compare_obj(computes[0], fake_compute_node,
                         subs=self.subs(),
                         comparators=self.comparators())

    def test_get_by_hypervisor(self):
        self.mox.StubOutWithMock(db, 'compute_node_search_by_hypervisor')
        db.compute_node_search_by_hypervisor(self.context, 'hyper').AndReturn(
            [fake_compute_node])
        self.mox.ReplayAll()
        computes = compute_node.ComputeNodeList.get_by_hypervisor(self.context,
                                                                  'hyper')
        self.assertEqual(1, len(computes))
        self.compare_obj(computes[0], fake_compute_node,
                         subs=self.subs(),
                         comparators=self.comparators())

    @mock.patch('nova.db.compute_node_get_all_by_pagination',
                return_value=[fake_compute_node])
    def test_get_by_pagination(self, fake_get_by_pagination):
        computes = compute_node.ComputeNodeList.get_by_pagination(
            self.context, limit=1, marker=1)
        self.assertEqual(1, len(computes))
        self.compare_obj(computes[0], fake_compute_node,
                         subs=self.subs(),
                         comparators=self.comparators())

    @mock.patch('nova.db.compute_nodes_get_by_service_id')
    def test__get_by_service(self, cn_get_by_svc_id):
        cn_get_by_svc_id.return_value = [fake_compute_node]
        computes = compute_node.ComputeNodeList._get_by_service(self.context,
                                                                123)
        self.assertEqual(1, len(computes))
        self.compare_obj(computes[0], fake_compute_node,
                         subs=self.subs(),
                         comparators=self.comparators())

    @mock.patch('nova.db.compute_node_get_all_by_host')
    def test_get_all_by_host(self, cn_get_all_by_host):
        cn_get_all_by_host.return_value = [fake_compute_node]
        computes = compute_node.ComputeNodeList.get_all_by_host(self.context,
                                                                'fake')
        self.assertEqual(1, len(computes))
        self.compare_obj(computes[0], fake_compute_node,
                         subs=self.subs(),
                         comparators=self.comparators())

    def test_compat_numa_topology(self):
        compute = compute_node.ComputeNode()
        versions = ovo_base.obj_tree_get_versions('ComputeNode')
        primitive = compute.obj_to_primitive(target_version='1.4',
                                             version_manifest=versions)
        self.assertNotIn('numa_topology', primitive)

    def test_compat_supported_hv_specs(self):
        compute = compute_node.ComputeNode()
        compute.supported_hv_specs = fake_supported_hv_specs
        versions = ovo_base.obj_tree_get_versions('ComputeNode')
        primitive = compute.obj_to_primitive(target_version='1.5',
                                             version_manifest=versions)
        self.assertNotIn('supported_hv_specs', primitive)

    def test_compat_host(self):
        compute = compute_node.ComputeNode()
        primitive = compute.obj_to_primitive(target_version='1.6')
        self.assertNotIn('host', primitive)

    def test_compat_pci_device_pools(self):
        compute = compute_node.ComputeNode()
        compute.pci_device_pools = fake_pci_device_pools.fake_pool_list
        versions = ovo_base.obj_tree_get_versions('ComputeNode')
        primitive = compute.obj_to_primitive(target_version='1.8',
                                             version_manifest=versions)
        self.assertNotIn('pci_device_pools', primitive)

    @mock.patch('nova.objects.Service.get_by_compute_host')
    def test_compat_service_id(self, mock_get):
        mock_get.return_value = objects.Service(id=1)
        compute = objects.ComputeNode(host='fake-host', service_id=None)
        primitive = compute.obj_to_primitive(target_version='1.12')
        self.assertEqual(1, primitive['nova_object.data']['service_id'])

    @mock.patch('nova.objects.Service.get_by_compute_host')
    def test_compat_service_id_compute_host_not_found(self, mock_get):
        mock_get.side_effect = exception.ComputeHostNotFound(host='fake-host')
        compute = objects.ComputeNode(host='fake-host', service_id=None)
        primitive = compute.obj_to_primitive(target_version='1.12')
        self.assertEqual(-1, primitive['nova_object.data']['service_id'])

    def test_update_from_virt_driver(self):
        # copy in case the update has a side effect
        resources = copy.deepcopy(fake_resources)
        compute = compute_node.ComputeNode()
        compute.update_from_virt_driver(resources)
        expected = fake_compute_with_resources
        self.assertTrue(base.obj_equal_prims(expected, compute))

    def test_update_from_virt_driver_missing_field(self):
        # NOTE(pmurray): update_from_virt_driver does not require
        # all fields to be present in resources. Validation of the
        # resources data structure would be done in a different method.
        resources = copy.deepcopy(fake_resources)
        del resources['vcpus']
        compute = compute_node.ComputeNode()
        compute.update_from_virt_driver(resources)
        expected = fake_compute_with_resources.obj_clone()
        del expected.vcpus
        self.assertTrue(base.obj_equal_prims(expected, compute))

    def test_update_from_virt_driver_extra_field(self):
        # copy in case the update has a side effect
        resources = copy.deepcopy(fake_resources)
        resources['extra_field'] = 'nonsense'
        compute = compute_node.ComputeNode()
        compute.update_from_virt_driver(resources)
        expected = fake_compute_with_resources
        self.assertTrue(base.obj_equal_prims(expected, compute))

    def test_update_from_virt_driver_bad_value(self):
        # copy in case the update has a side effect
        resources = copy.deepcopy(fake_resources)
        resources['vcpus'] = 'nonsense'
        compute = compute_node.ComputeNode()
        self.assertRaises(ValueError,
                          compute.update_from_virt_driver, resources)

    def test_compat_allocation_ratios(self):
        compute = compute_node.ComputeNode()
        primitive = compute.obj_to_primitive(target_version='1.13')
        self.assertNotIn('cpu_allocation_ratio', primitive)
        self.assertNotIn('ram_allocation_ratio', primitive)

    def test_compat_disk_allocation_ratio(self):
        compute = compute_node.ComputeNode()
        primitive = compute.obj_to_primitive(target_version='1.15')
        self.assertNotIn('disk_allocation_ratio', primitive)

    def test_compat_allocation_ratios_old_compute(self):
        self.flags(cpu_allocation_ratio=2.0, ram_allocation_ratio=3.0,
                   disk_allocation_ratio=0.9)
        compute_dict = fake_compute_node.copy()
        # old computes don't provide allocation ratios to the table
        compute_dict['cpu_allocation_ratio'] = None
        compute_dict['ram_allocation_ratio'] = None
        compute_dict['disk_allocation_ratio'] = None
        cls = objects.ComputeNode
        compute = cls._from_db_object(self.context, cls(), compute_dict)

        self.assertEqual(2.0, compute.cpu_allocation_ratio)
        self.assertEqual(3.0, compute.ram_allocation_ratio)
        self.assertEqual(0.9, compute.disk_allocation_ratio)

    def test_compat_allocation_ratios_default_values(self):
        compute_dict = fake_compute_node.copy()
        # new computes provide allocation ratios defaulted to 0.0
        compute_dict['cpu_allocation_ratio'] = 0.0
        compute_dict['ram_allocation_ratio'] = 0.0
        compute_dict['disk_allocation_ratio'] = 0.0
        cls = objects.ComputeNode
        compute = cls._from_db_object(self.context, cls(), compute_dict)

        self.assertEqual(16.0, compute.cpu_allocation_ratio)
        self.assertEqual(1.5, compute.ram_allocation_ratio)
        self.assertEqual(1.0, compute.disk_allocation_ratio)

    def test_compat_allocation_ratios_old_compute_default_values(self):
        compute_dict = fake_compute_node.copy()
        # old computes don't provide allocation ratios to the table
        compute_dict['cpu_allocation_ratio'] = None
        compute_dict['ram_allocation_ratio'] = None
        compute_dict['disk_allocation_ratio'] = None
        cls = objects.ComputeNode
        compute = cls._from_db_object(self.context, cls(), compute_dict)

        self.assertEqual(16.0, compute.cpu_allocation_ratio)
        self.assertEqual(1.5, compute.ram_allocation_ratio)
        self.assertEqual(1.0, compute.disk_allocation_ratio)


class TestComputeNodeObject(test_objects._LocalTest,
                            _TestComputeNodeObject):
    pass


class TestRemoteComputeNodeObject(test_objects._RemoteTest,
                                  _TestComputeNodeObject):
    pass
