setup_meraki_nets.py 17 KB

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