From aceb30e530b441189b0e80adfdee93f71dba0c5b Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 22 Nov 2014 14:03:09 -0500 Subject: [PATCH 1/9] Add basic CRUD. --- moto/__init__.py | 1 + moto/backends.py | 2 + moto/core/responses.py | 13 +++ moto/redshift/__init__.py | 12 +++ moto/redshift/exceptions.py | 24 +++++ moto/redshift/models.py | 107 +++++++++++++++++++ moto/redshift/responses.py | 112 ++++++++++++++++++++ moto/redshift/urls.py | 10 ++ moto/redshift/utils.py | 1 + tests/test_redshift/test_redshift.py | 148 +++++++++++++++++++++++++++ tests/test_redshift/test_server.py | 23 +++++ 11 files changed, 453 insertions(+) create mode 100644 moto/redshift/__init__.py create mode 100644 moto/redshift/exceptions.py create mode 100644 moto/redshift/models.py create mode 100644 moto/redshift/responses.py create mode 100644 moto/redshift/urls.py create mode 100644 moto/redshift/utils.py create mode 100644 tests/test_redshift/test_redshift.py create mode 100644 tests/test_redshift/test_server.py diff --git a/moto/__init__.py b/moto/__init__.py index 6daf3e290..75bd5a53c 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -11,6 +11,7 @@ from .ec2 import mock_ec2 # flake8: noqa from .elb import mock_elb # flake8: noqa from .emr import mock_emr # flake8: noqa from .iam import mock_iam # flake8: noqa +from .redshift import mock_redshift # flake8: noqa from .s3 import mock_s3 # flake8: noqa from .s3bucket_path import mock_s3bucket_path # flake8: noqa from .ses import mock_ses # flake8: noqa diff --git a/moto/backends.py b/moto/backends.py index 3a50f5769..d9df2133d 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -6,6 +6,7 @@ from moto.dynamodb2 import dynamodb_backend2 from moto.ec2 import ec2_backend from moto.elb import elb_backend from moto.emr import emr_backend +from moto.redshift import redshift_backend from moto.s3 import s3_backend from moto.s3bucket_path import s3bucket_path_backend from moto.ses import ses_backend @@ -21,6 +22,7 @@ BACKENDS = { 'ec2': ec2_backend, 'elb': elb_backend, 'emr': emr_backend, + 'redshift': redshift_backend, 's3': s3_backend, 's3bucket_path': s3bucket_path_backend, 'ses': ses_backend, diff --git a/moto/core/responses.py b/moto/core/responses.py index 726eac686..80e3e77f6 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -110,6 +110,19 @@ class BaseResponse(object): def _get_param(self, param_name): return self.querystring.get(param_name, [None])[0] + def _get_int_param(self, param_name): + val = self._get_param(param_name) + if val is not None: + return int(val) + + def _get_bool_param(self, param_name): + val = self._get_param(param_name) + if val is not None: + if val.lower() == 'true': + return True + elif val.lower() == 'false': + return False + def _get_multi_param(self, param_prefix): if param_prefix.endswith("."): prefix = param_prefix diff --git a/moto/redshift/__init__.py b/moto/redshift/__init__.py new file mode 100644 index 000000000..7adf47865 --- /dev/null +++ b/moto/redshift/__init__.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from .models import redshift_backends +from ..core.models import MockAWS + +redshift_backend = redshift_backends['us-east-1'] + + +def mock_redshift(func=None): + if func: + return MockAWS(redshift_backends)(func) + else: + return MockAWS(redshift_backends) diff --git a/moto/redshift/exceptions.py b/moto/redshift/exceptions.py new file mode 100644 index 000000000..13fccfb7b --- /dev/null +++ b/moto/redshift/exceptions.py @@ -0,0 +1,24 @@ +from __future__ import unicode_literals + +import json +from werkzeug.exceptions import BadRequest + + +class RedshiftClientError(BadRequest): + def __init__(self, code, message): + super(RedshiftClientError, self).__init__() + self.description = json.dumps({ + "Error": { + "Code": code, + "Message": message, + 'Type': 'Sender', + }, + 'RequestId': '6876f774-7273-11e4-85dc-39e55ca848d1', + }) + + +class ClusterNotFoundError(RedshiftClientError): + def __init__(self, cluster_identifier): + super(ClusterNotFoundError, self).__init__( + 'ClusterNotFound', + "Cluster {0} not found.".format(cluster_identifier)) diff --git a/moto/redshift/models.py b/moto/redshift/models.py new file mode 100644 index 000000000..f901a4809 --- /dev/null +++ b/moto/redshift/models.py @@ -0,0 +1,107 @@ +from __future__ import unicode_literals + +import boto.redshift +from moto.core import BaseBackend +from .exceptions import ClusterNotFoundError + + +class Cluster(object): + def __init__(self, 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): + self.cluster_identifier = cluster_identifier + self.node_type = node_type + self.master_username = master_username + self.master_user_password = master_user_password + self.db_name = db_name + self.cluster_security_groups = cluster_security_groups + self.vpc_security_group_ids = vpc_security_group_ids + self.cluster_subnet_group_name = cluster_subnet_group_name + self.availability_zone = availability_zone + self.preferred_maintenance_window = preferred_maintenance_window + self.cluster_parameter_group_name = cluster_parameter_group_name + self.automated_snapshot_retention_period = automated_snapshot_retention_period + self.port = port + self.cluster_version = cluster_version + self.allow_version_upgrade = allow_version_upgrade + self.publicly_accessible = publicly_accessible + self.encrypted = encrypted + + if cluster_type == 'single-node': + self.number_of_nodes = 1 + else: + self.number_of_nodes = number_of_nodes + + def to_json(self): + return { + "MasterUsername": self.master_username, + "MasterUserPassword": "****", + "ClusterVersion": self.cluster_version, + "VpcSecurityGroups": [], + "ClusterSubnetGroupName": self.cluster_subnet_group_name, + "AvailabilityZone": self.availability_zone, + "ClusterStatus": "creating", + "NumberOfNodes": self.number_of_nodes, + "AutomatedSnapshotRetentionPeriod": self.automated_snapshot_retention_period, + "PubliclyAccessible": self.publicly_accessible, + "Encrypted": self.encrypted, + "DBName": self.db_name, + "PreferredMaintenanceWindow": self.preferred_maintenance_window, + "ClusterParameterGroups": [], + "ClusterSecurityGroups": [], + "Port": self.port, + "NodeType": self.node_type, + "ClusterIdentifier": self.cluster_identifier, + "AllowVersionUpgrade": self.allow_version_upgrade, + } + + +class RedshiftBackend(BaseBackend): + + def __init__(self): + self.clusters = {} + + def create_cluster(self, **cluster_kwargs): + cluster_identifier = cluster_kwargs['cluster_identifier'] + cluster = Cluster(**cluster_kwargs) + self.clusters[cluster_identifier] = cluster + return cluster + + def describe_clusters(self, cluster_identifier=None): + clusters = self.clusters.values() + if cluster_identifier: + if cluster_identifier in self.clusters: + return [self.clusters[cluster_identifier]] + else: + raise ClusterNotFoundError(cluster_identifier) + return clusters + + def modify_cluster(self, **cluster_kwargs): + cluster_identifier = cluster_kwargs.pop('cluster_identifier') + new_cluster_identifier = cluster_kwargs.pop('new_cluster_identifier', None) + + cluster = self.describe_clusters(cluster_identifier)[0] + + for key, value in cluster_kwargs.items(): + setattr(cluster, key, value) + + if new_cluster_identifier: + self.delete_cluster(cluster_identifier) + cluster.cluster_identifier = new_cluster_identifier + self.clusters[new_cluster_identifier] = cluster + + return cluster + + def delete_cluster(self, cluster_identifier): + if cluster_identifier in self.clusters: + return self.clusters.pop(cluster_identifier) + raise ClusterNotFoundError(cluster_identifier) + + +redshift_backends = {} +for region in boto.redshift.regions(): + redshift_backends[region.name] = RedshiftBackend() diff --git a/moto/redshift/responses.py b/moto/redshift/responses.py new file mode 100644 index 000000000..6e3cae0b9 --- /dev/null +++ b/moto/redshift/responses.py @@ -0,0 +1,112 @@ +from __future__ import unicode_literals + +import json + +from moto.core.responses import BaseResponse +from .models import redshift_backends + + +class RedshiftResponse(BaseResponse): + + @property + def redshift_backend(self): + return redshift_backends[self.region] + + def create_cluster(self): + cluster_kwargs = { + "cluster_identifier": self._get_param('ClusterIdentifier'), + "node_type": self._get_param('NodeType'), + "master_username": self._get_param('MasterUsername'), + "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_subnet_group_name": self._get_param('ClusterSubnetGroupName'), + "availability_zone": self._get_param('AvailabilityZone'), + "preferred_maintenance_window": self._get_param('PreferredMaintenanceWindow'), + "cluster_parameter_group_name": self._get_param('ClusterParameterGroupName'), + "automated_snapshot_retention_period": self._get_int_param('AutomatedSnapshotRetentionPeriod'), + "port": self._get_int_param('Port'), + "cluster_version": self._get_param('ClusterVersion'), + "allow_version_upgrade": self._get_bool_param('AllowVersionUpgrade'), + "number_of_nodes": self._get_int_param('NumberOfNodes'), + "publicly_accessible": self._get_param("PubliclyAccessible"), + "encrypted": self._get_param("Encrypted"), + } + + cluster = self.redshift_backend.create_cluster(**cluster_kwargs) + + return json.dumps({ + "CreateClusterResponse": { + "CreateClusterResult": { + "Cluster": cluster.to_json(), + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def describe_clusters(self): + cluster_identifier = self._get_param("ClusterIdentifier") + clusters = self.redshift_backend.describe_clusters(cluster_identifier) + + return json.dumps({ + "DescribeClustersResponse": { + "DescribeClustersResult": { + "Clusters": [cluster.to_json() for cluster in clusters] + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def modify_cluster(self): + cluster_kwargs = { + "cluster_identifier": self._get_param('ClusterIdentifier'), + "new_cluster_identifier": self._get_param('NewClusterIdentifier'), + "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_subnet_group_name": self._get_param('ClusterSubnetGroupName'), + "preferred_maintenance_window": self._get_param('PreferredMaintenanceWindow'), + "cluster_parameter_group_name": self._get_param('ClusterParameterGroupName'), + "automated_snapshot_retention_period": self._get_int_param('AutomatedSnapshotRetentionPeriod'), + "cluster_version": self._get_param('ClusterVersion'), + "allow_version_upgrade": self._get_bool_param('AllowVersionUpgrade'), + "number_of_nodes": self._get_int_param('NumberOfNodes'), + "publicly_accessible": self._get_param("PubliclyAccessible"), + "encrypted": self._get_param("Encrypted"), + } + + cluster = self.redshift_backend.modify_cluster(**cluster_kwargs) + + return json.dumps({ + "ModifyClusterResponse": { + "ModifyClusterResult": { + "Cluster": cluster.to_json(), + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def delete_cluster(self): + cluster_identifier = self._get_param("ClusterIdentifier") + cluster = self.redshift_backend.delete_cluster(cluster_identifier) + + return json.dumps({ + "DeleteClusterResponse": { + "DeleteClusterResult": { + "Cluster": cluster.to_json() + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) diff --git a/moto/redshift/urls.py b/moto/redshift/urls.py new file mode 100644 index 000000000..5d3ab296c --- /dev/null +++ b/moto/redshift/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import RedshiftResponse + +url_bases = [ + "https?://redshift.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': RedshiftResponse().dispatch, +} diff --git a/moto/redshift/utils.py b/moto/redshift/utils.py new file mode 100644 index 000000000..baffc4882 --- /dev/null +++ b/moto/redshift/utils.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py new file mode 100644 index 000000000..89cb46687 --- /dev/null +++ b/tests/test_redshift/test_redshift.py @@ -0,0 +1,148 @@ +from __future__ import unicode_literals + +import boto +from boto.redshift.exceptions import ClusterNotFound +import sure # noqa + +from moto import mock_redshift + + +@mock_redshift +def test_create_cluster(): + conn = boto.redshift.connect_to_region("us-east-1") + cluster_identifier = 'my_cluster' + + conn.create_cluster( + cluster_identifier, + node_type="dw.hs1.xlarge", + master_username="username", + master_user_password="password", + db_name="my_db", + 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, + automated_snapshot_retention_period=10, + port=1234, + cluster_version="1.0", + allow_version_upgrade=True, + number_of_nodes=3, + ) + + cluster_response = conn.describe_clusters(cluster_identifier) + cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + + cluster['ClusterIdentifier'].should.equal(cluster_identifier) + cluster['NodeType'].should.equal("dw.hs1.xlarge") + cluster['MasterUsername'].should.equal("username") + cluster['DBName'].should.equal("my_db") + cluster['ClusterSecurityGroups'].should.equal([]) + cluster['VpcSecurityGroups'].should.equal([]) + cluster['ClusterSubnetGroupName'].should.equal(None) + cluster['AvailabilityZone'].should.equal("us-east-1d") + cluster['PreferredMaintenanceWindow'].should.equal("Mon:03:00-Mon:11:00") + cluster['ClusterParameterGroups'].should.equal([]) + cluster['AutomatedSnapshotRetentionPeriod'].should.equal(10) + cluster['Port'].should.equal(1234) + cluster['ClusterVersion'].should.equal("1.0") + cluster['AllowVersionUpgrade'].should.equal(True) + cluster['NumberOfNodes'].should.equal(3) + + +@mock_redshift +def test_create_single_node_cluster(): + conn = boto.redshift.connect_to_region("us-east-1") + cluster_identifier = 'my_cluster' + + conn.create_cluster( + cluster_identifier, + node_type="dw.hs1.xlarge", + master_username="username", + master_user_password="password", + db_name="my_db", + cluster_type="single-node", + ) + + cluster_response = conn.describe_clusters(cluster_identifier) + cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + + cluster['ClusterIdentifier'].should.equal(cluster_identifier) + cluster['NodeType'].should.equal("dw.hs1.xlarge") + cluster['MasterUsername'].should.equal("username") + cluster['DBName'].should.equal("my_db") + cluster['NumberOfNodes'].should.equal(1) + + +@mock_redshift +def test_describe_non_existant_cluster(): + conn = boto.redshift.connect_to_region("us-east-1") + conn.describe_clusters.when.called_with("not-a-cluster").should.throw(ClusterNotFound) + + +@mock_redshift +def test_delete_cluster(): + conn = boto.connect_redshift() + cluster_identifier = 'my_cluster' + + conn.create_cluster( + cluster_identifier, + node_type='single-node', + master_username="username", + master_user_password="password", + ) + + clusters = conn.describe_clusters()['DescribeClustersResponse']['DescribeClustersResult']['Clusters'] + list(clusters).should.have.length_of(1) + + conn.delete_cluster(cluster_identifier) + + clusters = conn.describe_clusters()['DescribeClustersResponse']['DescribeClustersResult']['Clusters'] + list(clusters).should.have.length_of(0) + + # Delete invalid id + conn.delete_cluster.when.called_with("not-a-cluster").should.throw(ClusterNotFound) + + +@mock_redshift +def test_modify_cluster(): + conn = boto.connect_redshift() + cluster_identifier = 'my_cluster' + + conn.create_cluster( + cluster_identifier, + node_type='single-node', + master_username="username", + master_user_password="password", + ) + + conn.modify_cluster( + cluster_identifier, + cluster_type="multi-node", + node_type="dw.hs1.xlarge", + number_of_nodes=2, + # cluster_security_groups=None, + # vpc_security_group_ids=None, + master_user_password="new_password", + # cluster_parameter_group_name=None, + automated_snapshot_retention_period=7, + preferred_maintenance_window="Tue:03:00-Tue:11:00", + allow_version_upgrade=False, + new_cluster_identifier="new_identifier", + ) + + cluster_response = conn.describe_clusters("new_identifier") + cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + + cluster['ClusterIdentifier'].should.equal("new_identifier") + 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) diff --git a/tests/test_redshift/test_server.py b/tests/test_redshift/test_server.py new file mode 100644 index 000000000..34d616d12 --- /dev/null +++ b/tests/test_redshift/test_server.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals + +import json +import sure # noqa + +import moto.server as server +from moto import mock_redshift + +''' +Test the different server responses +''' + + +@mock_redshift +def test_describe_clusters(): + backend = server.create_backend_app("redshift") + test_client = backend.test_client() + + res = test_client.get('/?Action=DescribeClusters') + + json_data = json.loads(res.data) + clusters = json_data['DescribeClustersResponse']['DescribeClustersResult']['Clusters'] + list(clusters).should.equal([]) From 93bd852419cb4a0b166cfb280095ca2278a1cf52 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 23 Nov 2014 17:59:38 -0500 Subject: [PATCH 2/9] Fix redshift test. Drop old boto version from testing. --- .travis.yml | 1 - tests/test_redshift/test_server.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 57c8270f6..fd4d4ad64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ python: env: matrix: - BOTO_VERSION=2.34.0 - - BOTO_VERSION=2.25.0 matrix: include: - python: "3.3" diff --git a/tests/test_redshift/test_server.py b/tests/test_redshift/test_server.py index 34d616d12..a6bdc93f3 100644 --- a/tests/test_redshift/test_server.py +++ b/tests/test_redshift/test_server.py @@ -18,6 +18,6 @@ def test_describe_clusters(): res = test_client.get('/?Action=DescribeClusters') - json_data = json.loads(res.data) + json_data = json.loads(res.data.decode("utf-8")) clusters = json_data['DescribeClustersResponse']['DescribeClustersResult']['Clusters'] list(clusters).should.equal([]) From 72a794612616eb94a6147ce196c5d4fc5f958d08 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 23 Nov 2014 18:16:22 -0500 Subject: [PATCH 3/9] Cleanup default parameters. --- moto/redshift/models.py | 27 +++++++++++++++++--------- moto/redshift/responses.py | 1 + tests/test_redshift/test_redshift.py | 29 ++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/moto/redshift/models.py b/moto/redshift/models.py index f901a4809..90030e4a3 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -12,29 +12,38 @@ class Cluster(object): preferred_maintenance_window, cluster_parameter_group_name, automated_snapshot_retention_period, port, cluster_version, allow_version_upgrade, number_of_nodes, publicly_accessible, - encrypted): + encrypted, region): self.cluster_identifier = cluster_identifier self.node_type = node_type self.master_username = master_username self.master_user_password = master_user_password - self.db_name = db_name + self.db_name = db_name if db_name else "dev" self.cluster_security_groups = cluster_security_groups self.vpc_security_group_ids = vpc_security_group_ids self.cluster_subnet_group_name = cluster_subnet_group_name - self.availability_zone = availability_zone - self.preferred_maintenance_window = preferred_maintenance_window self.cluster_parameter_group_name = cluster_parameter_group_name - self.automated_snapshot_retention_period = automated_snapshot_retention_period - self.port = port - self.cluster_version = cluster_version - self.allow_version_upgrade = allow_version_upgrade self.publicly_accessible = publicly_accessible self.encrypted = encrypted + 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.preferred_maintenance_window = preferred_maintenance_window if preferred_maintenance_window else "Mon:03:00-Mon:03:30" + + if availability_zone: + self.availability_zone = availability_zone + else: + # This could probably be smarter, but there doesn't appear to be a + # way to pull AZs for a region in boto + self.availability_zone = region + "a" + if cluster_type == 'single-node': self.number_of_nodes = 1 - else: + elif number_of_nodes: self.number_of_nodes = number_of_nodes + else: + self.number_of_nodes = 1 def to_json(self): return { diff --git a/moto/redshift/responses.py b/moto/redshift/responses.py index 6e3cae0b9..710302c36 100644 --- a/moto/redshift/responses.py +++ b/moto/redshift/responses.py @@ -33,6 +33,7 @@ class RedshiftResponse(BaseResponse): "number_of_nodes": self._get_int_param('NumberOfNodes'), "publicly_accessible": self._get_param("PubliclyAccessible"), "encrypted": self._get_param("Encrypted"), + "region": self.region, } cluster = self.redshift_backend.create_cluster(**cluster_kwargs) diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index 89cb46687..b5bc3b618 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -76,6 +76,35 @@ def test_create_single_node_cluster(): cluster['NumberOfNodes'].should.equal(1) +@mock_redshift +def test_default_cluster_attibutes(): + conn = boto.redshift.connect_to_region("us-east-1") + cluster_identifier = 'my_cluster' + + conn.create_cluster( + cluster_identifier, + node_type="dw.hs1.xlarge", + master_username="username", + master_user_password="password", + ) + + cluster_response = conn.describe_clusters(cluster_identifier) + cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + + cluster['DBName'].should.equal("dev") + # cluster['ClusterSecurityGroups'].should.equal([]) + # cluster['VpcSecurityGroups'].should.equal([]) + # 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([]) + cluster['AutomatedSnapshotRetentionPeriod'].should.equal(1) + cluster['Port'].should.equal(5439) + cluster['ClusterVersion'].should.equal("1.0") + cluster['AllowVersionUpgrade'].should.equal(True) + cluster['NumberOfNodes'].should.equal(1) + + @mock_redshift def test_describe_non_existant_cluster(): conn = boto.redshift.connect_to_region("us-east-1") From ef3e5448ea906f9158f32310d0819ab12d71d564 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 23 Nov 2014 21:36:19 -0500 Subject: [PATCH 4/9] Add cluster subnet group CRUD. --- moto/ec2/models.py | 17 ++++- moto/redshift/exceptions.py | 14 ++++ moto/redshift/models.py | 67 +++++++++++++++++++- moto/redshift/responses.py | 49 ++++++++++++++ tests/test_redshift/test_redshift.py | 95 ++++++++++++++++++++++++++-- 5 files changed, 233 insertions(+), 9 deletions(-) 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) From 25a31ee88a0b1fdf420bc32f0910b78535ecb529 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 23 Nov 2014 22:17:36 -0500 Subject: [PATCH 5/9] 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) From 2e484a895b037637a4e389d8320ac8f6640385da Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 23 Nov 2014 22:21:29 -0500 Subject: [PATCH 6/9] Fix for default security group. --- moto/redshift/models.py | 10 ++++++++-- tests/test_redshift/test_redshift.py | 7 +++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/moto/redshift/models.py b/moto/redshift/models.py index 6f1a89675..d40ea0871 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -25,7 +25,6 @@ class Cluster(object): self.master_username = master_username self.master_user_password = master_user_password self.db_name = db_name if db_name else "dev" - self.cluster_security_groups = cluster_security_groups self.vpc_security_group_ids = vpc_security_group_ids self.cluster_subnet_group_name = cluster_subnet_group_name self.cluster_parameter_group_name = cluster_parameter_group_name @@ -38,6 +37,11 @@ class Cluster(object): self.automated_snapshot_retention_period = 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_security_groups: + self.cluster_security_groups = cluster_security_groups + else: + self.cluster_security_groups = ["Default"] + if availability_zone: self.availability_zone = availability_zone else: @@ -140,7 +144,9 @@ class RedshiftBackend(BaseBackend): def __init__(self, ec2_backend): self.clusters = {} self.subnet_groups = {} - self.security_groups = {} + self.security_groups = { + "Default": SecurityGroup("Default", "Default Redshift Security Group") + } self.ec2_backend = ec2_backend def reset(self): diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index ba5e30065..881c65504 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -41,7 +41,7 @@ def test_create_cluster(): cluster['NodeType'].should.equal("dw.hs1.xlarge") cluster['MasterUsername'].should.equal("username") cluster['DBName'].should.equal("my_db") - cluster['ClusterSecurityGroups'].should.equal([]) + cluster['ClusterSecurityGroups'][0]['ClusterSecurityGroupName'].should.equal("Default") cluster['VpcSecurityGroups'].should.equal([]) cluster['ClusterSubnetGroupName'].should.equal(None) cluster['AvailabilityZone'].should.equal("us-east-1d") @@ -94,7 +94,6 @@ def test_default_cluster_attibutes(): cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] cluster['DBName'].should.equal("dev") - # cluster['ClusterSecurityGroups'].should.equal([]) # cluster['VpcSecurityGroups'].should.equal([]) cluster['ClusterSubnetGroupName'].should.equal(None) assert "us-east-" in cluster['AvailabilityZone'] @@ -326,13 +325,13 @@ def test_delete_cluster_security_group(): groups_response = conn.describe_cluster_security_groups() groups = groups_response['DescribeClusterSecurityGroupsResponse']['DescribeClusterSecurityGroupsResult']['ClusterSecurityGroups'] - groups.should.have.length_of(1) + groups.should.have.length_of(2) # The default group already exists 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) + groups.should.have.length_of(1) # Delete invalid id conn.delete_cluster_security_group.when.called_with("not-a-security-group").should.throw(ClusterSecurityGroupNotFound) From 2418c83252fe82a165a11970589d0782a119968d Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 23 Nov 2014 22:33:53 -0500 Subject: [PATCH 7/9] Fixes for VPC security groups. --- moto/redshift/models.py | 13 ++++++++++++- tests/test_redshift/test_redshift.py | 27 +++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/moto/redshift/models.py b/moto/redshift/models.py index d40ea0871..c5332215b 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -64,12 +64,23 @@ class Cluster(object): if security_group.cluster_security_group_name in self.cluster_security_groups ] + @property + def vpc_security_groups(self): + return [ + security_group for security_group + in self.redshift_backend.ec2_backend.describe_security_groups() + if security_group.id in self.vpc_security_group_ids + ] + def to_json(self): return { "MasterUsername": self.master_username, "MasterUserPassword": "****", "ClusterVersion": self.cluster_version, - "VpcSecurityGroups": [], + "VpcSecurityGroups": [{ + "Status": "active", + "VpcSecurityGroupId": group.id + } for group in self.vpc_security_groups], "ClusterSubnetGroupName": self.cluster_subnet_group_name, "AvailabilityZone": self.availability_zone, "ClusterStatus": "creating", diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index 881c65504..756cf0d9f 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -23,7 +23,6 @@ def test_create_cluster(): master_user_password="password", db_name="my_db", cluster_type="multi-node", - # vpc_security_group_ids=None, availability_zone="us-east-1d", preferred_maintenance_window="Mon:03:00-Mon:11:00", # cluster_parameter_group_name=None, @@ -94,7 +93,6 @@ def test_default_cluster_attibutes(): cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] cluster['DBName'].should.equal("dev") - # cluster['VpcSecurityGroups'].should.equal([]) cluster['ClusterSubnetGroupName'].should.equal(None) assert "us-east-" in cluster['AvailabilityZone'] cluster['PreferredMaintenanceWindow'].should.equal("Mon:03:00-Mon:03:30") @@ -159,6 +157,29 @@ def test_create_cluster_with_security_group(): set(group_names).should.equal(set(["security_group1", "security_group2"])) +@mock_redshift +@mock_ec2 +def test_create_cluster_with_vpc_security_groups(): + vpc_conn = boto.connect_vpc() + ec2_conn = boto.connect_ec2() + redshift_conn = boto.connect_redshift() + vpc = vpc_conn.create_vpc("10.0.0.0/16") + security_group = ec2_conn.create_security_group("vpc_security_group", "a group", vpc_id=vpc.id) + + redshift_conn.create_cluster( + "my_cluster", + node_type="dw.hs1.xlarge", + master_username="username", + master_user_password="password", + vpc_security_group_ids=[security_group.id], + ) + + cluster_response = redshift_conn.describe_clusters("my_cluster") + cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + group_ids = [group['VpcSecurityGroupId'] for group in cluster['VpcSecurityGroups']] + list(group_ids).should.equal([security_group.id]) + + @mock_redshift def test_describe_non_existant_cluster(): conn = boto.redshift.connect_to_region("us-east-1") @@ -211,7 +232,6 @@ def test_modify_cluster(): node_type="dw.hs1.xlarge", number_of_nodes=2, cluster_security_groups="security_group", - # vpc_security_group_ids=None, master_user_password="new_password", # cluster_parameter_group_name=None, automated_snapshot_retention_period=7, @@ -226,7 +246,6 @@ def test_modify_cluster(): cluster['ClusterIdentifier'].should.equal("new_identifier") cluster['NodeType'].should.equal("dw.hs1.xlarge") 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([]) cluster['AutomatedSnapshotRetentionPeriod'].should.equal(7) From 5bbcc4505ff6e21d27de925ea708efb12bd37558 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 23 Nov 2014 23:03:10 -0500 Subject: [PATCH 8/9] Add cluster parameter groups. --- moto/redshift/exceptions.py | 7 +++ moto/redshift/models.py | 63 ++++++++++++++++++++- moto/redshift/responses.py | 50 ++++++++++++++++- tests/test_redshift/test_redshift.py | 83 ++++++++++++++++++++++++++-- 4 files changed, 195 insertions(+), 8 deletions(-) diff --git a/moto/redshift/exceptions.py b/moto/redshift/exceptions.py index 6d1d3c90c..6d1b2c3bb 100644 --- a/moto/redshift/exceptions.py +++ b/moto/redshift/exceptions.py @@ -38,6 +38,13 @@ class ClusterSecurityGroupNotFoundError(RedshiftClientError): "Security group {0} not found.".format(group_identifier)) +class ClusterParameterGroupNotFoundError(RedshiftClientError): + def __init__(self, group_identifier): + super(ClusterParameterGroupNotFoundError, self).__init__( + 'ClusterParameterGroupNotFound', + "Parameter 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 c5332215b..c3189c951 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -5,6 +5,7 @@ from moto.core import BaseBackend from moto.ec2 import ec2_backends from .exceptions import ( ClusterNotFoundError, + ClusterParameterGroupNotFoundError, ClusterSecurityGroupNotFoundError, ClusterSubnetGroupNotFoundError, InvalidSubnetError, @@ -27,7 +28,6 @@ class Cluster(object): self.db_name = db_name if db_name else "dev" self.vpc_security_group_ids = vpc_security_group_ids self.cluster_subnet_group_name = cluster_subnet_group_name - self.cluster_parameter_group_name = cluster_parameter_group_name self.publicly_accessible = publicly_accessible self.encrypted = encrypted @@ -37,6 +37,11 @@ class Cluster(object): self.automated_snapshot_retention_period = 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: + self.cluster_parameter_group_name = [cluster_parameter_group_name] + else: + self.cluster_parameter_group_name = ['default.redshift-1.0'] + if cluster_security_groups: self.cluster_security_groups = cluster_security_groups else: @@ -72,6 +77,14 @@ class Cluster(object): if security_group.id in self.vpc_security_group_ids ] + @property + def parameter_groups(self): + return [ + parameter_group for parameter_group + in self.redshift_backend.describe_cluster_parameter_groups() + if parameter_group.cluster_parameter_group_name in self.cluster_parameter_group_name + ] + def to_json(self): return { "MasterUsername": self.master_username, @@ -90,7 +103,10 @@ class Cluster(object): "Encrypted": self.encrypted, "DBName": self.db_name, "PreferredMaintenanceWindow": self.preferred_maintenance_window, - "ClusterParameterGroups": [], + "ClusterParameterGroups": [{ + "ParameterApplyStatus": "in-sync", + "ParameterGroupName": group.cluster_parameter_group_name, + } for group in self.parameter_groups], "ClusterSecurityGroups": [{ "Status": "active", "ClusterSecurityGroupName": group.cluster_security_group_name, @@ -150,6 +166,21 @@ class SecurityGroup(object): } +class ParameterGroup(object): + + def __init__(self, cluster_parameter_group_name, group_family, description): + self.cluster_parameter_group_name = cluster_parameter_group_name + self.group_family = group_family + self.description = description + + def to_json(self): + return { + "ParameterGroupFamily": self.group_family, + "Description": self.description, + "ParameterGroupName": self.cluster_parameter_group_name, + } + + class RedshiftBackend(BaseBackend): def __init__(self, ec2_backend): @@ -158,6 +189,13 @@ class RedshiftBackend(BaseBackend): self.security_groups = { "Default": SecurityGroup("Default", "Default Redshift Security Group") } + self.parameter_groups = { + "default.redshift-1.0": ParameterGroup( + "default.redshift-1.0", + "redshift-1.0", + "Default Redshift parameter group", + ) + } self.ec2_backend = ec2_backend def reset(self): @@ -239,6 +277,27 @@ class RedshiftBackend(BaseBackend): return self.security_groups.pop(security_group_identifier) raise ClusterSecurityGroupNotFoundError(security_group_identifier) + def create_cluster_parameter_group(self, cluster_parameter_group_name, + group_family, description): + parameter_group = ParameterGroup(cluster_parameter_group_name, group_family, description) + self.parameter_groups[cluster_parameter_group_name] = parameter_group + + return parameter_group + + def describe_cluster_parameter_groups(self, parameter_group_name=None): + parameter_groups = self.parameter_groups.values() + if parameter_group_name: + if parameter_group_name in self.parameter_groups: + return [self.parameter_groups[parameter_group_name]] + else: + raise ClusterParameterGroupNotFoundError(parameter_group_name) + return parameter_groups + + def delete_cluster_parameter_group(self, parameter_group_name): + if parameter_group_name in self.parameter_groups: + return self.parameter_groups.pop(parameter_group_name) + raise ClusterParameterGroupNotFoundError(parameter_group_name) + redshift_backends = {} for region in boto.redshift.regions(): diff --git a/moto/redshift/responses.py b/moto/redshift/responses.py index 697e76a2b..a9c977b4e 100644 --- a/moto/redshift/responses.py +++ b/moto/redshift/responses.py @@ -82,7 +82,6 @@ class RedshiftResponse(BaseResponse): "publicly_accessible": self._get_param("PubliclyAccessible"), "encrypted": self._get_param("Encrypted"), } - cluster = self.redshift_backend.modify_cluster(**cluster_kwargs) return json.dumps({ @@ -206,3 +205,52 @@ class RedshiftResponse(BaseResponse): } } }) + + def create_cluster_parameter_group(self): + cluster_parameter_group_name = self._get_param('ParameterGroupName') + group_family = self._get_param('ParameterGroupFamily') + description = self._get_param('Description') + + parameter_group = self.redshift_backend.create_cluster_parameter_group( + cluster_parameter_group_name, + group_family, + description, + ) + + return json.dumps({ + "CreateClusterParameterGroupResponse": { + "CreateClusterParameterGroupResult": { + "ClusterParameterGroup": parameter_group.to_json(), + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def describe_cluster_parameter_groups(self): + cluster_parameter_group_name = self._get_param("ParameterGroupName") + parameter_groups = self.redshift_backend.describe_cluster_parameter_groups(cluster_parameter_group_name) + + return json.dumps({ + "DescribeClusterParameterGroupsResponse": { + "DescribeClusterParameterGroupsResult": { + "ParameterGroups": [parameter_group.to_json() for parameter_group in parameter_groups] + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def delete_cluster_parameter_group(self): + cluster_parameter_group_name = self._get_param("ParameterGroupName") + self.redshift_backend.delete_cluster_parameter_group(cluster_parameter_group_name) + + return json.dumps({ + "DeleteClusterParameterGroupResponse": { + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index 756cf0d9f..bb4b2ce10 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import boto from boto.redshift.exceptions import ( ClusterNotFound, + ClusterParameterGroupNotFound, ClusterSecurityGroupNotFound, ClusterSubnetGroupNotFound, ) @@ -25,7 +26,6 @@ def test_create_cluster(): cluster_type="multi-node", availability_zone="us-east-1d", preferred_maintenance_window="Mon:03:00-Mon:11:00", - # cluster_parameter_group_name=None, automated_snapshot_retention_period=10, port=1234, cluster_version="1.0", @@ -45,7 +45,7 @@ def test_create_cluster(): cluster['ClusterSubnetGroupName'].should.equal(None) cluster['AvailabilityZone'].should.equal("us-east-1d") cluster['PreferredMaintenanceWindow'].should.equal("Mon:03:00-Mon:11:00") - cluster['ClusterParameterGroups'].should.equal([]) + cluster['ClusterParameterGroups'][0]['ParameterGroupName'].should.equal("default.redshift-1.0") cluster['AutomatedSnapshotRetentionPeriod'].should.equal(10) cluster['Port'].should.equal(1234) cluster['ClusterVersion'].should.equal("1.0") @@ -96,7 +96,7 @@ def test_default_cluster_attibutes(): 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([]) + cluster['ClusterParameterGroups'][0]['ParameterGroupName'].should.equal("default.redshift-1.0") cluster['AutomatedSnapshotRetentionPeriod'].should.equal(1) cluster['Port'].should.equal(5439) cluster['ClusterVersion'].should.equal("1.0") @@ -180,6 +180,28 @@ def test_create_cluster_with_vpc_security_groups(): list(group_ids).should.equal([security_group.id]) +@mock_redshift +def test_create_cluster_with_parameter_group(): + conn = boto.connect_redshift() + conn.create_cluster_parameter_group( + "my_parameter_group", + "redshift-1.0", + "This is my parameter group", + ) + + conn.create_cluster( + "my_cluster", + node_type="dw.hs1.xlarge", + master_username="username", + master_user_password="password", + cluster_parameter_group_name='my_parameter_group', + ) + + cluster_response = conn.describe_clusters("my_cluster") + cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + cluster['ClusterParameterGroups'][0]['ParameterGroupName'].should.equal("my_parameter_group") + + @mock_redshift def test_describe_non_existant_cluster(): conn = boto.redshift.connect_to_region("us-east-1") @@ -218,6 +240,11 @@ def test_modify_cluster(): "security_group", "This is my security group", ) + conn.create_cluster_parameter_group( + "my_parameter_group", + "redshift-1.0", + "This is my parameter group", + ) conn.create_cluster( cluster_identifier, @@ -233,7 +260,7 @@ def test_modify_cluster(): number_of_nodes=2, cluster_security_groups="security_group", master_user_password="new_password", - # cluster_parameter_group_name=None, + cluster_parameter_group_name="my_parameter_group", automated_snapshot_retention_period=7, preferred_maintenance_window="Tue:03:00-Tue:11:00", allow_version_upgrade=False, @@ -247,7 +274,7 @@ def test_modify_cluster(): cluster['NodeType'].should.equal("dw.hs1.xlarge") cluster['ClusterSecurityGroups'][0]['ClusterSecurityGroupName'].should.equal("security_group") cluster['PreferredMaintenanceWindow'].should.equal("Tue:03:00-Tue:11:00") - # cluster['ClusterParameterGroups'].should.equal([]) + cluster['ClusterParameterGroups'][0]['ParameterGroupName'].should.equal("my_parameter_group") cluster['AutomatedSnapshotRetentionPeriod'].should.equal(7) cluster['AllowVersionUpgrade'].should.equal(False) cluster['NumberOfNodes'].should.equal(2) @@ -354,3 +381,49 @@ def test_delete_cluster_security_group(): # Delete invalid id conn.delete_cluster_security_group.when.called_with("not-a-security-group").should.throw(ClusterSecurityGroupNotFound) + + +@mock_redshift +def test_create_cluster_parameter_group(): + conn = boto.connect_redshift() + conn.create_cluster_parameter_group( + "my_parameter_group", + "redshift-1.0", + "This is my parameter group", + ) + + groups_response = conn.describe_cluster_parameter_groups("my_parameter_group") + my_group = groups_response['DescribeClusterParameterGroupsResponse']['DescribeClusterParameterGroupsResult']['ParameterGroups'][0] + + my_group['ParameterGroupName'].should.equal("my_parameter_group") + my_group['ParameterGroupFamily'].should.equal("redshift-1.0") + my_group['Description'].should.equal("This is my parameter group") + + +@mock_redshift +def test_describe_non_existant_parameter_group(): + conn = boto.redshift.connect_to_region("us-east-1") + conn.describe_cluster_parameter_groups.when.called_with("not-a-parameter-group").should.throw(ClusterParameterGroupNotFound) + + +@mock_redshift +def test_delete_cluster_parameter_group(): + conn = boto.connect_redshift() + conn.create_cluster_parameter_group( + "my_parameter_group", + "redshift-1.0", + "This is my parameter group", + ) + + groups_response = conn.describe_cluster_parameter_groups() + groups = groups_response['DescribeClusterParameterGroupsResponse']['DescribeClusterParameterGroupsResult']['ParameterGroups'] + groups.should.have.length_of(2) # The default group already exists + + conn.delete_cluster_parameter_group("my_parameter_group") + + groups_response = conn.describe_cluster_parameter_groups() + groups = groups_response['DescribeClusterParameterGroupsResponse']['DescribeClusterParameterGroupsResult']['ParameterGroups'] + groups.should.have.length_of(1) + + # Delete invalid id + conn.delete_cluster_parameter_group.when.called_with("not-a-parameter-group").should.throw(ClusterParameterGroupNotFound) From 50b80b68f01a0452e999b82e45d14536ce07a69f Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 23 Nov 2014 23:20:02 -0500 Subject: [PATCH 9/9] Add invalid subnet test. --- tests/test_redshift/test_redshift.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index bb4b2ce10..700301418 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -6,6 +6,7 @@ from boto.redshift.exceptions import ( ClusterParameterGroupNotFound, ClusterSecurityGroupNotFound, ClusterSubnetGroupNotFound, + InvalidSubnet, ) import sure # noqa @@ -305,6 +306,17 @@ def test_create_cluster_subnet_group(): set(subnet_ids).should.equal(set([subnet1.id, subnet2.id])) +@mock_redshift +@mock_ec2 +def test_create_invalid_cluster_subnet_group(): + redshift_conn = boto.connect_redshift() + redshift_conn.create_cluster_subnet_group.when.called_with( + "my_subnet", + "This is my subnet group", + subnet_ids=["subnet-1234"], + ).should.throw(InvalidSubnet) + + @mock_redshift def test_describe_non_existant_subnet_group(): conn = boto.redshift.connect_to_region("us-east-1")