dhcp-hook.py 38 KB

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