Merge "Allow specifying a set of fields of the Port and Chasis resources"

This commit is contained in:
Jenkins 2015-07-23 03:50:23 +00:00 committed by Gerrit Code Review
commit 66f83811a7
10 changed files with 339 additions and 33 deletions

View File

@ -167,10 +167,9 @@ def common_params_for_list(args, fields, field_labels):
params['detail'] = args.detail
if hasattr(args, 'fields'):
requested_fields = args.fields[0] if args.fields else None
if requested_fields is not None:
params['fields'] = requested_fields
requested_fields = args.fields[0] if args.fields else None
if requested_fields is not None:
params['fields'] = requested_fields
return params
@ -299,7 +298,9 @@ def check_for_invalid_fields(fields, valid_fields):
if not fields:
return
invalid_attr = set(fields) - set(valid_fields)
if invalid_attr:
raise exc.CommandError(_('Invalid field(s): %s') %
', '.join(invalid_attr))
invalid_fields = set(fields) - set(valid_fields)
if invalid_fields:
raise exc.CommandError(
_('Invalid field(s) requested: %(invalid)s. Valid fields '
'are: %(valid)s.') % {'invalid': ', '.join(invalid_fields),
'valid': ', '.join(valid_fields)})

View File

@ -126,7 +126,8 @@ class CommonParamsForListTest(test_utils.BaseTestCase):
def setUp(self):
super(CommonParamsForListTest, self).setUp()
self.args = mock.Mock(marker=None, limit=None, sort_key=None,
sort_dir=None, detail=False, spec=True)
sort_dir=None, detail=False, fields=None,
spec=True)
self.expected_params = {'detail': False}
def test_nothing_set(self):

View File

@ -18,6 +18,7 @@ import copy
import testtools
from testtools.matchers import HasLength
from ironicclient import exc
from ironicclient.tests.unit import utils
import ironicclient.v1.chassis
@ -67,6 +68,13 @@ fake_responses = {
{"chassis": [CHASSIS]},
),
},
'/v1/chassis/?fields=uuid,extra':
{
'GET': (
{},
{"chassis": [CHASSIS]},
),
},
'/v1/chassis/%s' % CHASSIS['uuid']:
{
'GET': (
@ -82,6 +90,13 @@ fake_responses = {
UPDATED_CHASSIS,
),
},
'/v1/chassis/%s?fields=uuid,description' % CHASSIS['uuid']:
{
'GET': (
{},
CHASSIS,
),
},
'/v1/chassis/%s/nodes' % CHASSIS['uuid']:
{
'GET': (
@ -96,6 +111,13 @@ fake_responses = {
{"nodes": [NODE]},
),
},
'/v1/chassis/%s/nodes?fields=uuid,extra' % CHASSIS['uuid']:
{
'GET': (
{},
{"nodes": [NODE]},
),
},
}
fake_responses_pagination = {
@ -243,6 +265,18 @@ class ChassisManagerTest(testtools.TestCase):
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(chassis))
def test_chassis_list_fields(self):
nodes = self.mgr.list(fields=['uuid', 'extra'])
expect = [
('GET', '/v1/chassis/?fields=uuid,extra', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(nodes))
def test_chassis_list_detail_and_fields_fail(self):
self.assertRaises(exc.InvalidAttribute, self.mgr.list,
detail=True, fields=['uuid', 'extra'])
def test_chassis_show(self):
chassis = self.mgr.get(CHASSIS['uuid'])
expect = [
@ -252,6 +286,16 @@ class ChassisManagerTest(testtools.TestCase):
self.assertEqual(CHASSIS['uuid'], chassis.uuid)
self.assertEqual(CHASSIS['description'], chassis.description)
def test_chassis_show_fields(self):
chassis = self.mgr.get(CHASSIS['uuid'], fields=['uuid', 'description'])
expect = [
('GET', '/v1/chassis/%s?fields=uuid,description' %
CHASSIS['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(CHASSIS['uuid'], chassis.uuid)
self.assertEqual(CHASSIS['description'], chassis.description)
def test_create(self):
chassis = self.mgr.create(**CREATE_CHASSIS)
expect = [
@ -297,6 +341,20 @@ class ChassisManagerTest(testtools.TestCase):
self.assertEqual(1, len(nodes))
self.assertEqual(NODE['uuid'], nodes[0].uuid)
def test_chassis_node_list_fields(self):
nodes = self.mgr.list_nodes(CHASSIS['uuid'], fields=['uuid', 'extra'])
expect = [
('GET', '/v1/chassis/%s/nodes?fields=uuid,extra' %
CHASSIS['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(nodes))
def test_chassis_node_list_detail_and_fields_fail(self):
self.assertRaises(exc.InvalidAttribute, self.mgr.list_nodes,
CHASSIS['uuid'], detail=True,
fields=['uuid', 'extra'])
def test_chassis_node_list_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = ironicclient.v1.chassis.ChassisManager(self.api)

View File

@ -22,7 +22,8 @@ import ironicclient.v1.chassis_shell as c_shell
class ChassisShellTest(utils.BaseTestCase):
def _get_client_mock_args(self, chassis=None, marker=None, limit=None,
sort_dir=None, sort_key=None, detail=False):
sort_dir=None, sort_key=None, detail=False,
fields=None):
args = mock.MagicMock(spec=True)
args.chassis = chassis
args.marker = marker
@ -30,6 +31,7 @@ class ChassisShellTest(utils.BaseTestCase):
args.sort_dir = sort_dir
args.sort_key = sort_key
args.detail = detail
args.fields = fields
return args
@ -59,6 +61,24 @@ class ChassisShellTest(utils.BaseTestCase):
c_shell.do_chassis_show,
client_mock, args)
def test_do_chassis_show_fields(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.chassis = 'chassis_uuid'
args.fields = [['uuid', 'description']]
c_shell.do_chassis_show(client_mock, args)
client_mock.chassis.get.assert_called_once_with(
'chassis_uuid', fields=['uuid', 'description'])
def test_do_chassis_show_invalid_fields(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.chassis = 'chassis_uuid'
args.fields = [['foo', 'bar']]
self.assertRaises(exceptions.CommandError,
c_shell.do_chassis_show,
client_mock, args)
def test_do_chassis_list(self):
client_mock = mock.MagicMock()
args = self._get_client_mock_args()
@ -111,6 +131,20 @@ class ChassisShellTest(utils.BaseTestCase):
client_mock, args)
self.assertFalse(client_mock.chassis.list.called)
def test_do_chassis_list_fields(self):
client_mock = mock.MagicMock()
args = self._get_client_mock_args(fields=[['uuid', 'description']])
c_shell.do_chassis_list(client_mock, args)
client_mock.chassis.list.assert_called_once_with(
fields=['uuid', 'description'], detail=False)
def test_do_chassis_list_invalid_fields(self):
client_mock = mock.MagicMock()
args = self._get_client_mock_args(fields=[['foo', 'bar']])
self.assertRaises(exceptions.CommandError,
c_shell.do_chassis_list,
client_mock, args)
def test_do_chassis_node_list(self):
client_mock = mock.MagicMock()
chassis_mock = mock.MagicMock(spec_set=[])
@ -128,3 +162,20 @@ class ChassisShellTest(utils.BaseTestCase):
c_shell.do_chassis_node_list(client_mock, args)
client_mock.chassis.list_nodes.assert_called_once_with(
chassis_mock, detail=True)
def test_do_chassis_node_list_fields(self):
client_mock = mock.MagicMock()
chassis_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(chassis=chassis_mock,
fields=[['uuid', 'power_state']])
c_shell.do_chassis_node_list(client_mock, args)
client_mock.chassis.list_nodes.assert_called_once_with(
chassis_mock, fields=['uuid', 'power_state'], detail=False)
def test_do_chassis_node_list_invalid_fields(self):
client_mock = mock.MagicMock()
chassis_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(chassis=chassis_mock,
fields=[['foo', 'bar']])
self.assertRaises(exceptions.CommandError,
c_shell.do_chassis_node_list, client_mock, args)

View File

@ -18,6 +18,7 @@ import copy
import testtools
from testtools.matchers import HasLength
from ironicclient import exc
from ironicclient.tests.unit import utils
import ironicclient.v1.port
@ -60,6 +61,13 @@ fake_responses = {
{"ports": [PORT]},
),
},
'/v1/ports/?fields=uuid,address':
{
'GET': (
{},
{"ports": [PORT]},
),
},
'/v1/ports/%s' % PORT['uuid']:
{
'GET': (
@ -75,6 +83,13 @@ fake_responses = {
UPDATED_PORT,
),
},
'/v1/ports/%s?fields=uuid,address' % PORT['uuid']:
{
'GET': (
{},
PORT,
),
},
'/v1/ports/detail?address=%s' % PORT['address']:
{
'GET': (
@ -173,6 +188,18 @@ class PortManagerTest(testtools.TestCase):
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(ports))
def test_port_list_fields(self):
ports = self.mgr.list(fields=['uuid', 'address'])
expect = [
('GET', '/v1/ports/?fields=uuid,address', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(ports))
def test_port_list_detail_and_fields_fail(self):
self.assertRaises(exc.InvalidAttribute, self.mgr.list,
detail=True, fields=['uuid', 'address'])
def test_ports_list_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = ironicclient.v1.port.PortManager(self.api)
@ -245,6 +272,16 @@ class PortManagerTest(testtools.TestCase):
self.assertEqual(PORT['address'], port.address)
self.assertEqual(PORT['node_uuid'], port.node_uuid)
def test_port_show_fields(self):
port = self.mgr.get(PORT['uuid'], fields=['uuid', 'address'])
expect = [
('GET', '/v1/ports/%s?fields=uuid,address' %
PORT['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(PORT['uuid'], port.uuid)
self.assertEqual(PORT['address'], port.address)
def test_create(self):
port = self.mgr.create(**CREATE_PORT)
expect = [

View File

@ -39,9 +39,10 @@ class PortShellTest(utils.BaseTestCase):
args = mock.MagicMock()
args.port = 'port_uuid'
args.address = False
args.fields = None
p_shell.do_port_show(client_mock, args)
client_mock.port.get.assert_called_once_with('port_uuid')
client_mock.port.get.assert_called_once_with('port_uuid', fields=None)
# assert get_by_address() wasn't called
self.assertFalse(client_mock.port.get_by_address.called)
@ -68,12 +69,34 @@ class PortShellTest(utils.BaseTestCase):
args = mock.MagicMock()
args.port = 'port_address'
args.address = True
args.fields = None
p_shell.do_port_show(client_mock, args)
client_mock.port.get_by_address.assert_called_once_with('port_address')
client_mock.port.get_by_address.assert_called_once_with('port_address',
fields=None)
# assert get() wasn't called
self.assertFalse(client_mock.port.get.called)
def test_do_port_show_fields(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.port = 'port_uuid'
args.address = False
args.fields = [['uuid', 'address']]
p_shell.do_port_show(client_mock, args)
client_mock.port.get.assert_called_once_with(
'port_uuid', fields=['uuid', 'address'])
def test_do_port_show_invalid_fields(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.port = 'port_uuid'
args.address = False
args.fields = [['foo', 'bar']]
self.assertRaises(exceptions.CommandError,
p_shell.do_port_show,
client_mock, args)
def test_do_port_update(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
@ -86,7 +109,8 @@ class PortShellTest(utils.BaseTestCase):
client_mock.port.update.assert_called_once_with('port_uuid', patch)
def _get_client_mock_args(self, address=None, marker=None, limit=None,
sort_dir=None, sort_key=None, detail=False):
sort_dir=None, sort_key=None, detail=False,
fields=None):
args = mock.MagicMock(spec=True)
args.address = address
args.marker = marker
@ -94,6 +118,7 @@ class PortShellTest(utils.BaseTestCase):
args.sort_dir = sort_dir
args.sort_key = sort_key
args.detail = detail
args.fields = fields
return args
@ -148,3 +173,18 @@ class PortShellTest(utils.BaseTestCase):
p_shell.do_port_list,
client_mock, args)
self.assertFalse(client_mock.port.list.called)
def test_do_port_list_fields(self):
client_mock = mock.MagicMock()
args = self._get_client_mock_args(fields=[['uuid', 'address']])
p_shell.do_port_list(client_mock, args)
client_mock.port.list.assert_called_once_with(
fields=['uuid', 'address'], detail=False)
def test_do_port_list_invalid_fields(self):
client_mock = mock.MagicMock()
args = self._get_client_mock_args(fields=[['foo', 'bar']])
self.assertRaises(exceptions.CommandError,
p_shell.do_port_list,
client_mock, args)

View File

@ -15,6 +15,7 @@
# under the License.
from ironicclient.common import base
from ironicclient.common.i18n import _
from ironicclient.common import utils
from ironicclient import exc
@ -35,7 +36,7 @@ class ChassisManager(base.Manager):
return '/v1/chassis/%s' % id if id else '/v1/chassis'
def list(self, marker=None, limit=None, sort_key=None,
sort_dir=None, detail=False):
sort_dir=None, detail=False, fields=None):
"""Retrieve a list of chassis.
:param marker: Optional, the UUID of a chassis, eg the last
@ -58,13 +59,22 @@ class ChassisManager(base.Manager):
:param detail: Optional, boolean whether to return detailed information
about chassis.
:param fields: Optional, a list with a specified set of fields
of the resource to be returned. Can not be used
when 'detail' is set.
:returns: A list of chassis.
"""
if limit is not None:
limit = int(limit)
filters = utils.common_filters(marker, limit, sort_key, sort_dir)
if detail and fields:
raise exc.InvalidAttribute(_("Can't fetch a subset of fields "
"with 'detail' set"))
filters = utils.common_filters(marker, limit, sort_key, sort_dir,
fields)
path = ''
if detail:
@ -79,7 +89,7 @@ class ChassisManager(base.Manager):
limit=limit)
def list_nodes(self, chassis_id, marker=None, limit=None,
sort_key=None, sort_dir=None, detail=False):
sort_key=None, sort_dir=None, detail=False, fields=None):
"""List all the nodes for a given chassis.
:param chassis_id: The UUID of the chassis.
@ -103,13 +113,22 @@ class ChassisManager(base.Manager):
:param detail: Optional, boolean whether to return detailed information
about nodes.
:param fields: Optional, a list with a specified set of fields
of the resource to be returned. Can not be used
when 'detail' is set.
:returns: A list of nodes.
"""
if limit is not None:
limit = int(limit)
filters = utils.common_filters(marker, limit, sort_key, sort_dir)
if detail and fields:
raise exc.InvalidAttribute(_("Can't fetch a subset of fields "
"with 'detail' set"))
filters = utils.common_filters(marker, limit, sort_key, sort_dir,
fields)
path = "%s/nodes" % chassis_id
if detail:
@ -124,7 +143,11 @@ class ChassisManager(base.Manager):
return self._list_pagination(self._path(path), "nodes",
limit=limit)
def get(self, chassis_id):
def get(self, chassis_id, fields=None):
if fields is not None:
chassis_id = '%s?fields=' % chassis_id
chassis_id += ','.join(fields)
try:
return self._list(self._path(chassis_id))[0]
except IndexError:

View File

@ -18,18 +18,32 @@ from ironicclient.openstack.common import cliutils
from ironicclient.v1 import resource_fields as res_fields
def _print_chassis_show(chassis):
fields = ['uuid', 'description', 'created_at', 'updated_at', 'extra']
def _print_chassis_show(chassis, fields=None):
if fields is None:
fields = res_fields.CHASSIS_DETAILED_RESOURCE.fields
data = dict([(f, getattr(chassis, f, '')) for f in fields])
cliutils.print_dict(data, wrap=72)
@cliutils.arg('chassis', metavar='<chassis>', help="UUID of the chassis.")
@cliutils.arg(
'--fields',
nargs='+',
dest='fields',
metavar='<field>',
action='append',
default=[],
help="One or more chassis fields. Only these fields will be fetched from "
"the server.")
def do_chassis_show(cc, args):
"""Show detailed information about a chassis."""
utils.check_empty_arg(args.chassis, '<chassis>')
chassis = cc.chassis.get(args.chassis)
_print_chassis_show(chassis)
fields = args.fields[0] if args.fields else None
utils.check_for_invalid_fields(
fields, res_fields.CHASSIS_DETAILED_RESOURCE.fields)
chassis = cc.chassis.get(args.chassis, fields=fields)
_print_chassis_show(chassis, fields=fields)
@cliutils.arg(
@ -60,6 +74,15 @@ def do_chassis_show(cc, args):
metavar='<direction>',
choices=['asc', 'desc'],
help='Sort direction: "asc" (the default) or "desc".')
@cliutils.arg(
'--fields',
nargs='+',
dest='fields',
metavar='<field>',
action='append',
default=[],
help="One or more chassis fields. Only these fields will be fetched from "
"the server. Can not be used when '--detail' is specified.")
def do_chassis_list(cc, args):
"""List the chassis."""
if args.detail:
@ -67,6 +90,14 @@ def do_chassis_list(cc, args):
field_labels = res_fields.CHASSIS_DETAILED_RESOURCE.labels
sort_fields = res_fields.CHASSIS_DETAILED_RESOURCE.sort_fields
sort_field_labels = res_fields.CHASSIS_DETAILED_RESOURCE.sort_labels
elif args.fields:
utils.check_for_invalid_fields(
args.fields[0], res_fields.CHASSIS_DETAILED_RESOURCE.fields)
resource = res_fields.Resource(args.fields[0])
fields = resource.fields
field_labels = resource.labels
sort_fields = res_fields.CHASSIS_DETAILED_RESOURCE.sort_fields
sort_field_labels = res_fields.CHASSIS_DETAILED_RESOURCE.sort_labels
else:
fields = res_fields.CHASSIS_RESOURCE.fields
field_labels = res_fields.CHASSIS_RESOURCE.labels
@ -166,11 +197,26 @@ def do_chassis_update(cc, args):
choices=['asc', 'desc'],
help='Sort direction: "asc" (the default) or "desc".')
@cliutils.arg('chassis', metavar='<chassis>', help="UUID of the chassis.")
@cliutils.arg(
'--fields',
nargs='+',
dest='fields',
metavar='<field>',
action='append',
default=[],
help="One or more node fields. Only these fields will be fetched from "
"the server. Can not be used when '--detail' is specified.")
def do_chassis_node_list(cc, args):
"""List the nodes contained in a chassis."""
if args.detail:
fields = res_fields.NODE_DETAILED_RESOURCE.fields
field_labels = res_fields.NODE_DETAILED_RESOURCE.labels
elif args.fields:
utils.check_for_invalid_fields(
args.fields[0], res_fields.NODE_DETAILED_RESOURCE.fields)
resource = res_fields.Resource(args.fields[0])
fields = resource.fields
field_labels = resource.labels
else:
fields = res_fields.NODE_RESOURCE.fields
field_labels = res_fields.NODE_RESOURCE.labels

View File

@ -15,6 +15,7 @@
# under the License.
from ironicclient.common import base
from ironicclient.common.i18n import _
from ironicclient.common import utils
from ironicclient import exc
@ -34,7 +35,7 @@ class PortManager(base.Manager):
return '/v1/ports/%s' % id if id else '/v1/ports'
def list(self, address=None, limit=None, marker=None, sort_key=None,
sort_dir=None, detail=False):
sort_dir=None, detail=False, fields=None):
"""Retrieve a list of port.
:param address: Optional, MAC address of a port, to get
@ -59,13 +60,22 @@ class PortManager(base.Manager):
:param detail: Optional, boolean whether to return detailed information
about ports.
:param fields: Optional, a list with a specified set of fields
of the resource to be returned. Can not be used
when 'detail' is set.
:returns: A list of ports.
"""
if limit is not None:
limit = int(limit)
filters = utils.common_filters(marker, limit, sort_key, sort_dir)
if detail and fields:
raise exc.InvalidAttribute(_("Can't fetch a subset of fields "
"with 'detail' set"))
filters = utils.common_filters(marker, limit, sort_key, sort_dir,
fields)
if address is not None:
filters.append('address=%s' % address)
@ -81,14 +91,23 @@ class PortManager(base.Manager):
return self._list_pagination(self._path(path), "ports",
limit=limit)
def get(self, port_id):
def get(self, port_id, fields=None):
if fields is not None:
port_id = '%s?fields=' % port_id
port_id += ','.join(fields)
try:
return self._list(self._path(port_id))[0]
except IndexError:
return None
def get_by_address(self, address):
path = "detail?address=%s" % address
def get_by_address(self, address, fields=None):
path = '?address=%s' % address
if fields is not None:
path += '&fields=' + ','.join(fields)
else:
path = 'detail' + path
ports = self._list(self._path(path), 'ports')
# get all the details of the port assuming that filtering by
# address returns a collection of one port if successful.

View File

@ -18,9 +18,10 @@ from ironicclient.openstack.common import cliutils
from ironicclient.v1 import resource_fields as res_fields
def _print_port_show(port):
fields = ['address', 'created_at', 'extra', 'node_uuid', 'updated_at',
'uuid']
def _print_port_show(port, fields=None):
if fields is None:
fields = res_fields.PORT_DETAILED_RESOURCE.fields
data = dict([(f, getattr(port, f, '')) for f in fields])
cliutils.print_dict(data, wrap=72)
@ -35,14 +36,26 @@ def _print_port_show(port):
action='store_true',
default=False,
help='<id> is the MAC address (instead of the UUID) of the port.')
@cliutils.arg(
'--fields',
nargs='+',
dest='fields',
metavar='<field>',
action='append',
default=[],
help="One or more port fields. Only these fields will be fetched from "
"the server.")
def do_port_show(cc, args):
"""Show detailed information about a port."""
fields = args.fields[0] if args.fields else None
utils.check_for_invalid_fields(
fields, res_fields.PORT_DETAILED_RESOURCE.fields)
if args.address:
port = cc.port.get_by_address(args.port)
port = cc.port.get_by_address(args.port, fields=fields)
else:
utils.check_empty_arg(args.port, '<id>')
port = cc.port.get(args.port)
_print_port_show(port)
port = cc.port.get(args.port, fields=fields)
_print_port_show(port, fields=fields)
@cliutils.arg(
@ -76,6 +89,15 @@ def do_port_show(cc, args):
metavar='<direction>',
choices=['asc', 'desc'],
help='Sort direction: "asc" (the default) or "desc".')
@cliutils.arg(
'--fields',
nargs='+',
dest='fields',
metavar='<field>',
action='append',
default=[],
help="One or more port fields. Only these fields will be fetched from "
"the server. Can not be used when '--detail' is specified.")
def do_port_list(cc, args):
"""List the ports."""
params = {}
@ -88,6 +110,14 @@ def do_port_list(cc, args):
field_labels = res_fields.PORT_DETAILED_RESOURCE.labels
sort_fields = res_fields.PORT_DETAILED_RESOURCE.sort_fields
sort_field_labels = res_fields.PORT_DETAILED_RESOURCE.sort_labels
elif args.fields:
utils.check_for_invalid_fields(
args.fields[0], res_fields.PORT_DETAILED_RESOURCE.fields)
resource = res_fields.Resource(args.fields[0])
fields = resource.fields
field_labels = resource.labels
sort_fields = res_fields.PORT_DETAILED_RESOURCE.sort_fields
sort_field_labels = res_fields.PORT_DETAILED_RESOURCE.sort_labels
else:
fields = res_fields.PORT_RESOURCE.fields
field_labels = res_fields.PORT_RESOURCE.labels