diff-route-tables.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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. import paramiko
  28. import os
  29. from sparker import Sparker, MessageType # type: ignore
  30. import time
  31. from subprocess import Popen, PIPE, call
  32. import shlex
  33. import re
  34. import json
  35. import argparse
  36. import CLEUCreds # type: ignore
  37. import shutil
  38. from cleu.config import Config as C # type: ignore
  39. routers = {}
  40. commands = {"ip_route": "show ip route", "ipv6_route": "show ipv6 route"}
  41. cache_dir = "/home/jclarke/routing-tables"
  42. # TODO: Integrate with NetBox to get edge routers
  43. ROUTER_FILE = "/home/jclarke/routers.json"
  44. WEBEX_ROOM = "Edge Routing Diffs"
  45. def send_command1(chan, command):
  46. chan.sendall(command + "\n")
  47. i = 0
  48. output = ""
  49. while i < 10:
  50. if chan.recv_ready():
  51. break
  52. i += 1
  53. time.sleep(i * 0.5)
  54. while chan.recv_ready():
  55. r = chan.recv(131070).decode("utf-8")
  56. output = output + r
  57. return output
  58. def send_command(chan, command):
  59. chan.sendall(command + "\n")
  60. time.sleep(0.5)
  61. output = ""
  62. i = 0
  63. while i < 60:
  64. r = chan.recv(65535)
  65. if len(r) == 0:
  66. raise EOFError("Remote host has closed the connection")
  67. r = r.decode("utf-8", "ignore")
  68. output += r
  69. if re.search(r"[#>]$", r.strip()):
  70. break
  71. time.sleep(1)
  72. return output
  73. if __name__ == "__main__":
  74. parser = argparse.ArgumentParser(description="Usage:")
  75. # script arguments
  76. parser.add_argument("--git-repo", "-g", metavar="<GIT_REPO_PATH>", help="Optional path to a git repo to store updates")
  77. parser.add_argument("--git-branch", "-b", metavar="<BRANCH_NAME>", help="Branch name to use to commit in git")
  78. parser.add_argument(
  79. "--notify",
  80. "-n",
  81. metavar="<ROUTER_NAME>",
  82. help="Only notify on routers with a given name (can be specified more than once)",
  83. action="append",
  84. )
  85. args = parser.parse_args()
  86. spark = Sparker(token=CLEUCreds.SPARK_TOKEN)
  87. ssh_client = paramiko.SSHClient()
  88. ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  89. try:
  90. fd = open(ROUTER_FILE, "r")
  91. routers = json.load(fd)
  92. fd.close()
  93. except Exception as e:
  94. print("ERROR: Failed to load routers file {}: {}".format(ROUTER_FILE, e))
  95. do_push = False
  96. for router, ip in list(routers.items()):
  97. try:
  98. ssh_client.connect(
  99. ip,
  100. username=CLEUCreds.NET_USER,
  101. password=CLEUCreds.NET_PASS,
  102. timeout=60,
  103. allow_agent=False,
  104. look_for_keys=False,
  105. )
  106. chan = ssh_client.invoke_shell()
  107. chan.settimeout(20)
  108. try:
  109. send_command(chan, "term length 0")
  110. send_command(chan, "term width 0")
  111. except:
  112. pass
  113. for fname, command in list(commands.items()):
  114. output = ""
  115. try:
  116. output = send_command(chan, command)
  117. except Exception as ie:
  118. print(f"Failed to get {command} from {router}: {ie}")
  119. continue
  120. fpath = f"{cache_dir}/{fname}-{router}"
  121. curr_path = fpath + ".curr"
  122. prev_path = fpath + ".prev"
  123. if len(output) < 600:
  124. # we got a truncated file
  125. continue
  126. with open(curr_path, "w") as fd:
  127. output = re.sub(r"\r", "", output)
  128. output = re.sub(r"([\d\.]+) (\[[^\n]+)", "\\1\n \\2", output)
  129. fd.write(re.sub(r"(via [\d\.]+), [^,\n]+([,\n])", "\\1\\2", output))
  130. if os.path.exists(prev_path):
  131. proc = Popen(
  132. shlex.split("/usr/bin/diff -b -B -w -u {} {}".format(prev_path, curr_path)),
  133. stdout=PIPE,
  134. stderr=PIPE,
  135. )
  136. out, err = proc.communicate()
  137. rc = proc.returncode
  138. if rc != 0:
  139. if (args.notify and router in args.notify) or not args.notify:
  140. spark.post_to_spark(
  141. C.WEBEX_TEAM,
  142. WEBEX_ROOM,
  143. "Routing table diff ({}) on **{}**:\n```\n{}\n```".format(
  144. command, router, re.sub(cache_dir + "/", "", out.decode("utf-8"))
  145. ),
  146. MessageType.BAD,
  147. )
  148. time.sleep(1)
  149. if args.git_repo:
  150. if os.path.isdir(args.git_repo):
  151. try:
  152. gfile = re.sub(r"\.curr", ".txt", os.path.basename(curr_path))
  153. shutil.copyfile(curr_path, args.git_repo + "/" + gfile)
  154. os.chdir(args.git_repo)
  155. call(f"git add {gfile}", shell=True)
  156. call(f'git commit -m "Routing table update" {gfile}', shell=True)
  157. do_push = True
  158. except Exception as ie:
  159. print(f"ERROR: Failed to commit to git repo {args.git_repo}: {ie}")
  160. else:
  161. print(f"ERROR: Git repo {args.git_repo} is not a directory")
  162. # print('XXX: Out = \'{}\''.format(out))
  163. os.rename(curr_path, prev_path)
  164. except Exception as e:
  165. ssh_client.close()
  166. print(f"Failed to get routing tables from {router}: {e}")
  167. continue
  168. ssh_client.close()
  169. if do_push:
  170. if not args.git_branch:
  171. print("ERROR: Cannot push without a branch")
  172. else:
  173. os.chdir(args.git_repo)
  174. call(f"git pull origin {args.git_branch}", shell=True)
  175. call(f"git push origin {args.git_branch}", shell=True)