update-netbox-tool.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. #!/usr/bin/env python
  2. #
  3. # Copyright (c) 2017-2024 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 elemental_utils import ElementalNetbox
  28. import requests
  29. from requests.packages.urllib3.exceptions import InsecureRequestWarning # type: ignore
  30. requests.packages.urllib3.disable_warnings(InsecureRequestWarning) # type: ignore
  31. import json
  32. import sys
  33. import re
  34. import os
  35. import argparse
  36. import CLEUCreds # type: ignore
  37. from cleu.config import Config as C # type: ignore
  38. CACHE_FILE = "netbox_tool_cache.json"
  39. SKU_MAP = {
  40. "WS-C3560CX-12PD-S": "WS-C3560CX-12PD-S",
  41. "C9300-48U": "C9300-48P",
  42. "C9300-48P": "C9300-48P",
  43. "C9300-24U": "C9300-24P",
  44. "C9300-24P": "C9300-24P",
  45. "WS-C3750X-24P-S": "WS-C3750X-24P-S",
  46. "WS-C3750X-24": "WS-C3750X-24P-S",
  47. "WS-C3750X-48P-S": "WS-C3750X-48P-S",
  48. "WS-C3750X-48": "WS-C3750X-48P-S",
  49. "WS-C3560CG-8": "WS-C3560CG-8PC-S",
  50. "WS-C3560CG-8PC-S": "WS-C3560CG-8PC-S",
  51. "C9500-48Y4C": "C9500-48Y4C",
  52. "CMICR-4PT": "CMICR-4PT",
  53. }
  54. TYPE_OBJ_MAP = {}
  55. INTF_MAP = {"IDF": "loopback0", "Access": "Vlan127"}
  56. INTF_CIDR_MAP = {"IDF": 32, "Access": 24}
  57. SITE_MAP = {"IDF": "IDF Closet", "Access": "Conference Space"}
  58. SITE_OBJ_MAP = {}
  59. ROLE_MAP = {"IDF": "L3 Access Switch", "Access": "L2 Access Switch"}
  60. ROLE_OBJ_MAP = {}
  61. VRF_NAME = "default"
  62. VRF_OBJ = None
  63. TENANT_NAME = "Infrastructure"
  64. TENANT_OBJ = None
  65. def get_devs():
  66. url = f"http://{C.TOOL}/get/switches/json"
  67. devices = []
  68. response = requests.request("GET", url)
  69. code = response.status_code
  70. if code == 200:
  71. j = response.json()
  72. for dev in j:
  73. dev_dic = {}
  74. if dev["IPAddress"] == "0.0.0.0":
  75. continue
  76. # Do not add MDF switches (or APs)
  77. if not re.search(r"^[0-9A-Za-z]{3}-", dev["Hostname"]):
  78. continue
  79. if dev["SKU"] not in SKU_MAP:
  80. continue
  81. dev_dic["type"] = SKU_MAP[dev["SKU"]]
  82. if re.search(r"^[0-9A-Za-z]{3}-[Xx]", dev["Hostname"]):
  83. dev_dic["role"] = ROLE_MAP["IDF"]
  84. dev_dic["intf"] = INTF_MAP["IDF"]
  85. dev_dic["cidr"] = INTF_CIDR_MAP["IDF"]
  86. dev_dic["site"] = SITE_MAP["IDF"]
  87. else:
  88. dev_dic["role"] = ROLE_MAP["Access"]
  89. dev_dic["intf"] = INTF_MAP["Access"]
  90. dev_dic["cidr"] = INTF_CIDR_MAP["Access"]
  91. dev_dic["site"] = SITE_MAP["Access"]
  92. dev_dic["name"] = dev["Hostname"]
  93. dev_dic["aliases"] = [f"{dev['Name']}", f"{dev['AssetTag']}"]
  94. dev_dic["ip"] = dev["IPAddress"]
  95. devices.append(dev_dic)
  96. return devices
  97. def delete_netbox_device(enb: ElementalNetbox, dname: str) -> None:
  98. try:
  99. dev_obj = enb.dcim.devices.get(name=dname)
  100. if dev_obj:
  101. if dev_obj.primary_ip4:
  102. dev_obj.primary_ip4.delete()
  103. dev_obj.delete()
  104. except Exception as e:
  105. sys.stderr.write(f"WARNING: Failed to delete NetBox device for {dname}: {e}\n")
  106. def populate_objects(enb: ElementalNetbox) -> None:
  107. global ROLE_OBJ_MAP, SITE_OBJ_MAP, TYPE_OBJ_MAP, TENANT_OBJ, VRF_OBJ
  108. for _, val in ROLE_MAP.items():
  109. ROLE_OBJ_MAP[val] = enb.dcim.device_roles.get(name=val)
  110. for _, val in SITE_MAP.items():
  111. SITE_OBJ_MAP[val] = enb.dcim.sites.get(name=val)
  112. for _, val in SKU_MAP.items():
  113. TYPE_OBJ_MAP[val] = enb.dcim.device_types.get(part_number=val)
  114. TENANT_OBJ = enb.tenancy.tenants.get(name=TENANT_NAME)
  115. VRF_OBJ = enb.ipam.vrfs.get(name=VRF_NAME)
  116. def add_netbox_device(enb: ElementalNetbox, dev: dict) -> None:
  117. role_obj = ROLE_OBJ_MAP[dev["role"]]
  118. type_obj = TYPE_OBJ_MAP[dev["type"]]
  119. tenant_obj = TENANT_OBJ
  120. site_obj = SITE_OBJ_MAP[dev["site"]]
  121. vrf_obj = VRF_OBJ
  122. if not role_obj:
  123. sys.stderr.write(f"ERROR: Invalid role for {dev['name']}: {dev['role']}\n")
  124. return
  125. if not type_obj:
  126. sys.stderr.write(f"ERROR: Invalid type for {dev['name']}: {dev['type']}\n")
  127. return
  128. if not site_obj:
  129. sys.stderr.write(f"ERROR: Invalid site for {dev['name']}: {dev['site']}\n")
  130. return
  131. dev_obj = enb.dcim.devices.create(
  132. name=dev["name"], device_role=role_obj.id, device_type=type_obj.id, site=site_obj.id, tenant=tenant_obj.id
  133. )
  134. if not dev_obj:
  135. sys.stderr.write(f"ERROR: Failed to create NetBox entry for {dev['name']}\n")
  136. return
  137. ip_obj = enb.ipam.ip_addresses.create(address=f"{dev['ip']}/{dev['cidr']}", tenant=tenant_obj.id, vrf=vrf_obj.id)
  138. if not ip_obj:
  139. dev_obj.delete()
  140. sys.stderr.write(f"ERROR: Failed to create IP entry for {dev['ip']}\n")
  141. return
  142. dev_intf = enb.dcim.interfaces.get(device=dev_obj.name, name=dev["intf"])
  143. if not dev_intf:
  144. dev_obj.delete()
  145. ip_obj.delete()
  146. sys.stderr.write(f"ERROR: Failed to find interface {dev['intf']} for {dev['name']}\n")
  147. return
  148. ip_obj.assigned_object_id = dev_intf.id
  149. ip_obj.assigned_object_type = "dcim.interface"
  150. dev["aliases"].sort()
  151. ip_obj.custom_fields["CNAMEs"] = ",".join(dev["aliases"])
  152. ip_obj.save()
  153. dev_obj.primary_ip4 = ip_obj.id
  154. dev_obj.save()
  155. if __name__ == "__main__":
  156. os.environ["NETBOX_ADDRESS"] = C.NETBOX_SERVER
  157. os.environ["NETBOX_API_TOKEN"] = CLEUCreds.NETBOX_API_TOKEN
  158. parser = argparse.ArgumentParser(description="Usage:")
  159. # script arguments
  160. parser.add_argument("--purge", help="Purge previous records", action="store_true")
  161. args = parser.parse_args()
  162. enb = ElementalNetbox()
  163. populate_objects(enb)
  164. prev_records = []
  165. if os.path.exists(CACHE_FILE):
  166. with open(CACHE_FILE) as fd:
  167. prev_records = json.load(fd)
  168. devs = get_devs()
  169. for record in prev_records:
  170. found_record = False
  171. for dev in devs:
  172. hname = dev["name"].replace(f".{C.DNS_DOMAIN}", "")
  173. if record == hname:
  174. found_record = True
  175. break
  176. if found_record:
  177. continue
  178. delete_netbox_device(enb, record)
  179. records = []
  180. for dev in devs:
  181. hname = dev["name"].replace(f".{C.DNS_DOMAIN}", "")
  182. records.append(hname)
  183. if args.purge:
  184. delete_netbox_device(enb, hname)
  185. dev_obj = enb.dcim.devices.get(name=hname)
  186. if not dev_obj:
  187. ip_obj = enb.ipam.ip_addresses.get(address=f"{dev['ip']}/{dev['cidr']}")
  188. cur_entry = None
  189. if ip_obj and ip_obj.assigned_object:
  190. cur_entry = ip_obj.assigned_object.device
  191. if cur_entry:
  192. print(f"INFO: Found old entry for IP {dev['ip']} => {cur_entry.name}")
  193. delete_netbox_device(enb, cur_entry.name)
  194. add_netbox_device(enb, dev)
  195. else:
  196. cur_entry = dev_obj
  197. create_new = True
  198. ip_obj = dev_obj.primary_ip4
  199. if ip_obj and ip_obj.address == f"{dev['ip']}/{dev['cidr']}":
  200. cnames = ip_obj.custom_fields["CNAMEs"]
  201. if not cnames:
  202. cnames = ""
  203. dev["aliases"].sort()
  204. cname_str = ",".join(dev["aliases"])
  205. if cname_str == cnames:
  206. create_new = False
  207. if create_new:
  208. print(f"INFO: Deleting entry for {hname}")
  209. delete_netbox_device(enb, hname)
  210. add_netbox_device(enb, dev)
  211. else:
  212. # print("Not creating a new entry for {} as it already exists".format(dev["name"]))
  213. pass
  214. with open(CACHE_FILE, "w") as fd:
  215. json.dump(records, fd, indent=4)