├── .gitignore ├── ChangeLog ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.md ├── dockermon.py ├── run-tests.sh ├── setup.cfg ├── setup.py ├── test_dockermon.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | venv/ 4 | build/ 5 | dist/ 6 | dockermon.egg-info 7 | .tox 8 | .eggs 9 | README.html 10 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2016-05-14 version 0.2.2 2 | * Send "Host" header (@lrusak in PR #2) 3 | 4 | 2015-08-09 version 0.2.1 5 | * Fixed manifest 6 | * Testing with tox 7 | 8 | 2015-08-09 version 0.2.0 9 | * Added support for TCP sockets and --socket-url switch 10 | * Added --version 11 | 12 | 2015-06-25 version 0.1.0 13 | * Initial release 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 CyberInt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE.txt ChangeLog Makefile run-tests.sh tox.ini 2 | include test_dockermon.py 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | $(error please pick a target) 3 | 4 | dist: 5 | python setup.py sdist 6 | 7 | test: 8 | python setup.py nosetests 9 | 10 | upload: 11 | python setup.py sdist upload 12 | 13 | 14 | .PHONY: all dist test upload 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dockermon - Docker Monitor Utility 2 | 3 | `dockermon` listens for events from docker daemon using the docker `/events` 4 | [HTTP API][api]. 5 | 6 | 7 | [api]: https://docs.docker.com/reference/api/docker_remote_api_v1.19/ 8 | 9 | # Usage 10 | 11 | ## Library 12 | You can use `dockermon` as a library and then call `watch(callback)`, you're 13 | callback function will be called with new event (dict). 14 | 15 | # Command Line 16 | The other option is to use `dockermon` as a command line tool and specify a 17 | program to call with every new event. The program will be launched and the event 18 | encode in JSON will be send to the program standard input. For example: 19 | 20 | python -m dockermon "jq --unbuffered ." 21 | 22 | If not program is specified, events will be printed to standard output as JSON 23 | objects, one per line. 24 | 25 | # Hacking 26 | "one script no external dependencies" is a design goal, let's not break it :) 27 | 28 | We use [tox](https://tox.readthedocs.org/en/latest/) to test. Please make sure 29 | your code passes on all supported platforms (see `tox.ini`) before sending a 30 | pull request. 31 | 32 | # Bugs and Project 33 | 34 | https://github.com/CyberInt/dockermon 35 | -------------------------------------------------------------------------------- /dockermon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """docker monitor using docker /events HTTP streaming API""" 3 | 4 | from contextlib import closing 5 | from functools import partial 6 | from socket import socket, AF_UNIX 7 | from subprocess import Popen, PIPE 8 | from sys import stdout, version_info 9 | import json 10 | import shlex 11 | 12 | if version_info[:2] < (3, 0): 13 | from httplib import OK as HTTP_OK 14 | from urlparse import urlparse 15 | else: 16 | from http.client import OK as HTTP_OK 17 | from urllib.parse import urlparse 18 | 19 | __version__ = '0.2.2' 20 | bufsize = 1024 21 | default_sock_url = 'ipc:///var/run/docker.sock' 22 | 23 | 24 | class DockermonError(Exception): 25 | pass 26 | 27 | 28 | def read_http_header(sock): 29 | """Read HTTP header from socket, return header and rest of data.""" 30 | buf = [] 31 | hdr_end = '\r\n\r\n' 32 | 33 | while True: 34 | buf.append(sock.recv(bufsize).decode('utf-8')) 35 | data = ''.join(buf) 36 | i = data.find(hdr_end) 37 | if i == -1: 38 | continue 39 | return data[:i], data[i + len(hdr_end):] 40 | 41 | 42 | def header_status(header): 43 | """Parse HTTP status line, return status (int) and reason.""" 44 | status_line = header[:header.find('\r')] 45 | # 'HTTP/1.1 200 OK' -> (200, 'OK') 46 | fields = status_line.split(None, 2) 47 | return int(fields[1]), fields[2] 48 | 49 | 50 | def connect(url): 51 | """Connect to UNIX or TCP socket. 52 | 53 | url can be either tcp://:port or ipc:// 54 | """ 55 | url = urlparse(url) 56 | if url.scheme == 'tcp': 57 | sock = socket() 58 | netloc = tuple(url.netloc.rsplit(':', 1)) 59 | hostname = socket.gethostname() 60 | elif url.scheme == 'ipc': 61 | sock = socket(AF_UNIX) 62 | netloc = url.path 63 | hostname = 'localhost' 64 | else: 65 | raise ValueError('unknown socket type: %s' % url.scheme) 66 | 67 | sock.connect(netloc) 68 | return sock, hostname 69 | 70 | 71 | def watch(callback, url=default_sock_url): 72 | """Watch docker events. Will call callback with each new event (dict). 73 | 74 | url can be either tcp://:port or ipc:// 75 | """ 76 | sock, hostname = connect(url) 77 | request = 'GET /events HTTP/1.1\nHost: %s\n\n' % hostname 78 | request = request.encode('utf-8') 79 | 80 | with closing(sock): 81 | sock.sendall(request) 82 | header, payload = read_http_header(sock) 83 | status, reason = header_status(header) 84 | if status != HTTP_OK: 85 | raise DockermonError('bad HTTP status: %s %s' % (status, reason)) 86 | 87 | # Messages are \r\n\r\n 88 | buf = [payload] 89 | while True: 90 | chunk = sock.recv(bufsize) 91 | if not chunk: 92 | raise EOFError('socket closed') 93 | buf.append(chunk.decode('utf-8')) 94 | data = ''.join(buf) 95 | i = data.find('\r\n') 96 | if i == -1: 97 | continue 98 | 99 | size = int(data[:i], 16) 100 | start = i + 2 # Skip initial \r\n 101 | 102 | if len(data) < start + size + 2: 103 | continue 104 | payload = data[start:start+size] 105 | callback(json.loads(payload)) 106 | buf = [data[start+size+2:]] # Skip \r\n suffix 107 | 108 | 109 | def print_callback(msg): 110 | """Print callback, prints message to stdout as JSON in one line.""" 111 | json.dump(msg, stdout) 112 | stdout.write('\n') 113 | stdout.flush() 114 | 115 | 116 | def prog_callback(prog, msg): 117 | """Program callback, calls prog with message in stdin""" 118 | pipe = Popen(prog, stdin=PIPE) 119 | data = json.dumps(msg) 120 | pipe.stdin.write(data.encode('utf-8')) 121 | pipe.stdin.close() 122 | 123 | 124 | if __name__ == '__main__': 125 | from argparse import ArgumentParser 126 | 127 | parser = ArgumentParser(description=__doc__) 128 | parser.add_argument('--prog', default=None, 129 | help='program to call (e.g. "jq --unbuffered .")') 130 | parser.add_argument( 131 | '--socket-url', default=default_sock_url, 132 | help='socket url (ipc:///path/to/sock or tcp:///host:port)') 133 | parser.add_argument( 134 | '--version', help='print version and exit', 135 | action='store_true', default=False) 136 | args = parser.parse_args() 137 | 138 | if args.version: 139 | print('dockermon %s' % __version__) 140 | raise SystemExit 141 | 142 | if args.prog: 143 | prog = shlex.split(args.prog) 144 | callback = partial(prog_callback, prog) 145 | else: 146 | callback = print_callback 147 | 148 | try: 149 | watch(callback, args.socket_url) 150 | except (KeyboardInterrupt, EOFError): 151 | pass 152 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | flake8 dockermon.py test_dockermon.py 7 | nosetests -vd $@ 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=2 3 | detailed-errors=1 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | 7 | def version(): 8 | with open('dockermon.py') as fo: 9 | for line in fo: 10 | if '__version__' not in line: 11 | continue 12 | # "__version__ = '0.1.0'" -> "0.1.0" 13 | return line.split('=')[1].strip().replace("'", '') 14 | 15 | 16 | def readme(): 17 | with open('README.md') as fo: 18 | return fo.read() 19 | 20 | setup( 21 | name='dockermon', 22 | version=version(), 23 | description='docker monitor using docker /events HTTP streaming API', 24 | long_description=readme(), 25 | author='CyberInt', 26 | author_email='tools@cyberint.com', 27 | license='MIT', 28 | url='https://github.com/CyberInt/dockermon', 29 | py_modules=['dockermon'], 30 | tests_require=['nose', 'flake8', 'tox'], 31 | classifiers=[ 32 | 'Development Status :: 4 - Beta', 33 | 'Intended Audience :: Developers', 34 | 'License :: OSI Approved :: MIT License', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 3', 38 | 'Topic :: Software Development :: Libraries', 39 | 'Topic :: Software Development :: Libraries :: Python Modules', 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /test_dockermon.py: -------------------------------------------------------------------------------- 1 | """dockermon testing""" 2 | 3 | import dockermon 4 | 5 | 6 | def test_http_status(): 7 | header = 'HTTP/1.1 200 Okie Dokie\r' 8 | parsed = dockermon.header_status(header) 9 | assert parsed == (200, 'Okie Dokie') 10 | 11 | 12 | def test_read_http_header(): 13 | pass # TBD 14 | 15 | 16 | def test_watch(): 17 | pass # TBD 18 | 19 | 20 | def test_print_callback(): 21 | pass 22 | 23 | 24 | def test_prog_callback(): 25 | pass # TBD 26 | 27 | 28 | def test_main(): 29 | pass # TBD 30 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34,pypy,pypy3,packaging 3 | 4 | [testenv] 5 | deps = 6 | nose 7 | flake8 8 | commands = ./run-tests.sh 9 | 10 | [testenv:packaging] 11 | skip_install = true 12 | deps = 13 | check-manifest 14 | commands = 15 | check-manifest 16 | --------------------------------------------------------------------------------