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:
parent
1f586f1977
commit
d9f595bd82
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Supports creation of port groups via ``ironic create`` and
|
||||
``openstack baremetal create`` commands.
|
||||
|
Loading…
Reference in New Issue