Merge lp:~james-w/conn-check/split into lp:conn-check
- split
- Merge into trunk
Proposed by
James Westby
Status: | Merged |
---|---|
Approved by: | James Westby |
Approved revision: | 16 |
Merged at revision: | 16 |
Proposed branch: | lp:~james-w/conn-check/split |
Merge into: | lp:conn-check |
Diff against target: |
2210 lines (+1039/-984) 8 files modified
conn_check/__init__.py (+3/-938) conn_check/check_impl.py (+325/-0) conn_check/checks.py (+310/-0) conn_check/main.py (+181/-0) conn_check/patterns.py (+153/-0) demo.yaml (+1/-1) setup.py (+1/-1) tests.py (+65/-44) |
To merge this branch: | bzr merge lp:~james-w/conn-check/split |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Westby (community) | Approve | ||
Review via email: mp+228217@code.launchpad.net |
Commit message
Refactor to split up conn_check.py.
Description of the change
Hi,
Split up the big file.
Thanks,
James
To post a comment you must log in.
Revision history for this message
James Westby (james-w) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'conn_check/__init__.py' | |||
2 | --- conn_check/__init__.py 2014-07-24 21:09:27 +0000 | |||
3 | +++ conn_check/__init__.py 2014-07-24 22:10:31 +0000 | |||
4 | @@ -3,41 +3,6 @@ | |||
5 | 3 | """ | 3 | """ |
6 | 4 | 4 | ||
7 | 5 | import os | 5 | import os |
8 | 6 | import re | ||
9 | 7 | import sys | ||
10 | 8 | import time | ||
11 | 9 | import glob | ||
12 | 10 | from pkg_resources import resource_stream | ||
13 | 11 | import traceback | ||
14 | 12 | import urlparse | ||
15 | 13 | import yaml | ||
16 | 14 | |||
17 | 15 | from argparse import ArgumentParser | ||
18 | 16 | from threading import Thread | ||
19 | 17 | |||
20 | 18 | from OpenSSL import SSL | ||
21 | 19 | from OpenSSL.crypto import load_certificate, FILETYPE_PEM | ||
22 | 20 | |||
23 | 21 | from twisted.internet import epollreactor | ||
24 | 22 | epollreactor.install() | ||
25 | 23 | |||
26 | 24 | from twisted.internet import reactor, ssl | ||
27 | 25 | from twisted.internet.defer import ( | ||
28 | 26 | returnValue, | ||
29 | 27 | inlineCallbacks, | ||
30 | 28 | maybeDeferred, | ||
31 | 29 | DeferredList, | ||
32 | 30 | Deferred) | ||
33 | 31 | from twisted.internet.error import DNSLookupError, TimeoutError | ||
34 | 32 | from twisted.internet.abstract import isIPAddress | ||
35 | 33 | from twisted.internet.protocol import ( | ||
36 | 34 | DatagramProtocol, | ||
37 | 35 | Protocol, | ||
38 | 36 | ClientCreator) | ||
39 | 37 | from twisted.python.failure import Failure | ||
40 | 38 | from twisted.python.threadpool import ThreadPool | ||
41 | 39 | from twisted.web.client import Agent | ||
42 | 40 | |||
43 | 41 | 6 | ||
44 | 42 | def get_version_string(): | 7 | def get_version_string(): |
45 | 43 | return open(os.path.join(os.path.dirname(__file__), | 8 | return open(os.path.join(os.path.dirname(__file__), |
46 | @@ -47,909 +12,9 @@ | |||
47 | 47 | def get_version(): | 12 | def get_version(): |
48 | 48 | return get_version_string().split('.') | 13 | return get_version_string().split('.') |
49 | 49 | 14 | ||
50 | 15 | |||
51 | 50 | __version__ = get_version_string() | 16 | __version__ = get_version_string() |
52 | 51 | 17 | ||
53 | 52 | 18 | ||
957 | 53 | CONNECT_TIMEOUT = 10 | 19 | from twisted.internet import epollreactor |
958 | 54 | CA_CERTS = [] | 20 | epollreactor.install() |
56 | 55 | |||
57 | 56 | for certFileName in glob.glob("/etc/ssl/certs/*.pem"): | ||
58 | 57 | # There might be some dead symlinks in there, so let's make sure it's real. | ||
59 | 58 | if os.path.exists(certFileName): | ||
60 | 59 | data = open(certFileName).read() | ||
61 | 60 | x509 = load_certificate(FILETYPE_PEM, data) | ||
62 | 61 | # Now, de-duplicate in case the same cert has multiple names. | ||
63 | 62 | CA_CERTS.append(x509) | ||
64 | 63 | |||
65 | 64 | |||
66 | 65 | class VerifyingContextFactory(ssl.CertificateOptions): | ||
67 | 66 | |||
68 | 67 | def __init__(self, verify, caCerts, verifyCallback=None): | ||
69 | 68 | ssl.CertificateOptions.__init__(self, verify=verify, | ||
70 | 69 | caCerts=caCerts, | ||
71 | 70 | method=SSL.SSLv23_METHOD) | ||
72 | 71 | self.verifyCallback = verifyCallback | ||
73 | 72 | |||
74 | 73 | def _makeContext(self): | ||
75 | 74 | context = ssl.CertificateOptions._makeContext(self) | ||
76 | 75 | if self.verifyCallback is not None: | ||
77 | 76 | context.set_verify( | ||
78 | 77 | SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, | ||
79 | 78 | self.verifyCallback) | ||
80 | 79 | return context | ||
81 | 80 | |||
82 | 81 | |||
83 | 82 | def maybeDeferToThread(f, *args, **kwargs): | ||
84 | 83 | """ | ||
85 | 84 | Call the function C{f} using a thread from the given threadpool and return | ||
86 | 85 | the result as a Deferred. | ||
87 | 86 | |||
88 | 87 | @param f: The function to call. May return a deferred. | ||
89 | 88 | @param *args: positional arguments to pass to f. | ||
90 | 89 | @param **kwargs: keyword arguments to pass to f. | ||
91 | 90 | |||
92 | 91 | @return: A Deferred which fires a callback with the result of f, or an | ||
93 | 92 | errback with a L{twisted.python.failure.Failure} if f throws an | ||
94 | 93 | exception. | ||
95 | 94 | """ | ||
96 | 95 | threadpool = reactor.getThreadPool() | ||
97 | 96 | |||
98 | 97 | d = Deferred() | ||
99 | 98 | |||
100 | 99 | def realOnResult(result): | ||
101 | 100 | if not isinstance(result, Failure): | ||
102 | 101 | reactor.callFromThread(d.callback, result) | ||
103 | 102 | else: | ||
104 | 103 | reactor.callFromThread(d.errback, result) | ||
105 | 104 | |||
106 | 105 | def onResult(success, result): | ||
107 | 106 | assert success | ||
108 | 107 | assert isinstance(result, Deferred) | ||
109 | 108 | result.addBoth(realOnResult) | ||
110 | 109 | |||
111 | 110 | threadpool.callInThreadWithCallback(onResult, maybeDeferred, | ||
112 | 111 | f, *args, **kwargs) | ||
113 | 112 | |||
114 | 113 | return d | ||
115 | 114 | |||
116 | 115 | |||
117 | 116 | class Check(object): | ||
118 | 117 | """Abstract base class for objects embodying connectivity checks.""" | ||
119 | 118 | |||
120 | 119 | def check(self, pattern, results): | ||
121 | 120 | """Run this check, if it matches the pattern. | ||
122 | 121 | |||
123 | 122 | If the pattern matches, and this is a leaf node in the check tree, | ||
124 | 123 | implementations of Check.check should call | ||
125 | 124 | results.notify_start, then either results.notify_success or | ||
126 | 125 | results.notify_failure. | ||
127 | 126 | """ | ||
128 | 127 | raise NotImplementedError("%r.check not implemented" % type(self)) | ||
129 | 128 | |||
130 | 129 | def skip(self, pattern, results): | ||
131 | 130 | """Indicate that this check has been skipped. | ||
132 | 131 | |||
133 | 132 | If the pattern matches and this is a leaf node in the check tree, | ||
134 | 133 | implementations of Check.skip should call results.notify_skip. | ||
135 | 134 | """ | ||
136 | 135 | raise NotImplementedError("%r.skip not implemented" % type(self)) | ||
137 | 136 | |||
138 | 137 | |||
139 | 138 | class Pattern(object): | ||
140 | 139 | """Abstract base class for patterns used to select subsets of checks.""" | ||
141 | 140 | |||
142 | 141 | def assume_prefix(self, prefix): | ||
143 | 142 | """Return an equivalent pattern with the given prefix baked in. | ||
144 | 143 | |||
145 | 144 | For example, if self.matches("bar") is True, then | ||
146 | 145 | self.assume_prefix("foo").matches("foobar") will be True. | ||
147 | 146 | """ | ||
148 | 147 | return PrefixPattern(prefix, self) | ||
149 | 148 | |||
150 | 149 | def failed(self): | ||
151 | 150 | """Return True if the pattern cannot match any string. | ||
152 | 151 | |||
153 | 152 | This is mainly used so we can bail out early when recursing into | ||
154 | 153 | check trees. | ||
155 | 154 | """ | ||
156 | 155 | return not self.prefix_matches("") | ||
157 | 156 | |||
158 | 157 | def prefix_matches(self, partial_name): | ||
159 | 158 | """Return True if the partial name (a prefix) is a potential match.""" | ||
160 | 159 | raise NotImplementedError("%r.prefix_matches not implemented" % | ||
161 | 160 | type(self)) | ||
162 | 161 | |||
163 | 162 | def matches(self, name): | ||
164 | 163 | """Return True if the given name matches.""" | ||
165 | 164 | raise NotImplementedError("%r.match not implemented" % | ||
166 | 165 | type(self)) | ||
167 | 166 | |||
168 | 167 | |||
169 | 168 | class ResultTracker(object): | ||
170 | 169 | """Base class for objects which report or record check results.""" | ||
171 | 170 | |||
172 | 171 | def notify_start(self, name, info): | ||
173 | 172 | """Register the start of a check.""" | ||
174 | 173 | |||
175 | 174 | def notify_skip(self, name): | ||
176 | 175 | """Register a check being skipped.""" | ||
177 | 176 | |||
178 | 177 | def notify_success(self, name, duration): | ||
179 | 178 | """Register a successful check.""" | ||
180 | 179 | |||
181 | 180 | def notify_failure(self, name, info, exc_info, duration): | ||
182 | 181 | """Register the failure of a check.""" | ||
183 | 182 | |||
184 | 183 | |||
185 | 184 | class PrefixResultWrapper(ResultTracker): | ||
186 | 185 | """ResultWrapper wrapper which adds a prefix to recorded results.""" | ||
187 | 186 | |||
188 | 187 | def __init__(self, wrapped, prefix): | ||
189 | 188 | """Initialize an instance.""" | ||
190 | 189 | super(PrefixResultWrapper, self).__init__() | ||
191 | 190 | self.wrapped = wrapped | ||
192 | 191 | self.prefix = prefix | ||
193 | 192 | |||
194 | 193 | def make_name(self, name): | ||
195 | 194 | """Make a name by prepending the prefix.""" | ||
196 | 195 | return "%s%s" % (self.prefix, name) | ||
197 | 196 | |||
198 | 197 | def notify_skip(self, name): | ||
199 | 198 | """Register a check being skipped.""" | ||
200 | 199 | self.wrapped.notify_skip(self.make_name(name)) | ||
201 | 200 | |||
202 | 201 | def notify_start(self, name, info): | ||
203 | 202 | """Register the start of a check.""" | ||
204 | 203 | self.wrapped.notify_start(self.make_name(name), info) | ||
205 | 204 | |||
206 | 205 | def notify_success(self, name, duration): | ||
207 | 206 | """Register success.""" | ||
208 | 207 | self.wrapped.notify_success(self.make_name(name), duration) | ||
209 | 208 | |||
210 | 209 | def notify_failure(self, name, info, exc_info, duration): | ||
211 | 210 | """Register failure.""" | ||
212 | 211 | self.wrapped.notify_failure(self.make_name(name), | ||
213 | 212 | info, exc_info, duration) | ||
214 | 213 | |||
215 | 214 | |||
216 | 215 | class FailureCountingResultWrapper(ResultTracker): | ||
217 | 216 | """ResultWrapper wrapper which counts failures.""" | ||
218 | 217 | |||
219 | 218 | def __init__(self, wrapped): | ||
220 | 219 | """Initialize an instance.""" | ||
221 | 220 | super(FailureCountingResultWrapper, self).__init__() | ||
222 | 221 | self.wrapped = wrapped | ||
223 | 222 | self.failure_count = 0 | ||
224 | 223 | |||
225 | 224 | def notify_skip(self, name): | ||
226 | 225 | """Register a check being skipped.""" | ||
227 | 226 | self.wrapped.notify_skip(name) | ||
228 | 227 | |||
229 | 228 | def notify_start(self, name, info): | ||
230 | 229 | """Register the start of a check.""" | ||
231 | 230 | self.failure_count += 1 | ||
232 | 231 | self.wrapped.notify_start(name, info) | ||
233 | 232 | |||
234 | 233 | def notify_success(self, name, duration): | ||
235 | 234 | """Register success.""" | ||
236 | 235 | self.failure_count -= 1 | ||
237 | 236 | self.wrapped.notify_success(name, duration) | ||
238 | 237 | |||
239 | 238 | def notify_failure(self, name, info, exc_info, duration): | ||
240 | 239 | """Register failure.""" | ||
241 | 240 | self.wrapped.notify_failure(name, info, exc_info, duration) | ||
242 | 241 | |||
243 | 242 | def any_failed(self): | ||
244 | 243 | """Return True if any checks using this wrapper failed so far.""" | ||
245 | 244 | return self.failure_count > 0 | ||
246 | 245 | |||
247 | 246 | |||
248 | 247 | class FailedPattern(Pattern): | ||
249 | 248 | """Patterns that always fail to match.""" | ||
250 | 249 | |||
251 | 250 | def assume_prefix(self, prefix): | ||
252 | 251 | """Return an equivalent pattern with the given prefix baked in.""" | ||
253 | 252 | return FAILED_PATTERN | ||
254 | 253 | |||
255 | 254 | def prefix_matches(self, partial_name): | ||
256 | 255 | """Return True if the partial name matches.""" | ||
257 | 256 | return False | ||
258 | 257 | |||
259 | 258 | def matches(self, name): | ||
260 | 259 | """Return True if the complete name matches.""" | ||
261 | 260 | return False | ||
262 | 261 | |||
263 | 262 | |||
264 | 263 | FAILED_PATTERN = FailedPattern() | ||
265 | 264 | |||
266 | 265 | |||
267 | 266 | PATTERN_TOKEN_RE = re.compile(r'\*|[^*]+') | ||
268 | 267 | |||
269 | 268 | |||
270 | 269 | def tokens_to_partial_re(tokens): | ||
271 | 270 | """Convert tokens to a regular expression for matching prefixes.""" | ||
272 | 271 | |||
273 | 272 | def token_to_re(token): | ||
274 | 273 | """Convert tokens to (begin, end, alt_end) triples.""" | ||
275 | 274 | if token == '*': | ||
276 | 275 | return (r'(?:.*', ')?', ')') | ||
277 | 276 | else: | ||
278 | 277 | chars = list(token) | ||
279 | 278 | begin = "".join(["(?:" + re.escape(c) for c in chars]) | ||
280 | 279 | end = "".join([")?" for c in chars]) | ||
281 | 280 | return (begin, end, end) | ||
282 | 281 | |||
283 | 282 | subexprs = map(token_to_re, tokens) | ||
284 | 283 | if len(subexprs) > 0: | ||
285 | 284 | # subexpressions like (.*)? aren't accepted, so we may have to use | ||
286 | 285 | # an alternate closing form for the last (innermost) subexpression | ||
287 | 286 | (begin, _, alt_end) = subexprs[-1] | ||
288 | 287 | subexprs[-1] = (begin, alt_end, alt_end) | ||
289 | 288 | return re.compile("".join([se[0] for se in subexprs] + | ||
290 | 289 | [se[1] for se in reversed(subexprs)] + | ||
291 | 290 | [r'\Z'])) | ||
292 | 291 | |||
293 | 292 | |||
294 | 293 | def tokens_to_re(tokens): | ||
295 | 294 | """Convert tokens to a regular expression for exact matching.""" | ||
296 | 295 | |||
297 | 296 | def token_to_re(token): | ||
298 | 297 | """Convert tokens to simple regular expressions.""" | ||
299 | 298 | if token == '*': | ||
300 | 299 | return r'.*' | ||
301 | 300 | else: | ||
302 | 301 | return re.escape(token) | ||
303 | 302 | |||
304 | 303 | return re.compile("".join(map(token_to_re, tokens) + [r'\Z'])) | ||
305 | 304 | |||
306 | 305 | |||
307 | 306 | class SimplePattern(Pattern): | ||
308 | 307 | """Pattern that matches according to the given pattern expression.""" | ||
309 | 308 | |||
310 | 309 | def __init__(self, pattern): | ||
311 | 310 | """Initialize an instance.""" | ||
312 | 311 | super(SimplePattern, self).__init__() | ||
313 | 312 | tokens = PATTERN_TOKEN_RE.findall(pattern) | ||
314 | 313 | self.partial_re = tokens_to_partial_re(tokens) | ||
315 | 314 | self.full_re = tokens_to_re(tokens) | ||
316 | 315 | |||
317 | 316 | def prefix_matches(self, partial_name): | ||
318 | 317 | """Return True if the partial name matches.""" | ||
319 | 318 | return self.partial_re.match(partial_name) is not None | ||
320 | 319 | |||
321 | 320 | def matches(self, name): | ||
322 | 321 | """Return True if the complete name matches.""" | ||
323 | 322 | return self.full_re.match(name) is not None | ||
324 | 323 | |||
325 | 324 | |||
326 | 325 | class PrefixPattern(Pattern): | ||
327 | 326 | """Pattern that assumes a previously given prefix.""" | ||
328 | 327 | |||
329 | 328 | def __init__(self, prefix, pattern): | ||
330 | 329 | """Initialize an instance.""" | ||
331 | 330 | super(PrefixPattern, self).__init__() | ||
332 | 331 | self.prefix = prefix | ||
333 | 332 | self.pattern = pattern | ||
334 | 333 | |||
335 | 334 | def assume_prefix(self, prefix): | ||
336 | 335 | """Return an equivalent pattern with the given prefix baked in.""" | ||
337 | 336 | return PrefixPattern(self.prefix + prefix, self.pattern) | ||
338 | 337 | |||
339 | 338 | def prefix_matches(self, partial_name): | ||
340 | 339 | """Return True if the partial name matches.""" | ||
341 | 340 | return self.pattern.prefix_matches(self.prefix + partial_name) | ||
342 | 341 | |||
343 | 342 | def matches(self, name): | ||
344 | 343 | """Return True if the complete name matches.""" | ||
345 | 344 | return self.pattern.matches(self.prefix + name) | ||
346 | 345 | |||
347 | 346 | |||
348 | 347 | class SumPattern(Pattern): | ||
349 | 348 | """Pattern that matches if at least one given pattern matches.""" | ||
350 | 349 | |||
351 | 350 | def __init__(self, patterns): | ||
352 | 351 | """Initialize an instance.""" | ||
353 | 352 | super(SumPattern, self).__init__() | ||
354 | 353 | self.patterns = patterns | ||
355 | 354 | |||
356 | 355 | def prefix_matches(self, partial_name): | ||
357 | 356 | """Return True if the partial name matches.""" | ||
358 | 357 | for pattern in self.patterns: | ||
359 | 358 | if pattern.prefix_matches(partial_name): | ||
360 | 359 | return True | ||
361 | 360 | return False | ||
362 | 361 | |||
363 | 362 | def matches(self, name): | ||
364 | 363 | """Return True if the complete name matches.""" | ||
365 | 364 | for pattern in self.patterns: | ||
366 | 365 | if pattern.matches(name): | ||
367 | 366 | return True | ||
368 | 367 | return False | ||
369 | 368 | |||
370 | 369 | |||
371 | 370 | class ConditionalCheck(Check): | ||
372 | 371 | """A Check that skips unless the given predicate is true at check time.""" | ||
373 | 372 | |||
374 | 373 | def __init__(self, wrapped, predicate): | ||
375 | 374 | """Initialize an instance.""" | ||
376 | 375 | super(ConditionalCheck, self).__init__() | ||
377 | 376 | self.wrapped = wrapped | ||
378 | 377 | self.predicate = predicate | ||
379 | 378 | |||
380 | 379 | def check(self, pattern, result): | ||
381 | 380 | """Skip the check.""" | ||
382 | 381 | if self.predicate(): | ||
383 | 382 | return self.wrapped.check(pattern, result) | ||
384 | 383 | else: | ||
385 | 384 | self.skip(pattern, result) | ||
386 | 385 | |||
387 | 386 | def skip(self, pattern, result): | ||
388 | 387 | """Skip the check.""" | ||
389 | 388 | self.wrapped.skip(pattern, result) | ||
390 | 389 | |||
391 | 390 | |||
392 | 391 | class FunctionCheck(Check): | ||
393 | 392 | """A Check which takes a check function.""" | ||
394 | 393 | |||
395 | 394 | def __init__(self, name, check, info=None, blocking=False): | ||
396 | 395 | """Initialize an instance.""" | ||
397 | 396 | super(FunctionCheck, self).__init__() | ||
398 | 397 | self.name = name | ||
399 | 398 | self.info = info | ||
400 | 399 | self.check_fn = check | ||
401 | 400 | self.blocking = blocking | ||
402 | 401 | |||
403 | 402 | @inlineCallbacks | ||
404 | 403 | def check(self, pattern, results): | ||
405 | 404 | """Call the check function.""" | ||
406 | 405 | if not pattern.matches(self.name): | ||
407 | 406 | returnValue(None) | ||
408 | 407 | results.notify_start(self.name, self.info) | ||
409 | 408 | start = time.time() | ||
410 | 409 | try: | ||
411 | 410 | if self.blocking: | ||
412 | 411 | result = yield maybeDeferToThread(self.check_fn) | ||
413 | 412 | else: | ||
414 | 413 | result = yield maybeDeferred(self.check_fn) | ||
415 | 414 | results.notify_success(self.name, time.time() - start) | ||
416 | 415 | returnValue(result) | ||
417 | 416 | except Exception: | ||
418 | 417 | results.notify_failure(self.name, self.info, | ||
419 | 418 | sys.exc_info(), time.time() - start) | ||
420 | 419 | |||
421 | 420 | def skip(self, pattern, results): | ||
422 | 421 | """Record the skip.""" | ||
423 | 422 | if not pattern.matches(self.name): | ||
424 | 423 | return | ||
425 | 424 | results.notify_skip(self.name) | ||
426 | 425 | |||
427 | 426 | |||
428 | 427 | class MultiCheck(Check): | ||
429 | 428 | """A composite check comprised of multiple subchecks.""" | ||
430 | 429 | |||
431 | 430 | def __init__(self, subchecks, strategy): | ||
432 | 431 | """Initialize an instance.""" | ||
433 | 432 | super(MultiCheck, self).__init__() | ||
434 | 433 | self.subchecks = list(subchecks) | ||
435 | 434 | self.strategy = strategy | ||
436 | 435 | |||
437 | 436 | def check(self, pattern, results): | ||
438 | 437 | """Run subchecks using the strategy supplied at creation time.""" | ||
439 | 438 | return self.strategy(self.subchecks, pattern, results) | ||
440 | 439 | |||
441 | 440 | def skip(self, pattern, results): | ||
442 | 441 | """Skip subchecks.""" | ||
443 | 442 | for subcheck in self.subchecks: | ||
444 | 443 | subcheck.skip(pattern, results) | ||
445 | 444 | |||
446 | 445 | |||
447 | 446 | class PrefixCheckWrapper(Check): | ||
448 | 447 | """Runs a given check, adding a prefix to its name. | ||
449 | 448 | |||
450 | 449 | This works by wrapping the pattern and result tracker objects | ||
451 | 450 | passed to .check and .skip. | ||
452 | 451 | """ | ||
453 | 452 | |||
454 | 453 | def __init__(self, wrapped, prefix): | ||
455 | 454 | """Initialize an instance.""" | ||
456 | 455 | super(PrefixCheckWrapper, self).__init__() | ||
457 | 456 | self.wrapped = wrapped | ||
458 | 457 | self.prefix = prefix | ||
459 | 458 | |||
460 | 459 | def do_subcheck(self, subcheck, pattern, results): | ||
461 | 460 | """Do a subcheck if the pattern could still match.""" | ||
462 | 461 | pattern = pattern.assume_prefix(self.prefix) | ||
463 | 462 | if not pattern.failed(): | ||
464 | 463 | results = PrefixResultWrapper(wrapped=results, | ||
465 | 464 | prefix=self.prefix) | ||
466 | 465 | return subcheck(pattern, results) | ||
467 | 466 | |||
468 | 467 | def check(self, pattern, results): | ||
469 | 468 | """Run the check, prefixing results.""" | ||
470 | 469 | return self.do_subcheck(self.wrapped.check, pattern, results) | ||
471 | 470 | |||
472 | 471 | def skip(self, pattern, results): | ||
473 | 472 | """Skip checks, prefixing results.""" | ||
474 | 473 | self.do_subcheck(self.wrapped.skip, pattern, results) | ||
475 | 474 | |||
476 | 475 | |||
477 | 476 | @inlineCallbacks | ||
478 | 477 | def sequential_strategy(subchecks, pattern, results): | ||
479 | 478 | """Run subchecks sequentially, skipping checks after the first failure. | ||
480 | 479 | |||
481 | 480 | This is most useful when the failure of one check in the sequence | ||
482 | 481 | would imply the failure of later checks -- for example, it probably | ||
483 | 482 | doesn't make sense to run an SSL check if the basic TCP check failed. | ||
484 | 483 | |||
485 | 484 | Use sequential_check to create a meta-check using this strategy. | ||
486 | 485 | """ | ||
487 | 486 | local_results = FailureCountingResultWrapper(wrapped=results) | ||
488 | 487 | failed = False | ||
489 | 488 | for subcheck in subchecks: | ||
490 | 489 | if failed: | ||
491 | 490 | subcheck.skip(pattern, local_results) | ||
492 | 491 | else: | ||
493 | 492 | yield maybeDeferred(subcheck.check, pattern, local_results) | ||
494 | 493 | if local_results.any_failed(): | ||
495 | 494 | failed = True | ||
496 | 495 | |||
497 | 496 | |||
498 | 497 | def parallel_strategy(subchecks, pattern, results): | ||
499 | 498 | """A strategy which runs the given subchecks in parallel. | ||
500 | 499 | |||
501 | 500 | Most checks can potentially block for long periods, and shouldn't have | ||
502 | 501 | interdependencies, so it makes sense to run them in parallel to | ||
503 | 502 | shorten the overall run time. | ||
504 | 503 | |||
505 | 504 | Use parallel_check to create a meta-check using this strategy. | ||
506 | 505 | """ | ||
507 | 506 | deferreds = [maybeDeferred(subcheck.check, pattern, results) | ||
508 | 507 | for subcheck in subchecks] | ||
509 | 508 | return DeferredList(deferreds) | ||
510 | 509 | |||
511 | 510 | |||
512 | 511 | def parallel_check(subchecks): | ||
513 | 512 | """Return a check that runs the given subchecks in parallel.""" | ||
514 | 513 | return MultiCheck(subchecks=subchecks, strategy=parallel_strategy) | ||
515 | 514 | |||
516 | 515 | |||
517 | 516 | def sequential_check(subchecks): | ||
518 | 517 | """Return a check that runs the given subchecks in sequence.""" | ||
519 | 518 | return MultiCheck(subchecks=subchecks, strategy=sequential_strategy) | ||
520 | 519 | |||
521 | 520 | |||
522 | 521 | def add_check_prefix(*args): | ||
523 | 522 | """Return an equivalent check with the given prefix prepended to its name. | ||
524 | 523 | |||
525 | 524 | The final argument should be a check; the remaining arguments are treated | ||
526 | 525 | as name components and joined with the check name using periods as | ||
527 | 526 | separators. For example, if the name of a check is "baz", then: | ||
528 | 527 | |||
529 | 528 | add_check_prefix("foo", "bar", check) | ||
530 | 529 | |||
531 | 530 | ...will return a check with the effective name "foo.bar.baz". | ||
532 | 531 | """ | ||
533 | 532 | args = list(args) | ||
534 | 533 | check = args.pop(-1) | ||
535 | 534 | path = ".".join(args) | ||
536 | 535 | return PrefixCheckWrapper(wrapped=check, prefix="%s." % (path,)) | ||
537 | 536 | |||
538 | 537 | |||
539 | 538 | def make_check(name, check, info=None, blocking=False): | ||
540 | 539 | """Make a check object from a function.""" | ||
541 | 540 | return FunctionCheck(name=name, check=check, info=info, blocking=blocking) | ||
542 | 541 | |||
543 | 542 | |||
544 | 543 | def guard_check(check, predicate): | ||
545 | 544 | """Wrap a check so that it is skipped unless the predicate is true.""" | ||
546 | 545 | return ConditionalCheck(wrapped=check, predicate=predicate) | ||
547 | 546 | |||
548 | 547 | |||
549 | 548 | class TCPCheckProtocol(Protocol): | ||
550 | 549 | |||
551 | 550 | def connectionMade(self): | ||
552 | 551 | self.transport.loseConnection() | ||
553 | 552 | |||
554 | 553 | |||
555 | 554 | @inlineCallbacks | ||
556 | 555 | def do_tcp_check(host, port, ssl=False, ssl_verify=True): | ||
557 | 556 | """Generic connection check function.""" | ||
558 | 557 | if not isIPAddress(host): | ||
559 | 558 | try: | ||
560 | 559 | ip = yield reactor.resolve(host, timeout=(1, CONNECT_TIMEOUT)) | ||
561 | 560 | except DNSLookupError: | ||
562 | 561 | raise ValueError("dns resolution failed") | ||
563 | 562 | else: | ||
564 | 563 | ip = host | ||
565 | 564 | creator = ClientCreator(reactor, TCPCheckProtocol) | ||
566 | 565 | try: | ||
567 | 566 | if ssl: | ||
568 | 567 | context = VerifyingContextFactory(ssl_verify, CA_CERTS) | ||
569 | 568 | yield creator.connectSSL(ip, port, context, | ||
570 | 569 | timeout=CONNECT_TIMEOUT) | ||
571 | 570 | else: | ||
572 | 571 | yield creator.connectTCP(ip, port, timeout=CONNECT_TIMEOUT) | ||
573 | 572 | except TimeoutError: | ||
574 | 573 | if ip == host: | ||
575 | 574 | raise ValueError("timed out") | ||
576 | 575 | else: | ||
577 | 576 | raise ValueError("timed out connecting to %s" % ip) | ||
578 | 577 | |||
579 | 578 | |||
580 | 579 | def make_tcp_check(host, port, **kwargs): | ||
581 | 580 | """Return a check for TCP connectivity.""" | ||
582 | 581 | return make_check("tcp.{}:{}".format(host, port), lambda: do_tcp_check(host, port), | ||
583 | 582 | info="%s:%s" % (host, port)) | ||
584 | 583 | |||
585 | 584 | |||
586 | 585 | def make_ssl_check(host, port, verify=True, **kwargs): | ||
587 | 586 | """Return a check for SSL setup.""" | ||
588 | 587 | return make_check("ssl.{}:{}".format(host, port), | ||
589 | 588 | lambda: do_tcp_check(host, port, ssl=True, | ||
590 | 589 | ssl_verify=verify), | ||
591 | 590 | info="%s:%s" % (host, port)) | ||
592 | 591 | |||
593 | 592 | |||
594 | 593 | class UDPCheckProtocol(DatagramProtocol): | ||
595 | 594 | |||
596 | 595 | def __init__(self, host, port, send, expect, deferred=None): | ||
597 | 596 | self.host = host | ||
598 | 597 | self.port = port | ||
599 | 598 | self.send = send | ||
600 | 599 | self.expect = expect | ||
601 | 600 | self.deferred = deferred | ||
602 | 601 | |||
603 | 602 | def _finish(self, success, result): | ||
604 | 603 | if not (self.delayed.cancelled or self.delayed.called): | ||
605 | 604 | self.delayed.cancel() | ||
606 | 605 | if self.deferred is not None: | ||
607 | 606 | if success: | ||
608 | 607 | self.deferred.callback(result) | ||
609 | 608 | else: | ||
610 | 609 | self.deferred.errback(result) | ||
611 | 610 | self.deferred = None | ||
612 | 611 | |||
613 | 612 | def startProtocol(self): | ||
614 | 613 | self.transport.write(self.send, (self.host, self.port)) | ||
615 | 614 | self.delayed = reactor.callLater(CONNECT_TIMEOUT, | ||
616 | 615 | self._finish, | ||
617 | 616 | False, TimeoutError()) | ||
618 | 617 | |||
619 | 618 | def datagramReceived(self, datagram, addr): | ||
620 | 619 | if datagram == self.expect: | ||
621 | 620 | self._finish(True, True) | ||
622 | 621 | else: | ||
623 | 622 | self._finish(False, ValueError("unexpected reply")) | ||
624 | 623 | |||
625 | 624 | |||
626 | 625 | @inlineCallbacks | ||
627 | 626 | def do_udp_check(host, port, send, expect): | ||
628 | 627 | """Generic connection check function.""" | ||
629 | 628 | if not isIPAddress(host): | ||
630 | 629 | try: | ||
631 | 630 | ip = yield reactor.resolve(host, timeout=(1, CONNECT_TIMEOUT)) | ||
632 | 631 | except DNSLookupError: | ||
633 | 632 | raise ValueError("dns resolution failed") | ||
634 | 633 | else: | ||
635 | 634 | ip = host | ||
636 | 635 | deferred = Deferred() | ||
637 | 636 | protocol = UDPCheckProtocol(host, port, send, expect, deferred) | ||
638 | 637 | reactor.listenUDP(0, protocol) | ||
639 | 638 | try: | ||
640 | 639 | yield deferred | ||
641 | 640 | except TimeoutError: | ||
642 | 641 | if ip == host: | ||
643 | 642 | raise ValueError("timed out") | ||
644 | 643 | else: | ||
645 | 644 | raise ValueError("timed out waiting for %s" % ip) | ||
646 | 645 | |||
647 | 646 | |||
648 | 647 | def make_udp_check(host, port, send, expect, **kwargs): | ||
649 | 648 | """Return a check for UDP connectivity.""" | ||
650 | 649 | return make_check("udp.{}:{}".format(host, port), | ||
651 | 650 | lambda: do_udp_check(host, port, send, expect), | ||
652 | 651 | info="%s:%s" % (host, port)) | ||
653 | 652 | |||
654 | 653 | |||
655 | 654 | def extract_host_port(url): | ||
656 | 655 | parsed = urlparse.urlparse(url) | ||
657 | 656 | host = parsed.hostname | ||
658 | 657 | port = parsed.port | ||
659 | 658 | scheme = parsed.scheme | ||
660 | 659 | if not scheme: | ||
661 | 660 | scheme = 'http' | ||
662 | 661 | if port is None: | ||
663 | 662 | if scheme == 'https': | ||
664 | 663 | port = 443 | ||
665 | 664 | else: | ||
666 | 665 | port = 80 | ||
667 | 666 | return host, port, scheme | ||
668 | 667 | |||
669 | 668 | |||
670 | 669 | def make_http_check(url, method='GET', expected_code=200, **kwargs): | ||
671 | 670 | subchecks = [] | ||
672 | 671 | host, port, scheme = extract_host_port(url) | ||
673 | 672 | subchecks.append(make_tcp_check(host, port)) | ||
674 | 673 | if scheme == 'https': | ||
675 | 674 | subchecks.append(make_ssl_check(host, port)) | ||
676 | 675 | |||
677 | 676 | @inlineCallbacks | ||
678 | 677 | def do_request(): | ||
679 | 678 | agent = Agent(reactor) | ||
680 | 679 | response = yield agent.request(method, url) | ||
681 | 680 | if response.code != expected_code: | ||
682 | 681 | raise RuntimeError( | ||
683 | 682 | "Unexpected response code: {}".format(response.code)) | ||
684 | 683 | |||
685 | 684 | subchecks.append(make_check('{}.{}'.format(method, url), do_request, | ||
686 | 685 | info='{} {}'.format(method, url))) | ||
687 | 686 | return sequential_check(subchecks) | ||
688 | 687 | |||
689 | 688 | |||
690 | 689 | def make_amqp_check(host, port, username, password, use_ssl=True, vhost="/", **kwargs): | ||
691 | 690 | """Return a check for AMQP connectivity.""" | ||
692 | 691 | from txamqp.protocol import AMQClient | ||
693 | 692 | from txamqp.client import TwistedDelegate | ||
694 | 693 | from txamqp.spec import load as load_spec | ||
695 | 694 | |||
696 | 695 | subchecks = [] | ||
697 | 696 | subchecks.append(make_tcp_check(host, port)) | ||
698 | 697 | |||
699 | 698 | if use_ssl: | ||
700 | 699 | subchecks.append(make_ssl_check(host, port, verify=False)) | ||
701 | 700 | |||
702 | 701 | @inlineCallbacks | ||
703 | 702 | def do_auth(): | ||
704 | 703 | """Connect and authenticate.""" | ||
705 | 704 | delegate = TwistedDelegate() | ||
706 | 705 | spec = load_spec(resource_stream('conn_check', 'amqp0-8.xml')) | ||
707 | 706 | creator = ClientCreator(reactor, AMQClient, | ||
708 | 707 | delegate, vhost, spec) | ||
709 | 708 | client = yield creator.connectTCP(host, port, timeout=CONNECT_TIMEOUT) | ||
710 | 709 | yield client.authenticate(username, password) | ||
711 | 710 | |||
712 | 711 | subchecks.append(make_check("auth", do_auth, | ||
713 | 712 | info="user %s" % (username,),)) | ||
714 | 713 | return sequential_check(subchecks) | ||
715 | 714 | |||
716 | 715 | |||
717 | 716 | def make_postgres_check(host, port, username, password, database, **kwargs): | ||
718 | 717 | """Return a check for Postgres connectivity.""" | ||
719 | 718 | |||
720 | 719 | import psycopg2 | ||
721 | 720 | subchecks = [] | ||
722 | 721 | connect_kw = {'host': host, 'user': username, 'database': database} | ||
723 | 722 | |||
724 | 723 | if host[0] != '/': | ||
725 | 724 | connect_kw['port'] = port | ||
726 | 725 | subchecks.append(make_tcp_check(host, port)) | ||
727 | 726 | |||
728 | 727 | if password is not None: | ||
729 | 728 | connect_kw['password'] = password | ||
730 | 729 | |||
731 | 730 | def check_auth(): | ||
732 | 731 | """Try to establish a postgres connection and log in.""" | ||
733 | 732 | conn = psycopg2.connect(**connect_kw) | ||
734 | 733 | conn.close() | ||
735 | 734 | |||
736 | 735 | subchecks.append(make_check("auth", check_auth, | ||
737 | 736 | info="user %s" % (username,), | ||
738 | 737 | blocking=True)) | ||
739 | 738 | return sequential_check(subchecks) | ||
740 | 739 | |||
741 | 740 | |||
742 | 741 | def make_redis_check(host, port, password=None, **kwargs): | ||
743 | 742 | """Make a check for the configured redis server.""" | ||
744 | 743 | import txredis | ||
745 | 744 | subchecks = [] | ||
746 | 745 | subchecks.append(make_tcp_check(host, port)) | ||
747 | 746 | |||
748 | 747 | @inlineCallbacks | ||
749 | 748 | def do_connect(): | ||
750 | 749 | """Connect and authenticate. | ||
751 | 750 | """ | ||
752 | 751 | client_creator = ClientCreator(reactor, txredis.client.RedisClient) | ||
753 | 752 | client = yield client_creator.connectTCP(host=host, port=port, | ||
754 | 753 | timeout=CONNECT_TIMEOUT) | ||
755 | 754 | |||
756 | 755 | if password is None: | ||
757 | 756 | ping = yield client.ping() | ||
758 | 757 | if not ping: | ||
759 | 758 | raise RuntimeError("failed to ping redis") | ||
760 | 759 | else: | ||
761 | 760 | resp = yield client.auth(password) | ||
762 | 761 | if resp != 'OK': | ||
763 | 762 | raise RuntimeError("failed to auth to redis") | ||
764 | 763 | |||
765 | 764 | connect_info = "connect with auth" if password is not None else "connect" | ||
766 | 765 | subchecks.append(make_check(connect_info, do_connect)) | ||
767 | 766 | return add_check_prefix('redis', sequential_check(subchecks)) | ||
768 | 767 | |||
769 | 768 | |||
770 | 769 | CHECKS = { | ||
771 | 770 | 'tcp': { | ||
772 | 771 | 'fn': make_tcp_check, | ||
773 | 772 | 'args': ['host', 'port'], | ||
774 | 773 | }, | ||
775 | 774 | 'ssl': { | ||
776 | 775 | 'fn': make_ssl_check, | ||
777 | 776 | 'args': ['host', 'port'], | ||
778 | 777 | }, | ||
779 | 778 | 'udp': { | ||
780 | 779 | 'fn': make_udp_check, | ||
781 | 780 | 'args': ['host', 'port', 'send', 'expect'], | ||
782 | 781 | }, | ||
783 | 782 | 'http': { | ||
784 | 783 | 'fn': make_http_check, | ||
785 | 784 | 'args': ['url'], | ||
786 | 785 | }, | ||
787 | 786 | 'amqp': { | ||
788 | 787 | 'fn': make_amqp_check, | ||
789 | 788 | 'args': ['host', 'port', 'username', 'password'], | ||
790 | 789 | }, | ||
791 | 790 | 'postgres': { | ||
792 | 791 | 'fn': make_postgres_check, | ||
793 | 792 | 'args': ['host', 'port', 'username', 'password', 'database'], | ||
794 | 793 | }, | ||
795 | 794 | 'redis': { | ||
796 | 795 | 'fn': make_redis_check, | ||
797 | 796 | 'args': ['host', 'port'], | ||
798 | 797 | }, | ||
799 | 798 | } | ||
800 | 799 | |||
801 | 800 | |||
802 | 801 | def check_from_description(check_description): | ||
803 | 802 | _type = check_description['type'] | ||
804 | 803 | check = CHECKS.get(_type, None) | ||
805 | 804 | if check is None: | ||
806 | 805 | raise AssertionError("Unknown check type: {}, available checks: {}".format( | ||
807 | 806 | _type, CHECKS.keys())) | ||
808 | 807 | for arg in check['args']: | ||
809 | 808 | if arg not in check_description: | ||
810 | 809 | raise AssertionError('{} missing from check: {}'.format(arg, | ||
811 | 810 | check_description)) | ||
812 | 811 | res = check['fn'](**check_description) | ||
813 | 812 | return res | ||
814 | 813 | |||
815 | 814 | |||
816 | 815 | def build_checks(check_descriptions): | ||
817 | 816 | subchecks = map(check_from_description, check_descriptions) | ||
818 | 817 | return parallel_check(subchecks) | ||
819 | 818 | |||
820 | 819 | |||
821 | 820 | @inlineCallbacks | ||
822 | 821 | def run_checks(checks, pattern, results): | ||
823 | 822 | """Make and run all the pertinent checks.""" | ||
824 | 823 | try: | ||
825 | 824 | yield checks.check(pattern, results) | ||
826 | 825 | finally: | ||
827 | 826 | reactor.stop() | ||
828 | 827 | |||
829 | 828 | |||
830 | 829 | class TimestampOutput(object): | ||
831 | 830 | |||
832 | 831 | def __init__(self, output): | ||
833 | 832 | self.start = time.time() | ||
834 | 833 | self.output = output | ||
835 | 834 | |||
836 | 835 | def write(self, data): | ||
837 | 836 | self.output.write("%.3f: %s" % (time.time() - self.start, data)) | ||
838 | 837 | |||
839 | 838 | |||
840 | 839 | class ConsoleOutput(ResultTracker): | ||
841 | 840 | """Displays check results.""" | ||
842 | 841 | |||
843 | 842 | def __init__(self, output, verbose, show_tracebacks, show_duration): | ||
844 | 843 | """Initialize an instance.""" | ||
845 | 844 | super(ConsoleOutput, self).__init__() | ||
846 | 845 | self.output = output | ||
847 | 846 | self.verbose = verbose | ||
848 | 847 | self.show_tracebacks = show_tracebacks | ||
849 | 848 | self.show_duration = show_duration | ||
850 | 849 | |||
851 | 850 | def format_duration(self, duration): | ||
852 | 851 | if not self.show_duration: | ||
853 | 852 | return "" | ||
854 | 853 | return " (%.3f ms)" % duration | ||
855 | 854 | |||
856 | 855 | def notify_start(self, name, info): | ||
857 | 856 | """Register the start of a check.""" | ||
858 | 857 | if self.verbose: | ||
859 | 858 | if info: | ||
860 | 859 | info = " (%s)" % (info,) | ||
861 | 860 | self.output.write("Starting %s%s...\n" % (name, info or '')) | ||
862 | 861 | |||
863 | 862 | def notify_skip(self, name): | ||
864 | 863 | """Register a check being skipped.""" | ||
865 | 864 | self.output.write("SKIPPING %s\n" % (name,)) | ||
866 | 865 | |||
867 | 866 | def notify_success(self, name, duration): | ||
868 | 867 | """Register a success.""" | ||
869 | 868 | self.output.write("OK %s%s\n" % ( | ||
870 | 869 | name, self.format_duration(duration))) | ||
871 | 870 | |||
872 | 871 | def notify_failure(self, name, info, exc_info, duration): | ||
873 | 872 | """Register a failure.""" | ||
874 | 873 | message = str(exc_info[1]).split("\n")[0] | ||
875 | 874 | if info: | ||
876 | 875 | message = "(%s): %s" % (info, message) | ||
877 | 876 | self.output.write("FAILED %s%s: %s\n" % ( | ||
878 | 877 | name, self.format_duration(duration), message)) | ||
879 | 878 | if self.show_tracebacks: | ||
880 | 879 | formatted = traceback.format_exception(exc_info[0], | ||
881 | 880 | exc_info[1], | ||
882 | 881 | exc_info[2], | ||
883 | 882 | None) | ||
884 | 883 | lines = "".join(formatted).split("\n") | ||
885 | 884 | if len(lines) > 0 and len(lines[-1]) == 0: | ||
886 | 885 | lines.pop() | ||
887 | 886 | indented = "\n".join([" %s" % (line,) for line in lines]) | ||
888 | 887 | self.output.write("%s\n" % (indented,)) | ||
889 | 888 | |||
890 | 889 | |||
891 | 890 | def main(*args): | ||
892 | 891 | """Parse arguments, then build and run checks in a reactor.""" | ||
893 | 892 | parser = ArgumentParser() | ||
894 | 893 | parser.add_argument("config_file", | ||
895 | 894 | help="Config file specifying the checks to run.") | ||
896 | 895 | parser.add_argument("patterns", nargs='*', | ||
897 | 896 | help="Patterns to filter the checks.") | ||
898 | 897 | parser.add_argument("-v", "--verbose", dest="verbose", | ||
899 | 898 | action="store_true", default=False, | ||
900 | 899 | help="Show additional status") | ||
901 | 900 | parser.add_argument("-d", "--duration", dest="show_duration", | ||
902 | 901 | action="store_true", default=False, | ||
903 | 902 | help="Show duration") | ||
904 | 903 | parser.add_argument("-t", "--tracebacks", dest="show_tracebacks", | ||
905 | 904 | action="store_true", default=False, | ||
906 | 905 | help="Show tracebacks on failure") | ||
907 | 906 | parser.add_argument("--validate", dest="validate", | ||
908 | 907 | action="store_true", default=False, | ||
909 | 908 | help="Only validate the config file, don't run checks.") | ||
910 | 909 | options = parser.parse_args(list(args)) | ||
911 | 910 | |||
912 | 911 | if options.patterns: | ||
913 | 912 | pattern = SumPattern(map(SimplePattern, options.patterns)) | ||
914 | 913 | else: | ||
915 | 914 | pattern = SimplePattern("*") | ||
916 | 915 | |||
917 | 916 | def make_daemon_thread(*args, **kw): | ||
918 | 917 | """Create a daemon thread.""" | ||
919 | 918 | thread = Thread(*args, **kw) | ||
920 | 919 | thread.daemon = True | ||
921 | 920 | return thread | ||
922 | 921 | |||
923 | 922 | threadpool = ThreadPool(minthreads=1) | ||
924 | 923 | threadpool.threadFactory = make_daemon_thread | ||
925 | 924 | reactor.threadpool = threadpool | ||
926 | 925 | reactor.callWhenRunning(threadpool.start) | ||
927 | 926 | |||
928 | 927 | output = sys.stdout | ||
929 | 928 | if options.show_duration: | ||
930 | 929 | output = TimestampOutput(output) | ||
931 | 930 | |||
932 | 931 | results = ConsoleOutput(output=output, | ||
933 | 932 | show_tracebacks=options.show_tracebacks, | ||
934 | 933 | show_duration=options.show_duration, | ||
935 | 934 | verbose=options.verbose) | ||
936 | 935 | results = FailureCountingResultWrapper(results) | ||
937 | 936 | with open(options.config_file) as f: | ||
938 | 937 | descriptions = yaml.load(f) | ||
939 | 938 | checks = build_checks(descriptions) | ||
940 | 939 | if not options.validate: | ||
941 | 940 | reactor.callWhenRunning(run_checks, checks, pattern, results) | ||
942 | 941 | |||
943 | 942 | reactor.run() | ||
944 | 943 | |||
945 | 944 | if results.any_failed(): | ||
946 | 945 | return 1 | ||
947 | 946 | else: | ||
948 | 947 | return 0 | ||
949 | 948 | |||
950 | 949 | |||
951 | 950 | def run(): | ||
952 | 951 | exit(main(*sys.argv[1:])) | ||
953 | 952 | |||
954 | 953 | |||
955 | 954 | if __name__ == '__main__': | ||
956 | 955 | run() | ||
959 | 956 | 21 | ||
960 | === added file 'conn_check/check_impl.py' | |||
961 | --- conn_check/check_impl.py 1970-01-01 00:00:00 +0000 | |||
962 | +++ conn_check/check_impl.py 2014-07-24 22:10:31 +0000 | |||
963 | @@ -0,0 +1,325 @@ | |||
964 | 1 | import sys | ||
965 | 2 | import time | ||
966 | 3 | |||
967 | 4 | from twisted.internet import reactor | ||
968 | 5 | from twisted.internet.defer import ( | ||
969 | 6 | returnValue, | ||
970 | 7 | inlineCallbacks, | ||
971 | 8 | maybeDeferred, | ||
972 | 9 | DeferredList, | ||
973 | 10 | Deferred) | ||
974 | 11 | from twisted.python.failure import Failure | ||
975 | 12 | |||
976 | 13 | |||
977 | 14 | def maybeDeferToThread(f, *args, **kwargs): | ||
978 | 15 | """ | ||
979 | 16 | Call the function C{f} using a thread from the given threadpool and return | ||
980 | 17 | the result as a Deferred. | ||
981 | 18 | |||
982 | 19 | @param f: The function to call. May return a deferred. | ||
983 | 20 | @param *args: positional arguments to pass to f. | ||
984 | 21 | @param **kwargs: keyword arguments to pass to f. | ||
985 | 22 | |||
986 | 23 | @return: A Deferred which fires a callback with the result of f, or an | ||
987 | 24 | errback with a L{twisted.python.failure.Failure} if f throws an | ||
988 | 25 | exception. | ||
989 | 26 | """ | ||
990 | 27 | threadpool = reactor.getThreadPool() | ||
991 | 28 | |||
992 | 29 | d = Deferred() | ||
993 | 30 | |||
994 | 31 | def realOnResult(result): | ||
995 | 32 | if not isinstance(result, Failure): | ||
996 | 33 | reactor.callFromThread(d.callback, result) | ||
997 | 34 | else: | ||
998 | 35 | reactor.callFromThread(d.errback, result) | ||
999 | 36 | |||
1000 | 37 | def onResult(success, result): | ||
1001 | 38 | assert success | ||
1002 | 39 | assert isinstance(result, Deferred) | ||
1003 | 40 | result.addBoth(realOnResult) | ||
1004 | 41 | |||
1005 | 42 | threadpool.callInThreadWithCallback(onResult, maybeDeferred, | ||
1006 | 43 | f, *args, **kwargs) | ||
1007 | 44 | |||
1008 | 45 | return d | ||
1009 | 46 | |||
1010 | 47 | |||
1011 | 48 | class Check(object): | ||
1012 | 49 | """Abstract base class for objects embodying connectivity checks.""" | ||
1013 | 50 | |||
1014 | 51 | def check(self, pattern, results): | ||
1015 | 52 | """Run this check, if it matches the pattern. | ||
1016 | 53 | |||
1017 | 54 | If the pattern matches, and this is a leaf node in the check tree, | ||
1018 | 55 | implementations of Check.check should call | ||
1019 | 56 | results.notify_start, then either results.notify_success or | ||
1020 | 57 | results.notify_failure. | ||
1021 | 58 | """ | ||
1022 | 59 | raise NotImplementedError("%r.check not implemented" % type(self)) | ||
1023 | 60 | |||
1024 | 61 | def skip(self, pattern, results): | ||
1025 | 62 | """Indicate that this check has been skipped. | ||
1026 | 63 | |||
1027 | 64 | If the pattern matches and this is a leaf node in the check tree, | ||
1028 | 65 | implementations of Check.skip should call results.notify_skip. | ||
1029 | 66 | """ | ||
1030 | 67 | raise NotImplementedError("%r.skip not implemented" % type(self)) | ||
1031 | 68 | |||
1032 | 69 | |||
1033 | 70 | class ConditionalCheck(Check): | ||
1034 | 71 | """A Check that skips unless the given predicate is true at check time.""" | ||
1035 | 72 | |||
1036 | 73 | def __init__(self, wrapped, predicate): | ||
1037 | 74 | """Initialize an instance.""" | ||
1038 | 75 | super(ConditionalCheck, self).__init__() | ||
1039 | 76 | self.wrapped = wrapped | ||
1040 | 77 | self.predicate = predicate | ||
1041 | 78 | |||
1042 | 79 | def check(self, pattern, result): | ||
1043 | 80 | """Skip the check.""" | ||
1044 | 81 | if self.predicate(): | ||
1045 | 82 | return self.wrapped.check(pattern, result) | ||
1046 | 83 | else: | ||
1047 | 84 | self.skip(pattern, result) | ||
1048 | 85 | |||
1049 | 86 | def skip(self, pattern, result): | ||
1050 | 87 | """Skip the check.""" | ||
1051 | 88 | self.wrapped.skip(pattern, result) | ||
1052 | 89 | |||
1053 | 90 | |||
1054 | 91 | class ResultTracker(object): | ||
1055 | 92 | """Base class for objects which report or record check results.""" | ||
1056 | 93 | |||
1057 | 94 | def notify_start(self, name, info): | ||
1058 | 95 | """Register the start of a check.""" | ||
1059 | 96 | |||
1060 | 97 | def notify_skip(self, name): | ||
1061 | 98 | """Register a check being skipped.""" | ||
1062 | 99 | |||
1063 | 100 | def notify_success(self, name, duration): | ||
1064 | 101 | """Register a successful check.""" | ||
1065 | 102 | |||
1066 | 103 | def notify_failure(self, name, info, exc_info, duration): | ||
1067 | 104 | """Register the failure of a check.""" | ||
1068 | 105 | |||
1069 | 106 | |||
1070 | 107 | class PrefixResultWrapper(ResultTracker): | ||
1071 | 108 | """ResultWrapper wrapper which adds a prefix to recorded results.""" | ||
1072 | 109 | |||
1073 | 110 | def __init__(self, wrapped, prefix): | ||
1074 | 111 | """Initialize an instance.""" | ||
1075 | 112 | super(PrefixResultWrapper, self).__init__() | ||
1076 | 113 | self.wrapped = wrapped | ||
1077 | 114 | self.prefix = prefix | ||
1078 | 115 | |||
1079 | 116 | def make_name(self, name): | ||
1080 | 117 | """Make a name by prepending the prefix.""" | ||
1081 | 118 | return "%s%s" % (self.prefix, name) | ||
1082 | 119 | |||
1083 | 120 | def notify_skip(self, name): | ||
1084 | 121 | """Register a check being skipped.""" | ||
1085 | 122 | self.wrapped.notify_skip(self.make_name(name)) | ||
1086 | 123 | |||
1087 | 124 | def notify_start(self, name, info): | ||
1088 | 125 | """Register the start of a check.""" | ||
1089 | 126 | self.wrapped.notify_start(self.make_name(name), info) | ||
1090 | 127 | |||
1091 | 128 | def notify_success(self, name, duration): | ||
1092 | 129 | """Register success.""" | ||
1093 | 130 | self.wrapped.notify_success(self.make_name(name), duration) | ||
1094 | 131 | |||
1095 | 132 | def notify_failure(self, name, info, exc_info, duration): | ||
1096 | 133 | """Register failure.""" | ||
1097 | 134 | self.wrapped.notify_failure(self.make_name(name), | ||
1098 | 135 | info, exc_info, duration) | ||
1099 | 136 | |||
1100 | 137 | |||
1101 | 138 | class FailureCountingResultWrapper(ResultTracker): | ||
1102 | 139 | """ResultWrapper wrapper which counts failures.""" | ||
1103 | 140 | |||
1104 | 141 | def __init__(self, wrapped): | ||
1105 | 142 | """Initialize an instance.""" | ||
1106 | 143 | super(FailureCountingResultWrapper, self).__init__() | ||
1107 | 144 | self.wrapped = wrapped | ||
1108 | 145 | self.failure_count = 0 | ||
1109 | 146 | |||
1110 | 147 | def notify_skip(self, name): | ||
1111 | 148 | """Register a check being skipped.""" | ||
1112 | 149 | self.wrapped.notify_skip(name) | ||
1113 | 150 | |||
1114 | 151 | def notify_start(self, name, info): | ||
1115 | 152 | """Register the start of a check.""" | ||
1116 | 153 | self.failure_count += 1 | ||
1117 | 154 | self.wrapped.notify_start(name, info) | ||
1118 | 155 | |||
1119 | 156 | def notify_success(self, name, duration): | ||
1120 | 157 | """Register success.""" | ||
1121 | 158 | self.failure_count -= 1 | ||
1122 | 159 | self.wrapped.notify_success(name, duration) | ||
1123 | 160 | |||
1124 | 161 | def notify_failure(self, name, info, exc_info, duration): | ||
1125 | 162 | """Register failure.""" | ||
1126 | 163 | self.wrapped.notify_failure(name, info, exc_info, duration) | ||
1127 | 164 | |||
1128 | 165 | def any_failed(self): | ||
1129 | 166 | """Return True if any checks using this wrapper failed so far.""" | ||
1130 | 167 | return self.failure_count > 0 | ||
1131 | 168 | |||
1132 | 169 | |||
1133 | 170 | class FunctionCheck(Check): | ||
1134 | 171 | """A Check which takes a check function.""" | ||
1135 | 172 | |||
1136 | 173 | def __init__(self, name, check, info=None, blocking=False): | ||
1137 | 174 | """Initialize an instance.""" | ||
1138 | 175 | super(FunctionCheck, self).__init__() | ||
1139 | 176 | self.name = name | ||
1140 | 177 | self.info = info | ||
1141 | 178 | self.check_fn = check | ||
1142 | 179 | self.blocking = blocking | ||
1143 | 180 | |||
1144 | 181 | @inlineCallbacks | ||
1145 | 182 | def check(self, pattern, results): | ||
1146 | 183 | """Call the check function.""" | ||
1147 | 184 | if not pattern.matches(self.name): | ||
1148 | 185 | returnValue(None) | ||
1149 | 186 | results.notify_start(self.name, self.info) | ||
1150 | 187 | start = time.time() | ||
1151 | 188 | try: | ||
1152 | 189 | if self.blocking: | ||
1153 | 190 | result = yield maybeDeferToThread(self.check_fn) | ||
1154 | 191 | else: | ||
1155 | 192 | result = yield maybeDeferred(self.check_fn) | ||
1156 | 193 | results.notify_success(self.name, time.time() - start) | ||
1157 | 194 | returnValue(result) | ||
1158 | 195 | except Exception: | ||
1159 | 196 | results.notify_failure(self.name, self.info, | ||
1160 | 197 | sys.exc_info(), time.time() - start) | ||
1161 | 198 | |||
1162 | 199 | def skip(self, pattern, results): | ||
1163 | 200 | """Record the skip.""" | ||
1164 | 201 | if not pattern.matches(self.name): | ||
1165 | 202 | return | ||
1166 | 203 | results.notify_skip(self.name) | ||
1167 | 204 | |||
1168 | 205 | |||
1169 | 206 | class MultiCheck(Check): | ||
1170 | 207 | """A composite check comprised of multiple subchecks.""" | ||
1171 | 208 | |||
1172 | 209 | def __init__(self, subchecks, strategy): | ||
1173 | 210 | """Initialize an instance.""" | ||
1174 | 211 | super(MultiCheck, self).__init__() | ||
1175 | 212 | self.subchecks = list(subchecks) | ||
1176 | 213 | self.strategy = strategy | ||
1177 | 214 | |||
1178 | 215 | def check(self, pattern, results): | ||
1179 | 216 | """Run subchecks using the strategy supplied at creation time.""" | ||
1180 | 217 | return self.strategy(self.subchecks, pattern, results) | ||
1181 | 218 | |||
1182 | 219 | def skip(self, pattern, results): | ||
1183 | 220 | """Skip subchecks.""" | ||
1184 | 221 | for subcheck in self.subchecks: | ||
1185 | 222 | subcheck.skip(pattern, results) | ||
1186 | 223 | |||
1187 | 224 | |||
1188 | 225 | class PrefixCheckWrapper(Check): | ||
1189 | 226 | """Runs a given check, adding a prefix to its name. | ||
1190 | 227 | |||
1191 | 228 | This works by wrapping the pattern and result tracker objects | ||
1192 | 229 | passed to .check and .skip. | ||
1193 | 230 | """ | ||
1194 | 231 | |||
1195 | 232 | def __init__(self, wrapped, prefix): | ||
1196 | 233 | """Initialize an instance.""" | ||
1197 | 234 | super(PrefixCheckWrapper, self).__init__() | ||
1198 | 235 | self.wrapped = wrapped | ||
1199 | 236 | self.prefix = prefix | ||
1200 | 237 | |||
1201 | 238 | def do_subcheck(self, subcheck, pattern, results): | ||
1202 | 239 | """Do a subcheck if the pattern could still match.""" | ||
1203 | 240 | pattern = pattern.assume_prefix(self.prefix) | ||
1204 | 241 | if not pattern.failed(): | ||
1205 | 242 | results = PrefixResultWrapper(wrapped=results, | ||
1206 | 243 | prefix=self.prefix) | ||
1207 | 244 | return subcheck(pattern, results) | ||
1208 | 245 | |||
1209 | 246 | def check(self, pattern, results): | ||
1210 | 247 | """Run the check, prefixing results.""" | ||
1211 | 248 | return self.do_subcheck(self.wrapped.check, pattern, results) | ||
1212 | 249 | |||
1213 | 250 | def skip(self, pattern, results): | ||
1214 | 251 | """Skip checks, prefixing results.""" | ||
1215 | 252 | self.do_subcheck(self.wrapped.skip, pattern, results) | ||
1216 | 253 | |||
1217 | 254 | |||
1218 | 255 | @inlineCallbacks | ||
1219 | 256 | def sequential_strategy(subchecks, pattern, results): | ||
1220 | 257 | """Run subchecks sequentially, skipping checks after the first failure. | ||
1221 | 258 | |||
1222 | 259 | This is most useful when the failure of one check in the sequence | ||
1223 | 260 | would imply the failure of later checks -- for example, it probably | ||
1224 | 261 | doesn't make sense to run an SSL check if the basic TCP check failed. | ||
1225 | 262 | |||
1226 | 263 | Use sequential_check to create a meta-check using this strategy. | ||
1227 | 264 | """ | ||
1228 | 265 | local_results = FailureCountingResultWrapper(wrapped=results) | ||
1229 | 266 | failed = False | ||
1230 | 267 | for subcheck in subchecks: | ||
1231 | 268 | if failed: | ||
1232 | 269 | subcheck.skip(pattern, local_results) | ||
1233 | 270 | else: | ||
1234 | 271 | yield maybeDeferred(subcheck.check, pattern, local_results) | ||
1235 | 272 | if local_results.any_failed(): | ||
1236 | 273 | failed = True | ||
1237 | 274 | |||
1238 | 275 | |||
1239 | 276 | def parallel_strategy(subchecks, pattern, results): | ||
1240 | 277 | """A strategy which runs the given subchecks in parallel. | ||
1241 | 278 | |||
1242 | 279 | Most checks can potentially block for long periods, and shouldn't have | ||
1243 | 280 | interdependencies, so it makes sense to run them in parallel to | ||
1244 | 281 | shorten the overall run time. | ||
1245 | 282 | |||
1246 | 283 | Use parallel_check to create a meta-check using this strategy. | ||
1247 | 284 | """ | ||
1248 | 285 | deferreds = [maybeDeferred(subcheck.check, pattern, results) | ||
1249 | 286 | for subcheck in subchecks] | ||
1250 | 287 | return DeferredList(deferreds) | ||
1251 | 288 | |||
1252 | 289 | |||
1253 | 290 | def parallel_check(subchecks): | ||
1254 | 291 | """Return a check that runs the given subchecks in parallel.""" | ||
1255 | 292 | return MultiCheck(subchecks=subchecks, strategy=parallel_strategy) | ||
1256 | 293 | |||
1257 | 294 | |||
1258 | 295 | def sequential_check(subchecks): | ||
1259 | 296 | """Return a check that runs the given subchecks in sequence.""" | ||
1260 | 297 | return MultiCheck(subchecks=subchecks, strategy=sequential_strategy) | ||
1261 | 298 | |||
1262 | 299 | |||
1263 | 300 | def add_check_prefix(*args): | ||
1264 | 301 | """Return an equivalent check with the given prefix prepended to its name. | ||
1265 | 302 | |||
1266 | 303 | The final argument should be a check; the remaining arguments are treated | ||
1267 | 304 | as name components and joined with the check name using periods as | ||
1268 | 305 | separators. For example, if the name of a check is "baz", then: | ||
1269 | 306 | |||
1270 | 307 | add_check_prefix("foo", "bar", check) | ||
1271 | 308 | |||
1272 | 309 | ...will return a check with the effective name "foo.bar.baz". | ||
1273 | 310 | """ | ||
1274 | 311 | args = list(args) | ||
1275 | 312 | check = args.pop(-1) | ||
1276 | 313 | path = ".".join(args) | ||
1277 | 314 | return PrefixCheckWrapper(wrapped=check, prefix="%s." % (path,)) | ||
1278 | 315 | |||
1279 | 316 | |||
1280 | 317 | def make_check(name, check, info=None, blocking=False): | ||
1281 | 318 | """Make a check object from a function.""" | ||
1282 | 319 | return FunctionCheck(name=name, check=check, info=info, blocking=blocking) | ||
1283 | 320 | |||
1284 | 321 | |||
1285 | 322 | def guard_check(check, predicate): | ||
1286 | 323 | """Wrap a check so that it is skipped unless the predicate is true.""" | ||
1287 | 324 | return ConditionalCheck(wrapped=check, predicate=predicate) | ||
1288 | 325 | |||
1289 | 0 | 326 | ||
1290 | === added file 'conn_check/checks.py' | |||
1291 | --- conn_check/checks.py 1970-01-01 00:00:00 +0000 | |||
1292 | +++ conn_check/checks.py 2014-07-24 22:10:31 +0000 | |||
1293 | @@ -0,0 +1,310 @@ | |||
1294 | 1 | import glob | ||
1295 | 2 | import os | ||
1296 | 3 | from pkg_resources import resource_stream | ||
1297 | 4 | import urlparse | ||
1298 | 5 | |||
1299 | 6 | from OpenSSL import SSL | ||
1300 | 7 | from OpenSSL.crypto import load_certificate, FILETYPE_PEM | ||
1301 | 8 | |||
1302 | 9 | from twisted.internet import reactor, ssl | ||
1303 | 10 | from twisted.internet.error import DNSLookupError, TimeoutError | ||
1304 | 11 | from twisted.internet.abstract import isIPAddress | ||
1305 | 12 | from twisted.internet.defer import ( | ||
1306 | 13 | Deferred, | ||
1307 | 14 | inlineCallbacks, | ||
1308 | 15 | ) | ||
1309 | 16 | from twisted.internet.protocol import ( | ||
1310 | 17 | ClientCreator, | ||
1311 | 18 | DatagramProtocol, | ||
1312 | 19 | Protocol, | ||
1313 | 20 | ) | ||
1314 | 21 | from twisted.web.client import Agent | ||
1315 | 22 | |||
1316 | 23 | from .check_impl import ( | ||
1317 | 24 | add_check_prefix, | ||
1318 | 25 | make_check, | ||
1319 | 26 | sequential_check, | ||
1320 | 27 | ) | ||
1321 | 28 | |||
1322 | 29 | |||
1323 | 30 | CONNECT_TIMEOUT = 10 | ||
1324 | 31 | CA_CERTS = [] | ||
1325 | 32 | |||
1326 | 33 | |||
1327 | 34 | for certFileName in glob.glob("/etc/ssl/certs/*.pem"): | ||
1328 | 35 | # There might be some dead symlinks in there, so let's make sure it's real. | ||
1329 | 36 | if os.path.exists(certFileName): | ||
1330 | 37 | data = open(certFileName).read() | ||
1331 | 38 | x509 = load_certificate(FILETYPE_PEM, data) | ||
1332 | 39 | # Now, de-duplicate in case the same cert has multiple names. | ||
1333 | 40 | CA_CERTS.append(x509) | ||
1334 | 41 | |||
1335 | 42 | |||
1336 | 43 | class TCPCheckProtocol(Protocol): | ||
1337 | 44 | |||
1338 | 45 | def connectionMade(self): | ||
1339 | 46 | self.transport.loseConnection() | ||
1340 | 47 | |||
1341 | 48 | |||
1342 | 49 | class VerifyingContextFactory(ssl.CertificateOptions): | ||
1343 | 50 | |||
1344 | 51 | def __init__(self, verify, caCerts, verifyCallback=None): | ||
1345 | 52 | ssl.CertificateOptions.__init__(self, verify=verify, | ||
1346 | 53 | caCerts=caCerts, | ||
1347 | 54 | method=SSL.SSLv23_METHOD) | ||
1348 | 55 | self.verifyCallback = verifyCallback | ||
1349 | 56 | |||
1350 | 57 | def _makeContext(self): | ||
1351 | 58 | context = ssl.CertificateOptions._makeContext(self) | ||
1352 | 59 | if self.verifyCallback is not None: | ||
1353 | 60 | context.set_verify( | ||
1354 | 61 | SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, | ||
1355 | 62 | self.verifyCallback) | ||
1356 | 63 | return context | ||
1357 | 64 | |||
1358 | 65 | |||
1359 | 66 | @inlineCallbacks | ||
1360 | 67 | def do_tcp_check(host, port, ssl=False, ssl_verify=True): | ||
1361 | 68 | """Generic connection check function.""" | ||
1362 | 69 | if not isIPAddress(host): | ||
1363 | 70 | try: | ||
1364 | 71 | ip = yield reactor.resolve(host, timeout=(1, CONNECT_TIMEOUT)) | ||
1365 | 72 | except DNSLookupError: | ||
1366 | 73 | raise ValueError("dns resolution failed") | ||
1367 | 74 | else: | ||
1368 | 75 | ip = host | ||
1369 | 76 | creator = ClientCreator(reactor, TCPCheckProtocol) | ||
1370 | 77 | try: | ||
1371 | 78 | if ssl: | ||
1372 | 79 | context = VerifyingContextFactory(ssl_verify, CA_CERTS) | ||
1373 | 80 | yield creator.connectSSL(ip, port, context, | ||
1374 | 81 | timeout=CONNECT_TIMEOUT) | ||
1375 | 82 | else: | ||
1376 | 83 | yield creator.connectTCP(ip, port, timeout=CONNECT_TIMEOUT) | ||
1377 | 84 | except TimeoutError: | ||
1378 | 85 | if ip == host: | ||
1379 | 86 | raise ValueError("timed out") | ||
1380 | 87 | else: | ||
1381 | 88 | raise ValueError("timed out connecting to %s" % ip) | ||
1382 | 89 | |||
1383 | 90 | |||
1384 | 91 | def make_tcp_check(host, port, **kwargs): | ||
1385 | 92 | """Return a check for TCP connectivity.""" | ||
1386 | 93 | return make_check("tcp.{}:{}".format(host, port), lambda: do_tcp_check(host, port), | ||
1387 | 94 | info="%s:%s" % (host, port)) | ||
1388 | 95 | |||
1389 | 96 | |||
1390 | 97 | def make_ssl_check(host, port, verify=True, **kwargs): | ||
1391 | 98 | """Return a check for SSL setup.""" | ||
1392 | 99 | return make_check("ssl.{}:{}".format(host, port), | ||
1393 | 100 | lambda: do_tcp_check(host, port, ssl=True, | ||
1394 | 101 | ssl_verify=verify), | ||
1395 | 102 | info="%s:%s" % (host, port)) | ||
1396 | 103 | |||
1397 | 104 | |||
1398 | 105 | class UDPCheckProtocol(DatagramProtocol): | ||
1399 | 106 | |||
1400 | 107 | def __init__(self, host, port, send, expect, deferred=None): | ||
1401 | 108 | self.host = host | ||
1402 | 109 | self.port = port | ||
1403 | 110 | self.send = send | ||
1404 | 111 | self.expect = expect | ||
1405 | 112 | self.deferred = deferred | ||
1406 | 113 | |||
1407 | 114 | def _finish(self, success, result): | ||
1408 | 115 | if not (self.delayed.cancelled or self.delayed.called): | ||
1409 | 116 | self.delayed.cancel() | ||
1410 | 117 | if self.deferred is not None: | ||
1411 | 118 | if success: | ||
1412 | 119 | self.deferred.callback(result) | ||
1413 | 120 | else: | ||
1414 | 121 | self.deferred.errback(result) | ||
1415 | 122 | self.deferred = None | ||
1416 | 123 | |||
1417 | 124 | def startProtocol(self): | ||
1418 | 125 | self.transport.write(self.send, (self.host, self.port)) | ||
1419 | 126 | self.delayed = reactor.callLater(CONNECT_TIMEOUT, | ||
1420 | 127 | self._finish, | ||
1421 | 128 | False, TimeoutError()) | ||
1422 | 129 | |||
1423 | 130 | def datagramReceived(self, datagram, addr): | ||
1424 | 131 | if datagram == self.expect: | ||
1425 | 132 | self._finish(True, True) | ||
1426 | 133 | else: | ||
1427 | 134 | self._finish(False, ValueError("unexpected reply")) | ||
1428 | 135 | |||
1429 | 136 | |||
1430 | 137 | @inlineCallbacks | ||
1431 | 138 | def do_udp_check(host, port, send, expect): | ||
1432 | 139 | """Generic connection check function.""" | ||
1433 | 140 | if not isIPAddress(host): | ||
1434 | 141 | try: | ||
1435 | 142 | ip = yield reactor.resolve(host, timeout=(1, CONNECT_TIMEOUT)) | ||
1436 | 143 | except DNSLookupError: | ||
1437 | 144 | raise ValueError("dns resolution failed") | ||
1438 | 145 | else: | ||
1439 | 146 | ip = host | ||
1440 | 147 | deferred = Deferred() | ||
1441 | 148 | protocol = UDPCheckProtocol(host, port, send, expect, deferred) | ||
1442 | 149 | reactor.listenUDP(0, protocol) | ||
1443 | 150 | try: | ||
1444 | 151 | yield deferred | ||
1445 | 152 | except TimeoutError: | ||
1446 | 153 | if ip == host: | ||
1447 | 154 | raise ValueError("timed out") | ||
1448 | 155 | else: | ||
1449 | 156 | raise ValueError("timed out waiting for %s" % ip) | ||
1450 | 157 | |||
1451 | 158 | |||
1452 | 159 | def make_udp_check(host, port, send, expect, **kwargs): | ||
1453 | 160 | """Return a check for UDP connectivity.""" | ||
1454 | 161 | return make_check("udp.{}:{}".format(host, port), | ||
1455 | 162 | lambda: do_udp_check(host, port, send, expect), | ||
1456 | 163 | info="%s:%s" % (host, port)) | ||
1457 | 164 | |||
1458 | 165 | |||
1459 | 166 | def extract_host_port(url): | ||
1460 | 167 | parsed = urlparse.urlparse(url) | ||
1461 | 168 | host = parsed.hostname | ||
1462 | 169 | port = parsed.port | ||
1463 | 170 | scheme = parsed.scheme | ||
1464 | 171 | if not scheme: | ||
1465 | 172 | scheme = 'http' | ||
1466 | 173 | if port is None: | ||
1467 | 174 | if scheme == 'https': | ||
1468 | 175 | port = 443 | ||
1469 | 176 | else: | ||
1470 | 177 | port = 80 | ||
1471 | 178 | return host, port, scheme | ||
1472 | 179 | |||
1473 | 180 | |||
1474 | 181 | def make_http_check(url, method='GET', expected_code=200, **kwargs): | ||
1475 | 182 | subchecks = [] | ||
1476 | 183 | host, port, scheme = extract_host_port(url) | ||
1477 | 184 | subchecks.append(make_tcp_check(host, port)) | ||
1478 | 185 | if scheme == 'https': | ||
1479 | 186 | subchecks.append(make_ssl_check(host, port)) | ||
1480 | 187 | |||
1481 | 188 | @inlineCallbacks | ||
1482 | 189 | def do_request(): | ||
1483 | 190 | agent = Agent(reactor) | ||
1484 | 191 | response = yield agent.request(method, url) | ||
1485 | 192 | if response.code != expected_code: | ||
1486 | 193 | raise RuntimeError( | ||
1487 | 194 | "Unexpected response code: {}".format(response.code)) | ||
1488 | 195 | |||
1489 | 196 | subchecks.append(make_check('{}.{}'.format(method, url), do_request, | ||
1490 | 197 | info='{} {}'.format(method, url))) | ||
1491 | 198 | return sequential_check(subchecks) | ||
1492 | 199 | |||
1493 | 200 | |||
1494 | 201 | def make_amqp_check(host, port, username, password, use_ssl=True, vhost="/", **kwargs): | ||
1495 | 202 | """Return a check for AMQP connectivity.""" | ||
1496 | 203 | from txamqp.protocol import AMQClient | ||
1497 | 204 | from txamqp.client import TwistedDelegate | ||
1498 | 205 | from txamqp.spec import load as load_spec | ||
1499 | 206 | |||
1500 | 207 | subchecks = [] | ||
1501 | 208 | subchecks.append(make_tcp_check(host, port)) | ||
1502 | 209 | |||
1503 | 210 | if use_ssl: | ||
1504 | 211 | subchecks.append(make_ssl_check(host, port, verify=False)) | ||
1505 | 212 | |||
1506 | 213 | @inlineCallbacks | ||
1507 | 214 | def do_auth(): | ||
1508 | 215 | """Connect and authenticate.""" | ||
1509 | 216 | delegate = TwistedDelegate() | ||
1510 | 217 | spec = load_spec(resource_stream('conn_check', 'amqp0-8.xml')) | ||
1511 | 218 | creator = ClientCreator(reactor, AMQClient, | ||
1512 | 219 | delegate, vhost, spec) | ||
1513 | 220 | client = yield creator.connectTCP(host, port, timeout=CONNECT_TIMEOUT) | ||
1514 | 221 | yield client.authenticate(username, password) | ||
1515 | 222 | |||
1516 | 223 | subchecks.append(make_check("auth", do_auth, | ||
1517 | 224 | info="user %s" % (username,),)) | ||
1518 | 225 | return sequential_check(subchecks) | ||
1519 | 226 | |||
1520 | 227 | |||
1521 | 228 | def make_postgres_check(host, port, username, password, database, **kwargs): | ||
1522 | 229 | """Return a check for Postgres connectivity.""" | ||
1523 | 230 | |||
1524 | 231 | import psycopg2 | ||
1525 | 232 | subchecks = [] | ||
1526 | 233 | connect_kw = {'host': host, 'user': username, 'database': database} | ||
1527 | 234 | |||
1528 | 235 | if host[0] != '/': | ||
1529 | 236 | connect_kw['port'] = port | ||
1530 | 237 | subchecks.append(make_tcp_check(host, port)) | ||
1531 | 238 | |||
1532 | 239 | if password is not None: | ||
1533 | 240 | connect_kw['password'] = password | ||
1534 | 241 | |||
1535 | 242 | def check_auth(): | ||
1536 | 243 | """Try to establish a postgres connection and log in.""" | ||
1537 | 244 | conn = psycopg2.connect(**connect_kw) | ||
1538 | 245 | conn.close() | ||
1539 | 246 | |||
1540 | 247 | subchecks.append(make_check("auth", check_auth, | ||
1541 | 248 | info="user %s" % (username,), | ||
1542 | 249 | blocking=True)) | ||
1543 | 250 | return sequential_check(subchecks) | ||
1544 | 251 | |||
1545 | 252 | |||
1546 | 253 | def make_redis_check(host, port, password=None, **kwargs): | ||
1547 | 254 | """Make a check for the configured redis server.""" | ||
1548 | 255 | import txredis | ||
1549 | 256 | subchecks = [] | ||
1550 | 257 | subchecks.append(make_tcp_check(host, port)) | ||
1551 | 258 | |||
1552 | 259 | @inlineCallbacks | ||
1553 | 260 | def do_connect(): | ||
1554 | 261 | """Connect and authenticate. | ||
1555 | 262 | """ | ||
1556 | 263 | client_creator = ClientCreator(reactor, txredis.client.RedisClient) | ||
1557 | 264 | client = yield client_creator.connectTCP(host=host, port=port, | ||
1558 | 265 | timeout=CONNECT_TIMEOUT) | ||
1559 | 266 | |||
1560 | 267 | if password is None: | ||
1561 | 268 | ping = yield client.ping() | ||
1562 | 269 | if not ping: | ||
1563 | 270 | raise RuntimeError("failed to ping redis") | ||
1564 | 271 | else: | ||
1565 | 272 | resp = yield client.auth(password) | ||
1566 | 273 | if resp != 'OK': | ||
1567 | 274 | raise RuntimeError("failed to auth to redis") | ||
1568 | 275 | |||
1569 | 276 | connect_info = "connect with auth" if password is not None else "connect" | ||
1570 | 277 | subchecks.append(make_check(connect_info, do_connect)) | ||
1571 | 278 | return add_check_prefix('redis', sequential_check(subchecks)) | ||
1572 | 279 | |||
1573 | 280 | |||
1574 | 281 | CHECKS = { | ||
1575 | 282 | 'tcp': { | ||
1576 | 283 | 'fn': make_tcp_check, | ||
1577 | 284 | 'args': ['host', 'port'], | ||
1578 | 285 | }, | ||
1579 | 286 | 'ssl': { | ||
1580 | 287 | 'fn': make_ssl_check, | ||
1581 | 288 | 'args': ['host', 'port'], | ||
1582 | 289 | }, | ||
1583 | 290 | 'udp': { | ||
1584 | 291 | 'fn': make_udp_check, | ||
1585 | 292 | 'args': ['host', 'port', 'send', 'expect'], | ||
1586 | 293 | }, | ||
1587 | 294 | 'http': { | ||
1588 | 295 | 'fn': make_http_check, | ||
1589 | 296 | 'args': ['url'], | ||
1590 | 297 | }, | ||
1591 | 298 | 'amqp': { | ||
1592 | 299 | 'fn': make_amqp_check, | ||
1593 | 300 | 'args': ['host', 'port', 'username', 'password'], | ||
1594 | 301 | }, | ||
1595 | 302 | 'postgres': { | ||
1596 | 303 | 'fn': make_postgres_check, | ||
1597 | 304 | 'args': ['host', 'port', 'username', 'password', 'database'], | ||
1598 | 305 | }, | ||
1599 | 306 | 'redis': { | ||
1600 | 307 | 'fn': make_redis_check, | ||
1601 | 308 | 'args': ['host', 'port'], | ||
1602 | 309 | }, | ||
1603 | 310 | } | ||
1604 | 0 | 311 | ||
1605 | === added file 'conn_check/main.py' | |||
1606 | --- conn_check/main.py 1970-01-01 00:00:00 +0000 | |||
1607 | +++ conn_check/main.py 2014-07-24 22:10:31 +0000 | |||
1608 | @@ -0,0 +1,181 @@ | |||
1609 | 1 | from argparse import ArgumentParser | ||
1610 | 2 | import sys | ||
1611 | 3 | from threading import Thread | ||
1612 | 4 | import time | ||
1613 | 5 | import traceback | ||
1614 | 6 | import yaml | ||
1615 | 7 | |||
1616 | 8 | from twisted.internet import reactor | ||
1617 | 9 | from twisted.internet.defer import ( | ||
1618 | 10 | inlineCallbacks, | ||
1619 | 11 | ) | ||
1620 | 12 | from twisted.python.threadpool import ThreadPool | ||
1621 | 13 | |||
1622 | 14 | from .check_impl import ( | ||
1623 | 15 | FailureCountingResultWrapper, | ||
1624 | 16 | parallel_check, | ||
1625 | 17 | ResultTracker, | ||
1626 | 18 | ) | ||
1627 | 19 | from .checks import CHECKS | ||
1628 | 20 | from .patterns import ( | ||
1629 | 21 | SimplePattern, | ||
1630 | 22 | SumPattern, | ||
1631 | 23 | ) | ||
1632 | 24 | |||
1633 | 25 | |||
1634 | 26 | def check_from_description(check_description): | ||
1635 | 27 | _type = check_description['type'] | ||
1636 | 28 | check = CHECKS.get(_type, None) | ||
1637 | 29 | if check is None: | ||
1638 | 30 | raise AssertionError("Unknown check type: {}, available checks: {}".format( | ||
1639 | 31 | _type, CHECKS.keys())) | ||
1640 | 32 | for arg in check['args']: | ||
1641 | 33 | if arg not in check_description: | ||
1642 | 34 | raise AssertionError('{} missing from check: {}'.format(arg, | ||
1643 | 35 | check_description)) | ||
1644 | 36 | res = check['fn'](**check_description) | ||
1645 | 37 | return res | ||
1646 | 38 | |||
1647 | 39 | |||
1648 | 40 | def build_checks(check_descriptions): | ||
1649 | 41 | subchecks = map(check_from_description, check_descriptions) | ||
1650 | 42 | return parallel_check(subchecks) | ||
1651 | 43 | |||
1652 | 44 | |||
1653 | 45 | @inlineCallbacks | ||
1654 | 46 | def run_checks(checks, pattern, results): | ||
1655 | 47 | """Make and run all the pertinent checks.""" | ||
1656 | 48 | try: | ||
1657 | 49 | yield checks.check(pattern, results) | ||
1658 | 50 | finally: | ||
1659 | 51 | reactor.stop() | ||
1660 | 52 | |||
1661 | 53 | |||
1662 | 54 | class TimestampOutput(object): | ||
1663 | 55 | |||
1664 | 56 | def __init__(self, output): | ||
1665 | 57 | self.start = time.time() | ||
1666 | 58 | self.output = output | ||
1667 | 59 | |||
1668 | 60 | def write(self, data): | ||
1669 | 61 | self.output.write("%.3f: %s" % (time.time() - self.start, data)) | ||
1670 | 62 | |||
1671 | 63 | |||
1672 | 64 | class ConsoleOutput(ResultTracker): | ||
1673 | 65 | """Displays check results.""" | ||
1674 | 66 | |||
1675 | 67 | def __init__(self, output, verbose, show_tracebacks, show_duration): | ||
1676 | 68 | """Initialize an instance.""" | ||
1677 | 69 | super(ConsoleOutput, self).__init__() | ||
1678 | 70 | self.output = output | ||
1679 | 71 | self.verbose = verbose | ||
1680 | 72 | self.show_tracebacks = show_tracebacks | ||
1681 | 73 | self.show_duration = show_duration | ||
1682 | 74 | |||
1683 | 75 | def format_duration(self, duration): | ||
1684 | 76 | if not self.show_duration: | ||
1685 | 77 | return "" | ||
1686 | 78 | return " (%.3f ms)" % duration | ||
1687 | 79 | |||
1688 | 80 | def notify_start(self, name, info): | ||
1689 | 81 | """Register the start of a check.""" | ||
1690 | 82 | if self.verbose: | ||
1691 | 83 | if info: | ||
1692 | 84 | info = " (%s)" % (info,) | ||
1693 | 85 | self.output.write("Starting %s%s...\n" % (name, info or '')) | ||
1694 | 86 | |||
1695 | 87 | def notify_skip(self, name): | ||
1696 | 88 | """Register a check being skipped.""" | ||
1697 | 89 | self.output.write("SKIPPING %s\n" % (name,)) | ||
1698 | 90 | |||
1699 | 91 | def notify_success(self, name, duration): | ||
1700 | 92 | """Register a success.""" | ||
1701 | 93 | self.output.write("OK %s%s\n" % ( | ||
1702 | 94 | name, self.format_duration(duration))) | ||
1703 | 95 | |||
1704 | 96 | def notify_failure(self, name, info, exc_info, duration): | ||
1705 | 97 | """Register a failure.""" | ||
1706 | 98 | message = str(exc_info[1]).split("\n")[0] | ||
1707 | 99 | if info: | ||
1708 | 100 | message = "(%s): %s" % (info, message) | ||
1709 | 101 | self.output.write("FAILED %s%s: %s\n" % ( | ||
1710 | 102 | name, self.format_duration(duration), message)) | ||
1711 | 103 | if self.show_tracebacks: | ||
1712 | 104 | formatted = traceback.format_exception(exc_info[0], | ||
1713 | 105 | exc_info[1], | ||
1714 | 106 | exc_info[2], | ||
1715 | 107 | None) | ||
1716 | 108 | lines = "".join(formatted).split("\n") | ||
1717 | 109 | if len(lines) > 0 and len(lines[-1]) == 0: | ||
1718 | 110 | lines.pop() | ||
1719 | 111 | indented = "\n".join([" %s" % (line,) for line in lines]) | ||
1720 | 112 | self.output.write("%s\n" % (indented,)) | ||
1721 | 113 | |||
1722 | 114 | |||
1723 | 115 | def main(*args): | ||
1724 | 116 | """Parse arguments, then build and run checks in a reactor.""" | ||
1725 | 117 | parser = ArgumentParser() | ||
1726 | 118 | parser.add_argument("config_file", | ||
1727 | 119 | help="Config file specifying the checks to run.") | ||
1728 | 120 | parser.add_argument("patterns", nargs='*', | ||
1729 | 121 | help="Patterns to filter the checks.") | ||
1730 | 122 | parser.add_argument("-v", "--verbose", dest="verbose", | ||
1731 | 123 | action="store_true", default=False, | ||
1732 | 124 | help="Show additional status") | ||
1733 | 125 | parser.add_argument("-d", "--duration", dest="show_duration", | ||
1734 | 126 | action="store_true", default=False, | ||
1735 | 127 | help="Show duration") | ||
1736 | 128 | parser.add_argument("-t", "--tracebacks", dest="show_tracebacks", | ||
1737 | 129 | action="store_true", default=False, | ||
1738 | 130 | help="Show tracebacks on failure") | ||
1739 | 131 | parser.add_argument("--validate", dest="validate", | ||
1740 | 132 | action="store_true", default=False, | ||
1741 | 133 | help="Only validate the config file, don't run checks.") | ||
1742 | 134 | options = parser.parse_args(list(args)) | ||
1743 | 135 | |||
1744 | 136 | if options.patterns: | ||
1745 | 137 | pattern = SumPattern(map(SimplePattern, options.patterns)) | ||
1746 | 138 | else: | ||
1747 | 139 | pattern = SimplePattern("*") | ||
1748 | 140 | |||
1749 | 141 | def make_daemon_thread(*args, **kw): | ||
1750 | 142 | """Create a daemon thread.""" | ||
1751 | 143 | thread = Thread(*args, **kw) | ||
1752 | 144 | thread.daemon = True | ||
1753 | 145 | return thread | ||
1754 | 146 | |||
1755 | 147 | threadpool = ThreadPool(minthreads=1) | ||
1756 | 148 | threadpool.threadFactory = make_daemon_thread | ||
1757 | 149 | reactor.threadpool = threadpool | ||
1758 | 150 | reactor.callWhenRunning(threadpool.start) | ||
1759 | 151 | |||
1760 | 152 | output = sys.stdout | ||
1761 | 153 | if options.show_duration: | ||
1762 | 154 | output = TimestampOutput(output) | ||
1763 | 155 | |||
1764 | 156 | results = ConsoleOutput(output=output, | ||
1765 | 157 | show_tracebacks=options.show_tracebacks, | ||
1766 | 158 | show_duration=options.show_duration, | ||
1767 | 159 | verbose=options.verbose) | ||
1768 | 160 | results = FailureCountingResultWrapper(results) | ||
1769 | 161 | with open(options.config_file) as f: | ||
1770 | 162 | descriptions = yaml.load(f) | ||
1771 | 163 | checks = build_checks(descriptions) | ||
1772 | 164 | if not options.validate: | ||
1773 | 165 | reactor.callWhenRunning(run_checks, checks, pattern, results) | ||
1774 | 166 | |||
1775 | 167 | reactor.run() | ||
1776 | 168 | |||
1777 | 169 | if results.any_failed(): | ||
1778 | 170 | return 1 | ||
1779 | 171 | else: | ||
1780 | 172 | return 0 | ||
1781 | 173 | |||
1782 | 174 | |||
1783 | 175 | def run(): | ||
1784 | 176 | exit(main(*sys.argv[1:])) | ||
1785 | 177 | |||
1786 | 178 | |||
1787 | 179 | if __name__ == '__main__': | ||
1788 | 180 | run() | ||
1789 | 181 | |||
1790 | 0 | 182 | ||
1791 | === added file 'conn_check/patterns.py' | |||
1792 | --- conn_check/patterns.py 1970-01-01 00:00:00 +0000 | |||
1793 | +++ conn_check/patterns.py 2014-07-24 22:10:31 +0000 | |||
1794 | @@ -0,0 +1,153 @@ | |||
1795 | 1 | import re | ||
1796 | 2 | |||
1797 | 3 | class Pattern(object): | ||
1798 | 4 | """Abstract base class for patterns used to select subsets of checks.""" | ||
1799 | 5 | |||
1800 | 6 | def assume_prefix(self, prefix): | ||
1801 | 7 | """Return an equivalent pattern with the given prefix baked in. | ||
1802 | 8 | |||
1803 | 9 | For example, if self.matches("bar") is True, then | ||
1804 | 10 | self.assume_prefix("foo").matches("foobar") will be True. | ||
1805 | 11 | """ | ||
1806 | 12 | return PrefixPattern(prefix, self) | ||
1807 | 13 | |||
1808 | 14 | def failed(self): | ||
1809 | 15 | """Return True if the pattern cannot match any string. | ||
1810 | 16 | |||
1811 | 17 | This is mainly used so we can bail out early when recursing into | ||
1812 | 18 | check trees. | ||
1813 | 19 | """ | ||
1814 | 20 | return not self.prefix_matches("") | ||
1815 | 21 | |||
1816 | 22 | def prefix_matches(self, partial_name): | ||
1817 | 23 | """Return True if the partial name (a prefix) is a potential match.""" | ||
1818 | 24 | raise NotImplementedError("%r.prefix_matches not implemented" % | ||
1819 | 25 | type(self)) | ||
1820 | 26 | |||
1821 | 27 | def matches(self, name): | ||
1822 | 28 | """Return True if the given name matches.""" | ||
1823 | 29 | raise NotImplementedError("%r.match not implemented" % | ||
1824 | 30 | type(self)) | ||
1825 | 31 | |||
1826 | 32 | |||
1827 | 33 | class FailedPattern(Pattern): | ||
1828 | 34 | """Patterns that always fail to match.""" | ||
1829 | 35 | |||
1830 | 36 | def assume_prefix(self, prefix): | ||
1831 | 37 | """Return an equivalent pattern with the given prefix baked in.""" | ||
1832 | 38 | return FAILED_PATTERN | ||
1833 | 39 | |||
1834 | 40 | def prefix_matches(self, partial_name): | ||
1835 | 41 | """Return True if the partial name matches.""" | ||
1836 | 42 | return False | ||
1837 | 43 | |||
1838 | 44 | def matches(self, name): | ||
1839 | 45 | """Return True if the complete name matches.""" | ||
1840 | 46 | return False | ||
1841 | 47 | |||
1842 | 48 | |||
1843 | 49 | FAILED_PATTERN = FailedPattern() | ||
1844 | 50 | |||
1845 | 51 | |||
1846 | 52 | PATTERN_TOKEN_RE = re.compile(r'\*|[^*]+') | ||
1847 | 53 | |||
1848 | 54 | |||
1849 | 55 | def tokens_to_partial_re(tokens): | ||
1850 | 56 | """Convert tokens to a regular expression for matching prefixes.""" | ||
1851 | 57 | |||
1852 | 58 | def token_to_re(token): | ||
1853 | 59 | """Convert tokens to (begin, end, alt_end) triples.""" | ||
1854 | 60 | if token == '*': | ||
1855 | 61 | return (r'(?:.*', ')?', ')') | ||
1856 | 62 | else: | ||
1857 | 63 | chars = list(token) | ||
1858 | 64 | begin = "".join(["(?:" + re.escape(c) for c in chars]) | ||
1859 | 65 | end = "".join([")?" for c in chars]) | ||
1860 | 66 | return (begin, end, end) | ||
1861 | 67 | |||
1862 | 68 | subexprs = map(token_to_re, tokens) | ||
1863 | 69 | if len(subexprs) > 0: | ||
1864 | 70 | # subexpressions like (.*)? aren't accepted, so we may have to use | ||
1865 | 71 | # an alternate closing form for the last (innermost) subexpression | ||
1866 | 72 | (begin, _, alt_end) = subexprs[-1] | ||
1867 | 73 | subexprs[-1] = (begin, alt_end, alt_end) | ||
1868 | 74 | return re.compile("".join([se[0] for se in subexprs] + | ||
1869 | 75 | [se[1] for se in reversed(subexprs)] + | ||
1870 | 76 | [r'\Z'])) | ||
1871 | 77 | |||
1872 | 78 | |||
1873 | 79 | def tokens_to_re(tokens): | ||
1874 | 80 | """Convert tokens to a regular expression for exact matching.""" | ||
1875 | 81 | |||
1876 | 82 | def token_to_re(token): | ||
1877 | 83 | """Convert tokens to simple regular expressions.""" | ||
1878 | 84 | if token == '*': | ||
1879 | 85 | return r'.*' | ||
1880 | 86 | else: | ||
1881 | 87 | return re.escape(token) | ||
1882 | 88 | |||
1883 | 89 | return re.compile("".join(map(token_to_re, tokens) + [r'\Z'])) | ||
1884 | 90 | |||
1885 | 91 | |||
1886 | 92 | class SimplePattern(Pattern): | ||
1887 | 93 | """Pattern that matches according to the given pattern expression.""" | ||
1888 | 94 | |||
1889 | 95 | def __init__(self, pattern): | ||
1890 | 96 | """Initialize an instance.""" | ||
1891 | 97 | super(SimplePattern, self).__init__() | ||
1892 | 98 | tokens = PATTERN_TOKEN_RE.findall(pattern) | ||
1893 | 99 | self.partial_re = tokens_to_partial_re(tokens) | ||
1894 | 100 | self.full_re = tokens_to_re(tokens) | ||
1895 | 101 | |||
1896 | 102 | def prefix_matches(self, partial_name): | ||
1897 | 103 | """Return True if the partial name matches.""" | ||
1898 | 104 | return self.partial_re.match(partial_name) is not None | ||
1899 | 105 | |||
1900 | 106 | def matches(self, name): | ||
1901 | 107 | """Return True if the complete name matches.""" | ||
1902 | 108 | return self.full_re.match(name) is not None | ||
1903 | 109 | |||
1904 | 110 | |||
1905 | 111 | class PrefixPattern(Pattern): | ||
1906 | 112 | """Pattern that assumes a previously given prefix.""" | ||
1907 | 113 | |||
1908 | 114 | def __init__(self, prefix, pattern): | ||
1909 | 115 | """Initialize an instance.""" | ||
1910 | 116 | super(PrefixPattern, self).__init__() | ||
1911 | 117 | self.prefix = prefix | ||
1912 | 118 | self.pattern = pattern | ||
1913 | 119 | |||
1914 | 120 | def assume_prefix(self, prefix): | ||
1915 | 121 | """Return an equivalent pattern with the given prefix baked in.""" | ||
1916 | 122 | return PrefixPattern(self.prefix + prefix, self.pattern) | ||
1917 | 123 | |||
1918 | 124 | def prefix_matches(self, partial_name): | ||
1919 | 125 | """Return True if the partial name matches.""" | ||
1920 | 126 | return self.pattern.prefix_matches(self.prefix + partial_name) | ||
1921 | 127 | |||
1922 | 128 | def matches(self, name): | ||
1923 | 129 | """Return True if the complete name matches.""" | ||
1924 | 130 | return self.pattern.matches(self.prefix + name) | ||
1925 | 131 | |||
1926 | 132 | |||
1927 | 133 | class SumPattern(Pattern): | ||
1928 | 134 | """Pattern that matches if at least one given pattern matches.""" | ||
1929 | 135 | |||
1930 | 136 | def __init__(self, patterns): | ||
1931 | 137 | """Initialize an instance.""" | ||
1932 | 138 | super(SumPattern, self).__init__() | ||
1933 | 139 | self.patterns = patterns | ||
1934 | 140 | |||
1935 | 141 | def prefix_matches(self, partial_name): | ||
1936 | 142 | """Return True if the partial name matches.""" | ||
1937 | 143 | for pattern in self.patterns: | ||
1938 | 144 | if pattern.prefix_matches(partial_name): | ||
1939 | 145 | return True | ||
1940 | 146 | return False | ||
1941 | 147 | |||
1942 | 148 | def matches(self, name): | ||
1943 | 149 | """Return True if the complete name matches.""" | ||
1944 | 150 | for pattern in self.patterns: | ||
1945 | 151 | if pattern.matches(name): | ||
1946 | 152 | return True | ||
1947 | 153 | return False | ||
1948 | 0 | 154 | ||
1949 | === modified file 'demo.yaml' | |||
1950 | --- demo.yaml 2014-07-24 19:30:30 +0000 | |||
1951 | +++ demo.yaml 2014-07-24 22:10:31 +0000 | |||
1952 | @@ -14,5 +14,5 @@ | |||
1953 | 14 | host: 127.0.0.1 | 14 | host: 127.0.0.1 |
1954 | 15 | port: 6379 | 15 | port: 6379 |
1955 | 16 | password: foobared | 16 | password: foobared |
1957 | 17 | - type: url | 17 | - type: http |
1958 | 18 | url: https://login.ubuntu.com/ | 18 | url: https://login.ubuntu.com/ |
1959 | 19 | 19 | ||
1960 | === modified file 'setup.py' | |||
1961 | --- setup.py 2014-07-24 15:20:04 +0000 | |||
1962 | +++ setup.py 2014-07-24 22:10:31 +0000 | |||
1963 | @@ -27,7 +27,7 @@ | |||
1964 | 27 | include_package_data=True, | 27 | include_package_data=True, |
1965 | 28 | entry_points={ | 28 | entry_points={ |
1966 | 29 | 'console_scripts': [ | 29 | 'console_scripts': [ |
1968 | 30 | 'conn-check = conn_check:run', | 30 | 'conn-check = conn_check.main:run', |
1969 | 31 | ], | 31 | ], |
1970 | 32 | }, | 32 | }, |
1971 | 33 | license='GPL3', | 33 | license='GPL3', |
1972 | 34 | 34 | ||
1973 | === modified file 'tests.py' | |||
1974 | --- tests.py 2014-07-24 21:09:27 +0000 | |||
1975 | +++ tests.py 2014-07-24 22:10:31 +0000 | |||
1976 | @@ -3,7 +3,28 @@ | |||
1977 | 3 | 3 | ||
1978 | 4 | from testtools import matchers | 4 | from testtools import matchers |
1979 | 5 | 5 | ||
1981 | 6 | import conn_check | 6 | from conn_check.check_impl import ( |
1982 | 7 | FunctionCheck, | ||
1983 | 8 | MultiCheck, | ||
1984 | 9 | parallel_strategy, | ||
1985 | 10 | PrefixCheckWrapper, | ||
1986 | 11 | sequential_strategy, | ||
1987 | 12 | ) | ||
1988 | 13 | from conn_check.checks import ( | ||
1989 | 14 | CHECKS, | ||
1990 | 15 | extract_host_port, | ||
1991 | 16 | make_amqp_check, | ||
1992 | 17 | make_http_check, | ||
1993 | 18 | make_postgres_check, | ||
1994 | 19 | make_redis_check, | ||
1995 | 20 | make_ssl_check, | ||
1996 | 21 | make_tcp_check, | ||
1997 | 22 | make_udp_check, | ||
1998 | 23 | ) | ||
1999 | 24 | from conn_check.main import ( | ||
2000 | 25 | build_checks, | ||
2001 | 26 | check_from_description, | ||
2002 | 27 | ) | ||
2003 | 7 | 28 | ||
2004 | 8 | 29 | ||
2005 | 9 | class FunctionCheckMatcher(testtools.Matcher): | 30 | class FunctionCheckMatcher(testtools.Matcher): |
2006 | @@ -15,7 +36,7 @@ | |||
2007 | 15 | 36 | ||
2008 | 16 | def match(self, matchee): | 37 | def match(self, matchee): |
2009 | 17 | checks = [] | 38 | checks = [] |
2011 | 18 | checks.append(matchers.IsInstance(conn_check.FunctionCheck)) | 39 | checks.append(matchers.IsInstance(FunctionCheck)) |
2012 | 19 | checks.append(matchers.Annotate( | 40 | checks.append(matchers.Annotate( |
2013 | 20 | "name doesn't match", | 41 | "name doesn't match", |
2014 | 21 | matchers.AfterPreprocessing(operator.attrgetter('name'), | 42 | matchers.AfterPreprocessing(operator.attrgetter('name'), |
2015 | @@ -43,7 +64,7 @@ | |||
2016 | 43 | 64 | ||
2017 | 44 | def match(self, matchee): | 65 | def match(self, matchee): |
2018 | 45 | checks = [] | 66 | checks = [] |
2020 | 46 | checks.append(matchers.IsInstance(conn_check.MultiCheck)) | 67 | checks.append(matchers.IsInstance(MultiCheck)) |
2021 | 47 | checks.append(matchers.AfterPreprocessing(operator.attrgetter('strategy'), | 68 | checks.append(matchers.AfterPreprocessing(operator.attrgetter('strategy'), |
2022 | 48 | matchers.Is(self.strategy))) | 69 | matchers.Is(self.strategy))) |
2023 | 49 | checks.append(matchers.AfterPreprocessing(operator.attrgetter('subchecks'), | 70 | checks.append(matchers.AfterPreprocessing(operator.attrgetter('subchecks'), |
2024 | @@ -58,40 +79,40 @@ | |||
2025 | 58 | class ExtractHostPortTests(testtools.TestCase): | 79 | class ExtractHostPortTests(testtools.TestCase): |
2026 | 59 | 80 | ||
2027 | 60 | def test_basic(self): | 81 | def test_basic(self): |
2029 | 61 | self.assertEqual(conn_check.extract_host_port('http://localhost:80/'), | 82 | self.assertEqual(extract_host_port('http://localhost:80/'), |
2030 | 62 | ('localhost', 80, 'http')) | 83 | ('localhost', 80, 'http')) |
2031 | 63 | 84 | ||
2032 | 64 | def test_no_scheme(self): | 85 | def test_no_scheme(self): |
2034 | 65 | self.assertEqual(conn_check.extract_host_port('//localhost/'), | 86 | self.assertEqual(extract_host_port('//localhost/'), |
2035 | 66 | ('localhost', 80, 'http')) | 87 | ('localhost', 80, 'http')) |
2036 | 67 | 88 | ||
2037 | 68 | def test_no_port_http(self): | 89 | def test_no_port_http(self): |
2039 | 69 | self.assertEqual(conn_check.extract_host_port('http://localhost/'), | 90 | self.assertEqual(extract_host_port('http://localhost/'), |
2040 | 70 | ('localhost', 80, 'http')) | 91 | ('localhost', 80, 'http')) |
2041 | 71 | 92 | ||
2042 | 72 | def test_no_port_https(self): | 93 | def test_no_port_https(self): |
2044 | 73 | self.assertEqual(conn_check.extract_host_port('https://localhost/'), | 94 | self.assertEqual(extract_host_port('https://localhost/'), |
2045 | 74 | ('localhost', 443, 'https')) | 95 | ('localhost', 443, 'https')) |
2046 | 75 | 96 | ||
2047 | 76 | 97 | ||
2048 | 77 | class ConnCheckTest(testtools.TestCase): | 98 | class ConnCheckTest(testtools.TestCase): |
2049 | 78 | 99 | ||
2050 | 79 | def test_make_tcp_check(self): | 100 | def test_make_tcp_check(self): |
2052 | 80 | result = conn_check.make_tcp_check('localhost', 8080) | 101 | result = make_tcp_check('localhost', 8080) |
2053 | 81 | self.assertThat(result, FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) | 102 | self.assertThat(result, FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) |
2054 | 82 | 103 | ||
2055 | 83 | def test_make_ssl_check(self): | 104 | def test_make_ssl_check(self): |
2057 | 84 | result = conn_check.make_ssl_check('localhost', 8080, verify=True) | 105 | result = make_ssl_check('localhost', 8080, verify=True) |
2058 | 85 | self.assertThat(result, FunctionCheckMatcher('ssl.localhost:8080', 'localhost:8080')) | 106 | self.assertThat(result, FunctionCheckMatcher('ssl.localhost:8080', 'localhost:8080')) |
2059 | 86 | 107 | ||
2060 | 87 | def test_make_udp_check(self): | 108 | def test_make_udp_check(self): |
2062 | 88 | result = conn_check.make_udp_check('localhost', 8080, 'foo', 'bar') | 109 | result = make_udp_check('localhost', 8080, 'foo', 'bar') |
2063 | 89 | self.assertThat(result, FunctionCheckMatcher('udp.localhost:8080', 'localhost:8080')) | 110 | self.assertThat(result, FunctionCheckMatcher('udp.localhost:8080', 'localhost:8080')) |
2064 | 90 | 111 | ||
2065 | 91 | def test_make_http_check(self): | 112 | def test_make_http_check(self): |
2067 | 92 | result = conn_check.make_http_check('http://localhost/') | 113 | result = make_http_check('http://localhost/') |
2068 | 93 | self.assertThat(result, | 114 | self.assertThat(result, |
2070 | 94 | MultiCheckMatcher(strategy=conn_check.sequential_strategy, | 115 | MultiCheckMatcher(strategy=sequential_strategy, |
2071 | 95 | subchecks=[ | 116 | subchecks=[ |
2072 | 96 | FunctionCheckMatcher('tcp.localhost:80', 'localhost:80'), | 117 | FunctionCheckMatcher('tcp.localhost:80', 'localhost:80'), |
2073 | 97 | FunctionCheckMatcher('GET.http://localhost/', 'GET http://localhost/') | 118 | FunctionCheckMatcher('GET.http://localhost/', 'GET http://localhost/') |
2074 | @@ -99,9 +120,9 @@ | |||
2075 | 99 | )) | 120 | )) |
2076 | 100 | 121 | ||
2077 | 101 | def test_make_http_check_https(self): | 122 | def test_make_http_check_https(self): |
2079 | 102 | result = conn_check.make_http_check('https://localhost/') | 123 | result = make_http_check('https://localhost/') |
2080 | 103 | self.assertThat(result, | 124 | self.assertThat(result, |
2082 | 104 | MultiCheckMatcher(strategy=conn_check.sequential_strategy, | 125 | MultiCheckMatcher(strategy=sequential_strategy, |
2083 | 105 | subchecks=[ | 126 | subchecks=[ |
2084 | 106 | FunctionCheckMatcher('tcp.localhost:443', 'localhost:443'), | 127 | FunctionCheckMatcher('tcp.localhost:443', 'localhost:443'), |
2085 | 107 | FunctionCheckMatcher('ssl.localhost:443', 'localhost:443'), | 128 | FunctionCheckMatcher('ssl.localhost:443', 'localhost:443'), |
2086 | @@ -110,10 +131,10 @@ | |||
2087 | 110 | )) | 131 | )) |
2088 | 111 | 132 | ||
2089 | 112 | def test_make_amqp_check(self): | 133 | def test_make_amqp_check(self): |
2094 | 113 | result = conn_check.make_amqp_check('localhost', 8080, 'foo', | 134 | result = make_amqp_check('localhost', 8080, 'foo', |
2095 | 114 | 'bar', use_ssl=True, vhost='/') | 135 | 'bar', use_ssl=True, vhost='/') |
2096 | 115 | self.assertIsInstance(result, conn_check.MultiCheck) | 136 | self.assertIsInstance(result, MultiCheck) |
2097 | 116 | self.assertIs(result.strategy, conn_check.sequential_strategy) | 137 | self.assertIs(result.strategy, sequential_strategy) |
2098 | 117 | self.assertEqual(len(result.subchecks), 3) | 138 | self.assertEqual(len(result.subchecks), 3) |
2099 | 118 | self.assertThat(result.subchecks[0], | 139 | self.assertThat(result.subchecks[0], |
2100 | 119 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) | 140 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) |
2101 | @@ -122,20 +143,20 @@ | |||
2102 | 122 | self.assertThat(result.subchecks[2], FunctionCheckMatcher('auth', 'user foo')) | 143 | self.assertThat(result.subchecks[2], FunctionCheckMatcher('auth', 'user foo')) |
2103 | 123 | 144 | ||
2104 | 124 | def test_make_amqp_check_no_ssl(self): | 145 | def test_make_amqp_check_no_ssl(self): |
2109 | 125 | result = conn_check.make_amqp_check('localhost', 8080, 'foo', | 146 | result = make_amqp_check('localhost', 8080, 'foo', |
2110 | 126 | 'bar', use_ssl=False, vhost='/') | 147 | 'bar', use_ssl=False, vhost='/') |
2111 | 127 | self.assertIsInstance(result, conn_check.MultiCheck) | 148 | self.assertIsInstance(result, MultiCheck) |
2112 | 128 | self.assertIs(result.strategy, conn_check.sequential_strategy) | 149 | self.assertIs(result.strategy, sequential_strategy) |
2113 | 129 | self.assertEqual(len(result.subchecks), 2) | 150 | self.assertEqual(len(result.subchecks), 2) |
2114 | 130 | self.assertThat(result.subchecks[0], | 151 | self.assertThat(result.subchecks[0], |
2115 | 131 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) | 152 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) |
2116 | 132 | self.assertThat(result.subchecks[1], FunctionCheckMatcher('auth', 'user foo')) | 153 | self.assertThat(result.subchecks[1], FunctionCheckMatcher('auth', 'user foo')) |
2117 | 133 | 154 | ||
2118 | 134 | def test_make_postgres_check(self): | 155 | def test_make_postgres_check(self): |
2123 | 135 | result = conn_check.make_postgres_check('localhost', 8080,'foo', | 156 | result = make_postgres_check('localhost', 8080,'foo', |
2124 | 136 | 'bar', 'test') | 157 | 'bar', 'test') |
2125 | 137 | self.assertIsInstance(result, conn_check.MultiCheck) | 158 | self.assertIsInstance(result, MultiCheck) |
2126 | 138 | self.assertIs(result.strategy, conn_check.sequential_strategy) | 159 | self.assertIs(result.strategy, sequential_strategy) |
2127 | 139 | self.assertEqual(len(result.subchecks), 2) | 160 | self.assertEqual(len(result.subchecks), 2) |
2128 | 140 | self.assertThat(result.subchecks[0], | 161 | self.assertThat(result.subchecks[0], |
2129 | 141 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) | 162 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) |
2130 | @@ -143,33 +164,33 @@ | |||
2131 | 143 | FunctionCheckMatcher('auth', 'user foo', blocking=True)) | 164 | FunctionCheckMatcher('auth', 'user foo', blocking=True)) |
2132 | 144 | 165 | ||
2133 | 145 | def test_make_postgres_check_local_socket(self): | 166 | def test_make_postgres_check_local_socket(self): |
2138 | 146 | result = conn_check.make_postgres_check('/local.sock', 8080,'foo', | 167 | result = make_postgres_check('/local.sock', 8080,'foo', |
2139 | 147 | 'bar', 'test') | 168 | 'bar', 'test') |
2140 | 148 | self.assertIsInstance(result, conn_check.MultiCheck) | 169 | self.assertIsInstance(result, MultiCheck) |
2141 | 149 | self.assertIs(result.strategy, conn_check.sequential_strategy) | 170 | self.assertIs(result.strategy, sequential_strategy) |
2142 | 150 | self.assertEqual(len(result.subchecks), 1) | 171 | self.assertEqual(len(result.subchecks), 1) |
2143 | 151 | self.assertThat(result.subchecks[0], | 172 | self.assertThat(result.subchecks[0], |
2144 | 152 | FunctionCheckMatcher('auth', 'user foo', blocking=True)) | 173 | FunctionCheckMatcher('auth', 'user foo', blocking=True)) |
2145 | 153 | 174 | ||
2146 | 154 | def test_make_redis_check(self): | 175 | def test_make_redis_check(self): |
2149 | 155 | result = conn_check.make_redis_check('localhost', 8080) | 176 | result = make_redis_check('localhost', 8080) |
2150 | 156 | self.assertIsInstance(result, conn_check.PrefixCheckWrapper) | 177 | self.assertIsInstance(result, PrefixCheckWrapper) |
2151 | 157 | self.assertEqual(result.prefix, 'redis.') | 178 | self.assertEqual(result.prefix, 'redis.') |
2152 | 158 | wrapped = result.wrapped | 179 | wrapped = result.wrapped |
2155 | 159 | self.assertIsInstance(wrapped, conn_check.MultiCheck) | 180 | self.assertIsInstance(wrapped, MultiCheck) |
2156 | 160 | self.assertIs(wrapped.strategy, conn_check.sequential_strategy) | 181 | self.assertIs(wrapped.strategy, sequential_strategy) |
2157 | 161 | self.assertEqual(len(wrapped.subchecks), 2) | 182 | self.assertEqual(len(wrapped.subchecks), 2) |
2158 | 162 | self.assertThat(wrapped.subchecks[0], | 183 | self.assertThat(wrapped.subchecks[0], |
2159 | 163 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) | 184 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) |
2160 | 164 | self.assertThat(wrapped.subchecks[1], FunctionCheckMatcher('connect', None)) | 185 | self.assertThat(wrapped.subchecks[1], FunctionCheckMatcher('connect', None)) |
2161 | 165 | 186 | ||
2162 | 166 | def test_make_redis_check_with_password(self): | 187 | def test_make_redis_check_with_password(self): |
2165 | 167 | result = conn_check.make_redis_check('localhost', 8080, 'foobar') | 188 | result = make_redis_check('localhost', 8080, 'foobar') |
2166 | 168 | self.assertIsInstance(result, conn_check.PrefixCheckWrapper) | 189 | self.assertIsInstance(result, PrefixCheckWrapper) |
2167 | 169 | self.assertEqual(result.prefix, 'redis.') | 190 | self.assertEqual(result.prefix, 'redis.') |
2168 | 170 | wrapped = result.wrapped | 191 | wrapped = result.wrapped |
2171 | 171 | self.assertIsInstance(wrapped, conn_check.MultiCheck) | 192 | self.assertIsInstance(wrapped, MultiCheck) |
2172 | 172 | self.assertIs(wrapped.strategy, conn_check.sequential_strategy) | 193 | self.assertIs(wrapped.strategy, sequential_strategy) |
2173 | 173 | self.assertEqual(len(wrapped.subchecks), 2) | 194 | self.assertEqual(len(wrapped.subchecks), 2) |
2174 | 174 | self.assertThat(wrapped.subchecks[0], | 195 | self.assertThat(wrapped.subchecks[0], |
2175 | 175 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) | 196 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) |
2176 | @@ -178,28 +199,28 @@ | |||
2177 | 178 | 199 | ||
2178 | 179 | def test_check_from_description_unknown_type(self): | 200 | def test_check_from_description_unknown_type(self): |
2179 | 180 | e = self.assertRaises(AssertionError, | 201 | e = self.assertRaises(AssertionError, |
2181 | 181 | conn_check.check_from_description, {'type': 'foo'}) | 202 | check_from_description, {'type': 'foo'}) |
2182 | 182 | self.assertEqual( | 203 | self.assertEqual( |
2183 | 183 | str(e), | 204 | str(e), |
2185 | 184 | "Unknown check type: foo, available checks: {}".format(conn_check.CHECKS.keys())) | 205 | "Unknown check type: foo, available checks: {}".format(CHECKS.keys())) |
2186 | 185 | 206 | ||
2187 | 186 | def test_check_from_description_missing_arg(self): | 207 | def test_check_from_description_missing_arg(self): |
2188 | 187 | description = {'type': 'tcp'} | 208 | description = {'type': 'tcp'} |
2189 | 188 | e = self.assertRaises(AssertionError, | 209 | e = self.assertRaises(AssertionError, |
2191 | 189 | conn_check.check_from_description, description) | 210 | check_from_description, description) |
2192 | 190 | self.assertEqual( | 211 | self.assertEqual( |
2193 | 191 | str(e), | 212 | str(e), |
2194 | 192 | "host missing from check: {}".format(description)) | 213 | "host missing from check: {}".format(description)) |
2195 | 193 | 214 | ||
2196 | 194 | def test_check_from_description_makes_check(self): | 215 | def test_check_from_description_makes_check(self): |
2197 | 195 | description = {'type': 'tcp', 'host': 'localhost', 'port': '8080'} | 216 | description = {'type': 'tcp', 'host': 'localhost', 'port': '8080'} |
2199 | 196 | result = conn_check.check_from_description(description) | 217 | result = check_from_description(description) |
2200 | 197 | self.assertThat(result, | 218 | self.assertThat(result, |
2201 | 198 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) | 219 | FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')) |
2202 | 199 | 220 | ||
2203 | 200 | def test_build_checks(self): | 221 | def test_build_checks(self): |
2204 | 201 | description = [{'type': 'tcp', 'host': 'localhost', 'port': '8080'}] | 222 | description = [{'type': 'tcp', 'host': 'localhost', 'port': '8080'}] |
2206 | 202 | result = conn_check.build_checks(description) | 223 | result = build_checks(description) |
2207 | 203 | self.assertThat(result, | 224 | self.assertThat(result, |
2209 | 204 | MultiCheckMatcher(strategy=conn_check.parallel_strategy, | 225 | MultiCheckMatcher(strategy=parallel_strategy, |
2210 | 205 | subchecks=[FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')])) | 226 | subchecks=[FunctionCheckMatcher('tcp.localhost:8080', 'localhost:8080')])) |