Merge remote-tracking branch 'upstream/master' into feat/cognitoidp-add-update-identity-provider
This commit is contained in:
commit
c7d3e1f8b6
52 changed files with 3081 additions and 229 deletions
|
|
@ -13,6 +13,7 @@ from .cloudformation import mock_cloudformation, mock_cloudformation_deprecated
|
|||
from .cloudwatch import mock_cloudwatch, mock_cloudwatch_deprecated # flake8: noqa
|
||||
from .cognitoidentity import mock_cognitoidentity, mock_cognitoidentity_deprecated # flake8: noqa
|
||||
from .cognitoidp import mock_cognitoidp, mock_cognitoidp_deprecated # flake8: noqa
|
||||
from .config import mock_config # flake8: noqa
|
||||
from .datapipeline import mock_datapipeline, mock_datapipeline_deprecated # flake8: noqa
|
||||
from .dynamodb import mock_dynamodb, mock_dynamodb_deprecated # flake8: noqa
|
||||
from .dynamodb2 import mock_dynamodb2, mock_dynamodb2_deprecated # flake8: noqa
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ class CertBundle(BaseModel):
|
|||
'KeyAlgorithm': key_algo,
|
||||
'NotAfter': datetime_to_epoch(self._cert.not_valid_after),
|
||||
'NotBefore': datetime_to_epoch(self._cert.not_valid_before),
|
||||
'Serial': self._cert.serial,
|
||||
'Serial': self._cert.serial_number,
|
||||
'SignatureAlgorithm': self._cert.signature_algorithm_oid._name.upper().replace('ENCRYPTION', ''),
|
||||
'Status': self.status, # One of PENDING_VALIDATION, ISSUED, INACTIVE, EXPIRED, VALIDATION_TIMED_OUT, REVOKED, FAILED.
|
||||
'Subject': 'CN={0}'.format(self.common_name),
|
||||
|
|
|
|||
|
|
@ -500,6 +500,11 @@ class LambdaStorage(object):
|
|||
except ValueError:
|
||||
return self._functions[name]['latest']
|
||||
|
||||
def list_versions_by_function(self, name):
|
||||
if name not in self._functions:
|
||||
return None
|
||||
return [self._functions[name]['latest']]
|
||||
|
||||
def get_arn(self, arn):
|
||||
return self._arns.get(arn, None)
|
||||
|
||||
|
|
@ -607,6 +612,9 @@ class LambdaBackend(BaseBackend):
|
|||
def get_function(self, function_name, qualifier=None):
|
||||
return self._lambdas.get_function(function_name, qualifier)
|
||||
|
||||
def list_versions_by_function(self, function_name):
|
||||
return self._lambdas.list_versions_by_function(function_name)
|
||||
|
||||
def get_function_by_arn(self, function_arn):
|
||||
return self._lambdas.get_arn(function_arn)
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,11 @@ class LambdaResponse(BaseResponse):
|
|||
self.setup_class(request, full_url, headers)
|
||||
if request.method == 'GET':
|
||||
# This is ListVersionByFunction
|
||||
raise ValueError("Cannot handle request")
|
||||
|
||||
path = request.path if hasattr(request, 'path') else path_url(request.url)
|
||||
function_name = path.split('/')[-2]
|
||||
return self._list_versions_by_function(function_name)
|
||||
|
||||
elif request.method == 'POST':
|
||||
return self._publish_function(request, full_url, headers)
|
||||
else:
|
||||
|
|
@ -151,6 +155,19 @@ class LambdaResponse(BaseResponse):
|
|||
|
||||
return 200, {}, json.dumps(result)
|
||||
|
||||
def _list_versions_by_function(self, function_name):
|
||||
result = {
|
||||
'Versions': []
|
||||
}
|
||||
|
||||
functions = self.lambda_backend.list_versions_by_function(function_name)
|
||||
if functions:
|
||||
for fn in functions:
|
||||
json_data = fn.get_configuration()
|
||||
result['Versions'].append(json_data)
|
||||
|
||||
return 200, {}, json.dumps(result)
|
||||
|
||||
def _create_function(self, request, full_url, headers):
|
||||
try:
|
||||
fn = self.lambda_backend.create_function(self.json_body)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ from moto.iot import iot_backends
|
|||
from moto.iotdata import iotdata_backends
|
||||
from moto.batch import batch_backends
|
||||
from moto.resourcegroupstaggingapi import resourcegroupstaggingapi_backends
|
||||
|
||||
from moto.config import config_backends
|
||||
|
||||
BACKENDS = {
|
||||
'acm': acm_backends,
|
||||
|
|
@ -57,6 +57,7 @@ BACKENDS = {
|
|||
'cloudwatch': cloudwatch_backends,
|
||||
'cognito-identity': cognitoidentity_backends,
|
||||
'cognito-idp': cognitoidp_backends,
|
||||
'config': config_backends,
|
||||
'datapipeline': datapipeline_backends,
|
||||
'dynamodb': dynamodb_backends,
|
||||
'dynamodb2': dynamodb_backends2,
|
||||
|
|
|
|||
|
|
@ -127,6 +127,49 @@ class FakeStack(BaseModel):
|
|||
self.status = "DELETE_COMPLETE"
|
||||
|
||||
|
||||
class FakeChange(BaseModel):
|
||||
|
||||
def __init__(self, action, logical_resource_id, resource_type):
|
||||
self.action = action
|
||||
self.logical_resource_id = logical_resource_id
|
||||
self.resource_type = resource_type
|
||||
|
||||
|
||||
class FakeChangeSet(FakeStack):
|
||||
|
||||
def __init__(self, stack_id, stack_name, stack_template, change_set_id, change_set_name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None, cross_stack_resources=None):
|
||||
super(FakeChangeSet, self).__init__(
|
||||
stack_id,
|
||||
stack_name,
|
||||
stack_template,
|
||||
parameters,
|
||||
region_name,
|
||||
notification_arns=notification_arns,
|
||||
tags=tags,
|
||||
role_arn=role_arn,
|
||||
cross_stack_resources=cross_stack_resources,
|
||||
create_change_set=True,
|
||||
)
|
||||
self.stack_name = stack_name
|
||||
self.change_set_id = change_set_id
|
||||
self.change_set_name = change_set_name
|
||||
self.changes = self.diff(template=template, parameters=parameters)
|
||||
|
||||
def diff(self, template, parameters=None):
|
||||
self.template = template
|
||||
self._parse_template()
|
||||
changes = []
|
||||
resources_by_action = self.resource_map.diff(self.template_dict, parameters)
|
||||
for action, resources in resources_by_action.items():
|
||||
for resource_name, resource in resources.items():
|
||||
changes.append(FakeChange(
|
||||
action=action,
|
||||
logical_resource_id=resource_name,
|
||||
resource_type=resource['ResourceType'],
|
||||
))
|
||||
return changes
|
||||
|
||||
|
||||
class FakeEvent(BaseModel):
|
||||
|
||||
def __init__(self, stack_id, stack_name, logical_resource_id, physical_resource_id, resource_type, resource_status, resource_status_reason=None, resource_properties=None):
|
||||
|
|
@ -171,24 +214,62 @@ class CloudFormationBackend(BaseBackend):
|
|||
return new_stack
|
||||
|
||||
def create_change_set(self, stack_name, change_set_name, template, parameters, region_name, change_set_type, notification_arns=None, tags=None, role_arn=None):
|
||||
stack_id = None
|
||||
stack_template = None
|
||||
if change_set_type == 'UPDATE':
|
||||
stacks = self.stacks.values()
|
||||
stack = None
|
||||
for s in stacks:
|
||||
if s.name == stack_name:
|
||||
stack = s
|
||||
stack_id = stack.stack_id
|
||||
stack_template = stack.template
|
||||
if stack is None:
|
||||
raise ValidationError(stack_name)
|
||||
|
||||
else:
|
||||
stack = self.create_stack(stack_name, template, parameters,
|
||||
region_name, notification_arns, tags,
|
||||
role_arn, create_change_set=True)
|
||||
stack_id = generate_stack_id(stack_name)
|
||||
stack_template = template
|
||||
|
||||
change_set_id = generate_changeset_id(change_set_name, region_name)
|
||||
self.stacks[change_set_name] = {'Id': change_set_id,
|
||||
'StackId': stack.stack_id}
|
||||
self.change_sets[change_set_id] = stack
|
||||
return change_set_id, stack.stack_id
|
||||
new_change_set = FakeChangeSet(
|
||||
stack_id=stack_id,
|
||||
stack_name=stack_name,
|
||||
stack_template=stack_template,
|
||||
change_set_id=change_set_id,
|
||||
change_set_name=change_set_name,
|
||||
template=template,
|
||||
parameters=parameters,
|
||||
region_name=region_name,
|
||||
notification_arns=notification_arns,
|
||||
tags=tags,
|
||||
role_arn=role_arn,
|
||||
cross_stack_resources=self.exports
|
||||
)
|
||||
self.change_sets[change_set_id] = new_change_set
|
||||
self.stacks[stack_id] = new_change_set
|
||||
return change_set_id, stack_id
|
||||
|
||||
def delete_change_set(self, change_set_name, stack_name=None):
|
||||
if change_set_name in self.change_sets:
|
||||
# This means arn was passed in
|
||||
del self.change_sets[change_set_name]
|
||||
else:
|
||||
for cs in self.change_sets:
|
||||
if self.change_sets[cs].change_set_name == change_set_name:
|
||||
del self.change_sets[cs]
|
||||
|
||||
def describe_change_set(self, change_set_name, stack_name=None):
|
||||
change_set = None
|
||||
if change_set_name in self.change_sets:
|
||||
# This means arn was passed in
|
||||
change_set = self.change_sets[change_set_name]
|
||||
else:
|
||||
for cs in self.change_sets:
|
||||
if self.change_sets[cs].change_set_name == change_set_name:
|
||||
change_set = self.change_sets[cs]
|
||||
if change_set is None:
|
||||
raise ValidationError(change_set_name)
|
||||
return change_set
|
||||
|
||||
def execute_change_set(self, change_set_name, stack_name=None):
|
||||
stack = None
|
||||
|
|
@ -197,7 +278,7 @@ class CloudFormationBackend(BaseBackend):
|
|||
stack = self.change_sets[change_set_name]
|
||||
else:
|
||||
for cs in self.change_sets:
|
||||
if self.change_sets[cs].name == change_set_name:
|
||||
if self.change_sets[cs].change_set_name == change_set_name:
|
||||
stack = self.change_sets[cs]
|
||||
if stack is None:
|
||||
raise ValidationError(stack_name)
|
||||
|
|
@ -223,6 +304,9 @@ class CloudFormationBackend(BaseBackend):
|
|||
else:
|
||||
return list(stacks)
|
||||
|
||||
def list_change_sets(self):
|
||||
return self.change_sets.values()
|
||||
|
||||
def list_stacks(self):
|
||||
return [
|
||||
v for v in self.stacks.values()
|
||||
|
|
|
|||
|
|
@ -465,36 +465,70 @@ class ResourceMap(collections.Mapping):
|
|||
ec2_models.ec2_backends[self._region_name].create_tags(
|
||||
[self[resource].physical_resource_id], self.tags)
|
||||
|
||||
def update(self, template, parameters=None):
|
||||
def diff(self, template, parameters=None):
|
||||
if parameters:
|
||||
self.input_parameters = parameters
|
||||
self.load_mapping()
|
||||
self.load_parameters()
|
||||
self.load_conditions()
|
||||
|
||||
old_template = self._resource_json_map
|
||||
new_template = template['Resources']
|
||||
|
||||
resource_names_by_action = {
|
||||
'Add': set(new_template) - set(old_template),
|
||||
'Modify': set(name for name in new_template if name in old_template and new_template[
|
||||
name] != old_template[name]),
|
||||
'Remove': set(old_template) - set(new_template)
|
||||
}
|
||||
resources_by_action = {
|
||||
'Add': {},
|
||||
'Modify': {},
|
||||
'Remove': {},
|
||||
}
|
||||
|
||||
for resource_name in resource_names_by_action['Add']:
|
||||
resources_by_action['Add'][resource_name] = {
|
||||
'LogicalResourceId': resource_name,
|
||||
'ResourceType': new_template[resource_name]['Type']
|
||||
}
|
||||
|
||||
for resource_name in resource_names_by_action['Modify']:
|
||||
resources_by_action['Modify'][resource_name] = {
|
||||
'LogicalResourceId': resource_name,
|
||||
'ResourceType': new_template[resource_name]['Type']
|
||||
}
|
||||
|
||||
for resource_name in resource_names_by_action['Remove']:
|
||||
resources_by_action['Remove'][resource_name] = {
|
||||
'LogicalResourceId': resource_name,
|
||||
'ResourceType': old_template[resource_name]['Type']
|
||||
}
|
||||
|
||||
return resources_by_action
|
||||
|
||||
def update(self, template, parameters=None):
|
||||
resources_by_action = self.diff(template, parameters)
|
||||
|
||||
old_template = self._resource_json_map
|
||||
new_template = template['Resources']
|
||||
self._resource_json_map = new_template
|
||||
|
||||
new_resource_names = set(new_template) - set(old_template)
|
||||
for resource_name in new_resource_names:
|
||||
for resource_name, resource in resources_by_action['Add'].items():
|
||||
resource_json = new_template[resource_name]
|
||||
new_resource = parse_and_create_resource(
|
||||
resource_name, resource_json, self, self._region_name)
|
||||
self._parsed_resources[resource_name] = new_resource
|
||||
|
||||
removed_resource_names = set(old_template) - set(new_template)
|
||||
for resource_name in removed_resource_names:
|
||||
for resource_name, resource in resources_by_action['Remove'].items():
|
||||
resource_json = old_template[resource_name]
|
||||
parse_and_delete_resource(
|
||||
resource_name, resource_json, self, self._region_name)
|
||||
self._parsed_resources.pop(resource_name)
|
||||
|
||||
resources_to_update = set(name for name in new_template if name in old_template and new_template[
|
||||
name] != old_template[name])
|
||||
tries = 1
|
||||
while resources_to_update and tries < 5:
|
||||
for resource_name in resources_to_update.copy():
|
||||
while resources_by_action['Modify'] and tries < 5:
|
||||
for resource_name, resource in resources_by_action['Modify'].copy().items():
|
||||
resource_json = new_template[resource_name]
|
||||
try:
|
||||
changed_resource = parse_and_update_resource(
|
||||
|
|
@ -505,7 +539,7 @@ class ResourceMap(collections.Mapping):
|
|||
last_exception = e
|
||||
else:
|
||||
self._parsed_resources[resource_name] = changed_resource
|
||||
resources_to_update.remove(resource_name)
|
||||
del resources_by_action['Modify'][resource_name]
|
||||
tries += 1
|
||||
if tries == 5:
|
||||
raise last_exception
|
||||
|
|
|
|||
|
|
@ -120,6 +120,31 @@ class CloudFormationResponse(BaseResponse):
|
|||
template = self.response_template(CREATE_CHANGE_SET_RESPONSE_TEMPLATE)
|
||||
return template.render(stack_id=stack_id, change_set_id=change_set_id)
|
||||
|
||||
def delete_change_set(self):
|
||||
stack_name = self._get_param('StackName')
|
||||
change_set_name = self._get_param('ChangeSetName')
|
||||
|
||||
self.cloudformation_backend.delete_change_set(change_set_name=change_set_name, stack_name=stack_name)
|
||||
if self.request_json:
|
||||
return json.dumps({
|
||||
'DeleteChangeSetResponse': {
|
||||
'DeleteChangeSetResult': {},
|
||||
}
|
||||
})
|
||||
else:
|
||||
template = self.response_template(DELETE_CHANGE_SET_RESPONSE_TEMPLATE)
|
||||
return template.render()
|
||||
|
||||
def describe_change_set(self):
|
||||
stack_name = self._get_param('StackName')
|
||||
change_set_name = self._get_param('ChangeSetName')
|
||||
change_set = self.cloudformation_backend.describe_change_set(
|
||||
change_set_name=change_set_name,
|
||||
stack_name=stack_name,
|
||||
)
|
||||
template = self.response_template(DESCRIBE_CHANGE_SET_RESPONSE_TEMPLATE)
|
||||
return template.render(change_set=change_set)
|
||||
|
||||
@amzn_request_id
|
||||
def execute_change_set(self):
|
||||
stack_name = self._get_param('StackName')
|
||||
|
|
@ -187,6 +212,11 @@ class CloudFormationResponse(BaseResponse):
|
|||
template = self.response_template(DESCRIBE_STACK_EVENTS_RESPONSE)
|
||||
return template.render(stack=stack)
|
||||
|
||||
def list_change_sets(self):
|
||||
change_sets = self.cloudformation_backend.list_change_sets()
|
||||
template = self.response_template(LIST_CHANGE_SETS_RESPONSE)
|
||||
return template.render(change_sets=change_sets)
|
||||
|
||||
def list_stacks(self):
|
||||
stacks = self.cloudformation_backend.list_stacks()
|
||||
template = self.response_template(LIST_STACKS_RESPONSE)
|
||||
|
|
@ -354,6 +384,66 @@ CREATE_CHANGE_SET_RESPONSE_TEMPLATE = """<CreateStackResponse>
|
|||
</CreateStackResponse>
|
||||
"""
|
||||
|
||||
DELETE_CHANGE_SET_RESPONSE_TEMPLATE = """<DeleteChangeSetResponse>
|
||||
<DeleteChangeSetResult>
|
||||
</DeleteChangeSetResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>3d3200a1-810e-3023-6cc3-example</RequestId>
|
||||
</ResponseMetadata>
|
||||
</DeleteChangeSetResponse>
|
||||
"""
|
||||
|
||||
DESCRIBE_CHANGE_SET_RESPONSE_TEMPLATE = """<DescribeChangeSetResponse>
|
||||
<DescribeChangeSetResult>
|
||||
<ChangeSetId>{{ change_set.change_set_id }}</ChangeSetId>
|
||||
<ChangeSetName>{{ change_set.change_set_name }}</ChangeSetName>
|
||||
<StackId>{{ change_set.stack_id }}</StackId>
|
||||
<StackName>{{ change_set.stack_name }}</StackName>
|
||||
<Description>{{ change_set.description }}</Description>
|
||||
<Parameters>
|
||||
{% for param_name, param_value in change_set.stack_parameters.items() %}
|
||||
<member>
|
||||
<ParameterKey>{{ param_name }}</ParameterKey>
|
||||
<ParameterValue>{{ param_value }}</ParameterValue>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Parameters>
|
||||
<CreationTime>2011-05-23T15:47:44Z</CreationTime>
|
||||
<ExecutionStatus>{{ change_set.execution_status }}</ExecutionStatus>
|
||||
<Status>{{ change_set.status }}</Status>
|
||||
<StatusReason>{{ change_set.status_reason }}</StatusReason>
|
||||
{% if change_set.notification_arns %}
|
||||
<NotificationARNs>
|
||||
{% for notification_arn in change_set.notification_arns %}
|
||||
<member>{{ notification_arn }}</member>
|
||||
{% endfor %}
|
||||
</NotificationARNs>
|
||||
{% else %}
|
||||
<NotificationARNs/>
|
||||
{% endif %}
|
||||
{% if change_set.role_arn %}
|
||||
<RoleARN>{{ change_set.role_arn }}</RoleARN>
|
||||
{% endif %}
|
||||
{% if change_set.changes %}
|
||||
<Changes>
|
||||
{% for change in change_set.changes %}
|
||||
<member>
|
||||
<Type>Resource</Type>
|
||||
<ResourceChange>
|
||||
<Action>{{ change.action }}</Action>
|
||||
<LogicalResourceId>{{ change.logical_resource_id }}</LogicalResourceId>
|
||||
<ResourceType>{{ change.resource_type }}</ResourceType>
|
||||
</ResourceChange>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Changes>
|
||||
{% endif %}
|
||||
{% if next_token %}
|
||||
<NextToken>{{ next_token }}</NextToken>
|
||||
{% endif %}
|
||||
</DescribeChangeSetResult>
|
||||
</DescribeChangeSetResponse>"""
|
||||
|
||||
EXECUTE_CHANGE_SET_RESPONSE_TEMPLATE = """<ExecuteChangeSetResponse>
|
||||
<ExecuteChangeSetResult>
|
||||
<ExecuteChangeSetResult/>
|
||||
|
|
@ -479,6 +569,27 @@ DESCRIBE_STACK_EVENTS_RESPONSE = """<DescribeStackEventsResponse xmlns="http://c
|
|||
</DescribeStackEventsResponse>"""
|
||||
|
||||
|
||||
LIST_CHANGE_SETS_RESPONSE = """<ListChangeSetsResponse>
|
||||
<ListChangeSetsResult>
|
||||
<Summaries>
|
||||
{% for change_set in change_sets %}
|
||||
<member>
|
||||
<StackId>{{ change_set.stack_id }}</StackId>
|
||||
<StackName>{{ change_set.stack_name }}</StackName>
|
||||
<ChangeSetId>{{ change_set.change_set_id }}</ChangeSetId>
|
||||
<ChangeSetName>{{ change_set.change_set_name }}</ChangeSetName>
|
||||
<ExecutionStatus>{{ change_set.execution_status }}</ExecutionStatus>
|
||||
<Status>{{ change_set.status }}</Status>
|
||||
<StatusReason>{{ change_set.status_reason }}</StatusReason>
|
||||
<CreationTime>2011-05-23T15:47:44Z</CreationTime>
|
||||
<Description>{{ change_set.description }}</Description>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Summaries>
|
||||
</ListChangeSetsResult>
|
||||
</ListChangeSetsResponse>"""
|
||||
|
||||
|
||||
LIST_STACKS_RESPONSE = """<ListStacksResponse>
|
||||
<ListStacksResult>
|
||||
<StackSummaries>
|
||||
|
|
|
|||
4
moto/config/__init__.py
Normal file
4
moto/config/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .models import config_backends
|
||||
from ..core.models import base_decorator
|
||||
|
||||
mock_config = base_decorator(config_backends)
|
||||
149
moto/config/exceptions.py
Normal file
149
moto/config/exceptions.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
from __future__ import unicode_literals
|
||||
from moto.core.exceptions import JsonRESTError
|
||||
|
||||
|
||||
class NameTooLongException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, name, location):
|
||||
message = '1 validation error detected: Value \'{name}\' at \'{location}\' failed to satisfy' \
|
||||
' constraint: Member must have length less than or equal to 256'.format(name=name, location=location)
|
||||
super(NameTooLongException, self).__init__("ValidationException", message)
|
||||
|
||||
|
||||
class InvalidConfigurationRecorderNameException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, name):
|
||||
message = 'The configuration recorder name \'{name}\' is not valid, blank string.'.format(name=name)
|
||||
super(InvalidConfigurationRecorderNameException, self).__init__("InvalidConfigurationRecorderNameException",
|
||||
message)
|
||||
|
||||
|
||||
class MaxNumberOfConfigurationRecordersExceededException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, name):
|
||||
message = 'Failed to put configuration recorder \'{name}\' because the maximum number of ' \
|
||||
'configuration recorders: 1 is reached.'.format(name=name)
|
||||
super(MaxNumberOfConfigurationRecordersExceededException, self).__init__(
|
||||
"MaxNumberOfConfigurationRecordersExceededException", message)
|
||||
|
||||
|
||||
class InvalidRecordingGroupException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
message = 'The recording group provided is not valid'
|
||||
super(InvalidRecordingGroupException, self).__init__("InvalidRecordingGroupException", message)
|
||||
|
||||
|
||||
class InvalidResourceTypeException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, bad_list, good_list):
|
||||
message = '{num} validation error detected: Value \'{bad_list}\' at ' \
|
||||
'\'configurationRecorder.recordingGroup.resourceTypes\' failed to satisfy constraint: ' \
|
||||
'Member must satisfy constraint: [Member must satisfy enum value set: {good_list}]'.format(
|
||||
num=len(bad_list), bad_list=bad_list, good_list=good_list)
|
||||
# For PY2:
|
||||
message = str(message)
|
||||
|
||||
super(InvalidResourceTypeException, self).__init__("ValidationException", message)
|
||||
|
||||
|
||||
class NoSuchConfigurationRecorderException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, name):
|
||||
message = 'Cannot find configuration recorder with the specified name \'{name}\'.'.format(name=name)
|
||||
super(NoSuchConfigurationRecorderException, self).__init__("NoSuchConfigurationRecorderException", message)
|
||||
|
||||
|
||||
class InvalidDeliveryChannelNameException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, name):
|
||||
message = 'The delivery channel name \'{name}\' is not valid, blank string.'.format(name=name)
|
||||
super(InvalidDeliveryChannelNameException, self).__init__("InvalidDeliveryChannelNameException",
|
||||
message)
|
||||
|
||||
|
||||
class NoSuchBucketException(JsonRESTError):
|
||||
"""We are *only* validating that there is value that is not '' here."""
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
message = 'Cannot find a S3 bucket with an empty bucket name.'
|
||||
super(NoSuchBucketException, self).__init__("NoSuchBucketException", message)
|
||||
|
||||
|
||||
class InvalidS3KeyPrefixException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
message = 'The s3 key prefix \'\' is not valid, empty s3 key prefix.'
|
||||
super(InvalidS3KeyPrefixException, self).__init__("InvalidS3KeyPrefixException", message)
|
||||
|
||||
|
||||
class InvalidSNSTopicARNException(JsonRESTError):
|
||||
"""We are *only* validating that there is value that is not '' here."""
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
message = 'The sns topic arn \'\' is not valid.'
|
||||
super(InvalidSNSTopicARNException, self).__init__("InvalidSNSTopicARNException", message)
|
||||
|
||||
|
||||
class InvalidDeliveryFrequency(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, value, good_list):
|
||||
message = '1 validation error detected: Value \'{value}\' at ' \
|
||||
'\'deliveryChannel.configSnapshotDeliveryProperties.deliveryFrequency\' failed to satisfy ' \
|
||||
'constraint: Member must satisfy enum value set: {good_list}'.format(value=value, good_list=good_list)
|
||||
super(InvalidDeliveryFrequency, self).__init__("InvalidDeliveryFrequency", message)
|
||||
|
||||
|
||||
class MaxNumberOfDeliveryChannelsExceededException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, name):
|
||||
message = 'Failed to put delivery channel \'{name}\' because the maximum number of ' \
|
||||
'delivery channels: 1 is reached.'.format(name=name)
|
||||
super(MaxNumberOfDeliveryChannelsExceededException, self).__init__(
|
||||
"MaxNumberOfDeliveryChannelsExceededException", message)
|
||||
|
||||
|
||||
class NoSuchDeliveryChannelException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, name):
|
||||
message = 'Cannot find delivery channel with specified name \'{name}\'.'.format(name=name)
|
||||
super(NoSuchDeliveryChannelException, self).__init__("NoSuchDeliveryChannelException", message)
|
||||
|
||||
|
||||
class NoAvailableConfigurationRecorderException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
message = 'Configuration recorder is not available to put delivery channel.'
|
||||
super(NoAvailableConfigurationRecorderException, self).__init__("NoAvailableConfigurationRecorderException",
|
||||
message)
|
||||
|
||||
|
||||
class NoAvailableDeliveryChannelException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
message = 'Delivery channel is not available to start configuration recorder.'
|
||||
super(NoAvailableDeliveryChannelException, self).__init__("NoAvailableDeliveryChannelException", message)
|
||||
|
||||
|
||||
class LastDeliveryChannelDeleteFailedException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, name):
|
||||
message = 'Failed to delete last specified delivery channel with name \'{name}\', because there, ' \
|
||||
'because there is a running configuration recorder.'.format(name=name)
|
||||
super(LastDeliveryChannelDeleteFailedException, self).__init__("LastDeliveryChannelDeleteFailedException", message)
|
||||
335
moto/config/models.py
Normal file
335
moto/config/models.py
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
import json
|
||||
import time
|
||||
import pkg_resources
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from boto3 import Session
|
||||
|
||||
from moto.config.exceptions import InvalidResourceTypeException, InvalidDeliveryFrequency, \
|
||||
InvalidConfigurationRecorderNameException, NameTooLongException, \
|
||||
MaxNumberOfConfigurationRecordersExceededException, InvalidRecordingGroupException, \
|
||||
NoSuchConfigurationRecorderException, NoAvailableConfigurationRecorderException, \
|
||||
InvalidDeliveryChannelNameException, NoSuchBucketException, InvalidS3KeyPrefixException, \
|
||||
InvalidSNSTopicARNException, MaxNumberOfDeliveryChannelsExceededException, NoAvailableDeliveryChannelException, \
|
||||
NoSuchDeliveryChannelException, LastDeliveryChannelDeleteFailedException
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
|
||||
DEFAULT_ACCOUNT_ID = 123456789012
|
||||
|
||||
|
||||
def datetime2int(date):
|
||||
return int(time.mktime(date.timetuple()))
|
||||
|
||||
|
||||
def snake_to_camels(original):
|
||||
parts = original.split('_')
|
||||
|
||||
camel_cased = parts[0].lower() + ''.join(p.title() for p in parts[1:])
|
||||
camel_cased = camel_cased.replace('Arn', 'ARN') # Config uses 'ARN' instead of 'Arn'
|
||||
|
||||
return camel_cased
|
||||
|
||||
|
||||
class ConfigEmptyDictable(BaseModel):
|
||||
"""Base class to make serialization easy. This assumes that the sub-class will NOT return 'None's in the JSON."""
|
||||
|
||||
def to_dict(self):
|
||||
data = {}
|
||||
for item, value in self.__dict__.items():
|
||||
if value is not None:
|
||||
if isinstance(value, ConfigEmptyDictable):
|
||||
data[snake_to_camels(item)] = value.to_dict()
|
||||
else:
|
||||
data[snake_to_camels(item)] = value
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ConfigRecorderStatus(ConfigEmptyDictable):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
self.recording = False
|
||||
self.last_start_time = None
|
||||
self.last_stop_time = None
|
||||
self.last_status = None
|
||||
self.last_error_code = None
|
||||
self.last_error_message = None
|
||||
self.last_status_change_time = None
|
||||
|
||||
def start(self):
|
||||
self.recording = True
|
||||
self.last_status = 'PENDING'
|
||||
self.last_start_time = datetime2int(datetime.utcnow())
|
||||
self.last_status_change_time = datetime2int(datetime.utcnow())
|
||||
|
||||
def stop(self):
|
||||
self.recording = False
|
||||
self.last_stop_time = datetime2int(datetime.utcnow())
|
||||
self.last_status_change_time = datetime2int(datetime.utcnow())
|
||||
|
||||
|
||||
class ConfigDeliverySnapshotProperties(ConfigEmptyDictable):
|
||||
|
||||
def __init__(self, delivery_frequency):
|
||||
self.delivery_frequency = delivery_frequency
|
||||
|
||||
|
||||
class ConfigDeliveryChannel(ConfigEmptyDictable):
|
||||
|
||||
def __init__(self, name, s3_bucket_name, prefix=None, sns_arn=None, snapshot_properties=None):
|
||||
self.name = name
|
||||
self.s3_bucket_name = s3_bucket_name
|
||||
self.s3_key_prefix = prefix
|
||||
self.sns_topic_arn = sns_arn
|
||||
self.config_snapshot_delivery_properties = snapshot_properties
|
||||
|
||||
|
||||
class RecordingGroup(ConfigEmptyDictable):
|
||||
|
||||
def __init__(self, all_supported=True, include_global_resource_types=False, resource_types=None):
|
||||
self.all_supported = all_supported
|
||||
self.include_global_resource_types = include_global_resource_types
|
||||
self.resource_types = resource_types
|
||||
|
||||
|
||||
class ConfigRecorder(ConfigEmptyDictable):
|
||||
|
||||
def __init__(self, role_arn, recording_group, name='default', status=None):
|
||||
self.name = name
|
||||
self.role_arn = role_arn
|
||||
self.recording_group = recording_group
|
||||
|
||||
if not status:
|
||||
self.status = ConfigRecorderStatus(name)
|
||||
else:
|
||||
self.status = status
|
||||
|
||||
|
||||
class ConfigBackend(BaseBackend):
|
||||
|
||||
def __init__(self):
|
||||
self.recorders = {}
|
||||
self.delivery_channels = {}
|
||||
|
||||
@staticmethod
|
||||
def _validate_resource_types(resource_list):
|
||||
# Load the service file:
|
||||
resource_package = 'botocore'
|
||||
resource_path = '/'.join(('data', 'config', '2014-11-12', 'service-2.json'))
|
||||
conifg_schema = json.loads(pkg_resources.resource_string(resource_package, resource_path))
|
||||
|
||||
# Verify that each entry exists in the supported list:
|
||||
bad_list = []
|
||||
for resource in resource_list:
|
||||
# For PY2:
|
||||
r_str = str(resource)
|
||||
|
||||
if r_str not in conifg_schema['shapes']['ResourceType']['enum']:
|
||||
bad_list.append(r_str)
|
||||
|
||||
if bad_list:
|
||||
raise InvalidResourceTypeException(bad_list, conifg_schema['shapes']['ResourceType']['enum'])
|
||||
|
||||
@staticmethod
|
||||
def _validate_delivery_snapshot_properties(properties):
|
||||
# Load the service file:
|
||||
resource_package = 'botocore'
|
||||
resource_path = '/'.join(('data', 'config', '2014-11-12', 'service-2.json'))
|
||||
conifg_schema = json.loads(pkg_resources.resource_string(resource_package, resource_path))
|
||||
|
||||
# Verify that the deliveryFrequency is set to an acceptable value:
|
||||
if properties.get('deliveryFrequency', None) not in \
|
||||
conifg_schema['shapes']['MaximumExecutionFrequency']['enum']:
|
||||
raise InvalidDeliveryFrequency(properties.get('deliveryFrequency', None),
|
||||
conifg_schema['shapes']['MaximumExecutionFrequency']['enum'])
|
||||
|
||||
def put_configuration_recorder(self, config_recorder):
|
||||
# Validate the name:
|
||||
if not config_recorder.get('name'):
|
||||
raise InvalidConfigurationRecorderNameException(config_recorder.get('name'))
|
||||
if len(config_recorder.get('name')) > 256:
|
||||
raise NameTooLongException(config_recorder.get('name'), 'configurationRecorder.name')
|
||||
|
||||
# We're going to assume that the passed in Role ARN is correct.
|
||||
|
||||
# Config currently only allows 1 configuration recorder for an account:
|
||||
if len(self.recorders) == 1 and not self.recorders.get(config_recorder['name']):
|
||||
raise MaxNumberOfConfigurationRecordersExceededException(config_recorder['name'])
|
||||
|
||||
# Is this updating an existing one?
|
||||
recorder_status = None
|
||||
if self.recorders.get(config_recorder['name']):
|
||||
recorder_status = self.recorders[config_recorder['name']].status
|
||||
|
||||
# Validate the Recording Group:
|
||||
if config_recorder.get('recordingGroup') is None:
|
||||
recording_group = RecordingGroup()
|
||||
else:
|
||||
rg = config_recorder['recordingGroup']
|
||||
|
||||
# If an empty dict is passed in, then bad:
|
||||
if not rg:
|
||||
raise InvalidRecordingGroupException()
|
||||
|
||||
# Can't have both the resource types specified and the other flags as True.
|
||||
if rg.get('resourceTypes') and (
|
||||
rg.get('allSupported', False) or
|
||||
rg.get('includeGlobalResourceTypes', False)):
|
||||
raise InvalidRecordingGroupException()
|
||||
|
||||
# Must supply resourceTypes if 'allSupported' is not supplied:
|
||||
if not rg.get('allSupported') and not rg.get('resourceTypes'):
|
||||
raise InvalidRecordingGroupException()
|
||||
|
||||
# Validate that the list provided is correct:
|
||||
self._validate_resource_types(rg.get('resourceTypes', []))
|
||||
|
||||
recording_group = RecordingGroup(
|
||||
all_supported=rg.get('allSupported', True),
|
||||
include_global_resource_types=rg.get('includeGlobalResourceTypes', False),
|
||||
resource_types=rg.get('resourceTypes', [])
|
||||
)
|
||||
|
||||
self.recorders[config_recorder['name']] = \
|
||||
ConfigRecorder(config_recorder['roleARN'], recording_group, name=config_recorder['name'],
|
||||
status=recorder_status)
|
||||
|
||||
def describe_configuration_recorders(self, recorder_names):
|
||||
recorders = []
|
||||
|
||||
if recorder_names:
|
||||
for rn in recorder_names:
|
||||
if not self.recorders.get(rn):
|
||||
raise NoSuchConfigurationRecorderException(rn)
|
||||
|
||||
# Format the recorder:
|
||||
recorders.append(self.recorders[rn].to_dict())
|
||||
|
||||
else:
|
||||
for recorder in self.recorders.values():
|
||||
recorders.append(recorder.to_dict())
|
||||
|
||||
return recorders
|
||||
|
||||
def describe_configuration_recorder_status(self, recorder_names):
|
||||
recorders = []
|
||||
|
||||
if recorder_names:
|
||||
for rn in recorder_names:
|
||||
if not self.recorders.get(rn):
|
||||
raise NoSuchConfigurationRecorderException(rn)
|
||||
|
||||
# Format the recorder:
|
||||
recorders.append(self.recorders[rn].status.to_dict())
|
||||
|
||||
else:
|
||||
for recorder in self.recorders.values():
|
||||
recorders.append(recorder.status.to_dict())
|
||||
|
||||
return recorders
|
||||
|
||||
def put_delivery_channel(self, delivery_channel):
|
||||
# Must have a configuration recorder:
|
||||
if not self.recorders:
|
||||
raise NoAvailableConfigurationRecorderException()
|
||||
|
||||
# Validate the name:
|
||||
if not delivery_channel.get('name'):
|
||||
raise InvalidDeliveryChannelNameException(delivery_channel.get('name'))
|
||||
if len(delivery_channel.get('name')) > 256:
|
||||
raise NameTooLongException(delivery_channel.get('name'), 'deliveryChannel.name')
|
||||
|
||||
# We are going to assume that the bucket exists -- but will verify if the bucket provided is blank:
|
||||
if not delivery_channel.get('s3BucketName'):
|
||||
raise NoSuchBucketException()
|
||||
|
||||
# We are going to assume that the bucket has the correct policy attached to it. We are only going to verify
|
||||
# if the prefix provided is not an empty string:
|
||||
if delivery_channel.get('s3KeyPrefix', None) == '':
|
||||
raise InvalidS3KeyPrefixException()
|
||||
|
||||
# Ditto for SNS -- Only going to assume that the ARN provided is not an empty string:
|
||||
if delivery_channel.get('snsTopicARN', None) == '':
|
||||
raise InvalidSNSTopicARNException()
|
||||
|
||||
# Config currently only allows 1 delivery channel for an account:
|
||||
if len(self.delivery_channels) == 1 and not self.delivery_channels.get(delivery_channel['name']):
|
||||
raise MaxNumberOfDeliveryChannelsExceededException(delivery_channel['name'])
|
||||
|
||||
if not delivery_channel.get('configSnapshotDeliveryProperties'):
|
||||
dp = None
|
||||
|
||||
else:
|
||||
# Validate the config snapshot delivery properties:
|
||||
self._validate_delivery_snapshot_properties(delivery_channel['configSnapshotDeliveryProperties'])
|
||||
|
||||
dp = ConfigDeliverySnapshotProperties(
|
||||
delivery_channel['configSnapshotDeliveryProperties']['deliveryFrequency'])
|
||||
|
||||
self.delivery_channels[delivery_channel['name']] = \
|
||||
ConfigDeliveryChannel(delivery_channel['name'], delivery_channel['s3BucketName'],
|
||||
prefix=delivery_channel.get('s3KeyPrefix', None),
|
||||
sns_arn=delivery_channel.get('snsTopicARN', None),
|
||||
snapshot_properties=dp)
|
||||
|
||||
def describe_delivery_channels(self, channel_names):
|
||||
channels = []
|
||||
|
||||
if channel_names:
|
||||
for cn in channel_names:
|
||||
if not self.delivery_channels.get(cn):
|
||||
raise NoSuchDeliveryChannelException(cn)
|
||||
|
||||
# Format the delivery channel:
|
||||
channels.append(self.delivery_channels[cn].to_dict())
|
||||
|
||||
else:
|
||||
for channel in self.delivery_channels.values():
|
||||
channels.append(channel.to_dict())
|
||||
|
||||
return channels
|
||||
|
||||
def start_configuration_recorder(self, recorder_name):
|
||||
if not self.recorders.get(recorder_name):
|
||||
raise NoSuchConfigurationRecorderException(recorder_name)
|
||||
|
||||
# Must have a delivery channel available as well:
|
||||
if not self.delivery_channels:
|
||||
raise NoAvailableDeliveryChannelException()
|
||||
|
||||
# Start recording:
|
||||
self.recorders[recorder_name].status.start()
|
||||
|
||||
def stop_configuration_recorder(self, recorder_name):
|
||||
if not self.recorders.get(recorder_name):
|
||||
raise NoSuchConfigurationRecorderException(recorder_name)
|
||||
|
||||
# Stop recording:
|
||||
self.recorders[recorder_name].status.stop()
|
||||
|
||||
def delete_configuration_recorder(self, recorder_name):
|
||||
if not self.recorders.get(recorder_name):
|
||||
raise NoSuchConfigurationRecorderException(recorder_name)
|
||||
|
||||
del self.recorders[recorder_name]
|
||||
|
||||
def delete_delivery_channel(self, channel_name):
|
||||
if not self.delivery_channels.get(channel_name):
|
||||
raise NoSuchDeliveryChannelException(channel_name)
|
||||
|
||||
# Check if a channel is recording -- if so, bad -- (there can only be 1 recorder):
|
||||
for recorder in self.recorders.values():
|
||||
if recorder.status.recording:
|
||||
raise LastDeliveryChannelDeleteFailedException(channel_name)
|
||||
|
||||
del self.delivery_channels[channel_name]
|
||||
|
||||
|
||||
config_backends = {}
|
||||
boto3_session = Session()
|
||||
for region in boto3_session.get_available_regions('config'):
|
||||
config_backends[region] = ConfigBackend()
|
||||
53
moto/config/responses.py
Normal file
53
moto/config/responses.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import json
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import config_backends
|
||||
|
||||
|
||||
class ConfigResponse(BaseResponse):
|
||||
|
||||
@property
|
||||
def config_backend(self):
|
||||
return config_backends[self.region]
|
||||
|
||||
def put_configuration_recorder(self):
|
||||
self.config_backend.put_configuration_recorder(self._get_param('ConfigurationRecorder'))
|
||||
return ""
|
||||
|
||||
def describe_configuration_recorders(self):
|
||||
recorders = self.config_backend.describe_configuration_recorders(self._get_param('ConfigurationRecorderNames'))
|
||||
schema = {'ConfigurationRecorders': recorders}
|
||||
return json.dumps(schema)
|
||||
|
||||
def describe_configuration_recorder_status(self):
|
||||
recorder_statuses = self.config_backend.describe_configuration_recorder_status(
|
||||
self._get_param('ConfigurationRecorderNames'))
|
||||
schema = {'ConfigurationRecordersStatus': recorder_statuses}
|
||||
return json.dumps(schema)
|
||||
|
||||
def put_delivery_channel(self):
|
||||
self.config_backend.put_delivery_channel(self._get_param('DeliveryChannel'))
|
||||
return ""
|
||||
|
||||
def describe_delivery_channels(self):
|
||||
delivery_channels = self.config_backend.describe_delivery_channels(self._get_param('DeliveryChannelNames'))
|
||||
schema = {'DeliveryChannels': delivery_channels}
|
||||
return json.dumps(schema)
|
||||
|
||||
def describe_delivery_channel_status(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete_delivery_channel(self):
|
||||
self.config_backend.delete_delivery_channel(self._get_param('DeliveryChannelName'))
|
||||
return ""
|
||||
|
||||
def delete_configuration_recorder(self):
|
||||
self.config_backend.delete_configuration_recorder(self._get_param('ConfigurationRecorderName'))
|
||||
return ""
|
||||
|
||||
def start_configuration_recorder(self):
|
||||
self.config_backend.start_configuration_recorder(self._get_param('ConfigurationRecorderName'))
|
||||
return ""
|
||||
|
||||
def stop_configuration_recorder(self):
|
||||
self.config_backend.stop_configuration_recorder(self._get_param('ConfigurationRecorderName'))
|
||||
return ""
|
||||
10
moto/config/urls.py
Normal file
10
moto/config/urls.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from __future__ import unicode_literals
|
||||
from .responses import ConfigResponse
|
||||
|
||||
url_bases = [
|
||||
"https?://config.(.+).amazonaws.com",
|
||||
]
|
||||
|
||||
url_paths = {
|
||||
'{0}/$': ConfigResponse.dispatch,
|
||||
}
|
||||
|
|
@ -66,6 +66,8 @@ class DynamoType(object):
|
|||
return int(self.value)
|
||||
except ValueError:
|
||||
return float(self.value)
|
||||
elif self.is_set():
|
||||
return set(self.value)
|
||||
else:
|
||||
return self.value
|
||||
|
||||
|
|
@ -509,15 +511,12 @@ class Table(BaseModel):
|
|||
elif 'Value' in val and DynamoType(val['Value']).value != current_attr[key].value:
|
||||
raise ValueError("The conditional request failed")
|
||||
elif 'ComparisonOperator' in val:
|
||||
comparison_func = get_comparison_func(
|
||||
val['ComparisonOperator'])
|
||||
dynamo_types = [
|
||||
DynamoType(ele) for ele in
|
||||
val.get("AttributeValueList", [])
|
||||
]
|
||||
for t in dynamo_types:
|
||||
if not comparison_func(current_attr[key].value, t.value):
|
||||
raise ValueError('The conditional request failed')
|
||||
if not current_attr[key].compare(val['ComparisonOperator'], dynamo_types):
|
||||
raise ValueError('The conditional request failed')
|
||||
if range_value:
|
||||
self.items[hash_value][range_value] = item
|
||||
else:
|
||||
|
|
@ -946,15 +945,12 @@ class DynamoDBBackend(BaseBackend):
|
|||
elif 'Value' in val and DynamoType(val['Value']).value != item_attr[key].value:
|
||||
raise ValueError("The conditional request failed")
|
||||
elif 'ComparisonOperator' in val:
|
||||
comparison_func = get_comparison_func(
|
||||
val['ComparisonOperator'])
|
||||
dynamo_types = [
|
||||
DynamoType(ele) for ele in
|
||||
val.get("AttributeValueList", [])
|
||||
]
|
||||
for t in dynamo_types:
|
||||
if not comparison_func(item_attr[key].value, t.value):
|
||||
raise ValueError('The conditional request failed')
|
||||
if not item_attr[key].compare(val['ComparisonOperator'], dynamo_types):
|
||||
raise ValueError('The conditional request failed')
|
||||
|
||||
# Update does not fail on new items, so create one
|
||||
if item is None:
|
||||
|
|
|
|||
|
|
@ -31,6 +31,67 @@ def get_empty_str_error():
|
|||
))
|
||||
|
||||
|
||||
def condition_expression_to_expected(condition_expression, expression_attribute_names, expression_attribute_values):
|
||||
"""
|
||||
Limited condition expression syntax parsing.
|
||||
Supports Global Negation ex: NOT(inner expressions).
|
||||
Supports simple AND conditions ex: cond_a AND cond_b and cond_c.
|
||||
Atomic expressions supported are attribute_exists(key), attribute_not_exists(key) and #key = :value.
|
||||
"""
|
||||
expected = {}
|
||||
if condition_expression and 'OR' not in condition_expression:
|
||||
reverse_re = re.compile('^NOT\s*\((.*)\)$')
|
||||
reverse_m = reverse_re.match(condition_expression.strip())
|
||||
|
||||
reverse = False
|
||||
if reverse_m:
|
||||
reverse = True
|
||||
condition_expression = reverse_m.group(1)
|
||||
|
||||
cond_items = [c.strip() for c in condition_expression.split('AND')]
|
||||
if cond_items:
|
||||
exists_re = re.compile('^attribute_exists\s*\((.*)\)$')
|
||||
not_exists_re = re.compile(
|
||||
'^attribute_not_exists\s*\((.*)\)$')
|
||||
equals_re = re.compile('^(#?\w+)\s*=\s*(\:?\w+)')
|
||||
|
||||
for cond in cond_items:
|
||||
exists_m = exists_re.match(cond)
|
||||
not_exists_m = not_exists_re.match(cond)
|
||||
equals_m = equals_re.match(cond)
|
||||
|
||||
if exists_m:
|
||||
attribute_name = expression_attribute_names_lookup(exists_m.group(1), expression_attribute_names)
|
||||
expected[attribute_name] = {'Exists': True if not reverse else False}
|
||||
elif not_exists_m:
|
||||
attribute_name = expression_attribute_names_lookup(not_exists_m.group(1), expression_attribute_names)
|
||||
expected[attribute_name] = {'Exists': False if not reverse else True}
|
||||
elif equals_m:
|
||||
attribute_name = expression_attribute_names_lookup(equals_m.group(1), expression_attribute_names)
|
||||
attribute_value = expression_attribute_values_lookup(equals_m.group(2), expression_attribute_values)
|
||||
expected[attribute_name] = {
|
||||
'AttributeValueList': [attribute_value],
|
||||
'ComparisonOperator': 'EQ' if not reverse else 'NEQ'}
|
||||
|
||||
return expected
|
||||
|
||||
|
||||
def expression_attribute_names_lookup(attribute_name, expression_attribute_names):
|
||||
if attribute_name.startswith('#') and attribute_name in expression_attribute_names:
|
||||
return expression_attribute_names[attribute_name]
|
||||
else:
|
||||
return attribute_name
|
||||
|
||||
|
||||
def expression_attribute_values_lookup(attribute_value, expression_attribute_values):
|
||||
if isinstance(attribute_value, six.string_types) and \
|
||||
attribute_value.startswith(':') and\
|
||||
attribute_value in expression_attribute_values:
|
||||
return expression_attribute_values[attribute_value]
|
||||
else:
|
||||
return attribute_value
|
||||
|
||||
|
||||
class DynamoHandler(BaseResponse):
|
||||
|
||||
def get_endpoint_name(self, headers):
|
||||
|
|
@ -220,24 +281,13 @@ class DynamoHandler(BaseResponse):
|
|||
# expression
|
||||
if not expected:
|
||||
condition_expression = self.body.get('ConditionExpression')
|
||||
if condition_expression and 'OR' not in condition_expression:
|
||||
cond_items = [c.strip()
|
||||
for c in condition_expression.split('AND')]
|
||||
|
||||
if cond_items:
|
||||
expected = {}
|
||||
overwrite = False
|
||||
exists_re = re.compile('^attribute_exists\s*\((.*)\)$')
|
||||
not_exists_re = re.compile(
|
||||
'^attribute_not_exists\s*\((.*)\)$')
|
||||
|
||||
for cond in cond_items:
|
||||
exists_m = exists_re.match(cond)
|
||||
not_exists_m = not_exists_re.match(cond)
|
||||
if exists_m:
|
||||
expected[exists_m.group(1)] = {'Exists': True}
|
||||
elif not_exists_m:
|
||||
expected[not_exists_m.group(1)] = {'Exists': False}
|
||||
expression_attribute_names = self.body.get('ExpressionAttributeNames', {})
|
||||
expression_attribute_values = self.body.get('ExpressionAttributeValues', {})
|
||||
expected = condition_expression_to_expected(condition_expression,
|
||||
expression_attribute_names,
|
||||
expression_attribute_values)
|
||||
if expected:
|
||||
overwrite = False
|
||||
|
||||
try:
|
||||
result = self.dynamodb_backend.put_item(name, item, expected, overwrite)
|
||||
|
|
@ -590,23 +640,11 @@ class DynamoHandler(BaseResponse):
|
|||
# expression
|
||||
if not expected:
|
||||
condition_expression = self.body.get('ConditionExpression')
|
||||
if condition_expression and 'OR' not in condition_expression:
|
||||
cond_items = [c.strip()
|
||||
for c in condition_expression.split('AND')]
|
||||
|
||||
if cond_items:
|
||||
expected = {}
|
||||
exists_re = re.compile('^attribute_exists\s*\((.*)\)$')
|
||||
not_exists_re = re.compile(
|
||||
'^attribute_not_exists\s*\((.*)\)$')
|
||||
|
||||
for cond in cond_items:
|
||||
exists_m = exists_re.match(cond)
|
||||
not_exists_m = not_exists_re.match(cond)
|
||||
if exists_m:
|
||||
expected[exists_m.group(1)] = {'Exists': True}
|
||||
elif not_exists_m:
|
||||
expected[not_exists_m.group(1)] = {'Exists': False}
|
||||
expression_attribute_names = self.body.get('ExpressionAttributeNames', {})
|
||||
expression_attribute_values = self.body.get('ExpressionAttributeValues', {})
|
||||
expected = condition_expression_to_expected(condition_expression,
|
||||
expression_attribute_names,
|
||||
expression_attribute_values)
|
||||
|
||||
# Support spaces between operators in an update expression
|
||||
# E.g. `a = b + c` -> `a=b+c`
|
||||
|
|
|
|||
57
moto/ec2/models.py
Executable file → Normal file
57
moto/ec2/models.py
Executable file → Normal file
|
|
@ -388,6 +388,7 @@ class Instance(TaggedEC2Resource, BotoInstance):
|
|||
self.ebs_optimized = kwargs.get("ebs_optimized", False)
|
||||
self.source_dest_check = "true"
|
||||
self.launch_time = utc_date_and_time()
|
||||
self.ami_launch_index = kwargs.get("ami_launch_index", 0)
|
||||
self.disable_api_termination = kwargs.get("disable_api_termination", False)
|
||||
self._spot_fleet_id = kwargs.get("spot_fleet_id", None)
|
||||
associate_public_ip = kwargs.get("associate_public_ip", False)
|
||||
|
|
@ -719,6 +720,7 @@ class InstanceBackend(object):
|
|||
instance_tags = tags.get('instance', {})
|
||||
|
||||
for index in range(count):
|
||||
kwargs["ami_launch_index"] = index
|
||||
new_instance = Instance(
|
||||
self,
|
||||
image_id,
|
||||
|
|
@ -2464,7 +2466,7 @@ class SubnetBackend(object):
|
|||
default_for_az, map_public_ip_on_launch)
|
||||
|
||||
# AWS associates a new subnet with the default Network ACL
|
||||
self.associate_default_network_acl_with_subnet(subnet_id)
|
||||
self.associate_default_network_acl_with_subnet(subnet_id, vpc_id)
|
||||
self.subnets[availability_zone][subnet_id] = subnet
|
||||
return subnet
|
||||
|
||||
|
|
@ -2879,7 +2881,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
|
|||
def __init__(self, ec2_backend, spot_request_id, price, image_id, type,
|
||||
valid_from, valid_until, launch_group, availability_zone_group,
|
||||
key_name, security_groups, user_data, instance_type, placement,
|
||||
kernel_id, ramdisk_id, monitoring_enabled, subnet_id, spot_fleet_id,
|
||||
kernel_id, ramdisk_id, monitoring_enabled, subnet_id, tags, spot_fleet_id,
|
||||
**kwargs):
|
||||
super(SpotInstanceRequest, self).__init__(**kwargs)
|
||||
ls = LaunchSpecification()
|
||||
|
|
@ -2903,6 +2905,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
|
|||
ls.monitored = monitoring_enabled
|
||||
ls.subnet_id = subnet_id
|
||||
self.spot_fleet_id = spot_fleet_id
|
||||
self.tags = tags
|
||||
|
||||
if security_groups:
|
||||
for group_name in security_groups:
|
||||
|
|
@ -2936,6 +2939,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
|
|||
security_group_names=[],
|
||||
security_group_ids=self.launch_specification.groups,
|
||||
spot_fleet_id=self.spot_fleet_id,
|
||||
tags=self.tags,
|
||||
)
|
||||
instance = reservation.instances[0]
|
||||
return instance
|
||||
|
|
@ -2951,15 +2955,16 @@ class SpotRequestBackend(object):
|
|||
valid_until, launch_group, availability_zone_group,
|
||||
key_name, security_groups, user_data,
|
||||
instance_type, placement, kernel_id, ramdisk_id,
|
||||
monitoring_enabled, subnet_id, spot_fleet_id=None):
|
||||
monitoring_enabled, subnet_id, tags=None, spot_fleet_id=None):
|
||||
requests = []
|
||||
tags = tags or {}
|
||||
for _ in range(count):
|
||||
spot_request_id = random_spot_request_id()
|
||||
request = SpotInstanceRequest(self,
|
||||
spot_request_id, price, image_id, type, valid_from, valid_until,
|
||||
launch_group, availability_zone_group, key_name, security_groups,
|
||||
user_data, instance_type, placement, kernel_id, ramdisk_id,
|
||||
monitoring_enabled, subnet_id, spot_fleet_id)
|
||||
monitoring_enabled, subnet_id, tags, spot_fleet_id)
|
||||
self.spot_instance_requests[spot_request_id] = request
|
||||
requests.append(request)
|
||||
return requests
|
||||
|
|
@ -2979,8 +2984,8 @@ class SpotRequestBackend(object):
|
|||
|
||||
class SpotFleetLaunchSpec(object):
|
||||
def __init__(self, ebs_optimized, group_set, iam_instance_profile, image_id,
|
||||
instance_type, key_name, monitoring, spot_price, subnet_id, user_data,
|
||||
weighted_capacity):
|
||||
instance_type, key_name, monitoring, spot_price, subnet_id, tag_specifications,
|
||||
user_data, weighted_capacity):
|
||||
self.ebs_optimized = ebs_optimized
|
||||
self.group_set = group_set
|
||||
self.iam_instance_profile = iam_instance_profile
|
||||
|
|
@ -2990,6 +2995,7 @@ class SpotFleetLaunchSpec(object):
|
|||
self.monitoring = monitoring
|
||||
self.spot_price = spot_price
|
||||
self.subnet_id = subnet_id
|
||||
self.tag_specifications = tag_specifications
|
||||
self.user_data = user_data
|
||||
self.weighted_capacity = float(weighted_capacity)
|
||||
|
||||
|
|
@ -3020,6 +3026,7 @@ class SpotFleetRequest(TaggedEC2Resource):
|
|||
monitoring=spec.get('monitoring._enabled'),
|
||||
spot_price=spec.get('spot_price', self.spot_price),
|
||||
subnet_id=spec['subnet_id'],
|
||||
tag_specifications=self._parse_tag_specifications(spec),
|
||||
user_data=spec.get('user_data'),
|
||||
weighted_capacity=spec['weighted_capacity'],
|
||||
)
|
||||
|
|
@ -3102,6 +3109,7 @@ class SpotFleetRequest(TaggedEC2Resource):
|
|||
monitoring_enabled=launch_spec.monitoring,
|
||||
subnet_id=launch_spec.subnet_id,
|
||||
spot_fleet_id=self.id,
|
||||
tags=launch_spec.tag_specifications,
|
||||
)
|
||||
self.spot_requests.extend(requests)
|
||||
self.fulfilled_capacity += added_weight
|
||||
|
|
@ -3124,6 +3132,25 @@ class SpotFleetRequest(TaggedEC2Resource):
|
|||
self.spot_requests = [req for req in self.spot_requests if req.instance.id not in instance_ids]
|
||||
self.ec2_backend.terminate_instances(instance_ids)
|
||||
|
||||
def _parse_tag_specifications(self, spec):
|
||||
try:
|
||||
tag_spec_num = max([int(key.split('.')[1]) for key in spec if key.startswith("tag_specification_set")])
|
||||
except ValueError: # no tag specifications
|
||||
return {}
|
||||
|
||||
tag_specifications = {}
|
||||
for si in range(1, tag_spec_num + 1):
|
||||
resource_type = spec["tag_specification_set.{si}._resource_type".format(si=si)]
|
||||
|
||||
tags = [key for key in spec if key.startswith("tag_specification_set.{si}._tag".format(si=si))]
|
||||
tag_num = max([int(key.split('.')[3]) for key in tags])
|
||||
tag_specifications[resource_type] = dict((
|
||||
spec["tag_specification_set.{si}._tag.{ti}._key".format(si=si, ti=ti)],
|
||||
spec["tag_specification_set.{si}._tag.{ti}._value".format(si=si, ti=ti)],
|
||||
) for ti in range(1, tag_num + 1))
|
||||
|
||||
return tag_specifications
|
||||
|
||||
|
||||
class SpotFleetBackend(object):
|
||||
def __init__(self):
|
||||
|
|
@ -3560,8 +3587,22 @@ class NetworkAclBackend(object):
|
|||
self.get_vpc(vpc_id)
|
||||
network_acl = NetworkAcl(self, network_acl_id, vpc_id, default)
|
||||
self.network_acls[network_acl_id] = network_acl
|
||||
if default:
|
||||
self.add_default_entries(network_acl_id)
|
||||
return network_acl
|
||||
|
||||
def add_default_entries(self, network_acl_id):
|
||||
default_acl_entries = [
|
||||
{'rule_number': 100, 'rule_action': 'allow', 'egress': 'true'},
|
||||
{'rule_number': 32767, 'rule_action': 'deny', 'egress': 'true'},
|
||||
{'rule_number': 100, 'rule_action': 'allow', 'egress': 'false'},
|
||||
{'rule_number': 32767, 'rule_action': 'deny', 'egress': 'false'}
|
||||
]
|
||||
for entry in default_acl_entries:
|
||||
self.create_network_acl_entry(network_acl_id=network_acl_id, rule_number=entry['rule_number'], protocol='-1',
|
||||
rule_action=entry['rule_action'], egress=entry['egress'], cidr_block='0.0.0.0/0',
|
||||
icmp_code=None, icmp_type=None, port_range_from=None, port_range_to=None)
|
||||
|
||||
def get_all_network_acls(self, network_acl_ids=None, filters=None):
|
||||
network_acls = self.network_acls.values()
|
||||
|
||||
|
|
@ -3636,9 +3677,9 @@ class NetworkAclBackend(object):
|
|||
new_acl.associations[new_assoc_id] = association
|
||||
return association
|
||||
|
||||
def associate_default_network_acl_with_subnet(self, subnet_id):
|
||||
def associate_default_network_acl_with_subnet(self, subnet_id, vpc_id):
|
||||
association_id = random_network_acl_subnet_association_id()
|
||||
acl = next(acl for acl in self.network_acls.values() if acl.default)
|
||||
acl = next(acl for acl in self.network_acls.values() if acl.default and acl.vpc_id == vpc_id)
|
||||
acl.associations[association_id] = NetworkAclAssociation(self, association_id,
|
||||
subnet_id, acl.id)
|
||||
|
||||
|
|
|
|||
|
|
@ -150,16 +150,18 @@ CREATE_VOLUME_RESPONSE = """<CreateVolumeResponse xmlns="http://ec2.amazonaws.co
|
|||
<availabilityZone>{{ volume.zone.name }}</availabilityZone>
|
||||
<status>creating</status>
|
||||
<createTime>{{ volume.create_time}}</createTime>
|
||||
<tagSet>
|
||||
{% for tag in volume.get_tags() %}
|
||||
<item>
|
||||
<resourceId>{{ tag.resource_id }}</resourceId>
|
||||
<resourceType>{{ tag.resource_type }}</resourceType>
|
||||
<key>{{ tag.key }}</key>
|
||||
<value>{{ tag.value }}</value>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</tagSet>
|
||||
{% if volume.get_tags() %}
|
||||
<tagSet>
|
||||
{% for tag in volume.get_tags() %}
|
||||
<item>
|
||||
<resourceId>{{ tag.resource_id }}</resourceId>
|
||||
<resourceType>{{ tag.resource_type }}</resourceType>
|
||||
<key>{{ tag.key }}</key>
|
||||
<value>{{ tag.value }}</value>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</tagSet>
|
||||
{% endif %}
|
||||
<volumeType>standard</volumeType>
|
||||
</CreateVolumeResponse>"""
|
||||
|
||||
|
|
@ -191,16 +193,18 @@ DESCRIBE_VOLUMES_RESPONSE = """<DescribeVolumesResponse xmlns="http://ec2.amazon
|
|||
</item>
|
||||
{% endif %}
|
||||
</attachmentSet>
|
||||
<tagSet>
|
||||
{% for tag in volume.get_tags() %}
|
||||
<item>
|
||||
<resourceId>{{ tag.resource_id }}</resourceId>
|
||||
<resourceType>{{ tag.resource_type }}</resourceType>
|
||||
<key>{{ tag.key }}</key>
|
||||
<value>{{ tag.value }}</value>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</tagSet>
|
||||
{% if volume.get_tags() %}
|
||||
<tagSet>
|
||||
{% for tag in volume.get_tags() %}
|
||||
<item>
|
||||
<resourceId>{{ tag.resource_id }}</resourceId>
|
||||
<resourceType>{{ tag.resource_type }}</resourceType>
|
||||
<key>{{ tag.key }}</key>
|
||||
<value>{{ tag.value }}</value>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</tagSet>
|
||||
{% endif %}
|
||||
<volumeType>standard</volumeType>
|
||||
</item>
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ EC2_RUN_INSTANCES = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc
|
|||
<reason/>
|
||||
<keyName>{{ instance.key_name }}</keyName>
|
||||
<ebsOptimized>{{ instance.ebs_optimized }}</ebsOptimized>
|
||||
<amiLaunchIndex>0</amiLaunchIndex>
|
||||
<amiLaunchIndex>{{ instance.ami_launch_index }}</amiLaunchIndex>
|
||||
<instanceType>{{ instance.instance_type }}</instanceType>
|
||||
<launchTime>{{ instance.launch_time }}</launchTime>
|
||||
<placement>
|
||||
|
|
@ -384,7 +384,7 @@ EC2_DESCRIBE_INSTANCES = """<DescribeInstancesResponse xmlns="http://ec2.amazona
|
|||
<reason>{{ instance._reason }}</reason>
|
||||
<keyName>{{ instance.key_name }}</keyName>
|
||||
<ebsOptimized>{{ instance.ebs_optimized }}</ebsOptimized>
|
||||
<amiLaunchIndex>0</amiLaunchIndex>
|
||||
<amiLaunchIndex>{{ instance.ami_launch_index }}</amiLaunchIndex>
|
||||
<productCodes/>
|
||||
<instanceType>{{ instance.instance_type }}</instanceType>
|
||||
<launchTime>{{ instance.launch_time }}</launchTime>
|
||||
|
|
|
|||
|
|
@ -107,6 +107,21 @@ DESCRIBE_SPOT_FLEET_TEMPLATE = """<DescribeSpotFleetRequestsResponse xmlns="http
|
|||
</item>
|
||||
{% endfor %}
|
||||
</groupSet>
|
||||
<tagSpecificationSet>
|
||||
{% for resource_type in launch_spec.tag_specifications %}
|
||||
<item>
|
||||
<resourceType>{{ resource_type }}</resourceType>
|
||||
<tag>
|
||||
{% for key, value in launch_spec.tag_specifications[resource_type].items() %}
|
||||
<item>
|
||||
<key>{{ key }}</key>
|
||||
<value>{{ value }}</value>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</tag>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</tagSpecificationSet>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</launchSpecifications>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from .responses import ECRResponse
|
|||
|
||||
url_bases = [
|
||||
"https?://ecr.(.+).amazonaws.com",
|
||||
"https?://api.ecr.(.+).amazonaws.com",
|
||||
]
|
||||
|
||||
url_paths = {
|
||||
|
|
|
|||
|
|
@ -32,3 +32,48 @@ class MalformedCertificate(RESTError):
|
|||
def __init__(self, cert):
|
||||
super(MalformedCertificate, self).__init__(
|
||||
'MalformedCertificate', 'Certificate {cert} is malformed'.format(cert=cert))
|
||||
|
||||
|
||||
class DuplicateTags(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
super(DuplicateTags, self).__init__(
|
||||
'InvalidInput', 'Duplicate tag keys found. Please note that Tag keys are case insensitive.')
|
||||
|
||||
|
||||
class TagKeyTooBig(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, tag, param='tags.X.member.key'):
|
||||
super(TagKeyTooBig, self).__init__(
|
||||
'ValidationError', "1 validation error detected: Value '{}' at '{}' failed to satisfy "
|
||||
"constraint: Member must have length less than or equal to 128.".format(tag, param))
|
||||
|
||||
|
||||
class TagValueTooBig(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, tag):
|
||||
super(TagValueTooBig, self).__init__(
|
||||
'ValidationError', "1 validation error detected: Value '{}' at 'tags.X.member.value' failed to satisfy "
|
||||
"constraint: Member must have length less than or equal to 256.".format(tag))
|
||||
|
||||
|
||||
class InvalidTagCharacters(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, tag, param='tags.X.member.key'):
|
||||
message = "1 validation error detected: Value '{}' at '{}' failed to satisfy ".format(tag, param)
|
||||
message += "constraint: Member must satisfy regular expression pattern: [\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]+"
|
||||
|
||||
super(InvalidTagCharacters, self).__init__('ValidationError', message)
|
||||
|
||||
|
||||
class TooManyTags(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, tags, param='tags'):
|
||||
super(TooManyTags, self).__init__(
|
||||
'ValidationError', "1 validation error detected: Value '{}' at '{}' failed to satisfy "
|
||||
"constraint: Member must have length less than or equal to 50.".format(tags, param))
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import base64
|
|||
import sys
|
||||
from datetime import datetime
|
||||
import json
|
||||
import re
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
|
@ -12,7 +13,8 @@ from moto.core import BaseBackend, BaseModel
|
|||
from moto.core.utils import iso_8601_datetime_without_milliseconds
|
||||
|
||||
from .aws_managed_policies import aws_managed_policies_data
|
||||
from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, MalformedCertificate
|
||||
from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, MalformedCertificate, \
|
||||
DuplicateTags, TagKeyTooBig, InvalidTagCharacters, TooManyTags, TagValueTooBig
|
||||
from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id
|
||||
|
||||
ACCOUNT_ID = 123456789012
|
||||
|
|
@ -32,7 +34,6 @@ class MFADevice(object):
|
|||
|
||||
|
||||
class Policy(BaseModel):
|
||||
|
||||
is_attachable = False
|
||||
|
||||
def __init__(self,
|
||||
|
|
@ -132,6 +133,8 @@ class Role(BaseModel):
|
|||
self.policies = {}
|
||||
self.managed_policies = {}
|
||||
self.create_date = datetime.now(pytz.utc)
|
||||
self.tags = {}
|
||||
self.description = ""
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||
|
|
@ -175,6 +178,9 @@ class Role(BaseModel):
|
|||
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
def get_tags(self):
|
||||
return [self.tags[tag] for tag in self.tags]
|
||||
|
||||
|
||||
class InstanceProfile(BaseModel):
|
||||
|
||||
|
|
@ -468,6 +474,16 @@ class IAMBackend(BaseBackend):
|
|||
policy = arns[policy_arn]
|
||||
policy.attach_to(self.get_role(role_name))
|
||||
|
||||
def update_role_description(self, role_name, role_description):
|
||||
role = self.get_role(role_name)
|
||||
role.description = role_description
|
||||
return role
|
||||
|
||||
def update_role(self, role_name, role_description):
|
||||
role = self.get_role(role_name)
|
||||
role.description = role_description
|
||||
return role
|
||||
|
||||
def detach_role_policy(self, policy_arn, role_name):
|
||||
arns = dict((p.arn, p) for p in self.managed_policies.values())
|
||||
try:
|
||||
|
|
@ -614,6 +630,86 @@ class IAMBackend(BaseBackend):
|
|||
role = self.get_role(role_name)
|
||||
return role.policies.keys()
|
||||
|
||||
def _validate_tag_key(self, tag_key, exception_param='tags.X.member.key'):
|
||||
"""Validates the tag key.
|
||||
|
||||
:param all_tags: Dict to check if there is a duplicate tag.
|
||||
:param tag_key: The tag key to check against.
|
||||
:param exception_param: The exception parameter to send over to help format the message. This is to reflect
|
||||
the difference between the tag and untag APIs.
|
||||
:return:
|
||||
"""
|
||||
# Validate that the key length is correct:
|
||||
if len(tag_key) > 128:
|
||||
raise TagKeyTooBig(tag_key, param=exception_param)
|
||||
|
||||
# Validate that the tag key fits the proper Regex:
|
||||
# [\w\s_.:/=+\-@]+ SHOULD be the same as the Java regex on the AWS documentation: [\p{L}\p{Z}\p{N}_.:/=+\-@]+
|
||||
match = re.findall(r'[\w\s_.:/=+\-@]+', tag_key)
|
||||
# Kudos if you can come up with a better way of doing a global search :)
|
||||
if not len(match) or len(match[0]) < len(tag_key):
|
||||
raise InvalidTagCharacters(tag_key, param=exception_param)
|
||||
|
||||
def _check_tag_duplicate(self, all_tags, tag_key):
|
||||
"""Validates that a tag key is not a duplicate
|
||||
|
||||
:param all_tags: Dict to check if there is a duplicate tag.
|
||||
:param tag_key: The tag key to check against.
|
||||
:return:
|
||||
"""
|
||||
if tag_key in all_tags:
|
||||
raise DuplicateTags()
|
||||
|
||||
def list_role_tags(self, role_name, marker, max_items=100):
|
||||
role = self.get_role(role_name)
|
||||
|
||||
max_items = int(max_items)
|
||||
tag_index = sorted(role.tags)
|
||||
start_idx = int(marker) if marker else 0
|
||||
|
||||
tag_index = tag_index[start_idx:start_idx + max_items]
|
||||
|
||||
if len(role.tags) <= (start_idx + max_items):
|
||||
marker = None
|
||||
else:
|
||||
marker = str(start_idx + max_items)
|
||||
|
||||
# Make the tag list of dict's:
|
||||
tags = [role.tags[tag] for tag in tag_index]
|
||||
|
||||
return tags, marker
|
||||
|
||||
def tag_role(self, role_name, tags):
|
||||
if len(tags) > 50:
|
||||
raise TooManyTags(tags)
|
||||
|
||||
role = self.get_role(role_name)
|
||||
|
||||
tag_keys = {}
|
||||
for tag in tags:
|
||||
# Need to index by the lowercase tag key since the keys are case insensitive, but their case is retained.
|
||||
ref_key = tag['Key'].lower()
|
||||
self._check_tag_duplicate(tag_keys, ref_key)
|
||||
self._validate_tag_key(tag['Key'])
|
||||
if len(tag['Value']) > 256:
|
||||
raise TagValueTooBig(tag['Value'])
|
||||
|
||||
tag_keys[ref_key] = tag
|
||||
|
||||
role.tags.update(tag_keys)
|
||||
|
||||
def untag_role(self, role_name, tag_keys):
|
||||
if len(tag_keys) > 50:
|
||||
raise TooManyTags(tag_keys, param='tagKeys')
|
||||
|
||||
role = self.get_role(role_name)
|
||||
|
||||
for key in tag_keys:
|
||||
ref_key = key.lower()
|
||||
self._validate_tag_key(key, exception_param='tagKeys')
|
||||
|
||||
role.tags.pop(ref_key, None)
|
||||
|
||||
def create_policy_version(self, policy_arn, policy_document, set_as_default):
|
||||
policy = self.get_policy(policy_arn)
|
||||
if not policy:
|
||||
|
|
@ -796,6 +892,16 @@ class IAMBackend(BaseBackend):
|
|||
|
||||
return users
|
||||
|
||||
def list_roles(self, path_prefix, marker, max_items):
|
||||
roles = None
|
||||
try:
|
||||
roles = self.roles.values()
|
||||
except KeyError:
|
||||
raise IAMNotFoundException(
|
||||
"Users {0}, {1}, {2} not found".format(path_prefix, marker, max_items))
|
||||
|
||||
return roles
|
||||
|
||||
def upload_signing_certificate(self, user_name, body):
|
||||
user = self.get_user(user_name)
|
||||
cert_id = random_resource_id(size=32)
|
||||
|
|
|
|||
|
|
@ -107,6 +107,69 @@ class IamResponse(BaseResponse):
|
|||
template = self.response_template(LIST_POLICIES_TEMPLATE)
|
||||
return template.render(policies=policies, marker=marker)
|
||||
|
||||
def list_entities_for_policy(self):
|
||||
policy_arn = self._get_param('PolicyArn')
|
||||
|
||||
# Options 'User'|'Role'|'Group'|'LocalManagedPolicy'|'AWSManagedPolicy
|
||||
entity = self._get_param('EntityFilter')
|
||||
path_prefix = self._get_param('PathPrefix')
|
||||
# policy_usage_filter = self._get_param('PolicyUsageFilter')
|
||||
marker = self._get_param('Marker')
|
||||
max_items = self._get_param('MaxItems')
|
||||
|
||||
entity_roles = []
|
||||
entity_groups = []
|
||||
entity_users = []
|
||||
|
||||
if entity == 'User':
|
||||
users = iam_backend.list_users(path_prefix, marker, max_items)
|
||||
if users:
|
||||
for user in users:
|
||||
for p in user.managed_policies:
|
||||
if p == policy_arn:
|
||||
entity_users.append(user.name)
|
||||
|
||||
elif entity == 'Role':
|
||||
roles = iam_backend.list_roles(path_prefix, marker, max_items)
|
||||
if roles:
|
||||
for role in roles:
|
||||
for p in role.managed_policies:
|
||||
if p == policy_arn:
|
||||
entity_roles.append(role.name)
|
||||
|
||||
elif entity == 'Group':
|
||||
groups = iam_backend.list_groups()
|
||||
if groups:
|
||||
for group in groups:
|
||||
for p in group.managed_policies:
|
||||
if p == policy_arn:
|
||||
entity_groups.append(group.name)
|
||||
|
||||
elif entity == 'LocalManagedPolicy' or entity == 'AWSManagedPolicy':
|
||||
users = iam_backend.list_users(path_prefix, marker, max_items)
|
||||
if users:
|
||||
for user in users:
|
||||
for p in user.managed_policies:
|
||||
if p == policy_arn:
|
||||
entity_users.append(user.name)
|
||||
|
||||
roles = iam_backend.list_roles(path_prefix, marker, max_items)
|
||||
if roles:
|
||||
for role in roles:
|
||||
for p in role.managed_policies:
|
||||
if p == policy_arn:
|
||||
entity_roles.append(role.name)
|
||||
|
||||
groups = iam_backend.list_groups()
|
||||
if groups:
|
||||
for group in groups:
|
||||
for p in group.managed_policies:
|
||||
if p == policy_arn:
|
||||
entity_groups.append(group.name)
|
||||
|
||||
template = self.response_template(LIST_ENTITIES_FOR_POLICY_TEMPLATE)
|
||||
return template.render(roles=entity_roles, users=entity_users, groups=entity_groups)
|
||||
|
||||
def create_role(self):
|
||||
role_name = self._get_param('RoleName')
|
||||
path = self._get_param('Path')
|
||||
|
|
@ -169,6 +232,20 @@ class IamResponse(BaseResponse):
|
|||
template = self.response_template(GENERIC_EMPTY_TEMPLATE)
|
||||
return template.render(name="UpdateAssumeRolePolicyResponse")
|
||||
|
||||
def update_role_description(self):
|
||||
role_name = self._get_param('RoleName')
|
||||
description = self._get_param('Description')
|
||||
role = iam_backend.update_role_description(role_name, description)
|
||||
template = self.response_template(UPDATE_ROLE_DESCRIPTION_TEMPLATE)
|
||||
return template.render(role=role)
|
||||
|
||||
def update_role(self):
|
||||
role_name = self._get_param('RoleName')
|
||||
description = self._get_param('Description')
|
||||
role = iam_backend.update_role(role_name, description)
|
||||
template = self.response_template(UPDATE_ROLE_TEMPLATE)
|
||||
return template.render(role=role)
|
||||
|
||||
def create_policy_version(self):
|
||||
policy_arn = self._get_param('PolicyArn')
|
||||
policy_document = self._get_param('PolicyDocument')
|
||||
|
|
@ -554,7 +631,8 @@ class IamResponse(BaseResponse):
|
|||
policies=account_details['managed_policies'],
|
||||
users=account_details['users'],
|
||||
groups=account_details['groups'],
|
||||
roles=account_details['roles']
|
||||
roles=account_details['roles'],
|
||||
get_groups_for_user=iam_backend.get_groups_for_user
|
||||
)
|
||||
|
||||
def create_saml_provider(self):
|
||||
|
|
@ -625,6 +703,65 @@ class IamResponse(BaseResponse):
|
|||
template = self.response_template(LIST_SIGNING_CERTIFICATES_TEMPLATE)
|
||||
return template.render(user_name=user_name, certificates=certs)
|
||||
|
||||
def list_role_tags(self):
|
||||
role_name = self._get_param('RoleName')
|
||||
marker = self._get_param('Marker')
|
||||
max_items = self._get_param('MaxItems', 100)
|
||||
|
||||
tags, marker = iam_backend.list_role_tags(role_name, marker, max_items)
|
||||
|
||||
template = self.response_template(LIST_ROLE_TAG_TEMPLATE)
|
||||
return template.render(tags=tags, marker=marker)
|
||||
|
||||
def tag_role(self):
|
||||
role_name = self._get_param('RoleName')
|
||||
tags = self._get_multi_param('Tags.member')
|
||||
|
||||
iam_backend.tag_role(role_name, tags)
|
||||
|
||||
template = self.response_template(TAG_ROLE_TEMPLATE)
|
||||
return template.render()
|
||||
|
||||
def untag_role(self):
|
||||
role_name = self._get_param('RoleName')
|
||||
tag_keys = self._get_multi_param('TagKeys.member')
|
||||
|
||||
iam_backend.untag_role(role_name, tag_keys)
|
||||
|
||||
template = self.response_template(UNTAG_ROLE_TEMPLATE)
|
||||
return template.render()
|
||||
|
||||
|
||||
LIST_ENTITIES_FOR_POLICY_TEMPLATE = """<ListEntitiesForPolicyResponse>
|
||||
<ListEntitiesForPolicyResult>
|
||||
<PolicyRoles>
|
||||
{% for role in roles %}
|
||||
<member>
|
||||
<RoleName>{{ role }}</RoleName>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</PolicyRoles>
|
||||
<PolicyGroups>
|
||||
{% for group in groups %}
|
||||
<member>
|
||||
<GroupName>{{ group }}</GroupName>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</PolicyGroups>
|
||||
<IsTruncated>false</IsTruncated>
|
||||
<PolicyUsers>
|
||||
{% for user in users %}
|
||||
<member>
|
||||
<UserName>{{ user }}</UserName>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</PolicyUsers>
|
||||
</ListEntitiesForPolicyResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>eb358e22-9d1f-11e4-93eb-190ecEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</ListEntitiesForPolicyResponse>"""
|
||||
|
||||
|
||||
ATTACH_ROLE_POLICY_TEMPLATE = """<AttachRolePolicyResponse>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -869,6 +1006,40 @@ GET_ROLE_POLICY_TEMPLATE = """<GetRolePolicyResponse xmlns="https://iam.amazonaw
|
|||
</ResponseMetadata>
|
||||
</GetRolePolicyResponse>"""
|
||||
|
||||
UPDATE_ROLE_TEMPLATE = """<UpdateRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<UpdateRoleResult>
|
||||
</UpdateRoleResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>df37e965-9967-11e1-a4c3-270EXAMPLE04</RequestId>
|
||||
</ResponseMetadata>
|
||||
</UpdateRoleResponse>"""
|
||||
|
||||
UPDATE_ROLE_DESCRIPTION_TEMPLATE = """<UpdateRoleDescriptionResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<UpdateRoleDescriptionResult>
|
||||
<Role>
|
||||
<Path>{{ role.path }}</Path>
|
||||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date.isoformat() }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
{% if role.tags %}
|
||||
<Tags>
|
||||
{% for tag in role.get_tags() %}
|
||||
<member>
|
||||
<Key>{{ tag['Key'] }}</Key>
|
||||
<Value>{{ tag['Value'] }}</Value>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Tags>
|
||||
{% endif %}
|
||||
</Role>
|
||||
</UpdateRoleDescriptionResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>df37e965-9967-11e1-a4c3-270EXAMPLE04</RequestId>
|
||||
</ResponseMetadata>
|
||||
</UpdateRoleDescriptionResponse>"""
|
||||
|
||||
GET_ROLE_TEMPLATE = """<GetRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<GetRoleResult>
|
||||
<Role>
|
||||
|
|
@ -878,6 +1049,16 @@ GET_ROLE_TEMPLATE = """<GetRoleResponse xmlns="https://iam.amazonaws.com/doc/201
|
|||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
{% if role.tags %}
|
||||
<Tags>
|
||||
{% for tag in role.get_tags() %}
|
||||
<member>
|
||||
<Key>{{ tag['Key'] }}</Key>
|
||||
<Value>{{ tag['Value'] }}</Value>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Tags>
|
||||
{% endif %}
|
||||
</Role>
|
||||
</GetRoleResult>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -1461,8 +1642,19 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
<UserDetailList>
|
||||
{% for user in users %}
|
||||
<member>
|
||||
<GroupList />
|
||||
<AttachedManagedPolicies/>
|
||||
<GroupList>
|
||||
{% for group in get_groups_for_user(user.name) %}
|
||||
<member>{{ group.name }}</member>
|
||||
{% endfor %}
|
||||
</GroupList>
|
||||
<AttachedManagedPolicies>
|
||||
{% for policy in user.managed_policies %}
|
||||
<member>
|
||||
<PolicyName>{{ user.managed_policies[policy].name }}</PolicyName>
|
||||
<PolicyArn>{{ policy }}</PolicyArn>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</AttachedManagedPolicies>
|
||||
<UserId>{{ user.id }}</UserId>
|
||||
<Path>{{ user.path }}</Path>
|
||||
<UserName>{{ user.name }}</UserName>
|
||||
|
|
@ -1476,33 +1668,55 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
<member>
|
||||
<GroupId>{{ group.id }}</GroupId>
|
||||
<AttachedManagedPolicies>
|
||||
{% for policy in group.managed_policies %}
|
||||
<member>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<PolicyArn>{{ policy.arn }}</PolicyArn>
|
||||
</member>
|
||||
{% for policy_arn in group.managed_policies %}
|
||||
<member>
|
||||
<PolicyName>{{ group.managed_policies[policy_arn].name }}</PolicyName>
|
||||
<PolicyArn>{{ policy_arn }}</PolicyArn>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</AttachedManagedPolicies>
|
||||
<GroupName>{{ group.name }}</GroupName>
|
||||
<Path>{{ group.path }}</Path>
|
||||
<Arn>{{ group.arn }}</Arn>
|
||||
<CreateDate>{{ group.create_date }}</CreateDate>
|
||||
<GroupPolicyList/>
|
||||
<GroupPolicyList>
|
||||
{% for policy in group.policies %}
|
||||
<member>
|
||||
<PolicyName>{{ policy }}</PolicyName>
|
||||
<PolicyDocument>{{ group.get_policy(policy) }}</PolicyDocument>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</GroupPolicyList>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</GroupDetailList>
|
||||
<RoleDetailList>
|
||||
{% for role in roles %}
|
||||
<member>
|
||||
<RolePolicyList/>
|
||||
<AttachedManagedPolicies>
|
||||
{% for policy in role.managed_policies %}
|
||||
<RolePolicyList>
|
||||
{% for inline_policy in role.policies %}
|
||||
<member>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<PolicyArn>{{ policy.arn }}</PolicyArn>
|
||||
<PolicyName>{{ inline_policy }}</PolicyName>
|
||||
<PolicyDocument>{{ role.policies[inline_policy] }}</PolicyDocument>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</RolePolicyList>
|
||||
<AttachedManagedPolicies>
|
||||
{% for policy_arn in role.managed_policies %}
|
||||
<member>
|
||||
<PolicyName>{{ role.managed_policies[policy_arn].name }}</PolicyName>
|
||||
<PolicyArn>{{ policy_arn }}</PolicyArn>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</AttachedManagedPolicies>
|
||||
<Tags>
|
||||
{% for tag in role.get_tags() %}
|
||||
<member>
|
||||
<Key>{{ tag['Key'] }}</Key>
|
||||
<Value>{{ tag['Value'] }}</Value>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Tags>
|
||||
<InstanceProfileList>
|
||||
{% for profile in instance_profiles %}
|
||||
<member>
|
||||
|
|
@ -1543,19 +1757,14 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
<PolicyId>{{ policy.id }}</PolicyId>
|
||||
<Path>{{ policy.path }}</Path>
|
||||
<PolicyVersionList>
|
||||
{% for policy_version in policy.versions %}
|
||||
<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>
|
||||
<Document>{{ policy_version.document }}</Document>
|
||||
<IsDefaultVersion>{{ policy_version.is_default }}</IsDefaultVersion>
|
||||
<VersionId>{{ policy_version.version_id }}</VersionId>
|
||||
<CreateDate>{{ policy_version.create_datetime }}</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</PolicyVersionList>
|
||||
<Arn>{{ policy.arn }}</Arn>
|
||||
<AttachmentCount>1</AttachmentCount>
|
||||
|
|
@ -1671,3 +1880,38 @@ LIST_SIGNING_CERTIFICATES_TEMPLATE = """<ListSigningCertificatesResponse>
|
|||
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</ListSigningCertificatesResponse>"""
|
||||
|
||||
|
||||
TAG_ROLE_TEMPLATE = """<TagRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<ResponseMetadata>
|
||||
<RequestId>EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</TagRoleResponse>"""
|
||||
|
||||
|
||||
LIST_ROLE_TAG_TEMPLATE = """<ListRoleTagsResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<ListRoleTagsResult>
|
||||
<IsTruncated>{{ 'true' if marker else 'false' }}</IsTruncated>
|
||||
{% if marker %}
|
||||
<Marker>{{ marker }}</Marker>
|
||||
{% endif %}
|
||||
<Tags>
|
||||
{% for tag in tags %}
|
||||
<member>
|
||||
<Key>{{ tag['Key'] }}</Key>
|
||||
<Value>{{ tag['Value'] }}</Value>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Tags>
|
||||
</ListRoleTagsResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</ListRoleTagsResponse>"""
|
||||
|
||||
|
||||
UNTAG_ROLE_TEMPLATE = """<UntagRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<ResponseMetadata>
|
||||
<RequestId>EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</UntagRoleResponse>"""
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class Key(BaseModel):
|
|||
self.account_id = "0123456789012"
|
||||
self.key_rotation_status = False
|
||||
self.deletion_date = None
|
||||
self.tags = {}
|
||||
|
||||
@property
|
||||
def physical_resource_id(self):
|
||||
|
|
@ -35,7 +36,7 @@ class Key(BaseModel):
|
|||
"KeyMetadata": {
|
||||
"AWSAccountId": self.account_id,
|
||||
"Arn": self.arn,
|
||||
"CreationDate": "2015-01-01 00:00:00",
|
||||
"CreationDate": datetime.strftime(datetime.utcnow(), "%s"),
|
||||
"Description": self.description,
|
||||
"Enabled": self.enabled,
|
||||
"KeyId": self.id,
|
||||
|
|
@ -63,7 +64,6 @@ class Key(BaseModel):
|
|||
)
|
||||
key.key_rotation_status = properties['EnableKeyRotation']
|
||||
key.enabled = properties['Enabled']
|
||||
|
||||
return key
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
|
|
@ -84,6 +84,18 @@ class KmsBackend(BaseBackend):
|
|||
self.keys[key.id] = key
|
||||
return key
|
||||
|
||||
def update_key_description(self, key_id, description):
|
||||
key = self.keys[self.get_key_id(key_id)]
|
||||
key.description = description
|
||||
|
||||
def tag_resource(self, key_id, tags):
|
||||
key = self.keys[self.get_key_id(key_id)]
|
||||
key.tags = tags
|
||||
|
||||
def list_resource_tags(self, key_id):
|
||||
key = self.keys[self.get_key_id(key_id)]
|
||||
return key.tags
|
||||
|
||||
def delete_key(self, key_id):
|
||||
if key_id in self.keys:
|
||||
if key_id in self.key_to_aliases:
|
||||
|
|
|
|||
|
|
@ -38,6 +38,28 @@ class KmsResponse(BaseResponse):
|
|||
policy, key_usage, description, self.region)
|
||||
return json.dumps(key.to_dict())
|
||||
|
||||
def update_key_description(self):
|
||||
key_id = self.parameters.get('KeyId')
|
||||
description = self.parameters.get('Description')
|
||||
|
||||
self.kms_backend.update_key_description(key_id, description)
|
||||
return json.dumps(None)
|
||||
|
||||
def tag_resource(self):
|
||||
key_id = self.parameters.get('KeyId')
|
||||
tags = self.parameters.get('Tags')
|
||||
self.kms_backend.tag_resource(key_id, tags)
|
||||
return json.dumps({})
|
||||
|
||||
def list_resource_tags(self):
|
||||
key_id = self.parameters.get('KeyId')
|
||||
tags = self.kms_backend.list_resource_tags(key_id)
|
||||
return json.dumps({
|
||||
"Tags": tags,
|
||||
"NextMarker": None,
|
||||
"Truncated": False,
|
||||
})
|
||||
|
||||
def describe_key(self):
|
||||
key_id = self.parameters.get('KeyId')
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -242,7 +242,8 @@ class LogsBackend(BaseBackend):
|
|||
if next_token is None:
|
||||
next_token = 0
|
||||
|
||||
groups = sorted(group.to_describe_dict() for name, group in self.groups.items() if name.startswith(log_group_name_prefix))
|
||||
groups = [group.to_describe_dict() for name, group in self.groups.items() if name.startswith(log_group_name_prefix)]
|
||||
groups = sorted(groups, key=lambda x: x['creationTime'], reverse=True)
|
||||
groups_page = groups[next_token:next_token + limit]
|
||||
|
||||
next_token += limit
|
||||
|
|
|
|||
|
|
@ -1113,4 +1113,4 @@ def httprettified(test):
|
|||
|
||||
if isinstance(test, ClassTypes):
|
||||
return decorate_class(test)
|
||||
return decorate_callable(test)
|
||||
return decorate_callable(test)
|
||||
|
|
@ -29,7 +29,6 @@ import re
|
|||
from .compat import BaseClass
|
||||
from .utils import decode_utf8
|
||||
|
||||
|
||||
STATUSES = {
|
||||
100: "Continue",
|
||||
101: "Switching Protocols",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class HealthCheck(BaseModel):
|
|||
self.id = health_check_id
|
||||
self.ip_address = health_check_args.get("ip_address")
|
||||
self.port = health_check_args.get("port", 80)
|
||||
self._type = health_check_args.get("type")
|
||||
self.type_ = health_check_args.get("type")
|
||||
self.resource_path = health_check_args.get("resource_path")
|
||||
self.fqdn = health_check_args.get("fqdn")
|
||||
self.search_string = health_check_args.get("search_string")
|
||||
|
|
@ -58,7 +58,7 @@ class HealthCheck(BaseModel):
|
|||
<HealthCheckConfig>
|
||||
<IPAddress>{{ health_check.ip_address }}</IPAddress>
|
||||
<Port>{{ health_check.port }}</Port>
|
||||
<Type>{{ health_check._type }}</Type>
|
||||
<Type>{{ health_check.type_ }}</Type>
|
||||
<ResourcePath>{{ health_check.resource_path }}</ResourcePath>
|
||||
<FullyQualifiedDomainName>{{ health_check.fqdn }}</FullyQualifiedDomainName>
|
||||
<RequestInterval>{{ health_check.request_interval }}</RequestInterval>
|
||||
|
|
@ -76,7 +76,7 @@ class RecordSet(BaseModel):
|
|||
|
||||
def __init__(self, kwargs):
|
||||
self.name = kwargs.get('Name')
|
||||
self._type = kwargs.get('Type')
|
||||
self.type_ = kwargs.get('Type')
|
||||
self.ttl = kwargs.get('TTL')
|
||||
self.records = kwargs.get('ResourceRecords', [])
|
||||
self.set_identifier = kwargs.get('SetIdentifier')
|
||||
|
|
@ -130,7 +130,7 @@ class RecordSet(BaseModel):
|
|||
def to_xml(self):
|
||||
template = Template("""<ResourceRecordSet>
|
||||
<Name>{{ record_set.name }}</Name>
|
||||
<Type>{{ record_set._type }}</Type>
|
||||
<Type>{{ record_set.type_ }}</Type>
|
||||
{% if record_set.set_identifier %}
|
||||
<SetIdentifier>{{ record_set.set_identifier }}</SetIdentifier>
|
||||
{% endif %}
|
||||
|
|
@ -183,7 +183,7 @@ class FakeZone(BaseModel):
|
|||
def upsert_rrset(self, record_set):
|
||||
new_rrset = RecordSet(record_set)
|
||||
for i, rrset in enumerate(self.rrsets):
|
||||
if rrset.name == new_rrset.name:
|
||||
if rrset.name == new_rrset.name and rrset.type_ == new_rrset.type_:
|
||||
self.rrsets[i] = new_rrset
|
||||
break
|
||||
else:
|
||||
|
|
@ -202,7 +202,7 @@ class FakeZone(BaseModel):
|
|||
record_sets = list(self.rrsets) # Copy the list
|
||||
if start_type:
|
||||
record_sets = [
|
||||
record_set for record_set in record_sets if record_set._type >= start_type]
|
||||
record_set for record_set in record_sets if record_set.type_ >= start_type]
|
||||
if start_name:
|
||||
record_sets = [
|
||||
record_set for record_set in record_sets if record_set.name >= start_name]
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import random
|
|||
import string
|
||||
import tempfile
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import six
|
||||
|
||||
|
|
@ -35,7 +36,7 @@ class FakeDeleteMarker(BaseModel):
|
|||
self.key = key
|
||||
self.name = key.name
|
||||
self.last_modified = datetime.datetime.utcnow()
|
||||
self._version_id = key.version_id + 1
|
||||
self._version_id = str(uuid.uuid4())
|
||||
|
||||
@property
|
||||
def last_modified_ISO8601(self):
|
||||
|
|
@ -115,15 +116,16 @@ class FakeKey(BaseModel):
|
|||
self.last_modified = datetime.datetime.utcnow()
|
||||
self._etag = None # must recalculate etag
|
||||
if self._is_versioned:
|
||||
self._version_id += 1
|
||||
self._version_id = str(uuid.uuid4())
|
||||
else:
|
||||
self._is_versioned = 0
|
||||
self._version_id = None
|
||||
|
||||
def restore(self, days):
|
||||
self._expiry = datetime.datetime.utcnow() + datetime.timedelta(days)
|
||||
|
||||
def increment_version(self):
|
||||
self._version_id += 1
|
||||
def refresh_version(self):
|
||||
self._version_id = str(uuid.uuid4())
|
||||
self.last_modified = datetime.datetime.utcnow()
|
||||
|
||||
@property
|
||||
def etag(self):
|
||||
|
|
@ -716,17 +718,18 @@ class S3Backend(BaseBackend):
|
|||
|
||||
def get_bucket_latest_versions(self, bucket_name):
|
||||
versions = self.get_bucket_versions(bucket_name)
|
||||
maximum_version_per_key = {}
|
||||
latest_modified_per_key = {}
|
||||
latest_versions = {}
|
||||
|
||||
for version in versions:
|
||||
name = version.name
|
||||
last_modified = version.last_modified
|
||||
version_id = version.version_id
|
||||
maximum_version_per_key[name] = max(
|
||||
version_id,
|
||||
maximum_version_per_key.get(name, -1)
|
||||
latest_modified_per_key[name] = max(
|
||||
last_modified,
|
||||
latest_modified_per_key.get(name, datetime.datetime.min)
|
||||
)
|
||||
if version_id == maximum_version_per_key[name]:
|
||||
if last_modified == latest_modified_per_key[name]:
|
||||
latest_versions[name] = version_id
|
||||
|
||||
return latest_versions
|
||||
|
|
@ -774,20 +777,19 @@ class S3Backend(BaseBackend):
|
|||
|
||||
bucket = self.get_bucket(bucket_name)
|
||||
|
||||
old_key = bucket.keys.get(key_name, None)
|
||||
if old_key is not None and bucket.is_versioned:
|
||||
new_version_id = old_key._version_id + 1
|
||||
else:
|
||||
new_version_id = 0
|
||||
|
||||
new_key = FakeKey(
|
||||
name=key_name,
|
||||
value=value,
|
||||
storage=storage,
|
||||
etag=etag,
|
||||
is_versioned=bucket.is_versioned,
|
||||
version_id=new_version_id)
|
||||
bucket.keys[key_name] = new_key
|
||||
version_id=str(uuid.uuid4()) if bucket.is_versioned else None)
|
||||
|
||||
keys = [
|
||||
key for key in bucket.keys.getlist(key_name, [])
|
||||
if key.version_id != new_key.version_id
|
||||
] + [new_key]
|
||||
bucket.keys.setlist(key_name, keys)
|
||||
|
||||
return new_key
|
||||
|
||||
|
|
@ -977,7 +979,7 @@ class S3Backend(BaseBackend):
|
|||
|
||||
# By this point, the destination key must exist, or KeyError
|
||||
if dest_bucket.is_versioned:
|
||||
dest_bucket.keys[dest_key_name].increment_version()
|
||||
dest_bucket.keys[dest_key_name].refresh_version()
|
||||
if storage is not None:
|
||||
key.set_storage_class(storage)
|
||||
if acl is not None:
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from .exceptions import BucketAlreadyExists, S3ClientError, MissingBucket, Missi
|
|||
MalformedACLError, InvalidNotificationARN, InvalidNotificationEvent
|
||||
from .models import s3_backend, get_canned_acl, FakeGrantee, FakeGrant, FakeAcl, FakeKey, FakeTagging, FakeTagSet, \
|
||||
FakeTag
|
||||
from .utils import bucket_name_from_url, metadata_from_headers, parse_region_from_url
|
||||
from .utils import bucket_name_from_url, clean_key_name, metadata_from_headers, parse_region_from_url
|
||||
from xml.dom import minidom
|
||||
|
||||
|
||||
|
|
@ -733,7 +733,7 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
# Copy key
|
||||
# you can have a quoted ?version=abc with a version Id, so work on
|
||||
# we need to parse the unquoted string first
|
||||
src_key = request.headers.get("x-amz-copy-source")
|
||||
src_key = clean_key_name(request.headers.get("x-amz-copy-source"))
|
||||
if isinstance(src_key, six.binary_type):
|
||||
src_key = src_key.decode('utf-8')
|
||||
src_key_parsed = urlparse(src_key)
|
||||
|
|
@ -1303,7 +1303,7 @@ S3_BUCKET_GET_VERSIONS = """<?xml version="1.0" encoding="UTF-8"?>
|
|||
{% for key in key_list %}
|
||||
<Version>
|
||||
<Key>{{ key.name }}</Key>
|
||||
<VersionId>{{ key.version_id }}</VersionId>
|
||||
<VersionId>{% if key.version_id is none %}null{% else %}{{ key.version_id }}{% endif %}</VersionId>
|
||||
<IsLatest>{% if latest_versions[key.name] == key.version_id %}true{% else %}false{% endif %}</IsLatest>
|
||||
<LastModified>{{ key.last_modified_ISO8601 }}</LastModified>
|
||||
<ETag>{{ key.etag }}</ETag>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue