529 lines
20 KiB
Python
529 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
|
|
exp_segment_id = self.neutron.segment_id_2['id']
|
|
mock_get_aggregates.assert_called_once_with(
|
|
mock.ANY, exp_segment_id)
|
|
|
|
self.assertIn('No valid host', server['fault']['message'])
|
|
self.assertIn(
|
|
'Failed to find aggregate related to segment %s' % exp_segment_id,
|
|
server['fault']['message'])
|