Allow creating portgroups via create commands

This change allows specifying portgroups (that themselves may contain
ports) inside the nodes dictionaries.

Related-Bug: #1618754
Change-Id: I858412d135717e78291bbef8b0861ef00c5d4fdb
This commit is contained in:
Vladyslav Drok 2017-01-25 16:20:17 +02:00
parent 1f586f1977
commit d9f595bd82
5 changed files with 227 additions and 26 deletions

View File

@ -10,11 +10,11 @@ or YAML format. It can be done in one of three ways:
$ ironic help create
usage: ironic create <file> [<file> ...]
Create baremetal resources (chassis, nodes, and ports). The resources may
be described in one or more JSON or YAML files. If any file cannot be
validated, no resources are created. An attempt is made to create all the
resources; those that could not be created are skipped (with a
corresponding error message).
Create baremetal resources (chassis, nodes, port groups and ports). The
resources may be described in one or more JSON or YAML files. If any file
cannot be validated, no resources are created. An attempt is made to
create all the resources; those that could not be created are skipped
(with a corresponding error message).
Positional arguments:
<file> File (.yaml or .json) containing descriptions of the resources
@ -64,10 +64,11 @@ ending with ``.json`` is assumed to contain valid JSON, and a file ending with
``.yaml`` is assumed to contain valid YAML. Specifying a file with any other
extension leads to an error.
The resources that can be created are chassis, nodes, and ports. Only chassis
and nodes are accepted at the top level of the file structure, but chassis and
nodes themselves can contain nodes or ports definitions nested under ``nodes``
(in case of chassis) or ``ports`` (in case of nodes) keys.
The resources that can be created are chassis, nodes, port groups and ports.
A chassis can contain nodes (and resources of nodes) definitions nested under
``"nodes"`` key. A node can contain port groups definitions nested under
``"portgroups"``, and ports definitions under ``"ports"`` keys. Ports can be
also nested under port groups in ``"ports"`` key.
The schema used to validate the supplied data is the following::
@ -109,6 +110,19 @@ command::
{
"name": "node-3",
"driver": "agent_ipmitool",
"portgroups": [
{
"name": "switch.cz7882.ports.1-2",
"ports": [
{
"address": "ff:00:00:00:00:00"
},
{
"address": "ff:00:00:00:00:01"
}
]
}
],
"ports": [
{
"address": "00:00:00:00:00:02"
@ -162,11 +176,12 @@ Creation Process
#. Each resource is created via issuing a POST request (with the resource's
dictionary representation in the body) to the ironic-api service. In the
case of nested resources (``"nodes"`` key inside chassis, or ``"ports"``
key inside nodes), the top-level resource is created first, followed by the
sub-resources. For example, if a chassis contains a list of nodes, the
chassis will be created first followed by the creation of each node. The
same is true for ports described within nodes.
case of nested resources (``"nodes"`` key inside chassis, ``"portgroups"``
key inside nodes, ``"ports"`` key inside nodes or portgroups), the top-level
resource is created first, followed by the sub-resources. For example, if a
chassis contains a list of nodes, the chassis will be created first followed
by the creation of each node. The same is true for ports and port groups
described within nodes.
#. If a resource could not be created, it does not stop the entire process.
Any sub-resources of the failed resource will not be created, but otherwise,

View File

@ -12,6 +12,7 @@
import jsonschema
import mock
import six
import six.moves.builtins as __builtin__
from ironicclient import exc
@ -35,6 +36,13 @@ valid_json = {
"extra": {
"a": "b"
}
}],
"portgroups": [{
"address": "00:00:00:00:00:02",
"name": "portgroup1",
"ports": [{
"address": "00:00:00:00:00:03"
}]
}]
}]
}],
@ -256,6 +264,15 @@ class CreateMethodsTest(utils.BaseTestCase):
)
self.client.node.create.assert_called_once_with(driver='fake')
def test_create_single_node_with_portgroups(self):
params = {'driver': 'fake', 'portgroups': ['some portgroups']}
self.client.node.create.return_value = mock.Mock(uuid='uuid')
self.assertEqual(
('uuid', None),
create_resources.create_single_node(self.client, **params)
)
self.client.node.create.assert_called_once_with(driver='fake')
def test_create_single_node_raises_client_exception(self):
params = {'driver': 'fake'}
e = exc.ClientException('foo')
@ -285,6 +302,28 @@ class CreateMethodsTest(utils.BaseTestCase):
)
self.client.port.create.assert_called_once_with(**params)
def test_create_single_portgroup(self):
params = {'address': 'fake-address', 'node_uuid': 'fake-node-uuid'}
self.client.portgroup.create.return_value = mock.Mock(
uuid='fake-portgroup-uuid')
self.assertEqual(
('fake-portgroup-uuid', None),
create_resources.create_single_portgroup(self.client, **params)
)
self.client.portgroup.create.assert_called_once_with(**params)
def test_create_single_portgroup_with_ports(self):
params = {'ports': ['some ports'], 'node_uuid': 'fake-node-uuid'}
self.client.portgroup.create.return_value = mock.Mock(
uuid='fake-portgroup-uuid')
self.assertEqual(
('fake-portgroup-uuid', None),
create_resources.create_single_portgroup(
self.client, **params)
)
self.client.portgroup.create.assert_called_once_with(
node_uuid='fake-node-uuid')
def test_create_single_chassis(self):
self.client.chassis.create.return_value = mock.Mock(uuid='uuid')
self.assertEqual(
@ -313,31 +352,49 @@ class CreateMethodsTest(utils.BaseTestCase):
def test_create_ports_two_node_uuids(self):
port = {'address': 'fake-address', 'node_uuid': 'node-uuid-1'}
self.client.port.create.return_value = mock.Mock(uuid='uuid')
errs = create_resources.create_ports(self.client, [port],
'node-uuid-2')
self.assertIsInstance(errs[0], exc.ClientException)
self.assertEqual(1, len(errs))
self.assertFalse(self.client.port.create.called)
def test_create_ports_two_portgroup_uuids(self):
port = {'address': 'fake-address', 'node_uuid': 'node-uuid-1',
'portgroup_uuid': 'pg-uuid-1'}
errs = create_resources.create_ports(self.client, [port],
'node-uuid-1', 'pg-uuid-2')
self.assertEqual(1, len(errs))
self.assertIsInstance(errs[0], exc.ClientException)
self.assertIn('port group', six.text_type(errs[0]))
self.assertFalse(self.client.port.create.called)
@mock.patch.object(create_resources, 'create_portgroups', autospec=True)
@mock.patch.object(create_resources, 'create_ports', autospec=True)
def test_create_nodes(self, mock_create_ports):
node = {'driver': 'fake', 'ports': ['list of ports']}
def test_create_nodes(self, mock_create_ports, mock_create_portgroups):
node = {'driver': 'fake', 'ports': ['list of ports'],
'portgroups': ['list of portgroups']}
self.client.node.create.return_value = mock.Mock(uuid='uuid')
self.assertEqual([], create_resources.create_nodes(self.client,
[node]))
self.client.node.create.assert_called_once_with(driver='fake')
mock_create_ports.assert_called_once_with(
self.client, ['list of ports'], node_uuid='uuid')
mock_create_portgroups.assert_called_once_with(
self.client, ['list of portgroups'], node_uuid='uuid')
@mock.patch.object(create_resources, 'create_portgroups', autospec=True)
@mock.patch.object(create_resources, 'create_ports', autospec=True)
def test_create_nodes_exception(self, mock_create_ports):
node = {'driver': 'fake', 'ports': ['list of ports']}
def test_create_nodes_exception(self, mock_create_ports,
mock_create_portgroups):
node = {'driver': 'fake', 'ports': ['list of ports'],
'portgroups': ['list of portgroups']}
self.client.node.create.side_effect = exc.ClientException('bar')
errs = create_resources.create_nodes(self.client, [node])
self.assertIsInstance(errs[0], exc.ClientException)
self.assertEqual(1, len(errs))
self.client.node.create.assert_called_once_with(driver='fake')
self.assertFalse(mock_create_ports.called)
self.assertFalse(mock_create_portgroups.called)
@mock.patch.object(create_resources, 'create_ports', autospec=True)
def test_create_nodes_two_chassis_uuids(self, mock_create_ports):
@ -350,14 +407,17 @@ class CreateMethodsTest(utils.BaseTestCase):
self.assertEqual(1, len(errs))
self.assertIsInstance(errs[0], exc.ClientException)
@mock.patch.object(create_resources, 'create_portgroups', autospec=True)
@mock.patch.object(create_resources, 'create_ports', autospec=True)
def test_create_nodes_no_ports(self, mock_create_ports):
def test_create_nodes_no_ports_portgroups(self, mock_create_ports,
mock_create_portgroups):
node = {'driver': 'fake'}
self.client.node.create.return_value = mock.Mock(uuid='uuid')
self.assertEqual([], create_resources.create_nodes(self.client,
[node]))
self.client.node.create.assert_called_once_with(driver='fake')
self.assertFalse(mock_create_ports.called)
self.assertFalse(mock_create_portgroups.called)
@mock.patch.object(create_resources, 'create_nodes', autospec=True)
def test_create_chassis(self, mock_create_nodes):
@ -387,3 +447,52 @@ class CreateMethodsTest(utils.BaseTestCase):
[chassis]))
self.client.chassis.create.assert_called_once_with(description='fake')
self.assertFalse(mock_create_nodes.called)
@mock.patch.object(create_resources, 'create_ports', autospec=True)
def test_create_portgroups(self, mock_create_ports):
portgroup = {'name': 'fake', 'ports': ['list of ports']}
portgroup_posted = {'name': 'fake', 'node_uuid': 'fake-node-uuid'}
self.client.portgroup.create.return_value = mock.Mock(uuid='uuid')
self.assertEqual([], create_resources.create_portgroups(
self.client, [portgroup], node_uuid='fake-node-uuid'))
self.client.portgroup.create.assert_called_once_with(
**portgroup_posted)
mock_create_ports.assert_called_once_with(
self.client, ['list of ports'], node_uuid='fake-node-uuid',
portgroup_uuid='uuid')
@mock.patch.object(create_resources, 'create_ports', autospec=True)
def test_create_portgroups_exception(self, mock_create_ports):
portgroup = {'name': 'fake', 'ports': ['list of ports']}
portgroup_posted = {'name': 'fake', 'node_uuid': 'fake-node-uuid'}
self.client.portgroup.create.side_effect = exc.ClientException('bar')
errs = create_resources.create_portgroups(
self.client, [portgroup], node_uuid='fake-node-uuid')
self.client.portgroup.create.assert_called_once_with(
**portgroup_posted)
self.assertFalse(mock_create_ports.called)
self.assertEqual(1, len(errs))
self.assertIsInstance(errs[0], exc.ClientException)
@mock.patch.object(create_resources, 'create_ports', autospec=True)
def test_create_portgroups_two_node_uuids(self, mock_create_ports):
portgroup = {'name': 'fake', 'node_uuid': 'fake-node-uuid-1',
'ports': ['list of ports']}
self.client.portgroup.create.side_effect = exc.ClientException('bar')
errs = create_resources.create_portgroups(
self.client, [portgroup], node_uuid='fake-node-uuid-2')
self.assertFalse(self.client.portgroup.create.called)
self.assertFalse(mock_create_ports.called)
self.assertEqual(1, len(errs))
self.assertIsInstance(errs[0], exc.ClientException)
@mock.patch.object(create_resources, 'create_ports', autospec=True)
def test_create_portgroups_no_ports(self, mock_create_ports):
portgroup = {'name': 'fake'}
portgroup_posted = {'name': 'fake', 'node_uuid': 'fake-node-uuid'}
self.client.portgroup.create.return_value = mock.Mock(uuid='uuid')
self.assertEqual([], create_resources.create_portgroups(
self.client, [portgroup], node_uuid='fake-node-uuid'))
self.client.portgroup.create.assert_called_once_with(
**portgroup_posted)
self.assertFalse(mock_create_ports.called)

View File

@ -142,7 +142,7 @@ def create_single_node(client, **params):
:param client: ironic client instance.
:param params: dictionary to be POSTed to /nodes endpoint, excluding
"ports" key.
"ports" and "portgroups" keys.
:returns: UUID of the created node or None in case of exception, and an
exception, if it appears.
:raises: InvalidAttribute, if some parameters passed to client's
@ -150,6 +150,7 @@ def create_single_node(client, **params):
:raises: ClientException, if the creation of the node fails.
"""
params.pop('ports', None)
params.pop('portgroups', None)
ret = client.node.create(**params)
return ret.uuid
@ -170,6 +171,24 @@ def create_single_port(client, **params):
return ret.uuid
@create_single_handler('port group')
def create_single_portgroup(client, **params):
"""Call the client to create a port group.
:param client: ironic client instance.
:param params: dictionary to be POSTed to /portgroups endpoint, excluding
"ports" key.
:returns: UUID of the created port group or None in case of exception, and
an exception, if it appears.
:raises: InvalidAttribute, if some parameters passed to client's
create_method are invalid.
:raises: ClientException, if the creation of the portgroup fails.
"""
params.pop('ports', None)
ret = client.portgroup.create(**params)
return ret.uuid
@create_single_handler('chassis')
def create_single_chassis(client, **params):
"""Call the client to create a chassis.
@ -188,13 +207,15 @@ def create_single_chassis(client, **params):
return ret.uuid
def create_ports(client, port_list, node_uuid):
def create_ports(client, port_list, node_uuid, portgroup_uuid=None):
"""Create ports from dictionaries.
:param client: ironic client instance.
:param port_list: list of dictionaries to be POSTed to /ports
endpoint.
:param node_uuid: UUID of a node the ports should be associated with.
:param portgroup_uuid: UUID of a port group the ports should be associated
with, if they are its members.
:returns: array of exceptions encountered during creation.
"""
errors = []
@ -209,12 +230,57 @@ def create_ports(client, port_list, node_uuid):
'port': port}))
continue
port['node_uuid'] = node_uuid
if portgroup_uuid:
port_portgroup_uuid = port.get('portgroup_uuid')
if port_portgroup_uuid and port_portgroup_uuid != portgroup_uuid:
errors.append(exc.ClientException(
'Cannot create a port as part of port group '
'%(portgroup_uuid)s because the port %(port)s has a '
'different port group UUID specified.',
{'portgroup_uuid': portgroup_uuid,
'port': port}))
continue
port['portgroup_uuid'] = portgroup_uuid
port_uuid, error = create_single_port(client, **port)
if error:
errors.append(error)
return errors
def create_portgroups(client, portgroup_list, node_uuid):
"""Create port groups from dictionaries.
:param client: ironic client instance.
:param portgroup_list: list of dictionaries to be POSTed to /portgroups
endpoint, if some of them contain "ports" key, its content is POSTed
separately to /ports endpoint.
:param node_uuid: UUID of a node the port groups should be associated with.
:returns: array of exceptions encountered during creation.
"""
errors = []
for portgroup in portgroup_list:
portgroup_node_uuid = portgroup.get('node_uuid')
if portgroup_node_uuid and portgroup_node_uuid != node_uuid:
errors.append(exc.ClientException(
'Cannot create a port group as part of node %(node_uuid)s '
'because the port group %(portgroup)s has a different node '
'UUID specified.',
{'node_uuid': node_uuid,
'portgroup': portgroup}))
continue
portgroup['node_uuid'] = node_uuid
portgroup_uuid, error = create_single_portgroup(client, **portgroup)
if error:
errors.append(error)
ports = portgroup.get('ports')
# Port group UUID == None means that port group creation failed, don't
# create the ports inside it
if ports is not None and portgroup_uuid is not None:
errors.extend(create_ports(client, ports, node_uuid,
portgroup_uuid=portgroup_uuid))
return errors
def create_nodes(client, node_list, chassis_uuid=None):
"""Create nodes from dictionaries.
@ -242,10 +308,15 @@ def create_nodes(client, node_list, chassis_uuid=None):
if error:
errors.append(error)
ports = node.get('ports')
portgroups = node.get('portgroups')
# Node UUID == None means that node creation failed, don't
# create the ports inside it
if ports is not None and node_uuid is not None:
errors.extend(create_ports(client, ports, node_uuid=node_uuid))
# create the port(group)s inside it
if node_uuid is not None:
if portgroups is not None:
errors.extend(
create_portgroups(client, portgroups, node_uuid))
if ports is not None:
errors.extend(create_ports(client, ports, node_uuid))
return errors

View File

@ -18,7 +18,7 @@ from ironicclient.v1 import create_resources
help='File (.yaml or .json) containing descriptions of the '
'resources to create. Can be specified multiple times.')
def do_create(cc, args):
"""Create baremetal resources (chassis, nodes, and ports).
"""Create baremetal resources (chassis, nodes, port groups and ports).
The resources may be described in one or more JSON or YAML files. If any
file cannot be validated, no resources are created. An attempt is made to

View File

@ -0,0 +1,6 @@
---
features:
- |
Supports creation of port groups via ``ironic create`` and
``openstack baremetal create`` commands.