Add redshift support for cloudformation.

This commit is contained in:
Steve Pulec 2015-08-08 13:29:37 -04:00
parent 323f720cb5
commit 73f03d1ccf
4 changed files with 312 additions and 15 deletions

View File

@ -8,6 +8,7 @@ from moto.ec2 import models as ec2_models
from moto.elb import models as elb_models from moto.elb import models as elb_models
from moto.iam import models as iam_models from moto.iam import models as iam_models
from moto.rds import models as rds_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.route53 import models as route53_models
from moto.sns import models as sns_models from moto.sns import models as sns_models
from moto.sqs import models as sqs_models from moto.sqs import models as sqs_models
@ -40,6 +41,9 @@ MODEL_MAP = {
"AWS::RDS::DBInstance": rds_models.Database, "AWS::RDS::DBInstance": rds_models.Database,
"AWS::RDS::DBSecurityGroup": rds_models.SecurityGroup, "AWS::RDS::DBSecurityGroup": rds_models.SecurityGroup,
"AWS::RDS::DBSubnetGroup": rds_models.SubnetGroup, "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::HealthCheck": route53_models.HealthCheck,
"AWS::Route53::HostedZone": route53_models.FakeZone, "AWS::Route53::HostedZone": route53_models.FakeZone,
"AWS::Route53::RecordSet": route53_models.RecordSet, "AWS::Route53::RecordSet": route53_models.RecordSet,

View File

@ -33,8 +33,8 @@ class Cluster(object):
self.allow_version_upgrade = allow_version_upgrade if allow_version_upgrade is not None else True 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.cluster_version = cluster_version if cluster_version else "1.0"
self.port = port if port else 5439 self.port = int(port) if port else 5439
self.automated_snapshot_retention_period = automated_snapshot_retention_period if automated_snapshot_retention_period else 1 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" self.preferred_maintenance_window = preferred_maintenance_window if preferred_maintenance_window else "Mon:03:00-Mon:03:30"
if cluster_parameter_group_name: if cluster_parameter_group_name:
@ -47,6 +47,7 @@ class Cluster(object):
else: else:
self.cluster_security_groups = ["Default"] self.cluster_security_groups = ["Default"]
self.region = region
if availability_zone: if availability_zone:
self.availability_zone = availability_zone self.availability_zone = availability_zone
else: else:
@ -57,10 +58,58 @@ class Cluster(object):
if cluster_type == 'single-node': if cluster_type == 'single-node':
self.number_of_nodes = 1 self.number_of_nodes = 1
elif number_of_nodes: elif number_of_nodes:
self.number_of_nodes = number_of_nodes self.number_of_nodes = int(number_of_nodes)
else: else:
self.number_of_nodes = 1 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 @property
def security_groups(self): def security_groups(self):
return [ return [
@ -128,6 +177,18 @@ class SubnetGroup(object):
if not self.subnets: if not self.subnets:
raise InvalidSubnetError(subnet_ids) 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 @property
def subnets(self): def subnets(self):
return self.ec2_backend.get_all_subnets(filters={'subnet-id': self.subnet_ids}) 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.group_family = group_family
self.description = description 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): def to_json(self):
return { return {
"ParameterGroupFamily": self.group_family, "ParameterGroupFamily": self.group_family,

View File

@ -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" }
}
}
}

View File

@ -8,6 +8,7 @@ import boto.ec2.autoscale
import boto.ec2.elb import boto.ec2.elb
from boto.exception import BotoServerError from boto.exception import BotoServerError
import boto.iam import boto.iam
import boto.redshift
import boto.sns import boto.sns
import boto.sqs import boto.sqs
import boto.vpc import boto.vpc
@ -20,6 +21,7 @@ from moto import (
mock_elb, mock_elb,
mock_iam, mock_iam,
mock_rds, mock_rds,
mock_redshift,
mock_route53, mock_route53,
mock_sns, mock_sns,
mock_sqs, mock_sqs,
@ -29,6 +31,7 @@ from .fixtures import (
ec2_classic_eip, ec2_classic_eip,
fn_join, fn_join,
rds_mysql_with_read_replica, rds_mysql_with_read_replica,
redshift,
route53_ec2_instance_with_public_ip, route53_ec2_instance_with_public_ip,
route53_health_check, route53_health_check,
route53_roundrobin, 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) load_balancer.instances[0].id.should.equal(ec2_instance.id)
list(load_balancer.availability_zones).should.equal(['us-east1']) 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") @mock_ec2()
load_balancer.physical_resource_id.should.equal(load_balancer_name) @mock_redshift()
ec2_instance.physical_resource_id.should.equal(instance_id) @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() @mock_ec2()