Coverage for watcher/applier/actions/volume_migration.py: 86%

108 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-17 12:22 +0000

1# Copyright 2017 NEC Corporation 

2# 

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

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

5# You may obtain 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, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 

12# implied. 

13# See the License for the specific language governing permissions and 

14# limitations under the License. 

15 

16import jsonschema 

17 

18from oslo_log import log 

19 

20from cinderclient import client as cinder_client 

21from watcher._i18n import _ 

22from watcher.applier.actions import base 

23from watcher.common import cinder_helper 

24from watcher.common import exception 

25from watcher.common import keystone_helper 

26from watcher.common import nova_helper 

27from watcher.common import utils 

28from watcher import conf 

29 

30CONF = conf.CONF 

31LOG = log.getLogger(__name__) 

32 

33 

34class VolumeMigrate(base.BaseAction): 

35 """Migrates a volume to destination node or type 

36 

37 By using this action, you will be able to migrate cinder volume. 

38 Migration type 'swap' can only be used for migrating attached volume. 

39 Migration type 'migrate' can be used for migrating detached volume to 

40 the pool of same volume type. 

41 Migration type 'retype' can be used for changing volume type of 

42 detached volume. 

43 

44 The action schema is:: 

45 

46 schema = Schema({ 

47 'resource_id': str, # should be a UUID 

48 'migration_type': str, # choices -> "swap", "migrate","retype" 

49 'destination_node': str, 

50 'destination_type': str, 

51 }) 

52 

53 The `resource_id` is the UUID of cinder volume to migrate. 

54 The `destination_node` is the destination block storage pool name. 

55 (list of available pools are returned by this command: ``cinder 

56 get-pools``) which is mandatory for migrating detached volume 

57 to the one with same volume type. 

58 The `destination_type` is the destination block storage type name. 

59 (list of available types are returned by this command: ``cinder 

60 type-list``) which is mandatory for migrating detached volume or 

61 swapping attached volume to the one with different volume type. 

62 """ 

63 

64 MIGRATION_TYPE = 'migration_type' 

65 SWAP = 'swap' 

66 RETYPE = 'retype' 

67 MIGRATE = 'migrate' 

68 DESTINATION_NODE = "destination_node" 

69 DESTINATION_TYPE = "destination_type" 

70 

71 def __init__(self, config, osc=None): 

72 super(VolumeMigrate, self).__init__(config) 

73 self.temp_username = utils.random_string(10) 

74 self.temp_password = utils.random_string(10) 

75 self.cinder_util = cinder_helper.CinderHelper(osc=self.osc) 

76 self.nova_util = nova_helper.NovaHelper(osc=self.osc) 

77 

78 @property 

79 def schema(self): 

80 return { 

81 'type': 'object', 

82 'properties': { 

83 'resource_id': { 

84 'type': 'string', 

85 "minlength": 1, 

86 "pattern": ("^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-" 

87 "([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-" 

88 "([a-fA-F0-9]){12}$") 

89 }, 

90 'resource_name': { 

91 'type': 'string', 

92 "minlength": 1 

93 }, 

94 'migration_type': { 

95 'type': 'string', 

96 "enum": ["swap", "retype", "migrate"] 

97 }, 

98 'destination_node': { 

99 "anyof": [ 

100 {'type': 'string', "minLength": 1}, 

101 {'type': 'None'} 

102 ] 

103 }, 

104 'destination_type': { 

105 "anyof": [ 

106 {'type': 'string', "minLength": 1}, 

107 {'type': 'None'} 

108 ] 

109 } 

110 }, 

111 'required': ['resource_id', 'migration_type'], 

112 'additionalProperties': False, 

113 } 

114 

115 def validate_parameters(self): 

116 jsonschema.validate(self.input_parameters, self.schema) 

117 return True 

118 

119 @property 

120 def volume_id(self): 

121 return self.input_parameters.get(self.RESOURCE_ID) 

122 

123 @property 

124 def migration_type(self): 

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

126 

127 @property 

128 def destination_node(self): 

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

130 

131 @property 

132 def destination_type(self): 

133 return self.input_parameters.get(self.DESTINATION_TYPE) 

134 

135 def _can_swap(self, volume): 

136 """Judge volume can be swapped""" 

137 

138 if not volume.attachments: 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true

139 return False 

140 instance_id = volume.attachments[0]['server_id'] 

141 instance_status = self.nova_util.find_instance(instance_id).status 

142 

143 if (volume.status == 'in-use' and 

144 instance_status in ('ACTIVE', 'PAUSED', 'RESIZED')): 

145 return True 

146 

147 return False 

148 

149 def _create_user(self, volume, user): 

150 """Create user with volume attribute and user information""" 

151 keystone_util = keystone_helper.KeystoneHelper(osc=self.osc) 

152 project_id = getattr(volume, 'os-vol-tenant-attr:tenant_id') 

153 user['project'] = project_id 

154 user['domain'] = keystone_util.get_project(project_id).domain_id 

155 user['roles'] = ['admin'] 

156 return keystone_util.create_user(user) 

157 

158 def _get_cinder_client(self, session): 

159 """Get cinder client by session""" 

160 return cinder_client.Client( 

161 CONF.cinder_client.api_version, 

162 session=session, 

163 endpoint_type=CONF.cinder_client.endpoint_type) 

164 

165 def _swap_volume(self, volume, dest_type): 

166 """Swap volume to dest_type 

167 

168 Limitation note: only for compute libvirt driver 

169 """ 

170 if not dest_type: 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true

171 raise exception.Invalid( 

172 message=(_("destination type is required when " 

173 "migration type is swap"))) 

174 

175 if not self._can_swap(volume): 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true

176 raise exception.Invalid( 

177 message=(_("Invalid state for swapping volume"))) 

178 

179 user_info = { 

180 'name': self.temp_username, 

181 'password': self.temp_password} 

182 user = self._create_user(volume, user_info) 

183 keystone_util = keystone_helper.KeystoneHelper(osc=self.osc) 

184 try: 

185 session = keystone_util.create_session( 

186 user.id, self.temp_password) 

187 temp_cinder = self._get_cinder_client(session) 

188 

189 # swap volume 

190 new_volume = self.cinder_util.create_volume( 

191 temp_cinder, volume, dest_type) 

192 self.nova_util.swap_volume(volume, new_volume) 

193 

194 # delete old volume 

195 self.cinder_util.delete_volume(volume) 

196 

197 finally: 

198 keystone_util.delete_user(user) 

199 

200 return True 

201 

202 def _migrate(self, volume_id, dest_node, dest_type): 

203 

204 try: 

205 volume = self.cinder_util.get_volume(volume_id) 

206 if self.migration_type == self.SWAP: 

207 if dest_node: 207 ↛ 209line 207 didn't jump to line 209 because the condition on line 207 was always true

208 LOG.warning("dest_node is ignored") 

209 return self._swap_volume(volume, dest_type) 

210 elif self.migration_type == self.RETYPE: 

211 return self.cinder_util.retype(volume, dest_type) 

212 elif self.migration_type == self.MIGRATE: 212 ↛ 215line 212 didn't jump to line 215 because the condition on line 212 was always true

213 return self.cinder_util.migrate(volume, dest_node) 

214 else: 

215 raise exception.Invalid( 

216 message=(_("Migration of type '%(migration_type)s' is not " 

217 "supported.") % 

218 {'migration_type': self.migration_type})) 

219 except exception.Invalid as ei: 

220 LOG.exception(ei) 

221 return False 

222 except Exception as e: 

223 LOG.critical("Unexpected exception occurred.") 

224 LOG.exception(e) 

225 return False 

226 

227 def execute(self): 

228 return self._migrate(self.volume_id, 

229 self.destination_node, 

230 self.destination_type) 

231 

232 def revert(self): 

233 LOG.warning("revert not supported") 

234 

235 def abort(self): 

236 pass 

237 

238 def pre_condition(self): 

239 pass 

240 

241 def post_condition(self): 

242 pass 

243 

244 def get_description(self): 

245 return "Moving a volume to destination_node or destination_type"