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

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# 

19 

20 

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 

26 

27LOG = log.getLogger(__name__) 

28 

29 

30class Migrate(base.BaseAction): 

31 """Migrates a server to a destination nova-compute host 

32 

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. 

38 

39 The action schema is:: 

40 

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 }) 

47 

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. 

51 

52 .. note:: 

53 

54 Nova API version must be 2.56 or above if `destination_node` parameter 

55 is given. 

56 

57 """ 

58 

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' 

65 

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 } 

100 

101 @property 

102 def instance_uuid(self): 

103 return self.resource_id 

104 

105 @property 

106 def migration_type(self): 

107 return self.input_parameters.get(self.MIGRATION_TYPE) 

108 

109 @property 

110 def destination_node(self): 

111 return self.input_parameters.get(self.DESTINATION_NODE) 

112 

113 @property 

114 def source_node(self): 

115 return self.input_parameters.get(self.SOURCE_NODE) 

116 

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}) 

127 

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) 

133 

134 return result 

135 

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 

148 

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") 

156 

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) 

160 

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) 

182 

183 def execute(self): 

184 return self.migrate(destination=self.destination_node) 

185 

186 def revert(self): 

187 return self.migrate(destination=self.source_node) 

188 

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) 

201 

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 

206 

207 def post_condition(self): 

208 # TODO(jed): check extra parameters (network response, etc.) 

209 pass 

210 

211 def get_description(self): 

212 """Description of the action""" 

213 return "Moving a VM instance from source_node to destination_node"