Coverage for watcher/common/cinder_helper.py: 94%
160 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# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13#
15import time
17from oslo_log import log
19from cinderclient import exceptions as cinder_exception
20from cinderclient.v3.volumes import Volume
21from watcher._i18n import _
22from watcher.common import clients
23from watcher.common import exception
24from watcher import conf
26CONF = conf.CONF
27LOG = log.getLogger(__name__)
30class CinderHelper(object):
32 def __init__(self, osc=None):
33 """:param osc: an OpenStackClients instance"""
34 self.osc = osc if osc else clients.OpenStackClients()
35 self.cinder = self.osc.cinder()
37 def get_storage_node_list(self):
38 return list(self.cinder.services.list(binary='cinder-volume'))
40 def get_storage_node_by_name(self, name):
41 """Get storage node by name(host@backendname)"""
42 try:
43 storages = [storage for storage in self.get_storage_node_list()
44 if storage.host == name]
45 if len(storages) != 1:
46 raise exception.StorageNodeNotFound(name=name)
47 return storages[0]
48 except Exception as exc:
49 LOG.exception(exc)
50 raise exception.StorageNodeNotFound(name=name)
52 def get_storage_pool_list(self):
53 return self.cinder.pools.list(detailed=True)
55 def get_storage_pool_by_name(self, name):
56 """Get pool by name(host@backend#poolname)"""
57 try:
58 pools = [pool for pool in self.get_storage_pool_list()
59 if pool.name == name]
60 if len(pools) != 1:
61 raise exception.PoolNotFound(name=name)
62 return pools[0]
63 except Exception as exc:
64 LOG.exception(exc)
65 raise exception.PoolNotFound(name=name)
67 def get_volume_list(self):
68 return self.cinder.volumes.list(search_opts={'all_tenants': True})
70 def get_volume_type_list(self):
71 return self.cinder.volume_types.list()
73 def get_volume_snapshots_list(self):
74 return self.cinder.volume_snapshots.list(
75 search_opts={'all_tenants': True})
77 def get_volume_type_by_backendname(self, backendname):
78 """Return a list of volume type"""
79 volume_type_list = self.get_volume_type_list()
81 volume_type = [volume_type.name for volume_type in volume_type_list
82 if volume_type.extra_specs.get(
83 'volume_backend_name') == backendname]
84 return volume_type
86 def get_volume(self, volume):
88 if isinstance(volume, Volume): 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 volume = volume.id
91 try:
92 volume = self.cinder.volumes.get(volume)
93 return volume
94 except cinder_exception.NotFound:
95 return self.cinder.volumes.find(name=volume)
97 def backendname_from_poolname(self, poolname):
98 """Get backendname from poolname"""
99 # pooolname formatted as host@backend#pool since ocata
100 # as of ocata, may as only host
101 backend = poolname.split('#')[0]
102 backendname = ""
103 try:
104 backendname = backend.split('@')[1]
105 except IndexError:
106 pass
107 return backendname
109 def _has_snapshot(self, volume):
110 """Judge volume has a snapshot"""
111 volume = self.get_volume(volume)
112 if volume.snapshot_id:
113 return True
114 return False
116 def get_deleting_volume(self, volume):
117 volume = self.get_volume(volume)
118 all_volume = self.get_volume_list()
119 for _volume in all_volume: 119 ↛ 120line 119 didn't jump to line 120 because the loop on line 119 never started
120 if getattr(_volume, 'os-vol-mig-status-attr:name_id') == volume.id:
121 return _volume
122 return False
124 def _can_get_volume(self, volume_id):
125 """Check to get volume with volume_id"""
126 try:
127 volume = self.get_volume(volume_id)
128 if not volume:
129 raise Exception
130 except cinder_exception.NotFound:
131 return False
132 else:
133 return True
135 def check_volume_deleted(self, volume, retry=120, retry_interval=10):
136 """Check volume has been deleted"""
137 volume = self.get_volume(volume)
138 while self._can_get_volume(volume.id) and retry:
139 volume = self.get_volume(volume.id)
140 time.sleep(retry_interval)
141 retry -= 1
142 LOG.debug("retry count: %s", retry)
143 LOG.debug("Waiting to complete deletion of volume %s", volume.id)
144 if self._can_get_volume(volume.id):
145 LOG.error("Volume deletion error: %s", volume.id)
146 return False
148 LOG.debug("Volume %s was deleted successfully.", volume.id)
149 return True
151 def check_migrated(self, volume, retry_interval=10):
152 volume = self.get_volume(volume)
153 final_status = ('success', 'error')
154 while getattr(volume, 'migration_status') not in final_status:
155 volume = self.get_volume(volume.id)
156 LOG.debug('Waiting the migration of %s', volume)
157 time.sleep(retry_interval)
158 if getattr(volume, 'migration_status') == 'error': 158 ↛ 154line 158 didn't jump to line 154 because the condition on line 158 was always true
159 host_name = getattr(volume, 'os-vol-host-attr:host')
160 error_msg = (("Volume migration error : "
161 "volume %(volume)s is now on host '%(host)s'.") %
162 {'volume': volume.id, 'host': host_name})
163 LOG.error(error_msg)
164 return False
166 host_name = getattr(volume, 'os-vol-host-attr:host')
167 if getattr(volume, 'migration_status') == 'success':
168 # check original volume deleted
169 deleting_volume = self.get_deleting_volume(volume)
170 if deleting_volume:
171 delete_id = getattr(deleting_volume, 'id')
172 if not self.check_volume_deleted(delete_id): 172 ↛ 181line 172 didn't jump to line 181 because the condition on line 172 was always true
173 return False
174 else:
175 host_name = getattr(volume, 'os-vol-host-attr:host')
176 error_msg = (("Volume migration error : "
177 "volume %(volume)s is now on host '%(host)s'.") %
178 {'volume': volume.id, 'host': host_name})
179 LOG.error(error_msg)
180 return False
181 LOG.debug(
182 "Volume migration succeeded : "
183 "volume %(volume)s is now on host '%(host)s'.",
184 {'volume': volume.id, 'host': host_name})
185 return True
187 def migrate(self, volume, dest_node):
188 """Migrate volume to dest_node"""
189 volume = self.get_volume(volume)
190 dest_backend = self.backendname_from_poolname(dest_node)
191 dest_type = self.get_volume_type_by_backendname(dest_backend)
192 if volume.volume_type not in dest_type:
193 raise exception.Invalid(
194 message=(_("Volume type must be same for migrating")))
196 source_node = getattr(volume, 'os-vol-host-attr:host')
197 LOG.debug("Volume %(volume)s found on host '%(host)s'.",
198 {'volume': volume.id, 'host': source_node})
200 self.cinder.volumes.migrate_volume(
201 volume, dest_node, False, True)
203 return self.check_migrated(volume)
205 def retype(self, volume, dest_type):
206 """Retype volume to dest_type with on-demand option"""
207 volume = self.get_volume(volume)
208 if volume.volume_type == dest_type:
209 raise exception.Invalid(
210 message=(_("Volume type must be different for retyping")))
212 source_node = getattr(volume, 'os-vol-host-attr:host')
213 LOG.debug(
214 "Volume %(volume)s found on host '%(host)s'.",
215 {'volume': volume.id, 'host': source_node})
217 self.cinder.volumes.retype(
218 volume, dest_type, "on-demand")
220 return self.check_migrated(volume)
222 def create_volume(self, cinder, volume,
223 dest_type, retry=120, retry_interval=10):
224 """Create volume of volume with dest_type using cinder"""
225 volume = self.get_volume(volume)
226 LOG.debug("start creating new volume")
227 new_volume = cinder.volumes.create(
228 getattr(volume, 'size'),
229 name=getattr(volume, 'name'),
230 volume_type=dest_type,
231 availability_zone=getattr(volume, 'availability_zone'))
232 while getattr(new_volume, 'status') != 'available' and retry:
233 new_volume = cinder.volumes.get(new_volume.id)
234 LOG.debug('Waiting volume creation of %s', new_volume)
235 time.sleep(retry_interval)
236 retry -= 1
237 LOG.debug("retry count: %s", retry)
239 if getattr(new_volume, 'status') != 'available':
240 error_msg = (_("Failed to create volume '%(volume)s. ") %
241 {'volume': new_volume.id})
242 raise Exception(error_msg)
244 LOG.debug("Volume %s was created successfully.", new_volume)
245 return new_volume
247 def delete_volume(self, volume):
248 """Delete volume"""
249 volume = self.get_volume(volume)
250 self.cinder.volumes.delete(volume)
251 result = self.check_volume_deleted(volume)
252 if not result:
253 error_msg = (_("Failed to delete volume '%(volume)s. ") %
254 {'volume': volume.id})
255 raise Exception(error_msg)