zbx_action.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. #!/usr/bin/env python
  2. # vim: expandtab:tabstop=4:shiftwidth=4
  3. '''
  4. Ansible module for zabbix actions
  5. '''
  6. #
  7. # Zabbix action ansible module
  8. #
  9. #
  10. # Copyright 2015 Red Hat Inc.
  11. #
  12. # Licensed under the Apache License, Version 2.0 (the "License");
  13. # you may not use this file except in compliance with the License.
  14. # You may obtain a copy of the License at
  15. #
  16. # http://www.apache.org/licenses/LICENSE-2.0
  17. #
  18. # Unless required by applicable law or agreed to in writing, software
  19. # distributed under the License is distributed on an "AS IS" BASIS,
  20. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  21. # See the License for the specific language governing permissions and
  22. # limitations under the License.
  23. #
  24. # This is in place because each module looks similar to each other.
  25. # These need duplicate code as their behavior is very similar
  26. # but different for each zabbix class.
  27. # pylint: disable=duplicate-code
  28. # pylint: disable=import-error
  29. from openshift_tools.monitoring.zbxapi import ZabbixAPI, ZabbixConnection, ZabbixAPIError
  30. def exists(content, key='result'):
  31. ''' Check if key exists in content or the size of content[key] > 0
  32. '''
  33. if not content.has_key(key):
  34. return False
  35. if not content[key]:
  36. return False
  37. return True
  38. def conditions_equal(zab_conditions, user_conditions):
  39. '''Compare two lists of conditions'''
  40. c_type = 'conditiontype'
  41. _op = 'operator'
  42. val = 'value'
  43. if len(user_conditions) != len(zab_conditions):
  44. return False
  45. for zab_cond, user_cond in zip(zab_conditions, user_conditions):
  46. if zab_cond[c_type] != str(user_cond[c_type]) or zab_cond[_op] != str(user_cond[_op]) or \
  47. zab_cond[val] != str(user_cond[val]):
  48. return False
  49. return True
  50. def filter_differences(zabbix_filters, user_filters):
  51. '''Determine the differences from user and zabbix for operations'''
  52. rval = {}
  53. for key, val in user_filters.items():
  54. if key == 'conditions':
  55. if not conditions_equal(zabbix_filters[key], val):
  56. rval[key] = val
  57. elif zabbix_filters[key] != str(val):
  58. rval[key] = val
  59. return rval
  60. # This logic is quite complex. We are comparing two lists of dictionaries.
  61. # The outer for-loops allow us to descend down into both lists at the same time
  62. # and then walk over the key,val pairs of the incoming user dict's changes
  63. # or updates. The if-statements are looking at different sub-object types and
  64. # comparing them. The other suggestion on how to write this is to write a recursive
  65. # compare function but for the time constraints and for complexity I decided to go
  66. # this route.
  67. # pylint: disable=too-many-branches
  68. def operation_differences(zabbix_ops, user_ops):
  69. '''Determine the differences from user and zabbix for operations'''
  70. # if they don't match, take the user options
  71. if len(zabbix_ops) != len(user_ops):
  72. return user_ops
  73. rval = {}
  74. for zab, user in zip(zabbix_ops, user_ops):
  75. for key, val in user.items():
  76. if key == 'opconditions':
  77. if len(zab[key]) != len(val):
  78. rval[key] = val
  79. break
  80. for z_cond, u_cond in zip(zab[key], user[key]):
  81. if not all([str(u_cond[op_key]) == z_cond[op_key] for op_key in \
  82. ['conditiontype', 'operator', 'value']]):
  83. rval[key] = val
  84. break
  85. elif key == 'opmessage':
  86. # Verify each passed param matches
  87. for op_msg_key, op_msg_val in val.items():
  88. if zab[key][op_msg_key] != str(op_msg_val):
  89. rval[key] = val
  90. break
  91. elif key == 'opmessage_grp':
  92. zab_grp_ids = set([ugrp['usrgrpid'] for ugrp in zab[key]])
  93. usr_grp_ids = set([ugrp['usrgrpid'] for ugrp in val])
  94. if usr_grp_ids != zab_grp_ids:
  95. rval[key] = val
  96. elif key == 'opmessage_usr':
  97. zab_usr_ids = set([usr['userid'] for usr in zab[key]])
  98. usr_ids = set([usr['userid'] for usr in val])
  99. if usr_ids != zab_usr_ids:
  100. rval[key] = val
  101. elif zab[key] != str(val):
  102. rval[key] = val
  103. return rval
  104. def get_users(zapi, users):
  105. '''get the mediatype id from the mediatype name'''
  106. rval_users = []
  107. for user in users:
  108. content = zapi.get_content('user',
  109. 'get',
  110. {'filter': {'alias': user}})
  111. rval_users.append({'userid': content['result'][0]['userid']})
  112. return rval_users
  113. def get_user_groups(zapi, groups):
  114. '''get the mediatype id from the mediatype name'''
  115. user_groups = []
  116. content = zapi.get_content('usergroup',
  117. 'get',
  118. {'search': {'name': groups}})
  119. for usr_grp in content['result']:
  120. user_groups.append({'usrgrpid': usr_grp['usrgrpid']})
  121. return user_groups
  122. def get_mediatype_id_by_name(zapi, m_name):
  123. '''get the mediatype id from the mediatype name'''
  124. content = zapi.get_content('mediatype',
  125. 'get',
  126. {'filter': {'description': m_name}})
  127. return content['result'][0]['mediatypeid']
  128. def get_priority(priority):
  129. ''' determine priority
  130. '''
  131. prior = 0
  132. if 'info' in priority:
  133. prior = 1
  134. elif 'warn' in priority:
  135. prior = 2
  136. elif 'avg' == priority or 'ave' in priority:
  137. prior = 3
  138. elif 'high' in priority:
  139. prior = 4
  140. elif 'dis' in priority:
  141. prior = 5
  142. return prior
  143. def get_event_source(from_src):
  144. '''Translate even str into value'''
  145. choices = ['trigger', 'discovery', 'auto', 'internal']
  146. rval = 0
  147. try:
  148. rval = choices.index(from_src)
  149. except ValueError as _:
  150. ZabbixAPIError('Value not found for event source [%s]' % from_src)
  151. return rval
  152. def get_status(inc_status):
  153. '''determine status for action'''
  154. rval = 1
  155. if inc_status == 'enabled':
  156. rval = 0
  157. return rval
  158. def get_condition_operator(inc_operator):
  159. ''' determine the condition operator'''
  160. vals = {'=': 0,
  161. '<>': 1,
  162. 'like': 2,
  163. 'not like': 3,
  164. 'in': 4,
  165. '>=': 5,
  166. '<=': 6,
  167. 'not in': 7,
  168. }
  169. return vals[inc_operator]
  170. def get_host_id_by_name(zapi, host_name):
  171. '''Get host id by name'''
  172. content = zapi.get_content('host',
  173. 'get',
  174. {'filter': {'name': host_name}})
  175. return content['result'][0]['hostid']
  176. def get_trigger_value(inc_trigger):
  177. '''determine the proper trigger value'''
  178. rval = 1
  179. if inc_trigger == 'PROBLEM':
  180. rval = 1
  181. else:
  182. rval = 0
  183. return rval
  184. def get_template_id_by_name(zapi, t_name):
  185. '''get the template id by name'''
  186. content = zapi.get_content('template',
  187. 'get',
  188. {'filter': {'host': t_name}})
  189. return content['result'][0]['templateid']
  190. def get_host_group_id_by_name(zapi, hg_name):
  191. '''Get hostgroup id by name'''
  192. content = zapi.get_content('hostgroup',
  193. 'get',
  194. {'filter': {'name': hg_name}})
  195. return content['result'][0]['groupid']
  196. def get_condition_type(event_source, inc_condition):
  197. '''determine the condition type'''
  198. c_types = {}
  199. if event_source == 'trigger':
  200. c_types = {'host group': 0,
  201. 'host': 1,
  202. 'trigger': 2,
  203. 'trigger name': 3,
  204. 'trigger severity': 4,
  205. 'trigger value': 5,
  206. 'time period': 6,
  207. 'host template': 13,
  208. 'application': 15,
  209. 'maintenance status': 16,
  210. }
  211. elif event_source == 'discovery':
  212. c_types = {'host IP': 7,
  213. 'discovered service type': 8,
  214. 'discovered service port': 9,
  215. 'discovery status': 10,
  216. 'uptime or downtime duration': 11,
  217. 'received value': 12,
  218. 'discovery rule': 18,
  219. 'discovery check': 19,
  220. 'proxy': 20,
  221. 'discovery object': 21,
  222. }
  223. elif event_source == 'auto':
  224. c_types = {'proxy': 20,
  225. 'host name': 22,
  226. 'host metadata': 24,
  227. }
  228. elif event_source == 'internal':
  229. c_types = {'host group': 0,
  230. 'host': 1,
  231. 'host template': 13,
  232. 'application': 15,
  233. 'event type': 23,
  234. }
  235. else:
  236. raise ZabbixAPIError('Unkown event source %s' % event_source)
  237. return c_types[inc_condition]
  238. def get_operation_type(inc_operation):
  239. ''' determine the correct operation type'''
  240. o_types = {'send message': 0,
  241. 'remote command': 1,
  242. 'add host': 2,
  243. 'remove host': 3,
  244. 'add to host group': 4,
  245. 'remove from host group': 5,
  246. 'link to template': 6,
  247. 'unlink from template': 7,
  248. 'enable host': 8,
  249. 'disable host': 9,
  250. }
  251. return o_types[inc_operation]
  252. def get_action_operations(zapi, inc_operations):
  253. '''Convert the operations into syntax for api'''
  254. for operation in inc_operations:
  255. operation['operationtype'] = get_operation_type(operation['operationtype'])
  256. if operation['operationtype'] == 0: # send message. Need to fix the
  257. operation['opmessage']['mediatypeid'] = \
  258. get_mediatype_id_by_name(zapi, operation['opmessage']['mediatypeid'])
  259. operation['opmessage_grp'] = get_user_groups(zapi, operation.get('opmessage_grp', []))
  260. operation['opmessage_usr'] = get_users(zapi, operation.get('opmessage_usr', []))
  261. if operation['opmessage']['default_msg']:
  262. operation['opmessage']['default_msg'] = 1
  263. else:
  264. operation['opmessage']['default_msg'] = 0
  265. # NOT supported for remote commands
  266. elif operation['operationtype'] == 1:
  267. continue
  268. # Handle Operation conditions:
  269. # Currently there is only 1 available which
  270. # is 'event acknowledged'. In the future
  271. # if there are any added we will need to pass this
  272. # option to a function and return the correct conditiontype
  273. if operation.has_key('opconditions'):
  274. for condition in operation['opconditions']:
  275. if condition['conditiontype'] == 'event acknowledged':
  276. condition['conditiontype'] = 14
  277. if condition['operator'] == '=':
  278. condition['operator'] = 0
  279. if condition['value'] == 'acknowledged':
  280. condition['value'] = 1
  281. else:
  282. condition['value'] = 0
  283. return inc_operations
  284. def get_operation_evaltype(inc_type):
  285. '''get the operation evaltype'''
  286. rval = 0
  287. if inc_type == 'and/or':
  288. rval = 0
  289. elif inc_type == 'and':
  290. rval = 1
  291. elif inc_type == 'or':
  292. rval = 2
  293. elif inc_type == 'custom':
  294. rval = 3
  295. return rval
  296. def get_action_conditions(zapi, event_source, inc_conditions):
  297. '''Convert the conditions into syntax for api'''
  298. calc_type = inc_conditions.pop('calculation_type')
  299. inc_conditions['evaltype'] = get_operation_evaltype(calc_type)
  300. for cond in inc_conditions['conditions']:
  301. cond['operator'] = get_condition_operator(cond['operator'])
  302. # Based on conditiontype we need to set the proper value
  303. # e.g. conditiontype = hostgroup then the value needs to be a hostgroup id
  304. # e.g. conditiontype = host the value needs to be a host id
  305. cond['conditiontype'] = get_condition_type(event_source, cond['conditiontype'])
  306. if cond['conditiontype'] == 0:
  307. cond['value'] = get_host_group_id_by_name(zapi, cond['value'])
  308. elif cond['conditiontype'] == 1:
  309. cond['value'] = get_host_id_by_name(zapi, cond['value'])
  310. elif cond['conditiontype'] == 4:
  311. cond['value'] = get_priority(cond['value'])
  312. elif cond['conditiontype'] == 5:
  313. cond['value'] = get_trigger_value(cond['value'])
  314. elif cond['conditiontype'] == 13:
  315. cond['value'] = get_template_id_by_name(zapi, cond['value'])
  316. elif cond['conditiontype'] == 16:
  317. cond['value'] = ''
  318. return inc_conditions
  319. def get_send_recovery(send_recovery):
  320. '''Get the integer value'''
  321. rval = 0
  322. if send_recovery:
  323. rval = 1
  324. return rval
  325. # The branches are needed for CRUD and error handling
  326. # pylint: disable=too-many-branches
  327. def main():
  328. '''
  329. ansible zabbix module for zbx_item
  330. '''
  331. module = AnsibleModule(
  332. argument_spec=dict(
  333. zbx_server=dict(default='https://localhost/zabbix/api_jsonrpc.php', type='str'),
  334. zbx_user=dict(default=os.environ.get('ZABBIX_USER', None), type='str'),
  335. zbx_password=dict(default=os.environ.get('ZABBIX_PASSWORD', None), type='str'),
  336. zbx_debug=dict(default=False, type='bool'),
  337. name=dict(default=None, type='str'),
  338. event_source=dict(default='trigger', choices=['trigger', 'discovery', 'auto', 'internal'], type='str'),
  339. action_subject=dict(default="{TRIGGER.NAME}: {TRIGGER.STATUS}", type='str'),
  340. action_message=dict(default="{TRIGGER.NAME}: {TRIGGER.STATUS}\r\n" +
  341. "Last value: {ITEM.LASTVALUE}\r\n\r\n{TRIGGER.URL}", type='str'),
  342. reply_subject=dict(default="{TRIGGER.NAME}: {TRIGGER.STATUS}", type='str'),
  343. reply_message=dict(default="Trigger: {TRIGGER.NAME}\r\nTrigger status: {TRIGGER.STATUS}\r\n" +
  344. "Trigger severity: {TRIGGER.SEVERITY}\r\nTrigger URL: {TRIGGER.URL}\r\n\r\n" +
  345. "Item values:\r\n\r\n1. {ITEM.NAME1} ({HOST.NAME1}:{ITEM.KEY1}): " +
  346. "{ITEM.VALUE1}\r\n2. {ITEM.NAME2} ({HOST.NAME2}:{ITEM.KEY2}): " +
  347. "{ITEM.VALUE2}\r\n3. {ITEM.NAME3} ({HOST.NAME3}:{ITEM.KEY3}): " +
  348. "{ITEM.VALUE3}", type='str'),
  349. send_recovery=dict(default=False, type='bool'),
  350. status=dict(default=None, type='str'),
  351. escalation_time=dict(default=60, type='int'),
  352. conditions_filter=dict(default=None, type='dict'),
  353. operations=dict(default=None, type='list'),
  354. state=dict(default='present', type='str'),
  355. ),
  356. #supports_check_mode=True
  357. )
  358. zapi = ZabbixAPI(ZabbixConnection(module.params['zbx_server'],
  359. module.params['zbx_user'],
  360. module.params['zbx_password'],
  361. module.params['zbx_debug']))
  362. #Set the instance and the template for the rest of the calls
  363. zbx_class_name = 'action'
  364. state = module.params['state']
  365. content = zapi.get_content(zbx_class_name,
  366. 'get',
  367. {'search': {'name': module.params['name']},
  368. 'selectFilter': 'extend',
  369. 'selectOperations': 'extend',
  370. })
  371. #******#
  372. # GET
  373. #******#
  374. if state == 'list':
  375. module.exit_json(changed=False, results=content['result'], state="list")
  376. #******#
  377. # DELETE
  378. #******#
  379. if state == 'absent':
  380. if not exists(content):
  381. module.exit_json(changed=False, state="absent")
  382. content = zapi.get_content(zbx_class_name, 'delete', [content['result'][0]['actionid']])
  383. module.exit_json(changed=True, results=content['result'], state="absent")
  384. # Create and Update
  385. if state == 'present':
  386. conditions = get_action_conditions(zapi, module.params['event_source'], module.params['conditions_filter'])
  387. operations = get_action_operations(zapi, module.params['operations'])
  388. params = {'name': module.params['name'],
  389. 'esc_period': module.params['escalation_time'],
  390. 'eventsource': get_event_source(module.params['event_source']),
  391. 'status': get_status(module.params['status']),
  392. 'def_shortdata': module.params['action_subject'],
  393. 'def_longdata': module.params['action_message'],
  394. 'r_shortdata': module.params['reply_subject'],
  395. 'r_longdata': module.params['reply_message'],
  396. 'recovery_msg': get_send_recovery(module.params['send_recovery']),
  397. 'filter': conditions,
  398. 'operations': operations,
  399. }
  400. # Remove any None valued params
  401. _ = [params.pop(key, None) for key in params.keys() if params[key] is None]
  402. #******#
  403. # CREATE
  404. #******#
  405. if not exists(content):
  406. content = zapi.get_content(zbx_class_name, 'create', params)
  407. if content.has_key('error'):
  408. module.exit_json(failed=True, changed=True, results=content['error'], state="present")
  409. module.exit_json(changed=True, results=content['result'], state='present')
  410. ########
  411. # UPDATE
  412. ########
  413. _ = params.pop('hostid', None)
  414. differences = {}
  415. zab_results = content['result'][0]
  416. for key, value in params.items():
  417. if key == 'operations':
  418. ops = operation_differences(zab_results[key], value)
  419. if ops:
  420. differences[key] = ops
  421. elif key == 'filter':
  422. filters = filter_differences(zab_results[key], value)
  423. if filters:
  424. differences[key] = filters
  425. elif zab_results[key] != value and zab_results[key] != str(value):
  426. differences[key] = value
  427. if not differences:
  428. module.exit_json(changed=False, results=zab_results, state="present")
  429. # We have differences and need to update.
  430. # action update requires an id, filters, and operations
  431. differences['actionid'] = zab_results['actionid']
  432. differences['operations'] = params['operations']
  433. differences['filter'] = params['filter']
  434. content = zapi.get_content(zbx_class_name, 'update', differences)
  435. if content.has_key('error'):
  436. module.exit_json(failed=True, changed=False, results=content['error'], state="present")
  437. module.exit_json(changed=True, results=content['result'], state="present")
  438. module.exit_json(failed=True,
  439. changed=False,
  440. results='Unknown state passed. %s' % state,
  441. state="unknown")
  442. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, locally-disabled
  443. # import module snippets. This are required
  444. from ansible.module_utils.basic import *
  445. main()