Skip to content

Commit

Permalink
Merge pull request #42 from romana/issue-38-http
Browse files Browse the repository at this point in the history
Add HTTP server for live stats and info
  • Loading branch information
jbrendel authored Aug 22, 2017
2 parents 4e6c177 + 7d1a100 commit 7bddd7c
Show file tree
Hide file tree
Showing 19 changed files with 836 additions and 252 deletions.
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ In order to develop or extend vpc-router, please read the [developer
documentation](DEVELOPERS.md) for information that might be useful to get you
started.

## Built-in HTTP server to see internal state and config

vpc-router comes with a built-in HTTP server. By default it listens on
`localhost:33289`. Send a GET request (with a web-browser, curl or wget, or any
client you wish) to `http://localhost:33290/` to receive a JSON formatted
output with the current internal configuration of vpc-router.

The listen address and port can be modified with the `-a` (address) and `-p`
(port) command line options.


## Configuration

### The route spec
Expand Down Expand Up @@ -208,7 +219,7 @@ You can see an example route spec file in `examples/route_spec_1.conf`.
### Mode 'http'

The following command starts vpc-router as a service daemon in the 'http'
mode. In opens a server port on which it listens for new route specs:
mode. It utilizes the built-in HTTP server to listen for new route specs:

$ vpcrouter -m http -r us-east-1 -v vpc-350d6a51

Expand All @@ -221,19 +232,8 @@ can be omitted if vpc-router is run on an instance in the region.
* `-v` specifies the VPC for which vpc-router should perform route updates.
Note: This can be omitted if vpc-router is run on an instance within the VPC.

In 'http' mode, vpc-router by default uses port 33289 and listens on localhost.
However, you can use the `-p` (port) and `-a` ('address') options to specify a
different listening port or address. Specifically, use `-a 0.0.0.0` to listen
on any interface and address.

There are a two URLs offered in 'http' mode:

* `/route_spec`: POST a new route spec here, or GET the current route spec.
* `/status`: GET a status overview, containing the route spec as well as the
current list of any failed IP addresses and currently configured routes.

In 'http' mode, new route specs are POSTed to
http://<listen-address>:<port>/route_spec
A new route spec can be POSTed to the `/route_spec` URL. The current route spec
can be retrieved with a GET to that URL.

For example:

Expand Down
2 changes: 1 addition & 1 deletion vpcrouter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
"""

__version__ = "1.6.3"
__version__ = "1.7.0"
176 changes: 164 additions & 12 deletions vpcrouter/currentstate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,167 @@
"""

#
# Global data/state shared between modules. Currently can just be a simple
# dict.
#

# A shared dict in which we keep the current route state, in case someone is
# interested.
CURRENT_STATE = {
"failed_ips" : [],
"route_spec" : {},
"routes" : {}
}
# Global data/state shared between modules. This includes some rendering
# options, since we produce output via the http module.

import datetime
import json


class StateError(Exception):
pass


class _CurrentState(object):
"""
Holds the current state of the system.
We use this for some communication between modules, but mostly to be able
to render some output of the current system state.
"""
def __init__(self):
self.starttime = datetime.datetime.now()
self.versions = ""
self.plugins = []
self.failed_ips = []
self.working_set = []
self.route_spec = {}
self.routes = {}
self.vpc_state = {}
self.conf = None
self.main_param_names = []
self._vpc_router_http = None

# The following top-level items are rendered as links and can be
# accessed with separate requests.
self.top_level_links = ["", "ips", "plugins", "route_info", "vpc"]

def add_plugin(self, plugin):
"""
Every plugin (watcher and health) is added so we can later get live
info from each plugin.
"""
self.plugins.append(plugin)

def get_plugins_info(self):
"""
Collect the current live info from all the registered plugins.
Return a dictionary, keyed on the plugin name.
"""
d = {}
for p in self.plugins:
d.update(p.get_info())
return d

def render_main_params(self):
"""
Return names and values for the main parameters (not the plugin
parameters).
"""
return {n: self.conf[n] for n in self.main_param_names}

def get_state_repr(self, path):
"""
Returns the current state, or sub-state, depending on the path.
"""
if path == "ips":
return {
"failed_ips" : self.failed_ips,
"working_set" : self.working_set,
}

if path == "route_info":
return {
"route_spec" : self.route_spec,
"routes" : self.routes,
}

if path == "plugins":
return self.get_plugins_info()

if path == "vpc":
return self.vpc_state

if path == "":
return {
"SERVER" : {
"version" : self.versions,
"start_time" : self.starttime.isoformat(),
"current_time" : datetime.datetime.now().isoformat()
},
"params" : self.render_main_params(),
"plugins" : {"_href" : "/plugins"},
"ips" : {"_href" : "/ips"},
"route_info" : {"_href" : "/route_info"},
"vpc" : {"_href" : "/vpc"}
}

def as_json(self, path="", with_indent=False):
"""
Return a rendering of the current state in JSON.
"""
if path not in self.top_level_links:
raise StateError("Unknown path")

return json.dumps(self.get_state_repr(path),
indent=4 if with_indent else None)

def as_html(self, path=""):
"""
Return a rendering of the current state in HTML.
"""
if path not in self.top_level_links:
raise StateError("Unknown path")

header = """
<html>
<head>
<title>VPC-router state</title>
</head>
<body>
<h3>VPC-router state</h3>
<hr>
<font face="courier">
"""

footer = """
</font>
</body>
</html>
"""

rep = self.get_state_repr(path)

def make_links(rep):
# Recursively create clickable links for _href elements
for e, v in rep.items():
if e == "_href":
v = '<a href=%s>%s</a>' % (v, v)
rep[e] = v
else:
if type(v) == dict:
make_links(v)

make_links(rep)

rep_str_lines = json.dumps(rep, indent=4).split("\n")
buf = []
for l in rep_str_lines:
# Replace leading spaces with '&nbsp;'
num_spaces = len(l) - len(l.lstrip())
l = "&nbsp;" * num_spaces + l[num_spaces:]
buf.append(l)

return "%s%s%s" % (header, "<br>\n".join(buf), footer)


# The module doesn't get reloaded, so no need to check, can just initialize
CURRENT_STATE = _CurrentState()
61 changes: 48 additions & 13 deletions vpcrouter/main/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
from vpcrouter import monitor
from vpcrouter import utils
from vpcrouter import watcher
from vpcrouter.currentstate import CURRENT_STATE
from vpcrouter.errors import ArgsError
from vpcrouter.main import http_server
from vpcrouter.plugin_framework import load_plugin
from vpcrouter.vpc import get_ec2_meta_data

Expand Down Expand Up @@ -55,11 +57,13 @@ def _setup_arg_parser(args_list, watcher_plugin_class, health_plugin_class):
parser = argparse.ArgumentParser(
description="VPC router: Manage routes in VPC route table")
# General arguments
parser.add_argument('--verbose', dest="verbose", action='store_true',
help="produces more output")
parser.add_argument('-l', '--logfile', dest='logfile',
default='-',
help="full path name for the logfile, "
"or '-' for logging to stdout "
"(default: '-' (logging to stdout))"),
"or '-' for logging to stdout, "
"default: '-' (logging to stdout)"),
parser.add_argument('-r', '--region', dest="region_name",
required=False, default=None,
help="the AWS region of the VPC")
Expand All @@ -69,17 +73,29 @@ def _setup_arg_parser(args_list, watcher_plugin_class, health_plugin_class):
parser.add_argument('--route_recheck_interval',
dest="route_recheck_interval",
required=False, default="30", type=int,
help="time between regular checks of VPC route tables")
help="time between regular checks of VPC route "
"tables, default: 30")
parser.add_argument('-a', '--address', dest="addr",
default="localhost",
help="address to listen on for HTTP requests, "
"default: localhost")
parser.add_argument('-p', '--port', dest="port",
default="33289", type=int,
help="port to listen on for HTTP requests, "
"default: 33289")
parser.add_argument('-m', '--mode', dest='mode', required=True,
help="name of the watcher plugin")
parser.add_argument('-H', '--health', dest='health', required=False,
default=monitor.MONITOR_DEFAULT_PLUGIN,
help="name of the health-check plugin")
parser.add_argument('--verbose', dest="verbose", action='store_true',
help="produces more output")
help="name of the health-check plugin, "
"default: %s" % monitor.MONITOR_DEFAULT_PLUGIN)

arglist = ["logfile", "region_name", "vpc_id", "route_recheck_interval",
"mode", "health", "verbose"]
"verbose", "addr", "port", "mode", "health"]

# Inform the CurrentState object of the main config parameter names, which
# should be rendered in an overview.
CURRENT_STATE.main_param_names = list(arglist)

# Let each watcher and health-monitor plugin add its own arguments.
for plugin_class in [watcher_plugin_class, health_plugin_class]:
Expand Down Expand Up @@ -131,6 +147,19 @@ def _parse_args(args_list, watcher_plugin_class, health_plugin_class):
conf['route_recheck_interval'] != 0:
raise ArgsError("route_recheck_interval argument must be either 0 "
"or at least 5")

if not 0 < conf['port'] < 65535:
raise ArgsError("Invalid listen port '%d' for built-in http server." %
conf['port'])

if not conf['addr'] == "localhost":
# Check if a proper address was specified (already raises a suitable
# ArgsError if not)
utils.ip_check(conf['addr'])

# Store a reference to the config dict in the current state
CURRENT_STATE.conf = conf

return conf


Expand Down Expand Up @@ -216,14 +245,20 @@ def main():
conf.update(meta_data)

try:
logging.info(
"*** Starting vpc-router (%s): mode: %s (%s), "
"health-check: %s (%s) ***" %
(vpcrouter.__version__,
conf['mode'], watcher_plugin_class.get_version(),
health_check_name, health_plugin_class.get_version()))
info_str = "vpc-router (%s): mode: %s (%s), " \
"health-check: %s (%s)" % \
(vpcrouter.__version__,
conf['mode'], watcher_plugin_class.get_version(),
health_check_name, health_plugin_class.get_version())
logging.info("*** Starting %s ***" % info_str)
CURRENT_STATE.versions = info_str

http_srv = http_server.VpcRouterHttpServer(conf)
CURRENT_STATE._vpc_router_http = http_srv

watcher.start_watcher(conf,
watcher_plugin_class, health_plugin_class)
http_srv.stop()
logging.info("*** Stopping vpc-router ***")
except Exception as e:
import traceback
Expand Down
Loading

0 comments on commit 7bddd7c

Please sign in to comment.