rpc.py 7.2 KB

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