osc-placement/osc_placement/resources/inventory.py

301 lines
9.6 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 collections import defaultdict
from osc_lib.command import command
from osc_lib import utils
BASE_URL = '/resource_providers/{uuid}/inventories'
PER_CLASS_URL = BASE_URL + '/{resource_class}'
RP_BASE_URL = '/resource_providers'
INVENTORY_FIELDS = {
'allocation_ratio': {
'type': float,
'required': False,
'help': ('It is used in determining whether consumption '
'of the resource of the provider can exceed '
'physical constraints. For example, for a vCPU resource '
'with: allocation_ratio = 16.0, total = 8. '
'Overall capacity is equal to 128 vCPUs.')
},
'min_unit': {
'type': int,
'required': False,
'help': ('A minimum amount any single allocation against '
'an inventory can have.')
},
'max_unit': {
'type': int,
'required': False,
'help': ('A maximum amount any single allocation against '
'an inventory can have.')
},
'reserved': {
'type': int,
'required': False,
'help': ('The amount of the resource a provider has reserved '
'for its own use.')
},
'step_size': {
'type': int,
'required': False,
'help': ('A representation of the divisible amount of the resource '
'that may be requested. For example, step_size = 5 means '
'that only values divisible by 5 (5, 10, 15, etc.) '
'can be requested.')
},
'total': {
'type': int,
'required': True,
'help': ('The actual amount of the resource that the provider '
'can accommodate.')
}
}
FIELDS = tuple(INVENTORY_FIELDS.keys())
RC_HELP = ('<resource_class> is an entity that indicates standard or '
'deployer-specific resources that can be provided by a resource '
'provider. For example, VCPU, MEMORY_MB, DISK_GB.')
def parse_resource_argument(resource):
parts = resource.split('=')
if len(parts) != 2:
raise ValueError(
'Resource argument must have "name=value" format')
name, value = parts
parts = name.split(':')
if len(parts) == 2:
name, field = parts
elif len(parts) == 1:
name = parts[0]
field = 'total'
else:
raise ValueError('Resource argument can contain only one colon')
if not all([name, field, value]):
raise ValueError('Name, field and value must be not empty')
if field not in INVENTORY_FIELDS:
raise ValueError('Unknown inventory field %s' % field)
value = INVENTORY_FIELDS[field]['type'](value)
return name, field, value
class SetInventory(command.Lister):
"""Replaces the set of inventory records for the resource provider.
Note that this is a full replacement of the existing inventory. If you
want to retain the existing inventory and add a new resource class
inventory, you must specify all resource class inventory, old and new.
If a specific inventory field is not specified for a given resource class,
it is assumed to be the total, i.e. --resource VCPU=16 is equivalent to
--resource VCPU:total=16.
Example: openstack resource provider inventory set <uuid> \
--resource VCPU=16 \
--resource MEMORY_MB=2048 \
--resource MEMORY_MB:step_size=128
"""
def get_parser(self, prog_name):
parser = super(SetInventory, self).get_parser(prog_name)
parser.add_argument(
'uuid',
metavar='<uuid>',
help='UUID of the resource provider'
)
fields_help = '\n'.join(
'{} - {}'.format(f, INVENTORY_FIELDS[f]['help'].lower())
for f in INVENTORY_FIELDS)
parser.add_argument(
'--resource',
metavar='<resource_class>:<inventory_field>=<value>',
help='String describing resource.\n' + RC_HELP + '\n'
'<inventory_field> (optional) can be:\n' + fields_help,
default=[],
action='append'
)
return parser
def take_action(self, parsed_args):
inventories = defaultdict(dict)
for r in parsed_args.resource:
name, field, value = parse_resource_argument(r)
inventories[name][field] = value
http = self.app.client_manager.placement
url = RP_BASE_URL + '/' + parsed_args.uuid
rp = http.request('GET', url).json()
payload = {'inventories': inventories,
'resource_provider_generation': rp['generation']}
url = BASE_URL.format(uuid=parsed_args.uuid)
resources = http.request('PUT', url, json=payload).json()
inventories = [
dict(resource_class=k, **v)
for k, v in resources['inventories'].items()
]
fields = ('resource_class', ) + FIELDS
rows = (utils.get_dict_properties(i, fields) for i in inventories)
return fields, rows
class SetClassInventory(command.ShowOne):
"""Replace the inventory record of the class for the resource provider.
Example: openstack resource provider inventory class set <uuid> VCPU \
--total 16 \
--max_unit 4 \
--reserved 1
"""
def get_parser(self, prog_name):
parser = super(SetClassInventory, self).get_parser(prog_name)
parser.add_argument(
'uuid',
metavar='<uuid>',
help='UUID of the resource provider'
)
parser.add_argument(
'resource_class',
metavar='<class>',
help=RC_HELP
)
for name, props in INVENTORY_FIELDS.items():
parser.add_argument(
'--' + name,
metavar='<{}>'.format(name),
required=props['required'],
type=props['type'],
help=props['help'])
return parser
def take_action(self, parsed_args):
http = self.app.client_manager.placement
url = RP_BASE_URL + '/' + parsed_args.uuid
rp = http.request('GET', url).json()
payload = {'resource_provider_generation': rp['generation']}
for field in FIELDS:
value = getattr(parsed_args, field, None)
if value is not None:
payload[field] = value
url = PER_CLASS_URL.format(uuid=parsed_args.uuid,
resource_class=parsed_args.resource_class)
resource = http.request('PUT', url, json=payload).json()
return FIELDS, utils.get_dict_properties(resource, FIELDS)
# TODO(avolkov): Add delete all inventories for RP (version 1.5)
class DeleteInventory(command.Command):
"""Delete the inventory for a given resource provider/class pair"""
def get_parser(self, prog_name):
parser = super(DeleteInventory, self).get_parser(prog_name)
parser.add_argument(
'uuid',
metavar='<uuid>',
help='UUID of the resource provider'
)
parser.add_argument(
'resource_class',
metavar='<resource_class>',
help=RC_HELP
)
return parser
def take_action(self, parsed_args):
http = self.app.client_manager.placement
url = PER_CLASS_URL.format(uuid=parsed_args.uuid,
resource_class=parsed_args.resource_class)
http.request('DELETE', url)
class ShowInventory(command.ShowOne):
"""Show the inventory for a given resource provider/class pair."""
def get_parser(self, prog_name):
parser = super(ShowInventory, self).get_parser(prog_name)
parser.add_argument(
'uuid',
metavar='<uuid>',
help='UUID of the resource provider'
)
parser.add_argument(
'resource_class',
metavar='<resource_class>',
help=RC_HELP
)
return parser
def take_action(self, parsed_args):
http = self.app.client_manager.placement
url = PER_CLASS_URL.format(uuid=parsed_args.uuid,
resource_class=parsed_args.resource_class)
resource = http.request('GET', url).json()
return FIELDS, utils.get_dict_properties(resource, FIELDS)
class ListInventory(command.Lister):
"""List inventories for a given resource provider."""
def get_parser(self, prog_name):
parser = super(ListInventory, self).get_parser(prog_name)
parser.add_argument(
'uuid',
metavar='<uuid>',
help='UUID of the resource provider'
)
return parser
def take_action(self, parsed_args):
http = self.app.client_manager.placement
url = BASE_URL.format(uuid=parsed_args.uuid)
resources = http.request('GET', url).json()
inventories = [
dict(resource_class=k, **v)
for k, v in resources['inventories'].items()
]
fields = ('resource_class', ) + FIELDS
rows = (utils.get_dict_properties(i, fields) for i in inventories)
return fields, rows