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