def test_compare(self): self.assertTrue(version._compare('1.0', version.gt('0.9'))) self.assertTrue(version._compare('1.0', version.ge('0.9'))) self.assertTrue(version._compare('1.0', version.ge('1.0'))) self.assertTrue(version._compare('1.0', version.eq('1.0'))) self.assertTrue(version._compare('1.0', version.le('1.0'))) self.assertTrue(version._compare('1.0', version.le('1.1'))) self.assertTrue(version._compare('1.0', version.lt('1.1'))) self.assertTrue( version._compare('1.1', version.gt('1.0'), version.lt('1.2'))) self.assertTrue( version._compare('0.3', version.eq('0.2'), version.eq('0.3'), op=any)) self.assertFalse(version._compare('1.0', version.gt('1.0'))) self.assertFalse(version._compare('1.0', version.ge('1.1'))) self.assertFalse(version._compare('1.0', version.eq('1.1'))) self.assertFalse(version._compare('1.0', version.le('0.9'))) self.assertFalse(version._compare('1.0', version.lt('0.9'))) self.assertRaises(ValueError, version._compare, 'abc', version.le('1.1')) self.assertRaises(ValueError, version._compare, '1.0', version.le('.0')) self.assertRaises(ValueError, version._compare, '1', version.le('2'))
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'))) parser.add_argument('--consumer-type', metavar='consumer_type', help='The type of the consumer. ' 'This option is required starting from ' '``--os-placement-api-version 1.38``.', required=self.compare_version(version.ge('1.38'))) return parser
def test_compare(self): self.assertTrue(version._compare('1.0', version.gt('0.9'))) self.assertTrue(version._compare('1.0', version.ge('0.9'))) self.assertTrue(version._compare('1.0', version.ge('1.0'))) self.assertTrue(version._compare('1.0', version.eq('1.0'))) self.assertTrue(version._compare('1.0', version.le('1.0'))) self.assertTrue(version._compare('1.0', version.le('1.1'))) self.assertTrue(version._compare('1.0', version.lt('1.1'))) self.assertTrue( version._compare('1.1', version.gt('1.0'), version.lt('1.2'))) self.assertTrue( version._compare('0.3', version.eq('0.2'), version.eq('0.3'), op=any)) # Test error message msg = 'Operation or argument is not supported with version 1.0; ' self.assertEqual((msg + 'requires version greater than 1.0'), version._compare('1.0', version.gt('1.0'))) self.assertEqual((msg + 'requires at least version 1.1'), version._compare('1.0', version.ge('1.1'))) self.assertEqual((msg + 'requires version 1.1'), version._compare('1.0', version.eq('1.1'))) self.assertEqual((msg + 'requires at most version 0.9'), version._compare('1.0', version.le('0.9'))) self.assertEqual((msg + 'requires version less than 0.9'), version._compare('1.0', version.lt('0.9'))) self.assertRaises(ValueError, version._compare, 'abc', version.le('1.1')) self.assertRaises(ValueError, version._compare, '1.0', version.le('.0')) self.assertRaises(ValueError, version._compare, '1', version.le('2')) ex = self.assertRaises(ValueError, version.compare, '1.0', version.ge('1.1')) self.assertEqual( 'Operation or argument is not supported with version 1.0; ' 'requires at least version 1.1', str(ex)) ex = self.assertRaises(ValueError, version.compare, '1.0', version.eq('1.1'), version.eq('1.5'), op=any) self.assertEqual( 'Operation or argument is not supported with version 1.0; ' 'requires version 1.1, or requires version 1.5', str(ex))
def take_action(self, parsed_args): http = self.app.client_manager.placement filters = {} if parsed_args.name: filters['name'] = parsed_args.name if parsed_args.uuid: filters['uuid'] = parsed_args.uuid if parsed_args.aggregate_uuid: self.check_version(version.ge('1.3')) self.deprecated_option_warning("--aggregate-uuid", "--member-of") filters['member_of'] = 'in:' + ','.join(parsed_args.aggregate_uuid) if parsed_args.resource: self.check_version(version.ge('1.4')) filters['resources'] = ','.join( resource.replace('=', ':') for resource in parsed_args.resource) if 'in_tree' in parsed_args and parsed_args.in_tree: self.check_version(version.ge('1.14')) filters['in_tree'] = parsed_args.in_tree # 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 parsed_args and parsed_args.required: self.check_version(version.ge('1.18')) if any(',' in required for required in parsed_args.required): self.check_version(version.ge('1.39')) required_traits = parsed_args.required forbidden_traits = [] if 'forbidden' in parsed_args and parsed_args.forbidden: self.check_version(version.ge('1.22')) forbidden_traits = ['!' + f for f in parsed_args.forbidden] # Then collect the required query params containing both required and # forbidden traits filters['required'] = common.get_required_query_param_from_args( required_traits, forbidden_traits) if 'member_of' in parsed_args and parsed_args.member_of: # Fail if --member-of but not high enough microversion. self.check_version(version.ge('1.3')) filters['member_of'] = [ 'in:' + aggs for aggs in parsed_args.member_of ] resources = http.request('GET', BASE_URL, params=filters).json()['resource_providers'] fields = ('uuid', 'name', 'generation') if self.compare_version(version.ge('1.14')): fields += ('root_provider_uuid', 'parent_provider_uuid') rows = (utils.get_dict_properties(r, fields) for r in resources) return fields, rows
class SetResourceProviderTrait(command.Lister): """Associate traits with the resource provider identified by {uuid}. All the associated traits will be replaced by the traits specified. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(SetResourceProviderTrait, self).get_parser(prog_name) parser.add_argument('uuid', metavar='<uuid>', help='UUID of the resource provider.') parser.add_argument('--trait', metavar='<trait>', help='Name of the trait. May be repeated.', default=[], action='append') return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = RP_BASE_URL.format(uuid=parsed_args.uuid) rp = http.request('GET', url).json() url = RP_TRAITS_URL.format(uuid=parsed_args.uuid) payload = { 'resource_provider_generation': rp['generation'], 'traits': parsed_args.trait } traits = http.request('PUT', url, json=payload).json()['traits'] return FIELDS, [[t] for t in traits]
class SetAggregate(command.Lister): """Associate a list of aggregates with the resource provider. Each request cleans up previously associated resource provider aggregates entirely and sets the new ones. Passing empty aggregate UUID list will remove all associations with aggregates for the particular resource provider. This command requires at least --os-placement-api-version 1.1. """ def get_parser(self, prog_name): parser = super(SetAggregate, self).get_parser(prog_name) parser.add_argument('uuid', metavar='<name>', help='UUID of the resource provider') parser.add_argument( '--aggregate', metavar='<aggregate_uuid>', help='UUID of the aggregate. Specify multiple times to associate ' 'a resource provider with multiple aggregates.', action='append', default=[]) return parser @version.check(version.ge('1.1')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL.format(uuid=parsed_args.uuid) resp = http.request('PUT', url, json=parsed_args.aggregate).json() return FIELDS, [[r] for r in resp['aggregates']]
def take_action(self, parsed_args): http = self.app.client_manager.placement data = {'name': parsed_args.name} if 'uuid' in parsed_args and parsed_args.uuid: data['uuid'] = parsed_args.uuid if ('parent_provider' in parsed_args and parsed_args.parent_provider): self.check_version(version.ge('1.14')) data['parent_provider_uuid'] = parsed_args.parent_provider resp = http.request('POST', BASE_URL, json=data) resource = http.request('GET', resp.headers['Location']).json() fields = ('uuid', 'name', 'generation') if self.compare_version(version.ge('1.14')): fields += ('root_provider_uuid', 'parent_provider_uuid') return fields, utils.get_dict_properties(resource, fields)
def test_check_mixin(self): class Test(version.CheckerMixin): app = mock.Mock() app.client_manager.placement.api_version = '1.2' t = Test() self.assertTrue(t.compare_version(version.le('1.3'))) self.assertTrue(t.check_version(version.ge('1.0'))) self.assertRaisesRegex(ValueError, 'Operation or argument is not supported', t.check_version, version.lt('1.2'))
def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL + '/' + parsed_args.uuid data = dict(name=parsed_args.name) # Not knowing the previous state of a resource the client cannot catch # it, but if the user tries to re-parent a resource provider the server # returns an easy to understand error: # Unable to save resource provider RP-ID: # Object action update failed because: # re-parenting a provider is not currently allowed. # (HTTP 400) if ('parent_provider' in parsed_args and parsed_args.parent_provider): self.check_version(version.ge('1.14')) data['parent_provider_uuid'] = parsed_args.parent_provider resource = http.request('PUT', url, json=data).json() fields = ('uuid', 'name', 'generation') if self.compare_version(version.ge('1.14')): fields += ('root_provider_uuid', 'parent_provider_uuid') return fields, utils.get_dict_properties(resource, fields)
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() props = {} fields = ('resource_provider', 'generation', 'resources') if self.compare_version(version.ge('1.12')): fields += ('project_id', 'user_id') props['project_id'] = resp.get('project_id') props['user_id'] = resp.get('user_id') if self.compare_version(version.ge('1.38')): fields += ('consumer_type', ) props['consumer_type'] = resp.get('consumer_type') allocs = [ dict(resource_provider=k, **props, **v) for k, v in per_provider ] rows = (utils.get_dict_properties(a, fields) for a in allocs) return fields, rows
def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL + '/' + parsed_args.uuid resource = http.request('GET', url).json() fields = ('uuid', 'name', 'generation') if self.compare_version(version.ge('1.14')): fields += ('root_provider_uuid', 'parent_provider_uuid') if parsed_args.allocations: allocs_url = ALLOCATIONS_URL.format(uuid=parsed_args.uuid) allocs = http.request('GET', allocs_url).json()['allocations'] resource['allocations'] = allocs fields += ('allocations',) return fields, utils.get_dict_properties(resource, fields)
class ListResourceClass(command.Lister): """Return a list of all resource classes. This command requires at least ``--os-placement-api-version 1.2``. """ def get_parser(self, prog_name): parser = super(ListResourceClass, self).get_parser(prog_name) return parser @version.check(version.ge('1.2')) def take_action(self, parsed_args): http = self.app.client_manager.placement resource_classes = http.request('GET', BASE_URL).json()['resource_classes'] rows = (utils.get_dict_properties(i, FIELDS) for i in resource_classes) return FIELDS, rows
class CreateResourceClass(command.Command): """Create a new resource class. This command requires at least ``--os-placement-api-version 1.2``. """ def get_parser(self, prog_name): parser = super(CreateResourceClass, self).get_parser(prog_name) parser.add_argument('name', metavar='<name>', help='Name of the resource class') return parser @version.check(version.ge('1.2')) def take_action(self, parsed_args): http = self.app.client_manager.placement http.request('POST', BASE_URL, json={'name': parsed_args.name})
def take_action(self, parsed_args): http = self.app.client_manager.placement filters = {} if parsed_args.name: filters['name'] = parsed_args.name if parsed_args.uuid: filters['uuid'] = parsed_args.uuid if parsed_args.aggregate_uuid: self.check_version(version.ge('1.3')) self.deprecated_option_warning("--aggregate-uuid", "--member-of") filters['member_of'] = 'in:' + ','.join(parsed_args.aggregate_uuid) if parsed_args.resource: self.check_version(version.ge('1.4')) filters['resources'] = ','.join( resource.replace('=', ':') for resource in parsed_args.resource) if 'in_tree' in parsed_args and parsed_args.in_tree: self.check_version(version.ge('1.14')) filters['in_tree'] = parsed_args.in_tree if 'required' in parsed_args and parsed_args.required: self.check_version(version.ge('1.18')) filters['required'] = ','.join(parsed_args.required) if 'forbidden' in parsed_args and parsed_args.forbidden: self.check_version(version.ge('1.22')) forbidden_traits = ','.join( ['!' + f for f in parsed_args.forbidden]) if 'required' in filters: filters['required'] += ',' + forbidden_traits else: filters['required'] = forbidden_traits if 'member_of' in parsed_args and parsed_args.member_of: # Fail if --member-of but not high enough microversion. self.check_version(version.ge('1.3')) filters['member_of'] = [ 'in:' + aggs for aggs in parsed_args.member_of] resources = http.request( 'GET', BASE_URL, params=filters).json()['resource_providers'] fields = ('uuid', 'name', 'generation') if self.compare_version(version.ge('1.14')): fields += ('root_provider_uuid', 'parent_provider_uuid') rows = (utils.get_dict_properties(r, fields) for r in resources) return fields, rows
class ListResourceProviderTrait(command.Lister): """List traits associated with the resource provider identified by {uuid}. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(ListResourceProviderTrait, self).get_parser(prog_name) parser.add_argument('uuid', metavar='<uuid>', help='UUID of the resource provider.') return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = RP_TRAITS_URL.format(uuid=parsed_args.uuid) traits = http.request('GET', url).json()['traits'] return FIELDS, [[t] for t in traits]
class ListAggregate(command.Lister): """List resource provider aggregates. This command requires at least --os-placement-api-version 1.1. """ def get_parser(self, prog_name): parser = super(ListAggregate, self).get_parser(prog_name) parser.add_argument('uuid', metavar='<uuid>', help='UUID of the resource provider') return parser @version.check(version.ge('1.1')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL.format(uuid=parsed_args.uuid) resp = http.request('GET', url).json() return FIELDS, [[r] for r in resp['aggregates']]
class DeleteTrait(command.Command): """Delete the trait specified by {name}. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(DeleteTrait, self).get_parser(prog_name) parser.add_argument('name', metavar='<name>', help='Name of the trait.') return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = '/'.join([BASE_URL, parsed_args.name]) http.request('DELETE', url)
class ListTrait(command.Lister): """Return a list of valid trait strings. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(ListTrait, self).get_parser(prog_name) parser.add_argument( '--name', metavar='<name>', help=('A string to filter traits. The following options ' 'are available: startswith operator filters the ' 'traits whose name begins with a specific prefix, ' 'e.g. name=startswith:CUSTOM, in operator filters ' 'the traits whose name is in the specified list, ' 'e.g. name=in:HW_CPU_X86_AVX,HW_CPU_X86_SSE, ' 'HW_CPU_X86_INVALID_FEATURE.')) parser.add_argument( '--associated', action='store_true', help=('If this parameter is presented, the returned ' 'traits will be those that are associated with at ' 'least one resource provider.')) return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL params = {} if parsed_args.name: params['name'] = parsed_args.name if parsed_args.associated: params['associated'] = parsed_args.associated traits = http.request('GET', url, params=params).json()['traits'] return FIELDS, [[t] for t in traits]
class ShowTrait(command.ShowOne): """Check if a trait name exists in this cloud. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(ShowTrait, self).get_parser(prog_name) parser.add_argument('name', metavar='<name>', help='Name of the trait.') return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = '/'.join([BASE_URL, parsed_args.name]) http.request('GET', url) return FIELDS, [parsed_args.name]
class ShowResourceClass(command.ShowOne): """Return a representation of the resource class identified by ``<name>``. This command requires at least ``--os-placement-api-version 1.2``. """ def get_parser(self, prog_name): parser = super(ShowResourceClass, self).get_parser(prog_name) parser.add_argument('name', metavar='<name>', help='Name of the resource class') return parser @version.check(version.ge('1.2')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = PER_CLASS_URL.format(name=parsed_args.name) resource = http.request('GET', url).json() return FIELDS, utils.get_dict_properties(resource, FIELDS)
class ResourceShowUsage(command.Lister, version.CheckerMixin): """Show resource usages for a project (and optionally user) per class. Gives a report of usage information for resources associated with the project identified by the ``project_id`` argument and user identified by the ``--user-id`` option. This command requires at least ``--os-placement-api-version 1.9``. """ def get_parser(self, prog_name): parser = super(ResourceShowUsage, self).get_parser(prog_name) parser.add_argument('project_id', metavar='<project-uuid>', help='ID of the project.') parser.add_argument('--user-id', metavar='<user-uuid>', help='ID of the user.') return parser @version.check(version.ge('1.9')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = USAGES_URL params = {'project_id': parsed_args.project_id} if parsed_args.user_id: params['user_id'] = parsed_args.user_id per_class = http.request('GET', url, params=params).json()['usages'] usages = [{ 'resource_class': k, 'usage': v } for k, v in per_class.items()] rows = (utils.get_dict_properties(u, FIELDS) for u in usages) return FIELDS, rows
class DeleteResourceClass(command.Command): """Delete the resource class identified by ``<name>``. Only custom resource classes can be deleted. This command requires at least ``--os-placement-api-version 1.2``. """ def get_parser(self, prog_name): parser = super(DeleteResourceClass, self).get_parser(prog_name) parser.add_argument('name', metavar='<name>', help='Name of the resource class') return parser @version.check(version.ge('1.2')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = PER_CLASS_URL.format(name=parsed_args.name) http.request('DELETE', url)
class DeleteResourceProviderTrait(command.Command): """Dissociate all the traits from the resource provider. Note that this command is not atomic if multiple processes are managing traits for the same provider. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(DeleteResourceProviderTrait, self).get_parser(prog_name) parser.add_argument('uuid', metavar='<uuid>', help='UUID of the resource provider.') return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = RP_TRAITS_URL.format(uuid=parsed_args.uuid) http.request('DELETE', url)
class SetResourceClass(command.Command): """Create or validate the existence of single resource class. Unlike ``openstack resource class create``, this command also succeeds if the resource class already exists, which makes this an idempotent check or create command. This command requires at least ``--os-placement-api-version 1.7``. """ def get_parser(self, prog_name): parser = super(SetResourceClass, self).get_parser(prog_name) parser.add_argument('name', metavar='<name>', help='Name of the resource class') return parser @version.check(version.ge('1.7')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL + '/' + parsed_args.name http.request('PUT', url)
class CreateTrait(command.Command): """Create a new custom trait. Custom traits must begin with the prefix ``CUSTOM_`` and contain only the letters A through Z, the numbers 0 through 9 and the underscore "_" character. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(CreateTrait, self).get_parser(prog_name) parser.add_argument('name', metavar='<name>', help='Name of the trait.') return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = '/'.join([BASE_URL, parsed_args.name]) http.request('PUT', url)
def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL.format(uuid=parsed_args.uuid) aggregate = parsed_args.aggregate generation = None if 'generation' in parsed_args and parsed_args.generation is not None: self.check_version(version.ge('1.19')) generation = parsed_args.generation if self.compare_version(version.lt('1.19')): resp = http.request('PUT', url, json=aggregate).json() # Microversion 1.19 and beyond a generation argument is # required to write aggregates. elif generation is not None: data = { 'aggregates': aggregate, 'resource_provider_generation': generation } resp = http.request('PUT', url, json=data).json() else: raise exceptions.CommandError('A generation must be specified.') return FIELDS, [[r] for r in resp['aggregates']]
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``.') 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']) if 'required' in group and group['required']: # Fail if --required but not high enough microversion. self.check_version(version.ge('1.17')) params[_get_key('required')] = ','.join(group['required']) if 'forbidden' in group and group['forbidden']: self.check_version(version.ge('1.22')) forbidden_traits = ','.join( ['!' + f for f in group['forbidden']]) if 'required' in params: params[_get_key('required')] += ',' + forbidden_traits else: params[_get_key('required')] = 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
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']) if 'required' in group and group['required']: # Fail if --required but not high enough microversion. self.check_version(version.ge('1.17')) params[_get_key('required')] = ','.join(group['required']) if 'forbidden' in group and group['forbidden']: self.check_version(version.ge('1.22')) forbidden_traits = ','.join( ['!' + f for f in group['forbidden']]) if 'required' in params: params[_get_key('required')] += ',' + forbidden_traits else: params[_get_key('required')] = 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
def take_action(self, parsed_args): http = self.app.client_manager.placement if parsed_args.aggregate: self.check_version(version.ge('1.3')) filters = {'member_of': parsed_args.uuid} url = common.url_with_filters(RP_BASE_URL, filters) rps = http.request('GET', url).json()['resource_providers'] if not rps: raise exceptions.CommandError( 'No resource providers found in aggregate with uuid %s.' % parsed_args.uuid) else: url = RP_BASE_URL + '/' + parsed_args.uuid rps = [http.request('GET', url).json()] resources_list = [] ret = 0 for rp in rps: inventories = collections.defaultdict(dict) url = BASE_URL.format(uuid=rp['uuid']) if parsed_args.amend: # Get existing inventories # TODO(melwitt): Do something to handle the possibility of the # GET failing here (example: resource provider deleted from # underneath us). payload = http.request('GET', url).json() inventories.update(payload['inventories']) payload['inventories'] = inventories else: payload = { 'inventories': inventories, 'resource_provider_generation': rp['generation'] } # Apply resource values to inventories for r in parsed_args.resource: name, field, value = parse_resource_argument(r) inventories[name][field] = value try: if not parsed_args.dry_run: resources = http.request('PUT', url, json=payload).json() else: resources = payload except Exception as exp: with excutils.save_and_reraise_exception() as err_ctx: if parsed_args.aggregate: self.log.error( _('Failed to set inventory for ' 'resource provider %(rp)s: %(exp)s.'), { 'rp': rp['uuid'], 'exp': exp }) err_ctx.reraise = False ret += 1 continue resources_list.append((rp['uuid'], resources)) if ret > 0: msg = _('Failed to set inventory for %(ret)s of %(total)s ' 'resource providers.') % { 'ret': ret, 'total': len(rps) } raise exceptions.CommandError(msg) def get_rows(fields, resources, rp_uuid=None): inventories = [ dict(resource_class=k, **v) for k, v in resources['inventories'].items() ] prepend = (rp_uuid, ) if rp_uuid else () # This is a generator expression rows = (prepend + utils.get_dict_properties(i, fields) for i in inventories) return rows fields = ('resource_class', ) + FIELDS if parsed_args.aggregate: # If this is an aggregate batch, create output that will include # resource provider as the first field to differentiate the values rows = () for rp_uuid, resources in resources_list: subrows = get_rows(fields, resources, rp_uuid=rp_uuid) rows = itertools.chain(rows, subrows) fields = ('resource_provider', ) + fields return fields, rows else: # If this was not an aggregate batch, show output for the one # resource provider (show payload of the first item in the list), # keeping the behavior prior to the addition of --aggregate option return fields, get_rows(fields, resources_list[0][1])
class SetAggregate(command.Lister, version.CheckerMixin): """Associate a list of aggregates with the resource provider. Each request cleans up previously associated resource provider aggregates entirely and sets the new ones. Passing empty aggregate UUID list will remove all associations with aggregates for the particular resource provider. This command requires at least ``--os-placement-api-version 1.1``. """ def get_parser(self, prog_name): parser = super(SetAggregate, self).get_parser(prog_name) parser.add_argument('uuid', metavar='<uuid>', help='UUID of the resource provider') parser.add_argument( '--aggregate', metavar='<aggregate_uuid>', help='UUID of the aggregate. Specify multiple times to associate ' 'a resource provider with multiple aggregates.', action='append', default=[]) parser.add_argument( '--generation', metavar='<resource_provider_generation>', type=int, help='The generation of resource provider. Must match the server-' 'side generation of the resource provider or the operation ' 'will fail.\n\n' 'This param requires at least ' '``--os-placement-api-version 1.19``.') return parser @version.check(version.ge('1.1')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL.format(uuid=parsed_args.uuid) aggregate = parsed_args.aggregate generation = None if 'generation' in parsed_args and parsed_args.generation is not None: self.check_version(version.ge('1.19')) generation = parsed_args.generation if self.compare_version(version.lt('1.19')): resp = http.request('PUT', url, json=aggregate).json() # Microversion 1.19 and beyond a generation argument is # required to write aggregates. elif generation is not None: data = { 'aggregates': aggregate, 'resource_provider_generation': generation } resp = http.request('PUT', url, json=data).json() else: raise exceptions.CommandError('A generation must be specified.') return FIELDS, [[r] for r in resp['aggregates']]