diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..8f2d40361 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..1266d508e --- /dev/null +++ b/CONTRIBUTING.md @@ -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/). diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..c3d7d3f65 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -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. diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index bfcfdbfa6..d5564fa61 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -339,11 +339,14 @@ class RestAPI(object): return status_code, {}, response 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) - 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) - responses.add_callback(responses.GET, stage_url.lower(), + responses.add_callback(responses.GET, stage_url_upper, callback=self.resource_callback) def create_stage(self, name, deployment_id, variables=None, description='', cacheClusterEnabled=None, cacheClusterSize=None): diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 18dfcb5fe..3b3a618e2 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -341,6 +341,7 @@ class AutoScalingBackend(BaseBackend): max_size = make_int(max_size) min_size = make_int(min_size) + desired_capacity = make_int(desired_capacity) default_cooldown = make_int(default_cooldown) if health_check_period is None: health_check_period = 300 diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 46d227300..7d21ccbe0 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -139,6 +139,8 @@ class LambdaFunction(object): print("Exception %s", ex) try: + original_stdout = sys.stdout + original_stderr = sys.stderr codeOut = StringIO() codeErr = StringIO() sys.stdout = codeOut @@ -154,8 +156,8 @@ class LambdaFunction(object): finally: codeErr.close() codeOut.close() - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ + sys.stdout = original_stdout + sys.stderr = original_stderr return self.convert(result) def invoke(self, body, request_headers, response_headers): diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 0a3dcc62d..a565c289c 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from datetime import datetime import json +import uuid import boto.cloudformation from moto.core import BaseBackend @@ -81,11 +82,10 @@ class FakeStack(object): def stack_outputs(self): return self.output_map.values() - def update(self, template, role_arn=None): - self._add_stack_event("UPDATE_IN_PROGRESS", - resource_status_reason="User Initiated") + def update(self, template, role_arn=None, parameters=None): + self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated") 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._add_stack_event("UPDATE_COMPLETE") self.status = "UPDATE_COMPLETE" @@ -111,6 +111,7 @@ class FakeEvent(object): self.resource_status_reason = resource_status_reason self.resource_properties = resource_properties self.timestamp = datetime.utcnow() + self.event_id = uuid.uuid4() class CloudFormationBackend(BaseBackend): @@ -163,9 +164,9 @@ class CloudFormationBackend(BaseBackend): if stack.name == name_or_stack_id: 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.update(template, role_arn) + stack.update(template, role_arn, parameters=parameters) return stack def list_stack_resources(self, stack_name_or_id): diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 9dcbdae29..337de2f2d 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -143,7 +143,7 @@ def clean_json(resource_json, resources_map): if 'Fn::If' in resource_json: 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) else: 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): 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 return None @@ -359,14 +359,13 @@ class ResourceMap(collections.Mapping): def load_conditions(self): conditions = self._template.get('Conditions', {}) - lazy_condition_map = LazyDict() + self.lazy_condition_map = LazyDict() for condition_name, condition in conditions.items(): - lazy_condition_map[condition_name] = functools.partial(parse_condition, - condition, self._parsed_resources, lazy_condition_map) + self.lazy_condition_map[condition_name] = functools.partial(parse_condition, + condition, self._parsed_resources, self.lazy_condition_map) - for condition_name in lazy_condition_map: - self._parsed_resources[ - condition_name] = lazy_condition_map[condition_name] + for condition_name in self.lazy_condition_map: + _ = self.lazy_condition_map[condition_name] def create(self): self.load_mapping() @@ -383,7 +382,9 @@ class ResourceMap(collections.Mapping): ec2_models.ec2_backends[self._region_name].create_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_parameters() self.load_conditions() diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index 272310d27..64923c7e6 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -147,6 +147,11 @@ class CloudFormationResponse(BaseResponse): stack_name).template else: 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) if stack.status == 'ROLLBACK_COMPLETE': @@ -157,6 +162,7 @@ class CloudFormationResponse(BaseResponse): name=stack_name, template=stack_body, role_arn=role_arn, + parameters=parameters ) if self.request_json: stack_body = { @@ -296,7 +302,7 @@ DESCRIBE_STACK_RESOURCES_RESPONSE = """ DESCRIBE_STACK_EVENTS_RESPONSE = """ - {% for event in stack.events %} + {% for event in stack.events[::-1] %} {{ event.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ') }} {{ event.resource_status }} diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 2e6b5e5b6..c7467feee 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -349,8 +349,8 @@ class NetworkInterfaceBackend(object): 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): super(Instance, self).__init__() self.ec2_backend = ec2_backend @@ -458,7 +458,10 @@ class Instance(BotoInstance, TaggedEC2Resource): key_name=properties.get("KeyName"), 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 def physical_resource_id(self): @@ -2956,7 +2959,7 @@ class ElasticAddress(object): @property 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): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 5a046c376..7efefdbaa 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -382,9 +382,11 @@ class EC2ContainerServiceBackend(BaseBackend): if not container_instances: raise Exception( "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): 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 task = Task(cluster, task_definition, container_instance_arn, overrides or {}, started_by or '') @@ -574,6 +576,25 @@ class EC2ContainerServiceBackend(BaseBackend): 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): pass diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index b28ec6a4e..338cfec28 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -220,3 +220,13 @@ class EC2ContainerServiceResponse(BaseResponse): 'failures': [ci.response_object for ci in failures], '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] + }) diff --git a/moto/iam/models.py b/moto/iam/models.py index 91c4a14d7..f00e02052 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -171,6 +171,7 @@ class Group(object): ) self.users = [] + self.policies = {} def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException @@ -178,6 +179,24 @@ class Group(object): raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') 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): @@ -589,6 +608,18 @@ class IAMBackend(BaseBackend): 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='/'): if user_name in self.users: raise IAMConflictException( diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 9bddd21df..cd9ddbf75 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -198,6 +198,32 @@ class IamResponse(BaseResponse): template = self.response_template(LIST_GROUPS_FOR_USER_TEMPLATE) 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): user_name = self._get_param('UserName') path = self._get_param('Path') @@ -206,6 +232,7 @@ class IamResponse(BaseResponse): template = self.response_template(USER_TEMPLATE) return template.render(action='Create', user=user) + def get_user(self): user_name = self._get_param('UserName') user = iam_backend.get_user(user_name) @@ -590,18 +617,16 @@ LIST_SERVER_CERTIFICATES_TEMPLATE = """ {% for certificate in server_certificates %} - - {{ certificate.cert_name }} - {% if certificate.path %} - {{ certificate.path }} - arn:aws:iam::123456789012:server-certificate/{{ certificate.path }}/{{ certificate.cert_name }} - {% else %} - arn:aws:iam::123456789012:server-certificate/{{ certificate.cert_name }} - {% endif %} - 2010-05-08T01:02:03.004Z - ASCACKCEVSQ6C2EXAMPLE - 2012-05-08T01:02:03.004Z - + {{ certificate.cert_name }} + {% if certificate.path %} + {{ certificate.path }} + arn:aws:iam::123456789012:server-certificate/{{ certificate.path }}/{{ certificate.cert_name }} + {% else %} + arn:aws:iam::123456789012:server-certificate/{{ certificate.cert_name }} + {% endif %} + 2010-05-08T01:02:03.004Z + ASCACKCEVSQ6C2EXAMPLE + 2012-05-08T01:02:03.004Z {% endfor %} @@ -713,6 +738,35 @@ LIST_GROUPS_FOR_USER_TEMPLATE = """ """ +LIST_GROUP_POLICIES_TEMPLATE = """ + + {% if marker is none %} + false + {% else %} + true + {{ marker }} + {% endif %} + + {% for policy in policies %} + {{ policy }} + {% endfor %} + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" + +GET_GROUP_POLICY_TEMPLATE = """ + + {{ policy_name }} + {{ group_name }} + {{ policy_document }} + + + 7e7cd8bc-99ef-11e1-a4c3-27EXAMPLE804 + +""" USER_TEMPLATE = """<{{ action }}UserResponse> <{{ action }}UserResult> diff --git a/moto/route53/models.py b/moto/route53/models.py index 338c6d30a..6e0ad35c0 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -226,8 +226,11 @@ class RecordSetGroup(object): def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] - zone_name = properties["HostedZoneName"] - hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name) + zone_name = properties.get("HostedZoneName") + 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"] for record_set in record_sets: hosted_zone.add_rrset(record_set) diff --git a/moto/s3/models.py b/moto/s3/models.py index c7bf557ca..77c6e1a00 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -90,7 +90,7 @@ class FakeKey(object): @property def response_dict(self): res = { - 'etag': self.etag, + 'Etag': self.etag, 'last-modified': self.last_modified_RFC1123, 'content-length': str(len(self.value)), } diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 5f4833772..2a6fc19b1 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -183,9 +183,8 @@ class Queue(object): self, camelcase_to_underscores(attribute)) return result - @property - def url(self): - return "http://sqs.{0}.amazonaws.com/123456789012/{1}".format(self.region, self.name) + def url(self, request_url): + return "{0}://{1}/123456789012/{2}".format(request_url.scheme, request_url.netloc, self.name) @property def messages(self): diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index 84886068e..75602b1b7 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from six.moves.urllib.parse import urlparse from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores @@ -58,26 +59,29 @@ class SQSResponse(BaseResponse): return status_code, headers, body def create_queue(self): + request_url = urlparse(self.uri) queue_name = self.querystring.get("QueueName")[0] queue = self.sqs_backend.create_queue(queue_name, visibility_timeout=self.attribute.get('VisibilityTimeout'), wait_time_seconds=self.attribute.get('WaitTimeSeconds')) 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): + request_url = urlparse(self.uri) queue_name = self.querystring.get("QueueName")[0] queue = self.sqs_backend.get_queue(queue_name) if queue: template = self.response_template(GET_QUEUE_URL_RESPONSE) - return template.render(queue=queue) + return template.render(queue=queue, request_url=request_url) else: return "", dict(status=404) def list_queues(self): + request_url = urlparse(self.uri) queue_name_prefix = self.querystring.get("QueueNamePrefix", [None])[0] queues = self.sqs_backend.list_queues(queue_name_prefix) 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): queue_name = self._get_queue_name() @@ -276,7 +280,7 @@ class SQSResponse(BaseResponse): CREATE_QUEUE_RESPONSE = """ - {{ queue.url }} + {{ queue.url(request_url) }} {{ queue.visibility_timeout }} @@ -286,7 +290,7 @@ CREATE_QUEUE_RESPONSE = """ GET_QUEUE_URL_RESPONSE = """ - {{ queue.url }} + {{ queue.url(request_url) }} 470a6f13-2ed9-4181-ad8a-2fdea142988e @@ -296,7 +300,7 @@ GET_QUEUE_URL_RESPONSE = """ LIST_QUEUES_RESPONSE = """ {% for queue in queues %} - {{ queue.url }} + {{ queue.url(request_url) }} {% endfor %} diff --git a/requirements-dev.txt b/requirements-dev.txt index 9bdccc6e4..554834a51 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,6 +5,6 @@ sure==1.2.24 coverage freezegun flask -boto3>=1.3.1 +boto3>=1.4.4 botocore>=1.4.28 six diff --git a/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py b/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py index 1f296cf0c..177da884e 100644 --- a/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py +++ b/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py @@ -236,6 +236,10 @@ template = { "Ref": "AWS::StackId" }, "Key": "Application" + }, + { + "Value": "Bar", + "Key": "Foo" } ], "SecurityGroupIds": [ diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index 619d8c3da..4085e6d8f 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -12,7 +12,7 @@ import sure # noqa import tests.backport_assert_raises # noqa 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 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 def test_creating_stacks_across_regions(): west1_conn = boto.cloudformation.connect_to_region("us-west-1") @@ -281,6 +332,60 @@ def test_cloudformation_params(): 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 def test_stack_tags(): 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 def test_update_stack_when_rolled_back(): 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[-1].resource_type.should.equal("AWS::CloudFormation::Stack") - # testing ordering of stack events without assuming resource events will - # not exist + # testing ordering of stack events without assuming resource events will not exist + # the AWS API returns events in reverse chronological order stack_events_to_look_for = iter([ - ("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None), - ("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None), - ("DELETE_IN_PROGRESS", "User Initiated"), ("DELETE_COMPLETE", None)]) + ("DELETE_COMPLETE", None), + ("DELETE_IN_PROGRESS", "User Initiated"), + ("UPDATE_COMPLETE", None), + ("UPDATE_IN_PROGRESS", "User Initiated"), + ("CREATE_COMPLETE", None), + ("CREATE_IN_PROGRESS", "User Initiated"), + ]) try: for event in events: event.stack_id.should.equal(stack_id) 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": event.logical_resource_id.should.equal("test_stack") diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 29e2dfa10..c69766209 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -354,16 +354,21 @@ def test_stack_events(): events[0].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 - # not exist + # testing ordering of stack events without assuming resource events will not exist + # the AWS API returns events in reverse chronological order stack_events_to_look_for = iter([ - ("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None), - ("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None), - ("DELETE_IN_PROGRESS", "User Initiated"), ("DELETE_COMPLETE", None)]) + ("DELETE_COMPLETE", None), + ("DELETE_IN_PROGRESS", "User Initiated"), + ("UPDATE_COMPLETE", None), + ("UPDATE_IN_PROGRESS", "User Initiated"), + ("CREATE_COMPLETE", None), + ("CREATE_IN_PROGRESS", "User Initiated"), + ]) try: for event in events: event.stack_id.should.equal(stack.stack_id) 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": event.logical_resource_id.should.equal("test_stack") diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index e2304f840..e5d231865 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -548,6 +548,7 @@ def test_autoscaling_group_with_elb(): "LaunchConfigurationName": {"Ref": "my-launch-config"}, "MinSize": "2", "MaxSize": "2", + "DesiredCapacity": "2", "LoadBalancerNames": [{"Ref": "my-elb"}] }, }, @@ -631,6 +632,7 @@ def test_autoscaling_group_update(): "LaunchConfigurationName": {"Ref": "my-launch-config"}, "MinSize": "2", "MaxSize": "2", + "DesiredCapacity": "2" }, }, @@ -655,6 +657,7 @@ def test_autoscaling_group_update(): asg = autoscale_conn.get_all_groups()[0] asg.min_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_json = json.dumps(asg_template) @@ -665,6 +668,7 @@ def test_autoscaling_group_update(): asg = autoscale_conn.get_all_groups()[0] asg.min_size.should.equal(2) asg.max_size.should.equal(3) + asg.desired_capacity.should.equal(2) @mock_ec2_deprecated() @@ -693,6 +697,7 @@ def test_vpc_single_instance_in_subnet(): ec2_conn = boto.ec2.connect_to_region("us-west-1") reservation = ec2_conn.get_all_instances()[0] instance = reservation.instances[0] + instance.tags["Foo"].should.equal("Bar") # Check that the EIP is attached the the EC2 instance eip = ec2_conn.get_all_addresses()[0] eip.domain.should.equal('vpc') @@ -714,7 +719,7 @@ def test_vpc_single_instance_in_subnet(): eip_resource = [ 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() @@ -1027,7 +1032,7 @@ def test_vpc_eip(): resources = stack.describe_resources() cfn_eip = [ 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() diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index 044d827c9..82b6be195 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -622,6 +622,58 @@ def test_describe_container_instances(): for arn in test_instance_arns: 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_ecs diff --git a/tests/test_iam/test_iam_groups.py b/tests/test_iam/test_iam_groups.py index a13d6de0b..7ca8d4ac0 100644 --- a/tests/test_iam/test_iam_groups.py +++ b/tests/test_iam/test_iam_groups.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import boto +import boto3 import sure # noqa from nose.tools import assert_raises @@ -72,3 +73,40 @@ def test_get_groups_for_user(): groups = conn.get_groups_for_user( 'my-user')['list_groups_for_user_response']['list_groups_for_user_result']['groups'] 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']) diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 653963122..19aa6d855 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -77,7 +77,7 @@ def test_create_queues_in_multiple_region(): list(west2_conn.list_queues()['QueueUrls']).should.have.length_of(1) 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 @@ -92,7 +92,7 @@ def test_get_queue_with_prefix(): queue = conn.list_queues(QueueNamePrefix="test-")['QueueUrls'] queue.should.have.length_of(1) 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