CLI for inventories
Co-Authored-by: Andrey Volkov <avolkov@mirantis.com> Blueprint: placement-osc-plugin Change-Id: Ie46b6217fda65cb5d0f1379d0b4a986b4c30a3eb
This commit is contained in:
parent
1fe143e219
commit
d6e223a191
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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'])
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue