Merge remote-tracking branch 'spulec/master' into improve_coverage
This commit is contained in:
commit
d680b1e025
45 changed files with 2446 additions and 159 deletions
|
|
@ -73,6 +73,7 @@ mock_kms = lazy_load(".kms", "mock_kms")
|
|||
mock_kms_deprecated = lazy_load(".kms", "mock_kms_deprecated")
|
||||
mock_logs = lazy_load(".logs", "mock_logs")
|
||||
mock_logs_deprecated = lazy_load(".logs", "mock_logs_deprecated")
|
||||
mock_managedblockchain = lazy_load(".managedblockchain", "mock_managedblockchain")
|
||||
mock_opsworks = lazy_load(".opsworks", "mock_opsworks")
|
||||
mock_opsworks_deprecated = lazy_load(".opsworks", "mock_opsworks_deprecated")
|
||||
mock_organizations = lazy_load(".organizations", "mock_organizations")
|
||||
|
|
|
|||
|
|
@ -56,8 +56,10 @@ class Deployment(BaseModel, dict):
|
|||
|
||||
|
||||
class IntegrationResponse(BaseModel, dict):
|
||||
def __init__(self, status_code, selection_pattern=None):
|
||||
self["responseTemplates"] = {"application/json": None}
|
||||
def __init__(self, status_code, selection_pattern=None, response_templates=None):
|
||||
if response_templates is None:
|
||||
response_templates = {"application/json": None}
|
||||
self["responseTemplates"] = response_templates
|
||||
self["statusCode"] = status_code
|
||||
if selection_pattern:
|
||||
self["selectionPattern"] = selection_pattern
|
||||
|
|
@ -72,8 +74,14 @@ class Integration(BaseModel, dict):
|
|||
self["requestTemplates"] = request_templates
|
||||
self["integrationResponses"] = {"200": IntegrationResponse(200)}
|
||||
|
||||
def create_integration_response(self, status_code, selection_pattern):
|
||||
integration_response = IntegrationResponse(status_code, selection_pattern)
|
||||
def create_integration_response(
|
||||
self, status_code, selection_pattern, response_templates
|
||||
):
|
||||
if response_templates == {}:
|
||||
response_templates = None
|
||||
integration_response = IntegrationResponse(
|
||||
status_code, selection_pattern, response_templates
|
||||
)
|
||||
self["integrationResponses"][status_code] = integration_response
|
||||
return integration_response
|
||||
|
||||
|
|
@ -956,7 +964,7 @@ class APIGatewayBackend(BaseBackend):
|
|||
raise InvalidRequestInput()
|
||||
integration = self.get_integration(function_id, resource_id, method_type)
|
||||
integration_response = integration.create_integration_response(
|
||||
status_code, selection_pattern
|
||||
status_code, selection_pattern, response_templates
|
||||
)
|
||||
return integration_response
|
||||
|
||||
|
|
|
|||
|
|
@ -419,11 +419,8 @@ class FakeAutoScalingGroup(BaseModel):
|
|||
curr_instance_count = len(self.active_instances())
|
||||
|
||||
if self.desired_capacity == curr_instance_count:
|
||||
self.autoscaling_backend.update_attached_elbs(self.name)
|
||||
self.autoscaling_backend.update_attached_target_groups(self.name)
|
||||
return
|
||||
|
||||
if self.desired_capacity > curr_instance_count:
|
||||
pass # Nothing to do here
|
||||
elif self.desired_capacity > curr_instance_count:
|
||||
# Need more instances
|
||||
count_needed = int(self.desired_capacity) - int(curr_instance_count)
|
||||
|
||||
|
|
@ -447,6 +444,7 @@ class FakeAutoScalingGroup(BaseModel):
|
|||
self.instance_states = list(
|
||||
set(self.instance_states) - set(instances_to_remove)
|
||||
)
|
||||
if self.name in self.autoscaling_backend.autoscaling_groups:
|
||||
self.autoscaling_backend.update_attached_elbs(self.name)
|
||||
self.autoscaling_backend.update_attached_target_groups(self.name)
|
||||
|
||||
|
|
@ -695,6 +693,7 @@ class AutoScalingBackend(BaseBackend):
|
|||
)
|
||||
group.instance_states.extend(new_instances)
|
||||
self.update_attached_elbs(group.name)
|
||||
self.update_attached_target_groups(group.name)
|
||||
|
||||
def set_instance_health(
|
||||
self, instance_id, health_status, should_respect_grace_period
|
||||
|
|
@ -938,8 +937,7 @@ class AutoScalingBackend(BaseBackend):
|
|||
standby_instances.append(instance_state)
|
||||
if should_decrement:
|
||||
group.desired_capacity = group.desired_capacity - len(instance_ids)
|
||||
else:
|
||||
group.set_desired_capacity(group.desired_capacity)
|
||||
group.set_desired_capacity(group.desired_capacity)
|
||||
return standby_instances, original_size, group.desired_capacity
|
||||
|
||||
def exit_standby_instances(self, group_name, instance_ids):
|
||||
|
|
@ -951,6 +949,7 @@ class AutoScalingBackend(BaseBackend):
|
|||
instance_state.lifecycle_state = "InService"
|
||||
standby_instances.append(instance_state)
|
||||
group.desired_capacity = group.desired_capacity + len(instance_ids)
|
||||
group.set_desired_capacity(group.desired_capacity)
|
||||
return standby_instances, original_size, group.desired_capacity
|
||||
|
||||
def terminate_instance(self, instance_id, should_decrement):
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import time
|
|||
from collections import defaultdict
|
||||
import copy
|
||||
import datetime
|
||||
from gzip import GzipFile
|
||||
|
||||
import docker
|
||||
import docker.errors
|
||||
import hashlib
|
||||
|
|
@ -983,6 +985,28 @@ class LambdaBackend(BaseBackend):
|
|||
func = self._lambdas.get_arn(function_arn)
|
||||
return func.invoke(json.dumps(event), {}, {})
|
||||
|
||||
def send_log_event(
|
||||
self, function_arn, filter_name, log_group_name, log_stream_name, log_events
|
||||
):
|
||||
data = {
|
||||
"messageType": "DATA_MESSAGE",
|
||||
"owner": ACCOUNT_ID,
|
||||
"logGroup": log_group_name,
|
||||
"logStream": log_stream_name,
|
||||
"subscriptionFilters": [filter_name],
|
||||
"logEvents": log_events,
|
||||
}
|
||||
|
||||
output = io.BytesIO()
|
||||
with GzipFile(fileobj=output, mode="w") as f:
|
||||
f.write(json.dumps(data, separators=(",", ":")).encode("utf-8"))
|
||||
payload_gz_encoded = base64.b64encode(output.getvalue()).decode("utf-8")
|
||||
|
||||
event = {"awslogs": {"data": payload_gz_encoded}}
|
||||
|
||||
func = self._lambdas.get_arn(function_arn)
|
||||
return func.invoke(json.dumps(event), {}, {})
|
||||
|
||||
def list_tags(self, resource):
|
||||
return self.get_function_by_arn(resource).tags
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ BACKENDS = {
|
|||
"kms": ("kms", "kms_backends"),
|
||||
"lambda": ("awslambda", "lambda_backends"),
|
||||
"logs": ("logs", "logs_backends"),
|
||||
"managedblockchain": ("managedblockchain", "managedblockchain_backends"),
|
||||
"moto_api": ("core", "moto_api_backends"),
|
||||
"opsworks": ("opsworks", "opsworks_backends"),
|
||||
"organizations": ("organizations", "organizations_backends"),
|
||||
|
|
|
|||
|
|
@ -316,6 +316,12 @@ class Table(BaseModel):
|
|||
}
|
||||
self.set_stream_specification(streams)
|
||||
self.lambda_event_source_mappings = {}
|
||||
self.continuous_backups = {
|
||||
"ContinuousBackupsStatus": "ENABLED", # One of 'ENABLED'|'DISABLED', it's enabled by default
|
||||
"PointInTimeRecoveryDescription": {
|
||||
"PointInTimeRecoveryStatus": "DISABLED" # One of 'ENABLED'|'DISABLED'
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(
|
||||
|
|
@ -1282,6 +1288,33 @@ class DynamoDBBackend(BaseBackend):
|
|||
self.tables = original_table_state
|
||||
raise
|
||||
|
||||
def describe_continuous_backups(self, table_name):
|
||||
table = self.get_table(table_name)
|
||||
|
||||
return table.continuous_backups
|
||||
|
||||
def update_continuous_backups(self, table_name, point_in_time_spec):
|
||||
table = self.get_table(table_name)
|
||||
|
||||
if (
|
||||
point_in_time_spec["PointInTimeRecoveryEnabled"]
|
||||
and table.continuous_backups["PointInTimeRecoveryDescription"][
|
||||
"PointInTimeRecoveryStatus"
|
||||
]
|
||||
== "DISABLED"
|
||||
):
|
||||
table.continuous_backups["PointInTimeRecoveryDescription"] = {
|
||||
"PointInTimeRecoveryStatus": "ENABLED",
|
||||
"EarliestRestorableDateTime": unix_time(),
|
||||
"LatestRestorableDateTime": unix_time(),
|
||||
}
|
||||
elif not point_in_time_spec["PointInTimeRecoveryEnabled"]:
|
||||
table.continuous_backups["PointInTimeRecoveryDescription"] = {
|
||||
"PointInTimeRecoveryStatus": "DISABLED"
|
||||
}
|
||||
|
||||
return table.continuous_backups
|
||||
|
||||
######################
|
||||
# LIST of methods where the logic completely resides in responses.py
|
||||
# Duplicated here so that the implementation coverage script is aware
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
from abc import abstractmethod
|
||||
|
||||
from moto.dynamodb2.exceptions import IncorrectOperandType, IncorrectDataType
|
||||
from moto.dynamodb2.exceptions import (
|
||||
IncorrectOperandType,
|
||||
IncorrectDataType,
|
||||
ProvidedKeyDoesNotExist,
|
||||
)
|
||||
from moto.dynamodb2.models import DynamoType
|
||||
from moto.dynamodb2.models.dynamo_type import DDBTypeConversion, DDBType
|
||||
from moto.dynamodb2.parsing.ast_nodes import (
|
||||
|
|
@ -193,7 +197,18 @@ class AddExecutor(NodeExecutor):
|
|||
value_to_add = self.get_action_value()
|
||||
if isinstance(value_to_add, DynamoType):
|
||||
if value_to_add.is_set():
|
||||
current_string_set = self.get_item_at_end_of_path(item)
|
||||
try:
|
||||
current_string_set = self.get_item_at_end_of_path(item)
|
||||
except ProvidedKeyDoesNotExist:
|
||||
current_string_set = DynamoType({value_to_add.type: []})
|
||||
SetExecutor.set(
|
||||
item_part_to_modify_with_set=self.get_item_before_end_of_path(
|
||||
item
|
||||
),
|
||||
element_to_set=self.get_element_to_action(),
|
||||
value_to_set=current_string_set,
|
||||
expression_attribute_names=self.expression_attribute_names,
|
||||
)
|
||||
assert isinstance(current_string_set, DynamoType)
|
||||
if not current_string_set.type == value_to_add.type:
|
||||
raise IncorrectDataType()
|
||||
|
|
@ -204,7 +219,11 @@ class AddExecutor(NodeExecutor):
|
|||
else:
|
||||
current_string_set.value.append(value)
|
||||
elif value_to_add.type == DDBType.NUMBER:
|
||||
existing_value = self.get_item_at_end_of_path(item)
|
||||
try:
|
||||
existing_value = self.get_item_at_end_of_path(item)
|
||||
except ProvidedKeyDoesNotExist:
|
||||
existing_value = DynamoType({DDBType.NUMBER: "0"})
|
||||
|
||||
assert isinstance(existing_value, DynamoType)
|
||||
if not existing_value.type == DDBType.NUMBER:
|
||||
raise IncorrectDataType()
|
||||
|
|
|
|||
|
|
@ -919,3 +919,32 @@ class DynamoHandler(BaseResponse):
|
|||
)
|
||||
response = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}}
|
||||
return dynamo_json_dump(response)
|
||||
|
||||
def describe_continuous_backups(self):
|
||||
name = self.body["TableName"]
|
||||
|
||||
if self.dynamodb_backend.get_table(name) is None:
|
||||
return self.error(
|
||||
"com.amazonaws.dynamodb.v20111205#TableNotFoundException",
|
||||
"Table not found: {}".format(name),
|
||||
)
|
||||
|
||||
response = self.dynamodb_backend.describe_continuous_backups(name)
|
||||
|
||||
return json.dumps({"ContinuousBackupsDescription": response})
|
||||
|
||||
def update_continuous_backups(self):
|
||||
name = self.body["TableName"]
|
||||
point_in_time_spec = self.body["PointInTimeRecoverySpecification"]
|
||||
|
||||
if self.dynamodb_backend.get_table(name) is None:
|
||||
return self.error(
|
||||
"com.amazonaws.dynamodb.v20111205#TableNotFoundException",
|
||||
"Table not found: {}".format(name),
|
||||
)
|
||||
|
||||
response = self.dynamodb_backend.update_continuous_backups(
|
||||
name, point_in_time_spec
|
||||
)
|
||||
|
||||
return json.dumps({"ContinuousBackupsDescription": response})
|
||||
|
|
|
|||
|
|
@ -560,8 +560,10 @@ class Instance(TaggedEC2Resource, BotoInstance):
|
|||
# worst case we'll get IP address exaustion... rarely
|
||||
pass
|
||||
|
||||
def add_block_device(self, size, device_path):
|
||||
volume = self.ec2_backend.create_volume(size, self.region_name)
|
||||
def add_block_device(self, size, device_path, snapshot_id=None, encrypted=False):
|
||||
volume = self.ec2_backend.create_volume(
|
||||
size, self.region_name, snapshot_id, encrypted
|
||||
)
|
||||
self.ec2_backend.attach_volume(volume.id, self.id, device_path)
|
||||
|
||||
def setup_defaults(self):
|
||||
|
|
@ -891,8 +893,12 @@ class InstanceBackend(object):
|
|||
new_instance.add_tags(instance_tags)
|
||||
if "block_device_mappings" in kwargs:
|
||||
for block_device in kwargs["block_device_mappings"]:
|
||||
device_name = block_device["DeviceName"]
|
||||
volume_size = block_device["Ebs"].get("VolumeSize")
|
||||
snapshot_id = block_device["Ebs"].get("SnapshotId")
|
||||
encrypted = block_device["Ebs"].get("Encrypted", False)
|
||||
new_instance.add_block_device(
|
||||
block_device["Ebs"]["VolumeSize"], block_device["DeviceName"]
|
||||
volume_size, device_name, snapshot_id, encrypted
|
||||
)
|
||||
else:
|
||||
new_instance.setup_defaults()
|
||||
|
|
|
|||
|
|
@ -4,10 +4,16 @@ from boto.ec2.instancetype import InstanceType
|
|||
from moto.autoscaling import autoscaling_backends
|
||||
from moto.core.responses import BaseResponse
|
||||
from moto.core.utils import camelcase_to_underscores
|
||||
from moto.ec2.utils import filters_from_querystring, dict_from_querystring
|
||||
from moto.ec2.exceptions import MissingParameterError
|
||||
from moto.ec2.utils import (
|
||||
filters_from_querystring,
|
||||
dict_from_querystring,
|
||||
)
|
||||
from moto.elbv2 import elbv2_backends
|
||||
from moto.core import ACCOUNT_ID
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
class InstanceResponse(BaseResponse):
|
||||
def describe_instances(self):
|
||||
|
|
@ -44,40 +50,31 @@ class InstanceResponse(BaseResponse):
|
|||
owner_id = self._get_param("OwnerId")
|
||||
user_data = self._get_param("UserData")
|
||||
security_group_names = self._get_multi_param("SecurityGroup")
|
||||
security_group_ids = self._get_multi_param("SecurityGroupId")
|
||||
nics = dict_from_querystring("NetworkInterface", self.querystring)
|
||||
instance_type = self._get_param("InstanceType", if_none="m1.small")
|
||||
placement = self._get_param("Placement.AvailabilityZone")
|
||||
subnet_id = self._get_param("SubnetId")
|
||||
private_ip = self._get_param("PrivateIpAddress")
|
||||
associate_public_ip = self._get_param("AssociatePublicIpAddress")
|
||||
key_name = self._get_param("KeyName")
|
||||
ebs_optimized = self._get_param("EbsOptimized") or False
|
||||
instance_initiated_shutdown_behavior = self._get_param(
|
||||
"InstanceInitiatedShutdownBehavior"
|
||||
)
|
||||
tags = self._parse_tag_specification("TagSpecification")
|
||||
region_name = self.region
|
||||
kwargs = {
|
||||
"instance_type": self._get_param("InstanceType", if_none="m1.small"),
|
||||
"placement": self._get_param("Placement.AvailabilityZone"),
|
||||
"region_name": self.region,
|
||||
"subnet_id": self._get_param("SubnetId"),
|
||||
"owner_id": owner_id,
|
||||
"key_name": self._get_param("KeyName"),
|
||||
"security_group_ids": self._get_multi_param("SecurityGroupId"),
|
||||
"nics": dict_from_querystring("NetworkInterface", self.querystring),
|
||||
"private_ip": self._get_param("PrivateIpAddress"),
|
||||
"associate_public_ip": self._get_param("AssociatePublicIpAddress"),
|
||||
"tags": self._parse_tag_specification("TagSpecification"),
|
||||
"ebs_optimized": self._get_param("EbsOptimized") or False,
|
||||
"instance_initiated_shutdown_behavior": self._get_param(
|
||||
"InstanceInitiatedShutdownBehavior"
|
||||
),
|
||||
}
|
||||
|
||||
mappings = self._parse_block_device_mapping()
|
||||
if mappings:
|
||||
kwargs["block_device_mappings"] = mappings
|
||||
|
||||
if self.is_not_dryrun("RunInstance"):
|
||||
new_reservation = self.ec2_backend.add_instances(
|
||||
image_id,
|
||||
min_count,
|
||||
user_data,
|
||||
security_group_names,
|
||||
instance_type=instance_type,
|
||||
placement=placement,
|
||||
region_name=region_name,
|
||||
subnet_id=subnet_id,
|
||||
owner_id=owner_id,
|
||||
key_name=key_name,
|
||||
security_group_ids=security_group_ids,
|
||||
nics=nics,
|
||||
private_ip=private_ip,
|
||||
associate_public_ip=associate_public_ip,
|
||||
tags=tags,
|
||||
ebs_optimized=ebs_optimized,
|
||||
instance_initiated_shutdown_behavior=instance_initiated_shutdown_behavior,
|
||||
image_id, min_count, user_data, security_group_names, **kwargs
|
||||
)
|
||||
|
||||
template = self.response_template(EC2_RUN_INSTANCES)
|
||||
|
|
@ -272,6 +269,58 @@ class InstanceResponse(BaseResponse):
|
|||
)
|
||||
return EC2_MODIFY_INSTANCE_ATTRIBUTE
|
||||
|
||||
def _parse_block_device_mapping(self):
|
||||
device_mappings = self._get_list_prefix("BlockDeviceMapping")
|
||||
mappings = []
|
||||
for device_mapping in device_mappings:
|
||||
self._validate_block_device_mapping(device_mapping)
|
||||
device_template = deepcopy(BLOCK_DEVICE_MAPPING_TEMPLATE)
|
||||
device_template["VirtualName"] = device_mapping.get("virtual_name")
|
||||
device_template["DeviceName"] = device_mapping.get("device_name")
|
||||
device_template["Ebs"]["SnapshotId"] = device_mapping.get(
|
||||
"ebs._snapshot_id"
|
||||
)
|
||||
device_template["Ebs"]["VolumeSize"] = device_mapping.get(
|
||||
"ebs._volume_size"
|
||||
)
|
||||
device_template["Ebs"]["DeleteOnTermination"] = device_mapping.get(
|
||||
"ebs._delete_on_termination", False
|
||||
)
|
||||
device_template["Ebs"]["VolumeType"] = device_mapping.get(
|
||||
"ebs._volume_type"
|
||||
)
|
||||
device_template["Ebs"]["Iops"] = device_mapping.get("ebs._iops")
|
||||
device_template["Ebs"]["Encrypted"] = device_mapping.get(
|
||||
"ebs._encrypted", False
|
||||
)
|
||||
mappings.append(device_template)
|
||||
|
||||
return mappings
|
||||
|
||||
@staticmethod
|
||||
def _validate_block_device_mapping(device_mapping):
|
||||
|
||||
if not any(mapping for mapping in device_mapping if mapping.startswith("ebs.")):
|
||||
raise MissingParameterError("ebs")
|
||||
if (
|
||||
"ebs._volume_size" not in device_mapping
|
||||
and "ebs._snapshot_id" not in device_mapping
|
||||
):
|
||||
raise MissingParameterError("size or snapshotId")
|
||||
|
||||
|
||||
BLOCK_DEVICE_MAPPING_TEMPLATE = {
|
||||
"VirtualName": None,
|
||||
"DeviceName": None,
|
||||
"Ebs": {
|
||||
"SnapshotId": None,
|
||||
"VolumeSize": None,
|
||||
"DeleteOnTermination": None,
|
||||
"VolumeType": None,
|
||||
"Iops": None,
|
||||
"Encrypted": None,
|
||||
},
|
||||
}
|
||||
|
||||
EC2_RUN_INSTANCES = (
|
||||
"""<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
|
||||
|
|
|
|||
|
|
@ -857,8 +857,30 @@ class IoTBackend(BaseBackend):
|
|||
del self.thing_groups[thing_group.arn]
|
||||
|
||||
def list_thing_groups(self, parent_group, name_prefix_filter, recursive):
|
||||
thing_groups = self.thing_groups.values()
|
||||
return thing_groups
|
||||
if recursive is None:
|
||||
recursive = True
|
||||
if name_prefix_filter is None:
|
||||
name_prefix_filter = ""
|
||||
if parent_group and parent_group not in [
|
||||
_.thing_group_name for _ in self.thing_groups.values()
|
||||
]:
|
||||
raise ResourceNotFoundException()
|
||||
thing_groups = [
|
||||
_ for _ in self.thing_groups.values() if _.parent_group_name == parent_group
|
||||
]
|
||||
if recursive:
|
||||
for g in thing_groups:
|
||||
thing_groups.extend(
|
||||
self.list_thing_groups(
|
||||
parent_group=g.thing_group_name,
|
||||
name_prefix_filter=None,
|
||||
recursive=False,
|
||||
)
|
||||
)
|
||||
# thing_groups = groups_to_process.values()
|
||||
return [
|
||||
_ for _ in thing_groups if _.thing_group_name.startswith(name_prefix_filter)
|
||||
]
|
||||
|
||||
def update_thing_group(
|
||||
self, thing_group_name, thing_group_properties, expected_version
|
||||
|
|
|
|||
|
|
@ -535,7 +535,7 @@ class IoTResponse(BaseResponse):
|
|||
# max_results = self._get_int_param("maxResults")
|
||||
parent_group = self._get_param("parentGroup")
|
||||
name_prefix_filter = self._get_param("namePrefixFilter")
|
||||
recursive = self._get_param("recursive")
|
||||
recursive = self._get_bool_param("recursive")
|
||||
thing_groups = self.iot_backend.list_thing_groups(
|
||||
parent_group=parent_group,
|
||||
name_prefix_filter=name_prefix_filter,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ class LogsClientError(JsonRESTError):
|
|||
|
||||
|
||||
class ResourceNotFoundException(LogsClientError):
|
||||
def __init__(self):
|
||||
def __init__(self, msg=None):
|
||||
self.code = 400
|
||||
super(ResourceNotFoundException, self).__init__(
|
||||
"ResourceNotFoundException", "The specified resource does not exist"
|
||||
"ResourceNotFoundException", msg or "The specified log group does not exist"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -28,3 +28,11 @@ class ResourceAlreadyExistsException(LogsClientError):
|
|||
super(ResourceAlreadyExistsException, self).__init__(
|
||||
"ResourceAlreadyExistsException", "The specified log group already exists"
|
||||
)
|
||||
|
||||
|
||||
class LimitExceededException(LogsClientError):
|
||||
def __init__(self):
|
||||
self.code = 400
|
||||
super(LimitExceededException, self).__init__(
|
||||
"LimitExceededException", "Resource limit exceeded."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from .exceptions import (
|
|||
ResourceNotFoundException,
|
||||
ResourceAlreadyExistsException,
|
||||
InvalidParameterException,
|
||||
LimitExceededException,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -57,6 +58,8 @@ class LogStream:
|
|||
0 # I'm guessing this is token needed for sequenceToken by put_events
|
||||
)
|
||||
self.events = []
|
||||
self.destination_arn = None
|
||||
self.filter_name = None
|
||||
|
||||
self.__class__._log_ids += 1
|
||||
|
||||
|
|
@ -97,11 +100,32 @@ class LogStream:
|
|||
self.lastIngestionTime = int(unix_time_millis())
|
||||
# TODO: make this match AWS if possible
|
||||
self.storedBytes += sum([len(log_event["message"]) for log_event in log_events])
|
||||
self.events += [
|
||||
events = [
|
||||
LogEvent(self.lastIngestionTime, log_event) for log_event in log_events
|
||||
]
|
||||
self.events += events
|
||||
self.uploadSequenceToken += 1
|
||||
|
||||
if self.destination_arn and self.destination_arn.split(":")[2] == "lambda":
|
||||
from moto.awslambda import lambda_backends # due to circular dependency
|
||||
|
||||
lambda_log_events = [
|
||||
{
|
||||
"id": event.eventId,
|
||||
"timestamp": event.timestamp,
|
||||
"message": event.message,
|
||||
}
|
||||
for event in events
|
||||
]
|
||||
|
||||
lambda_backends[self.region].send_log_event(
|
||||
self.destination_arn,
|
||||
self.filter_name,
|
||||
log_group_name,
|
||||
log_stream_name,
|
||||
lambda_log_events,
|
||||
)
|
||||
|
||||
return "{:056d}".format(self.uploadSequenceToken)
|
||||
|
||||
def get_log_events(
|
||||
|
|
@ -227,6 +251,7 @@ class LogGroup:
|
|||
self.retention_in_days = kwargs.get(
|
||||
"RetentionInDays"
|
||||
) # AWS defaults to Never Expire for log group retention
|
||||
self.subscription_filters = []
|
||||
|
||||
def create_log_stream(self, log_stream_name):
|
||||
if log_stream_name in self.streams:
|
||||
|
|
@ -386,6 +411,48 @@ class LogGroup:
|
|||
k: v for (k, v) in self.tags.items() if k not in tags_to_remove
|
||||
}
|
||||
|
||||
def describe_subscription_filters(self):
|
||||
return self.subscription_filters
|
||||
|
||||
def put_subscription_filter(
|
||||
self, filter_name, filter_pattern, destination_arn, role_arn
|
||||
):
|
||||
creation_time = int(unix_time_millis())
|
||||
|
||||
# only one subscription filter can be associated with a log group
|
||||
if self.subscription_filters:
|
||||
if self.subscription_filters[0]["filterName"] == filter_name:
|
||||
creation_time = self.subscription_filters[0]["creationTime"]
|
||||
else:
|
||||
raise LimitExceededException
|
||||
|
||||
for stream in self.streams.values():
|
||||
stream.destination_arn = destination_arn
|
||||
stream.filter_name = filter_name
|
||||
|
||||
self.subscription_filters = [
|
||||
{
|
||||
"filterName": filter_name,
|
||||
"logGroupName": self.name,
|
||||
"filterPattern": filter_pattern,
|
||||
"destinationArn": destination_arn,
|
||||
"roleArn": role_arn,
|
||||
"distribution": "ByLogStream",
|
||||
"creationTime": creation_time,
|
||||
}
|
||||
]
|
||||
|
||||
def delete_subscription_filter(self, filter_name):
|
||||
if (
|
||||
not self.subscription_filters
|
||||
or self.subscription_filters[0]["filterName"] != filter_name
|
||||
):
|
||||
raise ResourceNotFoundException(
|
||||
"The specified subscription filter does not exist."
|
||||
)
|
||||
|
||||
self.subscription_filters = []
|
||||
|
||||
|
||||
class LogsBackend(BaseBackend):
|
||||
def __init__(self, region_name):
|
||||
|
|
@ -557,6 +624,46 @@ class LogsBackend(BaseBackend):
|
|||
log_group = self.groups[log_group_name]
|
||||
log_group.untag(tags)
|
||||
|
||||
def describe_subscription_filters(self, log_group_name):
|
||||
log_group = self.groups.get(log_group_name)
|
||||
|
||||
if not log_group:
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
return log_group.describe_subscription_filters()
|
||||
|
||||
def put_subscription_filter(
|
||||
self, log_group_name, filter_name, filter_pattern, destination_arn, role_arn
|
||||
):
|
||||
# TODO: support other destinations like Kinesis stream
|
||||
from moto.awslambda import lambda_backends # due to circular dependency
|
||||
|
||||
log_group = self.groups.get(log_group_name)
|
||||
|
||||
if not log_group:
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
lambda_func = lambda_backends[self.region_name].get_function(destination_arn)
|
||||
|
||||
# no specific permission check implemented
|
||||
if not lambda_func:
|
||||
raise InvalidParameterException(
|
||||
"Could not execute the lambda function. "
|
||||
"Make sure you have given CloudWatch Logs permission to execute your function."
|
||||
)
|
||||
|
||||
log_group.put_subscription_filter(
|
||||
filter_name, filter_pattern, destination_arn, role_arn
|
||||
)
|
||||
|
||||
def delete_subscription_filter(self, log_group_name, filter_name):
|
||||
log_group = self.groups.get(log_group_name)
|
||||
|
||||
if not log_group:
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
log_group.delete_subscription_filter(filter_name)
|
||||
|
||||
|
||||
logs_backends = {}
|
||||
for region in Session().get_available_regions("logs"):
|
||||
|
|
|
|||
|
|
@ -178,3 +178,33 @@ class LogsResponse(BaseResponse):
|
|||
tags = self._get_param("tags")
|
||||
self.logs_backend.untag_log_group(log_group_name, tags)
|
||||
return ""
|
||||
|
||||
def describe_subscription_filters(self):
|
||||
log_group_name = self._get_param("logGroupName")
|
||||
|
||||
subscription_filters = self.logs_backend.describe_subscription_filters(
|
||||
log_group_name
|
||||
)
|
||||
|
||||
return json.dumps({"subscriptionFilters": subscription_filters})
|
||||
|
||||
def put_subscription_filter(self):
|
||||
log_group_name = self._get_param("logGroupName")
|
||||
filter_name = self._get_param("filterName")
|
||||
filter_pattern = self._get_param("filterPattern")
|
||||
destination_arn = self._get_param("destinationArn")
|
||||
role_arn = self._get_param("roleArn")
|
||||
|
||||
self.logs_backend.put_subscription_filter(
|
||||
log_group_name, filter_name, filter_pattern, destination_arn, role_arn
|
||||
)
|
||||
|
||||
return ""
|
||||
|
||||
def delete_subscription_filter(self):
|
||||
log_group_name = self._get_param("logGroupName")
|
||||
filter_name = self._get_param("filterName")
|
||||
|
||||
self.logs_backend.delete_subscription_filter(log_group_name, filter_name)
|
||||
|
||||
return ""
|
||||
|
|
|
|||
9
moto/managedblockchain/__init__.py
Normal file
9
moto/managedblockchain/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from __future__ import unicode_literals
|
||||
from .models import managedblockchain_backends
|
||||
from ..core.models import base_decorator, deprecated_base_decorator
|
||||
|
||||
managedblockchain_backend = managedblockchain_backends["us-east-1"]
|
||||
mock_managedblockchain = base_decorator(managedblockchain_backends)
|
||||
mock_managedblockchain_deprecated = deprecated_base_decorator(
|
||||
managedblockchain_backends
|
||||
)
|
||||
27
moto/managedblockchain/exceptions.py
Normal file
27
moto/managedblockchain/exceptions.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
from __future__ import unicode_literals
|
||||
from moto.core.exceptions import RESTError
|
||||
|
||||
|
||||
class ManagedBlockchainClientError(RESTError):
|
||||
code = 400
|
||||
|
||||
|
||||
class BadRequestException(ManagedBlockchainClientError):
|
||||
def __init__(self, pretty_called_method, operation_error):
|
||||
super(BadRequestException, self).__init__(
|
||||
"BadRequestException",
|
||||
"An error occurred (BadRequestException) when calling the {0} operation: {1}".format(
|
||||
pretty_called_method, operation_error
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ResourceNotFoundException(ManagedBlockchainClientError):
|
||||
def __init__(self, pretty_called_method, operation_error):
|
||||
self.code = 404
|
||||
super(ResourceNotFoundException, self).__init__(
|
||||
"ResourceNotFoundException",
|
||||
"An error occurred (BadRequestException) when calling the {0} operation: {1}".format(
|
||||
pretty_called_method, operation_error
|
||||
),
|
||||
)
|
||||
176
moto/managedblockchain/models.py
Normal file
176
moto/managedblockchain/models.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
from boto3 import Session
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
|
||||
from .exceptions import BadRequestException, ResourceNotFoundException
|
||||
|
||||
from .utils import get_network_id, get_member_id
|
||||
|
||||
FRAMEWORKS = [
|
||||
"HYPERLEDGER_FABRIC",
|
||||
]
|
||||
|
||||
FRAMEWORKVERSIONS = [
|
||||
"1.2",
|
||||
]
|
||||
|
||||
EDITIONS = [
|
||||
"STARTER",
|
||||
"STANDARD",
|
||||
]
|
||||
|
||||
|
||||
class ManagedBlockchainNetwork(BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
id,
|
||||
name,
|
||||
framework,
|
||||
frameworkversion,
|
||||
frameworkconfiguration,
|
||||
voting_policy,
|
||||
member_configuration,
|
||||
region,
|
||||
description=None,
|
||||
):
|
||||
self.creationdate = datetime.datetime.utcnow()
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.framework = framework
|
||||
self.frameworkversion = frameworkversion
|
||||
self.frameworkconfiguration = frameworkconfiguration
|
||||
self.voting_policy = voting_policy
|
||||
self.member_configuration = member_configuration
|
||||
self.region = region
|
||||
|
||||
def to_dict(self):
|
||||
# Format for list_networks
|
||||
d = {
|
||||
"Id": self.id,
|
||||
"Name": self.name,
|
||||
"Framework": self.framework,
|
||||
"FrameworkVersion": self.frameworkversion,
|
||||
"Status": "AVAILABLE",
|
||||
"CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
|
||||
}
|
||||
if self.description is not None:
|
||||
d["Description"] = self.description
|
||||
return d
|
||||
|
||||
def get_format(self):
|
||||
# Format for get_networks
|
||||
frameworkattributes = {
|
||||
"Fabric": {
|
||||
"OrderingServiceEndpoint": "orderer.{0}.managedblockchain.{1}.amazonaws.com:30001".format(
|
||||
self.id.lower(), self.region
|
||||
),
|
||||
"Edition": self.frameworkconfiguration["Fabric"]["Edition"],
|
||||
}
|
||||
}
|
||||
|
||||
vpcendpointname = "com.amazonaws.{0}.managedblockchain.{1}".format(
|
||||
self.region, self.id.lower()
|
||||
)
|
||||
|
||||
d = {
|
||||
"Id": self.id,
|
||||
"Name": self.name,
|
||||
"Framework": self.framework,
|
||||
"FrameworkVersion": self.frameworkversion,
|
||||
"FrameworkAttributes": frameworkattributes,
|
||||
"VpcEndpointServiceName": vpcendpointname,
|
||||
"VotingPolicy": self.voting_policy,
|
||||
"Status": "AVAILABLE",
|
||||
"CreationDate": self.creationdate.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
|
||||
}
|
||||
if self.description is not None:
|
||||
d["Description"] = self.description
|
||||
return d
|
||||
|
||||
|
||||
class ManagedBlockchainBackend(BaseBackend):
|
||||
def __init__(self, region_name):
|
||||
self.networks = {}
|
||||
self.region_name = region_name
|
||||
|
||||
def reset(self):
|
||||
region_name = self.region_name
|
||||
self.__dict__ = {}
|
||||
self.__init__(region_name)
|
||||
|
||||
def create_network(
|
||||
self,
|
||||
name,
|
||||
framework,
|
||||
frameworkversion,
|
||||
frameworkconfiguration,
|
||||
voting_policy,
|
||||
member_configuration,
|
||||
description=None,
|
||||
):
|
||||
self.name = name
|
||||
self.framework = framework
|
||||
self.frameworkversion = frameworkversion
|
||||
self.frameworkconfiguration = frameworkconfiguration
|
||||
self.voting_policy = voting_policy
|
||||
self.member_configuration = member_configuration
|
||||
self.description = description
|
||||
|
||||
# Check framework
|
||||
if framework not in FRAMEWORKS:
|
||||
raise BadRequestException("CreateNetwork", "Invalid request body")
|
||||
|
||||
# Check framework version
|
||||
if frameworkversion not in FRAMEWORKVERSIONS:
|
||||
raise BadRequestException(
|
||||
"CreateNetwork",
|
||||
"Invalid version {0} requested for framework HYPERLEDGER_FABRIC".format(
|
||||
frameworkversion
|
||||
),
|
||||
)
|
||||
|
||||
# Check edition
|
||||
if frameworkconfiguration["Fabric"]["Edition"] not in EDITIONS:
|
||||
raise BadRequestException("CreateNetwork", "Invalid request body")
|
||||
|
||||
## Generate network ID
|
||||
network_id = get_network_id()
|
||||
|
||||
## Generate memberid ID - will need to actually create member
|
||||
member_id = get_member_id()
|
||||
|
||||
self.networks[network_id] = ManagedBlockchainNetwork(
|
||||
id=network_id,
|
||||
name=name,
|
||||
framework=self.framework,
|
||||
frameworkversion=self.frameworkversion,
|
||||
frameworkconfiguration=self.frameworkconfiguration,
|
||||
voting_policy=self.voting_policy,
|
||||
member_configuration=self.member_configuration,
|
||||
region=self.region_name,
|
||||
description=self.description,
|
||||
)
|
||||
|
||||
# Return the network and member ID
|
||||
d = {"NetworkId": network_id, "MemberId": member_id}
|
||||
return d
|
||||
|
||||
def list_networks(self):
|
||||
return self.networks.values()
|
||||
|
||||
def get_network(self, network_id):
|
||||
if network_id not in self.networks:
|
||||
raise ResourceNotFoundException(
|
||||
"CreateNetwork", "Network {0} not found".format(network_id)
|
||||
)
|
||||
return self.networks.get(network_id)
|
||||
|
||||
|
||||
managedblockchain_backends = {}
|
||||
for region in Session().get_available_regions("managedblockchain"):
|
||||
managedblockchain_backends[region] = ManagedBlockchainBackend(region)
|
||||
90
moto/managedblockchain/responses.py
Normal file
90
moto/managedblockchain/responses.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
from six.moves.urllib.parse import urlparse, parse_qs
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import managedblockchain_backends
|
||||
from .utils import (
|
||||
region_from_managedblckchain_url,
|
||||
networkid_from_managedblockchain_url,
|
||||
)
|
||||
|
||||
|
||||
class ManagedBlockchainResponse(BaseResponse):
|
||||
def __init__(self, backend):
|
||||
super(ManagedBlockchainResponse, self).__init__()
|
||||
self.backend = backend
|
||||
|
||||
@classmethod
|
||||
def network_response(clazz, request, full_url, headers):
|
||||
region_name = region_from_managedblckchain_url(full_url)
|
||||
response_instance = ManagedBlockchainResponse(
|
||||
managedblockchain_backends[region_name]
|
||||
)
|
||||
return response_instance._network_response(request, full_url, headers)
|
||||
|
||||
def _network_response(self, request, full_url, headers):
|
||||
method = request.method
|
||||
if hasattr(request, "body"):
|
||||
body = request.body
|
||||
else:
|
||||
body = request.data
|
||||
parsed_url = urlparse(full_url)
|
||||
querystring = parse_qs(parsed_url.query, keep_blank_values=True)
|
||||
if method == "GET":
|
||||
return self._all_networks_response(request, full_url, headers)
|
||||
elif method == "POST":
|
||||
json_body = json.loads(body.decode("utf-8"))
|
||||
return self._network_response_post(json_body, querystring, headers)
|
||||
|
||||
def _all_networks_response(self, request, full_url, headers):
|
||||
mbcnetworks = self.backend.list_networks()
|
||||
response = json.dumps(
|
||||
{"Networks": [mbcnetwork.to_dict() for mbcnetwork in mbcnetworks]}
|
||||
)
|
||||
headers["content-type"] = "application/json"
|
||||
return 200, headers, response
|
||||
|
||||
def _network_response_post(self, json_body, querystring, headers):
|
||||
name = json_body["Name"]
|
||||
framework = json_body["Framework"]
|
||||
frameworkversion = json_body["FrameworkVersion"]
|
||||
frameworkconfiguration = json_body["FrameworkConfiguration"]
|
||||
voting_policy = json_body["VotingPolicy"]
|
||||
member_configuration = json_body["MemberConfiguration"]
|
||||
|
||||
# Optional
|
||||
description = json_body.get("Description", None)
|
||||
|
||||
response = self.backend.create_network(
|
||||
name,
|
||||
framework,
|
||||
frameworkversion,
|
||||
frameworkconfiguration,
|
||||
voting_policy,
|
||||
member_configuration,
|
||||
description,
|
||||
)
|
||||
return 201, headers, json.dumps(response)
|
||||
|
||||
@classmethod
|
||||
def networkid_response(clazz, request, full_url, headers):
|
||||
region_name = region_from_managedblckchain_url(full_url)
|
||||
response_instance = ManagedBlockchainResponse(
|
||||
managedblockchain_backends[region_name]
|
||||
)
|
||||
return response_instance._networkid_response(request, full_url, headers)
|
||||
|
||||
def _networkid_response(self, request, full_url, headers):
|
||||
method = request.method
|
||||
|
||||
if method == "GET":
|
||||
network_id = networkid_from_managedblockchain_url(full_url)
|
||||
return self._networkid_response_get(network_id, headers)
|
||||
|
||||
def _networkid_response_get(self, network_id, headers):
|
||||
mbcnetwork = self.backend.get_network(network_id)
|
||||
response = json.dumps({"Network": mbcnetwork.get_format()})
|
||||
headers["content-type"] = "application/json"
|
||||
return 200, headers, response
|
||||
9
moto/managedblockchain/urls.py
Normal file
9
moto/managedblockchain/urls.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from __future__ import unicode_literals
|
||||
from .responses import ManagedBlockchainResponse
|
||||
|
||||
url_bases = ["https?://managedblockchain.(.+).amazonaws.com"]
|
||||
|
||||
url_paths = {
|
||||
"{0}/networks$": ManagedBlockchainResponse.network_response,
|
||||
"{0}/networks/(?P<networkid>[^/.]+)$": ManagedBlockchainResponse.networkid_response,
|
||||
}
|
||||
29
moto/managedblockchain/utils.py
Normal file
29
moto/managedblockchain/utils.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import random
|
||||
import string
|
||||
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
|
||||
def region_from_managedblckchain_url(url):
|
||||
domain = urlparse(url).netloc
|
||||
|
||||
if "." in domain:
|
||||
return domain.split(".")[1]
|
||||
else:
|
||||
return "us-east-1"
|
||||
|
||||
|
||||
def networkid_from_managedblockchain_url(full_url):
|
||||
return full_url.split("/")[-1]
|
||||
|
||||
|
||||
def get_network_id():
|
||||
return "n-" + "".join(
|
||||
random.choice(string.ascii_uppercase + string.digits) for _ in range(26)
|
||||
)
|
||||
|
||||
|
||||
def get_member_id():
|
||||
return "m-" + "".join(
|
||||
random.choice(string.ascii_uppercase + string.digits) for _ in range(26)
|
||||
)
|
||||
|
|
@ -125,6 +125,9 @@ class OpsworkInstance(BaseModel):
|
|||
def status(self):
|
||||
if self.instance is None:
|
||||
return "stopped"
|
||||
# OpsWorks reports the "running" state as "online"
|
||||
elif self.instance._state.name == "running":
|
||||
return "online"
|
||||
return self.instance._state.name
|
||||
|
||||
def to_dict(self):
|
||||
|
|
|
|||
|
|
@ -136,3 +136,10 @@ class SnapshotCopyAlreadyEnabledFaultError(RedshiftClientError):
|
|||
cluster_identifier
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ClusterAlreadyExistsFaultError(RedshiftClientError):
|
||||
def __init__(self):
|
||||
super(ClusterAlreadyExistsFaultError, self).__init__(
|
||||
"ClusterAlreadyExists", "Cluster already exists"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from moto.core import BaseBackend, BaseModel
|
|||
from moto.core.utils import iso_8601_datetime_with_milliseconds
|
||||
from moto.ec2 import ec2_backends
|
||||
from .exceptions import (
|
||||
ClusterAlreadyExistsFaultError,
|
||||
ClusterNotFoundError,
|
||||
ClusterParameterGroupNotFoundError,
|
||||
ClusterSecurityGroupNotFoundError,
|
||||
|
|
@ -580,6 +581,8 @@ class RedshiftBackend(BaseBackend):
|
|||
|
||||
def create_cluster(self, **cluster_kwargs):
|
||||
cluster_identifier = cluster_kwargs["cluster_identifier"]
|
||||
if cluster_identifier in self.clusters:
|
||||
raise ClusterAlreadyExistsFaultError()
|
||||
cluster = Cluster(self, **cluster_kwargs)
|
||||
self.clusters[cluster_identifier] = cluster
|
||||
return cluster
|
||||
|
|
|
|||
|
|
@ -377,3 +377,12 @@ class NoSystemTags(S3ClientError):
|
|||
super(NoSystemTags, self).__init__(
|
||||
"InvalidTag", "System tags cannot be added/updated by requester"
|
||||
)
|
||||
|
||||
|
||||
class NoSuchUpload(S3ClientError):
|
||||
code = 404
|
||||
|
||||
def __init__(self):
|
||||
super(NoSuchUpload, self).__init__(
|
||||
"NoSuchUpload", "The specified multipart upload does not exist."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from .exceptions import (
|
|||
NoSuchPublicAccessBlockConfiguration,
|
||||
InvalidPublicAccessBlockConfiguration,
|
||||
WrongPublicAccessBlockAccountIdError,
|
||||
NoSuchUpload,
|
||||
)
|
||||
from .utils import clean_key_name, _VersionedKeyStore
|
||||
|
||||
|
|
@ -1478,6 +1479,9 @@ class S3Backend(BaseBackend):
|
|||
|
||||
def cancel_multipart(self, bucket_name, multipart_id):
|
||||
bucket = self.get_bucket(bucket_name)
|
||||
multipart_data = bucket.multiparts.get(multipart_id, None)
|
||||
if not multipart_data:
|
||||
raise NoSuchUpload()
|
||||
del bucket.multiparts[multipart_id]
|
||||
|
||||
def list_multipart(self, bucket_name, multipart_id):
|
||||
|
|
|
|||
|
|
@ -7,3 +7,21 @@ class MessageRejectedError(RESTError):
|
|||
|
||||
def __init__(self, message):
|
||||
super(MessageRejectedError, self).__init__("MessageRejected", message)
|
||||
|
||||
|
||||
class ConfigurationSetDoesNotExist(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message):
|
||||
super(ConfigurationSetDoesNotExist, self).__init__(
|
||||
"ConfigurationSetDoesNotExist", message
|
||||
)
|
||||
|
||||
|
||||
class EventDestinationAlreadyExists(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message):
|
||||
super(EventDestinationAlreadyExists, self).__init__(
|
||||
"EventDestinationAlreadyExists", message
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import email
|
||||
from email.utils import parseaddr
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.sns.models import sns_backends
|
||||
from .exceptions import MessageRejectedError
|
||||
from .exceptions import (
|
||||
MessageRejectedError,
|
||||
ConfigurationSetDoesNotExist,
|
||||
EventDestinationAlreadyExists,
|
||||
)
|
||||
from .utils import get_random_message_id
|
||||
from .feedback import COMMON_MAIL, BOUNCE, COMPLAINT, DELIVERY
|
||||
|
||||
|
|
@ -81,7 +86,11 @@ class SESBackend(BaseBackend):
|
|||
self.domains = []
|
||||
self.sent_messages = []
|
||||
self.sent_message_count = 0
|
||||
self.rejected_messages_count = 0
|
||||
self.sns_topics = {}
|
||||
self.config_set = {}
|
||||
self.config_set_event_destination = {}
|
||||
self.event_destinations = {}
|
||||
|
||||
def _is_verified_address(self, source):
|
||||
_, address = parseaddr(source)
|
||||
|
|
@ -118,6 +127,7 @@ class SESBackend(BaseBackend):
|
|||
if recipient_count > RECIPIENT_LIMIT:
|
||||
raise MessageRejectedError("Too many recipients.")
|
||||
if not self._is_verified_address(source):
|
||||
self.rejected_messages_count += 1
|
||||
raise MessageRejectedError("Email address not verified %s" % source)
|
||||
|
||||
self.__process_sns_feedback__(source, destinations, region)
|
||||
|
|
@ -135,6 +145,7 @@ class SESBackend(BaseBackend):
|
|||
if recipient_count > RECIPIENT_LIMIT:
|
||||
raise MessageRejectedError("Too many recipients.")
|
||||
if not self._is_verified_address(source):
|
||||
self.rejected_messages_count += 1
|
||||
raise MessageRejectedError("Email address not verified %s" % source)
|
||||
|
||||
self.__process_sns_feedback__(source, destinations, region)
|
||||
|
|
@ -237,5 +248,34 @@ class SESBackend(BaseBackend):
|
|||
|
||||
return {}
|
||||
|
||||
def create_configuration_set(self, configuration_set_name):
|
||||
self.config_set[configuration_set_name] = 1
|
||||
return {}
|
||||
|
||||
def create_configuration_set_event_destination(
|
||||
self, configuration_set_name, event_destination
|
||||
):
|
||||
|
||||
if self.config_set.get(configuration_set_name) is None:
|
||||
raise ConfigurationSetDoesNotExist("Invalid Configuration Set Name.")
|
||||
|
||||
if self.event_destinations.get(event_destination["Name"]):
|
||||
raise EventDestinationAlreadyExists("Duplicate Event destination Name.")
|
||||
|
||||
self.config_set_event_destination[configuration_set_name] = event_destination
|
||||
self.event_destinations[event_destination["Name"]] = 1
|
||||
|
||||
return {}
|
||||
|
||||
def get_send_statistics(self):
|
||||
|
||||
statistics = {}
|
||||
statistics["DeliveryAttempts"] = self.sent_message_count
|
||||
statistics["Rejects"] = self.rejected_messages_count
|
||||
statistics["Complaints"] = 0
|
||||
statistics["Bounces"] = 0
|
||||
statistics["Timestamp"] = datetime.datetime.utcnow()
|
||||
return statistics
|
||||
|
||||
|
||||
ses_backend = SESBackend()
|
||||
|
|
|
|||
|
|
@ -133,6 +133,48 @@ class EmailResponse(BaseResponse):
|
|||
template = self.response_template(SET_IDENTITY_NOTIFICATION_TOPIC_RESPONSE)
|
||||
return template.render()
|
||||
|
||||
def get_send_statistics(self):
|
||||
statistics = ses_backend.get_send_statistics()
|
||||
template = self.response_template(GET_SEND_STATISTICS)
|
||||
return template.render(all_statistics=[statistics])
|
||||
|
||||
def create_configuration_set(self):
|
||||
configuration_set_name = self.querystring.get("ConfigurationSet.Name")[0]
|
||||
ses_backend.create_configuration_set(
|
||||
configuration_set_name=configuration_set_name
|
||||
)
|
||||
template = self.response_template(CREATE_CONFIGURATION_SET)
|
||||
return template.render()
|
||||
|
||||
def create_configuration_set_event_destination(self):
|
||||
|
||||
configuration_set_name = self._get_param("ConfigurationSetName")
|
||||
is_configuration_event_enabled = self.querystring.get(
|
||||
"EventDestination.Enabled"
|
||||
)[0]
|
||||
configuration_event_name = self.querystring.get("EventDestination.Name")[0]
|
||||
event_topic_arn = self.querystring.get(
|
||||
"EventDestination.SNSDestination.TopicARN"
|
||||
)[0]
|
||||
event_matching_types = self._get_multi_param(
|
||||
"EventDestination.MatchingEventTypes.member"
|
||||
)
|
||||
|
||||
event_destination = {
|
||||
"Name": configuration_event_name,
|
||||
"Enabled": is_configuration_event_enabled,
|
||||
"EventMatchingTypes": event_matching_types,
|
||||
"SNSDestination": event_topic_arn,
|
||||
}
|
||||
|
||||
ses_backend.create_configuration_set_event_destination(
|
||||
configuration_set_name=configuration_set_name,
|
||||
event_destination=event_destination,
|
||||
)
|
||||
|
||||
template = self.response_template(CREATE_CONFIGURATION_SET_EVENT_DESTINATION)
|
||||
return template.render()
|
||||
|
||||
|
||||
VERIFY_EMAIL_IDENTITY = """<VerifyEmailIdentityResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
||||
<VerifyEmailIdentityResult/>
|
||||
|
|
@ -248,3 +290,37 @@ SET_IDENTITY_NOTIFICATION_TOPIC_RESPONSE = """<SetIdentityNotificationTopicRespo
|
|||
<RequestId>47e0ef1a-9bf2-11e1-9279-0100e8cf109a</RequestId>
|
||||
</ResponseMetadata>
|
||||
</SetIdentityNotificationTopicResponse>"""
|
||||
|
||||
GET_SEND_STATISTICS = """<GetSendStatisticsResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
||||
<GetSendStatisticsResult>
|
||||
<SendDataPoints>
|
||||
{% for statistics in all_statistics %}
|
||||
<item>
|
||||
<DeliveryAttempts>{{ statistics["DeliveryAttempts"] }}</DeliveryAttempts>
|
||||
<Rejects>{{ statistics["Rejects"] }}</Rejects>
|
||||
<Bounces>{{ statistics["Bounces"] }}</Bounces>
|
||||
<Complaints>{{ statistics["Complaints"] }}</Complaints>
|
||||
<Timestamp>{{ statistics["Timestamp"] }}</Timestamp>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</SendDataPoints>
|
||||
<ResponseMetadata>
|
||||
<RequestId>e0abcdfa-c866-11e0-b6d0-273d09173z49</RequestId>
|
||||
</ResponseMetadata>
|
||||
</GetSendStatisticsResult>
|
||||
</GetSendStatisticsResponse>"""
|
||||
|
||||
CREATE_CONFIGURATION_SET = """<CreateConfigurationSetResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
||||
<CreateConfigurationSetResult/>
|
||||
<ResponseMetadata>
|
||||
<RequestId>47e0ef1a-9bf2-11e1-9279-0100e8cf109a</RequestId>
|
||||
</ResponseMetadata>
|
||||
</CreateConfigurationSetResponse>"""
|
||||
|
||||
|
||||
CREATE_CONFIGURATION_SET_EVENT_DESTINATION = """<CreateConfigurationSetEventDestinationResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
||||
<CreateConfigurationSetEventDestinationResult/>
|
||||
<ResponseMetadata>
|
||||
<RequestId>67e0ef1a-9bf2-11e1-9279-0100e8cf109a</RequestId>
|
||||
</ResponseMetadata>
|
||||
</CreateConfigurationSetEventDestinationResponse>"""
|
||||
|
|
|
|||
|
|
@ -514,6 +514,16 @@ class SimpleSystemManagerBackend(BaseBackend):
|
|||
|
||||
def get_parameters(self, names, with_decryption):
|
||||
result = []
|
||||
|
||||
if len(names) > 10:
|
||||
raise ValidationException(
|
||||
"1 validation error detected: "
|
||||
"Value '[{}]' at 'names' failed to satisfy constraint: "
|
||||
"Member must have length less than or equal to 10.".format(
|
||||
", ".join(names)
|
||||
)
|
||||
)
|
||||
|
||||
for name in names:
|
||||
if name in self._parameters:
|
||||
result.append(self.get_parameter(name, with_decryption))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue