dhcp-hook.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. #!/usr/local/bin/python3
  2. #
  3. # Copyright (c) 2017-2020 Joe Clarke <jclarke@cisco.com>
  4. # All rights reserved.
  5. #
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions
  8. # are met:
  9. # 1. Redistributions of source code must retain the above copyright
  10. # notice, this list of conditions and the following disclaimer.
  11. # 2. Redistributions in binary form must reproduce the above copyright
  12. # notice, this list of conditions and the following disclaimer in the
  13. # documentation and/or other materials provided with the distribution.
  14. #
  15. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  16. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  17. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  18. # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  19. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  20. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  21. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  22. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  23. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  24. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  25. # SUCH DAMAGE.
  26. from __future__ import print_function
  27. from builtins import str
  28. import sys
  29. import json
  30. from sparker import Sparker, MessageType
  31. import re
  32. import requests
  33. from requests.packages.urllib3.exceptions import InsecureRequestWarning
  34. requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
  35. import time
  36. import traceback
  37. import socket
  38. import logging
  39. import CLEUCreds
  40. from cleu.config import Config as C
  41. AT_MACADDR = 9
  42. CNR_HEADERS = {"Accept": "application/json", "Authorization": CLEUCreds.JCLARKE_BASIC}
  43. DEFAULT_INT_TYPE = "GigabitEthernet"
  44. ALLOWED_TO_DELETE = ["jclarke@cisco.com", "ksekula@cisco.com", "anjesani@cisco.com"]
  45. def is_ascii(s):
  46. return all(ord(c) < 128 for c in s)
  47. def normalize_mac(mac):
  48. # Normalize all MAC addresses to colon-delimited format.
  49. mac_addr = "".join(l + ":" * (n % 2 == 1) for n, l in enumerate(list(re.sub(r"[:.-]", "", mac)))).strip(":")
  50. return mac_addr.lower()
  51. def get_from_cmx(**kwargs):
  52. marker = "green"
  53. if "user" in kwargs and kwargs["user"] == "gru":
  54. marker = "gru"
  55. if "ip" in kwargs:
  56. url = "{}?ip={}&marker={}&size=1440".format(CMX_GW, kwargs["ip"], marker)
  57. elif "mac" in kwargs:
  58. url = "{}?mac={}&marker={}&size=1440".format(C.CMX_GW, kwargs["mac"], marker)
  59. else:
  60. return None
  61. headers = {"Accept": "image/jpeg, application/json"}
  62. try:
  63. response = requests.request("GET", url, headers=headers, stream=True)
  64. response.raise_for_status()
  65. except Exception:
  66. logging.error("Encountered error getting data from cmx: {}".format(traceback.format_exc()))
  67. return None
  68. if response.headers.get("content-type") == "application/json":
  69. return None
  70. return response.raw.data
  71. def get_from_dnac(**kwargs):
  72. for dnac in C.DNACS:
  73. curl = "https://{}/dna/intent/api/v1/client-detail".format(dnac)
  74. # Get timestamp with milliseconds
  75. epoch = int(time.time() * 1000)
  76. turl = "https://{}/dna/system/api/v1/auth/token".format(dnac)
  77. theaders = {"content-type": "application/json", "authorization": CLEUCreds.JCLARKE_BASIC}
  78. try:
  79. response = requests.request("POST", turl, headers=theaders, verify=False)
  80. response.raise_for_status()
  81. except Exception as e:
  82. logging.warning("Unable to get an auth token from DNAC: {}".format(getattr(e, "message", repr(e))))
  83. continue
  84. j = response.json()
  85. cheaders = {"accept": "application/json", "x-auth-token": j["Token"]}
  86. params = {"macAddress": kwargs["mac"], "timestamp": epoch}
  87. try:
  88. response = requests.request("GET", curl, params=params, headers=cheaders, verify=False)
  89. response.raise_for_status()
  90. except Exception as e:
  91. logging.warning("Failed to find MAC address {} in DNAC: {}".format(kwargs["mac"], getattr(e, "message", repr(e))))
  92. continue
  93. j = response.json()
  94. if "detail" not in j:
  95. logging.warning("Got an unknown response from DNAC: '{}'".format(response.text))
  96. continue
  97. if "errorCode" in j["detail"]:
  98. continue
  99. return j["detail"]
  100. return None
  101. def get_from_pi(**kwargs):
  102. what = None
  103. if "user" in kwargs:
  104. url = 'https://{}/webacs/api/v2/data/ClientDetails.json?.full=true&userName="{}"&status=ASSOCIATED'.format(C.PI, kwargs["user"])
  105. what = "user"
  106. elif "mac" in kwargs:
  107. mac_addr = normalize_mac(kwargs["mac"])
  108. url = 'https://{}/webacs/api/v2/data/ClientDetails.json?.full=true&macAddress="{}"&status=ASSOCIATED'.format(C.PI, mac_addr)
  109. what = "mac"
  110. elif "ip" in kwargs:
  111. url = 'https://{}/webacs/api/v2/data/ClientDetails.json?.full=true&ipAddress="{}"&status=ASSOCIATED'.format(C.PI, kwargs["ip"])
  112. what = "ip"
  113. else:
  114. return None
  115. headers = {"Connection": "close"}
  116. done = False
  117. first = 0
  118. code = 401
  119. i = 0
  120. while code != 200 and i < 10:
  121. response = None
  122. try:
  123. response = requests.request("GET", url, auth=(CLEUCreds.PI_USER, CLEUCreds.PI_PASS), headers=headers, verify=False)
  124. except Exception as e:
  125. logging.error("Failed to get a response from PI for {}: {}".format(kwargs[what], e))
  126. return None
  127. code = response.status_code
  128. if code != 200:
  129. i += 1
  130. time.sleep(3)
  131. if code == 200:
  132. j = json.loads(response.text)
  133. if j["queryResponse"]["@count"] == 0:
  134. return None
  135. return j["queryResponse"]["entity"]
  136. else:
  137. logging.error("Failed to get a response from PI for {}: {}".format(kwargs[what], response.text))
  138. return None
  139. def parse_relay_info(outd):
  140. global DEFAULT_INT_TYPE
  141. res = {"vlan": "N/A", "port": "N/A", "switch": "N/A"}
  142. if "relayAgentCircuitId" in outd:
  143. octets = outd["relayAgentCircuitId"].split(":")
  144. if len(octets) > 4:
  145. res["vlan"] = int("".join(octets[2:4]), 16)
  146. first_part = int(octets[4], 16)
  147. port = str(first_part)
  148. if first_part != 0:
  149. port = str(first_part) + "/0"
  150. res["port"] = DEFAULT_INT_TYPE + port + "/" + str(int(octets[5], 16))
  151. if "relayAgentRemoteId" in outd:
  152. octets = outd["relayAgentRemoteId"].split(":")
  153. res["switch"] = bytes.fromhex("".join(octets[2:])).decode("utf-8", "ignore")
  154. if not is_ascii(res["switch"]) or res["switch"] == "":
  155. res["switch"] = "N/A"
  156. return res
  157. def check_for_reservation(ip):
  158. global CNR_HEADERS
  159. res = {}
  160. url = "{}/Reservation/{}".format(C.DHCP_BASE, ip)
  161. try:
  162. response = requests.request("GET", url, headers=CNR_HEADERS, verify=False)
  163. response.raise_for_status()
  164. except Exception as e:
  165. logging.warning("Did not get a good response from CNR for reservation {}: {}".format(ip, e))
  166. return None
  167. rsvp = response.json()
  168. res["mac"] = ":".join(rsvp["lookupKey"].split(":")[-6:])
  169. res["scope"] = rsvp["scope"]
  170. return res
  171. def check_for_reservation_by_mac(mac):
  172. global CNR_HEADERS
  173. res = {}
  174. mac_addr = normalize_mac(mac)
  175. url = "{}/Reservation".format(C.DHCP_BASE)
  176. try:
  177. response = requests.request("GET", url, headers=CNR_HEADERS, params={"lookupKey": mac_addr}, verify=False)
  178. response.raise_for_status()
  179. except Exception as e:
  180. logging.warning("Did not get a good response from CNR for reservation {}: {}".format(ip, e))
  181. return None
  182. j = response.json()
  183. if len(j) == 0:
  184. return None
  185. rsvp = j[0]
  186. res["mac"] = ":".join(rsvp["lookupKey"].split(":")[-6:])
  187. res["scope"] = rsvp["scope"]
  188. return res
  189. def create_reservation(ip, mac):
  190. global CNR_HEADERS, AT_MACADDR
  191. mac_addr = normalize_mac(mac)
  192. url = "{}/Reservation".format(C.DHCP_BASE)
  193. payload = {"ipaddr": ip, "lookupKey": "01:06:" + mac_addr, "lookupKeyType": AT_MACADDR}
  194. response = requests.request("POST", url, headers=CNR_HEADERS, json=payload, verify=False)
  195. response.raise_for_status()
  196. def delete_reservation(ip):
  197. global DHCP_BASE, CNR_HEADERS
  198. url = "{}/Reservation/{}".format(C.DHCP_BASE, ip)
  199. response = requests.request("DELETE", url, headers=CNR_HEADERS, verify=False)
  200. response.raise_for_status()
  201. def check_for_lease(ip):
  202. global CNR_HEADERS
  203. res = {}
  204. url = "{}/Lease/{}".format(C.DHCP_BASE, ip)
  205. try:
  206. response = requests.request("GET", url, headers=CNR_HEADERS, verify=False)
  207. response.raise_for_status()
  208. except Exception as e:
  209. logging.warning("Did not get a good response from CNR for IP {}: {}".format(ip, e))
  210. return None
  211. lease = response.json()
  212. if not "clientMacAddr" in lease:
  213. return None
  214. relay = parse_relay_info(lease)
  215. if "clientHostName" in lease:
  216. res["name"] = lease["clientHostName"]
  217. elif "client-dns-name" in lease:
  218. res["name"] = lease["clientDnsName"]
  219. else:
  220. res["name"] = "UNKNOWN"
  221. res["mac"] = lease["clientMacAddr"][lease["clientMacAddr"].rfind(",") + 1 :]
  222. res["scope"] = lease["scopeName"]
  223. res["state"] = lease["state"]
  224. res["relay-info"] = relay
  225. rsvp = check_for_reservation(ip)
  226. if rsvp and rsvp["mac"] == res["mac"]:
  227. res["is-reserved"] = True
  228. return res
  229. def check_for_mac(mac):
  230. global CNR_HEADERS
  231. url = "{}/Lease".format(C.DHCP_BASE)
  232. try:
  233. response = requests.request("GET", url, headers=CNR_HEADERS, verify=False, params={"clientMacAddr": mac})
  234. response.raise_for_status()
  235. except Exception as e:
  236. logging.warning("Did not get a good response from CNR for MAC {}: {}".format(mac, e))
  237. return None
  238. j = response.json()
  239. if len(j) == 0:
  240. return None
  241. leases = []
  242. for lease in j:
  243. res = {}
  244. if "address" not in lease:
  245. continue
  246. relay = parse_relay_info(lease)
  247. res["ip"] = lease["address"]
  248. if "clientHostName" in lease:
  249. res["name"] = lease["clientHostName"]
  250. elif "clientDnsName" in lease:
  251. res["name"] = lease["clientDnsName"]
  252. else:
  253. res["name"] = "UNKNOWN"
  254. res["scope"] = lease["scopeName"]
  255. res["state"] = lease["state"]
  256. res["relay-info"] = relay
  257. rsvp = check_for_reservation(res["ip"])
  258. if rsvp and rsvp["mac"] == mac:
  259. res["is-reserved"] = True
  260. leases.append(res)
  261. return leases
  262. def print_dnac(spark, what, details, msg):
  263. ohealth = None
  264. healths = {}
  265. host_info = ""
  266. ssid = ""
  267. loc = ""
  268. hinfo = ""
  269. sdetails = ""
  270. if "healthScore" in details:
  271. for score in details["healthScore"]:
  272. if "healthType" in score:
  273. if score["healthType"] == "OVERALL":
  274. ohealth = {}
  275. ohealth["score"] = score["score"]
  276. ohealth["reason"] = score["reason"]
  277. else:
  278. healths[score["healthType"]] = {"score": score["score"], "reason": score["reason"]}
  279. if details["hostOs"]:
  280. host_info = "running **{}**".format(details["hostOs"])
  281. if details["ssid"]:
  282. ssid = "associated to SSID **{}**".format(details["ssid"])
  283. if details["location"]:
  284. loc = "located in **{}**".format(details["location"])
  285. if details["port"] and details["clientConnection"]:
  286. sdetails = "connected to device **{}** on port **{}**".format(details["clientConnection"], details["port"])
  287. if ohealth is not None:
  288. hinfo = "with health score **{}/10**".format(ohealth["score"])
  289. if ohealth["reason"]:
  290. hinfo += " (reason: _{}_)".format(ohealth["reason"])
  291. if len(healths) > 0:
  292. hinfo += " ["
  293. for h, hobj in list(healths.items()):
  294. hinfo += "{} health: {} ".format(h, hobj["score"])
  295. if hobj["reason"] != "":
  296. hinfo += "(reason: {}) ".format(hobj["reason"])
  297. hinfo += "]"
  298. spark.post_to_spark(
  299. C.WEBEX_TEAM,
  300. SPARK_ROOM,
  301. "{} {} is a {} client {} {} {} {} {}".format(msg, what, details["hostType"], sdetails, ssid, loc, host_info, hinfo),
  302. )
  303. def print_pi(spark, what, ents, msg):
  304. for ent in ents:
  305. res = ent["clientDetailsDTO"]
  306. apdet = ""
  307. condet = ""
  308. vendet = ""
  309. if "apName" in res:
  310. apdet = "**{}** via ".format(res["apName"])
  311. if "connectionType" in res:
  312. condet = "is a **{}** client".format(res["connectionType"])
  313. if "vendor" in res:
  314. vendet = "of vendor type **{}**".format(res["vendor"])
  315. spark.post_to_spark(
  316. C.WEBEX_TEAM,
  317. SPARK_ROOM,
  318. "{} {} {} {}, connected to {}**{}** on interface **{}** with MAC address **{}** and IP address **{}** in **VLAN {}** located in **{}**.".format(
  319. msg,
  320. what,
  321. condet,
  322. vendet,
  323. apdet,
  324. res["deviceName"],
  325. res["clientInterface"],
  326. res["macAddress"],
  327. res["ipAddress"]["address"],
  328. res["vlan"],
  329. res["location"],
  330. ),
  331. )
  332. spark = Sparker(token=CLEUCreds.SPARK_TOKEN, logit=True)
  333. SPARK_ROOM = "DHCP Queries"
  334. if __name__ == "__main__":
  335. print("Content-type: application/json\r\n\r\n")
  336. output = sys.stdin.read()
  337. j = json.loads(output)
  338. logging.basicConfig(
  339. format="%(asctime)s - %(name)s - %(levelname)s : %(message)s", filename="/var/log/dhcp-hook.log", level=logging.DEBUG
  340. )
  341. logging.debug(json.dumps(j, indent=4))
  342. message_from = j["data"]["personEmail"]
  343. if message_from == "livenocbot@sparkbot.io":
  344. logging.debug("Person email is our bot")
  345. print('{"result":"success"}')
  346. sys.exit(0)
  347. tid = spark.get_team_id(C.WEBEX_TEAM)
  348. if tid is None:
  349. logging.error("Failed to get Spark Team ID")
  350. print('{"result":"fail"}')
  351. sys.exit(0)
  352. rid = spark.get_room_id(tid, SPARK_ROOM)
  353. if rid is None:
  354. logging.error("Failed to get Spark Room ID")
  355. print('{"result":"fail"}')
  356. sys.exit(0)
  357. if rid != j["data"]["roomId"]:
  358. logging.error("Spark Room ID is not the same as in the message ({} vs. {})".format(rid, j["data"]["roomId"]))
  359. print('{"result":"fail"}')
  360. sys.exit(0)
  361. mid = j["data"]["id"]
  362. msg = spark.get_message(mid)
  363. if msg is None:
  364. logging.error("Did not get a message")
  365. print('{"result":"error"}')
  366. sys.exit(0)
  367. person = spark.get_person(j["data"]["personId"])
  368. if person is not None:
  369. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "Hey, {}. Working on that for you...".format(person["nickName"]))
  370. else:
  371. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "Working on that for you...")
  372. txt = msg["text"]
  373. found_hit = False
  374. if re.search(r"\bhelp\b", txt, re.I):
  375. spark.post_to_spark(
  376. C.WEBEX_TEAM,
  377. SPARK_ROOM,
  378. 'To lookup a reservation, type `@Live NOC Bot reservation IP`. To lookup a lease by MAC, ask about the MAC. To lookup a lease by IP ask about the IP. To look up a user, ask about "user USERNAME".<br>Some question might be, `@Live NOC Bot who has lease 1.2.3.4` or `@Live NOC Bot what lease does 00:11:22:33:44:55 have` or `@Live NOC Bot tell me about user jsmith`.',
  379. )
  380. found_hit = True
  381. try:
  382. m = re.search(r"user(name)?\s+\b(?P<uname>[A-Za-z][\w\-\.\d]+)([\s\?\.]|$)", txt, re.I)
  383. if not found_hit and not m:
  384. m = re.search(r"(who|where)\s+is\s+\b(?P<uname>[A-Za-z][\w\-\.\d]+)([\s\?\.]|$)", txt, re.I)
  385. if not found_hit and m:
  386. found_hit = True
  387. uname = m.group("uname")
  388. usecret = ""
  389. if re.search(r"gru", m.group("uname"), re.I):
  390. uname = "rkamerma"
  391. usecret = "gru"
  392. res = get_from_pi(user=uname)
  393. if res is None:
  394. res = get_from_pi(user=uname + "@{}".format(C.AD_DOMAIN))
  395. if res is not None:
  396. print_pi(spark, m.group("uname"), res, "")
  397. for ent in res:
  398. dnacres = get_from_dnac(mac=ent["clientDetailsDTO"]["macAddress"].lower())
  399. if dnacres is not None:
  400. print_dnac(spark, m.group("uname"), dnacres, "")
  401. cmxres = get_from_cmx(mac=ent["clientDetailsDTO"]["macAddress"].lower(), user=usecret)
  402. if cmxres is not None:
  403. spark.post_to_spark_with_attach(
  404. C.WEBEX_TEAM,
  405. SPARK_ROOM,
  406. "{}'s location from CMX".format(m.group("uname")),
  407. cmxres,
  408. "{}_location.jpg".format(m.group("uname")),
  409. "image/jpeg",
  410. )
  411. else:
  412. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "Sorry, I can't find {}.".format(m.group("uname")))
  413. m = re.search(r"(remove|delete)\s+reservation.*?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", txt, re.I)
  414. if not m:
  415. m = re.search(r"(unreserve).*?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", txt, re.I)
  416. if not found_hit and m:
  417. found_hit = True
  418. if message_from not in ALLOWED_TO_DELETE:
  419. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I'm sorry, {}. I can't do that for you.".format(message_from))
  420. else:
  421. res = check_for_reservation(m.group(2))
  422. if res is None:
  423. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I didn't find a reservation for {}.".format(m.group(2)))
  424. else:
  425. try:
  426. delete_reservation(m.group(2))
  427. spark.post_to_spark(
  428. C.WEBEX_TEAM, SPARK_ROOM, "Reservation for {} deleted successfully.".format(m.group(2)), MessageType.GOOD
  429. )
  430. except Exception as e:
  431. spark.post_to_spark(
  432. C.WEBEX_TEAM, SPARK_ROOM, "Failed to delete reservation for {}: {}".format(m.group(2)), MessageType.BAD
  433. )
  434. m = re.search(r"(make|create|add)\s+reservation.*?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", txt, re.I)
  435. if not found_hit and m:
  436. found_hit = True
  437. res = check_for_reservation(m.group(2))
  438. if res is not None:
  439. spark.post_to_spark(
  440. C.WEBEX_TEAM, SPARK_ROOM, "_{}_ is already reserved by a client with MAC **{}**".format(m.group(2), res["mac"])
  441. )
  442. else:
  443. lres = check_for_lease(m.group(2))
  444. if lres is None:
  445. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "Did not find an existing lease for {}".format(m.group(2)))
  446. else:
  447. try:
  448. rres = check_for_reservation_by_mac(lres["mac"])
  449. if rres is not None:
  450. spark.post_to_spark(
  451. C.WEBEX_TEAM,
  452. SPARK_ROOM,
  453. "_{}_ already has a reservation for {} in scope {}.".format(lres["mac"], rres["ip"], lres["scope"]),
  454. )
  455. else:
  456. create_reservation(m.group(2), lres["mac"])
  457. spark.post_to_spark(
  458. C.WEBEX_TEAM, SPARK_ROOM, "Successfully added reservation for {}.".format(m.group(2)), MessageType.GOOD
  459. )
  460. except Exception as e:
  461. spark.post_to_spark(
  462. C.WEBEX_TEAM, SPARK_ROOM, "Failed to add reservation for {}: {}".format(m.group(2), e), MessageType.BAD
  463. )
  464. m = re.search(r"reservation.*?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", txt, re.I)
  465. if not found_hit and m:
  466. found_hit = True
  467. res = check_for_reservation(m.group(1))
  468. if res is not None:
  469. spark.post_to_spark(
  470. C.WEBEX_TEAM,
  471. SPARK_ROOM,
  472. "_{}_ is reserved by a client with MAC **{}** in scope **{}**.".format(m.group(1), res["mac"], res["scope"]),
  473. )
  474. else:
  475. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I did not find a reservation for {}.".format(m.group(1)))
  476. m = re.findall(r"\b([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\b", txt)
  477. if not found_hit and len(m) > 0:
  478. found_hit = True
  479. for hit in m:
  480. res = check_for_lease(hit)
  481. pires = get_from_pi(ip=hit)
  482. cmxres = None
  483. dnacres = None
  484. if res is not None:
  485. cmxres = get_from_cmx(mac=re.sub(r"(\d+,)+", "", res["mac"]))
  486. dnacres = get_from_dnac(mac=re.sub(r"(\d+,)+", "", res["mac"]))
  487. elif pires is not None:
  488. cmxres = get_from_cmx(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  489. dnacres = get_from_dnac(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  490. if res is not None:
  491. reserved = ""
  492. if "is-reserved" in res and res["is-reserved"]:
  493. reserved = " (Client has reserved this IP)"
  494. if re.search(r"available", res["state"]):
  495. port_info = res["relay-info"]["port"]
  496. if port_info != "N/A":
  497. port_info = '<a href="{}switchname={}&portname={}">**{}**</a>'.format(
  498. C.TOOL_BASE,
  499. "-".join(res["relay-info"]["switch"].split("-")[:-1]),
  500. res["relay-info"]["port"],
  501. res["relay-info"]["port"],
  502. )
  503. spark.post_to_spark(
  504. C.WEBEX_TEAM,
  505. SPARK_ROOM,
  506. "_{}_ is no longer leased, but _WAS_ leased by a client with name **{}** and MAC **{}** in scope **{}** (state: **{}**) and was connected to switch **{}** on port {} in VLAN **{}**{}.".format(
  507. hit,
  508. res["name"],
  509. res["mac"],
  510. res["scope"],
  511. res["state"],
  512. res["relay-info"]["switch"],
  513. port_info,
  514. res["relay-info"]["vlan"],
  515. reserved,
  516. ),
  517. )
  518. else:
  519. port_info = res["relay-info"]["port"]
  520. if port_info != "N/A":
  521. port_info = '<a href="{}switchname={}&portname={}">**{}**</a>'.format(
  522. C.TOOL_BASE,
  523. "-".join(res["relay-info"]["switch"].split("-")[:-1]),
  524. res["relay-info"]["port"],
  525. res["relay-info"]["port"],
  526. )
  527. spark.post_to_spark(
  528. C.WEBEX_TEAM,
  529. SPARK_ROOM,
  530. "_{}_ is leased by a client with name **{}** and MAC **{}** in scope **{}** (state: **{}**) and is connected to switch **{}** on port {} in VLAN **{}**{}.".format(
  531. hit,
  532. res["name"],
  533. res["mac"],
  534. res["scope"],
  535. res["state"],
  536. res["relay-info"]["switch"],
  537. port_info,
  538. res["relay-info"]["vlan"],
  539. reserved,
  540. ),
  541. )
  542. if pires is not None:
  543. print_pi(spark, hit, pires, "I also found this from Prime Infra:")
  544. if dnacres is not None:
  545. print_dnac(spark, hit, dnacres, "I also found this from Cisco DNA Center:")
  546. if cmxres is not None:
  547. spark.post_to_spark_with_attach(
  548. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit), "image/jpeg"
  549. )
  550. else:
  551. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I did not find a lease for {}.".format(hit))
  552. if pires is not None:
  553. print_pi(spark, hit, pires, "But I did get this from Prime Infra:")
  554. if dnacres is not None:
  555. print_dnac(spark, hit, dnacres, "But I did get this from Cisco DNA Center:")
  556. if cmxres is not None:
  557. spark.post_to_spark_with_attach(
  558. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit), "image/jpeg"
  559. )
  560. m = re.findall(
  561. "\\b(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){,6}[0-9A-Fa-f]{1,4})?::)\\b",
  562. txt,
  563. )
  564. if not found_hit and len(m) > 0:
  565. found_hit = True
  566. for hit in m:
  567. pires = get_from_pi(ip=hit)
  568. if pires is not None:
  569. print_pi(spark, hit, pires, "")
  570. dnacres = get_from_dnac(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  571. cmxres = get_from_cmx(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  572. if dnacres is not None:
  573. print_dnac(spark, hit, dnacres, "")
  574. if cmxres is not None:
  575. spark.post_to_spark_with_attach(
  576. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit), "image/jpeg"
  577. )
  578. else:
  579. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I did not find anything about {} in Prime Infra.".format(hit))
  580. m = re.findall(
  581. r"\b(([a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2})|([a-fA-F0-9]{4}\.[a-fA-F0-9]{4}\.[a-fA-F0-9]{4})|([a-fA-F0-9]{1,2}-[a-fA-F0-9]{1,2}-[a-fA-F0-9]{1,2}-[a-fA-F0-9]{1,2}-[a-fA-F0-9]{1,2}-[a-fA-F0-9]{1,2}))\b",
  582. txt,
  583. )
  584. if not found_hit and len(m) > 0:
  585. found_hit = True
  586. for hit in m:
  587. hmac = normalize_mac(hit[0])
  588. leases = check_for_mac(hmac)
  589. pires = get_from_pi(mac=hmac)
  590. cmxres = get_from_cmx(mac=re.sub(r"(\d+,)+", "", hmac))
  591. dnacres = get_from_dnac(mac=re.sub(r"(\d+,)+", "", hmac))
  592. if leases is not None:
  593. seen_ip = {}
  594. for res in leases:
  595. if res["ip"] in seen_ip:
  596. continue
  597. reserved = ""
  598. if "is-reserved" in res and res["is-reserved"]:
  599. reserved = " (Client has reserved this IP)"
  600. seen_ip[res["ip"]] = True
  601. if re.search(r"available", res["state"]):
  602. spark.post_to_spark(
  603. C.WEBEX_TEAM,
  604. SPARK_ROOM,
  605. "Client with MAC _{}_ no longer has a lease, but _USED TO HAVE_ lease **{}** (hostname: **{}**) in scope **{}** (state: **{}**) and was connected to switch **{}** on port **{}** in VLAN **{}**{}.".format(
  606. hit[0],
  607. res["ip"],
  608. res["name"],
  609. res["scope"],
  610. res["state"],
  611. res["relay-info"]["switch"],
  612. res["relay-info"]["port"],
  613. res["relay-info"]["vlan"],
  614. reserved,
  615. ),
  616. )
  617. else:
  618. spark.post_to_spark(
  619. C.WEBEX_TEAM,
  620. SPARK_ROOM,
  621. "Client with MAC _{}_ has lease **{}** (hostname: **{}**) in scope **{}** (state: **{}**) and is connected to switch **{}** on port **{}** in VLAN **{}**{}.".format(
  622. hit[0],
  623. res["ip"],
  624. res["name"],
  625. res["scope"],
  626. res["state"],
  627. res["relay-info"]["switch"],
  628. res["relay-info"]["port"],
  629. res["relay-info"]["vlan"],
  630. reserved,
  631. ),
  632. )
  633. if pires is not None:
  634. # spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, '```\n{}\n```'.format(json.dumps(pires, indent=4)))
  635. print_pi(spark, hit[0], pires, "I also found this from Prime Infra:")
  636. if dnacres is not None:
  637. print_dnac(spark, hit[0], dnacres, "I also found this fron Cisco DNA Center:")
  638. if cmxres is not None:
  639. spark.post_to_spark_with_attach(
  640. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit[0]), "image/jpeg"
  641. )
  642. else:
  643. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I did not find a lease for {}.".format(hit[0]))
  644. if pires is not None:
  645. print_pi(spark, hit[0], pires, "But I did get this from Prime Infra:")
  646. if dnacres is not None:
  647. print_dnac(spark, hit[0], dnacres, "But I did get this from Cisco DNA Center:")
  648. if cmxres is not None:
  649. spark.post_to_spark_with_attach(
  650. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit[0]), "image/jpeg"
  651. )
  652. m = re.search(r"answer", txt, re.I)
  653. if not found_hit and m:
  654. found_hit = True
  655. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "The answer is 42.")
  656. m = re.findall(r"([\w\d\-\.]+)", txt)
  657. if not found_hit and len(m) > 0:
  658. found_hit = False
  659. for hit in m:
  660. ip = None
  661. try:
  662. ip = socket.gethostbyname(hit)
  663. except:
  664. pass
  665. if ip:
  666. res = check_for_lease(ip)
  667. pires = get_from_pi(ip=ip)
  668. if res is not None:
  669. reserved = ""
  670. if "is-reserved" in res and res["is-reserved"]:
  671. reserved = " (Client has reserved this IP)"
  672. if re.search(r"available", res["state"]):
  673. found_hit = True
  674. spark.post_to_spark(
  675. C.WEBEX_TEAM,
  676. SPARK_ROOM,
  677. "Client with hostname _{}_ no longer has a lease, but _USED TO HAVE_ lease **{}** (hostname: **{}**) in scope **{}** (state: **{}**) and was connected to switch **{}** on port **{}** in VLAN **{}**{}.".format(
  678. hit,
  679. ip,
  680. res["name"],
  681. res["scope"],
  682. res["state"],
  683. res["relay-info"]["switch"],
  684. res["relay-info"]["port"],
  685. res["relay-info"]["vlan"],
  686. reserved,
  687. ),
  688. )
  689. else:
  690. found_hit = True
  691. spark.post_to_spark(
  692. C.WEBEX_TEAM,
  693. SPARK_ROOM,
  694. "Client with hostname _{}_ has lease **{}** (hostname: **{}**) in scope **{}** (state: **{}**) and is connected to switch **{}** on port **{}** in VLAN **{}**.".format(
  695. hit,
  696. ip,
  697. res["name"],
  698. res["scope"],
  699. res["state"],
  700. res["relay-info"]["switch"],
  701. res["relay-info"]["port"],
  702. res["relay-info"]["vlan"],
  703. ),
  704. )
  705. if pires is not None:
  706. found_hit = True
  707. # spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, '```\n{}\n```'.format(json.dumps(pires, indent=4)))
  708. print_pi(spark, hit, pires, "I also found this from Prime Infra:")
  709. dnacres = get_from_dnac(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  710. cmxres = get_from_cmx(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  711. if dnacres is not None:
  712. print_dnac(spark, hit, dnacres, "I also found this from Cisco DNA Center:")
  713. if cmxres is not None:
  714. spark.post_to_spark_with_attach(
  715. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit), "image/jpeg"
  716. )
  717. else:
  718. found_hit = True
  719. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I did not find a lease for {}.".format(hit))
  720. if pires is not None:
  721. print_pi(spark, hit, pires, "But I did get this from Prime Infra:")
  722. dnacres = get_from_dnac(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  723. cmxres = get_from_cmx(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  724. if dnacres is not None:
  725. print_dnac(spark, hit, dnacres, "But I did get this from Cisco DNA Center:")
  726. if cmxres is not None:
  727. spark.post_to_spark_with_attach(
  728. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit), "image/jpeg"
  729. )
  730. if not found_hit:
  731. spark.post_to_spark(
  732. C.WEBEX_TEAM,
  733. SPARK_ROOM,
  734. 'Sorry, I didn\'t get that. Please give me a MAC or IP (or "reservation IP" or "user USER") or just ask for "help".',
  735. )
  736. except Exception as e:
  737. logging.error("Error in obtaining data: {}".format(traceback.format_exc()))
  738. spark.post_to_spark(
  739. C.WEBEX_TEAM, SPARK_ROOM, "Whoops, I encountered an error:<br>\n```\n{}\n```".format(traceback.format_exc()), MessageType.BAD
  740. )
  741. print('{"result":"success"}')