ssh_autodetect.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. """
  2. The ssh_autodetect module is used to auto-detect the netmiko device_type to use to further initiate
  3. a new SSH connection with a remote host. This auto-detection is based on a unique class called
  4. **SSHDetect**.
  5. Notes
  6. -----
  7. The **SSHDetect** class is instantiated using the same parameters than a standard Netmiko
  8. connection (see the *netmiko.ssh_dispatacher.ConnectHandler* function). The only acceptable value
  9. for the 'device_type' argument is 'autodetect'.
  10. The auto-detection is solely based on the *SSH_MAPPER_BASE* dictionary. The keys are the name of
  11. the 'device_type' supported for auto-detection and the value is another dictionary describing how
  12. to handle the auto-detection.
  13. * "cmd" : The command to send to the remote device. **The command output must not require paging.**
  14. * "search_patterns" : A list of regex to compare with the output of the command
  15. * "priority" : An integer (0-99) which specifies the confidence of the match above
  16. * "dispatch" : The function to call to try the autodetection (per default SSHDetect._autodetect_std)
  17. Examples
  18. --------
  19. # Auto-detection section
  20. >>> from netmiko.ssh_autodetect import SSHDetect
  21. >>> from netmiko.ssh_dispatcher import ConnectHandler
  22. >>> remote_device = {'device_type': 'autodetect',
  23. 'host': 'remote.host',
  24. 'username': 'test',
  25. 'password': 'foo'}
  26. >>> guesser = SSHDetect(**remote_device)
  27. >>> best_match = guesser.autodetect()
  28. >>> print(best_match) # Name of the best device_type to use further
  29. >>> print(guesser.potential_matches) # Dictionary of the whole matching result
  30. # Netmiko connection creation section
  31. >>> remote_device['device_type'] = best_match
  32. >>> connection = ConnectHandler(**remote_device)
  33. """
  34. from __future__ import unicode_literals
  35. import re
  36. import time
  37. from netmiko.ssh_dispatcher import ConnectHandler
  38. from netmiko.base_connection import BaseConnection
  39. # 'dispatch' key is the SSHDetect method to call. dispatch key will be popped off dictionary
  40. # remaining keys indicate kwargs that will be passed to dispatch method.
  41. # Note, the 'cmd' needs to avoid output paging.
  42. SSH_MAPPER_BASE = {
  43. 'alcatel_aos': {
  44. "cmd": "show system",
  45. "search_patterns": ["Alcatel-Lucent"],
  46. "priority": 99,
  47. "dispatch": "_autodetect_std",
  48. },
  49. 'alcatel_sros': {
  50. "cmd": "show version | match TiMOS",
  51. "search_patterns": [
  52. "Nokia",
  53. "Alcatel",
  54. ],
  55. "priority": 99,
  56. "dispatch": "_autodetect_std",
  57. },
  58. 'arista_eos': {
  59. "cmd": "show version | inc rist",
  60. "search_patterns": ["Arista"],
  61. "priority": 99,
  62. "dispatch": "_autodetect_std",
  63. },
  64. 'cisco_ios': {
  65. "cmd": "show version | inc Cisco",
  66. "search_patterns": [
  67. "Cisco IOS Software",
  68. "Cisco Internetwork Operating System Software"
  69. ],
  70. "priority": 99,
  71. "dispatch": "_autodetect_std",
  72. },
  73. 'cisco_asa': {
  74. "cmd": "show version | inc Cisco",
  75. "search_patterns": ["Cisco Adaptive Security Appliance", "Cisco ASA"],
  76. "priority": 99,
  77. "dispatch": "_autodetect_std",
  78. },
  79. 'cisco_nxos': {
  80. "cmd": "show version | inc Cisco",
  81. "search_patterns": ["Cisco Nexus Operating System", "NX-OS"],
  82. "priority": 99,
  83. "dispatch": "_autodetect_std",
  84. },
  85. 'cisco_xr': {
  86. "cmd": "show version | inc Cisco",
  87. "search_patterns": ["Cisco IOS XR"],
  88. "priority": 99,
  89. "dispatch": "_autodetect_std",
  90. },
  91. 'huawei': {
  92. "cmd": "display version | inc Huawei",
  93. "search_patterns": [
  94. "Huawei Technologies",
  95. "Huawei Versatile Routing Platform Software"
  96. ],
  97. "priority": 99,
  98. "dispatch": "_autodetect_std",
  99. },
  100. 'juniper_junos': {
  101. "cmd": "show version | match JUNOS",
  102. "search_patterns": [
  103. "JUNOS Software Release",
  104. "JUNOS .+ Software",
  105. "JUNOS OS Kernel",
  106. ],
  107. "priority": 99,
  108. "dispatch": "_autodetect_std",
  109. },
  110. 'dell_force10': {
  111. "cmd": "show version | grep Type",
  112. "search_patterns": ["S4048-ON"],
  113. "priority": 99,
  114. "dispatch": "_autodetect_std",
  115. },
  116. }
  117. class SSHDetect(object):
  118. """
  119. The SSHDetect class tries to automatically guess the device type running on the SSH remote end.
  120. Be careful that the kwargs 'device_type' must be set to 'autodetect', otherwise it won't work at
  121. all.
  122. Parameters
  123. ----------
  124. *args : list
  125. The same *args that you might provide to the netmiko.ssh_dispatcher.ConnectHandler.
  126. *kwargs : dict
  127. The same *kwargs that you might provide to the netmiko.ssh_dispatcher.ConnectHandler.
  128. Attributes
  129. ----------
  130. connection : netmiko.terminal_server.TerminalServer
  131. A basic connection to the remote SSH end.
  132. potential_matches: dict
  133. Dict of (device_type, accuracy) that is populated through an interaction with the
  134. remote end.
  135. Methods
  136. -------
  137. autodetect()
  138. Try to determine the device type.
  139. """
  140. def __init__(self, *args, **kwargs):
  141. """
  142. Constructor of the SSHDetect class
  143. """
  144. if kwargs["device_type"] != "autodetect":
  145. raise ValueError("The connection device_type must be 'autodetect'")
  146. self.connection = ConnectHandler(*args, **kwargs)
  147. # Call the _test_channel_read() in base to clear initial data
  148. output = BaseConnection._test_channel_read(self.connection)
  149. self.initial_buffer = output
  150. self.potential_matches = {}
  151. self._results_cache = {}
  152. def autodetect(self):
  153. """
  154. Try to guess the best 'device_type' based on patterns defined in SSH_MAPPER_BASE
  155. Returns
  156. -------
  157. best_match : str or None
  158. The device type that is currently the best to use to interact with the device
  159. """
  160. for device_type, autodetect_dict in SSH_MAPPER_BASE.items():
  161. tmp_dict = autodetect_dict.copy()
  162. call_method = tmp_dict.pop("dispatch")
  163. autodetect_method = getattr(self, call_method)
  164. accuracy = autodetect_method(**tmp_dict)
  165. if accuracy:
  166. self.potential_matches[device_type] = accuracy
  167. if accuracy >= 99: # Stop the loop as we are sure of our match
  168. best_match = sorted(self.potential_matches.items(), key=lambda t: t[1],
  169. reverse=True)
  170. self.connection.disconnect()
  171. return best_match[0][0]
  172. if not self.potential_matches:
  173. self.connection.disconnect()
  174. return None
  175. best_match = sorted(self.potential_matches.items(), key=lambda t: t[1], reverse=True)
  176. self.connection.disconnect()
  177. return best_match[0][0]
  178. def _send_command(self, cmd=""):
  179. """
  180. Handle reading/writing channel directly. It is also sanitizing the output received.
  181. Parameters
  182. ----------
  183. cmd : str, optional
  184. The command to send to the remote device (default : "", just send a new line)
  185. Returns
  186. -------
  187. output : str
  188. The output from the command sent
  189. """
  190. self.connection.write_channel(cmd + "\n")
  191. time.sleep(1)
  192. output = self.connection._read_channel_timing()
  193. output = self.connection.strip_ansi_escape_codes(output)
  194. output = self.connection.strip_backspaces(output)
  195. return output
  196. def _send_command_wrapper(self, cmd):
  197. """
  198. Send command to the remote device with a caching feature to avoid sending the same command
  199. twice based on the SSH_MAPPER_BASE dict cmd key.
  200. Parameters
  201. ----------
  202. cmd : str
  203. The command to send to the remote device after checking cache.
  204. Returns
  205. -------
  206. response : str
  207. The response from the remote device.
  208. """
  209. cached_results = self._results_cache.get(cmd)
  210. if not cached_results:
  211. response = self._send_command(cmd)
  212. self._results_cache[cmd] = response
  213. return response
  214. else:
  215. return cached_results
  216. def _autodetect_std(self, cmd="", search_patterns=None, re_flags=re.I, priority=99):
  217. """
  218. Standard method to try to auto-detect the device type. This method will be called for each
  219. device_type present in SSH_MAPPER_BASE dict ('dispatch' key). It will attempt to send a
  220. command and match some regular expression from the ouput for each entry in SSH_MAPPER_BASE
  221. ('cmd' and 'search_pattern' keys).
  222. Parameters
  223. ----------
  224. cmd : str
  225. The command to send to the remote device after checking cache.
  226. search_patterns : list
  227. A list of regular expression to look for in the command's output (default: None).
  228. re_flags: re.flags, optional
  229. Any flags from the python re module to modify the regular expression (default: re.I).
  230. priority: int, optional
  231. The confidence the match is right between 0 and 99 (default: 99).
  232. """
  233. invalid_responses = [
  234. r'% Invalid input detected',
  235. r'syntax error, expecting',
  236. r'Error: Unrecognized command',
  237. r'%Error'
  238. ]
  239. if not cmd or not search_patterns:
  240. return 0
  241. try:
  242. response = self._send_command_wrapper(cmd)
  243. # Look for error conditions in output
  244. for pattern in invalid_responses:
  245. match = re.search(pattern, response, flags=re.I)
  246. if match:
  247. return 0
  248. for pattern in search_patterns:
  249. match = re.search(pattern, response, flags=re_flags)
  250. if match:
  251. return priority
  252. except Exception:
  253. return 0
  254. return 0