summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorspace <fengzhr@awcloud.com>2017-10-24 03:36:49 -0400
committerKaifeng Wang <kaifeng.w@gmail.com>2019-01-07 17:31:15 +0800
commitd278bb6f779eb8941754b29dbb6083a732330783 (patch)
tree3c6edf33f85e9c43294d8404c9406ad2d5bed34f
parenta8c1d06bd0badf1a24ede8194804c7bee06a9aa0 (diff)
introspection data backend: plugin layer
Configurable introspection data storage backend [1] is proposed to provide flexible extension of introspection data storage instead of the single support of Swift storage backend. This patch adds plugin mechanism for loading introspection storage, creates database backend and moves Swift storage into a plugin. [1] http://specs.openstack.org/openstack/ironic-inspector-specs/specs/configurable-introspection-data-backends.html Story: 1726713 Task: 11373 Co-Authored-By: Kaifeng Wang <kaifeng.w@gmail.com> Change-Id: Ie4d09dc0afc441b20a1e5e3bd8e742b1df918954
Notes
Notes (review): Code-Review+2: Dmitry Tantsur <divius.inside@gmail.com> Code-Review+2: Julia Kreger <juliaashleykreger@gmail.com> Workflow+1: Julia Kreger <juliaashleykreger@gmail.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Wed, 23 Jan 2019 03:29:15 +0000 Reviewed-on: https://review.openstack.org/514552 Project: openstack/ironic-inspector Branch: refs/heads/master
-rw-r--r--ironic_inspector/conductor/manager.py11
-rw-r--r--ironic_inspector/conf/processing.py8
-rw-r--r--ironic_inspector/main.py21
-rw-r--r--ironic_inspector/plugins/base.py10
-rw-r--r--ironic_inspector/plugins/introspection_data.py123
-rw-r--r--ironic_inspector/process.py58
-rw-r--r--ironic_inspector/test/unit/test_main.py78
-rw-r--r--ironic_inspector/test/unit/test_manager.py79
-rw-r--r--ironic_inspector/test/unit/test_plugins_introspection_data.py108
-rw-r--r--ironic_inspector/test/unit/test_process.py52
-rw-r--r--ironic_inspector/utils.py4
-rw-r--r--releasenotes/notes/introspection-data-db-store-0586292de05cbfd7.yaml6
-rw-r--r--setup.cfg4
13 files changed, 442 insertions, 120 deletions
diff --git a/ironic_inspector/conductor/manager.py b/ironic_inspector/conductor/manager.py
index c4ab958..568783f 100644
--- a/ironic_inspector/conductor/manager.py
+++ b/ironic_inspector/conductor/manager.py
@@ -22,6 +22,7 @@ from oslo_log import log
22import oslo_messaging as messaging 22import oslo_messaging as messaging
23from oslo_utils import reflection 23from oslo_utils import reflection
24 24
25from ironic_inspector.common.i18n import _
25from ironic_inspector.common import ironic as ir_utils 26from ironic_inspector.common import ironic as ir_utils
26from ironic_inspector import db 27from ironic_inspector import db
27from ironic_inspector import introspect 28from ironic_inspector import introspect
@@ -126,7 +127,15 @@ class ConductorManager(object):
126 127
127 @messaging.expected_exceptions(utils.Error) 128 @messaging.expected_exceptions(utils.Error)
128 def do_reapply(self, context, node_id, token=None): 129 def do_reapply(self, context, node_id, token=None):
129 process.reapply(node_id) 130 try:
131 data = process.get_introspection_data(node_id, processed=False,
132 get_json=True)
133 except utils.IntrospectionDataStoreDisabled:
134 raise utils.Error(_('Inspector is not configured to store '
135 'data. Set the [processing]store_data '
136 'configuration option to change this.'),
137 code=400)
138 process.reapply(node_id, data)
130 139
131 140
132def periodic_clean_up(): # pragma: no cover 141def periodic_clean_up(): # pragma: no cover
diff --git a/ironic_inspector/conf/processing.py b/ironic_inspector/conf/processing.py
index 9840f47..502c284 100644
--- a/ironic_inspector/conf/processing.py
+++ b/ironic_inspector/conf/processing.py
@@ -18,7 +18,6 @@ from ironic_inspector.common.i18n import _
18 18
19VALID_ADD_PORTS_VALUES = ('all', 'active', 'pxe', 'disabled') 19VALID_ADD_PORTS_VALUES = ('all', 'active', 'pxe', 'disabled')
20VALID_KEEP_PORTS_VALUES = ('all', 'present', 'added') 20VALID_KEEP_PORTS_VALUES = ('all', 'present', 'added')
21VALID_STORE_DATA_VALUES = ('none', 'swift')
22 21
23 22
24_OPTS = [ 23_OPTS = [
@@ -75,9 +74,10 @@ _OPTS = [
75 'aware of. This hook is ignored by default.')), 74 'aware of. This hook is ignored by default.')),
76 cfg.StrOpt('store_data', 75 cfg.StrOpt('store_data',
77 default='none', 76 default='none',
78 choices=VALID_STORE_DATA_VALUES, 77 help=_('The storage backend for storing introspection data. '
79 help=_('Method for storing introspection data. If set to \'none' 78 'Possible values are: \'none\', \'database\' and '
80 '\', introspection data will not be stored.')), 79 '\'swift\'. If set to \'none\', introspection data will '
80 'not be stored.')),
81 cfg.StrOpt('store_data_location', 81 cfg.StrOpt('store_data_location',
82 help=_('Name of the key to store the location of stored data ' 82 help=_('Name of the key to store the location of stored data '
83 'in the extra column of the Ironic database.')), 83 'in the extra column of the Ironic database.')),
diff --git a/ironic_inspector/main.py b/ironic_inspector/main.py
index 4a4cd50..e30da02 100644
--- a/ironic_inspector/main.py
+++ b/ironic_inspector/main.py
@@ -25,7 +25,6 @@ from ironic_inspector.common import context
25from ironic_inspector.common.i18n import _ 25from ironic_inspector.common.i18n import _
26from ironic_inspector.common import ironic as ir_utils 26from ironic_inspector.common import ironic as ir_utils
27from ironic_inspector.common import rpc 27from ironic_inspector.common import rpc
28from ironic_inspector.common import swift
29import ironic_inspector.conf 28import ironic_inspector.conf
30from ironic_inspector.conf import opts as conf_opts 29from ironic_inspector.conf import opts as conf_opts
31from ironic_inspector import node_cache 30from ironic_inspector import node_cache
@@ -289,15 +288,15 @@ def api_introspection_abort(node_id):
289@api('/v1/introspection/<node_id>/data', rule="introspection:data", 288@api('/v1/introspection/<node_id>/data', rule="introspection:data",
290 methods=['GET']) 289 methods=['GET'])
291def api_introspection_data(node_id): 290def api_introspection_data(node_id):
292 if CONF.processing.store_data == 'swift': 291 try:
293 if not uuidutils.is_uuid_like(node_id): 292 if not uuidutils.is_uuid_like(node_id):
294 node = ir_utils.get_node(node_id, fields=['uuid']) 293 node = ir_utils.get_node(node_id, fields=['uuid'])
295 node_id = node.uuid 294 node_id = node.uuid
296 res = swift.get_introspection_data(node_id) 295 res = process.get_introspection_data(node_id)
297 return res, 200, {'Content-Type': 'application/json'} 296 return res, 200, {'Content-Type': 'application/json'}
298 else: 297 except utils.IntrospectionDataStoreDisabled:
299 return error_response(_('Inspector is not configured to store data. ' 298 return error_response(_('Inspector is not configured to store data. '
300 'Set the [processing] store_data ' 299 'Set the [processing]store_data '
301 'configuration option to change this.'), 300 'configuration option to change this.'),
302 code=404) 301 code=404)
303 302
@@ -309,15 +308,9 @@ def api_introspection_reapply(node_id):
309 return error_response(_('User data processing is not ' 308 return error_response(_('User data processing is not '
310 'supported yet'), code=400) 309 'supported yet'), code=400)
311 310
312 if CONF.processing.store_data == 'swift': 311 client = rpc.get_client()
313 client = rpc.get_client() 312 client.call({}, 'do_reapply', node_id=node_id)
314 client.call({}, 'do_reapply', node_id=node_id) 313 return '', 202
315 return '', 202
316 else:
317 return error_response(_('Inspector is not configured to store'
318 ' data. Set the [processing] '
319 'store_data configuration option to '
320 'change this.'), code=400)
321 314
322 315
323def rule_repr(rule, short): 316def rule_repr(rule, short):
diff --git a/ironic_inspector/plugins/base.py b/ironic_inspector/plugins/base.py
index bfd8322..3d2a12a 100644
--- a/ironic_inspector/plugins/base.py
+++ b/ironic_inspector/plugins/base.py
@@ -142,6 +142,7 @@ _HOOKS_MGR = None
142_NOT_FOUND_HOOK_MGR = None 142_NOT_FOUND_HOOK_MGR = None
143_CONDITIONS_MGR = None 143_CONDITIONS_MGR = None
144_ACTIONS_MGR = None 144_ACTIONS_MGR = None
145_INTROSPECTION_DATA_MGR = None
145 146
146 147
147def missing_entrypoints_callback(names): 148def missing_entrypoints_callback(names):
@@ -229,3 +230,12 @@ def rule_actions_manager():
229 'ironic_inspector.rules.actions', 230 'ironic_inspector.rules.actions',
230 invoke_on_load=True) 231 invoke_on_load=True)
231 return _ACTIONS_MGR 232 return _ACTIONS_MGR
233
234
235def introspection_data_manager():
236 global _INTROSPECTION_DATA_MGR
237 if _INTROSPECTION_DATA_MGR is None:
238 _INTROSPECTION_DATA_MGR = stevedore.ExtensionManager(
239 'ironic_inspector.introspection_data.store',
240 invoke_on_load=True)
241 return _INTROSPECTION_DATA_MGR
diff --git a/ironic_inspector/plugins/introspection_data.py b/ironic_inspector/plugins/introspection_data.py
new file mode 100644
index 0000000..e0d8d6a
--- /dev/null
+++ b/ironic_inspector/plugins/introspection_data.py
@@ -0,0 +1,123 @@
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"""Backends for storing introspection data."""
15
16import abc
17import json
18
19from oslo_config import cfg
20from oslo_utils import excutils
21import six
22
23from ironic_inspector.common import swift
24from ironic_inspector import node_cache
25from ironic_inspector import utils
26
27
28CONF = cfg.CONF
29
30LOG = utils.getProcessingLogger(__name__)
31
32_STORAGE_EXCLUDED_KEYS = {'logs'}
33_UNPROCESSED_DATA_STORE_SUFFIX = 'UNPROCESSED'
34
35
36def _filter_data_excluded_keys(data):
37 return {k: v for k, v in data.items()
38 if k not in _STORAGE_EXCLUDED_KEYS}
39
40
41@six.add_metaclass(abc.ABCMeta)
42class BaseStorageBackend(object):
43
44 @abc.abstractmethod
45 def get(self, node_id, processed=True, get_json=False):
46 """Get introspected data from storage backend.
47
48 :param node_id: node UUID or name.
49 :param processed: Specify whether the data to be retrieved is
50 processed or not.
51 :param get_json: Specify whether return the introspection data in json
52 format, string value is returned if False.
53 :returns: the introspection data.
54 :raises: IntrospectionDataStoreDisabled if storage backend is disabled.
55 """
56
57 @abc.abstractmethod
58 def save(self, node_info, data, processed=True):
59 """Save introspected data to storage backend.
60
61 :param node_info: a NodeInfo object.
62 :param data: the introspected data to be saved, in dict format.
63 :param processed: Specify whether the data to be saved is processed or
64 not.
65 :raises: IntrospectionDataStoreDisabled if storage backend is disabled.
66 """
67
68
69class NoStore(BaseStorageBackend):
70 def get(self, node_id, processed=True, get_json=False):
71 raise utils.IntrospectionDataStoreDisabled(
72 'Introspection data storage is disabled')
73
74 def save(self, node_info, data, processed=True):
75 LOG.debug('Introspection data storage is disabled, the data will not '
76 'be saved', node_info=node_info)
77
78
79class SwiftStore(object):
80 def get(self, node_id, processed=True, get_json=False):
81 suffix = None if processed else _UNPROCESSED_DATA_STORE_SUFFIX
82 LOG.debug('Fetching introspection data from Swift for %s', node_id)
83 data = swift.get_introspection_data(node_id, suffix=suffix)
84 if get_json:
85 return json.loads(data)
86 return data
87
88 def save(self, node_info, data, processed=True):
89 suffix = None if processed else _UNPROCESSED_DATA_STORE_SUFFIX
90 swift_object_name = swift.store_introspection_data(
91 _filter_data_excluded_keys(data),
92 node_info.uuid,
93 suffix=suffix
94 )
95 LOG.info('Introspection data was stored in Swift object %s',
96 swift_object_name, node_info=node_info)
97 if CONF.processing.store_data_location:
98 node_info.patch([{'op': 'add', 'path': '/extra/%s' %
99 CONF.processing.store_data_location,
100 'value': swift_object_name}])
101
102
103class DatabaseStore(object):
104 def get(self, node_id, processed=True, get_json=False):
105 LOG.debug('Fetching introspection data from database for %(node)s',
106 {'node': node_id})
107 data = node_cache.get_introspection_data(node_id, processed)
108 if get_json:
109 return data
110 return json.dumps(data)
111
112 def save(self, node_info, data, processed=True):
113 introspection_data = _filter_data_excluded_keys(data)
114 try:
115 node_cache.store_introspection_data(node_info.uuid,
116 introspection_data, processed)
117 except Exception as e:
118 with excutils.save_and_reraise_exception():
119 LOG.exception('Failed to store introspection data in '
120 'database: %(exc)s', {'exc': e})
121 else:
122 LOG.info('Introspection data was stored in database',
123 node_info=node_info)
diff --git a/ironic_inspector/process.py b/ironic_inspector/process.py
index f21302f..4f86bbe 100644
--- a/ironic_inspector/process.py
+++ b/ironic_inspector/process.py
@@ -15,7 +15,6 @@
15 15
16import copy 16import copy
17import datetime 17import datetime
18import json
19import os 18import os
20 19
21from oslo_config import cfg 20from oslo_config import cfg
@@ -25,7 +24,6 @@ from oslo_utils import timeutils
25 24
26from ironic_inspector.common.i18n import _ 25from ironic_inspector.common.i18n import _
27from ironic_inspector.common import ironic as ir_utils 26from ironic_inspector.common import ironic as ir_utils
28from ironic_inspector.common import swift
29from ironic_inspector import introspection_state as istate 27from ironic_inspector import introspection_state as istate
30from ironic_inspector import node_cache 28from ironic_inspector import node_cache
31from ironic_inspector.plugins import base as plugins_base 29from ironic_inspector.plugins import base as plugins_base
@@ -38,7 +36,6 @@ CONF = cfg.CONF
38LOG = utils.getProcessingLogger(__name__) 36LOG = utils.getProcessingLogger(__name__)
39 37
40_STORAGE_EXCLUDED_KEYS = {'logs'} 38_STORAGE_EXCLUDED_KEYS = {'logs'}
41_UNPROCESSED_DATA_STORE_SUFFIX = 'UNPROCESSED'
42 39
43 40
44def _store_logs(introspection_data, node_info): 41def _store_logs(introspection_data, node_info):
@@ -143,48 +140,28 @@ def _filter_data_excluded_keys(data):
143 if k not in _STORAGE_EXCLUDED_KEYS} 140 if k not in _STORAGE_EXCLUDED_KEYS}
144 141
145 142
146def _store_data(node_info, data, suffix=None): 143def _store_data(node_info, data, processed=True):
147 if CONF.processing.store_data != 'swift': 144 introspection_data_manager = plugins_base.introspection_data_manager()
148 LOG.debug("Swift support is disabled, introspection data " 145 store = CONF.processing.store_data
149 "won't be stored", node_info=node_info) 146 ext = introspection_data_manager[store].obj
150 return 147 ext.save(node_info, data, processed)
151
152 swift_object_name = swift.store_introspection_data(
153 _filter_data_excluded_keys(data),
154 node_info.uuid,
155 suffix=suffix
156 )
157 LOG.info('Introspection data was stored in Swift in object '
158 '%s', swift_object_name, node_info=node_info)
159 if CONF.processing.store_data_location:
160 node_info.patch([{'op': 'add', 'path': '/extra/%s' %
161 CONF.processing.store_data_location,
162 'value': swift_object_name}])
163 148
164 149
165def _store_unprocessed_data(node_info, data): 150def _store_unprocessed_data(node_info, data):
166 # runs in background 151 # runs in background
167 try: 152 try:
168 _store_data(node_info, data, 153 _store_data(node_info, data, processed=False)
169 suffix=_UNPROCESSED_DATA_STORE_SUFFIX)
170 except Exception: 154 except Exception:
171 LOG.exception('Encountered exception saving unprocessed ' 155 LOG.exception('Encountered exception saving unprocessed '
172 'introspection data', node_info=node_info, 156 'introspection data', node_info=node_info,
173 data=data) 157 data=data)
174 158
175 159
176def _get_unprocessed_data(uuid): 160def get_introspection_data(uuid, processed=True, get_json=False):
177 if CONF.processing.store_data == 'swift': 161 introspection_data_manager = plugins_base.introspection_data_manager()
178 LOG.debug('Fetching unprocessed introspection data from ' 162 store = CONF.processing.store_data
179 'Swift for %s', uuid) 163 ext = introspection_data_manager[store].obj
180 return json.loads( 164 return ext.get(uuid, processed=processed, get_json=get_json)
181 swift.get_introspection_data(
182 uuid,
183 suffix=_UNPROCESSED_DATA_STORE_SUFFIX
184 )
185 )
186 else:
187 raise utils.Error(_('Swift support is disabled'), code=400)
188 165
189 166
190def process(introspection_data): 167def process(introspection_data):
@@ -309,7 +286,7 @@ def _finish(node_info, ironic, introspection_data, power_off=True):
309 node_info=node_info, data=introspection_data) 286 node_info=node_info, data=introspection_data)
310 287
311 288
312def reapply(node_ident): 289def reapply(node_ident, data=None):
313 """Re-apply introspection steps. 290 """Re-apply introspection steps.
314 291
315 Re-apply preprocessing, postprocessing and introspection rules on 292 Re-apply preprocessing, postprocessing and introspection rules on
@@ -331,15 +308,20 @@ def reapply(node_ident):
331 raise utils.Error(_('Node locked, please, try again later'), 308 raise utils.Error(_('Node locked, please, try again later'),
332 node_info=node_info, code=409) 309 node_info=node_info, code=409)
333 310
334 utils.executor().submit(_reapply, node_info) 311 utils.executor().submit(_reapply, node_info, data)
335 312
336 313
337def _reapply(node_info): 314def _reapply(node_info, data=None):
338 # runs in background 315 # runs in background
339 try: 316 try:
340 node_info.started_at = timeutils.utcnow() 317 node_info.started_at = timeutils.utcnow()
341 node_info.commit() 318 node_info.commit()
342 introspection_data = _get_unprocessed_data(node_info.uuid) 319 if data:
320 introspection_data = data
321 else:
322 introspection_data = get_introspection_data(node_info.uuid,
323 processed=False,
324 get_json=True)
343 except Exception as exc: 325 except Exception as exc:
344 LOG.exception('Encountered exception while fetching ' 326 LOG.exception('Encountered exception while fetching '
345 'stored introspection data', 327 'stored introspection data',
diff --git a/ironic_inspector/test/unit/test_main.py b/ironic_inspector/test/unit/test_main.py
index 1e24f16..c1b45e1 100644
--- a/ironic_inspector/test/unit/test_main.py
+++ b/ironic_inspector/test/unit/test_main.py
@@ -22,6 +22,7 @@ from oslo_utils import uuidutils
22 22
23from ironic_inspector.common import ironic as ir_utils 23from ironic_inspector.common import ironic as ir_utils
24from ironic_inspector.common import rpc 24from ironic_inspector.common import rpc
25from ironic_inspector.common import swift
25import ironic_inspector.conf 26import ironic_inspector.conf
26from ironic_inspector.conf import opts as conf_opts 27from ironic_inspector.conf import opts as conf_opts
27from ironic_inspector import introspection_state as istate 28from ironic_inspector import introspection_state as istate
@@ -29,6 +30,7 @@ from ironic_inspector import main
29from ironic_inspector import node_cache 30from ironic_inspector import node_cache
30from ironic_inspector.plugins import base as plugins_base 31from ironic_inspector.plugins import base as plugins_base
31from ironic_inspector.plugins import example as example_plugin 32from ironic_inspector.plugins import example as example_plugin
33from ironic_inspector.plugins import introspection_data as intros_data_plugin
32from ironic_inspector import process 34from ironic_inspector import process
33from ironic_inspector import rules 35from ironic_inspector import rules
34from ironic_inspector.test import base as test_base 36from ironic_inspector.test import base as test_base
@@ -297,10 +299,9 @@ class TestApiListStatus(GetStatusAPIBaseTest):
297 299
298 300
299class TestApiGetData(BaseAPITest): 301class TestApiGetData(BaseAPITest):
300 @mock.patch.object(main.swift, 'SwiftAPI', autospec=True) 302 def setUp(self):
301 def test_get_introspection_data(self, swift_mock): 303 super(TestApiGetData, self).setUp()
302 CONF.set_override('store_data', 'swift', 'processing') 304 self.introspection_data = {
303 data = {
304 'ipmi_address': '1.2.3.4', 305 'ipmi_address': '1.2.3.4',
305 'cpus': 2, 306 'cpus': 2,
306 'cpu_arch': 'x86_64', 307 'cpu_arch': 'x86_64',
@@ -310,44 +311,48 @@ class TestApiGetData(BaseAPITest):
310 'em1': {'mac': '11:22:33:44:55:66', 'ip': '1.2.0.1'}, 311 'em1': {'mac': '11:22:33:44:55:66', 'ip': '1.2.0.1'},
311 } 312 }
312 } 313 }
314
315 @mock.patch.object(swift, 'SwiftAPI', autospec=True)
316 def test_get_introspection_data_from_swift(self, swift_mock):
317 CONF.set_override('store_data', 'swift', 'processing')
313 swift_conn = swift_mock.return_value 318 swift_conn = swift_mock.return_value
314 swift_conn.get_object.return_value = json.dumps(data) 319 swift_conn.get_object.return_value = json.dumps(
320 self.introspection_data)
315 res = self.app.get('/v1/introspection/%s/data' % self.uuid) 321 res = self.app.get('/v1/introspection/%s/data' % self.uuid)
316 name = 'inspector_data-%s' % self.uuid 322 name = 'inspector_data-%s' % self.uuid
317 swift_conn.get_object.assert_called_once_with(name) 323 swift_conn.get_object.assert_called_once_with(name)
318 self.assertEqual(200, res.status_code) 324 self.assertEqual(200, res.status_code)
319 self.assertEqual(data, json.loads(res.data.decode('utf-8'))) 325 self.assertEqual(self.introspection_data,
326 json.loads(res.data.decode('utf-8')))
327
328 @mock.patch.object(intros_data_plugin, 'DatabaseStore',
329 autospec=True)
330 def test_get_introspection_data_from_db(self, db_mock):
331 CONF.set_override('store_data', 'database', 'processing')
332 db_store = db_mock.return_value
333 db_store.get.return_value = json.dumps(self.introspection_data)
334 res = self.app.get('/v1/introspection/%s/data' % self.uuid)
335 db_store.get.assert_called_once_with(self.uuid, processed=True,
336 get_json=False)
337 self.assertEqual(200, res.status_code)
338 self.assertEqual(self.introspection_data,
339 json.loads(res.data.decode('utf-8')))
320 340
321 @mock.patch.object(main.swift, 'SwiftAPI', autospec=True) 341 def test_introspection_data_not_stored(self):
322 def test_introspection_data_not_stored(self, swift_mock):
323 CONF.set_override('store_data', 'none', 'processing') 342 CONF.set_override('store_data', 'none', 'processing')
324 swift_conn = swift_mock.return_value
325 res = self.app.get('/v1/introspection/%s/data' % self.uuid) 343 res = self.app.get('/v1/introspection/%s/data' % self.uuid)
326 self.assertFalse(swift_conn.get_object.called)
327 self.assertEqual(404, res.status_code) 344 self.assertEqual(404, res.status_code)
328 345
329 @mock.patch.object(ir_utils, 'get_node', autospec=True) 346 @mock.patch.object(ir_utils, 'get_node', autospec=True)
330 @mock.patch.object(main.swift, 'SwiftAPI', autospec=True) 347 @mock.patch.object(main.process, 'get_introspection_data', autospec=True)
331 def test_with_name(self, swift_mock, get_mock): 348 def test_with_name(self, process_mock, get_mock):
332 get_mock.return_value = mock.Mock(uuid=self.uuid) 349 get_mock.return_value = mock.Mock(uuid=self.uuid)
333 CONF.set_override('store_data', 'swift', 'processing') 350 CONF.set_override('store_data', 'swift', 'processing')
334 data = { 351 process_mock.return_value = json.dumps(self.introspection_data)
335 'ipmi_address': '1.2.3.4',
336 'cpus': 2,
337 'cpu_arch': 'x86_64',
338 'memory_mb': 1024,
339 'local_gb': 20,
340 'interfaces': {
341 'em1': {'mac': '11:22:33:44:55:66', 'ip': '1.2.0.1'},
342 }
343 }
344 swift_conn = swift_mock.return_value
345 swift_conn.get_object.return_value = json.dumps(data)
346 res = self.app.get('/v1/introspection/name1/data') 352 res = self.app.get('/v1/introspection/name1/data')
347 name = 'inspector_data-%s' % self.uuid
348 swift_conn.get_object.assert_called_once_with(name)
349 self.assertEqual(200, res.status_code) 353 self.assertEqual(200, res.status_code)
350 self.assertEqual(data, json.loads(res.data.decode('utf-8'))) 354 self.assertEqual(self.introspection_data,
355 json.loads(res.data.decode('utf-8')))
351 get_mock.assert_called_once_with('name1', fields=['uuid']) 356 get_mock.assert_called_once_with('name1', fields=['uuid'])
352 357
353 358
@@ -361,8 +366,7 @@ class TestApiReapply(BaseAPITest):
361 self.rpc_get_client_mock.return_value = self.client_mock 366 self.rpc_get_client_mock.return_value = self.client_mock
362 CONF.set_override('store_data', 'swift', 'processing') 367 CONF.set_override('store_data', 'swift', 'processing')
363 368
364 def test_ok(self): 369 def test_api_ok(self):
365
366 self.app.post('/v1/introspection/%s/data/unprocessed' % 370 self.app.post('/v1/introspection/%s/data/unprocessed' %
367 self.uuid) 371 self.uuid)
368 self.client_mock.call.assert_called_once_with({}, 'do_reapply', 372 self.client_mock.call.assert_called_once_with({}, 'do_reapply',
@@ -377,18 +381,18 @@ class TestApiReapply(BaseAPITest):
377 message) 381 message)
378 self.assertFalse(self.client_mock.call.called) 382 self.assertFalse(self.client_mock.call.called)
379 383
380 def test_swift_disabled(self): 384 def test_get_introspection_data_error(self):
381 CONF.set_override('store_data', 'none', 'processing') 385 exc = utils.Error('The store is crashed', code=404)
386 self.client_mock.call.side_effect = exc
382 387
383 res = self.app.post('/v1/introspection/%s/data/unprocessed' % 388 res = self.app.post('/v1/introspection/%s/data/unprocessed' %
384 self.uuid) 389 self.uuid)
385 self.assertEqual(400, res.status_code) 390
391 self.assertEqual(404, res.status_code)
386 message = json.loads(res.data.decode())['error']['message'] 392 message = json.loads(res.data.decode())['error']['message']
387 self.assertEqual('Inspector is not configured to store ' 393 self.assertEqual(str(exc), message)
388 'data. Set the [processing] store_data ' 394 self.client_mock.call.assert_called_once_with({}, 'do_reapply',
389 'configuration option to change this.', 395 node_id=self.uuid)
390 message)
391 self.assertFalse(self.client_mock.call.called)
392 396
393 def test_generic_error(self): 397 def test_generic_error(self):
394 exc = utils.Error('Oops', code=400) 398 exc = utils.Error('Oops', code=400)
diff --git a/ironic_inspector/test/unit/test_manager.py b/ironic_inspector/test/unit/test_manager.py
index 71131f7..e374441 100644
--- a/ironic_inspector/test/unit/test_manager.py
+++ b/ironic_inspector/test/unit/test_manager.py
@@ -11,10 +11,13 @@
11# See the License for the specific language governing permissions and 11# See the License for the specific language governing permissions and
12# limitations under the License. 12# limitations under the License.
13 13
14import json
15
14import fixtures 16import fixtures
15import mock 17import mock
16import oslo_messaging as messaging 18import oslo_messaging as messaging
17 19
20from ironic_inspector.common import swift
18from ironic_inspector.conductor import manager 21from ironic_inspector.conductor import manager
19import ironic_inspector.conf 22import ironic_inspector.conf
20from ironic_inspector import introspect 23from ironic_inspector import introspect
@@ -302,11 +305,17 @@ class TestManagerReapply(BaseManagerTest):
302 super(TestManagerReapply, self).setUp() 305 super(TestManagerReapply, self).setUp()
303 CONF.set_override('store_data', 'swift', 'processing') 306 CONF.set_override('store_data', 'swift', 'processing')
304 307
305 def test_ok(self, reapply_mock): 308 @mock.patch.object(swift, 'store_introspection_data', autospec=True)
309 @mock.patch.object(swift, 'get_introspection_data', autospec=True)
310 def test_ok(self, swift_get_mock, swift_set_mock, reapply_mock):
311 swift_get_mock.return_value = json.dumps(self.data)
306 self.manager.do_reapply(self.context, self.uuid) 312 self.manager.do_reapply(self.context, self.uuid)
307 reapply_mock.assert_called_once_with(self.uuid) 313 reapply_mock.assert_called_once_with(self.uuid, data=self.data)
308 314
309 def test_node_locked(self, reapply_mock): 315 @mock.patch.object(swift, 'store_introspection_data', autospec=True)
316 @mock.patch.object(swift, 'get_introspection_data', autospec=True)
317 def test_node_locked(self, swift_get_mock, swift_set_mock, reapply_mock):
318 swift_get_mock.return_value = json.dumps(self.data)
310 exc = utils.Error('Locked.', code=409) 319 exc = utils.Error('Locked.', code=409)
311 reapply_mock.side_effect = exc 320 reapply_mock.side_effect = exc
312 321
@@ -317,9 +326,13 @@ class TestManagerReapply(BaseManagerTest):
317 self.assertEqual(utils.Error, exc.exc_info[0]) 326 self.assertEqual(utils.Error, exc.exc_info[0])
318 self.assertIn('Locked.', str(exc.exc_info[1])) 327 self.assertIn('Locked.', str(exc.exc_info[1]))
319 self.assertEqual(409, exc.exc_info[1].http_code) 328 self.assertEqual(409, exc.exc_info[1].http_code)
320 reapply_mock.assert_called_once_with(self.uuid) 329 reapply_mock.assert_called_once_with(self.uuid, data=self.data)
321 330
322 def test_node_not_found(self, reapply_mock): 331 @mock.patch.object(swift, 'store_introspection_data', autospec=True)
332 @mock.patch.object(swift, 'get_introspection_data', autospec=True)
333 def test_node_not_found(self, swift_get_mock, swift_set_mock,
334 reapply_mock):
335 swift_get_mock.return_value = json.dumps(self.data)
323 exc = utils.Error('Not found.', code=404) 336 exc = utils.Error('Not found.', code=404)
324 reapply_mock.side_effect = exc 337 reapply_mock.side_effect = exc
325 338
@@ -330,9 +343,11 @@ class TestManagerReapply(BaseManagerTest):
330 self.assertEqual(utils.Error, exc.exc_info[0]) 343 self.assertEqual(utils.Error, exc.exc_info[0])
331 self.assertIn('Not found.', str(exc.exc_info[1])) 344 self.assertIn('Not found.', str(exc.exc_info[1]))
332 self.assertEqual(404, exc.exc_info[1].http_code) 345 self.assertEqual(404, exc.exc_info[1].http_code)
333 reapply_mock.assert_called_once_with(self.uuid) 346 reapply_mock.assert_called_once_with(self.uuid, data=self.data)
334 347
335 def test_generic_error(self, reapply_mock): 348 @mock.patch.object(process, 'get_introspection_data', autospec=True)
349 def test_generic_error(self, get_data_mock, reapply_mock):
350 get_data_mock.return_value = self.data
336 exc = utils.Error('Oops', code=400) 351 exc = utils.Error('Oops', code=400)
337 reapply_mock.side_effect = exc 352 reapply_mock.side_effect = exc
338 353
@@ -343,4 +358,52 @@ class TestManagerReapply(BaseManagerTest):
343 self.assertEqual(utils.Error, exc.exc_info[0]) 358 self.assertEqual(utils.Error, exc.exc_info[0])
344 self.assertIn('Oops', str(exc.exc_info[1])) 359 self.assertIn('Oops', str(exc.exc_info[1]))
345 self.assertEqual(400, exc.exc_info[1].http_code) 360 self.assertEqual(400, exc.exc_info[1].http_code)
346 reapply_mock.assert_called_once_with(self.uuid) 361 reapply_mock.assert_called_once_with(self.uuid, data=self.data)
362 get_data_mock.assert_called_once_with(self.uuid, processed=False,
363 get_json=True)
364
365 @mock.patch.object(process, 'get_introspection_data', autospec=True)
366 def test_get_introspection_data_error(self, get_data_mock, reapply_mock):
367 exc = utils.Error('The store is empty', code=404)
368 get_data_mock.side_effect = exc
369
370 exc = self.assertRaises(messaging.rpc.ExpectedException,
371 self.manager.do_reapply,
372 self.context, self.uuid)
373
374 self.assertEqual(utils.Error, exc.exc_info[0])
375 self.assertIn('The store is empty', str(exc.exc_info[1]))
376 self.assertEqual(404, exc.exc_info[1].http_code)
377 get_data_mock.assert_called_once_with(self.uuid, processed=False,
378 get_json=True)
379 self.assertFalse(reapply_mock.called)
380
381 def test_store_data_disabled(self, reapply_mock):
382 CONF.set_override('store_data', 'none', 'processing')
383
384 exc = self.assertRaises(messaging.rpc.ExpectedException,
385 self.manager.do_reapply,
386 self.context, self.uuid)
387
388 self.assertEqual(utils.Error, exc.exc_info[0])
389 self.assertIn('Inspector is not configured to store data',
390 str(exc.exc_info[1]))
391 self.assertEqual(400, exc.exc_info[1].http_code)
392 self.assertFalse(reapply_mock.called)
393
394 @mock.patch.object(process, 'get_introspection_data', autospec=True)
395 def test_ok_swift(self, get_data_mock, reapply_mock):
396 get_data_mock.return_value = self.data
397 self.manager.do_reapply(self.context, self.uuid)
398 reapply_mock.assert_called_once_with(self.uuid, data=self.data)
399 get_data_mock.assert_called_once_with(self.uuid, processed=False,
400 get_json=True)
401
402 @mock.patch.object(process, 'get_introspection_data', autospec=True)
403 def test_ok_db(self, get_data_mock, reapply_mock):
404 get_data_mock.return_value = self.data
405 CONF.set_override('store_data', 'database', 'processing')
406 self.manager.do_reapply(self.context, self.uuid)
407 reapply_mock.assert_called_once_with(self.uuid, data=self.data)
408 get_data_mock.assert_called_once_with(self.uuid, processed=False,
409 get_json=True)
diff --git a/ironic_inspector/test/unit/test_plugins_introspection_data.py b/ironic_inspector/test/unit/test_plugins_introspection_data.py
new file mode 100644
index 0000000..c2b74d5
--- /dev/null
+++ b/ironic_inspector/test/unit/test_plugins_introspection_data.py
@@ -0,0 +1,108 @@
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
14import json
15
16import fixtures
17import mock
18from oslo_config import cfg
19
20from ironic_inspector.common import ironic as ir_utils
21from ironic_inspector import db
22from ironic_inspector import introspection_state as istate
23from ironic_inspector.plugins import introspection_data
24from ironic_inspector.test import base as test_base
25
26CONF = cfg.CONF
27
28
29class BaseTest(test_base.NodeTest):
30 data = {
31 'ipmi_address': '1.2.3.4',
32 'cpus': 2,
33 'cpu_arch': 'x86_64',
34 'memory_mb': 1024,
35 'local_gb': 20,
36 'interfaces': {
37 'em1': {'mac': '11:22:33:44:55:66', 'ip': '1.2.0.1'},
38 }
39 }
40
41 def setUp(self):
42 super(BaseTest, self).setUp()
43 self.cli_fixture = self.useFixture(
44 fixtures.MockPatchObject(ir_utils, 'get_client', autospec=True))
45 self.cli = self.cli_fixture.mock.return_value
46
47
48@mock.patch.object(introspection_data.swift, 'SwiftAPI', autospec=True)
49class TestSwiftStore(BaseTest):
50
51 def setUp(self):
52 super(TestSwiftStore, self).setUp()
53 self.driver = introspection_data.SwiftStore()
54
55 def test_get_data(self, swift_mock):
56 swift_conn = swift_mock.return_value
57 swift_conn.get_object.return_value = json.dumps(self.data)
58 name = 'inspector_data-%s' % self.uuid
59
60 res_data = self.driver.get(self.uuid)
61
62 swift_conn.get_object.assert_called_once_with(name)
63 self.assertEqual(self.data, json.loads(res_data))
64
65 def test_store_data(self, swift_mock):
66 swift_conn = swift_mock.return_value
67 name = 'inspector_data-%s' % self.uuid
68
69 self.driver.save(self.node_info, self.data)
70
71 data = introspection_data._filter_data_excluded_keys(self.data)
72 swift_conn.create_object.assert_called_once_with(name,
73 json.dumps(data))
74
75 def test_store_data_location(self, swift_mock):
76 CONF.set_override('store_data_location', 'inspector_data_object',
77 'processing')
78 swift_conn = swift_mock.return_value
79 name = 'inspector_data-%s' % self.uuid
80 patch = [{'path': '/extra/inspector_data_object',
81 'value': name, 'op': 'add'}]
82 expected = self.data
83
84 self.driver.save(self.node_info, self.data)
85
86 data = introspection_data._filter_data_excluded_keys(self.data)
87 swift_conn.create_object.assert_called_once_with(name,
88 json.dumps(data))
89 self.assertEqual(expected,
90 json.loads(swift_conn.create_object.call_args[0][1]))
91 self.cli.node.update.assert_any_call(self.uuid, patch)
92
93
94class TestDatabaseStore(BaseTest):
95 def setUp(self):
96 super(TestDatabaseStore, self).setUp()
97 self.driver = introspection_data.DatabaseStore()
98 session = db.get_writer_session()
99 with session.begin():
100 db.Node(uuid=self.node_info.uuid,
101 state=istate.States.starting).save(session)
102
103 def test_store_and_get_data(self):
104 self.driver.save(self.node_info, self.data)
105
106 res_data = self.driver.get(self.node_info.uuid)
107
108 self.assertEqual(self.data, json.loads(res_data))
diff --git a/ironic_inspector/test/unit/test_process.py b/ironic_inspector/test/unit/test_process.py
index a23d0b1..739ae55 100644
--- a/ironic_inspector/test/unit/test_process.py
+++ b/ironic_inspector/test/unit/test_process.py
@@ -28,11 +28,13 @@ from oslo_utils import uuidutils
28import six 28import six
29 29
30from ironic_inspector.common import ironic as ir_utils 30from ironic_inspector.common import ironic as ir_utils
31from ironic_inspector.common import swift
31from ironic_inspector import db 32from ironic_inspector import db
32from ironic_inspector import introspection_state as istate 33from ironic_inspector import introspection_state as istate
33from ironic_inspector import node_cache 34from ironic_inspector import node_cache
34from ironic_inspector.plugins import base as plugins_base 35from ironic_inspector.plugins import base as plugins_base
35from ironic_inspector.plugins import example as example_plugin 36from ironic_inspector.plugins import example as example_plugin
37from ironic_inspector.plugins import introspection_data as intros_data_plugin
36from ironic_inspector import process 38from ironic_inspector import process
37from ironic_inspector.pxe_filter import base as pxe_filter 39from ironic_inspector.pxe_filter import base as pxe_filter
38from ironic_inspector.test import base as test_base 40from ironic_inspector.test import base as test_base
@@ -259,22 +261,13 @@ class TestUnprocessedData(BaseProcessTest):
259 261
260 store_mock.assert_called_once_with(mock.ANY, expected) 262 store_mock.assert_called_once_with(mock.ANY, expected)
261 263
262 @mock.patch.object(process.swift, 'SwiftAPI', autospec=True) 264 def test_save_unprocessed_data_failure(self):
263 def test_save_unprocessed_data_failure(self, swift_mock):
264 CONF.set_override('store_data', 'swift', 'processing') 265 CONF.set_override('store_data', 'swift', 'processing')
265 name = 'inspector_data-%s-%s' % (
266 self.uuid,
267 process._UNPROCESSED_DATA_STORE_SUFFIX
268 )
269
270 swift_conn = swift_mock.return_value
271 swift_conn.create_object.side_effect = utils.Error('Oops')
272 266
273 res = process.process(self.data) 267 res = process.process(self.data)
274 268
275 # assert store failure doesn't break processing 269 # assert store failure doesn't break processing
276 self.assertEqual(self.fake_result_json, res) 270 self.assertEqual(self.fake_result_json, res)
277 swift_conn.create_object.assert_called_once_with(name, mock.ANY)
278 271
279 272
280@mock.patch.object(example_plugin.ExampleProcessingHook, 'before_processing', 273@mock.patch.object(example_plugin.ExampleProcessingHook, 'before_processing',
@@ -405,6 +398,7 @@ class TestProcessNode(BaseTest):
405 started_at=self.node_info.started_at, 398 started_at=self.node_info.started_at,
406 finished_at=self.node_info.finished_at, 399 finished_at=self.node_info.finished_at,
407 error=self.node_info.error).save(self.session) 400 error=self.node_info.error).save(self.session)
401 plugins_base._INTROSPECTION_DATA_MGR = None
408 402
409 def test_return_includes_uuid(self): 403 def test_return_includes_uuid(self):
410 ret_val = process._process_node(self.node_info, self.node, self.data) 404 ret_val = process._process_node(self.node_info, self.node, self.data)
@@ -485,8 +479,8 @@ class TestProcessNode(BaseTest):
485 finished_mock.assert_called_once_with( 479 finished_mock.assert_called_once_with(
486 self.node_info, istate.Events.finish) 480 self.node_info, istate.Events.finish)
487 481
488 @mock.patch.object(process.swift, 'SwiftAPI', autospec=True) 482 @mock.patch.object(swift, 'SwiftAPI', autospec=True)
489 def test_store_data(self, swift_mock): 483 def test_store_data_with_swift(self, swift_mock):
490 CONF.set_override('store_data', 'swift', 'processing') 484 CONF.set_override('store_data', 'swift', 'processing')
491 swift_conn = swift_mock.return_value 485 swift_conn = swift_mock.return_value
492 name = 'inspector_data-%s' % self.uuid 486 name = 'inspector_data-%s' % self.uuid
@@ -498,8 +492,8 @@ class TestProcessNode(BaseTest):
498 self.assertEqual(expected, 492 self.assertEqual(expected,
499 json.loads(swift_conn.create_object.call_args[0][1])) 493 json.loads(swift_conn.create_object.call_args[0][1]))
500 494
501 @mock.patch.object(process.swift, 'SwiftAPI', autospec=True) 495 @mock.patch.object(swift, 'SwiftAPI', autospec=True)
502 def test_store_data_no_logs(self, swift_mock): 496 def test_store_data_no_logs_with_swift(self, swift_mock):
503 CONF.set_override('store_data', 'swift', 'processing') 497 CONF.set_override('store_data', 'swift', 'processing')
504 swift_conn = swift_mock.return_value 498 swift_conn = swift_mock.return_value
505 name = 'inspector_data-%s' % self.uuid 499 name = 'inspector_data-%s' % self.uuid
@@ -511,8 +505,8 @@ class TestProcessNode(BaseTest):
511 self.assertNotIn('logs', 505 self.assertNotIn('logs',
512 json.loads(swift_conn.create_object.call_args[0][1])) 506 json.loads(swift_conn.create_object.call_args[0][1]))
513 507
514 @mock.patch.object(process.swift, 'SwiftAPI', autospec=True) 508 @mock.patch.object(swift, 'SwiftAPI', autospec=True)
515 def test_store_data_location(self, swift_mock): 509 def test_store_data_location_with_swift(self, swift_mock):
516 CONF.set_override('store_data', 'swift', 'processing') 510 CONF.set_override('store_data', 'swift', 'processing')
517 CONF.set_override('store_data_location', 'inspector_data_object', 511 CONF.set_override('store_data_location', 'inspector_data_object',
518 'processing') 512 'processing')
@@ -529,6 +523,28 @@ class TestProcessNode(BaseTest):
529 json.loads(swift_conn.create_object.call_args[0][1])) 523 json.loads(swift_conn.create_object.call_args[0][1]))
530 self.cli.node.update.assert_any_call(self.uuid, patch) 524 self.cli.node.update.assert_any_call(self.uuid, patch)
531 525
526 @mock.patch.object(node_cache, 'store_introspection_data', autospec=True)
527 def test_store_data_with_database(self, store_mock):
528 CONF.set_override('store_data', 'database', 'processing')
529
530 process._process_node(self.node_info, self.node, self.data)
531
532 data = intros_data_plugin._filter_data_excluded_keys(self.data)
533 store_mock.assert_called_once_with(self.node_info.uuid, data, True)
534 self.assertEqual(data, store_mock.call_args[0][1])
535
536 @mock.patch.object(node_cache, 'store_introspection_data', autospec=True)
537 def test_store_data_no_logs_with_database(self, store_mock):
538 CONF.set_override('store_data', 'database', 'processing')
539
540 self.data['logs'] = 'something'
541
542 process._process_node(self.node_info, self.node, self.data)
543
544 data = intros_data_plugin._filter_data_excluded_keys(self.data)
545 store_mock.assert_called_once_with(self.node_info.uuid, data, True)
546 self.assertNotIn('logs', store_mock.call_args[0][1])
547
532 548
533@mock.patch.object(process, '_reapply', autospec=True) 549@mock.patch.object(process, '_reapply', autospec=True)
534@mock.patch.object(node_cache, 'get_node', autospec=True) 550@mock.patch.object(node_cache, 'get_node', autospec=True)
@@ -558,7 +574,7 @@ class TestReapply(BaseTest):
558 blocking=False 574 blocking=False
559 ) 575 )
560 576
561 reapply_mock.assert_called_once_with(pop_mock.return_value) 577 reapply_mock.assert_called_once_with(pop_mock.return_value, data=None)
562 578
563 @prepare_mocks 579 @prepare_mocks
564 def test_locking_failed(self, pop_mock, reapply_mock): 580 def test_locking_failed(self, pop_mock, reapply_mock):
@@ -575,7 +591,7 @@ class TestReapply(BaseTest):
575 591
576@mock.patch.object(example_plugin.ExampleProcessingHook, 'before_update') 592@mock.patch.object(example_plugin.ExampleProcessingHook, 'before_update')
577@mock.patch.object(process.rules, 'apply', autospec=True) 593@mock.patch.object(process.rules, 'apply', autospec=True)
578@mock.patch.object(process.swift, 'SwiftAPI', autospec=True) 594@mock.patch.object(swift, 'SwiftAPI', autospec=True)
579@mock.patch.object(node_cache.NodeInfo, 'finished', autospec=True) 595@mock.patch.object(node_cache.NodeInfo, 'finished', autospec=True)
580@mock.patch.object(node_cache.NodeInfo, 'release_lock', autospec=True) 596@mock.patch.object(node_cache.NodeInfo, 'release_lock', autospec=True)
581class TestReapplyNode(BaseTest): 597class TestReapplyNode(BaseTest):
diff --git a/ironic_inspector/utils.py b/ironic_inspector/utils.py
index 69244a4..b0cfc21 100644
--- a/ironic_inspector/utils.py
+++ b/ironic_inspector/utils.py
@@ -140,6 +140,10 @@ class NodeStateInvalidEvent(Error):
140 """Invalid event attempted.""" 140 """Invalid event attempted."""
141 141
142 142
143class IntrospectionDataStoreDisabled(Error):
144 """Introspection data store is disabled."""
145
146
143class IntrospectionDataNotFound(NotFoundInCacheError): 147class IntrospectionDataNotFound(NotFoundInCacheError):
144 """Introspection data not found.""" 148 """Introspection data not found."""
145 149
diff --git a/releasenotes/notes/introspection-data-db-store-0586292de05cbfd7.yaml b/releasenotes/notes/introspection-data-db-store-0586292de05cbfd7.yaml
new file mode 100644
index 0000000..0758a43
--- /dev/null
+++ b/releasenotes/notes/introspection-data-db-store-0586292de05cbfd7.yaml
@@ -0,0 +1,6 @@
1---
2features:
3 - |
4 Adds the support to store introspection data in ironic-inspector database.
5 Set the option ``[processing]store_data`` to ``database`` to use this
6 feature. \ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 2c84cc1..47a98c5 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -43,6 +43,10 @@ ironic_inspector.hooks.processing =
43ironic_inspector.hooks.node_not_found = 43ironic_inspector.hooks.node_not_found =
44 example = ironic_inspector.plugins.example:example_not_found_hook 44 example = ironic_inspector.plugins.example:example_not_found_hook
45 enroll = ironic_inspector.plugins.discovery:enroll_node_not_found_hook 45 enroll = ironic_inspector.plugins.discovery:enroll_node_not_found_hook
46ironic_inspector.introspection_data.store =
47 none = ironic_inspector.plugins.introspection_data:NoStore
48 swift = ironic_inspector.plugins.introspection_data:SwiftStore
49 database = ironic_inspector.plugins.introspection_data:DatabaseStore
46ironic_inspector.rules.conditions = 50ironic_inspector.rules.conditions =
47 eq = ironic_inspector.plugins.rules:EqCondition 51 eq = ironic_inspector.plugins.rules:EqCondition
48 lt = ironic_inspector.plugins.rules:LtCondition 52 lt = ironic_inspector.plugins.rules:LtCondition