# Copyright (c) 2013-2016 Hewlett Packard Enterprise Development LP
#
# Redistribution and use of this software in source and binary forms,
# with or without modification, are permitted provided that the following
# conditions are met:
#
# Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 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.
#
# 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
# OWNER 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 argparse
import csv
import io
from requestbuilder import Arg
import requestbuilder.exceptions
from euca2ools.commands.argtypes import delimited_list
from euca2ools.commands.monitoring import CloudWatchRequest
from euca2ools.commands.monitoring.argtypes import cloudwatch_dimension
POINTS_PER_REQUEST = 20
DATUM_KEYS = {'dim': 'Dimensions',
'metric': 'MetricName',
'max': 'Maximum',
'min': 'Minimum',
'count': 'SampleCount',
'sum': 'Sum',
'ts': 'Timestamp',
'unit': 'Unit',
'val': 'Value'}
class PutMetricData(CloudWatchRequest):
"""
Add data values or statistics to a CloudWatch metric
A metric datum consists of a metric name, any of several attributes,
and either a simple, numeric value (-v) or a set of statistical
values (-s).
All metric data in a given invocation of %(prog)s must belong to one
namespace. %(prog)s supports the following attributes (and
equivalent aliases, in parentheses) for all data. Each of
these attributes has a corresponding command line option that
specifies that attribute for all metric data.
* MetricName (metric)
* Dimensions (dim)
* Timestamp (ts)
* Unit (unit)
Simple metric data have one additional attribute for their values:
* Value (val)
Statistical metric data have four additional attributes:
* Maximum (max)
* Minimum (min)
* SampleCount (count)
* Sum (sum)
The -v/--value option allows you to specify the value of a simple
metric datum. To specify other attributes for data given using
this option, use the options that correspond to them, such as
-d/--dimensions. In particular, the -m/--metric-name option is
required when -v/--value is used.
The -s/--metric-datum option allows for full control of each data
point's attributes. This is necessary for statistical data
points. To specify a metric datum using this option, join each
attribute's name or alias from the lists above with its value
using an '=' character, and join each of those pairs with ','
characters. If a value contains a ',' character, surround the
entire attribute with '"' characters.
For example, each of the following is a valid string
to pass to -s/--metric-datum:
MetricName=MyMetric,Value=1.5
MetricName=MyMetric,Maximum=5,Minimum=1,SampleCount=5,Sum=10
metric=MyMetric,val=9,"dim=InstanceId:i-12345678,Volume:/dev/sda"
Attributes specified via -s/--metric-datum take precedence over those
specified with attribute-specific command line options, such as
-d/--dimensions.
Timestamps must use a format specified in ISO 8601, such as
"1989-11-09T19:17:45.000+01:00". Note that the CloudWatch
service does not accept data with timestamps more than two weeks
in the past.
Dimensions' attributes are specified as a comma-separated list
of dimension names and values that are themselves separated by
':' characters. This means that when more than one dimension is
necessary, the entire Dimensions attribute must be enclosed in '"'
characters. Most shell environments require this to be escaped.
"""
ARGS = [Arg('-n', '--namespace', dest='Namespace', required=True,
help='namespace for the new metric data (required)'),
Arg('-v', '--value', dest='simple_values', route_to=None,
metavar='FLOAT', type=float, action='append',
help='''a simple value for a metric datum. Each use
specifies a new metric datum.'''),
Arg('-s', '--metric-datum', dest='attr_values', route_to=None,
action='append', metavar='KEY1=VALUE1,KEY2=VALUE2,...',
help='''names and values of the attributes for a metric
datum. When values include ',' characters, enclose the
entire name/value pair in '"' characters.'''),
# Euca2ools 3.4 extended the "key=value"-based syntax to allow
# one to supply arbitrary attributes of each datum. Since this
# this format is a strict superset of the original format for
# statistic values we silently treat the old option names as
# aliases for the newer, generic one.
Arg('--statistic-values', '--statisticValues', action='append',
dest='attr_values', route_to=None, help=argparse.SUPPRESS),
Arg('-m', '--metric-name', route_to=None, metavar='METRIC',
help='name of the metric to add metric data to'),
Arg('-d', '--dimensions', metavar='KEY1=VALUE1,KEY2=VALUE2,...',
route_to=None,
type=delimited_list(',', item_type=cloudwatch_dimension),
help='''one or more dimensions to associate with the new
metric data'''),
Arg('-t', '--timestamp', route_to=None,
metavar='YYYY-MM-DDThh:mm:ssZ',
help='timestamp for the new metric data'),
Arg('-u', '--unit', route_to=None, metavar='UNIT',
help='''unit in which to report the new metric data
points (e.g. Bytes)''')]
def configure(self):
CloudWatchRequest.configure(self)
data = []
# Plain values
for val in self.args.get('simple_values') or ():
data.append(self.__build_datum_from_value(val))
# Key/value-based data
for val in self.args.get('attr_values') or ():
data.append(self.__build_datum_from_pairs(val))
self.args['data'] = data
def main(self):
# The API limits us to 20 points per request. There are also
# limits of 40 KB per POST request and 8 KB per GET request
# that we do not consider here.
data = self.args.get('data') or []
for slice_start in range(0, len(data), POINTS_PER_REQUEST):
slice_end = min(slice_start + POINTS_PER_REQUEST, len(data))
self.params['MetricData'] = {'member': data[slice_start:slice_end]}
self.send()
return self.args['data']
def __build_datum_from_value(self, value):
datum = {}
try:
datum['Value'] = float(value)
except ValueError:
raise argparse.ArgumentTypeError(
"argument -v/--value: value '{0}' must be numeric"
.format(value))
self.__complete_datum(datum)
if not datum.get('MetricName'):
raise requestbuilder.exceptions.ArgumentError(
'argument -v/--value requires -m/--metric-name')
return datum
def __build_datum_from_pairs(self, pairs_as_str):
statistic_set_keys = ['Maximum', 'Minimum', 'SampleCount', 'Sum']
datum = {}
if not pairs_as_str.strip():
raise argparse.ArgumentTypeError(
"argument -s/--metric-datum: value must not be empty")
for pair in next(csv.reader(io.BytesIO(pairs_as_str))):
try:
key, val = pair.split('=')
except ValueError:
if pair.startswith('dim=') or pair.startswith('Dimensions='):
raise argparse.ArgumentTypeError(
"argument -s/--metric-datum: dimension names and "
"values in datum '{0}' must be separated with ':', "
"not '='".format(pairs_as_str))
raise argparse.ArgumentTypeError(
"argument -s/--metric-datum: '{0}' in datum '{1}' must "
"have format KEY=VALUE,...".format(pair, pairs_as_str))
key = DATUM_KEYS.get(key, key)
if key in statistic_set_keys:
try:
datum.setdefault('StatisticValues', {})[key] = float(val)
except ValueError:
raise argparse.ArgumentTypeError(
"argument -s/--metric-datum: {0} value for datum "
"'{1}' must be numeric".format(key, pairs_as_str))
elif key == 'Value':
try:
datum[key] = float(val)
except ValueError:
raise argparse.ArgumentTypeError(
"argument -s/--metric-datum: {0} value for datum "
"'{1}' must be numeric".format(key, pairs_as_str))
elif key == 'Dimensions':
datum.setdefault(key, {'member': []})
for dim_pair in val.split(','):
try:
dim_name, dim_val = dim_pair.split(':')
except ValueError:
raise argparse.ArgumentTypeError(
"argument -s/--metric-datum: dimension '{0}' for "
"datum '{1}' must have format KEY:VALUE,..."
.format(dim_pair, pairs_as_str))
datum[key]['member'].append(
{'Name': dim_name, 'Value': dim_val})
elif key in ('MetricName', 'Timestamp', 'Unit'):
datum[key] = val
else:
raise argparse.ArgumentTypeError(
"argument -s/--metric-datum: datum '{0}' contains "
"unrecognized attribute '{1}'".format(pairs_as_str, key))
self.__complete_datum(datum)
# Validate
if not datum.get('MetricName'):
raise argparse.ArgumentTypeError(
"argument -s/--metric-datum: datum '{0}' must have a "
"metric name; supply one individually with 'MetricName=NAME' "
"or set a default for this request with -m/--metric-name"
.format(pairs_as_str))
if 'StatisticValues' in datum:
if 'Value' in datum:
raise argparse.ArgumentTypeError(
"argument -s/--metric-datum: datum '{0}' must not "
"contain both Value and {1} attributes".format(
pairs_as_str, next(datum['StatisticValues'].values())))
for key in statistic_set_keys:
if key not in datum['StatisticValues']:
raise argparse.ArgumentTypeError(
"argument -s/--metric-datum: a {0} is required for "
"statistic datum '{1}'".format(key, pairs_as_str))
elif 'Value' not in datum:
raise argparse.ArgumentTypeError(
"argument -s/--metric-datum: datum '{0}' must contain "
"either a Value or a Maximum, Minimum, SampleCount, and Sum"
.format(pairs_as_str))
return datum
def __complete_datum(self, datum):
attr_map = {
'MetricName': 'metric_name',
'Timestamp': 'timestamp',
'Unit': 'unit'}
for key, val in attr_map.items():
if self.args.get(val):
datum.setdefault(key, self.args.get(val))
if self.args.get('dimensions'):
datum.setdefault('Dimensions',
{'member': self.args.get('dimensions')})