diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
index 0a3e0a0c2..58409901d 100644
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -317,6 +317,12 @@ def generate_resource_name(resource_type, stack_name, logical_id):
if truncated_name_prefix.endswith("-"):
truncated_name_prefix = truncated_name_prefix[:-1]
return "{0}-{1}".format(truncated_name_prefix, my_random_suffix)
+ elif resource_type == "AWS::S3::Bucket":
+ right_hand_part_of_name = "-{0}-{1}".format(logical_id, random_suffix())
+ max_stack_name_portion_len = 63 - len(right_hand_part_of_name)
+ return "{0}{1}".format(
+ stack_name[:max_stack_name_portion_len], right_hand_part_of_name
+ ).lower()
else:
return "{0}-{1}-{2}".format(stack_name, logical_id, random_suffix())
diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py
index 8672c706d..c7ced0186 100644
--- a/moto/cloudformation/responses.py
+++ b/moto/cloudformation/responses.py
@@ -10,6 +10,31 @@ from moto.s3 import s3_backend
from moto.core import ACCOUNT_ID
from .models import cloudformation_backends
from .exceptions import ValidationError
+from .utils import yaml_tag_constructor
+
+
+def get_template_summary_response_from_template(template_body):
+ def get_resource_types(template_dict):
+ resources = {}
+ for key, value in template_dict.items():
+ if key == "Resources":
+ resources = value
+
+ resource_types = []
+ for key, value in resources.items():
+ resource_types.append(value["Type"])
+ return resource_types
+
+ yaml.add_multi_constructor("", yaml_tag_constructor)
+
+ try:
+ template_dict = yaml.load(template_body, Loader=yaml.Loader)
+ except (yaml.parser.ParserError, yaml.scanner.ScannerError):
+ template_dict = json.loads(template_body)
+
+ resources_types = get_resource_types(template_dict)
+ template_dict["resourceTypes"] = resources_types
+ return template_dict
class CloudFormationResponse(BaseResponse):
@@ -269,6 +294,20 @@ class CloudFormationResponse(BaseResponse):
template = self.response_template(GET_TEMPLATE_RESPONSE_TEMPLATE)
return template.render(stack=stack)
+ def get_template_summary(self):
+ stack_name = self._get_param("StackName")
+ template_url = self._get_param("TemplateURL")
+ stack_body = self._get_param("TemplateBody")
+
+ if stack_name:
+ stack_body = self.cloudformation_backend.get_stack(stack_name).template
+ elif template_url:
+ stack_body = self._get_stack_from_s3_url(template_url)
+
+ template_summary = get_template_summary_response_from_template(stack_body)
+ template = self.response_template(GET_TEMPLATE_SUMMARY_TEMPLATE)
+ return template.render(template_summary=template_summary)
+
def update_stack(self):
stack_name = self._get_param("StackName")
role_arn = self._get_param("RoleARN")
@@ -743,7 +782,6 @@ DESCRIBE_STACKS_TEMPLATE = """
"""
-
DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE = """
@@ -758,7 +796,6 @@ DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE = """
"""
-
DESCRIBE_STACK_RESOURCES_RESPONSE = """
@@ -777,7 +814,6 @@ DESCRIBE_STACK_RESOURCES_RESPONSE = """
"""
-
DESCRIBE_STACK_EVENTS_RESPONSE = """
@@ -802,7 +838,6 @@ DESCRIBE_STACK_EVENTS_RESPONSE = """
@@ -823,7 +858,6 @@ LIST_CHANGE_SETS_RESPONSE = """
"""
-
LIST_STACKS_RESPONSE = """
@@ -840,7 +874,6 @@ LIST_STACKS_RESPONSE = """
"""
-
LIST_STACKS_RESOURCES_RESPONSE = """
@@ -860,7 +893,6 @@ LIST_STACKS_RESOURCES_RESPONSE = """
"""
-
GET_TEMPLATE_RESPONSE_TEMPLATE = """
{{ stack.template }}
@@ -870,7 +902,6 @@ GET_TEMPLATE_RESPONSE_TEMPLATE = """
"""
-
DELETE_STACK_RESPONSE_TEMPLATE = """
5ccc7dcd-744c-11e5-be70-example
@@ -878,7 +909,6 @@ DELETE_STACK_RESPONSE_TEMPLATE = """
"""
-
LIST_EXPORTS_RESPONSE = """
@@ -1139,3 +1169,19 @@ LIST_STACK_SET_OPERATION_RESULTS_RESPONSE_TEMPLATE = (
"""
)
+
+GET_TEMPLATE_SUMMARY_TEMPLATE = """
+
+ {{ template_summary.Description }}
+ {% for resource in template_summary.resourceTypes %}
+
+ {{ resource }}
+
+ {% endfor %}
+ {{ template_summary.AWSTemplateFormatVersion }}
+
+
+ b9b4b068-3a41-11e5-94eb-example
+
+
+"""
diff --git a/moto/core/utils.py b/moto/core/utils.py
index c9bf93473..235b895ec 100644
--- a/moto/core/utils.py
+++ b/moto/core/utils.py
@@ -356,3 +356,14 @@ def tags_from_query_string(
else:
response_values[tag_key] = None
return response_values
+
+
+def tags_from_cloudformation_tags_list(tags_list):
+ """Return tags in dict form from cloudformation resource tags form (list of dicts)"""
+ tags = {}
+ for entry in tags_list:
+ key = entry["Key"]
+ value = entry["Value"]
+ tags[key] = value
+
+ return tags
diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py
index 9090847be..e9843399f 100644
--- a/moto/ec2/responses/instances.py
+++ b/moto/ec2/responses/instances.py
@@ -818,13 +818,25 @@ EC2_DESCRIBE_INSTANCE_TYPES = """
{% for instance_type in instance_types %}
-
- {{ instance_type.name }}
- {{ instance_type.cores }}
- {{ instance_type.memory }}
- {{ instance_type.disk }}
- {{ instance_type.storageCount }}
- {{ instance_type.maxIpAddresses }}
- {{ instance_type.ebsOptimizedAvailable }}
+ {{ instance_type.name }}
+
+ {{ instance_type.cores }}
+ {{ instance_type.cores }}
+ 1
+
+
+ {{ instance_type.memory }}
+
+
+ {{ instance_type.disk }}
+
+
+
+
-
+ x86_64
+
+
+
{% endfor %}
diff --git a/moto/iam/models.py b/moto/iam/models.py
index 82dc84be5..49755e57a 100755
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -946,6 +946,10 @@ class AccountSummary(BaseModel):
return len(self._iam_backend.users)
+def filter_items_with_path_prefix(path_prefix, items):
+ return [role for role in items if role.path.startswith(path_prefix)]
+
+
class IAMBackend(BaseBackend):
def __init__(self):
self.instance_profiles = {}
@@ -1490,7 +1494,11 @@ class IAMBackend(BaseBackend):
def list_users(self, path_prefix, marker, max_items):
users = None
try:
+
users = self.users.values()
+ if path_prefix:
+ users = filter_items_with_path_prefix(path_prefix, users)
+
except KeyError:
raise IAMNotFoundException(
"Users {0}, {1}, {2} not found".format(path_prefix, marker, max_items)
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
index 8eb1730ea..6f785f8ac 100644
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -337,7 +337,6 @@ class IamResponse(BaseResponse):
def list_roles(self):
roles = iam_backend.get_roles()
-
template = self.response_template(LIST_ROLES_TEMPLATE)
return template.render(roles=roles)
diff --git a/moto/resourcegroups/urls.py b/moto/resourcegroups/urls.py
index b40179145..3e5f7b7f5 100644
--- a/moto/resourcegroups/urls.py
+++ b/moto/resourcegroups/urls.py
@@ -4,9 +4,14 @@ from .responses import ResourceGroupsResponse
url_bases = ["https?://resource-groups(-fips)?.(.+).amazonaws.com"]
url_paths = {
+ "{0}/delete-group$": ResourceGroupsResponse.dispatch,
+ "{0}/get-group$": ResourceGroupsResponse.dispatch,
+ "{0}/get-group-query$": ResourceGroupsResponse.dispatch,
"{0}/groups$": ResourceGroupsResponse.dispatch,
"{0}/groups/(?P[^/]+)$": ResourceGroupsResponse.dispatch,
"{0}/groups/(?P[^/]+)/query$": ResourceGroupsResponse.dispatch,
"{0}/groups-list$": ResourceGroupsResponse.dispatch,
"{0}/resources/(?P[^/]+)/tags$": ResourceGroupsResponse.dispatch,
+ "{0}/update-group$": ResourceGroupsResponse.dispatch,
+ "{0}/update-group-query$": ResourceGroupsResponse.dispatch,
}
diff --git a/moto/sns/models.py b/moto/sns/models.py
index 85196cd8f..76376e58f 100644
--- a/moto/sns/models.py
+++ b/moto/sns/models.py
@@ -580,7 +580,12 @@ class SNSBackend(BaseBackend):
return subscription.attributes
def set_subscription_attributes(self, arn, name, value):
- if name not in ["RawMessageDelivery", "DeliveryPolicy", "FilterPolicy"]:
+ if name not in [
+ "RawMessageDelivery",
+ "DeliveryPolicy",
+ "FilterPolicy",
+ "RedrivePolicy",
+ ]:
raise SNSInvalidParameter("AttributeName")
# TODO: should do validation
diff --git a/moto/sqs/exceptions.py b/moto/sqs/exceptions.py
index 77d7b9fb2..46d2af400 100644
--- a/moto/sqs/exceptions.py
+++ b/moto/sqs/exceptions.py
@@ -16,11 +16,13 @@ class ReceiptHandleIsInvalid(RESTError):
)
-class MessageAttributesInvalid(Exception):
- status_code = 400
+class MessageAttributesInvalid(RESTError):
+ code = 400
def __init__(self, description):
- self.description = description
+ super(MessageAttributesInvalid, self).__init__(
+ "MessageAttributesInvalid", description
+ )
class QueueDoesNotExist(RESTError):
diff --git a/moto/sqs/models.py b/moto/sqs/models.py
index 5b6e6410a..4befbb50a 100644
--- a/moto/sqs/models.py
+++ b/moto/sqs/models.py
@@ -18,6 +18,7 @@ from moto.core.utils import (
get_random_message_id,
unix_time,
unix_time_millis,
+ tags_from_cloudformation_tags_list,
)
from .utils import generate_receipt_handle
from .exceptions import (
@@ -87,7 +88,19 @@ class Message(BaseModel):
struct_format = "!I".encode("ascii") # ensure it's a bytestring
for name in sorted(self.message_attributes.keys()):
attr = self.message_attributes[name]
- data_type = attr["data_type"]
+ data_type_parts = attr["data_type"].split(".")
+ data_type = data_type_parts[0]
+
+ if data_type not in [
+ "String",
+ "Binary",
+ "Number",
+ ]:
+ raise MessageAttributesInvalid(
+ "The message attribute '{0}' has an invalid message attribute type, the set of supported type prefixes is Binary, Number, and String.".format(
+ name[0]
+ )
+ )
encoded = utf8("")
# Each part of each attribute is encoded right after it's
@@ -243,9 +256,7 @@ class Queue(BaseModel):
# Check some conditions
if self.fifo_queue and not self.name.endswith(".fifo"):
- raise MessageAttributesInvalid(
- "Queue name must end in .fifo for FIFO queues"
- )
+ raise InvalidParameterValue("Queue name must end in .fifo for FIFO queues")
@property
def pending_messages(self):
@@ -347,11 +358,17 @@ class Queue(BaseModel):
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
):
- properties = cloudformation_json["Properties"]
+ properties = deepcopy(cloudformation_json["Properties"])
+ # remove Tags from properties and convert tags list to dict
+ tags = properties.pop("Tags", [])
+ tags_dict = tags_from_cloudformation_tags_list(tags)
sqs_backend = sqs_backends[region_name]
return sqs_backend.create_queue(
- name=properties["QueueName"], region=region_name, **properties
+ name=properties["QueueName"],
+ tags=tags_dict,
+ region=region_name,
+ **properties
)
@classmethod
diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py
index 54a8bc267..29804256c 100644
--- a/moto/sqs/responses.py
+++ b/moto/sqs/responses.py
@@ -9,7 +9,6 @@ from six.moves.urllib.parse import urlparse
from .exceptions import (
EmptyBatchRequest,
InvalidAttributeName,
- MessageAttributesInvalid,
MessageNotInflight,
ReceiptHandleIsInvalid,
)
@@ -82,12 +81,7 @@ class SQSResponse(BaseResponse):
request_url = urlparse(self.uri)
queue_name = self._get_param("QueueName")
- try:
- queue = self.sqs_backend.create_queue(
- queue_name, self.tags, **self.attribute
- )
- except MessageAttributesInvalid as e:
- return self._error("InvalidParameterValue", e.description)
+ queue = self.sqs_backend.create_queue(queue_name, self.tags, **self.attribute)
template = self.response_template(CREATE_QUEUE_RESPONSE)
return template.render(queue_url=queue.url(request_url))
@@ -225,10 +219,7 @@ class SQSResponse(BaseResponse):
if len(message) > MAXIMUM_MESSAGE_LENGTH:
return ERROR_TOO_LONG_RESPONSE, dict(status=400)
- try:
- message_attributes = parse_message_attributes(self.querystring)
- except MessageAttributesInvalid as e:
- return e.description, dict(status=e.status_code)
+ message_attributes = parse_message_attributes(self.querystring)
queue_name = self._get_queue_name()
diff --git a/moto/sqs/utils.py b/moto/sqs/utils.py
index f3b8bbfe8..315fce56b 100644
--- a/moto/sqs/utils.py
+++ b/moto/sqs/utils.py
@@ -34,7 +34,7 @@ def parse_message_attributes(querystring, base="", value_namespace="Value."):
)
data_type_parts = data_type[0].split(".")
- if len(data_type_parts) > 2 or data_type_parts[0] not in [
+ if data_type_parts[0] not in [
"String",
"Binary",
"Number",
diff --git a/moto/ssm/exceptions.py b/moto/ssm/exceptions.py
index 2e715f16a..f68e47029 100644
--- a/moto/ssm/exceptions.py
+++ b/moto/ssm/exceptions.py
@@ -78,6 +78,13 @@ class InvalidDocumentOperation(JsonRESTError):
)
+class AccessDeniedException(JsonRESTError):
+ code = 400
+
+ def __init__(self, message):
+ super(AccessDeniedException, self).__init__("AccessDeniedException", message)
+
+
class InvalidDocumentContent(JsonRESTError):
code = 400
diff --git a/moto/ssm/models.py b/moto/ssm/models.py
index 28175bb06..07812c316 100644
--- a/moto/ssm/models.py
+++ b/moto/ssm/models.py
@@ -27,6 +27,7 @@ from .exceptions import (
ParameterNotFound,
DocumentAlreadyExists,
InvalidDocumentOperation,
+ AccessDeniedException,
InvalidDocument,
InvalidDocumentContent,
InvalidDocumentVersion,
@@ -965,6 +966,13 @@ class SimpleSystemManagerBackend(BaseBackend):
"The following filter key is not valid: Label. Valid filter keys include: [Path, Name, Type, KeyId, Tier]."
)
+ if by_path and key in ["Name", "Path", "Tier"]:
+ raise InvalidFilterKey(
+ "The following filter key is not valid: {key}. Valid filter keys include: [Type, KeyId].".format(
+ key=key
+ )
+ )
+
if not values:
raise InvalidFilterValue(
"The following filter values are missing : null for filter key Name."
@@ -1024,7 +1032,10 @@ class SimpleSystemManagerBackend(BaseBackend):
)
)
- if key != "Path" and option not in ["Equals", "BeginsWith"]:
+ allowed_options = ["Equals", "BeginsWith"]
+ if key == "Name":
+ allowed_options += ["Contains"]
+ if key != "Path" and option not in allowed_options:
raise InvalidFilterOption(
"The following filter option is not valid: {option}. Valid options include: [BeginsWith, Equals].".format(
option=option
@@ -1084,6 +1095,9 @@ class SimpleSystemManagerBackend(BaseBackend):
max_results=10,
):
"""Implement the get-parameters-by-path-API in the backend."""
+
+ self._validate_parameter_filters(filters, by_path=True)
+
result = []
# path could be with or without a trailing /. we handle this
# difference here.
@@ -1134,7 +1148,8 @@ class SimpleSystemManagerBackend(BaseBackend):
what = parameter.keyid
elif key == "Name":
what = "/" + parameter.name.lstrip("/")
- values = ["/" + value.lstrip("/") for value in values]
+ if option != "Contains":
+ values = ["/" + value.lstrip("/") for value in values]
elif key == "Path":
what = "/" + parameter.name.lstrip("/")
values = ["/" + value.strip("/") for value in values]
@@ -1147,6 +1162,8 @@ class SimpleSystemManagerBackend(BaseBackend):
what.startswith(value) for value in values
):
return False
+ elif option == "Contains" and not any(value in what for value in values):
+ return False
elif option == "Equals" and not any(what == value for value in values):
return False
elif option == "OneLevel":
@@ -1172,8 +1189,33 @@ class SimpleSystemManagerBackend(BaseBackend):
return True
def get_parameter(self, name, with_decryption):
- if name in self._parameters:
- return self._parameters[name][-1]
+ name_parts = name.split(":")
+ name_prefix = name_parts[0]
+
+ if len(name_parts) > 2:
+ return None
+
+ if name_prefix in self._parameters:
+ if len(name_parts) == 1:
+ return self._parameters[name][-1]
+
+ if len(name_parts) == 2:
+ version_or_label = name_parts[1]
+ parameters = self._parameters[name_prefix]
+
+ if version_or_label.isdigit():
+ result = list(
+ filter(lambda x: str(x.version) == version_or_label, parameters)
+ )
+ if len(result) > 0:
+ return result[-1]
+
+ result = list(
+ filter(lambda x: version_or_label in x.labels, parameters)
+ )
+ if len(result) > 0:
+ return result[-1]
+
return None
def label_parameter_version(self, name, version, labels):
@@ -1238,6 +1280,23 @@ class SimpleSystemManagerBackend(BaseBackend):
def put_parameter(
self, name, description, value, type, allowed_pattern, keyid, overwrite
):
+ if name.lower().lstrip("/").startswith("aws") or name.lower().lstrip(
+ "/"
+ ).startswith("ssm"):
+ is_path = name.count("/") > 1
+ if name.lower().startswith("/aws") and is_path:
+ raise AccessDeniedException(
+ "No access to reserved parameter name: {name}.".format(name=name)
+ )
+ if not is_path:
+ invalid_prefix_error = 'Parameter name: can\'t be prefixed with "aws" or "ssm" (case-insensitive).'
+ else:
+ invalid_prefix_error = (
+ 'Parameter name: can\'t be prefixed with "ssm" (case-insensitive). '
+ "If formed as a path, it can consist of sub-paths divided by slash symbol; each sub-path can be "
+ "formed as a mix of letters, numbers and the following 3 symbols .-_"
+ )
+ raise ValidationException(invalid_prefix_error)
previous_parameter_versions = self._parameters[name]
if len(previous_parameter_versions) == 0:
previous_parameter = None
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
index 1ebce46d7..41d3fad3e 100644
--- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
@@ -35,6 +35,14 @@ dummy_template = {
},
}
+dummy_template3 = {
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Description": "Stack 3",
+ "Resources": {
+ "VPC": {"Properties": {"CidrBlock": "192.168.0.0/16"}, "Type": "AWS::EC2::VPC"}
+ },
+}
+
dummy_template_yaml = """---
AWSTemplateFormatVersion: 2010-09-09
Description: Stack1 with yaml template
@@ -646,6 +654,31 @@ def test_boto3_create_stack():
)
+@mock_cloudformation
+def test_boto3_create_stack_s3_long_name():
+ cf_conn = boto3.client("cloudformation", region_name="us-east-1")
+
+ stack_name = "MyLongStackName01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012"
+
+ template = '{"Resources":{"HelloBucket":{"Type":"AWS::S3::Bucket"}}}'
+
+ cf_conn.create_stack(StackName=stack_name, TemplateBody=template)
+
+ cf_conn.get_template(StackName=stack_name)["TemplateBody"].should.equal(
+ json.loads(template, object_pairs_hook=OrderedDict)
+ )
+ provisioned_resource = cf_conn.list_stack_resources(StackName=stack_name)[
+ "StackResourceSummaries"
+ ][0]
+ provisioned_bucket_name = provisioned_resource["PhysicalResourceId"]
+ len(provisioned_bucket_name).should.be.lower_than(64)
+ logical_name_lower_case = provisioned_resource["LogicalResourceId"].lower()
+ bucket_name_stack_name_prefix = provisioned_bucket_name[
+ : provisioned_bucket_name.index("-" + logical_name_lower_case)
+ ]
+ stack_name.lower().should.contain(bucket_name_stack_name_prefix)
+
+
@mock_cloudformation
def test_boto3_create_stack_with_yaml():
cf_conn = boto3.client("cloudformation", region_name="us-east-1")
@@ -668,6 +701,48 @@ def test_boto3_create_stack_with_short_form_func_yaml():
)
+@mock_s3
+@mock_cloudformation
+def test_get_template_summary():
+ s3 = boto3.client("s3")
+ s3_conn = boto3.resource("s3", region_name="us-east-1")
+
+ conn = boto3.client("cloudformation", region_name="us-east-1")
+ result = conn.get_template_summary(TemplateBody=json.dumps(dummy_template3))
+
+ result["ResourceTypes"].should.equal(["AWS::EC2::VPC"])
+ result["Version"].should.equal("2010-09-09")
+ result["Description"].should.equal("Stack 3")
+
+ conn.create_stack(StackName="test_stack", TemplateBody=json.dumps(dummy_template3))
+
+ result = conn.get_template_summary(StackName="test_stack")
+
+ result["ResourceTypes"].should.equal(["AWS::EC2::VPC"])
+ result["Version"].should.equal("2010-09-09")
+ result["Description"].should.equal("Stack 3")
+
+ s3_conn.create_bucket(Bucket="foobar")
+ s3_conn.Object("foobar", "template-key").put(Body=json.dumps(dummy_template3))
+
+ key_url = s3.generate_presigned_url(
+ ClientMethod="get_object", Params={"Bucket": "foobar", "Key": "template-key"}
+ )
+
+ conn.create_stack(StackName="stack_from_url", TemplateURL=key_url)
+ result = conn.get_template_summary(TemplateURL=key_url)
+ result["ResourceTypes"].should.equal(["AWS::EC2::VPC"])
+ result["Version"].should.equal("2010-09-09")
+ result["Description"].should.equal("Stack 3")
+
+ conn = boto3.client("cloudformation", region_name="us-east-1")
+ result = conn.get_template_summary(TemplateBody=dummy_template_yaml)
+
+ result["ResourceTypes"].should.equal(["AWS::EC2::Instance"])
+ result["Version"].should.equal("2010-09-09")
+ result["Description"].should.equal("Stack1 with yaml template")
+
+
@mock_cloudformation
def test_boto3_create_stack_with_ref_yaml():
cf_conn = boto3.client("cloudformation", region_name="us-east-1")
diff --git a/tests/test_ec2/test_instance_types.py b/tests/test_ec2/test_instance_types.py
new file mode 100644
index 000000000..1385d6113
--- /dev/null
+++ b/tests/test_ec2/test_instance_types.py
@@ -0,0 +1,18 @@
+from __future__ import unicode_literals
+
+import boto3
+import sure # noqa
+
+from moto import mock_ec2
+
+
+@mock_ec2
+def test_describe_instance_types():
+ client = boto3.client("ec2", "us-east-1")
+ instance_types = client.describe_instance_types()
+
+ instance_types.should.have.key("InstanceTypes")
+ instance_types["InstanceTypes"].should_not.be.empty
+ instance_types["InstanceTypes"][0].should.have.key("InstanceType")
+ instance_types["InstanceTypes"][0].should.have.key("MemoryInfo")
+ instance_types["InstanceTypes"][0]["MemoryInfo"].should.have.key("SizeInMiB")
diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py
index 4ae5ad49e..610333303 100644
--- a/tests/test_iam/test_iam.py
+++ b/tests/test_iam/test_iam.py
@@ -762,6 +762,12 @@ def test_list_users():
user["Path"].should.equal("/")
user["Arn"].should.equal("arn:aws:iam::{}:user/my-user".format(ACCOUNT_ID))
+ conn.create_user(UserName="my-user-1", Path="myUser")
+ response = conn.list_users(PathPrefix="my")
+ user = response["Users"][0]
+ user["UserName"].should.equal("my-user-1")
+ user["Path"].should.equal("myUser")
+
@mock_iam()
def test_user_policies():
diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py
index 2ed757f6a..61edcaa9b 100644
--- a/tests/test_sqs/test_sqs.py
+++ b/tests/test_sqs/test_sqs.py
@@ -17,12 +17,34 @@ from boto.exception import SQSError
from boto.sqs.message import Message, RawMessage
from botocore.exceptions import ClientError
from freezegun import freeze_time
-from moto import mock_sqs, mock_sqs_deprecated, settings
+from moto import mock_sqs, mock_sqs_deprecated, mock_cloudformation, settings
from nose import SkipTest
from nose.tools import assert_raises
from tests.helpers import requires_boto_gte
from moto.core import ACCOUNT_ID
+sqs_template_with_tags = """
+{
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Resources": {
+ "SQSQueue": {
+ "Type": "AWS::SQS::Queue",
+ "Properties": {
+ "Tags" : [
+ {
+ "Key" : "keyname1",
+ "Value" : "value1"
+ },
+ {
+ "Key" : "keyname2",
+ "Value" : "value2"
+ }
+ ]
+ }
+ }
+ }
+}"""
+
@mock_sqs
def test_create_fifo_queue_fail():
@@ -248,6 +270,50 @@ def test_message_with_complex_attributes():
messages.should.have.length_of(1)
+@mock_sqs
+def test_message_with_attributes_have_labels():
+ sqs = boto3.resource("sqs", region_name="us-east-1")
+ queue = sqs.create_queue(QueueName="blah")
+ msg = queue.send_message(
+ MessageBody="derp",
+ MessageAttributes={
+ "timestamp": {
+ "DataType": "Number.java.lang.Long",
+ "StringValue": "1493147359900",
+ }
+ },
+ )
+ msg.get("MD5OfMessageBody").should.equal("58fd9edd83341c29f1aebba81c31e257")
+ msg.get("MD5OfMessageAttributes").should.equal("235c5c510d26fb653d073faed50ae77c")
+ msg.get("MessageId").should_not.contain(" \n")
+
+ messages = queue.receive_messages()
+ messages.should.have.length_of(1)
+
+
+@mock_sqs
+def test_message_with_attributes_invalid_datatype():
+ sqs = boto3.resource("sqs", region_name="us-east-1")
+ queue = sqs.create_queue(QueueName="blah")
+
+ with assert_raises(ClientError) as e:
+ queue.send_message(
+ MessageBody="derp",
+ MessageAttributes={
+ "timestamp": {
+ "DataType": "InvalidNumber",
+ "StringValue": "149314735990a",
+ }
+ },
+ )
+ ex = e.exception
+ ex.response["Error"]["Code"].should.equal("MessageAttributesInvalid")
+ ex.response["Error"]["Message"].should.equal(
+ "The message attribute 'timestamp' has an invalid message attribute type, the set of supported type "
+ "prefixes is Binary, Number, and String."
+ )
+
+
@mock_sqs
def test_send_message_with_message_group_id():
sqs = boto3.resource("sqs", region_name="us-east-1")
@@ -532,6 +598,54 @@ def test_send_receive_message_with_attributes():
)
+@mock_sqs
+def test_send_receive_message_with_attributes_with_labels():
+ sqs = boto3.resource("sqs", region_name="us-east-1")
+ conn = boto3.client("sqs", region_name="us-east-1")
+ conn.create_queue(QueueName="test-queue")
+ queue = sqs.Queue("test-queue")
+
+ body_one = "this is a test message"
+ body_two = "this is another test message"
+
+ queue.send_message(
+ MessageBody=body_one,
+ MessageAttributes={
+ "timestamp": {
+ "StringValue": "1493147359900",
+ "DataType": "Number.java.lang.Long",
+ }
+ },
+ )
+
+ queue.send_message(
+ MessageBody=body_two,
+ MessageAttributes={
+ "timestamp": {
+ "StringValue": "1493147359901",
+ "DataType": "Number.java.lang.Long",
+ }
+ },
+ )
+
+ messages = conn.receive_message(QueueUrl=queue.url, MaxNumberOfMessages=2)[
+ "Messages"
+ ]
+
+ message1 = messages[0]
+ message2 = messages[1]
+
+ message1.get("Body").should.equal(body_one)
+ message2.get("Body").should.equal(body_two)
+
+ message1.get("MD5OfMessageAttributes").should.equal(
+ "235c5c510d26fb653d073faed50ae77c"
+ )
+ message2.get("MD5OfMessageAttributes").should.equal(
+ "994258b45346a2cc3f9cbb611aa7af30"
+ )
+
+
@mock_sqs
def test_send_receive_message_timestamps():
sqs = boto3.resource("sqs", region_name="us-east-1")
@@ -1841,3 +1955,17 @@ def test_send_messages_to_fifo_without_message_group_id():
ex.response["Error"]["Message"].should.equal(
"The request must contain the parameter MessageGroupId."
)
+
+
+@mock_sqs
+@mock_cloudformation
+def test_create_from_cloudformation_json_with_tags():
+ cf = boto3.client("cloudformation", region_name="us-east-1")
+ client = boto3.client("sqs", region_name="us-east-1")
+
+ cf.create_stack(StackName="test-sqs", TemplateBody=sqs_template_with_tags)
+
+ queue_url = client.list_queues()["QueueUrls"][0]
+
+ queue_tags = client.list_queue_tags(QueueUrl=queue_url)["Tags"]
+ queue_tags.should.equal({"keyname1": "value1", "keyname2": "value2"})
diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py
index 837f81bf5..cc79ce93d 100644
--- a/tests/test_ssm/test_ssm_boto3.py
+++ b/tests/test_ssm/test_ssm_boto3.py
@@ -198,6 +198,33 @@ def test_get_parameters_by_path():
len(response["Parameters"]).should.equal(1)
response.should_not.have.key("NextToken")
+ filters = [{"Key": "Name", "Values": ["error"]}]
+ client.get_parameters_by_path.when.called_with(
+ Path="/baz", ParameterFilters=filters
+ ).should.throw(
+ ClientError,
+ "The following filter key is not valid: Name. "
+ "Valid filter keys include: [Type, KeyId].",
+ )
+
+ filters = [{"Key": "Path", "Values": ["/error"]}]
+ client.get_parameters_by_path.when.called_with(
+ Path="/baz", ParameterFilters=filters
+ ).should.throw(
+ ClientError,
+ "The following filter key is not valid: Path. "
+ "Valid filter keys include: [Type, KeyId].",
+ )
+
+ filters = [{"Key": "Tier", "Values": ["Standard"]}]
+ client.get_parameters_by_path.when.called_with(
+ Path="/baz", ParameterFilters=filters
+ ).should.throw(
+ ClientError,
+ "The following filter key is not valid: Tier. "
+ "Valid filter keys include: [Type, KeyId].",
+ )
+
@mock_ssm
def test_put_parameter():
@@ -272,6 +299,73 @@ def test_put_parameter():
)
+@mock_ssm
+def test_put_parameter_invalid_names():
+ client = boto3.client("ssm", region_name="us-east-1")
+
+ invalid_prefix_err = (
+ 'Parameter name: can\'t be prefixed with "aws" or "ssm" (case-insensitive).'
+ )
+
+ client.put_parameter.when.called_with(
+ Name="ssm_test", Value="value", Type="String"
+ ).should.throw(
+ ClientError, invalid_prefix_err,
+ )
+
+ client.put_parameter.when.called_with(
+ Name="SSM_TEST", Value="value", Type="String"
+ ).should.throw(
+ ClientError, invalid_prefix_err,
+ )
+
+ client.put_parameter.when.called_with(
+ Name="aws_test", Value="value", Type="String"
+ ).should.throw(
+ ClientError, invalid_prefix_err,
+ )
+
+ client.put_parameter.when.called_with(
+ Name="AWS_TEST", Value="value", Type="String"
+ ).should.throw(
+ ClientError, invalid_prefix_err,
+ )
+
+ ssm_path = "/ssm_test/path/to/var"
+ client.put_parameter.when.called_with(
+ Name=ssm_path, Value="value", Type="String"
+ ).should.throw(
+ ClientError,
+ 'Parameter name: can\'t be prefixed with "ssm" (case-insensitive). If formed as a path, it can consist of '
+ "sub-paths divided by slash symbol; each sub-path can be formed as a mix of letters, numbers and the following "
+ "3 symbols .-_",
+ )
+
+ ssm_path = "/SSM/PATH/TO/VAR"
+ client.put_parameter.when.called_with(
+ Name=ssm_path, Value="value", Type="String"
+ ).should.throw(
+ ClientError,
+ 'Parameter name: can\'t be prefixed with "ssm" (case-insensitive). If formed as a path, it can consist of '
+ "sub-paths divided by slash symbol; each sub-path can be formed as a mix of letters, numbers and the following "
+ "3 symbols .-_",
+ )
+
+ aws_path = "/aws_test/path/to/var"
+ client.put_parameter.when.called_with(
+ Name=aws_path, Value="value", Type="String"
+ ).should.throw(
+ ClientError, "No access to reserved parameter name: {}.".format(aws_path),
+ )
+
+ aws_path = "/AWS/PATH/TO/VAR"
+ client.put_parameter.when.called_with(
+ Name=aws_path, Value="value", Type="String"
+ ).should.throw(
+ ClientError, "No access to reserved parameter name: {}.".format(aws_path),
+ )
+
+
@mock_ssm
def test_put_parameter_china():
client = boto3.client("ssm", region_name="cn-north-1")
@@ -302,6 +396,64 @@ def test_get_parameter():
)
+@mock_ssm
+def test_get_parameter_with_version_and_labels():
+ client = boto3.client("ssm", region_name="us-east-1")
+
+ client.put_parameter(
+ Name="test-1", Description="A test parameter", Value="value", Type="String"
+ )
+ client.put_parameter(
+ Name="test-2", Description="A test parameter", Value="value", Type="String"
+ )
+
+ client.label_parameter_version(
+ Name="test-2", ParameterVersion=1, Labels=["test-label"]
+ )
+
+ response = client.get_parameter(Name="test-1:1", WithDecryption=False)
+
+ response["Parameter"]["Name"].should.equal("test-1")
+ response["Parameter"]["Value"].should.equal("value")
+ response["Parameter"]["Type"].should.equal("String")
+ response["Parameter"]["LastModifiedDate"].should.be.a(datetime.datetime)
+ response["Parameter"]["ARN"].should.equal(
+ "arn:aws:ssm:us-east-1:1234567890:parameter/test-1"
+ )
+
+ response = client.get_parameter(Name="test-2:1", WithDecryption=False)
+ response["Parameter"]["Name"].should.equal("test-2")
+ response["Parameter"]["Value"].should.equal("value")
+ response["Parameter"]["Type"].should.equal("String")
+ response["Parameter"]["LastModifiedDate"].should.be.a(datetime.datetime)
+ response["Parameter"]["ARN"].should.equal(
+ "arn:aws:ssm:us-east-1:1234567890:parameter/test-2"
+ )
+
+ response = client.get_parameter(Name="test-2:test-label", WithDecryption=False)
+ response["Parameter"]["Name"].should.equal("test-2")
+ response["Parameter"]["Value"].should.equal("value")
+ response["Parameter"]["Type"].should.equal("String")
+ response["Parameter"]["LastModifiedDate"].should.be.a(datetime.datetime)
+ response["Parameter"]["ARN"].should.equal(
+ "arn:aws:ssm:us-east-1:1234567890:parameter/test-2"
+ )
+
+ with assert_raises(ClientError) as ex:
+ client.get_parameter(Name="test-2:2:3", WithDecryption=False)
+ ex.exception.response["Error"]["Code"].should.equal("ParameterNotFound")
+ ex.exception.response["Error"]["Message"].should.equal(
+ "Parameter test-2:2:3 not found."
+ )
+
+ with assert_raises(ClientError) as ex:
+ client.get_parameter(Name="test-2:2", WithDecryption=False)
+ ex.exception.response["Error"]["Code"].should.equal("ParameterNotFound")
+ ex.exception.response["Error"]["Message"].should.equal(
+ "Parameter test-2:2 not found."
+ )
+
+
@mock_ssm
def test_get_parameters_errors():
client = boto3.client("ssm", region_name="us-east-1")
@@ -504,6 +656,9 @@ def test_describe_parameters_with_parameter_filters_name():
client = boto3.client("ssm", region_name="us-east-1")
client.put_parameter(Name="param", Value="value", Type="String")
client.put_parameter(Name="/param-2", Value="value-2", Type="String")
+ client.put_parameter(Name="/tangent-3", Value="value-3", Type="String")
+ client.put_parameter(Name="tangram-4", Value="value-4", Type="String")
+ client.put_parameter(Name="standby-5", Value="value-5", Type="String")
response = client.describe_parameters(
ParameterFilters=[{"Key": "Name", "Values": ["param"]}]
@@ -543,6 +698,22 @@ def test_describe_parameters_with_parameter_filters_name():
parameters.should.have.length_of(2)
response.should_not.have.key("NextToken")
+ response = client.describe_parameters(
+ ParameterFilters=[{"Key": "Name", "Option": "Contains", "Values": ["ram"]}]
+ )
+
+ parameters = response["Parameters"]
+ parameters.should.have.length_of(3)
+ response.should_not.have.key("NextToken")
+
+ response = client.describe_parameters(
+ ParameterFilters=[{"Key": "Name", "Option": "Contains", "Values": ["/tan"]}]
+ )
+
+ parameters = response["Parameters"]
+ parameters.should.have.length_of(2)
+ response.should_not.have.key("NextToken")
+
@mock_ssm
def test_describe_parameters_with_parameter_filters_path():