1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3# Copyright (c) 2017 Red Hat Inc.
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import (absolute_import, division, print_function)
7__metaclass__ = type
8
9
10DOCUMENTATION = '''
11
12module: manageiq_alert_profiles
13
14short_description: Configuration of alert profiles for ManageIQ
15extends_documentation_fragment:
16- community.general.manageiq
17
18author: Elad Alfassa (@elad661) <ealfassa@redhat.com>
19description:
20  - The manageiq_alert_profiles module supports adding, updating and deleting alert profiles in ManageIQ.
21
22options:
23  state:
24    type: str
25    description:
26      - absent - alert profile should not exist,
27      - present - alert profile should exist,
28    choices: ['absent', 'present']
29    default: 'present'
30  name:
31    type: str
32    description:
33      - The unique alert profile name in ManageIQ.
34      - Required when state is "absent" or "present".
35  resource_type:
36    type: str
37    description:
38      - The resource type for the alert profile in ManageIQ. Required when state is "present".
39    choices: ['Vm', 'ContainerNode', 'MiqServer', 'Host', 'Storage', 'EmsCluster',
40              'ExtManagementSystem', 'MiddlewareServer']
41  alerts:
42    type: list
43    elements: str
44    description:
45      - List of alert descriptions to assign to this profile.
46      - Required if state is "present"
47  notes:
48    type: str
49    description:
50      - Optional notes for this profile
51
52'''
53
54EXAMPLES = '''
55- name: Add an alert profile to ManageIQ
56  community.general.manageiq_alert_profiles:
57    state: present
58    name: Test profile
59    resource_type: ContainerNode
60    alerts:
61      - Test Alert 01
62      - Test Alert 02
63    manageiq_connection:
64      url: 'http://127.0.0.1:3000'
65      username: 'admin'
66      password: 'smartvm'
67      validate_certs: False
68
69- name: Delete an alert profile from ManageIQ
70  community.general.manageiq_alert_profiles:
71    state: absent
72    name: Test profile
73    manageiq_connection:
74      url: 'http://127.0.0.1:3000'
75      username: 'admin'
76      password: 'smartvm'
77      validate_certs: False
78'''
79
80RETURN = '''
81'''
82
83from ansible.module_utils.basic import AnsibleModule
84from ansible_collections.community.general.plugins.module_utils.manageiq import ManageIQ, manageiq_argument_spec
85
86
87class ManageIQAlertProfiles(object):
88    """ Object to execute alert profile management operations in manageiq.
89    """
90
91    def __init__(self, manageiq):
92        self.manageiq = manageiq
93
94        self.module = self.manageiq.module
95        self.api_url = self.manageiq.api_url
96        self.client = self.manageiq.client
97        self.url = '{api_url}/alert_definition_profiles'.format(api_url=self.api_url)
98
99    def get_profiles(self):
100        """ Get all alert profiles from ManageIQ
101        """
102        try:
103            response = self.client.get(self.url + '?expand=alert_definitions,resources')
104        except Exception as e:
105            self.module.fail_json(msg="Failed to query alert profiles: {error}".format(error=e))
106        return response.get('resources') or []
107
108    def get_alerts(self, alert_descriptions):
109        """ Get a list of alert hrefs from a list of alert descriptions
110        """
111        alerts = []
112        for alert_description in alert_descriptions:
113            alert = self.manageiq.find_collection_resource_or_fail("alert_definitions",
114                                                                   description=alert_description)
115            alerts.append(alert['href'])
116
117        return alerts
118
119    def add_profile(self, profile):
120        """ Add a new alert profile to ManageIQ
121        """
122        # find all alerts to add to the profile
123        # we do this first to fail early if one is missing.
124        alerts = self.get_alerts(profile['alerts'])
125
126        # build the profile dict to send to the server
127
128        profile_dict = dict(name=profile['name'],
129                            description=profile['name'],
130                            mode=profile['resource_type'])
131        if profile['notes']:
132            profile_dict['set_data'] = dict(notes=profile['notes'])
133
134        # send it to the server
135        try:
136            result = self.client.post(self.url, resource=profile_dict, action="create")
137        except Exception as e:
138            self.module.fail_json(msg="Creating profile failed {error}".format(error=e))
139
140        # now that it has been created, we can assign the alerts
141        self.assign_or_unassign(result['results'][0], alerts, "assign")
142
143        msg = "Profile {name} created successfully"
144        msg = msg.format(name=profile['name'])
145        return dict(changed=True, msg=msg)
146
147    def delete_profile(self, profile):
148        """ Delete an alert profile from ManageIQ
149        """
150        try:
151            self.client.post(profile['href'], action="delete")
152        except Exception as e:
153            self.module.fail_json(msg="Deleting profile failed: {error}".format(error=e))
154
155        msg = "Successfully deleted profile {name}".format(name=profile['name'])
156        return dict(changed=True, msg=msg)
157
158    def get_alert_href(self, alert):
159        """ Get an absolute href for an alert
160        """
161        return "{url}/alert_definitions/{id}".format(url=self.api_url, id=alert['id'])
162
163    def assign_or_unassign(self, profile, resources, action):
164        """ Assign or unassign alerts to profile, and validate the result.
165        """
166        alerts = [dict(href=href) for href in resources]
167
168        subcollection_url = profile['href'] + '/alert_definitions'
169        try:
170            result = self.client.post(subcollection_url, resources=alerts, action=action)
171            if len(result['results']) != len(alerts):
172                msg = "Failed to {action} alerts to profile '{name}'," +\
173                      "expected {expected} alerts to be {action}ed," +\
174                      "but only {changed} were {action}ed"
175                msg = msg.format(action=action,
176                                 name=profile['name'],
177                                 expected=len(alerts),
178                                 changed=result['results'])
179                self.module.fail_json(msg=msg)
180        except Exception as e:
181            msg = "Failed to {action} alerts to profile '{name}': {error}"
182            msg = msg.format(action=action, name=profile['name'], error=e)
183            self.module.fail_json(msg=msg)
184
185        return result['results']
186
187    def update_profile(self, old_profile, desired_profile):
188        """ Update alert profile in ManageIQ
189        """
190        changed = False
191        # we need to use client.get to query the alert definitions
192        old_profile = self.client.get(old_profile['href'] + '?expand=alert_definitions')
193
194        # figure out which alerts we need to assign / unassign
195        # alerts listed by the user:
196        desired_alerts = set(self.get_alerts(desired_profile['alerts']))
197
198        # alert which currently exist in the profile
199        if 'alert_definitions' in old_profile:
200            # we use get_alert_href to have a direct href to the alert
201            existing_alerts = set([self.get_alert_href(alert) for alert in old_profile['alert_definitions']])
202        else:
203            # no alerts in this profile
204            existing_alerts = set()
205
206        to_add = list(desired_alerts - existing_alerts)
207        to_remove = list(existing_alerts - desired_alerts)
208
209        # assign / unassign the alerts, if needed
210
211        if to_remove:
212            self.assign_or_unassign(old_profile, to_remove, "unassign")
213            changed = True
214        if to_add:
215            self.assign_or_unassign(old_profile, to_add, "assign")
216            changed = True
217
218        # update other properties
219        profile_dict = dict()
220
221        if old_profile['mode'] != desired_profile['resource_type']:
222            # mode needs to be updated
223            profile_dict['mode'] = desired_profile['resource_type']
224
225        # check if notes need to be updated
226        old_notes = old_profile.get('set_data', {}).get('notes')
227
228        if desired_profile['notes'] != old_notes:
229            profile_dict['set_data'] = dict(notes=desired_profile['notes'])
230
231        if profile_dict:
232            # if we have any updated values
233            changed = True
234            try:
235                result = self.client.post(old_profile['href'],
236                                          resource=profile_dict,
237                                          action="edit")
238            except Exception as e:
239                msg = "Updating profile '{name}' failed: {error}"
240                msg = msg.format(name=old_profile['name'], error=e)
241                self.module.fail_json(msg=msg, result=result)
242
243        if changed:
244            msg = "Profile {name} updated successfully".format(name=desired_profile['name'])
245        else:
246            msg = "No update needed for profile {name}".format(name=desired_profile['name'])
247        return dict(changed=changed, msg=msg)
248
249
250def main():
251    argument_spec = dict(
252        name=dict(type='str'),
253        resource_type=dict(type='str', choices=['Vm',
254                                                'ContainerNode',
255                                                'MiqServer',
256                                                'Host',
257                                                'Storage',
258                                                'EmsCluster',
259                                                'ExtManagementSystem',
260                                                'MiddlewareServer']),
261        alerts=dict(type='list', elements='str'),
262        notes=dict(type='str'),
263        state=dict(default='present', choices=['present', 'absent']),
264    )
265    # add the manageiq connection arguments to the arguments
266    argument_spec.update(manageiq_argument_spec())
267
268    module = AnsibleModule(argument_spec=argument_spec,
269                           required_if=[('state', 'present', ['name', 'resource_type']),
270                                        ('state', 'absent', ['name'])])
271
272    state = module.params['state']
273    name = module.params['name']
274
275    manageiq = ManageIQ(module)
276    manageiq_alert_profiles = ManageIQAlertProfiles(manageiq)
277
278    existing_profile = manageiq.find_collection_resource_by("alert_definition_profiles",
279                                                            name=name)
280
281    # we need to add or update the alert profile
282    if state == "present":
283        if not existing_profile:
284            # a profile with this name doesn't exist yet, let's create it
285            res_args = manageiq_alert_profiles.add_profile(module.params)
286        else:
287            # a profile with this name exists, we might need to update it
288            res_args = manageiq_alert_profiles.update_profile(existing_profile, module.params)
289
290    # this alert profile should not exist
291    if state == "absent":
292        # if we have an alert profile with this name, delete it
293        if existing_profile:
294            res_args = manageiq_alert_profiles.delete_profile(existing_profile)
295        else:
296            # This alert profile does not exist in ManageIQ, and that's okay
297            msg = "Alert profile '{name}' does not exist in ManageIQ"
298            msg = msg.format(name=name)
299            res_args = dict(changed=False, msg=msg)
300
301    module.exit_json(**res_args)
302
303
304if __name__ == "__main__":
305    main()
306