From de22cdd3c84e03ed76a261fa41c4ed2d829c8d65 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 13 Feb 2019 18:29:39 -0500 Subject: [PATCH] Add support for microversion 2.70 - expose device tags This adds support for microversion 2.70 which exposes the 'tag' field in the following APIs: * GET /servers/{server_id}/os-volume_attachments * GET /servers/{server_id}/os-volume_attachments/{volume_id} * POST /servers/{server_id}/os-volume_attachments * GET /servers/{server_id}/os-interface * GET /servers/{server_id}/os-interface/{port_id} * POST /servers/{server_id}/os-interface Which corresponds to showing the tag in the output of the following commands: * nova volume-attachments * nova volume-attach * nova interface-list * nova interface-attach Depends-On: https://review.openstack.org/631948/ Part of blueprint expose-virtual-device-tags-in-rest-api Change-Id: I5e9d7e0219605503a56d2cf745b95c6e05d01101 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 38 ++++++++++---- novaclient/tests/unit/v2/test_shell.py | 52 ++++++++++++++++--- novaclient/v2/shell.py | 14 +++-- .../microversion_v2_70-09cbe0933b3a9335.yaml | 12 +++++ 5 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/microversion_v2_70-09cbe0933b3a9335.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index a41a77af3..bb0e53d1d 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1") # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.69") +API_MAX_VERSION = api_versions.APIVersion("2.70") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index a1e298e7d..0f1857ff3 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2004,7 +2004,7 @@ class FakeSessionClient(base_client.SessionClient): "hosts": None}]}) def get_servers_1234_os_interface(self, **kw): - return (200, {}, { + attachments = { "interfaceAttachments": [ {"port_state": "ACTIVE", "net_id": "net-id-1", @@ -2017,27 +2017,38 @@ class FakeSessionClient(base_client.SessionClient): "port_id": "port-id-1", "mac_address": "aa:bb:cc:dd:ee:ff", "fixed_ips": [{"ip_address": "1.2.3.4"}], - }] - }) + } + ] + } + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in each attachment. + for attachment in attachments['interfaceAttachments']: + attachment['tag'] = 'test-tag' + return (200, {}, attachments) def post_servers_1234_os_interface(self, **kw): - return (200, {}, {'interfaceAttachment': {}}) + attachment = {} + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in the response. + attachment['tag'] = 'test-tag' + return (200, {}, {'interfaceAttachment': attachment}) def delete_servers_1234_os_interface_port_id(self, **kw): return (200, {}, None) def post_servers_1234_os_volume_attachments(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { - "volumeAttachment": - {"device": "/dev/vdb", - "volumeId": 2}}) + attachment = {"device": "/dev/vdb", "volumeId": 2} + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in the response. + attachment['tag'] = 'test-tag' + return (200, FAKE_RESPONSE_HEADERS, {"volumeAttachment": attachment}) def put_servers_1234_os_volume_attachments_Work(self, **kw): return (200, FAKE_RESPONSE_HEADERS, {"volumeAttachment": {"volumeId": 2}}) def get_servers_1234_os_volume_attachments(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { + attachments = { "volumeAttachments": [ {"display_name": "Work", "display_description": "volume for work", @@ -2047,7 +2058,14 @@ class FakeSessionClient(base_client.SessionClient): "attached": "2011-11-11T00:00:00Z", "size": 1024, "attachments": [{"id": "3333", "links": ''}], - "metadata": {}}]}) + "metadata": {}} + ] + } + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in each attachment. + for attachment in attachments['volumeAttachments']: + attachment['tag'] = 'test-tag' + return (200, FAKE_RESPONSE_HEADERS, attachments) def get_servers_1234_os_volume_attachments_Work(self, **kw): return (200, FAKE_RESPONSE_HEADERS, { diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index eecd9315e..778c74ac8 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3518,8 +3518,14 @@ class ShellTest(utils.TestCase): self.assert_called('GET', '/servers/1234/os-security-groups') def test_interface_list(self): - self.run_command('interface-list 1234') + out = self.run_command('interface-list 1234')[0] self.assert_called('GET', '/servers/1234/os-interface') + self.assertNotIn('Tag', out) + + def test_interface_list_v2_70(self): + out = self.run_command('interface-list 1234', api_version='2.70')[0] + self.assert_called('GET', '/servers/1234/os-interface') + self.assertIn('test-tag', out) def test_interface_attach(self): self.run_command('interface-attach --port-id port_id 1234') @@ -3533,20 +3539,37 @@ class ShellTest(utils.TestCase): api_version='2.48') def test_interface_attach_with_tag(self): - self.run_command( - 'interface-attach --port-id port_id --tag test_tag 1234', - api_version='2.49') + out = self.run_command( + 'interface-attach --port-id port_id --tag test-tag 1234', + api_version='2.49')[0] self.assert_called('POST', '/servers/1234/os-interface', {'interfaceAttachment': {'port_id': 'port_id', - 'tag': 'test_tag'}}) + 'tag': 'test-tag'}}) + self.assertNotIn('test-tag', out) + + def test_interface_attach_v2_70(self): + out = self.run_command( + 'interface-attach --port-id port_id --tag test-tag 1234', + api_version='2.70')[0] + self.assert_called('POST', '/servers/1234/os-interface', + {'interfaceAttachment': {'port_id': 'port_id', + 'tag': 'test-tag'}}) + self.assertIn('test-tag', out) def test_interface_detach(self): self.run_command('interface-detach 1234 port_id') self.assert_called('DELETE', '/servers/1234/os-interface/port_id') def test_volume_attachments(self): - self.run_command('volume-attachments 1234') + out = self.run_command('volume-attachments 1234')[0] self.assert_called('GET', '/servers/1234/os-volume_attachments') + self.assertNotIn('test-tag', out) + + def test_volume_attachments_v2_70(self): + out = self.run_command( + 'volume-attachments 1234', api_version='2.70')[0] + self.assert_called('GET', '/servers/1234/os-volume_attachments') + self.assertIn('test-tag', out) def test_volume_attach(self): self.run_command('volume-attach sample-server Work /dev/vdb') @@ -3568,14 +3591,26 @@ class ShellTest(utils.TestCase): api_version='2.48') def test_volume_attach_with_tag(self): - self.run_command( + out = self.run_command( 'volume-attach --tag test_tag sample-server Work /dev/vdb', - api_version='2.49') + api_version='2.49')[0] self.assert_called('POST', '/servers/1234/os-volume_attachments', {'volumeAttachment': {'device': '/dev/vdb', 'volumeId': 'Work', 'tag': 'test_tag'}}) + self.assertNotIn('test-tag', out) + + def test_volume_attach_with_tag_v2_70(self): + out = self.run_command( + 'volume-attach --tag test-tag sample-server Work /dev/vdb', + api_version='2.70')[0] + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'device': '/dev/vdb', + 'volumeId': 'Work', + 'tag': 'test-tag'}}) + self.assertIn('test-tag', out) def test_volume_update(self): self.run_command('volume-update sample-server Work Work') @@ -4045,6 +4080,7 @@ class ShellTest(utils.TestCase): # cell, they will be handled on the client side by being # skipped when forming the detailed lists for embedded # flavor information. + 70, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index cb0b1c4a2..4f65237d2 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2617,7 +2617,11 @@ def do_volume_attachments(cs, args): """List all the volumes attached to a server.""" volumes = cs.volumes.get_server_volumes(_find_server(cs, args.server).id) _translate_volume_attachments_keys(volumes) - utils.print_list(volumes, ['ID', 'DEVICE', 'SERVER ID', 'VOLUME ID']) + # Microversion >= 2.70 returns the tag value. + fields = ['ID', 'DEVICE', 'SERVER ID', 'VOLUME ID'] + if cs.api_version >= api_versions.APIVersion('2.70'): + fields.append('TAG') + utils.print_list(volumes, fields) @api_versions.wraps('2.0', '2.5') @@ -4497,9 +4501,11 @@ def do_evacuate(cs, args): utils.print_dict(res) -def _print_interfaces(interfaces): +def _print_interfaces(interfaces, show_tag=False): columns = ['Port State', 'Port ID', 'Net ID', 'IP addresses', 'MAC Addr'] + if show_tag: + columns.append('Tag') class FormattedInterface(object): def __init__(self, interface): @@ -4519,7 +4525,9 @@ def do_interface_list(cs, args): res = server.interface_list() if isinstance(res, list): - _print_interfaces(res) + # The "tag" field is in the response starting with microversion 2.70. + show_tag = cs.api_version >= api_versions.APIVersion('2.70') + _print_interfaces(res, show_tag=show_tag) @utils.arg('server', metavar='', help=_('Name or ID of server.')) diff --git a/releasenotes/notes/microversion_v2_70-09cbe0933b3a9335.yaml b/releasenotes/notes/microversion_v2_70-09cbe0933b3a9335.yaml new file mode 100644 index 000000000..93dee6e2d --- /dev/null +++ b/releasenotes/notes/microversion_v2_70-09cbe0933b3a9335.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Added support for `microversion 2.70`_ which outputs the `Tag` field in + the following commands: + + * ``nova interface-list`` + * ``nova interface-attach`` + * ``nova volume-attachments`` + * ``nova volume-attach`` + + .. _microversion 2.70: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id63