dns-hook.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. #!/usr/local/bin/python2
  2. #
  3. # Copyright (c) 2017-2019 Joe Clarke <jclarke@cisco.com>
  4. # All rights reserved.
  5. #
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions
  8. # are met:
  9. # 1. Redistributions of source code must retain the above copyright
  10. # notice, this list of conditions and the following disclaimer.
  11. # 2. Redistributions in binary form must reproduce the above copyright
  12. # notice, this list of conditions and the following disclaimer in the
  13. # documentation and/or other materials provided with the distribution.
  14. #
  15. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  16. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  17. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  18. # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  19. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  20. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  21. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  22. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  23. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  24. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  25. # SUCH DAMAGE.
  26. import sys
  27. import json
  28. from sparker import Sparker
  29. import re
  30. import requests
  31. from requests.packages.urllib3.exceptions import InsecureRequestWarning
  32. requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
  33. import time
  34. import traceback
  35. import socket
  36. import logging
  37. import CLEUCreds
  38. CMX_GW = 'http://cl-freebsd.ciscolive.network:8002/api/v0.1/cmx'
  39. DNS_BASE = 'https://dc1-dns.ciscolive.network:8443/web-services/rest/resource/'
  40. DOMAIN = 'ciscolive.network'
  41. CNR_HEADERS = {
  42. 'Accept': 'application/json',
  43. 'Content-Type': 'application/json',
  44. 'Authorization': CLEUCreds.JCLARKE_BASIC
  45. }
  46. ALLOWED_TO_CREATE = ['jclarke@cisco.com',
  47. 'ksekula@cisco.com', 'ayourtch@cisco.com', 'rkamerma@cisco.com', 'thulsdau@cisco.com',
  48. 'lhercot@cisco.com', 'pweijden@cisco.com', 'udiedric@cisco.com']
  49. spark = Sparker(token=CLEUCreds.SPARK_TOKEN, logit=True)
  50. SPARK_TEAM = 'CL19 NOC Team'
  51. SPARK_ROOM = 'DNS Queries'
  52. def check_for_alias(alias):
  53. global DNS_BASE, DOMAIN, CNR_HEADERS
  54. url = DNS_BASE + 'CCMRRSet' + '/{}'.format(alias)
  55. response = requests.request(
  56. 'GET', url, params={'zoneOrigin': DOMAIN}, headers=CNR_HEADERS, verify=False)
  57. if response.status_code == 404:
  58. return None
  59. res = {}
  60. j = response.json()
  61. hostname = ''
  62. for rr in j['rrs']['stringItem']:
  63. m = re.search(r'^IN CNAME (.+)', rr)
  64. if m:
  65. hostname = m.group(1)
  66. break
  67. res['hostname'] = hostname
  68. return res
  69. def create_alias(hostname, alias):
  70. global DNS_BASE, DOMAIN, CNR_HEADERS
  71. url = DNS_BASE + 'CCMRRSet' + '/{}'.format(alias)
  72. if re.search(r'\.', hostname) and not hostname.endswith('.'):
  73. hostname += '.'
  74. if not hostname.endswith('.'):
  75. hostname += '.' + DOMAIN + '.'
  76. rr_obj = {
  77. 'name': alias,
  78. 'zoneOrigin': DOMAIN,
  79. 'rrs': {
  80. 'stringItem': [
  81. 'IN CNAME {}'.format(hostname)
  82. ]
  83. }
  84. }
  85. response = requests.request(
  86. 'PUT', url, headers=CNR_HEADERS, json=rr_obj, verify=False)
  87. response.raise_for_status()
  88. def delete_alias(alias):
  89. global DNS_BASE, DOMAIN, CNR_HEADERS
  90. url = DNS_BASE + 'CCMRRSet' + '/{}'.format(alias)
  91. response = requests.request('DELETE', url, params={
  92. 'zoneOrigin': DOMAIN}, headers=CNR_HEADERS, verify=False)
  93. response.raise_for_status()
  94. def delete_record(hostname):
  95. global DNS_BASE, DOMAIN, CNR_HEADERS
  96. url = DNS_BASE + 'CCMHost' + '/{}'.format(hostname)
  97. response = requests.request('DELETE', url, params={
  98. 'zoneOrigin': DOMAIN}, headers=CNR_HEADERS, verify=False)
  99. response.raise_for_status()
  100. def check_for_record(hostname):
  101. global DNS_BASE, DOMAIN, CNR_HEADERS
  102. url = DNS_BASE + 'CCMHost' + '/{}'.format(hostname)
  103. response = requests.request(
  104. 'GET', url, params={'zoneOrigin': DOMAIN}, headers=CNR_HEADERS, verify=False)
  105. if response.status_code == 404:
  106. return None
  107. res = {}
  108. j = response.json()
  109. res['ip'] = j['addrs']['stringItem'][0]
  110. return res
  111. def create_record(hostname, ip, aliases):
  112. global DNS_BASE, DOMAIN, CNR_HEADERS
  113. url = DNS_BASE + 'CCMHost' + '/{}'.format(hostname)
  114. host_obj = {
  115. 'addrs': {
  116. 'stringItem': [
  117. ip
  118. ]
  119. },
  120. 'name': hostname,
  121. 'zoneOrigin': DOMAIN
  122. }
  123. if aliases is not None:
  124. aliases = re.sub(r'\s+', '', aliases)
  125. alist = aliases.split(',')
  126. alist = [x + '.' + DOMAIN +
  127. '.' if not x.endswith('.') else x for x in alist]
  128. host_obj['aliases'] = {
  129. 'stringItem': alist
  130. }
  131. response = requests.request(
  132. 'PUT', url, headers=CNR_HEADERS, json=host_obj, verify=False)
  133. response.raise_for_status()
  134. if __name__ == '__main__':
  135. print('Content-type: application/json\r\n\r\n')
  136. output = sys.stdin.read()
  137. j = json.loads(output)
  138. logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s : %(message)s',
  139. filename='/var/log/dns-hook.log', level=logging.DEBUG)
  140. logging.debug(json.dumps(j, indent=4))
  141. message_from = j['data']['personEmail']
  142. if message_from == 'livenocbot@sparkbot.io':
  143. logging.debug('Person email is our bot')
  144. print('{"result":"success"}')
  145. sys.exit(0)
  146. tid = spark.get_team_id(SPARK_TEAM)
  147. if tid is None:
  148. logging.error('Failed to get Spark Team ID')
  149. print('{"result":"fail"}')
  150. sys.exit(0)
  151. rid = spark.get_room_id(tid, SPARK_ROOM)
  152. if rid is None:
  153. logging.error('Failed to get Spark Room ID')
  154. print('{"result":"fail"}')
  155. sys.exit(0)
  156. if rid != j['data']['roomId']:
  157. logging.error('Spark Room ID is not the same as in the message ({} vs. {})'.format(
  158. rid, j['data']['roomId']))
  159. print('{"result":"fail"}')
  160. sys.exit(0)
  161. mid = j['data']['id']
  162. msg = spark.get_message(mid)
  163. if msg is None:
  164. logging.error('Did not get a message')
  165. print('{"result":"error"}')
  166. sys.exit(0)
  167. txt = msg['text']
  168. found_hit = False
  169. if re.search(r'\bhelp\b', txt, re.I):
  170. spark.post_to_spark(
  171. SPARK_TEAM, SPARK_ROOM, 'To create a new DNS entry, tell me things like, `Create record for HOST with IP and alias ALIAS`, `Create entry for HOST with IP`, `Add a DNS record for HOST with IP`')
  172. found_hit = True
  173. try:
  174. m = re.search(
  175. r'(remove|delete)\s+.*?(alias|cname)\s+([\w\-\.]+)', txt, re.I)
  176. if not found_hit and m:
  177. found_hit = True
  178. if message_from not in ALLOWED_TO_CREATE:
  179. spark.post_to_spark(
  180. SPARK_TEAM, SPARK_ROOM, 'I\'m sorry, {}. I can\'t do that for you.'.format(message_from))
  181. else:
  182. res = check_for_alias(m.group(3))
  183. if res is None:
  184. spark.post_to_spark(
  185. SPARK_TEAM, SPARK_ROOM, 'I didn\'t find an alias {}'.format(m.group(3)))
  186. else:
  187. try:
  188. delete_alias(m.group(3))
  189. spark.post_to_spark(
  190. SPARK_TEAM, SPARK_ROOM, 'Alias {} deleted successfully.'.format(m.group(3)))
  191. except Exception as e:
  192. spark.post_to_spark(
  193. SPARK_TEAM, SPARK_ROOM, 'Failed to delete alias {}: {}'.format(m.group(3), e))
  194. m = re.search(
  195. r'(remove|delete)\s+.*?for\s+([\w\-\.]+)', txt, re.I)
  196. if not found_hit and m:
  197. found_hit = True
  198. if message_from not in ALLOWED_TO_CREATE:
  199. spark.post_to_spark(
  200. SPARK_TEAM, SPARK_ROOM, 'I\'m sorry, {}. I can\'t do that for you.'.format(message_from))
  201. else:
  202. res = check_for_record(m.group(2))
  203. if res is None:
  204. spark.post_to_spark(
  205. SPARK_TEAM, SPARK_ROOM, 'I didn\'t find a DNS record for {}.'.format(m.group(2)))
  206. else:
  207. try:
  208. delete_record(m.group(2))
  209. spark.post_to_spark(
  210. SPARK_TEAM, SPARK_ROOM, 'DNS record for {} deleted successfully.'.format(m.group(2)))
  211. except Exception as e:
  212. spark.post_to_spark(
  213. SPARK_TEAM, SPARK_ROOM, 'Failed to delete DNS record for {}: {}'.format(m.group(2), e))
  214. m = re.search(
  215. r'(make|create|add)\s+.*?for\s+([\w\-\.]+)\s+.*?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(\s+.*?(alias(es)?|cname(s)?)\s+([\w\-\.]+(\s*,\s*[\w\-\.,\s]+)?))?', txt, re.I)
  216. if not found_hit and m:
  217. found_hit = True
  218. if message_from not in ALLOWED_TO_CREATE:
  219. spark.post_to_spark(
  220. SPARK_TEAM, SPARK_ROOM, 'I\'m sorry, {}. I can\'t do that for you.'.format(message_from))
  221. else:
  222. res = check_for_record(m.group(2))
  223. if res is not None:
  224. spark.post_to_spark(
  225. SPARK_TEAM, SPARK_ROOM, '_{}_ is already in DNS as **{}**'.format(m.group(2), res['ip']))
  226. else:
  227. hostname = re.sub(r'\.{}'.format(DOMAIN), '', m.group(2))
  228. try:
  229. create_record(m.group(2), m.group(3), m.group(8))
  230. spark.post_to_spark(
  231. SPARK_TEAM, SPARK_ROOM, 'Successfully created record for {}.'.format(m.group(2)))
  232. except Exception as e:
  233. spark.post_to_spark(
  234. SPARK_TEAM, SPARK_ROOM, 'Failed to create record for {}: {}'.format(m.group(2), e))
  235. m = re.search(
  236. r'(make|create|add)\s+(alias(es)?|cname(s)?)\s+([\w\-\.]+(\s*,\s*[\w\-\.,\s]+)?)\s+(for|to)\s+([\w\-\.]+)', txt, re.I)
  237. if not found_hit and m:
  238. found_hit = True
  239. if message_from not in ALLOWED_TO_CREATE:
  240. spark.post_to_spark(
  241. SPARK_TEAM, SPARK_ROOM, 'I\'m sorry, {}. I can\'t do that for you.'.format(message_from))
  242. else:
  243. aliases = m.group(5)
  244. aliases = re.sub(r'\s+', '', aliases)
  245. alist = aliases.split(',')
  246. already_exists = False
  247. for alias in alist:
  248. res = check_for_alias(alias)
  249. if res is not None:
  250. already_exists = True
  251. spark.post_to_spark(
  252. SPARK_TEAM, SPARK_ROOM, '_{}_ is already an alias for **{}**'.format(alias, res['hostname']))
  253. res = check_for_record(alias)
  254. if res is not None:
  255. already_exists = True
  256. spark.post_to_spark(
  257. SPARK_TEAM, SPARK_ROOM, '_{}_ is already a hostname with IP **{}**'.format(alias, res['ip']))
  258. if not already_exists:
  259. success = True
  260. for alias in alist:
  261. try:
  262. create_alias(m.group(8), alias)
  263. except Exception as e:
  264. spark.post_to_spark(
  265. SPARK_TEAM, SPARK_ROOM, 'Failed to create alias {}: {}'.format(alias, e))
  266. success = False
  267. if success:
  268. spark.post_to_spark(
  269. SPARK_TEAM, SPARK_ROOM, 'Successfully created alias(es) {} for {}'.format(aliases, m.group(8)))
  270. if not found_hit:
  271. spark.post_to_spark(SPARK_TEAM, SPARK_ROOM,
  272. 'Sorry, I didn\'t get that. Please ask me to create or delete a DNS entry; or just ask for "help".')
  273. except Exception as e:
  274. logging.error('Error in obtaining data: {}'.format(
  275. traceback.format_exc()))
  276. spark.post_to_spark(SPARK_TEAM, SPARK_ROOM,
  277. 'Whoops, I encountered an error:<br>\n```\n{}\n```'.format(traceback.format_exc()))
  278. print('{"result":"success"}')