#!/usr/bin/env python

"""
cwg - The CloudWatch Gateway

Requires an instance role that grants access to:
- cloudwatch:PutMetricData

Run cwg from upstart (or systemd, or init, ...),
reading from a named-pipe. Then just publish metrics
to the pipe in the following format:

MetricName Value Unit

- MetricName must meet AWS guidelines for metric names
- Value must be a number
- Unit must be one of the CloudWatch unit names
  (see list below in the code)

Copyright (c) 2016, Kris Feldmann
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

  1. Redistributions of source code must retain the above copyright
     notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above
     copyright notice, this list of conditions and the following
     disclaimer in the documentation and/or other materials provided
     with the distribution.

  3. Neither the name of the copyright holder nor the names of its
     contributors may be used to endorse or promote products derived
     from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

import subprocess
import sys
import json
import time


AWSCLI = '/usr/bin/aws'


def usage():
    "Print the usage statement to stderr."
    sys.stderr.write("Usage: cwg region namespace\n")

def _err(message):
    "Print an ERROR message to stdout."
    sys.stdout.write("ERROR: %s\n" % message)
    return None

def call_by_args(region, cmdtuple, argtuple):
    """
    Execute aws-cli and convert the JSON response
    to a dictionary.
    """
    args = [
        AWSCLI,
        '--region', region,
    ]
    args.extend(cmdtuple)
    if argtuple:
        args.extend(argtuple)
    try:
        outdata = subprocess.check_output(args)
    except (OSError, subprocess.CalledProcessError) as ex:
        _err('Failed to run %s %s %s: %s' \
            % (AWSCLI, cmdtuple[0], cmdtuple[1], ex))
        outdata = None
    if outdata: # Some calls do not return data
        try:
            outdict = json.loads(outdata)
        except json.JSONDecodeError as ex:
            _err(
                """Failed to parse json response from %s %s %s.
                Exception is: %s
                JSON source:
                %s
                """ % (AWSCLI, cmdtuple[0], cmdtuple[1], ex, outdata))
            outdict = None
    else:
        outdict = None
    return outdict

def valid_namespace(namespace):
    """
    Verify that the provided namespace value meets
    AWS requirements.
    """
    if len(namespace) > 256:
        return False
    if (namespace.translate(None,
                            "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
                            + "abcdefghijklmnopqrstuvwxyz.-_/#:") != ''):
        return False
    return True

def valid_unit(unit):
    """
    Verify that the provided unit name meets
    AWS requirements.
    """
    if unit in [
            "Seconds",
            "Microseconds",
            "Milliseconds",
            "Bytes",
            "Kilobytes",
            "Megabytes",
            "Gigabytes",
            "Terabytes",
            "Bits",
            "Kilobits",
            "Megabits",
            "Gigabits",
            "Terabits",
            "Percent",
            "Count",
            "Bytes/Second",
            "Kilobytes/Second",
            "Megabytes/Second",
            "Gigabytes/Second",
            "Terabytes/Second",
            "Bits/Second",
            "Kilobits/Second",
            "Megabits/Second",
            "Gigabits/Second",
            "Terabits/Second",
            "Count/Second",
            "None",
        ]:
        return True
    return False

def valid_value(value):
    "Validate that the provided value is numeric."
    if value.translate(None, '0123456789.-') == '':
        return True
    return False


### main ###############

if len(sys.argv) != 3:
    usage()
    raise SystemExit(1)

REGION = sys.argv[1]
NAMESPACE = sys.argv[2]

if not valid_namespace(NAMESPACE):
    _err("Invalid namespace (see AWS documentation): %s" % NAMESPACE)
    raise SystemExit(2)

DATA = []
while True:
    LINE = sys.stdin.readline()
    if LINE == '':
        if DATA:
            call_by_args(
                REGION,
                (
                    'cloudwatch',
                    'put-metric-data'
                ),
                (
                    '--namespace', NAMESPACE,
                    '--metric-data', json.dumps(DATA),
                ))
            DATA = []
        else:
            time.sleep(2)
        continue
    try:
        NAME, VALUE, UNIT = LINE.strip().split()
    except ValueError:
        _err("Malformed line: %s" % LINE)
        continue
    if not valid_unit(UNIT):
        _err("Invalid unit (see AWS documentation): %s" % UNIT)
        continue
    if not valid_value(VALUE):
        _err("Invalid value (should be numeric [.0-9-]): %s" % VALUE)
        continue
    sys.stdout.write("metric_namespace=%s %s=%s metric_unit=%s\n" \
                    % (NAMESPACE, NAME, VALUE, UNIT))
    sys.stdout.flush()
    DATA.append({
        'MetricName': NAME,
        'Value': float(VALUE),
        'Unit': UNIT,
        })
