Advertisement
Guest User

Untitled

a guest
Mar 16th, 2017
100
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 18.52 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2.  
  3. """
  4. Login and basic command-line interaction support using the Twisted asynchronous
  5. I/O framework. The Trigger Twister is just like the Mersenne Twister, except
  6. not at all.
  7. """
  8.  
  9. from __future__ import absolute_import
  10. from collections import deque
  11. import copy
  12. import fcntl
  13. import struct
  14. import sys
  15. import tty
  16.  
  17. from crochet import run_in_reactor, setup
  18. from twisted.conch.ssh import channel, common, session, transport
  19. from twisted.conch.client.direct import SSHClientFactory
  20. from twisted.conch.ssh import userauth
  21. from twisted.conch.ssh import connection
  22. from twisted.internet import defer, protocol, reactor, task
  23. from twisted.protocols.policies import TimeoutMixin
  24. from twisted.python import log
  25.  
  26. from trigger.conf import settings
  27. from trigger import exceptions
  28.  
  29.  
  30. # Initialize Crochet to "Start the reactor!" --Cuato
  31. setup()
  32.  
  33.  
  34. @run_in_reactor
  35. def connect(hostname, port, options, verifyHostKey, creds, prompt, has_error,
  36. delimiter, startup_commands, transport_class):
  37. """A generic connect function that runs within the crochet reactor."""
  38. d = defer.Deferred()
  39. factory = ClientFactory(d, hostname, options, verifyHostKey, creds, prompt,
  40. has_error, delimiter, startup_commands,
  41. transport_class)
  42. reactor.connectTCP(hostname, port, factory)
  43. return d
  44.  
  45.  
  46. class ClientFactory(SSHClientFactory):
  47. """Client factory responsible for standing up an SSH session.
  48. """
  49. def __init__(self, d, hostname, options, verifyHostKey,
  50. creds, prompt, has_error, delimiter, startup_commands,
  51. transport_class):
  52. self.d = d
  53. self.options = options
  54. self.verifyHostKey = verifyHostKey
  55. self.creds = creds
  56. self.hostname = hostname
  57. self.prompt = prompt
  58. self.has_error = has_error
  59. self.delimiter = delimiter
  60. self.startup_commands = startup_commands
  61. self.transport_class = transport_class
  62. self.timeout = 30
  63.  
  64. def buildProtocol(self, addr):
  65. trans = self.transport_class(self)
  66. # if self.options['ciphers']:
  67. # trans.supportedCiphers = self.options['ciphers']
  68. # if self.options['macs']:
  69. # trans.supportedMACs = self.options['macs']
  70. # if self.options['compress']:
  71. # trans.supportedCompressions[0:1] = ['zlib']
  72. # if self.options['host-key-algorithms']:
  73. # trans.supportedPublicKeys = self.options['host-key-algorithms']
  74. return trans
  75.  
  76.  
  77. class SendExpect(protocol.Protocol, TimeoutMixin):
  78. """
  79. Action for use with TriggerTelnet as a state machine.
  80.  
  81. Take a list of commands, and send them to the device until we run out or
  82. one errors. Wait for a prompt after each.
  83. """
  84. def __init__(self):
  85. self.factory = None
  86. self.connected = False
  87. self.disconnect = False
  88. self.initialized = False
  89. self.prompt = None
  90. self.startup_commands = []
  91. self.command_interval = 1
  92. self.incremental = None
  93. self.on_error = defer.Deferred()
  94. self.todo = deque()
  95. self.done = None
  96. self.doneLock = defer.DeferredLock()
  97.  
  98. def connectionMade(self):
  99. """Do this when we connect."""
  100. self.factory = self.transport.conn.transport.factory
  101. self.prompt = self.factory.prompt
  102. self.hostname = self.factory.hostname
  103. self.has_error = self.factory.has_error
  104. self.delimiter = self.factory.delimiter
  105. self.startup_commands = copy.copy(self.factory.startup_commands)
  106. self.commands = []
  107. self.commanditer = iter(self.commands)
  108. self.connected = True
  109. self.finished = defer.Deferred()
  110. self.results = self.factory.results = []
  111. self.data = ''
  112. log.msg('[%s] connectionMade, data: %r' % (self.hostname, self.data))
  113. # self.setTimeout(self.factory.timeout)
  114. # self.factory._init_commands(self)
  115.  
  116. def connectionLost(self, reason):
  117. self.finished.callback(None)
  118.  
  119. # Don't call _send_next, since we expect to see a prompt, which
  120. # will kick off initialization.
  121.  
  122. def _schedule_commands(self, results, commands):
  123. """Schedule commands onto device loop.
  124.  
  125. This is the actual routine to schedule a set of commands onto a device.
  126.  
  127. :param results: Typical twisted results deferred
  128. :type results: twisted.internet.defer
  129. :param commands: List containing commands to schedule onto device loop.
  130. :type commands: list
  131. """
  132. d = defer.Deferred()
  133. self.todo.append(d)
  134.  
  135. # Schedule next command to run after the previous
  136. # has finished.
  137. if self.done and self.done.called is False:
  138. self.done.addCallback(self._schedule_commands, commands)
  139. self.done = d
  140. return d
  141.  
  142. # First iteration, setup the previous results deferred.
  143. if not results and self.done is None:
  144. self.done = defer.Deferred()
  145. self.done.callback(None)
  146.  
  147. # Either initial state or we are ready to execute more commands.
  148. if results or self.done is None or self.done.called:
  149. log.msg("SCHEDULING THE FOLLOWING {0} :: {1} WAS PREVIOUS RESULTS".format( commands, self.done))
  150. self.commands = commands
  151. self.commanditer = iter(commands)
  152. self._send_next()
  153. self.done = d
  154.  
  155. # Each call must return a deferred.
  156. return d
  157.  
  158. def add_commands(self, commands, on_error):
  159. """Add commands to abstract list of outstanding commands to execute
  160.  
  161. The public method for `~trigger.netdevices.NetDevice` to use for appending more commands
  162. onto the device loop.
  163.  
  164. :param commands: A list of commands to schedule onto device"
  165. :type commands: list
  166. :param on_error: Error handler
  167. :type on_error: func
  168. """
  169.  
  170. # Exception handler to be used in case device throws invalid command warning.
  171. self.on_error.addCallback(on_error)
  172. d = self.doneLock.run(self._schedule_commands, None, commands)
  173. return d
  174.  
  175. def dataReceived(self, bytes):
  176. """Do this when we get data."""
  177. log.msg('[%s] BYTES: %r' % (self.hostname, bytes))
  178. self.data += bytes # See if the prompt matches, and if it doesn't, see if it is waiting
  179. # for more input (like a [y/n]) prompt), and continue, otherwise return
  180. # None
  181. m = self.prompt.search(self.data)
  182. if not m:
  183. # If the prompt confirms set the index to the matched bytes,
  184. def is_awaiting_confirmation(d):
  185. pass
  186.  
  187. if is_awaiting_confirmation(self.data):
  188. log.msg('[%s] Got confirmation prompt: %r' % (self.hostname,
  189. self.data))
  190. prompt_idx = self.data.find(bytes)
  191. else:
  192. return None
  193. else:
  194. # Or just use the matched regex object...
  195. log.msg('[%s] STATE: buffer %r' % (self.hostname, self.data))
  196. log.msg('[%s] STATE: prompt %r' % (self.hostname, m.group()))
  197. prompt_idx = m.start()
  198.  
  199. result = self.data[:prompt_idx]
  200. # Trim off the echoed-back command. This should *not* be necessary
  201. # since the telnet session is in WONT ECHO. This is confirmed with
  202. # a packet trace, and running self.transport.dont(ECHO) from
  203. # connectionMade() returns an AlreadyDisabled error. What's up?
  204. log.msg('[%s] result BEFORE: %r' % (self.hostname, result))
  205. result = result[result.find('\n')+1:]
  206. log.msg('[%s] result AFTER: %r' % (self.hostname, result))
  207.  
  208. if self.initialized:
  209. self.results.append(result)
  210. else:
  211. reactor.callLater(self.command_interval, self._send_next)
  212. return
  213.  
  214. if self.has_error(result) and not self.with_errors:
  215. log.msg('[%s] Command failed: %r' % (self.hostname, result))
  216. self.factory.err = exceptions.CommandFailure(result)
  217. # return None
  218. else:
  219. if self.command_interval:
  220. log.msg('[%s] Waiting %s seconds before sending next command' %
  221. (self.hostname, self.command_interval))
  222.  
  223. task.deferLater(reactor, self.command_interval, self._check_results)
  224.  
  225. def _check_results(self):
  226. # log.msg("TASK", task.deferLater(reactor, self.command_interval, self._are_we_done))
  227. task.deferLater(reactor, self.command_interval, self._are_we_done)
  228.  
  229. def _are_we_done(self):
  230. if self.todo:
  231. if self.results:
  232. payload = list(reversed(self.results))[:len(self.commands)]
  233. payload.reverse()
  234. d = self.todo.pop()
  235. d.callback(payload)
  236. return d
  237. else:
  238. task.deferLater(reactor, self.command_interval, self._check_results)
  239.  
  240. else:
  241. # Loop again.
  242. return
  243.  
  244. def _send_next(self):
  245. """Send the next command in the stack."""
  246. self.data = '' # Flush the buffer before next command
  247. self.resetTimeout()
  248.  
  249. if not self.initialized:
  250. log.msg('[%s] Not initialized, sending startup commands' %
  251. self.hostname)
  252. if self.startup_commands:
  253. next_init = self.startup_commands.pop(0)
  254. log.msg('[%s] Sending initialize command: %r' % (self.hostname,
  255. next_init))
  256. self.transport.write(next_init.strip() + self.delimiter)
  257. return
  258. else:
  259. log.msg('[%s] Successfully initialized for command execution' %
  260. self.hostname)
  261. self.initialized = True
  262.  
  263. if self.incremental:
  264. self.incremental(self.results)
  265.  
  266. try:
  267. next_command = self.commanditer.next()
  268. except StopIteration:
  269. log.msg('[%s] No more commands to send, moving on...' %
  270. self.hostname)
  271.  
  272. return
  273. # if self.todo:
  274. # payload = list(reversed(self.results))[:len(self.commands)]
  275. # payload.reverse()
  276. # d = self.todo.pop()
  277. # d.callback(payload)
  278.  
  279. # return d
  280. # else:
  281. # # Loop again.
  282. # return
  283.  
  284. if next_command is None:
  285. self.results.append(None)
  286. self._send_next()
  287. else:
  288. log.msg('[%s] Sending command %r' % (self.hostname, next_command))
  289. self.transport.write(next_command + '\n')
  290.  
  291. def timeoutConnection(self):
  292. """Do this when we timeout."""
  293. log.msg('[%s] Timed out while sending commands' % self.hostname)
  294. self.factory.err = exceptions.CommandTimeout('Timed out while '
  295. 'sending commands')
  296. self.transport.loseConnection()
  297.  
  298. def close(self):
  299. self.transport.loseConnection()
  300.  
  301.  
  302. class SSHAsyncPtyChannel(channel.SSHChannel):
  303. """A generic SSH Pty Channel that connects to a simple SendExpect CLI Protocol.
  304. """
  305. name = "session"
  306.  
  307. def openFailed(self, reason):
  308. """Channel failed handler."""
  309. self._commandConnected.errback(reason)
  310.  
  311. def channelOpen(self, data):
  312. # Request a pty even tho we are not actually using one.
  313. self._commandConnected = self.conn.transport.factory.d
  314. pr = session.packRequest_pty_req(
  315. settings.TERM_TYPE, (80, 24, 0, 0), ''
  316. )
  317. self.conn.sendRequest(self, 'pty-req', pr)
  318. d = self.conn.sendRequest(self, 'shell', '', wantReply=True)
  319. d.addCallback(self._gotResponse)
  320. d.addErrback(self._ebShellOpen)
  321.  
  322. def _window_resized(self, *args):
  323. """Triggered when the terminal is rezied."""
  324. win_size = self._get_window_size()
  325. new_size = win_size[1], win_size[0], win_size[2], win_size[3]
  326. self.conn.sendRequest(self, 'window-change',
  327. struct.pack('!4L', *new_size))
  328.  
  329. def _get_window_size(self):
  330. """Measure the terminal."""
  331. stdin_fileno = sys.stdin.fileno()
  332. winsz = fcntl.ioctl(stdin_fileno, tty.TIOCGWINSZ, '12345678')
  333. return struct.unpack('4H', winsz)
  334.  
  335. def _execFailure(self, reason):
  336. """Callback for when the exec command fails.
  337. """
  338. self._commandConnected.errback(reason)
  339.  
  340. def _execSuccess(self, ignored):
  341. """Callback for when the exec command succees.
  342. """
  343. # Might be an idea to use a protocol.Factory to generate the protocol instance
  344. # instead of hardcoding it.
  345. self._protocol = SendExpect()
  346. self._protocol.makeConnection(self)
  347. self._commandConnected.callback(self._protocol)
  348.  
  349. def _gotResponse(self, response):
  350. """
  351. Potentially useful if you want to do something after the shell is
  352. initialized.
  353.  
  354. If the shell never establishes, this won't be called.
  355. """
  356. log.msg('[%s] Got channel request response!' % 'blah')
  357. self._execSuccess(None)
  358.  
  359. def _ebShellOpen(self, reason):
  360. log.msg('[%s] Channel request failed: %s' % ('bloh', reason))
  361.  
  362.  
  363. def dataReceived(self, data):
  364. """Callback for when data is received.
  365.  
  366. Once data is received in the channel we defer to the protocol level dataReceived method.
  367. """
  368. self._protocol.dataReceived(data)
  369. # channel.SSHChannel.dataReceived(self, data)
  370.  
  371.  
  372. class ClientConnection(connection.SSHConnection):
  373.  
  374. def serviceStarted(self):
  375. self.openChannel(SSHAsyncPtyChannel(conn=self))
  376.  
  377.  
  378. class ClientUserAuth(userauth.SSHUserAuthClient):
  379. """Perform user authentication over SSH."""
  380. # The preferred order in which SSH authentication methods are tried.
  381. preferredOrder = settings.SSH_AUTHENTICATION_ORDER
  382.  
  383. def __init__(self, user, password, instance):
  384. self.user = user
  385. self.password = password
  386. self.instance = instance
  387.  
  388. def getPassword(self, prompt=None):
  389. """Send along the password."""
  390. log.msg('Performing password authentication', debug=True)
  391. return defer.succeed(self.password)
  392.  
  393. def getGenericAnswers(self, name, information, prompts):
  394. """
  395. Send along the password when authentication mechanism is not 'password'
  396. This is most commonly the case with 'keyboard-interactive', which even
  397. when configured within self.preferredOrder, does not work using default
  398. getPassword() method.
  399. """
  400. log.msg('Performing interactive authentication', debug=True)
  401. log.msg('Prompts: %r' % prompts, debug=True)
  402.  
  403. # The response must always a sequence, and the length must match that
  404. # of the prompts list
  405. response = [''] * len(prompts)
  406. for idx, prompt_tuple in enumerate(prompts):
  407. prompt, echo = prompt_tuple # e.g. [('Password: ', False)]
  408. if 'assword' in prompt:
  409. log.msg("Got password prompt: %r, sending password!" % prompt,
  410. debug=True)
  411. response[idx] = self.transport.factory.creds.password
  412.  
  413. return defer.succeed(response)
  414.  
  415. def ssh_USERAUTH_BANNER(self, packet):
  416. """Display SSH banner."""
  417. if self.transport.factory.display_banner:
  418. banner, language = common.getNS(packet)
  419. self.transport.factory.display_banner(banner, language)
  420.  
  421. def ssh_USERAUTH_FAILURE(self, packet):
  422. """
  423. An almost exact duplicate of SSHUserAuthClient.ssh_USERAUTH_FAILURE
  424. modified to forcefully disconnect. If we receive authentication
  425. failures, instead of looping until the server boots us and performing a
  426. sendDisconnect(), we raise a `~trigger.exceptions.LoginFailure` and
  427. call loseConnection().
  428.  
  429. See the base docstring for the method signature.
  430. """
  431. canContinue, partial = common.getNS(packet)
  432. partial = ord(partial)
  433. log.msg('Previous method: %r ' % self.lastAuth, debug=True)
  434.  
  435. # If the last method succeeded, track it. If network devices ever start
  436. # doing second-factor authentication this might be useful.
  437. if partial:
  438. self.authenticatedWith.append(self.lastAuth)
  439. # If it failed, track that too...
  440. else:
  441. log.msg('Previous method failed, skipping it...', debug=True)
  442. self.authenticatedWith.append(self.lastAuth)
  443.  
  444. def orderByPreference(meth):
  445. """
  446. Invoked once per authentication method in order to extract a
  447. comparison key which is then used for sorting.
  448.  
  449. @param meth: the authentication method.
  450. @type meth: C{str}
  451.  
  452. @return: the comparison key for C{meth}.
  453. @rtype: C{int}
  454. """
  455. if meth in self.preferredOrder:
  456. return self.preferredOrder.index(meth)
  457. else:
  458. # put the element at the end of the list.
  459. return len(self.preferredOrder)
  460.  
  461. canContinue = sorted([meth for meth in canContinue.split(',')
  462. if meth not in self.authenticatedWith],
  463. key=orderByPreference)
  464.  
  465. log.msg('Can continue with: %s' % canContinue)
  466. log.msg('Already tried: %s' % self.authenticatedWith, debug=True)
  467. return self._cbUserauthFailure(None, iter(canContinue))
  468.  
  469. def _cbUserauthFailure(self, result, iterator):
  470. """Callback for ssh_USERAUTH_FAILURE"""
  471. if result:
  472. return
  473. try:
  474. method = iterator.next()
  475. except StopIteration:
  476. msg = (
  477. 'No more authentication methods available.\n'
  478. 'Tried: %s\n'
  479. 'If not using ssh-agent w/ public key, make sure '
  480. 'SSH_AUTH_SOCK is not set and try again.\n'
  481. % (self.preferredOrder,)
  482. )
  483. self.transport.factory.err = exceptions.LoginFailure(msg)
  484. self.transport.loseConnection()
  485. else:
  486. d = defer.maybeDeferred(self.tryAuth, method)
  487. d.addCallback(self._cbUserauthFailure, iterator)
  488. return d
  489.  
  490.  
  491. class ClientTransport(transport.SSHClientTransport):
  492. def __init__(self, factory):
  493. self.factory = factory
  494.  
  495. def verifyHostKey(self, pubKey, fingerprint):
  496. return defer.succeed(1)
  497.  
  498. def connectionSecure(self):
  499. self.requestService(ClientUserAuth(self.factory.creds.username,
  500. self.factory.creds.password,
  501. ClientConnection()
  502. ))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement