Merge remote-tracking branch 'spulec/moto/master' into organizations_support
This commit is contained in:
commit
95700d6631
71 changed files with 2645 additions and 267 deletions
|
|
@ -3,7 +3,7 @@ import logging
|
|||
# logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||
|
||||
__title__ = 'moto'
|
||||
__version__ = '1.3.3'
|
||||
__version__ = '1.3.6'
|
||||
|
||||
from .acm import mock_acm # flake8: noqa
|
||||
from .apigateway import mock_apigateway, mock_apigateway_deprecated # flake8: noqa
|
||||
|
|
@ -24,6 +24,7 @@ from .elbv2 import mock_elbv2 # flake8: noqa
|
|||
from .emr import mock_emr, mock_emr_deprecated # flake8: noqa
|
||||
from .events import mock_events # flake8: noqa
|
||||
from .glacier import mock_glacier, mock_glacier_deprecated # flake8: noqa
|
||||
from .glue import mock_glue # flake8: noqa
|
||||
from .iam import mock_iam, mock_iam_deprecated # flake8: noqa
|
||||
from .kinesis import mock_kinesis, mock_kinesis_deprecated # flake8: noqa
|
||||
from .kms import mock_kms, mock_kms_deprecated # flake8: noqa
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from moto.elbv2 import elbv2_backends
|
|||
from moto.emr import emr_backends
|
||||
from moto.events import events_backends
|
||||
from moto.glacier import glacier_backends
|
||||
from moto.glue import glue_backends
|
||||
from moto.iam import iam_backends
|
||||
from moto.instance_metadata import instance_metadata_backends
|
||||
from moto.kinesis import kinesis_backends
|
||||
|
|
@ -66,6 +67,7 @@ BACKENDS = {
|
|||
'events': events_backends,
|
||||
'emr': emr_backends,
|
||||
'glacier': glacier_backends,
|
||||
'glue': glue_backends,
|
||||
'iam': iam_backends,
|
||||
'moto_api': moto_api_backends,
|
||||
'instance_metadata': instance_metadata_backends,
|
||||
|
|
|
|||
|
|
@ -387,6 +387,7 @@ class ResourceMap(collections.Mapping):
|
|||
"AWS::StackName": stack_name,
|
||||
"AWS::URLSuffix": "amazonaws.com",
|
||||
"AWS::NoValue": None,
|
||||
"AWS::Partition": "aws",
|
||||
}
|
||||
|
||||
def __getitem__(self, key):
|
||||
|
|
|
|||
|
|
@ -89,6 +89,17 @@ class BaseMockAWS(object):
|
|||
if inspect.ismethod(attr_value) and attr_value.__self__ is klass:
|
||||
continue
|
||||
|
||||
# Check if this is a staticmethod. If so, skip patching
|
||||
for cls in inspect.getmro(klass):
|
||||
if attr_value.__name__ not in cls.__dict__:
|
||||
continue
|
||||
bound_attr_value = cls.__dict__[attr_value.__name__]
|
||||
if not isinstance(bound_attr_value, staticmethod):
|
||||
break
|
||||
else:
|
||||
# It is a staticmethod, skip patching
|
||||
continue
|
||||
|
||||
try:
|
||||
setattr(klass, attr, self(attr_value, reset=False))
|
||||
except TypeError:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,17 @@ def has_empty_keys_or_values(_dict):
|
|||
)
|
||||
|
||||
|
||||
def get_empty_str_error():
|
||||
er = 'com.amazonaws.dynamodb.v20111205#ValidationException'
|
||||
return (400,
|
||||
{'server': 'amazon.com'},
|
||||
dynamo_json_dump({'__type': er,
|
||||
'message': ('One or more parameter values were '
|
||||
'invalid: An AttributeValue may not '
|
||||
'contain an empty string')}
|
||||
))
|
||||
|
||||
|
||||
class DynamoHandler(BaseResponse):
|
||||
|
||||
def get_endpoint_name(self, headers):
|
||||
|
|
@ -174,14 +185,7 @@ class DynamoHandler(BaseResponse):
|
|||
item = self.body['Item']
|
||||
|
||||
if has_empty_keys_or_values(item):
|
||||
er = 'com.amazonaws.dynamodb.v20111205#ValidationException'
|
||||
return (400,
|
||||
{'server': 'amazon.com'},
|
||||
dynamo_json_dump({'__type': er,
|
||||
'message': ('One or more parameter values were '
|
||||
'invalid: An AttributeValue may not '
|
||||
'contain an empty string')}
|
||||
))
|
||||
return get_empty_str_error()
|
||||
|
||||
overwrite = 'Expected' not in self.body
|
||||
if not overwrite:
|
||||
|
|
@ -523,6 +527,7 @@ class DynamoHandler(BaseResponse):
|
|||
return dynamo_json_dump(item_dict)
|
||||
|
||||
def update_item(self):
|
||||
|
||||
name = self.body['TableName']
|
||||
key = self.body['Key']
|
||||
update_expression = self.body.get('UpdateExpression')
|
||||
|
|
@ -533,6 +538,9 @@ class DynamoHandler(BaseResponse):
|
|||
'ExpressionAttributeValues', {})
|
||||
existing_item = self.dynamodb_backend.get_item(name, key)
|
||||
|
||||
if has_empty_keys_or_values(expression_attribute_values):
|
||||
return get_empty_str_error()
|
||||
|
||||
if 'Expected' in self.body:
|
||||
expected = self.body['Expected']
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from pkg_resources import resource_filename
|
|||
import boto.ec2
|
||||
|
||||
from collections import defaultdict
|
||||
import weakref
|
||||
from datetime import datetime
|
||||
from boto.ec2.instance import Instance as BotoInstance, Reservation
|
||||
from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType
|
||||
|
|
@ -2115,10 +2116,20 @@ class VPC(TaggedEC2Resource):
|
|||
|
||||
|
||||
class VPCBackend(object):
|
||||
__refs__ = defaultdict(list)
|
||||
|
||||
def __init__(self):
|
||||
self.vpcs = {}
|
||||
self.__refs__[self.__class__].append(weakref.ref(self))
|
||||
super(VPCBackend, self).__init__()
|
||||
|
||||
@classmethod
|
||||
def get_instances(cls):
|
||||
for inst_ref in cls.__refs__[cls]:
|
||||
inst = inst_ref()
|
||||
if inst is not None:
|
||||
yield inst
|
||||
|
||||
def create_vpc(self, cidr_block, instance_tenancy='default', amazon_provided_ipv6_cidr_block=False):
|
||||
vpc_id = random_vpc_id()
|
||||
vpc = VPC(self, vpc_id, cidr_block, len(self.vpcs) == 0, instance_tenancy, amazon_provided_ipv6_cidr_block)
|
||||
|
|
@ -2142,6 +2153,13 @@ class VPCBackend(object):
|
|||
raise InvalidVPCIdError(vpc_id)
|
||||
return self.vpcs.get(vpc_id)
|
||||
|
||||
# get vpc by vpc id and aws region
|
||||
def get_cross_vpc(self, vpc_id, peer_region):
|
||||
for vpcs in self.get_instances():
|
||||
if vpcs.region_name == peer_region:
|
||||
match_vpc = vpcs.get_vpc(vpc_id)
|
||||
return match_vpc
|
||||
|
||||
def get_all_vpcs(self, vpc_ids=None, filters=None):
|
||||
matches = self.vpcs.values()
|
||||
if vpc_ids:
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ from moto.core.responses import BaseResponse
|
|||
class VPCPeeringConnections(BaseResponse):
|
||||
|
||||
def create_vpc_peering_connection(self):
|
||||
peer_region = self._get_param('PeerRegion')
|
||||
if peer_region == self.region or peer_region is None:
|
||||
peer_vpc = self.ec2_backend.get_vpc(self._get_param('PeerVpcId'))
|
||||
else:
|
||||
peer_vpc = self.ec2_backend.get_cross_vpc(self._get_param('PeerVpcId'), peer_region)
|
||||
vpc = self.ec2_backend.get_vpc(self._get_param('VpcId'))
|
||||
peer_vpc = self.ec2_backend.get_vpc(self._get_param('PeerVpcId'))
|
||||
vpc_pcx = self.ec2_backend.create_vpc_peering_connection(vpc, peer_vpc)
|
||||
template = self.response_template(
|
||||
CREATE_VPC_PEERING_CONNECTION_RESPONSE)
|
||||
|
|
@ -41,26 +45,31 @@ class VPCPeeringConnections(BaseResponse):
|
|||
|
||||
|
||||
CREATE_VPC_PEERING_CONNECTION_RESPONSE = """
|
||||
<CreateVpcPeeringConnectionResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
|
||||
<requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId>
|
||||
<vpcPeeringConnection>
|
||||
<vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
|
||||
<CreateVpcPeeringConnectionResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
|
||||
<requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId>
|
||||
<vpcPeeringConnection>
|
||||
<vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
|
||||
<requesterVpcInfo>
|
||||
<ownerId>777788889999</ownerId>
|
||||
<vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
|
||||
<cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
|
||||
<ownerId>777788889999</ownerId>
|
||||
<vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
|
||||
<cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
|
||||
<peeringOptions>
|
||||
<allowEgressFromLocalClassicLinkToRemoteVpc>false</allowEgressFromLocalClassicLinkToRemoteVpc>
|
||||
<allowEgressFromLocalVpcToRemoteClassicLink>false</allowEgressFromLocalVpcToRemoteClassicLink>
|
||||
<allowDnsResolutionFromRemoteVpc>false</allowDnsResolutionFromRemoteVpc>
|
||||
</peeringOptions>
|
||||
</requesterVpcInfo>
|
||||
<accepterVpcInfo>
|
||||
<ownerId>123456789012</ownerId>
|
||||
<vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
|
||||
</accepterVpcInfo>
|
||||
<status>
|
||||
<code>initiating-request</code>
|
||||
<message>Initiating request to {accepter ID}.</message>
|
||||
<code>initiating-request</code>
|
||||
<message>Initiating Request to {accepter ID}</message>
|
||||
</status>
|
||||
<expirationTime>2014-02-18T14:37:25.000Z</expirationTime>
|
||||
<tagSet/>
|
||||
</vpcPeeringConnection>
|
||||
</vpcPeeringConnection>
|
||||
</CreateVpcPeeringConnectionResponse>
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import hashlib
|
|||
from copy import copy
|
||||
from random import random
|
||||
|
||||
from botocore.exceptions import ParamValidationError
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.ec2 import ec2_backends
|
||||
from moto.ecr.exceptions import ImageNotFoundException, RepositoryNotFoundException
|
||||
|
||||
from botocore.exceptions import ParamValidationError
|
||||
|
||||
DEFAULT_REGISTRY_ID = '012345678910'
|
||||
|
||||
|
||||
|
|
@ -97,13 +97,14 @@ class Repository(BaseObject):
|
|||
|
||||
class Image(BaseObject):
|
||||
|
||||
def __init__(self, tag, manifest, repository, registry_id=DEFAULT_REGISTRY_ID):
|
||||
def __init__(self, tag, manifest, repository, digest=None, registry_id=DEFAULT_REGISTRY_ID):
|
||||
self.image_tag = tag
|
||||
self.image_tags = [tag] if tag is not None else []
|
||||
self.image_manifest = manifest
|
||||
self.image_size_in_bytes = 50 * 1024 * 1024
|
||||
self.repository = repository
|
||||
self.registry_id = registry_id
|
||||
self.image_digest = None
|
||||
self.image_digest = digest
|
||||
self.image_pushed_at = None
|
||||
|
||||
def _create_digest(self):
|
||||
|
|
@ -115,6 +116,14 @@ class Image(BaseObject):
|
|||
self._create_digest()
|
||||
return self.image_digest
|
||||
|
||||
def get_image_manifest(self):
|
||||
return self.image_manifest
|
||||
|
||||
def update_tag(self, tag):
|
||||
self.image_tag = tag
|
||||
if tag not in self.image_tags and tag is not None:
|
||||
self.image_tags.append(tag)
|
||||
|
||||
@property
|
||||
def response_object(self):
|
||||
response_object = self.gen_response_object()
|
||||
|
|
@ -124,26 +133,26 @@ class Image(BaseObject):
|
|||
response_object['imageManifest'] = self.image_manifest
|
||||
response_object['repositoryName'] = self.repository
|
||||
response_object['registryId'] = self.registry_id
|
||||
return response_object
|
||||
return {k: v for k, v in response_object.items() if v is not None and v != [None]}
|
||||
|
||||
@property
|
||||
def response_list_object(self):
|
||||
response_object = self.gen_response_object()
|
||||
response_object['imageTag'] = self.image_tag
|
||||
response_object['imageDigest'] = "i don't know"
|
||||
return response_object
|
||||
return {k: v for k, v in response_object.items() if v is not None and v != [None]}
|
||||
|
||||
@property
|
||||
def response_describe_object(self):
|
||||
response_object = self.gen_response_object()
|
||||
response_object['imageTags'] = [self.image_tag]
|
||||
response_object['imageTags'] = self.image_tags
|
||||
response_object['imageDigest'] = self.get_image_digest()
|
||||
response_object['imageManifest'] = self.image_manifest
|
||||
response_object['repositoryName'] = self.repository
|
||||
response_object['registryId'] = self.registry_id
|
||||
response_object['imageSizeInBytes'] = self.image_size_in_bytes
|
||||
response_object['imagePushedAt'] = '2017-05-09'
|
||||
return response_object
|
||||
return {k: v for k, v in response_object.items() if v is not None and v != []}
|
||||
|
||||
@property
|
||||
def response_batch_get_image(self):
|
||||
|
|
@ -154,7 +163,7 @@ class Image(BaseObject):
|
|||
response_object['imageManifest'] = self.image_manifest
|
||||
response_object['repositoryName'] = self.repository
|
||||
response_object['registryId'] = self.registry_id
|
||||
return response_object
|
||||
return {k: v for k, v in response_object.items() if v is not None and v != [None]}
|
||||
|
||||
|
||||
class ECRBackend(BaseBackend):
|
||||
|
|
@ -231,7 +240,7 @@ class ECRBackend(BaseBackend):
|
|||
found = False
|
||||
for image in repository.images:
|
||||
if (('imageDigest' in image_id and image.get_image_digest() == image_id['imageDigest']) or
|
||||
('imageTag' in image_id and image.image_tag == image_id['imageTag'])):
|
||||
('imageTag' in image_id and image_id['imageTag'] in image.image_tags)):
|
||||
found = True
|
||||
response.add(image)
|
||||
if not found:
|
||||
|
|
@ -257,9 +266,16 @@ class ECRBackend(BaseBackend):
|
|||
else:
|
||||
raise Exception("{0} is not a repository".format(repository_name))
|
||||
|
||||
image = Image(image_tag, image_manifest, repository_name)
|
||||
repository.images.append(image)
|
||||
return image
|
||||
existing_images = list(filter(lambda x: x.response_object['imageManifest'] == image_manifest, repository.images))
|
||||
if not existing_images:
|
||||
# this image is not in ECR yet
|
||||
image = Image(image_tag, image_manifest, repository_name)
|
||||
repository.images.append(image)
|
||||
return image
|
||||
else:
|
||||
# update existing image
|
||||
existing_images[0].update_tag(image_tag)
|
||||
return existing_images[0]
|
||||
|
||||
def batch_get_image(self, repository_name, registry_id=None, image_ids=None, accepted_media_types=None):
|
||||
if repository_name in self.repositories:
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ class Task(BaseObject):
|
|||
|
||||
class Service(BaseObject):
|
||||
|
||||
def __init__(self, cluster, service_name, task_definition, desired_count, load_balancers=None):
|
||||
def __init__(self, cluster, service_name, task_definition, desired_count, load_balancers=None, scheduling_strategy=None):
|
||||
self.cluster_arn = cluster.arn
|
||||
self.arn = 'arn:aws:ecs:us-east-1:012345678910:service/{0}'.format(
|
||||
service_name)
|
||||
|
|
@ -202,6 +202,7 @@ class Service(BaseObject):
|
|||
}
|
||||
]
|
||||
self.load_balancers = load_balancers if load_balancers is not None else []
|
||||
self.scheduling_strategy = scheduling_strategy if scheduling_strategy is not None else 'REPLICA'
|
||||
self.pending_count = 0
|
||||
|
||||
@property
|
||||
|
|
@ -214,6 +215,7 @@ class Service(BaseObject):
|
|||
del response_object['name'], response_object['arn']
|
||||
response_object['serviceName'] = self.name
|
||||
response_object['serviceArn'] = self.arn
|
||||
response_object['schedulingStrategy'] = self.scheduling_strategy
|
||||
|
||||
for deployment in response_object['deployments']:
|
||||
if isinstance(deployment['createdAt'], datetime):
|
||||
|
|
@ -655,7 +657,7 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||
raise Exception("Could not find task {} on cluster {}".format(
|
||||
task_str, cluster_name))
|
||||
|
||||
def create_service(self, cluster_str, service_name, task_definition_str, desired_count, load_balancers=None):
|
||||
def create_service(self, cluster_str, service_name, task_definition_str, desired_count, load_balancers=None, scheduling_strategy=None):
|
||||
cluster_name = cluster_str.split('/')[-1]
|
||||
if cluster_name in self.clusters:
|
||||
cluster = self.clusters[cluster_name]
|
||||
|
|
@ -665,7 +667,7 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||
desired_count = desired_count if desired_count is not None else 0
|
||||
|
||||
service = Service(cluster, service_name,
|
||||
task_definition, desired_count, load_balancers)
|
||||
task_definition, desired_count, load_balancers, scheduling_strategy)
|
||||
cluster_service_pair = '{0}:{1}'.format(cluster_name, service_name)
|
||||
self.services[cluster_service_pair] = service
|
||||
|
||||
|
|
|
|||
|
|
@ -154,8 +154,9 @@ class EC2ContainerServiceResponse(BaseResponse):
|
|||
task_definition_str = self._get_param('taskDefinition')
|
||||
desired_count = self._get_int_param('desiredCount')
|
||||
load_balancers = self._get_param('loadBalancers')
|
||||
scheduling_strategy = self._get_param('schedulingStrategy')
|
||||
service = self.ecs_backend.create_service(
|
||||
cluster_str, service_name, task_definition_str, desired_count, load_balancers)
|
||||
cluster_str, service_name, task_definition_str, desired_count, load_balancers, scheduling_strategy)
|
||||
return json.dumps({
|
||||
'service': service.response_object
|
||||
})
|
||||
|
|
|
|||
|
|
@ -259,12 +259,22 @@ class ELBResponse(BaseResponse):
|
|||
|
||||
def describe_instance_health(self):
|
||||
load_balancer_name = self._get_param('LoadBalancerName')
|
||||
instance_ids = [list(param.values())[0] for param in self._get_list_prefix('Instances.member')]
|
||||
if len(instance_ids) == 0:
|
||||
instance_ids = self.elb_backend.get_load_balancer(
|
||||
load_balancer_name).instance_ids
|
||||
provided_instance_ids = [
|
||||
list(param.values())[0]
|
||||
for param in self._get_list_prefix('Instances.member')
|
||||
]
|
||||
registered_instances_id = self.elb_backend.get_load_balancer(
|
||||
load_balancer_name).instance_ids
|
||||
if len(provided_instance_ids) == 0:
|
||||
provided_instance_ids = registered_instances_id
|
||||
template = self.response_template(DESCRIBE_INSTANCE_HEALTH_TEMPLATE)
|
||||
return template.render(instance_ids=instance_ids)
|
||||
instances = []
|
||||
for instance_id in provided_instance_ids:
|
||||
state = "InService" \
|
||||
if instance_id in registered_instances_id\
|
||||
else "Unknown"
|
||||
instances.append({"InstanceId": instance_id, "State": state})
|
||||
return template.render(instances=instances)
|
||||
|
||||
def add_tags(self):
|
||||
|
||||
|
|
@ -689,11 +699,11 @@ SET_LOAD_BALANCER_POLICIES_FOR_BACKEND_SERVER_TEMPLATE = """<SetLoadBalancerPoli
|
|||
DESCRIBE_INSTANCE_HEALTH_TEMPLATE = """<DescribeInstanceHealthResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
|
||||
<DescribeInstanceHealthResult>
|
||||
<InstanceStates>
|
||||
{% for instance_id in instance_ids %}
|
||||
{% for instance in instances %}
|
||||
<member>
|
||||
<Description>N/A</Description>
|
||||
<InstanceId>{{ instance_id }}</InstanceId>
|
||||
<State>InService</State>
|
||||
<InstanceId>{{ instance['InstanceId'] }}</InstanceId>
|
||||
<State>{{ instance['State'] }}</State>
|
||||
<ReasonCode>N/A</ReasonCode>
|
||||
</member>
|
||||
{% endfor %}
|
||||
|
|
|
|||
5
moto/glue/__init__.py
Normal file
5
moto/glue/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
from .models import glue_backend
|
||||
|
||||
glue_backends = {"global": glue_backend}
|
||||
mock_glue = glue_backend.decorator
|
||||
24
moto/glue/exceptions.py
Normal file
24
moto/glue/exceptions.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from __future__ import unicode_literals
|
||||
from moto.core.exceptions import JsonRESTError
|
||||
|
||||
|
||||
class GlueClientError(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
|
||||
class DatabaseAlreadyExistsException(GlueClientError):
|
||||
def __init__(self):
|
||||
self.code = 400
|
||||
super(DatabaseAlreadyExistsException, self).__init__(
|
||||
'DatabaseAlreadyExistsException',
|
||||
'Database already exists.'
|
||||
)
|
||||
|
||||
|
||||
class TableAlreadyExistsException(GlueClientError):
|
||||
def __init__(self):
|
||||
self.code = 400
|
||||
super(TableAlreadyExistsException, self).__init__(
|
||||
'TableAlreadyExistsException',
|
||||
'Table already exists.'
|
||||
)
|
||||
60
moto/glue/models.py
Normal file
60
moto/glue/models.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.compat import OrderedDict
|
||||
from.exceptions import DatabaseAlreadyExistsException, TableAlreadyExistsException
|
||||
|
||||
|
||||
class GlueBackend(BaseBackend):
|
||||
|
||||
def __init__(self):
|
||||
self.databases = OrderedDict()
|
||||
|
||||
def create_database(self, database_name):
|
||||
if database_name in self.databases:
|
||||
raise DatabaseAlreadyExistsException()
|
||||
|
||||
database = FakeDatabase(database_name)
|
||||
self.databases[database_name] = database
|
||||
return database
|
||||
|
||||
def get_database(self, database_name):
|
||||
return self.databases[database_name]
|
||||
|
||||
def create_table(self, database_name, table_name, table_input):
|
||||
database = self.get_database(database_name)
|
||||
|
||||
if table_name in database.tables:
|
||||
raise TableAlreadyExistsException()
|
||||
|
||||
table = FakeTable(database_name, table_name, table_input)
|
||||
database.tables[table_name] = table
|
||||
return table
|
||||
|
||||
def get_table(self, database_name, table_name):
|
||||
database = self.get_database(database_name)
|
||||
return database.tables[table_name]
|
||||
|
||||
def get_tables(self, database_name):
|
||||
database = self.get_database(database_name)
|
||||
return [table for table_name, table in database.tables.items()]
|
||||
|
||||
|
||||
class FakeDatabase(BaseModel):
|
||||
|
||||
def __init__(self, database_name):
|
||||
self.name = database_name
|
||||
self.tables = OrderedDict()
|
||||
|
||||
|
||||
class FakeTable(BaseModel):
|
||||
|
||||
def __init__(self, database_name, table_name, table_input):
|
||||
self.database_name = database_name
|
||||
self.name = table_name
|
||||
self.table_input = table_input
|
||||
self.storage_descriptor = self.table_input.get('StorageDescriptor', {})
|
||||
self.partition_keys = self.table_input.get('PartitionKeys', [])
|
||||
|
||||
|
||||
glue_backend = GlueBackend()
|
||||
63
moto/glue/responses.py
Normal file
63
moto/glue/responses.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import glue_backend
|
||||
|
||||
|
||||
class GlueResponse(BaseResponse):
|
||||
|
||||
@property
|
||||
def glue_backend(self):
|
||||
return glue_backend
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
return json.loads(self.body)
|
||||
|
||||
def create_database(self):
|
||||
database_name = self.parameters['DatabaseInput']['Name']
|
||||
self.glue_backend.create_database(database_name)
|
||||
return ""
|
||||
|
||||
def get_database(self):
|
||||
database_name = self.parameters.get('Name')
|
||||
database = self.glue_backend.get_database(database_name)
|
||||
return json.dumps({'Database': {'Name': database.name}})
|
||||
|
||||
def create_table(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
table_input = self.parameters.get('TableInput')
|
||||
table_name = table_input.get('Name')
|
||||
self.glue_backend.create_table(database_name, table_name, table_input)
|
||||
return ""
|
||||
|
||||
def get_table(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
table_name = self.parameters.get('Name')
|
||||
table = self.glue_backend.get_table(database_name, table_name)
|
||||
return json.dumps({
|
||||
'Table': {
|
||||
'DatabaseName': table.database_name,
|
||||
'Name': table.name,
|
||||
'PartitionKeys': table.partition_keys,
|
||||
'StorageDescriptor': table.storage_descriptor
|
||||
}
|
||||
})
|
||||
|
||||
def get_tables(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
tables = self.glue_backend.get_tables(database_name)
|
||||
return json.dumps(
|
||||
{
|
||||
'TableList': [
|
||||
{
|
||||
'DatabaseName': table.database_name,
|
||||
'Name': table.name,
|
||||
'PartitionKeys': table.partition_keys,
|
||||
'StorageDescriptor': table.storage_descriptor
|
||||
} for table in tables
|
||||
]
|
||||
}
|
||||
)
|
||||
11
moto/glue/urls.py
Normal file
11
moto/glue/urls.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from .responses import GlueResponse
|
||||
|
||||
url_bases = [
|
||||
"https?://glue(.*).amazonaws.com"
|
||||
]
|
||||
|
||||
url_paths = {
|
||||
'{0}/$': GlueResponse.dispatch
|
||||
}
|
||||
1
moto/glue/utils.py
Normal file
1
moto/glue/utils.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from __future__ import unicode_literals
|
||||
|
|
@ -37,7 +37,6 @@ class Policy(BaseModel):
|
|||
description=None,
|
||||
document=None,
|
||||
path=None):
|
||||
self.document = document or {}
|
||||
self.name = name
|
||||
|
||||
self.attachment_count = 0
|
||||
|
|
@ -45,7 +44,7 @@ class Policy(BaseModel):
|
|||
self.id = random_policy_id()
|
||||
self.path = path or '/'
|
||||
self.default_version_id = default_version_id or 'v1'
|
||||
self.versions = []
|
||||
self.versions = [PolicyVersion(self.arn, document, True)]
|
||||
|
||||
self.create_datetime = datetime.now(pytz.utc)
|
||||
self.update_datetime = datetime.now(pytz.utc)
|
||||
|
|
@ -72,11 +71,11 @@ class ManagedPolicy(Policy):
|
|||
|
||||
def attach_to(self, obj):
|
||||
self.attachment_count += 1
|
||||
obj.managed_policies[self.name] = self
|
||||
obj.managed_policies[self.arn] = self
|
||||
|
||||
def detach_from(self, obj):
|
||||
self.attachment_count -= 1
|
||||
del obj.managed_policies[self.name]
|
||||
del obj.managed_policies[self.arn]
|
||||
|
||||
@property
|
||||
def arn(self):
|
||||
|
|
@ -477,11 +476,13 @@ class IAMBackend(BaseBackend):
|
|||
document=policy_document,
|
||||
path=path,
|
||||
)
|
||||
self.managed_policies[policy.name] = policy
|
||||
self.managed_policies[policy.arn] = policy
|
||||
return policy
|
||||
|
||||
def get_policy(self, policy_name):
|
||||
return self.managed_policies.get(policy_name)
|
||||
def get_policy(self, policy_arn):
|
||||
if policy_arn not in self.managed_policies:
|
||||
raise IAMNotFoundException("Policy {0} not found".format(policy_arn))
|
||||
return self.managed_policies.get(policy_arn)
|
||||
|
||||
def list_attached_role_policies(self, role_name, marker=None, max_items=100, path_prefix='/'):
|
||||
policies = self.get_role(role_name).managed_policies.values()
|
||||
|
|
@ -575,21 +576,18 @@ class IAMBackend(BaseBackend):
|
|||
return role.policies.keys()
|
||||
|
||||
def create_policy_version(self, policy_arn, policy_document, set_as_default):
|
||||
policy_name = policy_arn.split(':')[-1]
|
||||
policy_name = policy_name.split('/')[1]
|
||||
policy = self.get_policy(policy_name)
|
||||
policy = self.get_policy(policy_arn)
|
||||
if not policy:
|
||||
raise IAMNotFoundException("Policy not found")
|
||||
version = PolicyVersion(policy_arn, policy_document, set_as_default)
|
||||
policy.versions.append(version)
|
||||
version.version_id = 'v{0}'.format(len(policy.versions))
|
||||
if set_as_default:
|
||||
policy.default_version_id = version.version_id
|
||||
return version
|
||||
|
||||
def get_policy_version(self, policy_arn, version_id):
|
||||
policy_name = policy_arn.split(':')[-1]
|
||||
policy_name = policy_name.split('/')[1]
|
||||
policy = self.get_policy(policy_name)
|
||||
policy = self.get_policy(policy_arn)
|
||||
if not policy:
|
||||
raise IAMNotFoundException("Policy not found")
|
||||
for version in policy.versions:
|
||||
|
|
@ -598,19 +596,18 @@ class IAMBackend(BaseBackend):
|
|||
raise IAMNotFoundException("Policy version not found")
|
||||
|
||||
def list_policy_versions(self, policy_arn):
|
||||
policy_name = policy_arn.split(':')[-1]
|
||||
policy_name = policy_name.split('/')[1]
|
||||
policy = self.get_policy(policy_name)
|
||||
policy = self.get_policy(policy_arn)
|
||||
if not policy:
|
||||
raise IAMNotFoundException("Policy not found")
|
||||
return policy.versions
|
||||
|
||||
def delete_policy_version(self, policy_arn, version_id):
|
||||
policy_name = policy_arn.split(':')[-1]
|
||||
policy_name = policy_name.split('/')[1]
|
||||
policy = self.get_policy(policy_name)
|
||||
policy = self.get_policy(policy_arn)
|
||||
if not policy:
|
||||
raise IAMNotFoundException("Policy not found")
|
||||
if version_id == policy.default_version_id:
|
||||
raise IAMConflictException(
|
||||
"Cannot delete the default version of a policy")
|
||||
for i, v in enumerate(policy.versions):
|
||||
if v.version_id == version_id:
|
||||
del policy.versions[i]
|
||||
|
|
@ -905,5 +902,32 @@ class IAMBackend(BaseBackend):
|
|||
def delete_account_alias(self, alias):
|
||||
self.account_aliases = []
|
||||
|
||||
def get_account_authorization_details(self, filter):
|
||||
policies = self.managed_policies.values()
|
||||
local_policies = set(policies) - set(aws_managed_policies)
|
||||
returned_policies = []
|
||||
|
||||
if len(filter) == 0:
|
||||
return {
|
||||
'instance_profiles': self.instance_profiles.values(),
|
||||
'roles': self.roles.values(),
|
||||
'groups': self.groups.values(),
|
||||
'users': self.users.values(),
|
||||
'managed_policies': self.managed_policies.values()
|
||||
}
|
||||
|
||||
if 'AWSManagedPolicy' in filter:
|
||||
returned_policies = aws_managed_policies
|
||||
if 'LocalManagedPolicy' in filter:
|
||||
returned_policies = returned_policies + list(local_policies)
|
||||
|
||||
return {
|
||||
'instance_profiles': self.instance_profiles.values(),
|
||||
'roles': self.roles.values() if 'Role' in filter else [],
|
||||
'groups': self.groups.values() if 'Group' in filter else [],
|
||||
'users': self.users.values() if 'User' in filter else [],
|
||||
'managed_policies': returned_policies
|
||||
}
|
||||
|
||||
|
||||
iam_backend = IAMBackend()
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ class IamResponse(BaseResponse):
|
|||
template = self.response_template(CREATE_POLICY_TEMPLATE)
|
||||
return template.render(policy=policy)
|
||||
|
||||
def get_policy(self):
|
||||
policy_arn = self._get_param('PolicyArn')
|
||||
policy = iam_backend.get_policy(policy_arn)
|
||||
template = self.response_template(GET_POLICY_TEMPLATE)
|
||||
return template.render(policy=policy)
|
||||
|
||||
def list_attached_role_policies(self):
|
||||
marker = self._get_param('Marker')
|
||||
max_items = self._get_int_param('MaxItems', 100)
|
||||
|
|
@ -534,6 +540,18 @@ class IamResponse(BaseResponse):
|
|||
template = self.response_template(DELETE_ACCOUNT_ALIAS_TEMPLATE)
|
||||
return template.render()
|
||||
|
||||
def get_account_authorization_details(self):
|
||||
filter_param = self._get_multi_param('Filter.member')
|
||||
account_details = iam_backend.get_account_authorization_details(filter_param)
|
||||
template = self.response_template(GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE)
|
||||
return template.render(
|
||||
instance_profiles=account_details['instance_profiles'],
|
||||
policies=account_details['managed_policies'],
|
||||
users=account_details['users'],
|
||||
groups=account_details['groups'],
|
||||
roles=account_details['roles']
|
||||
)
|
||||
|
||||
|
||||
ATTACH_ROLE_POLICY_TEMPLATE = """<AttachRolePolicyResponse>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -589,6 +607,25 @@ CREATE_POLICY_TEMPLATE = """<CreatePolicyResponse>
|
|||
</ResponseMetadata>
|
||||
</CreatePolicyResponse>"""
|
||||
|
||||
GET_POLICY_TEMPLATE = """<GetPolicyResponse>
|
||||
<GetPolicyResult>
|
||||
<Policy>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<Description>{{ policy.description }}</Description>
|
||||
<DefaultVersionId>{{ policy.default_version_id }}</DefaultVersionId>
|
||||
<PolicyId>{{ policy.id }}</PolicyId>
|
||||
<Path>{{ policy.path }}</Path>
|
||||
<Arn>{{ policy.arn }}</Arn>
|
||||
<AttachmentCount>{{ policy.attachment_count }}</AttachmentCount>
|
||||
<CreateDate>{{ policy.create_datetime.isoformat() }}</CreateDate>
|
||||
<UpdateDate>{{ policy.update_datetime.isoformat() }}</UpdateDate>
|
||||
</Policy>
|
||||
</GetPolicyResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>684f0917-3d22-11e4-a4a0-cffb9EXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</GetPolicyResponse>"""
|
||||
|
||||
LIST_ATTACHED_ROLE_POLICIES_TEMPLATE = """<ListAttachedRolePoliciesResponse>
|
||||
<ListAttachedRolePoliciesResult>
|
||||
{% if marker is none %}
|
||||
|
|
@ -1309,3 +1346,144 @@ DELETE_ACCOUNT_ALIAS_TEMPLATE = """<DeleteAccountAliasResponse xmlns="https://ia
|
|||
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</DeleteAccountAliasResponse>"""
|
||||
|
||||
|
||||
LIST_GROUPS_FOR_USER_TEMPLATE = """<ListGroupsForUserResponse>
|
||||
<ListGroupsForUserResult>
|
||||
<Groups>
|
||||
{% for group in groups %}
|
||||
<member>
|
||||
<Path>{{ group.path }}</Path>
|
||||
<GroupName>{{ group.name }}</GroupName>
|
||||
<GroupId>{{ group.id }}</GroupId>
|
||||
<Arn>{{ group.arn }}</Arn>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Groups>
|
||||
<IsTruncated>false</IsTruncated>
|
||||
</ListGroupsForUserResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</ListGroupsForUserResponse>"""
|
||||
|
||||
|
||||
GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<GetAccountAuthorizationDetailsResult>
|
||||
<IsTruncated>false</IsTruncated>
|
||||
<UserDetailList>
|
||||
{% for user in users %}
|
||||
<member>
|
||||
<GroupList />
|
||||
<AttachedManagedPolicies/>
|
||||
<UserId>{{ user.id }}</UserId>
|
||||
<Path>{{ user.path }}</Path>
|
||||
<UserName>{{ user.name }}</UserName>
|
||||
<Arn>{{ user.arn }}</Arn>
|
||||
<CreateDate>2012-05-09T15:45:35Z</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</UserDetailList>
|
||||
<Marker>
|
||||
EXAMPLEkakv9BCuUNFDtxWSyfzetYwEx2ADc8dnzfvERF5S6YMvXKx41t6gCl/eeaCX3Jo94/
|
||||
bKqezEAg8TEVS99EKFLxm3jtbpl25FDWEXAMPLE
|
||||
</Marker>
|
||||
<GroupDetailList>
|
||||
{% for group in groups %}
|
||||
<member>
|
||||
<GroupId>{{ group.id }}</GroupId>
|
||||
<AttachedManagedPolicies>
|
||||
{% for policy in group.managed_policies %}
|
||||
<member>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<PolicyArn>{{ policy.arn }}</PolicyArn>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</AttachedManagedPolicies>
|
||||
<GroupName>{{ group.name }}</GroupName>
|
||||
<Path>{{ group.path }}</Path>
|
||||
<Arn>{{ group.arn }}</Arn>
|
||||
<CreateDate>2012-05-09T16:27:11Z</CreateDate>
|
||||
<GroupPolicyList/>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</GroupDetailList>
|
||||
<RoleDetailList>
|
||||
{% for role in roles %}
|
||||
<member>
|
||||
<RolePolicyList/>
|
||||
<AttachedManagedPolicies>
|
||||
{% for policy in role.managed_policies %}
|
||||
<member>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<PolicyArn>{{ policy.arn }}</PolicyArn>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</AttachedManagedPolicies>
|
||||
<InstanceProfileList>
|
||||
{% for profile in instance_profiles %}
|
||||
<member>
|
||||
<Id>{{ profile.id }}</Id>
|
||||
<Roles>
|
||||
{% for role in profile.roles %}
|
||||
<member>
|
||||
<Path>{{ role.path }}</Path>
|
||||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>2012-05-09T15:45:35Z</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Roles>
|
||||
<InstanceProfileName>{{ profile.name }}</InstanceProfileName>
|
||||
<Path>{{ profile.path }}</Path>
|
||||
<Arn>{{ profile.arn }}</Arn>
|
||||
<CreateDate>2012-05-09T16:27:11Z</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</InstanceProfileList>
|
||||
<Path>{{ role.path }}</Path>
|
||||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>2014-07-30T17:09:20Z</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</RoleDetailList>
|
||||
<Policies>
|
||||
{% for policy in policies %}
|
||||
<member>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<DefaultVersionId>{{ policy.default_version_id }}</DefaultVersionId>
|
||||
<PolicyId>{{ policy.id }}</PolicyId>
|
||||
<Path>{{ policy.path }}</Path>
|
||||
<PolicyVersionList>
|
||||
<member>
|
||||
<Document>
|
||||
{"Version":"2012-10-17","Statement":{"Effect":"Allow",
|
||||
"Action":["iam:CreatePolicy","iam:CreatePolicyVersion",
|
||||
"iam:DeletePolicy","iam:DeletePolicyVersion","iam:GetPolicy",
|
||||
"iam:GetPolicyVersion","iam:ListPolicies",
|
||||
"iam:ListPolicyVersions","iam:SetDefaultPolicyVersion"],
|
||||
"Resource":"*"}}
|
||||
</Document>
|
||||
<IsDefaultVersion>true</IsDefaultVersion>
|
||||
<VersionId>v1</VersionId>
|
||||
<CreateDate>2012-05-09T16:27:11Z</CreateDate>
|
||||
</member>
|
||||
</PolicyVersionList>
|
||||
<Arn>{{ policy.arn }}</Arn>
|
||||
<AttachmentCount>1</AttachmentCount>
|
||||
<CreateDate>2012-05-09T16:27:11Z</CreateDate>
|
||||
<IsAttachable>true</IsAttachable>
|
||||
<UpdateDate>2012-05-09T16:27:11Z</UpdateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Policies>
|
||||
</GetAccountAuthorizationDetailsResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>92e79ae7-7399-11e4-8c85-4b53eEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</GetAccountAuthorizationDetailsResponse>"""
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
from __future__ import unicode_literals
|
||||
import time
|
||||
import boto3
|
||||
import string
|
||||
import random
|
||||
|
||||
import hashlib
|
||||
import uuid
|
||||
import random
|
||||
import re
|
||||
from datetime import datetime
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
import string
|
||||
import time
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
|
||||
import boto3
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from .exceptions import (
|
||||
ResourceNotFoundException,
|
||||
InvalidRequestException,
|
||||
|
|
@ -271,15 +274,37 @@ class IoTBackend(BaseBackend):
|
|||
|
||||
def list_thing_types(self, thing_type_name=None):
|
||||
if thing_type_name:
|
||||
# It's wierd but thing_type_name is filterd by forward match, not complete match
|
||||
# It's weird but thing_type_name is filtered by forward match, not complete match
|
||||
return [_ for _ in self.thing_types.values() if _.thing_type_name.startswith(thing_type_name)]
|
||||
thing_types = self.thing_types.values()
|
||||
return thing_types
|
||||
return self.thing_types.values()
|
||||
|
||||
def list_things(self, attribute_name, attribute_value, thing_type_name):
|
||||
# TODO: filter by attributess or thing_type
|
||||
things = self.things.values()
|
||||
return things
|
||||
def list_things(self, attribute_name, attribute_value, thing_type_name, max_results, token):
|
||||
all_things = [_.to_dict() for _ in self.things.values()]
|
||||
if attribute_name is not None and thing_type_name is not None:
|
||||
filtered_things = list(filter(lambda elem:
|
||||
attribute_name in elem["attributes"] and
|
||||
elem["attributes"][attribute_name] == attribute_value and
|
||||
"thingTypeName" in elem and
|
||||
elem["thingTypeName"] == thing_type_name, all_things))
|
||||
elif attribute_name is not None and thing_type_name is None:
|
||||
filtered_things = list(filter(lambda elem:
|
||||
attribute_name in elem["attributes"] and
|
||||
elem["attributes"][attribute_name] == attribute_value, all_things))
|
||||
elif attribute_name is None and thing_type_name is not None:
|
||||
filtered_things = list(
|
||||
filter(lambda elem: "thingTypeName" in elem and elem["thingTypeName"] == thing_type_name, all_things))
|
||||
else:
|
||||
filtered_things = all_things
|
||||
|
||||
if token is None:
|
||||
things = filtered_things[0:max_results]
|
||||
next_token = str(max_results) if len(filtered_things) > max_results else None
|
||||
else:
|
||||
token = int(token)
|
||||
things = filtered_things[token:token + max_results]
|
||||
next_token = str(token + max_results) if len(filtered_things) > token + max_results else None
|
||||
|
||||
return things, next_token
|
||||
|
||||
def describe_thing(self, thing_name):
|
||||
things = [_ for _ in self.things.values() if _.thing_name == thing_name]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import iot_backends
|
||||
import json
|
||||
|
||||
|
||||
class IoTResponse(BaseResponse):
|
||||
|
|
@ -32,30 +34,39 @@ class IoTResponse(BaseResponse):
|
|||
return json.dumps(dict(thingTypeName=thing_type_name, thingTypeArn=thing_type_arn))
|
||||
|
||||
def list_thing_types(self):
|
||||
# previous_next_token = self._get_param("nextToken")
|
||||
# max_results = self._get_int_param("maxResults")
|
||||
previous_next_token = self._get_param("nextToken")
|
||||
max_results = self._get_int_param("maxResults", 50) # not the default, but makes testing easier
|
||||
thing_type_name = self._get_param("thingTypeName")
|
||||
thing_types = self.iot_backend.list_thing_types(
|
||||
thing_type_name=thing_type_name
|
||||
)
|
||||
# TODO: implement pagination in the future
|
||||
next_token = None
|
||||
return json.dumps(dict(thingTypes=[_.to_dict() for _ in thing_types], nextToken=next_token))
|
||||
|
||||
thing_types = [_.to_dict() for _ in thing_types]
|
||||
if previous_next_token is None:
|
||||
result = thing_types[0:max_results]
|
||||
next_token = str(max_results) if len(thing_types) > max_results else None
|
||||
else:
|
||||
token = int(previous_next_token)
|
||||
result = thing_types[token:token + max_results]
|
||||
next_token = str(token + max_results) if len(thing_types) > token + max_results else None
|
||||
|
||||
return json.dumps(dict(thingTypes=result, nextToken=next_token))
|
||||
|
||||
def list_things(self):
|
||||
# previous_next_token = self._get_param("nextToken")
|
||||
# max_results = self._get_int_param("maxResults")
|
||||
previous_next_token = self._get_param("nextToken")
|
||||
max_results = self._get_int_param("maxResults", 50) # not the default, but makes testing easier
|
||||
attribute_name = self._get_param("attributeName")
|
||||
attribute_value = self._get_param("attributeValue")
|
||||
thing_type_name = self._get_param("thingTypeName")
|
||||
things = self.iot_backend.list_things(
|
||||
things, next_token = self.iot_backend.list_things(
|
||||
attribute_name=attribute_name,
|
||||
attribute_value=attribute_value,
|
||||
thing_type_name=thing_type_name,
|
||||
max_results=max_results,
|
||||
token=previous_next_token
|
||||
)
|
||||
# TODO: implement pagination in the future
|
||||
next_token = None
|
||||
return json.dumps(dict(things=[_.to_dict() for _ in things], nextToken=next_token))
|
||||
|
||||
return json.dumps(dict(things=things, nextToken=next_token))
|
||||
|
||||
def describe_thing(self):
|
||||
thing_name = self._get_param("thingName")
|
||||
|
|
|
|||
|
|
@ -29,5 +29,5 @@ class ResourceAlreadyExistsException(LogsClientError):
|
|||
self.code = 400
|
||||
super(ResourceAlreadyExistsException, self).__init__(
|
||||
'ResourceAlreadyExistsException',
|
||||
'The specified resource already exists.'
|
||||
'The specified log group already exists'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class LogStream:
|
|||
self.events += [LogEvent(self.lastIngestionTime, log_event) for log_event in log_events]
|
||||
self.uploadSequenceToken += 1
|
||||
|
||||
return self.uploadSequenceToken
|
||||
return '{:056d}'.format(self.uploadSequenceToken)
|
||||
|
||||
def get_log_events(self, log_group_name, log_stream_name, start_time, end_time, limit, next_token, start_from_head):
|
||||
def filter_func(event):
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ old_socksocket = None
|
|||
old_ssl_wrap_socket = None
|
||||
old_sslwrap_simple = None
|
||||
old_sslsocket = None
|
||||
old_sslcontext_wrap_socket = None
|
||||
|
||||
if PY3: # pragma: no cover
|
||||
basestring = (bytes, str)
|
||||
|
|
@ -100,6 +101,10 @@ try: # pragma: no cover
|
|||
if not PY3:
|
||||
old_sslwrap_simple = ssl.sslwrap_simple
|
||||
old_sslsocket = ssl.SSLSocket
|
||||
try:
|
||||
old_sslcontext_wrap_socket = ssl.SSLContext.wrap_socket
|
||||
except AttributeError:
|
||||
pass
|
||||
except ImportError: # pragma: no cover
|
||||
ssl = None
|
||||
|
||||
|
|
@ -281,7 +286,7 @@ class fakesock(object):
|
|||
return {
|
||||
'notAfter': shift.strftime('%b %d %H:%M:%S GMT'),
|
||||
'subjectAltName': (
|
||||
('DNS', '*%s' % self._host),
|
||||
('DNS', '*.%s' % self._host),
|
||||
('DNS', self._host),
|
||||
('DNS', '*'),
|
||||
),
|
||||
|
|
@ -772,7 +777,7 @@ class URIMatcher(object):
|
|||
|
||||
def __init__(self, uri, entries, match_querystring=False):
|
||||
self._match_querystring = match_querystring
|
||||
if type(uri).__name__ == 'SRE_Pattern':
|
||||
if type(uri).__name__ in ('SRE_Pattern', 'Pattern'):
|
||||
self.regex = uri
|
||||
result = urlsplit(uri.pattern)
|
||||
if result.scheme == 'https':
|
||||
|
|
@ -1012,6 +1017,10 @@ class httpretty(HttpBaseClass):
|
|||
if ssl:
|
||||
ssl.wrap_socket = old_ssl_wrap_socket
|
||||
ssl.SSLSocket = old_sslsocket
|
||||
try:
|
||||
ssl.SSLContext.wrap_socket = old_sslcontext_wrap_socket
|
||||
except AttributeError:
|
||||
pass
|
||||
ssl.__dict__['wrap_socket'] = old_ssl_wrap_socket
|
||||
ssl.__dict__['SSLSocket'] = old_sslsocket
|
||||
|
||||
|
|
@ -1058,6 +1067,14 @@ class httpretty(HttpBaseClass):
|
|||
ssl.wrap_socket = fake_wrap_socket
|
||||
ssl.SSLSocket = FakeSSLSocket
|
||||
|
||||
try:
|
||||
def fake_sslcontext_wrap_socket(cls, *args, **kwargs):
|
||||
return fake_wrap_socket(*args, **kwargs)
|
||||
|
||||
ssl.SSLContext.wrap_socket = fake_sslcontext_wrap_socket
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
ssl.__dict__['wrap_socket'] = fake_wrap_socket
|
||||
ssl.__dict__['SSLSocket'] = FakeSSLSocket
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ class Database(BaseModel):
|
|||
if self.publicly_accessible is None:
|
||||
self.publicly_accessible = True
|
||||
|
||||
self.copy_tags_to_snapshot = kwargs.get("copy_tags_to_snapshot")
|
||||
if self.copy_tags_to_snapshot is None:
|
||||
self.copy_tags_to_snapshot = False
|
||||
|
||||
self.backup_retention_period = kwargs.get("backup_retention_period")
|
||||
if self.backup_retention_period is None:
|
||||
self.backup_retention_period = 1
|
||||
|
|
@ -137,6 +141,7 @@ class Database(BaseModel):
|
|||
"multi_az": properties.get("MultiAZ"),
|
||||
"port": properties.get('Port', 3306),
|
||||
"publicly_accessible": properties.get("PubliclyAccessible"),
|
||||
"copy_tags_to_snapshot": properties.get("CopyTagsToSnapshot"),
|
||||
"region": region_name,
|
||||
"security_groups": security_groups,
|
||||
"storage_encrypted": properties.get("StorageEncrypted"),
|
||||
|
|
@ -217,6 +222,7 @@ class Database(BaseModel):
|
|||
</DBSubnetGroup>
|
||||
{% endif %}
|
||||
<PubliclyAccessible>{{ database.publicly_accessible }}</PubliclyAccessible>
|
||||
<CopyTagsToSnapshot>{{ database.copy_tags_to_snapshot }}</CopyTagsToSnapshot>
|
||||
<AutoMinorVersionUpgrade>{{ database.auto_minor_version_upgrade }}</AutoMinorVersionUpgrade>
|
||||
<AllocatedStorage>{{ database.allocated_storage }}</AllocatedStorage>
|
||||
<StorageEncrypted>{{ database.storage_encrypted }}</StorageEncrypted>
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ class Database(BaseModel):
|
|||
self.publicly_accessible = kwargs.get("publicly_accessible")
|
||||
if self.publicly_accessible is None:
|
||||
self.publicly_accessible = True
|
||||
self.copy_tags_to_snapshot = kwargs.get("copy_tags_to_snapshot")
|
||||
if self.copy_tags_to_snapshot is None:
|
||||
self.copy_tags_to_snapshot = False
|
||||
self.backup_retention_period = kwargs.get("backup_retention_period")
|
||||
if self.backup_retention_period is None:
|
||||
self.backup_retention_period = 1
|
||||
|
|
@ -208,6 +211,7 @@ class Database(BaseModel):
|
|||
</DBSubnetGroup>
|
||||
{% endif %}
|
||||
<PubliclyAccessible>{{ database.publicly_accessible }}</PubliclyAccessible>
|
||||
<CopyTagsToSnapshot>{{ database.copy_tags_to_snapshot }}</CopyTagsToSnapshot>
|
||||
<AutoMinorVersionUpgrade>{{ database.auto_minor_version_upgrade }}</AutoMinorVersionUpgrade>
|
||||
<AllocatedStorage>{{ database.allocated_storage }}</AllocatedStorage>
|
||||
<StorageEncrypted>{{ database.storage_encrypted }}</StorageEncrypted>
|
||||
|
|
@ -304,6 +308,7 @@ class Database(BaseModel):
|
|||
"db_parameter_group_name": properties.get('DBParameterGroupName'),
|
||||
"port": properties.get('Port', 3306),
|
||||
"publicly_accessible": properties.get("PubliclyAccessible"),
|
||||
"copy_tags_to_snapshot": properties.get("CopyTagsToSnapshot"),
|
||||
"region": region_name,
|
||||
"security_groups": security_groups,
|
||||
"storage_encrypted": properties.get("StorageEncrypted"),
|
||||
|
|
@ -362,6 +367,7 @@ class Database(BaseModel):
|
|||
"PreferredBackupWindow": "{{ database.preferred_backup_window }}",
|
||||
"PreferredMaintenanceWindow": "{{ database.preferred_maintenance_window }}",
|
||||
"PubliclyAccessible": "{{ database.publicly_accessible }}",
|
||||
"CopyTagsToSnapshot": "{{ database.copy_tags_to_snapshot }}",
|
||||
"AllocatedStorage": "{{ database.allocated_storage }}",
|
||||
"Endpoint": {
|
||||
"Address": "{{ database.address }}",
|
||||
|
|
@ -411,10 +417,10 @@ class Database(BaseModel):
|
|||
|
||||
|
||||
class Snapshot(BaseModel):
|
||||
def __init__(self, database, snapshot_id, tags=None):
|
||||
def __init__(self, database, snapshot_id, tags):
|
||||
self.database = database
|
||||
self.snapshot_id = snapshot_id
|
||||
self.tags = tags or []
|
||||
self.tags = tags
|
||||
self.created_at = iso_8601_datetime_with_milliseconds(datetime.datetime.now())
|
||||
|
||||
@property
|
||||
|
|
@ -456,6 +462,20 @@ class Snapshot(BaseModel):
|
|||
</DBSnapshot>""")
|
||||
return template.render(snapshot=self, database=self.database)
|
||||
|
||||
def get_tags(self):
|
||||
return self.tags
|
||||
|
||||
def add_tags(self, tags):
|
||||
new_keys = [tag_set['Key'] for tag_set in tags]
|
||||
self.tags = [tag_set for tag_set in self.tags if tag_set[
|
||||
'Key'] not in new_keys]
|
||||
self.tags.extend(tags)
|
||||
return self.tags
|
||||
|
||||
def remove_tags(self, tag_keys):
|
||||
self.tags = [tag_set for tag_set in self.tags if tag_set[
|
||||
'Key'] not in tag_keys]
|
||||
|
||||
|
||||
class SecurityGroup(BaseModel):
|
||||
|
||||
|
|
@ -691,6 +711,10 @@ class RDS2Backend(BaseBackend):
|
|||
raise DBSnapshotAlreadyExistsError(db_snapshot_identifier)
|
||||
if len(self.snapshots) >= int(os.environ.get('MOTO_RDS_SNAPSHOT_LIMIT', '100')):
|
||||
raise SnapshotQuotaExceededError()
|
||||
if tags is None:
|
||||
tags = list()
|
||||
if database.copy_tags_to_snapshot and not tags:
|
||||
tags = database.get_tags()
|
||||
snapshot = Snapshot(database, db_snapshot_identifier, tags)
|
||||
self.snapshots[db_snapshot_identifier] = snapshot
|
||||
return snapshot
|
||||
|
|
@ -787,13 +811,13 @@ class RDS2Backend(BaseBackend):
|
|||
|
||||
def delete_database(self, db_instance_identifier, db_snapshot_name=None):
|
||||
if db_instance_identifier in self.databases:
|
||||
if db_snapshot_name:
|
||||
self.create_snapshot(db_instance_identifier, db_snapshot_name)
|
||||
database = self.databases.pop(db_instance_identifier)
|
||||
if database.is_replica:
|
||||
primary = self.find_db_from_id(database.source_db_identifier)
|
||||
primary.remove_replica(database)
|
||||
database.status = 'deleting'
|
||||
if db_snapshot_name:
|
||||
self.snapshots[db_snapshot_name] = Snapshot(database, db_snapshot_name)
|
||||
return database
|
||||
else:
|
||||
raise DBInstanceNotFoundError(db_instance_identifier)
|
||||
|
|
@ -1028,8 +1052,8 @@ class RDS2Backend(BaseBackend):
|
|||
if resource_name in self.security_groups:
|
||||
return self.security_groups[resource_name].get_tags()
|
||||
elif resource_type == 'snapshot': # DB Snapshot
|
||||
# TODO: Complete call to tags on resource type DB Snapshot
|
||||
return []
|
||||
if resource_name in self.snapshots:
|
||||
return self.snapshots[resource_name].get_tags()
|
||||
elif resource_type == 'subgrp': # DB subnet group
|
||||
if resource_name in self.subnet_groups:
|
||||
return self.subnet_groups[resource_name].get_tags()
|
||||
|
|
@ -1059,7 +1083,8 @@ class RDS2Backend(BaseBackend):
|
|||
if resource_name in self.security_groups:
|
||||
return self.security_groups[resource_name].remove_tags(tag_keys)
|
||||
elif resource_type == 'snapshot': # DB Snapshot
|
||||
return None
|
||||
if resource_name in self.snapshots:
|
||||
return self.snapshots[resource_name].remove_tags(tag_keys)
|
||||
elif resource_type == 'subgrp': # DB subnet group
|
||||
if resource_name in self.subnet_groups:
|
||||
return self.subnet_groups[resource_name].remove_tags(tag_keys)
|
||||
|
|
@ -1088,7 +1113,8 @@ class RDS2Backend(BaseBackend):
|
|||
if resource_name in self.security_groups:
|
||||
return self.security_groups[resource_name].add_tags(tags)
|
||||
elif resource_type == 'snapshot': # DB Snapshot
|
||||
return []
|
||||
if resource_name in self.snapshots:
|
||||
return self.snapshots[resource_name].add_tags(tags)
|
||||
elif resource_type == 'subgrp': # DB subnet group
|
||||
if resource_name in self.subnet_groups:
|
||||
return self.subnet_groups[resource_name].add_tags(tags)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class RDS2Response(BaseResponse):
|
|||
"allocated_storage": self._get_int_param('AllocatedStorage'),
|
||||
"availability_zone": self._get_param("AvailabilityZone"),
|
||||
"backup_retention_period": self._get_param("BackupRetentionPeriod"),
|
||||
"copy_tags_to_snapshot": self._get_param("CopyTagsToSnapshot"),
|
||||
"db_instance_class": self._get_param('DBInstanceClass'),
|
||||
"db_instance_identifier": self._get_param('DBInstanceIdentifier'),
|
||||
"db_name": self._get_param("DBName"),
|
||||
|
|
@ -159,7 +160,7 @@ class RDS2Response(BaseResponse):
|
|||
def create_db_snapshot(self):
|
||||
db_instance_identifier = self._get_param('DBInstanceIdentifier')
|
||||
db_snapshot_identifier = self._get_param('DBSnapshotIdentifier')
|
||||
tags = self._get_param('Tags', [])
|
||||
tags = self.unpack_complex_list_params('Tags.Tag', ('Key', 'Value'))
|
||||
snapshot = self.backend.create_snapshot(db_instance_identifier, db_snapshot_identifier, tags)
|
||||
template = self.response_template(CREATE_SNAPSHOT_TEMPLATE)
|
||||
return template.render(snapshot=snapshot)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ class Cluster(TaggableResourceMixin, BaseModel):
|
|||
super(Cluster, self).__init__(region_name, tags)
|
||||
self.redshift_backend = redshift_backend
|
||||
self.cluster_identifier = cluster_identifier
|
||||
self.create_time = iso_8601_datetime_with_milliseconds(datetime.datetime.now())
|
||||
self.status = 'available'
|
||||
self.node_type = node_type
|
||||
self.master_username = master_username
|
||||
|
|
@ -237,6 +238,7 @@ class Cluster(TaggableResourceMixin, BaseModel):
|
|||
"Address": self.endpoint,
|
||||
"Port": self.port
|
||||
},
|
||||
'ClusterCreateTime': self.create_time,
|
||||
"PendingModifiedValues": [],
|
||||
"Tags": self.tags,
|
||||
"IamRoles": [{
|
||||
|
|
|
|||
|
|
@ -27,8 +27,14 @@ class FakeDeleteMarker(BaseModel):
|
|||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
self.name = key.name
|
||||
self.last_modified = datetime.datetime.utcnow()
|
||||
self._version_id = key.version_id + 1
|
||||
|
||||
@property
|
||||
def last_modified_ISO8601(self):
|
||||
return iso_8601_datetime_with_milliseconds(self.last_modified)
|
||||
|
||||
@property
|
||||
def version_id(self):
|
||||
return self._version_id
|
||||
|
|
@ -630,10 +636,7 @@ class S3Backend(BaseBackend):
|
|||
latest_versions = {}
|
||||
|
||||
for version in versions:
|
||||
if isinstance(version, FakeDeleteMarker):
|
||||
name = version.key.name
|
||||
else:
|
||||
name = version.name
|
||||
name = version.name
|
||||
version_id = version.version_id
|
||||
maximum_version_per_key[name] = max(
|
||||
version_id,
|
||||
|
|
|
|||
|
|
@ -1273,10 +1273,10 @@ S3_BUCKET_GET_VERSIONS = """<?xml version="1.0" encoding="UTF-8"?>
|
|||
{% endfor %}
|
||||
{% for marker in delete_marker_list %}
|
||||
<DeleteMarker>
|
||||
<Key>{{ marker.key.name }}</Key>
|
||||
<Key>{{ marker.name }}</Key>
|
||||
<VersionId>{{ marker.version_id }}</VersionId>
|
||||
<IsLatest>{% if latest_versions[marker.key.name] == marker.version_id %}true{% else %}false{% endif %}</IsLatest>
|
||||
<LastModified>{{ marker.key.last_modified_ISO8601 }}</LastModified>
|
||||
<IsLatest>{% if latest_versions[marker.name] == marker.version_id %}true{% else %}false{% endif %}</IsLatest>
|
||||
<LastModified>{{ marker.last_modified_ISO8601 }}</LastModified>
|
||||
<Owner>
|
||||
<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>
|
||||
<DisplayName>webfile</DisplayName>
|
||||
|
|
|
|||
|
|
@ -33,20 +33,27 @@ class SecretsManagerBackend(BaseBackend):
|
|||
self.name = kwargs.get('name', '')
|
||||
self.createdate = int(time.time())
|
||||
self.secret_string = ''
|
||||
self.rotation_enabled = False
|
||||
self.rotation_lambda_arn = ''
|
||||
self.auto_rotate_after_days = 0
|
||||
self.version_id = ''
|
||||
|
||||
def reset(self):
|
||||
region_name = self.region
|
||||
self.__dict__ = {}
|
||||
self.__init__(region_name)
|
||||
|
||||
def _is_valid_identifier(self, identifier):
|
||||
return identifier in (self.name, self.secret_id)
|
||||
|
||||
def get_secret_value(self, secret_id, version_id, version_stage):
|
||||
|
||||
if self.secret_id == '':
|
||||
if not self._is_valid_identifier(secret_id):
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
response = json.dumps({
|
||||
"ARN": secret_arn(self.region, self.secret_id),
|
||||
"Name": self.secret_id,
|
||||
"Name": self.name,
|
||||
"VersionId": "A435958A-D821-4193-B719-B7769357AER4",
|
||||
"SecretString": self.secret_string,
|
||||
"VersionStages": [
|
||||
|
|
@ -61,15 +68,94 @@ class SecretsManagerBackend(BaseBackend):
|
|||
|
||||
self.secret_string = secret_string
|
||||
self.secret_id = name
|
||||
self.name = name
|
||||
|
||||
response = json.dumps({
|
||||
"ARN": secret_arn(self.region, name),
|
||||
"Name": self.secret_id,
|
||||
"Name": self.name,
|
||||
"VersionId": "A435958A-D821-4193-B719-B7769357AER4",
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
def describe_secret(self, secret_id):
|
||||
if not self._is_valid_identifier(secret_id):
|
||||
raise ResourceNotFoundException
|
||||
|
||||
response = json.dumps({
|
||||
"ARN": secret_arn(self.region, self.secret_id),
|
||||
"Name": self.name,
|
||||
"Description": "",
|
||||
"KmsKeyId": "",
|
||||
"RotationEnabled": self.rotation_enabled,
|
||||
"RotationLambdaARN": self.rotation_lambda_arn,
|
||||
"RotationRules": {
|
||||
"AutomaticallyAfterDays": self.auto_rotate_after_days
|
||||
},
|
||||
"LastRotatedDate": None,
|
||||
"LastChangedDate": None,
|
||||
"LastAccessedDate": None,
|
||||
"DeletedDate": None,
|
||||
"Tags": [
|
||||
{
|
||||
"Key": "",
|
||||
"Value": ""
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
def rotate_secret(self, secret_id, client_request_token=None,
|
||||
rotation_lambda_arn=None, rotation_rules=None):
|
||||
|
||||
rotation_days = 'AutomaticallyAfterDays'
|
||||
|
||||
if not self._is_valid_identifier(secret_id):
|
||||
raise ResourceNotFoundException
|
||||
|
||||
if client_request_token:
|
||||
token_length = len(client_request_token)
|
||||
if token_length < 32 or token_length > 64:
|
||||
msg = (
|
||||
'ClientRequestToken '
|
||||
'must be 32-64 characters long.'
|
||||
)
|
||||
raise InvalidParameterException(msg)
|
||||
|
||||
if rotation_lambda_arn:
|
||||
if len(rotation_lambda_arn) > 2048:
|
||||
msg = (
|
||||
'RotationLambdaARN '
|
||||
'must <= 2048 characters long.'
|
||||
)
|
||||
raise InvalidParameterException(msg)
|
||||
|
||||
if rotation_rules:
|
||||
if rotation_days in rotation_rules:
|
||||
rotation_period = rotation_rules[rotation_days]
|
||||
if rotation_period < 1 or rotation_period > 1000:
|
||||
msg = (
|
||||
'RotationRules.AutomaticallyAfterDays '
|
||||
'must be within 1-1000.'
|
||||
)
|
||||
raise InvalidParameterException(msg)
|
||||
|
||||
self.version_id = client_request_token or ''
|
||||
self.rotation_lambda_arn = rotation_lambda_arn or ''
|
||||
if rotation_rules:
|
||||
self.auto_rotate_after_days = rotation_rules.get(rotation_days, 0)
|
||||
if self.auto_rotate_after_days > 0:
|
||||
self.rotation_enabled = True
|
||||
|
||||
response = json.dumps({
|
||||
"ARN": secret_arn(self.region, self.secret_id),
|
||||
"Name": self.name,
|
||||
"VersionId": self.version_id
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
def get_random_password(self, password_length,
|
||||
exclude_characters, exclude_numbers,
|
||||
exclude_punctuation, exclude_uppercase,
|
||||
|
|
|
|||
|
|
@ -44,3 +44,21 @@ class SecretsManagerResponse(BaseResponse):
|
|||
include_space=include_space,
|
||||
require_each_included_type=require_each_included_type
|
||||
)
|
||||
|
||||
def describe_secret(self):
|
||||
secret_id = self._get_param('SecretId')
|
||||
return secretsmanager_backends[self.region].describe_secret(
|
||||
secret_id=secret_id
|
||||
)
|
||||
|
||||
def rotate_secret(self):
|
||||
client_request_token = self._get_param('ClientRequestToken')
|
||||
rotation_lambda_arn = self._get_param('RotationLambdaARN')
|
||||
rotation_rules = self._get_param('RotationRules')
|
||||
secret_id = self._get_param('SecretId')
|
||||
return secretsmanager_backends[self.region].rotate_secret(
|
||||
secret_id=secret_id,
|
||||
client_request_token=client_request_token,
|
||||
rotation_lambda_arn=rotation_lambda_arn,
|
||||
rotation_rules=rotation_rules
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ class DomainDispatcherApplication(object):
|
|||
self.service = service
|
||||
|
||||
def get_backend_for_host(self, host):
|
||||
if host == 'moto_api':
|
||||
return host
|
||||
|
||||
if self.service:
|
||||
return self.service
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ class SESBackend(BaseBackend):
|
|||
self.sent_messages = []
|
||||
self.sent_message_count = 0
|
||||
|
||||
def _is_verified_address(self, address):
|
||||
def _is_verified_address(self, source):
|
||||
_, address = parseaddr(source)
|
||||
if address in self.addresses:
|
||||
return True
|
||||
user, host = address.split('@', 1)
|
||||
|
|
|
|||
|
|
@ -385,10 +385,22 @@ class SQSBackend(BaseBackend):
|
|||
def create_queue(self, name, **kwargs):
|
||||
queue = self.queues.get(name)
|
||||
if queue:
|
||||
# Queue already exist. If attributes don't match, throw error
|
||||
for key, value in kwargs.items():
|
||||
if getattr(queue, camelcase_to_underscores(key)) != value:
|
||||
raise QueueAlreadyExists("The specified queue already exists.")
|
||||
try:
|
||||
kwargs.pop('region')
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
new_queue = Queue(name, region=self.region_name, **kwargs)
|
||||
|
||||
queue_attributes = queue.attributes
|
||||
new_queue_attributes = new_queue.attributes
|
||||
|
||||
for key in ['CreatedTimestamp', 'LastModifiedTimestamp']:
|
||||
queue_attributes.pop(key)
|
||||
new_queue_attributes.pop(key)
|
||||
|
||||
if queue_attributes != new_queue_attributes:
|
||||
raise QueueAlreadyExists("The specified queue already exists.")
|
||||
else:
|
||||
try:
|
||||
kwargs.pop('region')
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ class SQSResponse(BaseResponse):
|
|||
try:
|
||||
wait_time = int(self.querystring.get("WaitTimeSeconds")[0])
|
||||
except TypeError:
|
||||
wait_time = queue.receive_message_wait_time_seconds
|
||||
wait_time = int(queue.receive_message_wait_time_seconds)
|
||||
|
||||
if wait_time < 0 or wait_time > 20:
|
||||
return self._error(
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ from collections import defaultdict
|
|||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core.exceptions import RESTError
|
||||
from moto.ec2 import ec2_backends
|
||||
from moto.cloudformation import cloudformation_backends
|
||||
|
||||
import datetime
|
||||
import time
|
||||
import uuid
|
||||
import itertools
|
||||
|
||||
|
||||
class Parameter(BaseModel):
|
||||
|
|
@ -67,7 +69,7 @@ class Command(BaseModel):
|
|||
instance_ids=None, max_concurrency='', max_errors='',
|
||||
notification_config=None, output_s3_bucket_name='',
|
||||
output_s3_key_prefix='', output_s3_region='', parameters=None,
|
||||
service_role_arn='', targets=None):
|
||||
service_role_arn='', targets=None, backend_region='us-east-1'):
|
||||
|
||||
if instance_ids is None:
|
||||
instance_ids = []
|
||||
|
|
@ -88,9 +90,9 @@ class Command(BaseModel):
|
|||
self.status = 'Success'
|
||||
self.status_details = 'Details placeholder'
|
||||
|
||||
now = datetime.datetime.now()
|
||||
self.requested_date_time = now.isoformat()
|
||||
expires_after = now + datetime.timedelta(0, timeout_seconds)
|
||||
self.requested_date_time = datetime.datetime.now()
|
||||
self.requested_date_time_iso = self.requested_date_time.isoformat()
|
||||
expires_after = self.requested_date_time + datetime.timedelta(0, timeout_seconds)
|
||||
self.expires_after = expires_after.isoformat()
|
||||
|
||||
self.comment = comment
|
||||
|
|
@ -105,6 +107,32 @@ class Command(BaseModel):
|
|||
self.parameters = parameters
|
||||
self.service_role_arn = service_role_arn
|
||||
self.targets = targets
|
||||
self.backend_region = backend_region
|
||||
|
||||
# Get instance ids from a cloud formation stack target.
|
||||
stack_instance_ids = [self.get_instance_ids_by_stack_ids(target['Values']) for
|
||||
target in self.targets if
|
||||
target['Key'] == 'tag:aws:cloudformation:stack-name']
|
||||
|
||||
self.instance_ids += list(itertools.chain.from_iterable(stack_instance_ids))
|
||||
|
||||
# Create invocations with a single run command plugin.
|
||||
self.invocations = []
|
||||
for instance_id in self.instance_ids:
|
||||
self.invocations.append(
|
||||
self.invocation_response(instance_id, "aws:runShellScript"))
|
||||
|
||||
def get_instance_ids_by_stack_ids(self, stack_ids):
|
||||
instance_ids = []
|
||||
cloudformation_backend = cloudformation_backends[self.backend_region]
|
||||
for stack_id in stack_ids:
|
||||
stack_resources = cloudformation_backend.list_stack_resources(stack_id)
|
||||
instance_resources = [
|
||||
instance.id for instance in stack_resources
|
||||
if instance.type == "AWS::EC2::Instance"]
|
||||
instance_ids.extend(instance_resources)
|
||||
|
||||
return instance_ids
|
||||
|
||||
def response_object(self):
|
||||
r = {
|
||||
|
|
@ -122,7 +150,7 @@ class Command(BaseModel):
|
|||
'OutputS3BucketName': self.output_s3_bucket_name,
|
||||
'OutputS3KeyPrefix': self.output_s3_key_prefix,
|
||||
'Parameters': self.parameters,
|
||||
'RequestedDateTime': self.requested_date_time,
|
||||
'RequestedDateTime': self.requested_date_time_iso,
|
||||
'ServiceRole': self.service_role_arn,
|
||||
'Status': self.status,
|
||||
'StatusDetails': self.status_details,
|
||||
|
|
@ -132,6 +160,50 @@ class Command(BaseModel):
|
|||
|
||||
return r
|
||||
|
||||
def invocation_response(self, instance_id, plugin_name):
|
||||
# Calculate elapsed time from requested time and now. Use a hardcoded
|
||||
# elapsed time since there is no easy way to convert a timedelta to
|
||||
# an ISO 8601 duration string.
|
||||
elapsed_time_iso = "PT5M"
|
||||
elapsed_time_delta = datetime.timedelta(minutes=5)
|
||||
end_time = self.requested_date_time + elapsed_time_delta
|
||||
|
||||
r = {
|
||||
'CommandId': self.command_id,
|
||||
'InstanceId': instance_id,
|
||||
'Comment': self.comment,
|
||||
'DocumentName': self.document_name,
|
||||
'PluginName': plugin_name,
|
||||
'ResponseCode': 0,
|
||||
'ExecutionStartDateTime': self.requested_date_time_iso,
|
||||
'ExecutionElapsedTime': elapsed_time_iso,
|
||||
'ExecutionEndDateTime': end_time.isoformat(),
|
||||
'Status': 'Success',
|
||||
'StatusDetails': 'Success',
|
||||
'StandardOutputContent': '',
|
||||
'StandardOutputUrl': '',
|
||||
'StandardErrorContent': '',
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
def get_invocation(self, instance_id, plugin_name):
|
||||
invocation = next(
|
||||
(invocation for invocation in self.invocations
|
||||
if invocation['InstanceId'] == instance_id), None)
|
||||
|
||||
if invocation is None:
|
||||
raise RESTError(
|
||||
'InvocationDoesNotExist',
|
||||
'An error occurred (InvocationDoesNotExist) when calling the GetCommandInvocation operation')
|
||||
|
||||
if plugin_name is not None and invocation['PluginName'] != plugin_name:
|
||||
raise RESTError(
|
||||
'InvocationDoesNotExist',
|
||||
'An error occurred (InvocationDoesNotExist) when calling the GetCommandInvocation operation')
|
||||
|
||||
return invocation
|
||||
|
||||
|
||||
class SimpleSystemManagerBackend(BaseBackend):
|
||||
|
||||
|
|
@ -140,6 +212,11 @@ class SimpleSystemManagerBackend(BaseBackend):
|
|||
self._resource_tags = defaultdict(lambda: defaultdict(dict))
|
||||
self._commands = []
|
||||
|
||||
# figure out what region we're in
|
||||
for region, backend in ssm_backends.items():
|
||||
if backend == self:
|
||||
self._region = region
|
||||
|
||||
def delete_parameter(self, name):
|
||||
try:
|
||||
del self._parameters[name]
|
||||
|
|
@ -260,7 +337,8 @@ class SimpleSystemManagerBackend(BaseBackend):
|
|||
output_s3_region=kwargs.get('OutputS3Region', ''),
|
||||
parameters=kwargs.get('Parameters', {}),
|
||||
service_role_arn=kwargs.get('ServiceRoleArn', ''),
|
||||
targets=kwargs.get('Targets', []))
|
||||
targets=kwargs.get('Targets', []),
|
||||
backend_region=self._region)
|
||||
|
||||
self._commands.append(command)
|
||||
return {
|
||||
|
|
@ -298,6 +376,18 @@ class SimpleSystemManagerBackend(BaseBackend):
|
|||
command for command in self._commands
|
||||
if instance_id in command.instance_ids]
|
||||
|
||||
def get_command_invocation(self, **kwargs):
|
||||
"""
|
||||
https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetCommandInvocation.html
|
||||
"""
|
||||
|
||||
command_id = kwargs.get('CommandId')
|
||||
instance_id = kwargs.get('InstanceId')
|
||||
plugin_name = kwargs.get('PluginName', None)
|
||||
|
||||
command = self.get_command_by_id(command_id)
|
||||
return command.get_invocation(instance_id, plugin_name)
|
||||
|
||||
|
||||
ssm_backends = {}
|
||||
for region, ec2_backend in ec2_backends.items():
|
||||
|
|
|
|||
|
|
@ -210,3 +210,8 @@ class SimpleSystemManagerResponse(BaseResponse):
|
|||
return json.dumps(
|
||||
self.ssm_backend.list_commands(**self.request_params)
|
||||
)
|
||||
|
||||
def get_command_invocation(self):
|
||||
return json.dumps(
|
||||
self.ssm_backend.get_command_invocation(**self.request_params)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue