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