setup-meraki-nets.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. try:
  35. from yaml import CLoader as Loader
  36. except ImportError:
  37. from yaml import Loader
  38. BANNER = "[{}] **********************************************************"
  39. def main():
  40. parser = argparse.ArgumentParser(prog=sys.argv[0], description="Add devices to network")
  41. parser.add_argument("--config", "-c", metavar="<CONFIG FILE>", help="Path to the organization configuration file", required=True)
  42. parser.add_argument("--networks", "-n", metavar="<NETWORK>[,<NETWORK>[,...]]", help="Comma-separated list of networks to process")
  43. args = parser.parse_args()
  44. colorama.init()
  45. if not os.path.isfile(args.config):
  46. print("Config file {} does not exist or is not a file!".format(args.config))
  47. sys.exit(1)
  48. print(BANNER.format("Loading config file"))
  49. with open(args.config, "r") as c:
  50. config = yaml.load(c, Loader=Loader)
  51. print("{}ok{}\n".format(Fore.GREEN, Style.RESET_ALL))
  52. for key in ["api_key", "organization", "networks"]:
  53. if key not in config:
  54. print("Invalid config: {} is missing!".format(key))
  55. sys.exit(1)
  56. meraki = Meraki(key=config["api_key"])
  57. orgs = meraki.get_organizations()
  58. org = None
  59. for o in orgs:
  60. if o.get("name") == config["organization"]:
  61. org = o
  62. break
  63. if org is None:
  64. print("Failed to find organization {} in this profile!".format(config["organization"]))
  65. sys.exit(1)
  66. nets = org.get_networks()
  67. inv = org.get_inventory()
  68. errors = 0
  69. configure_nets = None
  70. if args.networks is not None:
  71. configure_nets = args.networks.split(",")
  72. for nname, network in config["networks"].items():
  73. nerrors = 0
  74. net_obj = None
  75. print(BANNER.format("Configuring network {}".format(nname)))
  76. if configure_nets is not None and nname not in configure_nets:
  77. print("{}skipping (not in specified network list){}".format(Fore.BLUE, Style.RESET_ALL))
  78. continue
  79. validn = True
  80. for key in ["address", "timezone"]:
  81. if key not in network:
  82. print("{}Invalid network config for {}: {} is missing!{}".format(Fore.RED, nname, key, Style.RESET_ALL))
  83. errors += 1
  84. validn = False
  85. break
  86. if not validn:
  87. continue
  88. for n in nets:
  89. if n.get("name") == nname:
  90. net_obj = n
  91. break
  92. if net_obj is None:
  93. nargs = {"timezone": network["timezone"]}
  94. if "copy_from_network" in network:
  95. for n in nets:
  96. if n.get("name") == network["copy_from_network"]:
  97. nargs["copy_from_network_id"] = n.get("id")
  98. break
  99. net_obj = org.create_network(nname, **nargs)
  100. if net_obj is None:
  101. print("{}Error creating new network {}!{}".format(Fore.RED, nname, Style.RESET_ALL))
  102. errors += 1
  103. continue
  104. if "devices" in network:
  105. for serial, dev in network["devices"].items():
  106. if "name" not in dev:
  107. print("{}Invalid device {}: name is missing!{}".format(Fore.RED, serial, Style.RESET_ALL))
  108. nerrors += 1
  109. continue
  110. inv_dev = [device for device in inv if device["serial"] == serial]
  111. if len(inv_dev) == 1:
  112. dev_obj = None
  113. if inv_dev[0]["networkId"] is not None and inv_dev[0]["networkId"] != net_obj.get("id"):
  114. try:
  115. inv_net_obj = Network(key=config["api_key"], id=inv_dev[0]["networkId"])
  116. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=inv_net_obj)
  117. res = dev_obj.remove_device()
  118. if not res:
  119. print(
  120. "{}Error removing {} from network {}!{}".format(
  121. Fore.RED, inv_dev[0]["serial"], inv_dev[0]["networkId"], Style.RESET_ALL
  122. )
  123. )
  124. nerrors += 1
  125. continue
  126. print(
  127. "{}update: removed {} from network {}{}".format(
  128. Fore.YELLOW, inv_dev[0]["serial"], inv_dev[0]["networkId"], Style.RESET_ALL
  129. )
  130. )
  131. res = net_obj.claim_device(dev_obj)
  132. if not res:
  133. print("{}Error claiming {}!{}".format(Fore.RED, inv_dev[0]["serial"], Style.RESET_ALL))
  134. nerrors += 1
  135. continue
  136. print("{}update: claimed {}{}".format(Fore.YELLOW, inv_dev[0]["serial"], Style.RESET_ALL))
  137. dev_obj = res
  138. except Exception as e:
  139. print(
  140. "{}Error updating device network membership for {}: {}{}".format(
  141. Fore.RED, inv_dev[0]["serial"], e, Style.RESET_ALL
  142. )
  143. )
  144. nerrors += 1
  145. continue
  146. elif inv_dev[0]["networkId"] is None:
  147. try:
  148. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=net_obj)
  149. res = net_obj.claim_device(dev_obj)
  150. if not res:
  151. print("{}Error claiming device {}{}".format(Fore.RED, inv_dev[0]["serial"], Style.RESET_ALL))
  152. nerrors += 1
  153. continue
  154. print("{}update: claimed {}{}".format(Fore.YELLOW, inv_dev[0]["serial"], Style.RESET_ALL))
  155. dev_obj = res
  156. except Exception as e:
  157. print("{}Error claiming device {}: {}{}".format(Fore.RED, inv_dev[0]["serial"], e, Style.RESET_ALL))
  158. nerrors += 1
  159. continue
  160. else:
  161. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=net_obj)
  162. print("{}ok: {} is in network{}".format(Fore.GREEN, inv_dev[0]["serial"], Style.RESET_ALL))
  163. dev_location = network["address"]
  164. dev_name = dev["name"]
  165. if "location" in dev:
  166. dev_location += "\n" + dev["location"]
  167. dev_obj.update_device(name=dev_name, address=dev_location, move_map_marker=True)
  168. print("{}update: updated {} name and location{}".format(Fore.YELLOW, inv_dev[0]["serial"], Style.RESET_ALL))
  169. else:
  170. print("{}Error finding {} in inventory!{}".format(Fore.RED, serial, Style.RESET_ALL))
  171. nerrors += 1
  172. if "vlans" in network:
  173. # Ugh. There is no API to enable VLANs yet. So it's best to
  174. # make this a manual step. We could interact over the web, but
  175. # then we'd need to ask for a real user's credentials.
  176. #
  177. # If we copied from an existing network, then we assume that
  178. # network has VLANs enabled. If not, this will fail.
  179. #
  180. if "copy_from_network" not in network:
  181. print("\n")
  182. input(
  183. '!!! Enable VLANs for network "{}" manually in the dashboard (under Security appliance > Addressing & VLANs), then hit \
  184. enter to proceed !!!'.format(
  185. nname
  186. )
  187. )
  188. print("")
  189. for vname, vlan in network["vlans"].items():
  190. done_msg = ""
  191. if int(vlan["id"]) != 1:
  192. vlan_obj = net_obj.create_vlan(vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"])
  193. done_msg = "{}update: created VLAN {} (id={}, subnet={}, appliance_ip={}){}".format(
  194. Fore.YELLOW, vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"], Style.RESET_ALL
  195. )
  196. else:
  197. vlan_obj = Vlan(key=config["api_key"], id=1, net=net_obj)
  198. done_msg = "{}ok: VLAN with ID {} exists{}".format(Fore.GREEN, vlan["id"], Style.RESET_ALL)
  199. if vlan_obj is None:
  200. print(
  201. "{}Error creating VLAN {} (id={}, subnet={}, appliance_ip={})!{}".format(
  202. Fore.RED, vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"], Style.RESET_ALL
  203. )
  204. )
  205. nerrors += 1
  206. continue
  207. print(done_msg)
  208. vargs = {}
  209. for key in ["reserved_ip_ranges", "fixed_ip_assignments", "dns_nameservers"]:
  210. if key in vlan:
  211. vargs[key] = vlan[key]
  212. res = vlan_obj.update_vlan(**vargs)
  213. vargs_str = ", ".join(["{}={}".format(k, v) for k, v in vargs.items()])
  214. if not res:
  215. print("{}Error updating VLAN {} ({})!{}".format(Fore.RED, vname, vargs_str, Style.RESET_ALL))
  216. nerrors += 1
  217. else:
  218. print("{}update: Update VLAN {} ({}){}".format(Fore.YELLOW, vname, vargs_str, Style.RESET_ALL))
  219. if "ssids" in network:
  220. if len(network["ssids"]) > 15:
  221. print("{}Only fifteen SSIDs are allowed per network!{}".format(Fore.RED, Style.RESET_ALL))
  222. nerrors += 1
  223. else:
  224. si = 0
  225. for sname, ssid in network["ssids"].items():
  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:
  230. sargs[key] = ssid[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 network:
  240. for serial, switch in network["switches"].items():
  241. dev_obj = Device(key=config["api_key"], id=serial, net=net_obj)
  242. if not dev_obj.realize():
  243. print("{}Device {} is not in network {}{}".format(Fore.RED, serial, net_obj.get("name"), Style.RESET_ALL))
  244. nerrors += 1
  245. continue
  246. for port_range, switchport in switch.items():
  247. ports = []
  248. if isinstance(port_range, int):
  249. port_obj = SwitchPort(key=config["api_key"], id=port_range, dev=dev_obj)
  250. ports.append(port_obj)
  251. else:
  252. prs = port_range.split(",")
  253. for pr in prs:
  254. pr = pr.strip()
  255. if isinstance(pr, int):
  256. port_obj = SwitchPort(key=config["api_key"], id=pr, dev=dev_obj)
  257. ports.append(pr)
  258. else:
  259. if "-" not in pr:
  260. print("{}Port range {} is invalid.{}".format(Fore.RED, pr, Style.RESET_ALL))
  261. nerrors += 1
  262. continue
  263. (start, end) = pr.split("-")
  264. start = start.strip()
  265. end = end.strip()
  266. if not isinstance(start, int) or not isinstance(end, int):
  267. print(
  268. "{}Error with port range '{}', {} and {} must be integers{}".format(
  269. Fore.RED, pr, start, end, Style.RESET_ALL
  270. )
  271. )
  272. nerrors += 1
  273. continue
  274. if start >= end:
  275. print("{}Error with port range {}; start must be less than end{}".format(Fore.RED, pr, Style.RESET_ALL))
  276. nerrors += 1
  277. continue
  278. pi = start
  279. while pi <= end:
  280. port_obj = SwitchPort(key=config["api_key"], id=pi, dev=dev_obj)
  281. ports.append(port_obj)
  282. pi += 1
  283. for port in ports:
  284. pargs = {}
  285. for key in ["name", "tags", "enabled", "type", "vlan", "voice_vlan", "allowed_vlans", "poe_enabled"]:
  286. if key in switchport:
  287. pargs[key] = switchport[key]
  288. res = port.update_switchport(**pargs)
  289. pargs_str = ", ".join(["{}={}".format(k, v) for k, v in pargs.items()])
  290. if not res:
  291. print("{}Error updating switchport range {} ({}){}".format(Fore.RED, port_range, pargs_str, Style.RESET_ALL))
  292. nerrors += 1
  293. else:
  294. print("{}update: Update switchport range {} ({}){}".format(Fore.YELLOW, port_range, pargs_str, Style.RESET_ALL))
  295. if nerrors == 0:
  296. print("{}ok: network {} has been setup successfully!{}\n".format(Fore.GREEN, nname, Style.RESET_ALL))
  297. else:
  298. print(
  299. "{}Error fully configuring network {}. See the errors above for more details.{}\n".format(Fore.RED, nname, Style.RESET_ALL)
  300. )
  301. errors += nerrors
  302. if errors == 0:
  303. print("{}ok: all networks have been setup successfully!{}\n".format(Fore.GREEN, Style.RESET_ALL))
  304. else:
  305. print(
  306. "{}There were errors setting up some of the networks. See the output above for more details.{}\n".format(
  307. Fore.RED, Style.RESET_ALL
  308. )
  309. )
  310. sys.exit(1)
  311. if __name__ == "__main__":
  312. main()