Coverage for watcher/decision_engine/model/collector/cinder.py: 86%

102 statements  

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

1# -*- encoding: utf-8 -*- 

2# Copyright 2017 NEC Corporation 

3# 

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

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

6# You may obtain a copy of the License at 

7# 

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

9# 

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

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

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

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

14# limitations under the License. 

15 

16from oslo_log import log 

17 

18from watcher.common import cinder_helper 

19from watcher.common import exception 

20from watcher.decision_engine.model.collector import base 

21from watcher.decision_engine.model import element 

22from watcher.decision_engine.model import model_root 

23from watcher.decision_engine.model.notification import cinder 

24from watcher.decision_engine.scope import storage as storage_scope 

25 

26LOG = log.getLogger(__name__) 

27 

28 

29class CinderClusterDataModelCollector(base.BaseClusterDataModelCollector): 

30 """Cinder cluster data model collector 

31 

32 The Cinder cluster data model collector creates an in-memory 

33 representation of the resources exposed by the storage service. 

34 """ 

35 SCHEMA = { 

36 "$schema": "http://json-schema.org/draft-04/schema#", 

37 "type": "array", 

38 "items": { 

39 "type": "object", 

40 "properties": { 

41 "availability_zones": { 

42 "type": "array", 

43 "items": { 

44 "type": "object", 

45 "properties": { 

46 "name": { 

47 "type": "string" 

48 } 

49 }, 

50 "additionalProperties": False 

51 } 

52 }, 

53 "volume_types": { 

54 "type": "array", 

55 "items": { 

56 "type": "object", 

57 "properties": { 

58 "name": { 

59 "type": "string" 

60 } 

61 }, 

62 "additionalProperties": False 

63 } 

64 }, 

65 "exclude": { 

66 "type": "array", 

67 "items": { 

68 "type": "object", 

69 "properties": { 

70 "storage_pools": { 

71 "type": "array", 

72 "items": { 

73 "type": "object", 

74 "properties": { 

75 "name": { 

76 "type": "string" 

77 } 

78 }, 

79 "additionalProperties": False 

80 } 

81 }, 

82 "volumes": { 

83 "type": "array", 

84 "items": { 

85 "type": "object", 

86 "properties": { 

87 "uuid": { 

88 "type": "string" 

89 } 

90 }, 

91 "additionalProperties": False 

92 } 

93 }, 

94 "projects": { 

95 "type": "array", 

96 "items": { 

97 "type": "object", 

98 "properties": { 

99 "uuid": { 

100 "type": "string" 

101 } 

102 }, 

103 "additionalProperties": False 

104 } 

105 }, 

106 "additionalProperties": False 

107 } 

108 } 

109 } 

110 }, 

111 "additionalProperties": False 

112 } 

113 } 

114 

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

116 super(CinderClusterDataModelCollector, self).__init__(config, osc) 

117 

118 @property 

119 def notification_endpoints(self): 

120 """Associated notification endpoints 

121 

122 :return: Associated notification endpoints 

123 :rtype: List of :py:class:`~.EventsNotificationEndpoint` instances 

124 """ 

125 return [ 

126 cinder.CapacityNotificationEndpoint(self), 

127 cinder.VolumeCreateEnd(self), 

128 cinder.VolumeDeleteEnd(self), 

129 cinder.VolumeUpdateEnd(self), 

130 cinder.VolumeAttachEnd(self), 

131 cinder.VolumeDetachEnd(self), 

132 cinder.VolumeResizeEnd(self) 

133 ] 

134 

135 def get_audit_scope_handler(self, audit_scope): 

136 self._audit_scope_handler = storage_scope.StorageScope( 

137 audit_scope, self.config) 

138 if self._data_model_scope is None or ( 138 ↛ 143line 138 didn't jump to line 143 because the condition on line 138 was always true

139 len(self._data_model_scope) > 0 and ( 

140 self._data_model_scope != audit_scope)): 

141 self._data_model_scope = audit_scope 

142 self._cluster_data_model = None 

143 LOG.debug("audit scope %s", audit_scope) 

144 return self._audit_scope_handler 

145 

146 def execute(self): 

147 """Build the storage cluster data model""" 

148 LOG.debug("Building latest Cinder cluster data model") 

149 

150 if self._audit_scope_handler is None: 150 ↛ 151line 150 didn't jump to line 151 because the condition on line 150 was never true

151 LOG.debug("No audit, Don't Build storage data model") 

152 return 

153 if self._data_model_scope is None: 

154 LOG.debug("No audit scope, Don't Build storage data model") 

155 return 

156 

157 builder = CinderModelBuilder(self.osc) 

158 return builder.execute(self._data_model_scope) 

159 

160 

161class CinderModelBuilder(base.BaseModelBuilder): 

162 """Build the graph-based model 

163 

164 This model builder adds the following data" 

165 - Storage-related knowledge (Cinder) 

166 

167 """ 

168 

169 def __init__(self, osc): 

170 self.osc = osc 

171 self.model = model_root.StorageModelRoot() 

172 self.cinder = osc.cinder() 

173 self.cinder_helper = cinder_helper.CinderHelper(osc=self.osc) 

174 

175 def _add_physical_layer(self): 

176 """Add the physical layer of the graph. 

177 

178 This includes components which represent actual infrastructure 

179 hardware. 

180 """ 

181 for snode in self.call_retry( 

182 self.cinder_helper.get_storage_node_list): 

183 self.add_storage_node(snode) 

184 for pool in self.call_retry(self.cinder_helper.get_storage_pool_list): 

185 pool = self._build_storage_pool(pool) 

186 self.model.add_pool(pool) 

187 storage_name = getattr(pool, 'name') 

188 try: 

189 storage_node = self.model.get_node_by_name( 

190 storage_name) 

191 # Connect the instance to its compute node 

192 self.model.map_pool(pool, storage_node) 

193 except exception.StorageNodeNotFound: 

194 continue 

195 

196 def add_storage_node(self, node): 

197 # Build and add base node. 

198 storage_node = self.build_storage_node(node) 

199 self.model.add_node(storage_node) 

200 

201 def add_storage_pool(self, pool): 

202 storage_pool = self._build_storage_pool(pool) 

203 self.model.add_pool(storage_pool) 

204 

205 def build_storage_node(self, node): 

206 """Build a storage node from a Cinder storage node 

207 

208 :param node: A storage node 

209 :type node: :py:class:`~cinderclient.v3.services.Service` 

210 """ 

211 # node.host is formatted as host@backendname since ocata, 

212 # or may be only host as of ocata 

213 backend = "" 

214 try: 

215 backend = node.host.split('@')[1] 

216 except IndexError: 

217 pass 

218 

219 volume_type = self.call_retry( 

220 self.cinder_helper.get_volume_type_by_backendname, backend) 

221 

222 # build up the storage node. 

223 node_attributes = { 

224 "host": node.host, 

225 "zone": node.zone, 

226 "state": node.state, 

227 "status": node.status, 

228 "volume_type": volume_type} 

229 

230 storage_node = element.StorageNode(**node_attributes) 

231 return storage_node 

232 

233 def _build_storage_pool(self, pool): 

234 """Build a storage pool from a Cinder storage pool 

235 

236 :param pool: A storage pool 

237 :type pool: :py:class:`~cinderclient.v3.pools.Pool` 

238 :raises: exception.InvalidPoolAttributeValue 

239 """ 

240 # build up the storage pool. 

241 

242 attrs = ["total_volumes", "total_capacity_gb", 

243 "free_capacity_gb", "provisioned_capacity_gb", 

244 "allocated_capacity_gb"] 

245 

246 node_attributes = {"name": pool.name} 

247 for attr in attrs: 

248 try: 

249 node_attributes[attr] = int(getattr(pool, attr)) 

250 except AttributeError: 

251 LOG.debug("Attribute %s for pool %s is not provided", 

252 attr, pool.name) 

253 except ValueError: 

254 raise exception.InvalidPoolAttributeValue( 

255 name=pool.name, attribute=attr) 

256 

257 storage_pool = element.Pool(**node_attributes) 

258 return storage_pool 

259 

260 def _add_virtual_layer(self): 

261 """Add the virtual layer to the graph. 

262 

263 This layer is the virtual components of the infrastructure. 

264 """ 

265 self._add_virtual_storage() 

266 

267 def _add_virtual_storage(self): 

268 volumes = self.call_retry(self.cinder_helper.get_volume_list) 

269 for vol in volumes: 

270 volume = self._build_volume_node(vol) 

271 self.model.add_volume(volume) 

272 pool_name = getattr(vol, 'os-vol-host-attr:host') 

273 if pool_name is None: 273 ↛ 275line 273 didn't jump to line 275 because the condition on line 273 was never true

274 # The volume is not attached to any pool 

275 continue 

276 try: 

277 pool = self.model.get_pool_by_pool_name( 

278 pool_name) 

279 self.model.map_volume(volume, pool) 

280 except exception.PoolNotFound: 

281 continue 

282 

283 def _build_volume_node(self, volume): 

284 """Build an volume node 

285 

286 Create an volume node for the graph using cinder and the 

287 `volume` cinder object. 

288 :param instance: Cinder Volume object. 

289 :return: A volume node for the graph. 

290 """ 

291 attachments = [{k: v for k, v in iter(d.items()) if k in ( 

292 'server_id', 'attachment_id')} for d in volume.attachments] 

293 

294 volume_attributes = { 

295 "uuid": volume.id, 

296 "size": volume.size, 

297 "status": volume.status, 

298 "attachments": attachments, 

299 "name": volume.name or "", 

300 "multiattach": volume.multiattach, 

301 "snapshot_id": volume.snapshot_id or "", 

302 "project_id": getattr(volume, 'os-vol-tenant-attr:tenant_id'), 

303 "metadata": volume.metadata, 

304 "bootable": volume.bootable} 

305 

306 return element.Volume(**volume_attributes) 

307 

308 def execute(self, model_scope): 

309 """Instantiates the graph with the openstack cluster data. 

310 

311 The graph is populated along 2 layers: virtual and physical. As each 

312 new layer is built connections are made back to previous layers. 

313 """ 

314 # TODO(Dantali0n): Use scope to limit size of model 

315 self._add_physical_layer() 

316 self._add_virtual_layer() 

317 return self.model