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
« 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.
16import jsonschema
18from oslo_log import log
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
30CONF = conf.CONF
31LOG = log.getLogger(__name__)
34class VolumeMigrate(base.BaseAction):
35 """Migrates a volume to destination node or type
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.
44 The action schema is::
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 })
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 """
64 MIGRATION_TYPE = 'migration_type'
65 SWAP = 'swap'
66 RETYPE = 'retype'
67 MIGRATE = 'migrate'
68 DESTINATION_NODE = "destination_node"
69 DESTINATION_TYPE = "destination_type"
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)
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 }
115 def validate_parameters(self):
116 jsonschema.validate(self.input_parameters, self.schema)
117 return True
119 @property
120 def volume_id(self):
121 return self.input_parameters.get(self.RESOURCE_ID)
123 @property
124 def migration_type(self):
125 return self.input_parameters.get(self.MIGRATION_TYPE)
127 @property
128 def destination_node(self):
129 return self.input_parameters.get(self.DESTINATION_NODE)
131 @property
132 def destination_type(self):
133 return self.input_parameters.get(self.DESTINATION_TYPE)
135 def _can_swap(self, volume):
136 """Judge volume can be swapped"""
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
143 if (volume.status == 'in-use' and
144 instance_status in ('ACTIVE', 'PAUSED', 'RESIZED')):
145 return True
147 return False
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)
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)
165 def _swap_volume(self, volume, dest_type):
166 """Swap volume to dest_type
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")))
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")))
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)
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)
194 # delete old volume
195 self.cinder_util.delete_volume(volume)
197 finally:
198 keystone_util.delete_user(user)
200 return True
202 def _migrate(self, volume_id, dest_node, dest_type):
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
227 def execute(self):
228 return self._migrate(self.volume_id,
229 self.destination_node,
230 self.destination_type)
232 def revert(self):
233 LOG.warning("revert not supported")
235 def abort(self):
236 pass
238 def pre_condition(self):
239 pass
241 def post_condition(self):
242 pass
244 def get_description(self):
245 return "Moving a volume to destination_node or destination_type"