289 lines
13 KiB
Python
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
|