diff --git a/moto/ec2/models.py b/moto/ec2/models.py index ce2e08715..18d4ae6f3 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1576,6 +1576,12 @@ class Subnet(TaggedEC2Resource): ) return subnet + @property + def availability_zone(self): + # This could probably be smarter, but there doesn't appear to be a + # way to pull AZs for a region in boto + return self.ec2_backend.region_name + "a" + @property def physical_resource_id(self): return self.id @@ -2435,6 +2441,15 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, ElasticAddressBackend, KeyPairBackend, DHCPOptionsSetBackend, NetworkAclBackend): + def __init__(self, region_name): + super(EC2Backend, self).__init__() + self.region_name = region_name + + def reset(self): + region_name = self.region_name + self.__dict__ = {} + self.__init__(region_name) + # Use this to generate a proper error template response when in a response handler. def raise_error(self, code, message): raise EC2ClientError(code, message) @@ -2488,4 +2503,4 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, ec2_backends = {} for region in boto.ec2.regions(): - ec2_backends[region.name] = EC2Backend() + ec2_backends[region.name] = EC2Backend(region.name) diff --git a/moto/redshift/exceptions.py b/moto/redshift/exceptions.py index 13fccfb7b..13e3b35ce 100644 --- a/moto/redshift/exceptions.py +++ b/moto/redshift/exceptions.py @@ -22,3 +22,17 @@ class ClusterNotFoundError(RedshiftClientError): super(ClusterNotFoundError, self).__init__( 'ClusterNotFound', "Cluster {0} not found.".format(cluster_identifier)) + + +class ClusterSubnetGroupNotFound(RedshiftClientError): + def __init__(self, subnet_identifier): + super(ClusterSubnetGroupNotFound, self).__init__( + 'ClusterSubnetGroupNotFound', + "Subnet group {0} not found.".format(subnet_identifier)) + + +class InvalidSubnetError(RedshiftClientError): + def __init__(self, subnet_identifier): + super(InvalidSubnetError, self).__init__( + 'InvalidSubnet', + "Subnet {0} not found.".format(subnet_identifier)) diff --git a/moto/redshift/models.py b/moto/redshift/models.py index 90030e4a3..f70de584c 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -2,7 +2,8 @@ from __future__ import unicode_literals import boto.redshift from moto.core import BaseBackend -from .exceptions import ClusterNotFoundError +from moto.ec2 import ec2_backends +from .exceptions import ClusterNotFoundError, ClusterSubnetGroupNotFound, InvalidSubnetError class Cluster(object): @@ -69,10 +70,51 @@ class Cluster(object): } +class SubnetGroup(object): + + def __init__(self, ec2_backend, cluster_subnet_group_name, description, subnet_ids): + self.ec2_backend = ec2_backend + self.cluster_subnet_group_name = cluster_subnet_group_name + self.description = description + self.subnet_ids = subnet_ids + if not self.subnets: + raise InvalidSubnetError(subnet_ids) + + @property + def subnets(self): + return self.ec2_backend.get_all_subnets(filters={'subnet-id': self.subnet_ids}) + + @property + def vpc_id(self): + return self.subnets[0].vpc_id + + def to_json(self): + return { + "VpcId": self.vpc_id, + "Description": self.description, + "ClusterSubnetGroupName": self.cluster_subnet_group_name, + "SubnetGroupStatus": "Complete", + "Subnets": [{ + "SubnetStatus": "Active", + "SubnetIdentifier": subnet.id, + "SubnetAvailabilityZone": { + "Name": subnet.availability_zone + }, + } for subnet in self.subnets], + } + + class RedshiftBackend(BaseBackend): - def __init__(self): + def __init__(self, ec2_backend): self.clusters = {} + self.subnet_groups = {} + self.ec2_backend = ec2_backend + + def reset(self): + ec2_backend = self.ec2_backend + self.__dict__ = {} + self.__init__(ec2_backend) def create_cluster(self, **cluster_kwargs): cluster_identifier = cluster_kwargs['cluster_identifier'] @@ -110,7 +152,26 @@ class RedshiftBackend(BaseBackend): return self.clusters.pop(cluster_identifier) raise ClusterNotFoundError(cluster_identifier) + def create_cluster_subnet_group(self, cluster_subnet_group_name, description, subnet_ids): + subnet_group = SubnetGroup(self.ec2_backend, cluster_subnet_group_name, description, subnet_ids) + self.subnet_groups[cluster_subnet_group_name] = subnet_group + return subnet_group + + def describe_cluster_subnet_groups(self, subnet_identifier): + subnet_groups = self.subnet_groups.values() + if subnet_identifier: + if subnet_identifier in self.subnet_groups: + return [self.subnet_groups[subnet_identifier]] + else: + raise ClusterSubnetGroupNotFound(subnet_identifier) + return subnet_groups + + def delete_cluster_subnet_group(self, subnet_identifier): + if subnet_identifier in self.subnet_groups: + return self.subnet_groups.pop(subnet_identifier) + raise ClusterSubnetGroupNotFound(subnet_identifier) + redshift_backends = {} for region in boto.redshift.regions(): - redshift_backends[region.name] = RedshiftBackend() + redshift_backends[region.name] = RedshiftBackend(ec2_backends[region.name]) diff --git a/moto/redshift/responses.py b/moto/redshift/responses.py index 710302c36..a83fbc76e 100644 --- a/moto/redshift/responses.py +++ b/moto/redshift/responses.py @@ -111,3 +111,52 @@ class RedshiftResponse(BaseResponse): } } }) + + def create_cluster_subnet_group(self): + cluster_subnet_group_name = self._get_param('ClusterSubnetGroupName') + description = self._get_param('Description') + subnet_ids = self._get_multi_param('SubnetIds.member') + + subnet_group = self.redshift_backend.create_cluster_subnet_group( + cluster_subnet_group_name=cluster_subnet_group_name, + description=description, + subnet_ids=subnet_ids, + ) + + return json.dumps({ + "CreateClusterSubnetGroupResponse": { + "CreateClusterSubnetGroupResult": { + "ClusterSubnetGroup": subnet_group.to_json(), + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def describe_cluster_subnet_groups(self): + subnet_identifier = self._get_param("ClusterSubnetGroupName") + subnet_groups = self.redshift_backend.describe_cluster_subnet_groups(subnet_identifier) + + return json.dumps({ + "DescribeClusterSubnetGroupsResponse": { + "DescribeClusterSubnetGroupsResult": { + "ClusterSubnetGroups": [subnet_group.to_json() for subnet_group in subnet_groups] + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def delete_cluster_subnet_group(self): + subnet_identifier = self._get_param("ClusterSubnetGroupName") + self.redshift_backend.delete_cluster_subnet_group(subnet_identifier) + + return json.dumps({ + "DeleteClusterSubnetGroupResponse": { + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index b5bc3b618..c0355c703 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals import boto -from boto.redshift.exceptions import ClusterNotFound +from boto.redshift.exceptions import ClusterNotFound, ClusterSubnetGroupNotFound import sure # noqa -from moto import mock_redshift +from moto import mock_ec2, mock_redshift @mock_redshift @@ -21,7 +21,6 @@ def test_create_cluster(): cluster_type="multi-node", # cluster_security_groups=None, # vpc_security_group_ids=None, - # cluster_subnet_group_name=None, availability_zone="us-east-1d", preferred_maintenance_window="Mon:03:00-Mon:11:00", # cluster_parameter_group_name=None, @@ -94,7 +93,7 @@ def test_default_cluster_attibutes(): cluster['DBName'].should.equal("dev") # cluster['ClusterSecurityGroups'].should.equal([]) # cluster['VpcSecurityGroups'].should.equal([]) - # cluster['ClusterSubnetGroupName'].should.equal(None) + cluster['ClusterSubnetGroupName'].should.equal(None) assert "us-east-" in cluster['AvailabilityZone'] cluster['PreferredMaintenanceWindow'].should.equal("Mon:03:00-Mon:03:30") # cluster['ClusterParameterGroups'].should.equal([]) @@ -105,6 +104,32 @@ def test_default_cluster_attibutes(): cluster['NumberOfNodes'].should.equal(1) +@mock_redshift +@mock_ec2 +def test_create_cluster_in_subnet_group(): + vpc_conn = boto.connect_vpc() + vpc = vpc_conn.create_vpc("10.0.0.0/16") + subnet = vpc_conn.create_subnet(vpc.id, "10.0.0.0/24") + redshift_conn = boto.connect_redshift() + redshift_conn.create_cluster_subnet_group( + "my_subnet_group", + "This is my subnet group", + subnet_ids=[subnet.id], + ) + + redshift_conn.create_cluster( + "my_cluster", + node_type="dw.hs1.xlarge", + master_username="username", + master_user_password="password", + cluster_subnet_group_name='my_subnet_group', + ) + + cluster_response = redshift_conn.describe_clusters("my_cluster") + cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + cluster['ClusterSubnetGroupName'].should.equal('my_subnet_group') + + @mock_redshift def test_describe_non_existant_cluster(): conn = boto.redshift.connect_to_region("us-east-1") @@ -169,9 +194,69 @@ def test_modify_cluster(): cluster['NodeType'].should.equal("dw.hs1.xlarge") # cluster['ClusterSecurityGroups'].should.equal([]) # cluster['VpcSecurityGroups'].should.equal([]) - # cluster['ClusterSubnetGroupName'].should.equal(None) cluster['PreferredMaintenanceWindow'].should.equal("Tue:03:00-Tue:11:00") # cluster['ClusterParameterGroups'].should.equal([]) cluster['AutomatedSnapshotRetentionPeriod'].should.equal(7) cluster['AllowVersionUpgrade'].should.equal(False) cluster['NumberOfNodes'].should.equal(2) + + +@mock_redshift +@mock_ec2 +def test_create_cluster_subnet_group(): + vpc_conn = boto.connect_vpc() + vpc = vpc_conn.create_vpc("10.0.0.0/16") + subnet1 = vpc_conn.create_subnet(vpc.id, "10.0.0.0/24") + subnet2 = vpc_conn.create_subnet(vpc.id, "10.0.1.0/24") + + redshift_conn = boto.connect_redshift() + + redshift_conn.create_cluster_subnet_group( + "my_subnet", + "This is my subnet group", + subnet_ids=[subnet1.id, subnet2.id], + ) + + list(redshift_conn.describe_cluster_subnet_groups()).should.have.length_of(1) + + subnets_response = redshift_conn.describe_cluster_subnet_groups("my_subnet") + my_subnet = subnets_response['DescribeClusterSubnetGroupsResponse']['DescribeClusterSubnetGroupsResult']['ClusterSubnetGroups'][0] + + my_subnet['ClusterSubnetGroupName'].should.equal("my_subnet") + my_subnet['Description'].should.equal("This is my subnet group") + subnet_ids = [subnet['SubnetIdentifier'] for subnet in my_subnet['Subnets']] + set(subnet_ids).should.equal(set([subnet1.id, subnet2.id])) + + +@mock_redshift +def test_describe_non_existant_subnet_group(): + conn = boto.redshift.connect_to_region("us-east-1") + conn.describe_cluster_subnet_groups.when.called_with("not-a-subnet-group").should.throw(ClusterSubnetGroupNotFound) + + +@mock_redshift +@mock_ec2 +def test_delete_cluster_subnet_group(): + vpc_conn = boto.connect_vpc() + vpc = vpc_conn.create_vpc("10.0.0.0/16") + subnet = vpc_conn.create_subnet(vpc.id, "10.0.0.0/24") + redshift_conn = boto.connect_redshift() + + redshift_conn.create_cluster_subnet_group( + "my_subnet", + "This is my subnet group", + subnet_ids=[subnet.id], + ) + + subnets_response = redshift_conn.describe_cluster_subnet_groups() + subnets = subnets_response['DescribeClusterSubnetGroupsResponse']['DescribeClusterSubnetGroupsResult']['ClusterSubnetGroups'] + subnets.should.have.length_of(1) + + redshift_conn.delete_cluster_subnet_group("my_subnet") + + subnets_response = redshift_conn.describe_cluster_subnet_groups() + subnets = subnets_response['DescribeClusterSubnetGroupsResponse']['DescribeClusterSubnetGroupsResult']['ClusterSubnetGroups'] + subnets.should.have.length_of(0) + + # Delete invalid id + redshift_conn.describe_cluster_subnet_groups.when.called_with("not-a-subnet-group").should.throw(ClusterSubnetGroupNotFound)