summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api-ref/source/introspection-api-v1-introspection-management.inc20
-rw-r--r--doc/source/user/http-api.rst2
-rw-r--r--ironic_inspector/common/rpc.py4
-rw-r--r--ironic_inspector/conductor/manager.py27
-rw-r--r--ironic_inspector/main.py19
-rw-r--r--ironic_inspector/process.py29
-rw-r--r--ironic_inspector/test/functional.py8
-rw-r--r--ironic_inspector/test/unit/test_main.py24
-rw-r--r--ironic_inspector/test/unit/test_manager.py13
-rw-r--r--ironic_inspector/test/unit/test_process.py10
-rw-r--r--releasenotes/notes/post-introspection-data-9cdd39a3de446e92.yaml7
11 files changed, 123 insertions, 40 deletions
diff --git a/api-ref/source/introspection-api-v1-introspection-management.inc b/api-ref/source/introspection-api-v1-introspection-management.inc
index 1b5cb35..69acb7a 100644
--- a/api-ref/source/introspection-api-v1-introspection-management.inc
+++ b/api-ref/source/introspection-api-v1-introspection-management.inc
@@ -70,28 +70,32 @@ The response will contain introspection data in the form of json string.
70 :language: javascript 70 :language: javascript
71 71
72 72
73Reapply Introspection on stored data 73Reapply Introspection on data
74==================================== 74=============================
75 75
76.. rest_method:: POST /v1/introspection/{node_id}/data/unprocessed 76.. rest_method:: POST /v1/introspection/{node_id}/data/unprocessed
77 77
78This method triggers introspection on stored unprocessed data. 78This method triggers introspection on either stored introspection data or raw
79No data is allowed to be sent along with the request. 79introspection data provided in the request. If the introspection data is
80provided in the request body, it should be a valid JSON with content similar to
81ramdisk callback request.
80 82
81.. note:: 83.. versionadded:: 1.15
84 Unprocessed introspection data can be sent via request body.
82 85
83 Requires enabling introspection storage backend via ``[processing]store_data``. 86.. note::
87 Reapplying introspection on stored data is only possible when a storage
88 backend is enabled via ``[processing]store_data``.
84 89
85Normal response codes: 202 90Normal response codes: 202
86 91
87Error codes: 92Error codes:
88 93
89* 400 - bad request or store not configured 94* 400 - bad request, store not configured or malformed data in request body
90* 401, 403 - missing or invalid authentication 95* 401, 403 - missing or invalid authentication
91* 404 - node not found for Node ID 96* 404 - node not found for Node ID
92* 409 - inspector locked node for processing 97* 409 - inspector locked node for processing
93 98
94
95Request 99Request
96------- 100-------
97 101
diff --git a/doc/source/user/http-api.rst b/doc/source/user/http-api.rst
index eaaf610..debb9b1 100644
--- a/doc/source/user/http-api.rst
+++ b/doc/source/user/http-api.rst
@@ -398,4 +398,4 @@ Version History
398* **1.13** adds ``manage_boot`` parameter for the introspection API. 398* **1.13** adds ``manage_boot`` parameter for the introspection API.
399* **1.14** allows formatting to be applied to strings nested in dicts and lists 399* **1.14** allows formatting to be applied to strings nested in dicts and lists
400 in the actions of introspection rules. 400 in the actions of introspection rules.
401 401* **1.15** allows reapply with provided introspection data from request.
diff --git a/ironic_inspector/common/rpc.py b/ironic_inspector/common/rpc.py
index 31e5311..94f49ef 100644
--- a/ironic_inspector/common/rpc.py
+++ b/ironic_inspector/common/rpc.py
@@ -33,7 +33,7 @@ def get_transport():
33def get_client(): 33def get_client():
34 """Get a RPC client instance.""" 34 """Get a RPC client instance."""
35 target = messaging.Target(topic=manager.MANAGER_TOPIC, server=CONF.host, 35 target = messaging.Target(topic=manager.MANAGER_TOPIC, server=CONF.host,
36 version='1.1') 36 version='1.2')
37 transport = get_transport() 37 transport = get_transport()
38 return messaging.RPCClient(transport, target) 38 return messaging.RPCClient(transport, target)
39 39
@@ -43,7 +43,7 @@ def get_server(endpoints):
43 43
44 transport = get_transport() 44 transport = get_transport()
45 target = messaging.Target(topic=manager.MANAGER_TOPIC, server=CONF.host, 45 target = messaging.Target(topic=manager.MANAGER_TOPIC, server=CONF.host,
46 version='1.1') 46 version='1.2')
47 return messaging.get_rpc_server( 47 return messaging.get_rpc_server(
48 transport, target, endpoints, executor='eventlet', 48 transport, target, endpoints, executor='eventlet',
49 access_policy=dispatcher.DefaultRPCAccessPolicy) 49 access_policy=dispatcher.DefaultRPCAccessPolicy)
diff --git a/ironic_inspector/conductor/manager.py b/ironic_inspector/conductor/manager.py
index b0bfeba..29ca30e 100644
--- a/ironic_inspector/conductor/manager.py
+++ b/ironic_inspector/conductor/manager.py
@@ -39,7 +39,7 @@ MANAGER_TOPIC = 'ironic-inspector-conductor'
39 39
40class ConductorManager(object): 40class ConductorManager(object):
41 """ironic inspector conductor manager""" 41 """ironic inspector conductor manager"""
42 RPC_API_VERSION = '1.1' 42 RPC_API_VERSION = '1.2'
43 43
44 target = messaging.Target(version=RPC_API_VERSION) 44 target = messaging.Target(version=RPC_API_VERSION)
45 45
@@ -126,16 +126,21 @@ class ConductorManager(object):
126 introspect.abort(node_id, token=token) 126 introspect.abort(node_id, token=token)
127 127
128 @messaging.expected_exceptions(utils.Error) 128 @messaging.expected_exceptions(utils.Error)
129 def do_reapply(self, context, node_uuid, token=None): 129 def do_reapply(self, context, node_uuid, token=None, data=None):
130 try: 130 if not data:
131 data = process.get_introspection_data(node_uuid, processed=False, 131 try:
132 get_json=True) 132 data = process.get_introspection_data(node_uuid,
133 except utils.IntrospectionDataStoreDisabled: 133 processed=False,
134 raise utils.Error(_('Inspector is not configured to store ' 134 get_json=True)
135 'data. Set the [processing]store_data ' 135 except utils.IntrospectionDataStoreDisabled:
136 'configuration option to change this.'), 136 raise utils.Error(_('Inspector is not configured to store '
137 code=400) 137 'introspection data. Set the '
138 process.reapply(node_uuid, data) 138 '[processing]store_data configuration '
139 'option to change this.'))
140 else:
141 process.store_introspection_data(node_uuid, data, processed=False)
142
143 process.reapply(node_uuid, data=data)
139 144
140 145
141def periodic_clean_up(): # pragma: no cover 146def periodic_clean_up(): # pragma: no cover
diff --git a/ironic_inspector/main.py b/ironic_inspector/main.py
index 6a7842c..44f1ec1 100644
--- a/ironic_inspector/main.py
+++ b/ironic_inspector/main.py
@@ -39,7 +39,7 @@ app = flask.Flask(__name__)
39LOG = utils.getProcessingLogger(__name__) 39LOG = utils.getProcessingLogger(__name__)
40 40
41MINIMUM_API_VERSION = (1, 0) 41MINIMUM_API_VERSION = (1, 0)
42CURRENT_API_VERSION = (1, 14) 42CURRENT_API_VERSION = (1, 15)
43DEFAULT_API_VERSION = CURRENT_API_VERSION 43DEFAULT_API_VERSION = CURRENT_API_VERSION
44_LOGGING_EXCLUDED_KEYS = ('logs',) 44_LOGGING_EXCLUDED_KEYS = ('logs',)
45 45
@@ -307,14 +307,25 @@ def api_introspection_data(node_id):
307@api('/v1/introspection/<node_id>/data/unprocessed', 307@api('/v1/introspection/<node_id>/data/unprocessed',
308 rule="introspection:reapply", methods=['POST']) 308 rule="introspection:reapply", methods=['POST'])
309def api_introspection_reapply(node_id): 309def api_introspection_reapply(node_id):
310 data = None
310 if flask.request.content_length: 311 if flask.request.content_length:
311 return error_response(_('User data processing is not ' 312 try:
312 'supported yet'), code=400) 313 data = flask.request.get_json(force=True)
314 except Exception:
315 raise utils.Error(
316 _('Invalid data: expected a JSON object, got %s') % data)
317 if not isinstance(data, dict):
318 raise utils.Error(
319 _('Invalid data: expected a JSON object, got %s') %
320 data.__class__.__name__)
321 LOG.debug("Received reapply data from request", data=data)
322
313 if not uuidutils.is_uuid_like(node_id): 323 if not uuidutils.is_uuid_like(node_id):
314 node = ir_utils.get_node(node_id, fields=['uuid']) 324 node = ir_utils.get_node(node_id, fields=['uuid'])
315 node_id = node.uuid 325 node_id = node.uuid
326
316 client = rpc.get_client() 327 client = rpc.get_client()
317 client.call({}, 'do_reapply', node_uuid=node_id) 328 client.call({}, 'do_reapply', node_uuid=node_id, data=data)
318 return '', 202 329 return '', 202
319 330
320 331
diff --git a/ironic_inspector/process.py b/ironic_inspector/process.py
index 8d476cf..6810b80 100644
--- a/ironic_inspector/process.py
+++ b/ironic_inspector/process.py
@@ -140,7 +140,15 @@ def _filter_data_excluded_keys(data):
140 if k not in _STORAGE_EXCLUDED_KEYS} 140 if k not in _STORAGE_EXCLUDED_KEYS}
141 141
142 142
143def _store_data(node_uuid, data, processed=True): 143def store_introspection_data(node_uuid, data, processed=True):
144 """Store introspection data to the storage backend.
145
146 :param node_uuid: node UUID
147 :param data: Introspection data to be saved
148 :param processed: The type of introspection data, set to True means the
149 introspection data is processed, otherwise unprocessed.
150 :raises: utils.Error
151 """
144 introspection_data_manager = plugins_base.introspection_data_manager() 152 introspection_data_manager = plugins_base.introspection_data_manager()
145 store = CONF.processing.store_data 153 store = CONF.processing.store_data
146 ext = introspection_data_manager[store].obj 154 ext = introspection_data_manager[store].obj
@@ -150,13 +158,22 @@ def _store_data(node_uuid, data, processed=True):
150def _store_unprocessed_data(node_uuid, data): 158def _store_unprocessed_data(node_uuid, data):
151 # runs in background 159 # runs in background
152 try: 160 try:
153 _store_data(node_uuid, data, processed=False) 161 store_introspection_data(node_uuid, data, processed=False)
154 except Exception: 162 except Exception:
155 LOG.exception('Encountered exception saving unprocessed ' 163 LOG.exception('Encountered exception saving unprocessed '
156 'introspection data for node %s', node_uuid, data=data) 164 'introspection data for node %s', node_uuid, data=data)
157 165
158 166
159def get_introspection_data(uuid, processed=True, get_json=False): 167def get_introspection_data(uuid, processed=True, get_json=False):
168 """Get introspection data from the storage backend.
169
170 :param uuid: node UUID
171 :param processed: Indicates the type of introspection data to be read,
172 set True to request processed introspection data.
173 :param get_json: Specify whether return the introspection data in json
174 format, string value is returned if False.
175 :raises: utils.Error
176 """
160 introspection_data_manager = plugins_base.introspection_data_manager() 177 introspection_data_manager = plugins_base.introspection_data_manager()
161 store = CONF.processing.store_data 178 store = CONF.processing.store_data
162 ext = introspection_data_manager[store].obj 179 ext = introspection_data_manager[store].obj
@@ -242,7 +259,7 @@ def _process_node(node_info, node, introspection_data):
242 # NOTE(dtantsur): repeat the check in case something changed 259 # NOTE(dtantsur): repeat the check in case something changed
243 ir_utils.check_provision_state(node) 260 ir_utils.check_provision_state(node)
244 _run_post_hooks(node_info, introspection_data) 261 _run_post_hooks(node_info, introspection_data)
245 _store_data(node_info.uuid, introspection_data) 262 store_introspection_data(node_info.uuid, introspection_data)
246 263
247 ironic = ir_utils.get_client() 264 ironic = ir_utils.get_client()
248 pxe_filter.driver().sync(ironic) 265 pxe_filter.driver().sync(ironic)
@@ -292,8 +309,8 @@ def reapply(node_uuid, data=None):
292 stored data. 309 stored data.
293 310
294 :param node_uuid: node UUID 311 :param node_uuid: node UUID
312 :param data: unprocessed introspection data to be reapplied
295 :raises: utils.Error 313 :raises: utils.Error
296
297 """ 314 """
298 315
299 LOG.debug('Processing re-apply introspection request for node ' 316 LOG.debug('Processing re-apply introspection request for node '
@@ -307,7 +324,7 @@ def reapply(node_uuid, data=None):
307 raise utils.Error(_('Node locked, please, try again later'), 324 raise utils.Error(_('Node locked, please, try again later'),
308 node_info=node_info, code=409) 325 node_info=node_info, code=409)
309 326
310 utils.executor().submit(_reapply, node_info, data) 327 utils.executor().submit(_reapply, node_info, introspection_data=data)
311 328
312 329
313def _reapply(node_info, introspection_data=None): 330def _reapply(node_info, introspection_data=None):
@@ -350,6 +367,6 @@ def _reapply_with_data(node_info, introspection_data):
350 '\n'.join(failures), node_info=node_info) 367 '\n'.join(failures), node_info=node_info)
351 368
352 _run_post_hooks(node_info, introspection_data) 369 _run_post_hooks(node_info, introspection_data)
353 _store_data(node_info.uuid, introspection_data) 370 store_introspection_data(node_info.uuid, introspection_data)
354 node_info.invalidate_cache() 371 node_info.invalidate_cache()
355 rules.apply(node_info, introspection_data) 372 rules.apply(node_info, introspection_data)
diff --git a/ironic_inspector/test/functional.py b/ironic_inspector/test/functional.py
index cdae214..f7edfc0 100644
--- a/ironic_inspector/test/functional.py
+++ b/ironic_inspector/test/functional.py
@@ -634,6 +634,14 @@ class Test(Base):
634 self.assertEqual(store_processing_call, 634 self.assertEqual(store_processing_call,
635 store_mock.call_args_list[-1]) 635 store_mock.call_args_list[-1])
636 636
637 # Reapply with provided data
638 res = self.call_reapply(self.uuid, data=copy.deepcopy(self.data))
639 self.assertEqual(202, res.status_code)
640 self.assertEqual('', res.text)
641 eventlet.greenthread.sleep(DEFAULT_SLEEP)
642
643 self.check_status(status, finished=True, state=istate.States.finished)
644
637 @mock.patch.object(swift, 'store_introspection_data', autospec=True) 645 @mock.patch.object(swift, 'store_introspection_data', autospec=True)
638 @mock.patch.object(swift, 'get_introspection_data', autospec=True) 646 @mock.patch.object(swift, 'get_introspection_data', autospec=True)
639 def test_edge_state_transitions(self, get_mock, store_mock): 647 def test_edge_state_transitions(self, get_mock, store_mock):
diff --git a/ironic_inspector/test/unit/test_main.py b/ironic_inspector/test/unit/test_main.py
index 690f8ce..ce0a503 100644
--- a/ironic_inspector/test/unit/test_main.py
+++ b/ironic_inspector/test/unit/test_main.py
@@ -370,17 +370,26 @@ class TestApiReapply(BaseAPITest):
370 self.app.post('/v1/introspection/%s/data/unprocessed' % 370 self.app.post('/v1/introspection/%s/data/unprocessed' %
371 self.uuid) 371 self.uuid)
372 self.client_mock.call.assert_called_once_with({}, 'do_reapply', 372 self.client_mock.call.assert_called_once_with({}, 'do_reapply',
373 node_uuid=self.uuid) 373 node_uuid=self.uuid,
374 data=None)
374 375
375 def test_user_data(self): 376 def test_user_data(self):
376 res = self.app.post('/v1/introspection/%s/data/unprocessed' % 377 res = self.app.post('/v1/introspection/%s/data/unprocessed' %
377 self.uuid, data='some data') 378 self.uuid, data='some data')
378 self.assertEqual(400, res.status_code) 379 self.assertEqual(400, res.status_code)
379 message = json.loads(res.data.decode())['error']['message'] 380 message = json.loads(res.data.decode())['error']['message']
380 self.assertEqual('User data processing is not supported yet', 381 self.assertIn('Invalid data: expected a JSON object', message)
381 message)
382 self.assertFalse(self.client_mock.call.called) 382 self.assertFalse(self.client_mock.call.called)
383 383
384 def test_user_data_valid(self):
385 data = {"foo": "bar"}
386 res = self.app.post('/v1/introspection/%s/data/unprocessed' %
387 self.uuid, data=json.dumps(data))
388 self.assertEqual(202, res.status_code)
389 self.client_mock.call.assert_called_once_with({}, 'do_reapply',
390 node_uuid=self.uuid,
391 data=data)
392
384 def test_get_introspection_data_error(self): 393 def test_get_introspection_data_error(self):
385 exc = utils.Error('The store is crashed', code=404) 394 exc = utils.Error('The store is crashed', code=404)
386 self.client_mock.call.side_effect = exc 395 self.client_mock.call.side_effect = exc
@@ -392,7 +401,8 @@ class TestApiReapply(BaseAPITest):
392 message = json.loads(res.data.decode())['error']['message'] 401 message = json.loads(res.data.decode())['error']['message']
393 self.assertEqual(str(exc), message) 402 self.assertEqual(str(exc), message)
394 self.client_mock.call.assert_called_once_with({}, 'do_reapply', 403 self.client_mock.call.assert_called_once_with({}, 'do_reapply',
395 node_uuid=self.uuid) 404 node_uuid=self.uuid,
405 data=None)
396 406
397 def test_generic_error(self): 407 def test_generic_error(self):
398 exc = utils.Error('Oops', code=400) 408 exc = utils.Error('Oops', code=400)
@@ -405,7 +415,8 @@ class TestApiReapply(BaseAPITest):
405 message = json.loads(res.data.decode())['error']['message'] 415 message = json.loads(res.data.decode())['error']['message']
406 self.assertEqual(str(exc), message) 416 self.assertEqual(str(exc), message)
407 self.client_mock.call.assert_called_once_with({}, 'do_reapply', 417 self.client_mock.call.assert_called_once_with({}, 'do_reapply',
408 node_uuid=self.uuid) 418 node_uuid=self.uuid,
419 data=None)
409 420
410 @mock.patch.object(ir_utils, 'get_node', autospec=True) 421 @mock.patch.object(ir_utils, 'get_node', autospec=True)
411 def test_reapply_with_node_name(self, get_mock): 422 def test_reapply_with_node_name(self, get_mock):
@@ -413,7 +424,8 @@ class TestApiReapply(BaseAPITest):
413 self.app.post('/v1/introspection/%s/data/unprocessed' % 424 self.app.post('/v1/introspection/%s/data/unprocessed' %
414 'fake-node') 425 'fake-node')
415 self.client_mock.call.assert_called_once_with({}, 'do_reapply', 426 self.client_mock.call.assert_called_once_with({}, 'do_reapply',
416 node_uuid=self.uuid) 427 node_uuid=self.uuid,
428 data=None)
417 get_mock.assert_called_once_with('fake-node', fields=['uuid']) 429 get_mock.assert_called_once_with('fake-node', fields=['uuid'])
418 430
419 431
diff --git a/ironic_inspector/test/unit/test_manager.py b/ironic_inspector/test/unit/test_manager.py
index e374441..35f0473 100644
--- a/ironic_inspector/test/unit/test_manager.py
+++ b/ironic_inspector/test/unit/test_manager.py
@@ -386,8 +386,8 @@ class TestManagerReapply(BaseManagerTest):
386 self.context, self.uuid) 386 self.context, self.uuid)
387 387
388 self.assertEqual(utils.Error, exc.exc_info[0]) 388 self.assertEqual(utils.Error, exc.exc_info[0])
389 self.assertIn('Inspector is not configured to store data', 389 self.assertIn('Inspector is not configured to store introspection '
390 str(exc.exc_info[1])) 390 'data', str(exc.exc_info[1]))
391 self.assertEqual(400, exc.exc_info[1].http_code) 391 self.assertEqual(400, exc.exc_info[1].http_code)
392 self.assertFalse(reapply_mock.called) 392 self.assertFalse(reapply_mock.called)
393 393
@@ -407,3 +407,12 @@ class TestManagerReapply(BaseManagerTest):
407 reapply_mock.assert_called_once_with(self.uuid, data=self.data) 407 reapply_mock.assert_called_once_with(self.uuid, data=self.data)
408 get_data_mock.assert_called_once_with(self.uuid, processed=False, 408 get_data_mock.assert_called_once_with(self.uuid, processed=False,
409 get_json=True) 409 get_json=True)
410
411 @mock.patch.object(process, 'store_introspection_data', autospec=True)
412 @mock.patch.object(process, 'get_introspection_data', autospec=True)
413 def test_reapply_with_data(self, get_mock, store_mock, reapply_mock):
414 self.manager.do_reapply(self.context, self.uuid, data=self.data)
415 reapply_mock.assert_called_once_with(self.uuid, data=self.data)
416 store_mock.assert_called_once_with(self.uuid, self.data,
417 processed=False)
418 self.assertFalse(get_mock.called)
diff --git a/ironic_inspector/test/unit/test_process.py b/ironic_inspector/test/unit/test_process.py
index 3d89630..8ea3606 100644
--- a/ironic_inspector/test/unit/test_process.py
+++ b/ironic_inspector/test/unit/test_process.py
@@ -584,6 +584,16 @@ class TestReapply(BaseTest):
584 blocking=False 584 blocking=False
585 ) 585 )
586 586
587 @prepare_mocks
588 def test_reapply_with_data(self, pop_mock, reapply_mock):
589 process.reapply(self.uuid, data=self.data)
590 pop_mock.assert_called_once_with(self.uuid, locked=False)
591 pop_mock.return_value.acquire_lock.assert_called_once_with(
592 blocking=False
593 )
594 reapply_mock.assert_called_once_with(pop_mock.return_value,
595 introspection_data=self.data)
596
587 597
588@mock.patch.object(example_plugin.ExampleProcessingHook, 'before_update') 598@mock.patch.object(example_plugin.ExampleProcessingHook, 'before_update')
589@mock.patch.object(process.rules, 'apply', autospec=True) 599@mock.patch.object(process.rules, 'apply', autospec=True)
diff --git a/releasenotes/notes/post-introspection-data-9cdd39a3de446e92.yaml b/releasenotes/notes/post-introspection-data-9cdd39a3de446e92.yaml
new file mode 100644
index 0000000..c686425
--- /dev/null
+++ b/releasenotes/notes/post-introspection-data-9cdd39a3de446e92.yaml
@@ -0,0 +1,7 @@
1---
2features:
3 - |
4 Adds support to reapply with provided unprocessed introspection data. The
5 introspection data is supplied in the body of POST request to
6 ``/v1/introspection/<node_id>/data/unprocessed``. The introspection data
7 will also be saved to storage backend.