# 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 = (' 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 \ --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='', 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=':=', help='String describing resource.\n' + RC_HELP + '\n' ' (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 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='', help='UUID of the resource provider' ) parser.add_argument( 'resource_class', metavar='', 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='', help='UUID of the resource provider' ) parser.add_argument( 'resource_class', metavar='', 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='', help='UUID of the resource provider' ) parser.add_argument( 'resource_class', metavar='', 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='', 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