diff --git a/ironic/common/inspection_rules/actions.py b/ironic/common/inspection_rules/actions.py index 2e4e11327d..edd08fa624 100644 --- a/ironic/common/inspection_rules/actions.py +++ b/ironic/common/inspection_rules/actions.py @@ -11,6 +11,9 @@ # under the License. import abc +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry from oslo_log import log @@ -39,6 +42,7 @@ ACTIONS = { "set-port-attribute": "SetPortAttributeAction", "extend-port-attribute": "ExtendPortAttributeAction", "del-port-attribute": "DelPortAttributeAction", + "api-call": "CallAPIHookAction", } @@ -407,3 +411,46 @@ class DelPortAttributeAction(ActionBase): 'path': path, 'port_id': port_id, 'exc': str(exc)} LOG.error(msg) raise exception.RuleActionExecutionFailure(reason=msg) + + +class CallAPIHookAction(ActionBase): + FORMATTED_ARGS = ['url'] + OPTIONAL_PARAMS = [ + 'headers', 'proxies', 'timeout', 'retries', 'backoff_factor' + ] + + def __call__(self, task, url, headers=None, proxies=None, + timeout=5, retries=3, backoff_factor=0.3): + try: + timeout = float(timeout) + if timeout <= 0: + raise ValueError("timeout must be greater than zero") + retries = int(retries) + backoff_factor = float(backoff_factor) + retry_strategy = Retry( + total=retries, + backoff_factor=backoff_factor, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET"], + raise_on_status=False + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + session = requests.Session() + session.mount("http://", adapter) + session.mount("https://", adapter) + request_kwargs = {} + if headers: + request_kwargs['headers'] = headers + if proxies: + request_kwargs['proxies'] = proxies + response = session.get(url, timeout=timeout, **request_kwargs) + response.raise_for_status() + except ValueError as exc: + msg = _("Invalid parameter: %s") % exc + LOG.error(msg) + raise exception.RuleActionExecutionFailure(reason=msg) + except requests.exceptions.RequestException as exc: + msg = _("Request to %(url)s failed: %(exc)s") % { + 'url': url, 'exc': exc} + LOG.error(msg) + raise exception.RuleActionExecutionFailure(reason=msg) diff --git a/ironic/tests/unit/common/test_inspection_rule.py b/ironic/tests/unit/common/test_inspection_rule.py index d7cbe42991..5f906c44d4 100644 --- a/ironic/tests/unit/common/test_inspection_rule.py +++ b/ironic/tests/unit/common/test_inspection_rule.py @@ -693,6 +693,27 @@ class TestActions(TestInspectionRules): self.assertEqual('value1', task.node.extra['test1']) self.assertEqual('value2', task.node.extra['test2']) + @mock.patch( + 'ironic.common.inspection_rules.actions.requests.Session', + autospec=True) + def test_call_api_hook_action_success(self, mock_session): + """Test CallAPIHookAction successfully calls an API.""" + mock_session_instance = mock_session.return_value + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.raise_for_status.return_value = None + mock_session_instance.get.return_value = mock_response + + with task_manager.acquire(self.context, self.node.uuid) as task: + action = inspection_rules.actions.CallAPIHookAction() + test_url = 'http://example.com/simple_hook' + action(task, url=test_url) + mock_session_instance.mount.assert_any_call("http://", mock.ANY) + mock_session_instance.mount.assert_any_call("https://", mock.ANY) + mock_session_instance.get.assert_called_once_with( + test_url, timeout=5) + mock_response.raise_for_status.assert_called_once() + class TestShallowMask(TestInspectionRules): def setUp(self): diff --git a/releasenotes/notes/add-api-call-inspection-action-985aee4347ed9217.yaml b/releasenotes/notes/add-api-call-inspection-action-985aee4347ed9217.yaml new file mode 100644 index 0000000000..cd222abf56 --- /dev/null +++ b/releasenotes/notes/add-api-call-inspection-action-985aee4347ed9217.yaml @@ -0,0 +1,36 @@ +--- +features: + - | + Added a new 'api-call' action plugin for Ironic inspection rules. + + This action allows triggering an HTTP GET request to a given URL when a + rule matches successfully during node inspection. It is useful for + integrating with external systems such as webhooks, alerting, or + automation tools. + + The following options are supported: + + * url (required): The HTTP endpoint to call + * timeout (optional, default: 5): Timeout in seconds + * retries (optional, default: 3): Number of retries on failure + * backoff_factor (optional, default: 0.3): Delay factor for retry attempts + * headers, proxies (optional): Additional request configuration + + Retry applies to status codes 429, 500, 502, 503, and 504. + + Example rule:: + + [ + { + "description": "Trigger webhook after node inspection", + "actions": [ + { + "action": "api-call", + "url": "http://example.com/hook", + "timeout": 10, + "retries": 5, + "backoff_factor": 1 + } + ] + } + ]