224 lines
9.3 KiB
Python
224 lines
9.3 KiB
Python
# Copyright 2021 Red Hat, Inc.
|
|
#
|
|
# 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 threading
|
|
|
|
from lxml import etree
|
|
from nova.tests.functional import integrated_helpers
|
|
from nova.tests.functional.libvirt import base as libvirt_base
|
|
|
|
|
|
class LiveMigrationWithLockBase(
|
|
libvirt_base.LibvirtMigrationMixin,
|
|
libvirt_base.ServersTestBase,
|
|
integrated_helpers.InstanceHelperMixin
|
|
):
|
|
"""Base for live migration tests which require live migration to be
|
|
locked for certain period of time and then unlocked afterwards.
|
|
|
|
Separate base class is needed because locking mechanism could work
|
|
in an unpredicted way if two tests for the same class would try to
|
|
use it simultaneously. Every test using this mechanism should use
|
|
separate class instance.
|
|
"""
|
|
|
|
api_major_version = 'v2.1'
|
|
microversion = '2.74'
|
|
ADMIN_API = True
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
# We will allow only one live migration to be processed at any
|
|
# given period of time
|
|
self.flags(max_concurrent_live_migrations='1')
|
|
self.src_hostname = self.start_compute(hostname='src')
|
|
self.dest_hostname = self.start_compute(hostname='dest')
|
|
|
|
self.src = self.computes[self.src_hostname]
|
|
self.dest = self.computes[self.dest_hostname]
|
|
|
|
# Live migration's execution could be locked if needed
|
|
self.lock_live_migration = threading.Lock()
|
|
|
|
def _migrate_stub(self, domain, destination, params, flags):
|
|
# Execute only if live migration is not locked
|
|
with self.lock_live_migration:
|
|
self.dest.driver._host.get_connection().createXML(
|
|
params['destination_xml'],
|
|
'fake-createXML-doesnt-care-about-flags')
|
|
conn = self.src.driver._host.get_connection()
|
|
|
|
# Because migrateToURI3 is spawned in a background thread,
|
|
# this method does not block the upper nova layers. Because
|
|
# we don't want nova to think the live migration has
|
|
# finished until this method is done, the last thing we do
|
|
# is make fakelibvirt's Domain.jobStats() return
|
|
# VIR_DOMAIN_JOB_COMPLETED.
|
|
server = etree.fromstring(
|
|
params['destination_xml']
|
|
).find('./uuid').text
|
|
dom = conn.lookupByUUIDString(server)
|
|
dom.complete_job()
|
|
|
|
|
|
class LiveMigrationQueuedAbortTestVmStatus(LiveMigrationWithLockBase):
|
|
"""Functional test for bug #1949808.
|
|
|
|
This test is used to confirm that VM's state is reverted properly
|
|
when queued Live migration is aborted.
|
|
"""
|
|
|
|
def test_queued_live_migration_abort_vm_status(self):
|
|
# Lock live migrations
|
|
self.lock_live_migration.acquire()
|
|
|
|
# Start instances: first one would be used to occupy
|
|
# executor's live migration queue, second one would be used
|
|
# to actually confirm that queued live migrations are
|
|
# aborted properly.
|
|
self.server_a = self._create_server(
|
|
host=self.src_hostname, networks='none')
|
|
self.server_b = self._create_server(
|
|
host=self.src_hostname, networks='none')
|
|
# Issue live migration requests for both servers. We expect that
|
|
# server_a live migration would be running, but locked by
|
|
# self.lock_live_migration and server_b live migration would be
|
|
# queued.
|
|
self._live_migrate(
|
|
self.server_a,
|
|
migration_expected_state='running',
|
|
server_expected_state='MIGRATING'
|
|
)
|
|
self._live_migrate(
|
|
self.server_b,
|
|
migration_expected_state='queued',
|
|
server_expected_state='MIGRATING'
|
|
)
|
|
|
|
# Abort live migration for server_b
|
|
serverb_migration = self.api.api_get(
|
|
'/os-migrations?instance_uuid=%s' % self.server_b['id']
|
|
).body['migrations'].pop()
|
|
|
|
self.api.api_delete(
|
|
'/servers/%s/migrations/%s' % (self.server_b['id'],
|
|
serverb_migration['id']))
|
|
self._wait_for_migration_status(self.server_b, ['cancelled'])
|
|
# Unlock live migrations and confirm that server_a becomes
|
|
# active again after successful live migration
|
|
self.lock_live_migration.release()
|
|
self._wait_for_state_change(self.server_a, 'ACTIVE')
|
|
|
|
# FIXME(artom) Assert the server_b never comes out of 'MIGRATING'
|
|
self.assertRaises(
|
|
AssertionError,
|
|
self._wait_for_state_change, self.server_b, 'ACTIVE')
|
|
self._wait_for_state_change(self.server_b, 'MIGRATING')
|
|
|
|
|
|
class LiveMigrationQueuedAbortTestLeftoversRemoved(LiveMigrationWithLockBase):
|
|
"""Functional test for bug #1960412.
|
|
|
|
Placement allocations for live migration and inactive Neutron port
|
|
bindings on destination host created by Nova control plane when live
|
|
migration is initiated should be removed when queued live migration
|
|
is aborted using Nova API.
|
|
"""
|
|
|
|
def test_queued_live_migration_abort_leftovers_removed(self):
|
|
# Lock live migrations
|
|
self.lock_live_migration.acquire()
|
|
|
|
# Start instances: first one would be used to occupy
|
|
# executor's live migration queue, second one would be used
|
|
# to actually confirm that queued live migrations are
|
|
# aborted properly.
|
|
# port_1 is created automatically when neutron fixture is
|
|
# initialized, port_2 is created manually
|
|
self.server_a = self._create_server(
|
|
host=self.src_hostname,
|
|
networks=[{'port': self.neutron.port_1['id']}])
|
|
self.neutron.create_port({'port': self.neutron.port_2})
|
|
self.server_b = self._create_server(
|
|
host=self.src_hostname,
|
|
networks=[{'port': self.neutron.port_2['id']}])
|
|
# Issue live migration requests for both servers. We expect that
|
|
# server_a live migration would be running, but locked by
|
|
# self.lock_live_migration and server_b live migration would be
|
|
# queued.
|
|
self._live_migrate(
|
|
self.server_a,
|
|
migration_expected_state='running',
|
|
server_expected_state='MIGRATING'
|
|
)
|
|
self._live_migrate(
|
|
self.server_b,
|
|
migration_expected_state='queued',
|
|
server_expected_state='MIGRATING'
|
|
)
|
|
|
|
# Abort live migration for server_b
|
|
migration_server_a = self.api.api_get(
|
|
'/os-migrations?instance_uuid=%s' % self.server_a['id']
|
|
).body['migrations'].pop()
|
|
migration_server_b = self.api.api_get(
|
|
'/os-migrations?instance_uuid=%s' % self.server_b['id']
|
|
).body['migrations'].pop()
|
|
|
|
self.api.api_delete(
|
|
'/servers/%s/migrations/%s' % (self.server_b['id'],
|
|
migration_server_b['id']))
|
|
self._wait_for_migration_status(self.server_b, ['cancelled'])
|
|
# Unlock live migrations and confirm that server_a becomes
|
|
# active again after successful live migration
|
|
self.lock_live_migration.release()
|
|
self._wait_for_state_change(self.server_a, 'ACTIVE')
|
|
self._wait_for_migration_status(self.server_a, ['completed'])
|
|
# FIXME(astupnikov) Assert the server_b never comes out of 'MIGRATING'
|
|
# This should be fixed after bug #1949808 is addressed
|
|
self._wait_for_state_change(self.server_b, 'MIGRATING')
|
|
|
|
# FIXME(astupnikov) Because of bug #1960412 allocations for aborted
|
|
# queued live migration (server_b) would not be removed. Allocations
|
|
# for completed live migration (server_a) should be empty.
|
|
allocations_server_a_migration = self.placement.get(
|
|
'/allocations/%s' % migration_server_a['uuid']
|
|
).body['allocations']
|
|
self.assertEqual({}, allocations_server_a_migration)
|
|
allocations_server_b_migration = self.placement.get(
|
|
'/allocations/%s' % migration_server_b['uuid']
|
|
).body['allocations']
|
|
src_uuid = self.api.api_get(
|
|
'os-hypervisors?hypervisor_hostname_pattern=%s' %
|
|
self.src_hostname).body['hypervisors'][0]['id']
|
|
self.assertIn(src_uuid, allocations_server_b_migration)
|
|
|
|
# FIXME(astupnikov) Because of bug #1960412 INACTIVE port binding
|
|
# on destination host would not be removed when queued live migration
|
|
# is aborted, so 2 port bindings would exist for server_b port from
|
|
# Neutron's perspective.
|
|
# server_a should be migrated to dest compute, server_b should still
|
|
# be hosted by src compute.
|
|
port_binding_server_a = copy.deepcopy(
|
|
self.neutron._port_bindings[self.neutron.port_1['id']]
|
|
)
|
|
self.assertEqual(1, len(port_binding_server_a))
|
|
self.assertNotIn('src', port_binding_server_a)
|
|
port_binding_server_b = copy.deepcopy(
|
|
self.neutron._port_bindings[self.neutron.port_2['id']]
|
|
)
|
|
self.assertEqual(2, len(port_binding_server_b))
|