Coverage for watcher/applier/actions/migration.py: 84%
81 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
1# -*- encoding: utf-8 -*-
2# Copyright (c) 2015 b<>com
3#
4# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
15# implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18#
21from oslo_log import log
22from watcher._i18n import _
23from watcher.applier.actions import base
24from watcher.common import exception
25from watcher.common import nova_helper
27LOG = log.getLogger(__name__)
30class Migrate(base.BaseAction):
31 """Migrates a server to a destination nova-compute host
33 This action will allow you to migrate a server to another compute
34 destination host.
35 Migration type 'live' can only be used for migrating active VMs.
36 Migration type 'cold' can be used for migrating non-active VMs
37 as well active VMs, which will be shut down while migrating.
39 The action schema is::
41 schema = Schema({
42 'resource_id': str, # should be a UUID
43 'migration_type': str, # choices -> "live", "cold"
44 'destination_node': str,
45 'source_node': str,
46 })
48 The `resource_id` is the UUID of the server to migrate.
49 The `source_node` and `destination_node` parameters are respectively the
50 source and the destination compute hostname.
52 .. note::
54 Nova API version must be 2.56 or above if `destination_node` parameter
55 is given.
57 """
59 # input parameters constants
60 MIGRATION_TYPE = 'migration_type'
61 LIVE_MIGRATION = 'live'
62 COLD_MIGRATION = 'cold'
63 DESTINATION_NODE = 'destination_node'
64 SOURCE_NODE = 'source_node'
66 @property
67 def schema(self):
68 return {
69 'type': 'object',
70 'properties': {
71 'destination_node': {
72 "anyof": [
73 {'type': 'string', "minLength": 1},
74 {'type': 'None'}
75 ]
76 },
77 'migration_type': {
78 'type': 'string',
79 "enum": ["live", "cold"]
80 },
81 'resource_id': {
82 'type': 'string',
83 "minlength": 1,
84 "pattern": ("^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
85 "([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
86 "([a-fA-F0-9]){12}$")
87 },
88 'resource_name': {
89 'type': 'string',
90 "minlength": 1
91 },
92 'source_node': {
93 'type': 'string',
94 "minLength": 1
95 }
96 },
97 'required': ['migration_type', 'resource_id', 'source_node'],
98 'additionalProperties': False,
99 }
101 @property
102 def instance_uuid(self):
103 return self.resource_id
105 @property
106 def migration_type(self):
107 return self.input_parameters.get(self.MIGRATION_TYPE)
109 @property
110 def destination_node(self):
111 return self.input_parameters.get(self.DESTINATION_NODE)
113 @property
114 def source_node(self):
115 return self.input_parameters.get(self.SOURCE_NODE)
117 def _live_migrate_instance(self, nova, destination):
118 result = None
119 try:
120 result = nova.live_migrate_instance(instance_id=self.instance_uuid,
121 dest_hostname=destination)
122 except nova_helper.nvexceptions.ClientException as e:
123 LOG.debug("Nova client exception occurred while live "
124 "migrating instance "
125 "%(instance)s.Exception: %(exception)s",
126 {'instance': self.instance_uuid, 'exception': e})
128 except Exception as e:
129 LOG.exception(e)
130 LOG.critical("Unexpected error occurred. Migration failed for "
131 "instance %s. Leaving instance on previous "
132 "host.", self.instance_uuid)
134 return result
136 def _cold_migrate_instance(self, nova, destination):
137 result = None
138 try:
139 result = nova.watcher_non_live_migrate_instance(
140 instance_id=self.instance_uuid,
141 dest_hostname=destination)
142 except Exception as exc:
143 LOG.exception(exc)
144 LOG.critical("Unexpected error occurred. Migration failed for "
145 "instance %s. Leaving instance on previous "
146 "host.", self.instance_uuid)
147 return result
149 def _abort_cold_migrate(self, nova):
150 # TODO(adisky): currently watcher uses its own version of cold migrate
151 # implement cold migrate using nova dependent on the blueprint
152 # https://blueprints.launchpad.net/nova/+spec/cold-migration-with-target
153 # Abort operation for cold migrate is dependent on blueprint
154 # https://blueprints.launchpad.net/nova/+spec/abort-cold-migration
155 LOG.warning("Abort operation for cold migration is not implemented")
157 def _abort_live_migrate(self, nova, source, destination):
158 return nova.abort_live_migrate(instance_id=self.instance_uuid,
159 source=source, destination=destination)
161 def migrate(self, destination=None):
162 nova = nova_helper.NovaHelper(osc=self.osc)
163 if destination is None: 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true
164 LOG.debug("Migrating instance %s, destination node will be "
165 "determined by nova-scheduler", self.instance_uuid)
166 else:
167 LOG.debug("Migrate instance %s to %s", self.instance_uuid,
168 destination)
169 instance = nova.find_instance(self.instance_uuid)
170 if instance:
171 if self.migration_type == self.LIVE_MIGRATION:
172 return self._live_migrate_instance(nova, destination)
173 elif self.migration_type == self.COLD_MIGRATION: 173 ↛ 176line 173 didn't jump to line 176 because the condition on line 173 was always true
174 return self._cold_migrate_instance(nova, destination)
175 else:
176 raise exception.Invalid(
177 message=(_("Migration of type '%(migration_type)s' is not "
178 "supported.") %
179 {'migration_type': self.migration_type}))
180 else:
181 raise exception.InstanceNotFound(name=self.instance_uuid)
183 def execute(self):
184 return self.migrate(destination=self.destination_node)
186 def revert(self):
187 return self.migrate(destination=self.source_node)
189 def abort(self):
190 nova = nova_helper.NovaHelper(osc=self.osc)
191 instance = nova.find_instance(self.instance_uuid)
192 if instance: 192 ↛ 200line 192 didn't jump to line 200 because the condition on line 192 was always true
193 if self.migration_type == self.COLD_MIGRATION: 193 ↛ 194line 193 didn't jump to line 194 because the condition on line 193 was never true
194 return self._abort_cold_migrate(nova)
195 elif self.migration_type == self.LIVE_MIGRATION: 195 ↛ exitline 195 didn't return from function 'abort' because the condition on line 195 was always true
196 return self._abort_live_migrate(
197 nova, source=self.source_node,
198 destination=self.destination_node)
199 else:
200 raise exception.InstanceNotFound(name=self.instance_uuid)
202 def pre_condition(self):
203 # TODO(jed): check if the instance exists / check if the instance is on
204 # the source_node
205 pass
207 def post_condition(self):
208 # TODO(jed): check extra parameters (network response, etc.)
209 pass
211 def get_description(self):
212 """Description of the action"""
213 return "Moving a VM instance from source_node to destination_node"