setup-meraki-nets.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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. parser.add_argument("--test", help="Test command", action=store_true)
  44. args = parser.parse_args()
  45. colorama.init()
  46. if not os.path.isfile(args.config):
  47. print("Config file {} does not exist or is not a file!".format(args.config))
  48. exit(1)
  49. print(BANNER.format("Loading config file"))
  50. with open(args.config, "r") as c:
  51. config = yaml.load(c, Loader=Loader)
  52. print("{}ok{}\n".format(Fore.GREEN, Style.RESET_ALL))
  53. for key in ["api_key", "organization", "networks"]:
  54. if key not in config:
  55. print("Invalid config: {} is missing!".format(key))
  56. exit(1)
  57. meraki = Meraki(key=config["api_key"])
  58. orgs = meraki.get_organizations()
  59. org = None
  60. for o in orgs:
  61. if o.get("name") == config["organization"]:
  62. org = o
  63. break
  64. if org is None:
  65. print("Failed to find organization {} in this profile!".format(config["organization"]))
  66. exit(1)
  67. nets = org.get_networks()
  68. inv = org.get_inventory()
  69. errors = 0
  70. configure_nets = None
  71. if args.networks is not None:
  72. configure_nets = args.networks.split(",")
  73. for nname, network in config["networks"].items():
  74. nerrors = 0
  75. net_obj = None
  76. print(BANNER.format("Configuring network {}".format(nname)))
  77. if configure_nets is not None and nname not in configure_nets:
  78. print("{}skipping (not in specified network list){}".format(Fore.BLUE, Style.RESET_ALL))
  79. continue
  80. validn = True
  81. for key in ["address", "timezone"]:
  82. if key not in network:
  83. print("{}Invalid network config for {}: {} is missing!{}".format(Fore.RED, nname, key, Style.RESET_ALL))
  84. errors += 1
  85. validn = False
  86. break
  87. if not validn:
  88. continue
  89. for n in nets:
  90. if n.get("name") == nname:
  91. net_obj = n
  92. break
  93. if net_obj is None:
  94. nargs = {"timezone": network["timezone"]}
  95. if "copy_from_network" in network:
  96. for n in nets:
  97. if n.get("name") == network["copy_from_network"]:
  98. nargs["copy_from_network_id"] = n.get("id")
  99. break
  100. net_obj = org.create_network(nname, **nargs)
  101. if net_obj is None:
  102. print("{}Error creating new network {}!{}".format(Fore.RED, nname, Style.RESET_ALL))
  103. errors += 1
  104. continue
  105. if "client_bandwidth_limit" in network:
  106. if not isinstance(network["client_bandwidth_limit"], int) or network["client_bandwidth_limit"] < 0:
  107. print(
  108. "{}Client bandwidth limit for network {} must be an integer greater than or equal to 0!{}".format(
  109. Fore.RED, nname, Style.RESET_ALL
  110. )
  111. )
  112. nerrors += 1
  113. continue
  114. shape_ret = net_obj.apply_shaping(int(network["client_bandwidth_limit"]))
  115. if shape_ret:
  116. print("{}update: added traffic shaping to network {}{}".format(Fore.YELLOW, nname, Style.RESET_ALL))
  117. else:
  118. print("{}Error adding shaping to network {}!{}".format(Fore.RED, nname, Style.RESET_ALL))
  119. nerrors += 1
  120. if "devices" in network:
  121. for serial, dev in network["devices"].items():
  122. if "name" not in dev:
  123. print("{}Invalid device {}: name is missing!{}".format(Fore.RED, serial, Style.RESET_ALL))
  124. nerrors += 1
  125. continue
  126. inv_dev = [device for device in inv if device["serial"] == serial]
  127. if len(inv_dev) == 1:
  128. dev_obj = None
  129. if inv_dev[0]["networkId"] is not None and inv_dev[0]["networkId"] != net_obj.get("id"):
  130. try:
  131. inv_net_obj = Network(key=config["api_key"], id=inv_dev[0]["networkId"])
  132. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=inv_net_obj)
  133. res = dev_obj.remove_device()
  134. if not res:
  135. print(
  136. "{}Error removing {} from network {}!{}".format(
  137. Fore.RED, inv_dev[0]["serial"], inv_dev[0]["networkId"], Style.RESET_ALL
  138. )
  139. )
  140. nerrors += 1
  141. continue
  142. print(
  143. "{}update: removed {} from network {}{}".format(
  144. Fore.YELLOW, inv_dev[0]["serial"], inv_dev[0]["networkId"], Style.RESET_ALL
  145. )
  146. )
  147. res = net_obj.claim_device(dev_obj)
  148. if not res:
  149. print("{}Error claiming {}!{}".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(
  156. "{}Error updating device network membership for {}: {}{}".format(
  157. Fore.RED, inv_dev[0]["serial"], e, Style.RESET_ALL
  158. )
  159. )
  160. nerrors += 1
  161. continue
  162. elif inv_dev[0]["networkId"] is None:
  163. try:
  164. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=net_obj)
  165. res = net_obj.claim_device(dev_obj)
  166. if not res:
  167. print("{}Error claiming device {}{}".format(Fore.RED, inv_dev[0]["serial"], Style.RESET_ALL))
  168. nerrors += 1
  169. continue
  170. print("{}update: claimed {}{}".format(Fore.YELLOW, inv_dev[0]["serial"], Style.RESET_ALL))
  171. dev_obj = res
  172. except Exception as e:
  173. print("{}Error claiming device {}: {}{}".format(Fore.RED, inv_dev[0]["serial"], e, Style.RESET_ALL))
  174. nerrors += 1
  175. continue
  176. else:
  177. dev_obj = Device(key=config["api_key"], id=inv_dev[0]["serial"], net=net_obj)
  178. print("{}ok: {} is in network{}".format(Fore.GREEN, inv_dev[0]["serial"], Style.RESET_ALL))
  179. dev_location = network["address"]
  180. dev_name = dev["name"]
  181. dparams = {"name": dev_name, "move_map_marker": True}
  182. if "location" in dev:
  183. dev_location += "\n" + dev["location"]
  184. dparams["address"] = dev_location
  185. if "tags" in dev:
  186. dparams["tags"] = dev["tags"]
  187. dev_obj.update_device(**dparams)
  188. print("{}update: updated {} parameters{}".format(Fore.YELLOW, inv_dev[0]["serial"], Style.RESET_ALL))
  189. else:
  190. print("{}Error finding {} in inventory!{}".format(Fore.RED, serial, Style.RESET_ALL))
  191. nerrors += 1
  192. if "vlans" in network:
  193. vres = net_obj.enable_vlans()
  194. if not vres:
  195. print("{}Error enabling VLANs for network {}!{}".format(Fore.RED, nname, Style.RESET_ALL))
  196. nerrors += 1
  197. continue
  198. else:
  199. print("{}update: enabled VLANs for network {}{}".format(Fore.YELLOW, nname, Style.RESET_ALL))
  200. for vname, vlan in network["vlans"].items():
  201. done_msg = ""
  202. if int(vlan["id"]) != 1:
  203. vlan_obj = net_obj.create_vlan(vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"])
  204. done_msg = "{}update: created VLAN {} (id={}, subnet={}, appliance_ip={}){}".format(
  205. Fore.YELLOW, vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"], Style.RESET_ALL
  206. )
  207. else:
  208. vlan_obj = Vlan(key=config["api_key"], id=1, net=net_obj)
  209. done_msg = "{}ok: VLAN with ID {} exists{}".format(Fore.GREEN, vlan["id"], Style.RESET_ALL)
  210. if vlan_obj is None:
  211. print(
  212. "{}Error creating VLAN {} (id={}, subnet={}, appliance_ip={})!{}".format(
  213. Fore.RED, vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"], Style.RESET_ALL
  214. )
  215. )
  216. nerrors += 1
  217. continue
  218. print(done_msg)
  219. vargs = {}
  220. for key in ["reserved_ip_ranges", "fixed_ip_assignments", "dns_nameservers"]:
  221. if key in vlan:
  222. vargs[key] = vlan[key]
  223. res = vlan_obj.update_vlan(**vargs)
  224. vargs_str = ", ".join(["{}={}".format(k, v) for k, v in vargs.items()])
  225. if not res:
  226. print("{}Error updating VLAN {} ({})!{}".format(Fore.RED, vname, vargs_str, Style.RESET_ALL))
  227. nerrors += 1
  228. else:
  229. print("{}update: Update VLAN {} ({}){}".format(Fore.YELLOW, vname, vargs_str, Style.RESET_ALL))
  230. if "ssids" in network:
  231. if len(network["ssids"].keys()) > 15:
  232. print("{}Only fifteen SSIDs are allowed per network!{}".format(Fore.RED, Style.RESET_ALL))
  233. nerrors += 1
  234. else:
  235. si = 0
  236. for sname, ssid in network["ssids"].items():
  237. ssid_obj = SSID(key=config["api_key"], id=si, name=sname, net=net_obj)
  238. sargs = {}
  239. for key in ["name", "enabled", "auth_mode", "encryption_mode", "psk", "ip_assignment_mode", "tags"]:
  240. if key in ssid:
  241. sargs[key] = ssid[key]
  242. res = ssid_obj.update_ssid(**sargs)
  243. sargs_str = ", ".join(["{}={}".format(k, v) for k, v in sargs.items()])
  244. if not res:
  245. print("{}Error updating SSID {} ({})!{}".format(Fore.RED, sname, sargs_str, Style.RESET_ALL))
  246. nerrors += 1
  247. else:
  248. print("{}update: Update SSID {} ({}){}".format(Fore.YELLOW, sname, sargs_str, Style.RESET_ALL))
  249. if "allow_lan_access" in ssid and ssid["allow_lan_access"]:
  250. sres = ssid_obj.allow_local_lan()
  251. if not sres:
  252. print("{}Error allowing local LAN access for SSID {}!{}".format(Fore.RED, sname, Style.RESET_ALL))
  253. nerrors += 1
  254. else:
  255. print("{}update: Allowing local LAN access for SSID {}{}".format(Fore.YELLOW, sname, Style.RESET_ALL))
  256. si += 1
  257. if "switches" in network:
  258. for serial, switch in network["switches"].items():
  259. dev_obj = Device(key=config["api_key"], id=serial, net=net_obj)
  260. if not dev_obj.realize():
  261. print("{}Device {} is not in network {}{}".format(Fore.RED, serial, net_obj.get("name"), Style.RESET_ALL))
  262. nerrors += 1
  263. continue
  264. for port_range, switchport in switch.items():
  265. ports = []
  266. if isinstance(port_range, int) or port_range.isnumeric():
  267. port_obj = SwitchPort(key=config["api_key"], id=port_range, dev=dev_obj)
  268. ports.append(port_obj)
  269. else:
  270. prs = port_range.split(",")
  271. for pr in prs:
  272. pr = pr.strip()
  273. if pr.isnumeric():
  274. port_obj = SwitchPort(key=config["api_key"], id=pr, dev=dev_obj)
  275. ports.append(pr)
  276. else:
  277. if "-" not in pr:
  278. print("{}Port range {} is invalid.{}".format(Fore.RED, pr, Style.RESET_ALL))
  279. nerrors += 1
  280. continue
  281. (start, end) = pr.split("-")
  282. start = start.strip()
  283. end = end.strip()
  284. if not start.isnumeric() or not end.isnumeric():
  285. print(
  286. "{}Error with port range '{}', {} and {} must be integers{}".format(
  287. Fore.RED, pr, start, end, Style.RESET_ALL
  288. )
  289. )
  290. nerrors += 1
  291. continue
  292. if start >= end:
  293. print("{}Error with port range {}; start must be less than end{}".format(Fore.RED, pr, Style.RESET_ALL))
  294. nerrors += 1
  295. continue
  296. pi = start
  297. while pi <= end:
  298. port_obj = SwitchPort(key=config["api_key"], id=pi, dev=dev_obj)
  299. ports.append(port_obj)
  300. pi += 1
  301. for port in ports:
  302. pargs = {}
  303. for key in [
  304. "name",
  305. "tags",
  306. "enabled",
  307. "type",
  308. "vlan",
  309. "voice_vlan",
  310. "allowed_vlans",
  311. "rstp_enabled",
  312. "poe_enabled",
  313. ]:
  314. if key in switchport:
  315. pargs[key] = switchport[key]
  316. res = port.update_switchport(**pargs)
  317. pargs_str = ", ".join(["{}={}".format(k, v) for k, v in pargs.items()])
  318. if not res:
  319. print("{}Error updating switchport range {} ({}){}".format(Fore.RED, port_range, pargs_str, Style.RESET_ALL))
  320. nerrors += 1
  321. else:
  322. print("{}update: Update switchport range {} ({}){}".format(Fore.YELLOW, port_range, pargs_str, Style.RESET_ALL))
  323. if nerrors == 0:
  324. print("{}ok: network {} has been setup successfully!{}\n".format(Fore.GREEN, nname, Style.RESET_ALL))
  325. else:
  326. print(
  327. "{}Error fully configuring network {}. See the errors above for more details.{}\n".format(Fore.RED, nname, Style.RESET_ALL)
  328. )
  329. errors += nerrors
  330. if errors == 0:
  331. print("{}ok: all networks have been setup successfully!{}\n".format(Fore.GREEN, Style.RESET_ALL))
  332. else:
  333. print(
  334. "{}There were errors setting up some of the networks. See the output above for more details.{}\n".format(
  335. Fore.RED, Style.RESET_ALL
  336. )
  337. )
  338. exit(1)
  339. if __name__ == "__main__":
  340. main()