trigger 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trigger/__init__.py +7 -0
- trigger/acl/__init__.py +32 -0
- trigger/acl/autoacl.py +70 -0
- trigger/acl/db.py +324 -0
- trigger/acl/dicts.py +357 -0
- trigger/acl/grammar.py +112 -0
- trigger/acl/ios.py +222 -0
- trigger/acl/junos.py +422 -0
- trigger/acl/models.py +118 -0
- trigger/acl/parser.py +168 -0
- trigger/acl/queue.py +296 -0
- trigger/acl/support.py +1431 -0
- trigger/acl/tools.py +746 -0
- trigger/bin/__init__.py +0 -0
- trigger/bin/acl.py +233 -0
- trigger/bin/acl_script.py +574 -0
- trigger/bin/aclconv.py +82 -0
- trigger/bin/check_access.py +93 -0
- trigger/bin/check_syntax.py +66 -0
- trigger/bin/fe.py +197 -0
- trigger/bin/find_access.py +191 -0
- trigger/bin/gnng.py +434 -0
- trigger/bin/gong.py +86 -0
- trigger/bin/load_acl.py +841 -0
- trigger/bin/load_config.py +18 -0
- trigger/bin/netdev.py +317 -0
- trigger/bin/optimizer.py +638 -0
- trigger/bin/run_cmds.py +18 -0
- trigger/changemgmt/__init__.py +352 -0
- trigger/changemgmt/bounce.py +57 -0
- trigger/cmds.py +1217 -0
- trigger/conf/__init__.py +94 -0
- trigger/conf/global_settings.py +674 -0
- trigger/contrib/__init__.py +7 -0
- trigger/exceptions.py +307 -0
- trigger/gorc.py +172 -0
- trigger/netdevices/__init__.py +1288 -0
- trigger/netdevices/loader.py +174 -0
- trigger/netscreen.py +1030 -0
- trigger/packages/__init__.py +6 -0
- trigger/packages/peewee.py +8084 -0
- trigger/rancid.py +463 -0
- trigger/tacacsrc.py +584 -0
- trigger/twister.py +2203 -0
- trigger/twister2.py +745 -0
- trigger/utils/__init__.py +88 -0
- trigger/utils/cli.py +349 -0
- trigger/utils/importlib.py +77 -0
- trigger/utils/network.py +157 -0
- trigger/utils/rcs.py +178 -0
- trigger/utils/templates.py +81 -0
- trigger/utils/url.py +78 -0
- trigger/utils/xmltodict.py +298 -0
- trigger-2.0.0.dist-info/METADATA +146 -0
- trigger-2.0.0.dist-info/RECORD +61 -0
- trigger-2.0.0.dist-info/WHEEL +5 -0
- trigger-2.0.0.dist-info/entry_points.txt +15 -0
- trigger-2.0.0.dist-info/licenses/AUTHORS.md +20 -0
- trigger-2.0.0.dist-info/licenses/LICENSE.md +28 -0
- trigger-2.0.0.dist-info/top_level.txt +2 -0
- twisted/plugins/trigger_xmlrpc.py +124 -0
trigger/twister2.py
ADDED
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Login and basic command-line interaction support using the Twisted asynchronous
|
|
3
|
+
I/O framework. The Trigger Twister is just like the Mersenne Twister, except
|
|
4
|
+
not at all.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import fcntl
|
|
8
|
+
import os
|
|
9
|
+
import struct
|
|
10
|
+
import sys
|
|
11
|
+
import tty
|
|
12
|
+
from collections import deque
|
|
13
|
+
from copy import copy
|
|
14
|
+
|
|
15
|
+
from crochet import run_in_reactor, setup
|
|
16
|
+
|
|
17
|
+
setup()
|
|
18
|
+
|
|
19
|
+
from twisted.conch.endpoints import (
|
|
20
|
+
SSHCommandClientEndpoint,
|
|
21
|
+
TCP4ClientEndpoint,
|
|
22
|
+
_CommandTransport,
|
|
23
|
+
_ConnectionReady,
|
|
24
|
+
_ExistingConnectionHelper,
|
|
25
|
+
_NewConnectionHelper,
|
|
26
|
+
_UserAuth,
|
|
27
|
+
connectProtocol,
|
|
28
|
+
)
|
|
29
|
+
from twisted.conch.ssh import common, session, transport
|
|
30
|
+
from twisted.conch.ssh.channel import SSHChannel
|
|
31
|
+
from twisted.internet import defer, protocol, reactor
|
|
32
|
+
from twisted.internet.defer import CancelledError
|
|
33
|
+
from twisted.protocols.policies import TimeoutMixin
|
|
34
|
+
from twisted.python import log
|
|
35
|
+
|
|
36
|
+
from trigger import exceptions, tacacsrc
|
|
37
|
+
from trigger.conf import settings
|
|
38
|
+
from trigger.twister import (
|
|
39
|
+
has_ioslike_error,
|
|
40
|
+
is_awaiting_confirmation,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@run_in_reactor
|
|
45
|
+
def generate_endpoint(device):
|
|
46
|
+
"""Generate Trigger endpoint for a given device.
|
|
47
|
+
|
|
48
|
+
The purpose of this function is to generate endpoint clients for use by a `~trigger.netdevices.NetDevice` object.
|
|
49
|
+
|
|
50
|
+
:param device: `~trigger.netdevices.NetDevice` object
|
|
51
|
+
"""
|
|
52
|
+
creds = tacacsrc.get_device_password(device.nodeName)
|
|
53
|
+
return TriggerSSHShellClientEndpointBase.newConnection(
|
|
54
|
+
reactor, creds.username, device, password=creds.password
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SSHSessionAddress:
|
|
59
|
+
"""This object represents an endpoint's session details.
|
|
60
|
+
|
|
61
|
+
This object would typically be loaded as follows:
|
|
62
|
+
|
|
63
|
+
:Example:
|
|
64
|
+
>>> sess = SSHSessionAddress()
|
|
65
|
+
>>> sess.server = "1.2.3.4"
|
|
66
|
+
>>> sess.username = "cisco"
|
|
67
|
+
>>> sess.command = ""
|
|
68
|
+
|
|
69
|
+
We load command with a null string as Cisco device's typically do not support bash!
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, server, username, command):
|
|
73
|
+
self.server = server
|
|
74
|
+
self.username = username
|
|
75
|
+
self.command = command
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _TriggerShellChannel(SSHChannel):
|
|
79
|
+
"""This is the Trigger subclassed Channel object."""
|
|
80
|
+
|
|
81
|
+
name = b"session"
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
creator,
|
|
86
|
+
command,
|
|
87
|
+
protocolFactory,
|
|
88
|
+
commandConnected,
|
|
89
|
+
incremental,
|
|
90
|
+
with_errors,
|
|
91
|
+
prompt_pattern,
|
|
92
|
+
timeout,
|
|
93
|
+
command_interval,
|
|
94
|
+
):
|
|
95
|
+
SSHChannel.__init__(self)
|
|
96
|
+
self._creator = creator
|
|
97
|
+
self._protocolFactory = protocolFactory
|
|
98
|
+
self._command = command
|
|
99
|
+
self._commandConnected = commandConnected
|
|
100
|
+
self.incremental = incremental
|
|
101
|
+
self.with_errors = with_errors
|
|
102
|
+
self.prompt = prompt_pattern
|
|
103
|
+
self.timeout = timeout
|
|
104
|
+
self.command_interval = command_interval
|
|
105
|
+
self._reason = None
|
|
106
|
+
|
|
107
|
+
def openFailed(self, reason):
|
|
108
|
+
"""Channel failed handler."""
|
|
109
|
+
self._commandConnected.errback(reason)
|
|
110
|
+
|
|
111
|
+
def channelOpen(self, ignored):
|
|
112
|
+
"""Channel opened handler.
|
|
113
|
+
|
|
114
|
+
Once channel is opened, setup the terminal environment and signal
|
|
115
|
+
endpoint to load the shell subsystem.
|
|
116
|
+
"""
|
|
117
|
+
pr = session.packRequest_pty_req(
|
|
118
|
+
os.environ["TERM"], self._get_window_size(), ""
|
|
119
|
+
)
|
|
120
|
+
self.conn.sendRequest(self, "pty-req", pr)
|
|
121
|
+
|
|
122
|
+
command = self.conn.sendRequest(self, "shell", "", wantReply=True)
|
|
123
|
+
# signal.signal(signal.SIGWINCH, self._window_resized)
|
|
124
|
+
command.addCallbacks(self._execSuccess, self._execFailure)
|
|
125
|
+
|
|
126
|
+
def _window_resized(self, *args):
|
|
127
|
+
"""Triggered when the terminal is rezied."""
|
|
128
|
+
win_size = self._get_window_size()
|
|
129
|
+
new_size = win_size[1], win_size[0], win_size[2], win_size[3]
|
|
130
|
+
self.conn.sendRequest(self, "window-change", struct.pack("!4L", *new_size))
|
|
131
|
+
|
|
132
|
+
def _get_window_size(self):
|
|
133
|
+
"""Measure the terminal."""
|
|
134
|
+
stdin_fileno = sys.stdin.fileno()
|
|
135
|
+
winsz = fcntl.ioctl(stdin_fileno, tty.TIOCGWINSZ, "12345678")
|
|
136
|
+
return struct.unpack("4H", winsz)
|
|
137
|
+
|
|
138
|
+
def _execFailure(self, reason):
|
|
139
|
+
"""Callback for when the exec command fails."""
|
|
140
|
+
self._commandConnected.errback(reason)
|
|
141
|
+
|
|
142
|
+
def _execSuccess(self, ignored):
|
|
143
|
+
"""Callback for when the exec command succees."""
|
|
144
|
+
self._protocol = self._protocolFactory.buildProtocol(
|
|
145
|
+
SSHSessionAddress(
|
|
146
|
+
self.conn.transport.transport.getPeer(),
|
|
147
|
+
self.conn.transport.creator.username,
|
|
148
|
+
self._command,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
self._bind_protocol_data()
|
|
152
|
+
self._protocol.makeConnection(self)
|
|
153
|
+
self._commandConnected.callback(self._protocol)
|
|
154
|
+
|
|
155
|
+
def _bind_protocol_data(self):
|
|
156
|
+
"""Helper method to bind protocol related attributes to the channel."""
|
|
157
|
+
# This was a string before, now it's a NetDevice.
|
|
158
|
+
self._protocol.device = self.conn.transport.creator.device or None
|
|
159
|
+
|
|
160
|
+
# FIXME(jathan): Is this potentially non-thread-safe?
|
|
161
|
+
self._protocol.startup_commands = copy(self._protocol.device.startup_commands)
|
|
162
|
+
|
|
163
|
+
self._protocol.incremental = self.incremental or None
|
|
164
|
+
self._protocol.prompt = self.prompt or None
|
|
165
|
+
self._protocol.with_errors = self.with_errors or None
|
|
166
|
+
self._protocol.timeout = self.timeout or None
|
|
167
|
+
self._protocol.command_interval = self.command_interval or None
|
|
168
|
+
|
|
169
|
+
def dataReceived(self, data):
|
|
170
|
+
"""Callback for when data is received.
|
|
171
|
+
|
|
172
|
+
Once data is received in the channel we defer to the protocol level dataReceived method.
|
|
173
|
+
"""
|
|
174
|
+
self._protocol.dataReceived(data)
|
|
175
|
+
# SSHChannel.dataReceived(self, data)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class _TriggerUserAuth(_UserAuth):
|
|
179
|
+
"""Perform user authentication over SSH."""
|
|
180
|
+
|
|
181
|
+
# The preferred order in which SSH authentication methods are tried.
|
|
182
|
+
preferredOrder = settings.SSH_AUTHENTICATION_ORDER
|
|
183
|
+
|
|
184
|
+
def getPassword(self, prompt=None):
|
|
185
|
+
"""Send along the password."""
|
|
186
|
+
log.msg("Performing password authentication", debug=True)
|
|
187
|
+
return defer.succeed(self.password)
|
|
188
|
+
|
|
189
|
+
def getGenericAnswers(self, name, information, prompts):
|
|
190
|
+
"""
|
|
191
|
+
Send along the password when authentication mechanism is not 'password'
|
|
192
|
+
This is most commonly the case with 'keyboard-interactive', which even
|
|
193
|
+
when configured within self.preferredOrder, does not work using default
|
|
194
|
+
getPassword() method.
|
|
195
|
+
"""
|
|
196
|
+
log.msg("Performing interactive authentication", debug=True)
|
|
197
|
+
log.msg(f"Prompts: {prompts!r}", debug=True)
|
|
198
|
+
|
|
199
|
+
# The response must always a sequence, and the length must match that
|
|
200
|
+
# of the prompts list
|
|
201
|
+
response = [""] * len(prompts)
|
|
202
|
+
for idx, prompt_tuple in enumerate(prompts):
|
|
203
|
+
prompt, echo = prompt_tuple # e.g. [('Password: ', False)]
|
|
204
|
+
if "assword" in prompt:
|
|
205
|
+
log.msg(
|
|
206
|
+
f"Got password prompt: {prompt!r}, sending password!", debug=True
|
|
207
|
+
)
|
|
208
|
+
response[idx] = self.password
|
|
209
|
+
|
|
210
|
+
return defer.succeed(response)
|
|
211
|
+
|
|
212
|
+
def ssh_USERAUTH_FAILURE(self, packet):
|
|
213
|
+
"""
|
|
214
|
+
An almost exact duplicate of SSHUserAuthClient.ssh_USERAUTH_FAILURE
|
|
215
|
+
modified to forcefully disconnect. If we receive authentication
|
|
216
|
+
failures, instead of looping until the server boots us and performing a
|
|
217
|
+
sendDisconnect(), we raise a `~trigger.exceptions.LoginFailure` and
|
|
218
|
+
call loseConnection().
|
|
219
|
+
See the base docstring for the method signature.
|
|
220
|
+
"""
|
|
221
|
+
canContinue, partial = common.getNS(packet)
|
|
222
|
+
partial = ord(partial)
|
|
223
|
+
log.msg(f"Previous method: {self.lastAuth!r} ", debug=True)
|
|
224
|
+
|
|
225
|
+
# If the last method succeeded, track it. If network devices ever start
|
|
226
|
+
# doing second-factor authentication this might be useful.
|
|
227
|
+
if partial:
|
|
228
|
+
self.authenticatedWith.append(self.lastAuth)
|
|
229
|
+
# If it failed, track that too...
|
|
230
|
+
else:
|
|
231
|
+
log.msg("Previous method failed, skipping it...", debug=True)
|
|
232
|
+
self.authenticatedWith.append(self.lastAuth)
|
|
233
|
+
|
|
234
|
+
def orderByPreference(meth):
|
|
235
|
+
"""
|
|
236
|
+
Invoked once per authentication method in order to extract a
|
|
237
|
+
comparison key which is then used for sorting.
|
|
238
|
+
@param meth: the authentication method.
|
|
239
|
+
@type meth: C{str}
|
|
240
|
+
@return: the comparison key for C{meth}.
|
|
241
|
+
@rtype: C{int}
|
|
242
|
+
"""
|
|
243
|
+
if meth in self.preferredOrder:
|
|
244
|
+
return self.preferredOrder.index(meth)
|
|
245
|
+
else:
|
|
246
|
+
# put the element at the end of the list.
|
|
247
|
+
return len(self.preferredOrder)
|
|
248
|
+
|
|
249
|
+
canContinue = sorted(
|
|
250
|
+
[
|
|
251
|
+
meth
|
|
252
|
+
for meth in canContinue.split(",")
|
|
253
|
+
if meth not in self.authenticatedWith
|
|
254
|
+
],
|
|
255
|
+
key=orderByPreference,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
log.msg(f"Can continue with: {canContinue}")
|
|
259
|
+
log.msg(f"Already tried: {self.authenticatedWith}", debug=True)
|
|
260
|
+
return self._cbUserauthFailure(None, iter(canContinue))
|
|
261
|
+
|
|
262
|
+
def _cbUserauthFailure(self, result, iterator):
|
|
263
|
+
"""Callback for ssh_USERAUTH_FAILURE"""
|
|
264
|
+
if result:
|
|
265
|
+
return
|
|
266
|
+
try:
|
|
267
|
+
method = iterator.next()
|
|
268
|
+
except StopIteration:
|
|
269
|
+
msg = (
|
|
270
|
+
"No more authentication methods available.\n"
|
|
271
|
+
f"Tried: {self.preferredOrder}\n"
|
|
272
|
+
"If not using ssh-agent w/ public key, make sure "
|
|
273
|
+
"SSH_AUTH_SOCK is not set and try again.\n"
|
|
274
|
+
)
|
|
275
|
+
self.transport.factory.err = exceptions.LoginFailure(msg)
|
|
276
|
+
self.transport.loseConnection()
|
|
277
|
+
else:
|
|
278
|
+
d = defer.maybeDeferred(self.tryAuth, method)
|
|
279
|
+
d.addCallback(self._cbUserauthFailure, iterator)
|
|
280
|
+
return d
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class _TriggerCommandTransport(_CommandTransport):
|
|
284
|
+
def connectionMade(self):
|
|
285
|
+
"""
|
|
286
|
+
Once the connection is up, set the ciphers but don't do anything else!
|
|
287
|
+
"""
|
|
288
|
+
self.currentEncryptions = transport.SSHCiphers("none", "none", "none", "none")
|
|
289
|
+
self.currentEncryptions.setKeys("", "", "", "", "", "")
|
|
290
|
+
|
|
291
|
+
# FIXME(jathan): Make sure that this isn't causing a regression to:
|
|
292
|
+
# https://github.com/trigger/trigger/pull/198
|
|
293
|
+
def dataReceived(self, data):
|
|
294
|
+
"""
|
|
295
|
+
Explicity override version detection for edge cases where "SSH-"
|
|
296
|
+
isn't on the first line of incoming data.
|
|
297
|
+
"""
|
|
298
|
+
# Store incoming data in a local buffer until we've detected the
|
|
299
|
+
# presence of 'SSH-', then handover to default .dataReceived() for
|
|
300
|
+
# version banner processing.
|
|
301
|
+
if not hasattr(self, "my_buf"):
|
|
302
|
+
self.my_buf = ""
|
|
303
|
+
self.my_buf = self.my_buf + data
|
|
304
|
+
|
|
305
|
+
preVersion = self.gotVersion
|
|
306
|
+
|
|
307
|
+
# One extra loop should be enough to get the banner to come through.
|
|
308
|
+
if not self.gotVersion and b"SSH-" not in self.my_buf:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
# This call should populate the SSH version and carry on as usual.
|
|
312
|
+
_CommandTransport.dataReceived(self, data)
|
|
313
|
+
|
|
314
|
+
# We have now seen the SSH version in the banner.
|
|
315
|
+
# signal that the connection has been made successfully.
|
|
316
|
+
if self.gotVersion and not preVersion:
|
|
317
|
+
_CommandTransport.connectionMade(self)
|
|
318
|
+
|
|
319
|
+
def connectionSecure(self):
|
|
320
|
+
"""
|
|
321
|
+
When the connection is secure, start the authentication process.
|
|
322
|
+
"""
|
|
323
|
+
self._state = b"AUTHENTICATING"
|
|
324
|
+
|
|
325
|
+
command = _ConnectionReady(self.connectionReady)
|
|
326
|
+
|
|
327
|
+
self._userauth = _TriggerUserAuth(self.creator.username, command)
|
|
328
|
+
self._userauth.password = self.creator.password
|
|
329
|
+
if self.creator.keys:
|
|
330
|
+
self._userauth.keys = list(self.creator.keys)
|
|
331
|
+
|
|
332
|
+
if self.creator.agentEndpoint is not None:
|
|
333
|
+
d = self._userauth.connectToAgent(self.creator.agentEndpoint)
|
|
334
|
+
else:
|
|
335
|
+
d = defer.succeed(None)
|
|
336
|
+
|
|
337
|
+
def maybeGotAgent(ignored):
|
|
338
|
+
self.requestService(self._userauth)
|
|
339
|
+
|
|
340
|
+
d.addBoth(maybeGotAgent)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class _TriggerSessionTransport(_TriggerCommandTransport):
|
|
344
|
+
def verifyHostKey(self, hostKey, fingerprint):
|
|
345
|
+
self.transport.getPeer().host
|
|
346
|
+
|
|
347
|
+
self._state = b"SECURING"
|
|
348
|
+
return defer.succeed(1)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class _NewTriggerConnectionHelperBase(_NewConnectionHelper):
|
|
352
|
+
"""
|
|
353
|
+
Return object used for establishing an async session rather than executing
|
|
354
|
+
a single command.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
def __init__(
|
|
358
|
+
self,
|
|
359
|
+
reactor,
|
|
360
|
+
device,
|
|
361
|
+
port,
|
|
362
|
+
username,
|
|
363
|
+
keys,
|
|
364
|
+
password,
|
|
365
|
+
agentEndpoint,
|
|
366
|
+
knownHosts,
|
|
367
|
+
ui,
|
|
368
|
+
):
|
|
369
|
+
self.reactor = reactor
|
|
370
|
+
self.device = device
|
|
371
|
+
self.hostname = device.nodeName
|
|
372
|
+
self.port = port
|
|
373
|
+
self.username = username
|
|
374
|
+
self.keys = keys
|
|
375
|
+
self.password = password
|
|
376
|
+
self.agentEndpoint = agentEndpoint
|
|
377
|
+
if knownHosts is None:
|
|
378
|
+
knownHosts = self._knownHosts()
|
|
379
|
+
self.knownHosts = knownHosts
|
|
380
|
+
self.ui = ui
|
|
381
|
+
|
|
382
|
+
def secureConnection(self):
|
|
383
|
+
protocol = _TriggerSessionTransport(self)
|
|
384
|
+
ready = protocol.connectionReady
|
|
385
|
+
|
|
386
|
+
sshClient = TCP4ClientEndpoint(self.reactor, self.hostname, self.port)
|
|
387
|
+
|
|
388
|
+
d = connectProtocol(sshClient, protocol)
|
|
389
|
+
d.addCallback(lambda ignored: ready)
|
|
390
|
+
return d
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class TriggerEndpointClientFactory(protocol.Factory):
|
|
394
|
+
"""
|
|
395
|
+
Factory for all clients. Subclass me.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
def __init__(self, creds=None, init_commands=None):
|
|
399
|
+
self.creds = tacacsrc.validate_credentials(creds)
|
|
400
|
+
self.results = []
|
|
401
|
+
self.err = None
|
|
402
|
+
|
|
403
|
+
# Setup and run the initial commands
|
|
404
|
+
if init_commands is None:
|
|
405
|
+
init_commands = [] # We need this to be a list
|
|
406
|
+
self.init_commands = init_commands
|
|
407
|
+
log.msg(f"INITIAL COMMANDS: {self.init_commands!r}", debug=True)
|
|
408
|
+
self.initialized = False
|
|
409
|
+
|
|
410
|
+
def clientConnectionFailed(self, connector, reason):
|
|
411
|
+
"""Do this when the connection fails."""
|
|
412
|
+
log.msg(f"Client connection failed. Reason: {reason}")
|
|
413
|
+
self.d.errback(reason)
|
|
414
|
+
|
|
415
|
+
def clientConnectionLost(self, connector, reason):
|
|
416
|
+
"""Do this when the connection is lost."""
|
|
417
|
+
log.msg(f"Client connection lost. Reason: {reason}")
|
|
418
|
+
if self.err:
|
|
419
|
+
log.msg(f"Got err: {self.err!r}")
|
|
420
|
+
# log.err(self.err)
|
|
421
|
+
self.d.errback(self.err)
|
|
422
|
+
else:
|
|
423
|
+
log.msg(f"Got results: {self.results!r}")
|
|
424
|
+
self.d.callback(self.results)
|
|
425
|
+
|
|
426
|
+
def stopFactory(self):
|
|
427
|
+
# IF we're out of channels, shut it down!
|
|
428
|
+
log.msg("All done!")
|
|
429
|
+
|
|
430
|
+
def _init_commands(self, protocol):
|
|
431
|
+
"""
|
|
432
|
+
Execute any initial commands specified.
|
|
433
|
+
|
|
434
|
+
:param protocol: A Protocol instance (e.g. action) to which to write
|
|
435
|
+
the commands.
|
|
436
|
+
"""
|
|
437
|
+
if not self.initialized:
|
|
438
|
+
log.msg("Not initialized, sending init commands", debug=True)
|
|
439
|
+
for next_init in self.init_commands:
|
|
440
|
+
log.msg(f"Sending: {next_init!r}", debug=True)
|
|
441
|
+
protocol.write(next_init + "\r\n")
|
|
442
|
+
else:
|
|
443
|
+
self.initialized = True
|
|
444
|
+
|
|
445
|
+
def connection_success(self, conn, transport):
|
|
446
|
+
log.msg("Connection success.")
|
|
447
|
+
self.conn = conn
|
|
448
|
+
self.transport = transport
|
|
449
|
+
log.msg(f"Connection information: {self.transport}")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class TriggerSSHShellClientEndpointBase(SSHCommandClientEndpoint):
|
|
453
|
+
"""
|
|
454
|
+
Base class for SSH endpoints.
|
|
455
|
+
|
|
456
|
+
Subclass me when you want to create a new ssh client.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
@classmethod
|
|
460
|
+
def newConnection(
|
|
461
|
+
cls,
|
|
462
|
+
reactor,
|
|
463
|
+
username,
|
|
464
|
+
device,
|
|
465
|
+
keys=None,
|
|
466
|
+
password=None,
|
|
467
|
+
port=22,
|
|
468
|
+
agentEndpoint=None,
|
|
469
|
+
knownHosts=None,
|
|
470
|
+
ui=None,
|
|
471
|
+
):
|
|
472
|
+
helper = _NewTriggerConnectionHelperBase(
|
|
473
|
+
reactor,
|
|
474
|
+
device,
|
|
475
|
+
port,
|
|
476
|
+
username,
|
|
477
|
+
keys,
|
|
478
|
+
password,
|
|
479
|
+
agentEndpoint,
|
|
480
|
+
knownHosts,
|
|
481
|
+
ui,
|
|
482
|
+
)
|
|
483
|
+
return cls(helper)
|
|
484
|
+
|
|
485
|
+
@classmethod
|
|
486
|
+
def existingConnection(cls, connection):
|
|
487
|
+
"""Overload stock existinConnection to not require ``commands``."""
|
|
488
|
+
helper = _ExistingConnectionHelper(connection)
|
|
489
|
+
return cls(helper)
|
|
490
|
+
|
|
491
|
+
def __init__(self, creator):
|
|
492
|
+
self._creator = creator
|
|
493
|
+
|
|
494
|
+
def _executeCommand(
|
|
495
|
+
self,
|
|
496
|
+
connection,
|
|
497
|
+
protocolFactory,
|
|
498
|
+
command,
|
|
499
|
+
incremental,
|
|
500
|
+
with_errors,
|
|
501
|
+
prompt_pattern,
|
|
502
|
+
timeout,
|
|
503
|
+
command_interval,
|
|
504
|
+
):
|
|
505
|
+
"""Establish the session on a given endpoint.
|
|
506
|
+
|
|
507
|
+
For IOS like devices this is normally just a null string.
|
|
508
|
+
"""
|
|
509
|
+
commandConnected = defer.Deferred()
|
|
510
|
+
|
|
511
|
+
def disconnectOnFailure(passthrough):
|
|
512
|
+
# Close the connection immediately in case of cancellation, since
|
|
513
|
+
# that implies user wants it gone immediately (e.g. a timeout):
|
|
514
|
+
immediate = passthrough.check(CancelledError)
|
|
515
|
+
self._creator.cleanupConnection(connection, immediate)
|
|
516
|
+
return passthrough
|
|
517
|
+
|
|
518
|
+
commandConnected.addErrback(disconnectOnFailure)
|
|
519
|
+
|
|
520
|
+
channel = _TriggerShellChannel(
|
|
521
|
+
self._creator,
|
|
522
|
+
command,
|
|
523
|
+
protocolFactory,
|
|
524
|
+
commandConnected,
|
|
525
|
+
incremental,
|
|
526
|
+
with_errors,
|
|
527
|
+
prompt_pattern,
|
|
528
|
+
timeout,
|
|
529
|
+
command_interval,
|
|
530
|
+
)
|
|
531
|
+
connection.openChannel(channel)
|
|
532
|
+
self.connected = True
|
|
533
|
+
return commandConnected
|
|
534
|
+
|
|
535
|
+
def connect(
|
|
536
|
+
self,
|
|
537
|
+
factory,
|
|
538
|
+
command="",
|
|
539
|
+
incremental=None,
|
|
540
|
+
with_errors=None,
|
|
541
|
+
prompt_pattern=None,
|
|
542
|
+
timeout=0,
|
|
543
|
+
command_interval=1,
|
|
544
|
+
):
|
|
545
|
+
"""Method to initiate SSH connection to device.
|
|
546
|
+
|
|
547
|
+
:param factory: Trigger factory responsible for setting up connection
|
|
548
|
+
:type factory: `~trigger.twister2.TriggerEndpointClientFactory`
|
|
549
|
+
"""
|
|
550
|
+
d = self._creator.secureConnection()
|
|
551
|
+
d.addCallback(
|
|
552
|
+
self._executeCommand,
|
|
553
|
+
factory,
|
|
554
|
+
command,
|
|
555
|
+
incremental,
|
|
556
|
+
with_errors,
|
|
557
|
+
prompt_pattern,
|
|
558
|
+
timeout,
|
|
559
|
+
command_interval,
|
|
560
|
+
)
|
|
561
|
+
return d
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class IoslikeSendExpect(protocol.Protocol, TimeoutMixin):
|
|
565
|
+
"""
|
|
566
|
+
Action for use with TriggerTelnet as a state machine.
|
|
567
|
+
|
|
568
|
+
Take a list of commands, and send them to the device until we run out or
|
|
569
|
+
one errors. Wait for a prompt after each.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
def __init__(self):
|
|
573
|
+
self.device = None
|
|
574
|
+
self.commands = []
|
|
575
|
+
self.commanditer = iter(self.commands)
|
|
576
|
+
self.connected = False
|
|
577
|
+
self.disconnect = False
|
|
578
|
+
self.initialized = False
|
|
579
|
+
self.startup_commands = []
|
|
580
|
+
# FIXME(tom) This sux and should be set by trigger settings
|
|
581
|
+
self.timeout = 10
|
|
582
|
+
self.on_error = defer.Deferred()
|
|
583
|
+
self.todo = deque()
|
|
584
|
+
self.done = None
|
|
585
|
+
self.doneLock = defer.DeferredLock()
|
|
586
|
+
|
|
587
|
+
def connectionMade(self):
|
|
588
|
+
"""Do this when we connect."""
|
|
589
|
+
self.connected = True
|
|
590
|
+
self.finished = defer.Deferred()
|
|
591
|
+
self.results = self.factory.results = []
|
|
592
|
+
self.data = ""
|
|
593
|
+
log.msg(f"[{self.device}] connectionMade, data: {self.data!r}")
|
|
594
|
+
# self.factory._init_commands(self)
|
|
595
|
+
|
|
596
|
+
def connectionLost(self, reason):
|
|
597
|
+
self.finished.callback(None)
|
|
598
|
+
|
|
599
|
+
# Don't call _send_next, since we expect to see a prompt, which
|
|
600
|
+
# will kick off initialization.
|
|
601
|
+
|
|
602
|
+
def _schedule_commands(self, results, commands):
|
|
603
|
+
"""Schedule commands onto device loop.
|
|
604
|
+
|
|
605
|
+
This is the actual routine to schedule a set of commands onto a device.
|
|
606
|
+
|
|
607
|
+
:param results: Typical twisted results deferred
|
|
608
|
+
:type results: twisted.internet.defer
|
|
609
|
+
:param commands: List containing commands to schedule onto device loop.
|
|
610
|
+
:type commands: list
|
|
611
|
+
"""
|
|
612
|
+
d = defer.Deferred()
|
|
613
|
+
self.todo.append(d)
|
|
614
|
+
|
|
615
|
+
# Schedule next command to run after the previous
|
|
616
|
+
# has finished.
|
|
617
|
+
if self.done and self.done.called is False:
|
|
618
|
+
self.done.addCallback(self._schedule_commands, commands)
|
|
619
|
+
self.done = d
|
|
620
|
+
return d
|
|
621
|
+
|
|
622
|
+
# First iteration, setup the previous results deferred.
|
|
623
|
+
if not results and self.done is None:
|
|
624
|
+
self.done = defer.Deferred()
|
|
625
|
+
self.done.callback(None)
|
|
626
|
+
|
|
627
|
+
# Either initial state or we are ready to execute more commands.
|
|
628
|
+
if results or self.done is None or self.done.called:
|
|
629
|
+
log.msg(
|
|
630
|
+
f"SCHEDULING THE FOLLOWING {commands} :: {self.done} WAS PREVIOUS RESULTS"
|
|
631
|
+
)
|
|
632
|
+
self.commands = commands
|
|
633
|
+
self.commanditer = iter(commands)
|
|
634
|
+
self._send_next()
|
|
635
|
+
self.done = d
|
|
636
|
+
|
|
637
|
+
# Each call must return a deferred.
|
|
638
|
+
return d
|
|
639
|
+
|
|
640
|
+
def add_commands(self, commands, on_error):
|
|
641
|
+
"""Add commands to abstract list of outstanding commands to execute
|
|
642
|
+
|
|
643
|
+
The public method for `~trigger.netdevices.NetDevice` to use for appending more commands
|
|
644
|
+
onto the device loop.
|
|
645
|
+
|
|
646
|
+
:param commands: A list of commands to schedule onto device"
|
|
647
|
+
:type commands: list
|
|
648
|
+
:param on_error: Error handler
|
|
649
|
+
:type on_error: func
|
|
650
|
+
"""
|
|
651
|
+
# Exception handler to be used in case device throws invalid command warning.
|
|
652
|
+
self.on_error.addCallback(on_error)
|
|
653
|
+
d = self.doneLock.run(self._schedule_commands, None, commands)
|
|
654
|
+
return d
|
|
655
|
+
|
|
656
|
+
def dataReceived(self, bytes):
|
|
657
|
+
"""Do this when we get data."""
|
|
658
|
+
log.msg(f"[{self.device}] BYTES: {bytes!r}")
|
|
659
|
+
self.data += (
|
|
660
|
+
bytes # See if the prompt matches, and if it doesn't, see if it is waiting
|
|
661
|
+
)
|
|
662
|
+
# for more input (like a [y/n]) prompt), and continue, otherwise return
|
|
663
|
+
# None
|
|
664
|
+
m = self.prompt.search(self.data)
|
|
665
|
+
if not m:
|
|
666
|
+
# If the prompt confirms set the index to the matched bytes,
|
|
667
|
+
if is_awaiting_confirmation(self.data):
|
|
668
|
+
log.msg(f"[{self.device}] Got confirmation prompt: {self.data!r}")
|
|
669
|
+
prompt_idx = self.data.find(bytes)
|
|
670
|
+
else:
|
|
671
|
+
return None
|
|
672
|
+
else:
|
|
673
|
+
# Or just use the matched regex object...
|
|
674
|
+
prompt_idx = m.start()
|
|
675
|
+
|
|
676
|
+
result = self.data[:prompt_idx]
|
|
677
|
+
# Trim off the echoed-back command. This should *not* be necessary
|
|
678
|
+
# since the telnet session is in WONT ECHO. This is confirmed with
|
|
679
|
+
# a packet trace, and running self.transport.dont(ECHO) from
|
|
680
|
+
# connectionMade() returns an AlreadyDisabled error. What's up?
|
|
681
|
+
log.msg(f"[{self.device}] result BEFORE: {result!r}")
|
|
682
|
+
result = result[result.find("\n") + 1 :]
|
|
683
|
+
log.msg(f"[{self.device}] result AFTER: {result!r}")
|
|
684
|
+
|
|
685
|
+
if self.initialized:
|
|
686
|
+
self.results.append(result)
|
|
687
|
+
|
|
688
|
+
if has_ioslike_error(result) and not self.with_errors:
|
|
689
|
+
log.msg(f"[{self.device}] Command failed: {result!r}")
|
|
690
|
+
self.factory.err = exceptions.IoslikeCommandFailure(result)
|
|
691
|
+
else:
|
|
692
|
+
if self.command_interval:
|
|
693
|
+
log.msg(
|
|
694
|
+
f"[{self.device}] Waiting {self.command_interval} seconds before sending next command"
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
reactor.callLater(self.command_interval, self._send_next)
|
|
698
|
+
|
|
699
|
+
def _send_next(self):
|
|
700
|
+
"""Send the next command in the stack."""
|
|
701
|
+
self.data = ""
|
|
702
|
+
self.resetTimeout()
|
|
703
|
+
|
|
704
|
+
if not self.initialized:
|
|
705
|
+
log.msg(f"[{self.device}] Not initialized, sending startup commands")
|
|
706
|
+
if self.startup_commands:
|
|
707
|
+
next_init = self.startup_commands.pop(0)
|
|
708
|
+
log.msg(f"[{self.device}] Sending initialize command: {next_init!r}")
|
|
709
|
+
self.transport.write(next_init.strip() + self.device.delimiter)
|
|
710
|
+
return None
|
|
711
|
+
else:
|
|
712
|
+
log.msg(
|
|
713
|
+
f"[{self.device}] Successfully initialized for command execution"
|
|
714
|
+
)
|
|
715
|
+
self.initialized = True
|
|
716
|
+
|
|
717
|
+
if self.incremental:
|
|
718
|
+
self.incremental(self.results)
|
|
719
|
+
|
|
720
|
+
try:
|
|
721
|
+
next_command = self.commanditer.next()
|
|
722
|
+
except StopIteration:
|
|
723
|
+
log.msg(f"[{self.device}] No more commands to send, moving on...")
|
|
724
|
+
|
|
725
|
+
if self.todo:
|
|
726
|
+
payload = list(reversed(self.results))[: len(self.commands)]
|
|
727
|
+
payload.reverse()
|
|
728
|
+
d = self.todo.pop()
|
|
729
|
+
d.callback(payload)
|
|
730
|
+
return d
|
|
731
|
+
else:
|
|
732
|
+
return
|
|
733
|
+
|
|
734
|
+
if next_command is None:
|
|
735
|
+
self.results.append(None)
|
|
736
|
+
self._send_next()
|
|
737
|
+
else:
|
|
738
|
+
log.msg(f"[{self.device}] Sending command {next_command!r}")
|
|
739
|
+
self.transport.write(next_command + "\n")
|
|
740
|
+
|
|
741
|
+
def timeoutConnection(self):
|
|
742
|
+
"""Do this when we timeout."""
|
|
743
|
+
log.msg(f"[{self.device}] Timed out while sending commands")
|
|
744
|
+
self.factory.err = exceptions.CommandTimeout("Timed out while sending commands")
|
|
745
|
+
self.transport.loseConnection()
|