setup-meraki-nets.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. #!/usr/bin/env python
  2. #
  3. # Copyright (c) 2018-2020 Joe Clarke <jclarke@cisco.com>
  4. #
  5. # Redistribution and use in source and binary forms, with or without
  6. # modification, are permitted provided that the following conditions
  7. # are met:
  8. # 1. Redistributions of source code must retain the above copyright
  9. # notice, this list of conditions and the following disclaimer.
  10. # 2. Redistributions in binary form must reproduce the above copyright
  11. # notice, this list of conditions and the following disclaimer in the
  12. # documentation and/or other materials provided with the distribution.
  13. #
  14. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  15. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  16. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  17. # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  18. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  19. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  20. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  21. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  22. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  23. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  24. # SUCH DAMAGE.
  25. from __future__ import print_function
  26. from builtins import input
  27. from meraki_api import Meraki, Network, Vlan, SwitchPort, SSID, Device
  28. import yaml
  29. import argparse
  30. import sys
  31. import os
  32. from colorama import Fore, Style
  33. import colorama
  34. BANNER = "[{}] **********************************************************"
  35. def main():
  36. parser = argparse.ArgumentParser(prog=sys.argv[0], description="Add devices to network")
  37. parser.add_argument("--config", "-c", metavar="<CONFIG FILE>", help="Path to the organization configuration file", required=True)
  38. parser.add_argument("--networks", "-n", metavar="<NETWORK>[,<NETWORK>[,...]]", help="Comma-separated list of networks to process")
  39. args = parser.parse_args()
  40. colorama.init()
  41. if not os.path.isfile(args.config):
  42. print("Config file {} does not exist or is not a file!".format(args.config))
  43. sys.exit(1)
  44. print(BANNER.format("Loading config file"))
  45. with open(args.config, "r") as c:
  46. config = yaml.load(c)
  47. print("{}ok{}\n".format(Fore.GREEN, Style.RESET_ALL))
  48. for key in ["api_key", "organization", "networks"]:
  49. if key not in config:
  50. print("Invalid config: {} is missing!".format(key))
  51. sys.exit(1)
  52. meraki = Meraki(key=config["api_key"])
  53. orgs = meraki.get_organizations()
  54. org = None
  55. for o in orgs:
  56. if o.get("name") == config["organization"]:
  57. org = o
  58. break
  59. if org is None:
  60. print("Failed to find organization {} in this profile!".format(config["organization"]))
  61. sys.exit(1)
  62. nets = org.get_networks()
  63. inv = org.get_inventory()
  64. errors = 0
  65. configure_nets = None
  66. if args.networks is not None:
  67. configure_nets = args.networks.split(",")
  68. for net in config["networks"]:
  69. nerrors = 0
  70. net_obj = None
  71. nname = list(net.keys())[0]
  72. print(BANNER.format("Configuring network {}".format(nname)))
  73. if configure_nets is not None and nname not in configure_nets:
  74. print("{}skipping (not in specified network list){}".format(Fore.BLUE, Style.RESET_ALL))
  75. continue
  76. validn = True
  77. for key in ["address", "timezone"]:
  78. if key not in net[nname]:
  79. print("{}Invalid network config for {}: {} is missing!{}".format(Fore.RED, nname, key, Style.RESET_ALL))
  80. errors += 1
  81. validn = False
  82. break
  83. if not validn:
  84. continue
  85. for n in nets:
  86. if n.get("name") == nname:
  87. net_obj = n
  88. break
  89. if net_obj is None:
  90. nargs = {"timezone": net[nname]["timezone"]}
  91. if "copy_from_network" in net[nname]:
  92. for n in nets:
  93. if n.get("name") == net[nname]["copy_from_network"]:
  94. nargs["copy_from_network_id"] = n.get("id")
  95. break
  96. net_obj = org.create_network(nname, **nargs)
  97. if net_obj is None:
  98. print("{}Error creating new network {}!{}".format(Fore.RED, nname, Style.RESET_ALL))
  99. errors += 1
  100. continue
  101. if "devices" in net[nname]:
  102. for dev in net[nname]["devices"]:
  103. serial = list(dev.keys())[0]
  104. if "name" not in dev[serial]:
  105. print("{}Invalid device {}: name is missing!{}".format(Fore.RED, serial, Style.RESET_ALL))
  106. nerrors += 1
  107. continue
  108. inv_dev = [device for device in inv if device["serial"] == serial]
  109. if len(inv_dev) == 1:
  110. dev_obj = None
  111. if inv_dev[0]["networkId"] is not None and inv_dev[0]["networkId"] != net_obj.get("id"):
  112. try:
  113. inv_net_obj = Network(key=config["api_key"], id=inv_dev[0]["networkId"])
  114. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=inv_net_obj)
  115. res = dev_obj.remove_device()
  116. if not res:
  117. print(
  118. "{}Error removing {} from network {}!{}".format(
  119. Fore.RED, inv_dev[0]["serial"], inv_dev[0]["networkId"], Style.RESET_ALL
  120. )
  121. )
  122. nerrors += 1
  123. continue
  124. print(
  125. "{}update: removed {} from network {}{}".format(
  126. Fore.YELLOW, inv_dev[0]["serial"], inv_dev[0]["networkId"], Style.RESET_ALL
  127. )
  128. )
  129. res = net_obj.claim_device(dev_obj)
  130. if not res:
  131. print("{}Error claiming {}!{}".format(Fore.RED, inv_dev[0]["serial"], Style.RESET_ALL))
  132. nerrors += 1
  133. continue
  134. print("{}update: claimed {}{}".format(Fore.YELLOW, inv_dev[0]["serial"], Style.RESET_ALL))
  135. dev_obj = res
  136. except Exception as e:
  137. print(
  138. "{}Error updating device network membership for {}: {}{}".format(
  139. Fore.RED, inv_dev[0]["serial"], e, Style.RESET_ALL
  140. )
  141. )
  142. nerrors += 1
  143. continue
  144. elif inv_dev[0]["networkId"] is None:
  145. try:
  146. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=net_obj)
  147. res = net_obj.claim_device(dev_obj)
  148. if not res:
  149. print("{}Error claiming device {}{}".format(Fore.RED, inv_dev[0]["serial"], Style.RESET_ALL))
  150. nerrors += 1
  151. continue
  152. print("{}update: claimed {}{}".format(Fore.YELLOW, inv_dev[0]["serial"], Style.RESET_ALL))
  153. dev_obj = res
  154. except Exception as e:
  155. print("{}Error claiming device {}: {}{}".format(Fore.RED, inv_dev[0]["serial"], e, Style.RESET_ALL))
  156. nerrors += 1
  157. continue
  158. else:
  159. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=net_obj)
  160. print("{}ok: {} is in network{}".format(Fore.GREEN, inv_dev[0]["serial"], Style.RESET_ALL))
  161. dev_location = net[nname]["address"]
  162. dev_name = dev[serial]["name"]
  163. if "location" in dev[serial]:
  164. dev_location += "\n" + dev[serial]["location"]
  165. dev_obj.update_device(name=dev_name, address=dev_location, move_map_marker=True)
  166. print("{}update: updated {} name and location{}".format(Fore.YELLOW, inv_dev[0]["serial"], Style.RESET_ALL))
  167. else:
  168. print("{}Error finding {} in inventory!{}".format(Fore.RED, serial, Style.RESET_ALL))
  169. nerrors += 1
  170. if "vlans" in net[nname]:
  171. # Ugh. There is no API to enable VLANs yet. So it's best to
  172. # make this a manual step. We could interact over the web, but
  173. # then we'd need to ask for a real user's credentials.
  174. #
  175. # If we copied from an existing network, then we assume that
  176. # network has VLANs enabled. If not, this will fail.
  177. #
  178. if "copy_from_network" not in net[nname]:
  179. print("\n")
  180. input(
  181. '!!! Enable VLANs for network "{}" manually in the dashboard (under Security appliance > Addressing & VLANs), then hit \
  182. enter to proceed !!!'.format(
  183. nname
  184. )
  185. )
  186. print("")
  187. for vlan in net[nname]["vlans"]:
  188. vname = list(vlan.keys())[0]
  189. done_msg = ""
  190. if int(vlan[vname]["id"]) != 1:
  191. vlan_obj = net_obj.create_vlan(vname, vlan[vname]["id"], vlan[vname]["subnet"], vlan[vname]["appliance_ip"])
  192. done_msg = "{}update: created VLAN {} (id={}, subnet={}, appliance_ip={}){}".format(
  193. Fore.YELLOW, vname, vlan[vname]["id"], vlan[vname]["subnet"], vlan[vname]["appliance_ip"], Style.RESET_ALL
  194. )
  195. else:
  196. vlan_obj = Vlan(key=config["api_key"], id=1, net=net_obj)
  197. done_msg = "{}ok: VLAN with ID {} exists{}".format(Fore.GREEN, vlan[vname]["id"], Style.RESET_ALL)
  198. if vlan_obj is None:
  199. print(
  200. "{}Error creating VLAN {} (id={}, subnet={}, appliance_ip={})!{}".format(
  201. Fore.RED, vname, vlan[vname]["id"], vlan[vname]["subnet"], vlan[vname]["appliance_ip"], Style.RESET_ALL
  202. )
  203. )
  204. nerrors += 1
  205. continue
  206. print(done_msg)
  207. vargs = {}
  208. for key in ["reserved_ip_ranges", "fixed_ip_assignments", "dns_nameservers"]:
  209. if key in vlan[vname]:
  210. vargs[key] = vlan[vname][key]
  211. res = vlan_obj.update_vlan(**vargs)
  212. vargs_str = ", ".join(["{}={}".format(k, v) for k, v in vargs.items()])
  213. if not res:
  214. print("{}Error updating VLAN {} ({})!{}".format(Fore.RED, vname, vargs_str, Style.RESET_ALL))
  215. nerrors += 1
  216. else:
  217. print("{}update: Update VLAN {} ({}){}".format(Fore.YELLOW, vname, vargs_str, Style.RESET_ALL))
  218. if "ssids" in net[nname]:
  219. if len(net[nname]["ssids"]) > 15:
  220. print("{}Only fifteen SSIDs are allowed per network!{}".format(Fore.RED, Style.RESET_ALL))
  221. nerrors += 1
  222. else:
  223. si = 0
  224. for ssid in net[nname]["ssids"]:
  225. sname = list(ssid.keys())[0]
  226. ssid_obj = SSID(key=config["api_key"], id=si, name=sname, net=net_obj)
  227. sargs = {}
  228. for key in ["name", "enabled", "auth_mode", "encryption_mode", "psk", "ip_assignment_mode"]:
  229. if key in ssid[sname]:
  230. sargs[key] = ssid[sname][key]
  231. res = ssid_obj.update_ssid(**sargs)
  232. sargs_str = ", ".join(["{}={}".format(k, v) for k, v in sargs.items()])
  233. if not res:
  234. print("{}Error updating SSID {} ({})!{}".format(Fore.RED, sname, sargs_str, Style.RESET_ALL))
  235. nerrors += 1
  236. else:
  237. print("{}update: Update SSID {} ({}){}".format(Fore.YELLOW, sname, sargs_str, Style.RESET_ALL))
  238. si += 1
  239. if "switches" in net[nname]:
  240. for switch in net[nname]["switches"]:
  241. serial = list(switch.keys())[0]
  242. dev_obj = Device(key=config["api_key"], id=serial, net=net_obj)
  243. if not dev_obj.realize():
  244. print("{}Device {} is not in network {}{}".format(Fore.RED, serial, net_obj.get("name"), Style.RESET_ALL))
  245. nerrors += 1
  246. continue
  247. for switchport in switch[serial]:
  248. port_range = list(switchport.keys())[0]
  249. ports = []
  250. if isinstance(port_range, int):
  251. port_obj = SwitchPort(key=config["api_key"], id=port_range, dev=dev_obj)
  252. ports.append(port_obj)
  253. else:
  254. prs = port_range.split(",")
  255. for pr in prs:
  256. pr = pr.strip()
  257. if isinstance(pr, int):
  258. port_obj = SwitchPort(key=config["api_key"], id=pr, dev=dev_obj)
  259. ports.append(pr)
  260. else:
  261. if "-" not in pr:
  262. print("{}Port range {} is invalid.{}".format(Fore.RED, pr, Style.RESET_ALL))
  263. nerrors += 1
  264. continue
  265. (start, end) = pr.split("-")
  266. start = start.strip()
  267. end = end.strip()
  268. if not isinstance(start, int) or not isinstance(end, int):
  269. print(
  270. "{}Error with port range '{}', {} and {} must be integers{}".format(
  271. Fore.RED, pr, start, end, Style.RESET_ALL
  272. )
  273. )
  274. nerrors += 1
  275. continue
  276. if start >= end:
  277. print("{}Error with port range {}; start must be less than end{}".format(Fore.RED, pr, Style.RESET_ALL))
  278. nerrors += 1
  279. continue
  280. pi = start
  281. while pi <= end:
  282. port_obj = SwitchPort(key=config["api_key"], id=pi, dev=dev_obj)
  283. ports.append(port_obj)
  284. pi += 1
  285. for port in ports:
  286. pargs = {}
  287. for key in ["name", "tags", "enabled", "type", "vlan", "voice_vlan", "allowed_vlans", "poe_enabled"]:
  288. if key in switchport[port_range]:
  289. pargs[key] = switchport[port_range][key]
  290. res = port.update_switchport(**pargs)
  291. pargs_str = ", ".join(["{}={}".format(k, v) for k, v in pargs.items()])
  292. if not res:
  293. print("{}Error updating switchport range {} ({}){}".format(Fore.RED, port_range, pargs_str, Style.RESET_ALL))
  294. nerrors += 1
  295. else:
  296. print("{}update: Update switchport range {} ({}){}".format(Fore.YELLOW, port_range, pargs_str, Style.RESET_ALL))
  297. if nerrors == 0:
  298. print("{}ok: network {} has been setup successfully!{}\n".format(Fore.GREEN, nname, Style.RESET_ALL))
  299. else:
  300. print(
  301. "{}Error fully configuring network {}. See the errors above for more details.{}\n".format(Fore.RED, nname, Style.RESET_ALL)
  302. )
  303. errors += nerrors
  304. if errors == 0:
  305. print("{}ok: all networks have been setup successfully!{}\n".format(Fore.GREEN, Style.RESET_ALL))
  306. else:
  307. print(
  308. "{}There were errors setting up some of the networks. See the output above for more details.{}\n".format(
  309. Fore.RED, Style.RESET_ALL
  310. )
  311. )
  312. sys.exit(1)
  313. if __name__ == "__main__":
  314. main()