diff-route-tables.py 7.4 KB

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