Skip to content

Commit 1baf923

Browse files
author
Tiziano Fogli
committed
icinga/audit: add script to dump defined checks
Bug: T395443 Change-Id: If591e2abbdaf9a144512cd0fe45f5dab12ebbe00
1 parent c142c4f commit 1baf923

File tree

3 files changed

+359
-0
lines changed

3 files changed

+359
-0
lines changed

icinga-sunsetting/audit.py

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
#!/usr/bin/env python3
2+
3+
import pandas as pds
4+
import re
5+
import csv
6+
import click
7+
import sys
8+
import json
9+
import jq
10+
11+
from pypuppetdb import connect
12+
from box import Box
13+
from collections import defaultdict
14+
15+
RTYPES = [
16+
{
17+
"rtype": "Nrpe::Monitor_service",
18+
"filter": """
19+
.[]
20+
| select(.file | startswith("/etc") | not)
21+
| .title as $title
22+
| .parameters as $parameters
23+
| .file as $file
24+
| .line as $line
25+
| [
26+
([.tags[] | select(startswith("profile:"))] | sort)
27+
| ($title),
28+
"Nrpe::Monitor_service",
29+
(
30+
if ($parameters.nrpe_command | startswith("/usr/bin/sudo"))
31+
then ($parameters.nrpe_command | split(" ")[0:2] | join(" "))
32+
else ($parameters.nrpe_command | split(" ")[0])
33+
end
34+
),
35+
(. | join("|")),
36+
$parameters.migration_task,
37+
$file,
38+
$line
39+
]
40+
| @csv
41+
""",
42+
},
43+
{
44+
"rtype": "Monitoring::Service",
45+
"filter": """
46+
.[]
47+
| select(.parameters.check_command | startswith("nrpe_check") | not)
48+
| select(.parameters.check_command | startswith("check_prometheus") | not)
49+
| .title as $title
50+
| .parameters as $parameters
51+
| .file as $file
52+
| .line as $line
53+
| [
54+
([.tags[] | select(startswith("profile:"))] | sort)
55+
| ($title),
56+
"Monitoring::Service",
57+
($parameters.check_command | split("!")[0]),
58+
(. | join("|")),
59+
$parameters.migration_task,
60+
$file,
61+
$line
62+
]
63+
| @csv
64+
""",
65+
},
66+
{
67+
"rtype": "Monitoring::Check_prometheus",
68+
"filter": """
69+
.[]
70+
| select(.file | startswith("/etc") | not)
71+
| .title as $title
72+
| .parameters as $parameters
73+
| .file as $file
74+
| .line as $line
75+
| [
76+
([.tags[] | select(startswith("profile:"))] | sort)
77+
| ($title),
78+
"Monitoring::Check_prometheus",
79+
"promql",
80+
(. | join("|")),
81+
"None",
82+
$file,
83+
$line
84+
]
85+
| @csv
86+
""",
87+
},
88+
]
89+
90+
91+
def pdbquery(db, rtype, jqfilter):
92+
93+
pql = f"""
94+
resources [title, parameters, tags, file, line]{{
95+
type = '{rtype}'
96+
}}
97+
"""
98+
99+
resources = jq.compile(jqfilter).input_value(list(db.pql(pql))).all()
100+
101+
return resources
102+
103+
104+
def make_inner_dict():
105+
return {"titles": [], "profiles": set()}
106+
107+
108+
def generalize(strings, placeholder="X", debug=False):
109+
pattern = r"[a-zA-Z0-9]+|[^a-zA-Z0-9]"
110+
patternwosep = r"[a-zA-Z0-9]+"
111+
maxlen = 0
112+
maxstr = re.findall(pattern, strings[0])
113+
for s in strings:
114+
segs = re.findall(pattern, s)
115+
lsegs = len(segs)
116+
if lsegs >= maxlen:
117+
maxlen = lsegs
118+
maxstr = segs
119+
120+
if debug:
121+
print(json.dumps({"generalize_maxstr": maxstr}, indent=2), file=sys.stderr)
122+
123+
generalized = []
124+
for seg in maxstr:
125+
common = True
126+
for s in strings:
127+
if seg not in re.findall(patternwosep, s):
128+
common = False
129+
break
130+
if (common) or (len(re.findall(patternwosep, seg)) == 0):
131+
generalized.append(seg)
132+
else:
133+
generalized.append(placeholder)
134+
135+
return re.sub(r"(X(?:[^a-zA-Z0-9]X)+)", placeholder, "".join(generalized))
136+
137+
138+
@click.command()
139+
@click.option(
140+
"--input-file",
141+
required=True,
142+
help="""
143+
Input file to parse.
144+
Mandatory format: title, resourcetype, command, profiles, task, file, line.
145+
The profiles field is a pipe-separated list of profiles.
146+
If invoked with --resource-list-update, the file may not exist; in that case,
147+
it will be created and updated by querying PuppetDB on localhost.
148+
""",
149+
)
150+
@click.option(
151+
"--resource-list-update",
152+
is_flag=True,
153+
default=False,
154+
help="Force a query to PuppetDB on localhost to update the input file in place.",
155+
)
156+
@click.option("--debug", is_flag=True, default=False, help="Print verbose debug ouput")
157+
def main(input_file, resource_list_update, debug):
158+
"""Query PuppetDB on localhost to produce a list of all Icinga checks defined in Puppet.
159+
The list will be deduplicated based on where the checks are physically declared,
160+
and a heuristic is applied to the resource titles to approximate which parts of the titles are parameterized.
161+
"""
162+
163+
if resource_list_update:
164+
# ssh puppetdb1003.eqiad.wmnet -L 8080:localhost:8080
165+
db = connect()
166+
167+
res = set()
168+
for e in RTYPES:
169+
eb = Box(e)
170+
r = pdbquery(db, eb.rtype, eb.filter)
171+
for i in r:
172+
res.add(i)
173+
174+
with open(input_file, "w") as f:
175+
f.write("\n".join(res))
176+
177+
groups = defaultdict(make_inner_dict)
178+
df = pds.read_csv(input_file, sep=",")
179+
df.columns = [
180+
"title",
181+
"resourcetype",
182+
"command",
183+
"profiles",
184+
"task",
185+
"file",
186+
"line",
187+
]
188+
df = df.fillna("Missing")
189+
df = df.astype(
190+
{
191+
"title": "string",
192+
"resourcetype": "string",
193+
"command": "string",
194+
"profiles": "string",
195+
"task": "string",
196+
"file": "string",
197+
"line": "int64",
198+
}
199+
)
200+
201+
def group(entry):
202+
e = Box(entry.to_dict())
203+
groups[f"{e.resourcetype}#{e.command}#{e.task}#{e.file}#{e.line}"][
204+
"titles"
205+
].append(e.title)
206+
for p in e.profiles.split("|"):
207+
groups[f"{e.resourcetype}#{e.command}#{e.task}#{e.file}#{e.line}"][
208+
"profiles"
209+
].add(p)
210+
211+
df.apply(group, axis=1)
212+
213+
outraw = []
214+
for k, v in groups.items():
215+
bv = Box(v)
216+
resourcetype, command, task, file, line = k.split("#")
217+
if debug:
218+
print(
219+
json.dumps(
220+
{
221+
"titles": bv.titles,
222+
"title": generalize(bv.titles, debug=debug),
223+
"resourcetype": resourcetype,
224+
"command": command,
225+
"profiles_list": list(bv.profiles),
226+
"profiles": "|".join(list(bv.profiles)),
227+
"task": task,
228+
"file": file,
229+
"line": line,
230+
},
231+
indent=2,
232+
),
233+
file=sys.stderr,
234+
)
235+
outraw.append(
236+
{
237+
"title": generalize(bv.titles, debug=debug),
238+
"resourcetype": resourcetype,
239+
"command": command,
240+
"profiles": "|".join(list(bv.profiles)),
241+
"task": task,
242+
"file": file,
243+
"line": line,
244+
}
245+
)
246+
247+
odf = pds.DataFrame(outraw)
248+
odf = odf.astype(
249+
{
250+
"title": "string",
251+
"resourcetype": "string",
252+
"command": "string",
253+
"profiles": "string",
254+
"task": "string",
255+
"file": "string",
256+
"line": "int64",
257+
}
258+
)
259+
260+
print(
261+
odf.to_csv(
262+
index=False, header=False, quotechar='"', quoting=csv.QUOTE_NONNUMERIC
263+
)
264+
)
265+
266+
267+
if __name__ == "__main__":
268+
main()

icinga-sunsetting/queries.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Queries executed by audit.py
2+
3+
## Dump Nrpe::Monitor_service resources
4+
```
5+
curl -G -H "Accept: application/json" http://localhost:8080/pdb/query/v4/resources \
6+
--data-urlencode 'query=["=","type", "Nrpe::Monitor_service"]' | \
7+
jq -r '
8+
.[]
9+
| select(.file | startswith("/etc") | not)
10+
| .title as $title
11+
| .parameters as $parameters
12+
| .file as $file
13+
| .line as $line
14+
| [
15+
([.tags[] | select(startswith("profile:"))] | sort)
16+
| ($title),
17+
"Nrpe::Monitor_service",
18+
(
19+
if ($parameters.nrpe_command | startswith("/usr/bin/sudo"))
20+
then ($parameters.nrpe_command | split(" ")[0:2] | join(" "))
21+
else ($parameters.nrpe_command | split(" ")[0])
22+
end
23+
),
24+
(. | join("|")),
25+
$parameters.migration_task,
26+
$file,
27+
$line
28+
]
29+
| @csv
30+
' | sort | uniq > nrpe_checks.csv
31+
```
32+
33+
## Dump monitoring::Check_prometheus
34+
```
35+
curl -G -H "Accept: application/json" \
36+
http://localhost:8080/pdb/query/v4/resources \
37+
--data-urlencode 'query=["=","type", "Monitoring::Check_prometheus"]' | \
38+
jq -r '
39+
.[]
40+
| select(.file | startswith("/etc") | not)
41+
| .title as $title
42+
| .parameters as $parameters
43+
| .file as $file
44+
| .line as $line
45+
| [
46+
([.tags[] | select(startswith("profile:"))] | sort)
47+
| ($title),
48+
"Monitoring::Check_prometheus",
49+
"promql",
50+
(. | join("|")),
51+
"T321808",
52+
$file,
53+
$line
54+
]
55+
| @csv
56+
' | sort | uniq > prometheus_checks.csv
57+
```
58+
59+
## Dump pure Monitoring::Service resources
60+
```
61+
curl -G -H "Accept: application/json" \
62+
http://localhost:8080/pdb/query/v4/resources \
63+
--data-urlencode 'query=["=","type", "Monitoring::Service"]' | \
64+
jq -r '
65+
.[]
66+
| select(.parameters.check_command | startswith("nrpe_check") | not)
67+
| select(.parameters.check_command | startswith("check_prometheus") | not)
68+
| .title as $title
69+
| .parameters as $parameters
70+
| .file as $file
71+
| .line as $line
72+
| [
73+
([.tags[] | select(startswith("profile:"))] | sort)
74+
| ($title),
75+
"Monitoring::Service",
76+
($parameters.check_command | split("!")[0]),
77+
(. | join("|")),
78+
$parameters.migration_task,
79+
$file,
80+
$line
81+
]
82+
| @csv
83+
' | sort | uniq > monitoring_services.csv
84+
```
85+
86+

icinga-sunsetting/requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pandas
2+
jq
3+
python-box
4+
pypuppetdb
5+
click

0 commit comments

Comments
 (0)