Skip to content

Commit 22856c7

Browse files
authored
Increase performance of 'icinga2_object' (#389)
1 parent 0bba765 commit 22856c7

24 files changed

+377
-334
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
major_changes:
2+
- |
3+
The performance of the action plugin :code:`icinga2_object` has been greatly improved.
4+
Instead of writing individual objects to files and later merging them,
5+
they are instead now merged in memory on a per destination basis.
6+
This means that configuration files no longer have to be assembled after the fact.
7+
8+
This also drops the :code:`order` parameter previously used to define the order in which
9+
objects are written if they belong to the same destination file.
10+
The new behavior only changes the order in the files but does not change the end result.
11+
12+
A performance gain of up to 80% has been seen in testing.
13+
14+
known_issues:
15+
- |
16+
With the changes in :code:`icinga2_object` arises a problem.
17+
The prior directory structure within :code:`icinga2_fragments_path` (default: :code:`/var/tmp/icinga/`) does not fit the new approach for writing configuration files.
18+
Some paths that would become directories before are now treated as files.
19+
If the old directory structure is present on a remote host, deployment with the new method will most likely fail due to this.
20+
21+
If the execution of :code:`icinga2_object` fails, deleting :code:`icinga2_fragments_path` should fix the problem.
22+
This, however, is a manual step that needs to be done.

plugins/action/icinga2_object.py

Lines changed: 115 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from ansible.errors import AnsibleError
55
from ansible.plugins.action import ActionBase
66
from ansible.utils.vars import merge_hash
7+
from ansible.utils.display import Display
78
from ansible_collections.netways.icinga.plugins.module_utils.parse import Icinga2Parser
89

910

@@ -13,111 +14,133 @@ def run(self, tmp=None, task_vars=None):
1314

1415
result = super(ActionModule, self).run(tmp, task_vars)
1516

16-
args = dict()
17-
args = self._task.args.copy()
18-
args = merge_hash(args.pop('args', {}), args)
19-
object_type = args.pop('type', None)
20-
21-
if object_type not in task_vars['icinga2_object_types']:
22-
raise AnsibleError('unknown Icinga object type: %s' % object_type)
23-
24-
#
25-
# distribute to object type as module (name: icinga2_type)
26-
#
27-
obj = dict()
28-
obj = self._execute_module(
29-
module_name='icinga2_'+object_type.lower(),
30-
module_args=args,
31-
task_vars=task_vars,
32-
tmp=tmp
33-
)
34-
35-
if 'failed' in obj:
36-
raise AnsibleError('Call to module failed: %s' % obj['msg'])
37-
if 'skipped' in obj and obj['skipped']:
38-
raise AnsibleError('Call to module was skipped: %s' % obj['msg'])
39-
40-
#
41-
# file path handling for assemble
42-
#
43-
path = task_vars['icinga2_fragments_path'] + '/' + obj['file'] + '/'
44-
file_fragment = path + obj['order'] + '_' + object_type.lower() + '-' + obj['name']
45-
46-
if obj['state'] != 'absent':
47-
file_args = dict()
48-
file_args['state'] = 'directory'
49-
file_args['path'] = path
50-
file_module = self._execute_module(
51-
module_name='file',
52-
module_args=file_args,
53-
task_vars=task_vars,
54-
tmp=tmp
17+
arguments = dict()
18+
arguments = self._task.args.copy()
19+
20+
# Create dict to bundle objects that will end up in the same file
21+
destinations = dict()
22+
23+
# Deprecate order key
24+
display = Display()
25+
if [x for x in arguments['objects'] if 'order' in x]:
26+
display.deprecated(
27+
'The \'order\' parameter for different object types is deprecated. It no longer has any effect.'
5528
)
56-
result = merge_hash(result, file_module)
5729

58-
varlist = list() # list of variables from 'apply for'
30+
for args in arguments['objects']:
31+
args = merge_hash(args.pop('args', {}), args)
32+
object_type = args.pop('type', None)
33+
34+
if object_type not in task_vars['icinga2_object_types']:
35+
raise AnsibleError('unknown Icinga object type: %s' % object_type)
5936

6037
#
61-
# quoting of object name?
38+
# distribute to object type as module (name: icinga2_type)
6239
#
63-
if obj['name'] not in task_vars['icinga2_combined_constants']:
64-
object_name = '"' + obj['name'] + '"'
65-
else:
66-
object_name = obj['name']
40+
obj = dict()
41+
obj = self._execute_module(
42+
module_name='icinga2_'+object_type.lower(),
43+
module_args=args,
44+
task_vars=task_vars,
45+
tmp=tmp
46+
)
47+
48+
if 'failed' in obj:
49+
raise AnsibleError('Call to module failed: %s' % obj['msg'])
50+
if 'skipped' in obj and obj['skipped']:
51+
raise AnsibleError('Call to module was skipped: %s' % obj['msg'])
6752

6853
#
69-
# apply rule?
54+
# file path handling for assemble
7055
#
71-
if 'apply' in obj and obj['apply'] and not obj['args']['assign']:
72-
raise AnsibleError('Apply rule %s is missing the assign rule.' % obj['name'])
73-
if 'apply' in obj and obj['apply']:
74-
object_content = 'apply ' + object_type
75-
if 'apply_target' in obj and obj['apply_target']:
76-
object_content += ' ' + object_name + ' to ' + obj['apply_target']
77-
elif 'apply_for' in obj and obj['apply_for']:
78-
object_content += ' for (' + obj['apply_for'] + ') '
79-
r = re.search(r'^(.+)\s+in\s+', obj['apply_for'])
80-
if r:
81-
tmp = r.group(1).strip()
82-
r = re.search(r'^(.+)=>(.+)$', tmp)
56+
path = task_vars['icinga2_fragments_path'] + '/' + obj['file']
57+
if path not in destinations:
58+
destinations[path] = list()
59+
60+
if obj['state'] != 'absent':
61+
varlist = list() # list of variables from 'apply for'
62+
63+
#
64+
# quoting of object name?
65+
#
66+
if obj['name'] not in task_vars['icinga2_combined_constants']:
67+
object_name = '"' + obj['name'] + '"'
68+
else:
69+
object_name = obj['name']
70+
71+
#
72+
# apply rule?
73+
#
74+
if 'apply' in obj and obj['apply'] and not obj['args']['assign']:
75+
raise AnsibleError('Apply rule %s is missing the assign rule.' % obj['name'])
76+
if 'apply' in obj and obj['apply']:
77+
object_content = 'apply ' + object_type
78+
if 'apply_target' in obj and obj['apply_target']:
79+
object_content += ' ' + object_name + ' to ' + obj['apply_target']
80+
elif 'apply_for' in obj and obj['apply_for']:
81+
object_content += ' for (' + obj['apply_for'] + ') '
82+
r = re.search(r'^(.+)\s+in\s+', obj['apply_for'])
8383
if r:
84-
varlist.extend([r.group(1).strip(), r.group(2).strip()])
85-
else:
86-
varlist.append(tmp)
84+
tmp = r.group(1).strip()
85+
r = re.search(r'^(.+)=>(.+)$', tmp)
86+
if r:
87+
varlist.extend([r.group(1).strip(), r.group(2).strip()])
88+
else:
89+
varlist.append(tmp)
90+
else:
91+
object_content += ' ' + object_name
92+
#
93+
# template?
94+
#
95+
elif 'template' in obj and obj['template']:
96+
object_content = 'template ' + object_type + ' ' + object_name
97+
#
98+
# object
99+
#
87100
else:
88-
object_content += ' ' + object_name
89-
#
90-
# template?
91-
#
92-
elif 'template' in obj and obj['template']:
93-
object_content = 'template ' + object_type + ' ' + object_name
94-
#
95-
# object
96-
#
97-
else:
98-
object_content = 'object ' + object_type + ' ' + object_name
99-
object_content += ' {\n'
101+
object_content = 'object ' + object_type + ' ' + object_name
102+
object_content += ' {\n'
100103

101-
#
102-
# imports?
103-
#
104-
if 'imports' in obj:
105-
for item in obj['imports']:
106-
object_content += ' import "' + str(item) + '"\n'
107-
object_content += '\n'
104+
#
105+
# imports?
106+
#
107+
if 'imports' in obj:
108+
for item in obj['imports']:
109+
object_content += ' import "' + str(item) + '"\n'
110+
object_content += '\n'
111+
112+
#
113+
# parser
114+
#
115+
object_content += Icinga2Parser().parse(obj['args'], list(task_vars['icinga2_combined_constants'].keys())+task_vars['icinga2_reserved']+varlist+list(obj['args'].keys()), 2) + '}\n'
116+
destinations[path] += [object_content]
117+
118+
119+
120+
for destination, objects in destinations.items():
121+
# Remove duplicate entries and sort list to ensure idempotency
122+
objects = list(set(objects))
123+
objects.sort()
124+
125+
config_string = '\n\n'.join(objects)
126+
127+
file_args = dict()
128+
file_args['state'] = 'directory'
129+
file_args['path'] = '/'.join(destination.split('/')[:-1])
130+
file_module = self._execute_module(
131+
module_name='file',
132+
module_args=file_args,
133+
task_vars=task_vars,
134+
tmp=tmp
135+
)
136+
result = merge_hash(result, file_module)
137+
138+
varlist = list() # list of variables from 'apply for'
108139

109-
#
110-
# parser
111-
#
112-
object_content += Icinga2Parser().parse(
113-
obj['args'],
114-
list(task_vars['icinga2_combined_constants'].keys()) + task_vars['icinga2_reserved'] + varlist + list(obj['args'].keys()),
115-
2
116-
) + '}\n'
117140
copy_action = self._task.copy()
118141
copy_action.args = dict()
119-
copy_action.args['dest'] = file_fragment
120-
copy_action.args['content'] = object_content
142+
copy_action.args['dest'] = destination
143+
copy_action.args['content'] = config_string
121144

122145
copy_action = self._shared_loader_obj.action_loader.get(
123146
'copy',
@@ -130,19 +153,6 @@ def run(self, tmp=None, task_vars=None):
130153
)
131154

132155
result = merge_hash(result, copy_action.run(task_vars=task_vars))
133-
else:
134-
# remove file if does not belong to a feature
135-
if 'features-available' not in path:
136-
file_args = dict()
137-
file_args['state'] = 'absent'
138-
file_args['path'] = file_fragment
139-
file_module = self._execute_module(
140-
module_name='file',
141-
module_args=file_args,
142-
task_vars=task_vars,
143-
tmp=tmp
144-
)
145-
result = merge_hash(result, file_module)
146-
result['dest'] = file_fragment
147156

157+
result['destinations'] = list(destinations.keys())
148158
return result

roles/icinga2/defaults/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ icinga2_features:
1414
- name: checker
1515
- name: notification
1616
- name: mainlog
17+
icinga2_objects: []
1718
icinga2_remote_objects: []
1819
_icinga2_custom_conf_paths: []
1920
icinga2_config_host: "{{ ansible_fqdn }}"

0 commit comments

Comments
 (0)