Add python API and CLI for port groups

This patch adds commands that allows to work with ironic portgroups.
The following ironic CLI commands are introduced:

 * ironic portgroup-list
 * ironic portgroup-show
 * ironic portgroup-port-list
 * ironic portgroup-create
 * ironic portgroup-delete
 * ironic portgroup-update

Also extends ironic port-create with --portgroup.
Portgroup support was added in Ironic API version 1.24.

Change-Id: Id8afa902026ce4466e96cc7bfb7fb97447d65809
Co-Authored-By: Vasyl Saienko (vsaienko@mirantis.com)
Co-Authored-By: William Stevenson (will.stevenson@sap.com)
Partial-bug: #1618754
This commit is contained in:
William Stevenson 2015-07-16 23:18:41 +01:00 committed by Vasyl Saienko
parent 56f19eef8b
commit 13fe7cc5bc
14 changed files with 1373 additions and 8 deletions

View File

@ -434,8 +434,8 @@ class TestBaremetalPortList(TestBaremetalPort):
self.baremetal_mock.port.list.assert_called_with(**kwargs)
collist = ('UUID', 'Address', 'Created At', 'Extra', 'Node UUID',
'Local Link Connection', 'PXE boot enabled', 'Updated At',
'Internal Info')
'Local Link Connection', 'Portgroup UUID',
'PXE boot enabled', 'Updated At', 'Internal Info')
self.assertEqual(collist, columns)
datalist = ((
@ -447,6 +447,7 @@ class TestBaremetalPortList(TestBaremetalPort):
'',
'',
'',
'',
''
), )
self.assertEqual(datalist, tuple(data))

View File

@ -56,6 +56,13 @@ PORT = {'id': 456,
'node_id': 123,
'address': 'AA:AA:AA:AA:AA:AA',
'extra': {}}
PORTGROUP = {'id': 987,
'uuid': '11111111-2222-3333-4444-555555555555',
'name': 'Portgroup',
'node_id': 123,
'address': 'AA:BB:CC:DD:EE:FF',
'extra': {}}
POWER_STATE = {'power_state': 'power off',
'target_power_state': 'power on'}
@ -266,6 +273,27 @@ fake_responses = {
{"ports": [PORT]},
),
},
'/v1/nodes/%s/portgroups' % NODE1['uuid']:
{
'GET': (
{},
{"portgroups": [PORTGROUP]},
),
},
'/v1/nodes/%s/portgroups/detail' % NODE1['uuid']:
{
'GET': (
{},
{"portgroups": [PORTGROUP]},
),
},
'/v1/nodes/%s/portgroups?fields=uuid,address' % NODE1['uuid']:
{
'GET': (
{},
{"portgroups": [PORTGROUP]},
),
},
'/v1/nodes/%s/maintenance' % NODE1['uuid']:
{
'PUT': (
@ -394,6 +422,20 @@ fake_responses_pagination = {
{"ports": [PORT]},
),
},
'/v1/nodes/%s/portgroups?limit=1' % NODE1['uuid']:
{
'GET': (
{},
{"portgroups": [PORTGROUP]},
),
},
'/v1/nodes/%s/portgroups?marker=%s' % (NODE1['uuid'], PORTGROUP['uuid']):
{
'GET': (
{},
{"portgroups": [PORTGROUP]},
),
},
}
fake_responses_sorting = {
@ -425,6 +467,20 @@ fake_responses_sorting = {
{"ports": [PORT]},
),
},
'/v1/nodes/%s/portgroups?sort_key=updated_at' % NODE1['uuid']:
{
'GET': (
{},
{"portgroups": [PORTGROUP]},
),
},
'/v1/nodes/%s/portgroups?sort_dir=desc' % NODE1['uuid']:
{
'GET': (
{},
{"portgroups": [PORTGROUP]},
),
},
}

View File

@ -28,6 +28,7 @@ PORT = {'id': 987,
'address': 'AA:BB:CC:DD:EE:FF',
'pxe_enabled': True,
'local_link_connection': {},
'portgroup_uuid': '55555555-4444-3333-2222-111111111111',
'extra': {}}
PORT2 = {'id': 988,
@ -36,6 +37,7 @@ PORT2 = {'id': 988,
'address': 'AA:AA:AA:BB:BB:BB',
'pxe_enabled': True,
'local_link_connection': {},
'portgroup_uuid': '55555555-4444-3333-2222-111111111111',
'extra': {}}
CREATE_PORT = copy.deepcopy(PORT)
@ -285,6 +287,7 @@ class PortManagerTest(testtools.TestCase):
self.assertEqual(PORT['pxe_enabled'], port.pxe_enabled)
self.assertEqual(PORT['local_link_connection'],
port.local_link_connection)
self.assertEqual(PORT['portgroup_uuid'], port.portgroup_uuid)
def test_ports_show_by_address(self):
port = self.mgr.get_by_address(PORT['address'])
@ -299,6 +302,7 @@ class PortManagerTest(testtools.TestCase):
self.assertEqual(PORT['pxe_enabled'], port.pxe_enabled)
self.assertEqual(PORT['local_link_connection'],
port.local_link_connection)
self.assertEqual(PORT['portgroup_uuid'], port.portgroup_uuid)
def test_port_show_fields(self):
port = self.mgr.get(PORT['uuid'], fields=['uuid', 'address'])

View File

@ -32,7 +32,8 @@ class PortShellTest(utils.BaseTestCase):
port = object()
p_shell._print_port_show(port)
exp = ['address', 'created_at', 'extra', 'node_uuid', 'updated_at',
'uuid', 'pxe_enabled', 'local_link_connection', 'internal_info']
'uuid', 'pxe_enabled', 'local_link_connection', 'internal_info',
'portgroup_uuid']
act = actual.keys()
self.assertEqual(sorted(exp), sorted(act))
@ -275,6 +276,18 @@ class PortShellTest(utils.BaseTestCase):
self.assertRaises(exceptions.CommandError,
p_shell.do_port_create, client_mock, args)
def test_do_port_create_portgroup_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.address = 'address'
args.node_uuid = 'uuid'
args.portgroup_uuid = 'portgroup-uuid'
args.json = False
p_shell.do_port_create(client_mock, args)
client_mock.port.create.assert_called_once_with(
address='address', node_uuid='uuid',
portgroup_uuid='portgroup-uuid')
def test_do_port_delete(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()

View File

@ -0,0 +1,445 @@
# Copyright 2016 Mirantis, Inc.
# All Rights Reserved.
#
# 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 copy
import mock
import testtools
from ironicclient.common.apiclient import exceptions
from ironicclient.tests.unit import utils
import ironicclient.v1.portgroup
import ironicclient.v1.portgroup_shell as pg_shell
PORTGROUP = {'id': 987,
'uuid': '11111111-2222-3333-4444-555555555555',
'name': 'Portgroup-name',
'node_uuid': '66666666-7777-8888-9999-000000000000',
'address': 'AA:BB:CC:DD:EE:FF',
'extra': {}}
PORTGROUP2 = {'id': 988,
'uuid': '55555555-4444-3333-2222-111111111111',
'name': 'Portgroup2-name',
'node_uuid': '66666666-7777-8888-9999-000000000000',
'address': 'AA:AA:AA:BB:BB:BB',
'extra': {}}
NODE1 = {'id': 123,
'uuid': '66666666-7777-8888-9999-000000000000',
'chassis_uuid': 'aaaaaaaa-1111-bbbb-2222-cccccccccccc',
'maintenance': False,
'provision_state': 'available',
'driver': 'fake',
'driver_info': {'user': 'foo', 'password': 'bar'},
'properties': {'num_cpu': 4},
'name': 'fake-node-1',
'resource_class': 'foo',
'extra': {}}
PORT = {'id': 456,
'uuid': '11111111-2222-3333-4444-555555555555',
'portgroup_uuid': '11111111-2222-3333-4444-555555555555',
'node_id': 123,
'address': 'AA:AA:AA:AA:AA:AA',
'extra': {}}
CREATE_PORTGROUP = copy.deepcopy(PORTGROUP)
del CREATE_PORTGROUP['id']
del CREATE_PORTGROUP['uuid']
UPDATED_PORTGROUP = copy.deepcopy(PORTGROUP)
NEW_ADDR = 'AA:AA:AA:AA:AA:AA'
UPDATED_PORTGROUP['address'] = NEW_ADDR
fake_responses = {
'/v1/portgroups':
{
'GET': (
{},
{"portgroups": [PORTGROUP, PORTGROUP2]},
),
'POST': (
{},
CREATE_PORTGROUP,
),
},
'/v1/portgroups/detail':
{
'GET': (
{},
{"portgroups": [PORTGROUP, PORTGROUP2]},
),
},
'/v1/portgroups/%s' % PORTGROUP['uuid']:
{
'GET': (
{},
PORTGROUP,
),
'DELETE': (
{},
None,
),
'PATCH': (
{},
UPDATED_PORTGROUP,
),
},
'/v1/portgroups/detail?address=%s' % PORTGROUP['address']:
{
'GET': (
{},
{"portgroups": [PORTGROUP]},
),
},
'/v1/portgroups/?address=%s' % PORTGROUP['address']:
{
'GET': (
{},
{"portgroups": [PORTGROUP]},
),
},
'/v1/portgroups/%s/ports' % PORTGROUP['name']:
{
'GET': (
{},
{"ports": [PORT]},
),
},
'/v1/portgroups/%s/ports' % PORTGROUP['uuid']:
{
'GET': (
{},
{"ports": [PORT]},
),
},
}
fake_responses_pagination = {
'/v1/portgroups':
{
'GET': (
{},
{"portgroups": [PORTGROUP],
"next": "http://127.0.0.1:6385/v1/portgroups/?limit=1"}
),
},
'/v1/portgroups/?limit=1':
{
'GET': (
{},
{"portgroups": [PORTGROUP2]}
),
},
'/v1/portgroups/?marker=%s' % PORTGROUP['uuid']:
{
'GET': (
{},
{"portgroups": [PORTGROUP2]}
),
},
}
fake_responses_sorting = {
'/v1/portgroups/?sort_key=updated_at':
{
'GET': (
{},
{"portgroups": [PORTGROUP2, PORTGROUP]}
),
},
'/v1/portgroups/?sort_dir=desc':
{
'GET': (
{},
{"portgroups": [PORTGROUP2, PORTGROUP]}
),
},
}
class PortgroupManagerTest(testtools.TestCase):
def setUp(self):
super(PortgroupManagerTest, self).setUp()
self.api = utils.FakeAPI(fake_responses)
self.mgr = ironicclient.v1.portgroup.PortgroupManager(self.api)
def test_portgroups_list(self):
portgroups = self.mgr.list()
expect = [
('GET', '/v1/portgroups', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(portgroups))
expected_resp = ({}, {"portgroups": [PORTGROUP, PORTGROUP2]},)
self.assertEqual(expected_resp,
self.api.responses['/v1/portgroups']['GET'])
def test_portgroups_list_by_address(self):
portgroups = self.mgr.list(address=PORTGROUP['address'])
expect = [
('GET', '/v1/portgroups/?address=%s' % PORTGROUP['address'], {},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(portgroups))
expected_resp = ({}, {"portgroups": [PORTGROUP, PORTGROUP2]},)
self.assertEqual(expected_resp,
self.api.responses['/v1/portgroups']['GET'])
def test_portgroups_list_by_address_detail(self):
portgroups = self.mgr.list(address=PORTGROUP['address'], detail=True)
expect = [
('GET', '/v1/portgroups/detail?address=%s' % PORTGROUP['address'],
{}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(portgroups))
self.assertIn(
PORTGROUP,
self.api.responses['/v1/portgroups']['GET'][1]['portgroups'])
def test_portgroups_list_detail(self):
portgroups = self.mgr.list(detail=True)
expect = [
('GET', '/v1/portgroups/detail', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(portgroups))
expected_resp = ({}, {"portgroups": [PORTGROUP, PORTGROUP2]},)
self.assertEqual(expected_resp,
self.api.responses['/v1/portgroups']['GET'])
def test_portgroups_show(self):
portgroup = self.mgr.get(PORTGROUP['uuid'])
expect = [
('GET', '/v1/portgroups/%s' % PORTGROUP['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(PORTGROUP['uuid'], portgroup.uuid)
self.assertEqual(PORTGROUP['name'], portgroup.name)
self.assertEqual(PORTGROUP['node_uuid'], portgroup.node_uuid)
self.assertEqual(PORTGROUP['address'], portgroup.address)
expected_resp = ({}, PORTGROUP,)
self.assertEqual(
expected_resp,
self.api.responses['/v1/portgroups/%s'
% PORTGROUP['uuid']]['GET'])
def test_portgroups_show_by_address(self):
portgroup = self.mgr.get_by_address(PORTGROUP['address'])
expect = [
('GET', '/v1/portgroups/detail?address=%s' % PORTGROUP['address'],
{}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(PORTGROUP['uuid'], portgroup.uuid)
self.assertEqual(PORTGROUP['name'], portgroup.name)
self.assertEqual(PORTGROUP['node_uuid'], portgroup.node_uuid)
self.assertEqual(PORTGROUP['address'], portgroup.address)
expected_resp = ({}, {"portgroups": [PORTGROUP]},)
self.assertEqual(
expected_resp,
self.api.responses['/v1/portgroups/detail?address=%s'
% PORTGROUP['address']]['GET'])
def test_create(self):
portgroup = self.mgr.create(**CREATE_PORTGROUP)
expect = [
('POST', '/v1/portgroups', {}, CREATE_PORTGROUP),
]
self.assertEqual(expect, self.api.calls)
self.assertTrue(portgroup)
self.assertIn(
PORTGROUP,
self.api.responses['/v1/portgroups']['GET'][1]['portgroups'])
def test_delete(self):
portgroup = self.mgr.delete(portgroup_id=PORTGROUP['uuid'])
expect = [
('DELETE', '/v1/portgroups/%s' % PORTGROUP['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertIsNone(portgroup)
expected_resp = ({}, PORTGROUP,)
self.assertEqual(
expected_resp,
self.api.responses['/v1/portgroups/%s'
% PORTGROUP['uuid']]['GET'])
def test_do_portgroup_delete_multiple_with_exception(self):
client_mock = mock.MagicMock()
client_mock.portgroup.delete.side_effect = (
[exceptions.ClientException, None])
args = mock.MagicMock()
args.portgroup = ['pg_uuid1', 'pg_uuid2']
self.assertRaises(exceptions.ClientException,
pg_shell.do_portgroup_delete,
client_mock, args)
client_mock.portgroup.delete.assert_has_calls(
[mock.call('pg_uuid1'), mock.call('pg_uuid2')])
expected_resp = ({}, PORTGROUP,)
self.assertEqual(
expected_resp,
self.api.responses['/v1/portgroups/%s'
% PORTGROUP['uuid']]['GET'])
def test_update(self):
patch = {'op': 'replace',
'value': NEW_ADDR,
'path': '/address'}
portgroup = self.mgr.update(portgroup_id=PORTGROUP['uuid'],
patch=patch)
expect = [
('PATCH', '/v1/portgroups/%s' % PORTGROUP['uuid'], {}, patch),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(NEW_ADDR, portgroup.address)
expected_resp = ({}, PORTGROUP,)
self.assertEqual(
expected_resp,
self.api.responses['/v1/portgroups/%s'
% PORTGROUP['uuid']]['GET'])
def test_portgroup_port_list_with_uuid(self):
ports = self.mgr.list_ports(PORTGROUP['uuid'])
expect = [
('GET', '/v1/portgroups/%s/ports' % PORTGROUP['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(ports))
self.assertEqual(PORT['uuid'], ports[0].uuid)
self.assertEqual(PORT['address'], ports[0].address)
expected_resp = ({}, {"ports": [PORT]},)
self.assertEqual(
expected_resp,
self.api.responses['/v1/portgroups/%s/ports'
% PORTGROUP['uuid']]['GET'])
def test_portgroup_port_list_with_name(self):
ports = self.mgr.list_ports(PORTGROUP['name'])
expect = [
('GET', '/v1/portgroups/%s/ports' % PORTGROUP['name'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(ports))
self.assertEqual(PORT['uuid'], ports[0].uuid)
self.assertEqual(PORT['address'], ports[0].address)
expected_resp = ({}, {"ports": [PORT]},)
self.assertEqual(
expected_resp,
self.api.responses['/v1/portgroups/%s/ports'
% PORTGROUP['name']]['GET'])
class PortgroupManagerPaginationTest(testtools.TestCase):
def setUp(self):
super(PortgroupManagerPaginationTest, self).setUp()
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = ironicclient.v1.portgroup.PortgroupManager(self.api)
def test_portgroups_list_limit(self):
portgroups = self.mgr.list(limit=1)
expect = [
('GET', '/v1/portgroups/?limit=1', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(portgroups))
expected_resp = (
{}, {"next": "http://127.0.0.1:6385/v1/portgroups/?limit=1",
"portgroups": [PORTGROUP]},)
self.assertEqual(expected_resp,
self.api.responses['/v1/portgroups']['GET'])
def test_portgroups_list_marker(self):
portgroups = self.mgr.list(marker=PORTGROUP['uuid'])
expect = [
('GET', '/v1/portgroups/?marker=%s' % PORTGROUP['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(portgroups))
expected_resp = (
{}, {"next": "http://127.0.0.1:6385/v1/portgroups/?limit=1",
"portgroups": [PORTGROUP]},)
self.assertEqual(expected_resp,
self.api.responses['/v1/portgroups']['GET'])
def test_portgroups_list_pagination_no_limit(self):
portgroups = self.mgr.list(limit=0)
expect = [
('GET', '/v1/portgroups', {}, None),
('GET', '/v1/portgroups/?limit=1', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(portgroups))
expected_resp = (
{}, {"next": "http://127.0.0.1:6385/v1/portgroups/?limit=1",
"portgroups": [PORTGROUP]},)
self.assertEqual(expected_resp,
self.api.responses['/v1/portgroups']['GET'])
class PortgroupManagerSortingTest(testtools.TestCase):
def setUp(self):
super(PortgroupManagerSortingTest, self).setUp()
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = ironicclient.v1.portgroup.PortgroupManager(self.api)
def test_portgroups_list_sort_key(self):
portgroups = self.mgr.list(sort_key='updated_at')
expect = [
('GET', '/v1/portgroups/?sort_key=updated_at', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(portgroups))
expected_resp = ({}, {"portgroups": [PORTGROUP2, PORTGROUP]},)
self.assertEqual(
expected_resp,
self.api.responses['/v1/portgroups/?sort_key=updated_at']['GET'])
def test_portgroups_list_sort_dir(self):
portgroups = self.mgr.list(sort_dir='desc')
expect = [
('GET', '/v1/portgroups/?sort_dir=desc', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(portgroups))
expected_resp = ({}, {"portgroups": [PORTGROUP2, PORTGROUP]},)
self.assertEqual(
expected_resp,
self.api.responses['/v1/portgroups/?sort_dir=desc']['GET'])

View File

@ -0,0 +1,294 @@
# Copyright 2016 SAP Ltd.
#
# 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 mock
from ironicclient.common.apiclient import exceptions
from ironicclient.common import cliutils
from ironicclient.common import utils as commonutils
from ironicclient.tests.unit import utils
import ironicclient.v1.portgroup_shell as pg_shell
class PortgroupShellTest(utils.BaseTestCase):
def test_portgroup_show(self):
actual = {}
fake_print_dict = lambda data, *args, **kwargs: actual.update(data)
with mock.patch.object(cliutils, 'print_dict', fake_print_dict):
portgroup = object()
pg_shell._print_portgroup_show(portgroup)
exp = ['address', 'created_at', 'extra', 'standalone_ports_supported',
'node_uuid', 'updated_at', 'uuid', 'name', 'internal_info']
act = actual.keys()
self.assertEqual(sorted(exp), sorted(act))
def test_do_portgroup_show(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.portgroup = 'portgroup_uuid'
args.address = False
args.fields = None
args.json = False
pg_shell.do_portgroup_show(client_mock, args)
client_mock.portgroup.get.assert_called_once_with('portgroup_uuid',
fields=None)
self.assertFalse(client_mock.portgroup.get_by_address.called)
def test_do_portgroup_show_space_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.portgroup = ' '
args.address = False
args.json = False
self.assertRaises(exceptions.CommandError,
pg_shell.do_portgroup_show,
client_mock, args)
def test_do_portgroup_show_empty_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.portgroup = ''
args.address = False
args.json = False
self.assertRaises(exceptions.CommandError,
pg_shell.do_portgroup_show,
client_mock, args)
def test_do_portgroup_show_by_address(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.portgroup = 'portgroup_address'
args.address = True
args.fields = None
args.json = False
pg_shell.do_portgroup_show(client_mock, args)
client_mock.portgroup.get_by_address.assert_called_once_with(
'portgroup_address', fields=None)
self.assertFalse(client_mock.portgroup.get.called)
def test_do_portgroup_update(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.portgroup = 'portgroup_uuid'
args.op = 'add'
args.attributes = [['arg1=val1', 'arg2=val2']]
args.json = False
pg_shell.do_portgroup_update(client_mock, args)
patch = commonutils.args_array_to_patch(args.op, args.attributes[0])
client_mock.portgroup.update.assert_called_once_with('portgroup_uuid',
patch)
def _get_client_mock_args(self, address=None, marker=None, limit=None,
sort_dir=None, sort_key=None, detail=False,
fields=None, node=None, json=False,
portgroup=None):
args = mock.MagicMock(spec=True)
args.address = address
args.node = node
args.marker = marker
args.limit = limit
args.sort_dir = sort_dir
args.sort_key = sort_key
args.detail = detail
args.fields = fields
args.json = json
args.portgroup = portgroup
return args
def test_do_portgroup_list(self):
client_mock = mock.MagicMock()
args = self._get_client_mock_args()
pg_shell.do_portgroup_list(client_mock, args)
client_mock.portgroup.list.assert_called_once_with(detail=False)
def test_do_portgroup_list_detail(self):
client_mock = mock.MagicMock()
args = self._get_client_mock_args(detail=True)
pg_shell.do_portgroup_list(client_mock, args)
client_mock.portgroup.list.assert_called_once_with(detail=True)
def test_do_portgroup_list_sort_key(self):
client_mock = mock.MagicMock()
args = self._get_client_mock_args(sort_key='uuid',
detail=False)
pg_shell.do_portgroup_list(client_mock, args)
client_mock.portgroup.list.assert_called_once_with(sort_key='uuid',
detail=False)
def test_do_portgroup_list_wrong_sort_key(self):
client_mock = mock.MagicMock()
args = self._get_client_mock_args(sort_key='node_uuid',
detail=False)
self.assertRaises(exceptions.CommandError,
pg_shell.do_portgroup_list,
client_mock, args)
self.assertFalse(client_mock.portgroup.list.called)
def test_do_portgroup_list_detail_sort_key(self):
client_mock = mock.MagicMock()
args = self._get_client_mock_args(sort_key='uuid',
detail=True)
pg_shell.do_portgroup_list(client_mock, args)
client_mock.portgroup.list.assert_called_once_with(sort_key='uuid',
detail=True)
def test_do_portgroup_list_detail_wrong_sort_key(self):
client_mock = mock.MagicMock()
args = self._get_client_mock_args(sort_key='node_uuid',
detail=True)
self.assertRaises(exceptions.CommandError,
pg_shell.do_portgroup_list,
client_mock, args)
self.assertFalse(client_mock.portgroup.list.called)
def test_do_portgroup_port_list(self):
client_mock = mock.MagicMock()
pg_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(portgroup=pg_mock)
pg_shell.do_portgroup_port_list(client_mock, args)
client_mock.portgroup.list_ports.assert_called_once_with(
pg_mock, detail=False)
def test_do_portgroup_port_list_detail(self):
client_mock = mock.MagicMock()
pg_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(portgroup=pg_mock, detail=True)
pg_shell.do_portgroup_port_list(client_mock, args)
client_mock.portgroup.list_ports.assert_called_once_with(
pg_mock, detail=True)
def test_do_portgroup_port_list_sort_key(self):
client_mock = mock.MagicMock()
pg_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(portgroup=pg_mock,
sort_key='created_at')
pg_shell.do_portgroup_port_list(client_mock, args)
client_mock.portgroup.list_ports.assert_called_once_with(
pg_mock, detail=False, sort_key='created_at')
def test_do_portgroup_port_list_wrong_sort_key(self):
client_mock = mock.MagicMock()
pg_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(portgroup=pg_mock,
sort_key='node_uuid')
self.assertRaises(exceptions.CommandError,
pg_shell.do_portgroup_port_list,
client_mock, args)
self.assertFalse(client_mock.portgroup.list_ports.called)
def test_do_portgroup_port_list_detail_sort_key(self):
client_mock = mock.MagicMock()
pg_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(portgroup=pg_mock,
sort_key='created_at',
detail=True)
pg_shell.do_portgroup_port_list(client_mock, args)
client_mock.portgroup.list_ports.assert_called_once_with(
pg_mock, detail=True, sort_key='created_at')
def test_do_portgroup_port_list_detail_wrong_sort_key(self):
client_mock = mock.MagicMock()
pg_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(portgroup=pg_mock,
sort_key='node_uuid',
detail=True)
self.assertRaises(exceptions.CommandError,
pg_shell.do_portgroup_port_list,
client_mock, args)
self.assertFalse(client_mock.portgroup.list_ports.called)
def test_do_portgroup_port_list_sort_dir(self):
client_mock = mock.MagicMock()
pg_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(portgroup=pg_mock,
sort_dir='desc')
pg_shell.do_portgroup_port_list(client_mock, args)
client_mock.portgroup.list_ports.assert_called_once_with(
pg_mock, detail=False, sort_dir='desc')
def test_do_portgroup_port_list_wrong_sort_dir(self):
client_mock = mock.MagicMock()
pg_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(portgroup=pg_mock,
sort_dir='abc')
self.assertRaises(exceptions.CommandError,
pg_shell.do_portgroup_port_list,
client_mock, args)
self.assertFalse(client_mock.portgroup.list_ports.called)
def test_do_portgroup_port_list_fields(self):
client_mock = mock.MagicMock()
pg_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(portgroup=pg_mock,
fields=[['uuid', 'address']])
pg_shell.do_portgroup_port_list(client_mock, args)
client_mock.portgroup.list_ports.assert_called_once_with(
pg_mock, detail=False, fields=['uuid', 'address'])
def test_do_portgroup_port_list_wrong_fields(self):
client_mock = mock.MagicMock()
pg_mock = mock.MagicMock(spec_set=[])
args = self._get_client_mock_args(portgroup=pg_mock,
fields=[['foo', 'bar']])
self.assertRaises(exceptions.CommandError,
pg_shell.do_portgroup_port_list,
client_mock, args)
self.assertFalse(client_mock.portgroup.list_ports.called)
def test_do_portgroup_create(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.json = False
pg_shell.do_portgroup_create(client_mock, args)
client_mock.portgroup.create.assert_called_once_with()
def test_do_portgroup_address(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.address = 'aa:bb:cc:dd:ee:ff'
args.json = False
pg_shell.do_portgroup_create(client_mock, args)
client_mock.portgroup.create.assert_called_once_with(
address='aa:bb:cc:dd:ee:ff')
def test_do_portgroup_node_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.node_uuid = 'node-uuid'
args.json = False
pg_shell.do_portgroup_create(client_mock, args)
client_mock.portgroup.create.assert_called_once_with(
node_uuid='node-uuid')
def test_do_portgroup_delete(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.portgroup = ['portgroup_uuid']
pg_shell.do_portgroup_delete(client_mock, args)
client_mock.portgroup.delete.assert_called_once_with('portgroup_uuid')

View File

@ -22,6 +22,7 @@ from ironicclient.v1 import chassis
from ironicclient.v1 import driver
from ironicclient.v1 import node
from ironicclient.v1 import port
from ironicclient.v1 import portgroup
class Client(object):
@ -62,3 +63,4 @@ class Client(object):
self.node = node.NodeManager(self.http_client)
self.port = port.PortManager(self.http_client)
self.driver = driver.DriverManager(self.http_client)
self.portgroup = portgroup.PortgroupManager(self.http_client)

View File

@ -28,7 +28,8 @@ class Port(base.Resource):
class PortManager(base.CreateManager):
resource_class = Port
_creation_attributes = ['address', 'extra', 'local_link_connection',
'node_uuid', 'pxe_enabled', 'uuid']
'node_uuid', 'portgroup_uuid', 'pxe_enabled',
'uuid']
_resource_name = 'ports'
def list(self, address=None, limit=None, marker=None, sort_key=None,

View File

@ -150,6 +150,11 @@ def do_port_list(cc, args):
help="Key/value metadata describing Local link connection information. "
"Valid keys are switch_info, switch_id, port_id."
"Can be specified multiple times.")
@cliutils.arg(
'--portgroup',
metavar="<portgroup>",
dest='portgroup_uuid',
help='UUID of the portgroup that this port belongs to.')
@cliutils.arg(
'--pxe-enabled',
metavar='<boolean>',
@ -168,7 +173,8 @@ def do_port_list(cc, args):
def do_port_create(cc, args):
"""Create a new port."""
field_list = ['address', 'extra', 'node_uuid', 'uuid',
'local_link_connection', 'pxe_enabled']
'local_link_connection', 'portgroup_uuid',
'pxe_enabled']
fields = dict((k, v) for (k, v) in vars(args).items()
if k in field_list and not (v is None))
fields = utils.args_array_to_dict(fields, 'extra')

View File

@ -0,0 +1,204 @@
# Copyright 2016 SAP Ltd.
#
# 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 ironicclient.common import base
from ironicclient.common.i18n import _
from ironicclient.common import utils
from ironicclient import exc
class Portgroup(base.Resource):
def __repr__(self):
return "<Portgroup %s>" % self._info
class PortgroupManager(base.CreateManager):
resource_class = Portgroup
_resource_name = 'portgroups'
_creation_attributes = ['address', 'extra', 'name', 'node_uuid',
'standalone_ports_supported']
def list(self, node=None, address=None, limit=None, marker=None,
sort_key=None, sort_dir=None, detail=False, fields=None):
"""Retrieve a list of portgroups.
:param node: Optional, UUID or name of a node, to get
the portgroups for that node.
:param address: Optional, MAC address of a portgroup, to get
the portgroup which has this MAC address.
:param marker: Optional, the UUID of a portgroup, eg the last
portgroup from a previous result set. Return
the next result set.
:param limit: The maximum number of results to return per
request, if:
1) limit > 0, the maximum number of portgroups to return.
2) limit == 0, return the entire list of portgroups.
3) limit == None, the number of items returned respect the
maximum imposed by the Ironic API (see Ironic's
api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:param detail: Optional, boolean whether to return detailed information
about portgroups.
: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 portgroups.
:raises: InvalidAttribute if a subset of fields is requested with
detail option set.
"""
if limit is not None:
limit = int(limit)
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)
if node is not None:
filters.append('node=%s' % node)
path = ''
if detail:
path += 'detail'
if filters:
path += '?' + '&'.join(filters)
if limit is None:
return self._list(self._path(path), "portgroups")
else:
return self._list_pagination(self._path(path), "portgroups",
limit=limit)
def list_ports(self, portgroup_id, marker=None, limit=None, sort_key=None,
sort_dir=None, detail=False, fields=None):
"""List all the ports for a given portgroup.
:param portgroup_id: Name or UUID of the portgroup.
:param marker: Optional, the UUID of a port, eg the last
port from a previous result set. Return
the next result set.
:param limit: The maximum number of results to return per
request, if:
1) limit > 0, the maximum number of ports to return.
2) limit == 0, return the entire list of ports.
3) limit param is NOT specified (None), the number of items
returned respect the maximum imposed by the Ironic API
(see Ironic's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
: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)
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/ports" % portgroup_id
if detail:
path += '/detail'
if filters:
path += '?' + '&'.join(filters)
if limit is None:
return self._list(self._path(path), "ports")
else:
return self._list_pagination(self._path(path), "ports",
limit=limit)
def get(self, portgroup_id, fields=None):
"""Get a port group with the specified identifier.
:param portgroup_id: The UUID or name of a portgroup.
: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 :class:`Portgroup` object.
"""
return self._get(resource_id=portgroup_id, fields=fields)
def get_by_address(self, address, fields=None):
"""Get a port group with the specified MAC address.
:param address: The MAC address of a portgroup.
: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 :class:`Portgroup` object.
"""
path = '?address=%s' % address
if fields is not None:
path += '&fields=' + ','.join(fields)
else:
path = 'detail' + path
portgroups = self._list(self._path(path), 'portgroups')
# get all the details of the portgroup assuming that
# filtering by address returns a collection of one portgroup
# if successful.
if len(portgroups) == 1:
return portgroups[0]
else:
raise exc.NotFound()
def delete(self, portgroup_id):
"""Delete the Portgroup from the DB.
:param portgroup_id: The UUID or name of a portgroup.
"""
return self._delete(resource_id=portgroup_id)
def update(self, portgroup_id, patch):
"""Update the Portgroup.
:param portgroup_id: The UUID or name of a portgroup.
:param patch: The patch request with updates.
"""
return self._update(resource_id=portgroup_id, patch=patch)

View File

@ -0,0 +1,292 @@
# Copyright 2015 SAP Ltd.
# All Rights Reserved.
#
# 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 ironicclient.common.apiclient import exceptions
from ironicclient.common import cliutils
from ironicclient.common.i18n import _
from ironicclient.common import utils
from ironicclient.v1 import resource_fields as res_fields
def _print_portgroup_show(portgroup, fields=None, json=False):
if fields is None:
fields = res_fields.PORTGROUP_DETAILED_RESOURCE.fields
data = dict([(f, getattr(portgroup, f, '')) for f in fields])
cliutils.print_dict(data, wrap=72, json_flag=json)
@cliutils.arg(
'portgroup',
metavar='<id>',
help="Name or UUID of the portgroup "
"(or MAC address if --address is specified).")
@cliutils.arg(
'--address',
dest='address',
action='store_true',
default=False,
help='<id> is the MAC address (instead of the UUID) of the portgroup.')
@cliutils.arg(
'--fields',
nargs='+',
dest='fields',
metavar='<field>',
action='append',
default=[],
help="One or more portgroup fields. Only these fields will be fetched "
"from the server.")
def do_portgroup_show(cc, args):
"""Show detailed information about a portgroup."""
fields = args.fields[0] if args.fields else None
utils.check_for_invalid_fields(
fields, res_fields.PORTGROUP_DETAILED_RESOURCE.fields)
if args.address:
portgroup = cc.portgroup.get_by_address(args.portgroup, fields=fields)
else:
utils.check_empty_arg(args.portgroup, '<id>')
portgroup = cc.portgroup.get(args.portgroup, fields=fields)
_print_portgroup_show(portgroup, fields=fields, json=args.json)
@cliutils.arg(
'--detail',
dest='detail',
action='store_true',
default=False,
help="Show detailed information about portgroups.")
@cliutils.arg(
'-n', '--node',
dest='node',
metavar='<node>',
help='UUID of the node that this portgroup belongs to.')
@cliutils.arg(
'-a', '--address',
metavar='<mac-address>',
help='Only show information for the portgroup with this MAC address.')
@cliutils.arg(
'--limit',
metavar='<limit>',
type=int,
help='Maximum number of portgroups to return per request, '
'0 for no limit. Default is the maximum number used '
'by the Ironic API Service.')
@cliutils.arg(
'--marker',
metavar='<portgroup>',
help='Portgroup UUID (for example, of the last portgroup in the list '
'from a previous request). '
'Returns the list of portgroups after this UUID.')
@cliutils.arg(
'--sort-key',
metavar='<field>',
help='Portgroup field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
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 portgroup fields. Only these fields will be fetched "
"from the server. Can not be used when '--detail' is specified.")
def do_portgroup_list(cc, args):
"""List the portgroups."""
params = {}
if args.address is not None:
params['address'] = args.address
if args.node is not None:
params['node'] = args.node
if args.detail:
fields = res_fields.PORTGROUP_DETAILED_RESOURCE.fields
field_labels = res_fields.PORTGROUP_DETAILED_RESOURCE.labels
elif args.fields:
utils.check_for_invalid_fields(
args.fields[0], res_fields.PORTGROUP_DETAILED_RESOURCE.fields)
resource = res_fields.Resource(args.fields[0])
fields = resource.fields
field_labels = resource.labels
else:
fields = res_fields.PORTGROUP_RESOURCE.fields
field_labels = res_fields.PORTGROUP_RESOURCE.labels
sort_fields = res_fields.PORTGROUP_DETAILED_RESOURCE.sort_fields
sort_field_labels = res_fields.PORTGROUP_DETAILED_RESOURCE.sort_labels
params.update(utils.common_params_for_list(args,
sort_fields,
sort_field_labels))
portgroup = cc.portgroup.list(**params)
cliutils.print_list(portgroup, fields,
field_labels=field_labels,
sortby_index=None,
json_flag=args.json)
@cliutils.arg(
'-a', '--address',
metavar='<address>',
help='MAC address for this portgroup.')
@cliutils.arg(
'-n', '--node',
dest='node_uuid',
metavar='<node>',
required=True,
help='UUID of the node that this portgroup belongs to.')
@cliutils.arg(
'--name',
metavar="<name>",
help='Name for the portgroup.')
@cliutils.arg(
'-e', '--extra',
metavar="<key=value>",
action='append',
help="Record arbitrary key/value metadata. "
"Can be specified multiple times.")
@cliutils.arg(
'--standalone-ports-supported',
metavar="<boolean>",
help='Specifies whether ports from this portgroup can be used '
'in stand alone mode.')
@cliutils.arg(
'-u', '--uuid',
metavar='<uuid>',
help="UUID of the portgroup.")
def do_portgroup_create(cc, args):
"""Create a new portgroup."""
field_list = ['address', 'extra', 'node_uuid', 'name', 'uuid',
'standalone_ports_supported']
fields = dict((k, v) for (k, v) in vars(args).items()
if k in field_list and not (v is None))
fields = utils.args_array_to_dict(fields, 'extra')
portgroup = cc.portgroup.create(**fields)
data = dict([(f, getattr(portgroup, f, '')) for f in field_list])
cliutils.print_dict(data, wrap=72, json_flag=args.json)
@cliutils.arg(
'--detail',
dest='detail',
action='store_true',
default=False,
help="Show detailed information about the ports.")
@cliutils.arg(
'--limit',
metavar='<limit>',
type=int,
help='Maximum number of ports to return per request, '
'0 for no limit. Default is the maximum number used '
'by the Ironic API Service.')
@cliutils.arg(
'--marker',
metavar='<port>',
help='Port UUID (for example, of the last port in the list from a '
'previous request). Returns the list of ports after this UUID.')
@cliutils.arg(
'--sort-key',
metavar='<field>',
help='Port field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
metavar='<direction>',
choices=['asc', 'desc'],
help='Sort direction: "asc" (the default) or "desc".')
@cliutils.arg(
'portgroup',
metavar='<portgroup>',
help="Name or UUID of the portgroup.")
@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_portgroup_port_list(cc, args):
"""List the ports associated with a portgroup."""
if args.detail:
fields = res_fields.PORT_DETAILED_RESOURCE.fields
field_labels = res_fields.PORT_DETAILED_RESOURCE.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
else:
fields = res_fields.PORT_RESOURCE.fields
field_labels = res_fields.PORT_RESOURCE.labels
sort_fields = res_fields.PORT_DETAILED_RESOURCE.sort_fields
sort_field_labels = res_fields.PORT_DETAILED_RESOURCE.sort_labels
params = utils.common_params_for_list(args, sort_fields,
sort_field_labels)
ports = cc.portgroup.list_ports(args.portgroup, **params)
cliutils.print_list(ports, fields,
field_labels=field_labels,
sortby_index=None,
json_flag=args.json)
@cliutils.arg('portgroup', metavar='<portgroup>', nargs='+',
help="UUID or Name of the portgroup.")
def do_portgroup_delete(cc, args):
"""Delete a portgroup."""
failures = []
for p in args.portgroup:
try:
cc.portgroup.delete(p)
print('Deleted portgroup %s' % p)
except exceptions.ClientException as e:
failures.append(_("Failed to delete portgroup %(pg)s: %(error)s")
% {'pg': p, 'error': e})
if failures:
raise exceptions.ClientException("\n".join(failures))
@cliutils.arg('portgroup', metavar='<portgroup>',
help="UUID or Name of the portgroup.")
@cliutils.arg(
'op',
metavar='<op>',
choices=['add', 'replace', 'remove'],
help="Operation: 'add', 'replace', or 'remove'.")
@cliutils.arg(
'attributes',
metavar='<path=value>',
nargs='+',
action='append',
default=[],
help="Attribute to add, replace, or remove. Can be specified multiple "
"times. For 'remove', only <path> is necessary.")
def do_portgroup_update(cc, args):
"""Update information about a portgroup."""
patch = utils.args_array_to_patch(args.op, args.attributes[0])
portgroup = cc.portgroup.update(args.portgroup, patch)
_print_portgroup_show(portgroup, json=args.json)

View File

@ -68,7 +68,9 @@ class Resource(object):
'uuid': 'UUID',
'local_link_connection': 'Local Link Connection',
'pxe_enabled': 'PXE boot enabled',
'portgroup_uuid': 'Portgroup UUID',
'network_interface': 'Network Interface',
'standalone_ports_supported': 'Standalone Ports Supported'
}
def __init__(self, field_ids, sort_excluded=None):
@ -197,19 +199,48 @@ PORT_DETAILED_RESOURCE = Resource(
'extra',
'node_uuid',
'local_link_connection',
'portgroup_uuid',
'pxe_enabled',
'updated_at',
'internal_info',
],
sort_excluded=[
'extra',
# The server cannot sort on "node_uuid" because it isn't a column in
# the "ports" database table. "node_id" is stored, but it is internal
# to ironic. See bug #1443003 for more details.
# The server cannot sort on "node_uuid" or "portgroup_uuid" because
# they aren't columns in the "ports" database table. "node_id" and
# "portgroup_id" are stored, but it is internal to ironic.
# See bug #1443003 for more details.
'node_uuid',
'portgroup_uuid',
'internal_info',
])
PORT_RESOURCE = Resource(
['uuid',
'address',
])
# Portgroups
PORTGROUP_DETAILED_RESOURCE = Resource(
['uuid',
'address',
'created_at',
'extra',
'standalone_ports_supported',
'node_uuid',
'name',
'updated_at',
'internal_info',
],
sort_excluded=[
'extra',
# The server cannot sort on "node_uuid" because it isn't a column in
# the "portgroups" database table. "node_id" is stored, but it is
# internal to ironic. See bug #1443003 for more details.
'node_uuid',
'internal_info',
])
PORTGROUP_RESOURCE = Resource(
['uuid',
'address',
'name',
])

View File

@ -17,11 +17,13 @@ from ironicclient.v1 import create_resources_shell
from ironicclient.v1 import driver_shell
from ironicclient.v1 import node_shell
from ironicclient.v1 import port_shell
from ironicclient.v1 import portgroup_shell
COMMAND_MODULES = [
chassis_shell,
node_shell,
port_shell,
portgroup_shell,
driver_shell,
create_resources_shell,
]

View File

@ -0,0 +1,14 @@
---
features:
- |
Extends the Ironic CLI with new commands:
* ironic portgroup-create
* ironic portgroup-show
* ironic portgroup-list
* ironic portgroup-delete
* ironic portgroup-update
* ironic portgroup-port-list
Also extends ironic port-create with --portgroup.
Portgroup support was added in Ironic API version 1.24.