Add cloudwatch tags (#4179)
* Update cloudwatch.put_metric_alarm to accept TreatMissingData and Tags parameter * Add parameter ExtendedStatistic and EvaluateLowSampleCountPercentile to cloudwatch.put_metric_alarm * Add parameter ThresholdMetricId to cloudwatch.put_metric_alarm
This commit is contained in:
parent
1800733162
commit
4df099c724
36
moto/cloudwatch/exceptions.py
Normal file
36
moto/cloudwatch/exceptions.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from moto.core.exceptions import RESTError
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidFormat(RESTError):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
super().__init__(__class__.__name__, message)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidParameterValue(RESTError):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
super().__init__(__class__.__name__, message)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceNotFound(RESTError):
|
||||||
|
code = 404
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(__class__.__name__, "Unknown")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceNotFoundException(RESTError):
|
||||||
|
code = 404
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(__class__.__name__, "Unknown")
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(RESTError):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
super().__init__(__class__.__name__, message)
|
@ -2,17 +2,28 @@ import json
|
|||||||
|
|
||||||
from boto3 import Session
|
from boto3 import Session
|
||||||
|
|
||||||
from moto.core.utils import iso_8601_datetime_without_milliseconds
|
from moto.core.utils import (
|
||||||
|
iso_8601_datetime_without_milliseconds,
|
||||||
|
iso_8601_datetime_with_nanoseconds,
|
||||||
|
)
|
||||||
from moto.core import BaseBackend, BaseModel, CloudFormationModel
|
from moto.core import BaseBackend, BaseModel, CloudFormationModel
|
||||||
from moto.core.exceptions import RESTError
|
|
||||||
from moto.logs import logs_backends
|
from moto.logs import logs_backends
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from dateutil.tz import tzutc
|
from dateutil.tz import tzutc
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from .exceptions import (
|
||||||
|
InvalidFormat,
|
||||||
|
ResourceNotFound,
|
||||||
|
ValidationError,
|
||||||
|
InvalidParameterValue,
|
||||||
|
ResourceNotFoundException,
|
||||||
|
)
|
||||||
from .utils import make_arn_for_dashboard, make_arn_for_alarm
|
from .utils import make_arn_for_dashboard, make_arn_for_alarm
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
|
||||||
from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID
|
from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID
|
||||||
|
from ..utilities.tagging_service import TaggingService
|
||||||
|
|
||||||
_EMPTY_LIST = tuple()
|
_EMPTY_LIST = tuple()
|
||||||
|
|
||||||
@ -89,6 +100,7 @@ def daterange(start, stop, step=timedelta(days=1), inclusive=False):
|
|||||||
class FakeAlarm(BaseModel):
|
class FakeAlarm(BaseModel):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
region_name,
|
||||||
name,
|
name,
|
||||||
namespace,
|
namespace,
|
||||||
metric_name,
|
metric_name,
|
||||||
@ -99,6 +111,7 @@ class FakeAlarm(BaseModel):
|
|||||||
period,
|
period,
|
||||||
threshold,
|
threshold,
|
||||||
statistic,
|
statistic,
|
||||||
|
extended_statistic,
|
||||||
description,
|
description,
|
||||||
dimensions,
|
dimensions,
|
||||||
alarm_actions,
|
alarm_actions,
|
||||||
@ -106,11 +119,14 @@ class FakeAlarm(BaseModel):
|
|||||||
insufficient_data_actions,
|
insufficient_data_actions,
|
||||||
unit,
|
unit,
|
||||||
actions_enabled,
|
actions_enabled,
|
||||||
region="us-east-1",
|
treat_missing_data,
|
||||||
|
evaluate_low_sample_count_percentile,
|
||||||
|
threshold_metric_id,
|
||||||
rule=None,
|
rule=None,
|
||||||
):
|
):
|
||||||
|
self.region_name = region_name
|
||||||
self.name = name
|
self.name = name
|
||||||
self.alarm_arn = make_arn_for_alarm(region, DEFAULT_ACCOUNT_ID, name)
|
self.alarm_arn = make_arn_for_alarm(region_name, DEFAULT_ACCOUNT_ID, name)
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
self.metric_name = metric_name
|
self.metric_name = metric_name
|
||||||
self.metric_data_queries = metric_data_queries
|
self.metric_data_queries = metric_data_queries
|
||||||
@ -120,6 +136,7 @@ class FakeAlarm(BaseModel):
|
|||||||
self.period = period
|
self.period = period
|
||||||
self.threshold = threshold
|
self.threshold = threshold
|
||||||
self.statistic = statistic
|
self.statistic = statistic
|
||||||
|
self.extended_statistic = extended_statistic
|
||||||
self.description = description
|
self.description = description
|
||||||
self.dimensions = [
|
self.dimensions = [
|
||||||
Dimension(dimension["name"], dimension["value"]) for dimension in dimensions
|
Dimension(dimension["name"], dimension["value"]) for dimension in dimensions
|
||||||
@ -129,14 +146,21 @@ class FakeAlarm(BaseModel):
|
|||||||
self.ok_actions = ok_actions
|
self.ok_actions = ok_actions
|
||||||
self.insufficient_data_actions = insufficient_data_actions
|
self.insufficient_data_actions = insufficient_data_actions
|
||||||
self.unit = unit
|
self.unit = unit
|
||||||
self.configuration_updated_timestamp = datetime.utcnow()
|
self.configuration_updated_timestamp = iso_8601_datetime_with_nanoseconds(
|
||||||
|
datetime.now(tz=tzutc())
|
||||||
|
)
|
||||||
|
self.treat_missing_data = treat_missing_data
|
||||||
|
self.evaluate_low_sample_count_percentile = evaluate_low_sample_count_percentile
|
||||||
|
self.threshold_metric_id = threshold_metric_id
|
||||||
|
|
||||||
self.history = []
|
self.history = []
|
||||||
|
|
||||||
self.state_reason = ""
|
self.state_reason = "Unchecked: Initial alarm creation"
|
||||||
self.state_reason_data = "{}"
|
self.state_reason_data = "{}"
|
||||||
self.state_value = "OK"
|
self.state_value = "OK"
|
||||||
self.state_updated_timestamp = datetime.utcnow()
|
self.state_updated_timestamp = iso_8601_datetime_with_nanoseconds(
|
||||||
|
datetime.now(tz=tzutc())
|
||||||
|
)
|
||||||
|
|
||||||
# only used for composite alarms
|
# only used for composite alarms
|
||||||
self.rule = rule
|
self.rule = rule
|
||||||
@ -156,7 +180,9 @@ class FakeAlarm(BaseModel):
|
|||||||
self.state_reason = reason
|
self.state_reason = reason
|
||||||
self.state_reason_data = reason_data
|
self.state_reason_data = reason_data
|
||||||
self.state_value = state_value
|
self.state_value = state_value
|
||||||
self.state_updated_timestamp = datetime.utcnow()
|
self.state_updated_timestamp = iso_8601_datetime_with_nanoseconds(
|
||||||
|
datetime.now(tz=tzutc())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def are_dimensions_same(metric_dimensions, dimensions):
|
def are_dimensions_same(metric_dimensions, dimensions):
|
||||||
@ -273,11 +299,18 @@ class Statistics:
|
|||||||
|
|
||||||
|
|
||||||
class CloudWatchBackend(BaseBackend):
|
class CloudWatchBackend(BaseBackend):
|
||||||
def __init__(self):
|
def __init__(self, region_name):
|
||||||
|
self.region_name = region_name
|
||||||
self.alarms = {}
|
self.alarms = {}
|
||||||
self.dashboards = {}
|
self.dashboards = {}
|
||||||
self.metric_data = []
|
self.metric_data = []
|
||||||
self.paged_metric_data = {}
|
self.paged_metric_data = {}
|
||||||
|
self.tagger = TaggingService()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
region_name = self.region_name
|
||||||
|
self.__dict__ = {}
|
||||||
|
self.__init__(region_name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
# Retrieve a list of all OOTB metrics that are provided by metrics providers
|
# Retrieve a list of all OOTB metrics that are provided by metrics providers
|
||||||
@ -300,6 +333,7 @@ class CloudWatchBackend(BaseBackend):
|
|||||||
period,
|
period,
|
||||||
threshold,
|
threshold,
|
||||||
statistic,
|
statistic,
|
||||||
|
extended_statistic,
|
||||||
description,
|
description,
|
||||||
dimensions,
|
dimensions,
|
||||||
alarm_actions,
|
alarm_actions,
|
||||||
@ -307,32 +341,54 @@ class CloudWatchBackend(BaseBackend):
|
|||||||
insufficient_data_actions,
|
insufficient_data_actions,
|
||||||
unit,
|
unit,
|
||||||
actions_enabled,
|
actions_enabled,
|
||||||
region="us-east-1",
|
treat_missing_data,
|
||||||
|
evaluate_low_sample_count_percentile,
|
||||||
|
threshold_metric_id,
|
||||||
rule=None,
|
rule=None,
|
||||||
|
tags=None,
|
||||||
):
|
):
|
||||||
|
if extended_statistic and not extended_statistic.startswith("p"):
|
||||||
|
raise InvalidParameterValue(
|
||||||
|
f"The value {extended_statistic} for parameter ExtendedStatistic is not supported."
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
evaluate_low_sample_count_percentile
|
||||||
|
and evaluate_low_sample_count_percentile not in ("evaluate", "ignore")
|
||||||
|
):
|
||||||
|
raise ValidationError(
|
||||||
|
f"Option {evaluate_low_sample_count_percentile} is not supported. "
|
||||||
|
"Supported options for parameter EvaluateLowSampleCountPercentile are evaluate and ignore."
|
||||||
|
)
|
||||||
|
|
||||||
alarm = FakeAlarm(
|
alarm = FakeAlarm(
|
||||||
name,
|
region_name=self.region_name,
|
||||||
namespace,
|
name=name,
|
||||||
metric_name,
|
namespace=namespace,
|
||||||
metric_data_queries,
|
metric_name=metric_name,
|
||||||
comparison_operator,
|
metric_data_queries=metric_data_queries,
|
||||||
evaluation_periods,
|
comparison_operator=comparison_operator,
|
||||||
datapoints_to_alarm,
|
evaluation_periods=evaluation_periods,
|
||||||
period,
|
datapoints_to_alarm=datapoints_to_alarm,
|
||||||
threshold,
|
period=period,
|
||||||
statistic,
|
threshold=threshold,
|
||||||
description,
|
statistic=statistic,
|
||||||
dimensions,
|
extended_statistic=extended_statistic,
|
||||||
alarm_actions,
|
description=description,
|
||||||
ok_actions,
|
dimensions=dimensions,
|
||||||
insufficient_data_actions,
|
alarm_actions=alarm_actions,
|
||||||
unit,
|
ok_actions=ok_actions,
|
||||||
actions_enabled,
|
insufficient_data_actions=insufficient_data_actions,
|
||||||
region,
|
unit=unit,
|
||||||
|
actions_enabled=actions_enabled,
|
||||||
|
treat_missing_data=treat_missing_data,
|
||||||
|
evaluate_low_sample_count_percentile=evaluate_low_sample_count_percentile,
|
||||||
|
threshold_metric_id=threshold_metric_id,
|
||||||
rule=rule,
|
rule=rule,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.alarms[name] = alarm
|
self.alarms[name] = alarm
|
||||||
|
self.tagger.tag_resource(alarm.alarm_arn, tags)
|
||||||
|
|
||||||
return alarm
|
return alarm
|
||||||
|
|
||||||
def get_all_alarms(self):
|
def get_all_alarms(self):
|
||||||
@ -371,13 +427,6 @@ class CloudWatchBackend(BaseBackend):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def delete_alarms(self, alarm_names):
|
def delete_alarms(self, alarm_names):
|
||||||
for alarm_name in alarm_names:
|
|
||||||
if alarm_name not in self.alarms:
|
|
||||||
raise RESTError(
|
|
||||||
"ResourceNotFound",
|
|
||||||
"Alarm {0} not found".format(alarm_name),
|
|
||||||
status=404,
|
|
||||||
)
|
|
||||||
for alarm_name in alarm_names:
|
for alarm_name in alarm_names:
|
||||||
self.alarms.pop(alarm_name, None)
|
self.alarms.pop(alarm_name, None)
|
||||||
|
|
||||||
@ -549,17 +598,16 @@ class CloudWatchBackend(BaseBackend):
|
|||||||
if reason_data is not None:
|
if reason_data is not None:
|
||||||
json.loads(reason_data)
|
json.loads(reason_data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise RESTError("InvalidFormat", "StateReasonData is invalid JSON")
|
raise InvalidFormat("Unknown")
|
||||||
|
|
||||||
if alarm_name not in self.alarms:
|
if alarm_name not in self.alarms:
|
||||||
raise RESTError(
|
raise ResourceNotFound
|
||||||
"ResourceNotFound", "Alarm {0} not found".format(alarm_name), status=404
|
|
||||||
)
|
|
||||||
|
|
||||||
if state_value not in ("OK", "ALARM", "INSUFFICIENT_DATA"):
|
if state_value not in ("OK", "ALARM", "INSUFFICIENT_DATA"):
|
||||||
raise RESTError(
|
raise ValidationError(
|
||||||
"InvalidParameterValue",
|
"1 validation error detected: "
|
||||||
"StateValue is not one of OK | ALARM | INSUFFICIENT_DATA",
|
f"Value '{state_value}' at 'stateValue' failed to satisfy constraint: "
|
||||||
|
"Member must satisfy enum value set: [INSUFFICIENT_DATA, ALARM, OK]"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.alarms[alarm_name].update_state(reason, reason_data, state_value)
|
self.alarms[alarm_name].update_state(reason, reason_data, state_value)
|
||||||
@ -567,9 +615,7 @@ class CloudWatchBackend(BaseBackend):
|
|||||||
def list_metrics(self, next_token, namespace, metric_name, dimensions):
|
def list_metrics(self, next_token, namespace, metric_name, dimensions):
|
||||||
if next_token:
|
if next_token:
|
||||||
if next_token not in self.paged_metric_data:
|
if next_token not in self.paged_metric_data:
|
||||||
raise RESTError(
|
raise InvalidParameterValue("Request parameter NextToken is invalid")
|
||||||
"PaginationException", "Request parameter NextToken is invalid"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
metrics = self.paged_metric_data[next_token]
|
metrics = self.paged_metric_data[next_token]
|
||||||
del self.paged_metric_data[next_token] # Cant reuse same token twice
|
del self.paged_metric_data[next_token] # Cant reuse same token twice
|
||||||
@ -591,6 +637,21 @@ class CloudWatchBackend(BaseBackend):
|
|||||||
new_metrics.append(md)
|
new_metrics.append(md)
|
||||||
return new_metrics
|
return new_metrics
|
||||||
|
|
||||||
|
def list_tags_for_resource(self, arn):
|
||||||
|
return self.tagger.get_tag_dict_for_resource(arn)
|
||||||
|
|
||||||
|
def tag_resource(self, arn, tags):
|
||||||
|
if arn not in self.tagger.tags.keys():
|
||||||
|
raise ResourceNotFoundException
|
||||||
|
|
||||||
|
self.tagger.tag_resource(arn, tags)
|
||||||
|
|
||||||
|
def untag_resource(self, arn, tag_keys):
|
||||||
|
if arn not in self.tagger.tags.keys():
|
||||||
|
raise ResourceNotFoundException
|
||||||
|
|
||||||
|
self.tagger.untag_resource_using_names(arn, tag_keys)
|
||||||
|
|
||||||
def _get_paginated(self, metrics):
|
def _get_paginated(self, metrics):
|
||||||
if len(metrics) > 500:
|
if len(metrics) > 500:
|
||||||
next_token = str(uuid4())
|
next_token = str(uuid4())
|
||||||
@ -629,13 +690,13 @@ class LogGroup(CloudFormationModel):
|
|||||||
|
|
||||||
cloudwatch_backends = {}
|
cloudwatch_backends = {}
|
||||||
for region in Session().get_available_regions("cloudwatch"):
|
for region in Session().get_available_regions("cloudwatch"):
|
||||||
cloudwatch_backends[region] = CloudWatchBackend()
|
cloudwatch_backends[region] = CloudWatchBackend(region)
|
||||||
for region in Session().get_available_regions(
|
for region in Session().get_available_regions(
|
||||||
"cloudwatch", partition_name="aws-us-gov"
|
"cloudwatch", partition_name="aws-us-gov"
|
||||||
):
|
):
|
||||||
cloudwatch_backends[region] = CloudWatchBackend()
|
cloudwatch_backends[region] = CloudWatchBackend(region)
|
||||||
for region in Session().get_available_regions("cloudwatch", partition_name="aws-cn"):
|
for region in Session().get_available_regions("cloudwatch", partition_name="aws-cn"):
|
||||||
cloudwatch_backends[region] = CloudWatchBackend()
|
cloudwatch_backends[region] = CloudWatchBackend(region)
|
||||||
|
|
||||||
# List of services that provide OOTB CW metrics
|
# List of services that provide OOTB CW metrics
|
||||||
# See the S3Backend constructor for an example
|
# See the S3Backend constructor for an example
|
||||||
|
@ -70,6 +70,7 @@ class CloudWatchResponse(BaseResponse):
|
|||||||
period = self._get_param("Period")
|
period = self._get_param("Period")
|
||||||
threshold = self._get_param("Threshold")
|
threshold = self._get_param("Threshold")
|
||||||
statistic = self._get_param("Statistic")
|
statistic = self._get_param("Statistic")
|
||||||
|
extended_statistic = self._get_param("ExtendedStatistic")
|
||||||
description = self._get_param("AlarmDescription")
|
description = self._get_param("AlarmDescription")
|
||||||
dimensions = self._get_list_prefix("Dimensions.member")
|
dimensions = self._get_list_prefix("Dimensions.member")
|
||||||
alarm_actions = self._get_multi_param("AlarmActions.member")
|
alarm_actions = self._get_multi_param("AlarmActions.member")
|
||||||
@ -79,28 +80,38 @@ class CloudWatchResponse(BaseResponse):
|
|||||||
"InsufficientDataActions.member"
|
"InsufficientDataActions.member"
|
||||||
)
|
)
|
||||||
unit = self._get_param("Unit")
|
unit = self._get_param("Unit")
|
||||||
|
treat_missing_data = self._get_param("TreatMissingData")
|
||||||
|
evaluate_low_sample_count_percentile = self._get_param(
|
||||||
|
"EvaluateLowSampleCountPercentile"
|
||||||
|
)
|
||||||
|
threshold_metric_id = self._get_param("ThresholdMetricId")
|
||||||
# fetch AlarmRule to re-use this method for composite alarms as well
|
# fetch AlarmRule to re-use this method for composite alarms as well
|
||||||
rule = self._get_param("AlarmRule")
|
rule = self._get_param("AlarmRule")
|
||||||
|
tags = self._get_multi_param("Tags.member")
|
||||||
alarm = self.cloudwatch_backend.put_metric_alarm(
|
alarm = self.cloudwatch_backend.put_metric_alarm(
|
||||||
name,
|
name=name,
|
||||||
namespace,
|
namespace=namespace,
|
||||||
metric_name,
|
metric_name=metric_name,
|
||||||
metric_data_queries,
|
metric_data_queries=metric_data_queries,
|
||||||
comparison_operator,
|
comparison_operator=comparison_operator,
|
||||||
evaluation_periods,
|
evaluation_periods=evaluation_periods,
|
||||||
datapoints_to_alarm,
|
datapoints_to_alarm=datapoints_to_alarm,
|
||||||
period,
|
period=period,
|
||||||
threshold,
|
threshold=threshold,
|
||||||
statistic,
|
statistic=statistic,
|
||||||
description,
|
extended_statistic=extended_statistic,
|
||||||
dimensions,
|
description=description,
|
||||||
alarm_actions,
|
dimensions=dimensions,
|
||||||
ok_actions,
|
alarm_actions=alarm_actions,
|
||||||
insufficient_data_actions,
|
ok_actions=ok_actions,
|
||||||
unit,
|
insufficient_data_actions=insufficient_data_actions,
|
||||||
actions_enabled,
|
unit=unit,
|
||||||
self.region,
|
actions_enabled=actions_enabled,
|
||||||
|
treat_missing_data=treat_missing_data,
|
||||||
|
evaluate_low_sample_count_percentile=evaluate_low_sample_count_percentile,
|
||||||
|
threshold_metric_id=threshold_metric_id,
|
||||||
rule=rule,
|
rule=rule,
|
||||||
|
tags=tags,
|
||||||
)
|
)
|
||||||
template = self.response_template(PUT_METRIC_ALARM_TEMPLATE)
|
template = self.response_template(PUT_METRIC_ALARM_TEMPLATE)
|
||||||
return template.render(alarm=alarm)
|
return template.render(alarm=alarm)
|
||||||
@ -301,6 +312,35 @@ class CloudWatchResponse(BaseResponse):
|
|||||||
template = self.response_template(SET_ALARM_STATE_TEMPLATE)
|
template = self.response_template(SET_ALARM_STATE_TEMPLATE)
|
||||||
return template.render()
|
return template.render()
|
||||||
|
|
||||||
|
@amzn_request_id
|
||||||
|
def list_tags_for_resource(self):
|
||||||
|
resource_arn = self._get_param("ResourceARN")
|
||||||
|
|
||||||
|
tags = self.cloudwatch_backend.list_tags_for_resource(resource_arn)
|
||||||
|
|
||||||
|
template = self.response_template(LIST_TAGS_FOR_RESOURCE_TEMPLATE)
|
||||||
|
return template.render(tags=tags)
|
||||||
|
|
||||||
|
@amzn_request_id
|
||||||
|
def tag_resource(self):
|
||||||
|
resource_arn = self._get_param("ResourceARN")
|
||||||
|
tags = self._get_multi_param("Tags.member")
|
||||||
|
|
||||||
|
self.cloudwatch_backend.tag_resource(resource_arn, tags)
|
||||||
|
|
||||||
|
template = self.response_template(TAG_RESOURCE_TEMPLATE)
|
||||||
|
return template.render()
|
||||||
|
|
||||||
|
@amzn_request_id
|
||||||
|
def untag_resource(self):
|
||||||
|
resource_arn = self._get_param("ResourceARN")
|
||||||
|
tag_keys = self._get_multi_param("TagKeys.member")
|
||||||
|
|
||||||
|
self.cloudwatch_backend.untag_resource(resource_arn, tag_keys)
|
||||||
|
|
||||||
|
template = self.response_template(UNTAG_RESOURCE_TEMPLATE)
|
||||||
|
return template.render()
|
||||||
|
|
||||||
|
|
||||||
PUT_METRIC_ALARM_TEMPLATE = """<PutMetricAlarmResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
|
PUT_METRIC_ALARM_TEMPLATE = """<PutMetricAlarmResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
|
||||||
<ResponseMetadata>
|
<ResponseMetadata>
|
||||||
@ -409,12 +449,24 @@ DESCRIBE_ALARMS_TEMPLATE = """<DescribeAlarmsResponse xmlns="http://monitoring.a
|
|||||||
{% if alarm.statistic is not none %}
|
{% if alarm.statistic is not none %}
|
||||||
<Statistic>{{ alarm.statistic }}</Statistic>
|
<Statistic>{{ alarm.statistic }}</Statistic>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if alarm.extended_statistic is not none %}
|
||||||
|
<ExtendedStatistic>{{ alarm.extended_statistic }}</ExtendedStatistic>
|
||||||
|
{% endif %}
|
||||||
{% if alarm.threshold is not none %}
|
{% if alarm.threshold is not none %}
|
||||||
<Threshold>{{ alarm.threshold }}</Threshold>
|
<Threshold>{{ alarm.threshold }}</Threshold>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if alarm.unit is not none %}
|
{% if alarm.unit is not none %}
|
||||||
<Unit>{{ alarm.unit }}</Unit>
|
<Unit>{{ alarm.unit }}</Unit>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if alarm.treat_missing_data is not none %}
|
||||||
|
<TreatMissingData>{{ alarm.treat_missing_data }}</TreatMissingData>
|
||||||
|
{% endif %}
|
||||||
|
{% if alarm.evaluate_low_sample_count_percentile is not none %}
|
||||||
|
<EvaluateLowSampleCountPercentile>{{ alarm.evaluate_low_sample_count_percentile }}</EvaluateLowSampleCountPercentile>
|
||||||
|
{% endif %}
|
||||||
|
{% if alarm.threshold_metric_id is not none %}
|
||||||
|
<ThresholdMetricId>{{ alarm.threshold_metric_id }}</ThresholdMetricId>
|
||||||
|
{% endif %}
|
||||||
{% if alarm.rule is not none %}
|
{% if alarm.rule is not none %}
|
||||||
<AlarmRule>{{ alarm.rule }}</AlarmRule>
|
<AlarmRule>{{ alarm.rule }}</AlarmRule>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -654,3 +706,34 @@ ERROR_RESPONSE_TEMPLATE = """<ErrorResponse xmlns="http://monitoring.amazonaws.c
|
|||||||
</Error>
|
</Error>
|
||||||
<RequestId>{{ request_id }}</RequestId>
|
<RequestId>{{ request_id }}</RequestId>
|
||||||
</ErrorResponse>"""
|
</ErrorResponse>"""
|
||||||
|
|
||||||
|
LIST_TAGS_FOR_RESOURCE_TEMPLATE = """<ListTagsForResourceResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
|
||||||
|
<ListTagsForResourceResult>
|
||||||
|
<Tags>
|
||||||
|
{% for key, value in tags.items() %}
|
||||||
|
<member>
|
||||||
|
<Key>{{ key }}</Key>
|
||||||
|
<Value>{{ value }}</Value>
|
||||||
|
</member>
|
||||||
|
{% endfor %}
|
||||||
|
</Tags>
|
||||||
|
</ListTagsForResourceResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>{{ request_id }}</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</ListTagsForResourceResponse>
|
||||||
|
"""
|
||||||
|
|
||||||
|
TAG_RESOURCE_TEMPLATE = """<TagResourceResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
|
||||||
|
<TagResourceResult/>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>{{ request_id }}</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</TagResourceResponse>"""
|
||||||
|
|
||||||
|
UNTAG_RESOURCE_TEMPLATE = """<UntagResourceResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
|
||||||
|
<UntagResourceResult/>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>{{ request_id }}</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</UntagResourceResponse>"""
|
||||||
|
@ -188,14 +188,17 @@ def iso_8601_datetime_with_milliseconds(datetime):
|
|||||||
return datetime.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
return datetime.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
||||||
|
|
||||||
|
|
||||||
|
# Even Python does not support nanoseconds, other languages like Go do (needed for Terraform)
|
||||||
|
def iso_8601_datetime_with_nanoseconds(datetime):
|
||||||
|
return datetime.strftime("%Y-%m-%dT%H:%M:%S.%f000Z")
|
||||||
|
|
||||||
|
|
||||||
def iso_8601_datetime_without_milliseconds(datetime):
|
def iso_8601_datetime_without_milliseconds(datetime):
|
||||||
return None if datetime is None else datetime.strftime("%Y-%m-%dT%H:%M:%S") + "Z"
|
return None if datetime is None else datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
def iso_8601_datetime_without_milliseconds_s3(datetime):
|
def iso_8601_datetime_without_milliseconds_s3(datetime):
|
||||||
return (
|
return None if datetime is None else datetime.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
||||||
None if datetime is None else datetime.strftime("%Y-%m-%dT%H:%M:%S.000") + "Z"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
RFC1123 = "%a, %d %b %Y %H:%M:%S GMT"
|
RFC1123 = "%a, %d %b %Y %H:%M:%S GMT"
|
||||||
|
@ -627,5 +627,22 @@
|
|||||||
"name": "amzn-ami-vpc-nat-2018.03.0.20210721.0-x86_64-ebs",
|
"name": "amzn-ami-vpc-nat-2018.03.0.20210721.0-x86_64-ebs",
|
||||||
"virtualization_type": "hvm",
|
"virtualization_type": "hvm",
|
||||||
"hypervisor": "xen"
|
"hypervisor": "xen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ami_id": "ami-000c540e28953ace2",
|
||||||
|
"state": "available",
|
||||||
|
"public": true,
|
||||||
|
"owner_id": "137112412989",
|
||||||
|
"image_location": "amazon/amzn-ami-minimal-hvm-2018.03.0.20181129-x86_64-ebs",
|
||||||
|
"sriov": "simple",
|
||||||
|
"root_device_type": "ebs",
|
||||||
|
"root_device_name": "/dev/xvda",
|
||||||
|
"description": "Amazon Linux AMI 2018.03.0.20181129 x86_64 Minimal HVM ebs",
|
||||||
|
"image_type": "machine",
|
||||||
|
"platform": "Linux/UNIX",
|
||||||
|
"architecture": "x86_64",
|
||||||
|
"name": "amzn-ami-minimal-hvm-2018.03.0.20181129-x86_64-ebs",
|
||||||
|
"virtualization_type": "hvm",
|
||||||
|
"hypervisor": "xen"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -12,6 +12,7 @@ TestAccAWSCloudWatchEventConnection
|
|||||||
TestAccAWSCloudWatchEventPermission
|
TestAccAWSCloudWatchEventPermission
|
||||||
TestAccAWSCloudWatchEventRule
|
TestAccAWSCloudWatchEventRule
|
||||||
TestAccAWSCloudwatchLogGroupDataSource
|
TestAccAWSCloudwatchLogGroupDataSource
|
||||||
|
TestAccAWSCloudWatchMetricAlarm
|
||||||
TestAccAWSDataSourceCloudwatch
|
TestAccAWSDataSourceCloudwatch
|
||||||
TestAccAWSDataSourceElasticBeanstalkHostedZone
|
TestAccAWSDataSourceElasticBeanstalkHostedZone
|
||||||
TestAccAWSDataSourceIAMGroup
|
TestAccAWSDataSourceIAMGroup
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
# from __future__ import unicode_literals
|
from datetime import datetime, timedelta
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
from datetime import datetime, timedelta
|
from dateutil.tz import tzutc
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
import pytest
|
import pytest
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@ -95,34 +96,12 @@ def test_get_dashboard_fail():
|
|||||||
|
|
||||||
|
|
||||||
@mock_cloudwatch
|
@mock_cloudwatch
|
||||||
def test_delete_invalid_alarm():
|
def test_delete_alarms_without_error():
|
||||||
|
# given
|
||||||
cloudwatch = boto3.client("cloudwatch", "eu-west-1")
|
cloudwatch = boto3.client("cloudwatch", "eu-west-1")
|
||||||
|
|
||||||
cloudwatch.put_metric_alarm(
|
# when/then
|
||||||
AlarmName="testalarm1",
|
cloudwatch.delete_alarms(AlarmNames=["not-exists"])
|
||||||
MetricName="cpu",
|
|
||||||
Namespace="blah",
|
|
||||||
Period=10,
|
|
||||||
EvaluationPeriods=5,
|
|
||||||
Statistic="Average",
|
|
||||||
Threshold=2,
|
|
||||||
ComparisonOperator="GreaterThanThreshold",
|
|
||||||
ActionsEnabled=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# trying to delete an alarm which is not created along with valid alarm.
|
|
||||||
with pytest.raises(ClientError) as e:
|
|
||||||
cloudwatch.delete_alarms(AlarmNames=["InvalidAlarmName", "testalarm1"])
|
|
||||||
e.value.response["Error"]["Code"].should.equal("ResourceNotFound")
|
|
||||||
|
|
||||||
resp = cloudwatch.describe_alarms(AlarmNames=["testalarm1"])
|
|
||||||
# making sure other alarms are not deleted in case of an error.
|
|
||||||
len(resp["MetricAlarms"]).should.equal(1)
|
|
||||||
|
|
||||||
# test to check if the error raises if only one invalid alarm is tried to delete.
|
|
||||||
with pytest.raises(ClientError) as e:
|
|
||||||
cloudwatch.delete_alarms(AlarmNames=["InvalidAlarmName"])
|
|
||||||
e.value.response["Error"]["Code"].should.equal("ResourceNotFound")
|
|
||||||
|
|
||||||
|
|
||||||
@mock_cloudwatch
|
@mock_cloudwatch
|
||||||
@ -917,3 +896,458 @@ def test_get_metric_data_for_multiple_metrics():
|
|||||||
|
|
||||||
res2 = [res for res in response["MetricDataResults"] if res["Id"] == "result2"][0]
|
res2 = [res for res in response["MetricDataResults"] if res["Id"] == "result2"][0]
|
||||||
res2["Values"].should.equal([25.0])
|
res2["Values"].should.equal([25.0])
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
def test_put_metric_alarm():
|
||||||
|
# given
|
||||||
|
region_name = "eu-central-1"
|
||||||
|
client = boto3.client("cloudwatch", region_name=region_name)
|
||||||
|
alarm_name = "test-alarm"
|
||||||
|
sns_topic_arn = f"arn:aws:sns:${region_name}:${ACCOUNT_ID}:test-topic"
|
||||||
|
|
||||||
|
# when
|
||||||
|
client.put_metric_alarm(
|
||||||
|
AlarmName=alarm_name,
|
||||||
|
AlarmDescription="test alarm",
|
||||||
|
ActionsEnabled=True,
|
||||||
|
OKActions=[sns_topic_arn],
|
||||||
|
AlarmActions=[sns_topic_arn],
|
||||||
|
InsufficientDataActions=[sns_topic_arn],
|
||||||
|
MetricName="5XXError",
|
||||||
|
Namespace="AWS/ApiGateway",
|
||||||
|
Statistic="Sum",
|
||||||
|
Dimensions=[
|
||||||
|
{"Name": "ApiName", "Value": "test-api"},
|
||||||
|
{"Name": "Stage", "Value": "default"},
|
||||||
|
],
|
||||||
|
Period=60,
|
||||||
|
Unit="Seconds",
|
||||||
|
EvaluationPeriods=1,
|
||||||
|
DatapointsToAlarm=1,
|
||||||
|
Threshold=1.0,
|
||||||
|
ComparisonOperator="GreaterThanOrEqualToThreshold",
|
||||||
|
TreatMissingData="notBreaching",
|
||||||
|
Tags=[{"Key": "key-1", "Value": "value-1"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
alarms = client.describe_alarms(AlarmNames=[alarm_name])["MetricAlarms"]
|
||||||
|
alarms.should.have.length_of(1)
|
||||||
|
|
||||||
|
alarm = alarms[0]
|
||||||
|
alarm["AlarmName"].should.equal(alarm_name)
|
||||||
|
alarm["AlarmArn"].should.equal(
|
||||||
|
f"arn:aws:cloudwatch:{region_name}:{ACCOUNT_ID}:alarm:{alarm_name}"
|
||||||
|
)
|
||||||
|
alarm["AlarmDescription"].should.equal("test alarm")
|
||||||
|
alarm["AlarmConfigurationUpdatedTimestamp"].should.be.a(datetime)
|
||||||
|
alarm["AlarmConfigurationUpdatedTimestamp"].tzinfo.should.equal(tzutc())
|
||||||
|
alarm["ActionsEnabled"].should.be.ok
|
||||||
|
alarm["OKActions"].should.equal([sns_topic_arn])
|
||||||
|
alarm["AlarmActions"].should.equal([sns_topic_arn])
|
||||||
|
alarm["InsufficientDataActions"].should.equal([sns_topic_arn])
|
||||||
|
alarm["StateValue"].should.equal("OK")
|
||||||
|
alarm["StateReason"].should.equal("Unchecked: Initial alarm creation")
|
||||||
|
alarm["StateUpdatedTimestamp"].should.be.a(datetime)
|
||||||
|
alarm["StateUpdatedTimestamp"].tzinfo.should.equal(tzutc())
|
||||||
|
alarm["MetricName"].should.equal("5XXError")
|
||||||
|
alarm["Namespace"].should.equal("AWS/ApiGateway")
|
||||||
|
alarm["Statistic"].should.equal("Sum")
|
||||||
|
sorted(alarm["Dimensions"], key=itemgetter("Name")).should.equal(
|
||||||
|
sorted(
|
||||||
|
[
|
||||||
|
{"Name": "ApiName", "Value": "test-api"},
|
||||||
|
{"Name": "Stage", "Value": "default"},
|
||||||
|
],
|
||||||
|
key=itemgetter("Name"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
alarm["Period"].should.equal(60)
|
||||||
|
alarm["Unit"].should.equal("Seconds")
|
||||||
|
alarm["EvaluationPeriods"].should.equal(1)
|
||||||
|
alarm["DatapointsToAlarm"].should.equal(1)
|
||||||
|
alarm["Threshold"].should.equal(1.0)
|
||||||
|
alarm["ComparisonOperator"].should.equal("GreaterThanOrEqualToThreshold")
|
||||||
|
alarm["TreatMissingData"].should.equal("notBreaching")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
def test_put_metric_alarm_with_percentile():
|
||||||
|
# given
|
||||||
|
region_name = "eu-central-1"
|
||||||
|
client = boto3.client("cloudwatch", region_name=region_name)
|
||||||
|
alarm_name = "test-alarm"
|
||||||
|
|
||||||
|
# when
|
||||||
|
client.put_metric_alarm(
|
||||||
|
AlarmName=alarm_name,
|
||||||
|
AlarmDescription="test alarm",
|
||||||
|
ActionsEnabled=True,
|
||||||
|
MetricName="5XXError",
|
||||||
|
Namespace="AWS/ApiGateway",
|
||||||
|
ExtendedStatistic="p90",
|
||||||
|
Dimensions=[
|
||||||
|
{"Name": "ApiName", "Value": "test-api"},
|
||||||
|
{"Name": "Stage", "Value": "default"},
|
||||||
|
],
|
||||||
|
Period=60,
|
||||||
|
Unit="Seconds",
|
||||||
|
EvaluationPeriods=1,
|
||||||
|
DatapointsToAlarm=1,
|
||||||
|
Threshold=1.0,
|
||||||
|
ComparisonOperator="GreaterThanOrEqualToThreshold",
|
||||||
|
TreatMissingData="notBreaching",
|
||||||
|
EvaluateLowSampleCountPercentile="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
alarms = client.describe_alarms(AlarmNames=[alarm_name])["MetricAlarms"]
|
||||||
|
alarms.should.have.length_of(1)
|
||||||
|
|
||||||
|
alarm = alarms[0]
|
||||||
|
alarm["AlarmName"].should.equal(alarm_name)
|
||||||
|
alarm["AlarmArn"].should.equal(
|
||||||
|
f"arn:aws:cloudwatch:{region_name}:{ACCOUNT_ID}:alarm:{alarm_name}"
|
||||||
|
)
|
||||||
|
alarm["AlarmDescription"].should.equal("test alarm")
|
||||||
|
alarm["AlarmConfigurationUpdatedTimestamp"].should.be.a(datetime)
|
||||||
|
alarm["AlarmConfigurationUpdatedTimestamp"].tzinfo.should.equal(tzutc())
|
||||||
|
alarm["ActionsEnabled"].should.be.ok
|
||||||
|
alarm["StateValue"].should.equal("OK")
|
||||||
|
alarm["StateReason"].should.equal("Unchecked: Initial alarm creation")
|
||||||
|
alarm["StateUpdatedTimestamp"].should.be.a(datetime)
|
||||||
|
alarm["StateUpdatedTimestamp"].tzinfo.should.equal(tzutc())
|
||||||
|
alarm["MetricName"].should.equal("5XXError")
|
||||||
|
alarm["Namespace"].should.equal("AWS/ApiGateway")
|
||||||
|
alarm["ExtendedStatistic"].should.equal("p90")
|
||||||
|
sorted(alarm["Dimensions"], key=itemgetter("Name")).should.equal(
|
||||||
|
sorted(
|
||||||
|
[
|
||||||
|
{"Name": "ApiName", "Value": "test-api"},
|
||||||
|
{"Name": "Stage", "Value": "default"},
|
||||||
|
],
|
||||||
|
key=itemgetter("Name"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
alarm["Period"].should.equal(60)
|
||||||
|
alarm["Unit"].should.equal("Seconds")
|
||||||
|
alarm["EvaluationPeriods"].should.equal(1)
|
||||||
|
alarm["DatapointsToAlarm"].should.equal(1)
|
||||||
|
alarm["Threshold"].should.equal(1.0)
|
||||||
|
alarm["ComparisonOperator"].should.equal("GreaterThanOrEqualToThreshold")
|
||||||
|
alarm["TreatMissingData"].should.equal("notBreaching")
|
||||||
|
alarm["EvaluateLowSampleCountPercentile"].should.equal("ignore")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
def test_put_metric_alarm_with_anomaly_detection():
|
||||||
|
# given
|
||||||
|
region_name = "eu-central-1"
|
||||||
|
client = boto3.client("cloudwatch", region_name=region_name)
|
||||||
|
alarm_name = "test-alarm"
|
||||||
|
metrics = [
|
||||||
|
{
|
||||||
|
"Id": "m1",
|
||||||
|
"ReturnData": True,
|
||||||
|
"MetricStat": {
|
||||||
|
"Metric": {
|
||||||
|
"MetricName": "CPUUtilization",
|
||||||
|
"Namespace": "AWS/EC2",
|
||||||
|
"Dimensions": [
|
||||||
|
{"Name": "instanceId", "Value": "i-1234567890abcdef0"}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"Stat": "Average",
|
||||||
|
"Period": 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "t1",
|
||||||
|
"ReturnData": False,
|
||||||
|
"Expression": "ANOMALY_DETECTION_BAND(m1, 3)",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# when
|
||||||
|
client.put_metric_alarm(
|
||||||
|
AlarmName=alarm_name,
|
||||||
|
ActionsEnabled=True,
|
||||||
|
Metrics=metrics,
|
||||||
|
EvaluationPeriods=2,
|
||||||
|
ComparisonOperator="GreaterThanOrEqualToThreshold",
|
||||||
|
ThresholdMetricId="t1",
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
alarms = client.describe_alarms(AlarmNames=[alarm_name])["MetricAlarms"]
|
||||||
|
alarms.should.have.length_of(1)
|
||||||
|
|
||||||
|
alarm = alarms[0]
|
||||||
|
alarm["AlarmName"].should.equal(alarm_name)
|
||||||
|
alarm["AlarmArn"].should.equal(
|
||||||
|
f"arn:aws:cloudwatch:{region_name}:{ACCOUNT_ID}:alarm:{alarm_name}"
|
||||||
|
)
|
||||||
|
alarm["AlarmConfigurationUpdatedTimestamp"].should.be.a(datetime)
|
||||||
|
alarm["AlarmConfigurationUpdatedTimestamp"].tzinfo.should.equal(tzutc())
|
||||||
|
alarm["StateValue"].should.equal("OK")
|
||||||
|
alarm["StateReason"].should.equal("Unchecked: Initial alarm creation")
|
||||||
|
alarm["StateUpdatedTimestamp"].should.be.a(datetime)
|
||||||
|
alarm["StateUpdatedTimestamp"].tzinfo.should.equal(tzutc())
|
||||||
|
alarm["EvaluationPeriods"].should.equal(2)
|
||||||
|
alarm["ComparisonOperator"].should.equal("GreaterThanOrEqualToThreshold")
|
||||||
|
alarm["Metrics"].should.equal(metrics)
|
||||||
|
alarm["ThresholdMetricId"].should.equal("t1")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
def test_put_metric_alarm_error_extended_statistic():
|
||||||
|
# given
|
||||||
|
region_name = "eu-central-1"
|
||||||
|
client = boto3.client("cloudwatch", region_name=region_name)
|
||||||
|
alarm_name = "test-alarm"
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(ClientError) as e:
|
||||||
|
client.put_metric_alarm(
|
||||||
|
AlarmName=alarm_name,
|
||||||
|
ActionsEnabled=True,
|
||||||
|
MetricName="5XXError",
|
||||||
|
Namespace="AWS/ApiGateway",
|
||||||
|
ExtendedStatistic="90",
|
||||||
|
Dimensions=[
|
||||||
|
{"Name": "ApiName", "Value": "test-api"},
|
||||||
|
{"Name": "Stage", "Value": "default"},
|
||||||
|
],
|
||||||
|
Period=60,
|
||||||
|
Unit="Seconds",
|
||||||
|
EvaluationPeriods=1,
|
||||||
|
DatapointsToAlarm=1,
|
||||||
|
Threshold=1.0,
|
||||||
|
ComparisonOperator="GreaterThanOrEqualToThreshold",
|
||||||
|
TreatMissingData="notBreaching",
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
ex = e.value
|
||||||
|
ex.operation_name.should.equal("PutMetricAlarm")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("InvalidParameterValue")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
"The value 90 for parameter ExtendedStatistic is not supported."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
def test_put_metric_alarm_error_evaluate_low_sample_count_percentile():
|
||||||
|
# given
|
||||||
|
region_name = "eu-central-1"
|
||||||
|
client = boto3.client("cloudwatch", region_name=region_name)
|
||||||
|
alarm_name = "test-alarm"
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(ClientError) as e:
|
||||||
|
client.put_metric_alarm(
|
||||||
|
AlarmName=alarm_name,
|
||||||
|
ActionsEnabled=True,
|
||||||
|
MetricName="5XXError",
|
||||||
|
Namespace="AWS/ApiGateway",
|
||||||
|
ExtendedStatistic="p90",
|
||||||
|
Dimensions=[
|
||||||
|
{"Name": "ApiName", "Value": "test-api"},
|
||||||
|
{"Name": "Stage", "Value": "default"},
|
||||||
|
],
|
||||||
|
Period=60,
|
||||||
|
Unit="Seconds",
|
||||||
|
EvaluationPeriods=1,
|
||||||
|
DatapointsToAlarm=1,
|
||||||
|
Threshold=1.0,
|
||||||
|
ComparisonOperator="GreaterThanOrEqualToThreshold",
|
||||||
|
TreatMissingData="notBreaching",
|
||||||
|
EvaluateLowSampleCountPercentile="unknown",
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
ex = e.value
|
||||||
|
ex.operation_name.should.equal("PutMetricAlarm")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("ValidationError")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
"Option unknown is not supported. "
|
||||||
|
"Supported options for parameter EvaluateLowSampleCountPercentile are evaluate and ignore."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
def test_list_tags_for_resource():
|
||||||
|
# given
|
||||||
|
client = boto3.client("cloudwatch", region_name="eu-central-1")
|
||||||
|
alarm_name = "test-alarm"
|
||||||
|
client.put_metric_alarm(
|
||||||
|
AlarmName=alarm_name,
|
||||||
|
AlarmDescription="test alarm",
|
||||||
|
ActionsEnabled=True,
|
||||||
|
MetricName="5XXError",
|
||||||
|
Namespace="AWS/ApiGateway",
|
||||||
|
Statistic="Sum",
|
||||||
|
Dimensions=[
|
||||||
|
{"Name": "ApiName", "Value": "test-api"},
|
||||||
|
{"Name": "Stage", "Value": "default"},
|
||||||
|
],
|
||||||
|
Period=60,
|
||||||
|
Unit="Seconds",
|
||||||
|
EvaluationPeriods=1,
|
||||||
|
DatapointsToAlarm=1,
|
||||||
|
Threshold=1.0,
|
||||||
|
ComparisonOperator="GreaterThanOrEqualToThreshold",
|
||||||
|
Tags=[{"Key": "key-1", "Value": "value-1"}],
|
||||||
|
)
|
||||||
|
arn = client.describe_alarms(AlarmNames=[alarm_name])["MetricAlarms"][0]["AlarmArn"]
|
||||||
|
|
||||||
|
# when
|
||||||
|
response = client.list_tags_for_resource(ResourceARN=arn)
|
||||||
|
|
||||||
|
# then
|
||||||
|
response["Tags"].should.equal([{"Key": "key-1", "Value": "value-1"}])
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
def test_list_tags_for_resource_with_unknown_resource():
|
||||||
|
# given
|
||||||
|
region_name = "eu-central-1"
|
||||||
|
client = boto3.client("cloudwatch", region_name=region_name)
|
||||||
|
|
||||||
|
# when
|
||||||
|
response = client.list_tags_for_resource(
|
||||||
|
ResourceARN=make_arn_for_alarm(
|
||||||
|
region=region_name, account_id=ACCOUNT_ID, alarm_name="unknown"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
response["Tags"].should.be.empty
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
def test_tag_resource():
|
||||||
|
# given
|
||||||
|
client = boto3.client("cloudwatch", region_name="eu-central-1")
|
||||||
|
alarm_name = "test-alarm"
|
||||||
|
client.put_metric_alarm(
|
||||||
|
AlarmName=alarm_name,
|
||||||
|
AlarmDescription="test alarm",
|
||||||
|
ActionsEnabled=True,
|
||||||
|
MetricName="5XXError",
|
||||||
|
Namespace="AWS/ApiGateway",
|
||||||
|
Statistic="Sum",
|
||||||
|
Dimensions=[
|
||||||
|
{"Name": "ApiName", "Value": "test-api"},
|
||||||
|
{"Name": "Stage", "Value": "default"},
|
||||||
|
],
|
||||||
|
Period=60,
|
||||||
|
Unit="Seconds",
|
||||||
|
EvaluationPeriods=1,
|
||||||
|
DatapointsToAlarm=1,
|
||||||
|
Threshold=1.0,
|
||||||
|
ComparisonOperator="GreaterThanOrEqualToThreshold",
|
||||||
|
Tags=[{"Key": "key-1", "Value": "value-1"}],
|
||||||
|
)
|
||||||
|
arn = client.describe_alarms(AlarmNames=[alarm_name])["MetricAlarms"][0]["AlarmArn"]
|
||||||
|
|
||||||
|
# when
|
||||||
|
client.tag_resource(ResourceARN=arn, Tags=[{"Key": "key-2", "Value": "value-2"}])
|
||||||
|
|
||||||
|
# then
|
||||||
|
response = client.list_tags_for_resource(ResourceARN=arn)
|
||||||
|
sorted(response["Tags"], key=itemgetter("Key")).should.equal(
|
||||||
|
sorted(
|
||||||
|
[
|
||||||
|
{"Key": "key-1", "Value": "value-1"},
|
||||||
|
{"Key": "key-2", "Value": "value-2"},
|
||||||
|
],
|
||||||
|
key=itemgetter("Key"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
def test_tag_resource_error_not_exists():
|
||||||
|
# given
|
||||||
|
region_name = "eu-central-1"
|
||||||
|
client = boto3.client("cloudwatch", region_name=region_name)
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(ClientError) as e:
|
||||||
|
client.tag_resource(
|
||||||
|
ResourceARN=make_arn_for_alarm(
|
||||||
|
region=region_name, account_id=ACCOUNT_ID, alarm_name="unknown"
|
||||||
|
),
|
||||||
|
Tags=[{"Key": "key-1", "Value": "value-1"},],
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
ex = e.value
|
||||||
|
ex.operation_name.should.equal("TagResource")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
|
||||||
|
ex.response["Error"]["Code"].should.contain("ResourceNotFoundException")
|
||||||
|
ex.response["Error"]["Message"].should.equal("Unknown")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
def test_untag_resource():
|
||||||
|
# given
|
||||||
|
client = boto3.client("cloudwatch", region_name="eu-central-1")
|
||||||
|
alarm_name = "test-alarm"
|
||||||
|
client.put_metric_alarm(
|
||||||
|
AlarmName=alarm_name,
|
||||||
|
AlarmDescription="test alarm",
|
||||||
|
ActionsEnabled=True,
|
||||||
|
MetricName="5XXError",
|
||||||
|
Namespace="AWS/ApiGateway",
|
||||||
|
Statistic="Sum",
|
||||||
|
Dimensions=[
|
||||||
|
{"Name": "ApiName", "Value": "test-api"},
|
||||||
|
{"Name": "Stage", "Value": "default"},
|
||||||
|
],
|
||||||
|
Period=60,
|
||||||
|
Unit="Seconds",
|
||||||
|
EvaluationPeriods=1,
|
||||||
|
DatapointsToAlarm=1,
|
||||||
|
Threshold=1.0,
|
||||||
|
ComparisonOperator="GreaterThanOrEqualToThreshold",
|
||||||
|
Tags=[
|
||||||
|
{"Key": "key-1", "Value": "value-1"},
|
||||||
|
{"Key": "key-2", "Value": "value-2"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
arn = client.describe_alarms(AlarmNames=[alarm_name])["MetricAlarms"][0]["AlarmArn"]
|
||||||
|
|
||||||
|
# when
|
||||||
|
client.untag_resource(ResourceARN=arn, TagKeys=["key-2"])
|
||||||
|
|
||||||
|
# then
|
||||||
|
response = client.list_tags_for_resource(ResourceARN=arn)
|
||||||
|
response["Tags"].should.equal([{"Key": "key-1", "Value": "value-1"}])
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
def test_untag_resource_error_not_exists():
|
||||||
|
# given
|
||||||
|
region_name = "eu-central-1"
|
||||||
|
client = boto3.client("cloudwatch", region_name=region_name)
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(ClientError) as e:
|
||||||
|
client.untag_resource(
|
||||||
|
ResourceARN=make_arn_for_alarm(
|
||||||
|
region=region_name, account_id=ACCOUNT_ID, alarm_name="unknown"
|
||||||
|
),
|
||||||
|
TagKeys=["key-1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
ex = e.value
|
||||||
|
ex.operation_name.should.equal("UntagResource")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
|
||||||
|
ex.response["Error"]["Code"].should.contain("ResourceNotFoundException")
|
||||||
|
ex.response["Error"]["Message"].should.equal("Unknown")
|
||||||
|
@ -290,7 +290,7 @@ def test_ami_filters():
|
|||||||
|
|
||||||
amis_by_architecture = conn.get_all_images(filters={"architecture": "x86_64"})
|
amis_by_architecture = conn.get_all_images(filters={"architecture": "x86_64"})
|
||||||
set([ami.id for ami in amis_by_architecture]).should.contain(imageB.id)
|
set([ami.id for ami in amis_by_architecture]).should.contain(imageB.id)
|
||||||
len(amis_by_architecture).should.equal(38)
|
len(amis_by_architecture).should.equal(39)
|
||||||
|
|
||||||
amis_by_kernel = conn.get_all_images(filters={"kernel-id": "k-abcd1234"})
|
amis_by_kernel = conn.get_all_images(filters={"kernel-id": "k-abcd1234"})
|
||||||
set([ami.id for ami in amis_by_kernel]).should.equal(set([imageB.id]))
|
set([ami.id for ami in amis_by_kernel]).should.equal(set([imageB.id]))
|
||||||
@ -312,14 +312,14 @@ def test_ami_filters():
|
|||||||
ami_ids_by_state = [ami.id for ami in amis_by_state]
|
ami_ids_by_state = [ami.id for ami in amis_by_state]
|
||||||
ami_ids_by_state.should.contain(imageA.id)
|
ami_ids_by_state.should.contain(imageA.id)
|
||||||
ami_ids_by_state.should.contain(imageB.id)
|
ami_ids_by_state.should.contain(imageB.id)
|
||||||
len(amis_by_state).should.equal(39)
|
len(amis_by_state).should.equal(40)
|
||||||
|
|
||||||
amis_by_name = conn.get_all_images(filters={"name": imageA.name})
|
amis_by_name = conn.get_all_images(filters={"name": imageA.name})
|
||||||
set([ami.id for ami in amis_by_name]).should.equal(set([imageA.id]))
|
set([ami.id for ami in amis_by_name]).should.equal(set([imageA.id]))
|
||||||
|
|
||||||
amis_by_public = conn.get_all_images(filters={"is-public": "true"})
|
amis_by_public = conn.get_all_images(filters={"is-public": "true"})
|
||||||
set([ami.id for ami in amis_by_public]).should.contain(imageB.id)
|
set([ami.id for ami in amis_by_public]).should.contain(imageB.id)
|
||||||
len(amis_by_public).should.equal(38)
|
len(amis_by_public).should.equal(39)
|
||||||
|
|
||||||
amis_by_nonpublic = conn.get_all_images(filters={"is-public": "false"})
|
amis_by_nonpublic = conn.get_all_images(filters={"is-public": "false"})
|
||||||
set([ami.id for ami in amis_by_nonpublic]).should.contain(imageA.id)
|
set([ami.id for ami in amis_by_nonpublic]).should.contain(imageA.id)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user