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
« 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.
16from oslo_log import log
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
26LOG = log.getLogger(__name__)
29class CinderClusterDataModelCollector(base.BaseClusterDataModelCollector):
30 """Cinder cluster data model collector
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 }
115 def __init__(self, config, osc=None):
116 super(CinderClusterDataModelCollector, self).__init__(config, osc)
118 @property
119 def notification_endpoints(self):
120 """Associated notification endpoints
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 ]
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
146 def execute(self):
147 """Build the storage cluster data model"""
148 LOG.debug("Building latest Cinder cluster data model")
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
157 builder = CinderModelBuilder(self.osc)
158 return builder.execute(self._data_model_scope)
161class CinderModelBuilder(base.BaseModelBuilder):
162 """Build the graph-based model
164 This model builder adds the following data"
165 - Storage-related knowledge (Cinder)
167 """
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)
175 def _add_physical_layer(self):
176 """Add the physical layer of the graph.
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
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)
201 def add_storage_pool(self, pool):
202 storage_pool = self._build_storage_pool(pool)
203 self.model.add_pool(storage_pool)
205 def build_storage_node(self, node):
206 """Build a storage node from a Cinder storage node
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
219 volume_type = self.call_retry(
220 self.cinder_helper.get_volume_type_by_backendname, backend)
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}
230 storage_node = element.StorageNode(**node_attributes)
231 return storage_node
233 def _build_storage_pool(self, pool):
234 """Build a storage pool from a Cinder storage pool
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.
242 attrs = ["total_volumes", "total_capacity_gb",
243 "free_capacity_gb", "provisioned_capacity_gb",
244 "allocated_capacity_gb"]
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)
257 storage_pool = element.Pool(**node_attributes)
258 return storage_pool
260 def _add_virtual_layer(self):
261 """Add the virtual layer to the graph.
263 This layer is the virtual components of the infrastructure.
264 """
265 self._add_virtual_storage()
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
283 def _build_volume_node(self, volume):
284 """Build an volume node
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]
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}
306 return element.Volume(**volume_attributes)
308 def execute(self, model_scope):
309 """Instantiates the graph with the openstack cluster data.
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