Coverage for nova/objects/migration.py: 96%

172 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-17 15:08 +0000

1# Copyright 2013 IBM Corp. 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); you may 

4# not use this file except in compliance with the License. You may obtain 

5# a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

12# License for the specific language governing permissions and limitations 

13# under the License. 

14 

15from oslo_db import exception as db_exc 

16from oslo_log import log as logging 

17from oslo_utils import uuidutils 

18from oslo_utils import versionutils 

19 

20from nova.db.main import api as db 

21from nova import exception 

22from nova.i18n import _ 

23from nova import objects 

24from nova.objects import base 

25from nova.objects import fields 

26 

27 

28LOG = logging.getLogger(__name__) 

29 

30 

31def determine_migration_type(migration): 

32 if isinstance(migration, dict): 

33 old_instance_type_id = migration['old_instance_type_id'] 

34 new_instance_type_id = migration['new_instance_type_id'] 

35 else: 

36 old_instance_type_id = migration.old_instance_type_id 

37 new_instance_type_id = migration.new_instance_type_id 

38 

39 if old_instance_type_id != new_instance_type_id: 

40 return 'resize' 

41 

42 return 'migration' 

43 

44 

45@base.NovaObjectRegistry.register 

46class Migration(base.NovaPersistentObject, base.NovaObject): 

47 # Version 1.0: Initial version 

48 # Version 1.1: String attributes updated to support unicode 

49 # Version 1.2: Added migration_type and hidden 

50 # Version 1.3: Added get_by_id_and_instance() 

51 # Version 1.4: Added migration progress detail 

52 # Version 1.5: Added uuid 

53 # Version 1.6: Added cross_cell_move and get_by_uuid(). 

54 # Version 1.7: Added user_id and project_id 

55 # Version 1.8: Added dest_compute_id 

56 VERSION = '1.8' 

57 

58 fields = { 

59 'id': fields.IntegerField(), 

60 'uuid': fields.UUIDField(), 

61 'source_compute': fields.StringField(nullable=True), # source hostname 

62 'dest_compute': fields.StringField(nullable=True), # dest hostname 

63 'source_node': fields.StringField(nullable=True), # source nodename 

64 'dest_node': fields.StringField(nullable=True), # dest nodename 

65 # ID of ComputeNode matching dest_node 

66 'dest_compute_id': fields.IntegerField(nullable=True), 

67 'dest_host': fields.StringField(nullable=True), # dest host IP 

68 # TODO(stephenfin): Rename these to old_flavor_id, new_flavor_id in 

69 # v2.0 

70 'old_instance_type_id': fields.IntegerField(nullable=True), 

71 'new_instance_type_id': fields.IntegerField(nullable=True), 

72 'instance_uuid': fields.StringField(nullable=True), 

73 'status': fields.StringField(nullable=True), 

74 'migration_type': fields.MigrationTypeField(nullable=False), 

75 'hidden': fields.BooleanField(nullable=False, default=False), 

76 'memory_total': fields.IntegerField(nullable=True), 

77 'memory_processed': fields.IntegerField(nullable=True), 

78 'memory_remaining': fields.IntegerField(nullable=True), 

79 'disk_total': fields.IntegerField(nullable=True), 

80 'disk_processed': fields.IntegerField(nullable=True), 

81 'disk_remaining': fields.IntegerField(nullable=True), 

82 'cross_cell_move': fields.BooleanField(default=False), 

83 # request context user id 

84 'user_id': fields.StringField(nullable=True), 

85 # request context project id 

86 'project_id': fields.StringField(nullable=True), 

87 } 

88 

89 @staticmethod 

90 def _from_db_object(context, migration, db_migration): 

91 for key in migration.fields: 

92 value = db_migration[key] 

93 if key == 'migration_type' and value is None: 

94 value = determine_migration_type(db_migration) 

95 elif key == 'uuid' and value is None: 

96 continue 

97 setattr(migration, key, value) 

98 

99 migration._context = context 

100 migration.obj_reset_changes() 

101 migration._ensure_uuid() 

102 return migration 

103 

104 def obj_make_compatible(self, primitive, target_version): 

105 super(Migration, self).obj_make_compatible(primitive, target_version) 

106 target_version = versionutils.convert_version_to_tuple(target_version) 

107 if target_version < (1, 2): 

108 if 'migration_type' in primitive: 

109 del primitive['migration_type'] 

110 del primitive['hidden'] 

111 if target_version < (1, 4): 

112 if 'memory_total' in primitive: 

113 del primitive['memory_total'] 

114 del primitive['memory_processed'] 

115 del primitive['memory_remaining'] 

116 del primitive['disk_total'] 

117 del primitive['disk_processed'] 

118 del primitive['disk_remaining'] 

119 if target_version < (1, 5): 

120 if 'uuid' in primitive: 

121 del primitive['uuid'] 

122 if target_version < (1, 6) and 'cross_cell_move' in primitive: 

123 del primitive['cross_cell_move'] 

124 if target_version < (1, 7): 

125 if 'user_id' in primitive: 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true

126 del primitive['user_id'] 

127 if 'project_id' in primitive: 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true

128 del primitive['project_id'] 

129 if target_version < (1, 8): 

130 primitive.pop('dest_compute_id', None) 

131 

132 @base.lazy_load_counter 

133 def obj_load_attr(self, attrname): 

134 if attrname == 'migration_type': 

135 # NOTE(danms): The only reason we'd need to load this is if 

136 # some older node sent us one. So, guess the type. 

137 self.migration_type = determine_migration_type(self) 

138 elif attrname in ['hidden', 'cross_cell_move']: 138 ↛ 141line 138 didn't jump to line 141 because the condition on line 138 was always true

139 self.obj_set_defaults(attrname) 

140 else: 

141 super(Migration, self).obj_load_attr(attrname) 

142 

143 def _ensure_uuid(self): 

144 if 'uuid' in self: 

145 return 

146 

147 self.uuid = uuidutils.generate_uuid() 

148 try: 

149 self.save() 

150 except db_exc.DBDuplicateEntry: 

151 # NOTE(danms) We raced to generate a uuid for this, 

152 # so fetch the winner and use that uuid 

153 fresh = self.__class__.get_by_id(self.context, self.id) 

154 self.uuid = fresh.uuid 

155 

156 @base.remotable_classmethod 

157 def get_by_uuid(cls, context, migration_uuid): 

158 db_migration = db.migration_get_by_uuid(context, migration_uuid) 

159 return cls._from_db_object(context, cls(), db_migration) 

160 

161 @base.remotable_classmethod 

162 def get_by_id(cls, context, migration_id): 

163 db_migration = db.migration_get(context, migration_id) 

164 return cls._from_db_object(context, cls(), db_migration) 

165 

166 @base.remotable_classmethod 

167 def get_by_id_and_instance(cls, context, migration_id, instance_uuid): 

168 db_migration = db.migration_get_by_id_and_instance( 

169 context, migration_id, instance_uuid) 

170 return cls._from_db_object(context, cls(), db_migration) 

171 

172 @base.remotable_classmethod 

173 def get_by_instance_and_status(cls, context, instance_uuid, status): 

174 db_migration = db.migration_get_by_instance_and_status( 

175 context, instance_uuid, status) 

176 return cls._from_db_object(context, cls(), db_migration) 

177 

178 @base.remotable 

179 def create(self): 

180 if self.obj_attr_is_set('id'): 

181 raise exception.ObjectActionError(action='create', 

182 reason='already created') 

183 if 'uuid' not in self: 

184 self.uuid = uuidutils.generate_uuid() 

185 # Record who is initiating the migration which is 

186 # not necessarily the owner of the instance. 

187 if 'user_id' not in self: 

188 self.user_id = self._context.user_id 

189 if 'project_id' not in self: 

190 self.project_id = self._context.project_id 

191 updates = self.obj_get_changes() 

192 if 'migration_type' not in updates: 

193 raise exception.ObjectActionError( 

194 action="create", 

195 reason=_("cannot create a Migration object without a " 

196 "migration_type set")) 

197 version = versionutils.convert_version_to_tuple(self.VERSION) 

198 if 'dest_node' in updates and 'dest_compute_id' not in updates: 

199 # NOTE(danms): This is not really the best idea, as we should try 

200 # not to have different behavior based on the version of the 

201 # object. However, this exception helps us find cases in testing 

202 # where these may not be updated together. We can remove this 

203 # later. 

204 if version >= (1, 8): 

205 raise exception.ObjectActionError( 

206 action='create', 

207 reason=_('cannot create a Migration object with a ' 

208 'dest_node but no dest_compute_id')) 

209 else: 

210 LOG.warning('Migration is being created for %s but no ' 

211 'compute_id is set', self.dest_node) 

212 db_migration = db.migration_create(self._context, updates) 

213 self._from_db_object(self._context, self, db_migration) 

214 

215 @base.remotable 

216 def save(self): 

217 updates = self.obj_get_changes() 

218 updates.pop('id', None) 

219 db_migration = db.migration_update(self._context, self.id, updates) 

220 self._from_db_object(self._context, self, db_migration) 

221 self.obj_reset_changes() 

222 

223 @property 

224 def instance(self): 

225 if not hasattr(self, '_cached_instance'): 

226 self._cached_instance = objects.Instance.get_by_uuid( 

227 self._context, self.instance_uuid, 

228 expected_attrs=['migration_context', 'flavor']) 

229 return self._cached_instance 

230 

231 @instance.setter 

232 def instance(self, instance): 

233 self._cached_instance = instance 

234 

235 @property 

236 def is_live_migration(self): 

237 return self.migration_type == fields.MigrationType.LIVE_MIGRATION 

238 

239 @property 

240 def is_resize(self): 

241 return self.migration_type == fields.MigrationType.RESIZE 

242 

243 @property 

244 def is_same_host_resize(self): 

245 return self.is_resize and self.source_node == self.dest_node 

246 

247 def get_dest_compute_id(self): 

248 """Try to determine the ComputeNode id this migration targets. 

249 

250 This should be just the dest_compute_id field, but for migrations 

251 created by older compute nodes, we may not have that set. If not, 

252 look up the compute the old way for compatibility. 

253 

254 :raises:ComputeHostNotFound if the destination compute is missing 

255 """ 

256 if 'dest_compute_id' not in self: 

257 self.dest_compute_id = ( 

258 objects.ComputeNode.get_by_host_and_nodename( 

259 self._context, 

260 self.dest_compute, 

261 self.dest_node).id) 

262 return self.dest_compute_id 

263 

264 

265@base.NovaObjectRegistry.register 

266class MigrationList(base.ObjectListBase, base.NovaObject): 

267 # Version 1.0: Initial version 

268 # Migration <= 1.1 

269 # Version 1.1: Added use_slave to get_unconfirmed_by_dest_compute 

270 # Version 1.2: Migration version 1.2 

271 # Version 1.3: Added a new function to get in progress migrations 

272 # for an instance. 

273 # Version 1.4: Added sort_keys, sort_dirs, limit, marker kwargs to 

274 # get_by_filters for migrations pagination support. 

275 # Version 1.5: Added a new function to get in progress migrations 

276 # and error migrations for a given host + node. 

277 VERSION = '1.5' 

278 

279 fields = { 

280 'objects': fields.ListOfObjectsField('Migration'), 

281 } 

282 

283 @staticmethod 

284 @db.select_db_reader_mode 

285 def _db_migration_get_unconfirmed_by_dest_compute( 

286 context, confirm_window, dest_compute, use_slave=False): 

287 return db.migration_get_unconfirmed_by_dest_compute( 

288 context, confirm_window, dest_compute) 

289 

290 @base.remotable_classmethod 

291 def get_unconfirmed_by_dest_compute(cls, context, confirm_window, 

292 dest_compute, use_slave=False): 

293 db_migrations = cls._db_migration_get_unconfirmed_by_dest_compute( 

294 context, confirm_window, dest_compute, use_slave=use_slave) 

295 return base.obj_make_list(context, cls(context), objects.Migration, 

296 db_migrations) 

297 

298 @base.remotable_classmethod 

299 def get_in_progress_by_host_and_node(cls, context, host, node): 

300 db_migrations = db.migration_get_in_progress_by_host_and_node( 

301 context, host, node) 

302 return base.obj_make_list(context, cls(context), objects.Migration, 

303 db_migrations) 

304 

305 @base.remotable_classmethod 

306 def get_by_filters(cls, context, filters, sort_keys=None, sort_dirs=None, 

307 limit=None, marker=None): 

308 db_migrations = db.migration_get_all_by_filters( 

309 context, filters, sort_keys=sort_keys, sort_dirs=sort_dirs, 

310 limit=limit, marker=marker) 

311 return base.obj_make_list(context, cls(context), objects.Migration, 

312 db_migrations) 

313 

314 @base.remotable_classmethod 

315 def get_in_progress_by_instance(cls, context, instance_uuid, 

316 migration_type=None): 

317 db_migrations = db.migration_get_in_progress_by_instance( 

318 context, instance_uuid, migration_type) 

319 return base.obj_make_list(context, cls(context), objects.Migration, 

320 db_migrations) 

321 

322 @base.remotable_classmethod 

323 def get_in_progress_and_error(cls, context, host, node): 

324 db_migrations = \ 

325 db.migration_get_in_progress_and_error_by_host_and_node( 

326 context, host, node) 

327 return base.obj_make_list(context, cls(context), objects.Migration, 

328 db_migrations)