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

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# 

14 

15import time 

16 

17from oslo_log import log 

18 

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 

25 

26CONF = conf.CONF 

27LOG = log.getLogger(__name__) 

28 

29 

30class CinderHelper(object): 

31 

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

36 

37 def get_storage_node_list(self): 

38 return list(self.cinder.services.list(binary='cinder-volume')) 

39 

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) 

51 

52 def get_storage_pool_list(self): 

53 return self.cinder.pools.list(detailed=True) 

54 

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) 

66 

67 def get_volume_list(self): 

68 return self.cinder.volumes.list(search_opts={'all_tenants': True}) 

69 

70 def get_volume_type_list(self): 

71 return self.cinder.volume_types.list() 

72 

73 def get_volume_snapshots_list(self): 

74 return self.cinder.volume_snapshots.list( 

75 search_opts={'all_tenants': True}) 

76 

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

80 

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 

85 

86 def get_volume(self, volume): 

87 

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 

90 

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) 

96 

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 

108 

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 

115 

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 

123 

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 

134 

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 

147 

148 LOG.debug("Volume %s was deleted successfully.", volume.id) 

149 return True 

150 

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 

165 

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 

186 

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

195 

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

199 

200 self.cinder.volumes.migrate_volume( 

201 volume, dest_node, False, True) 

202 

203 return self.check_migrated(volume) 

204 

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

211 

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

216 

217 self.cinder.volumes.retype( 

218 volume, dest_type, "on-demand") 

219 

220 return self.check_migrated(volume) 

221 

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) 

238 

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) 

243 

244 LOG.debug("Volume %s was created successfully.", new_volume) 

245 return new_volume 

246 

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)