rpc.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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. def glean_modules(node, depth=0):
  77. modules = []
  78. modules_list = node.get('chassis{}-module'.format('-sub' * depth), [])
  79. # Junos like to return single children directly instead of as a single-item list
  80. if hasattr(modules_list, 'items'):
  81. modules_list = [modules_list]
  82. for module in modules_list:
  83. m = {
  84. 'name': module['name'],
  85. 'part_id': module.get('model-number', ''),
  86. 'serial': module.get('serial-number', ''),
  87. }
  88. submodules = glean_modules(module, depth + 1)
  89. if submodules:
  90. m['modules'] = submodules
  91. modules.append(m)
  92. return modules
  93. rpc_reply = self.manager.dispatch('get-chassis-inventory')
  94. inventory_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['chassis-inventory']['chassis']
  95. result = dict()
  96. # Gather chassis data
  97. result['chassis'] = {
  98. 'serial': inventory_raw['serial-number'],
  99. 'description': inventory_raw['description'],
  100. }
  101. # Gather modules
  102. result['modules'] = glean_modules(inventory_raw)
  103. return result
  104. class IOSSSH(RPCClient):
  105. """
  106. SSH client for Cisco IOS devices
  107. """
  108. def __enter__(self):
  109. # Initiate a connection to the device
  110. self.ssh = SSH2(connect_timeout=CONNECT_TIMEOUT)
  111. self.ssh.connect(self.host)
  112. self.ssh.login(Account(self.username, self.password))
  113. # Disable terminal paging
  114. self.ssh.execute("terminal length 0")
  115. return self
  116. def __exit__(self, exc_type, exc_val, exc_tb):
  117. # Close the connection to the device
  118. self.ssh.send("exit\r")
  119. self.ssh.close()
  120. def get_inventory(self):
  121. result = dict()
  122. # Gather chassis data
  123. try:
  124. self.ssh.execute("show version")
  125. show_version = self.ssh.response
  126. serial = re.search("Processor board ID ([^\s]+)", show_version).groups()[0]
  127. description = re.search("\r\n\r\ncisco ([^\s]+)", show_version).groups()[0]
  128. except:
  129. raise RuntimeError("Failed to glean chassis info from device.")
  130. result['chassis'] = {
  131. 'serial': serial,
  132. 'description': description,
  133. }
  134. # Gather modules
  135. result['modules'] = []
  136. try:
  137. self.ssh.execute("show inventory")
  138. show_inventory = self.ssh.response
  139. # Split modules on double line
  140. modules_raw = show_inventory.strip().split('\r\n\r\n')
  141. for module_raw in modules_raw:
  142. try:
  143. m_name = re.search('NAME: "([^"]+)"', module_raw).group(1)
  144. m_pid = re.search('PID: ([^\s]+)', module_raw).group(1)
  145. m_serial = re.search('SN: ([^\s]+)', module_raw).group(1)
  146. # Omit built-in modules and those with no PID
  147. if m_serial != result['chassis']['serial'] and m_pid.lower() != 'unspecified':
  148. result['modules'].append({
  149. 'name': m_name,
  150. 'part_id': m_pid,
  151. 'serial': m_serial,
  152. })
  153. except AttributeError:
  154. continue
  155. except:
  156. raise RuntimeError("Failed to glean module info from device.")
  157. return result
  158. class OpengearSSH(RPCClient):
  159. """
  160. SSH client for Opengear devices
  161. """
  162. def __enter__(self):
  163. # Initiate a connection to the device
  164. self.ssh = paramiko.SSHClient()
  165. self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  166. try:
  167. self.ssh.connect(self.host, username=self.username, password=self.password, timeout=CONNECT_TIMEOUT)
  168. except paramiko.AuthenticationException:
  169. # Try default Opengear credentials if the configured creds don't work
  170. self.ssh.connect(self.host, username='root', password='default')
  171. return self
  172. def __exit__(self, exc_type, exc_val, exc_tb):
  173. # Close the connection to the device
  174. self.ssh.close()
  175. def get_inventory(self):
  176. try:
  177. stdin, stdout, stderr = self.ssh.exec_command("showserial")
  178. serial = stdout.readlines()[0].strip()
  179. except:
  180. raise RuntimeError("Failed to glean chassis serial from device.")
  181. # Older models don't provide serial info
  182. if serial == "No serial number information available":
  183. serial = ''
  184. try:
  185. stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model")
  186. description = stdout.readlines()[0].split(' ', 1)[1].strip()
  187. except:
  188. raise RuntimeError("Failed to glean chassis description from device.")
  189. return {
  190. 'chassis': {
  191. 'serial': serial,
  192. 'description': description,
  193. },
  194. 'modules': [],
  195. }
  196. # For mapping platform -> NC client
  197. RPC_CLIENTS = {
  198. 'juniper-junos': JunosNC,
  199. 'cisco-ios': IOSSSH,
  200. 'opengear': OpengearSSH,
  201. }