__init__.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. #
  2. # Copyright (c) 2018 Joe Clarke <jclarke@cisco.com>
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions
  6. # are met:
  7. # 1. Redistributions of source code must retain the above copyright
  8. # notice, this list of conditions and the following disclaimer.
  9. # 2. Redistributions in binary form must reproduce the above copyright
  10. # notice, this list of conditions and the following disclaimer in the
  11. # documentation and/or other materials provided with the distribution.
  12. #
  13. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  14. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  15. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  16. # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  17. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  18. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  19. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  20. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  21. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  22. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  23. # SUCH DAMAGE.
  24. import requests
  25. import logging
  26. import json
  27. class Meraki(object):
  28. MERAKI_API = 'https://api.meraki.com/api/v0'
  29. _headers = {
  30. 'X-Cisco-Meraki-API-Key': None,
  31. 'Content-type': 'application/json'
  32. }
  33. _logit = False
  34. _key = None
  35. _name = None
  36. _id = None
  37. _class_name = 'Meraki'
  38. _name_prop = 'name'
  39. _id_prop = 'id'
  40. _api_url_endpoint = ''
  41. def __init__(self, **kwargs):
  42. if 'logit' in kwargs:
  43. self._logit = kwargs['logit']
  44. if 'key' in kwargs:
  45. self._key = kwargs['key']
  46. self._headers['X-Cisco-Meraki-API-Key'] = kwargs['key']
  47. if 'name' in kwargs:
  48. self._name = kwargs['name']
  49. if 'id' in kwargs:
  50. self._id = str(kwargs['id'])
  51. self.__dict = {}
  52. self._initialized = False
  53. def set_key(self, key):
  54. self._key = key
  55. self._headers['X-Cisco-Meraki-API-Key'] = key
  56. def set_name(self, name):
  57. self._name = name
  58. def _set_id(self, id):
  59. self._id = id
  60. def __check_headers(self):
  61. if self._headers['X-Cisco-Meraki-API-Key'] is None:
  62. msg = 'Meraki API key is not set!'
  63. if self._logit:
  64. logging.error(msg)
  65. else:
  66. print(msg)
  67. return False
  68. return True
  69. def realize(self):
  70. return self._check_obj()
  71. @staticmethod
  72. def _get_json_errors(response):
  73. res = ''
  74. try:
  75. jobj = response.json()
  76. if 'errors' in jobj:
  77. res = ', '.join(jobj['errors'])
  78. except:
  79. pass
  80. return res
  81. def _check_obj(self):
  82. if self._initialized:
  83. return True
  84. if self._id is None:
  85. msg = '{} id is not set!'.format(self._class_name)
  86. if self._logit:
  87. logging.error(msg)
  88. else:
  89. print(msg)
  90. return False
  91. return self.__get_current_obj()
  92. def __get_current_obj(self):
  93. if not self.__check_headers():
  94. return False
  95. url = self.MERAKI_API + self._api_url_endpoint + '/' + self._id
  96. try:
  97. response = requests.request('GET', url, headers=self._headers)
  98. response.raise_for_status()
  99. except Exception as e:
  100. msg = 'Error getting {} for {}: {} ({})'.format(
  101. self._class_name.lower(), self._id, e, Meraki._get_json_errors(response))
  102. if self._logit:
  103. logging.error(msg)
  104. else:
  105. print(msg)
  106. return False
  107. jobj = response.json()
  108. self.__populate_obj(jobj)
  109. # Object may not have a name yet.
  110. if self._name_prop in jobj:
  111. self._name = jobj[self._name_prop]
  112. self._initialized = True
  113. return True
  114. def __populate_obj(self, jobj):
  115. for k, v in jobj.items():
  116. self.__dict[k] = v
  117. def get(self, field):
  118. if not self._check_obj():
  119. raise Exception('Failed to initialize object!')
  120. if field in self.__dict:
  121. return self.__dict[field]
  122. else:
  123. raise Exception('Field {} does not exist'.format(field))
  124. def set(self, field, value):
  125. if not self._check_obj():
  126. raise Exception('Failed to initialize object!')
  127. self.__dict[field] = value
  128. def get_organizations(self):
  129. if not self.__check_headers():
  130. return None
  131. url = self.MERAKI_API + '/organizations'
  132. try:
  133. response = requests.request('GET', url, headers=self._headers)
  134. response.raise_for_status()
  135. except Exception as e:
  136. msg = 'Error getting list of organizations: {} ({})'.format(
  137. e, Meraki._get_json_errors(response))
  138. if self._logit:
  139. logging.error(msg)
  140. else:
  141. print(msg)
  142. return None
  143. res = []
  144. for org in response.json():
  145. org_obj = Organization(
  146. key=self._key, name=org[Organization._name_prop], id=org[Organization._id_prop])
  147. res.append(org_obj)
  148. return res
  149. class Organization(Meraki):
  150. def __init__(self, **kwargs):
  151. super(Organization, self).__init__(**kwargs)
  152. self._class_name = 'Organization'
  153. self._api_url_endpoint = '/organizations'
  154. self.ORG_API = self.MERAKI_API + self._api_url_endpoint
  155. def get_inventory(self):
  156. if not self._check_obj():
  157. return None
  158. url = self.ORG_API + '/' + self._id + '/inventory'
  159. try:
  160. response = requests.request('GET', url, headers=self._headers)
  161. response.raise_for_status()
  162. except Exception as e:
  163. msg = 'Error getting inventory for organization {}: {} ({})'.format(
  164. self._name, e, Meraki._get_json_errors(response))
  165. if self._logit:
  166. logging.error(msg)
  167. else:
  168. print(msg)
  169. return None
  170. return response.json()
  171. def get_networks(self):
  172. if not self._check_obj():
  173. return None
  174. url = self.ORG_API + '/' + self._id + '/networks'
  175. try:
  176. response = requests.request('GET', url, headers=self._headers)
  177. response.raise_for_status()
  178. except Exception as e:
  179. msg = 'Error getting list of networks for {}: {} ({})'.format(
  180. self._name, e, Meraki._get_json_errors(response))
  181. if self._logit:
  182. logging.error(msg)
  183. else:
  184. print(msg)
  185. return None
  186. res = []
  187. for n in response.json():
  188. net = Network(
  189. key=self._key, name=n[Network._name_prop], id=n[Network._id_prop
  190. ])
  191. res.append(net)
  192. return res
  193. def create_network(self, name, **kwargs):
  194. if not self._check_obj():
  195. return None
  196. payload = {
  197. 'name': name
  198. }
  199. for key in ['type', 'tags', 'timezone', 'copy_from_network_id']:
  200. if key in kwargs:
  201. if key == 'timezone':
  202. payload['timeZone'] = kwargs[key]
  203. elif key == 'copy_from_network_id':
  204. payload['copyFromNetworkId'] = kwargs[key]
  205. else:
  206. payload[key] = kwargs[key]
  207. if 'type' not in kwargs:
  208. payload['type'] = 'wireless switch appliance'
  209. url = self.ORG_API + '/' + self._id + '/networks'
  210. try:
  211. response = requests.request(
  212. 'POST', url, json=payload, headers=self._headers)
  213. response.raise_for_status()
  214. except Exception as e:
  215. msg = 'Error creating new network {} in {}: {} ({})'.format(
  216. name, self._name, e, Meraki._get_json_errors(response))
  217. if self._logit:
  218. logging.error(msg)
  219. else:
  220. print(msg)
  221. return None
  222. jobj = response.json()
  223. net_obj = Network(key=self._key, id=jobj['id'], name=jobj['name'])
  224. return net_obj
  225. class Network(Meraki):
  226. def __init__(self, **kwargs):
  227. super(Network, self).__init__(**kwargs)
  228. self._class_name = 'Network'
  229. self._api_url_endpoint = '/networks'
  230. self.NET_API = self.MERAKI_API + self._api_url_endpoint
  231. def get_devices(self):
  232. if not self._check_obj():
  233. return None
  234. url = self.NET_API + '/' + self._id + '/devices'
  235. try:
  236. response = requests.request('GET', url, headers=self._headers)
  237. response.raise_for_status()
  238. except Exception as e:
  239. msg = 'Error getting device list for network {}: {} ({})'.format(
  240. self._name, e, Meraki._get_json_errors(response))
  241. if self._logit:
  242. logging.error(msg)
  243. else:
  244. print(msg)
  245. return None
  246. res = []
  247. for d in response.json():
  248. dev = Device(
  249. key=self._key, name=d[Device._name_prop], id=d[Device._id_prop])
  250. res.append(dev)
  251. return res
  252. def claim_device(self, dev):
  253. if not self._check_obj():
  254. return None
  255. url = self.NET_API + '/' + self._id + '/devices/claim'
  256. payload = {
  257. 'serial': dev._id
  258. }
  259. try:
  260. response = requests.request(
  261. 'POST', url, json=payload, headers=self._headers)
  262. response.raise_for_status()
  263. except Exception as e:
  264. msg = 'Error claiming device {} for network {}: {} ({})'.format(
  265. dev._id, self._name, e, Meraki._get_json_errors(response))
  266. if self._logit:
  267. logging.error(msg)
  268. else:
  269. print(msg)
  270. return None
  271. new_dev = Device(key=self._key, id=dev._id, net=self)
  272. return new_dev
  273. def create_vlan(self, name, id, subnet, appliance_ip):
  274. if not self._check_obj():
  275. return None
  276. payload = {
  277. 'id': id,
  278. 'name': name,
  279. 'subnet': subnet,
  280. 'applianceIp': appliance_ip
  281. }
  282. url = self.NET_API + '/' + self._id + '/vlans'
  283. try:
  284. response = requests.request(
  285. 'POST', url, json=payload, headers=self._headers)
  286. response.raise_for_status()
  287. except Exception as e:
  288. msg = 'Error adding VLAN {} (ID: {}) to network {}: {} ({})'.format(
  289. name, id, self._name, e, Meraki._get_json_errors(response))
  290. if self._logit:
  291. logging.error(msg)
  292. else:
  293. print(msg)
  294. return None
  295. jobj = response.json()
  296. vlan_obj = Vlan(
  297. key=self._key, id=jobj['id'], name=jobj['name'], net=self)
  298. return vlan_obj
  299. class SSID(Meraki):
  300. _id_prop = 'number'
  301. def __init__(self, **kwargs):
  302. super(SSID, self).__init__(**kwargs)
  303. if not 'net' in kwargs:
  304. raise TypeError('Missing mandatory net argument!')
  305. self._net = kwargs['net']
  306. self._class_name = 'SSID'
  307. self._api_url_endpoint = '/networks/' + \
  308. self._net.get(Network._id_prop) + '/ssids'
  309. self.SSID_API = self.MERAKI_API + self._api_url_endpoint
  310. def update_ssid(self, **kwargs):
  311. if not self._check_obj():
  312. return False
  313. payload = {}
  314. for key in ['name', 'enabled', 'auth_mode', 'encryption_mode', 'psk', 'ip_assignment_mode']:
  315. if key in kwargs:
  316. if key == 'auth_mode':
  317. payload['authMode'] = kwargs[key]
  318. elif key == 'encryption_mode':
  319. payload['encryptionMode'] = kwargs[key]
  320. elif key == 'ip_assignment_mode':
  321. payload['ipAssignmentMode'] = kwargs[key]
  322. else:
  323. payload[key] = kwargs[key]
  324. if len(payload) == 0:
  325. return False
  326. if 'ip_assignment_mode' not in kwargs:
  327. payload['ipAssignmentMode'] = 'Bridge mode'
  328. url = self.SSID_API + '/' + self._id
  329. try:
  330. response = requests.request(
  331. 'PUT', url, json=payload, headers=self._headers)
  332. response.raise_for_status()
  333. except Exception as e:
  334. msg = 'Error updating SSID properties for {}: {} ({})'.format(
  335. self._id, e, Meraki._get_json_errors(response))
  336. if self._logit:
  337. logging.error(msg)
  338. else:
  339. print(msg)
  340. return False
  341. jobj = response.json()
  342. for k, v in jobj.items():
  343. self.set(k, v)
  344. return True
  345. class Vlan(Meraki):
  346. def __init__(self, **kwargs):
  347. super(Vlan, self).__init__(**kwargs)
  348. if not 'net' in kwargs:
  349. raise TypeError('Missing mandatory net argument!')
  350. self._net = kwargs['net']
  351. self._class_name = 'Vlan'
  352. self._api_url_endpoint = '/networks/' + \
  353. self._net.get(Network._id_prop) + '/vlans'
  354. self.VLAN_API = self.MERAKI_API + self._api_url_endpoint
  355. def update_vlan(self, **kwargs):
  356. if not self._check_obj():
  357. return False
  358. payload = {}
  359. for key in ['name', 'subnet', 'appliance_ip', 'fixed_ip_assignments', 'reserved_ip_ranges', 'dns_nameservers']:
  360. if key in kwargs:
  361. if key == 'appliance_ip':
  362. payload['applianceIp'] = kwargs[key]
  363. elif key == 'fixed_ip_assignments':
  364. payload['fixedIpAssignments'] = kwargs[key]
  365. elif key == 'reserved_ip_ranges':
  366. payload['reservedIpRanges'] = kwargs[key]
  367. elif key == 'dns_nameservers':
  368. payload['dnsNameservers'] = kwargs[key]
  369. else:
  370. payload[key] = kwargs[key]
  371. if len(payload) == 0:
  372. return False
  373. url = self.VLAN_API + '/' + self._id
  374. try:
  375. response = requests.request(
  376. 'PUT', url, json=payload, headers=self._headers)
  377. response.raise_for_status()
  378. except Exception as e:
  379. msg = 'Error updating VLAN properties for {}: {} ({})'.format(
  380. self._name, e, Meraki._get_json_errors(response))
  381. if self._logit:
  382. logging.error(msg)
  383. else:
  384. print(msg)
  385. return False
  386. jobj = response.json()
  387. for k, v in jobj.items():
  388. self.set(k, v)
  389. return True
  390. class Device(Meraki):
  391. _id_prop = 'serial'
  392. def __init__(self, **kwargs):
  393. super(Device, self).__init__(**kwargs)
  394. if not 'net' in kwargs:
  395. raise TypeError('Missing mandatory net argument!')
  396. self._net = kwargs['net']
  397. self._class_name = 'Device'
  398. self._api_url_endpoint = '/networks/' + \
  399. self._net.get(Network._id_prop) + '/devices'
  400. self.DEV_API = self.MERAKI_API + self._api_url_endpoint
  401. def remove_device(self):
  402. if not self._check_obj():
  403. return False
  404. url = self.DEV_API + '/' + self._id + '/remove'
  405. try:
  406. response = requests.request('POST', url, headers=self._headers)
  407. response.raise_for_status()
  408. except Exception as e:
  409. msg = 'Failed to remove device {} for network {}: {} ({})'.format(
  410. self._name, self._net.get(Network._name_prop), e, Meraki._get_json_errors(response))
  411. if self._logit:
  412. logging.error(msg)
  413. else:
  414. print(msg)
  415. return False
  416. return True
  417. def update_device(self, **kwargs):
  418. if not self._check_obj():
  419. return False
  420. payload = {}
  421. for key in ['name', 'tags', 'lat', 'lng', 'address', 'move_map_marker']:
  422. if key in kwargs:
  423. if key == 'move_map_marker':
  424. payload['moveMapMarker'] = kwargs[key]
  425. else:
  426. payload[key] = kwargs[key]
  427. if len(payload) == 0:
  428. return False
  429. url = self.DEV_API + '/' + self._id
  430. try:
  431. response = requests.request(
  432. 'PUT', url, json=payload, headers=self._headers)
  433. response.raise_for_status()
  434. except Exception as e:
  435. msg = 'Error updating device properties for {}: {} ({})'.format(
  436. self._name, e, Meraki._get_json_errors(response))
  437. if self._logit:
  438. logging.error(msg)
  439. else:
  440. print(msg)
  441. return False
  442. jobj = response.json()
  443. for k, v in jobj.items():
  444. if k == 'moveMapMarker':
  445. continue
  446. self.set(k, v)
  447. return True
  448. class SwitchPort(Meraki):
  449. _id_prop = 'number'
  450. def __init__(self, **kwargs):
  451. super(SwitchPort, self).__init__(**kwargs)
  452. if not 'dev' in kwargs:
  453. raise TypeError('Missing mandatory dev argument!')
  454. self._dev = kwargs['dev']
  455. self._class_name = 'SwitchPort'
  456. self._api_url_endpoint = '/devices/' + \
  457. self._net.get(Network._id_prop) + '/switchPorts'
  458. self.SWP_API = self.MERAKI_API + self._api_url_endpoint
  459. def update_switchport(self, **kwargs):
  460. if not self._check_obj():
  461. return False
  462. payload = {}
  463. for key in ['name', 'tags', 'enabled', 'type', 'vlan', 'voice_vlan', 'allowed_vlans', 'poe_enabled']:
  464. if key in kwargs:
  465. if key == 'voice_vlan':
  466. payload['voiceVlan'] = kwargs[key]
  467. elif key == 'allowed_vlans':
  468. payload['allowedVlans'] = kwargs[key]
  469. elif key == 'poe_enabled':
  470. payload['poeEnabled'] = kwargs[key]
  471. else:
  472. payload[key] = kwargs[key]
  473. if len(payload) == 0:
  474. return False
  475. url = self.SWP_API + '/' + self._id
  476. try:
  477. response = requests.request(
  478. 'PUT', url, json=payload, headers=self._headers)
  479. response.raise_for_status()
  480. except Exception as e:
  481. msg = 'Error updating switchport properties for {} on device {}: {} ({})'.format(
  482. self._id, self._dev.get(Device._name_prop), e, Meraki._get_json_errors(response))
  483. if self._logit:
  484. logging.error(msg)
  485. else:
  486. print(msg)
  487. return False
  488. jobj = response.json()
  489. for k, v in jobj.items():
  490. self.set(k, v)
  491. return True