diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index abba3defd..5306ce0d9 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -8,6 +8,7 @@ from moto.ec2 import models as ec2_models from moto.elb import models as elb_models from moto.iam import models as iam_models from moto.rds import models as rds_models +from moto.redshift import models as redshift_models from moto.route53 import models as route53_models from moto.sns import models as sns_models from moto.sqs import models as sqs_models @@ -40,6 +41,9 @@ MODEL_MAP = { "AWS::RDS::DBInstance": rds_models.Database, "AWS::RDS::DBSecurityGroup": rds_models.SecurityGroup, "AWS::RDS::DBSubnetGroup": rds_models.SubnetGroup, + "AWS::Redshift::Cluster": redshift_models.Cluster, + "AWS::Redshift::ClusterParameterGroup": redshift_models.ParameterGroup, + "AWS::Redshift::ClusterSubnetGroup": redshift_models.SubnetGroup, "AWS::Route53::HealthCheck": route53_models.HealthCheck, "AWS::Route53::HostedZone": route53_models.FakeZone, "AWS::Route53::RecordSet": route53_models.RecordSet, diff --git a/moto/redshift/models.py b/moto/redshift/models.py index c3189c951..bd81526df 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -33,8 +33,8 @@ class Cluster(object): self.allow_version_upgrade = allow_version_upgrade if allow_version_upgrade is not None else True self.cluster_version = cluster_version if cluster_version else "1.0" - self.port = port if port else 5439 - self.automated_snapshot_retention_period = automated_snapshot_retention_period if automated_snapshot_retention_period else 1 + self.port = int(port) if port else 5439 + self.automated_snapshot_retention_period = int(automated_snapshot_retention_period) if automated_snapshot_retention_period else 1 self.preferred_maintenance_window = preferred_maintenance_window if preferred_maintenance_window else "Mon:03:00-Mon:03:30" if cluster_parameter_group_name: @@ -47,6 +47,7 @@ class Cluster(object): else: self.cluster_security_groups = ["Default"] + self.region = region if availability_zone: self.availability_zone = availability_zone else: @@ -57,10 +58,58 @@ class Cluster(object): if cluster_type == 'single-node': self.number_of_nodes = 1 elif number_of_nodes: - self.number_of_nodes = number_of_nodes + self.number_of_nodes = int(number_of_nodes) else: self.number_of_nodes = 1 + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + redshift_backend = redshift_backends[region_name] + properties = cloudformation_json['Properties'] + + if 'ClusterSubnetGroupName' in properties: + subnet_group_name = properties['ClusterSubnetGroupName'].cluster_subnet_group_name + else: + subnet_group_name = None + cluster = redshift_backend.create_cluster( + cluster_identifier=resource_name, + node_type=properties.get('NodeType'), + master_username=properties.get('MasterUsername'), + master_user_password=properties.get('MasterUserPassword'), + db_name=properties.get('DBName'), + cluster_type=properties.get('ClusterType'), + cluster_security_groups=properties.get('ClusterSecurityGroups', []), + vpc_security_group_ids=properties.get('VpcSecurityGroupIds', []), + cluster_subnet_group_name=subnet_group_name, + availability_zone=properties.get('AvailabilityZone'), + preferred_maintenance_window=properties.get('PreferredMaintenanceWindow'), + cluster_parameter_group_name=properties.get('ClusterParameterGroupName'), + automated_snapshot_retention_period=properties.get('AutomatedSnapshotRetentionPeriod'), + port=properties.get('Port'), + cluster_version=properties.get('ClusterVersion'), + allow_version_upgrade=properties.get('AllowVersionUpgrade'), + number_of_nodes=properties.get('NumberOfNodes'), + publicly_accessible=properties.get("PubliclyAccessible"), + encrypted=properties.get("Encrypted"), + region=region_name, + ) + return cluster + + def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException + if attribute_name == 'Endpoint.Address': + return self.endpoint + elif attribute_name == 'Endpoint.Port': + return self.port + raise UnformattedGetAttTemplateException() + + @property + def endpoint(self): + return "{0}.cg034hpkmmjt.{1}.redshift.amazonaws.com".format( + self.cluster_identifier, + self.region, + ) + @property def security_groups(self): return [ @@ -128,6 +177,18 @@ class SubnetGroup(object): if not self.subnets: raise InvalidSubnetError(subnet_ids) + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + redshift_backend = redshift_backends[region_name] + properties = cloudformation_json['Properties'] + + subnet_group = redshift_backend.create_cluster_subnet_group( + cluster_subnet_group_name=resource_name, + description=properties.get("Description"), + subnet_ids=properties.get("SubnetIds", []), + ) + return subnet_group + @property def subnets(self): return self.ec2_backend.get_all_subnets(filters={'subnet-id': self.subnet_ids}) @@ -173,6 +234,18 @@ class ParameterGroup(object): self.group_family = group_family self.description = description + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + redshift_backend = redshift_backends[region_name] + properties = cloudformation_json['Properties'] + + parameter_group = redshift_backend.create_cluster_parameter_group( + cluster_parameter_group_name=resource_name, + description=properties.get("Description"), + group_family=properties.get("ParameterGroupFamily"), + ) + return parameter_group + def to_json(self): return { "ParameterGroupFamily": self.group_family, diff --git a/tests/test_cloudformation/fixtures/redshift.py b/tests/test_cloudformation/fixtures/redshift.py new file mode 100644 index 000000000..90e171659 --- /dev/null +++ b/tests/test_cloudformation/fixtures/redshift.py @@ -0,0 +1,187 @@ +from __future__ import unicode_literals + +template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters" : { + "DatabaseName" : { + "Description" : "The name of the first database to be created when the cluster is created", + "Type" : "String", + "Default" : "dev", + "AllowedPattern" : "([a-z]|[0-9])+" + }, + "ClusterType" : { + "Description" : "The type of cluster", + "Type" : "String", + "Default" : "single-node", + "AllowedValues" : [ "single-node", "multi-node" ] + }, + "NumberOfNodes" : { + "Description" : "The number of compute nodes in the cluster. For multi-node clusters, the NumberOfNodes parameter must be greater than 1", + "Type" : "Number", + "Default" : "1" + }, + "NodeType" : { + "Description" : "The type of node to be provisioned", + "Type" : "String", + "Default" : "dw1.xlarge", + "AllowedValues" : [ "dw1.xlarge", "dw1.8xlarge", "dw2.large", "dw2.8xlarge" ] + }, + "MasterUsername" : { + "Description" : "The user name that is associated with the master user account for the cluster that is being created", + "Type" : "String", + "Default" : "defaultuser", + "AllowedPattern" : "([a-z])([a-z]|[0-9])*" + }, + "MasterUserPassword" : { + "Description" : "The password that is associated with the master user account for the cluster that is being created.", + "Type" : "String", + "NoEcho" : "true" + }, + "InboundTraffic" : { + "Description" : "Allow inbound traffic to the cluster from this CIDR range.", + "Type" : "String", + "MinLength": "9", + "MaxLength": "18", + "Default" : "0.0.0.0/0", + "AllowedPattern" : "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", + "ConstraintDescription" : "must be a valid CIDR range of the form x.x.x.x/x." + }, + "PortNumber" : { + "Description" : "The port number on which the cluster accepts incoming connections.", + "Type" : "Number", + "Default" : "5439" + } + }, + "Conditions" : { + "IsMultiNodeCluster" : { + "Fn::Equals" : [{ "Ref" : "ClusterType" }, "multi-node" ] + } + }, + "Resources" : { + "RedshiftCluster" : { + "Type" : "AWS::Redshift::Cluster", + "DependsOn" : "AttachGateway", + "Properties" : { + "ClusterType" : { "Ref" : "ClusterType" }, + "NumberOfNodes" : { "Fn::If" : [ "IsMultiNodeCluster", { "Ref" : "NumberOfNodes" }, { "Ref" : "AWS::NoValue" }]}, + "NodeType" : { "Ref" : "NodeType" }, + "DBName" : { "Ref" : "DatabaseName" }, + "MasterUsername" : { "Ref" : "MasterUsername" }, + "MasterUserPassword" : { "Ref" : "MasterUserPassword" }, + "ClusterParameterGroupName" : { "Ref" : "RedshiftClusterParameterGroup" }, + "VpcSecurityGroupIds" : [ { "Ref" : "SecurityGroup" } ], + "ClusterSubnetGroupName" : { "Ref" : "RedshiftClusterSubnetGroup" }, + "PubliclyAccessible" : "true", + "Port" : { "Ref" : "PortNumber" } + } + }, + "RedshiftClusterParameterGroup" : { + "Type" : "AWS::Redshift::ClusterParameterGroup", + "Properties" : { + "Description" : "Cluster parameter group", + "ParameterGroupFamily" : "redshift-1.0", + "Parameters" : [{ + "ParameterName" : "enable_user_activity_logging", + "ParameterValue" : "true" + }] + } + }, + "RedshiftClusterSubnetGroup" : { + "Type" : "AWS::Redshift::ClusterSubnetGroup", + "Properties" : { + "Description" : "Cluster subnet group", + "SubnetIds" : [ { "Ref" : "PublicSubnet" } ] + } + }, + "VPC" : { + "Type" : "AWS::EC2::VPC", + "Properties" : { + "CidrBlock" : "10.0.0.0/16" + } + }, + "PublicSubnet" : { + "Type" : "AWS::EC2::Subnet", + "Properties" : { + "CidrBlock" : "10.0.0.0/24", + "VpcId" : { "Ref" : "VPC" } + } + }, + "SecurityGroup" : { + "Type" : "AWS::EC2::SecurityGroup", + "Properties" : { + "GroupDescription" : "Security group", + "SecurityGroupIngress" : [ { + "CidrIp" : { "Ref": "InboundTraffic" }, + "FromPort" : { "Ref" : "PortNumber" }, + "ToPort" : { "Ref" : "PortNumber" }, + "IpProtocol" : "tcp" + } ], + "VpcId" : { "Ref" : "VPC" } + } + }, + "myInternetGateway" : { + "Type" : "AWS::EC2::InternetGateway" + }, + "AttachGateway" : { + "Type" : "AWS::EC2::VPCGatewayAttachment", + "Properties" : { + "VpcId" : { "Ref" : "VPC" }, + "InternetGatewayId" : { "Ref" : "myInternetGateway" } + } + }, + "PublicRouteTable" : { + "Type" : "AWS::EC2::RouteTable", + "Properties" : { + "VpcId" : { + "Ref" : "VPC" + } + } + }, + "PublicRoute" : { + "Type" : "AWS::EC2::Route", + "DependsOn" : "AttachGateway", + "Properties" : { + "RouteTableId" : { + "Ref" : "PublicRouteTable" + }, + "DestinationCidrBlock" : "0.0.0.0/0", + "GatewayId" : { + "Ref" : "myInternetGateway" + } + } + }, + "PublicSubnetRouteTableAssociation" : { + "Type" : "AWS::EC2::SubnetRouteTableAssociation", + "Properties" : { + "SubnetId" : { + "Ref" : "PublicSubnet" + }, + "RouteTableId" : { + "Ref" : "PublicRouteTable" + } + } + } + }, + "Outputs" : { + "ClusterEndpoint" : { + "Description" : "Cluster endpoint", + "Value" : { "Fn::Join" : [ ":", [ { "Fn::GetAtt" : [ "RedshiftCluster", "Endpoint.Address" ] }, { "Fn::GetAtt" : [ "RedshiftCluster", "Endpoint.Port" ] } ] ] } + }, + "ClusterName" : { + "Description" : "Name of cluster", + "Value" : { "Ref" : "RedshiftCluster" } + }, + "ParameterGroupName" : { + "Description" : "Name of parameter group", + "Value" : { "Ref" : "RedshiftClusterParameterGroup" } + }, + "RedshiftClusterSubnetGroupName" : { + "Description" : "Name of cluster subnet group", + "Value" : { "Ref" : "RedshiftClusterSubnetGroup" } + }, + "RedshiftClusterSecurityGroupName" : { + "Description" : "Name of cluster security group", + "Value" : { "Ref" : "SecurityGroup" } + } + } +} \ No newline at end of file diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 5b9387b13..c1305d843 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -8,6 +8,7 @@ import boto.ec2.autoscale import boto.ec2.elb from boto.exception import BotoServerError import boto.iam +import boto.redshift import boto.sns import boto.sqs import boto.vpc @@ -20,6 +21,7 @@ from moto import ( mock_elb, mock_iam, mock_rds, + mock_redshift, mock_route53, mock_sns, mock_sqs, @@ -29,6 +31,7 @@ from .fixtures import ( ec2_classic_eip, fn_join, rds_mysql_with_read_replica, + redshift, route53_ec2_instance_with_public_ip, route53_health_check, route53_roundrobin, @@ -288,20 +291,50 @@ def test_stack_elb_integration_with_attached_ec2_instances(): load_balancer.instances[0].id.should.equal(ec2_instance.id) list(load_balancer.availability_zones).should.equal(['us-east1']) - load_balancer_name = load_balancer.name - stack = conn.describe_stacks()[0] - stack_resources = stack.describe_resources() - stack_resources.should.have.length_of(2) - for resource in stack_resources: - if resource.resource_type == 'AWS::ElasticLoadBalancing::LoadBalancer': - load_balancer = resource - else: - ec2_instance = resource - load_balancer.logical_resource_id.should.equal("MyELB") - load_balancer.physical_resource_id.should.equal(load_balancer_name) - ec2_instance.physical_resource_id.should.equal(instance_id) +@mock_ec2() +@mock_redshift() +@mock_cloudformation() +def test_redshift_stack(): + redshift_template_json = json.dumps(redshift.template) + + vpc_conn = boto.vpc.connect_to_region("us-west-2") + conn = boto.cloudformation.connect_to_region("us-west-2") + conn.create_stack( + "redshift_stack", + template_body=redshift_template_json, + parameters=[ + ("DatabaseName", "mydb"), + ("ClusterType", "multi-node"), + ("NumberOfNodes", 2), + ("NodeType", "dw1.xlarge"), + ("MasterUsername", "myuser"), + ("MasterUserPassword", "mypass"), + ("InboundTraffic", "10.0.0.1/16"), + ("PortNumber", 5439), + ] + ) + + redshift_conn = boto.redshift.connect_to_region("us-west-2") + + cluster_res = redshift_conn.describe_clusters() + clusters = cluster_res['DescribeClustersResponse']['DescribeClustersResult']['Clusters'] + clusters.should.have.length_of(1) + cluster = clusters[0] + cluster['DBName'].should.equal("mydb") + cluster['NumberOfNodes'].should.equal(2) + cluster['NodeType'].should.equal("dw1.xlarge") + cluster['MasterUsername'].should.equal("myuser") + cluster['Port'].should.equal(5439) + cluster['VpcSecurityGroups'].should.have.length_of(1) + security_group_id = cluster['VpcSecurityGroups'][0]['VpcSecurityGroupId'] + + groups = vpc_conn.get_all_security_groups(group_ids=[security_group_id]) + groups.should.have.length_of(1) + group = groups[0] + group.rules.should.have.length_of(1) + group.rules[0].grants[0].cidr_ip.should.equal("10.0.0.1/16") @mock_ec2()