zbxapi.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. #!/usr/bin/env python
  2. # vim: expandtab:tabstop=4:shiftwidth=4
  3. '''
  4. ZabbixAPI ansible module
  5. '''
  6. # Copyright 2015 Red Hat Inc.
  7. #
  8. # Licensed under the Apache License, Version 2.0 (the "License");
  9. # you may not use this file except in compliance with the License.
  10. # You may obtain a copy of the License at
  11. #
  12. # http://www.apache.org/licenses/LICENSE-2.0
  13. #
  14. # Unless required by applicable law or agreed to in writing, software
  15. # distributed under the License is distributed on an "AS IS" BASIS,
  16. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. # See the License for the specific language governing permissions and
  18. # limitations under the License.
  19. #
  20. # Purpose: An ansible module to communicate with zabbix.
  21. #
  22. # pylint: disable=line-too-long
  23. # Disabling line length for readability
  24. import json
  25. import httplib2
  26. import sys
  27. import os
  28. import re
  29. import copy
  30. class ZabbixAPIError(Exception):
  31. '''
  32. ZabbixAPIError
  33. Exists to propagate errors up from the api
  34. '''
  35. pass
  36. class ZabbixAPI(object):
  37. '''
  38. ZabbixAPI class
  39. '''
  40. classes = {
  41. 'Action': ['create', 'delete', 'get', 'update'],
  42. 'Alert': ['get'],
  43. 'Application': ['create', 'delete', 'get', 'massadd', 'update'],
  44. 'Configuration': ['export', 'import'],
  45. 'Dcheck': ['get'],
  46. 'Dhost': ['get'],
  47. 'Drule': ['copy', 'create', 'delete', 'get', 'isreadable', 'iswritable', 'update'],
  48. 'Dservice': ['get'],
  49. 'Event': ['acknowledge', 'get'],
  50. 'Graph': ['create', 'delete', 'get', 'update'],
  51. 'Graphitem': ['get'],
  52. 'Graphprototype': ['create', 'delete', 'get', 'update'],
  53. 'History': ['get'],
  54. 'Hostgroup': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'massadd', 'massremove', 'massupdate', 'update'],
  55. 'Hostinterface': ['create', 'delete', 'get', 'massadd', 'massremove', 'replacehostinterfaces', 'update'],
  56. 'Host': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'massadd', 'massremove', 'massupdate', 'update'],
  57. 'Hostprototype': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'update'],
  58. 'Httptest': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'update'],
  59. 'Iconmap': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'update'],
  60. 'Image': ['create', 'delete', 'get', 'update'],
  61. 'Item': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'update'],
  62. 'Itemprototype': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'update'],
  63. 'Maintenance': ['create', 'delete', 'get', 'update'],
  64. 'Map': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'update'],
  65. 'Mediatype': ['create', 'delete', 'get', 'update'],
  66. 'Proxy': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'update'],
  67. 'Screen': ['create', 'delete', 'get', 'update'],
  68. 'Screenitem': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'update', 'updatebyposition'],
  69. 'Script': ['create', 'delete', 'execute', 'get', 'getscriptsbyhosts', 'update'],
  70. 'Service': ['adddependencies', 'addtimes', 'create', 'delete', 'deletedependencies', 'deletetimes', 'get', 'getsla', 'isreadable', 'iswritable', 'update'],
  71. 'Template': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'massadd', 'massremove', 'massupdate', 'update'],
  72. 'Templatescreen': ['copy', 'create', 'delete', 'get', 'isreadable', 'iswritable', 'update'],
  73. 'Templatescreenitem': ['get'],
  74. 'Trigger': ['adddependencies', 'create', 'delete', 'deletedependencies', 'get', 'isreadable', 'iswritable', 'update'],
  75. 'Triggerprototype': ['create', 'delete', 'get', 'update'],
  76. 'User': ['addmedia', 'create', 'delete', 'deletemedia', 'get', 'isreadable', 'iswritable', 'login', 'logout', 'update', 'updatemedia', 'updateprofile'],
  77. 'Usergroup': ['create', 'delete', 'get', 'isreadable', 'iswritable', 'massadd', 'massupdate', 'update'],
  78. 'Usermacro': ['create', 'createglobal', 'delete', 'deleteglobal', 'get', 'update', 'updateglobal'],
  79. 'Usermedia': ['get'],
  80. }
  81. def __init__(self, data=None):
  82. if not data:
  83. data = {}
  84. self.server = data.get('server', None)
  85. self.username = data.get('user', None)
  86. self.password = data.get('password', None)
  87. if any([value == None for value in [self.server, self.username, self.password]]):
  88. print 'Please specify zabbix server url, username, and password.'
  89. sys.exit(1)
  90. self.verbose = data.get('verbose', False)
  91. self.use_ssl = data.has_key('use_ssl')
  92. self.auth = None
  93. for cname, _ in self.classes.items():
  94. setattr(self, cname.lower(), getattr(self, cname)(self))
  95. # pylint: disable=no-member
  96. # This method does not exist until the metaprogramming executed
  97. results = self.user.login(user=self.username, password=self.password)
  98. if results[0]['status'] == '200':
  99. if results[1].has_key('result'):
  100. self.auth = results[1]['result']
  101. elif results[1].has_key('error'):
  102. print "Unable to authenticate with zabbix server. {0} ".format(results[1]['error'])
  103. sys.exit(1)
  104. else:
  105. print "Error in call to zabbix. Http status: {0}.".format(results[0]['status'])
  106. sys.exit(1)
  107. def perform(self, method, rpc_params):
  108. '''
  109. This method calls your zabbix server.
  110. It requires the following parameters in order for a proper request to be processed:
  111. jsonrpc - the version of the JSON-RPC protocol used by the API;
  112. the Zabbix API implements JSON-RPC version 2.0;
  113. method - the API method being called;
  114. rpc_params - parameters that will be passed to the API method;
  115. id - an arbitrary identifier of the request;
  116. auth - a user authentication token; since we don't have one yet, it's set to null.
  117. '''
  118. http_method = "POST"
  119. jsonrpc = "2.0"
  120. rid = 1
  121. http = None
  122. if self.use_ssl:
  123. http = httplib2.Http()
  124. else:
  125. http = httplib2.Http(disable_ssl_certificate_validation=True,)
  126. headers = {}
  127. headers["Content-type"] = "application/json"
  128. body = {
  129. "jsonrpc": jsonrpc,
  130. "method": method,
  131. "params": rpc_params.get('params', {}),
  132. "id": rid,
  133. 'auth': self.auth,
  134. }
  135. if method in ['user.login', 'api.version']:
  136. del body['auth']
  137. body = json.dumps(body)
  138. if self.verbose:
  139. print body
  140. print method
  141. print headers
  142. httplib2.debuglevel = 1
  143. response, content = http.request(self.server, http_method, body, headers)
  144. if response['status'] not in ['200', '201']:
  145. raise ZabbixAPIError('Error calling zabbix. Zabbix returned %s' % response['status'])
  146. if self.verbose:
  147. print response
  148. print content
  149. try:
  150. content = json.loads(content)
  151. except ValueError as err:
  152. content = {"error": err.message}
  153. return response, content
  154. @staticmethod
  155. def meta(cname, method_names):
  156. '''
  157. This bit of metaprogramming is where the ZabbixAPI subclasses are created.
  158. For each of ZabbixAPI.classes we create a class from the key and methods
  159. from the ZabbixAPI.classes values. We pass a reference to ZabbixAPI class
  160. to each subclass in order for each to be able to call the perform method.
  161. '''
  162. def meta_method(_class, method_name):
  163. '''
  164. This meta method allows a class to add methods to it.
  165. '''
  166. # This template method is a stub method for each of the subclass
  167. # methods.
  168. def template_method(self, params=None, **rpc_params):
  169. '''
  170. This template method is a stub method for each of the subclass methods.
  171. '''
  172. if params:
  173. rpc_params['params'] = params
  174. else:
  175. rpc_params['params'] = copy.deepcopy(rpc_params)
  176. return self.parent.perform(cname.lower()+"."+method_name, rpc_params)
  177. template_method.__doc__ = \
  178. "https://www.zabbix.com/documentation/2.4/manual/api/reference/%s/%s" % \
  179. (cname.lower(), method_name)
  180. template_method.__name__ = method_name
  181. # this is where the template method is placed inside of the subclass
  182. # e.g. setattr(User, "create", stub_method)
  183. setattr(_class, template_method.__name__, template_method)
  184. # This class call instantiates a subclass. e.g. User
  185. _class = type(cname,
  186. (object,),
  187. {'__doc__': \
  188. "https://www.zabbix.com/documentation/2.4/manual/api/reference/%s" % cname.lower()})
  189. def __init__(self, parent):
  190. '''
  191. This init method gets placed inside of the _class
  192. to allow it to be instantiated. A reference to the parent class(ZabbixAPI)
  193. is passed in to allow each class access to the perform method.
  194. '''
  195. self.parent = parent
  196. # This attaches the init to the subclass. e.g. Create
  197. setattr(_class, __init__.__name__, __init__)
  198. # For each of our ZabbixAPI.classes dict values
  199. # Create a method and attach it to our subclass.
  200. # e.g. 'User': ['delete', 'get', 'updatemedia', 'updateprofile',
  201. # 'update', 'iswritable', 'logout', 'addmedia', 'create',
  202. # 'login', 'deletemedia', 'isreadable'],
  203. # User.delete
  204. # User.get
  205. for method_name in method_names:
  206. meta_method(_class, method_name)
  207. # Return our subclass with all methods attached
  208. return _class
  209. # Attach all ZabbixAPI.classes to ZabbixAPI class through metaprogramming
  210. for _class_name, _method_names in ZabbixAPI.classes.items():
  211. setattr(ZabbixAPI, _class_name, ZabbixAPI.meta(_class_name, _method_names))
  212. def exists(content, key='result'):
  213. ''' Check if key exists in content or the size of content[key] > 0
  214. '''
  215. if not content.has_key(key):
  216. return False
  217. if not content[key]:
  218. return False
  219. return True
  220. def diff_content(from_zabbix, from_user, ignore=None):
  221. ''' Compare passed in object to results returned from zabbix
  222. '''
  223. terms = ['search', 'output', 'groups', 'select', 'expand', 'filter']
  224. if ignore:
  225. terms.extend(ignore)
  226. regex = '(' + '|'.join(terms) + ')'
  227. retval = {}
  228. for key, value in from_user.items():
  229. if re.findall(regex, key):
  230. continue
  231. # special case here for templates. You query templates and
  232. # the zabbix api returns parentTemplates. These will obviously fail.
  233. # So when its templates compare against parentTemplates.
  234. if key == 'templates' and from_zabbix.has_key('parentTemplates'):
  235. if from_zabbix['parentTemplates'] != value:
  236. retval[key] = value
  237. elif from_zabbix[key] != str(value):
  238. retval[key] = str(value)
  239. return retval
  240. def main():
  241. '''
  242. This main method runs the ZabbixAPI Ansible Module
  243. '''
  244. module = AnsibleModule(
  245. argument_spec=dict(
  246. server=dict(default='https://localhost/zabbix/api_jsonrpc.php', type='str'),
  247. user=dict(default=None, type='str'),
  248. password=dict(default=None, type='str'),
  249. zbx_class=dict(choices=ZabbixAPI.classes.keys()),
  250. params=dict(),
  251. debug=dict(default=False, type='bool'),
  252. state=dict(default='present', type='str'),
  253. ignore=dict(default=None, type='list'),
  254. ),
  255. #supports_check_mode=True
  256. )
  257. user = module.params.get('user', None)
  258. if not user:
  259. user = os.environ['ZABBIX_USER']
  260. passwd = module.params.get('password', None)
  261. if not passwd:
  262. passwd = os.environ['ZABBIX_PASSWORD']
  263. api_data = {
  264. 'user': user,
  265. 'password': passwd,
  266. 'server': module.params['server'],
  267. 'verbose': module.params['debug']
  268. }
  269. if not user or not passwd or not module.params['server']:
  270. module.fail_json(msg='Please specify the user, password, and the zabbix server.')
  271. zapi = ZabbixAPI(api_data)
  272. ignore = module.params['ignore']
  273. zbx_class = module.params.get('zbx_class')
  274. rpc_params = module.params.get('params', {})
  275. state = module.params.get('state')
  276. # Get the instance we are trying to call
  277. zbx_class_inst = zapi.__getattribute__(zbx_class.lower())
  278. # perform get
  279. # Get the instance's method we are trying to call
  280. zbx_action_method = zapi.__getattribute__(zbx_class.capitalize()).__dict__['get']
  281. _, content = zbx_action_method(zbx_class_inst, rpc_params)
  282. if state == 'list':
  283. module.exit_json(changed=False, results=content['result'], state="list")
  284. if state == 'absent':
  285. if not exists(content):
  286. module.exit_json(changed=False, state="absent")
  287. # If we are coming from a query, we need to pass in the correct rpc_params for delete.
  288. # specifically the zabbix class name + 'id'
  289. # if rpc_params is a list then we need to pass it. (list of ids to delete)
  290. idname = zbx_class.lower() + "id"
  291. if not isinstance(rpc_params, list) and content['result'][0].has_key(idname):
  292. rpc_params = [content['result'][0][idname]]
  293. zbx_action_method = zapi.__getattribute__(zbx_class.capitalize()).__dict__['delete']
  294. _, content = zbx_action_method(zbx_class_inst, rpc_params)
  295. module.exit_json(changed=True, results=content['result'], state="absent")
  296. if state == 'present':
  297. # It's not there, create it!
  298. if not exists(content):
  299. zbx_action_method = zapi.__getattribute__(zbx_class.capitalize()).__dict__['create']
  300. _, content = zbx_action_method(zbx_class_inst, rpc_params)
  301. module.exit_json(changed=True, results=content['result'], state='present')
  302. # It's there and the same, do nothing!
  303. diff_params = diff_content(content['result'][0], rpc_params, ignore)
  304. if not diff_params:
  305. module.exit_json(changed=False, results=content['result'], state="present")
  306. # Add the id to update with
  307. idname = zbx_class.lower() + "id"
  308. diff_params[idname] = content['result'][0][idname]
  309. ## It's there and not the same, update it!
  310. zbx_action_method = zapi.__getattribute__(zbx_class.capitalize()).__dict__['update']
  311. _, content = zbx_action_method(zbx_class_inst, diff_params)
  312. module.exit_json(changed=True, results=content, state="present")
  313. module.exit_json(failed=True,
  314. changed=False,
  315. results='Unknown state passed. %s' % state,
  316. state="unknown")
  317. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, locally-disabled
  318. # import module snippets. This are required
  319. from ansible.module_utils.basic import *
  320. main()