CLI for traits (v1.6)

Change-Id: Id1543439bc5e97f7d78aa5e062a9b188d8fb8cbe
Partially-Implements: blueprint placement-osc-plugin-rocky
This commit is contained in:
Andrey Volkov 2017-10-23 12:36:42 +03:00 committed by Balazs Gibizer
parent 2357807c95
commit 61b08c5ac7
6 changed files with 425 additions and 0 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -21,6 +21,7 @@ SUPPORTED_VERSIONS = [
'1.3',
'1.4',
'1.5',
'1.6',
]

View File

@ -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

View File

@ -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