Skip to content

Commit 9732b2a

Browse files
fix: move filters and event sources as builtins
fix: bring webhook as builtin fix: re-raise asyncio.CancelledError fix: handle asyncio.CancelledError properly feat: map deprecated plugins to builtin names docs: update examples using builtins fix: remove mock runtime.yml from test data
1 parent 88e8703 commit 9732b2a

28 files changed

+3756
-18
lines changed

ansible_rulebook/engine.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
from ansible_rulebook.util import (
4444
collect_ansible_facts,
4545
find_builtin_filter,
46+
find_builtin_source,
4647
has_builtin_filter,
48+
has_builtin_source,
4749
send_session_stats,
4850
substitute_variables,
4951
)
@@ -130,6 +132,8 @@ async def start_source(
130132
module = runpy.run_path(
131133
os.path.join(source_dirs[0], source.source_name + ".py")
132134
)
135+
elif has_builtin_source(source.source_name):
136+
module = runpy.run_path(find_builtin_source(source.source_name))
133137
elif has_source(*split_collection_name(source.source_name)):
134138
module = runpy.run_path(
135139
find_source(*split_collection_name(source.source_name))
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import multiprocessing as mp
2+
from typing import Any
3+
4+
DOCUMENTATION = r"""
5+
---
6+
short_description: Change dashes to underscores.
7+
description:
8+
- An event filter that changes dashes in keys to underscores.
9+
For instance, the key X-Y becomes the new key X_Y.
10+
options:
11+
overwrite:
12+
description:
13+
- Overwrite the values if there is a collision with a new key.
14+
type: bool
15+
default: true
16+
"""
17+
18+
EXAMPLES = r"""
19+
- ansible.eda.alertmanager:
20+
host: 0.0.0.0
21+
port: 5050
22+
filters:
23+
- eda.builtin.dashes_to_underscores:
24+
overwrite: false
25+
"""
26+
27+
28+
def _should_replace_key(obj: dict, new_key: str, overwrite: bool) -> bool:
29+
"""Check if the new key should replace the old one."""
30+
return (new_key in obj and overwrite) or (new_key not in obj)
31+
32+
33+
def _process_dict(obj: dict, queue: list, logger, overwrite: bool) -> None:
34+
"""Process dictionary keys, replacing dashes with underscores."""
35+
# list() required: dict modified during iteration (line 40: del obj[key])
36+
for key in list(obj.keys()): # NOSONAR(S7504)
37+
value = obj[key]
38+
queue.append(value)
39+
if "-" in key:
40+
new_key = key.replace("-", "_")
41+
del obj[key]
42+
if _should_replace_key(obj, new_key, overwrite):
43+
obj[new_key] = value
44+
logger.info("Replacing %s with %s", key, new_key)
45+
46+
47+
def main(
48+
event: dict[str, Any],
49+
overwrite: bool = True, # noqa: FBT001, FBT002
50+
) -> dict[str, Any]:
51+
"""Change dashes in keys to underscores."""
52+
logger = mp.get_logger()
53+
logger.info("dashes_to_underscores")
54+
queue = [event]
55+
while queue:
56+
obj = queue.pop()
57+
if isinstance(obj, dict):
58+
_process_dict(obj, queue, logger, overwrite)
59+
elif isinstance(obj, list):
60+
queue.extend(obj)
61+
62+
return event
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from typing import Any
5+
6+
import dpath
7+
8+
DOCUMENTATION = r"""
9+
---
10+
short_description: Extract hosts from the event data and insert them to
11+
the meta dict.
12+
description:
13+
- An ansible-rulebook event filter that extracts hosts from the event
14+
data and inserts them to the meta dict. In ansible-rulebook, this
15+
will limit an action running on hosts in the meta dict.
16+
options:
17+
host_path:
18+
description:
19+
- The json path inside the event data to find hosts.
20+
- Do nothing if the key is not present or does exist in event.
21+
type: str
22+
default: null
23+
path_separator:
24+
description:
25+
- The separator to interpret host_path.
26+
type: str
27+
default: "."
28+
host_separator:
29+
description:
30+
- The separator to interpret host string.
31+
- host_path can point to a string or a list. If it is a single
32+
string but contains multiple hosts, use this parameter to
33+
delimits the hosts. Treat the value as a single host if the
34+
parameter is not present.
35+
type: str
36+
default: null
37+
raise_error:
38+
description:
39+
- Whether raise PathNotExistError if host_path does not
40+
exist in the event.
41+
- It is recommended to turn it on during the rulebook
42+
development time. You can then turn it off for production.
43+
type: bool
44+
default: false
45+
log_error:
46+
description:
47+
- Whether log an error message if host_path does not
48+
exist in the event.
49+
- You can turn if off if it is expected to have events not
50+
having the host_path to avoid noises in the log.
51+
type: bool
52+
default: true
53+
"""
54+
55+
EXAMPLES = r"""
56+
- ansible.eda.alertmanager:
57+
host: 0.0.0.0
58+
port: 5050
59+
filters:
60+
- eda.builtin.insert_hosts_to_meta:
61+
host_path: "app.target"
62+
path_separator: "."
63+
host_separator: ";"
64+
raise_error: true
65+
log_error: true
66+
"""
67+
68+
LOGGER = logging.getLogger(__name__)
69+
70+
71+
class PathNotExistError(Exception):
72+
"""Cannot find the path in the event."""
73+
74+
75+
def _handle_path_error(
76+
event: dict[str, Any],
77+
host_path: str,
78+
error: KeyError,
79+
*,
80+
raise_error: bool,
81+
log_error: bool,
82+
) -> dict[str, Any]:
83+
"""Handle error when host path doesn't exist in event."""
84+
msg = f"Event {event} does not contain {host_path}"
85+
if log_error:
86+
LOGGER.error(msg) # noqa: TRY400
87+
if raise_error:
88+
raise PathNotExistError(msg) from error
89+
return event
90+
91+
92+
def _validate_host_list(hosts: list | tuple) -> None:
93+
"""Validate that all items in the host list are strings."""
94+
for host in hosts:
95+
if not isinstance(host, str):
96+
msg = f"{host} is not a valid hostname"
97+
raise TypeError(msg)
98+
99+
100+
def _normalize_hosts(
101+
hosts: str | list | tuple, host_separator: str | None
102+
) -> list[str]:
103+
"""Normalize hosts to a list of strings."""
104+
if isinstance(hosts, str):
105+
return hosts.split(host_separator) if host_separator else [hosts]
106+
if isinstance(hosts, (list, tuple)):
107+
_validate_host_list(hosts)
108+
return list(hosts)
109+
msg = f"{hosts} is not a valid hostname"
110+
raise TypeError(msg)
111+
112+
113+
# pylint: disable=too-many-arguments
114+
def main(
115+
event: dict[str, Any],
116+
host_path: str | None = None,
117+
host_separator: str | None = None,
118+
path_separator: str = ".",
119+
*,
120+
raise_error: bool = False,
121+
log_error: bool = True,
122+
) -> dict[str, Any]:
123+
"""Extract hosts from event data and insert into meta dict."""
124+
if not host_path:
125+
return event
126+
127+
try:
128+
hosts = dpath.get(event, host_path, path_separator)
129+
except KeyError as error:
130+
return _handle_path_error(
131+
event,
132+
host_path,
133+
error,
134+
raise_error=raise_error,
135+
log_error=log_error,
136+
)
137+
138+
normalized_hosts = _normalize_hosts(hosts, host_separator)
139+
140+
if "meta" not in event:
141+
event["meta"] = {}
142+
event["meta"]["hosts"] = normalized_hosts
143+
return event
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from __future__ import annotations
2+
3+
import fnmatch
4+
from typing import Any, Optional
5+
6+
DOCUMENTATION = r"""
7+
---
8+
short_description: Filter keys out of events.
9+
description:
10+
- An event filter that filters keys out of events.
11+
Includes override excludes.
12+
This is useful to exclude information from events that is not
13+
needed by the rule engine.
14+
options:
15+
exclude_keys:
16+
description:
17+
- A list of strings or patterns to remove.
18+
type: list
19+
elements: str
20+
default: null
21+
include_keys:
22+
description:
23+
- A list of strings or patterns to keep even if it matches
24+
exclude_keys patterns.
25+
type: list
26+
elements: str
27+
default: null
28+
notes:
29+
- The values in both parameters - include_keys and exclude_keys,
30+
must be a full path in top-to-bottom order to the keys to be
31+
filtered (or left-to-right order if it is given as a list),
32+
as shown in the examples below.
33+
"""
34+
35+
EXAMPLES = r"""
36+
- eda.builtin.generic:
37+
payload:
38+
key1:
39+
key2:
40+
f_ignore_1: 1
41+
f_ignore_2: 2
42+
key3:
43+
key4:
44+
f_use_1: 42
45+
f_use_2: 45
46+
filters:
47+
- eda.builtin.json_filter:
48+
include_keys:
49+
- key3
50+
- key4
51+
- f_use*
52+
exclude_keys: ['key1', 'key2', 'f_ignore_1']
53+
"""
54+
55+
56+
def _matches_include_keys(include_keys: list[str], string: str) -> bool:
57+
return any(fnmatch.fnmatch(string, pattern) for pattern in include_keys)
58+
59+
60+
def _matches_exclude_keys(exclude_keys: list[str], string: str) -> bool:
61+
return any(fnmatch.fnmatch(string, pattern) for pattern in exclude_keys)
62+
63+
64+
def _should_include(item: str, include_keys: list[str]) -> bool:
65+
"""Check if item should be included based on include_keys."""
66+
return (item in include_keys) or _matches_include_keys(include_keys, item)
67+
68+
69+
def _should_exclude(item: str, exclude_keys: list[str]) -> bool:
70+
"""Check if item should be excluded based on exclude_keys."""
71+
return (item in exclude_keys) or _matches_exclude_keys(exclude_keys, item)
72+
73+
74+
def _process_dict_keys(
75+
obj: dict[str, Any],
76+
queue: list,
77+
exclude_keys: list[str],
78+
include_keys: list[str],
79+
) -> None:
80+
"""Process dictionary keys for filtering."""
81+
# list() required: dict modified during iteration (line 85: del obj[item])
82+
for item in list(obj.keys()): # NOSONAR(S7504)
83+
if _should_include(item, include_keys):
84+
queue.append(obj[item])
85+
elif _should_exclude(item, exclude_keys):
86+
del obj[item]
87+
else:
88+
queue.append(obj[item])
89+
90+
91+
def main(
92+
event: dict[str, Any],
93+
exclude_keys: Optional[list[str]] = None, # noqa: UP045
94+
include_keys: Optional[list[str]] = None, # noqa: UP045
95+
) -> dict[str, Any]:
96+
"""Filter keys out of events."""
97+
exclude_keys = exclude_keys or []
98+
include_keys = include_keys or []
99+
100+
queue = [event]
101+
while queue:
102+
obj = queue.pop()
103+
if isinstance(obj, dict):
104+
_process_dict_keys(obj, queue, exclude_keys, include_keys)
105+
106+
return event
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Any
2+
3+
DOCUMENTATION = r"""
4+
---
5+
short_description: Do nothing.
6+
description:
7+
- An event filter that does nothing to the input.
8+
"""
9+
10+
11+
def main(event: dict[str, Any]) -> dict[str, Any]:
12+
"""Return the input."""
13+
return event

0 commit comments

Comments
 (0)