rpc.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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 inventory items.
  29. {
  30. 'chassis': {
  31. 'serial': <str>,
  32. 'description': <str>,
  33. }
  34. 'items': [
  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. name = neighbor_raw.get('lldp-remote-system-name')
  109. if name:
  110. neighbor['name'] = name.split('.')[0] # Split hostname from domain if one is present
  111. else:
  112. neighbor['name'] = ''
  113. try:
  114. neighbor['remote-interface'] = neighbor_raw['lldp-remote-port-description']
  115. except KeyError:
  116. # Older versions of Junos report on interface ID instead of description
  117. neighbor['remote-interface'] = neighbor_raw.get('lldp-remote-port-id')
  118. neighbor['chassis-id'] = neighbor_raw.get('lldp-remote-chassis-id')
  119. result.append(neighbor)
  120. return result
  121. def get_inventory(self):
  122. def glean_items(node, depth=0):
  123. items = []
  124. items_list = node.get('chassis{}-module'.format('-sub' * depth), [])
  125. # Junos like to return single children directly instead of as a single-item list
  126. if hasattr(items_list, 'items'):
  127. items_list = [items_list]
  128. for item in items_list:
  129. m = {
  130. 'name': item['name'],
  131. 'part_id': item.get('model-number') or item.get('part-number', ''),
  132. 'serial': item.get('serial-number', ''),
  133. }
  134. child_items = glean_items(item, depth + 1)
  135. if child_items:
  136. m['items'] = child_items
  137. items.append(m)
  138. return items
  139. rpc_reply = self.manager.dispatch('get-chassis-inventory')
  140. inventory_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['chassis-inventory']['chassis']
  141. result = dict()
  142. # Gather chassis data
  143. result['chassis'] = {
  144. 'serial': inventory_raw['serial-number'],
  145. 'description': inventory_raw['description'],
  146. }
  147. # Gather inventory items
  148. result['items'] = glean_items(inventory_raw)
  149. return result
  150. class IOSSSH(SSHClient):
  151. """
  152. SSH client for Cisco IOS devices
  153. """
  154. def get_inventory(self):
  155. def version():
  156. def parse(cmd_out, rex):
  157. for i in cmd_out:
  158. match = re.search(rex, i)
  159. if match:
  160. return match.groups()[0]
  161. sh_ver = self._send('show version').split('\r\n')
  162. return {
  163. 'serial': parse(sh_ver, 'Processor board ID ([^\s]+)'),
  164. 'description': parse(sh_ver, 'cisco ([^\s]+)')
  165. }
  166. def items(chassis_serial=None):
  167. cmd = self._send('show inventory').split('\r\n\r\n')
  168. for i in cmd:
  169. i_fmt = i.replace('\r\n', ' ')
  170. try:
  171. m_name = re.search('NAME: "([^"]+)"', i_fmt).group(1)
  172. m_pid = re.search('PID: ([^\s]+)', i_fmt).group(1)
  173. m_serial = re.search('SN: ([^\s]+)', i_fmt).group(1)
  174. # Omit built-in items and those with no PID
  175. if m_serial != chassis_serial and m_pid.lower() != 'unspecified':
  176. yield {
  177. 'name': m_name,
  178. 'part_id': m_pid,
  179. 'serial': m_serial,
  180. }
  181. except AttributeError:
  182. continue
  183. self._send('term length 0')
  184. sh_version = version()
  185. return {
  186. 'chassis': sh_version,
  187. 'items': list(items(chassis_serial=sh_version.get('serial')))
  188. }
  189. class OpengearSSH(SSHClient):
  190. """
  191. SSH client for Opengear devices
  192. """
  193. default_credentials = {
  194. 'username': 'root',
  195. 'password': 'default',
  196. }
  197. def get_inventory(self):
  198. try:
  199. stdin, stdout, stderr = self.ssh.exec_command("showserial")
  200. serial = stdout.readlines()[0].strip()
  201. except:
  202. raise RuntimeError("Failed to glean chassis serial from device.")
  203. # Older models don't provide serial info
  204. if serial == "No serial number information available":
  205. serial = ''
  206. try:
  207. stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model")
  208. description = stdout.readlines()[0].split(' ', 1)[1].strip()
  209. except:
  210. raise RuntimeError("Failed to glean chassis description from device.")
  211. return {
  212. 'chassis': {
  213. 'serial': serial,
  214. 'description': description,
  215. },
  216. 'items': [],
  217. }
  218. # For mapping platform -> NC client
  219. RPC_CLIENTS = {
  220. 'juniper-junos': JunosNC,
  221. 'cisco-ios': IOSSSH,
  222. 'opengear': OpengearSSH,
  223. }