diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 942f43cad..f9270766f 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.36") +API_MAX_VERSION = api_versions.APIVersion("2.37") diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 6d03b7914..3d8c5128e 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -172,3 +172,59 @@ class TestServersTagsV226(base.ClientTestBase): uuid = self._boot_server_with_tags() self.nova("server-tag-delete-all %s" % uuid) self.assertEqual([], self.client.servers.tag_list(uuid)) + + +class TestServersAutoAllocateNetworkCLI(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.37' + + def _find_network_in_table(self, table): + # Example: + # +-----------------+-----------------------------------+ + # | Property | Value | + # +-----------------+-----------------------------------+ + # | private network | 192.168.154.128 | + # +-----------------+-----------------------------------+ + for line in table.split('\n'): + if '|' in line: + l_property, l_value = line.split('|')[1:3] + if ' network' in l_property.strip(): + return ' '.join(l_property.strip().split()[:-1]) + + def test_boot_server_with_auto_network(self): + """Tests that the CLI defaults to 'auto' when --nic isn't specified. + """ + server_info = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--image %(image)s ' % {'name': self.name_generate('server'), + 'flavor': self.flavor.id, + 'image': self.image.id})) + server_id = self._get_value_from_the_table(server_info, 'id') + self.addCleanup(self.wait_for_resource_delete, + server_id, self.client.servers) + self.addCleanup(self.client.servers.delete, server_id) + # get the server details to verify there is a network, we don't care + # what the network name is, we just want to see an entry show up + server_info = self.nova('show', params=server_id) + network = self._find_network_in_table(server_info) + self.assertIsNotNone( + network, 'Auto-allocated network not found: %s' % server_info) + + def test_boot_server_with_no_network(self): + """Tests that '--nic none' is honored. + """ + server_info = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--image %(image)s --nic none' % + {'name': self.name_generate('server'), + 'flavor': self.flavor.id, + 'image': self.image.id})) + server_id = self._get_value_from_the_table(server_info, 'id') + self.addCleanup(self.wait_for_resource_delete, + server_id, self.client.servers) + self.addCleanup(self.client.servers.delete, server_id) + # get the server details to verify there is not a network + server_info = self.nova('show', params=server_id) + network = self._find_network_in_table(server_info) + self.assertIsNone( + network, 'Unexpected network allocation: %s' % server_info) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 06d993092..2e057b03a 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -42,6 +42,11 @@ class ServersTest(utils.FixturedTestCase): if self.api_version: self.cs.api_version = api_versions.APIVersion(self.api_version) + def _get_server_create_default_nics(self): + """Callback for default nics kwarg when creating a server. + """ + return None + def test_list_servers(self): sl = self.cs.servers.list() self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) @@ -132,7 +137,8 @@ class ServersTest(utils.FixturedTestCase): files={ '/etc/passwd': 'some data', # a file '/tmp/foo.txt': six.StringIO('data'), # a stream - } + }, + nics=self._get_server_create_default_nics() ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -191,7 +197,8 @@ class ServersTest(utils.FixturedTestCase): meta={'foo': 'bar'}, userdata="hello moto", key_name="fakekey", - block_device_mapping_v2=bdm + block_device_mapping_v2=bdm, + nics=self._get_server_create_default_nics() ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-volumes_boot') @@ -239,7 +246,8 @@ class ServersTest(utils.FixturedTestCase): userdata="hello moto", key_name="fakekey", access_ip_v6=access_ip_v6, - access_ip_v4=access_ip_v4 + access_ip_v4=access_ip_v4, + nics=self._get_server_create_default_nics() ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -256,6 +264,7 @@ class ServersTest(utils.FixturedTestCase): '/etc/passwd': 'some data', # a file '/tmp/foo.txt': six.StringIO('data'), # a stream }, + nics=self._get_server_create_default_nics(), ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -273,6 +282,7 @@ class ServersTest(utils.FixturedTestCase): '/etc/passwd': 'some data', # a file '/tmp/foo.txt': six.StringIO('data'), # a stream }, + nics=self._get_server_create_default_nics(), ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -290,6 +300,7 @@ class ServersTest(utils.FixturedTestCase): '/etc/passwd': 'some data', # a file '/tmp/foo.txt': six.StringIO('data'), # a stream }, + nics=self._get_server_create_default_nics(), ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -303,7 +314,8 @@ class ServersTest(utils.FixturedTestCase): image=1, flavor=1, admin_pass=test_password, - key_name=test_key + key_name=test_key, + nics=self._get_server_create_default_nics() ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -328,6 +340,7 @@ class ServersTest(utils.FixturedTestCase): '/etc/passwd': 'some data', # a file '/tmp/foo.txt': six.StringIO('data'), # a stream }, + nics=self._get_server_create_default_nics(), ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -343,7 +356,8 @@ class ServersTest(utils.FixturedTestCase): name="My server", image=1, flavor=1, - disk_config=disk_config + disk_config=disk_config, + nics=self._get_server_create_default_nics(), ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -976,6 +990,16 @@ class ServersTest(utils.FixturedTestCase): key_name="fakekey" ) + def test_create_server_with_nics_auto(self): + """Negative test for specifying nics='auto' before 2.37 + """ + self.assertRaises(ValueError, + self.cs.servers.create, + name='test', + image='d9d8d53c-4b4a-4144-a5e5-b30d9f1fe46a', + flavor='1', + nics='auto') + class ServersV26Test(ServersTest): @@ -1074,7 +1098,8 @@ class ServersV219Test(ServersV217Test): flavor=1, meta={'foo': 'bar'}, userdata="hello moto", - key_name="fakekey" + key_name="fakekey", + nics=self._get_server_create_default_nics() ) self.assert_called('POST', '/servers') @@ -1260,3 +1285,39 @@ class ServersV232Test(ServersV226Test): image=1, flavor=1, meta={'foo': 'bar'}, userdata="hello moto", key_name="fakekey", block_device_mapping_v2=bdm) + + +class ServersV2_37Test(ServersV226Test): + + api_version = "2.37" + + def _get_server_create_default_nics(self): + return 'auto' + + def test_create_server_no_nics(self): + """Tests that nics are required in microversion 2.37+ + """ + self.assertRaises(ValueError, self.cs.servers.create, + name='test', + image='d9d8d53c-4b4a-4144-a5e5-b30d9f1fe46a', + flavor='1') + + def test_create_server_with_nics_auto(self): + s = self.cs.servers.create( + name='test', image='d9d8d53c-4b4a-4144-a5e5-b30d9f1fe46a', + flavor='1', nics=self._get_server_create_default_nics()) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + def test_add_floating_ip(self): + # self.cs.floating_ips.list() is not available after 2.35 + pass + + def test_add_floating_ip_to_fixed(self): + # self.cs.floating_ips.list() is not available after 2.35 + pass + + def test_remove_floating_ip(self): + # self.cs.floating_ips.list() is not available after 2.35 + pass diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index db8f1abf4..34eec493e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -606,6 +606,64 @@ class ShellTest(utils.TestCase): cmd, api_version='2.32') self.assertIn('tag=tag', six.text_type(ex)) + def test_boot_invalid_nics_v2_36_auto(self): + """This is a negative test to make sure we fail with the correct + message. --nic auto isn't allowed before 2.37. + """ + cmd = ('boot --image %s --flavor 1 --nic auto test' % FAKE_UUID_1) + ex = self.assertRaises(exceptions.CommandError, self.run_command, + cmd, api_version='2.36') + self.assertNotIn('auto,none', six.text_type(ex)) + + def test_boot_invalid_nics_v2_37(self): + """This is a negative test to make sure we fail with the correct + message. + """ + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=1 --nic auto some-server' % FAKE_UUID_1) + ex = self.assertRaises(exceptions.CommandError, self.run_command, + cmd, api_version='2.37') + self.assertIn('auto,none', six.text_type(ex)) + + def test_boot_nics_auto_allocate_default(self): + """Tests that if microversion>=2.37 is specified and no --nics are + specified that a single --nic with net-id=auto is used. + """ + cmd = 'boot --image %s --flavor 1 some-server' % FAKE_UUID_1 + self.run_command(cmd, api_version='2.37') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + }, + }, + ) + + def test_boot_nics_auto_allocate_none(self): + """Tests specifying '--nic none' with microversion 2.37 + """ + cmd = 'boot --image %s --flavor 1 --nic none some-server' % FAKE_UUID_1 + self.run_command(cmd, api_version='2.37') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'none', + }, + }, + ) + def test_boot_nics_ipv6(self): cmd = ('boot --image %s --flavor 1 ' '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server' % @@ -3062,6 +3120,7 @@ class ShellTest(utils.TestCase): 32, # doesn't require separate version-wrapped methods in # novaclient 34, # doesn't require any changes in novaclient + 37, # There are no versioned wrapped shell method changes for this ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index ebeaaee30..e24fa43a5 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -709,27 +709,32 @@ class ServerManager(base.BootingManagerWithFind): body['server']['block_device_mapping_v2'] = block_device_mapping_v2 if nics is not None: - # NOTE(tr3buchet): nics can be an empty list - all_net_data = [] - for nic_info in nics: - net_data = {} - # if value is empty string, do not send value in body - if nic_info.get('net-id'): - net_data['uuid'] = nic_info['net-id'] - if (nic_info.get('v4-fixed-ip') and - nic_info.get('v6-fixed-ip')): - raise base.exceptions.CommandError(_( - "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be" - " provided.")) - elif nic_info.get('v4-fixed-ip'): - net_data['fixed_ip'] = nic_info['v4-fixed-ip'] - elif nic_info.get('v6-fixed-ip'): - net_data['fixed_ip'] = nic_info['v6-fixed-ip'] - if nic_info.get('port-id'): - net_data['port'] = nic_info['port-id'] - if nic_info.get('tag'): - net_data['tag'] = nic_info['tag'] - all_net_data.append(net_data) + # With microversion 2.37+ nics can be an enum of 'auto' or 'none' + # or a list of dicts. + if isinstance(nics, six.string_types): + all_net_data = nics + else: + # NOTE(tr3buchet): nics can be an empty list + all_net_data = [] + for nic_info in nics: + net_data = {} + # if value is empty string, do not send value in body + if nic_info.get('net-id'): + net_data['uuid'] = nic_info['net-id'] + if (nic_info.get('v4-fixed-ip') and + nic_info.get('v6-fixed-ip')): + raise base.exceptions.CommandError(_( + "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' " + "may be provided.")) + elif nic_info.get('v4-fixed-ip'): + net_data['fixed_ip'] = nic_info['v4-fixed-ip'] + elif nic_info.get('v6-fixed-ip'): + net_data['fixed_ip'] = nic_info['v6-fixed-ip'] + if nic_info.get('port-id'): + net_data['port'] = nic_info['port-id'] + if nic_info.get('tag'): + net_data['tag'] = nic_info['tag'] + all_net_data.append(net_data) body['server']['networks'] = all_net_data if disk_config is not None: @@ -1219,6 +1224,14 @@ class ServerManager(base.BootingManagerWithFind): base.getid(server)) return base.TupleWithMeta((resp, body), resp) + def _validate_create_nics(self, nics): + # nics are required with microversion 2.37+ and can be a string or list + if self.api_version > api_versions.APIVersion('2.36'): + if not nics: + raise ValueError('nics are required after microversion 2.36') + elif nics and not isinstance(nics, list): + raise ValueError('nics must be a list') + def create(self, name, image, flavor, meta=None, files=None, reservation_id=None, min_count=None, max_count=None, security_groups=None, userdata=None, @@ -1259,9 +1272,17 @@ class ServerManager(base.BootingManagerWithFind): device mappings for this server. :param block_device_mapping_v2: (optional extension) A dict of block device mappings for this server. - :param nics: (optional extension) an ordered list of nics to be - added to this server, with information about - connected networks, fixed IPs, port etc. + :param nics: An ordered list of nics (dicts) to be added to this + server, with information about connected networks, + fixed IPs, port etc. + Beginning in microversion 2.37 this field is required and + also supports a single string value of 'auto' or 'none'. + The 'auto' value means the Compute service will + automatically allocate a network for the project if one + is not available. This is the same behavior as not + passing anything for nics before microversion 2.37. The + 'none' value tells the Compute service to not allocate + any networking for the server. :param scheduler_hints: (optional extension) arbitrary key-value pairs specified by the client to help boot an instance :param config_drive: (optional extension) value for config drive @@ -1289,6 +1310,8 @@ class ServerManager(base.BootingManagerWithFind): if "description" in kwargs and self.api_version < descr_microversion: raise exceptions.UnsupportedAttribute("description", "2.19") + self._validate_create_nics(nics) + tags_microversion = api_versions.APIVersion("2.32") if self.api_version < tags_microversion: if nics: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index b49656a55..a823cd4a9 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -188,7 +188,16 @@ def _parse_block_device_mapping_v2(args, image): def _parse_nics(cs, args): - if cs.api_version >= api_versions.APIVersion('2.32'): + supports_auto_alloc = cs.api_version >= api_versions.APIVersion('2.37') + if supports_auto_alloc: + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , " + "with only one of net-id, net-name or port-id " + "specified. Specifying a --nic of auto or none cannot " + "be used with any other --nic value.")) + elif cs.api_version >= api_versions.APIVersion('2.32'): err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " "the form --nic , " "with only one of net-id, net-name or port-id " "specified.")) + auto_or_none = False nics = [] for nic_str in args.nics: nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", @@ -209,6 +219,13 @@ def _parse_nics(cs, args): for kv_str in nic_str.split(","): try: + # handle the special auto/none cases + if kv_str in ('auto', 'none'): + if not supports_auto_alloc: + raise exceptions.CommandError(err_msg % nic_str) + nics.append(kv_str) + auto_or_none = True + continue k, v = kv_str.split("=", 1) except ValueError: raise exceptions.CommandError(err_msg % nic_str) @@ -225,6 +242,9 @@ def _parse_nics(cs, args): else: raise exceptions.CommandError(err_msg % nic_str) + if auto_or_none: + continue + if nic_info['v4-fixed-ip'] and not netutils.is_valid_ipv4( nic_info['v4-fixed-ip']): raise exceptions.CommandError(_("Invalid ipv4 address.")) @@ -238,6 +258,17 @@ def _parse_nics(cs, args): nics.append(nic_info) + if nics: + if auto_or_none: + if len(nics) > 1: + raise exceptions.CommandError(err_msg % nic_str) + # change the single list entry to a string + nics = nics[0] + else: + # Default to 'auto' if API version >= 2.37 and nothing was specified + if supports_auto_alloc: + nics = 'auto' + return nics @@ -577,6 +608,7 @@ def _boot(cs, args): dest='nics', default=[], start_version='2.32', + end_version='2.36', help=_("Create a NIC on the server. " "Specify option multiple times to create multiple nics. " "net-id: attach NIC to network with this UUID " @@ -587,6 +619,31 @@ def _boot(cs, args): "port-id: attach NIC to port with this UUID " "tag: interface metadata tag (optional) " "(either port-id or net-id must be provided).")) +@utils.arg( + '--nic', + metavar="", + action='append', + dest='nics', + default=[], + start_version='2.37', + help=_("Create a NIC on the server. " + "Specify option multiple times to create multiple nics unless " + "using the special 'auto' or 'none' values. " + "auto: automatically allocate network resources if none are " + "available. This cannot be specified with any other nic value and " + "cannot be specified multiple times. " + "none: do not attach a NIC at all. This cannot be specified " + "with any other nic value and cannot be specified multiple times. " + "net-id: attach NIC to network with a specific UUID. " + "net-name: attach NIC to network with this name " + "(either port-id or net-id or net-name must be provided), " + "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " + "port-id: attach NIC to port with this UUID " + "tag: interface metadata tag (optional) " + "(either port-id or net-id must be provided).")) @utils.arg( '--config-drive', metavar="", diff --git a/releasenotes/notes/microversion-2.37-d03da96406a45e67.yaml b/releasenotes/notes/microversion-2.37-d03da96406a45e67.yaml new file mode 100644 index 000000000..211a4f9ce --- /dev/null +++ b/releasenotes/notes/microversion-2.37-d03da96406a45e67.yaml @@ -0,0 +1,27 @@ +--- +features: + - | + The 2.37 microversion is now supported. This introduces the following + changes: + + * CLI: The **--nic** value for the **nova boot** command now takes two + special values, 'auto' and 'none'. If --nic is not specified, the + CLI defaults to 'auto'. + * Python API: The **nics** kwarg is required when creating a server using + the *novaclient.v2.servers.ServerManager.create* API. The **nics** + value can be a list of dicts or a string with value 'auto' or + 'none'. + +upgrade: + - | + With the 2.37 microversion, the **nics** kwarg is required when creating + a server using the *novaclient.v2.servers.ServerManager.create* API. The + **nics** value can be a list of dicts or an enum string with one of the + following values: + + * **auto**: This tells the Compute service to automatically allocate a + network for the project if one is not available and then associate + an IP from that network with the server. This is the same behavior as + passing nics=None before the 2.37 microversion. + * **none**: This tells the Compute service to not allocate any networking + for the server.