From 25a31ee88a0b1fdf420bc32f0910b78535ecb529 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 23 Nov 2014 22:17:36 -0500 Subject: [PATCH] Add cluster security groups. --- moto/redshift/exceptions.py | 11 +++- moto/redshift/models.py | 65 +++++++++++++++++--- moto/redshift/responses.py | 56 +++++++++++++++-- tests/test_redshift/test_redshift.py | 90 +++++++++++++++++++++++++--- 4 files changed, 201 insertions(+), 21 deletions(-) diff --git a/moto/redshift/exceptions.py b/moto/redshift/exceptions.py index 13e3b35ce..6d1d3c90c 100644 --- a/moto/redshift/exceptions.py +++ b/moto/redshift/exceptions.py @@ -24,13 +24,20 @@ class ClusterNotFoundError(RedshiftClientError): "Cluster {0} not found.".format(cluster_identifier)) -class ClusterSubnetGroupNotFound(RedshiftClientError): +class ClusterSubnetGroupNotFoundError(RedshiftClientError): def __init__(self, subnet_identifier): - super(ClusterSubnetGroupNotFound, self).__init__( + super(ClusterSubnetGroupNotFoundError, self).__init__( 'ClusterSubnetGroupNotFound', "Subnet group {0} not found.".format(subnet_identifier)) +class ClusterSecurityGroupNotFoundError(RedshiftClientError): + def __init__(self, group_identifier): + super(ClusterSecurityGroupNotFoundError, self).__init__( + 'ClusterSecurityGroupNotFound', + "Security group {0} not found.".format(group_identifier)) + + class InvalidSubnetError(RedshiftClientError): def __init__(self, subnet_identifier): super(InvalidSubnetError, self).__init__( diff --git a/moto/redshift/models.py b/moto/redshift/models.py index f70de584c..6f1a89675 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -3,17 +3,23 @@ from __future__ import unicode_literals import boto.redshift from moto.core import BaseBackend from moto.ec2 import ec2_backends -from .exceptions import ClusterNotFoundError, ClusterSubnetGroupNotFound, InvalidSubnetError +from .exceptions import ( + ClusterNotFoundError, + ClusterSecurityGroupNotFoundError, + ClusterSubnetGroupNotFoundError, + InvalidSubnetError, +) class Cluster(object): - def __init__(self, cluster_identifier, node_type, master_username, + def __init__(self, redshift_backend, cluster_identifier, node_type, master_username, master_user_password, db_name, cluster_type, cluster_security_groups, vpc_security_group_ids, cluster_subnet_group_name, availability_zone, preferred_maintenance_window, cluster_parameter_group_name, automated_snapshot_retention_period, port, cluster_version, allow_version_upgrade, number_of_nodes, publicly_accessible, encrypted, region): + self.redshift_backend = redshift_backend self.cluster_identifier = cluster_identifier self.node_type = node_type self.master_username = master_username @@ -46,6 +52,14 @@ class Cluster(object): else: self.number_of_nodes = 1 + @property + def security_groups(self): + return [ + security_group for security_group + in self.redshift_backend.describe_cluster_security_groups() + if security_group.cluster_security_group_name in self.cluster_security_groups + ] + def to_json(self): return { "MasterUsername": self.master_username, @@ -62,7 +76,10 @@ class Cluster(object): "DBName": self.db_name, "PreferredMaintenanceWindow": self.preferred_maintenance_window, "ClusterParameterGroups": [], - "ClusterSecurityGroups": [], + "ClusterSecurityGroups": [{ + "Status": "active", + "ClusterSecurityGroupName": group.cluster_security_group_name, + } for group in self.security_groups], "Port": self.port, "NodeType": self.node_type, "ClusterIdentifier": self.cluster_identifier, @@ -104,11 +121,26 @@ class SubnetGroup(object): } +class SecurityGroup(object): + def __init__(self, cluster_security_group_name, description): + self.cluster_security_group_name = cluster_security_group_name + self.description = description + + def to_json(self): + return { + "EC2SecurityGroups": [], + "IPRanges": [], + "Description": self.description, + "ClusterSecurityGroupName": self.cluster_security_group_name, + } + + class RedshiftBackend(BaseBackend): def __init__(self, ec2_backend): self.clusters = {} self.subnet_groups = {} + self.security_groups = {} self.ec2_backend = ec2_backend def reset(self): @@ -118,7 +150,7 @@ class RedshiftBackend(BaseBackend): def create_cluster(self, **cluster_kwargs): cluster_identifier = cluster_kwargs['cluster_identifier'] - cluster = Cluster(**cluster_kwargs) + cluster = Cluster(self, **cluster_kwargs) self.clusters[cluster_identifier] = cluster return cluster @@ -157,19 +189,38 @@ class RedshiftBackend(BaseBackend): self.subnet_groups[cluster_subnet_group_name] = subnet_group return subnet_group - def describe_cluster_subnet_groups(self, subnet_identifier): + def describe_cluster_subnet_groups(self, subnet_identifier=None): 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) + raise ClusterSubnetGroupNotFoundError(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) + raise ClusterSubnetGroupNotFoundError(subnet_identifier) + + def create_cluster_security_group(self, cluster_security_group_name, description): + security_group = SecurityGroup(cluster_security_group_name, description) + self.security_groups[cluster_security_group_name] = security_group + return security_group + + def describe_cluster_security_groups(self, security_group_name=None): + security_groups = self.security_groups.values() + if security_group_name: + if security_group_name in self.security_groups: + return [self.security_groups[security_group_name]] + else: + raise ClusterSecurityGroupNotFoundError(security_group_name) + return security_groups + + def delete_cluster_security_group(self, security_group_identifier): + if security_group_identifier in self.security_groups: + return self.security_groups.pop(security_group_identifier) + raise ClusterSecurityGroupNotFoundError(security_group_identifier) redshift_backends = {} diff --git a/moto/redshift/responses.py b/moto/redshift/responses.py index a83fbc76e..697e76a2b 100644 --- a/moto/redshift/responses.py +++ b/moto/redshift/responses.py @@ -20,8 +20,8 @@ class RedshiftResponse(BaseResponse): "master_user_password": self._get_param('MasterUserPassword'), "db_name": self._get_param('DBName'), "cluster_type": self._get_param('ClusterType'), - "cluster_security_groups": self._get_multi_param('ClusterSecurityGroups'), - "vpc_security_group_ids": self._get_multi_param('VpcSecurityGroupIds'), + "cluster_security_groups": self._get_multi_param('ClusterSecurityGroups.member'), + "vpc_security_group_ids": self._get_multi_param('VpcSecurityGroupIds.member'), "cluster_subnet_group_name": self._get_param('ClusterSubnetGroupName'), "availability_zone": self._get_param('AvailabilityZone'), "preferred_maintenance_window": self._get_param('PreferredMaintenanceWindow'), @@ -35,7 +35,6 @@ class RedshiftResponse(BaseResponse): "encrypted": self._get_param("Encrypted"), "region": self.region, } - cluster = self.redshift_backend.create_cluster(**cluster_kwargs) return json.dumps({ @@ -71,8 +70,8 @@ class RedshiftResponse(BaseResponse): "node_type": self._get_param('NodeType'), "master_user_password": self._get_param('MasterUserPassword'), "cluster_type": self._get_param('ClusterType'), - "cluster_security_groups": self._get_multi_param('ClusterSecurityGroups'), - "vpc_security_group_ids": self._get_multi_param('VpcSecurityGroupIds'), + "cluster_security_groups": self._get_multi_param('ClusterSecurityGroups.member'), + "vpc_security_group_ids": self._get_multi_param('VpcSecurityGroupIds.member'), "cluster_subnet_group_name": self._get_param('ClusterSubnetGroupName'), "preferred_maintenance_window": self._get_param('PreferredMaintenanceWindow'), "cluster_parameter_group_name": self._get_param('ClusterParameterGroupName'), @@ -160,3 +159,50 @@ class RedshiftResponse(BaseResponse): } } }) + + def create_cluster_security_group(self): + cluster_security_group_name = self._get_param('ClusterSecurityGroupName') + description = self._get_param('Description') + + security_group = self.redshift_backend.create_cluster_security_group( + cluster_security_group_name=cluster_security_group_name, + description=description, + ) + + return json.dumps({ + "CreateClusterSecurityGroupResponse": { + "CreateClusterSecurityGroupResult": { + "ClusterSecurityGroup": security_group.to_json(), + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def describe_cluster_security_groups(self): + cluster_security_group_name = self._get_param("ClusterSecurityGroupName") + security_groups = self.redshift_backend.describe_cluster_security_groups(cluster_security_group_name) + + return json.dumps({ + "DescribeClusterSecurityGroupsResponse": { + "DescribeClusterSecurityGroupsResult": { + "ClusterSecurityGroups": [security_group.to_json() for security_group in security_groups] + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def delete_cluster_security_group(self): + security_group_identifier = self._get_param("ClusterSecurityGroupName") + self.redshift_backend.delete_cluster_security_group(security_group_identifier) + + return json.dumps({ + "DeleteClusterSecurityGroupResponse": { + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index c0355c703..ba5e30065 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -1,7 +1,11 @@ from __future__ import unicode_literals import boto -from boto.redshift.exceptions import ClusterNotFound, ClusterSubnetGroupNotFound +from boto.redshift.exceptions import ( + ClusterNotFound, + ClusterSecurityGroupNotFound, + ClusterSubnetGroupNotFound, +) import sure # noqa from moto import mock_ec2, mock_redshift @@ -19,7 +23,6 @@ def test_create_cluster(): master_user_password="password", db_name="my_db", cluster_type="multi-node", - # cluster_security_groups=None, # vpc_security_group_ids=None, availability_zone="us-east-1d", preferred_maintenance_window="Mon:03:00-Mon:11:00", @@ -130,6 +133,33 @@ def test_create_cluster_in_subnet_group(): cluster['ClusterSubnetGroupName'].should.equal('my_subnet_group') +@mock_redshift +def test_create_cluster_with_security_group(): + conn = boto.redshift.connect_to_region("us-east-1") + conn.create_cluster_security_group( + "security_group1", + "This is my security group", + ) + conn.create_cluster_security_group( + "security_group2", + "This is my security group", + ) + + cluster_identifier = 'my_cluster' + conn.create_cluster( + cluster_identifier, + node_type="dw.hs1.xlarge", + master_username="username", + master_user_password="password", + cluster_security_groups=["security_group1", "security_group2"] + ) + + cluster_response = conn.describe_clusters(cluster_identifier) + cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + group_names = [group['ClusterSecurityGroupName'] for group in cluster['ClusterSecurityGroups']] + set(group_names).should.equal(set(["security_group1", "security_group2"])) + + @mock_redshift def test_describe_non_existant_cluster(): conn = boto.redshift.connect_to_region("us-east-1") @@ -164,6 +194,10 @@ def test_delete_cluster(): def test_modify_cluster(): conn = boto.connect_redshift() cluster_identifier = 'my_cluster' + conn.create_cluster_security_group( + "security_group", + "This is my security group", + ) conn.create_cluster( cluster_identifier, @@ -177,7 +211,7 @@ def test_modify_cluster(): cluster_type="multi-node", node_type="dw.hs1.xlarge", number_of_nodes=2, - # cluster_security_groups=None, + cluster_security_groups="security_group", # vpc_security_group_ids=None, master_user_password="new_password", # cluster_parameter_group_name=None, @@ -192,7 +226,7 @@ def test_modify_cluster(): cluster['ClusterIdentifier'].should.equal("new_identifier") cluster['NodeType'].should.equal("dw.hs1.xlarge") - # cluster['ClusterSecurityGroups'].should.equal([]) + cluster['ClusterSecurityGroups'][0]['ClusterSecurityGroupName'].should.equal("security_group") # cluster['VpcSecurityGroups'].should.equal([]) cluster['PreferredMaintenanceWindow'].should.equal("Tue:03:00-Tue:11:00") # cluster['ClusterParameterGroups'].should.equal([]) @@ -217,8 +251,6 @@ def test_create_cluster_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] @@ -259,4 +291,48 @@ def test_delete_cluster_subnet_group(): 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) + redshift_conn.delete_cluster_subnet_group.when.called_with("not-a-subnet-group").should.throw(ClusterSubnetGroupNotFound) + + +@mock_redshift +def test_create_cluster_security_group(): + conn = boto.connect_redshift() + conn.create_cluster_security_group( + "my_security_group", + "This is my security group", + ) + + groups_response = conn.describe_cluster_security_groups("my_security_group") + my_group = groups_response['DescribeClusterSecurityGroupsResponse']['DescribeClusterSecurityGroupsResult']['ClusterSecurityGroups'][0] + + my_group['ClusterSecurityGroupName'].should.equal("my_security_group") + my_group['Description'].should.equal("This is my security group") + list(my_group['IPRanges']).should.equal([]) + + +@mock_redshift +def test_describe_non_existant_security_group(): + conn = boto.redshift.connect_to_region("us-east-1") + conn.describe_cluster_security_groups.when.called_with("not-a-security-group").should.throw(ClusterSecurityGroupNotFound) + + +@mock_redshift +def test_delete_cluster_security_group(): + conn = boto.connect_redshift() + conn.create_cluster_security_group( + "my_security_group", + "This is my security group", + ) + + groups_response = conn.describe_cluster_security_groups() + groups = groups_response['DescribeClusterSecurityGroupsResponse']['DescribeClusterSecurityGroupsResult']['ClusterSecurityGroups'] + groups.should.have.length_of(1) + + conn.delete_cluster_security_group("my_security_group") + + groups_response = conn.describe_cluster_security_groups() + groups = groups_response['DescribeClusterSecurityGroupsResponse']['DescribeClusterSecurityGroupsResult']['ClusterSecurityGroups'] + groups.should.have.length_of(0) + + # Delete invalid id + conn.delete_cluster_security_group.when.called_with("not-a-security-group").should.throw(ClusterSecurityGroupNotFound)