summaryrefslogtreecommitdiff
path: root/nova/tests/functional/libvirt/test_live_migration.py
blob: 31ff9dfca05bce70f75bf4ff7377b99c10b7c0c3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# 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 both servers become
        # active again after successful (server_a) and aborted
        # (server_b) live migrations
        self.lock_live_migration.release()
        self._wait_for_state_change(self.server_a, 'ACTIVE')
        self._wait_for_state_change(self.server_b, 'ACTIVE')


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 both servers become
        # active again after successful (server_a) and aborted
        # (server_b) live migrations
        self.lock_live_migration.release()
        self._wait_for_state_change(self.server_a, 'ACTIVE')
        self._wait_for_migration_status(self.server_a, ['completed'])
        self._wait_for_state_change(self.server_b, 'ACTIVE')

        # Allocations for both successful (server_a) and aborted queued live
        # migration (server_b) should be removed.
        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']
        self.assertEqual({}, allocations_server_b_migration)

        # INACTIVE port binding  on destination host should be removed when
        # queued live migration is aborted, so only 1 port binding would
        # exist for ports attached to both servers.
        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(1, len(port_binding_server_b))
        self.assertNotIn('dest', port_binding_server_b)