osc-placement/osc_placement/resources/allocation_candidate.py

289 lines
13 KiB
Python

# 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 argparse
import collections
from osc_lib.command import command
from osc_lib import exceptions
from osc_placement.resources import common
from osc_placement import version
BASE_URL = '/allocation_candidates'
class GroupAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
group, = values
namespace._current_group = group
groups = namespace.__dict__.setdefault('groups', {})
groups[group] = collections.defaultdict(list)
class AppendToGroup(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
if getattr(namespace, '_current_group', None) is None:
groups = namespace.__dict__.setdefault('groups', {})
namespace._current_group = ''
groups[''] = collections.defaultdict(list)
namespace.groups[namespace._current_group][self.dest].append(values)
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='<resource_class>=<value>',
dest='resources',
action=AppendToGroup,
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 VCPU=4 --resource DISK_GB=64 '
'--resource MEMORY_MB=2048``'
)
parser.add_argument(
'--limit',
metavar='<limit>',
help='A positive integer to limit '
'the maximum number of allocation candidates. '
'This option requires at least '
'``--os-placement-api-version 1.16``.'
)
parser.add_argument(
'--required',
metavar='<required>',
action=AppendToGroup,
help='A required trait. May be repeated. Allocation candidates '
'must collectively contain all of the required traits. '
'This option requires at least '
'``--os-placement-api-version 1.17``. '
'Since ``--os-placement-api-version 1.39`` the value of '
'this parameter can be a comma separated list of trait names '
'to express OR relationship between those traits.'
)
parser.add_argument(
'--forbidden',
metavar='<forbidden>',
action=AppendToGroup,
help='A forbidden trait. May be repeated. Returned allocation '
'candidates must not contain any of the specified traits. '
'This option requires at least '
'``--os-placement-api-version 1.22``.'
)
# NOTE(tetsuro): --aggregate-uuid is deprecated in Jan 2020 in 1.x
# release. Do not remove before Jan 2021 and a 2.x release.
aggregate_group = parser.add_mutually_exclusive_group()
aggregate_group.add_argument(
"--member-of",
action=AppendToGroup,
metavar='<member_of>',
help='A list of comma-separated UUIDs of the resource provider '
'aggregates. The returned allocation candidates must be '
'associated with at least one of the aggregates identified '
'by uuid. This param requires at least '
'``--os-placement-api-version 1.21`` and can be repeated to '
'add(restrict) the condition with '
'``--os-placement-api-version 1.24`` or greater. '
'For example, to get candidates in either of agg1 or agg2 '
'and definitely in agg3, specify:\n\n'
'``--member_of <agg1>,<agg2> --member_of <agg3>``'
)
aggregate_group.add_argument(
'--aggregate-uuid',
action=AppendToGroup,
metavar='<aggregate_uuid>',
help=argparse.SUPPRESS
)
parser.add_argument(
'--group',
action=GroupAction,
metavar='<group>',
help='An integer to group granular requests. If specified, '
'following given options of resources, required/forbidden '
'traits, and aggregate are associated to that group and will '
'be satisfied by the same resource provider in the response. '
'Can be repeated to get candidates from multiple resource '
'providers in the same resource provider tree. '
'For example, ``--group 1 --resource VCPU=3 --required '
'HW_CPU_X86_AVX --group 2 --resource VCPU=2 --required '
'HW_CPU_X86_SSE`` will provide candidates where three VCPUs '
'comes from a provider with ``HW_CPU_X86_AVX`` trait and '
'two VCPUs from a provider with ``HW_CPU_X86_SSE`` trait. '
'This option requires at least '
'``--os-placement-api-version 1.25`` or greater, but to have '
'placement server be aware of resource provider tree, use '
'``--os-placement-api-version 1.29`` or greater.'
)
parser.add_argument(
'--group-policy',
choices=['none', 'isolate'],
default='none',
metavar='<group_policy>',
help='This indicates how the groups should interact when multiple '
'groups are supplied. With group_policy=none (default), '
'separate groups may or may not be satisfied by the same '
'provider. With group_policy=isolate, numbered groups are '
'guaranteed to be satisfied by different providers.'
)
return parser
@version.check(version.ge('1.10'))
def take_action(self, parsed_args):
http = self.app.client_manager.placement
params = {}
if 'groups' not in parsed_args:
raise exceptions.CommandError(
'At least one --resource must be specified.')
if 'limit' in parsed_args and parsed_args.limit:
# Fail if --limit but not high enough microversion.
self.check_version(version.ge('1.16'))
params['limit'] = int(parsed_args.limit)
if any(parsed_args.groups):
self.check_version(version.ge('1.25'))
params['group_policy'] = parsed_args.group_policy
for suffix, group in parsed_args.groups.items():
def _get_key(name):
return name + suffix
if 'resources' not in group:
raise exceptions.CommandError(
'--resources should be provided in group %s', suffix)
for resource in group['resources']:
if not len(resource.split('=')) == 2:
raise exceptions.CommandError(
'Arguments to --resource must be of form '
'<resource_class>=<value>')
params[_get_key('resources')] = ','.join(
resource.replace('=', ':') for resource in group['resources'])
# We need to handle required and forbidden together as they all
# end up in the same query param on the API.
# First just check that the requested feature is aligned with the
# request microversion
required_traits = []
if 'required' in group and group['required']:
# Fail if --required but not high enough microversion.
self.check_version(version.ge('1.17'))
if any(',' in required for required in group['required']):
self.check_version(version.ge('1.39'))
required_traits = group['required']
forbidden_traits = []
if 'forbidden' in group and group['forbidden']:
self.check_version(version.ge('1.22'))
forbidden_traits = ['!' + f for f in group['forbidden']]
# Then collect the required query params containing both required
# and forbidden traits
params[_get_key('required')] = (
common.get_required_query_param_from_args(
required_traits, forbidden_traits)
)
if 'aggregate_uuid' in group and group['aggregate_uuid']:
# Fail if --aggregate_uuid but not high enough microversion.
self.check_version(version.ge('1.21'))
self.deprecated_option_warning(
"--aggregate-uuid", "--member-of")
params[_get_key('member_of')] = 'in:' + ','.join(
group['aggregate_uuid'])
if 'member_of' in group and group['member_of']:
# Fail if --member-of but not high enough microversion.
self.check_version(version.ge('1.21'))
params[_get_key('member_of')] = [
'in:' + aggs for aggs in group['member_of']]
resp = http.request('GET', BASE_URL, params=params).json()
rp_resources = {}
include_traits = self.compare_version(version.ge('1.17'))
if include_traits:
rp_traits = {}
for rp_uuid, resources in resp['provider_summaries'].items():
rp_resources[rp_uuid] = ','.join(
'%s=%s/%s' % (rc, value['used'], value['capacity'])
for rc, value in resources['resources'].items())
if include_traits:
rp_traits[rp_uuid] = ','.join(resources['traits'])
rows = []
if self.compare_version(version.ge('1.12')):
for i, allocation_req in enumerate(resp['allocation_requests']):
for rp, resources in allocation_req['allocations'].items():
req = ','.join(
'%s=%s' % (rc, value)
for rc, value in resources['resources'].items())
if include_traits:
row = [i + 1, req, rp, rp_resources[rp], rp_traits[rp]]
else:
row = [i + 1, req, rp, rp_resources[rp]]
rows.append(row)
else:
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, rp_resources[rp]])
fields = ('#', 'allocation', 'resource provider',
'inventory used/capacity')
if include_traits:
fields += ('traits',)
return fields, rows