Supports string values 'none' and 'auto' for networks

Supports to specify string values 'none' and 'auto' for
networks while creating nova server. auto: automatically
allocate network resources if none are available;
none: do not attach a NIC at all. Nova supports these
string values since api version '2.37'.

Change-Id: If65f193dfdaedec3e549a8bd7b39e5f1c02be658
Blueprint: support-auto-none-special-network
Closes-Bug: #1646360
This commit is contained in:
huangtianhua 2016-12-06 10:27:36 +08:00
parent a13ba93777
commit d5f2944f4c
5 changed files with 349 additions and 60 deletions

View File

@ -530,3 +530,7 @@ class InvalidServiceVersion(HeatException):
class InvalidTemplateVersions(HeatException):
msg_fmt = _('A template version alias %(version)s was added for a '
'template class that has no official YYYY-MM-DD version.')
class UnableToAutoAllocateNetwork(HeatException):
msg_fmt = _('Unable to automatically allocate a network: %(message)s')

View File

@ -59,9 +59,9 @@ class NovaClientPlugin(client_plugin.ClientPlugin):
NOVA_API_VERSION = '2.1'
validate_versions = [
V2_2, V2_8, V2_10, V2_15, V2_26
V2_2, V2_8, V2_10, V2_15, V2_26, V2_37
] = [
'2.2', '2.8', '2.10', '2.15', '2.26'
'2.2', '2.8', '2.10', '2.15', '2.26', '2.37'
]
supported_versions = [NOVA_API_VERSION] + validate_versions

View File

@ -109,10 +109,12 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin,
_NETWORK_KEYS = (
NETWORK_UUID, NETWORK_ID, NETWORK_FIXED_IP, NETWORK_PORT,
NETWORK_SUBNET, NETWORK_PORT_EXTRA, NETWORK_FLOATING_IP
NETWORK_SUBNET, NETWORK_PORT_EXTRA, NETWORK_FLOATING_IP,
ALLOCATE_NETWORK,
) = (
'uuid', 'network', 'fixed_ip', 'port',
'subnet', 'port_extra_properties', 'floating_ip'
'subnet', 'port_extra_properties', 'floating_ip',
'allocate_network',
)
_SOFTWARE_CONFIG_FORMATS = (
@ -127,6 +129,12 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin,
'POLL_SERVER_CFN', 'POLL_SERVER_HEAT', 'POLL_TEMP_URL', 'ZAQAR_MESSAGE'
)
_ALLOCATE_TYPES = (
NETWORK_NONE, NETWORK_AUTO,
) = (
'none', 'auto',
)
ATTRIBUTES = (
NAME_ATTR, ADDRESSES, NETWORKS_ATTR, FIRST_ADDRESS,
INSTANCE_NAME, ACCESSIPV4, ACCESSIPV6, CONSOLE_URLS, TAGS_ATTR
@ -405,6 +413,23 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin,
constraints.CustomConstraint('neutron.network')
]
),
ALLOCATE_NETWORK: properties.Schema(
properties.Schema.STRING,
_('The special string values of network, '
'auto: means either a network that is already '
'available to the project will be used, or if one '
'does not exist, will be automatically created for '
'the project; none: means no networking will be '
'allocated for the created server. Supported by '
'Nova API since version "2.37". This property can '
'not be used with other network keys.'),
support_status=support.SupportStatus(version='9.0.0'),
constraints=[
constraints.AllowedValues(
[NETWORK_NONE, NETWORK_AUTO])
],
update_allowed=True,
),
NETWORK_FIXED_IP: properties.Schema(
properties.Schema.STRING,
_('Fixed IP address to specify for the port '
@ -754,7 +779,15 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin,
server = None
try:
server = self.client().servers.create(
# if 'auto' or 'none' is specified, we get the string type
# nics after self._build_nics(), and the string network
# is supported since nova microversion 2.37
if isinstance(nics, six.string_types):
nc = self.client(version=self.client_plugin().V2_37)
else:
nc = self.client()
server = nc.servers.create(
name=self._server_name(),
image=image,
flavor=flavor,
@ -1370,16 +1403,38 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin,
if image:
self._validate_image_flavor(image, flavor)
# network properties 'uuid' and 'network' shouldn't be used
# both at once for all networks
networks = self.properties[self.NETWORKS] or []
# record if any networks include explicit ports
networks_with_port = False
for network in networks:
networks_with_port = (networks_with_port or
network.get(self.NETWORK_PORT) is not None)
self._validate_network(network)
has_str_net = self._str_network(networks) is not None
if has_str_net:
if len(networks) != 1:
msg = _('Property "%s" can not be specified if '
'multiple network interfaces set for '
'server.') % self.ALLOCATE_NETWORK
raise exception.StackValidationFailed(message=msg)
# Check if str_network is allowed to use
try:
self.client(
version=self.client_plugin().V2_37)
except exception.InvalidServiceVersion as ex:
msg = (_('Cannot use "%(prop)s" property - compute service '
'does not support the required api '
'microversion: %(ex)s')
% {'prop': self.ALLOCATE_NETWORK,
'ex': six.text_type(ex)})
raise exception.StackValidationFailed(message=msg)
# record if any networks include explicit ports
has_port = any(n[self.NETWORK_PORT] is not None for n in networks)
# if 'security_groups' present for the server and explicit 'port'
# in one or more entries in 'networks', raise validation error
if has_port and self.properties[self.SECURITY_GROUPS]:
raise exception.ResourcePropertyConflict(
self.SECURITY_GROUPS,
"/".join([self.NETWORKS, self.NETWORK_PORT]))
# Check if tags is allowed to use
if self.properties[self.TAGS]:
try:
@ -1396,13 +1451,6 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin,
if metadata or personality:
limits = self.client_plugin().absolute_limits()
# if 'security_groups' present for the server and explicit 'port'
# in one or more entries in 'networks', raise validation error
if networks_with_port and self.properties[self.SECURITY_GROUPS]:
raise exception.ResourcePropertyConflict(
self.SECURITY_GROUPS,
"/".join([self.NETWORKS, self.NETWORK_PORT]))
# verify that the number of metadata entries is not greater
# than the maximum number allowed in the provider's absolute
# limits

View File

@ -35,16 +35,28 @@ class ServerNetworkMixin(object):
subnet = network.get(self.NETWORK_SUBNET)
fixed_ip = network.get(self.NETWORK_FIXED_IP)
floating_ip = network.get(self.NETWORK_FLOATING_IP)
str_network = network.get(self.ALLOCATE_NETWORK)
if net_id is None and port is None and subnet is None:
msg = _('One of the properties "%(id)s", "%(port_id)s" '
'or "%(subnet)s" should be set for the '
if (net_id is None and
port is None and
subnet is None and
not str_network):
msg = _('One of the properties "%(id)s", "%(port_id)s", '
'"%(str_network)s" or "%(subnet)s" should be set for the '
'specified network of server "%(server)s".'
'') % dict(id=self.NETWORK_ID,
port_id=self.NETWORK_PORT,
subnet=self.NETWORK_SUBNET,
str_network=self.ALLOCATE_NETWORK,
server=self.name)
raise exception.StackValidationFailed(message=msg)
# can not specify str_network with other keys of networks
# at the same time
has_value_keys = [k for k, v in network.items() if v is not None]
if str_network and len(has_value_keys) != 1:
msg = _('Can not specify "%s" with other keys of networks '
'at the same time.') % self.ALLOCATE_NETWORK
raise exception.StackValidationFailed(message=msg)
if port is not None and not self.is_using_neutron():
msg = _('Property "%s" is supported only for '
@ -214,6 +226,11 @@ class ServerNetworkMixin(object):
def _build_nics(self, networks, security_groups=None):
if not networks:
return None
str_network = self._str_network(networks)
if str_network:
return str_network
nics = []
for idx, net in enumerate(networks):
@ -329,8 +346,42 @@ class ServerNetworkMixin(object):
if net is not None:
net['port'] = props['port']
def calculate_networks(self, old_nets, new_nets, ifaces,
security_groups=None):
def _get_available_networks(self):
# first we get the private networks owned by the tenant
search_opts = {'tenant_id': self.context.tenant_id, 'shared': False,
'admin_state_up': True, }
nc = self.client('neutron')
nets = nc.list_networks(**search_opts).get('networks', [])
# second we get the public shared networks
search_opts = {'shared': True}
nets += nc.list_networks(**search_opts).get('networks', [])
ids = [net['id'] for net in nets]
return ids
def _auto_allocate_network(self):
topology = self.client('neutron').get_auto_allocated_topology(
self.context.tenant_id)['auto_allocated_topology']
return topology['id']
def _calculate_using_str_network(self, ifaces, str_net):
add_nets = []
remove_ports = [iface.port_id for iface in ifaces or []]
if str_net == self.NETWORK_AUTO:
nets = self._get_available_networks()
if not nets:
nets = [self._auto_allocate_network()]
if len(nets) > 1:
msg = 'Multiple possible networks found.'
raise exception.UnableToAutoAllocateNetwork(message=msg)
add_nets.append({'port_id': None, 'net_id': nets[0], 'fip': None})
return remove_ports, add_nets
def _calculate_using_list_networks(self, old_nets, new_nets, ifaces,
security_groups):
remove_ports = []
add_nets = []
attach_first_free_port = False
@ -351,33 +402,38 @@ class ServerNetworkMixin(object):
# 3. detach unmatched networks, which were present in old_nets
# 4. attach unmatched networks, which were present in new_nets
else:
# remove not updated networks from old and new networks lists,
# also get list these networks
not_updated_nets = self._exclude_not_updated_networks(old_nets,
new_nets)
# if old net is string net, remove the interfaces
if self._str_network(old_nets):
remove_ports = [iface.port_id for iface in ifaces or []]
else:
# remove not updated networks from old and new networks lists,
# also get list these networks
not_updated_nets = self._exclude_not_updated_networks(
old_nets,
new_nets)
self.update_networks_matching_iface_port(
old_nets + not_updated_nets, ifaces)
self.update_networks_matching_iface_port(
old_nets + not_updated_nets, ifaces)
# according to nova interface-detach command detached port
# will be deleted
inter_port_data = self._data_get_ports()
inter_port_ids = [p['id'] for p in inter_port_data]
for net in old_nets:
port_id = net.get(self.NETWORK_PORT)
# we can't match the port for some user case, like:
# the internal port was detached in nova first, then
# user update template to detach this nic. The internal
# port will remains till we delete the server resource.
if port_id:
remove_ports.append(port_id)
if port_id in inter_port_ids:
# if we have internal port with such id, remove it
# instantly.
self._delete_internal_port(port_id)
if net.get(self.NETWORK_FLOATING_IP):
self._floating_ip_disassociate(
net.get(self.NETWORK_FLOATING_IP))
# according to nova interface-detach command detached port
# will be deleted
inter_port_data = self._data_get_ports()
inter_port_ids = [p['id'] for p in inter_port_data]
for net in old_nets:
port_id = net.get(self.NETWORK_PORT)
# we can't match the port for some user case, like:
# the internal port was detached in nova first, then
# user update template to detach this nic. The internal
# port will remains till we delete the server resource.
if port_id:
remove_ports.append(port_id)
if port_id in inter_port_ids:
# if we have internal port with such id, remove it
# instantly.
self._delete_internal_port(port_id)
if net.get(self.NETWORK_FLOATING_IP):
self._floating_ip_disassociate(
net.get(self.NETWORK_FLOATING_IP))
handler_kwargs = {'port_id': None, 'net_id': None, 'fip': None}
# if new_nets is None, we should attach first free port,
@ -415,6 +471,23 @@ class ServerNetworkMixin(object):
return remove_ports, add_nets
def _str_network(self, networks):
# if user specify 'allocate_network', return it
# otherwise we return None
for net in networks or []:
str_net = net.get(self.ALLOCATE_NETWORK)
if str_net:
return str_net
def calculate_networks(self, old_nets, new_nets, ifaces,
security_groups=None):
new_str_net = self._str_network(new_nets)
if new_str_net:
return self._calculate_using_str_network(ifaces, new_str_net)
else:
return self._calculate_using_list_networks(
old_nets, new_nets, ifaces, security_groups)
def update_floating_ip_association(self, floating_ip, flip_associate):
if self.is_using_neutron() and flip_associate.get('port_id'):
self._floating_ip_neutron_associate(floating_ip, flip_associate)

View File

@ -507,6 +507,31 @@ class ServersTest(common.HeatTestCase):
args, kwargs = mock_create.call_args
self.assertEqual({}, kwargs['meta'])
def test_server_create_with_str_network(self):
stack_name = 'server_with_str_network'
return_server = self.fc.servers.list()[1]
(tmpl, stack) = self._setup_test_stack(stack_name)
mock_nc = self.patchobject(nova.NovaClientPlugin, '_create',
return_value=self.fc)
self.patchobject(glance.GlanceClientPlugin, 'get_image',
return_value=self.mock_image)
self.patchobject(nova.NovaClientPlugin, 'get_flavor',
return_value=self.mock_flavor)
self.patchobject(neutron.NeutronClientPlugin,
'find_resourceid_by_name_or_id')
props = tmpl['Resources']['WebServer']['Properties']
props['networks'] = [{'allocate_network': 'none'}]
resource_defns = tmpl.resource_definitions(stack)
server = servers.Server('WebServer',
resource_defns['WebServer'], stack)
self.patchobject(server, 'store_external_ports')
create_mock = self.patchobject(self.fc.servers, 'create',
return_value=return_server)
scheduler.TaskRunner(server.create)()
mock_nc.assert_called_with(version='2.37')
self.assertEqual('none', create_mock.call_args[1]['nics'])
def test_server_create_with_image_id(self):
return_server = self.fc.servers.list()[1]
return_server.id = '5678'
@ -1299,8 +1324,9 @@ class ServersTest(common.HeatTestCase):
'find_resourceid_by_name_or_id')
ex = self.assertRaises(exception.StackValidationFailed,
server.validate)
self.assertIn(_('One of the properties "network", "port" or "subnet" '
'should be set for the specified network of '
self.assertIn(_('One of the properties "network", "port", '
'"allocate_network" or "subnet" should be set '
'for the specified network of '
'server "%s".') % server.name,
six.text_type(ex))
@ -1329,6 +1355,30 @@ class ServersTest(common.HeatTestCase):
'corresponding port can not be retrieved.'),
six.text_type(ex))
def test_server_validate_with_networks_str_net(self):
stack_name = 'srv_networks_str_nets'
(tmpl, stack) = self._setup_test_stack(stack_name)
# create a server with 'uuid' and 'network' properties
tmpl['Resources']['WebServer']['Properties']['networks'] = (
[{'network': '6b1688bb-18a0-4754-ab05-19daaedc5871',
'allocate_network': 'auto'}])
self.patchobject(nova.NovaClientPlugin, '_create',
return_value=self.fc)
resource_defns = tmpl.resource_definitions(stack)
server = servers.Server('server_validate_net_list_str',
resource_defns['WebServer'], stack)
self.patchobject(glance.GlanceClientPlugin, 'get_image',
return_value=self.mock_image)
self.patchobject(nova.NovaClientPlugin, 'get_flavor',
return_value=self.mock_flavor)
self.patchobject(neutron.NeutronClientPlugin,
'find_resourceid_by_name_or_id')
ex = self.assertRaises(exception.StackValidationFailed,
server.validate)
self.assertIn(_('Can not specify "allocate_network" with '
'other keys of networks at the same time.'),
six.text_type(ex))
def test_server_validate_port_fixed_ip(self):
stack_name = 'port_with_fixed_ip'
(tmpl, stack) = self._setup_test_stack(stack_name,
@ -3140,10 +3190,12 @@ class ServersTest(common.HeatTestCase):
def create_old_net(self, port=None, net=None,
ip=None, uuid=None, subnet=None,
port_extra_properties=None, floating_ip=None):
port_extra_properties=None, floating_ip=None,
str_network=None):
return {'port': port, 'network': net, 'fixed_ip': ip, 'uuid': uuid,
'subnet': subnet, 'floating_ip': floating_ip,
'port_extra_properties': port_extra_properties}
'port_extra_properties': port_extra_properties,
'allocate_network': str_network}
def create_fake_iface(self, port, net, ip):
class fake_interface(object):
@ -3213,7 +3265,8 @@ class ServersTest(common.HeatTestCase):
old_nets_copy = copy.deepcopy(old_nets)
for net in new_nets_copy:
for key in ('port', 'network', 'fixed_ip', 'uuid', 'subnet',
'port_extra_properties', 'floating_ip'):
'port_extra_properties', 'floating_ip',
'allocate_network'):
net.setdefault(key)
matched_nets = server._exclude_not_updated_networks(old_nets,
@ -3244,7 +3297,8 @@ class ServersTest(common.HeatTestCase):
old_nets_copy = copy.deepcopy(old_nets)
for net in new_nets_copy:
for key in ('port', 'network', 'fixed_ip', 'uuid', 'subnet',
'port_extra_properties', 'floating_ip'):
'port_extra_properties', 'floating_ip',
'allocate_network'):
net.setdefault(key)
matched_nets = server._exclude_not_updated_networks(old_nets, new_nets)
@ -3268,7 +3322,8 @@ class ServersTest(common.HeatTestCase):
'subnet': None,
'uuid': None,
'port_extra_properties': None,
'floating_ip': None}]
'floating_ip': None,
'allocate_network': None}]
new_nets_copy = copy.deepcopy(new_nets)
matched_nets = server._exclude_not_updated_networks(old_nets, new_nets)
@ -3311,35 +3366,40 @@ class ServersTest(common.HeatTestCase):
'subnet': None,
'floating_ip': None,
'port_extra_properties': None,
'uuid': None},
'uuid': None,
'allocate_network': None},
{'port': 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'network': 'gggggggg-1111-1111-1111-gggggggggggg',
'fixed_ip': '1.2.3.4',
'subnet': None,
'port_extra_properties': None,
'floating_ip': None,
'uuid': None},
'uuid': None,
'allocate_network': None},
{'port': 'cccccccc-cccc-cccc-cccc-cccccccccccc',
'network': 'gggggggg-1111-1111-1111-gggggggggggg',
'fixed_ip': None,
'subnet': None,
'port_extra_properties': None,
'floating_ip': None,
'uuid': None},
'uuid': None,
'allocate_network': None},
{'port': 'dddddddd-dddd-dddd-dddd-dddddddddddd',
'network': None,
'fixed_ip': None,
'subnet': None,
'port_extra_properties': None,
'floating_ip': None,
'uuid': None},
'uuid': None,
'allocate_network': None},
{'port': 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
'uuid': 'gggggggg-1111-1111-1111-gggggggggggg',
'fixed_ip': '5.6.7.8',
'subnet': None,
'port_extra_properties': None,
'floating_ip': None,
'network': None}]
'network': None,
'allocate_network': None}]
self.patchobject(neutron.NeutronClientPlugin,
'find_resourceid_by_name_or_id',
@ -3514,6 +3574,110 @@ class ServersTest(common.HeatTestCase):
self.assertEqual(1, mock_detach_check.call_count)
self.assertEqual(1, mock_attach_check.call_count)
def _test_server_update_to_auto(self, available_multi_nets=None):
multi_nets = available_multi_nets or []
return_server = self.fc.servers.list()[1]
return_server.id = '5678'
server = self._create_test_server(return_server, 'networks_update')
old_networks = [
{'port': '95e25541-d26a-478d-8f36-ae1c8f6b74dc'}]
before_props = self.server_props.copy()
before_props['networks'] = old_networks
update_props = self.server_props.copy()
update_props['networks'] = [{'allocate_network': 'auto'}]
update_template = server.t.freeze(properties=update_props)
server.t = server.t.freeze(properties=before_props)
self.patchobject(self.fc.servers, 'get', return_value=return_server)
poor_interfaces = [
self.create_fake_iface('95e25541-d26a-478d-8f36-ae1c8f6b74dc',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'11.12.13.14')
]
self.patchobject(return_server, 'interface_list',
return_value=poor_interfaces)
self.patchobject(server, '_get_available_networks',
return_value=multi_nets)
mock_detach = self.patchobject(return_server, 'interface_detach')
mock_attach = self.patchobject(return_server, 'interface_attach')
updater = scheduler.TaskRunner(server.update, update_template)
if not multi_nets:
self.patchobject(nova.NovaClientPlugin, 'check_interface_detach',
return_value=True)
self.patchobject(nova.NovaClientPlugin,
'check_interface_attach',
return_value=True)
auto_allocate_net = '9cfe6c74-c105-4906-9a1f-81d9064e9bca'
self.patchobject(server, '_auto_allocate_network',
return_value=[auto_allocate_net])
updater()
self.assertEqual((server.UPDATE, server.COMPLETE), server.state)
self.assertEqual(1, mock_detach.call_count)
self.assertEqual(1, mock_attach.call_count)
mock_attach.called_once_with(
{'port_id': None,
'net_id': auto_allocate_net,
'fip': None})
else:
self.assertRaises(exception.ResourceFailure, updater)
self.assertEqual(0, mock_detach.call_count)
self.assertEqual(0, mock_attach.call_count)
def test_server_update_str_networks_auto(self):
self._test_server_update_to_auto()
def test_server_update_str_networks_auto_multi_nets(self):
available_nets = ['net_1', 'net_2']
self._test_server_update_to_auto(available_nets)
def test_server_update_str_networks_none(self):
return_server = self.fc.servers.list()[1]
return_server.id = '5678'
server = self._create_test_server(return_server, 'networks_update')
old_networks = [
{'port': '95e25541-d26a-478d-8f36-ae1c8f6b74dc'},
{'port': '4121f61a-1b2e-4ab0-901e-eade9b1cb09d'},
{'network': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'fixed_ip': '31.32.33.34'}]
before_props = self.server_props.copy()
before_props['networks'] = old_networks
update_props = self.server_props.copy()
update_props['networks'] = [{'allocate_network': 'none'}]
update_template = server.t.freeze(properties=update_props)
server.t = server.t.freeze(properties=before_props)
self.patchobject(self.fc.servers, 'get', return_value=return_server)
port_interfaces = [
self.create_fake_iface('95e25541-d26a-478d-8f36-ae1c8f6b74dc',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'11.12.13.14'),
self.create_fake_iface('4121f61a-1b2e-4ab0-901e-eade9b1cb09d',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'21.22.23.24'),
self.create_fake_iface('0907fa82-a024-43c2-9fc5-efa1bccaa74a',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'31.32.33.34')
]
self.patchobject(return_server, 'interface_list',
return_value=port_interfaces)
mock_detach = self.patchobject(return_server, 'interface_detach')
self.patchobject(nova.NovaClientPlugin,
'check_interface_detach',
return_value=True)
mock_attach = self.patchobject(return_server, 'interface_attach')
scheduler.TaskRunner(server.update, update_template)()
self.assertEqual((server.UPDATE, server.COMPLETE), server.state)
self.assertEqual(3, mock_detach.call_count)
self.assertEqual(0, mock_attach.call_count)
def test_server_update_networks_with_complex_parameters(self):
return_server = self.fc.servers.list()[1]
return_server.id = '5678'