コード例 #1
0
 def test_resource_request_from_extra_specs_append_request(self):
     extra_specs = {
         'resources:VCPU': '2',
         'resources:MEMORY_MB': '2048',
         'trait:HW_CPU_X86_AVX': 'required',
     }
     existing_req = utils.ResourceRequest()
     existing_req._rg_by_id[None] = objects.RequestGroup(
         use_same_provider=False, required_traits={
             'CUSTOM_MAGIC',
         })
     # Build up a ResourceRequest from the inside to compare against.
     expected = utils.ResourceRequest()
     expected._rg_by_id[None] = objects.RequestGroup(
         use_same_provider=False,
         resources={
             'VCPU': 2,
             'MEMORY_MB': 2048,
         },
         required_traits={
             # In addition to traits from extra spec, we get traits from a
             # previous existing resource request
             'HW_CPU_X86_AVX',
             'CUSTOM_MAGIC',
         })
     self.assertResourceRequestsEqual(
         expected,
         utils.ResourceRequest.from_extra_specs(extra_specs,
                                                req=existing_req))
コード例 #2
0
    def test_resource_request_from_image_props_append_request(self):
        props = {'trait:CUSTOM_MAGIC': 'required'}
        image_meta_props = objects.ImageMetaProps.from_dict(props)

        existing_req = utils.ResourceRequest()
        existing_req._rg_by_id[None] = objects.RequestGroup(
            use_same_provider=False,
            resources={
                'VCPU': 2,
                'MEMORY_MB': 2048,
            },
            required_traits={
                'HW_CPU_X86_AVX',
            })
        # Build up a ResourceRequest from the inside to compare against.
        expected = utils.ResourceRequest()
        expected._rg_by_id[None] = objects.RequestGroup(
            use_same_provider=False,
            resources={
                'VCPU': 2,
                'MEMORY_MB': 2048,
            },
            required_traits={
                # In addition to information already contained in the existing
                # resource request, we add the traits from image properties
                'HW_CPU_X86_AVX',
                'CUSTOM_MAGIC',
            })
        self.assertResourceRequestsEqual(
            expected,
            utils.ResourceRequest.from_image_props(image_meta_props,
                                                   req=existing_req))
コード例 #3
0
 def test_resources_from_request_spec_flavor_and_image_traits(self):
     image = self._get_image_with_traits()
     flavor = objects.Flavor(vcpus=1,
                             memory_mb=1024,
                             root_gb=10,
                             ephemeral_gb=5,
                             swap=0,
                             extra_specs={
                                 'trait:CUSTOM_FLAVOR_TRAIT': 'required',
                                 'trait:CUSTOM_IMAGE_TRAIT2': 'required'
                             })
     expected_resources = utils.ResourceRequest()
     expected_resources._rg_by_id[None] = objects.RequestGroup(
         use_same_provider=False,
         resources={
             'VCPU': 1,
             'MEMORY_MB': 1024,
             'DISK_GB': 15,
         },
         required_traits={
             # trait:CUSTOM_IMAGE_TRAIT2 is defined in both extra_specs and
             # image metadata. We get a union of both.
             'CUSTOM_IMAGE_TRAIT1',
             'CUSTOM_IMAGE_TRAIT2',
             'CUSTOM_FLAVOR_TRAIT',
         })
     self._test_resources_from_request_spec(expected_resources, flavor,
                                            image)
コード例 #4
0
 def test_resource_request_from_extra_specs(self):
     extra_specs = {
         'resources:VCPU': '2',
         'resources:MEMORY_MB': '2048',
         'trait:HW_CPU_X86_AVX': 'required',
         # Key skipped because no colons
         'nocolons': '42',
         'trait:CUSTOM_MAGIC': 'required',
         # Resource skipped because invalid resource class name
         'resources86:CUTSOM_MISSPELLED': '86',
         'resources1:SRIOV_NET_VF': '1',
         # Resource skipped because non-int-able value
         'resources86:CUSTOM_FOO': 'seven',
         # Resource skipped because negative value
         'resources86:CUSTOM_NEGATIVE': '-7',
         'resources1:IPV4_ADDRESS': '1',
         # Trait skipped because unsupported value
         'trait86:CUSTOM_GOLD': 'preferred',
         'trait1:CUSTOM_PHYSNET_NET1': 'required',
         'resources2:SRIOV_NET_VF': '1',
         'resources2:IPV4_ADDRESS': '2',
         'trait2:CUSTOM_PHYSNET_NET2': 'required',
         'trait2:HW_NIC_ACCEL_SSL': 'required',
         # Groupings that don't quite match the patterns are ignored
         'resources_5:SRIOV_NET_VF': '7',
         'traitFoo:HW_NIC_ACCEL_SSL': 'required',
         # Solo resource, no corresponding traits
         'resources3:DISK_GB': '5',
     }
     # Build up a ResourceRequest from the inside to compare against.
     expected = utils.ResourceRequest()
     expected._rg_by_id[None] = plib.RequestGroup(use_same_provider=False,
                                                  resources={
                                                      'VCPU': 2,
                                                      'MEMORY_MB': 2048,
                                                  },
                                                  required_traits={
                                                      'HW_CPU_X86_AVX',
                                                      'CUSTOM_MAGIC',
                                                  })
     expected._rg_by_id['1'] = plib.RequestGroup(resources={
         'SRIOV_NET_VF': 1,
         'IPV4_ADDRESS': 1,
     },
                                                 required_traits={
                                                     'CUSTOM_PHYSNET_NET1',
                                                 })
     expected._rg_by_id['2'] = plib.RequestGroup(resources={
         'SRIOV_NET_VF': 1,
         'IPV4_ADDRESS': 2,
     },
                                                 required_traits={
                                                     'CUSTOM_PHYSNET_NET2',
                                                     'HW_NIC_ACCEL_SSL',
                                                 })
     expected._rg_by_id['3'] = plib.RequestGroup(resources={
         'DISK_GB': 5,
     })
     self.assertResourceRequestsEqual(
         expected, utils.ResourceRequest.from_extra_specs(extra_specs))
コード例 #5
0
 def test_process_use_force_hosts(self):
     fake_nodes = objects.ComputeNodeList(objects=[
         objects.ComputeNode(host='fake-host',
                             uuid='12345678-1234-1234-1234-123456789012')
     ])
     self.mock_host_manager.get_compute_nodes_by_host_or_node.\
         return_value = fake_nodes
     flavor = objects.Flavor(vcpus=1,
                             memory_mb=1024,
                             root_gb=15,
                             ephemeral_gb=0,
                             swap=0)
     fake_spec = objects.RequestSpec(flavor=flavor, force_hosts=['test'])
     expected = utils.ResourceRequest()
     expected._rg_by_id[None] = objects.RequestGroup(
         use_same_provider=False,
         resources={
             'VCPU': 1,
             'MEMORY_MB': 1024,
             'DISK_GB': 15,
         },
         in_tree='12345678-1234-1234-1234-123456789012',
     )
     resources = utils.resources_from_request_spec(self.context, fake_spec,
                                                   self.mock_host_manager)
     self.assertResourceRequestsEqual(expected, resources)
     expected_querystring = (
         'in_tree=12345678-1234-1234-1234-123456789012&'
         'limit=1000&resources=DISK_GB%3A15%2CMEMORY_MB%3A1024%2CVCPU%3A1')
     self.assertEqual(expected_querystring, resources.to_querystring())
     self.mock_host_manager.get_compute_nodes_by_host_or_node.\
         assert_called_once_with(self.context, 'test', None, cell=None)
コード例 #6
0
ファイル: test_utils.py プロジェクト: juju812/openstack_kolla
 def test_resources_from_request_spec_no_limit_based_on_hint(self, hints):
     """Tests that there is no limit applied to the
     GET /allocation_candidates query string if a given scheduler hint
     is in the request spec.
     """
     flavor = objects.Flavor(vcpus=1,
                             memory_mb=1024,
                             root_gb=15,
                             ephemeral_gb=0,
                             swap=0)
     fake_spec = objects.RequestSpec(
         flavor=flavor, scheduler_hints=hints)
     expected = utils.ResourceRequest()
     expected._rg_by_id[None] = objects.RequestGroup(
         use_same_provider=False,
         resources={
             'VCPU': 1,
             'MEMORY_MB': 1024,
             'DISK_GB': 15,
         },
     )
     expected._limit = None
     resources = utils.resources_from_request_spec(fake_spec)
     self.assertResourceRequestsEqual(expected, resources)
     expected_querystring = (
         'resources=DISK_GB%3A15%2CMEMORY_MB%3A1024%2CVCPU%3A1'
     )
     self.assertEqual(expected_querystring, resources.to_querystring())
コード例 #7
0
 def test_get_resources_from_request_spec_granular(self):
     flavor = objects.Flavor(
         vcpus=1,
         memory_mb=1024,
         root_gb=10,
         ephemeral_gb=0,
         swap=0,
         extra_specs={
             'resources1:VGPU': '1',
             'resources1:VGPU_DISPLAY_HEAD': '2',
             # Replace
             'resources3:VCPU': '2',
             # Stay separate (don't sum)
             'resources42:SRIOV_NET_VF': '1',
             'resources24:SRIOV_NET_VF': '2',
             # Ignore
             'some:bogus': 'value',
             # Custom in the unnumbered group (merge with DISK_GB)
             'resources:CUSTOM_THING': '123',
             # Traits make it through
             'trait3:CUSTOM_SILVER': 'required',
             'trait3:CUSTOM_GOLD': 'required',
             # Delete standard
             'resources86:MEMORY_MB': '0',
             # Standard and custom zeroes don't make it through
             'resources:IPV4_ADDRESS': '0',
             'resources:CUSTOM_FOO': '0',
             # Bogus values don't make it through
             'resources1:MEMORY_MB': 'bogus'
         })
     expected_resources = utils.ResourceRequest()
     expected_resources._rg_by_id[None] = plib.RequestGroup(
         use_same_provider=False,
         resources={
             'DISK_GB': 10,
             'CUSTOM_THING': 123,
         })
     expected_resources._rg_by_id['1'] = plib.RequestGroup(
         resources={
             'VGPU': 1,
             'VGPU_DISPLAY_HEAD': 2,
         })
     expected_resources._rg_by_id['3'] = plib.RequestGroup(
         resources={
             'VCPU': 2,
         },
         required_traits={
             'CUSTOM_GOLD',
             'CUSTOM_SILVER',
         })
     expected_resources._rg_by_id['24'] = plib.RequestGroup(resources={
         'SRIOV_NET_VF':
         2,
     }, )
     expected_resources._rg_by_id['42'] = plib.RequestGroup(
         resources={
             'SRIOV_NET_VF': 1,
         })
     self._test_resources_from_request_spec(flavor, expected_resources)
コード例 #8
0
 def test_resource_request_add_group_inserts_the_group(self):
     req = utils.ResourceRequest()
     rg1 = objects.RequestGroup()
     req.add_request_group(rg1)
     rg2 = objects.RequestGroup()
     req.add_request_group(rg2)
     self.assertIs(rg1, req.get_request_group(1))
     self.assertIs(rg2, req.get_request_group(2))
コード例 #9
0
def get_flags_by_flavor_specs(flavor):
    req_spec = objects.RequestSpec(flavor=flavor)
    resource_request = scheduler_utils.ResourceRequest(req_spec)
    required_traits = resource_request.all_required_traits

    flags = [TRAITS_CPU_MAPPING.get(trait) for trait in required_traits]

    return set(flags)
コード例 #10
0
ファイル: utils.py プロジェクト: irfanullakhan/nova
def get_flags_by_flavor_specs(flavor: 'objects.Flavor') -> ty.Set[str]:
    req_spec = objects.RequestSpec(flavor=flavor)
    resource_request = scheduler_utils.ResourceRequest(req_spec)
    required_traits = resource_request.all_required_traits

    flags = [TRAITS_CPU_MAPPING[trait] for trait in required_traits
             if trait in TRAITS_CPU_MAPPING]

    return set(flags)
コード例 #11
0
 def test_process_no_force_hosts_or_force_nodes(self):
     flavor = objects.Flavor(vcpus=1,
                             memory_mb=1024,
                             root_gb=15,
                             ephemeral_gb=0,
                             swap=0)
     fake_spec = objects.RequestSpec(flavor=flavor)
     expected = utils.ResourceRequest()
     resources = utils.resources_from_request_spec(fake_spec)
     self.assertEqual(expected._limit, resources._limit)
コード例 #12
0
    def test_resource_request_from_image_props(self):
        props = {'trait:CUSTOM_TRUSTED': 'required'}
        image_meta_props = objects.ImageMetaProps.from_dict(props)

        # Build up a ResourceRequest from the inside to compare against.
        expected = utils.ResourceRequest()
        expected._rg_by_id[None] = objects.RequestGroup(
            use_same_provider=False, required_traits={
                'CUSTOM_TRUSTED',
            })
        self.assertResourceRequestsEqual(
            expected, utils.ResourceRequest.from_image_props(image_meta_props))
コード例 #13
0
    def test_resource_request_add_group_inserts_the_group_implicit_group(self):
        req = utils.ResourceRequest()
        # this implicitly creates the unnumbered group
        unnumbered_rg = req.get_request_group(None)

        rg1 = objects.RequestGroup()
        req.add_request_group(rg1)
        rg2 = objects.RequestGroup()
        req.add_request_group(rg2)

        self.assertIs(rg1, req.get_request_group(1))
        self.assertIs(rg2, req.get_request_group(2))
        self.assertIs(unnumbered_rg, req.get_request_group(None))
コード例 #14
0
 def test_resources_from_request_spec_with_no_disk(self):
     flavor = objects.Flavor(vcpus=1,
                             memory_mb=1024,
                             root_gb=0,
                             ephemeral_gb=0,
                             swap=0)
     expected_resources = utils.ResourceRequest()
     expected_resources._rg_by_id[None] = plib.RequestGroup(
         use_same_provider=False,
         resources={
             'VCPU': 1,
             'MEMORY_MB': 1024,
         })
     self._test_resources_from_request_spec(flavor, expected_resources)
コード例 #15
0
 def test_resources_from_request_spec_flavor_only(self):
     flavor = objects.Flavor(vcpus=1,
                             memory_mb=1024,
                             root_gb=10,
                             ephemeral_gb=5,
                             swap=0)
     expected_resources = utils.ResourceRequest()
     expected_resources._rg_by_id[None] = objects.RequestGroup(
         use_same_provider=False,
         resources={
             'VCPU': 1,
             'MEMORY_MB': 1024,
             'DISK_GB': 15,
         })
     self._test_resources_from_request_spec(expected_resources, flavor)
コード例 #16
0
 def test_get_resources_from_request_spec_remove_flavor_amounts(self):
     flavor = objects.Flavor(vcpus=1,
                             memory_mb=1024,
                             root_gb=10,
                             ephemeral_gb=5,
                             swap=0,
                             extra_specs={
                                 "resources:VCPU": 0,
                                 "resources:DISK_GB": 0
                             })
     expected_resources = utils.ResourceRequest()
     expected_resources._rg_by_id[None] = plib.RequestGroup(
         use_same_provider=False, resources={
             "MEMORY_MB": 1024,
         })
     self._test_resources_from_request_spec(flavor, expected_resources)
コード例 #17
0
 def test_get_resources_from_request_spec_custom_resource_class(self):
     flavor = objects.Flavor(vcpus=1,
                             memory_mb=1024,
                             root_gb=10,
                             ephemeral_gb=5,
                             swap=0,
                             extra_specs={"resources:CUSTOM_TEST_CLASS": 1})
     expected_resources = utils.ResourceRequest()
     expected_resources._rg_by_id[None] = plib.RequestGroup(
         use_same_provider=False,
         resources={
             "VCPU": 1,
             "MEMORY_MB": 1024,
             "DISK_GB": 15,
             "CUSTOM_TEST_CLASS": 1,
         })
     self._test_resources_from_request_spec(flavor, expected_resources)
コード例 #18
0
 def test_resources_from_request_spec_flavor_forbidden_trait(self):
     flavor = objects.Flavor(
         vcpus=1,
         memory_mb=1024,
         root_gb=10,
         ephemeral_gb=5,
         swap=0,
         extra_specs={'trait:CUSTOM_FLAVOR_TRAIT': 'forbidden'})
     expected_resources = utils.ResourceRequest()
     expected_resources._rg_by_id[None] = objects.RequestGroup(
         use_same_provider=False,
         resources={
             'VCPU': 1,
             'MEMORY_MB': 1024,
             'DISK_GB': 15,
         },
         forbidden_traits={
             'CUSTOM_FLAVOR_TRAIT',
         })
     self._test_resources_from_request_spec(expected_resources, flavor)
コード例 #19
0
 def test_process_no_force_hosts_or_force_nodes(self):
     flavor = objects.Flavor(vcpus=1,
                             memory_mb=1024,
                             root_gb=15,
                             ephemeral_gb=0,
                             swap=0)
     expected = utils.ResourceRequest()
     expected._rg_by_id[None] = objects.RequestGroup(
         use_same_provider=False,
         resources={
             'VCPU': 1,
             'MEMORY_MB': 1024,
             'DISK_GB': 15,
         },
     )
     rr = self._test_resources_from_request_spec(expected, flavor)
     expected_querystring = (
         'limit=1000&'
         'resources=DISK_GB%3A15%2CMEMORY_MB%3A1024%2CVCPU%3A1')
     self.assertEqual(expected_querystring, rr.to_querystring())
コード例 #20
0
 def test_get_resources_from_request_spec_vgpu(self):
     flavor = objects.Flavor(vcpus=1,
                             memory_mb=1024,
                             root_gb=10,
                             ephemeral_gb=0,
                             swap=0,
                             extra_specs={
                                 "resources:VGPU": 1,
                                 "resources:VGPU_DISPLAY_HEAD": 1
                             })
     expected_resources = utils.ResourceRequest()
     expected_resources._rg_by_id[None] = plib.RequestGroup(
         use_same_provider=False,
         resources={
             "VCPU": 1,
             "MEMORY_MB": 1024,
             "DISK_GB": 10,
             "VGPU": 1,
             "VGPU_DISPLAY_HEAD": 1,
         })
     self._test_resources_from_request_spec(flavor, expected_resources)
コード例 #21
0
def isolate_aggregates(ctxt, request_spec):
    """Prepare list of aggregates that should be isolated.

    This filter will prepare the list of aggregates that should be
    ignored by the placement service. It checks if aggregates has metadata
    'trait:<trait_name>='required' and if <trait_name> is not present in
    either of flavor extra specs or image properties, then those aggregates
    will be included in the list of isolated aggregates.

    Precisely this filter gets the trait request form the image and
    flavor and unions them. Then it accumulates the set of aggregates that
    request traits are "non_matching_by_metadata_keys" and uses that to
    produce the list of isolated aggregates.
    """

    if not CONF.scheduler.enable_isolated_aggregate_filtering:
        return False

    # Get required traits set in flavor and image
    res_req = utils.ResourceRequest(request_spec)
    required_traits = res_req.all_required_traits

    keys = ['trait:%s' % trait for trait in required_traits]

    isolated_aggregates = (
        objects.aggregate.AggregateList.get_non_matching_by_metadata_keys(
            ctxt, keys, 'trait:', value='required'))

    # Set list of isolated aggregates to destination object of request_spec
    if isolated_aggregates:
        if ('requested_destination' not in request_spec
                or request_spec.requested_destination is None):
            request_spec.requested_destination = objects.Destination()

        destination = request_spec.requested_destination
        destination.append_forbidden_aggregates(agg.uuid
                                                for agg in isolated_aggregates)

    return True
コード例 #22
0
 def test_process_use_force_hosts(self):
     flavor = objects.Flavor(vcpus=1,
                             memory_mb=1024,
                             root_gb=15,
                             ephemeral_gb=0,
                             swap=0)
     fake_spec = objects.RequestSpec(flavor=flavor, force_hosts=['test'])
     expected = utils.ResourceRequest()
     expected._rg_by_id[None] = objects.RequestGroup(
         use_same_provider=False,
         resources={
             'VCPU': 1,
             'MEMORY_MB': 1024,
             'DISK_GB': 15,
         },
     )
     expected._limit = None
     resources = utils.resources_from_request_spec(fake_spec)
     self.assertResourceRequestsEqual(expected, resources)
     expected_querystring = (
         'resources=DISK_GB%3A15%2CMEMORY_MB%3A1024%2CVCPU%3A1')
     self.assertEqual(expected_querystring, resources.to_querystring())
コード例 #23
0
ファイル: test_report_client.py プロジェクト: jwatford/nova
 def test_alloc_cands_smoke(self):
     """Simple call to get_allocation_candidates for version checking."""
     with self._interceptor():
         self.client.get_allocation_candidates(self.context,
                                               utils.ResourceRequest())
コード例 #24
0
    def test_get_resources_from_request_spec_granular(self):
        flavor = objects.Flavor(
            vcpus=1,
            memory_mb=1024,
            root_gb=10,
            ephemeral_gb=0,
            swap=0,
            extra_specs={
                'resources1:VGPU': '1',
                'resources1:VGPU_DISPLAY_HEAD': '2',
                # Replace
                'resources3:VCPU': '2',
                # Stay separate (don't sum)
                'resources42:SRIOV_NET_VF': '1',
                'resources24:SRIOV_NET_VF': '2',
                # Ignore
                'some:bogus': 'value',
                # Custom in the unnumbered group (merge with DISK_GB)
                'resources:CUSTOM_THING': '123',
                # Traits make it through
                'trait3:CUSTOM_SILVER': 'required',
                'trait3:CUSTOM_GOLD': 'required',
                # Delete standard
                'resources86:MEMORY_MB': '0',
                # Standard and custom zeroes don't make it through
                'resources:IPV4_ADDRESS': '0',
                'resources:CUSTOM_FOO': '0',
                # Bogus values don't make it through
                'resources1:MEMORY_MB': 'bogus',
                'group_policy': 'none'
            })
        expected_resources = utils.ResourceRequest()
        expected_resources._group_policy = 'none'
        expected_resources._rg_by_id[None] = objects.RequestGroup(
            use_same_provider=False,
            resources={
                'DISK_GB': 10,
                'CUSTOM_THING': 123,
            })
        expected_resources._rg_by_id['1'] = objects.RequestGroup(
            resources={
                'VGPU': 1,
                'VGPU_DISPLAY_HEAD': 2,
            })
        expected_resources._rg_by_id['3'] = objects.RequestGroup(
            resources={
                'VCPU': 2,
            },
            required_traits={
                'CUSTOM_GOLD',
                'CUSTOM_SILVER',
            })
        expected_resources._rg_by_id['24'] = objects.RequestGroup(resources={
            'SRIOV_NET_VF':
            2,
        }, )
        expected_resources._rg_by_id['42'] = objects.RequestGroup(
            resources={
                'SRIOV_NET_VF': 1,
            })

        rr = self._test_resources_from_request_spec(expected_resources, flavor)
        expected_querystring = ('group_policy=none&'
                                'limit=1000&'
                                'required3=CUSTOM_GOLD%2CCUSTOM_SILVER&'
                                'resources=CUSTOM_THING%3A123%2CDISK_GB%3A10&'
                                'resources1=VGPU%3A1%2CVGPU_DISPLAY_HEAD%3A2&'
                                'resources24=SRIOV_NET_VF%3A2&'
                                'resources3=VCPU%3A2&'
                                'resources42=SRIOV_NET_VF%3A1')
        self.assertEqual(expected_querystring, rr.to_querystring())
コード例 #25
0
    def test_resource_request_from_extra_specs(self):
        extra_specs = {
            'resources:VCPU': '2',
            'resources:MEMORY_MB': '2048',
            'trait:HW_CPU_X86_AVX': 'required',
            # Key skipped because no colons
            'nocolons': '42',
            'trait:CUSTOM_MAGIC': 'required',
            'trait:CUSTOM_BRONZE': 'forbidden',
            # Resource skipped because invalid resource class name
            'resources86:CUTSOM_MISSPELLED': '86',
            'resources1:SRIOV_NET_VF': '1',
            # Resource skipped because non-int-able value
            'resources86:CUSTOM_FOO': 'seven',
            # Resource skipped because negative value
            'resources86:CUSTOM_NEGATIVE': '-7',
            'resources1:IPV4_ADDRESS': '1',
            # Trait skipped because unsupported value
            'trait86:CUSTOM_GOLD': 'preferred',
            'trait1:CUSTOM_PHYSNET_NET1': 'required',
            'trait1:CUSTOM_PHYSNET_NET2': 'forbidden',
            'resources2:SRIOV_NET_VF': '1',
            'resources2:IPV4_ADDRESS': '2',
            'trait2:CUSTOM_PHYSNET_NET2': 'required',
            'trait2:HW_NIC_ACCEL_SSL': 'required',
            # Groupings that don't quite match the patterns are ignored
            'resources_5:SRIOV_NET_VF': '7',
            'traitFoo:HW_NIC_ACCEL_SSL': 'required',
            # Solo resource, no corresponding traits
            'resources3:DISK_GB': '5',
            'group_policy': 'isolate',
        }
        # Build up a ResourceRequest from the inside to compare against.
        expected = utils.ResourceRequest()
        expected._group_policy = 'isolate'
        expected._rg_by_id[None] = objects.RequestGroup(
            use_same_provider=False,
            resources={
                'VCPU': 2,
                'MEMORY_MB': 2048,
            },
            required_traits={
                'HW_CPU_X86_AVX',
                'CUSTOM_MAGIC',
            },
            forbidden_traits={
                'CUSTOM_BRONZE',
            },
        )
        expected._rg_by_id['1'] = objects.RequestGroup(
            resources={
                'SRIOV_NET_VF': 1,
                'IPV4_ADDRESS': 1,
            },
            required_traits={
                'CUSTOM_PHYSNET_NET1',
            },
            forbidden_traits={
                'CUSTOM_PHYSNET_NET2',
            },
        )
        expected._rg_by_id['2'] = objects.RequestGroup(
            resources={
                'SRIOV_NET_VF': 1,
                'IPV4_ADDRESS': 2,
            },
            required_traits={
                'CUSTOM_PHYSNET_NET2',
                'HW_NIC_ACCEL_SSL',
            })
        expected._rg_by_id['3'] = objects.RequestGroup(resources={
            'DISK_GB': 5,
        })

        rr = utils.ResourceRequest.from_extra_specs(extra_specs)
        self.assertResourceRequestsEqual(expected, rr)
        expected_querystring = (
            'group_policy=isolate&'
            'limit=1000&'
            'required=CUSTOM_MAGIC%2CHW_CPU_X86_AVX%2C%21CUSTOM_BRONZE&'
            'required1=CUSTOM_PHYSNET_NET1%2C%21CUSTOM_PHYSNET_NET2&'
            'required2=CUSTOM_PHYSNET_NET2%2CHW_NIC_ACCEL_SSL&'
            'resources=MEMORY_MB%3A2048%2CVCPU%3A2&'
            'resources1=IPV4_ADDRESS%3A1%2CSRIOV_NET_VF%3A1&'
            'resources2=IPV4_ADDRESS%3A2%2CSRIOV_NET_VF%3A1&'
            'resources3=DISK_GB%3A5')
        self.assertEqual(expected_querystring, rr.to_querystring())