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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A collection of CLI tools and utilities used by Trigger.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__author__ = "Jathan McCollum, Mike Biancaniello"
|
|
6
|
+
__maintainer__ = "Jathan McCollum"
|
|
7
|
+
__email__ = "jathan.mccollum@teamaol.com"
|
|
8
|
+
__copyright__ = "Copyright 2008-2013, AOL Inc."
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from collections import namedtuple
|
|
12
|
+
|
|
13
|
+
from .cli import get_user
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def crypt_md5(passwd):
|
|
17
|
+
"""
|
|
18
|
+
Returns an md5-crypt hash of a clear-text password.
|
|
19
|
+
|
|
20
|
+
To get md5-crypt from ``crypt(3)`` you must pass an 8-char string starting with
|
|
21
|
+
'$1$' and ending with '$', resulting in a 12-char salt. This only works on
|
|
22
|
+
systems where md5-crypt is default and is currently assumed to be Linux.
|
|
23
|
+
|
|
24
|
+
:param passwd:
|
|
25
|
+
Password string to be encrypted
|
|
26
|
+
"""
|
|
27
|
+
import platform
|
|
28
|
+
|
|
29
|
+
if platform.system() == "Linux":
|
|
30
|
+
import crypt
|
|
31
|
+
import hashlib
|
|
32
|
+
import time
|
|
33
|
+
|
|
34
|
+
salt = "$1$" + hashlib.md5(str(time.time())).hexdigest()[0:8] + "$"
|
|
35
|
+
crypted = crypt.crypt(passwd, salt)
|
|
36
|
+
|
|
37
|
+
else:
|
|
38
|
+
try:
|
|
39
|
+
from passlib.hash import md5_crypt
|
|
40
|
+
except ImportError:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"""When not using Linux, generating md5-crypt password hashes requires the `passlib` module."""
|
|
43
|
+
)
|
|
44
|
+
else:
|
|
45
|
+
crypted = md5_crypt.encrypt(passwd)
|
|
46
|
+
|
|
47
|
+
return crypted
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
JuniperElement = namedtuple("JuniperElement", "key value")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def strip_juniper_namespace(path, key, value):
|
|
54
|
+
"""
|
|
55
|
+
Given a Juniper XML element, strip the namespace and return a 2-tuple.
|
|
56
|
+
|
|
57
|
+
This is designed to be used as a ``postprocessor`` with
|
|
58
|
+
`~trigger.utils.xmltodict.parse()`.
|
|
59
|
+
|
|
60
|
+
:param key:
|
|
61
|
+
The attribute name of the element.
|
|
62
|
+
|
|
63
|
+
:param value:
|
|
64
|
+
The value of the element.
|
|
65
|
+
"""
|
|
66
|
+
marr = re.match(r"(ns1:|ns0:)", key)
|
|
67
|
+
if marr:
|
|
68
|
+
ns = marr.group(0)
|
|
69
|
+
key = key.replace(ns, "")
|
|
70
|
+
|
|
71
|
+
return JuniperElement(key, value)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
NodePort = namedtuple("HostPort", "nodeName nodePort")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_node_port(nodeport, delimiter=":"):
|
|
78
|
+
"""
|
|
79
|
+
Parse a string in format 'hostname' or 'hostname:port' and return them
|
|
80
|
+
as a 2-tuple.
|
|
81
|
+
"""
|
|
82
|
+
node, _, port = nodeport.partition(delimiter)
|
|
83
|
+
if port.isdigit():
|
|
84
|
+
port = int(port)
|
|
85
|
+
else:
|
|
86
|
+
port = None
|
|
87
|
+
|
|
88
|
+
return NodePort(str(node), port)
|
trigger/utils/cli.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface utilities for Trigger tools. Intended for re-usable
|
|
3
|
+
pieces of code like user prompts, that don't fit in other utils modules.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__author__ = "Jathan McCollum"
|
|
7
|
+
__maintainer__ = "Jathan McCollum"
|
|
8
|
+
__email__ = "jathan.mccollum@teamaol.com"
|
|
9
|
+
__copyright__ = "Copyright 2006-2012, AOL Inc.; 2013 Salesforce.com"
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
import os
|
|
13
|
+
import pwd
|
|
14
|
+
import struct
|
|
15
|
+
import sys
|
|
16
|
+
import termios
|
|
17
|
+
import time
|
|
18
|
+
import tty
|
|
19
|
+
from fcntl import ioctl
|
|
20
|
+
|
|
21
|
+
from pytz import UTC, timezone
|
|
22
|
+
|
|
23
|
+
# Exports
|
|
24
|
+
__all__ = (
|
|
25
|
+
"yesno",
|
|
26
|
+
"get_terminal_width",
|
|
27
|
+
"get_terminal_size",
|
|
28
|
+
"Whirlygig",
|
|
29
|
+
"NullDevice",
|
|
30
|
+
"print_severed_head",
|
|
31
|
+
"min_sec",
|
|
32
|
+
"pretty_time",
|
|
33
|
+
"proceed",
|
|
34
|
+
"get_user",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Functions
|
|
39
|
+
def yesno(prompt, default=False, autoyes=False):
|
|
40
|
+
"""
|
|
41
|
+
Present a yes-or-no prompt, get input, and return a boolean.
|
|
42
|
+
|
|
43
|
+
The ``default`` argument is ignored if ``autoyes`` is set.
|
|
44
|
+
|
|
45
|
+
:param prompt:
|
|
46
|
+
Prompt text
|
|
47
|
+
|
|
48
|
+
:param default:
|
|
49
|
+
Yes if True; No if False
|
|
50
|
+
|
|
51
|
+
:param autoyes:
|
|
52
|
+
Automatically return True
|
|
53
|
+
|
|
54
|
+
Default behavior (hitting "enter" returns ``False``)::
|
|
55
|
+
|
|
56
|
+
>>> yesno('Blow up the moon?')
|
|
57
|
+
Blow up the moon? (y/N)
|
|
58
|
+
False
|
|
59
|
+
|
|
60
|
+
Reversed behavior (hitting "enter" returns ``True``)::
|
|
61
|
+
|
|
62
|
+
>>> yesno('Blow up the moon?', default=True)
|
|
63
|
+
Blow up the moon? (Y/n)
|
|
64
|
+
True
|
|
65
|
+
|
|
66
|
+
Automatically return ``True`` with ``autoyes``; no prompt is displayed::
|
|
67
|
+
|
|
68
|
+
>>> yesno('Blow up the moon?', autoyes=True)
|
|
69
|
+
True
|
|
70
|
+
"""
|
|
71
|
+
if autoyes:
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
sys.stdout.write(prompt)
|
|
75
|
+
if default:
|
|
76
|
+
sys.stdout.write(" (Y/n) ")
|
|
77
|
+
else:
|
|
78
|
+
sys.stdout.write(" (y/N) ")
|
|
79
|
+
sys.stdout.flush()
|
|
80
|
+
|
|
81
|
+
fd = sys.stdin.fileno()
|
|
82
|
+
attr = termios.tcgetattr(fd)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
tty.setraw(fd)
|
|
86
|
+
yn = sys.stdin.read(1)
|
|
87
|
+
finally:
|
|
88
|
+
termios.tcsetattr(fd, termios.TCSANOW, attr)
|
|
89
|
+
print("")
|
|
90
|
+
|
|
91
|
+
if yn in ("y", "Y"):
|
|
92
|
+
return True
|
|
93
|
+
elif yn in ("n", "N"):
|
|
94
|
+
return False
|
|
95
|
+
else:
|
|
96
|
+
return default
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def proceed():
|
|
100
|
+
"""Present a proceed prompt. Return ``True`` if Y, else ``False``"""
|
|
101
|
+
return input("\nDo you wish to proceed? [y/N] ").lower().startswith("y")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_terminal_width():
|
|
105
|
+
"""Find and return stdout's terminal width, if applicable."""
|
|
106
|
+
try:
|
|
107
|
+
width = struct.unpack("hhhh", ioctl(1, termios.TIOCGWINSZ, " " * 8))[1]
|
|
108
|
+
except OSError:
|
|
109
|
+
width = sys.maxsize
|
|
110
|
+
|
|
111
|
+
return width
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_terminal_size():
|
|
115
|
+
"""Find and return stdouts terminal size as (height, width)"""
|
|
116
|
+
rows, cols = os.popen("stty size", "r").read().split()
|
|
117
|
+
return rows, cols
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_user():
|
|
121
|
+
"""Return the name of the current user."""
|
|
122
|
+
return pwd.getpwuid(os.getuid())[0]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def print_severed_head():
|
|
126
|
+
"""
|
|
127
|
+
Prints a demon holding a severed head. Best used when things go wrong, like
|
|
128
|
+
production-impacting network outages caused by fat-fingered ACL changes.
|
|
129
|
+
|
|
130
|
+
Thanks to Jeff Sullivan for this best error message ever.
|
|
131
|
+
"""
|
|
132
|
+
print(r"""
|
|
133
|
+
|
|
134
|
+
_( (~\
|
|
135
|
+
_ _ / ( \> > \
|
|
136
|
+
-/~/ / ~\ :; \ _ > /(~\/
|
|
137
|
+
|| | | /\ ;\ |l _____ |; ( \/ > >
|
|
138
|
+
_\\)\)\)/ ;;; `8o __-~ ~\ d| \ //
|
|
139
|
+
///(())(__/~;;\ "88p;. -. _\_;.oP (_._/ /
|
|
140
|
+
(((__ __ \\ \ `>,% (\ (\./)8" ;:' i
|
|
141
|
+
)))--`.'-- (( ;,8 \ ,;%%%: ./V^^^V' ;. ;.
|
|
142
|
+
((\ | /)) .,88 `: ..,,;;;;,-::::::'_::\ ||\ ;[8: ;
|
|
143
|
+
)| ~-~ |(|(888; ..``'::::8888oooooo. :\`^^^/,,~--._ |88:: |
|
|
144
|
+
|\ -===- /| \8;; ``:. oo.8888888888:`((( o.ooo8888Oo;:;:' |
|
|
145
|
+
|_~-___-~_| `-\. ` `o`88888888b` )) 888b88888P""' ;
|
|
146
|
+
; ~~~~;~~ "`--_`. b`888888888;(.,"888b888" ..::;-'
|
|
147
|
+
; ; ~"-.... b`8888888:::::.`8888. .:;;;''
|
|
148
|
+
; ; `:::. `:::OOO:::::::.`OO' ;;;''
|
|
149
|
+
: ; `. "``::::::'' .'
|
|
150
|
+
; `. \_ /
|
|
151
|
+
; ; +: ~~-- `:' -'; ACL LOADS FAILED
|
|
152
|
+
`: : .::/
|
|
153
|
+
; ;;+_ :::. :..;;; YOU LOSE
|
|
154
|
+
;;;;;;,;;;;;;;;,;
|
|
155
|
+
|
|
156
|
+
""")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def pretty_time(t):
|
|
160
|
+
"""
|
|
161
|
+
Print a pretty version of timestamp, including timezone info. Expects
|
|
162
|
+
the incoming datetime object to have proper tzinfo.
|
|
163
|
+
|
|
164
|
+
:param t:
|
|
165
|
+
A ``datetime.datetime`` object
|
|
166
|
+
|
|
167
|
+
>>> import datetime
|
|
168
|
+
>>> from pytz import timezone
|
|
169
|
+
>>> localzone = timezone('US/Eastern')
|
|
170
|
+
<DstTzInfo 'US/Eastern' EST-1 day, 19:00:00 STD>
|
|
171
|
+
>>> t = datetime.datetime.now(localzone)
|
|
172
|
+
>>> print t
|
|
173
|
+
2011-07-19 12:40:30.820920-04:00
|
|
174
|
+
>>> print pretty_time(t)
|
|
175
|
+
09:40 PDT
|
|
176
|
+
>>> t = localzone.localize(datetime.datetime(2011,07,20,04,13))
|
|
177
|
+
>>> print t
|
|
178
|
+
2011-07-20 04:13:00-05:00
|
|
179
|
+
>>> print pretty_time(t)
|
|
180
|
+
tomorrow 02:13 PDT
|
|
181
|
+
"""
|
|
182
|
+
from trigger.conf import settings
|
|
183
|
+
|
|
184
|
+
localzone = timezone(os.environ.get("TZ", settings.BOUNCE_DEFAULT_TZ))
|
|
185
|
+
t = t.astimezone(localzone)
|
|
186
|
+
ct = t.replace(tzinfo=None) # convert to naive time
|
|
187
|
+
# to make the following calculations easier
|
|
188
|
+
# calculate naive 'now' in local time
|
|
189
|
+
# passing localzone into datetime.now directly can cause
|
|
190
|
+
# problems, see the 'pytz' docs if curious
|
|
191
|
+
now = datetime.datetime.now(UTC)
|
|
192
|
+
now = now.astimezone(localzone)
|
|
193
|
+
now = now.replace(tzinfo=None)
|
|
194
|
+
# and compute midnight
|
|
195
|
+
midnight = datetime.datetime.combine(now, datetime.time())
|
|
196
|
+
midnight += datetime.timedelta(1)
|
|
197
|
+
tomorrow = midnight + datetime.timedelta(1)
|
|
198
|
+
thisweek = midnight + datetime.timedelta(6)
|
|
199
|
+
if ct < midnight:
|
|
200
|
+
return t.strftime("%H:%M %Z")
|
|
201
|
+
elif ct < tomorrow:
|
|
202
|
+
return t.strftime("tomorrow %H:%M %Z")
|
|
203
|
+
elif ct < thisweek:
|
|
204
|
+
return t.strftime("%A %H:%M %Z")
|
|
205
|
+
else:
|
|
206
|
+
return t.strftime("%Y-%m-%d %H:%M %Z")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def min_sec(secs):
|
|
210
|
+
"""
|
|
211
|
+
Takes an epoch timestamp and returns string of minutes:seconds.
|
|
212
|
+
|
|
213
|
+
:param secs:
|
|
214
|
+
Timestamp (in seconds)
|
|
215
|
+
|
|
216
|
+
>>> import time
|
|
217
|
+
>>> start = time.time() # Wait a few seconds
|
|
218
|
+
>>> finish = time.time()
|
|
219
|
+
>>> min_sec(finish - start)
|
|
220
|
+
'0:11'
|
|
221
|
+
"""
|
|
222
|
+
secs = int(secs)
|
|
223
|
+
return "%d:%02d" % (secs / 60, secs % 60)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def setup_tty_for_pty(func):
|
|
227
|
+
"""
|
|
228
|
+
Sets up tty for raw mode while retaining original tty settings and then
|
|
229
|
+
starts the reactor to connect to the pty. Upon exiting pty, restores
|
|
230
|
+
original tty settings.
|
|
231
|
+
|
|
232
|
+
:param func:
|
|
233
|
+
The callable to run after the tty is ready, such as ``reactor.run``
|
|
234
|
+
"""
|
|
235
|
+
# Preserve original tty settings
|
|
236
|
+
stdin_fileno = sys.stdin.fileno()
|
|
237
|
+
old_ttyattr = tty.tcgetattr(stdin_fileno)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
# Enter raw mode on the local tty.
|
|
241
|
+
tty.setraw(stdin_fileno)
|
|
242
|
+
raw_ta = tty.tcgetattr(stdin_fileno)
|
|
243
|
+
raw_ta[tty.LFLAG] |= tty.ISIG
|
|
244
|
+
raw_ta[tty.OFLAG] |= tty.OPOST | tty.ONLCR
|
|
245
|
+
|
|
246
|
+
# Pass ^C through so we can abort traceroute, etc.
|
|
247
|
+
raw_ta[tty.CC][tty.VINTR] = "\x18" # ^X is the new ^C
|
|
248
|
+
|
|
249
|
+
# Ctrl-Z is used by a lot of vendors to exit config mode
|
|
250
|
+
raw_ta[tty.CC][tty.VSUSP] = 0 # disable ^Z
|
|
251
|
+
tty.tcsetattr(stdin_fileno, tty.TCSANOW, raw_ta)
|
|
252
|
+
|
|
253
|
+
# Execute our callable here
|
|
254
|
+
func()
|
|
255
|
+
|
|
256
|
+
finally:
|
|
257
|
+
# Restore original tty settings
|
|
258
|
+
tty.tcsetattr(stdin_fileno, tty.TCSANOW, old_ttyattr)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def update_password_and_reconnect(hostname):
|
|
262
|
+
"""
|
|
263
|
+
Prompts the user to update their password and reconnect to the target
|
|
264
|
+
device
|
|
265
|
+
|
|
266
|
+
:param hostname: Hostname of the device to connect to.
|
|
267
|
+
"""
|
|
268
|
+
if yesno(
|
|
269
|
+
"Authentication failed, would you like to update your password?", default=True
|
|
270
|
+
):
|
|
271
|
+
from trigger import tacacsrc
|
|
272
|
+
|
|
273
|
+
tacacsrc.update_credentials(hostname)
|
|
274
|
+
if yesno(f"\nReconnect to {hostname}?", default=True):
|
|
275
|
+
# Replaces the current process w/ same pid
|
|
276
|
+
args = [sys.argv[0]]
|
|
277
|
+
for arg in ("-o", "--oob"):
|
|
278
|
+
if arg in sys.argv:
|
|
279
|
+
idx = sys.argv.index(arg)
|
|
280
|
+
args.append(sys.argv[idx])
|
|
281
|
+
break
|
|
282
|
+
args.append(hostname)
|
|
283
|
+
os.execl(sys.executable, sys.executable, *args)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# Classes
|
|
287
|
+
class NullDevice:
|
|
288
|
+
"""
|
|
289
|
+
Used to supress output to ``sys.stdout`` (aka ``print``).
|
|
290
|
+
|
|
291
|
+
Example::
|
|
292
|
+
|
|
293
|
+
>>> from trigger.utils.cli import NullDevice
|
|
294
|
+
>>> import sys
|
|
295
|
+
>>> print "1 - this will print to STDOUT"
|
|
296
|
+
1 - this will print to STDOUT
|
|
297
|
+
>>> original_stdout = sys.stdout # keep a reference to STDOUT
|
|
298
|
+
>>> sys.stdout = NullDevice() # redirect the real STDOUT
|
|
299
|
+
>>> print "2 - this won't print"
|
|
300
|
+
>>>
|
|
301
|
+
>>> sys.stdout = original_stdout # turn STDOUT back on
|
|
302
|
+
>>> print "3 - this will print to SDTDOUT"
|
|
303
|
+
3 - this will print to SDTDOUT
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
def write(self, s):
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class Whirlygig:
|
|
311
|
+
"""
|
|
312
|
+
Prints a whirlygig for use in displaying pending operation in a command-line tool.
|
|
313
|
+
Guaranteed to make the user feel warm and fuzzy and be 1000% bug-free.
|
|
314
|
+
|
|
315
|
+
:param start_msg: The status message displayed to the user (e.g. "Doing stuff:")
|
|
316
|
+
:param done_msg: The completion message displayed upon completion (e.g. "Done.")
|
|
317
|
+
:param max: Integer of the number of whirlygig repetitions to perform
|
|
318
|
+
|
|
319
|
+
Example::
|
|
320
|
+
|
|
321
|
+
>>> Whirlygig("Doing stuff:", "Done.", 12).run()
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def __init__(self, start_msg="", done_msg="", max=100):
|
|
325
|
+
self.unbuff = os.fdopen(sys.stdout.fileno(), "w", 0)
|
|
326
|
+
self.start_msg = start_msg
|
|
327
|
+
self.done_msg = done_msg
|
|
328
|
+
self.max = max
|
|
329
|
+
self.whirlygig = ["|", "/", "-", "\\"]
|
|
330
|
+
self.whirl = self.whirlygig[:]
|
|
331
|
+
self.first = False
|
|
332
|
+
|
|
333
|
+
def do_whirl(self, whirl):
|
|
334
|
+
if not self.first:
|
|
335
|
+
self.unbuff.write(self.start_msg + " ")
|
|
336
|
+
self.first = True
|
|
337
|
+
self.unbuff.write(f"\b{whirl.pop(0)}")
|
|
338
|
+
|
|
339
|
+
def run(self):
|
|
340
|
+
"""Executes the whirlygig!"""
|
|
341
|
+
cnt = 1
|
|
342
|
+
while cnt <= self.max:
|
|
343
|
+
try:
|
|
344
|
+
self.do_whirl(self.whirl)
|
|
345
|
+
except IndexError:
|
|
346
|
+
self.whirl = self.whirlygig[:]
|
|
347
|
+
time.sleep(0.1)
|
|
348
|
+
cnt += 1
|
|
349
|
+
print("\b" + self.done_msg)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utils to import modules.
|
|
3
|
+
|
|
4
|
+
Taken verbatim from ``django.utils.importlib`` in Django 1.4.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
# Exports
|
|
11
|
+
__all__ = ("import_module", "import_module_from_path")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Functions
|
|
15
|
+
def _resolve_name(name, package, level):
|
|
16
|
+
"""Return the absolute name of the module to be imported."""
|
|
17
|
+
if not hasattr(package, "rindex"):
|
|
18
|
+
raise ValueError("'package' not set to a string")
|
|
19
|
+
dot = len(package)
|
|
20
|
+
for x in range(level, 1, -1):
|
|
21
|
+
try:
|
|
22
|
+
dot = package.rindex(".", 0, dot)
|
|
23
|
+
except ValueError:
|
|
24
|
+
raise ValueError("attempted relative import beyond top-level package")
|
|
25
|
+
return f"{package[:dot]}.{name}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def import_module(name, package=None):
|
|
29
|
+
"""
|
|
30
|
+
Import a module and return the module object.
|
|
31
|
+
|
|
32
|
+
The ``package`` argument is required when performing a relative import. It
|
|
33
|
+
specifies the package to use as the anchor point from which to resolve the
|
|
34
|
+
relative import to an absolute import.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
if name.startswith("."):
|
|
38
|
+
if not package:
|
|
39
|
+
raise TypeError("relative imports require the 'package' argument")
|
|
40
|
+
level = 0
|
|
41
|
+
for character in name:
|
|
42
|
+
if character != ".":
|
|
43
|
+
break
|
|
44
|
+
level += 1
|
|
45
|
+
name = _resolve_name(name[level:], package, level)
|
|
46
|
+
__import__(name)
|
|
47
|
+
return sys.modules[name]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def import_module_from_path(full_path, global_name):
|
|
51
|
+
"""
|
|
52
|
+
Import a module from a file path and return the module object.
|
|
53
|
+
|
|
54
|
+
Allows one to import from anywhere, something ``__import__()`` does not do.
|
|
55
|
+
The module is added to ``sys.modules`` as ``global_name``.
|
|
56
|
+
|
|
57
|
+
:param full_path:
|
|
58
|
+
The absolute path to the module .py file
|
|
59
|
+
|
|
60
|
+
:param global_name:
|
|
61
|
+
The name assigned to the module in sys.modules. To avoid
|
|
62
|
+
confusion, the global_name should be the same as the variable to which
|
|
63
|
+
you're assigning the returned module.
|
|
64
|
+
"""
|
|
65
|
+
path, filename = os.path.split(full_path)
|
|
66
|
+
module, ext = os.path.splitext(filename)
|
|
67
|
+
sys.path.append(path)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
mymodule = __import__(module)
|
|
71
|
+
sys.modules[global_name] = mymodule
|
|
72
|
+
except ImportError:
|
|
73
|
+
raise ImportError(f"Module could not be imported from {full_path}.")
|
|
74
|
+
finally:
|
|
75
|
+
del sys.path[-1]
|
|
76
|
+
|
|
77
|
+
return mymodule
|
trigger/utils/network.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions that perform network-based things like ping, port tests, etc.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import socket
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
from trigger.conf import settings
|
|
11
|
+
|
|
12
|
+
# Exports
|
|
13
|
+
__all__ = ("ping", "test_tcp_port", "test_ssh", "address_is_internal")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Constants
|
|
17
|
+
# SSH version strings used to validate SSH banners.
|
|
18
|
+
SSH_VERSION_STRINGS = (
|
|
19
|
+
"SSH-1.99",
|
|
20
|
+
"SSH-2.0",
|
|
21
|
+
"dcos_sshd run in non-FIPS mode", # Cisco Nexus not in FIPS mode
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Functions
|
|
26
|
+
def ping(host, count=1, timeout=5):
|
|
27
|
+
"""
|
|
28
|
+
Returns pass/fail for a ping. Supports POSIX only.
|
|
29
|
+
|
|
30
|
+
:param host:
|
|
31
|
+
Hostname or address
|
|
32
|
+
|
|
33
|
+
:param count:
|
|
34
|
+
Repeat count
|
|
35
|
+
|
|
36
|
+
:param timeout:
|
|
37
|
+
Timeout in seconds
|
|
38
|
+
|
|
39
|
+
>>> from trigger.utils import network
|
|
40
|
+
>>> network.ping('aol.com')
|
|
41
|
+
True
|
|
42
|
+
>>> network.ping('192.168.199.253')
|
|
43
|
+
False
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
ping_command = "ping -q -c%d -W%d %s" % (count, timeout, host)
|
|
47
|
+
status = None
|
|
48
|
+
with open(os.devnull, "w") as devnull_fd:
|
|
49
|
+
status = subprocess.call(
|
|
50
|
+
shlex.split(ping_command),
|
|
51
|
+
stdout=devnull_fd,
|
|
52
|
+
stderr=devnull_fd,
|
|
53
|
+
close_fds=True,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Linux RC: 0 = success, 256 = failure, 512 = unknown host
|
|
57
|
+
# Darwin RC: 0 = success, 512 = failure, 17408 = unknown host
|
|
58
|
+
return status == 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_tcp_port(host, port=23, timeout=5, check_result=False, expected_result=""):
|
|
62
|
+
"""
|
|
63
|
+
Attempts to connect to a TCP port. Returns a Boolean.
|
|
64
|
+
|
|
65
|
+
If ``check_result`` is set, the first line of output is retreived from the
|
|
66
|
+
connection and the starting characters must match ``expected_result``.
|
|
67
|
+
|
|
68
|
+
:param host:
|
|
69
|
+
Hostname or address
|
|
70
|
+
|
|
71
|
+
:param port:
|
|
72
|
+
Destination port
|
|
73
|
+
|
|
74
|
+
:param timeout:
|
|
75
|
+
Timeout in seconds
|
|
76
|
+
|
|
77
|
+
:param check_result:
|
|
78
|
+
Whether or not to do a string check (e.g. version banner)
|
|
79
|
+
|
|
80
|
+
:param expected_result:
|
|
81
|
+
The expected result!
|
|
82
|
+
|
|
83
|
+
>>> test_tcp_port('aol.com', 80)
|
|
84
|
+
True
|
|
85
|
+
>>> test_tcp_port('aol.com', 12345)
|
|
86
|
+
False
|
|
87
|
+
"""
|
|
88
|
+
# Python 3: Use socket instead of deprecated telnetlib for port testing
|
|
89
|
+
sock = None
|
|
90
|
+
try:
|
|
91
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
92
|
+
sock.settimeout(timeout)
|
|
93
|
+
sock.connect((host, port))
|
|
94
|
+
|
|
95
|
+
if check_result:
|
|
96
|
+
# Read the initial banner/response
|
|
97
|
+
sock.settimeout(2) # Short timeout for reading
|
|
98
|
+
result = sock.recv(1024)
|
|
99
|
+
return result.startswith(
|
|
100
|
+
expected_result.encode()
|
|
101
|
+
if isinstance(expected_result, str)
|
|
102
|
+
else expected_result
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return True
|
|
106
|
+
except (TimeoutError, OSError):
|
|
107
|
+
return False
|
|
108
|
+
finally:
|
|
109
|
+
if sock:
|
|
110
|
+
sock.close()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_ssh(host, port=22, timeout=5, version=SSH_VERSION_STRINGS):
|
|
114
|
+
"""
|
|
115
|
+
Connect to a TCP port and confirm the SSH version. Defaults to SSHv2.
|
|
116
|
+
|
|
117
|
+
Note that the default of ('SSH-1.99', 'SSH-2.0') both indicate SSHv2 per
|
|
118
|
+
RFC 4253. (Ref: http://en.wikipedia.org/wiki/Secure_Shell#Version_1.99)
|
|
119
|
+
|
|
120
|
+
:param host:
|
|
121
|
+
Hostname or address
|
|
122
|
+
|
|
123
|
+
:param port:
|
|
124
|
+
Destination port
|
|
125
|
+
|
|
126
|
+
:param timeout:
|
|
127
|
+
Timeout in seconds
|
|
128
|
+
|
|
129
|
+
:param version:
|
|
130
|
+
The SSH version prefix (e.g. "SSH-2.0"). This may also be a tuple of
|
|
131
|
+
prefixes.
|
|
132
|
+
|
|
133
|
+
>>> test_ssh('localhost')
|
|
134
|
+
True
|
|
135
|
+
>>> test_ssh('localhost', version='SSH-1.5')
|
|
136
|
+
False
|
|
137
|
+
"""
|
|
138
|
+
return test_tcp_port(
|
|
139
|
+
host, port, timeout, check_result=True, expected_result=version
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def address_is_internal(ip):
|
|
144
|
+
"""
|
|
145
|
+
Determines if an IP address is internal to your network. Relies on
|
|
146
|
+
networks specified in :mod:`settings.INTERNAL_NETWORKS`.
|
|
147
|
+
|
|
148
|
+
:param ip:
|
|
149
|
+
IP address to test.
|
|
150
|
+
|
|
151
|
+
>>> address_is_internal('1.1.1.1')
|
|
152
|
+
False
|
|
153
|
+
"""
|
|
154
|
+
for i in settings.INTERNAL_NETWORKS:
|
|
155
|
+
if ip in i:
|
|
156
|
+
return True
|
|
157
|
+
return False
|