setup-meraki-nets.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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 "client_limit" in network:
  101. if not isinstance(network["client_limit"], int) or network["client_limit"] < 0:
  102. print(
  103. "{}Client limit for network {} must be an integer greater than or equal to 0!{}".format(
  104. Fore.RED, nname, Style.RESET_ALL
  105. )
  106. )
  107. nerrors += 1
  108. continue
  109. shape_ret = net_obj.apply_shaping(int(network["client_limit"]))
  110. if shape_ret:
  111. print("{}update: added traffic shaping to network {}{}".format(Fore.YELLOW, nname, Style.RESET_ALL))
  112. else:
  113. print("{}Error adding shaping to network {}!{}".format(Fore.RED, nname, Style.RESET_ALL))
  114. nerrors += 1
  115. if net_obj is None:
  116. print("{}Error creating new network {}!{}".format(Fore.RED, nname, Style.RESET_ALL))
  117. errors += 1
  118. continue
  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 = {}
  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(name=dev_name, move_map_marker=True, **dparams)
  187. print("{}update: updated {} name and location{}".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. # Ugh. There is no API to enable VLANs yet. So it's best to
  193. # make this a manual step. We could interact over the web, but
  194. # then we'd need to ask for a real user's credentials.
  195. #
  196. # If we copied from an existing network, then we assume that
  197. # network has VLANs enabled. If not, this will fail.
  198. #
  199. if "copy_from_network" not in network:
  200. print("\n")
  201. input(
  202. '!!! Enable VLANs for network "{}" manually in the dashboard (under Security appliance > Addressing & VLANs), then hit \
  203. enter to proceed !!!'.format(
  204. nname
  205. )
  206. )
  207. print("")
  208. for vname, vlan in network["vlans"].items():
  209. done_msg = ""
  210. if int(vlan["id"]) != 1:
  211. vlan_obj = net_obj.create_vlan(vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"])
  212. done_msg = "{}update: created VLAN {} (id={}, subnet={}, appliance_ip={}){}".format(
  213. Fore.YELLOW, vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"], Style.RESET_ALL
  214. )
  215. else:
  216. vlan_obj = Vlan(key=config["api_key"], id=1, net=net_obj)
  217. done_msg = "{}ok: VLAN with ID {} exists{}".format(Fore.GREEN, vlan["id"], Style.RESET_ALL)
  218. if vlan_obj is None:
  219. print(
  220. "{}Error creating VLAN {} (id={}, subnet={}, appliance_ip={})!{}".format(
  221. Fore.RED, vname, vlan["id"], vlan["subnet"], vlan["appliance_ip"], Style.RESET_ALL
  222. )
  223. )
  224. nerrors += 1
  225. continue
  226. print(done_msg)
  227. vargs = {}
  228. for key in ["reserved_ip_ranges", "fixed_ip_assignments", "dns_nameservers"]:
  229. if key in vlan:
  230. vargs[key] = vlan[key]
  231. res = vlan_obj.update_vlan(**vargs)
  232. vargs_str = ", ".join(["{}={}".format(k, v) for k, v in vargs.items()])
  233. if not res:
  234. print("{}Error updating VLAN {} ({})!{}".format(Fore.RED, vname, vargs_str, Style.RESET_ALL))
  235. nerrors += 1
  236. else:
  237. print("{}update: Update VLAN {} ({}){}".format(Fore.YELLOW, vname, vargs_str, Style.RESET_ALL))
  238. if "ssids" in network:
  239. if len(network["ssids"]) > 15:
  240. print("{}Only fifteen SSIDs are allowed per network!{}".format(Fore.RED, Style.RESET_ALL))
  241. nerrors += 1
  242. else:
  243. si = 0
  244. for sname, ssid in network["ssids"].items():
  245. ssid_obj = SSID(key=config["api_key"], id=si, name=sname, net=net_obj)
  246. sargs = {}
  247. for key in ["name", "enabled", "auth_mode", "encryption_mode", "psk", "ip_assignment_mode", "tags"]:
  248. if key in ssid:
  249. sargs[key] = ssid[key]
  250. res = ssid_obj.update_ssid(**sargs)
  251. sargs_str = ", ".join(["{}={}".format(k, v) for k, v in sargs.items()])
  252. if not res:
  253. print("{}Error updating SSID {} ({})!{}".format(Fore.RED, sname, sargs_str, Style.RESET_ALL))
  254. nerrors += 1
  255. else:
  256. print("{}update: Update SSID {} ({}){}".format(Fore.YELLOW, sname, sargs_str, Style.RESET_ALL))
  257. si += 1
  258. if "switches" in network:
  259. for serial, switch in network["switches"].items():
  260. dev_obj = Device(key=config["api_key"], id=serial, net=net_obj)
  261. if not dev_obj.realize():
  262. print("{}Device {} is not in network {}{}".format(Fore.RED, serial, net_obj.get("name"), Style.RESET_ALL))
  263. nerrors += 1
  264. continue
  265. for port_range, switchport in switch.items():
  266. ports = []
  267. if isinstance(port_range, int):
  268. port_obj = SwitchPort(key=config["api_key"], id=port_range, dev=dev_obj)
  269. ports.append(port_obj)
  270. else:
  271. prs = port_range.split(",")
  272. for pr in prs:
  273. pr = pr.strip()
  274. if isinstance(pr, int):
  275. port_obj = SwitchPort(key=config["api_key"], id=pr, dev=dev_obj)
  276. ports.append(pr)
  277. else:
  278. if "-" not in pr:
  279. print("{}Port range {} is invalid.{}".format(Fore.RED, pr, Style.RESET_ALL))
  280. nerrors += 1
  281. continue
  282. (start, end) = pr.split("-")
  283. start = start.strip()
  284. end = end.strip()
  285. if not isinstance(start, int) or not isinstance(end, int):
  286. print(
  287. "{}Error with port range '{}', {} and {} must be integers{}".format(
  288. Fore.RED, pr, start, end, Style.RESET_ALL
  289. )
  290. )
  291. nerrors += 1
  292. continue
  293. if start >= end:
  294. print("{}Error with port range {}; start must be less than end{}".format(Fore.RED, pr, Style.RESET_ALL))
  295. nerrors += 1
  296. continue
  297. pi = start
  298. while pi <= end:
  299. port_obj = SwitchPort(key=config["api_key"], id=pi, dev=dev_obj)
  300. ports.append(port_obj)
  301. pi += 1
  302. for port in ports:
  303. pargs = {}
  304. for key in ["name", "tags", "enabled", "type", "vlan", "voice_vlan", "allowed_vlans", "poe_enabled"]:
  305. if key in switchport:
  306. pargs[key] = switchport[key]
  307. res = port.update_switchport(**pargs)
  308. pargs_str = ", ".join(["{}={}".format(k, v) for k, v in pargs.items()])
  309. if not res:
  310. print("{}Error updating switchport range {} ({}){}".format(Fore.RED, port_range, pargs_str, Style.RESET_ALL))
  311. nerrors += 1
  312. else:
  313. print("{}update: Update switchport range {} ({}){}".format(Fore.YELLOW, port_range, pargs_str, Style.RESET_ALL))
  314. if nerrors == 0:
  315. print("{}ok: network {} has been setup successfully!{}\n".format(Fore.GREEN, nname, Style.RESET_ALL))
  316. else:
  317. print(
  318. "{}Error fully configuring network {}. See the errors above for more details.{}\n".format(Fore.RED, nname, Style.RESET_ALL)
  319. )
  320. errors += nerrors
  321. if errors == 0:
  322. print("{}ok: all networks have been setup successfully!{}\n".format(Fore.GREEN, Style.RESET_ALL))
  323. else:
  324. print(
  325. "{}There were errors setting up some of the networks. See the output above for more details.{}\n".format(
  326. Fore.RED, Style.RESET_ALL
  327. )
  328. )
  329. sys.exit(1)
  330. if __name__ == "__main__":
  331. main()