meraki_api.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. from __future__ import print_function
  2. #
  3. # Copyright (c) 2018-2021 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 builtins import str
  26. from builtins import object
  27. import requests
  28. import logging
  29. class Meraki(object):
  30. MERAKI_API = "https://api.meraki.com/api/v1"
  31. _headers = {"X-Cisco-Meraki-API-Key": None, "Content-type": "application/json"}
  32. _logit = False
  33. _key = None
  34. _name = None
  35. _id = None
  36. _class_name = "Meraki"
  37. _name_prop = "name"
  38. _id_prop = "id"
  39. _api_url_endpoint = ""
  40. def __init__(self, **kwargs):
  41. if "logit" in kwargs:
  42. self._logit = kwargs["logit"]
  43. if "key" in kwargs:
  44. self._key = kwargs["key"]
  45. self._headers["X-Cisco-Meraki-API-Key"] = kwargs["key"]
  46. if "name" in kwargs:
  47. self._name = kwargs["name"]
  48. if "id" in kwargs:
  49. self._id = str(kwargs["id"])
  50. self.__dict = {}
  51. self._initialized = False
  52. def set_key(self, key):
  53. self._key = key
  54. self._headers["X-Cisco-Meraki-API-Key"] = key
  55. def set_name(self, name):
  56. self._name = name
  57. def _set_id(self, id):
  58. self._id = id
  59. def __check_headers(self):
  60. if self._headers["X-Cisco-Meraki-API-Key"] is None:
  61. msg = "Meraki API key is not set!"
  62. if self._logit:
  63. logging.error(msg)
  64. else:
  65. print(msg)
  66. return False
  67. return True
  68. def realize(self):
  69. return self._check_obj()
  70. @staticmethod
  71. def _get_json_errors(response):
  72. res = ""
  73. try:
  74. jobj = response.json()
  75. if "errors" in jobj:
  76. res = ", ".join(jobj["errors"])
  77. except Exception:
  78. pass
  79. return res
  80. def _check_obj(self):
  81. if self._initialized:
  82. return True
  83. if self._id is None:
  84. msg = "{} id is not set!".format(self._class_name)
  85. if self._logit:
  86. logging.error(msg)
  87. else:
  88. print(msg)
  89. return False
  90. return self.__get_current_obj()
  91. def __get_current_obj(self):
  92. if not self.__check_headers():
  93. return False
  94. url = self.MERAKI_API + self._api_url_endpoint + "/" + self._id
  95. try:
  96. response = requests.request("GET", url, headers=self._headers)
  97. response.raise_for_status()
  98. except Exception as e:
  99. msg = "Error getting {} for {}: {} ({})".format(self._class_name.lower(), self._id, e, Meraki._get_json_errors(response))
  100. if self._logit:
  101. logging.error(msg)
  102. else:
  103. print(msg)
  104. return False
  105. jobj = response.json()
  106. self.__populate_obj(jobj)
  107. # Object may not have a name yet.
  108. if self._name_prop in jobj:
  109. self._name = jobj[self._name_prop]
  110. self._initialized = True
  111. return True
  112. def __populate_obj(self, jobj):
  113. for k, v in list(jobj.items()):
  114. self.__dict[k] = v
  115. def get(self, field):
  116. if not self._check_obj():
  117. raise Exception("Failed to initialize object!")
  118. if field in self.__dict:
  119. return self.__dict[field]
  120. else:
  121. raise Exception("Field {} does not exist".format(field))
  122. def set(self, field, value):
  123. if not self._check_obj():
  124. raise Exception("Failed to initialize object!")
  125. self.__dict[field] = value
  126. def get_organizations(self):
  127. if not self.__check_headers():
  128. return None
  129. url = self.MERAKI_API + "/organizations"
  130. try:
  131. response = requests.request("GET", url, headers=self._headers)
  132. response.raise_for_status()
  133. except Exception as e:
  134. msg = "Error getting list of organizations: {} ({})".format(e, Meraki._get_json_errors(response))
  135. if self._logit:
  136. logging.error(msg)
  137. else:
  138. print(msg)
  139. return None
  140. res = []
  141. for org in response.json():
  142. org_obj = Organization(key=self._key, name=org[Organization._name_prop], id=org[Organization._id_prop])
  143. res.append(org_obj)
  144. return res
  145. class Organization(Meraki):
  146. def __init__(self, **kwargs):
  147. super(Organization, self).__init__(**kwargs)
  148. self._class_name = "Organization"
  149. self._api_url_endpoint = "/organizations"
  150. self.ORG_API = self.MERAKI_API + self._api_url_endpoint
  151. def get_inventory(self):
  152. if not self._check_obj():
  153. return None
  154. url = self.ORG_API + "/" + self._id + "/inventoryDevices"
  155. try:
  156. response = requests.request("GET", url, headers=self._headers)
  157. response.raise_for_status()
  158. except Exception as e:
  159. msg = "Error getting inventory for organization {}: {} ({})".format(self._name, e, Meraki._get_json_errors(response))
  160. if self._logit:
  161. logging.error(msg)
  162. else:
  163. print(msg)
  164. return None
  165. return response.json()
  166. def get_networks(self):
  167. if not self._check_obj():
  168. return None
  169. url = self.ORG_API + "/" + self._id + "/networks"
  170. try:
  171. response = requests.request("GET", url, headers=self._headers)
  172. response.raise_for_status()
  173. except Exception as e:
  174. msg = "Error getting list of networks for {}: {} ({})".format(self._name, e, Meraki._get_json_errors(response))
  175. if self._logit:
  176. logging.error(msg)
  177. else:
  178. print(msg)
  179. return None
  180. res = []
  181. for n in response.json():
  182. net = Network(key=self._key, name=n[Network._name_prop], id=n[Network._id_prop])
  183. res.append(net)
  184. return res
  185. def create_network(self, name, **kwargs):
  186. if not self._check_obj():
  187. return None
  188. payload = {"name": name}
  189. for key in ["type", "tags", "timezone", "copy_from_network_id"]:
  190. if key in kwargs:
  191. if key == "timezone":
  192. payload["timeZone"] = kwargs[key]
  193. elif key == "copy_from_network_id":
  194. payload["copyFromNetworkId"] = kwargs[key]
  195. elif key == "type":
  196. payload["productTypes"] = kwargs[key]
  197. else:
  198. payload[key] = kwargs[key]
  199. if "type" not in kwargs:
  200. payload["productTypes"] = ["wireless", "switch", "appliance"]
  201. url = self.ORG_API + "/" + self._id + "/networks"
  202. try:
  203. response = requests.request("POST", url, json=payload, headers=self._headers)
  204. response.raise_for_status()
  205. except Exception as e:
  206. msg = "Error creating new network {} in {}: {} ({})".format(name, self._name, e, Meraki._get_json_errors(response))
  207. if self._logit:
  208. logging.error(msg)
  209. else:
  210. print(msg)
  211. return None
  212. jobj = response.json()
  213. net_obj = Network(key=self._key, id=jobj["id"], name=jobj["name"])
  214. return net_obj
  215. class Network(Meraki):
  216. def __init__(self, **kwargs):
  217. super(Network, self).__init__(**kwargs)
  218. self._class_name = "Network"
  219. self._api_url_endpoint = "/networks"
  220. self.NET_API = self.MERAKI_API + self._api_url_endpoint
  221. def get_devices(self):
  222. if not self._check_obj():
  223. return None
  224. url = self.NET_API + "/" + self._id + "/devices"
  225. try:
  226. response = requests.request("GET", url, headers=self._headers)
  227. response.raise_for_status()
  228. except Exception as e:
  229. msg = "Error getting device list for network {}: {} ({})".format(self._name, e, Meraki._get_json_errors(response))
  230. if self._logit:
  231. logging.error(msg)
  232. else:
  233. print(msg)
  234. return None
  235. res = []
  236. for d in response.json():
  237. dev = Device(key=self._key, name=d[Device._name_prop], id=d[Device._id_prop])
  238. res.append(dev)
  239. return res
  240. def claim_device(self, dev):
  241. if not self._check_obj():
  242. return None
  243. url = self.NET_API + "/" + self._id + "/devices/claim"
  244. payload = {"serials": [dev._id]}
  245. try:
  246. response = requests.request("POST", url, json=payload, headers=self._headers)
  247. response.raise_for_status()
  248. except Exception as e:
  249. msg = "Error claiming device {} for network {}: {} ({})".format(dev._id, self._name, e, Meraki._get_json_errors(response))
  250. if self._logit:
  251. logging.error(msg)
  252. else:
  253. print(msg)
  254. return None
  255. new_dev = Device(key=self._key, id=dev._id, net=self)
  256. return new_dev
  257. def enable_vlans(self):
  258. if not self._check_obj():
  259. return False
  260. payload = {"vlansEnabled": True}
  261. url = self.NET_API + "/" + self._id + "/appliance/vlans/settings"
  262. try:
  263. response = requests.request("PUT", url, json=payload, headers=self._headers)
  264. response.raise_for_status()
  265. except Exception as e:
  266. msg = "Error enabling VLANs for network {}: {} ({})".format(self._name, e, Meraki._get_json_errors(response))
  267. if self._logit:
  268. logging.error(msg)
  269. else:
  270. print(msg)
  271. return False
  272. return True
  273. def create_vlan(self, name, id, subnet, appliance_ip):
  274. if not self._check_obj():
  275. return None
  276. payload = {"id": id, "name": name, "subnet": subnet, "applianceIp": appliance_ip}
  277. url = self.NET_API + "/" + self._id + "/appliance/vlans"
  278. try:
  279. response = requests.request("POST", url, json=payload, headers=self._headers)
  280. response.raise_for_status()
  281. except Exception as e:
  282. msg = "Error adding VLAN {} (ID: {}) to network {}: {} ({})".format(name, id, self._name, e, Meraki._get_json_errors(response))
  283. if self._logit:
  284. logging.error(msg)
  285. else:
  286. print(msg)
  287. return None
  288. jobj = response.json()
  289. vlan_obj = Vlan(key=self._key, id=jobj["id"], name=jobj["name"], net=self)
  290. return vlan_obj
  291. def apply_shaping(self, client_limit):
  292. if not self._check_obj():
  293. return False
  294. payload = {"globalBandwidthLimits": {"limitUp": client_limit, "limitDown": client_limit}}
  295. url = self.NET_API + "/" + self._id + "/appliance/trafficShaping"
  296. try:
  297. response = requests.request("PUT", url, json=payload, headers=self._headers)
  298. response.raise_for_status()
  299. except Exception as e:
  300. msg = "Error setting traffic shaping limit in network {}: {} ({})".format(self._name, e, Meraki._get_json_errors(response))
  301. if self._logit:
  302. logging.error(msg)
  303. else:
  304. print(msg)
  305. return False
  306. return True
  307. class SSID(Meraki):
  308. _id_prop = "number"
  309. def __init__(self, **kwargs):
  310. super(SSID, self).__init__(**kwargs)
  311. if "net" not in kwargs:
  312. raise TypeError("Missing mandatory net argument!")
  313. self._net = kwargs["net"]
  314. self._class_name = "SSID"
  315. self._api_url_endpoint = "/networks/" + self._net.get(Network._id_prop) + "/wireless/ssids"
  316. self.SSID_API = self.MERAKI_API + self._api_url_endpoint
  317. def update_ssid(self, **kwargs):
  318. if not self._check_obj():
  319. return False
  320. payload = {}
  321. for key in ["name", "enabled", "auth_mode", "encryption_mode", "psk", "ip_assignment_mode", "tags"]:
  322. if key in kwargs:
  323. if key == "auth_mode":
  324. payload["authMode"] = kwargs[key]
  325. elif key == "encryption_mode":
  326. payload["encryptionMode"] = kwargs[key]
  327. elif key == "ip_assignment_mode":
  328. payload["ipAssignmentMode"] = kwargs[key]
  329. elif key == "tags":
  330. payload["availabilityTags"] = kwargs[key]
  331. payload["availableOnAllAps"] = False
  332. else:
  333. payload[key] = kwargs[key]
  334. if len(payload) == 0:
  335. return False
  336. if "ip_assignment_mode" not in kwargs:
  337. payload["ipAssignmentMode"] = "Bridge mode"
  338. url = self.SSID_API + "/" + self._id
  339. try:
  340. response = requests.request("PUT", url, json=payload, headers=self._headers)
  341. response.raise_for_status()
  342. except Exception as e:
  343. msg = "Error updating SSID properties for {}: {} ({})".format(self._id, e, Meraki._get_json_errors(response))
  344. if self._logit:
  345. logging.error(msg)
  346. else:
  347. print(msg)
  348. return False
  349. jobj = response.json()
  350. for k, v in list(jobj.items()):
  351. self.set(k, v)
  352. return True
  353. def allow_local_lan(self):
  354. if not self._check_obj():
  355. return False
  356. payload = {"allowLanAccess": True}
  357. url = self.SSID_API + "/" + self._id + "/firewall/l3FirewallRules"
  358. try:
  359. response = requests.request("PUT", url, json=payload, headers=self._headers)
  360. response.raise_for_status()
  361. except Exception as e:
  362. msg = "Error updating SSID firewall for {}: {} ({})".format(self._id, e, Meraki._get_json_errors(response))
  363. if self._logit:
  364. logging.error(msg)
  365. else:
  366. print(msg)
  367. return False
  368. return True
  369. class Vlan(Meraki):
  370. def __init__(self, **kwargs):
  371. super(Vlan, self).__init__(**kwargs)
  372. if "net" not in kwargs:
  373. raise TypeError("Missing mandatory net argument!")
  374. self._net = kwargs["net"]
  375. self._class_name = "Vlan"
  376. self._api_url_endpoint = "/networks/" + self._net.get(Network._id_prop) + "/appliance/vlans"
  377. self.VLAN_API = self.MERAKI_API + self._api_url_endpoint
  378. def update_vlan(self, **kwargs):
  379. if not self._check_obj():
  380. return False
  381. payload = {}
  382. for key in ["name", "subnet", "appliance_ip", "fixed_ip_assignments", "reserved_ip_ranges", "dns_nameservers"]:
  383. if key in kwargs:
  384. if key == "appliance_ip":
  385. payload["applianceIp"] = kwargs[key]
  386. elif key == "fixed_ip_assignments":
  387. payload["fixedIpAssignments"] = kwargs[key]
  388. elif key == "reserved_ip_ranges":
  389. payload["reservedIpRanges"] = kwargs[key]
  390. elif key == "dns_nameservers":
  391. payload["dnsNameservers"] = kwargs[key]
  392. else:
  393. payload[key] = kwargs[key]
  394. if len(payload) == 0:
  395. return False
  396. url = self.VLAN_API + "/" + self._id
  397. try:
  398. response = requests.request("PUT", url, json=payload, headers=self._headers)
  399. response.raise_for_status()
  400. except Exception as e:
  401. msg = "Error updating VLAN properties for {}: {} ({})".format(self._name, e, Meraki._get_json_errors(response))
  402. if self._logit:
  403. logging.error(msg)
  404. else:
  405. print(msg)
  406. return False
  407. jobj = response.json()
  408. for k, v in list(jobj.items()):
  409. self.set(k, v)
  410. return True
  411. class Device(Meraki):
  412. _id_prop = "serial"
  413. def __init__(self, **kwargs):
  414. super(Device, self).__init__(**kwargs)
  415. if "net" not in kwargs:
  416. raise TypeError("Missing mandatory net argument!")
  417. self._net = kwargs["net"]
  418. self._class_name = "Device"
  419. self._api_url_endpoint = "/devices"
  420. self.DEV_API = self.MERAKI_API + self._api_url_endpoint
  421. self.OLD_DEV_API = self.MERAKI_API + "/networks/" + self._net.get(Network._id_prop) + "/devices"
  422. def remove_device(self):
  423. if not self._check_obj():
  424. return False
  425. url = self.OLD_DEV_API + "/" + self._id + "/remove"
  426. try:
  427. response = requests.request("POST", url, headers=self._headers)
  428. response.raise_for_status()
  429. except Exception as e:
  430. msg = "Failed to remove device {} for network {}: {} ({})".format(
  431. self._name, self._net.get(Network._name_prop), e, Meraki._get_json_errors(response)
  432. )
  433. if self._logit:
  434. logging.error(msg)
  435. else:
  436. print(msg)
  437. return False
  438. return True
  439. def update_device(self, **kwargs):
  440. if not self._check_obj():
  441. return False
  442. payload = {}
  443. for key in ["name", "tags", "lat", "lng", "address", "move_map_marker"]:
  444. if key in kwargs:
  445. if key == "move_map_marker":
  446. payload["moveMapMarker"] = kwargs[key]
  447. else:
  448. payload[key] = kwargs[key]
  449. if len(payload) == 0:
  450. return False
  451. url = self.DEV_API + "/" + self._id
  452. try:
  453. response = requests.request("PUT", url, json=payload, headers=self._headers)
  454. response.raise_for_status()
  455. except Exception as e:
  456. msg = "Error updating device properties for {}: {} ({})".format(self._name, e, Meraki._get_json_errors(response))
  457. if self._logit:
  458. logging.error(msg)
  459. else:
  460. print(msg)
  461. return False
  462. jobj = response.json()
  463. for k, v in list(jobj.items()):
  464. if k == "moveMapMarker":
  465. continue
  466. self.set(k, v)
  467. return True
  468. class SwitchPort(Meraki):
  469. _id_prop = "number"
  470. def __init__(self, **kwargs):
  471. super(SwitchPort, self).__init__(**kwargs)
  472. if "dev" not in kwargs:
  473. raise TypeError("Missing mandatory dev argument!")
  474. self._dev = kwargs["dev"]
  475. self._class_name = "SwitchPort"
  476. self._api_url_endpoint = "/devices/" + self._dev.get(Device._id_prop) + "/switch/ports"
  477. self.SWP_API = self.MERAKI_API + self._api_url_endpoint
  478. def update_switchport(self, **kwargs):
  479. if not self._check_obj():
  480. return False
  481. payload = {}
  482. for key in ["name", "tags", "enabled", "type", "vlan", "voice_vlan", "allowed_vlans", "rstp_enabled", "poe_enabled"]:
  483. if key in kwargs:
  484. if key == "voice_vlan":
  485. payload["voiceVlan"] = kwargs[key]
  486. elif key == "allowed_vlans":
  487. payload["allowedVlans"] = kwargs[key]
  488. elif key == "poe_enabled":
  489. payload["poeEnabled"] = kwargs[key]
  490. elif key == "rstp_enabled":
  491. payload["rstpEnabled"] = kwargs[key]
  492. else:
  493. payload[key] = kwargs[key]
  494. if len(payload) == 0:
  495. return False
  496. url = self.SWP_API + "/" + self._id
  497. try:
  498. response = requests.request("PUT", url, json=payload, headers=self._headers)
  499. response.raise_for_status()
  500. except Exception as e:
  501. msg = "Error updating switchport properties for {} on device {}: {} ({})".format(
  502. self._id, self._dev.get(Device._name_prop), e, Meraki._get_json_errors(response)
  503. )
  504. if self._logit:
  505. logging.error(msg)
  506. else:
  507. print(msg)
  508. return False
  509. jobj = response.json()
  510. for k, v in list(jobj.items()):
  511. self.set(k, v)
  512. return True