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([])