#    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 oslo_utils import uuidutils

from nova.api.openstack.placement import lib as placement_lib
from nova import context
from nova import exception
from nova.objects import fields
from nova.objects import resource_provider as rp_obj
from nova import test
from nova.tests import fixtures
from nova.tests import uuidsentinel as uuids


def _add_inventory(rp, rc, total, **kwargs):
    kwargs.setdefault('max_unit', total)
    inv = rp_obj.Inventory(rp._context, resource_provider=rp,
                           resource_class=rc, total=total, **kwargs)
    inv.obj_set_defaults()
    rp.add_inventory(inv)


def _set_traits(rp, *traits):
    tlist = []
    for tname in traits:
        try:
            trait = rp_obj.Trait.get_by_name(rp._context, tname)
        except exception.TraitNotFound:
            trait = rp_obj.Trait(rp._context, name=tname)
            trait.create()
        tlist.append(trait)
    rp.set_traits(rp_obj.TraitList(objects=tlist))


def _allocate_from_provider(rp, rc, used):
    # NOTE(efried): Always use a random consumer UUID - we don't want to
    # override any existing allocations from the test case.
    rp_obj.AllocationList(
        rp._context, objects=[
            rp_obj.Allocation(
                rp._context, resource_provider=rp, resource_class=rc,
                consumer_id=uuidutils.generate_uuid(), used=used)]
    ).create_all()


def _provider_uuids_from_iterable(objs):
    """Return the set of resource_provider.uuid from an iterable.

    :param objs: Iterable of any object with a resource_provider attribute
                 (e.g. an AllocationRequest.resource_requests or an
                 AllocationCandidates.provider_summaries).
    """
    return set(obj.resource_provider.uuid for obj in objs)


def _find_summary_for_provider(p_sums, rp_uuid):
    for summary in p_sums:
        if summary.resource_provider.uuid == rp_uuid:
            return summary


def _find_summary_for_resource(p_sum, rc_name):
    for resource in p_sum.resources:
        if resource.resource_class == rc_name:
            return resource


class ProviderDBHelperTestCase(test.NoDBTestCase):

    USES_DB_SELF = True

    def setUp(self):
        super(ProviderDBHelperTestCase, self).setUp()
        self.useFixture(fixtures.Database())
        self.api_db = self.useFixture(fixtures.Database(database='api'))
        self.ctx = context.RequestContext('fake-user', 'fake-project')
        self.requested_resources = {
            fields.ResourceClass.VCPU: 1,
            fields.ResourceClass.MEMORY_MB: 64,
            fields.ResourceClass.DISK_GB: 1500,
        }
        # For debugging purposes, populated by _create_provider and used by
        # _validate_allocation_requests to make failure results more readable.
        self.rp_uuid_to_name = {}

    def _create_provider(self, name, *aggs):
        rp = rp_obj.ResourceProvider(self.ctx, name=name,
                                     uuid=getattr(uuids, name))
        rp.create()
        if aggs:
            rp.set_aggregates(aggs)
        self.rp_uuid_to_name[rp.uuid] = name
        return rp

    def test_get_provider_ids_matching_all(self):
        # These RPs are named based on whether we expect them to be 'incl'uded
        # or 'excl'uded in the result.

        # No inventory records.  This one should never show up in a result.
        self._create_provider('no_inventory')

        # Inventory of adequate CPU and memory, no allocations against it.
        excl_big_cm_noalloc = self._create_provider('big_cm_noalloc')
        _add_inventory(excl_big_cm_noalloc, fields.ResourceClass.VCPU, 15)
        _add_inventory(excl_big_cm_noalloc, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)

        # Adequate inventory, no allocations against it.
        incl_biginv_noalloc = self._create_provider('biginv_noalloc')
        _add_inventory(incl_biginv_noalloc, fields.ResourceClass.VCPU, 15)
        _add_inventory(incl_biginv_noalloc, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)
        _add_inventory(incl_biginv_noalloc, fields.ResourceClass.DISK_GB, 2000)

        # No allocations, but inventory unusable.  Try to hit all the possible
        # reasons for exclusion.
        # VCPU min_unit too high
        excl_badinv_min_unit = self._create_provider('badinv_min_unit')
        _add_inventory(excl_badinv_min_unit, fields.ResourceClass.VCPU, 12,
                       min_unit=6)
        _add_inventory(excl_badinv_min_unit, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)
        _add_inventory(excl_badinv_min_unit, fields.ResourceClass.DISK_GB,
                       2000)
        # MEMORY_MB max_unit too low
        excl_badinv_max_unit = self._create_provider('badinv_max_unit')
        _add_inventory(excl_badinv_max_unit, fields.ResourceClass.VCPU, 15)
        _add_inventory(excl_badinv_max_unit, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=512)
        _add_inventory(excl_badinv_max_unit, fields.ResourceClass.DISK_GB,
                       2000)
        # DISK_GB unsuitable step_size
        excl_badinv_step_size = self._create_provider('badinv_step_size')
        _add_inventory(excl_badinv_step_size, fields.ResourceClass.VCPU, 15)
        _add_inventory(excl_badinv_step_size, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)
        _add_inventory(excl_badinv_step_size, fields.ResourceClass.DISK_GB,
                       2000, step_size=7)
        # Not enough total VCPU
        excl_badinv_total = self._create_provider('badinv_total')
        _add_inventory(excl_badinv_total, fields.ResourceClass.VCPU, 4)
        _add_inventory(excl_badinv_total, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)
        _add_inventory(excl_badinv_total, fields.ResourceClass.DISK_GB, 2000)
        # Too much reserved MEMORY_MB
        excl_badinv_reserved = self._create_provider('badinv_reserved')
        _add_inventory(excl_badinv_reserved, fields.ResourceClass.VCPU, 15)
        _add_inventory(excl_badinv_reserved, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048, reserved=3500)
        _add_inventory(excl_badinv_reserved, fields.ResourceClass.DISK_GB,
                       2000)
        # DISK_GB allocation ratio blows it up
        excl_badinv_alloc_ratio = self._create_provider('badinv_alloc_ratio')
        _add_inventory(excl_badinv_alloc_ratio, fields.ResourceClass.VCPU, 15)
        _add_inventory(excl_badinv_alloc_ratio, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)
        _add_inventory(excl_badinv_alloc_ratio, fields.ResourceClass.DISK_GB,
                       2000, allocation_ratio=0.5)

        # Inventory consumed in one RC, but available in the others
        excl_1invunavail = self._create_provider('1invunavail')
        _add_inventory(excl_1invunavail, fields.ResourceClass.VCPU, 10)
        _allocate_from_provider(excl_1invunavail, fields.ResourceClass.VCPU, 7)
        _add_inventory(excl_1invunavail, fields.ResourceClass.MEMORY_MB, 4096)
        _allocate_from_provider(excl_1invunavail,
                                fields.ResourceClass.MEMORY_MB, 1024)
        _add_inventory(excl_1invunavail, fields.ResourceClass.DISK_GB, 2000)
        _allocate_from_provider(excl_1invunavail,
                                fields.ResourceClass.DISK_GB, 400)

        # Inventory all consumed
        excl_allused = self._create_provider('allused')
        _add_inventory(excl_allused, fields.ResourceClass.VCPU, 10)
        _allocate_from_provider(excl_allused, fields.ResourceClass.VCPU, 7)
        _add_inventory(excl_allused, fields.ResourceClass.MEMORY_MB, 4000)
        _allocate_from_provider(excl_allused,
                                fields.ResourceClass.MEMORY_MB, 1500)
        _allocate_from_provider(excl_allused,
                                fields.ResourceClass.MEMORY_MB, 2000)
        _add_inventory(excl_allused, fields.ResourceClass.DISK_GB, 1500)
        _allocate_from_provider(excl_allused, fields.ResourceClass.DISK_GB, 1)

        # Inventory available in requested classes, but unavailable in others
        incl_extra_full = self._create_provider('extra_full')
        _add_inventory(incl_extra_full, fields.ResourceClass.VCPU, 20)
        _allocate_from_provider(incl_extra_full, fields.ResourceClass.VCPU, 15)
        _add_inventory(incl_extra_full, fields.ResourceClass.MEMORY_MB, 4096)
        _allocate_from_provider(incl_extra_full,
                                fields.ResourceClass.MEMORY_MB, 1024)
        _add_inventory(incl_extra_full, fields.ResourceClass.DISK_GB, 2000)
        _allocate_from_provider(incl_extra_full, fields.ResourceClass.DISK_GB,
                                400)
        _add_inventory(incl_extra_full, fields.ResourceClass.PCI_DEVICE, 4)
        _allocate_from_provider(incl_extra_full,
                                fields.ResourceClass.PCI_DEVICE, 1)
        _allocate_from_provider(incl_extra_full,
                                fields.ResourceClass.PCI_DEVICE, 3)

        # Inventory available in a unrequested classes, not in requested ones
        excl_extra_avail = self._create_provider('extra_avail')
        # Incompatible step size
        _add_inventory(excl_extra_avail, fields.ResourceClass.VCPU, 10,
                       step_size=3)
        # Not enough left after reserved + used
        _add_inventory(excl_extra_avail, fields.ResourceClass.MEMORY_MB, 4096,
                       max_unit=2048, reserved=2048)
        _allocate_from_provider(excl_extra_avail,
                                fields.ResourceClass.MEMORY_MB, 1040)
        # Allocation ratio math
        _add_inventory(excl_extra_avail, fields.ResourceClass.DISK_GB, 2000,
                       allocation_ratio=0.5)
        _add_inventory(excl_extra_avail, fields.ResourceClass.IPV4_ADDRESS, 48)
        custom_special = rp_obj.ResourceClass(self.ctx, name='CUSTOM_SPECIAL')
        custom_special.create()
        _add_inventory(excl_extra_avail, 'CUSTOM_SPECIAL', 100)
        _allocate_from_provider(excl_extra_avail, 'CUSTOM_SPECIAL', 99)

        resources = {
            fields.ResourceClass.STANDARD.index(fields.ResourceClass.VCPU): 5,
            fields.ResourceClass.STANDARD.index(
                fields.ResourceClass.MEMORY_MB): 1024,
            fields.ResourceClass.STANDARD.index(
                fields.ResourceClass.DISK_GB): 1500
        }

        # Run it!
        res = rp_obj._get_provider_ids_matching_all(self.ctx, resources)

        # We should get all the incl_* RPs
        expected = [incl_biginv_noalloc, incl_extra_full]

        self.assertEqual(set(rp.id for rp in expected), set(res))


class AllocationCandidatesTestCase(ProviderDBHelperTestCase):
    """Tests a variety of scenarios with both shared and non-shared resource
    providers that the AllocationCandidates.get_by_requests() method returns a
    set of alternative allocation requests and provider summaries that may be
    used by the scheduler to sort/weigh the options it has for claiming
    resources against providers.
    """

    def setUp(self):
        super(AllocationCandidatesTestCase, self).setUp()
        self.requested_resources = {
            fields.ResourceClass.VCPU: 1,
            fields.ResourceClass.MEMORY_MB: 64,
            fields.ResourceClass.DISK_GB: 1500,
        }
        # For debugging purposes, populated by _create_provider and used by
        # _validate_allocation_requests to make failure results more readable.
        self.rp_uuid_to_name = {}

    def _get_allocation_candidates(self, requests=None):
        if requests is None:
            requests = [placement_lib.RequestGroup(
                use_same_provider=False,
                resources=self.requested_resources)]
        return rp_obj.AllocationCandidates.get_by_requests(self.ctx, requests)

    def _validate_allocation_requests(self, expected, candidates):
        """Assert correctness of allocation requests in allocation candidates.

        This is set up to make it easy for the caller to specify the expected
        result, to make that expected structure readable for someone looking at
        the test case, and to make test failures readable for debugging.

        :param expected: A list of lists of tuples representing the expected
                         allocation requests, of the form:
             [
                [(resource_provider_name, resource_class_name, resource_count),
                 ...,
                ],
                ...
             ]
        :param candidates: The result from AllocationCandidates.get_by_requests
        """
        # Extract/convert allocation requests from candidates
        observed = []
        for ar in candidates.allocation_requests:
            rrs = []
            for rr in ar.resource_requests:
                rrs.append((self.rp_uuid_to_name[rr.resource_provider.uuid],
                            rr.resource_class, rr.amount))
            rrs.sort()
            observed.append(rrs)
        observed.sort()

        # Sort the guts of the expected structure
        for rr in expected:
            rr.sort()
        expected.sort()

        # Now we ought to be able to compare them
        self.assertEqual(expected, observed)

    def test_all_local(self):
        """Create some resource providers that can satisfy the request for
        resources with local (non-shared) resources and verify that the
        allocation requests returned by AllocationCandidates correspond with
        each of these resource providers.
        """
        # Create three compute node providers with VCPU, RAM and local disk
        for name in ('cn1', 'cn2', 'cn3'):
            cn = self._create_provider(name)
            _add_inventory(cn, fields.ResourceClass.VCPU, 24,
                           allocation_ratio=16.0)
            _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 32768,
                           min_unit=64, step_size=64, allocation_ratio=1.5)
            total_gb = 1000 if name == 'cn3' else 2000
            _add_inventory(cn, fields.ResourceClass.DISK_GB, total_gb,
                           reserved=100, min_unit=10, step_size=10,
                           allocation_ratio=1.0)

        # Ask for the alternative placement possibilities and verify each
        # provider is returned
        alloc_cands = self._get_allocation_candidates()

        # Verify the provider summary information indicates 0 usage and
        # capacity calculated from above inventory numbers for both compute
        # nodes
        # TODO(efried): Come up with a terse & readable way to validate
        # provider summaries
        p_sums = alloc_cands.provider_summaries

        self.assertEqual(set([uuids.cn1, uuids.cn2]),
                         _provider_uuids_from_iterable(p_sums))

        cn1_p_sum = _find_summary_for_provider(p_sums, uuids.cn1)
        self.assertIsNotNone(cn1_p_sum)
        self.assertEqual(3, len(cn1_p_sum.resources))

        cn1_p_sum_vcpu = _find_summary_for_resource(cn1_p_sum, 'VCPU')
        self.assertIsNotNone(cn1_p_sum_vcpu)

        expected_capacity = (24 * 16.0)
        self.assertEqual(expected_capacity, cn1_p_sum_vcpu.capacity)
        self.assertEqual(0, cn1_p_sum_vcpu.used)

        # Let's verify the disk for the second compute node
        cn2_p_sum = _find_summary_for_provider(p_sums, uuids.cn2)
        self.assertIsNotNone(cn2_p_sum)
        self.assertEqual(3, len(cn2_p_sum.resources))

        cn2_p_sum_disk = _find_summary_for_resource(cn2_p_sum, 'DISK_GB')
        self.assertIsNotNone(cn2_p_sum_disk)

        expected_capacity = ((2000 - 100) * 1.0)
        self.assertEqual(expected_capacity, cn2_p_sum_disk.capacity)
        self.assertEqual(0, cn2_p_sum_disk.used)

        # Verify the allocation requests that are returned. There should be 2
        # allocation requests, one for each compute node, containing 3
        # resources in each allocation request, one each for VCPU, RAM, and
        # disk. The amounts of the requests should correspond to the requested
        # resource amounts in the filter:resources dict passed to
        # AllocationCandidates.get_by_requests().
        expected = [
            [('cn1', fields.ResourceClass.VCPU, 1),
             ('cn1', fields.ResourceClass.MEMORY_MB, 64),
             ('cn1', fields.ResourceClass.DISK_GB, 1500)],
            [('cn2', fields.ResourceClass.VCPU, 1),
             ('cn2', fields.ResourceClass.MEMORY_MB, 64),
             ('cn2', fields.ResourceClass.DISK_GB, 1500)],
        ]
        self._validate_allocation_requests(expected, alloc_cands)

    def test_local_with_shared_disk(self):
        """Create some resource providers that can satisfy the request for
        resources with local VCPU and MEMORY_MB but rely on a shared storage
        pool to satisfy DISK_GB and verify that the allocation requests
        returned by AllocationCandidates have DISK_GB served up by the shared
        storage pool resource provider and VCPU/MEMORY_MB by the compute node
        providers
        """
        # The aggregate that will be associated to everything...
        agg_uuid = uuids.agg

        # Create two compute node providers with VCPU, RAM and NO local disk,
        # associated with the aggregate.
        for name in ('cn1', 'cn2'):
            cn = self._create_provider(name, agg_uuid)
            _add_inventory(cn, fields.ResourceClass.VCPU, 24,
                           allocation_ratio=16.0)
            _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 1024,
                           min_unit=64, allocation_ratio=1.5)

        # Create the shared storage pool, asociated with the same aggregate
        ss = self._create_provider('shared storage', agg_uuid)

        # Give the shared storage pool some inventory of DISK_GB
        _add_inventory(ss, fields.ResourceClass.DISK_GB, 2000, reserved=100,
                       min_unit=10)

        # Mark the shared storage pool as having inventory shared among any
        # provider associated via aggregate
        _set_traits(ss, "MISC_SHARES_VIA_AGGREGATE")

        # Ask for the alternative placement possibilities and verify each
        # compute node provider is listed in the allocation requests as well as
        # the shared storage pool provider
        alloc_cands = self._get_allocation_candidates()

        # Verify the provider summary information indicates 0 usage and
        # capacity calculated from above inventory numbers for both compute
        # nodes
        # TODO(efried): Come up with a terse & readable way to validate
        # provider summaries
        p_sums = alloc_cands.provider_summaries

        self.assertEqual(set([uuids.cn1, uuids.cn2, ss.uuid]),
                         _provider_uuids_from_iterable(p_sums))

        cn1_p_sum = _find_summary_for_provider(p_sums, uuids.cn1)
        self.assertIsNotNone(cn1_p_sum)
        self.assertEqual(2, len(cn1_p_sum.resources))

        cn1_p_sum_vcpu = _find_summary_for_resource(cn1_p_sum, 'VCPU')
        self.assertIsNotNone(cn1_p_sum_vcpu)

        expected_capacity = (24 * 16.0)
        self.assertEqual(expected_capacity, cn1_p_sum_vcpu.capacity)
        self.assertEqual(0, cn1_p_sum_vcpu.used)

        # Let's verify memory for the second compute node
        cn2_p_sum = _find_summary_for_provider(p_sums, uuids.cn2)
        self.assertIsNotNone(cn2_p_sum)
        self.assertEqual(2, len(cn2_p_sum.resources))

        cn2_p_sum_ram = _find_summary_for_resource(cn2_p_sum, 'MEMORY_MB')
        self.assertIsNotNone(cn2_p_sum_ram)

        expected_capacity = (1024 * 1.5)
        self.assertEqual(expected_capacity, cn2_p_sum_ram.capacity)
        self.assertEqual(0, cn2_p_sum_ram.used)

        # Let's verify only diks for the shared storage pool
        ss_p_sum = _find_summary_for_provider(p_sums, ss.uuid)
        self.assertIsNotNone(ss_p_sum)
        self.assertEqual(1, len(ss_p_sum.resources))

        ss_p_sum_disk = _find_summary_for_resource(ss_p_sum, 'DISK_GB')
        self.assertIsNotNone(ss_p_sum_disk)

        expected_capacity = ((2000 - 100) * 1.0)
        self.assertEqual(expected_capacity, ss_p_sum_disk.capacity)
        self.assertEqual(0, ss_p_sum_disk.used)

        # Verify the allocation requests that are returned. There should be 2
        # allocation requests, one for each compute node, containing 3
        # resources in each allocation request, one each for VCPU, RAM, and
        # disk. The amounts of the requests should correspond to the requested
        # resource amounts in the filter:resources dict passed to
        # AllocationCandidates.get_by_requests(). The providers for VCPU and
        # MEMORY_MB should be the compute nodes while the provider for the
        # DISK_GB should be the shared storage pool
        expected = [
            [('cn1', fields.ResourceClass.VCPU, 1),
             ('cn1', fields.ResourceClass.MEMORY_MB, 64),
             ('shared storage', fields.ResourceClass.DISK_GB, 1500)],
            [('cn2', fields.ResourceClass.VCPU, 1),
             ('cn2', fields.ResourceClass.MEMORY_MB, 64),
             ('shared storage', fields.ResourceClass.DISK_GB, 1500)],
        ]
        self._validate_allocation_requests(expected, alloc_cands)

        # Test for bug #1705071. We query for allocation candidates with a
        # request for ONLY the DISK_GB (the resource that is shared with
        # compute nodes) and no VCPU/MEMORY_MB. Before the fix for bug
        # #1705071, this resulted in a KeyError

        alloc_cands = self._get_allocation_candidates(
            requests=[placement_lib.RequestGroup(
                use_same_provider=False,
                resources={
                    'DISK_GB': 10,
                }
            )]
        )

        # We should only have provider summary information for the sharing
        # storage provider, since that's the only provider that can be
        # allocated against for this request.  In the future, we may look into
        # returning the shared-with providers in the provider summaries, but
        # that's a distant possibility.
        self.assertEqual(
            set([ss.uuid]),
            _provider_uuids_from_iterable(alloc_cands.provider_summaries))

        # The allocation_requests will only include the shared storage
        # provider because the only thing we're requesting to allocate is
        # against the provider of DISK_GB, which happens to be the shared
        # storage provider.
        expected = [[('shared storage', fields.ResourceClass.DISK_GB, 10)]]
        self._validate_allocation_requests(expected, alloc_cands)

    def test_local_with_shared_custom_resource(self):
        """Create some resource providers that can satisfy the request for
        resources with local VCPU and MEMORY_MB but rely on a shared resource
        provider to satisfy a custom resource requirement and verify that the
        allocation requests returned by AllocationCandidates have the custom
        resource served up by the shared custom resource provider and
        VCPU/MEMORY_MB by the compute node providers
        """
        # The aggregate that will be associated to everything...
        agg_uuid = uuids.agg

        # Create two compute node providers with VCPU, RAM and NO local
        # CUSTOM_MAGIC resources, associated with the aggregate.
        for name in ('cn1', 'cn2'):
            cn = self._create_provider(name, agg_uuid)
            _add_inventory(cn, fields.ResourceClass.VCPU, 24,
                           allocation_ratio=16.0)
            _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 1024,
                           min_unit=64, allocation_ratio=1.5)

        # Create a custom resource called MAGIC
        magic_rc = rp_obj.ResourceClass(
            self.ctx,
            name='CUSTOM_MAGIC',
        )
        magic_rc.create()

        # Create the shared provider that serves CUSTOM_MAGIC, associated with
        # the same aggregate
        magic_p = self._create_provider('shared custom resource provider',
                                        agg_uuid)
        _add_inventory(magic_p, magic_rc.name, 2048, reserved=1024,
                       min_unit=10)

        # Mark the magic provider as having inventory shared among any provider
        # associated via aggregate
        _set_traits(magic_p, "MISC_SHARES_VIA_AGGREGATE")

        # The resources we will request
        requested_resources = {
            fields.ResourceClass.VCPU: 1,
            fields.ResourceClass.MEMORY_MB: 64,
            magic_rc.name: 512,
        }

        alloc_cands = self._get_allocation_candidates(
            requests=[placement_lib.RequestGroup(
                use_same_provider=False, resources=requested_resources)])

        # Verify the allocation requests that are returned. There should be 2
        # allocation requests, one for each compute node, containing 3
        # resources in each allocation request, one each for VCPU, RAM, and
        # MAGIC. The amounts of the requests should correspond to the requested
        # resource amounts in the filter:resources dict passed to
        # AllocationCandidates.get_by_requests(). The providers for VCPU and
        # MEMORY_MB should be the compute nodes while the provider for the
        # MAGIC should be the shared custom resource provider.
        expected = [
            [('cn1', fields.ResourceClass.VCPU, 1),
             ('cn1', fields.ResourceClass.MEMORY_MB, 64),
             ('shared custom resource provider', magic_rc.name, 512)],
            [('cn2', fields.ResourceClass.VCPU, 1),
             ('cn2', fields.ResourceClass.MEMORY_MB, 64),
             ('shared custom resource provider', magic_rc.name, 512)],
        ]
        self._validate_allocation_requests(expected, alloc_cands)

    def test_mix_local_and_shared(self):
        # The aggregate that will be associated to shared storage pool
        agg_uuid = uuids.agg

        # Create three compute node providers with VCPU and RAM, but only
        # the third compute node has DISK. The first two computes will
        # share the storage from the shared storage pool.
        for name in ('cn1', 'cn2', 'cn3'):
            cn = self._create_provider(name)
            _add_inventory(cn, fields.ResourceClass.VCPU, 24,
                           allocation_ratio=16.0)
            _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 1024,
                           min_unit=64, allocation_ratio=1.5)
            if name == 'cn3':
                _add_inventory(cn, fields.ResourceClass.DISK_GB, 2000,
                               reserved=100, min_unit=10)
            else:
                cn.set_aggregates([agg_uuid])

        # Create the shared storage pool
        ss = self._create_provider('shared storage')

        # Give the shared storage pool some inventory of DISK_GB
        _add_inventory(ss, fields.ResourceClass.DISK_GB, 2000, reserved=100,
                       min_unit=10)

        _set_traits(ss, "MISC_SHARES_VIA_AGGREGATE")

        # Put the ss RP in the same aggregate as the first two compute nodes
        ss.set_aggregates([agg_uuid])

        alloc_cands = self._get_allocation_candidates()

        # Expect cn1, cn2, cn3 and ss in the summaries
        self.assertEqual(
            set([uuids.cn1, uuids.cn2, ss.uuid, uuids.cn3]),
            _provider_uuids_from_iterable(alloc_cands.provider_summaries))

        # Expect three allocation requests: (cn1, ss), (cn2, ss), (cn3)
        expected = [
            [('cn1', fields.ResourceClass.VCPU, 1),
             ('cn1', fields.ResourceClass.MEMORY_MB, 64),
             ('shared storage', fields.ResourceClass.DISK_GB, 1500)],
            [('cn2', fields.ResourceClass.VCPU, 1),
             ('cn2', fields.ResourceClass.MEMORY_MB, 64),
             ('shared storage', fields.ResourceClass.DISK_GB, 1500)],
            [('cn3', fields.ResourceClass.VCPU, 1),
             ('cn3', fields.ResourceClass.MEMORY_MB, 64),
             ('cn3', fields.ResourceClass.DISK_GB, 1500)],
        ]
        self._validate_allocation_requests(expected, alloc_cands)

    def test_common_rc(self):
        """Candidates when cn and shared have inventory in the same class."""
        cn = self._create_provider('cn', uuids.agg1)
        _add_inventory(cn, fields.ResourceClass.VCPU, 24)
        _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 2048)
        _add_inventory(cn, fields.ResourceClass.DISK_GB, 1600)

        ss = self._create_provider('ss', uuids.agg1)
        _set_traits(ss, "MISC_SHARES_VIA_AGGREGATE")
        _add_inventory(ss, fields.ResourceClass.DISK_GB, 1600)

        alloc_cands = self._get_allocation_candidates()

        # One allocation_request should have cn + ss; the other should have
        # just the cn.
        expected = [
            [('cn', fields.ResourceClass.VCPU, 1),
             ('cn', fields.ResourceClass.MEMORY_MB, 64),
             ('cn', fields.ResourceClass.DISK_GB, 1500)],
            # TODO(efried): Due to bug #1724613, the cn + ss candidate is not
            # returned.  Uncomment this when the bug is fixed.
            # [('cn', fields.ResourceClass.VCPU, 1),
            #  ('cn', fields.ResourceClass.MEMORY_MB, 64),
            #  ('ss', fields.ResourceClass.DISK_GB, 1500)],
        ]

        self._validate_allocation_requests(expected, alloc_cands)

    def test_common_rc_traits_split(self):
        """Validate filters when traits are split across cn and shared RPs."""
        # NOTE(efried): This test case only applies to the scenario where we're
        # requesting resources via the RequestGroup where
        # use_same_provider=False

        cn = self._create_provider('cn', uuids.agg1)
        _add_inventory(cn, fields.ResourceClass.VCPU, 24)
        _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 2048)
        _add_inventory(cn, fields.ResourceClass.DISK_GB, 1600)
        # The compute node's disk is SSD
        _set_traits(cn, 'HW_CPU_X86_SSE', 'STORAGE_DISK_SSD')

        ss = self._create_provider('ss', uuids.agg1)
        _add_inventory(ss, fields.ResourceClass.DISK_GB, 1600)
        # The shared storage's disk is RAID
        _set_traits(ss, 'MISC_SHARES_VIA_AGGREGATE', 'CUSTOM_RAID')

        alloc_cands = rp_obj.AllocationCandidates.get_by_requests(
            self.ctx, [
                placement_lib.RequestGroup(
                    use_same_provider=False,
                    resources=self.requested_resources,
                    required_traits=set(['HW_CPU_X86_SSE', 'STORAGE_DISK_SSD',
                                         'CUSTOM_RAID'])
                )
            ]
        )

        # TODO(efried): Okay, bear with me here:
        # TODO(efried): Bug #1724633: we'd *like* to get no candidates, because
        # there's no single DISK_GB resource with both STORAGE_DISK_SSD and
        # CUSTOM_RAID traits.  So this is the ideal expected value:
        # expected = []
        # TODO(efried): But under the design as currently conceived, we would
        # get the cn + ss candidate, because that combination satisfies both
        # traits:
        # expected = [
        #     [('cn', fields.ResourceClass.VCPU, 1),
        #      ('cn', fields.ResourceClass.MEMORY_MB, 64),
        #      ('ss', fields.ResourceClass.DISK_GB, 1500)],
        # ]
        # TODO(efried): However, until https://review.openstack.org/#/c/479766/
        # lands, the traits are ignored, so this behaves just like
        # test_common_rc above, which is subject to bug #1724613:
        expected = [
            [('cn', fields.ResourceClass.VCPU, 1),
             ('cn', fields.ResourceClass.MEMORY_MB, 64),
             ('cn', fields.ResourceClass.DISK_GB, 1500)],
        ]

        self._validate_allocation_requests(expected, alloc_cands)

    def test_all_sharing_providers(self):
        ss1 = self._create_provider('ss1', uuids.agg1)
        _set_traits(ss1, "MISC_SHARES_VIA_AGGREGATE")
        _add_inventory(ss1, fields.ResourceClass.IPV4_ADDRESS, 24)
        _add_inventory(ss1, fields.ResourceClass.SRIOV_NET_VF, 16)
        _add_inventory(ss1, fields.ResourceClass.DISK_GB, 1600)

        ss2 = self._create_provider('ss2', uuids.agg1)
        _set_traits(ss2, "MISC_SHARES_VIA_AGGREGATE")
        _add_inventory(ss2, fields.ResourceClass.DISK_GB, 1600)

        alloc_cands = self._get_allocation_candidates(requests=[
            placement_lib.RequestGroup(
                use_same_provider=False,
                resources={
                    'IPV4_ADDRESS': 2,
                    'SRIOV_NET_VF': 1,
                    'DISK_GB': 1500,
                }
            )]
        )

        # TODO(efried): Bug https://bugs.launchpad.net/nova/+bug/1730730
        # We expect two candidates: one that gets all the resources from ss1;
        # and one that gets the DISK_GB from ss2 and the rest from ss1:
        # expected = [
        #     [('ss1', fields.ResourceClass.IPV4_ADDRESS, 2),
        #      ('ss1', fields.ResourceClass.SRIOV_NET_VF, 1),
        #      ('ss1', fields.ResourceClass.DISK_GB, 1500)],
        #     [('ss1', fields.ResourceClass.IPV4_ADDRESS, 2),
        #      ('ss1', fields.ResourceClass.SRIOV_NET_VF, 1),
        #      ('ss2', fields.ResourceClass.DISK_GB, 1500)],
        # ]
        # But here's what we're actually seeing:
        # One with all the resources coming from ss1
        expected = [
            [('ss1', fields.ResourceClass.IPV4_ADDRESS, 2),
             ('ss1', fields.ResourceClass.SRIOV_NET_VF, 1),
             ('ss1', fields.ResourceClass.DISK_GB, 1500)],
        # One with the DISK_GB coming from ss2 and IPV4_ADDRESS coming from ss1
            [('ss1', fields.ResourceClass.IPV4_ADDRESS, 2),
             ('ss2', fields.ResourceClass.DISK_GB, 1500)],
        # One with the DISK_GB coming from ss2 and SRIOV_NET_VF coming from ss1
            [('ss1', fields.ResourceClass.SRIOV_NET_VF, 1),
             ('ss2', fields.ResourceClass.DISK_GB, 1500)],
        # Of special brokenness are those last two, which don't even have all
        # of the requested resources:
        ]
        self._validate_allocation_requests(expected, alloc_cands)

#    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 oslo_utils import uuidutils

from nova.api.openstack.placement import lib as placement_lib
from nova import context
from nova import exception
from nova.objects import fields
from nova.objects import resource_provider as rp_obj
from nova import test
from nova.tests import fixtures
from nova.tests import uuidsentinel as uuids


def _add_inventory(rp, rc, total, **kwargs):
    kwargs.setdefault('max_unit', total)
    inv = rp_obj.Inventory(rp._context, resource_provider=rp,
                           resource_class=rc, total=total, **kwargs)
    inv.obj_set_defaults()
    rp.add_inventory(inv)


def _set_traits(rp, *traits):
    tlist = []
    for tname in traits:
        try:
            trait = rp_obj.Trait.get_by_name(rp._context, tname)
        except exception.TraitNotFound:
            trait = rp_obj.Trait(rp._context, name=tname)
            trait.create()
        tlist.append(trait)
    rp.set_traits(rp_obj.TraitList(objects=tlist))


def _allocate_from_provider(rp, rc, used):
    # NOTE(efried): Always use a random consumer UUID - we don't want to
    # override any existing allocations from the test case.
    rp_obj.AllocationList(
        rp._context, objects=[
            rp_obj.Allocation(
                rp._context, resource_provider=rp, resource_class=rc,
                consumer_id=uuidutils.generate_uuid(), used=used)]
    ).create_all()


def _provider_uuids_from_iterable(objs):
    """Return the set of resource_provider.uuid from an iterable.

    :param objs: Iterable of any object with a resource_provider attribute
                 (e.g. an AllocationRequest.resource_requests or an
                 AllocationCandidates.provider_summaries).
    """
    return set(obj.resource_provider.uuid for obj in objs)


def _find_summary_for_provider(p_sums, rp_uuid):
    for summary in p_sums:
        if summary.resource_provider.uuid == rp_uuid:
            return summary


def _find_summary_for_resource(p_sum, rc_name):
    for resource in p_sum.resources:
        if resource.resource_class == rc_name:
            return resource


class ProviderDBHelperTestCase(test.NoDBTestCase):

    USES_DB_SELF = True

    def setUp(self):
        super(ProviderDBHelperTestCase, self).setUp()
        self.useFixture(fixtures.Database())
        self.api_db = self.useFixture(fixtures.Database(database='api'))
        self.ctx = context.RequestContext('fake-user', 'fake-project')
        self.requested_resources = {
            fields.ResourceClass.VCPU: 1,
            fields.ResourceClass.MEMORY_MB: 64,
            fields.ResourceClass.DISK_GB: 1500,
        }
        # For debugging purposes, populated by _create_provider and used by
        # _validate_allocation_requests to make failure results more readable.
        self.rp_uuid_to_name = {}

    def _create_provider(self, name, *aggs):
        rp = rp_obj.ResourceProvider(self.ctx, name=name,
                                     uuid=getattr(uuids, name))
        rp.create()
        if aggs:
            rp.set_aggregates(aggs)
        self.rp_uuid_to_name[rp.uuid] = name
        return rp

    def test_get_provider_ids_matching_all(self):
        # These RPs are named based on whether we expect them to be 'incl'uded
        # or 'excl'uded in the result.

        # No inventory records.  This one should never show up in a result.
        self._create_provider('no_inventory')

        # Inventory of adequate CPU and memory, no allocations against it.
        excl_big_cm_noalloc = self._create_provider('big_cm_noalloc')
        _add_inventory(excl_big_cm_noalloc, fields.ResourceClass.VCPU, 15)
        _add_inventory(excl_big_cm_noalloc, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)

        # Adequate inventory, no allocations against it.
        incl_biginv_noalloc = self._create_provider('biginv_noalloc')
        _add_inventory(incl_biginv_noalloc, fields.ResourceClass.VCPU, 15)
        _add_inventory(incl_biginv_noalloc, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)
        _add_inventory(incl_biginv_noalloc, fields.ResourceClass.DISK_GB, 2000)

        # No allocations, but inventory unusable.  Try to hit all the possible
        # reasons for exclusion.
        # VCPU min_unit too high
        excl_badinv_min_unit = self._create_provider('badinv_min_unit')
        _add_inventory(excl_badinv_min_unit, fields.ResourceClass.VCPU, 12,
                       min_unit=6)
        _add_inventory(excl_badinv_min_unit, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)
        _add_inventory(excl_badinv_min_unit, fields.ResourceClass.DISK_GB,
                       2000)
        # MEMORY_MB max_unit too low
        excl_badinv_max_unit = self._create_provider('badinv_max_unit')
        _add_inventory(excl_badinv_max_unit, fields.ResourceClass.VCPU, 15)
        _add_inventory(excl_badinv_max_unit, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=512)
        _add_inventory(excl_badinv_max_unit, fields.ResourceClass.DISK_GB,
                       2000)
        # DISK_GB unsuitable step_size
        excl_badinv_step_size = self._create_provider('badinv_step_size')
        _add_inventory(excl_badinv_step_size, fields.ResourceClass.VCPU, 15)
        _add_inventory(excl_badinv_step_size, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)
        _add_inventory(excl_badinv_step_size, fields.ResourceClass.DISK_GB,
                       2000, step_size=7)
        # Not enough total VCPU
        excl_badinv_total = self._create_provider('badinv_total')
        _add_inventory(excl_badinv_total, fields.ResourceClass.VCPU, 4)
        _add_inventory(excl_badinv_total, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)
        _add_inventory(excl_badinv_total, fields.ResourceClass.DISK_GB, 2000)
        # Too much reserved MEMORY_MB
        excl_badinv_reserved = self._create_provider('badinv_reserved')
        _add_inventory(excl_badinv_reserved, fields.ResourceClass.VCPU, 15)
        _add_inventory(excl_badinv_reserved, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048, reserved=3500)
        _add_inventory(excl_badinv_reserved, fields.ResourceClass.DISK_GB,
                       2000)
        # DISK_GB allocation ratio blows it up
        excl_badinv_alloc_ratio = self._create_provider('badinv_alloc_ratio')
        _add_inventory(excl_badinv_alloc_ratio, fields.ResourceClass.VCPU, 15)
        _add_inventory(excl_badinv_alloc_ratio, fields.ResourceClass.MEMORY_MB,
                       4096, max_unit=2048)
        _add_inventory(excl_badinv_alloc_ratio, fields.ResourceClass.DISK_GB,
                       2000, allocation_ratio=0.5)

        # Inventory consumed in one RC, but available in the others
        excl_1invunavail = self._create_provider('1invunavail')
        _add_inventory(excl_1invunavail, fields.ResourceClass.VCPU, 10)
        _allocate_from_provider(excl_1invunavail, fields.ResourceClass.VCPU, 7)
        _add_inventory(excl_1invunavail, fields.ResourceClass.MEMORY_MB, 4096)
        _allocate_from_provider(excl_1invunavail,
                                fields.ResourceClass.MEMORY_MB, 1024)
        _add_inventory(excl_1invunavail, fields.ResourceClass.DISK_GB, 2000)
        _allocate_from_provider(excl_1invunavail,
                                fields.ResourceClass.DISK_GB, 400)

        # Inventory all consumed
        excl_allused = self._create_provider('allused')
        _add_inventory(excl_allused, fields.ResourceClass.VCPU, 10)
        _allocate_from_provider(excl_allused, fields.ResourceClass.VCPU, 7)
        _add_inventory(excl_allused, fields.ResourceClass.MEMORY_MB, 4000)
        _allocate_from_provider(excl_allused,
                                fields.ResourceClass.MEMORY_MB, 1500)
        _allocate_from_provider(excl_allused,
                                fields.ResourceClass.MEMORY_MB, 2000)
        _add_inventory(excl_allused, fields.ResourceClass.DISK_GB, 1500)
        _allocate_from_provider(excl_allused, fields.ResourceClass.DISK_GB, 1)

        # Inventory available in requested classes, but unavailable in others
        incl_extra_full = self._create_provider('extra_full')
        _add_inventory(incl_extra_full, fields.ResourceClass.VCPU, 20)
        _allocate_from_provider(incl_extra_full, fields.ResourceClass.VCPU, 15)
        _add_inventory(incl_extra_full, fields.ResourceClass.MEMORY_MB, 4096)
        _allocate_from_provider(incl_extra_full,
                                fields.ResourceClass.MEMORY_MB, 1024)
        _add_inventory(incl_extra_full, fields.ResourceClass.DISK_GB, 2000)
        _allocate_from_provider(incl_extra_full, fields.ResourceClass.DISK_GB,
                                400)
        _add_inventory(incl_extra_full, fields.ResourceClass.PCI_DEVICE, 4)
        _allocate_from_provider(incl_extra_full,
                                fields.ResourceClass.PCI_DEVICE, 1)
        _allocate_from_provider(incl_extra_full,
                                fields.ResourceClass.PCI_DEVICE, 3)

        # Inventory available in a unrequested classes, not in requested ones
        excl_extra_avail = self._create_provider('extra_avail')
        # Incompatible step size
        _add_inventory(excl_extra_avail, fields.ResourceClass.VCPU, 10,
                       step_size=3)
        # Not enough left after reserved + used
        _add_inventory(excl_extra_avail, fields.ResourceClass.MEMORY_MB, 4096,
                       max_unit=2048, reserved=2048)
        _allocate_from_provider(excl_extra_avail,
                                fields.ResourceClass.MEMORY_MB, 1040)
        # Allocation ratio math
        _add_inventory(excl_extra_avail, fields.ResourceClass.DISK_GB, 2000,
                       allocation_ratio=0.5)
        _add_inventory(excl_extra_avail, fields.ResourceClass.IPV4_ADDRESS, 48)
        custom_special = rp_obj.ResourceClass(self.ctx, name='CUSTOM_SPECIAL')
        custom_special.create()
        _add_inventory(excl_extra_avail, 'CUSTOM_SPECIAL', 100)
        _allocate_from_provider(excl_extra_avail, 'CUSTOM_SPECIAL', 99)

        resources = {
            fields.ResourceClass.STANDARD.index(fields.ResourceClass.VCPU): 5,
            fields.ResourceClass.STANDARD.index(
                fields.ResourceClass.MEMORY_MB): 1024,
            fields.ResourceClass.STANDARD.index(
                fields.ResourceClass.DISK_GB): 1500
        }

        # Run it!
        res = rp_obj._get_provider_ids_matching_all(self.ctx, resources)

        # We should get all the incl_* RPs
        expected = [incl_biginv_noalloc, incl_extra_full]

        self.assertEqual(set(rp.id for rp in expected), set(res))


class AllocationCandidatesTestCase(ProviderDBHelperTestCase):
    """Tests a variety of scenarios with both shared and non-shared resource
    providers that the AllocationCandidates.get_by_requests() method returns a
    set of alternative allocation requests and provider summaries that may be
    used by the scheduler to sort/weigh the options it has for claiming
    resources against providers.
    """

    def setUp(self):
        super(AllocationCandidatesTestCase, self).setUp()
        self.requested_resources = {
            fields.ResourceClass.VCPU: 1,
            fields.ResourceClass.MEMORY_MB: 64,
            fields.ResourceClass.DISK_GB: 1500,
        }
        # For debugging purposes, populated by _create_provider and used by
        # _validate_allocation_requests to make failure results more readable.
        self.rp_uuid_to_name = {}

    def _get_allocation_candidates(self, requests=None):
        if requests is None:
            requests = [placement_lib.RequestGroup(
                use_same_provider=False,
                resources=self.requested_resources)]
        return rp_obj.AllocationCandidates.get_by_requests(self.ctx, requests)

    def _validate_allocation_requests(self, expected, candidates):
        """Assert correctness of allocation requests in allocation candidates.

        This is set up to make it easy for the caller to specify the expected
        result, to make that expected structure readable for someone looking at
        the test case, and to make test failures readable for debugging.

        :param expected: A list of lists of tuples representing the expected
                         allocation requests, of the form:
             [
                [(resource_provider_name, resource_class_name, resource_count),
                 ...,
                ],
                ...
             ]
        :param candidates: The result from AllocationCandidates.get_by_requests
        """
        # Extract/convert allocation requests from candidates
        observed = []
        for ar in candidates.allocation_requests:
            rrs = []
            for rr in ar.resource_requests:
                rrs.append((self.rp_uuid_to_name[rr.resource_provider.uuid],
                            rr.resource_class, rr.amount))
            rrs.sort()
            observed.append(rrs)
        observed.sort()

        # Sort the guts of the expected structure
        for rr in expected:
            rr.sort()
        expected.sort()

        # Now we ought to be able to compare them
        self.assertEqual(expected, observed)

    def test_all_local(self):
        """Create some resource providers that can satisfy the request for
        resources with local (non-shared) resources and verify that the
        allocation requests returned by AllocationCandidates correspond with
        each of these resource providers.
        """
        # Create three compute node providers with VCPU, RAM and local disk
        for name in ('cn1', 'cn2', 'cn3'):
            cn = self._create_provider(name)
            _add_inventory(cn, fields.ResourceClass.VCPU, 24,
                           allocation_ratio=16.0)
            _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 32768,
                           min_unit=64, step_size=64, allocation_ratio=1.5)
            total_gb = 1000 if name == 'cn3' else 2000
            _add_inventory(cn, fields.ResourceClass.DISK_GB, total_gb,
                           reserved=100, min_unit=10, step_size=10,
                           allocation_ratio=1.0)

        # Ask for the alternative placement possibilities and verify each
        # provider is returned
        alloc_cands = self._get_allocation_candidates()

        # Verify the provider summary information indicates 0 usage and
        # capacity calculated from above inventory numbers for both compute
        # nodes
        # TODO(efried): Come up with a terse & readable way to validate
        # provider summaries
        p_sums = alloc_cands.provider_summaries

        self.assertEqual(set([uuids.cn1, uuids.cn2]),
                         _provider_uuids_from_iterable(p_sums))

        cn1_p_sum = _find_summary_for_provider(p_sums, uuids.cn1)
        self.assertIsNotNone(cn1_p_sum)
        self.assertEqual(3, len(cn1_p_sum.resources))

        cn1_p_sum_vcpu = _find_summary_for_resource(cn1_p_sum, 'VCPU')
        self.assertIsNotNone(cn1_p_sum_vcpu)

        expected_capacity = (24 * 16.0)
        self.assertEqual(expected_capacity, cn1_p_sum_vcpu.capacity)
        self.assertEqual(0, cn1_p_sum_vcpu.used)

        # Let's verify the disk for the second compute node
        cn2_p_sum = _find_summary_for_provider(p_sums, uuids.cn2)
        self.assertIsNotNone(cn2_p_sum)
        self.assertEqual(3, len(cn2_p_sum.resources))

        cn2_p_sum_disk = _find_summary_for_resource(cn2_p_sum, 'DISK_GB')
        self.assertIsNotNone(cn2_p_sum_disk)

        expected_capacity = ((2000 - 100) * 1.0)
        self.assertEqual(expected_capacity, cn2_p_sum_disk.capacity)
        self.assertEqual(0, cn2_p_sum_disk.used)

        # Verify the allocation requests that are returned. There should be 2
        # allocation requests, one for each compute node, containing 3
        # resources in each allocation request, one each for VCPU, RAM, and
        # disk. The amounts of the requests should correspond to the requested
        # resource amounts in the filter:resources dict passed to
        # AllocationCandidates.get_by_requests().
        expected = [
            [('cn1', fields.ResourceClass.VCPU, 1),
             ('cn1', fields.ResourceClass.MEMORY_MB, 64),
             ('cn1', fields.ResourceClass.DISK_GB, 1500)],
            [('cn2', fields.ResourceClass.VCPU, 1),
             ('cn2', fields.ResourceClass.MEMORY_MB, 64),
             ('cn2', fields.ResourceClass.DISK_GB, 1500)],
        ]
        self._validate_allocation_requests(expected, alloc_cands)

    def test_local_with_shared_disk(self):
        """Create some resource providers that can satisfy the request for
        resources with local VCPU and MEMORY_MB but rely on a shared storage
        pool to satisfy DISK_GB and verify that the allocation requests
        returned by AllocationCandidates have DISK_GB served up by the shared
        storage pool resource provider and VCPU/MEMORY_MB by the compute node
        providers
        """
        # The aggregate that will be associated to everything...
        agg_uuid = uuids.agg

        # Create two compute node providers with VCPU, RAM and NO local disk,
        # associated with the aggregate.
        for name in ('cn1', 'cn2'):
            cn = self._create_provider(name, agg_uuid)
            _add_inventory(cn, fields.ResourceClass.VCPU, 24,
                           allocation_ratio=16.0)
            _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 1024,
                           min_unit=64, allocation_ratio=1.5)

        # Create the shared storage pool, asociated with the same aggregate
        ss = self._create_provider('shared storage', agg_uuid)

        # Give the shared storage pool some inventory of DISK_GB
        _add_inventory(ss, fields.ResourceClass.DISK_GB, 2000, reserved=100,
                       min_unit=10)

        # Mark the shared storage pool as having inventory shared among any
        # provider associated via aggregate
        _set_traits(ss, "MISC_SHARES_VIA_AGGREGATE")

        # Ask for the alternative placement possibilities and verify each
        # compute node provider is listed in the allocation requests as well as
        # the shared storage pool provider
        alloc_cands = self._get_allocation_candidates()

        # Verify the provider summary information indicates 0 usage and
        # capacity calculated from above inventory numbers for both compute
        # nodes
        # TODO(efried): Come up with a terse & readable way to validate
        # provider summaries
        p_sums = alloc_cands.provider_summaries

        self.assertEqual(set([uuids.cn1, uuids.cn2, ss.uuid]),
                         _provider_uuids_from_iterable(p_sums))

        cn1_p_sum = _find_summary_for_provider(p_sums, uuids.cn1)
        self.assertIsNotNone(cn1_p_sum)
        self.assertEqual(2, len(cn1_p_sum.resources))

        cn1_p_sum_vcpu = _find_summary_for_resource(cn1_p_sum, 'VCPU')
        self.assertIsNotNone(cn1_p_sum_vcpu)

        expected_capacity = (24 * 16.0)
        self.assertEqual(expected_capacity, cn1_p_sum_vcpu.capacity)
        self.assertEqual(0, cn1_p_sum_vcpu.used)

        # Let's verify memory for the second compute node
        cn2_p_sum = _find_summary_for_provider(p_sums, uuids.cn2)
        self.assertIsNotNone(cn2_p_sum)
        self.assertEqual(2, len(cn2_p_sum.resources))

        cn2_p_sum_ram = _find_summary_for_resource(cn2_p_sum, 'MEMORY_MB')
        self.assertIsNotNone(cn2_p_sum_ram)

        expected_capacity = (1024 * 1.5)
        self.assertEqual(expected_capacity, cn2_p_sum_ram.capacity)
        self.assertEqual(0, cn2_p_sum_ram.used)

        # Let's verify only diks for the shared storage pool
        ss_p_sum = _find_summary_for_provider(p_sums, ss.uuid)
        self.assertIsNotNone(ss_p_sum)
        self.assertEqual(1, len(ss_p_sum.resources))

        ss_p_sum_disk = _find_summary_for_resource(ss_p_sum, 'DISK_GB')
        self.assertIsNotNone(ss_p_sum_disk)

        expected_capacity = ((2000 - 100) * 1.0)
        self.assertEqual(expected_capacity, ss_p_sum_disk.capacity)
        self.assertEqual(0, ss_p_sum_disk.used)

        # Verify the allocation requests that are returned. There should be 2
        # allocation requests, one for each compute node, containing 3
        # resources in each allocation request, one each for VCPU, RAM, and
        # disk. The amounts of the requests should correspond to the requested
        # resource amounts in the filter:resources dict passed to
        # AllocationCandidates.get_by_requests(). The providers for VCPU and
        # MEMORY_MB should be the compute nodes while the provider for the
        # DISK_GB should be the shared storage pool
        expected = [
            [('cn1', fields.ResourceClass.VCPU, 1),
             ('cn1', fields.ResourceClass.MEMORY_MB, 64),
             ('shared storage', fields.ResourceClass.DISK_GB, 1500)],
            [('cn2', fields.ResourceClass.VCPU, 1),
             ('cn2', fields.ResourceClass.MEMORY_MB, 64),
             ('shared storage', fields.ResourceClass.DISK_GB, 1500)],
        ]
        self._validate_allocation_requests(expected, alloc_cands)

        # Test for bug #1705071. We query for allocation candidates with a
        # request for ONLY the DISK_GB (the resource that is shared with
        # compute nodes) and no VCPU/MEMORY_MB. Before the fix for bug
        # #1705071, this resulted in a KeyError

        alloc_cands = self._get_allocation_candidates(
            requests=[placement_lib.RequestGroup(
                use_same_provider=False,
                resources={
                    'DISK_GB': 10,
                }
            )]
        )

        # We should only have provider summary information for the sharing
        # storage provider, since that's the only provider that can be
        # allocated against for this request.  In the future, we may look into
        # returning the shared-with providers in the provider summaries, but
        # that's a distant possibility.
        self.assertEqual(
            set([ss.uuid]),
            _provider_uuids_from_iterable(alloc_cands.provider_summaries))

        # The allocation_requests will only include the shared storage
        # provider because the only thing we're requesting to allocate is
        # against the provider of DISK_GB, which happens to be the shared
        # storage provider.
        expected = [[('shared storage', fields.ResourceClass.DISK_GB, 10)]]
        self._validate_allocation_requests(expected, alloc_cands)

    def test_local_with_shared_custom_resource(self):
        """Create some resource providers that can satisfy the request for
        resources with local VCPU and MEMORY_MB but rely on a shared resource
        provider to satisfy a custom resource requirement and verify that the
        allocation requests returned by AllocationCandidates have the custom
        resource served up by the shared custom resource provider and
        VCPU/MEMORY_MB by the compute node providers
        """
        # The aggregate that will be associated to everything...
        agg_uuid = uuids.agg

        # Create two compute node providers with VCPU, RAM and NO local
        # CUSTOM_MAGIC resources, associated with the aggregate.
        for name in ('cn1', 'cn2'):
            cn = self._create_provider(name, agg_uuid)
            _add_inventory(cn, fields.ResourceClass.VCPU, 24,
                           allocation_ratio=16.0)
            _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 1024,
                           min_unit=64, allocation_ratio=1.5)

        # Create a custom resource called MAGIC
        magic_rc = rp_obj.ResourceClass(
            self.ctx,
            name='CUSTOM_MAGIC',
        )
        magic_rc.create()

        # Create the shared provider that serves CUSTOM_MAGIC, associated with
        # the same aggregate
        magic_p = self._create_provider('shared custom resource provider',
                                        agg_uuid)
        _add_inventory(magic_p, magic_rc.name, 2048, reserved=1024,
                       min_unit=10)

        # Mark the magic provider as having inventory shared among any provider
        # associated via aggregate
        _set_traits(magic_p, "MISC_SHARES_VIA_AGGREGATE")

        # The resources we will request
        requested_resources = {
            fields.ResourceClass.VCPU: 1,
            fields.ResourceClass.MEMORY_MB: 64,
            magic_rc.name: 512,
        }

        alloc_cands = self._get_allocation_candidates(
            requests=[placement_lib.RequestGroup(
                use_same_provider=False, resources=requested_resources)])

        # Verify the allocation requests that are returned. There should be 2
        # allocation requests, one for each compute node, containing 3
        # resources in each allocation request, one each for VCPU, RAM, and
        # MAGIC. The amounts of the requests should correspond to the requested
        # resource amounts in the filter:resources dict passed to
        # AllocationCandidates.get_by_requests(). The providers for VCPU and
        # MEMORY_MB should be the compute nodes while the provider for the
        # MAGIC should be the shared custom resource provider.
        expected = [
            [('cn1', fields.ResourceClass.VCPU, 1),
             ('cn1', fields.ResourceClass.MEMORY_MB, 64),
             ('shared custom resource provider', magic_rc.name, 512)],
            [('cn2', fields.ResourceClass.VCPU, 1),
             ('cn2', fields.ResourceClass.MEMORY_MB, 64),
             ('shared custom resource provider', magic_rc.name, 512)],
        ]
        self._validate_allocation_requests(expected, alloc_cands)

    def test_mix_local_and_shared(self):
        # The aggregate that will be associated to shared storage pool
        agg_uuid = uuids.agg

        # Create three compute node providers with VCPU and RAM, but only
        # the third compute node has DISK. The first two computes will
        # share the storage from the shared storage pool.
        for name in ('cn1', 'cn2', 'cn3'):
            cn = self._create_provider(name)
            _add_inventory(cn, fields.ResourceClass.VCPU, 24,
                           allocation_ratio=16.0)
            _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 1024,
                           min_unit=64, allocation_ratio=1.5)
            if name == 'cn3':
                _add_inventory(cn, fields.ResourceClass.DISK_GB, 2000,
                               reserved=100, min_unit=10)
            else:
                cn.set_aggregates([agg_uuid])

        # Create the shared storage pool
        ss = self._create_provider('shared storage')

        # Give the shared storage pool some inventory of DISK_GB
        _add_inventory(ss, fields.ResourceClass.DISK_GB, 2000, reserved=100,
                       min_unit=10)

        _set_traits(ss, "MISC_SHARES_VIA_AGGREGATE")

        # Put the ss RP in the same aggregate as the first two compute nodes
        ss.set_aggregates([agg_uuid])

        alloc_cands = self._get_allocation_candidates()

        # Expect cn1, cn2, cn3 and ss in the summaries
        self.assertEqual(
            set([uuids.cn1, uuids.cn2, ss.uuid, uuids.cn3]),
            _provider_uuids_from_iterable(alloc_cands.provider_summaries))

        # Expect three allocation requests: (cn1, ss), (cn2, ss), (cn3)
        expected = [
            [('cn1', fields.ResourceClass.VCPU, 1),
             ('cn1', fields.ResourceClass.MEMORY_MB, 64),
             ('shared storage', fields.ResourceClass.DISK_GB, 1500)],
            [('cn2', fields.ResourceClass.VCPU, 1),
             ('cn2', fields.ResourceClass.MEMORY_MB, 64),
             ('shared storage', fields.ResourceClass.DISK_GB, 1500)],
            [('cn3', fields.ResourceClass.VCPU, 1),
             ('cn3', fields.ResourceClass.MEMORY_MB, 64),
             ('cn3', fields.ResourceClass.DISK_GB, 1500)],
        ]
        self._validate_allocation_requests(expected, alloc_cands)

    def test_common_rc(self):
        """Candidates when cn and shared have inventory in the same class."""
        cn = self._create_provider('cn', uuids.agg1)
        _add_inventory(cn, fields.ResourceClass.VCPU, 24)
        _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 2048)
        _add_inventory(cn, fields.ResourceClass.DISK_GB, 1600)

        ss = self._create_provider('ss', uuids.agg1)
        _set_traits(ss, "MISC_SHARES_VIA_AGGREGATE")
        _add_inventory(ss, fields.ResourceClass.DISK_GB, 1600)

        alloc_cands = self._get_allocation_candidates()

        # One allocation_request should have cn + ss; the other should have
        # just the cn.
        expected = [
            [('cn', fields.ResourceClass.VCPU, 1),
             ('cn', fields.ResourceClass.MEMORY_MB, 64),
             ('cn', fields.ResourceClass.DISK_GB, 1500)],
            # TODO(efried): Due to bug #1724613, the cn + ss candidate is not
            # returned.  Uncomment this when the bug is fixed.
            # [('cn', fields.ResourceClass.VCPU, 1),
            #  ('cn', fields.ResourceClass.MEMORY_MB, 64),
            #  ('ss', fields.ResourceClass.DISK_GB, 1500)],
        ]

        self._validate_allocation_requests(expected, alloc_cands)

    def test_common_rc_traits_split(self):
        """Validate filters when traits are split across cn and shared RPs."""
        # NOTE(efried): This test case only applies to the scenario where we're
        # requesting resources via the RequestGroup where
        # use_same_provider=False

        cn = self._create_provider('cn', uuids.agg1)
        _add_inventory(cn, fields.ResourceClass.VCPU, 24)
        _add_inventory(cn, fields.ResourceClass.MEMORY_MB, 2048)
        _add_inventory(cn, fields.ResourceClass.DISK_GB, 1600)
        # The compute node's disk is SSD
        _set_traits(cn, 'HW_CPU_X86_SSE', 'STORAGE_DISK_SSD')

        ss = self._create_provider('ss', uuids.agg1)
        _add_inventory(ss, fields.ResourceClass.DISK_GB, 1600)
        # The shared storage's disk is RAID
        _set_traits(ss, 'MISC_SHARES_VIA_AGGREGATE', 'CUSTOM_RAID')

        alloc_cands = rp_obj.AllocationCandidates.get_by_requests(
            self.ctx, [
                placement_lib.RequestGroup(
                    use_same_provider=False,
                    resources=self.requested_resources,
                    required_traits=set(['HW_CPU_X86_SSE', 'STORAGE_DISK_SSD',
                                         'CUSTOM_RAID'])
                )
            ]
        )

        # TODO(efried): Okay, bear with me here:
        # TODO(efried): Bug #1724633: we'd *like* to get no candidates, because
        # there's no single DISK_GB resource with both STORAGE_DISK_SSD and
        # CUSTOM_RAID traits.  So this is the ideal expected value:
        # expected = []
        # TODO(efried): But under the design as currently conceived, we would
        # get the cn + ss candidate, because that combination satisfies both
        # traits:
        # expected = [
        #     [('cn', fields.ResourceClass.VCPU, 1),
        #      ('cn', fields.ResourceClass.MEMORY_MB, 64),
        #      ('ss', fields.ResourceClass.DISK_GB, 1500)],
        # ]
        # TODO(efried): However, until https://review.openstack.org/#/c/479766/
        # lands, the traits are ignored, so this behaves just like
        # test_common_rc above, which is subject to bug #1724613:
        expected = [
            [('cn', fields.ResourceClass.VCPU, 1),
             ('cn', fields.ResourceClass.MEMORY_MB, 64),
             ('cn', fields.ResourceClass.DISK_GB, 1500)],
        ]

        self._validate_allocation_requests(expected, alloc_cands)

    def test_all_sharing_providers(self):
        ss1 = self._create_provider('ss1', uuids.agg1)
        _set_traits(ss1, "MISC_SHARES_VIA_AGGREGATE")
        _add_inventory(ss1, fields.ResourceClass.IPV4_ADDRESS, 24)
        _add_inventory(ss1, fields.ResourceClass.SRIOV_NET_VF, 16)
        _add_inventory(ss1, fields.ResourceClass.DISK_GB, 1600)

        ss2 = self._create_provider('ss2', uuids.agg1)
        _set_traits(ss2, "MISC_SHARES_VIA_AGGREGATE")
        _add_inventory(ss2, fields.ResourceClass.DISK_GB, 1600)

        alloc_cands = self._get_allocation_candidates(requests=[
            placement_lib.RequestGroup(
                use_same_provider=False,
                resources={
                    'IPV4_ADDRESS': 2,
                    'SRIOV_NET_VF': 1,
                    'DISK_GB': 1500,
                }
            )]
        )

        # TODO(efried): Bug https://bugs.launchpad.net/nova/+bug/1730730
        # We expect two candidates: one that gets all the resources from ss1;
        # and one that gets the DISK_GB from ss2 and the rest from ss1:
        # expected = [
        #     [('ss1', fields.ResourceClass.IPV4_ADDRESS, 2),
        #      ('ss1', fields.ResourceClass.SRIOV_NET_VF, 1),
        #      ('ss1', fields.ResourceClass.DISK_GB, 1500)],
        #     [('ss1', fields.ResourceClass.IPV4_ADDRESS, 2),
        #      ('ss1', fields.ResourceClass.SRIOV_NET_VF, 1),
        #      ('ss2', fields.ResourceClass.DISK_GB, 1500)],
        # ]
        # But here's what we're actually seeing:
        # One with all the resources coming from ss1
        expected = [
            [('ss1', fields.ResourceClass.IPV4_ADDRESS, 2),
             ('ss1', fields.ResourceClass.SRIOV_NET_VF, 1),
             ('ss1', fields.ResourceClass.DISK_GB, 1500)],
        # One with the DISK_GB coming from ss2 and IPV4_ADDRESS coming from ss1
            [('ss1', fields.ResourceClass.IPV4_ADDRESS, 2),
             ('ss2', fields.ResourceClass.DISK_GB, 1500)],
        # One with the DISK_GB coming from ss2 and SRIOV_NET_VF coming from ss1
            [('ss1', fields.ResourceClass.SRIOV_NET_VF, 1),
             ('ss2', fields.ResourceClass.DISK_GB, 1500)],
        # Of special brokenness are those last two, which don't even have all
        # of the requested resources:
        ]
        self._validate_allocation_requests(expected, alloc_cands)

#    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. # NOTE(efried): Always use a random consumer UUID - we don't want to # override any existing allocations from the test case. Return the set of resource_provider.uuid from an iterable.      :param objs: Iterable of any object with a resource_provider attribute                  (e.g. an AllocationRequest.resource_requests or an                  AllocationCandidates.provider_summaries). # For debugging purposes, populated by _create_provider and used by # _validate_allocation_requests to make failure results more readable. # These RPs are named based on whether we expect them to be 'incl'uded # or 'excl'uded in the result. # No inventory records.  This one should never show up in a result. # Inventory of adequate CPU and memory, no allocations against it. # Adequate inventory, no allocations against it. # No allocations, but inventory unusable.  Try to hit all the possible # reasons for exclusion. # VCPU min_unit too high # MEMORY_MB max_unit too low # DISK_GB unsuitable step_size # Not enough total VCPU # Too much reserved MEMORY_MB # DISK_GB allocation ratio blows it up # Inventory consumed in one RC, but available in the others # Inventory all consumed # Inventory available in requested classes, but unavailable in others # Inventory available in a unrequested classes, not in requested ones # Incompatible step size # Not enough left after reserved + used # Allocation ratio math # Run it! # We should get all the incl_* RPs Tests a variety of scenarios with both shared and non-shared resource     providers that the AllocationCandidates.get_by_requests() method returns a     set of alternative allocation requests and provider summaries that may be     used by the scheduler to sort/weigh the options it has for claiming     resources against providers. # For debugging purposes, populated by _create_provider and used by # _validate_allocation_requests to make failure results more readable. Assert correctness of allocation requests in allocation candidates.          This is set up to make it easy for the caller to specify the expected         result, to make that expected structure readable for someone looking at         the test case, and to make test failures readable for debugging.          :param expected: A list of lists of tuples representing the expected                          allocation requests, of the form:              [                 [(resource_provider_name, resource_class_name, resource_count),                  ...,                 ],                 ...              ]         :param candidates: The result from AllocationCandidates.get_by_requests # Extract/convert allocation requests from candidates # Sort the guts of the expected structure # Now we ought to be able to compare them Create some resource providers that can satisfy the request for         resources with local (non-shared) resources and verify that the         allocation requests returned by AllocationCandidates correspond with         each of these resource providers. # Create three compute node providers with VCPU, RAM and local disk # Ask for the alternative placement possibilities and verify each # provider is returned # Verify the provider summary information indicates 0 usage and # capacity calculated from above inventory numbers for both compute # nodes # TODO(efried): Come up with a terse & readable way to validate # provider summaries # Let's verify the disk for the second compute node # Verify the allocation requests that are returned. There should be 2 # allocation requests, one for each compute node, containing 3 # resources in each allocation request, one each for VCPU, RAM, and # disk. The amounts of the requests should correspond to the requested # resource amounts in the filter:resources dict passed to # AllocationCandidates.get_by_requests(). Create some resource providers that can satisfy the request for         resources with local VCPU and MEMORY_MB but rely on a shared storage         pool to satisfy DISK_GB and verify that the allocation requests         returned by AllocationCandidates have DISK_GB served up by the shared         storage pool resource provider and VCPU/MEMORY_MB by the compute node         providers # The aggregate that will be associated to everything... # Create two compute node providers with VCPU, RAM and NO local disk, # associated with the aggregate. # Create the shared storage pool, asociated with the same aggregate # Give the shared storage pool some inventory of DISK_GB # Mark the shared storage pool as having inventory shared among any # provider associated via aggregate # Ask for the alternative placement possibilities and verify each # compute node provider is listed in the allocation requests as well as # the shared storage pool provider # Verify the provider summary information indicates 0 usage and # capacity calculated from above inventory numbers for both compute # nodes # TODO(efried): Come up with a terse & readable way to validate # provider summaries # Let's verify memory for the second compute node # Let's verify only diks for the shared storage pool # Verify the allocation requests that are returned. There should be 2 # allocation requests, one for each compute node, containing 3 # resources in each allocation request, one each for VCPU, RAM, and # disk. The amounts of the requests should correspond to the requested # resource amounts in the filter:resources dict passed to # AllocationCandidates.get_by_requests(). The providers for VCPU and # MEMORY_MB should be the compute nodes while the provider for the # DISK_GB should be the shared storage pool # Test for bug #1705071. We query for allocation candidates with a # request for ONLY the DISK_GB (the resource that is shared with # compute nodes) and no VCPU/MEMORY_MB. Before the fix for bug # #1705071, this resulted in a KeyError # We should only have provider summary information for the sharing # storage provider, since that's the only provider that can be # allocated against for this request.  In the future, we may look into # returning the shared-with providers in the provider summaries, but # that's a distant possibility. # The allocation_requests will only include the shared storage # provider because the only thing we're requesting to allocate is # against the provider of DISK_GB, which happens to be the shared # storage provider. Create some resource providers that can satisfy the request for         resources with local VCPU and MEMORY_MB but rely on a shared resource         provider to satisfy a custom resource requirement and verify that the         allocation requests returned by AllocationCandidates have the custom         resource served up by the shared custom resource provider and         VCPU/MEMORY_MB by the compute node providers # The aggregate that will be associated to everything... # Create two compute node providers with VCPU, RAM and NO local # CUSTOM_MAGIC resources, associated with the aggregate. # Create a custom resource called MAGIC # Create the shared provider that serves CUSTOM_MAGIC, associated with # the same aggregate # Mark the magic provider as having inventory shared among any provider # associated via aggregate # The resources we will request # Verify the allocation requests that are returned. There should be 2 # allocation requests, one for each compute node, containing 3 # resources in each allocation request, one each for VCPU, RAM, and # MAGIC. The amounts of the requests should correspond to the requested # resource amounts in the filter:resources dict passed to # AllocationCandidates.get_by_requests(). The providers for VCPU and # MEMORY_MB should be the compute nodes while the provider for the # MAGIC should be the shared custom resource provider. # The aggregate that will be associated to shared storage pool # Create three compute node providers with VCPU and RAM, but only # the third compute node has DISK. The first two computes will # share the storage from the shared storage pool. # Create the shared storage pool # Give the shared storage pool some inventory of DISK_GB # Put the ss RP in the same aggregate as the first two compute nodes # Expect cn1, cn2, cn3 and ss in the summaries # Expect three allocation requests: (cn1, ss), (cn2, ss), (cn3) Candidates when cn and shared have inventory in the same class. # One allocation_request should have cn + ss; the other should have # just the cn. # TODO(efried): Due to bug #1724613, the cn + ss candidate is not # returned.  Uncomment this when the bug is fixed. # [('cn', fields.ResourceClass.VCPU, 1), #  ('cn', fields.ResourceClass.MEMORY_MB, 64), #  ('ss', fields.ResourceClass.DISK_GB, 1500)], Validate filters when traits are split across cn and shared RPs. # NOTE(efried): This test case only applies to the scenario where we're # requesting resources via the RequestGroup where # use_same_provider=False # The compute node's disk is SSD # The shared storage's disk is RAID # TODO(efried): Okay, bear with me here: # TODO(efried): Bug #1724633: we'd *like* to get no candidates, because # there's no single DISK_GB resource with both STORAGE_DISK_SSD and # CUSTOM_RAID traits.  So this is the ideal expected value: # expected = [] # TODO(efried): But under the design as currently conceived, we would # get the cn + ss candidate, because that combination satisfies both # traits: # expected = [ #     [('cn', fields.ResourceClass.VCPU, 1), #      ('cn', fields.ResourceClass.MEMORY_MB, 64), #      ('ss', fields.ResourceClass.DISK_GB, 1500)], # ] # TODO(efried): However, until https://review.openstack.org/#/c/479766/ # lands, the traits are ignored, so this behaves just like # test_common_rc above, which is subject to bug #1724613: # TODO(efried): Bug https://bugs.launchpad.net/nova/+bug/1730730 # We expect two candidates: one that gets all the resources from ss1; # and one that gets the DISK_GB from ss2 and the rest from ss1: # expected = [ #     [('ss1', fields.ResourceClass.IPV4_ADDRESS, 2), #      ('ss1', fields.ResourceClass.SRIOV_NET_VF, 1), #      ('ss1', fields.ResourceClass.DISK_GB, 1500)], #     [('ss1', fields.ResourceClass.IPV4_ADDRESS, 2), #      ('ss1', fields.ResourceClass.SRIOV_NET_VF, 1), #      ('ss2', fields.ResourceClass.DISK_GB, 1500)], # ] # But here's what we're actually seeing: # One with all the resources coming from ss1 # One with the DISK_GB coming from ss2 and IPV4_ADDRESS coming from ss1 # One with the DISK_GB coming from ss2 and SRIOV_NET_VF coming from ss1 # Of special brokenness are those last two, which don't even have all # of the requested resources: