Merge in master.

This commit is contained in:
Steve Pulec 2017-03-05 09:58:39 -05:00
commit 3b4ef2cf15
26 changed files with 565 additions and 65 deletions

74
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,74 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project maintainer at spulec@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

4
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,4 @@
### Contributing code
If you have improvements to Moto, send us your pull requests! For those
just getting started, Github has a [howto](https://help.github.com/articles/using-pull-requests/).

33
ISSUE_TEMPLATE.md Normal file
View File

@ -0,0 +1,33 @@
## Reporting Bugs
Please be aware of the following things when filing bug reports:
1. Avoid raising duplicate issues. *Please* use the GitHub issue search feature
to check whether your bug report or feature request has been mentioned in
the past.
2. When filing bug reports about exceptions or tracebacks, please include the
*complete* traceback. Partial tracebacks, or just the exception text, are
not helpful.
3. Make sure you provide a suitable amount of information to work with. This
means you should provide:
- Guidance on **how to reproduce the issue**. Ideally, this should be a
*small* code sample that can be run immediately by the maintainers.
Failing that, let us know what you're doing, how often it happens, what
environment you're using, etc. Be thorough: it prevents us needing to ask
further questions.
- Tell us **what you expected to happen**. When we run your example code,
what are we expecting to happen? What does "success" look like for your
code?
- Tell us **what actually happens**. It's not helpful for you to say "it
doesn't work" or "it fails". Tell us *how* it fails: do you get an
exception? A hang? How was the actual result different from your expected
result?
- Tell us **what version of Moto you're using**, and
**how you installed it**. Tell us whether you're using standalone server
mode or the Python mocks. If you are using the Python mocks, include the
version of boto/boto3/botocore.
If you do not provide all of these things, it will take us much longer to
fix your problem.

View File

@ -339,11 +339,14 @@ class RestAPI(object):
return status_code, {}, response return status_code, {}, response
def update_integration_mocks(self, stage_name): def update_integration_mocks(self, stage_name):
stage_url = STAGE_URL.format(api_id=self.id, stage_url_lower = STAGE_URL.format(api_id=self.id.lower(),
region_name=self.region_name, stage_name=stage_name) region_name=self.region_name, stage_name=stage_name)
responses.add_callback(responses.GET, stage_url.upper(), stage_url_upper = STAGE_URL.format(api_id=self.id.upper(),
region_name=self.region_name, stage_name=stage_name)
responses.add_callback(responses.GET, stage_url_lower,
callback=self.resource_callback) callback=self.resource_callback)
responses.add_callback(responses.GET, stage_url.lower(), responses.add_callback(responses.GET, stage_url_upper,
callback=self.resource_callback) callback=self.resource_callback)
def create_stage(self, name, deployment_id, variables=None, description='', cacheClusterEnabled=None, cacheClusterSize=None): def create_stage(self, name, deployment_id, variables=None, description='', cacheClusterEnabled=None, cacheClusterSize=None):

View File

@ -341,6 +341,7 @@ class AutoScalingBackend(BaseBackend):
max_size = make_int(max_size) max_size = make_int(max_size)
min_size = make_int(min_size) min_size = make_int(min_size)
desired_capacity = make_int(desired_capacity)
default_cooldown = make_int(default_cooldown) default_cooldown = make_int(default_cooldown)
if health_check_period is None: if health_check_period is None:
health_check_period = 300 health_check_period = 300

View File

@ -139,6 +139,8 @@ class LambdaFunction(object):
print("Exception %s", ex) print("Exception %s", ex)
try: try:
original_stdout = sys.stdout
original_stderr = sys.stderr
codeOut = StringIO() codeOut = StringIO()
codeErr = StringIO() codeErr = StringIO()
sys.stdout = codeOut sys.stdout = codeOut
@ -154,8 +156,8 @@ class LambdaFunction(object):
finally: finally:
codeErr.close() codeErr.close()
codeOut.close() codeOut.close()
sys.stdout = sys.__stdout__ sys.stdout = original_stdout
sys.stderr = sys.__stderr__ sys.stderr = original_stderr
return self.convert(result) return self.convert(result)
def invoke(self, body, request_headers, response_headers): def invoke(self, body, request_headers, response_headers):

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import datetime from datetime import datetime
import json import json
import uuid
import boto.cloudformation import boto.cloudformation
from moto.core import BaseBackend from moto.core import BaseBackend
@ -81,11 +82,10 @@ class FakeStack(object):
def stack_outputs(self): def stack_outputs(self):
return self.output_map.values() return self.output_map.values()
def update(self, template, role_arn=None): def update(self, template, role_arn=None, parameters=None):
self._add_stack_event("UPDATE_IN_PROGRESS", self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated")
resource_status_reason="User Initiated")
self.template = template self.template = template
self.resource_map.update(json.loads(template)) self.resource_map.update(json.loads(template), parameters)
self.output_map = self._create_output_map() self.output_map = self._create_output_map()
self._add_stack_event("UPDATE_COMPLETE") self._add_stack_event("UPDATE_COMPLETE")
self.status = "UPDATE_COMPLETE" self.status = "UPDATE_COMPLETE"
@ -111,6 +111,7 @@ class FakeEvent(object):
self.resource_status_reason = resource_status_reason self.resource_status_reason = resource_status_reason
self.resource_properties = resource_properties self.resource_properties = resource_properties
self.timestamp = datetime.utcnow() self.timestamp = datetime.utcnow()
self.event_id = uuid.uuid4()
class CloudFormationBackend(BaseBackend): class CloudFormationBackend(BaseBackend):
@ -163,9 +164,9 @@ class CloudFormationBackend(BaseBackend):
if stack.name == name_or_stack_id: if stack.name == name_or_stack_id:
return stack return stack
def update_stack(self, name, template, role_arn=None): def update_stack(self, name, template, role_arn=None, parameters=None):
stack = self.get_stack(name) stack = self.get_stack(name)
stack.update(template, role_arn) stack.update(template, role_arn, parameters=parameters)
return stack return stack
def list_stack_resources(self, stack_name_or_id): def list_stack_resources(self, stack_name_or_id):

View File

@ -143,7 +143,7 @@ def clean_json(resource_json, resources_map):
if 'Fn::If' in resource_json: if 'Fn::If' in resource_json:
condition_name, true_value, false_value = resource_json['Fn::If'] condition_name, true_value, false_value = resource_json['Fn::If']
if resources_map[condition_name]: if resources_map.lazy_condition_map[condition_name]:
return clean_json(true_value, resources_map) return clean_json(true_value, resources_map)
else: else:
return clean_json(false_value, resources_map) return clean_json(false_value, resources_map)
@ -207,7 +207,7 @@ def parse_resource(logical_id, resource_json, resources_map):
def parse_and_create_resource(logical_id, resource_json, resources_map, region_name): def parse_and_create_resource(logical_id, resource_json, resources_map, region_name):
condition = resource_json.get('Condition') condition = resource_json.get('Condition')
if condition and not resources_map[condition]: if condition and not resources_map.lazy_condition_map[condition]:
# If this has a False condition, don't create the resource # If this has a False condition, don't create the resource
return None return None
@ -359,14 +359,13 @@ class ResourceMap(collections.Mapping):
def load_conditions(self): def load_conditions(self):
conditions = self._template.get('Conditions', {}) conditions = self._template.get('Conditions', {})
lazy_condition_map = LazyDict() self.lazy_condition_map = LazyDict()
for condition_name, condition in conditions.items(): for condition_name, condition in conditions.items():
lazy_condition_map[condition_name] = functools.partial(parse_condition, self.lazy_condition_map[condition_name] = functools.partial(parse_condition,
condition, self._parsed_resources, lazy_condition_map) condition, self._parsed_resources, self.lazy_condition_map)
for condition_name in lazy_condition_map: for condition_name in self.lazy_condition_map:
self._parsed_resources[ _ = self.lazy_condition_map[condition_name]
condition_name] = lazy_condition_map[condition_name]
def create(self): def create(self):
self.load_mapping() self.load_mapping()
@ -383,7 +382,9 @@ class ResourceMap(collections.Mapping):
ec2_models.ec2_backends[self._region_name].create_tags( ec2_models.ec2_backends[self._region_name].create_tags(
[self[resource].physical_resource_id], self.tags) [self[resource].physical_resource_id], self.tags)
def update(self, template): def update(self, template, parameters=None):
if parameters:
self.input_parameters = parameters
self.load_mapping() self.load_mapping()
self.load_parameters() self.load_parameters()
self.load_conditions() self.load_conditions()

View File

@ -147,6 +147,11 @@ class CloudFormationResponse(BaseResponse):
stack_name).template stack_name).template
else: else:
stack_body = self._get_param('TemplateBody') stack_body = self._get_param('TemplateBody')
parameters = dict([
(parameter['parameter_key'], parameter['parameter_value'])
for parameter
in self._get_list_prefix("Parameters.member")
])
stack = self.cloudformation_backend.get_stack(stack_name) stack = self.cloudformation_backend.get_stack(stack_name)
if stack.status == 'ROLLBACK_COMPLETE': if stack.status == 'ROLLBACK_COMPLETE':
@ -157,6 +162,7 @@ class CloudFormationResponse(BaseResponse):
name=stack_name, name=stack_name,
template=stack_body, template=stack_body,
role_arn=role_arn, role_arn=role_arn,
parameters=parameters
) )
if self.request_json: if self.request_json:
stack_body = { stack_body = {
@ -296,7 +302,7 @@ DESCRIBE_STACK_RESOURCES_RESPONSE = """<DescribeStackResourcesResponse>
DESCRIBE_STACK_EVENTS_RESPONSE = """<DescribeStackEventsResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/"> DESCRIBE_STACK_EVENTS_RESPONSE = """<DescribeStackEventsResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
<DescribeStackEventsResult> <DescribeStackEventsResult>
<StackEvents> <StackEvents>
{% for event in stack.events %} {% for event in stack.events[::-1] %}
<member> <member>
<Timestamp>{{ event.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ') }}</Timestamp> <Timestamp>{{ event.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ') }}</Timestamp>
<ResourceStatus>{{ event.resource_status }}</ResourceStatus> <ResourceStatus>{{ event.resource_status }}</ResourceStatus>

View File

@ -349,8 +349,8 @@ class NetworkInterfaceBackend(object):
return generic_filter(filters, enis) return generic_filter(filters, enis)
class Instance(BotoInstance, TaggedEC2Resource):
class Instance(TaggedEC2Resource, BotoInstance):
def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs): def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs):
super(Instance, self).__init__() super(Instance, self).__init__()
self.ec2_backend = ec2_backend self.ec2_backend = ec2_backend
@ -458,7 +458,10 @@ class Instance(BotoInstance, TaggedEC2Resource):
key_name=properties.get("KeyName"), key_name=properties.get("KeyName"),
private_ip=properties.get('PrivateIpAddress'), private_ip=properties.get('PrivateIpAddress'),
) )
return reservation.instances[0] instance = reservation.instances[0]
for tag in properties.get("Tags", []):
instance.add_tag(tag["Key"], tag["Value"])
return instance
@property @property
def physical_resource_id(self): def physical_resource_id(self):
@ -2956,7 +2959,7 @@ class ElasticAddress(object):
@property @property
def physical_resource_id(self): def physical_resource_id(self):
return self.allocation_id if self.allocation_id else self.public_ip return self.public_ip
def get_cfn_attribute(self, attribute_name): def get_cfn_attribute(self, attribute_name):
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException from moto.cloudformation.exceptions import UnformattedGetAttTemplateException

View File

@ -382,9 +382,11 @@ class EC2ContainerServiceBackend(BaseBackend):
if not container_instances: if not container_instances:
raise Exception( raise Exception(
"No instances found in cluster {}".format(cluster_name)) "No instances found in cluster {}".format(cluster_name))
active_container_instances = [x for x in container_instances if
self.container_instances[cluster_name][x].status == 'ACTIVE']
for _ in range(count or 1): for _ in range(count or 1):
container_instance_arn = self.container_instances[cluster_name][ container_instance_arn = self.container_instances[cluster_name][
container_instances[randint(0, len(container_instances) - 1)] active_container_instances[randint(0, len(active_container_instances) - 1)]
].containerInstanceArn ].containerInstanceArn
task = Task(cluster, task_definition, container_instance_arn, task = Task(cluster, task_definition, container_instance_arn,
overrides or {}, started_by or '') overrides or {}, started_by or '')
@ -574,6 +576,25 @@ class EC2ContainerServiceBackend(BaseBackend):
return container_instance_objects, failures return container_instance_objects, failures
def update_container_instances_state(self, cluster_str, list_container_instance_ids, status):
cluster_name = cluster_str.split('/')[-1]
if cluster_name not in self.clusters:
raise Exception("{0} is not a cluster".format(cluster_name))
status = status.upper()
if status not in ['ACTIVE', 'DRAINING']:
raise Exception("An error occurred (InvalidParameterException) when calling the UpdateContainerInstancesState operation: Container instances status should be one of [ACTIVE,DRAINING]")
failures = []
container_instance_objects = []
for container_instance_id in list_container_instance_ids:
container_instance = self.container_instances[cluster_name].get(container_instance_id, None)
if container_instance is not None:
container_instance.status = status
container_instance_objects.append(container_instance)
else:
failures.append(ContainerInstanceFailure('MISSING', container_instance_id))
return container_instance_objects, failures
def deregister_container_instance(self, cluster_str, container_instance_str): def deregister_container_instance(self, cluster_str, container_instance_str):
pass pass

View File

@ -220,3 +220,13 @@ class EC2ContainerServiceResponse(BaseResponse):
'failures': [ci.response_object for ci in failures], 'failures': [ci.response_object for ci in failures],
'containerInstances': [ci.response_object for ci in container_instances] 'containerInstances': [ci.response_object for ci in container_instances]
}) })
def update_container_instances_state(self):
cluster_str = self._get_param('cluster')
list_container_instance_arns = self._get_param('containerInstances')
status_str = self._get_param('status')
container_instances, failures = self.ecs_backend.update_container_instances_state(cluster_str, list_container_instance_arns, status_str)
return json.dumps({
'failures': [ci.response_object for ci in failures],
'containerInstances': [ci.response_object for ci in container_instances]
})

View File

@ -171,6 +171,7 @@ class Group(object):
) )
self.users = [] self.users = []
self.policies = {}
def get_cfn_attribute(self, attribute_name): def get_cfn_attribute(self, attribute_name):
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
@ -178,6 +179,24 @@ class Group(object):
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
raise UnformattedGetAttTemplateException() raise UnformattedGetAttTemplateException()
def get_policy(self, policy_name):
try:
policy_json = self.policies[policy_name]
except KeyError:
raise IAMNotFoundException("Policy {0} not found".format(policy_name))
return {
'policy_name': policy_name,
'policy_document': policy_json,
'group_name': self.name,
}
def put_policy(self, policy_name, policy_json):
self.policies[policy_name] = policy_json
def list_policies(self):
return self.policies.keys()
class User(object): class User(object):
@ -589,6 +608,18 @@ class IAMBackend(BaseBackend):
return groups return groups
def put_group_policy(self, group_name, policy_name, policy_json):
group = self.get_group(group_name)
group.put_policy(policy_name, policy_json)
def list_group_policies(self, group_name, marker=None, max_items=None):
group = self.get_group(group_name)
return group.list_policies()
def get_group_policy(self, group_name, policy_name):
group = self.get_group(group_name)
return group.get_policy(policy_name)
def create_user(self, user_name, path='/'): def create_user(self, user_name, path='/'):
if user_name in self.users: if user_name in self.users:
raise IAMConflictException( raise IAMConflictException(

View File

@ -198,6 +198,32 @@ class IamResponse(BaseResponse):
template = self.response_template(LIST_GROUPS_FOR_USER_TEMPLATE) template = self.response_template(LIST_GROUPS_FOR_USER_TEMPLATE)
return template.render(groups=groups) return template.render(groups=groups)
def put_group_policy(self):
group_name = self._get_param('GroupName')
policy_name = self._get_param('PolicyName')
policy_document = self._get_param('PolicyDocument')
iam_backend.put_group_policy(group_name, policy_name, policy_document)
template = self.response_template(GENERIC_EMPTY_TEMPLATE)
return template.render(name="PutGroupPolicyResponse")
def list_group_policies(self):
group_name = self._get_param('GroupName')
marker = self._get_param('Marker')
max_items = self._get_param('MaxItems')
policies = iam_backend.list_group_policies(group_name,
marker=marker, max_items=max_items)
template = self.response_template(LIST_GROUP_POLICIES_TEMPLATE)
return template.render(name="ListGroupPoliciesResponse",
policies=policies,
marker=marker)
def get_group_policy(self):
group_name = self._get_param('GroupName')
policy_name = self._get_param('PolicyName')
policy_result = iam_backend.get_group_policy(group_name, policy_name)
template = self.response_template(GET_GROUP_POLICY_TEMPLATE)
return template.render(name="GetGroupPolicyResponse", **policy_result)
def create_user(self): def create_user(self):
user_name = self._get_param('UserName') user_name = self._get_param('UserName')
path = self._get_param('Path') path = self._get_param('Path')
@ -206,6 +232,7 @@ class IamResponse(BaseResponse):
template = self.response_template(USER_TEMPLATE) template = self.response_template(USER_TEMPLATE)
return template.render(action='Create', user=user) return template.render(action='Create', user=user)
def get_user(self): def get_user(self):
user_name = self._get_param('UserName') user_name = self._get_param('UserName')
user = iam_backend.get_user(user_name) user = iam_backend.get_user(user_name)
@ -590,18 +617,16 @@ LIST_SERVER_CERTIFICATES_TEMPLATE = """<ListServerCertificatesResponse>
<ServerCertificateMetadataList> <ServerCertificateMetadataList>
{% for certificate in server_certificates %} {% for certificate in server_certificates %}
<member> <member>
<ServerCertificateMetadata> <ServerCertificateName>{{ certificate.cert_name }}</ServerCertificateName>
<ServerCertificateName>{{ certificate.cert_name }}</ServerCertificateName> {% if certificate.path %}
{% if certificate.path %} <Path>{{ certificate.path }}</Path>
<Path>{{ certificate.path }}</Path> <Arn>arn:aws:iam::123456789012:server-certificate/{{ certificate.path }}/{{ certificate.cert_name }}</Arn>
<Arn>arn:aws:iam::123456789012:server-certificate/{{ certificate.path }}/{{ certificate.cert_name }}</Arn> {% else %}
{% else %} <Arn>arn:aws:iam::123456789012:server-certificate/{{ certificate.cert_name }}</Arn>
<Arn>arn:aws:iam::123456789012:server-certificate/{{ certificate.cert_name }}</Arn> {% endif %}
{% endif %} <UploadDate>2010-05-08T01:02:03.004Z</UploadDate>
<UploadDate>2010-05-08T01:02:03.004Z</UploadDate> <ServerCertificateId>ASCACKCEVSQ6C2EXAMPLE</ServerCertificateId>
<ServerCertificateId>ASCACKCEVSQ6C2EXAMPLE</ServerCertificateId> <Expiration>2012-05-08T01:02:03.004Z</Expiration>
<Expiration>2012-05-08T01:02:03.004Z</Expiration>
</ServerCertificateMetadata>
</member> </member>
{% endfor %} {% endfor %}
</ServerCertificateMetadataList> </ServerCertificateMetadataList>
@ -713,6 +738,35 @@ LIST_GROUPS_FOR_USER_TEMPLATE = """<ListGroupsForUserResponse>
</ResponseMetadata> </ResponseMetadata>
</ListGroupsForUserResponse>""" </ListGroupsForUserResponse>"""
LIST_GROUP_POLICIES_TEMPLATE = """<ListGroupPoliciesResponse>
<ListGroupPoliciesResult>
{% if marker is none %}
<IsTruncated>false</IsTruncated>
{% else %}
<IsTruncated>true</IsTruncated>
<Marker>{{ marker }}</Marker>
{% endif %}
<PolicyNames>
{% for policy in policies %}
<member>{{ policy }}</member>
{% endfor %}
</PolicyNames>
</ListGroupPoliciesResult>
<ResponseMetadata>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata>
</ListGroupPoliciesResponse>"""
GET_GROUP_POLICY_TEMPLATE = """<GetGroupPolicyResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<GetGroupPolicyResult>
<PolicyName>{{ policy_name }}</PolicyName>
<GroupName>{{ group_name }}</GroupName>
<PolicyDocument>{{ policy_document }}</PolicyDocument>
</GetGroupPolicyResult>
<ResponseMetadata>
<RequestId>7e7cd8bc-99ef-11e1-a4c3-27EXAMPLE804</RequestId>
</ResponseMetadata>
</GetGroupPolicyResponse>"""
USER_TEMPLATE = """<{{ action }}UserResponse> USER_TEMPLATE = """<{{ action }}UserResponse>
<{{ action }}UserResult> <{{ action }}UserResult>

View File

@ -226,8 +226,11 @@ class RecordSetGroup(object):
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
properties = cloudformation_json['Properties'] properties = cloudformation_json['Properties']
zone_name = properties["HostedZoneName"] zone_name = properties.get("HostedZoneName")
hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name) if zone_name:
hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name)
else:
hosted_zone = route53_backend.get_hosted_zone(properties["HostedZoneId"])
record_sets = properties["RecordSets"] record_sets = properties["RecordSets"]
for record_set in record_sets: for record_set in record_sets:
hosted_zone.add_rrset(record_set) hosted_zone.add_rrset(record_set)

View File

@ -90,7 +90,7 @@ class FakeKey(object):
@property @property
def response_dict(self): def response_dict(self):
res = { res = {
'etag': self.etag, 'Etag': self.etag,
'last-modified': self.last_modified_RFC1123, 'last-modified': self.last_modified_RFC1123,
'content-length': str(len(self.value)), 'content-length': str(len(self.value)),
} }

View File

@ -183,9 +183,8 @@ class Queue(object):
self, camelcase_to_underscores(attribute)) self, camelcase_to_underscores(attribute))
return result return result
@property def url(self, request_url):
def url(self): return "{0}://{1}/123456789012/{2}".format(request_url.scheme, request_url.netloc, self.name)
return "http://sqs.{0}.amazonaws.com/123456789012/{1}".format(self.region, self.name)
@property @property
def messages(self): def messages(self):

View File

@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from six.moves.urllib.parse import urlparse
from moto.core.responses import BaseResponse from moto.core.responses import BaseResponse
from moto.core.utils import camelcase_to_underscores from moto.core.utils import camelcase_to_underscores
@ -58,26 +59,29 @@ class SQSResponse(BaseResponse):
return status_code, headers, body return status_code, headers, body
def create_queue(self): def create_queue(self):
request_url = urlparse(self.uri)
queue_name = self.querystring.get("QueueName")[0] queue_name = self.querystring.get("QueueName")[0]
queue = self.sqs_backend.create_queue(queue_name, visibility_timeout=self.attribute.get('VisibilityTimeout'), queue = self.sqs_backend.create_queue(queue_name, visibility_timeout=self.attribute.get('VisibilityTimeout'),
wait_time_seconds=self.attribute.get('WaitTimeSeconds')) wait_time_seconds=self.attribute.get('WaitTimeSeconds'))
template = self.response_template(CREATE_QUEUE_RESPONSE) template = self.response_template(CREATE_QUEUE_RESPONSE)
return template.render(queue=queue) return template.render(queue=queue, request_url=request_url)
def get_queue_url(self): def get_queue_url(self):
request_url = urlparse(self.uri)
queue_name = self.querystring.get("QueueName")[0] queue_name = self.querystring.get("QueueName")[0]
queue = self.sqs_backend.get_queue(queue_name) queue = self.sqs_backend.get_queue(queue_name)
if queue: if queue:
template = self.response_template(GET_QUEUE_URL_RESPONSE) template = self.response_template(GET_QUEUE_URL_RESPONSE)
return template.render(queue=queue) return template.render(queue=queue, request_url=request_url)
else: else:
return "", dict(status=404) return "", dict(status=404)
def list_queues(self): def list_queues(self):
request_url = urlparse(self.uri)
queue_name_prefix = self.querystring.get("QueueNamePrefix", [None])[0] queue_name_prefix = self.querystring.get("QueueNamePrefix", [None])[0]
queues = self.sqs_backend.list_queues(queue_name_prefix) queues = self.sqs_backend.list_queues(queue_name_prefix)
template = self.response_template(LIST_QUEUES_RESPONSE) template = self.response_template(LIST_QUEUES_RESPONSE)
return template.render(queues=queues) return template.render(queues=queues, request_url=request_url)
def change_message_visibility(self): def change_message_visibility(self):
queue_name = self._get_queue_name() queue_name = self._get_queue_name()
@ -276,7 +280,7 @@ class SQSResponse(BaseResponse):
CREATE_QUEUE_RESPONSE = """<CreateQueueResponse> CREATE_QUEUE_RESPONSE = """<CreateQueueResponse>
<CreateQueueResult> <CreateQueueResult>
<QueueUrl>{{ queue.url }}</QueueUrl> <QueueUrl>{{ queue.url(request_url) }}</QueueUrl>
<VisibilityTimeout>{{ queue.visibility_timeout }}</VisibilityTimeout> <VisibilityTimeout>{{ queue.visibility_timeout }}</VisibilityTimeout>
</CreateQueueResult> </CreateQueueResult>
<ResponseMetadata> <ResponseMetadata>
@ -286,7 +290,7 @@ CREATE_QUEUE_RESPONSE = """<CreateQueueResponse>
GET_QUEUE_URL_RESPONSE = """<GetQueueUrlResponse> GET_QUEUE_URL_RESPONSE = """<GetQueueUrlResponse>
<GetQueueUrlResult> <GetQueueUrlResult>
<QueueUrl>{{ queue.url }}</QueueUrl> <QueueUrl>{{ queue.url(request_url) }}</QueueUrl>
</GetQueueUrlResult> </GetQueueUrlResult>
<ResponseMetadata> <ResponseMetadata>
<RequestId>470a6f13-2ed9-4181-ad8a-2fdea142988e</RequestId> <RequestId>470a6f13-2ed9-4181-ad8a-2fdea142988e</RequestId>
@ -296,7 +300,7 @@ GET_QUEUE_URL_RESPONSE = """<GetQueueUrlResponse>
LIST_QUEUES_RESPONSE = """<ListQueuesResponse> LIST_QUEUES_RESPONSE = """<ListQueuesResponse>
<ListQueuesResult> <ListQueuesResult>
{% for queue in queues %} {% for queue in queues %}
<QueueUrl>{{ queue.url }}</QueueUrl> <QueueUrl>{{ queue.url(request_url) }}</QueueUrl>
{% endfor %} {% endfor %}
</ListQueuesResult> </ListQueuesResult>
<ResponseMetadata> <ResponseMetadata>

View File

@ -5,6 +5,6 @@ sure==1.2.24
coverage coverage
freezegun freezegun
flask flask
boto3>=1.3.1 boto3>=1.4.4
botocore>=1.4.28 botocore>=1.4.28
six six

View File

@ -236,6 +236,10 @@ template = {
"Ref": "AWS::StackId" "Ref": "AWS::StackId"
}, },
"Key": "Application" "Key": "Application"
},
{
"Value": "Bar",
"Key": "Foo"
} }
], ],
"SecurityGroupIds": [ "SecurityGroupIds": [

View File

@ -12,7 +12,7 @@ import sure # noqa
import tests.backport_assert_raises # noqa import tests.backport_assert_raises # noqa
from nose.tools import assert_raises from nose.tools import assert_raises
from moto import mock_cloudformation_deprecated, mock_s3_deprecated from moto import mock_cloudformation_deprecated, mock_s3_deprecated, mock_route53_deprecated
from moto.cloudformation import cloudformation_backends from moto.cloudformation import cloudformation_backends
dummy_template = { dummy_template = {
@ -69,6 +69,57 @@ def test_create_stack():
}) })
@mock_cloudformation_deprecated
@mock_route53_deprecated
def test_create_stack_hosted_zone_by_id():
conn = boto.connect_cloudformation()
dummy_template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Stack 1",
"Parameters": {
},
"Resources": {
"Bar": {
"Type" : "AWS::Route53::HostedZone",
"Properties" : {
"Name" : "foo.bar.baz",
}
},
},
}
dummy_template2 = {
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Stack 2",
"Parameters": {
"ZoneId": { "Type": "String" }
},
"Resources": {
"Foo": {
"Properties": {
"HostedZoneId": {"Ref": "ZoneId"},
"RecordSets": []
},
"Type": "AWS::Route53::RecordSetGroup"
}
},
}
conn.create_stack(
"test_stack",
template_body=json.dumps(dummy_template),
parameters={}.items()
)
r53_conn = boto.connect_route53()
zone_id = r53_conn.get_zones()[0].id
conn.create_stack(
"test_stack",
template_body=json.dumps(dummy_template2),
parameters={"ZoneId": zone_id}.items()
)
stack = conn.describe_stacks()[0]
assert stack.list_resources()
@mock_cloudformation_deprecated @mock_cloudformation_deprecated
def test_creating_stacks_across_regions(): def test_creating_stacks_across_regions():
west1_conn = boto.cloudformation.connect_to_region("us-west-1") west1_conn = boto.cloudformation.connect_to_region("us-west-1")
@ -281,6 +332,60 @@ def test_cloudformation_params():
param.value.should.equal('testing123') param.value.should.equal('testing123')
@mock_cloudformation_deprecated
def test_cloudformation_params_conditions_and_resources_are_distinct():
dummy_template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Stack 1",
"Conditions": {
"FooEnabled": {
"Fn::Equals": [
{
"Ref": "FooEnabled"
},
"true"
]
},
"FooDisabled": {
"Fn::Not": [
{
"Fn::Equals": [
{
"Ref": "FooEnabled"
},
"true"
]
}
]
}
},
"Parameters": {
"FooEnabled": {
"Type": "String",
"AllowedValues": [
"true",
"false"
]
}
},
"Resources": {
"Bar": {
"Properties": {
"CidrBlock": "192.168.0.0/16",
},
"Condition": "FooDisabled",
"Type": "AWS::EC2::VPC"
}
}
}
dummy_template_json = json.dumps(dummy_template)
cfn = boto.connect_cloudformation()
cfn.create_stack('test_stack1', template_body=dummy_template_json, parameters=[('FooEnabled', 'true')])
stack = cfn.describe_stacks('test_stack1')[0]
resources = stack.list_resources()
assert not [resource for resource in resources if resource.logical_resource_id == 'Bar']
@mock_cloudformation_deprecated @mock_cloudformation_deprecated
def test_stack_tags(): def test_stack_tags():
conn = boto.connect_cloudformation() conn = boto.connect_cloudformation()
@ -341,6 +446,42 @@ def test_update_stack():
}) })
@mock_cloudformation_deprecated
def test_update_stack_with_parameters():
dummy_template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Stack",
"Resources": {
"VPC": {
"Properties": {
"CidrBlock": {"Ref": "Bar"}
},
"Type": "AWS::EC2::VPC"
}
},
"Parameters": {
"Bar": {
"Type": "String"
}
}
}
dummy_template_json = json.dumps(dummy_template)
conn = boto.connect_cloudformation()
conn.create_stack(
"test_stack",
template_body=dummy_template_json,
parameters=[("Bar", "192.168.0.0/16")]
)
conn.update_stack(
"test_stack",
template_body=dummy_template_json,
parameters=[("Bar", "192.168.0.1/16")]
)
stack = conn.describe_stacks()[0]
assert stack.parameters[0].value == "192.168.0.1/16"
@mock_cloudformation_deprecated @mock_cloudformation_deprecated
def test_update_stack_when_rolled_back(): def test_update_stack_when_rolled_back():
conn = boto.connect_cloudformation() conn = boto.connect_cloudformation()
@ -374,16 +515,21 @@ def test_describe_stack_events_shows_create_update_and_delete():
events[0].resource_type.should.equal("AWS::CloudFormation::Stack") events[0].resource_type.should.equal("AWS::CloudFormation::Stack")
events[-1].resource_type.should.equal("AWS::CloudFormation::Stack") events[-1].resource_type.should.equal("AWS::CloudFormation::Stack")
# testing ordering of stack events without assuming resource events will # testing ordering of stack events without assuming resource events will not exist
# not exist # the AWS API returns events in reverse chronological order
stack_events_to_look_for = iter([ stack_events_to_look_for = iter([
("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None), ("DELETE_COMPLETE", None),
("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None), ("DELETE_IN_PROGRESS", "User Initiated"),
("DELETE_IN_PROGRESS", "User Initiated"), ("DELETE_COMPLETE", None)]) ("UPDATE_COMPLETE", None),
("UPDATE_IN_PROGRESS", "User Initiated"),
("CREATE_COMPLETE", None),
("CREATE_IN_PROGRESS", "User Initiated"),
])
try: try:
for event in events: for event in events:
event.stack_id.should.equal(stack_id) event.stack_id.should.equal(stack_id)
event.stack_name.should.equal("test_stack") event.stack_name.should.equal("test_stack")
event.event_id.should.match(r"[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}")
if event.resource_type == "AWS::CloudFormation::Stack": if event.resource_type == "AWS::CloudFormation::Stack":
event.logical_resource_id.should.equal("test_stack") event.logical_resource_id.should.equal("test_stack")

View File

@ -354,16 +354,21 @@ def test_stack_events():
events[0].resource_type.should.equal("AWS::CloudFormation::Stack") events[0].resource_type.should.equal("AWS::CloudFormation::Stack")
events[-1].resource_type.should.equal("AWS::CloudFormation::Stack") events[-1].resource_type.should.equal("AWS::CloudFormation::Stack")
# testing ordering of stack events without assuming resource events will # testing ordering of stack events without assuming resource events will not exist
# not exist # the AWS API returns events in reverse chronological order
stack_events_to_look_for = iter([ stack_events_to_look_for = iter([
("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None), ("DELETE_COMPLETE", None),
("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None), ("DELETE_IN_PROGRESS", "User Initiated"),
("DELETE_IN_PROGRESS", "User Initiated"), ("DELETE_COMPLETE", None)]) ("UPDATE_COMPLETE", None),
("UPDATE_IN_PROGRESS", "User Initiated"),
("CREATE_COMPLETE", None),
("CREATE_IN_PROGRESS", "User Initiated"),
])
try: try:
for event in events: for event in events:
event.stack_id.should.equal(stack.stack_id) event.stack_id.should.equal(stack.stack_id)
event.stack_name.should.equal("test_stack") event.stack_name.should.equal("test_stack")
event.event_id.should.match(r"[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}")
if event.resource_type == "AWS::CloudFormation::Stack": if event.resource_type == "AWS::CloudFormation::Stack":
event.logical_resource_id.should.equal("test_stack") event.logical_resource_id.should.equal("test_stack")

View File

@ -548,6 +548,7 @@ def test_autoscaling_group_with_elb():
"LaunchConfigurationName": {"Ref": "my-launch-config"}, "LaunchConfigurationName": {"Ref": "my-launch-config"},
"MinSize": "2", "MinSize": "2",
"MaxSize": "2", "MaxSize": "2",
"DesiredCapacity": "2",
"LoadBalancerNames": [{"Ref": "my-elb"}] "LoadBalancerNames": [{"Ref": "my-elb"}]
}, },
}, },
@ -631,6 +632,7 @@ def test_autoscaling_group_update():
"LaunchConfigurationName": {"Ref": "my-launch-config"}, "LaunchConfigurationName": {"Ref": "my-launch-config"},
"MinSize": "2", "MinSize": "2",
"MaxSize": "2", "MaxSize": "2",
"DesiredCapacity": "2"
}, },
}, },
@ -655,6 +657,7 @@ def test_autoscaling_group_update():
asg = autoscale_conn.get_all_groups()[0] asg = autoscale_conn.get_all_groups()[0]
asg.min_size.should.equal(2) asg.min_size.should.equal(2)
asg.max_size.should.equal(2) asg.max_size.should.equal(2)
asg.desired_capacity.should.equal(2)
asg_template['Resources']['my-as-group']['Properties']['MaxSize'] = 3 asg_template['Resources']['my-as-group']['Properties']['MaxSize'] = 3
asg_template_json = json.dumps(asg_template) asg_template_json = json.dumps(asg_template)
@ -665,6 +668,7 @@ def test_autoscaling_group_update():
asg = autoscale_conn.get_all_groups()[0] asg = autoscale_conn.get_all_groups()[0]
asg.min_size.should.equal(2) asg.min_size.should.equal(2)
asg.max_size.should.equal(3) asg.max_size.should.equal(3)
asg.desired_capacity.should.equal(2)
@mock_ec2_deprecated() @mock_ec2_deprecated()
@ -693,6 +697,7 @@ def test_vpc_single_instance_in_subnet():
ec2_conn = boto.ec2.connect_to_region("us-west-1") ec2_conn = boto.ec2.connect_to_region("us-west-1")
reservation = ec2_conn.get_all_instances()[0] reservation = ec2_conn.get_all_instances()[0]
instance = reservation.instances[0] instance = reservation.instances[0]
instance.tags["Foo"].should.equal("Bar")
# Check that the EIP is attached the the EC2 instance # Check that the EIP is attached the the EC2 instance
eip = ec2_conn.get_all_addresses()[0] eip = ec2_conn.get_all_addresses()[0]
eip.domain.should.equal('vpc') eip.domain.should.equal('vpc')
@ -714,7 +719,7 @@ def test_vpc_single_instance_in_subnet():
eip_resource = [ eip_resource = [
resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0]
eip_resource.physical_resource_id.should.equal(eip.allocation_id) eip_resource.physical_resource_id.should.equal(eip.public_ip)
@mock_cloudformation() @mock_cloudformation()
@ -1027,7 +1032,7 @@ def test_vpc_eip():
resources = stack.describe_resources() resources = stack.describe_resources()
cfn_eip = [ cfn_eip = [
resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0]
cfn_eip.physical_resource_id.should.equal(eip.allocation_id) cfn_eip.physical_resource_id.should.equal(eip.public_ip)
@mock_ec2_deprecated() @mock_ec2_deprecated()

View File

@ -622,6 +622,58 @@ def test_describe_container_instances():
for arn in test_instance_arns: for arn in test_instance_arns:
response_arns.should.contain(arn) response_arns.should.contain(arn)
@mock_ec2
@mock_ecs
def test_update_container_instances_state():
ecs_client = boto3.client('ecs', region_name='us-east-1')
ec2 = boto3.resource('ec2', region_name='us-east-1')
test_cluster_name = 'test_ecs_cluster'
_ = ecs_client.create_cluster(
clusterName=test_cluster_name
)
instance_to_create = 3
test_instance_arns = []
for i in range(0, instance_to_create):
test_instance = ec2.create_instances(
ImageId="ami-1234abcd",
MinCount=1,
MaxCount=1,
)[0]
instance_id_document = json.dumps(
ec2_utils.generate_instance_identity_document(test_instance)
)
response = ecs_client.register_container_instance(
cluster=test_cluster_name,
instanceIdentityDocument=instance_id_document)
test_instance_arns.append(response['containerInstance']['containerInstanceArn'])
test_instance_ids = list(map((lambda x: x.split('/')[1]), test_instance_arns))
response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='DRAINING')
len(response['failures']).should.equal(0)
len(response['containerInstances']).should.equal(instance_to_create)
response_statuses = [ci['status'] for ci in response['containerInstances']]
for status in response_statuses:
status.should.equal('DRAINING')
response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='DRAINING')
len(response['failures']).should.equal(0)
len(response['containerInstances']).should.equal(instance_to_create)
response_statuses = [ci['status'] for ci in response['containerInstances']]
for status in response_statuses:
status.should.equal('DRAINING')
response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='ACTIVE')
len(response['failures']).should.equal(0)
len(response['containerInstances']).should.equal(instance_to_create)
response_statuses = [ci['status'] for ci in response['containerInstances']]
for status in response_statuses:
status.should.equal('ACTIVE')
ecs_client.update_container_instances_state.when.called_with(cluster=test_cluster_name, containerInstances=test_instance_ids, status='test_status').should.throw(Exception)
@mock_ec2 @mock_ec2
@mock_ecs @mock_ecs

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import boto import boto
import boto3
import sure # noqa import sure # noqa
from nose.tools import assert_raises from nose.tools import assert_raises
@ -72,3 +73,40 @@ def test_get_groups_for_user():
groups = conn.get_groups_for_user( groups = conn.get_groups_for_user(
'my-user')['list_groups_for_user_response']['list_groups_for_user_result']['groups'] 'my-user')['list_groups_for_user_response']['list_groups_for_user_result']['groups']
groups.should.have.length_of(2) groups.should.have.length_of(2)
@mock_iam_deprecated()
def test_put_group_policy():
conn = boto.connect_iam()
conn.create_group('my-group')
conn.put_group_policy('my-group', 'my-policy', '{"some": "json"}')
@mock_iam_deprecated()
def test_get_group_policy():
conn = boto.connect_iam()
conn.create_group('my-group')
with assert_raises(BotoServerError):
conn.get_group_policy('my-group', 'my-policy')
conn.put_group_policy('my-group', 'my-policy', '{"some": "json"}')
policy = conn.get_group_policy('my-group', 'my-policy')
@mock_iam_deprecated()
def test_get_all_group_policies():
conn = boto.connect_iam()
conn.create_group('my-group')
policies = conn.get_all_group_policies('my-group')['list_group_policies_response']['list_group_policies_result']['policy_names']
assert policies == []
conn.put_group_policy('my-group', 'my-policy', '{"some": "json"}')
policies = conn.get_all_group_policies('my-group')['list_group_policies_response']['list_group_policies_result']['policy_names']
assert policies == ['my-policy']
@mock_iam()
def test_list_group_policies():
conn = boto3.client('iam')
conn.create_group(GroupName='my-group')
policies = conn.list_group_policies(GroupName='my-group')['PolicyNames'].should.be.empty
conn.put_group_policy(GroupName='my-group', PolicyName='my-policy', PolicyDocument='{"some": "json"}')
policies = conn.list_group_policies(GroupName='my-group')['PolicyNames'].should.equal(['my-policy'])

View File

@ -77,7 +77,7 @@ def test_create_queues_in_multiple_region():
list(west2_conn.list_queues()['QueueUrls']).should.have.length_of(1) list(west2_conn.list_queues()['QueueUrls']).should.have.length_of(1)
west1_conn.list_queues()['QueueUrls'][0].should.equal( west1_conn.list_queues()['QueueUrls'][0].should.equal(
'http://sqs.us-west-1.amazonaws.com/123456789012/blah') 'https://us-west-1.queue.amazonaws.com/123456789012/blah')
@mock_sqs @mock_sqs
@ -92,7 +92,7 @@ def test_get_queue_with_prefix():
queue = conn.list_queues(QueueNamePrefix="test-")['QueueUrls'] queue = conn.list_queues(QueueNamePrefix="test-")['QueueUrls']
queue.should.have.length_of(1) queue.should.have.length_of(1)
queue[0].should.equal( queue[0].should.equal(
"http://sqs.us-west-1.amazonaws.com/123456789012/test-queue") "https://us-west-1.queue.amazonaws.com/123456789012/test-queue")
@mock_sqs @mock_sqs