Add support for v2.37 and auto-allocated networking

This adds support for the v2.37 microversion. The networks
part of the server create request is required in this
microversion so if nothing is specified for --nic arguments
on the command line we default to 'auto' for backward
compatibility in the CLI.

Part of blueprint get-me-a-network

Change-Id: I6636ddcd3be7bf393d2d69cc6c1ba5c7d65ff674
This commit is contained in:
Matt Riedemann 2016-08-09 13:08:09 -04:00
parent 84d86d3f3c
commit 030ce53d4e
7 changed files with 315 additions and 32 deletions

View File

@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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))

View File

@ -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:

View File

@ -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 <auto,none,net-id=net-uuid,"
"net-name=network-name,v4-fixed-ip=ip-addr,"
"v6-fixed-ip=ip-addr,port-id=port-uuid,tag=tag>, "
"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 <net-id=net-uuid,"
"net-name=network-name,v4-fixed-ip=ip-addr,"
@ -202,6 +211,7 @@ def _parse_nics(cs, args):
"v6-fixed-ip=ip-addr,port-id=port-uuid>, "
"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="<auto,none,"
"net-id=net-uuid,net-name=network-name,port-id=port-uuid,"
"v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr,tag=tag>",
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="<value>",

View File

@ -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.