1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11ANSIBLE_METADATA = {
12    'metadata_version': '1.1',
13    'status': ['preview'],
14    'supported_by': 'community'
15}
16
17DOCUMENTATION = '''
18---
19module: zabbix_action
20
21short_description: Create/Delete/Update Zabbix actions
22
23version_added: "2.8"
24
25description:
26    - This module allows you to create, modify and delete Zabbix actions.
27
28author:
29    - Ruben Tsirunyan (@rubentsirunyan)
30    - Ruben Harutyunov (@K-DOT)
31
32requirements:
33    - "zabbix-api >= 0.5.4"
34
35options:
36    name:
37        description:
38            - Name of the action
39        required: true
40    event_source:
41        description:
42            - Type of events that the action will handle.
43            - Required when C(state=present).
44        required: false
45        choices: ['trigger', 'discovery', 'auto_registration', 'internal']
46    state:
47        description:
48            - State of the action.
49            - On C(present), it will create an action if it does not exist or update the action if the associated data is different.
50            - On C(absent), it will remove the action if it exists.
51        choices: ['present', 'absent']
52        default: 'present'
53    status:
54        description:
55            - Status of the action.
56        choices: ['enabled', 'disabled']
57        default: 'enabled'
58    pause_in_maintenance:
59        description:
60            - Whether to pause escalation during maintenance periods or not.
61            - Can be used when I(event_source=trigger).
62        type: 'bool'
63        default: true
64    esc_period:
65        description:
66            - Default operation step duration. Must be greater than 60 seconds. Accepts seconds, time unit with suffix and user macro.
67            - Required when C(state=present).
68        required: false
69    conditions:
70        type: list
71        description:
72            - List of dictionaries of conditions to evaluate.
73            - For more information about suboptions of this option please
74              check out Zabbix API documentation U(https://www.zabbix.com/documentation/3.4/manual/api/reference/action/object#action_filter_condition)
75        suboptions:
76            type:
77                description: Type (label) of the condition.
78                choices:
79                    # trigger
80                    - host_group
81                    - host
82                    - trigger
83                    - trigger_name
84                    - trigger_severity
85                    - time_period
86                    - host_template
87                    - application
88                    - maintenance_status
89                    - event_tag
90                    - event_tag_value
91                    # discovery
92                    - host_IP
93                    - discovered_service_type
94                    - discovered_service_port
95                    - discovery_status
96                    - uptime_or_downtime_duration
97                    - received_value
98                    - discovery_rule
99                    - discovery_check
100                    - proxy
101                    - discovery_object
102                    # auto_registration
103                    - proxy
104                    - host_name
105                    - host_metadata
106                    # internal
107                    - host_group
108                    - host
109                    - host_template
110                    - application
111                    - event_type
112            value:
113                description:
114                    - Value to compare with.
115                    - When I(type) is set to C(discovery_status), the choices
116                      are C(up), C(down), C(discovered), C(lost).
117                    - When I(type) is set to C(discovery_object), the choices
118                      are C(host), C(service).
119                    - When I(type) is set to C(event_type), the choices
120                      are C(item in not supported state), C(item in normal state),
121                      C(LLD rule in not supported state),
122                      C(LLD rule in normal state), C(trigger in unknown state), C(trigger in normal state).
123                    - When I(type) is set to C(trigger_severity), the choices
124                      are (case-insensitive) C(not classified), C(information), C(warning), C(average), C(high), C(disaster)
125                      irrespective of user-visible names being changed in Zabbix. Defaults to C(not classified) if omitted.
126                    - Besides the above options, this is usually either the name
127                      of the object or a string to compare with.
128            operator:
129                description:
130                    - Condition operator.
131                    - When I(type) is set to C(time_period), the choices are C(in), C(not in).
132                    - C(matches), C(does not match), C(Yes) and C(No) condition operators work only with >= Zabbix 4.0
133                choices:
134                    - '='
135                    - '<>'
136                    - 'like'
137                    - 'not like'
138                    - 'in'
139                    - '>='
140                    - '<='
141                    - 'not in'
142                    - 'matches'
143                    - 'does not match'
144                    - 'Yes'
145                    - 'No'
146            formulaid:
147                description:
148                    - Arbitrary unique ID that is used to reference the condition from a custom expression.
149                    - Can only contain upper-case letters.
150                    - Required for custom expression filters.
151    eval_type:
152        description:
153            - Filter condition evaluation method.
154            - Defaults to C(andor) if conditions are less then 2 or if
155              I(formula) is not specified.
156            - Defaults to C(custom_expression) when formula is specified.
157        choices:
158            - 'andor'
159            - 'and'
160            - 'or'
161            - 'custom_expression'
162    formula:
163        description:
164            - User-defined expression to be used for evaluating conditions of filters with a custom expression.
165            - The expression must contain IDs that reference specific filter conditions by its formulaid.
166            - The IDs used in the expression must exactly match the ones
167              defined in the filter conditions. No condition can remain unused or omitted.
168            - Required for custom expression filters.
169            - Use sequential IDs that start at "A". If non-sequential IDs are used, Zabbix re-indexes them.
170              This makes each module run notice the difference in IDs and update the action.
171    default_message:
172        description:
173            - Problem message default text.
174    default_subject:
175        description:
176            - Problem message default subject.
177    recovery_default_message:
178        description:
179            - Recovery message text.
180            - Works only with >= Zabbix 3.2
181    recovery_default_subject:
182        description:
183            - Recovery message subject.
184            - Works only with >= Zabbix 3.2
185    acknowledge_default_message:
186        description:
187            - Update operation (known as "Acknowledge operation" before Zabbix 4.0) message text.
188            - Works only with >= Zabbix 3.4
189    acknowledge_default_subject:
190        description:
191            - Update operation (known as "Acknowledge operation" before Zabbix 4.0) message subject.
192            - Works only with >= Zabbix 3.4
193    operations:
194        type: list
195        description:
196            - List of action operations
197        suboptions:
198            type:
199                description:
200                    - Type of operation.
201                choices:
202                    - send_message
203                    - remote_command
204                    - add_host
205                    - remove_host
206                    - add_to_host_group
207                    - remove_from_host_group
208                    - link_to_template
209                    - unlink_from_template
210                    - enable_host
211                    - disable_host
212                    - set_host_inventory_mode
213            esc_period:
214                description:
215                    - Duration of an escalation step in seconds.
216                    - Must be greater than 60 seconds.
217                    - Accepts seconds, time unit with suffix and user macro.
218                    - If set to 0 or 0s, the default action escalation period will be used.
219                default: 0s
220            esc_step_from:
221                description:
222                    - Step to start escalation from.
223                default: 1
224            esc_step_to:
225                description:
226                    - Step to end escalation at.
227                default: 1
228            send_to_groups:
229                type: list
230                description:
231                    - User groups to send messages to.
232            send_to_users:
233                type: list
234                description:
235                    - Users (usernames or aliases) to send messages to.
236            message:
237                description:
238                    - Operation message text.
239                    - Will check the 'default message' and use the text from I(default_message) if this and I(default_subject) are not specified
240            subject:
241                description:
242                    - Operation message subject.
243                    - Will check the 'default message' and use the text from I(default_subject) if this and I(default_subject) are not specified
244            media_type:
245                description:
246                    - Media type that will be used to send the message.
247                    - Set to C(all) for all media types
248                default: 'all'
249            operation_condition:
250                type: 'str'
251                description:
252                    - The action operation condition object defines a condition that must be met to perform the current operation.
253                choices:
254                    - acknowledged
255                    - not_acknowledged
256            host_groups:
257                type: list
258                description:
259                    - List of host groups host should be added to.
260                    - Required when I(type=add_to_host_group) or I(type=remove_from_host_group).
261            templates:
262                type: list
263                description:
264                    - List of templates host should be linked to.
265                    - Required when I(type=link_to_template) or I(type=unlink_from_template).
266            inventory:
267                description:
268                    - Host inventory mode.
269                    - Required when I(type=set_host_inventory_mode).
270            command_type:
271                description:
272                    - Type of operation command.
273                    - Required when I(type=remote_command).
274                choices:
275                    - custom_script
276                    - ipmi
277                    - ssh
278                    - telnet
279                    - global_script
280            command:
281                description:
282                    - Command to run.
283                    - Required when I(type=remote_command) and I(command_type!=global_script).
284            execute_on:
285                description:
286                    - Target on which the custom script operation command will be executed.
287                    - Required when I(type=remote_command) and I(command_type=custom_script).
288                choices:
289                    - agent
290                    - server
291                    - proxy
292            run_on_groups:
293                description:
294                    - Host groups to run remote commands on.
295                    - Required when I(type=remote_command) if I(run_on_hosts) is not set.
296            run_on_hosts:
297                description:
298                    - Hosts to run remote commands on.
299                    - Required when I(type=remote_command) if I(run_on_groups) is not set.
300                    - If set to 0 the command will be run on the current host.
301            ssh_auth_type:
302                description:
303                    - Authentication method used for SSH commands.
304                    - Required when I(type=remote_command) and I(command_type=ssh).
305                choices:
306                    - password
307                    - public_key
308            ssh_privatekey_file:
309                description:
310                    - Name of the private key file used for SSH commands with public key authentication.
311                    - Required when I(type=remote_command) and I(command_type=ssh).
312            ssh_publickey_file:
313                description:
314                    - Name of the public key file used for SSH commands with public key authentication.
315                    - Required when I(type=remote_command) and I(command_type=ssh).
316            username:
317                description:
318                    - User name used for authentication.
319                    - Required when I(type=remote_command) and I(command_type in [ssh, telnet]).
320            password:
321                description:
322                    - Password used for authentication.
323                    - Required when I(type=remote_command) and I(command_type in [ssh, telnet]).
324            port:
325                description:
326                    - Port number used for authentication.
327                    - Required when I(type=remote_command) and I(command_type in [ssh, telnet]).
328            script_name:
329                description:
330                    - The name of script used for global script commands.
331                    - Required when I(type=remote_command) and I(command_type=global_script).
332    recovery_operations:
333        type: list
334        description:
335            - List of recovery operations.
336            - C(Suboptions) are the same as for I(operations).
337            - Works only with >= Zabbix 3.2
338    acknowledge_operations:
339        type: list
340        description:
341            - List of acknowledge operations.
342            - C(Suboptions) are the same as for I(operations).
343            - Works only with >= Zabbix 3.4
344
345notes:
346    - Only Zabbix >= 3.0 is supported.
347
348
349extends_documentation_fragment:
350    - zabbix
351'''
352
353EXAMPLES = '''
354# Trigger action with only one condition
355- name: Deploy trigger action
356  zabbix_action:
357    server_url: "http://zabbix.example.com/zabbix/"
358    login_user: Admin
359    login_password: secret
360    name: "Send alerts to Admin"
361    event_source: 'trigger'
362    state: present
363    status: enabled
364    esc_period: 60
365    conditions:
366      - type: 'trigger_severity'
367        operator: '>='
368        value: 'Information'
369    operations:
370      - type: send_message
371        subject: "Something bad is happening"
372        message: "Come on, guys do something"
373        media_type: 'Email'
374        send_to_users:
375          - 'Admin'
376
377# Trigger action with multiple conditions and operations
378- name: Deploy trigger action
379  zabbix_action:
380    server_url: "http://zabbix.example.com/zabbix/"
381    login_user: Admin
382    login_password: secret
383    name: "Send alerts to Admin"
384    event_source: 'trigger'
385    state: present
386    status: enabled
387    esc_period: 60
388    conditions:
389      - type: 'trigger_name'
390        operator: 'like'
391        value: 'Zabbix agent is unreachable'
392        formulaid: A
393      - type: 'trigger_severity'
394        operator: '>='
395        value: 'disaster'
396        formulaid: B
397    formula: A or B
398    operations:
399      - type: send_message
400        media_type: 'Email'
401        send_to_users:
402          - 'Admin'
403      - type: remote_command
404        command: 'systemctl restart zabbix-agent'
405        command_type: custom_script
406        execute_on: server
407        run_on_hosts:
408          - 0
409
410# Trigger action with recovery and acknowledge operations
411- name: Deploy trigger action
412  zabbix_action:
413    server_url: "http://zabbix.example.com/zabbix/"
414    login_user: Admin
415    login_password: secret
416    name: "Send alerts to Admin"
417    event_source: 'trigger'
418    state: present
419    status: enabled
420    esc_period: 60
421    conditions:
422      - type: 'trigger_severity'
423        operator: '>='
424        value: 'Information'
425    operations:
426      - type: send_message
427        subject: "Something bad is happening"
428        message: "Come on, guys do something"
429        media_type: 'Email'
430        send_to_users:
431          - 'Admin'
432    recovery_operations:
433      - type: send_message
434        subject: "Host is down"
435        message: "Come on, guys do something"
436        media_type: 'Email'
437        send_to_users:
438          - 'Admin'
439    acknowledge_operations:
440      - type: send_message
441        media_type: 'Email'
442        send_to_users:
443          - 'Admin'
444'''
445
446RETURN = '''
447msg:
448    description: The result of the operation
449    returned: success
450    type: str
451    sample: 'Action Deleted: Register webservers, ID: 0001'
452'''
453
454
455import atexit
456import traceback
457
458try:
459    from zabbix_api import ZabbixAPI
460    HAS_ZABBIX_API = True
461except ImportError:
462    ZBX_IMP_ERR = traceback.format_exc()
463    HAS_ZABBIX_API = False
464
465from ansible.module_utils.basic import AnsibleModule, missing_required_lib
466
467
468class Zapi(object):
469    """
470    A simple wrapper over the Zabbix API
471    """
472    def __init__(self, module, zbx):
473        self._module = module
474        self._zapi = zbx
475
476    def check_if_action_exists(self, name):
477        """Check if action exists.
478
479        Args:
480            name: Name of the action.
481
482        Returns:
483            The return value. True for success, False otherwise.
484
485        """
486        try:
487            _action = self._zapi.action.get({
488                "selectOperations": "extend",
489                "selectRecoveryOperations": "extend",
490                "selectAcknowledgeOperations": "extend",
491                "selectFilter": "extend",
492                'selectInventory': 'extend',
493                'filter': {'name': [name]}
494            })
495            if len(_action) > 0:
496                _action[0]['recovery_operations'] = _action[0].pop('recoveryOperations', [])
497                _action[0]['acknowledge_operations'] = _action[0].pop('acknowledgeOperations', [])
498            return _action
499        except Exception as e:
500            self._module.fail_json(msg="Failed to check if action '%s' exists: %s" % (name, e))
501
502    def get_action_by_name(self, name):
503        """Get action by name
504
505        Args:
506            name: Name of the action.
507
508        Returns:
509            dict: Zabbix action
510
511        """
512        try:
513            action_list = self._zapi.action.get({
514                'output': 'extend',
515                'selectInventory': 'extend',
516                'filter': {'name': [name]}
517            })
518            if len(action_list) < 1:
519                self._module.fail_json(msg="Action not found: " % name)
520            else:
521                return action_list[0]
522        except Exception as e:
523            self._module.fail_json(msg="Failed to get ID of '%s': %s" % (name, e))
524
525    def get_host_by_host_name(self, host_name):
526        """Get host by host name
527
528        Args:
529            host_name: host name.
530
531        Returns:
532            host matching host name
533
534        """
535        try:
536            host_list = self._zapi.host.get({
537                'output': 'extend',
538                'selectInventory': 'extend',
539                'filter': {'host': [host_name]}
540            })
541            if len(host_list) < 1:
542                self._module.fail_json(msg="Host not found: %s" % host_name)
543            else:
544                return host_list[0]
545        except Exception as e:
546            self._module.fail_json(msg="Failed to get host '%s': %s" % (host_name, e))
547
548    def get_hostgroup_by_hostgroup_name(self, hostgroup_name):
549        """Get host group by host group name
550
551        Args:
552            hostgroup_name: host group name.
553
554        Returns:
555            host group matching host group name
556
557        """
558        try:
559            hostgroup_list = self._zapi.hostgroup.get({
560                'output': 'extend',
561                'selectInventory': 'extend',
562                'filter': {'name': [hostgroup_name]}
563            })
564            if len(hostgroup_list) < 1:
565                self._module.fail_json(msg="Host group not found: %s" % hostgroup_name)
566            else:
567                return hostgroup_list[0]
568        except Exception as e:
569            self._module.fail_json(msg="Failed to get host group '%s': %s" % (hostgroup_name, e))
570
571    def get_template_by_template_name(self, template_name):
572        """Get template by template name
573
574        Args:
575            template_name: template name.
576
577        Returns:
578            template matching template name
579
580        """
581        try:
582            template_list = self._zapi.template.get({
583                'output': 'extend',
584                'selectInventory': 'extend',
585                'filter': {'host': [template_name]}
586            })
587            if len(template_list) < 1:
588                self._module.fail_json(msg="Template not found: %s" % template_name)
589            else:
590                return template_list[0]
591        except Exception as e:
592            self._module.fail_json(msg="Failed to get template '%s': %s" % (template_name, e))
593
594    def get_trigger_by_trigger_name(self, trigger_name):
595        """Get trigger by trigger name
596
597        Args:
598            trigger_name: trigger name.
599
600        Returns:
601            trigger matching trigger name
602
603        """
604        try:
605            trigger_list = self._zapi.trigger.get({
606                'output': 'extend',
607                'selectInventory': 'extend',
608                'filter': {'description': [trigger_name]}
609            })
610            if len(trigger_list) < 1:
611                self._module.fail_json(msg="Trigger not found: %s" % trigger_name)
612            else:
613                return trigger_list[0]
614        except Exception as e:
615            self._module.fail_json(msg="Failed to get trigger '%s': %s" % (trigger_name, e))
616
617    def get_discovery_rule_by_discovery_rule_name(self, discovery_rule_name):
618        """Get discovery rule by discovery rule name
619
620        Args:
621            discovery_rule_name: discovery rule name.
622
623        Returns:
624            discovery rule matching discovery rule name
625
626        """
627        try:
628            discovery_rule_list = self._zapi.drule.get({
629                'output': 'extend',
630                'selectInventory': 'extend',
631                'filter': {'name': [discovery_rule_name]}
632            })
633            if len(discovery_rule_list) < 1:
634                self._module.fail_json(msg="Discovery rule not found: %s" % discovery_rule_name)
635            else:
636                return discovery_rule_list[0]
637        except Exception as e:
638            self._module.fail_json(msg="Failed to get discovery rule '%s': %s" % (discovery_rule_name, e))
639
640    def get_discovery_check_by_discovery_check_name(self, discovery_check_name):
641        """Get discovery check  by discovery check name
642
643        Args:
644            discovery_check_name: discovery check name.
645
646        Returns:
647            discovery check matching discovery check name
648
649        """
650        try:
651            discovery_check_list = self._zapi.dcheck.get({
652                'output': 'extend',
653                'selectInventory': 'extend',
654                'filter': {'name': [discovery_check_name]}
655            })
656            if len(discovery_check_list) < 1:
657                self._module.fail_json(msg="Discovery check not found: %s" % discovery_check_name)
658            else:
659                return discovery_check_list[0]
660        except Exception as e:
661            self._module.fail_json(msg="Failed to get discovery check '%s': %s" % (discovery_check_name, e))
662
663    def get_proxy_by_proxy_name(self, proxy_name):
664        """Get proxy by proxy name
665
666        Args:
667            proxy_name: proxy name.
668
669        Returns:
670            proxy matching proxy name
671
672        """
673        try:
674            proxy_list = self._zapi.proxy.get({
675                'output': 'extend',
676                'selectInventory': 'extend',
677                'filter': {'host': [proxy_name]}
678            })
679            if len(proxy_list) < 1:
680                self._module.fail_json(msg="Proxy not found: %s" % proxy_name)
681            else:
682                return proxy_list[0]
683        except Exception as e:
684            self._module.fail_json(msg="Failed to get proxy '%s': %s" % (proxy_name, e))
685
686    def get_mediatype_by_mediatype_name(self, mediatype_name):
687        """Get mediatype by mediatype name
688
689        Args:
690            mediatype_name: mediatype name
691
692        Returns:
693            mediatype matching mediatype name
694
695        """
696        try:
697            if str(mediatype_name).lower() == 'all':
698                return '0'
699            mediatype_list = self._zapi.mediatype.get({
700                'output': 'extend',
701                'selectInventory': 'extend',
702                'filter': {'description': [mediatype_name]}
703            })
704            if len(mediatype_list) < 1:
705                self._module.fail_json(msg="Media type not found: %s" % mediatype_name)
706            else:
707                return mediatype_list[0]['mediatypeid']
708        except Exception as e:
709            self._module.fail_json(msg="Failed to get mediatype '%s': %s" % (mediatype_name, e))
710
711    def get_user_by_user_name(self, user_name):
712        """Get user by user name
713
714        Args:
715            user_name: user name
716
717        Returns:
718            user matching user name
719
720        """
721        try:
722            user_list = self._zapi.user.get({
723                'output': 'extend',
724                'selectInventory':
725                'extend', 'filter': {'alias': [user_name]}
726            })
727            if len(user_list) < 1:
728                self._module.fail_json(msg="User not found: %s" % user_name)
729            else:
730                return user_list[0]
731        except Exception as e:
732            self._module.fail_json(msg="Failed to get user '%s': %s" % (user_name, e))
733
734    def get_usergroup_by_usergroup_name(self, usergroup_name):
735        """Get usergroup by usergroup name
736
737        Args:
738            usergroup_name: usergroup name
739
740        Returns:
741            usergroup matching usergroup name
742
743        """
744        try:
745            usergroup_list = self._zapi.usergroup.get({
746                'output': 'extend',
747                'selectInventory': 'extend',
748                'filter': {'name': [usergroup_name]}
749            })
750            if len(usergroup_list) < 1:
751                self._module.fail_json(msg="User group not found: %s" % usergroup_name)
752            else:
753                return usergroup_list[0]
754        except Exception as e:
755            self._module.fail_json(msg="Failed to get user group '%s': %s" % (usergroup_name, e))
756
757    # get script by script name
758    def get_script_by_script_name(self, script_name):
759        """Get script by script name
760
761        Args:
762            script_name: script name
763
764        Returns:
765            script matching script name
766
767        """
768        try:
769            if script_name is None:
770                return {}
771            script_list = self._zapi.script.get({
772                'output': 'extend',
773                'selectInventory': 'extend',
774                'filter': {'name': [script_name]}
775            })
776            if len(script_list) < 1:
777                self._module.fail_json(msg="Script not found: %s" % script_name)
778            else:
779                return script_list[0]
780        except Exception as e:
781            self._module.fail_json(msg="Failed to get script '%s': %s" % (script_name, e))
782
783
784class Action(object):
785    """
786    Restructures the user defined action data to fit the Zabbix API requirements
787    """
788    def __init__(self, module, zbx, zapi_wrapper):
789        self._module = module
790        self._zapi = zbx
791        self._zapi_wrapper = zapi_wrapper
792
793    def _construct_parameters(self, **kwargs):
794        """Construct parameters.
795
796        Args:
797            **kwargs: Arbitrary keyword parameters.
798
799        Returns:
800            dict: dictionary of specified parameters
801        """
802
803        _params = {
804            'name': kwargs['name'],
805            'eventsource': to_numeric_value([
806                'trigger',
807                'discovery',
808                'auto_registration',
809                'internal'], kwargs['event_source']),
810            'esc_period': kwargs.get('esc_period'),
811            'filter': kwargs['conditions'],
812            'def_longdata': kwargs['default_message'],
813            'def_shortdata': kwargs['default_subject'],
814            'r_longdata': kwargs['recovery_default_message'],
815            'r_shortdata': kwargs['recovery_default_subject'],
816            'ack_longdata': kwargs['acknowledge_default_message'],
817            'ack_shortdata': kwargs['acknowledge_default_subject'],
818            'operations': kwargs['operations'],
819            'recovery_operations': kwargs.get('recovery_operations'),
820            'acknowledge_operations': kwargs.get('acknowledge_operations'),
821            'status': to_numeric_value([
822                'enabled',
823                'disabled'], kwargs['status'])
824        }
825        if kwargs['event_source'] == 'trigger':
826            if float(self._zapi.api_version().rsplit('.', 1)[0]) >= 4.0:
827                _params['pause_suppressed'] = '1' if kwargs['pause_in_maintenance'] else '0'
828            else:
829                _params['maintenance_mode'] = '1' if kwargs['pause_in_maintenance'] else '0'
830
831        return _params
832
833    def check_difference(self, **kwargs):
834        """Check difference between action and user specified parameters.
835
836        Args:
837            **kwargs: Arbitrary keyword parameters.
838
839        Returns:
840            dict: dictionary of differences
841        """
842        existing_action = convert_unicode_to_str(self._zapi_wrapper.check_if_action_exists(kwargs['name'])[0])
843        parameters = convert_unicode_to_str(self._construct_parameters(**kwargs))
844        change_parameters = {}
845        _diff = cleanup_data(compare_dictionaries(parameters, existing_action, change_parameters))
846        return _diff
847
848    def update_action(self, **kwargs):
849        """Update action.
850
851        Args:
852            **kwargs: Arbitrary keyword parameters.
853
854        Returns:
855            action: updated action
856        """
857        try:
858            if self._module.check_mode:
859                self._module.exit_json(msg="Action would be updated if check mode was not specified: %s" % kwargs, changed=True)
860            kwargs['actionid'] = kwargs.pop('action_id')
861            return self._zapi.action.update(kwargs)
862        except Exception as e:
863            self._module.fail_json(msg="Failed to update action '%s': %s" % (kwargs['actionid'], e))
864
865    def add_action(self, **kwargs):
866        """Add action.
867
868        Args:
869            **kwargs: Arbitrary keyword parameters.
870
871        Returns:
872            action: added action
873        """
874        try:
875            if self._module.check_mode:
876                self._module.exit_json(msg="Action would be added if check mode was not specified", changed=True)
877            parameters = self._construct_parameters(**kwargs)
878            action_list = self._zapi.action.create(parameters)
879            return action_list['actionids'][0]
880        except Exception as e:
881            self._module.fail_json(msg="Failed to create action '%s': %s" % (kwargs['name'], e))
882
883    def delete_action(self, action_id):
884        """Delete action.
885
886        Args:
887            action_id: Action id
888
889        Returns:
890            action: deleted action
891        """
892        try:
893            if self._module.check_mode:
894                self._module.exit_json(msg="Action would be deleted if check mode was not specified", changed=True)
895            return self._zapi.action.delete([action_id])
896        except Exception as e:
897            self._module.fail_json(msg="Failed to delete action '%s': %s" % (action_id, e))
898
899
900class Operations(object):
901    """
902    Restructures the user defined operation data to fit the Zabbix API requirements
903    """
904    def __init__(self, module, zbx, zapi_wrapper):
905        self._module = module
906        # self._zapi = zbx
907        self._zapi_wrapper = zapi_wrapper
908
909    def _construct_operationtype(self, operation):
910        """Construct operation type.
911
912        Args:
913            operation: operation to construct
914
915        Returns:
916            str: constructed operation
917        """
918        try:
919            return to_numeric_value([
920                "send_message",
921                "remote_command",
922                "add_host",
923                "remove_host",
924                "add_to_host_group",
925                "remove_from_host_group",
926                "link_to_template",
927                "unlink_from_template",
928                "enable_host",
929                "disable_host",
930                "set_host_inventory_mode"], operation['type']
931            )
932        except Exception as e:
933            self._module.fail_json(msg="Unsupported value '%s' for operation type." % operation['type'])
934
935    def _construct_opmessage(self, operation):
936        """Construct operation message.
937
938        Args:
939            operation: operation to construct the message
940
941        Returns:
942            dict: constructed operation message
943        """
944        try:
945            return {
946                'default_msg': '0' if operation.get('message') is not None or operation.get('subject')is not None else '1',
947                'mediatypeid': self._zapi_wrapper.get_mediatype_by_mediatype_name(
948                    operation.get('media_type')
949                ) if operation.get('media_type') is not None else '0',
950                'message': operation.get('message'),
951                'subject': operation.get('subject'),
952            }
953        except Exception as e:
954            self._module.fail_json(msg="Failed to construct operation message. The error was: %s" % e)
955
956    def _construct_opmessage_usr(self, operation):
957        """Construct operation message user.
958
959        Args:
960            operation: operation to construct the message user
961
962        Returns:
963            list: constructed operation message user or None if operation not found
964        """
965        if operation.get('send_to_users') is None:
966            return None
967        return [{
968            'userid': self._zapi_wrapper.get_user_by_user_name(_user)['userid']
969        } for _user in operation.get('send_to_users')]
970
971    def _construct_opmessage_grp(self, operation):
972        """Construct operation message group.
973
974        Args:
975            operation: operation to construct the message group
976
977        Returns:
978            list: constructed operation message group or None if operation not found
979        """
980        if operation.get('send_to_groups') is None:
981            return None
982        return [{
983            'usrgrpid': self._zapi_wrapper.get_usergroup_by_usergroup_name(_group)['usrgrpid']
984        } for _group in operation.get('send_to_groups')]
985
986    def _construct_opcommand(self, operation):
987        """Construct operation command.
988
989        Args:
990            operation: operation to construct command
991
992        Returns:
993            list: constructed operation command
994        """
995        try:
996            return {
997                'type': to_numeric_value([
998                    'custom_script',
999                    'ipmi',
1000                    'ssh',
1001                    'telnet',
1002                    'global_script'], operation.get('command_type', 'custom_script')),
1003                'command': operation.get('command'),
1004                'execute_on': to_numeric_value([
1005                    'agent',
1006                    'server',
1007                    'proxy'], operation.get('execute_on', 'server')),
1008                'scriptid': self._zapi_wrapper.get_script_by_script_name(
1009                    operation.get('script_name')
1010                ).get('scriptid'),
1011                'authtype': to_numeric_value([
1012                    'password',
1013                    'private_key'
1014                ], operation.get('ssh_auth_type', 'password')),
1015                'privatekey': operation.get('ssh_privatekey_file'),
1016                'publickey': operation.get('ssh_publickey_file'),
1017                'username': operation.get('username'),
1018                'password': operation.get('password'),
1019                'port': operation.get('port')
1020            }
1021        except Exception as e:
1022            self._module.fail_json(msg="Failed to construct operation command. The error was: %s" % e)
1023
1024    def _construct_opcommand_hst(self, operation):
1025        """Construct operation command host.
1026
1027        Args:
1028            operation: operation to construct command host
1029
1030        Returns:
1031            list: constructed operation command host
1032        """
1033        if operation.get('run_on_hosts') is None:
1034            return None
1035        return [{
1036            'hostid': self._zapi_wrapper.get_host_by_host_name(_host)['hostid']
1037        } if str(_host) != '0' else {'hostid': '0'} for _host in operation.get('run_on_hosts')]
1038
1039    def _construct_opcommand_grp(self, operation):
1040        """Construct operation command group.
1041
1042        Args:
1043            operation: operation to construct command group
1044
1045        Returns:
1046            list: constructed operation command group
1047        """
1048        if operation.get('run_on_groups') is None:
1049            return None
1050        return [{
1051            'groupid': self._zapi_wrapper.get_hostgroup_by_hostgroup_name(_group)['hostid']
1052        } for _group in operation.get('run_on_groups')]
1053
1054    def _construct_opgroup(self, operation):
1055        """Construct operation group.
1056
1057        Args:
1058            operation: operation to construct group
1059
1060        Returns:
1061            list: constructed operation group
1062        """
1063        return [{
1064            'groupid': self._zapi_wrapper.get_hostgroup_by_hostgroup_name(_group)['groupid']
1065        } for _group in operation.get('host_groups', [])]
1066
1067    def _construct_optemplate(self, operation):
1068        """Construct operation template.
1069
1070        Args:
1071            operation: operation to construct template
1072
1073        Returns:
1074            list: constructed operation template
1075        """
1076        return [{
1077            'templateid': self._zapi_wrapper.get_template_by_template_name(_template)['templateid']
1078        } for _template in operation.get('templates', [])]
1079
1080    def _construct_opinventory(self, operation):
1081        """Construct operation inventory.
1082
1083        Args:
1084            operation: operation to construct inventory
1085
1086        Returns:
1087            dict: constructed operation inventory
1088        """
1089        return {'inventory_mode': operation.get('inventory')}
1090
1091    def _construct_opconditions(self, operation):
1092        """Construct operation conditions.
1093
1094        Args:
1095            operation: operation to construct the conditions
1096
1097        Returns:
1098            list: constructed operation conditions
1099        """
1100        _opcond = operation.get('operation_condition')
1101        if _opcond is not None:
1102            if _opcond == 'acknowledged':
1103                _value = '1'
1104            elif _opcond == 'not_acknowledged':
1105                _value = '0'
1106            return [{
1107                'conditiontype': '14',
1108                'operator': '0',
1109                'value': _value
1110            }]
1111        return []
1112
1113    def construct_the_data(self, operations):
1114        """Construct the operation data using helper methods.
1115
1116        Args:
1117            operation: operation to construct
1118
1119        Returns:
1120            list: constructed operation data
1121        """
1122        constructed_data = []
1123        for op in operations:
1124            operation_type = self._construct_operationtype(op)
1125            constructed_operation = {
1126                'operationtype': operation_type,
1127                'esc_period': op.get('esc_period'),
1128                'esc_step_from': op.get('esc_step_from'),
1129                'esc_step_to': op.get('esc_step_to')
1130            }
1131            # Send Message type
1132            if constructed_operation['operationtype'] == '0':
1133                constructed_operation['opmessage'] = self._construct_opmessage(op)
1134                constructed_operation['opmessage_usr'] = self._construct_opmessage_usr(op)
1135                constructed_operation['opmessage_grp'] = self._construct_opmessage_grp(op)
1136                constructed_operation['opconditions'] = self._construct_opconditions(op)
1137
1138            # Send Command type
1139            if constructed_operation['operationtype'] == '1':
1140                constructed_operation['opcommand'] = self._construct_opcommand(op)
1141                constructed_operation['opcommand_hst'] = self._construct_opcommand_hst(op)
1142                constructed_operation['opcommand_grp'] = self._construct_opcommand_grp(op)
1143                constructed_operation['opconditions'] = self._construct_opconditions(op)
1144
1145            # Add to/Remove from host group
1146            if constructed_operation['operationtype'] in ('4', '5'):
1147                constructed_operation['opgroup'] = self._construct_opgroup(op)
1148
1149            # Link/Unlink template
1150            if constructed_operation['operationtype'] in ('6', '7'):
1151                constructed_operation['optemplate'] = self._construct_optemplate(op)
1152
1153            # Set inventory mode
1154            if constructed_operation['operationtype'] == '10':
1155                constructed_operation['opinventory'] = self._construct_opinventory(op)
1156
1157            constructed_data.append(constructed_operation)
1158
1159        return cleanup_data(constructed_data)
1160
1161
1162class RecoveryOperations(Operations):
1163    """
1164    Restructures the user defined recovery operations data to fit the Zabbix API requirements
1165    """
1166    def _construct_operationtype(self, operation):
1167        """Construct operation type.
1168
1169        Args:
1170            operation: operation to construct type
1171
1172        Returns:
1173            str: constructed operation type
1174        """
1175        try:
1176            return to_numeric_value([
1177                "send_message",
1178                "remote_command",
1179                None,
1180                None,
1181                None,
1182                None,
1183                None,
1184                None,
1185                None,
1186                None,
1187                None,
1188                "notify_all_involved"], operation['type']
1189            )
1190        except Exception as e:
1191            self._module.fail_json(msg="Unsupported value '%s' for recovery operation type." % operation['type'])
1192
1193    def construct_the_data(self, operations):
1194        """Construct the recovery operations data using helper methods.
1195
1196        Args:
1197            operation: operation to construct
1198
1199        Returns:
1200            list: constructed recovery operations data
1201        """
1202        constructed_data = []
1203        for op in operations:
1204            operation_type = self._construct_operationtype(op)
1205            constructed_operation = {
1206                'operationtype': operation_type,
1207            }
1208
1209            # Send Message type
1210            if constructed_operation['operationtype'] in ('0', '11'):
1211                constructed_operation['opmessage'] = self._construct_opmessage(op)
1212                constructed_operation['opmessage_usr'] = self._construct_opmessage_usr(op)
1213                constructed_operation['opmessage_grp'] = self._construct_opmessage_grp(op)
1214
1215            # Send Command type
1216            if constructed_operation['operationtype'] == '1':
1217                constructed_operation['opcommand'] = self._construct_opcommand(op)
1218                constructed_operation['opcommand_hst'] = self._construct_opcommand_hst(op)
1219                constructed_operation['opcommand_grp'] = self._construct_opcommand_grp(op)
1220
1221            constructed_data.append(constructed_operation)
1222
1223        return cleanup_data(constructed_data)
1224
1225
1226class AcknowledgeOperations(Operations):
1227    """
1228    Restructures the user defined acknowledge operations data to fit the Zabbix API requirements
1229    """
1230    def _construct_operationtype(self, operation):
1231        """Construct operation type.
1232
1233        Args:
1234            operation: operation to construct type
1235
1236        Returns:
1237            str: constructed operation type
1238        """
1239        try:
1240            return to_numeric_value([
1241                "send_message",
1242                "remote_command",
1243                None,
1244                None,
1245                None,
1246                None,
1247                None,
1248                None,
1249                None,
1250                None,
1251                None,
1252                None,
1253                "notify_all_involved"], operation['type']
1254            )
1255        except Exception as e:
1256            self._module.fail_json(msg="Unsupported value '%s' for acknowledge operation type." % operation['type'])
1257
1258    def construct_the_data(self, operations):
1259        """Construct the acknowledge operations data using helper methods.
1260
1261        Args:
1262            operation: operation to construct
1263
1264        Returns:
1265            list: constructed acknowledge operations data
1266        """
1267        constructed_data = []
1268        for op in operations:
1269            operation_type = self._construct_operationtype(op)
1270            constructed_operation = {
1271                'operationtype': operation_type,
1272            }
1273
1274            # Send Message type
1275            if constructed_operation['operationtype'] in ('0', '11'):
1276                constructed_operation['opmessage'] = self._construct_opmessage(op)
1277                constructed_operation['opmessage_usr'] = self._construct_opmessage_usr(op)
1278                constructed_operation['opmessage_grp'] = self._construct_opmessage_grp(op)
1279
1280            # Send Command type
1281            if constructed_operation['operationtype'] == '1':
1282                constructed_operation['opcommand'] = self._construct_opcommand(op)
1283                constructed_operation['opcommand_hst'] = self._construct_opcommand_hst(op)
1284                constructed_operation['opcommand_grp'] = self._construct_opcommand_grp(op)
1285
1286            constructed_data.append(constructed_operation)
1287
1288        return cleanup_data(constructed_data)
1289
1290
1291class Filter(object):
1292    """
1293    Restructures the user defined filter conditions to fit the Zabbix API requirements
1294    """
1295    def __init__(self, module, zbx, zapi_wrapper):
1296        self._module = module
1297        self._zapi = zbx
1298        self._zapi_wrapper = zapi_wrapper
1299
1300    def _construct_evaltype(self, _eval_type, _formula, _conditions):
1301        """Construct the eval type
1302
1303        Args:
1304            _formula: zabbix condition evaluation formula
1305            _conditions: list of conditions to check
1306
1307        Returns:
1308            dict: constructed acknowledge operations data
1309        """
1310        if len(_conditions) <= 1:
1311            return {
1312                'evaltype': '0',
1313                'formula': None
1314            }
1315        if _eval_type == 'andor':
1316            return {
1317                'evaltype': '0',
1318                'formula': None
1319            }
1320        if _eval_type == 'and':
1321            return {
1322                'evaltype': '1',
1323                'formula': None
1324            }
1325        if _eval_type == 'or':
1326            return {
1327                'evaltype': '2',
1328                'formula': None
1329            }
1330        if _eval_type == 'custom_expression':
1331            if _formula is not None:
1332                return {
1333                    'evaltype': '3',
1334                    'formula': _formula
1335                }
1336            else:
1337                self._module.fail_json(msg="'formula' is required when 'eval_type' is set to 'custom_expression'")
1338        if _formula is not None:
1339            return {
1340                'evaltype': '3',
1341                'formula': _formula
1342            }
1343        return {
1344            'evaltype': '0',
1345            'formula': None
1346        }
1347
1348    def _construct_conditiontype(self, _condition):
1349        """Construct the condition type
1350
1351        Args:
1352            _condition: condition to check
1353
1354        Returns:
1355            str: constructed condition type data
1356        """
1357        try:
1358            return to_numeric_value([
1359                "host_group",
1360                "host",
1361                "trigger",
1362                "trigger_name",
1363                "trigger_severity",
1364                "trigger_value",
1365                "time_period",
1366                "host_ip",
1367                "discovered_service_type",
1368                "discovered_service_port",
1369                "discovery_status",
1370                "uptime_or_downtime_duration",
1371                "received_value",
1372                "host_template",
1373                None,
1374                "application",
1375                "maintenance_status",
1376                None,
1377                "discovery_rule",
1378                "discovery_check",
1379                "proxy",
1380                "discovery_object",
1381                "host_name",
1382                "event_type",
1383                "host_metadata",
1384                "event_tag",
1385                "event_tag_value"], _condition['type']
1386            )
1387        except Exception as e:
1388            self._module.fail_json(msg="Unsupported value '%s' for condition type." % _condition['type'])
1389
1390    def _construct_operator(self, _condition):
1391        """Construct operator
1392
1393        Args:
1394            _condition: condition to construct
1395
1396        Returns:
1397            str: constructed operator
1398        """
1399        try:
1400            return to_numeric_value([
1401                "=",
1402                "<>",
1403                "like",
1404                "not like",
1405                "in",
1406                ">=",
1407                "<=",
1408                "not in",
1409                "matches",
1410                "does not match",
1411                "Yes",
1412                "No"], _condition['operator']
1413            )
1414        except Exception as e:
1415            self._module.fail_json(msg="Unsupported value '%s' for operator." % _condition['operator'])
1416
1417    def _construct_value(self, conditiontype, value):
1418        """Construct operator
1419
1420        Args:
1421            conditiontype: type of condition to construct
1422            value: value to construct
1423
1424        Returns:
1425            str: constructed value
1426        """
1427        try:
1428            # Host group
1429            if conditiontype == '0':
1430                return self._zapi_wrapper.get_hostgroup_by_hostgroup_name(value)['groupid']
1431            # Host
1432            if conditiontype == '1':
1433                return self._zapi_wrapper.get_host_by_host_name(value)['hostid']
1434            # Trigger
1435            if conditiontype == '2':
1436                return self._zapi_wrapper.get_trigger_by_trigger_name(value)['triggerid']
1437            # Trigger name: return as is
1438            # Trigger severity
1439            if conditiontype == '4':
1440                return to_numeric_value([
1441                    "not classified",
1442                    "information",
1443                    "warning",
1444                    "average",
1445                    "high",
1446                    "disaster"], value or "not classified"
1447                )
1448
1449            # Trigger value
1450            if conditiontype == '5':
1451                return to_numeric_value([
1452                    "ok",
1453                    "problem"], value or "ok"
1454                )
1455            # Time period: return as is
1456            # Host IP: return as is
1457            # Discovered service type
1458            if conditiontype == '8':
1459                return to_numeric_value([
1460                    "SSH",
1461                    "LDAP",
1462                    "SMTP",
1463                    "FTP",
1464                    "HTTP",
1465                    "POP",
1466                    "NNTP",
1467                    "IMAP",
1468                    "TCP",
1469                    "Zabbix agent",
1470                    "SNMPv1 agent",
1471                    "SNMPv2 agent",
1472                    "ICMP ping",
1473                    "SNMPv3 agent",
1474                    "HTTPS",
1475                    "Telnet"], value
1476                )
1477            # Discovered service port: return as is
1478            # Discovery status
1479            if conditiontype == '10':
1480                return to_numeric_value([
1481                    "up",
1482                    "down",
1483                    "discovered",
1484                    "lost"], value
1485                )
1486            if conditiontype == '13':
1487                return self._zapi_wrapper.get_template_by_template_name(value)['templateid']
1488            if conditiontype == '18':
1489                return self._zapi_wrapper.get_discovery_rule_by_discovery_rule_name(value)['druleid']
1490            if conditiontype == '19':
1491                return self._zapi_wrapper.get_discovery_check_by_discovery_check_name(value)['dcheckid']
1492            if conditiontype == '20':
1493                return self._zapi_wrapper.get_proxy_by_proxy_name(value)['proxyid']
1494            if conditiontype == '21':
1495                return to_numeric_value([
1496                    "pchldrfor0",
1497                    "host",
1498                    "service"], value
1499                )
1500            if conditiontype == '23':
1501                return to_numeric_value([
1502                    "item in not supported state",
1503                    "item in normal state",
1504                    "LLD rule in not supported state",
1505                    "LLD rule in normal state",
1506                    "trigger in unknown state",
1507                    "trigger in normal state"], value
1508                )
1509            return value
1510        except Exception as e:
1511            self._module.fail_json(
1512                msg="""Unsupported value '%s' for specified condition type.
1513                       Check out Zabbix API documentation for supported values for
1514                       condition type '%s' at
1515                       https://www.zabbix.com/documentation/3.4/manual/api/reference/action/object#action_filter_condition""" % (value, conditiontype)
1516            )
1517
1518    def construct_the_data(self, _eval_type, _formula, _conditions):
1519        """Construct the user defined filter conditions to fit the Zabbix API
1520        requirements operations data using helper methods.
1521
1522        Args:
1523            _formula:  zabbix condition evaluation formula
1524            _conditions: conditions to construct
1525
1526        Returns:
1527            dict: user defined filter conditions
1528        """
1529        if _conditions is None:
1530            return None
1531        constructed_data = {}
1532        constructed_data['conditions'] = []
1533        for cond in _conditions:
1534            condition_type = self._construct_conditiontype(cond)
1535            constructed_data['conditions'].append({
1536                "conditiontype": condition_type,
1537                "value": self._construct_value(condition_type, cond.get("value")),
1538                "value2": cond.get("value2"),
1539                "formulaid": cond.get("formulaid"),
1540                "operator": self._construct_operator(cond)
1541            })
1542        _constructed_evaltype = self._construct_evaltype(
1543            _eval_type,
1544            _formula,
1545            constructed_data['conditions']
1546        )
1547        constructed_data['evaltype'] = _constructed_evaltype['evaltype']
1548        constructed_data['formula'] = _constructed_evaltype['formula']
1549        return cleanup_data(constructed_data)
1550
1551
1552def convert_unicode_to_str(data):
1553    """Converts unicode objects to strings in dictionary
1554    args:
1555        data: unicode object
1556
1557    Returns:
1558        dict: strings in dictionary
1559    """
1560    if isinstance(data, dict):
1561        return dict(map(convert_unicode_to_str, data.items()))
1562    elif isinstance(data, (list, tuple, set)):
1563        return type(data)(map(convert_unicode_to_str, data))
1564    elif data is None:
1565        return data
1566    else:
1567        return str(data)
1568
1569
1570def to_numeric_value(strs, value):
1571    """Converts string values to integers
1572    Args:
1573        value: string value
1574
1575    Returns:
1576        int: converted integer
1577    """
1578    strs = [s.lower() if isinstance(s, str) else s for s in strs]
1579    value = value.lower()
1580    tmp_dict = dict(zip(strs, list(range(len(strs)))))
1581    return str(tmp_dict[value])
1582
1583
1584def compare_lists(l1, l2, diff_dict):
1585    """
1586    Compares l1 and l2 lists and adds the items that are different
1587    to the diff_dict dictionary.
1588    Used in recursion with compare_dictionaries() function.
1589    Args:
1590        l1: first list to compare
1591        l2: second list to compare
1592        diff_dict: dictionary to store the difference
1593
1594    Returns:
1595        dict: items that are different
1596    """
1597    if len(l1) != len(l2):
1598        diff_dict.append(l1)
1599        return diff_dict
1600    for i, item in enumerate(l1):
1601        if isinstance(item, dict):
1602            diff_dict.insert(i, {})
1603            diff_dict[i] = compare_dictionaries(item, l2[i], diff_dict[i])
1604        else:
1605            if item != l2[i]:
1606                diff_dict.append(item)
1607    while {} in diff_dict:
1608        diff_dict.remove({})
1609    return diff_dict
1610
1611
1612def compare_dictionaries(d1, d2, diff_dict):
1613    """
1614    Compares d1 and d2 dictionaries and adds the items that are different
1615    to the diff_dict dictionary.
1616    Used in recursion with compare_lists() function.
1617    Args:
1618        d1: first dictionary to compare
1619        d2: second dictionary to compare
1620        diff_dict: dictionary to store the difference
1621
1622    Returns:
1623        dict: items that are different
1624    """
1625    for k, v in d1.items():
1626        if k not in d2:
1627            diff_dict[k] = v
1628            continue
1629        if isinstance(v, dict):
1630            diff_dict[k] = {}
1631            compare_dictionaries(v, d2[k], diff_dict[k])
1632            if diff_dict[k] == {}:
1633                del diff_dict[k]
1634            else:
1635                diff_dict[k] = v
1636        elif isinstance(v, list):
1637            diff_dict[k] = []
1638            compare_lists(v, d2[k], diff_dict[k])
1639            if diff_dict[k] == []:
1640                del diff_dict[k]
1641            else:
1642                diff_dict[k] = v
1643        else:
1644            if v != d2[k]:
1645                diff_dict[k] = v
1646    return diff_dict
1647
1648
1649def cleanup_data(obj):
1650    """Removes the None values from the object and returns the object
1651    Args:
1652        obj: object to cleanup
1653
1654    Returns:
1655       object: cleaned object
1656    """
1657    if isinstance(obj, (list, tuple, set)):
1658        return type(obj)(cleanup_data(x) for x in obj if x is not None)
1659    elif isinstance(obj, dict):
1660        return type(obj)((cleanup_data(k), cleanup_data(v))
1661                         for k, v in obj.items() if k is not None and v is not None)
1662    else:
1663        return obj
1664
1665
1666def main():
1667    """Main ansible module function
1668    """
1669
1670    module = AnsibleModule(
1671        argument_spec=dict(
1672            server_url=dict(type='str', required=True, aliases=['url']),
1673            login_user=dict(type='str', required=True),
1674            login_password=dict(type='str', required=True, no_log=True),
1675            http_login_user=dict(type='str', required=False, default=None),
1676            http_login_password=dict(type='str', required=False, default=None, no_log=True),
1677            validate_certs=dict(type='bool', required=False, default=True),
1678            esc_period=dict(type='int', required=False),
1679            timeout=dict(type='int', default=10),
1680            name=dict(type='str', required=True),
1681            event_source=dict(type='str', required=False, choices=['trigger', 'discovery', 'auto_registration', 'internal']),
1682            state=dict(type='str', required=False, default='present', choices=['present', 'absent']),
1683            status=dict(type='str', required=False, default='enabled', choices=['enabled', 'disabled']),
1684            pause_in_maintenance=dict(type='bool', required=False, default=True),
1685            default_message=dict(type='str', required=False, default=''),
1686            default_subject=dict(type='str', required=False, default=''),
1687            recovery_default_message=dict(type='str', required=False, default=''),
1688            recovery_default_subject=dict(type='str', required=False, default=''),
1689            acknowledge_default_message=dict(type='str', required=False, default=''),
1690            acknowledge_default_subject=dict(type='str', required=False, default=''),
1691            conditions=dict(
1692                type='list',
1693                required=False,
1694                default=[],
1695                elements='dict',
1696                options=dict(
1697                    formulaid=dict(type='str', required=False),
1698                    operator=dict(type='str', required=True),
1699                    type=dict(type='str', required=True),
1700                    value=dict(type='str', required=True),
1701                    value2=dict(type='str', required=False)
1702                )
1703            ),
1704            formula=dict(type='str', required=False, default=None),
1705            eval_type=dict(type='str', required=False, default=None, choices=['andor', 'and', 'or', 'custom_expression']),
1706            operations=dict(
1707                type='list',
1708                required=False,
1709                default=[],
1710                elements='dict',
1711                options=dict(
1712                    type=dict(
1713                        type='str',
1714                        required=True,
1715                        choices=[
1716                            'send_message',
1717                            'remote_command',
1718                            'add_host',
1719                            'remove_host',
1720                            'add_to_host_group',
1721                            'remove_from_host_group',
1722                            'link_to_template',
1723                            'unlink_from_template',
1724                            'enable_host',
1725                            'disable_host',
1726                            'set_host_inventory_mode',
1727                        ]
1728                    ),
1729                    esc_period=dict(type='int', required=False),
1730                    esc_step_from=dict(type='int', required=False, default=1),
1731                    esc_step_to=dict(type='int', required=False, default=1),
1732                    operation_condition=dict(
1733                        type='str',
1734                        required=False,
1735                        default=None,
1736                        choices=['acknowledged', 'not_acknowledged']
1737                    ),
1738                    # when type is remote_command
1739                    command_type=dict(
1740                        type='str',
1741                        required=False,
1742                        choices=[
1743                            'custom_script',
1744                            'ipmi',
1745                            'ssh',
1746                            'telnet',
1747                            'global_script'
1748                        ]
1749                    ),
1750                    command=dict(type='str', required=False),
1751                    execute_on=dict(
1752                        type='str',
1753                        required=False,
1754                        choices=['agent', 'server', 'proxy']
1755                    ),
1756                    password=dict(type='str', required=False, no_log=True),
1757                    port=dict(type='int', required=False),
1758                    run_on_groups=dict(type='list', required=False),
1759                    run_on_hosts=dict(type='list', required=False),
1760                    script_name=dict(type='str', required=False),
1761                    ssh_auth_type=dict(
1762                        type='str',
1763                        required=False,
1764                        default='password',
1765                        choices=['password', 'public_key']
1766                    ),
1767                    ssh_privatekey_file=dict(type='str', required=False),
1768                    ssh_publickey_file=dict(type='str', required=False),
1769                    username=dict(type='str', required=False),
1770                    # when type is send_message
1771                    media_type=dict(type='str', required=False),
1772                    subject=dict(type='str', required=False),
1773                    message=dict(type='str', required=False),
1774                    send_to_groups=dict(type='list', required=False),
1775                    send_to_users=dict(type='list', required=False),
1776                    # when type is add_to_host_group or remove_from_host_group
1777                    host_groups=dict(type='list', required=False),
1778                    # when type is set_host_inventory_mode
1779                    inventory=dict(type='str', required=False),
1780                    # when type is link_to_template or unlink_from_template
1781                    templates=dict(type='list', required=False)
1782                ),
1783                required_if=[
1784                    ['type', 'remote_command', ['command_type']],
1785                    ['type', 'remote_command', ['run_on_groups', 'run_on_hosts'], True],
1786                    ['command_type', 'custom_script', [
1787                        'command',
1788                        'execute_on'
1789                    ]],
1790                    ['command_type', 'ipmi', ['command']],
1791                    ['command_type', 'ssh', [
1792                        'command',
1793                        'password',
1794                        'username',
1795                        'port',
1796                        'ssh_auth_type',
1797                        'ssh_privatekey_file',
1798                        'ssh_publickey_file'
1799                    ]],
1800                    ['command_type', 'telnet', [
1801                        'command',
1802                        'password',
1803                        'username',
1804                        'port'
1805                    ]],
1806                    ['command_type', 'global_script', ['script_name']],
1807                    ['type', 'add_to_host_group', ['host_groups']],
1808                    ['type', 'remove_from_host_group', ['host_groups']],
1809                    ['type', 'link_to_template', ['templates']],
1810                    ['type', 'unlink_from_template', ['templates']],
1811                    ['type', 'set_host_inventory_mode', ['inventory']],
1812                    ['type', 'send_message', ['send_to_users', 'send_to_groups'], True]
1813                ]
1814            ),
1815            recovery_operations=dict(
1816                type='list',
1817                required=False,
1818                default=[],
1819                elements='dict',
1820                options=dict(
1821                    type=dict(
1822                        type='str',
1823                        required=True,
1824                        choices=[
1825                            'send_message',
1826                            'remote_command',
1827                            'notify_all_involved'
1828                        ]
1829                    ),
1830                    # when type is remote_command
1831                    command_type=dict(
1832                        type='str',
1833                        required=False,
1834                        choices=[
1835                            'custom_script',
1836                            'ipmi',
1837                            'ssh',
1838                            'telnet',
1839                            'global_script'
1840                        ]
1841                    ),
1842                    command=dict(type='str', required=False),
1843                    execute_on=dict(
1844                        type='str',
1845                        required=False,
1846                        choices=['agent', 'server', 'proxy']
1847                    ),
1848                    password=dict(type='str', required=False, no_log=True),
1849                    port=dict(type='int', required=False),
1850                    run_on_groups=dict(type='list', required=False),
1851                    run_on_hosts=dict(type='list', required=False),
1852                    script_name=dict(type='str', required=False),
1853                    ssh_auth_type=dict(
1854                        type='str',
1855                        required=False,
1856                        default='password',
1857                        choices=['password', 'public_key']
1858                    ),
1859                    ssh_privatekey_file=dict(type='str', required=False),
1860                    ssh_publickey_file=dict(type='str', required=False),
1861                    username=dict(type='str', required=False),
1862                    # when type is send_message
1863                    media_type=dict(type='str', required=False),
1864                    subject=dict(type='str', required=False),
1865                    message=dict(type='str', required=False),
1866                    send_to_groups=dict(type='list', required=False),
1867                    send_to_users=dict(type='list', required=False),
1868                ),
1869                required_if=[
1870                    ['type', 'remote_command', ['command_type']],
1871                    ['type', 'remote_command', [
1872                        'run_on_groups',
1873                        'run_on_hosts'
1874                    ], True],
1875                    ['command_type', 'custom_script', [
1876                        'command',
1877                        'execute_on'
1878                    ]],
1879                    ['command_type', 'ipmi', ['command']],
1880                    ['command_type', 'ssh', [
1881                        'command',
1882                        'password',
1883                        'username',
1884                        'port',
1885                        'ssh_auth_type',
1886                        'ssh_privatekey_file',
1887                        'ssh_publickey_file'
1888                    ]],
1889                    ['command_type', 'telnet', [
1890                        'command',
1891                        'password',
1892                        'username',
1893                        'port'
1894                    ]],
1895                    ['command_type', 'global_script', ['script_name']],
1896                    ['type', 'send_message', ['send_to_users', 'send_to_groups'], True]
1897                ]
1898            ),
1899            acknowledge_operations=dict(
1900                type='list',
1901                required=False,
1902                default=[],
1903                elements='dict',
1904                options=dict(
1905                    type=dict(
1906                        type='str',
1907                        required=True,
1908                        choices=[
1909                            'send_message',
1910                            'remote_command',
1911                            'notify_all_involved'
1912                        ]
1913                    ),
1914                    # when type is remote_command
1915                    command_type=dict(
1916                        type='str',
1917                        required=False,
1918                        choices=[
1919                            'custom_script',
1920                            'ipmi',
1921                            'ssh',
1922                            'telnet',
1923                            'global_script'
1924                        ]
1925                    ),
1926                    command=dict(type='str', required=False),
1927                    execute_on=dict(
1928                        type='str',
1929                        required=False,
1930                        choices=['agent', 'server', 'proxy']
1931                    ),
1932                    password=dict(type='str', required=False, no_log=True),
1933                    port=dict(type='int', required=False),
1934                    run_on_groups=dict(type='list', required=False),
1935                    run_on_hosts=dict(type='list', required=False),
1936                    script_name=dict(type='str', required=False),
1937                    ssh_auth_type=dict(
1938                        type='str',
1939                        required=False,
1940                        default='password',
1941                        choices=['password', 'public_key']
1942                    ),
1943                    ssh_privatekey_file=dict(type='str', required=False),
1944                    ssh_publickey_file=dict(type='str', required=False),
1945                    username=dict(type='str', required=False),
1946                    # when type is send_message
1947                    media_type=dict(type='str', required=False),
1948                    subject=dict(type='str', required=False),
1949                    message=dict(type='str', required=False),
1950                    send_to_groups=dict(type='list', required=False),
1951                    send_to_users=dict(type='list', required=False),
1952                ),
1953                required_if=[
1954                    ['type', 'remote_command', ['command_type']],
1955                    ['type', 'remote_command', [
1956                        'run_on_groups',
1957                        'run_on_hosts'
1958                    ], True],
1959                    ['command_type', 'custom_script', [
1960                        'command',
1961                        'execute_on'
1962                    ]],
1963                    ['command_type', 'ipmi', ['command']],
1964                    ['command_type', 'ssh', [
1965                        'command',
1966                        'password',
1967                        'username',
1968                        'port',
1969                        'ssh_auth_type',
1970                        'ssh_privatekey_file',
1971                        'ssh_publickey_file'
1972                    ]],
1973                    ['command_type', 'telnet', [
1974                        'command',
1975                        'password',
1976                        'username',
1977                        'port'
1978                    ]],
1979                    ['command_type', 'global_script', ['script_name']],
1980                    ['type', 'send_message', ['send_to_users', 'send_to_groups'], True]
1981                ]
1982            )
1983        ),
1984        required_if=[
1985            ['state', 'present', [
1986                'esc_period',
1987                'event_source'
1988            ]]
1989        ],
1990        supports_check_mode=True
1991    )
1992
1993    if not HAS_ZABBIX_API:
1994        module.fail_json(msg=missing_required_lib('zabbix-api', url='https://pypi.org/project/zabbix-api/'), exception=ZBX_IMP_ERR)
1995
1996    server_url = module.params['server_url']
1997    login_user = module.params['login_user']
1998    login_password = module.params['login_password']
1999    http_login_user = module.params['http_login_user']
2000    http_login_password = module.params['http_login_password']
2001    validate_certs = module.params['validate_certs']
2002    timeout = module.params['timeout']
2003    name = module.params['name']
2004    esc_period = module.params['esc_period']
2005    event_source = module.params['event_source']
2006    state = module.params['state']
2007    status = module.params['status']
2008    pause_in_maintenance = module.params['pause_in_maintenance']
2009    default_message = module.params['default_message']
2010    default_subject = module.params['default_subject']
2011    recovery_default_message = module.params['recovery_default_message']
2012    recovery_default_subject = module.params['recovery_default_subject']
2013    acknowledge_default_message = module.params['acknowledge_default_message']
2014    acknowledge_default_subject = module.params['acknowledge_default_subject']
2015    conditions = module.params['conditions']
2016    formula = module.params['formula']
2017    eval_type = module.params['eval_type']
2018    operations = module.params['operations']
2019    recovery_operations = module.params['recovery_operations']
2020    acknowledge_operations = module.params['acknowledge_operations']
2021
2022    try:
2023        zbx = ZabbixAPI(server_url, timeout=timeout, user=http_login_user,
2024                        passwd=http_login_password, validate_certs=validate_certs)
2025        zbx.login(login_user, login_password)
2026        atexit.register(zbx.logout)
2027    except Exception as e:
2028        module.fail_json(msg="Failed to connect to Zabbix server: %s" % e)
2029
2030    zapi_wrapper = Zapi(module, zbx)
2031
2032    action = Action(module, zbx, zapi_wrapper)
2033
2034    action_exists = zapi_wrapper.check_if_action_exists(name)
2035    ops = Operations(module, zbx, zapi_wrapper)
2036    recovery_ops = RecoveryOperations(module, zbx, zapi_wrapper)
2037    acknowledge_ops = AcknowledgeOperations(module, zbx, zapi_wrapper)
2038    fltr = Filter(module, zbx, zapi_wrapper)
2039
2040    if action_exists:
2041        action_id = zapi_wrapper.get_action_by_name(name)['actionid']
2042        if state == "absent":
2043            result = action.delete_action(action_id)
2044            module.exit_json(changed=True, msg="Action Deleted: %s, ID: %s" % (name, result))
2045        else:
2046            difference = action.check_difference(
2047                action_id=action_id,
2048                name=name,
2049                event_source=event_source,
2050                esc_period=esc_period,
2051                status=status,
2052                pause_in_maintenance=pause_in_maintenance,
2053                default_message=default_message,
2054                default_subject=default_subject,
2055                recovery_default_message=recovery_default_message,
2056                recovery_default_subject=recovery_default_subject,
2057                acknowledge_default_message=acknowledge_default_message,
2058                acknowledge_default_subject=acknowledge_default_subject,
2059                operations=ops.construct_the_data(operations),
2060                recovery_operations=recovery_ops.construct_the_data(recovery_operations),
2061                acknowledge_operations=acknowledge_ops.construct_the_data(acknowledge_operations),
2062                conditions=fltr.construct_the_data(eval_type, formula, conditions)
2063            )
2064
2065            if difference == {}:
2066                module.exit_json(changed=False, msg="Action is up to date: %s" % (name))
2067            else:
2068                result = action.update_action(
2069                    action_id=action_id,
2070                    **difference
2071                )
2072                module.exit_json(changed=True, msg="Action Updated: %s, ID: %s" % (name, result))
2073    else:
2074        if state == "absent":
2075            module.exit_json(changed=False)
2076        else:
2077            action_id = action.add_action(
2078                name=name,
2079                event_source=event_source,
2080                esc_period=esc_period,
2081                status=status,
2082                pause_in_maintenance=pause_in_maintenance,
2083                default_message=default_message,
2084                default_subject=default_subject,
2085                recovery_default_message=recovery_default_message,
2086                recovery_default_subject=recovery_default_subject,
2087                acknowledge_default_message=acknowledge_default_message,
2088                acknowledge_default_subject=acknowledge_default_subject,
2089                operations=ops.construct_the_data(operations),
2090                recovery_operations=recovery_ops.construct_the_data(recovery_operations),
2091                acknowledge_operations=acknowledge_ops.construct_the_data(acknowledge_operations),
2092                conditions=fltr.construct_the_data(eval_type, formula, conditions)
2093            )
2094            module.exit_json(changed=True, msg="Action created: %s, ID: %s" % (name, action_id))
2095
2096
2097if __name__ == '__main__':
2098    main()
2099