CLI for traits (v1.6)
Change-Id: Id1543439bc5e97f7d78aa5e062a9b188d8fb8cbe Partially-Implements: blueprint placement-osc-plugin-rocky
This commit is contained in:
parent
2357807c95
commit
61b08c5ac7
|
@ -0,0 +1,251 @@
|
|||
# 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_placement import version
|
||||
|
||||
|
||||
BASE_URL = '/traits'
|
||||
RP_BASE_URL = '/resource_providers/{uuid}'
|
||||
RP_TRAITS_URL = '/resource_providers/{uuid}/traits'
|
||||
FIELDS = ('name',)
|
||||
|
||||
|
||||
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 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)
|
||||
|
||||
|
||||
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 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 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 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)
|
|
@ -190,3 +190,46 @@ class BaseTestCase(base.BaseTestCase):
|
|||
|
||||
def resource_class_delete(self, name):
|
||||
return self.openstack('resource class delete ' + name)
|
||||
|
||||
def trait_list(self, name=None, associated=False):
|
||||
cmd = 'trait list'
|
||||
if name:
|
||||
cmd += ' --name ' + name
|
||||
if associated:
|
||||
cmd += ' --associated'
|
||||
return self.openstack(cmd, use_json=True)
|
||||
|
||||
def trait_show(self, name):
|
||||
cmd = 'trait show %s' % name
|
||||
return self.openstack(cmd, use_json=True)
|
||||
|
||||
def trait_create(self, name):
|
||||
cmd = 'trait create %s' % name
|
||||
self.openstack(cmd)
|
||||
|
||||
def cleanup():
|
||||
try:
|
||||
self.trait_delete(name)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
# may have already been deleted by a test case
|
||||
err_message = exc.output.decode('utf-8').lower()
|
||||
if 'not found' not in err_message:
|
||||
raise
|
||||
self.addCleanup(cleanup)
|
||||
|
||||
def trait_delete(self, name):
|
||||
cmd = 'trait delete %s' % name
|
||||
self.openstack(cmd)
|
||||
|
||||
def resource_provider_trait_list(self, uuid):
|
||||
cmd = 'resource provider trait list %s ' % uuid
|
||||
return self.openstack(cmd, use_json=True)
|
||||
|
||||
def resource_provider_trait_set(self, uuid, *traits):
|
||||
cmd = 'resource provider trait set %s ' % uuid
|
||||
cmd += ' '.join('--trait %s' % trait for trait in traits)
|
||||
return self.openstack(cmd, use_json=True)
|
||||
|
||||
def resource_provider_trait_delete(self, uuid):
|
||||
cmd = 'resource provider trait delete %s ' % uuid
|
||||
self.openstack(cmd)
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
# 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 uuid
|
||||
|
||||
from osc_placement.tests.functional import base
|
||||
|
||||
|
||||
TRAIT = 'CUSTOM_FAKE_HW_GPU_CLASS_{}'.format(
|
||||
str(uuid.uuid4()).replace('-', '').upper())
|
||||
|
||||
|
||||
class TestTrait(base.BaseTestCase):
|
||||
VERSION = '1.6'
|
||||
|
||||
def test_list_traits(self):
|
||||
self.assertTrue(len(self.trait_list()) > 0)
|
||||
|
||||
def test_list_associated_traits(self):
|
||||
self.trait_create(TRAIT)
|
||||
rp = self.resource_provider_create()
|
||||
self.resource_provider_trait_set(rp['uuid'], TRAIT)
|
||||
self.assertIn(TRAIT,
|
||||
{t['name'] for t in self.trait_list(associated=True)})
|
||||
|
||||
def test_list_traits_startswith(self):
|
||||
self.trait_create(TRAIT)
|
||||
rp = self.resource_provider_create()
|
||||
self.resource_provider_trait_set(rp['uuid'], TRAIT)
|
||||
traits = {t['name'] for t in self.trait_list(
|
||||
name='startswith:' + TRAIT)}
|
||||
self.assertEqual(1, len(traits))
|
||||
self.assertIn(TRAIT, traits)
|
||||
|
||||
def test_list_traits_startswith_unknown_trait(self):
|
||||
traits = {t['name'] for t in self.trait_list(
|
||||
name='startswith:CUSTOM_FOO')}
|
||||
self.assertEqual(0, len(traits))
|
||||
|
||||
def test_list_traits_in(self):
|
||||
self.trait_create(TRAIT)
|
||||
rp = self.resource_provider_create()
|
||||
self.resource_provider_trait_set(rp['uuid'], TRAIT)
|
||||
traits = {t['name'] for t in self.trait_list(
|
||||
name='in:' + TRAIT)}
|
||||
self.assertEqual(1, len(traits))
|
||||
self.assertIn(TRAIT, traits)
|
||||
|
||||
def test_list_traits_in_unknown_trait(self):
|
||||
traits = {t['name'] for t in self.trait_list(name='in:CUSTOM_FOO')}
|
||||
self.assertEqual(0, len(traits))
|
||||
|
||||
def test_show_trait(self):
|
||||
self.trait_create(TRAIT)
|
||||
self.assertEqual({'name': TRAIT}, self.trait_show(TRAIT))
|
||||
|
||||
def test_fail_show_unknown_trait(self):
|
||||
self.assertCommandFailed('Not found', self.trait_show, 'UNKNOWN')
|
||||
|
||||
def test_set_multiple_traits(self):
|
||||
self.trait_create(TRAIT + '1')
|
||||
self.trait_create(TRAIT + '2')
|
||||
rp = self.resource_provider_create()
|
||||
self.resource_provider_trait_set(rp['uuid'], TRAIT + '1', TRAIT + '2')
|
||||
traits = {t['name'] for t in self.resource_provider_trait_list(
|
||||
rp['uuid'])}
|
||||
self.assertEqual(2, len(traits))
|
||||
|
||||
def test_set_known_and_unknown_traits(self):
|
||||
self.trait_create(TRAIT)
|
||||
rp = self.resource_provider_create()
|
||||
self.assertCommandFailed(
|
||||
'No such trait',
|
||||
self.resource_provider_trait_set, rp['uuid'], TRAIT, TRAIT + '1')
|
||||
self.assertEqual([], self.resource_provider_trait_list(rp['uuid']))
|
||||
|
||||
def test_delete_traits_from_provider(self):
|
||||
self.trait_create(TRAIT)
|
||||
rp = self.resource_provider_create()
|
||||
self.resource_provider_trait_set(rp['uuid'], TRAIT)
|
||||
traits = {t['name'] for t in self.resource_provider_trait_list(
|
||||
rp['uuid'])}
|
||||
self.assertEqual(1, len(traits))
|
||||
self.assertIn(TRAIT, traits)
|
||||
self.resource_provider_trait_delete(rp['uuid'])
|
||||
traits = {t['name'] for t in self.resource_provider_trait_list(
|
||||
rp['uuid'])}
|
||||
self.assertEqual(0, len(traits))
|
||||
|
||||
def test_delete_trait(self):
|
||||
self.trait_create(TRAIT)
|
||||
self.trait_delete(TRAIT)
|
||||
self.assertCommandFailed('Not found', self.trait_show, TRAIT)
|
||||
|
||||
def test_fail_rp_trait_list_unknown_uuid(self):
|
||||
self.assertCommandFailed(
|
||||
'No resource provider', self.resource_provider_trait_list, 123)
|
|
@ -21,6 +21,7 @@ SUPPORTED_VERSIONS = [
|
|||
'1.3',
|
||||
'1.4',
|
||||
'1.5',
|
||||
'1.6',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
The following list of trait related commands was added for microversion `1.6`_:
|
||||
- ``openstack trait list``
|
||||
- ``openstack trait show``
|
||||
- ``openstack trait create``
|
||||
- ``openstack trait delete``
|
||||
- ``openstack resource provider trait list``
|
||||
- ``openstack resource provider trait set``
|
||||
- ``openstack resource provider trait delete``
|
||||
|
||||
See the `command documentation`__ for more details.
|
||||
|
||||
.. _1.6: https://docs.openstack.org/nova/latest/user/placement.html#traits-api
|
||||
|
||||
.. __: https://docs.openstack.org/osc-placement/latest/cli/index.html
|
|
@ -47,6 +47,13 @@ openstack.placement.v1 =
|
|||
resource_class_create = osc_placement.resources.resource_class:CreateResourceClass
|
||||
resource_class_show = osc_placement.resources.resource_class:ShowResourceClass
|
||||
resource_class_delete = osc_placement.resources.resource_class:DeleteResourceClass
|
||||
trait_list = osc_placement.resources.trait:ListTrait
|
||||
trait_show = osc_placement.resources.trait:ShowTrait
|
||||
trait_create = osc_placement.resources.trait:CreateTrait
|
||||
trait_delete = osc_placement.resources.trait:DeleteTrait
|
||||
resource_provider_trait_list = osc_placement.resources.trait:ListResourceProviderTrait
|
||||
resource_provider_trait_set = osc_placement.resources.trait:SetResourceProviderTrait
|
||||
resource_provider_trait_delete = osc_placement.resources.trait:DeleteResourceProviderTrait
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
|
|
Loading…
Reference in New Issue