nova/nova/tests/functional/test_routed_networks.py

530 lines
20 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import mock
from oslo_utils.fixture import uuidsentinel as uuids
from nova.network import constants
from nova.tests import fixtures as nova_fixtures
from nova.tests.functional import integrated_helpers
class NeutronRoutedNetworksFixture(nova_fixtures.NeutronFixture):
tenant_id = nova_fixtures.NeutronFixture.tenant_id
network_multisegment = {
'id': uuids.network_multisegment,
'name': 'net-multisegment',
'description': '',
'status': 'ACTIVE',
'admin_state_up': True,
'tenant_id': tenant_id,
'project_id': tenant_id,
'shared': False,
'mtu': 1450,
'router:external': False,
'availability_zone_hints': [],
'availability_zones': [
'nova'
],
'port_security_enabled': True,
'ipv4_address_scope': None,
'ipv6_address_scope': None,
'segments': [
{
"provider:network_type": "flat",
"provider:physical_network": "default",
"provider:segmentation_id": 0
},
{
"provider:network_type": "vlan",
"provider:physical_network": "public",
"provider:segmentation_id": 2
},
],
}
segment_id_0 = {
"name": "",
"network_id": network_multisegment['id'],
"segmentation_id": 0,
"network_type": "flat",
"physical_network": "default",
"revision_number": 1,
"id": uuids.segment_id_0,
"created_at": "2018-03-19T19:16:56Z",
"updated_at": "2018-03-19T19:16:56Z",
"description": "",
}
segment_id_2 = {
"name": "",
"network_id": network_multisegment['id'],
"segmentation_id": 2,
"network_type": "vlan",
"physical_network": "public",
"revision_number": 3,
"id": uuids.segment_id_2,
"created_at": "2018-03-19T19:16:56Z",
"updated_at": "2018-03-19T19:16:56Z",
"description": "",
}
segments = [segment_id_0, segment_id_2]
subnet_for_segment_id_0 = {
'id': uuids.subnet_for_segment_id_0,
'name': 'public-subnet',
'description': '',
'ip_version': 4,
'ipv6_address_mode': None,
'ipv6_ra_mode': None,
'enable_dhcp': True,
'network_id': network_multisegment['id'],
'tenant_id': tenant_id,
'project_id': tenant_id,
'dns_nameservers': [],
'gateway_ip': '192.168.1.1',
'allocation_pools': [
{
'start': '192.168.1.1',
'end': '192.168.1.254'
}
],
'host_routes': [],
'cidr': '192.168.1.1/24',
'segment_id': segment_id_0['id'],
}
subnet_for_segment_id_2 = {
'id': uuids.subnet_for_segment_id_2,
'name': 'vlan-subnet',
'description': '',
'ip_version': 4,
'ipv6_address_mode': None,
'ipv6_ra_mode': None,
'enable_dhcp': True,
'network_id': network_multisegment['id'],
'tenant_id': tenant_id,
'project_id': tenant_id,
'dns_nameservers': [],
'gateway_ip': '192.168.2.1',
'allocation_pools': [
{
'start': '192.168.2.1',
'end': '192.168.2.254'
}
],
'host_routes': [],
'cidr': '192.168.2.1/24',
'segment_id': segment_id_2['id'],
}
network_multisegment['subnets'] = [subnet_for_segment_id_0['id'],
subnet_for_segment_id_2['id']]
# Use this port only if you want a unbound port.
port_with_deferred_ip_allocation = {
'id': uuids.port_with_deferred_ip_allocation,
'name': '',
'description': '',
'network_id': network_multisegment['id'],
'admin_state_up': True,
'status': 'ACTIVE',
'mac_address': 'fa:16:3e:4c:2c:12',
# The allocation is deferred, so fixed_ips should be null *before*
# the port is binding.
# NOTE(sbauza): Make sure you modify the value if you look at the port
# after it's bound.
'fixed_ips': [],
'tenant_id': tenant_id,
'project_id': tenant_id,
'device_id': '',
'binding:profile': {},
'binding:vnic_type': 'normal',
'binding:vif_type': 'ovs',
'ip_allocation': "deferred",
}
# Use this port if you want to fake the port being already bound
port_with_deferred_ip_allocation_bound_to_segment_0 = \
copy.deepcopy(port_with_deferred_ip_allocation)
port_with_deferred_ip_allocation_bound_to_segment_0.update({
'fixed_ips': [{
'ip_address': '192.168.1.4',
'subnet_id': subnet_for_segment_id_0['id']
}],
})
port_on_segment_id_0 = {
'id': uuids.port_on_segment_id_0,
'name': '',
'description': '',
'network_id': network_multisegment['id'],
'admin_state_up': True,
'status': 'ACTIVE',
'mac_address': 'fa:16:3e:4c:2c:13',
'fixed_ips': [
{
'ip_address': '192.168.1.3',
'subnet_id': subnet_for_segment_id_0['id']
}
],
'tenant_id': tenant_id,
'project_id': tenant_id,
'device_id': '',
'binding:profile': {},
'binding:vnic_type': 'normal',
'binding:vif_type': 'ovs',
'ip_allocation': "immediate",
}
port_on_segment_id_2 = {
'id': uuids.port_on_segment_id_2,
'name': '',
'description': '',
'network_id': network_multisegment['id'],
'admin_state_up': True,
'status': 'ACTIVE',
'mac_address': 'fa:16:3e:4c:2c:13',
'fixed_ips': [
{
'ip_address': '192.168.2.4',
'subnet_id': subnet_for_segment_id_2['id']
}
],
'tenant_id': tenant_id,
'project_id': tenant_id,
'device_id': '',
'binding:profile': {},
'binding:vnic_type': 'normal',
'binding:vif_type': 'ovs',
'ip_allocation': "immediate",
}
def __init__(self, test):
super().__init__(test)
# add extra ports and the related network to the neutron fixture
# specifically for these tests.
self._networks[
self.network_multisegment['id']
] = self.network_multisegment
self._ports[
self.port_with_deferred_ip_allocation['id']
] = copy.deepcopy(self.port_with_deferred_ip_allocation)
self._ports[
self.port_on_segment_id_0['id']
] = copy.deepcopy(self.port_on_segment_id_0)
self._ports[
self.port_on_segment_id_2['id']
] = copy.deepcopy(self.port_on_segment_id_2)
self._subnets[
self.subnet_for_segment_id_0['id']
] = copy.deepcopy(self.subnet_for_segment_id_0)
self._subnets[
self.subnet_for_segment_id_2['id']
] = copy.deepcopy(self.subnet_for_segment_id_2)
def list_extensions(self, *args, **kwargs):
return {
'extensions': [
{
# Copied from neutron-lib segment.py
"updated": "2016-02-24T17:00:00-00:00",
"name": constants.SEGMENT,
"links": [],
"alias": "segment",
"description": "Segments extension."
}
]
}
def list_subnets(self, retrieve_all=True, **_params):
if 'network_id' in _params:
network_id = _params['network_id']
assert network_id in self._networks, ('Network %s not in fixture' %
network_id)
filtered_subnets = []
for subnet in list(self._subnets.values()):
if subnet['network_id'] == network_id:
filtered_subnets.append(copy.deepcopy(subnet))
return {'subnets': filtered_subnets}
else:
return super().list_subnets(retrieve_all, **_params)
def create_port(self, body=None):
body = body or {'port': {}}
network_id = body['port'].get('network_id')
assert network_id in self._networks, ('Network %s not in fixture' %
network_id)
# Redefine the default port template to use for creating a new one to
# be the port already allocated on segment #0.
# NOTE(sbauza): Segment #0 is always used when booting an instance
# without a provided port as the first supported host is related to it.
# FIXME(sbauza): Do something here to not blindly set the segment
# without verifying which compute service is used by the instance.
self.default_port = (
self.port_with_deferred_ip_allocation_bound_to_segment_0
)
return super().create_port(body)
class RoutedNetworkTests(integrated_helpers._IntegratedTestBase):
compute_driver = 'fake.MediumFakeDriver'
microversion = 'latest'
ADMIN_API = True
def setUp(self):
self.flags(
query_placement_for_routed_network_aggregates=True,
group='scheduler')
# We will create 5 hosts, let's make sure we order them by their index.
weights = {'host1': 500, 'host2': 400, 'host3': 300, 'host4': 200,
'host5': 100}
self.useFixture(nova_fixtures.HostNameWeigherFixture(weights=weights))
super().setUp()
# Amend the usual neutron fixture with specific routed networks
self.neutron = self.useFixture(NeutronRoutedNetworksFixture(self))
# let's create 5 computes with their respective records
for i in range(1, 6):
setattr(self, 'compute%s' % i, self._start_compute('host%s' % i))
setattr(self, 'compute%s_rp_uuid' % i,
self._get_provider_uuid_by_host('host%s' % i))
setattr(self, 'compute%s_service_id' % i,
self.admin_api.get_services(host='host%s' % i,
binary='nova-compute')[0]['id']
)
# Simulate the placement setup neutron does for multi segment networks
segment_ids = [segment["id"] for segment in self.neutron.segments]
self.assertEqual(2, len(segment_ids))
# We have 5 computes and the network has two segments. Let's create a
# setup where the network has segments on host2 to host5 but not on
# host1. The HostNameWeigherFixture prefers host1 over host2 over host3
# over host4 over host5. So this way we can check if the scheduler
# selects host with available network segment.
# The segments are for this net :
# * segment 0 is for host2, host4 and host5
# * segment 2 is for host3 and host5
self.segment_id_to_compute_rp_uuid = {
segment_ids[0]: [self.compute2_rp_uuid, self.compute4_rp_uuid,
self.compute5_rp_uuid],
segment_ids[1]: [self.compute3_rp_uuid, self.compute5_rp_uuid],
}
self._create_multisegment_placement_setup(
self.segment_id_to_compute_rp_uuid)
def _create_multisegment_placement_setup(self, segment_to_compute_rp):
self.segment_id_to_aggregate_id = {}
# map each segment to one compute
for segment_id, compute_rp_uuids in segment_to_compute_rp.items():
# create segment RP
segment_rp_req = {
"name": segment_id,
"uuid": segment_id,
"parent_provider_uuid": None,
}
self.placement.post(
"/resource_providers", body=segment_rp_req, version="1.20"
)
# create aggregate around the segment RP and the compute RP
aggregate_uuid = getattr(uuids, segment_id)
self.segment_id_to_aggregate_id[segment_id] = aggregate_uuid
# as we created the segment RP above we assume that it does not
# have any aggregate and its generation is 0
self.assertEqual(
200,
self.placement.put(
"/resource_providers/%s/aggregates" % segment_id,
body={
"aggregates": [aggregate_uuid],
"resource_provider_generation": 0,
},
version="1.20",
).status,
)
# get compute RPs and append the new aggregate to it
for compute_rp_uuid in compute_rp_uuids:
resp = self.placement.get(
"/resource_providers/%s/aggregates" % compute_rp_uuid,
version="1.20",
).body
resp["aggregates"].append(aggregate_uuid)
self.assertEqual(
200,
self.placement.put(
"/resource_providers/%s/aggregates" % compute_rp_uuid,
body=resp,
version="1.20",
).status,
)
def test_boot_with_deferred_port(self):
# Neutron only assigns the deferred port to a segment when the port is
# bound to a host. So the scheduler can select any host that has
# a segment for the network.
port = self.neutron.port_with_deferred_ip_allocation
server = self._create_server(
name='server-with-routed-net',
networks=[{'port': port['id']}])
# HostNameWeigherFixture prefers host1 but the port is in a network
# that has only segments on host2 to host5.
self.assertEqual('host2', server['OS-EXT-SRV-ATTR:host'])
def test_boot_with_immediate_port(self):
# Immediate port should be assigned to a network segment by neutron
# during port create. So the scheduler should only select a host that
# is connected to that network segment
port = self.neutron.port_on_segment_id_2
server = self._create_server(
name='server-with-routed-net',
networks=[{'port': port['id']}])
# Since the port is on the segment ID 2, only host3 and host5 are
# accepted, so host3 always wins because of the weigher.
self.assertEqual('host3', server['OS-EXT-SRV-ATTR:host'])
def test_boot_with_network(self):
# Port is created _after_ scheduling to a host so the scheduler can
# select either host2 to host5 initially based on the segment
# availability of the network. But then nova needs to create
# the port in deferred mode so that the already selected host could
# not conflict with the neutron segment assignment at port create.
net = self.neutron.network_multisegment
server = self._create_server(
name='server-with-routed-net',
networks=[{'uuid': net['id']}])
# host2 always wins over host3 to host5 because of the weigher.
self.assertEqual('host2', server['OS-EXT-SRV-ATTR:host'])
# Verify that we used a port with a deferred ip allocation
ip_addr = server['addresses'][net['name']][0][
'addr']
self.assertEqual(
self.neutron.port_with_deferred_ip_allocation_bound_to_segment_0[
'fixed_ips'][0]['ip_address'],
ip_addr)
def test_boot_with_two_nics(self):
# Test a scenario with a user trying to have two different NICs within
# two different segments not intertwined.
port0 = self.neutron.port_on_segment_id_0
port1 = self.neutron.port_on_segment_id_2
# Here we ask for a server with one NIC on each segment.
server = self._create_server(
name='server-with-routed-net',
networks=[{'port': port0['id']}, {'port': port1['id']}])
# host2 should win with the weigher but as we asked for both segments,
# only host5 supports them.
self.assertEqual('host5', server['OS-EXT-SRV-ATTR:host'])
def test_migrate(self):
net = self.neutron.network_multisegment
server = self._create_server(
name='server-with-routed-net',
networks=[{'uuid': net['id']}])
# Make sure we landed on host2 since both segments were accepted
self.assertEqual('host2', server['OS-EXT-SRV-ATTR:host'])
server = self._migrate_server(server)
# HostNameWeigherFixture prefers host1 but the port is in a network
# that has only segments on the other hosts.
# host2 is avoided as the source and only host4 is left on this
# segment.
self.assertEqual('host4', server['OS-EXT-SRV-ATTR:host'])
def test_live_migrate(self):
net = self.neutron.network_multisegment
server = self._create_server(
name='server-with-routed-net',
networks=[{'uuid': net['id']}])
self.assertEqual('host2', server['OS-EXT-SRV-ATTR:host'])
self._live_migrate(server)
server = self.api.get_server(server['id'])
# HostNameWeigherFixture prefers host1 but the port is in a network
# that has only segments on the other hosts.
# host2 is avoided as the source and only host4 is left on this
# segment.
self.assertEqual('host4', server['OS-EXT-SRV-ATTR:host'])
def test_evacuate(self):
net = self.neutron.network_multisegment
server = self._create_server(
name='server-with-routed-net',
networks=[{'uuid': net['id']}])
# The instance landed on host2 as the segment was related.
self.assertEqual('host2', server['OS-EXT-SRV-ATTR:host'])
self.admin_api.put_service_force_down(self.compute2_service_id, True)
server = self._evacuate_server(server)
# HostNameWeigherFixture prefers host1 but the port is in a network
# that has only segments on the other hosts.
# host2 is avoided as the source and only host4 is left on this
# segment.
self.assertEqual('host4', server['OS-EXT-SRV-ATTR:host'])
def test_unshelve_after_shelve(self):
net = self.neutron.network_multisegment
server = self._create_server(
name='server-with-routed-net',
networks=[{'uuid': net['id']}])
self.assertEqual('host2', server['OS-EXT-SRV-ATTR:host'])
# Shelve does offload the instance so unshelve will ask the scheduler
# again.
server = self._shelve_server(server)
server = self._unshelve_server(server)
# HostNameWeigherFixture prefers host1 but the port is in a network
# that has only segments on the others. Since the instance was
# offloaded, we can now again support host2.
self.assertEqual('host2', server['OS-EXT-SRV-ATTR:host'])
def test_boot_with_immediate_port_fails_due_to_config(self):
# We will fake the fact that somehow the segment has no related
# aggregate (maybe because Neutron got a exception when trying to
# create the aggregate by calling the Nova API)
port = self.neutron.port_on_segment_id_2
with mock.patch(
'nova.scheduler.client.report.SchedulerReportClient.'
'_get_provider_aggregates',
return_value=None
) as mock_get_aggregates:
server = self._create_server(
name='server-with-routed-net',
networks=[{'port': port['id']}],
expected_state='ERROR')
# Make sure we correctly looked up at which aggregates were related to
# the segment ID #2
expected_subnet_id = self.neutron.subnet_for_segment_id_2['id']
expected_segment_id = self.neutron.segment_id_2['id']
mock_get_aggregates.assert_called_once_with(
mock.ANY, expected_segment_id)
self.assertIn('No valid host', server['fault']['message'])
self.assertIn(
'Aggregates not found for the subnet %s' % expected_subnet_id,
server['fault']['message'])