rpc.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. from Exscript import Account
  2. from Exscript.protocols import SSH2
  3. from ncclient import manager
  4. import paramiko
  5. import re
  6. import xmltodict
  7. CONNECT_TIMEOUT = 5 # seconds
  8. class RPCClient(object):
  9. def __init__(self, device, username='', password=''):
  10. self.username = username
  11. self.password = password
  12. try:
  13. self.host = str(device.primary_ip.address.ip)
  14. except AttributeError:
  15. raise Exception("Specified device ({}) does not have a primary IP defined.".format(device))
  16. def get_lldp_neighbors(self):
  17. """
  18. Returns a list of dictionaries, each representing an LLDP neighbor adjacency.
  19. {
  20. 'local-interface': <str>,
  21. 'name': <str>,
  22. 'remote-interface': <str>,
  23. 'chassis-id': <str>,
  24. }
  25. """
  26. raise NotImplementedError("Feature not implemented for this platform.")
  27. def get_inventory(self):
  28. """
  29. Returns a dictionary representing the device chassis and installed modules.
  30. {
  31. 'chassis': {
  32. 'serial': <str>,
  33. 'description': <str>,
  34. }
  35. 'modules': [
  36. {
  37. 'name': <str>,
  38. 'part_id': <str>,
  39. 'serial': <str>,
  40. },
  41. ...
  42. ]
  43. }
  44. """
  45. raise NotImplementedError("Feature not implemented for this platform.")
  46. class JunosNC(RPCClient):
  47. """
  48. NETCONF client for Juniper Junos devices
  49. """
  50. def __enter__(self):
  51. # Initiate a connection to the device
  52. self.manager = manager.connect(host=self.host, username=self.username, password=self.password,
  53. hostkey_verify=False, timeout=CONNECT_TIMEOUT)
  54. return self
  55. def __exit__(self, exc_type, exc_val, exc_tb):
  56. # Close the connection to the device
  57. self.manager.close_session()
  58. def get_lldp_neighbors(self):
  59. rpc_reply = self.manager.dispatch('get-lldp-neighbors-information')
  60. lldp_neighbors_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['lldp-neighbors-information']['lldp-neighbor-information']
  61. result = []
  62. for neighbor_raw in lldp_neighbors_raw:
  63. neighbor = dict()
  64. neighbor['local-interface'] = neighbor_raw.get('lldp-local-port-id')
  65. neighbor['name'] = neighbor_raw.get('lldp-remote-system-name')
  66. neighbor['name'] = neighbor['name'].split('.')[0] # Split hostname from domain if one is present
  67. try:
  68. neighbor['remote-interface'] = neighbor_raw['lldp-remote-port-description']
  69. except KeyError:
  70. # Older versions of Junos report on interface ID instead of description
  71. neighbor['remote-interface'] = neighbor_raw.get('lldp-remote-port-id')
  72. neighbor['chassis-id'] = neighbor_raw.get('lldp-remote-chassis-id')
  73. result.append(neighbor)
  74. return result
  75. def get_inventory(self):
  76. rpc_reply = self.manager.dispatch('get-chassis-inventory')
  77. inventory_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['chassis-inventory']['chassis']
  78. result = dict()
  79. # Gather chassis data
  80. result['chassis'] = {
  81. 'serial': inventory_raw['serial-number'],
  82. 'description': inventory_raw['description'],
  83. }
  84. # Gather modules
  85. result['modules'] = []
  86. for module in inventory_raw['chassis-module']:
  87. try:
  88. # Skip built-in modules
  89. if module['name'] and module['serial-number'] != inventory_raw['serial-number']:
  90. result['modules'].append({
  91. 'name': module['name'],
  92. 'part_id': module['model-number'] or '',
  93. 'serial': module['serial-number'] or '',
  94. })
  95. except KeyError:
  96. pass
  97. return result
  98. class IOSSSH(RPCClient):
  99. """
  100. SSH client for Cisco IOS devices
  101. """
  102. def __enter__(self):
  103. # Initiate a connection to the device
  104. self.ssh = SSH2(connect_timeout=CONNECT_TIMEOUT)
  105. self.ssh.connect(self.host)
  106. self.ssh.login(Account(self.username, self.password))
  107. # Disable terminal paging
  108. self.ssh.execute("terminal length 0")
  109. return self
  110. def __exit__(self, exc_type, exc_val, exc_tb):
  111. # Close the connection to the device
  112. self.ssh.send("exit\r")
  113. self.ssh.close()
  114. def get_inventory(self):
  115. result = dict()
  116. # Gather chassis data
  117. try:
  118. self.ssh.execute("show version")
  119. show_version = self.ssh.response
  120. serial = re.search("Processor board ID ([^\s]+)", show_version).groups()[0]
  121. description = re.search("\r\n\r\ncisco ([^\s]+)", show_version).groups()[0]
  122. except:
  123. raise RuntimeError("Failed to glean chassis info from device.")
  124. result['chassis'] = {
  125. 'serial': serial,
  126. 'description': description,
  127. }
  128. # Gather modules
  129. result['modules'] = []
  130. try:
  131. self.ssh.execute("show inventory")
  132. show_inventory = self.ssh.response
  133. # Split modules on double line
  134. modules_raw = show_inventory.strip().split('\r\n\r\n')
  135. for module_raw in modules_raw:
  136. try:
  137. m_name = re.search('NAME: "([^"]+)"', module_raw).group(1)
  138. m_pid = re.search('PID: ([^\s]+)', module_raw).group(1)
  139. m_serial = re.search('SN: ([^\s]+)', module_raw).group(1)
  140. # Omit built-in modules and those with no PID
  141. if m_serial != result['chassis']['serial'] and m_pid.lower() != 'unspecified':
  142. result['modules'].append({
  143. 'name': m_name,
  144. 'part_id': m_pid,
  145. 'serial': m_serial,
  146. })
  147. except AttributeError:
  148. continue
  149. except:
  150. raise RuntimeError("Failed to glean module info from device.")
  151. return result
  152. class OpengearSSH(RPCClient):
  153. """
  154. SSH client for Opengear devices
  155. """
  156. def __enter__(self):
  157. # Initiate a connection to the device
  158. self.ssh = paramiko.SSHClient()
  159. self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  160. try:
  161. self.ssh.connect(self.host, username=self.username, password=self.password, timeout=CONNECT_TIMEOUT)
  162. except paramiko.AuthenticationException:
  163. # Try default Opengear credentials if the configured creds don't work
  164. self.ssh.connect(self.host, username='root', password='default')
  165. return self
  166. def __exit__(self, exc_type, exc_val, exc_tb):
  167. # Close the connection to the device
  168. self.ssh.close()
  169. def get_inventory(self):
  170. try:
  171. stdin, stdout, stderr = self.ssh.exec_command("showserial")
  172. serial = stdout.readlines()[0].strip()
  173. except:
  174. raise RuntimeError("Failed to glean chassis serial from device.")
  175. # Older models don't provide serial info
  176. if serial == "No serial number information available":
  177. serial = ''
  178. try:
  179. stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model")
  180. description = stdout.readlines()[0].split(' ', 1)[1].strip()
  181. except:
  182. raise RuntimeError("Failed to glean chassis description from device.")
  183. return {
  184. 'chassis': {
  185. 'serial': serial,
  186. 'description': description,
  187. },
  188. 'modules': [],
  189. }
  190. # For mapping platform -> NC client
  191. RPC_CLIENTS = {
  192. 'juniper-junos': JunosNC,
  193. 'cisco-ios': IOSSSH,
  194. 'opengear': OpengearSSH,
  195. }