diff options
-rw-r--r-- | doc/source/create_command.rst | 43 | ||||
-rw-r--r-- | ironicclient/tests/unit/v1/test_create_resources.py | 121 | ||||
-rw-r--r-- | ironicclient/v1/create_resources.py | 81 | ||||
-rw-r--r-- | ironicclient/v1/create_resources_shell.py | 2 | ||||
-rw-r--r-- | releasenotes/notes/add-portgroups-to-create-command-6d685277f7af79df.yaml | 6 |
5 files changed, 227 insertions, 26 deletions
diff --git a/doc/source/create_command.rst b/doc/source/create_command.rst index 15723c1..ec966c9 100644 --- a/doc/source/create_command.rst +++ b/doc/source/create_command.rst @@ -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, diff --git a/ironicclient/tests/unit/v1/test_create_resources.py b/ironicclient/tests/unit/v1/test_create_resources.py index 7bd92d9..4b0cf9c 100644 --- a/ironicclient/tests/unit/v1/test_create_resources.py +++ b/ironicclient/tests/unit/v1/test_create_resources.py @@ -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) diff --git a/ironicclient/v1/create_resources.py b/ironicclient/v1/create_resources.py index 348abb8..746871b 100644 --- a/ironicclient/v1/create_resources.py +++ b/ironicclient/v1/create_resources.py @@ -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 diff --git a/ironicclient/v1/create_resources_shell.py b/ironicclient/v1/create_resources_shell.py index cd46aa7..03c8628 100644 --- a/ironicclient/v1/create_resources_shell.py +++ b/ironicclient/v1/create_resources_shell.py @@ -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 diff --git a/releasenotes/notes/add-portgroups-to-create-command-6d685277f7af79df.yaml b/releasenotes/notes/add-portgroups-to-create-command-6d685277f7af79df.yaml new file mode 100644 index 0000000..8782c89 --- /dev/null +++ b/releasenotes/notes/add-portgroups-to-create-command-6d685277f7af79df.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Supports creation of port groups via ``ironic create`` and + ``openstack baremetal create`` commands. + |