dhcp-hook.py 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084
  1. #!/usr/bin/env python
  2. #
  3. # Copyright (c) 2017-2023 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 # type: ignore
  31. import re
  32. import requests
  33. from requests.packages.urllib3.exceptions import InsecureRequestWarning # type: ignore
  34. requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
  35. import time
  36. import traceback
  37. import socket
  38. import logging
  39. import CLEUCreds # type: ignore
  40. from cleu.config import Config as C # type: ignore
  41. AT_MACADDR = 9
  42. CNR_HEADERS = {"Accept": "application/json"}
  43. BASIC_AUTH = (CLEUCreds.CPNR_USERNAME, CLEUCreds.CPNR_PASSWORD)
  44. REST_TIMEOUT = 10
  45. DEFAULT_INT_TYPE = "GigabitEthernet"
  46. ALLOWED_TO_DELETE = ["jclarke@cisco.com", "josterfe@cisco.com", "anjesani@cisco.com"]
  47. def is_ascii(s):
  48. return all(ord(c) < 128 for c in s)
  49. def normalize_mac(mac):
  50. # Normalize all MAC addresses to colon-delimited format.
  51. mac_addr = "".join(l + ":" * (n % 2 == 1) for n, l in enumerate(list(re.sub(r"[:.-]", "", mac)))).strip(":")
  52. return mac_addr.lower()
  53. # TODO: We don't use CMX anymore. This needs to work with DNS Spaces?
  54. def get_from_cmx(**kwargs):
  55. marker = "green"
  56. if "user" in kwargs and kwargs["user"] == "gru":
  57. marker = "gru"
  58. elif "user" in kwargs:
  59. marker = kwargs["user"]
  60. if "ip" in kwargs:
  61. url = "{}?ip={}&marker={}&size=1440".format(C.CMX_GW, kwargs["ip"], marker)
  62. elif "mac" in kwargs:
  63. url = "{}?mac={}&marker={}&size=1440".format(C.CMX_GW, kwargs["mac"], marker)
  64. else:
  65. return None
  66. headers = {"Accept": "image/jpeg, application/json"}
  67. try:
  68. response = requests.request("GET", url, headers=headers, stream=True)
  69. response.raise_for_status()
  70. except Exception:
  71. logging.error("Encountered error getting data from cmx: {}".format(traceback.format_exc()))
  72. return None
  73. if response.headers.get("content-type") == "application/json":
  74. return None
  75. return response.raw.data
  76. def get_from_dnac(**kwargs):
  77. dna_obj = {
  78. "ostype": None,
  79. "type": None,
  80. "location": None,
  81. "ap": None,
  82. "ssid": None,
  83. "health": None,
  84. "onboard": None,
  85. "connect": None,
  86. "reason": None,
  87. "band": None,
  88. }
  89. for dnac in C.DNACS:
  90. curl = "https://{}/dna/intent/api/v1/client-detail".format(dnac)
  91. # Get timestamp with milliseconds
  92. # epoch = int(time.time() * 1000)
  93. turl = "https://{}/dna/system/api/v1/auth/token".format(dnac)
  94. theaders = {"content-type": "application/json"}
  95. try:
  96. response = requests.request("POST", turl, headers=theaders, auth=BASIC_AUTH, verify=False, timeout=REST_TIMEOUT)
  97. response.raise_for_status()
  98. except Exception as e:
  99. logging.warning("Unable to get an auth token from DNAC: {}".format(getattr(e, "message", repr(e))))
  100. continue
  101. j = response.json()
  102. if "Token" not in j:
  103. logging.warning(f"Failed to get a Token element from DNAC {dnac}: {response.text}")
  104. continue
  105. cheaders = {"accept": "application/json", "x-auth-token": j["Token"]}
  106. # params = {"macAddress": kwargs["mac"], "timestamp": epoch}
  107. params = {"macAddress": kwargs["mac"]}
  108. try:
  109. response = requests.request("GET", curl, params=params, headers=cheaders, verify=False)
  110. response.raise_for_status()
  111. except Exception as e:
  112. logging.warning("Failed to find MAC address {} in DNAC: {}".format(kwargs["mac"], getattr(e, "message", repr(e))))
  113. continue
  114. j = response.json()
  115. if "detail" not in j:
  116. logging.warning("Got an unknown response from DNAC: '{}'".format(response.text))
  117. continue
  118. if "errorCode" in j["detail"] or len(j["detail"].keys()) == 0:
  119. continue
  120. detail = j["detail"]
  121. if "hostType" in detail:
  122. dna_obj["type"] = detail["hostType"]
  123. if "userId" in detail:
  124. dna_obj["user"] = detail["userId"]
  125. if "hostOs" in detail and detail["hostOs"]:
  126. dna_obj["ostype"] = detail["hostOs"]
  127. elif "subType" in detail:
  128. dna_obj["ostype"] = detail["subType"]
  129. if "healthScore" in detail:
  130. for hscore in detail["healthScore"]:
  131. if hscore["healthType"] == "OVERALL":
  132. dna_obj["health"] = hscore["score"]
  133. if hscore["reason"] != "":
  134. dna_obj["reason"] = hscore["reason"]
  135. elif hscore["healthType"] == "ONBOARDED":
  136. dna_obj["onboard"] = hscore["score"]
  137. elif hscore["healthType"] == "CONNECTED":
  138. dna_obj["connect"] = hscore["score"]
  139. if "ssid" in detail:
  140. dna_obj["ssid"] = detail["ssid"]
  141. if "location" in detail:
  142. dna_obj["location"] = detail["location"]
  143. if "clientConnection" in detail:
  144. dna_obj["ap"] = detail["clientConnection"]
  145. if "connectionInfo" in j and "band" in j["connectionInfo"]:
  146. dna_obj["band"] = j["connectionInfo"]["band"]
  147. return dna_obj
  148. return None
  149. def get_user_from_dnac(**kwargs):
  150. dna_obj = {
  151. "ostype": None,
  152. "type": None,
  153. "location": None,
  154. "ap": None,
  155. "ssid": None,
  156. "health": None,
  157. "onboard": None,
  158. "connect": None,
  159. "reason": None,
  160. "band": None,
  161. "mac": None,
  162. }
  163. for dnac in C.DNACS:
  164. curl = "https://{}/dna/intent/api/v1/client-enrichment-details".format(dnac)
  165. turl = "https://{}/dna/system/api/v1/auth/token".format(dnac)
  166. theaders = {"content-type": "application/json"}
  167. try:
  168. response = requests.request("POST", turl, headers=theaders, auth=BASIC_AUTH, verify=False, timeout=REST_TIMEOUT)
  169. response.raise_for_status()
  170. except Exception as e:
  171. logging.warning("Unable to get an auth token from DNAC: {}".format(getattr(e, "message", repr(e))))
  172. continue
  173. j = response.json()
  174. if "Token" not in j:
  175. logging.warning(f"Failed to get a Token element from DNAC {dnac}: {response.text}")
  176. continue
  177. cheaders = {
  178. "accept": "application/json",
  179. "x-auth-token": j["Token"],
  180. "entity_type": "network_user_id",
  181. "entity_value": kwargs["user"],
  182. }
  183. try:
  184. response = requests.request("GET", curl, headers=cheaders, verify=False)
  185. response.raise_for_status()
  186. except Exception as e:
  187. logging.warning("Failed to find user {} in DNAC: {}".format(kwargs["user"], getattr(e, "message", repr(e))))
  188. continue
  189. j = response.json()
  190. if len(j) == 0 or "userDetails" not in j[0]:
  191. logging.warning("Got an unknown response from DNAC: '{}'".format(response.text))
  192. continue
  193. if len(j[0]["userDetails"].keys()) == 0:
  194. continue
  195. detail = j[0]["userDetails"]
  196. if "hostType" in detail:
  197. dna_obj["type"] = detail["hostType"]
  198. if "userId" in detail:
  199. dna_obj["user"] = detail["userId"]
  200. dna_obj["mac"] = detail["hostMac"]
  201. if "hostOs" in detail and detail["hostOs"]:
  202. dna_obj["ostype"] = detail["hostOs"]
  203. elif "subType" in detail:
  204. dna_obj["ostype"] = detail["subType"]
  205. if "healthScore" in detail:
  206. for hscore in detail["healthScore"]:
  207. if hscore["healthType"] == "OVERALL":
  208. dna_obj["health"] = hscore["score"]
  209. if hscore["reason"] != "":
  210. dna_obj["reason"] = hscore["reason"]
  211. elif hscore["healthType"] == "ONBOARDED":
  212. dna_obj["onboard"] = hscore["score"]
  213. elif hscore["healthType"] == "CONNECTED":
  214. dna_obj["connect"] = hscore["score"]
  215. if "ssid" in detail:
  216. dna_obj["ssid"] = detail["ssid"]
  217. if "location" in detail:
  218. dna_obj["location"] = detail["location"]
  219. if "clientConnection" in detail:
  220. dna_obj["ap"] = detail["clientConnection"]
  221. if "frequency" in detail:
  222. dna_obj["band"] = detail["frequency"]
  223. return dna_obj
  224. return None
  225. # TODO: We don't use PI anymore. Remove this in favor of DNAC.
  226. def get_from_pi(**kwargs):
  227. # what = None
  228. # if "user" in kwargs:
  229. # url = 'https://{}/webacs/api/v2/data/ClientDetails.json?.full=true&userName="{}"&status=ASSOCIATED'.format(C.PI, kwargs["user"])
  230. # what = "user"
  231. # elif "mac" in kwargs:
  232. # mac_addr = normalize_mac(kwargs["mac"])
  233. # url = 'https://{}/webacs/api/v2/data/ClientDetails.json?.full=true&macAddress="{}"&status=ASSOCIATED'.format(C.PI, mac_addr)
  234. # what = "mac"
  235. # elif "ip" in kwargs:
  236. # url = 'https://{}/webacs/api/v2/data/ClientDetails.json?.full=true&ipAddress="{}"&status=ASSOCIATED'.format(C.PI, kwargs["ip"])
  237. # what = "ip"
  238. # else:
  239. # return None
  240. # headers = {"Connection": "close"}
  241. # done = False
  242. # first = 0
  243. # code = 401
  244. # i = 0
  245. # while code != 200 and i < 10:
  246. # response = None
  247. # try:
  248. # response = requests.request("GET", url, auth=(CLEUCreds.PI_USER, CLEUCreds.PI_PASS), headers=headers, verify=False)
  249. # except Exception as e:
  250. # logging.error("Failed to get a response from PI for {}: {}".format(kwargs[what], e))
  251. # return None
  252. # code = response.status_code
  253. # if code != 200:
  254. # i += 1
  255. # time.sleep(3)
  256. # if code == 200:
  257. # j = json.loads(response.text)
  258. # if j["queryResponse"]["@count"] == 0:
  259. # return None
  260. # return j["queryResponse"]["entity"]
  261. # else:
  262. # logging.error("Failed to get a response from PI for {}: {}".format(kwargs[what], response.text))
  263. return None
  264. def parse_relay_info(outd):
  265. global DEFAULT_INT_TYPE
  266. res = {"vlan": "N/A", "port": "N/A", "switch": "N/A"}
  267. if "relayAgentCircuitId" in outd:
  268. octets = outd["relayAgentCircuitId"].split(":")
  269. if len(octets) > 4:
  270. res["vlan"] = int("".join(octets[2:4]), 16)
  271. first_part = int(octets[4], 16)
  272. port = str(first_part)
  273. if first_part != 0:
  274. port = str(first_part) + "/0"
  275. res["port"] = DEFAULT_INT_TYPE + port + "/" + str(int(octets[5], 16))
  276. if "relayAgentRemoteId" in outd:
  277. octets = outd["relayAgentRemoteId"].split(":")
  278. res["switch"] = bytes.fromhex("".join(octets[2:])).decode("utf-8", "ignore")
  279. if not is_ascii(res["switch"]) or res["switch"] == "":
  280. res["switch"] = "N/A"
  281. return res
  282. def check_for_reservation(ip):
  283. global CNR_HEADERS, BASIC_AUTH
  284. res = {}
  285. url = "{}/Reservation/{}".format(C.DHCP_BASE, ip)
  286. try:
  287. response = requests.request("GET", url, auth=BASIC_AUTH, headers=CNR_HEADERS, verify=False, timeout=REST_TIMEOUT)
  288. response.raise_for_status()
  289. except Exception as e:
  290. logging.warning("Did not get a good response from CNR for reservation {}: {}".format(ip, e))
  291. return None
  292. rsvp = response.json()
  293. res["mac"] = ":".join(rsvp["lookupKey"].split(":")[-6:])
  294. res["scope"] = rsvp["scope"]
  295. return res
  296. def check_for_reservation_by_mac(mac):
  297. global CNR_HEADERS, BASIC_AUTH
  298. res = {}
  299. mac_addr = normalize_mac(mac)
  300. url = "{}/Reservation".format(C.DHCP_BASE)
  301. try:
  302. response = requests.request(
  303. "GET", url, auth=BASIC_AUTH, headers=CNR_HEADERS, params={"lookupKey": mac_addr}, verify=False, timeout=REST_TIMEOUT
  304. )
  305. response.raise_for_status()
  306. except Exception as e:
  307. logging.warning("Did not get a good response from CNR for reservation {}: {}".format(ip, e))
  308. return None
  309. j = response.json()
  310. if len(j) == 0:
  311. return None
  312. rsvp = j[0]
  313. res["mac"] = ":".join(rsvp["lookupKey"].split(":")[-6:])
  314. res["scope"] = rsvp["scope"]
  315. return res
  316. def create_reservation(ip, mac):
  317. global CNR_HEADERS, BASIC_AUTH, AT_MACADDR
  318. mac_addr = normalize_mac(mac)
  319. url = "{}/Reservation".format(C.DHCP_BASE)
  320. payload = {"ipaddr": ip, "lookupKey": "01:06:" + mac_addr, "lookupKeyType": AT_MACADDR}
  321. response = requests.request("POST", url, auth=BASIC_AUTH, headers=CNR_HEADERS, json=payload, verify=False, timeout=REST_TIMEOUT)
  322. response.raise_for_status()
  323. def delete_reservation(ip):
  324. global CNR_HEADERS, BASIC_AUTH
  325. url = "{}/Reservation/{}".format(C.DHCP_BASE, ip)
  326. response = requests.request("DELETE", url, auth=BASIC_AUTH, headers=CNR_HEADERS, verify=False, timeout=REST_TIMEOUT)
  327. response.raise_for_status()
  328. def check_for_lease(ip):
  329. global CNR_HEADERS, BASIC_AUTH
  330. res = {}
  331. url = "{}/Lease/{}".format(C.DHCP_BASE, ip)
  332. try:
  333. response = requests.request("GET", url, auth=BASIC_AUTH, headers=CNR_HEADERS, verify=False, timeout=REST_TIMEOUT)
  334. response.raise_for_status()
  335. except Exception as e:
  336. logging.warning("Did not get a good response from CNR for IP {}: {}".format(ip, e))
  337. return None
  338. lease = response.json()
  339. if not "clientMacAddr" in lease:
  340. return None
  341. relay = parse_relay_info(lease)
  342. if "clientHostName" in lease:
  343. res["name"] = lease["clientHostName"]
  344. elif "client-dns-name" in lease:
  345. res["name"] = lease["clientDnsName"]
  346. else:
  347. res["name"] = "UNKNOWN"
  348. res["mac"] = lease["clientMacAddr"][lease["clientMacAddr"].rfind(",") + 1 :]
  349. res["scope"] = lease["scopeName"]
  350. res["state"] = lease["state"]
  351. res["relay-info"] = relay
  352. rsvp = check_for_reservation(ip)
  353. if rsvp and rsvp["mac"] == res["mac"]:
  354. res["is-reserved"] = True
  355. return res
  356. def check_for_mac(mac):
  357. global CNR_HEADERS, BASIC_AUTH
  358. url = "{}/Lease".format(C.DHCP_BASE)
  359. try:
  360. response = requests.request(
  361. "GET", url, auth=BASIC_AUTH, headers=CNR_HEADERS, verify=False, params={"clientMacAddr": mac}, timeout=REST_TIMEOUT
  362. )
  363. response.raise_for_status()
  364. except Exception as e:
  365. logging.warning("Did not get a good response from CPNR for MAC {}: {}".format(mac, e))
  366. return None
  367. j = response.json()
  368. if len(j) == 0:
  369. return None
  370. leases = []
  371. for lease in j:
  372. res = {}
  373. if "address" not in lease:
  374. continue
  375. relay = parse_relay_info(lease)
  376. res["ip"] = lease["address"]
  377. if "clientHostName" in lease:
  378. res["name"] = lease["clientHostName"]
  379. elif "clientDnsName" in lease:
  380. res["name"] = lease["clientDnsName"]
  381. else:
  382. res["name"] = "UNKNOWN"
  383. res["scope"] = lease["scopeName"]
  384. res["state"] = lease["state"]
  385. res["relay-info"] = relay
  386. rsvp = check_for_reservation(res["ip"])
  387. if rsvp and rsvp["mac"] == mac:
  388. res["is-reserved"] = True
  389. leases.append(res)
  390. return leases
  391. def print_dnac(spark, what, dna_obj, msg):
  392. host_info = ""
  393. ssid = ""
  394. loc = ""
  395. hinfo = ""
  396. sdetails = ""
  397. if dna_obj["health"]:
  398. hinfo = f"with health score **{dna_obj['health']}/10**"
  399. if dna_obj["reason"]:
  400. hinfo += f" (reason: _{dna_obj['reason']}_)"
  401. hinfo += " ["
  402. for h in ("onboard", "connect"):
  403. if dna_obj[h]:
  404. hinfo += f"{h.upper()} health: {dna_obj[h]} "
  405. hinfo = hinfo.rstrip() + "]"
  406. if dna_obj["ostype"]:
  407. host_info = f"running **{dna_obj['ostype']}**"
  408. if dna_obj["ssid"]:
  409. ssid = f"associated to SSID **{dna_obj['ssid']}**"
  410. if dna_obj["location"]:
  411. loc = f"located in **{dna_obj['location']}**"
  412. if dna_obj["ap"]:
  413. sdetails = f"connected to AP **{dna_obj['ap']}**"
  414. if dna_obj["band"]:
  415. sdetails += f" at **{dna_obj['band']} GHz**"
  416. dna_msg = f"{msg} {what} is a {dna_obj['type']} client {sdetails} {ssid} {loc} {host_info} {hinfo}"
  417. spark.post_to_spark(
  418. C.WEBEX_TEAM,
  419. SPARK_ROOM,
  420. dna_msg,
  421. )
  422. def print_pi(spark, what, ents, msg):
  423. for ent in ents:
  424. res = ent["clientDetailsDTO"]
  425. apdet = ""
  426. condet = ""
  427. vendet = ""
  428. if "apName" in res:
  429. apdet = "**{}** via ".format(res["apName"])
  430. if "connectionType" in res:
  431. condet = "is a **{}** client".format(res["connectionType"])
  432. if "vendor" in res:
  433. vendet = "of vendor type **{}**".format(res["vendor"])
  434. spark.post_to_spark(
  435. C.WEBEX_TEAM,
  436. SPARK_ROOM,
  437. "{} {} {} {}, connected to {}**{}** on interface **{}** with MAC address **{}** and IP address **{}** in **VLAN {}** located in **{}**.".format(
  438. msg,
  439. what,
  440. condet,
  441. vendet,
  442. apdet,
  443. res["deviceName"],
  444. res["clientInterface"],
  445. res["macAddress"],
  446. res["ipAddress"]["address"],
  447. res["vlan"],
  448. res["location"],
  449. ),
  450. )
  451. spark = Sparker(token=CLEUCreds.SPARK_TOKEN, logit=True)
  452. SPARK_ROOM = "DHCP Queries"
  453. if __name__ == "__main__":
  454. print("Content-type: application/json\r\n\r\n")
  455. output = sys.stdin.read()
  456. j = json.loads(output)
  457. logging.basicConfig(
  458. format="%(asctime)s - %(name)s - %(levelname)s : %(message)s", filename="/var/log/dhcp-hook.log", level=logging.DEBUG
  459. )
  460. logging.debug(json.dumps(j, indent=4))
  461. message_from = j["data"]["personEmail"]
  462. if message_from == "livenocbot@sparkbot.io":
  463. logging.debug("Person email is our bot")
  464. print('{"result":"success"}')
  465. sys.exit(0)
  466. tid = spark.get_team_id(C.WEBEX_TEAM)
  467. if tid is None:
  468. logging.error("Failed to get Spark Team ID")
  469. print('{"result":"fail"}')
  470. sys.exit(0)
  471. rid = spark.get_room_id(tid, SPARK_ROOM)
  472. if rid is None:
  473. logging.error("Failed to get Spark Room ID")
  474. print('{"result":"fail"}')
  475. sys.exit(0)
  476. if rid != j["data"]["roomId"]:
  477. logging.error("Spark Room ID is not the same as in the message ({} vs. {})".format(rid, j["data"]["roomId"]))
  478. print('{"result":"fail"}')
  479. sys.exit(0)
  480. mid = j["data"]["id"]
  481. msg = spark.get_message(mid)
  482. if msg is None:
  483. logging.error("Did not get a message")
  484. print('{"result":"error"}')
  485. sys.exit(0)
  486. person = spark.get_person(j["data"]["personId"])
  487. if person is not None:
  488. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "Hey, {}. Working on that for you...".format(person["nickName"]))
  489. else:
  490. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "Working on that for you...")
  491. txt = msg["text"]
  492. found_hit = False
  493. if re.search(r"\bhelp\b", txt, re.I):
  494. spark.post_to_spark(
  495. C.WEBEX_TEAM,
  496. SPARK_ROOM,
  497. '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`.',
  498. )
  499. found_hit = True
  500. try:
  501. m = re.search(r"user(name)?\s+\b(?P<uname>[A-Za-z][\w\-\.\d]+)([\s\?\.]|$)", txt, re.I)
  502. if not found_hit and not m:
  503. m = re.search(r"(who|where)\s+is\s+\b(?P<uname>[A-Za-z][\w\-\.\d]+)([\s\?\.]|$)", txt, re.I)
  504. if not found_hit and m:
  505. found_hit = True
  506. uname = m.group("uname")
  507. usecret = ""
  508. if re.search(r"gru", m.group("uname"), re.I):
  509. uname = "rkamerma"
  510. usecret = "gru"
  511. if uname == "pacman":
  512. with open("pacman.gif", "rb") as fd:
  513. cmxres = fd.read()
  514. spark.post_to_spark_with_attach(
  515. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.gif".format(uname), "image/gif"
  516. )
  517. else:
  518. res = get_user_from_dnac(user=uname)
  519. if res is None:
  520. res = get_user_from_dnac(user=uname + "@{}".format(C.AD_DOMAIN))
  521. if res is not None:
  522. print_dnac(spark, uname, res, "")
  523. hmac = normalize_mac(res["mac"])
  524. leases = check_for_mac(hmac)
  525. cmxres = get_from_cmx(mac=hmac, user=uname)
  526. if leases is not None:
  527. seen_ip = {}
  528. for lres in leases:
  529. if lres["ip"] in seen_ip:
  530. continue
  531. reserved = ""
  532. if "is-reserved" in lres and lres["is-reserved"]:
  533. reserved = " (Client has reserved this IP)"
  534. seen_ip[lres["ip"]] = True
  535. if re.search(r"available", lres["state"]):
  536. port_info = lres["relay-info"]["port"]
  537. if port_info != "N/A":
  538. port_info = '<a href="{}switchname={}&portname={}">**{}**</a>'.format(
  539. C.TOOL_BASE,
  540. "-".join(lres["relay-info"]["switch"].split("-")[:-1]),
  541. lres["relay-info"]["port"],
  542. lres["relay-info"]["port"],
  543. )
  544. spark.post_to_spark(
  545. C.WEBEX_TEAM,
  546. SPARK_ROOM,
  547. "User {} has MAC _{}_ and no longer has a lease, but _USED TO HAVE_ lease **{}** (hostname: **{}**) in scope **{}** (state: **{}**) and was connected to switch **{}** on port {} in VLAN **{}**{}.".format(
  548. uname,
  549. res["mac"],
  550. lres["ip"],
  551. lres["name"],
  552. lres["scope"],
  553. lres["state"],
  554. lres["relay-info"]["switch"],
  555. port_info,
  556. lres["relay-info"]["vlan"],
  557. reserved,
  558. ),
  559. )
  560. else:
  561. port_info = lres["relay-info"]["port"]
  562. if port_info != "N/A":
  563. port_info = '<a href="{}switchname={}&portname={}">**{}**</a>'.format(
  564. C.TOOL_BASE,
  565. "-".join(lres["relay-info"]["switch"].split("-")[:-1]),
  566. lres["relay-info"]["port"],
  567. lres["relay-info"]["port"],
  568. )
  569. spark.post_to_spark(
  570. C.WEBEX_TEAM,
  571. SPARK_ROOM,
  572. "User {} with MAC _{}_ has lease **{}** (hostname: **{}**) in scope **{}** (state: **{}**) and is connected to switch **{}** on port {} in VLAN **{}**{}.".format(
  573. uname,
  574. res["mac"],
  575. lres["ip"],
  576. lres["name"],
  577. lres["scope"],
  578. lres["state"],
  579. lres["relay-info"]["switch"],
  580. port_info,
  581. lres["relay-info"]["vlan"],
  582. reserved,
  583. ),
  584. )
  585. if cmxres:
  586. spark.post_to_spark_with_attach(
  587. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(uname), "image/jpeg"
  588. )
  589. else:
  590. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "Sorry, I can't find {}.".format(m.group("uname")))
  591. m = re.search(r"(remove|delete)\s+(the\s+)?reservation.*?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", txt, re.I)
  592. if not m:
  593. m = re.search(r"(unreserve)(.*?)([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", txt, re.I)
  594. if not found_hit and m:
  595. found_hit = True
  596. if message_from not in ALLOWED_TO_DELETE:
  597. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I'm sorry, {}. I can't do that for you.".format(message_from))
  598. else:
  599. res = check_for_reservation(m.group(3))
  600. if res is None:
  601. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I didn't find a reservation for {}.".format(m.group(3)))
  602. else:
  603. try:
  604. delete_reservation(m.group(3))
  605. spark.post_to_spark(
  606. C.WEBEX_TEAM, SPARK_ROOM, "Reservation for {} deleted successfully.".format(m.group(3)), MessageType.GOOD
  607. )
  608. except Exception as e:
  609. spark.post_to_spark(
  610. C.WEBEX_TEAM, SPARK_ROOM, "Failed to delete reservation for {}: {}".format(m.group(3)), MessageType.BAD
  611. )
  612. m = re.search(r"(make|create|add)\s+(a\s+)?reservation.*?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", txt, re.I)
  613. if not found_hit and m:
  614. found_hit = True
  615. res = check_for_reservation(m.group(3))
  616. if res is not None:
  617. spark.post_to_spark(
  618. C.WEBEX_TEAM, SPARK_ROOM, "_{}_ is already reserved by a client with MAC **{}**".format(m.group(3), res["mac"])
  619. )
  620. else:
  621. lres = check_for_lease(m.group(3))
  622. if lres is None:
  623. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "Did not find an existing lease for {}".format(m.group(3)))
  624. else:
  625. try:
  626. rres = check_for_reservation_by_mac(lres["mac"])
  627. if rres is not None:
  628. spark.post_to_spark(
  629. C.WEBEX_TEAM,
  630. SPARK_ROOM,
  631. "_{}_ already has a reservation for {} in scope {}.".format(lres["mac"], rres["ip"], lres["scope"]),
  632. )
  633. else:
  634. create_reservation(m.group(3), lres["mac"])
  635. spark.post_to_spark(
  636. C.WEBEX_TEAM, SPARK_ROOM, "Successfully added reservation for {}.".format(m.group(3)), MessageType.GOOD
  637. )
  638. except Exception as e:
  639. spark.post_to_spark(
  640. C.WEBEX_TEAM, SPARK_ROOM, "Failed to add reservation for {}: {}".format(m.group(3), e), MessageType.BAD
  641. )
  642. m = re.search(r"reservation.*?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", txt, re.I)
  643. if not found_hit and m:
  644. found_hit = True
  645. res = check_for_reservation(m.group(1))
  646. if res is not None:
  647. spark.post_to_spark(
  648. C.WEBEX_TEAM,
  649. SPARK_ROOM,
  650. "_{}_ is reserved by a client with MAC **{}** in scope **{}**.".format(m.group(1), res["mac"], res["scope"]),
  651. )
  652. else:
  653. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I did not find a reservation for {}.".format(m.group(1)))
  654. m = re.findall(r"\b([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\b", txt)
  655. if not found_hit and len(m) > 0:
  656. found_hit = True
  657. for hit in m:
  658. res = check_for_lease(hit)
  659. pires = get_from_pi(ip=hit)
  660. cmxres = None
  661. dnacres = None
  662. if res is not None:
  663. cmxres = get_from_cmx(mac=re.sub(r"(\d+,)+", "", res["mac"]))
  664. dnacres = get_from_dnac(mac=re.sub(r"(\d+,)+", "", res["mac"]))
  665. elif pires is not None:
  666. cmxres = get_from_cmx(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  667. dnacres = get_from_dnac(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  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. port_info = res["relay-info"]["port"]
  674. if port_info != "N/A":
  675. port_info = '<a href="{}switchname={}&portname={}">**{}**</a>'.format(
  676. C.TOOL_BASE,
  677. "-".join(res["relay-info"]["switch"].split("-")[:-1]),
  678. res["relay-info"]["port"],
  679. res["relay-info"]["port"],
  680. )
  681. spark.post_to_spark(
  682. C.WEBEX_TEAM,
  683. SPARK_ROOM,
  684. "_{}_ 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(
  685. hit,
  686. res["name"],
  687. res["mac"],
  688. res["scope"],
  689. res["state"],
  690. res["relay-info"]["switch"],
  691. port_info,
  692. res["relay-info"]["vlan"],
  693. reserved,
  694. ),
  695. )
  696. else:
  697. port_info = res["relay-info"]["port"]
  698. if port_info != "N/A":
  699. port_info = '<a href="{}switchname={}&portname={}">**{}**</a>'.format(
  700. C.TOOL_BASE,
  701. "-".join(res["relay-info"]["switch"].split("-")[:-1]),
  702. res["relay-info"]["port"],
  703. res["relay-info"]["port"],
  704. )
  705. spark.post_to_spark(
  706. C.WEBEX_TEAM,
  707. SPARK_ROOM,
  708. "_{}_ is leased by a client with name **{}** and MAC **{}** in scope **{}** (state: **{}**) and is connected to switch **{}** on port {} in VLAN **{}**{}.".format(
  709. hit,
  710. res["name"],
  711. res["mac"],
  712. res["scope"],
  713. res["state"],
  714. res["relay-info"]["switch"],
  715. port_info,
  716. res["relay-info"]["vlan"],
  717. reserved,
  718. ),
  719. )
  720. if pires is not None:
  721. print_pi(spark, hit, pires, "I also found this from Prime Infra:")
  722. if dnacres is not None:
  723. print_dnac(spark, hit, dnacres, "I also found this from Cisco DNA Center:")
  724. if cmxres is not None:
  725. spark.post_to_spark_with_attach(
  726. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit), "image/jpeg"
  727. )
  728. else:
  729. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I did not find a lease for {}.".format(hit))
  730. if pires is not None:
  731. print_pi(spark, hit, pires, "But I did get this from Prime Infra:")
  732. if dnacres is not None:
  733. print_dnac(spark, hit, dnacres, "But I did get this from Cisco DNA Center:")
  734. if cmxres is not None:
  735. spark.post_to_spark_with_attach(
  736. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit), "image/jpeg"
  737. )
  738. m = re.findall(
  739. "\\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",
  740. txt,
  741. )
  742. if not found_hit and len(m) > 0:
  743. found_hit = True
  744. for hit in m:
  745. pires = get_from_pi(ip=hit)
  746. if pires is not None:
  747. print_pi(spark, hit, pires, "")
  748. dnacres = get_from_dnac(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  749. cmxres = get_from_cmx(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  750. if dnacres is not None:
  751. print_dnac(spark, hit, dnacres, "")
  752. if cmxres is not None:
  753. spark.post_to_spark_with_attach(
  754. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit), "image/jpeg"
  755. )
  756. else:
  757. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I did not find anything about {} in Prime Infra.".format(hit))
  758. m = re.findall(
  759. 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",
  760. txt,
  761. )
  762. if not found_hit and len(m) > 0:
  763. found_hit = True
  764. for hit in m:
  765. hmac = normalize_mac(hit[0])
  766. leases = check_for_mac(hmac)
  767. pires = get_from_pi(mac=hmac)
  768. cmxres = get_from_cmx(mac=re.sub(r"(\d+,)+", "", hmac))
  769. dnacres = get_from_dnac(mac=re.sub(r"(\d+,)+", "", hmac))
  770. if leases is not None:
  771. seen_ip = {}
  772. for res in leases:
  773. if res["ip"] in seen_ip:
  774. continue
  775. reserved = ""
  776. if "is-reserved" in res and res["is-reserved"]:
  777. reserved = " (Client has reserved this IP)"
  778. seen_ip[res["ip"]] = True
  779. if re.search(r"available", res["state"]):
  780. port_info = res["relay-info"]["port"]
  781. if port_info != "N/A":
  782. port_info = '<a href="{}switchname={}&portname={}">**{}**</a>'.format(
  783. C.TOOL_BASE,
  784. "-".join(res["relay-info"]["switch"].split("-")[:-1]),
  785. res["relay-info"]["port"],
  786. res["relay-info"]["port"],
  787. )
  788. spark.post_to_spark(
  789. C.WEBEX_TEAM,
  790. SPARK_ROOM,
  791. "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(
  792. hit[0],
  793. res["ip"],
  794. res["name"],
  795. res["scope"],
  796. res["state"],
  797. res["relay-info"]["switch"],
  798. port_info,
  799. res["relay-info"]["vlan"],
  800. reserved,
  801. ),
  802. )
  803. else:
  804. port_info = res["relay-info"]["port"]
  805. if port_info != "N/A":
  806. port_info = '<a href="{}switchname={}&portname={}">**{}**</a>'.format(
  807. C.TOOL_BASE,
  808. "-".join(res["relay-info"]["switch"].split("-")[:-1]),
  809. res["relay-info"]["port"],
  810. res["relay-info"]["port"],
  811. )
  812. spark.post_to_spark(
  813. C.WEBEX_TEAM,
  814. SPARK_ROOM,
  815. "Client with MAC _{}_ has lease **{}** (hostname: **{}**) in scope **{}** (state: **{}**) and is connected to switch **{}** on port {} in VLAN **{}**{}.".format(
  816. hit[0],
  817. res["ip"],
  818. res["name"],
  819. res["scope"],
  820. res["state"],
  821. res["relay-info"]["switch"],
  822. port_info,
  823. res["relay-info"]["vlan"],
  824. reserved,
  825. ),
  826. )
  827. if pires is not None:
  828. # spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, '```\n{}\n```'.format(json.dumps(pires, indent=4)))
  829. print_pi(spark, hit[0], pires, "I also found this from Prime Infra:")
  830. if dnacres is not None:
  831. print_dnac(spark, hit[0], dnacres, "I also found this fron Cisco DNA Center:")
  832. if cmxres is not None:
  833. spark.post_to_spark_with_attach(
  834. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit[0]), "image/jpeg"
  835. )
  836. else:
  837. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I did not find a lease for {}.".format(hit[0]))
  838. if pires is not None:
  839. print_pi(spark, hit[0], pires, "But I did get this from Prime Infra:")
  840. if dnacres is not None:
  841. print_dnac(spark, hit[0], dnacres, "But I did get this from Cisco DNA Center:")
  842. if cmxres is not None:
  843. spark.post_to_spark_with_attach(
  844. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit[0]), "image/jpeg"
  845. )
  846. m = re.search(r"answer", txt, re.I)
  847. if not found_hit and m:
  848. found_hit = True
  849. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "The answer is 42.")
  850. m = re.findall(r"([\w\d\-\.]+)", txt)
  851. if not found_hit and len(m) > 0:
  852. found_hit = False
  853. for hit in m:
  854. ip = None
  855. try:
  856. ip = socket.gethostbyname(hit)
  857. except:
  858. pass
  859. if ip:
  860. res = check_for_lease(ip)
  861. pires = get_from_pi(ip=ip)
  862. if res is not None:
  863. reserved = ""
  864. if "is-reserved" in res and res["is-reserved"]:
  865. reserved = " (Client has reserved this IP)"
  866. if re.search(r"available", res["state"]):
  867. found_hit = True
  868. spark.post_to_spark(
  869. C.WEBEX_TEAM,
  870. SPARK_ROOM,
  871. "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(
  872. hit,
  873. ip,
  874. res["name"],
  875. res["scope"],
  876. res["state"],
  877. res["relay-info"]["switch"],
  878. res["relay-info"]["port"],
  879. res["relay-info"]["vlan"],
  880. reserved,
  881. ),
  882. )
  883. else:
  884. found_hit = True
  885. spark.post_to_spark(
  886. C.WEBEX_TEAM,
  887. SPARK_ROOM,
  888. "Client with hostname _{}_ has lease **{}** (hostname: **{}**) in scope **{}** (state: **{}**) and is connected to switch **{}** on port **{}** in VLAN **{}**.".format(
  889. hit,
  890. ip,
  891. res["name"],
  892. res["scope"],
  893. res["state"],
  894. res["relay-info"]["switch"],
  895. res["relay-info"]["port"],
  896. res["relay-info"]["vlan"],
  897. ),
  898. )
  899. if pires is not None:
  900. found_hit = True
  901. # spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, '```\n{}\n```'.format(json.dumps(pires, indent=4)))
  902. print_pi(spark, hit, pires, "I also found this from Prime Infra:")
  903. dnacres = get_from_dnac(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  904. cmxres = get_from_cmx(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  905. if dnacres is not None:
  906. print_dnac(spark, hit, dnacres, "I also found this from Cisco DNA Center:")
  907. if cmxres is not None:
  908. spark.post_to_spark_with_attach(
  909. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit), "image/jpeg"
  910. )
  911. else:
  912. found_hit = True
  913. spark.post_to_spark(C.WEBEX_TEAM, SPARK_ROOM, "I did not find a lease for {}.".format(hit))
  914. if pires is not None:
  915. print_pi(spark, hit, pires, "But I did get this from Prime Infra:")
  916. dnacres = get_from_dnac(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  917. cmxres = get_from_cmx(mac=pires[0]["clientDetailsDTO"]["macAddress"])
  918. if dnacres is not None:
  919. print_dnac(spark, hit, dnacres, "But I did get this from Cisco DNA Center:")
  920. if cmxres is not None:
  921. spark.post_to_spark_with_attach(
  922. C.WEBEX_TEAM, SPARK_ROOM, "Location from CMX", cmxres, "{}_location.jpg".format(hit), "image/jpeg"
  923. )
  924. if not found_hit:
  925. spark.post_to_spark(
  926. C.WEBEX_TEAM,
  927. SPARK_ROOM,
  928. 'Sorry, I didn\'t get that. Please give me a MAC or IP (or "reservation IP" or "user USER") or just ask for "help".',
  929. )
  930. except Exception as e:
  931. logging.error("Error in obtaining data: {}".format(traceback.format_exc()))
  932. spark.post_to_spark(
  933. C.WEBEX_TEAM, SPARK_ROOM, "Whoops, I encountered an error:<br>\n```\n{}\n```".format(traceback.format_exc()), MessageType.BAD
  934. )
  935. print('{"result":"success"}')