CLI for inventories

Co-Authored-by: Andrey Volkov <avolkov@mirantis.com>
Blueprint: placement-osc-plugin
Change-Id: Ie46b6217fda65cb5d0f1379d0b4a986b4c30a3eb
This commit is contained in:
Roman Podoliaka 2017-04-12 19:04:05 +03:00 committed by Andrey Volkov
parent 1fe143e219
commit d6e223a191
4 changed files with 514 additions and 0 deletions

View File

@ -0,0 +1,292 @@
# 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': False,
'help': ('The actual amount of the resource that the provider '
'can accommodate.')
}
}
FIELDS = tuple(INVENTORY_FIELDS.keys())
RC_HELP = ('<resource_class> is 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.
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

View File

@ -79,3 +79,31 @@ class BaseTestCase(base.BaseTestCase):
def resource_provider_delete(self, uuid):
return self.openstack('resource provider delete ' + uuid)
def resource_inventory_show(self, uuid, resource_class):
cmd = 'resource provider inventory show {uuid} {rc}'.format(
uuid=uuid, rc=resource_class
)
return self.openstack(cmd, use_json=True)
def resource_inventory_list(self, uuid):
return self.openstack('resource provider inventory list ' + uuid,
use_json=True)
def resource_inventory_delete(self, uuid, resource_class):
cmd = 'resource provider inventory delete {uuid} {rc}'.format(
uuid=uuid, rc=resource_class
)
self.openstack(cmd)
def resource_inventory_set(self, uuid, *resources):
cmd = 'resource provider inventory set {uuid} {resources}'.format(
uuid=uuid, resources=' '.join(
['--resource %s' % r for r in resources]))
return self.openstack(cmd, use_json=True)
def resource_inventory_class_set(self, uuid, resource_class, **kwargs):
opts = ['--%s=%s' % (k, v) for k, v in kwargs.items()]
cmd = 'resource provider inventory class set {uuid} {rc} {opts}'.\
format(uuid=uuid, rc=resource_class, opts=' '.join(opts))
return self.openstack(cmd, use_json=True)

View File

@ -0,0 +1,189 @@
# 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 subprocess
from osc_placement.tests.functional import base
class TestInventory(base.BaseTestCase):
def setUp(self):
super(TestInventory, self).setUp()
self.rp = self.resource_provider_create()
def test_inventory_show(self):
rp_uuid = self.rp['uuid']
expected = {'min_unit': 1,
'max_unit': 12,
'reserved': 0,
'step_size': 1,
'total': 12,
'allocation_ratio': 16.0}
args = ['VCPU:%s=%s' % (k, v) for k, v in expected.items()]
self.resource_inventory_set(rp_uuid, *args)
self.assertEqual(expected,
self.resource_inventory_show(rp_uuid, 'VCPU'))
def test_inventory_show_not_found(self):
rp_uuid = self.rp['uuid']
exc = self.assertRaises(subprocess.CalledProcessError,
self.resource_inventory_show,
rp_uuid, 'VCPU')
self.assertIn('No inventory of class VCPU for {}'.format(rp_uuid),
exc.output.decode('utf-8'))
def test_inventory_delete(self):
rp_uuid = self.rp['uuid']
self.resource_inventory_set(rp_uuid, 'VCPU=8')
self.resource_inventory_delete(rp_uuid, 'VCPU')
exc = self.assertRaises(subprocess.CalledProcessError,
self.resource_inventory_show,
rp_uuid, 'VCPU')
self.assertIn('No inventory of class VCPU for {}'.format(rp_uuid),
exc.output.decode('utf-8'))
def test_inventory_delete_not_found(self):
exc = self.assertRaises(subprocess.CalledProcessError,
self.resource_inventory_delete,
self.rp['uuid'], 'VCPU')
self.assertIn('No inventory of class VCPU found for delete',
exc.output.decode('utf-8'))
class TestSetInventory(base.BaseTestCase):
def test_fail_if_no_rp(self):
exc = self.assertRaises(
subprocess.CalledProcessError,
self.openstack, 'resource provider inventory set')
self.assertIn('too few arguments', exc.output.decode('utf-8'))
def test_set_empty_inventories(self):
rp = self.resource_provider_create()
self.assertEqual([], self.resource_inventory_set(rp['uuid']))
def test_fail_if_incorrect_resource(self):
rp = self.resource_provider_create()
# wrong format
exc = self.assertRaises(subprocess.CalledProcessError,
self.resource_inventory_set,
rp['uuid'], 'VCPU')
self.assertIn('must have "name=value"', exc.output.decode('utf-8'))
exc = self.assertRaises(subprocess.CalledProcessError,
self.resource_inventory_set,
rp['uuid'], 'VCPU==')
self.assertIn('must have "name=value"', exc.output.decode('utf-8'))
exc = self.assertRaises(subprocess.CalledProcessError,
self.resource_inventory_set,
rp['uuid'], '=10')
self.assertIn('must be not empty', exc.output.decode('utf-8'))
exc = self.assertRaises(subprocess.CalledProcessError,
self.resource_inventory_set,
rp['uuid'], 'v=')
self.assertIn('must be not empty', exc.output.decode('utf-8'))
# unknown class
exc = self.assertRaises(subprocess.CalledProcessError,
self.resource_inventory_set,
rp['uuid'], 'UNKNOWN_CPU=16')
self.assertIn('Unknown resource class', exc.output.decode('utf-8'))
# unknown property
exc = self.assertRaises(subprocess.CalledProcessError,
self.resource_inventory_set,
rp['uuid'], 'VCPU:fake=16')
self.assertIn('Unknown inventory field', exc.output.decode('utf-8'))
def test_set_multiple_classes(self):
rp = self.resource_provider_create()
resp = self.resource_inventory_set(
rp['uuid'],
'VCPU=8',
'VCPU:max_unit=4',
'MEMORY_MB=1024',
'MEMORY_MB:reserved=256',
'DISK_GB=16',
'DISK_GB:allocation_ratio=1.5',
'DISK_GB:min_unit=2',
'DISK_GB:step_size=2')
def check(inventories):
self.assertEqual(8, inventories['VCPU']['total'])
self.assertEqual(4, inventories['VCPU']['max_unit'])
self.assertEqual(1024, inventories['MEMORY_MB']['total'])
self.assertEqual(256, inventories['MEMORY_MB']['reserved'])
self.assertEqual(16, inventories['DISK_GB']['total'])
self.assertEqual(2, inventories['DISK_GB']['min_unit'])
self.assertEqual(2, inventories['DISK_GB']['step_size'])
self.assertEqual(1.5, inventories['DISK_GB']['allocation_ratio'])
check({r['resource_class']: r for r in resp})
resp = self.resource_inventory_list(rp['uuid'])
check({r['resource_class']: r for r in resp})
def test_set_known_and_unknown_class(self):
rp = self.resource_provider_create()
exc = self.assertRaises(subprocess.CalledProcessError,
self.resource_inventory_set,
rp['uuid'], 'VCPU=8', 'UNKNOWN=4')
self.assertIn('Unknown resource class', exc.output.decode('utf-8'))
self.assertEqual([], self.resource_inventory_list(rp['uuid']))
def test_replace_previous_values(self):
"""Test each new set call replaces previous inventories totally."""
rp = self.resource_provider_create()
# set disk inventory first
self.resource_inventory_set(rp['uuid'], 'DISK_GB=16')
# set memory and vcpu inventories
self.resource_inventory_set(rp['uuid'], 'MEMORY_MB=16', 'VCPU=32')
resp = self.resource_inventory_list(rp['uuid'])
inv = {r['resource_class']: r for r in resp}
# no disk inventory as it was overwritten
self.assertNotIn('DISK_GB', inv)
self.assertIn('VCPU', inv)
self.assertIn('MEMORY_MB', inv)
def test_delete_via_set(self):
rp = self.resource_provider_create()
self.resource_inventory_set(rp['uuid'], 'DISK_GB=16')
self.resource_inventory_set(rp['uuid'])
self.assertEqual([], self.resource_inventory_list(rp['uuid']))
def test_fail_if_incorrect_parameters_set_class_inventory(self):
exc = self.assertRaises(
subprocess.CalledProcessError,
self.openstack, 'resource provider inventory class set')
self.assertIn('too few arguments', exc.output.decode('utf-8'))
exc = self.assertRaises(
subprocess.CalledProcessError,
self.openstack, 'resource provider inventory class set fake_uuid')
self.assertIn('too few arguments', exc.output.decode('utf-8'))
exc = self.assertRaises(
subprocess.CalledProcessError,
self.openstack,
('resource provider inventory class set '
'fake_uuid fake_class --totals 5'))
self.assertIn('unrecognized arguments', exc.output.decode('utf-8'))
def test_set_inventory_for_resource_class(self):
rp = self.resource_provider_create()
self.resource_inventory_set(rp['uuid'], 'MEMORY_MB=16', 'VCPU=32')
self.resource_inventory_class_set(
rp['uuid'], 'MEMORY_MB', total=128, step_size=16)
resp = self.resource_inventory_list(rp['uuid'])
inv = {r['resource_class']: r for r in resp}
self.assertEqual(128, inv['MEMORY_MB']['total'])
self.assertEqual(16, inv['MEMORY_MB']['step_size'])
self.assertEqual(32, inv['VCPU']['total'])

View File

@ -32,6 +32,11 @@ openstack.placement.v1 =
resource_provider_show = osc_placement.resources.resource_provider:ShowResourceProvider
resource_provider_set = osc_placement.resources.resource_provider:SetResourceProvider
resource_provider_delete = osc_placement.resources.resource_provider:DeleteResourceProvider
resource_provider_inventory_set = osc_placement.resources.inventory:SetInventory
resource_provider_inventory_class_set = osc_placement.resources.inventory:SetClassInventory
resource_provider_inventory_list = osc_placement.resources.inventory:ListInventory
resource_provider_inventory_show = osc_placement.resources.inventory:ShowInventory
resource_provider_inventory_delete = osc_placement.resources.inventory:DeleteInventory
[build_sphinx]
source-dir = doc/source