diff --git a/osc_placement/resources/allocation_candidate.py b/osc_placement/resources/allocation_candidate.py new file mode 100644 index 0000000..4044ff5 --- /dev/null +++ b/osc_placement/resources/allocation_candidate.py @@ -0,0 +1,100 @@ +# 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 osc_lib.command import command +from osc_lib import exceptions + +from osc_placement import version + + +BASE_URL = '/allocation_candidates' +FIELDS = ('#', 'allocation', 'resource provider', 'inventory used/capacity') + + +class ListAllocationCandidate(command.Lister, version.CheckerMixin): + + """List allocation candidates. + + Returns a representation of a collection of allocation requests and + resource provider summaries. Each allocation request has information + to issue an "openstack resource provider allocation set" request to claim + resources against a related set of resource providers. + + As several allocation requests are available its necessary to select one. + To make a decision, resource provider summaries are provided with the + inventory/capacity information. + + For example:: + + $ export OS_PLACEMENT_API_VERSION=1.10 + $ openstack allocation candidate list --resource VCPU=1 + +---+------------+-------------------------+-------------------------+ + | # | allocation | resource provider | inventory used/capacity | + +---+------------+-------------------------+-------------------------+ + | 1 | VCPU=1 | 66bcaca9-9263-45b1-a569 | VCPU=0/128 | + | | | -ea708ff7a968 | | + +---+------------+-------------------------+-------------------------+ + + In this case, the user is looking for resource providers that can have + capacity to allocate 1 VCPU resource class. There is one resource provider + that can serve that allocation request and that resource providers current + VCPU inventory used is 0 and available capacity is 128. + + This command requires at least ``--os-placement-api-version 1.10``. + """ + + def get_parser(self, prog_name): + parser = super(ListAllocationCandidate, self).get_parser(prog_name) + + parser.add_argument( + '--resource', + metavar='=', + action='append', + default=[], + help='String indicating an amount of resource of a specified ' + 'class that providers in each allocation request must ' + 'collectively have the capacity and availability to serve. ' + 'Can be specified multiple times per resource class. ' + 'For example: ' + '``--resource VCP=4 --resource DISK_GB=64 ' + '--resource MEMORY_MB=2048``' + ) + + return parser + + @version.check(version.ge('1.10')) + def take_action(self, parsed_args): + if not parsed_args.resource: + raise exceptions.CommandError( + 'At least one --resource must be specified.') + + http = self.app.client_manager.placement + + params = {'resources': ','.join( + resource.replace('=', ':') for resource in parsed_args.resource)} + resp = http.request('GET', BASE_URL, params=params).json() + + rps = {} + for rp_uuid, resources in resp['provider_summaries'].items(): + rps[rp_uuid] = ','.join( + '%s=%s/%s' % (rc, value['used'], value['capacity']) + for rc, value in resources['resources'].items()) + rows = [] + for i, allocation_req in enumerate(resp['allocation_requests']): + for allocation in allocation_req['allocations']: + rp = allocation['resource_provider']['uuid'] + req = ','.join( + '%s=%s' % (rc, value) + for rc, value in allocation['resources'].items()) + rows.append([i + 1, req, rp, rps[rp]]) + + return FIELDS, rows diff --git a/osc_placement/tests/functional/base.py b/osc_placement/tests/functional/base.py index 6136611..46c7095 100644 --- a/osc_placement/tests/functional/base.py +++ b/osc_placement/tests/functional/base.py @@ -248,3 +248,8 @@ class BaseTestCase(base.BaseTestCase): def resource_provider_trait_delete(self, uuid): cmd = 'resource provider trait delete %s ' % uuid self.openstack(cmd) + + def allocation_candidate_list(self, *resources): + cmd = 'allocation candidate list ' + ' '.join( + '--resource %s' % resource for resource in resources) + return self.openstack(cmd, use_json=True) diff --git a/osc_placement/tests/functional/test_allocation_candidate.py b/osc_placement/tests/functional/test_allocation_candidate.py new file mode 100644 index 0000000..544f771 --- /dev/null +++ b/osc_placement/tests/functional/test_allocation_candidate.py @@ -0,0 +1,92 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from osc_placement.tests.functional import base + + +def sorted_resources(resource): + return ','.join(sorted(resource.split(','))) + + +class TestAllocationCandidate(base.BaseTestCase): + VERSION = '1.10' + + def test_list_no_resource_specified_error(self): + self.assertCommandFailed( + 'At least one --resource must be specified', + self.openstack, 'allocation candidate list') + + def test_list_empty(self): + self.assertEqual([], self.allocation_candidate_list( + 'MEMORY_MB=999999999')) + + def test_list_one(self): + rp = self.resource_provider_create() + self.resource_inventory_set(rp['uuid'], 'MEMORY_MB=1024') + candidates = self.allocation_candidate_list('MEMORY_MB=256') + self.assertIn( + rp['uuid'], + [candidate['resource provider'] for candidate in candidates]) + + def assertResourceEqual(self, r1, r2): + self.assertEqual(sorted_resources(r1), sorted_resources(r2)) + + def test_list_multiple(self): + rp1 = self.resource_provider_create() + rp2 = self.resource_provider_create() + self.resource_inventory_set( + rp1['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') + self.resource_inventory_set( + rp2['uuid'], 'MEMORY_MB=16384', 'DISK_GB=1024') + candidates = self.allocation_candidate_list( + 'MEMORY_MB=1024', 'DISK_GB=80') + rps = {c['resource provider']: c for c in candidates} + self.assertResourceEqual( + 'MEMORY_MB=1024,DISK_GB=80', rps[rp1['uuid']]['allocation']) + self.assertResourceEqual( + 'MEMORY_MB=1024,DISK_GB=80', rps[rp2['uuid']]['allocation']) + self.assertResourceEqual( + 'MEMORY_MB=0/8192,DISK_GB=0/512', + rps[rp1['uuid']]['inventory used/capacity']) + self.assertResourceEqual( + 'MEMORY_MB=0/16384,DISK_GB=0/1024', + rps[rp2['uuid']]['inventory used/capacity']) + + def test_list_shared(self): + rp1 = self.resource_provider_create() + rp2 = self.resource_provider_create() + self.resource_inventory_set(rp1['uuid'], 'MEMORY_MB=8192') + self.resource_inventory_set(rp2['uuid'], 'DISK_GB=1024') + agg = str(uuid.uuid4()) + self.resource_provider_aggregate_set(rp1['uuid'], agg) + self.resource_provider_aggregate_set(rp2['uuid'], agg) + self.resource_provider_trait_set( + rp2['uuid'], 'MISC_SHARES_VIA_AGGREGATE') + candidates = self.allocation_candidate_list( + 'MEMORY_MB=1024', 'DISK_GB=80') + rps = {c['resource provider']: c for c in candidates} + self.assertResourceEqual( + 'MEMORY_MB=1024', rps[rp1['uuid']]['allocation']) + self.assertResourceEqual( + 'DISK_GB=80', rps[rp2['uuid']]['allocation']) + self.assertResourceEqual( + 'MEMORY_MB=0/8192', rps[rp1['uuid']]['inventory used/capacity']) + self.assertResourceEqual( + 'DISK_GB=0/1024', rps[rp2['uuid']]['inventory used/capacity']) + self.assertEqual( + rps[rp2['uuid']]['#'], rps[rp1['uuid']]['#']) + + def test_fail_if_unknown_rc(self): + self.assertCommandFailed( + 'No such resource', self.allocation_candidate_list, 'UNKNOWN=10') diff --git a/osc_placement/version.py b/osc_placement/version.py index be841fb..f935bbc 100644 --- a/osc_placement/version.py +++ b/osc_placement/version.py @@ -25,6 +25,7 @@ SUPPORTED_VERSIONS = [ '1.7', '1.8', '1.9', + '1.10', ] diff --git a/releasenotes/notes/microversion-1.10-03ab71969921a0e4.yaml b/releasenotes/notes/microversion-1.10-03ab71969921a0e4.yaml new file mode 100644 index 0000000..7012678 --- /dev/null +++ b/releasenotes/notes/microversion-1.10-03ab71969921a0e4.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + The ``openstack allocation candidate list`` command is + available starting from microversion `1.10`_. + + See the command documentation for `allocation candidate list`_ for + more details. + + .. _1.10: https://docs.openstack.org/nova/latest/user/placement.html#allocation-candidates-maximum-in-pike + .. _allocation candidate list: https://docs.openstack.org/osc-placement/latest/cli/index.html#allocation-candidate-list diff --git a/setup.cfg b/setup.cfg index 8c0a6ec..2fb6ce3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,6 +56,7 @@ openstack.placement.v1 = resource_provider_trait_list = osc_placement.resources.trait:ListResourceProviderTrait resource_provider_trait_set = osc_placement.resources.trait:SetResourceProviderTrait resource_provider_trait_delete = osc_placement.resources.trait:DeleteResourceProviderTrait + allocation_candidate_list = osc_placement.resources.allocation_candidate:ListAllocationCandidate [build_sphinx] source-dir = doc/source