setup-meraki-nets.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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 meraki_api import Meraki, Network, Vlan, SwitchPort, SSID, Device
  27. import yaml
  28. import argparse
  29. import sys
  30. import os
  31. from colorama import Fore, Style
  32. import colorama
  33. try:
  34. from yaml import CLoader as Loader
  35. except ImportError:
  36. from yaml import Loader
  37. BANNER = "[{}] **********************************************************"
  38. def main():
  39. # Setup command line arguments.
  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. 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. 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. 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 "client_bandwidth_limit" in network:
  105. if not isinstance(network["client_bandwidth_limit"], int) or network["client_bandwidth_limit"] < 0:
  106. print(
  107. "{}Client bandwidth limit for network {} must be an integer greater than or equal to 0!{}".format(
  108. Fore.RED, nname, Style.RESET_ALL
  109. )
  110. )
  111. nerrors += 1
  112. continue
  113. shape_ret = net_obj.apply_shaping(int(network["client_bandwidth_limit"]))
  114. if shape_ret:
  115. print("{}update: added traffic shaping to network {}{}".format(Fore.YELLOW, nname, Style.RESET_ALL))
  116. else:
  117. print("{}Error adding shaping to network {}!{}".format(Fore.RED, nname, Style.RESET_ALL))
  118. nerrors += 1
  119. if "devices" in network:
  120. for serial, dev in network["devices"].items():
  121. if "name" not in dev:
  122. print("{}Invalid device {}: name is missing!{}".format(Fore.RED, serial, Style.RESET_ALL))
  123. nerrors += 1
  124. continue
  125. inv_dev = [device for device in inv if device["serial"] == serial]
  126. if len(inv_dev) == 1:
  127. dev_obj = None
  128. if inv_dev[0]["networkId"] is not None and inv_dev[0]["networkId"] != net_obj.get("id"):
  129. try:
  130. inv_net_obj = Network(key=config["api_key"], id=inv_dev[0]["networkId"])
  131. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=inv_net_obj)
  132. res = dev_obj.remove_device()
  133. if not res:
  134. print(
  135. "{}Error removing {} from network {}!{}".format(
  136. Fore.RED, inv_dev[0]["serial"], inv_dev[0]["networkId"], Style.RESET_ALL
  137. )
  138. )
  139. nerrors += 1
  140. continue
  141. print(
  142. "{}update: removed {} from network {}{}".format(
  143. Fore.YELLOW, inv_dev[0]["serial"], inv_dev[0]["networkId"], Style.RESET_ALL
  144. )
  145. )
  146. res = net_obj.claim_device(dev_obj)
  147. if not res:
  148. print("{}Error claiming {}!{}".format(Fore.RED, inv_dev[0]["serial"], Style.RESET_ALL))
  149. nerrors += 1
  150. continue
  151. print("{}update: claimed {}{}".format(Fore.YELLOW, inv_dev[0]["serial"], Style.RESET_ALL))
  152. dev_obj = res
  153. except Exception as e:
  154. print(
  155. "{}Error updating device network membership for {}: {}{}".format(
  156. Fore.RED, inv_dev[0]["serial"], e, Style.RESET_ALL
  157. )
  158. )
  159. nerrors += 1
  160. continue
  161. elif inv_dev[0]["networkId"] is None:
  162. try:
  163. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=net_obj)
  164. res = net_obj.claim_device(dev_obj)
  165. if not res:
  166. print("{}Error claiming device {}{}".format(Fore.RED, inv_dev[0]["serial"], Style.RESET_ALL))
  167. nerrors += 1
  168. continue
  169. print("{}update: claimed {}{}".format(Fore.YELLOW, inv_dev[0]["serial"], Style.RESET_ALL))
  170. dev_obj = res
  171. except Exception as e:
  172. print("{}Error claiming device {}: {}{}".format(Fore.RED, inv_dev[0]["serial"], e, Style.RESET_ALL))
  173. nerrors += 1
  174. continue
  175. else:
  176. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=net_obj)
  177. print("{}ok: {} is in network{}".format(Fore.GREEN, inv_dev[0]["serial"], Style.RESET_ALL))
  178. dev_location = network["address"]
  179. dev_name = dev["name"]
  180. dparams = {"name": dev_name, "move_map_marker": True}
  181. if "location" in dev:
  182. dev_location += "\n" + dev["location"]
  183. dparams["address"] = dev_location
  184. if "tags" in dev:
  185. dparams["tags"] = dev["tags"]
  186. dev_obj.update_device(**dparams)
  187. print("{}update: updated {} parameters{}".format(Fore.YELLOW, inv_dev[0]["serial"], Style.RESET_ALL))
  188. else:
  189. print("{}Error finding {} in inventory!{}".format(Fore.RED, serial, Style.RESET_ALL))
  190. nerrors += 1
  191. if "vlans" in network:
  192. vres = net_obj.enable_vlans()
  193. if not vres:
  194. print("{}Error enabling VLANs for network {}!{}".format(Fore.RED, nname, Style.RESET_ALL))
  195. nerrors += 1
  196. continue
  197. else:
  198. print("{}update: enabled VLANs for network {}{}".format(Fore.YELLOW, nname, Style.RESET_ALL))
  199. for vname, vlan in network["vlans"].items():
  200. done_msg = ""
  201. if int(vlan["id"]) != 1:
  202. vlan_obj = net_obj.create_vlan(vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"])
  203. done_msg = "{}update: created VLAN {} (id={}, subnet={}, appliance_ip={}){}".format(
  204. Fore.YELLOW, vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"], Style.RESET_ALL
  205. )
  206. else:
  207. vlan_obj = Vlan(key=config["api_key"], id=1, net=net_obj)
  208. done_msg = "{}ok: VLAN with ID {} exists{}".format(Fore.GREEN, vlan["id"], Style.RESET_ALL)
  209. if vlan_obj is None:
  210. print(
  211. "{}Error creating VLAN {} (id={}, subnet={}, appliance_ip={})!{}".format(
  212. Fore.RED, vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"], Style.RESET_ALL
  213. )
  214. )
  215. nerrors += 1
  216. continue
  217. print(done_msg)
  218. vargs = {}
  219. for key in ["reserved_ip_ranges", "fixed_ip_assignments", "dns_nameservers"]:
  220. if key in vlan:
  221. vargs[key] = vlan[key]
  222. res = vlan_obj.update_vlan(**vargs)
  223. vargs_str = ", ".join(["{}={}".format(k, v) for k, v in vargs.items()])
  224. if not res:
  225. print("{}Error updating VLAN {} ({})!{}".format(Fore.RED, vname, vargs_str, Style.RESET_ALL))
  226. nerrors += 1
  227. else:
  228. print("{}update: Update VLAN {} ({}){}".format(Fore.YELLOW, vname, vargs_str, Style.RESET_ALL))
  229. if "ssids" in network:
  230. if len(network["ssids"].keys()) > 15:
  231. print("{}Only fifteen SSIDs are allowed per network!{}".format(Fore.RED, Style.RESET_ALL))
  232. nerrors += 1
  233. else:
  234. si = 0
  235. for sname, ssid in network["ssids"].items():
  236. ssid_obj = SSID(key=config["api_key"], id=si, name=sname, net=net_obj)
  237. sargs = {}
  238. for key in ["name", "enabled", "auth_mode", "encryption_mode", "psk", "ip_assignment_mode", "tags"]:
  239. if key in ssid:
  240. sargs[key] = ssid[key]
  241. res = ssid_obj.update_ssid(**sargs)
  242. sargs_str = ", ".join(["{}={}".format(k, v) for k, v in sargs.items()])
  243. if not res:
  244. print("{}Error updating SSID {} ({})!{}".format(Fore.RED, sname, sargs_str, Style.RESET_ALL))
  245. nerrors += 1
  246. else:
  247. print("{}update: Update SSID {} ({}){}".format(Fore.YELLOW, sname, sargs_str, Style.RESET_ALL))
  248. if "allow_lan_access" in ssid and ssid["allow_lan_access"]:
  249. sres = ssid_obj.allow_local_lan()
  250. if not sres:
  251. print("{}Error allowing local LAN access for SSID {}!{}".format(Fore.RED, sname, Style.RESET_ALL))
  252. nerrors += 1
  253. else:
  254. print("{}update: Allowing local LAN access for SSID {}{}".format(Fore.YELLOW, sname, Style.RESET_ALL))
  255. si += 1
  256. if "switches" in network:
  257. for serial, switch in network["switches"].items():
  258. dev_obj = Device(key=config["api_key"], id=serial, net=net_obj)
  259. if not dev_obj.realize():
  260. print("{}Device {} is not in network {}{}".format(Fore.RED, serial, net_obj.get("name"), Style.RESET_ALL))
  261. nerrors += 1
  262. continue
  263. for port_range, switchport in switch.items():
  264. ports = []
  265. if isinstance(port_range, int) or port_range.isnumeric():
  266. port_obj = SwitchPort(key=config["api_key"], id=port_range, dev=dev_obj)
  267. ports.append(port_obj)
  268. else:
  269. prs = port_range.split(",")
  270. for pr in prs:
  271. pr = pr.strip()
  272. if pr.isnumeric():
  273. port_obj = SwitchPort(key=config["api_key"], id=pr, dev=dev_obj)
  274. ports.append(pr)
  275. else:
  276. if "-" not in pr:
  277. print("{}Port range {} is invalid.{}".format(Fore.RED, pr, Style.RESET_ALL))
  278. nerrors += 1
  279. continue
  280. (start, end) = pr.split("-")
  281. start = start.strip()
  282. end = end.strip()
  283. if not start.isnumeric() or not end.isnumeric():
  284. print(
  285. "{}Error with port range '{}', {} and {} must be integers{}".format(
  286. Fore.RED, pr, start, end, Style.RESET_ALL
  287. )
  288. )
  289. nerrors += 1
  290. continue
  291. if start >= end:
  292. print("{}Error with port range {}; start must be less than end{}".format(Fore.RED, pr, Style.RESET_ALL))
  293. nerrors += 1
  294. continue
  295. pi = start
  296. while pi <= end:
  297. port_obj = SwitchPort(key=config["api_key"], id=pi, dev=dev_obj)
  298. ports.append(port_obj)
  299. pi += 1
  300. for port in ports:
  301. pargs = {}
  302. for key in [
  303. "name",
  304. "tags",
  305. "enabled",
  306. "type",
  307. "vlan",
  308. "voice_vlan",
  309. "allowed_vlans",
  310. "rstp_enabled",
  311. "poe_enabled",
  312. ]:
  313. if key in switchport:
  314. pargs[key] = switchport[key]
  315. res = port.update_switchport(**pargs)
  316. pargs_str = ", ".join(["{}={}".format(k, v) for k, v in pargs.items()])
  317. if not res:
  318. print("{}Error updating switchport range {} ({}){}".format(Fore.RED, port_range, pargs_str, Style.RESET_ALL))
  319. nerrors += 1
  320. else:
  321. print("{}update: Update switchport range {} ({}){}".format(Fore.YELLOW, port_range, pargs_str, Style.RESET_ALL))
  322. if nerrors == 0:
  323. print("{}ok: network {} has been setup successfully!{}\n".format(Fore.GREEN, nname, Style.RESET_ALL))
  324. else:
  325. print(
  326. "{}Error fully configuring network {}. See the errors above for more details.{}\n".format(Fore.RED, nname, Style.RESET_ALL)
  327. )
  328. errors += nerrors
  329. if errors == 0:
  330. print("{}ok: all networks have been setup successfully!{}\n".format(Fore.GREEN, Style.RESET_ALL))
  331. else:
  332. print(
  333. "{}There were errors setting up some of the networks. See the output above for more details.{}\n".format(
  334. Fore.RED, Style.RESET_ALL
  335. )
  336. )
  337. exit(1)
  338. if __name__ == "__main__":
  339. main()