setup_meraki_nets.py 17 KB

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