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. import meraki_api
  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_api.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 = meraki_api.Network(key=config["api_key"], id=inv_dev[0]["networkId"])
  114. dev_obj = meraki_api.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 = meraki_api.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 = meraki_api.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 = meraki_api.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 = meraki_api.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 = meraki_api.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 = meraki_api.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 = meraki_api.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 = meraki_api.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()