Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Michał Jaworski
committed
Oct 29, 2014
1 parent
206e8b8
commit 5c33c05
Showing
11 changed files
with
710 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pytest | ||
mock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
requests | ||
python-consul |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
# -*- coding: utf-8 -*- | ||
import sys | ||
import argparse | ||
import logging | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
DEFAULT_CONSUL_HTTP_API_PORT = 8500 | ||
DEFAULT_TTL = 10 | ||
|
||
|
||
class CustomFormatter(argparse.HelpFormatter): | ||
def __init__(self, prog): | ||
# default max_help_position increased for readability | ||
super(CustomFormatter, self).__init__(prog, max_help_position=50) | ||
|
||
def _format_action_invocation(self, action): | ||
""" | ||
Hack _format_action_invocation to display metavar for only one | ||
of options | ||
""" | ||
if not action.option_strings: | ||
metavar, = self._metavar_formatter(action, action.dest)(1) | ||
return metavar | ||
|
||
else: | ||
parts = [] | ||
|
||
# if the Optional doesn't take a value, format is: | ||
# -s, --long | ||
if action.nargs == 0: | ||
parts.extend(action.option_strings) | ||
|
||
# if the Optional takes a value, format is: | ||
# -s ARGS, --long ARGS | ||
else: | ||
default = action.dest.upper() | ||
args_string = self._format_args(action, default) | ||
|
||
# here is the hack: do not add args to first option part | ||
# if it is both long and short | ||
if len(action.option_strings) > 1: | ||
parts.append(action.option_strings[0] + "") | ||
remaining = action.option_strings[1:] | ||
else: | ||
remaining = action.option_strings | ||
|
||
for option_string in remaining: | ||
parts.append('%s=%s' % (option_string, args_string)) | ||
|
||
return ', '.join(parts) | ||
|
||
def add_usage(self, usage, actions, groups, prefix=None): | ||
""" | ||
Hack add_usage to add fake "-- command [arguments]" to usage | ||
""" | ||
actions.append(argparse._StoreAction( | ||
option_strings=[], | ||
dest="-- command [arguments]" | ||
)) | ||
return super(CustomFormatter, self).add_usage( | ||
usage, actions, groups, prefix | ||
) | ||
|
||
|
||
def coordinates(coordinates_string): | ||
""" parse coordinates string | ||
:param coordinates_string: string in "hostname" or "hostname:port" format | ||
:return: (hostname, port) two-tuple | ||
""" | ||
if ':' in coordinates_string: | ||
try: | ||
hostname, port = coordinates_string.split(":") | ||
port = int(port) | ||
|
||
if not hostname: | ||
raise ValueError() | ||
|
||
except ValueError: | ||
raise ValueError("Coordinate should be hostname or hostname:port ") | ||
else: | ||
hostname = coordinates_string | ||
port = DEFAULT_CONSUL_HTTP_API_PORT | ||
|
||
return hostname, port | ||
|
||
|
||
def get_parser(): | ||
""" Create ianotor argument parser with a set of reasonable defaults | ||
:return: argument parser | ||
""" | ||
parser = argparse.ArgumentParser( | ||
"ianitor", | ||
description="Doorkeeper for consul discovered services.", | ||
formatter_class=CustomFormatter, | ||
) | ||
|
||
parser.add_argument( | ||
"--consul-agent", | ||
metavar="hostname[:port]", type=coordinates, default="localhost", | ||
help="set consul agent address" | ||
) | ||
|
||
parser.add_argument( | ||
"--ttl", | ||
metavar="seconds", type=float, default=DEFAULT_TTL, | ||
help="set TTL of service in consul cluster" | ||
) | ||
|
||
parser.add_argument( | ||
"--heartbeat", | ||
metavar="seconds", type=float, default=None, | ||
help="set rocess poll heartbeat (defaults to ttl/10)", | ||
) | ||
|
||
parser.add_argument( | ||
"--tags", | ||
action="append", metavar="tag", | ||
help="set service tags in consul cluster (can be used multiple times)", | ||
) | ||
|
||
parser.add_argument( | ||
"--id", | ||
help="set service id - must be node unique (defaults to service name)" | ||
) | ||
|
||
parser.add_argument( | ||
"--port", | ||
help="set service port", | ||
) | ||
|
||
parser.add_argument( | ||
"-v", "--verbose", | ||
action="count", | ||
help="enable logging to stdout (use multiple times to increase verbosity)", # noqa | ||
) | ||
|
||
parser.add_argument( | ||
metavar="service-name", | ||
dest="service_name", | ||
help="service name in consul cluster", | ||
) | ||
|
||
return parser | ||
|
||
|
||
def parse_args(): | ||
""" | ||
Parse program arguments. | ||
This function ensures that argv arguments after '--' won't be parsed by | ||
`argparse` and will be returned as separate list. | ||
:return: (args, command) two-tuple | ||
""" | ||
|
||
parser = get_parser() | ||
|
||
try: | ||
split_point = sys.argv.index('--') | ||
|
||
except ValueError: | ||
if "--help" in sys.argv or "-h" in sys.argv or len(sys.argv) == 1: | ||
parser.print_help() | ||
exit(0) | ||
else: | ||
parser.print_usage() | ||
print(parser.prog, ": error: command missing") | ||
exit(1) | ||
|
||
else: | ||
argv = sys.argv[1:split_point] | ||
invocation = sys.argv[split_point + 1:] | ||
|
||
args = parser.parse_args(argv) | ||
|
||
# set default heartbeat to ttl / 10. if not specified | ||
if not args.heartbeat: | ||
args.heartbeat = args.ttl / 10. | ||
logger.debug( | ||
"heartbeat not specified, setting to %s" % args.heartbeat | ||
) | ||
|
||
return args, invocation |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
# -*- coding: utf-8 -*- | ||
from time import sleep | ||
import signal | ||
import logging | ||
|
||
import consul | ||
|
||
from ianitor.service import Service | ||
from ianitor.args_parser import parse_args | ||
|
||
|
||
SIGNALS = [ | ||
member | ||
for member | ||
in dir(signal) | ||
if member.startswith("SIG") and '_' not in member | ||
] | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def setup_logging(verbosity): | ||
ilogger = logging.getLogger('ianitor') | ||
|
||
if verbosity: | ||
handler = logging.StreamHandler() | ||
if verbosity == 1: | ||
handler.setLevel(logging.ERROR) | ||
if verbosity == 2: | ||
handler.setLevel(logging.WARNING) | ||
if verbosity >= 3: | ||
handler.setLevel(logging.DEBUG) | ||
else: | ||
handler = logging.NullHandler() | ||
|
||
formatter = logging.Formatter( | ||
'[%(levelname)s] %(name)s: %(message)s' | ||
) | ||
|
||
handler.setFormatter(formatter) | ||
ilogger.setLevel(logging.DEBUG) | ||
ilogger.addHandler(handler) | ||
|
||
|
||
def main(): | ||
args, command = parse_args() | ||
setup_logging(args.verbose) | ||
|
||
session = consul.Consul(*args.consul_agent) | ||
|
||
service = Service( | ||
command, | ||
session=session, | ||
ttl=args.ttl, | ||
service_name=args.service_name, | ||
service_id=args.id, | ||
tags=args.tags, | ||
port=args.port | ||
) | ||
|
||
service.start() | ||
|
||
def signal_handler(signal_number, *_): | ||
service.process.send_signal(signal_number) | ||
|
||
for signal_name in SIGNALS: | ||
try: | ||
signum = getattr(signal, signal_name) | ||
signal.signal(signum, signal_handler) | ||
except RuntimeError: | ||
# signals that cannot be catched will raise RuntimeException | ||
pass | ||
|
||
while sleep(args.heartbeat) or service.is_up(): | ||
service.keep_alive() | ||
|
||
logger.info("process quit with errorcode %s %s" % ( | ||
service.process.poll(), | ||
"(signal)" if service.process.poll() < 0 else "" | ||
)) | ||
|
||
service.deregister() |
Oops, something went wrong.