summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2019-01-23 03:29:15 +0000
committerGerrit Code Review <review@openstack.org>2019-01-23 03:29:15 +0000
commit13e70283b13e1c4e65a965197f23709ce9343e80 (patch)
tree9c71755707c9084a8d948e9ce8a1b93d54d8825d
parent20f984817a7eb7e9b9ac15a71d5178d19d291c5c (diff)
parentd278bb6f779eb8941754b29dbb6083a732330783 (diff)
Merge "introspection data backend: plugin layer"
-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 dc6aece..2fa00b3 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 447caef..ebaf016 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 6042185..3ce6c6e 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