rpc.py 8.6 KB

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