335 lines
14 KiB
Python
335 lines
14 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.
|
|
|
|
from osc_lib.command import command
|
|
from osc_lib import exceptions
|
|
from osc_lib import utils
|
|
|
|
from osc_placement import version
|
|
|
|
|
|
BASE_URL = '/allocations'
|
|
|
|
|
|
def parse_allocations(allocation_strings):
|
|
allocations = {}
|
|
for allocation_string in allocation_strings:
|
|
if '=' not in allocation_string or ',' not in allocation_string:
|
|
raise ValueError('Incorrect allocation string format')
|
|
parsed = dict(kv.split('=') for kv in allocation_string.split(','))
|
|
if 'rp' not in parsed:
|
|
raise ValueError('Resource provider parameter is required '
|
|
'for allocation string')
|
|
resources = {k: int(v) for k, v in parsed.items() if k != 'rp'}
|
|
if parsed['rp'] not in allocations:
|
|
allocations[parsed['rp']] = resources
|
|
else:
|
|
prev_rp = allocations[parsed['rp']]
|
|
for resource, value in resources.items():
|
|
if resource in prev_rp and prev_rp[resource] != value:
|
|
raise exceptions.CommandError(
|
|
'Conflict detected for '
|
|
'resource provider {} resource class {}'.format(
|
|
parsed['rp'], resource))
|
|
allocations[parsed['rp']].update(resources)
|
|
return allocations
|
|
|
|
|
|
class SetAllocation(command.Lister, version.CheckerMixin):
|
|
"""Replaces the set of resource allocation(s) for a given consumer.
|
|
|
|
Note that this is a full replacement of the existing allocations. If you
|
|
want to retain the existing allocations and add a new resource class
|
|
allocation, you must specify all resource class allocations, old and new.
|
|
|
|
From ``--os-placement-api-version 1.8`` it is required to specify
|
|
``--project-id`` and ``--user-id`` to set allocations. It is highly
|
|
recommended to provide a ``--project-id`` and ``--user-id`` when setting
|
|
allocations for accounting and data consistency reasons.
|
|
|
|
Starting with ``--os-placement-api-version 1.12`` the API response
|
|
contains the ``project_id`` and ``user_id`` of allocations which also
|
|
appears in the CLI output.
|
|
|
|
Starting with ``--os-placement-api-version 1.28`` a consumer generation is
|
|
used which facilitates safe concurrent modification of an allocation.
|
|
"""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(SetAllocation, self).get_parser(prog_name)
|
|
|
|
parser.add_argument(
|
|
'uuid',
|
|
metavar='<uuid>',
|
|
help='UUID of the consumer'
|
|
)
|
|
parser.add_argument(
|
|
'--allocation',
|
|
metavar='<rp=resource-provider-id,'
|
|
'resource-class-name=amount-of-resource-used>',
|
|
action='append',
|
|
default=[],
|
|
help='Create (or update) an allocation of a resource class. '
|
|
'Specify option multiple times to set multiple allocations.'
|
|
)
|
|
parser.add_argument(
|
|
'--project-id',
|
|
metavar='project_id',
|
|
help='ID of the consuming project. '
|
|
'This option is required starting from '
|
|
'``--os-placement-api-version 1.8``.',
|
|
required=self.compare_version(version.ge('1.8'))
|
|
)
|
|
parser.add_argument(
|
|
'--user-id',
|
|
metavar='user_id',
|
|
help='ID of the consuming user. '
|
|
'This option is required starting from '
|
|
'``--os-placement-api-version 1.8``.',
|
|
required=self.compare_version(version.ge('1.8'))
|
|
)
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
http = self.app.client_manager.placement
|
|
url = BASE_URL + '/' + parsed_args.uuid
|
|
|
|
# Determine if we need to honor consumer generations.
|
|
supports_consumer_generation = self.compare_version(version.ge('1.28'))
|
|
if supports_consumer_generation:
|
|
# Get the existing consumer generation via GET.
|
|
payload = http.request('GET', url).json()
|
|
consumer_generation = payload.get('consumer_generation')
|
|
|
|
allocations = parse_allocations(parsed_args.allocation)
|
|
if not allocations:
|
|
raise exceptions.CommandError(
|
|
'At least one resource allocation must be specified')
|
|
|
|
if self.compare_version(version.ge('1.12')):
|
|
allocations = {
|
|
rp: {'resources': resources}
|
|
for rp, resources in allocations.items()}
|
|
else:
|
|
allocations = [
|
|
{'resource_provider': {'uuid': rp}, 'resources': resources}
|
|
for rp, resources in allocations.items()]
|
|
|
|
payload = {'allocations': allocations}
|
|
# Include consumer_generation for 1.28+. Note that if this is the
|
|
# first set of allocations the consumer_generation will be None.
|
|
if supports_consumer_generation:
|
|
payload['consumer_generation'] = consumer_generation
|
|
if self.compare_version(version.ge('1.8')):
|
|
payload['project_id'] = parsed_args.project_id
|
|
payload['user_id'] = parsed_args.user_id
|
|
elif parsed_args.project_id or parsed_args.user_id:
|
|
self.log.warning('--project-id and --user-id options do not '
|
|
'affect allocation for '
|
|
'--os-placement-api-version less than 1.8')
|
|
http.request('PUT', url, json=payload)
|
|
resp = http.request('GET', url).json()
|
|
per_provider = resp['allocations'].items()
|
|
|
|
fields = ('resource_provider', 'generation', 'resources')
|
|
allocs = [dict(resource_provider=k, **v) for k, v in per_provider]
|
|
if self.compare_version(version.ge('1.12')):
|
|
fields += ('project_id', 'user_id')
|
|
[alloc.update(project_id=resp['project_id'],
|
|
user_id=resp['user_id'])
|
|
for alloc in allocs]
|
|
|
|
rows = (utils.get_dict_properties(a, fields) for a in allocs)
|
|
return fields, rows
|
|
|
|
|
|
class UnsetAllocation(command.Lister, version.CheckerMixin):
|
|
"""Removes one or more sets of provider allocations for a consumer.
|
|
|
|
Note that omitting both the ``--provider`` and the ``--resource-class``
|
|
option is equivalent to removing all allocations for the given consumer.
|
|
|
|
This command requires ``--os-placement-api-version 1.12`` or greater. Use
|
|
``openstack resource provider allocation set`` for older versions.
|
|
"""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(UnsetAllocation, self).get_parser(prog_name)
|
|
|
|
parser.add_argument(
|
|
'uuid',
|
|
metavar='<consumer_uuid>',
|
|
help='UUID of the consumer. It is strongly recommended to use '
|
|
'``--os-placement-api-version 1.28`` or greater when using '
|
|
'this option to ensure the other allocation information is '
|
|
'retained. '
|
|
)
|
|
parser.add_argument(
|
|
'--provider',
|
|
metavar='provider_uuid',
|
|
action='append',
|
|
default=[],
|
|
help='UUID of a specific resource provider from which to remove '
|
|
'allocations for the given consumer. This is useful when the '
|
|
'consumer has allocations on more than one provider, for '
|
|
'example after evacuating a server to another compute node '
|
|
'and you want to cleanup allocations on the source compute '
|
|
'node resource provider in order to delete it. Specify '
|
|
'multiple times to remove allocations against multiple '
|
|
'resource providers. Omit this option to remove all '
|
|
'allocations for the consumer, or to remove all allocations'
|
|
'of a specific resource class from all the resource provider '
|
|
'with the ``--resource_class`` option. '
|
|
)
|
|
parser.add_argument(
|
|
'--resource-class',
|
|
metavar='resource_class',
|
|
action='append',
|
|
default=[],
|
|
help='Name of a resource class from which to remove allocations '
|
|
'for the given consumer. This is useful when the consumer '
|
|
'has allocations on more than one resource class. '
|
|
'By default, this will remove allocations for the given '
|
|
'resource class from all the providers. If ``--provider`` '
|
|
'option is also specified, allocations to remove will be '
|
|
'limited to that resource class of the given resource '
|
|
'provider.'
|
|
)
|
|
return parser
|
|
|
|
# NOTE(mriedem): We require >= 1.12 because PUT requires project_id/user_id
|
|
# since 1.8 but GET does not return project_id/user_id until 1.12 and we
|
|
# do not want to add --project-id and --user-id options to this command
|
|
# like in the set command. If someone needs to use an older microversion or
|
|
# change the user/project they can use the set command.
|
|
@version.check(version.ge('1.12'))
|
|
def take_action(self, parsed_args):
|
|
http = self.app.client_manager.placement
|
|
url = BASE_URL + '/' + parsed_args.uuid
|
|
|
|
# Get the current allocations.
|
|
payload = http.request('GET', url).json()
|
|
allocations = payload['allocations']
|
|
|
|
if parsed_args.resource_class:
|
|
# Remove the given resource class. Do not error out if the
|
|
# consumer does not have allocations against that resource
|
|
# class.
|
|
rp_uuids = set(allocations)
|
|
if parsed_args.provider:
|
|
# If providers are also specified, we limit to remove
|
|
# allocations only from those providers
|
|
rp_uuids &= set(parsed_args.provider)
|
|
for rp_uuid in rp_uuids:
|
|
for rc in parsed_args.resource_class:
|
|
allocations[rp_uuid]['resources'].pop(rc, None)
|
|
if not allocations[rp_uuid]['resources']:
|
|
allocations.pop(rp_uuid, None)
|
|
else:
|
|
if parsed_args.provider:
|
|
# Remove the given provider(s) from the allocations if it
|
|
# exists. Do not error out if the consumer does not have
|
|
# allocations against a provider in case we lost a race since
|
|
# the allocations are in the state the user wants them in
|
|
# anyway.
|
|
for rp_uuid in parsed_args.provider:
|
|
allocations.pop(rp_uuid, None)
|
|
else:
|
|
# No --provider(s) specified so remove allocations from all
|
|
# providers.
|
|
allocations = {}
|
|
|
|
supports_consumer_generation = self.compare_version(version.ge('1.28'))
|
|
# 1.28+ allows PUTing an empty allocations dict as long as a
|
|
# consumer_generation is specified.
|
|
if allocations or supports_consumer_generation:
|
|
payload['allocations'] = allocations
|
|
http.request('PUT', url, json=payload)
|
|
else:
|
|
# The user must have removed all of the allocations so just DELETE
|
|
# the allocations since we cannot PUT with an empty allocations
|
|
# dict before 1.28.
|
|
http.request('DELETE', url)
|
|
|
|
resp = http.request('GET', url).json()
|
|
per_provider = resp['allocations'].items()
|
|
|
|
fields = ('resource_provider', 'generation', 'resources',
|
|
'project_id', 'user_id')
|
|
allocs = [dict(project_id=resp['project_id'], user_id=resp['user_id'],
|
|
resource_provider=k, **v) for k, v in per_provider]
|
|
rows = (utils.get_dict_properties(a, fields) for a in allocs)
|
|
return fields, rows
|
|
|
|
|
|
class ShowAllocation(command.Lister, version.CheckerMixin):
|
|
"""Show resource allocations for a given consumer.
|
|
|
|
Starting with ``--os-placement-api-version 1.12`` the API response contains
|
|
the ``project_id`` and ``user_id`` of allocations which also appears in the
|
|
CLI output.
|
|
"""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(ShowAllocation, self).get_parser(prog_name)
|
|
|
|
parser.add_argument(
|
|
'uuid',
|
|
metavar='<uuid>',
|
|
help='UUID of the consumer'
|
|
)
|
|
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
http = self.app.client_manager.placement
|
|
|
|
url = BASE_URL + '/' + parsed_args.uuid
|
|
resp = http.request('GET', url).json()
|
|
per_provider = resp['allocations'].items()
|
|
if self.compare_version(version.ge('1.12')):
|
|
allocs = [dict(
|
|
resource_provider=k,
|
|
project_id=resp['project_id'],
|
|
user_id=resp['user_id'],
|
|
**v) for k, v in per_provider]
|
|
else:
|
|
allocs = [dict(resource_provider=k, **v) for k, v in per_provider]
|
|
|
|
fields = ('resource_provider', 'generation', 'resources')
|
|
if self.compare_version(version.ge('1.12')):
|
|
fields += ('project_id', 'user_id')
|
|
|
|
rows = (utils.get_dict_properties(a, fields) for a in allocs)
|
|
return fields, rows
|
|
|
|
|
|
class DeleteAllocation(command.Command):
|
|
"""Delete all resource allocations for a given consumer."""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(DeleteAllocation, self).get_parser(prog_name)
|
|
|
|
parser.add_argument(
|
|
'uuid',
|
|
metavar='<uuid>',
|
|
help='UUID of the consumer'
|
|
)
|
|
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
http = self.app.client_manager.placement
|
|
|
|
url = BASE_URL + '/' + parsed_args.uuid
|
|
http.request('DELETE', url)
|