update-dns-tool.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. #!/usr/bin/env python3
  2. #
  3. # Copyright (c) 2017-2020 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. from __future__ import print_function
  27. from builtins import str
  28. import requests
  29. from requests.packages.urllib3.exceptions import InsecureRequestWarning
  30. requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
  31. import json
  32. import sys
  33. import re
  34. import os
  35. import argparse
  36. import CLEUCreds
  37. from cleu.config import Config as C
  38. CNR_HEADERS = {"authorization": CLEUCreds.JCLARKE_BASIC, "accept": "application/json", "content-type": "application/json"}
  39. CACHE_FILE = "dns_records.dat"
  40. def get_devs():
  41. url = "http://{}/get/switches/json".format(C.TOOL)
  42. devices = []
  43. response = requests.request("GET", url)
  44. code = response.status_code
  45. if code == 200:
  46. j = response.json()
  47. for dev in j:
  48. dev_dic = {}
  49. if dev["IPAddress"] == "0.0.0.0":
  50. continue
  51. if not re.search(r"^0", dev["Hostname"]):
  52. continue
  53. dev_dic["name"] = dev["Hostname"]
  54. dev_dic["aliases"] = [str("{}.{}.".format(dev["Name"], C.DNS_DOMAIN)), str("{}.{}.".format(dev["AssetTag"], C.DNS_DOMAIN))]
  55. dev_dic["ip"] = dev["IPAddress"]
  56. devices.append(dev_dic)
  57. return devices
  58. def purge_rr(name, url, zone):
  59. params = {"zoneOrigin": zone}
  60. try:
  61. response = requests.request("DELETE", url, headers=CNR_HEADERS, params=params, verify=False)
  62. response.raise_for_status()
  63. print("Purged entry for {}".format(name))
  64. except Exception as e:
  65. sys.stderr.write("Error purging entry for {}: {}\n".format(name, e))
  66. def purge_rrs(hname, dev):
  67. aname = hname
  68. cnames = []
  69. for alias in dev["aliases"]:
  70. cnames.append(alias.split(".")[0])
  71. pname = ".".join(dev["ip"].split(".")[::-1][0:3])
  72. ubase = C.DNS_BASE + "CCMRRSet" + "/{}"
  73. url = ubase.format(aname)
  74. purge_rr(aname, url, C.DNS_DOMAIN)
  75. for cname in cnames:
  76. url = ubase.format(cname)
  77. purge_rr(cname, url, C.DNS_DOMAIN)
  78. url = ubase.format(pname)
  79. purge_rr(pname, url, "10.in-addr.arpa")
  80. def add_entry(url, hname, dev):
  81. global CNR_HEADERS
  82. try:
  83. rrset = [
  84. "IN 0 A {}".format(dev["ip"]),
  85. ]
  86. rrset_obj = {"name": hname, "rrs": {"stringItem": rrset}, "zoneOrigin": C.DNS_DOMAIN}
  87. response = requests.request("PUT", url, headers=CNR_HEADERS, json=rrset_obj, verify=False)
  88. response.raise_for_status()
  89. print("Added entry for {} ==> {}".format(hname, dev["ip"]))
  90. except Exception as e:
  91. sys.stderr.write("Error adding entry for {}: {}\n".format(hname, e))
  92. return
  93. for alias in dev["aliases"]:
  94. aname = alias.split(".")[0]
  95. alias_rrset_obj = {
  96. "name": aname,
  97. "rrs": {"stringItem": ["IN 0 CNAME {}.{}.".format(hname, C.DNS_DOMAIN)]},
  98. "zoneOrigin": C.DNS_DOMAIN,
  99. }
  100. url = C.DNS_BASE + "CCMRRSet" + "/{}".format(aname)
  101. try:
  102. response = requests.request("PUT", url, headers=CNR_HEADERS, json=alias_rrset_obj, verify=False)
  103. response.raise_for_status()
  104. print("Added CNAME entry {} ==> {}".format(alias, hname))
  105. except Exception as e:
  106. sys.stderr.write("Error adding CNAME {} for {}: {}\n".format(alias, hname, e))
  107. try:
  108. ptr_rrset = ["IN 0 PTR {}.{}.".format(hname, C.DNS_DOMAIN)]
  109. rip = ".".join(dev["ip"].split(".")[::-1][0:3])
  110. ptr_rrset_obj = {"name": rip, "rrs": {"stringItem": ptr_rrset}, "zoneOrigin": "10.in-addr.arpa."}
  111. url = C.DNS_BASE + "CCMRRSet" + "/{}".format(rip)
  112. response = requests.request("PUT", url, headers=CNR_HEADERS, json=ptr_rrset_obj, verify=False)
  113. response.raise_for_status()
  114. print("Added PTR entry {} ==> {}".format(rip, hname))
  115. except Exception as e:
  116. sys.stderr.write("Error adding PTR entry for {}: {}\n".format(rip, e))
  117. if __name__ == "__main__":
  118. parser = argparse.ArgumentParser(description="Usage:")
  119. # script arguments
  120. parser.add_argument("--purge", help="Purge previous records", action="store_true")
  121. args = parser.parse_args()
  122. prev_records = []
  123. if os.path.exists(CACHE_FILE):
  124. fd = open(CACHE_FILE, "r")
  125. prev_records = json.load(fd)
  126. fd.close()
  127. devs = get_devs()
  128. for record in prev_records:
  129. found_record = False
  130. for dev in devs:
  131. hname = dev["name"].replace(".{}".format(C.DNS_DOMAIN), "")
  132. if record == hname:
  133. found_record = True
  134. break
  135. if found_record:
  136. continue
  137. url = C.DNS_BASE + "CCMHost" + "/{}".format(record)
  138. try:
  139. response = requests.request("DELETE", url, headers=CNR_HEADERS, params={"zoneOrigin": C.DNS_DOMAIN}, verify=False)
  140. response.raise_for_status()
  141. except Exception as e:
  142. sys.stderr.write("Failed to delete entry for {}\n".format(record))
  143. records = []
  144. for dev in devs:
  145. hname = dev["name"].replace(".{}".format(C.DNS_DOMAIN), "")
  146. records.append(hname)
  147. if args.purge:
  148. purge_rrs(hname, dev)
  149. url = C.DNS_BASE + "CCMHost" + "/{}".format(hname)
  150. response = requests.request("GET", url, headers=CNR_HEADERS, params={"zoneOrigin": C.DNS_DOMAIN}, verify=False)
  151. url = C.DNS_BASE + "CCMRRSet" + "/{}".format(hname)
  152. if response.status_code == 404:
  153. iurl = C.DNS_BASE + "CCMHost"
  154. response = requests.request(
  155. "GET", iurl, params={"zoneOrigin": C.DNS_DOMAIN, "addrs": dev["ip"] + "$"}, headers=CNR_HEADERS, verify=False
  156. )
  157. cur_entry = []
  158. if response.status_code != 404:
  159. cur_entry = response.json()
  160. if len(cur_entry) > 0:
  161. print("Found entry for {}: {}".format(dev["ip"], response.status_code))
  162. cur_entry = response.json()
  163. if len(cur_entry) > 1:
  164. print("ERROR: Found multiple entries for IP {}".format(dev["ip"]))
  165. continue
  166. print("Found old entry for IP {} => {}".format(dev["ip"], cur_entry[0]["name"]))
  167. durl = C.DNS_BASE + "CCMHost" + "/{}".format(cur_entry[0]["name"])
  168. try:
  169. response = requests.request("DELETE", durl, params={"zoneOrigin": C.DNS_DOMAIN}, headers=CNR_HEADERS, verify=False)
  170. response.raise_for_status()
  171. except Exception as e:
  172. sys.stderr.write("Failed to delete stale entry for {} ({})\n".format(cur_entry[0]["name"], dev["ip"]))
  173. continue
  174. add_entry(url, hname, dev)
  175. else:
  176. cur_entry = response.json()
  177. create_new = True
  178. for addr in cur_entry["addrs"]["stringItem"]:
  179. if addr == dev["ip"]:
  180. if "aliases" in dev and "aliases" in cur_entry:
  181. if (len(dev["aliases"]) > 0 and "stringItem" not in cur_entry["aliases"]) or (
  182. len(dev["aliases"]) != len(cur_entry["aliases"]["stringItem"])
  183. ):
  184. break
  185. common = set(dev["aliases"]) & set(cur_entry["aliases"]["stringItem"])
  186. if len(common) != len(dev["aliases"]):
  187. break
  188. create_new = False
  189. break
  190. elif ("aliases" in dev and "aliases" not in cur_entry) or ("aliases" in cur_entry and "aliases" not in dev):
  191. break
  192. else:
  193. create_new = False
  194. break
  195. if create_new:
  196. print("Deleting entry for {}".format(hname))
  197. purge_rrs(hname, dev)
  198. add_entry(url, hname, dev)
  199. else:
  200. # print("Not creating a new entry for {} as it already exists".format(dev["name"]))
  201. pass
  202. fd = open(CACHE_FILE, "w")
  203. json.dump(records, fd, indent=4)
  204. fd.close()