1
2
3
4
5
6
7
8
9
10
11
12
13
14 __doc__="""TelnetClient
15
16 TelnetClient is used by TelnetClient to issue commands to a machine
17 and return their output over the telnet protocol.
18
19 Device Tree Parameters are:
20
21 zTelnetLoginTries - number of times to try login default: 1
22 zTelnetLoginTimeout - timeout for expect statements during login default: 2
23 zTelnetPromptTimeout - pause used during prompt discovery default: 0.2
24 zTelnetCommandTimeout - default timeout when executing a command default: 5
25 zTelnetLoginRegex - regex to match the login prompt default: 'ogin:.$'
26 zTelnetPasswordRegex - regext to match the password prompt default: 'assword:.$'
27
28 Other Parameters that are used by both TelnetClient and SshClient:
29 zCommandPathList - list of path to check for a command
30 zCommandExistanceCheck - shell command issued to look for executible
31 must echo succ if executible is found
32 default: test -f executible
33
34 """
35
36 import Globals
37
38
39 try:
40 from twisted.conch import telnet
41 except:
42 from twisted.protocols import telnet
43
44 from twisted.internet import reactor
45
46 import re
47 import logging
48 log = logging.getLogger("zen.TelnetClient")
49
50 import CollectorClient
51 from Exceptions import *
52
53 from Products.ZenUtils.Utils import unused
54
55 defaultPromptTimeout = 10
56 defaultLoginRegex = 'ogin:.$'
57 defaultPasswordRegex = 'assword:'
58 defaultEnable = False
59 defaultTermLength = False
60
61 responseMap = ("WILL", "WONT", "DO", "DONT")
62
64 """
65 Check to see if a device supports telnet
66
67 @param hostname: name or IP address of device
68 @type hostname: string
69 @return: whether or not telnet port is available
70 @rtype: integer
71 @todo: support alternate ports
72 """
73 from telnetlib import Telnet
74 import socket
75 try:
76 tn = Telnet(hostname)
77 tn.close()
78 return 1
79 except socket.error:
80 return 0
81
82
84 """
85 State-machine-based class for telnet
86
87 To switch from one state to the next, methods
88 return the next state.
89 """
90 mode = 'Login'
91
92 timeout = 0
93 timeoutID = None
94 p1 = ""
95 p2 = ""
96 commandPrompt = ""
97 command = ''
98 enabled = -1
99 scCallLater = None
100 bytes = ''
101 lastwrite = ''
102 result = ''
103 buffer = ""
104
114
115
117 """
118 Do we support this telnet feature?
119 Reply back appropriately.
120
121 @param feature: IAC feature request
122 @type feature: string
123 """
124 log.debug("Received telnet DO feature %s" % ord(feature))
125 if ord(feature) == 1:
126 self._iac_response(telnet.WILL, feature)
127 else:
128 self._iac_response(telnet.WONT, feature)
129
131 """
132 Do we support this telnet feature?
133 Reply back appropriately.
134
135 @param feature: IAC feature request
136 @type feature: string
137 """
138
139 log.debug("Received telnet DONT feature %s" % ord(feature))
140 self._iac_response(telnet.WONT, feature)
141
142
144 """
145 Do we support this telnet feature?
146 Reply back appropriately.
147
148 @param feature: IAC feature request
149 @type feature: string
150 """
151 log.debug("Received telnet WILL feature %s" % ord(feature))
152
153 self._iac_response(telnet.DONT, feature)
154
155
157 """
158 Do we support this telnet feature?
159 Reply back appropriately.
160
161 @param feature: IAC feature request
162 @type feature: string
163 """
164 log.debug("Received telnet WONT feature %s" % ord(feature))
165
166 self._iac_response(telnet.DONT, feature)
167
168
170 """
171 Respond to IAC request with our response
172
173 @param action: IAC action
174 @type action: string
175 @param feature: IAC feature request
176 @type feature: string
177 """
178 log.debug("Sending telnet action %s feature %s" %
179 (responseMap[ord(action)-251], ord(feature)))
180 self.write(telnet.IAC+action+feature)
181
182
184 """
185 Write data across the wire and record it.
186
187 @param data: data to write
188 @type data: string
189 """
190 self.lastwrite = data
191 self.transport.write(data)
192
193
195 """
196 Given data returned from the remote device, test out
197 the current chunk of data to determine whether
198 or not to switch states, or just add the chunk to
199 the list of data received from the host.
200
201 If we find the end condition for the state, process
202 the line.
203
204 @param chunk: data
205 @type chunk: string
206 """
207 self.buffer = self.buffer + chunk
208 regex = None
209 if self.factory.modeRegex.has_key(self.mode):
210 regex = self.factory.modeRegex[self.mode]
211 log.debug("Mode '%s' regex = %s" % (self.mode, regex))
212 log.debug("Chunk received = '%s'" % chunk)
213 if regex and re.search(regex, chunk):
214 self.processLine(self.buffer)
215 self.buffer = ""
216
217
219 """
220 Call a method that looks like 'telnet_*' where '*' is filled
221 in by the current mode. telnet_* methods should return a string which
222 will become the new mode.
223
224 @param line: data
225 @type line: string
226 """
227 line = re.sub("\r\n|\r", "\n", line)
228
229 if self.lastwrite.startswith(line):
230 self.lastwrite = self.lastwrite[len(line):]
231 line = ''
232 elif line.find(self.lastwrite) == 0:
233 line = line[len(self.lastwrite):]
234 log.debug("mode = %s", self.mode)
235 self.mode = getattr(self, "telnet_"+self.mode)(line)
236
237
239 """
240 Look for data and send to processLine()
241
242 @param data: output from telnet
243 @type data: string
244 """
245 telnet.Telnet.dataReceived(self, data)
246 log.debug('Line %r', self.bytes)
247 if self.bytes:
248 self.processLine(self.bytes)
249 self.bytes = ''
250
251
253 """
254 Store any bytes received
255
256 @param bytes: output from telnet
257 @type bytes: string
258 """
259 self.bytes += bytes
260
261
263 """
264 Start a timer to decide if we continue or not.
265
266 @param timeout: time in seconds to wait
267 @type timeout: integer
268 @param timeoutfunc: override for the default timeout timer
269 @type timeoutfunc: function
270 """
271 self.cancelTimeout()
272 if timeoutfunc is None: timeoutfunc = self.defaultTimeout
273 self.timeoutID = reactor.callLater(timeout, timeoutfunc)
274
275
282
283
295
296
297
319
320
345
346
348 """
349 Called when the password prompt is expected
350
351 @param data: data sent back from the remote device
352 @type data: string
353 @return: next state (Password, FindPrompt)
354 @rtype: string
355 """
356 log.debug('Search for password regex (%s) in (%s) finds: %r' % \
357 (self.factory.passwordRegex, data, \
358 re.search(self.factory.loginRegex, data)))
359 if not re.search(self.factory.passwordRegex, data):
360 return 'Password'
361 log.debug("Sending password %s" % self.factory.password)
362 self.write(self.factory.password + '\n')
363 self.startTimeout(self.factory.promptTimeout)
364 return 'FindPrompt'
365
366
368 """
369 Switch to 'enable' mode on a Cisco device
370
371 @param unused: unused (unused)
372 @type unused: string
373 @return: next state (Password)
374 @rtype: string
375 """
376 self.write('enable\n')
377 self.startTimeout(self.factory.loginTimeout, self.loginTimeout)
378 return "Password"
379
380
382 """
383 Called after login to figure out the command prompt
384
385 @param data: data sent back from the remote device
386 @type data: string
387 @return: next state (ClearPromptData, FindPrompt, Password)
388 @rtype: string
389 """
390 if not data.strip(): return 'FindPrompt'
391 if re.search(self.factory.loginRegex, data):
392 return self.telnet_Login(data)
393 self.p1 = data
394 if self.p1 == self.p2:
395 self.cancelTimeout()
396 self.commandPrompt = self.p1
397 log.debug("found command prompt '%s'" % self.p1)
398 self.factory.modeRegex['Command'] = re.escape(self.p1) + "$"
399 self.factory.modeRegex['SendCommand'] = re.escape(self.p1) + "$"
400 if self.factory.enable:
401 self.factory.enable = False
402
403 return self.telnet_Enable("")
404 else:
405 self.scCallLater = reactor.callLater(1.0,
406 self.telnet_SendCommand, "")
407 return "ClearPromptData"
408 self.p2 = self.p1
409 self.p1 = ""
410 log.debug("sending \\n")
411 reactor.callLater(.1, self.write, "\n")
412 return 'FindPrompt'
413
414
416 """
417 Called to try to restore sanity to output from the user.
418 Send an empty string to get back a prompt
419
420 @param unused: unused (unused)
421 @type unused: string
422 @return: next state (ClearPromptData)
423 @rtype: string
424 """
425 if self.scCallLater: self.scCallLater.cancel()
426 self.scCallLater = reactor.callLater(1.0, self.telnet_SendCommand, "")
427 return "ClearPromptData"
428
429
446
447
477
478
480 """
481 Return the current command to run
482
483 @return: next command to run
484 @rtype: string
485 """
486 return self.factory._commands[self.factory.cmdindex]
487
488
489
491 """
492 Reactor code to start communications and invoke our
493 telnet transport mechanism.
494 """
495
496 - def __init__(self, hostname, ip, port, plugins=[], options=None,
497 device=None, datacollector=None):
498 """
499 Initializer
500
501 @param hostname: hostname of the device
502 @type hostname: string
503 @param ip: IP address of the device
504 @type ip: string
505 @param port: port number to use to connect to device
506 @type port: integer
507 @param plugins: plugins
508 @type plugins: list of plugins
509 @param options: options
510 @type options: list
511 @param device: name of device
512 @type device: string
513 @param datacollector: object
514 @type datacollector: object
515 """
516 CollectorClient.CollectorClient.__init__(self, hostname, ip, port,
517 plugins, options, device, datacollector)
518 global defaultPromptTimeout
519 global defaultLoginRegex
520 global defaultPasswordRegex
521 global defaultEnable
522
523 self.protocol = TelnetClientProtocol
524 self.modeRegex = {
525 'FindPrompt' : '.*',
526 'WasteTime' : '.*',
527 'Done' : '',
528 }
529 self.promptPause = 1
530
531 if options:
532 defaultPromptTimeout = options.promptTimeout
533 defaultLoginRegex = options.loginRegex
534 defaultPasswordRegex = options.passwordRegex
535 defaultEnable = options.enable
536
537 if device:
538 self.promptTimeout = getattr(device,
539 'zTelnetPromptTimeout', defaultPromptTimeout)
540 self.loginRegex = getattr(device,
541 'zTelnetLoginRegex', defaultLoginRegex)
542 self.passwordRegex = getattr(device,
543 'zTelnetPasswordRegex', defaultPasswordRegex)
544 self.enable = getattr(device,
545 'zTelnetEnable', defaultEnable)
546 self.termlen = getattr(device,
547 'zTelnetTermLength', defaultTermLength)
548
549 else:
550 self.promptTimeout = defaultPromptTimeout
551 self.loginRegex = defaultLoginRegex
552 self.passwordRegex = defaultPasswordRegex
553 self.enable = defaultEnable
554 self.termlen = defaultTermLength
555
556 self.modeRegex['Login'] = self.loginRegex
557 self.modeRegex['Password'] = self.passwordRegex
558
559
561 """
562 Start telnet collection.
563 """
564 if self.termlen:
565 self._commands.insert(0, "terminal length 0")
566 reactor.connectTCP(self.ip, self.port, self)
567
568
570 """
571 Add new commands to be run reset cmdindex to 0
572
573 @param commands: commands to run on the remote device
574 @type commands: list of commands
575 """
576 CollectorClient.CollectorClient.addCommand(self, commands)
577 if self.myprotocol.mode != "Command":
578 self.myprotocol.telnet_SendCommand("")
579
580
582 """
583 If we don't connect let the modeler know
584
585 @param connector: unused (unused)
586 @type connector: unused
587 @param reason: error message to report
588 @type reason: string
589 """
590 unused(connector)
591 log.warn(reason.getErrorMessage())
592 self.clientFinished()
593
594
595
597 """
598 Command-line telnet options
599 """
600
601 parser = CollectorClient.buildOptions(parser,usage)
602
603 parser.add_option('-r', '--promptTimeout',
604 dest='promptTimeout',
605 type = 'float',
606 default = defaultPromptTimeout,
607 help='Timeout when discovering prompt')
608 parser.add_option('-x', '--loginRegex',
609 dest='loginRegex',
610 default = defaultLoginRegex,
611 help='Python regular expression that will find the login prompt')
612 parser.add_option('-w', '--passwordRegex',
613 dest='passwordRegex',
614 default = defaultPasswordRegex,
615 help='Python regex that will find the password prompt')
616 parser.add_option('--enable',
617 dest='enable', action='store_true', default=False,
618 help="Enter 'enable' mode on a Cisco device")
619 parser.add_option('--termlen',
620 dest='termlen', action='store_true', default=False,
621 help="Enter 'send terminal length 0' on a Cisco device")
622 return parser
623
624
626 """
627 Fake class to provide plugin instances for command-line processing.
628 """
631
634
635
637 """
638 The TelntClient class expects plugins.
639 Convert commands like 'ls a', 'ls b' to plugin instances.
640 Duplicate commands will (eventually) be removed.
641 This is used to support command-line arguments.
642
643 @param commands: list of commands from command-line
644 @type commands: list of strings
645 @return: list of commands, plugin-style
646 @rtype: list of FakePlugins
647 """
648 return [ FakePlugin( cmd ) for cmd in commands ]
649
650
652 """
653 Test harness main()
654
655 Usage:
656
657 python TelnetClient.py hostname[:port] comand [command]
658
659 Each command must be enclosed in quotes (") to be interpreted
660 properly as a complete unit.
661 """
662 import socket
663 import getpass
664 import pprint
665
666 parser = buildOptions()
667 options = CollectorClient.parseOptions(parser, 23)
668 if not options.password:
669 options.password = getpass.getpass("%s@%s's password: " %
670 (options.username, options.hostname))
671 logging.basicConfig()
672 log.setLevel(options.logseverity)
673 commands = commandsToPlugins( options.commands )
674 client = TelnetClient(options.hostname,
675 socket.gethostbyname(options.hostname),
676 options.port,
677 plugins=commands, options=options)
678 client.run()
679 client.clientFinished= reactor.stop
680
681 reactor.run()
682
683 pprint.pprint(client.getResults())
684
685 if __name__ == '__main__':
686 main()
687